Pico AI 代理是一个专为 iOS、macOS、iPadOS 和 VisionOS 开发者设计的反向代理。
Pico AI 代理之前名为 SwiftOpenAIProxy。
PicoProxy 使用服务器端 Swift 编写,并使用 HummingBird 作为其 HTTP 服务器。
在 2023 年 12 月,我遭遇了一次重大黑客攻击,我的 OpenAI 密钥被盗用。 他们迅速用完了我整个 2,500 美元的每月限额,导致 OpenAI 产生了一笔意外账单。 此次事件还迫使我将我的应用程序 Pico 下线,导致我错过了利润丰厚的圣诞节销售期。
作为对此事件的回应,我开发了 Pico AI Server,这是第一个用服务器端 Swift 创建的 OpenAI 代理。 此工具对于 Swift 开发者来说特别方便,因为它允许轻松进行自定义以满足他们的特定要求。
Pico AI 代理旨在与任何现有的 OpenAI 库兼容。 它可以与 CleverBird、OpenAISwift、OpenAI-Kit、MacPaw OpenAI 等库无缝协作,也可以与您的自定义代码集成。
API | Chat 异步 | Chat 流式传输 | 嵌入 (Embeddings) | 音频 | 图像 |
---|---|---|---|---|---|
OpenAI | ✅ | ✅ | ✅ | ✅ | ✅ |
Anthropic | ❌ | ✅ | ❌ | ❌ | ❌ |
OpenAI
chat
、audio
、embeddings
、fine-tune
、image
Anthropic
2023-06-01
claude-3-opus-20240229
claude-3-sonnet-20240229
claude-3-haiku-20240307
要设置 Pico AI 代理,您需要:
在 OpenAI 生成一个 OpenAI API 密钥。 可选择在 Anthropic 生成一个 Anthropic Claude API 密钥。
使用 openssl rand -base64 32
在 macOS 终端中创建一个新的 JWT 私钥
注意:此 JWT 令牌用于验证您的客户端应用程序。 它是 App Store 服务器库用于与 Apple App Store API 通信的不同 JWT 令牌。
在 https://appstoreconnect.apple.com/apps 上找到您的 App bundle Id、Apple app Id 和团队 Id。 在常规部分的App 信息下,您将找到这些详细信息。
团队 Id 是一个 10 个字符的字符串,可以在 https://developer.apple.com/account 的会员资格详情下找到。
在 App Store Connect 的用户和访问选项卡下的 App 内购买 此处生成密钥。 您还将在同一页面上找到 Issuer Id 和 Key Id。
有关更多详细信息,请参见创建 App Store Connect API 的 API 密钥
要从 Xcode 运行 Pico AI 代理,请将下面列出的环境变量和参数设置为“如何设置 Pico AI 代理”中列出的信息。
可以使用 Target -> Edit scheme 在 Xcode 中编辑环境变量和参数。
参数 | 默认值 | 方案中的默认值 |
---|---|---|
--hostname | 0.0.0.0 | |
--port | 8080 | 8080 |
--target | https://api.openai.com |
从 Xcode 启动时,Pico AI 代理可在 https://:8080 访问。 部署在 Railway 上时,Pico AI 代理将默认为端口 443 (https)。
所有流量都将转发到 target
。“target”可以修改为将流量定向到任何 API,无论它是否符合 OpenAI API,只要您的客户端应用程序兼容。 target 是所有流量转发到的站点。 您可以将 target 更改为任何 API,即使 API 不符合 OpenAI(只要您的客户端应用兼容)。
变量 | 描述 | 参考 |
---|---|---|
OpenAI-APIKey | OpenAI API 密钥 (sk-...) | https://platform.openai.com |
OpenAI-Organization | OpenAI 组织标识符 (org-...) | https://platform.openai.com |
Anthropic-APIKey | Anthropic API 密钥 (sk-ant-api3-...) | https://docs.anthropic.com/claude/docs/ |
如果为 1,则带有有效 OpenAI 密钥和 org 头的请求将被转发到 OpenAI,无需修改(已弃用) |
变量 | 描述 | 参考 |
---|---|---|
appTeamId | Apple 团队 ID | https://appstoreconnect.apple.com/ |
appBundleId | 例如:com.example.myapp | https://appstoreconnect.apple.com/ |
appAppleId | App 信息 -> 常规信息下的 Apple Id | https://appstoreconnect.apple.com/ |
变量 | 描述 | 参考 |
---|---|---|
IAPPrivateKey | IAP 私钥 | https://appstoreconnect.apple.com/access/api/subs |
IAPIssuerId | IAP 发行者 Id | https://appstoreconnect.apple.com/access/api/subs |
IAPKeyId | IAP 密钥 Id | https://appstoreconnect.apple.com/access/api/subs |
Pico AI 代理中的 IAPPrivateKey
采用 PKCS #8 格式,这是一种多行格式。 该格式以 -----BEGIN PRIVATE KEY-----
开头,以 -----END PRIVATE KEY-----
结尾。 在这些标记之间,密钥包含四行 base64 编码的数据。 但是,虽然 Xcode 支持带有换行符的环境变量,但许多托管服务(例如 Railway)不支持。
为了确保跨不同环境的兼容性,Pico AI 代理要求将私钥压缩为单行。 这是通过将所有换行符替换为 \\n
(双反斜杠后跟 n
)来实现的。
Pico AI 代理的正确格式的 IAPPrivateKey
应显示为一行:-----BEGIN PRIVATE KEY-----\\n<LINE1>\\n<LINE2>\\n<LINE3>\\n<LINE4>\\n-----END PRIVATE KEY-----
,其中 <LINE1>
、<LINE2>
、<LINE3>
和 <LINE4>
代表密钥的 base64 编码数据。
变量 | 描述 | 参考 |
---|---|---|
JWTPrivateKey | https://jwt.net.cn/introduction |
变量 | 默认值 | 描述 |
---|---|---|
enableRateLimiter | 0 | 设置为 1 以激活速率限制器 |
userMinuteRateLimit | 15 | 每个注册用户每分钟的最大查询次数 |
userHourlyRateLimit | 50 | 每个注册用户每小时的最大查询次数 |
userPermanentBlock | 50 | 永久禁止用户的阻止请求阈值 |
anonMinuteRateLimit | 60 | 所有匿名用户每分钟的组合最大查询次数 |
anonHourlyRateLimit | 200 | 所有匿名用户每小时的组合最大查询次数 |
anonPermanentBlock | 50 | 禁止所有匿名用户的阻止请求阈值 |
默认情况下,速率限制器处于关闭状态。 要激活,请将 enableRateLimiter
设置为 1。
速率限制器会计算请求,并且不区分不同的模型或 LLM 提供商。 它主要用于防范滥用流量。
用户由 StoreKit 2 Transaction.purchase() 调用中的应用程序帐户令牌识别。 未识别的用户被视为匿名用户。 对于所有用户都经过识别的应用程序,请考虑删除匿名用户限制(anonHourlyRateLimit
、anonMinuteRateLimit
和 anonPermanentBlock
)。
有三个速率级别可以单独设置或禁用
userMinuteRateLimit
和 anonHourlyRateLimit
)userHourlyRateLimit
和 anonMinuteRateLimit
)userPermanentBlock
和 anonPermanentBlock
)如果达到 1 分钟限制,用户将被阻止 5 分钟。 如果达到每小时限制,用户将被阻止 60 分钟。 如果用户超过了 userPermanentBlock
或 anonPermanentBlock
中设置的值,他们将被永久禁止。 这些值在 Pico AI Proxy 中是硬编码的。
请注意,Pico AI Proxy 目前不持久化数据。 服务器重启后,所有永久阻止的用户都将被解除阻止。
注意: 强烈建议在用户订阅时,将 appAccountToken
设置为用户标识 UUID,如下所示
let result = try await subscription.product.purchase(options: [.appAccountToken(userIdentifyingUUID)])
设置 appAccountToken
使 Pico Proxy 能够使用该值进行用户特定的速率限制。
以下是 Pico Proxy 验证购买并授予用户访问权限的步骤概述
Transaction
。VerificationResult.jwsRepresentation
中的已签名 JWS 事务。https://<proxy name>.up.railway.app/appstore
。注意: 客户端应用程序应始终在收到 401 Unauthorized 错误时获取新的会话令牌。
以下代码基于 WWDC StoreKit 2 Backyard Birds 示例代码。
import StoreKit
@MainActor
public final class StoreSubscriptionController: ObservableObject {
@Published public private(set) var jwsTransaction: String?
public func purchase(option subscription: Subscription) async -> PurchaseFinishedAction {
let action: PurchaseFinishedAction
do {
// Add user identifier to transaction
let idUUID = UUID()
let result = try await subscription.product.purchase(options: [.appAccountToken(idUUID)])
switch result {
case .success(let verificationResult):
// Set the JWS token after purchase
jwsTransaction = verificationResult.jwsRepresentation
...
}
}
}
// Handle push notification from App Store
internal func handle(update status: Product.SubscriptionInfo.Status) {
guard case .verified(let transaction) = status.transaction,
case .verified(let renewalInfo) = status.renewalInfo else {
return
}
if status.state == .subscribed || status.state == .inGracePeriod {
jwsTransaction = status.transaction.jwsRepresentation
}
...
}
// Handle updated entitlement
func updateEntitlement(groupID: String) async {
guard let statuses = try? await Product.SubscriptionInfo.status(for: groupID) else {
return
}
for status in statuses {
guard case .verified(let transaction) = status.transaction,
case .verified(let renewalInfo) = status.renewalInfo else {
continue
}
if status.state == .subscribed || status.state == .inGracePeriod {
jwsTransaction = status.transaction.jwsRepresentation
}
...
}
}
要验证用户身份,请调用 Pico Proxy 的 appstore
端点
class PicoClient {
var authToken: String?
func authenticate() async throws {
// Set body to `jwsTransaction` property of `StoreSubscriptionController`
guard let body = await StoreActor.shared.subscriptionController.jwsTransaction else {
// User has no subscription
throw YourClientError.noSubscription
}
let tokenRequest = Request<Token>(
path: "appstore",
method: .post,
body: body,
headers: nil)
let clientConfiguration = APIClient.Configuration(baseURL: "<Pico Proxy URL here>")
let client = APIClient(configuration: clientConfiguration)
let tokenResponse = try await client.send(tokenRequest)
self.authToken = tokenResponse.value.token
}
func chatConnection() -> OpenAIAPIConnection {
return OpenAIAPIConnection(apiKey: authToken ?? "NO_KEY",
organization: organization,
scheme: scheme.rawValue,
host: host,
chatCompletionPath: chatCompletionPath,
port: port)
}
...
}
注意: 不建议使用此方法,因为 App Store 收据已弃用,并且该过程速度较慢且容易出错。 建议改用上述 StoreKit 2。 虽然在技术上可以结合使用 StoreKit 2 和 App Store 收据,但不建议这样做,因为购买和交易包含在 App Store 收据之间存在延迟,这可能导致 Pico Proxy 出现不正确的 Unauthorized 错误。
此流程略有不同
https://<proxy name>.up.railway.app/appstore
。使用 CleverBird
import Get
import CleverBird
var token: Token? = nil
func completion(prompt: String) async await {
let openAIConnection = OpenAIAPIConnection(apiKey: token.token, organization: "", scheme: "http", host: "localhost", port: 8080)
let chatThread = ChatThread()
.addSystemMessage(content: "You are a helpful assistant.")
.addUserMessage(content: "Who won the world series in 2020?")
do {
let completion = try await chatThread.complete(using: openAIAPIConnection)
} catch CleverBird.proxyAuthenticationRequired {
// Client needs to re-authenticate
token = try await fetchToken()
try await completion(prompt: String)
} catch CleverBirdError.unauthorized {
// Prompt user to buy a subscription
}
}
func fetchToken() async throws -> Token {
let body: String?
/*
// Fetch app store receipt
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
FileManager.default.fileExists(atPath: appStoreReceiptURL.path),
let receiptData = try? Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped) {
body = receiptData.base64EncodedString(options: [])
} else {
// when running the app in Xcode Sandbox, there will be no receipt. In sandbox, Pico AI Proxy will accept
// the receipt Id.
body = "transaction Id here"
}
*/
// Validating receipts is temporary disabled
body = "transaction Id here"
let tokenRequest = Request<Token>(
path: "appstore",
method: .post,
body: body,
headers: nil)
let tokenResponse = try await AIClient.openAIAPIConnection.client.send(tokenRequest)
return tokenResponse.value
}
struct Token: Codable {
let token: String
}
可选:使用 app account token 跟踪用户
// Create new UUID
let id = UUID()
// Add id to user account
// Purchase subscription
let result = try await product.purchase(options: [.appAccountToken(idUUID)])
Pico AI Proxy 将自动从收据中提取应用程序帐户令牌。
Pico AI Proxy 可能会生成与授权问题相关的两个不同的错误代码:unauthorized (401) 和 proxyAuthenticationRequired (407)。 unauthorized 错误表明用户缺少有效的 App Store 订阅。 另一方面,proxyAuthenticationRequired 错误表示客户端的身份验证令牌不再有效,这种情况可能在服务器重启后出现。 在后一种情况下,可以通过不需要用户干预的简单重新身份验证过程来实现重新授权。
使用下面的链接在 Railway 上部署 Pico AI Proxy。 该链接包含一个推荐代码。
或者,Pico AI Proxy 也可以手动安装在任何其他托管提供商上。