-
Notifications
You must be signed in to change notification settings - Fork 16
fix: support configurable Codex usage endpoints #108
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
10d172f
aebb231
ecfa241
916ef7e
4c3e55d
c54f790
f3bc2e2
49d96d8
b4d38a3
71e7537
9f74e62
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -305,8 +305,9 @@ struct CodexAuth: Codable { | |||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /// codex-lb row payload from store.db/accounts table | ||||||||||||||||||||||||||||||||||
| private struct CodexLBEncryptedAccount { | ||||||||||||||||||||||||||||||||||
| struct CodexLBEncryptedAccount { | ||||||||||||||||||||||||||||||||||
| let accountId: String? | ||||||||||||||||||||||||||||||||||
| let chatGPTAccountId: String? | ||||||||||||||||||||||||||||||||||
| let email: String? | ||||||||||||||||||||||||||||||||||
| let planType: String? | ||||||||||||||||||||||||||||||||||
| let status: String? | ||||||||||||||||||||||||||||||||||
|
|
@@ -327,12 +328,23 @@ enum OpenAIAuthSource { | |||||||||||||||||||||||||||||||||
| struct OpenAIAuthAccount { | ||||||||||||||||||||||||||||||||||
| let accessToken: String | ||||||||||||||||||||||||||||||||||
| let accountId: String? | ||||||||||||||||||||||||||||||||||
| let externalUsageAccountId: String? | ||||||||||||||||||||||||||||||||||
| let email: String? | ||||||||||||||||||||||||||||||||||
| let authSource: String | ||||||||||||||||||||||||||||||||||
| let sourceLabels: [String] | ||||||||||||||||||||||||||||||||||
| let source: OpenAIAuthSource | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| enum CodexEndpointMode: Equatable { | ||||||||||||||||||||||||||||||||||
| case directChatGPT | ||||||||||||||||||||||||||||||||||
| case external(usageURL: URL) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| struct CodexEndpointConfiguration: Equatable { | ||||||||||||||||||||||||||||||||||
| let mode: CodexEndpointMode | ||||||||||||||||||||||||||||||||||
| let source: String | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /// Auth source types for Claude account discovery | ||||||||||||||||||||||||||||||||||
| enum ClaudeAuthSource { | ||||||||||||||||||||||||||||||||||
| case opencodeAuth | ||||||||||||||||||||||||||||||||||
|
|
@@ -902,6 +914,74 @@ final class TokenManager: @unchecked Sendable { | |||||||||||||||||||||||||||||||||
| return current as? String | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| func codexEndpointConfiguration( | ||||||||||||||||||||||||||||||||||
| from configDictionary: [String: Any]?, | ||||||||||||||||||||||||||||||||||
| sourcePath: String? = nil | ||||||||||||||||||||||||||||||||||
| ) -> CodexEndpointConfiguration { | ||||||||||||||||||||||||||||||||||
| let defaultConfiguration = CodexEndpointConfiguration( | ||||||||||||||||||||||||||||||||||
| mode: .directChatGPT, | ||||||||||||||||||||||||||||||||||
| source: "Default ChatGPT usage endpoint" | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| guard let configDictionary else { | ||||||||||||||||||||||||||||||||||
| return defaultConfiguration | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if let explicitUsageURL = resolveConfigValue( | ||||||||||||||||||||||||||||||||||
| nestedString(in: configDictionary, path: ["opencode-bar", "codex", "usageURL"]) | ||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||
| if let resolvedURL = URL(string: explicitUsageURL), | ||||||||||||||||||||||||||||||||||
| let scheme = resolvedURL.scheme?.lowercased(), | ||||||||||||||||||||||||||||||||||
| scheme == "http" || scheme == "https", | ||||||||||||||||||||||||||||||||||
| resolvedURL.host != nil { | ||||||||||||||||||||||||||||||||||
| return CodexEndpointConfiguration( | ||||||||||||||||||||||||||||||||||
| mode: .external(usageURL: resolvedURL), | ||||||||||||||||||||||||||||||||||
| source: sourcePath ?? "opencode-bar.codex.usageURL" | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| logger.warning( | ||||||||||||||||||||||||||||||||||
| "Ignoring invalid Codex usage URL override '\(explicitUsageURL, privacy: .public)' from \(sourcePath ?? "opencode-bar.codex.usageURL", privacy: .public)" | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if let baseURLString = resolveConfigValue( | ||||||||||||||||||||||||||||||||||
| nestedString(in: configDictionary, path: ["provider", "openai", "options", "baseURL"]) | ||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||
| if let baseURL = URL(string: baseURLString), | ||||||||||||||||||||||||||||||||||
| let scheme = baseURL.scheme?.lowercased(), | ||||||||||||||||||||||||||||||||||
| scheme == "http" || scheme == "https", | ||||||||||||||||||||||||||||||||||
| let host = baseURL.host { | ||||||||||||||||||||||||||||||||||
| var components = URLComponents() | ||||||||||||||||||||||||||||||||||
| components.scheme = scheme | ||||||||||||||||||||||||||||||||||
| components.host = host | ||||||||||||||||||||||||||||||||||
| components.port = baseURL.port | ||||||||||||||||||||||||||||||||||
| components.path = "/api/codex/usage" | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if let usageURL = components.url { | ||||||||||||||||||||||||||||||||||
| return CodexEndpointConfiguration( | ||||||||||||||||||||||||||||||||||
| mode: .external(usageURL: usageURL), | ||||||||||||||||||||||||||||||||||
| source: sourcePath ?? "provider.openai.options.baseURL" | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| logger.warning( | ||||||||||||||||||||||||||||||||||
| "Ignoring invalid OpenAI base URL '\(baseURLString, privacy: .public)' from \(sourcePath ?? "provider.openai.options.baseURL", privacy: .public)" | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return defaultConfiguration | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| func getCodexEndpointConfiguration() -> CodexEndpointConfiguration { | ||||||||||||||||||||||||||||||||||
| let config = readOpenCodeConfigJSON() | ||||||||||||||||||||||||||||||||||
| return codexEndpointConfiguration( | ||||||||||||||||||||||||||||||||||
| from: config, | ||||||||||||||||||||||||||||||||||
| sourcePath: lastFoundOpenCodeConfigPath?.path | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| private struct SearchAPIKeyLookupSource { | ||||||||||||||||||||||||||||||||||
| let dictionary: [String: Any]? | ||||||||||||||||||||||||||||||||||
| let sourcePath: String? | ||||||||||||||||||||||||||||||||||
|
|
@@ -1172,6 +1252,7 @@ final class TokenManager: @unchecked Sendable { | |||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||
| SELECT | ||||||||||||||||||||||||||||||||||
| id, | ||||||||||||||||||||||||||||||||||
| chatgpt_account_id, | ||||||||||||||||||||||||||||||||||
| email, | ||||||||||||||||||||||||||||||||||
| plan_type, | ||||||||||||||||||||||||||||||||||
| status, | ||||||||||||||||||||||||||||||||||
|
|
@@ -1184,6 +1265,33 @@ final class TokenManager: @unchecked Sendable { | |||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||
| SELECT | ||||||||||||||||||||||||||||||||||
| id, | ||||||||||||||||||||||||||||||||||
| chatgpt_account_id, | ||||||||||||||||||||||||||||||||||
| email, | ||||||||||||||||||||||||||||||||||
| NULL AS plan_type, | ||||||||||||||||||||||||||||||||||
| NULL AS status, | ||||||||||||||||||||||||||||||||||
| access_token_encrypted, | ||||||||||||||||||||||||||||||||||
| NULL AS refresh_token_encrypted, | ||||||||||||||||||||||||||||||||||
| NULL AS id_token_encrypted, | ||||||||||||||||||||||||||||||||||
| NULL AS last_refresh | ||||||||||||||||||||||||||||||||||
| FROM accounts | ||||||||||||||||||||||||||||||||||
| """, | ||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||
| SELECT | ||||||||||||||||||||||||||||||||||
| id, | ||||||||||||||||||||||||||||||||||
| NULL AS chatgpt_account_id, | ||||||||||||||||||||||||||||||||||
| email, | ||||||||||||||||||||||||||||||||||
| plan_type, | ||||||||||||||||||||||||||||||||||
| status, | ||||||||||||||||||||||||||||||||||
| access_token_encrypted, | ||||||||||||||||||||||||||||||||||
| refresh_token_encrypted, | ||||||||||||||||||||||||||||||||||
| id_token_encrypted, | ||||||||||||||||||||||||||||||||||
| last_refresh | ||||||||||||||||||||||||||||||||||
| FROM accounts | ||||||||||||||||||||||||||||||||||
| """, | ||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||
| SELECT | ||||||||||||||||||||||||||||||||||
| id, | ||||||||||||||||||||||||||||||||||
| NULL AS chatgpt_account_id, | ||||||||||||||||||||||||||||||||||
| email, | ||||||||||||||||||||||||||||||||||
| NULL AS plan_type, | ||||||||||||||||||||||||||||||||||
| NULL AS status, | ||||||||||||||||||||||||||||||||||
|
|
@@ -1222,19 +1330,20 @@ final class TokenManager: @unchecked Sendable { | |||||||||||||||||||||||||||||||||
| throw CodexLBError.sqliteStepFailed(stepStatus) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| guard let accessTokenEncrypted = sqliteColumnData(statement, index: 4), !accessTokenEncrypted.isEmpty else { | ||||||||||||||||||||||||||||||||||
| guard let accessTokenEncrypted = sqliteColumnData(statement, index: 5), !accessTokenEncrypted.isEmpty else { | ||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| let account = CodexLBEncryptedAccount( | ||||||||||||||||||||||||||||||||||
| accountId: sqliteColumnString(statement, index: 0), | ||||||||||||||||||||||||||||||||||
| email: sqliteColumnString(statement, index: 1), | ||||||||||||||||||||||||||||||||||
| planType: sqliteColumnString(statement, index: 2), | ||||||||||||||||||||||||||||||||||
| status: sqliteColumnString(statement, index: 3), | ||||||||||||||||||||||||||||||||||
| chatGPTAccountId: sqliteColumnString(statement, index: 1), | ||||||||||||||||||||||||||||||||||
| email: sqliteColumnString(statement, index: 2), | ||||||||||||||||||||||||||||||||||
| planType: sqliteColumnString(statement, index: 3), | ||||||||||||||||||||||||||||||||||
| status: sqliteColumnString(statement, index: 4), | ||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The SQL query column order is now: But line 1140 reads The result: Fix:
Suggested change
Wait — |
||||||||||||||||||||||||||||||||||
| accessTokenEncrypted: accessTokenEncrypted, | ||||||||||||||||||||||||||||||||||
| refreshTokenEncrypted: sqliteColumnData(statement, index: 5), | ||||||||||||||||||||||||||||||||||
| idTokenEncrypted: sqliteColumnData(statement, index: 6), | ||||||||||||||||||||||||||||||||||
| lastRefresh: sqliteColumnString(statement, index: 7) | ||||||||||||||||||||||||||||||||||
| refreshTokenEncrypted: sqliteColumnData(statement, index: 6), | ||||||||||||||||||||||||||||||||||
| idTokenEncrypted: sqliteColumnData(statement, index: 7), | ||||||||||||||||||||||||||||||||||
| lastRefresh: sqliteColumnString(statement, index: 8) | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| accounts.append(account) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
@@ -1373,6 +1482,29 @@ final class TokenManager: @unchecked Sendable { | |||||||||||||||||||||||||||||||||
| return token | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| func makeCodexLBOpenAIAccount( | ||||||||||||||||||||||||||||||||||
| from encryptedAccount: CodexLBEncryptedAccount, | ||||||||||||||||||||||||||||||||||
| accessToken: String, | ||||||||||||||||||||||||||||||||||
| authSourcePath: String | ||||||||||||||||||||||||||||||||||
| ) -> OpenAIAuthAccount { | ||||||||||||||||||||||||||||||||||
| let normalizedAccountId = encryptedAccount.accountId? | ||||||||||||||||||||||||||||||||||
| .trimmingCharacters(in: .whitespacesAndNewlines) | ||||||||||||||||||||||||||||||||||
| let normalizedChatGPTAccountId = encryptedAccount.chatGPTAccountId? | ||||||||||||||||||||||||||||||||||
| .trimmingCharacters(in: .whitespacesAndNewlines) | ||||||||||||||||||||||||||||||||||
| let normalizedEmail = encryptedAccount.email? | ||||||||||||||||||||||||||||||||||
| .trimmingCharacters(in: .whitespacesAndNewlines) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return OpenAIAuthAccount( | ||||||||||||||||||||||||||||||||||
| accessToken: accessToken, | ||||||||||||||||||||||||||||||||||
| accountId: normalizedAccountId?.isEmpty == true ? nil : normalizedAccountId, | ||||||||||||||||||||||||||||||||||
| externalUsageAccountId: normalizedChatGPTAccountId?.isEmpty == true ? nil : normalizedChatGPTAccountId, | ||||||||||||||||||||||||||||||||||
| email: normalizedEmail?.isEmpty == true ? nil : normalizedEmail, | ||||||||||||||||||||||||||||||||||
| authSource: authSourcePath, | ||||||||||||||||||||||||||||||||||
| sourceLabels: [openAISourceLabel(for: .codexLB)], | ||||||||||||||||||||||||||||||||||
| source: .codexLB | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| func readCodexLBOpenAIAccounts() -> [OpenAIAuthAccount] { | ||||||||||||||||||||||||||||||||||
| return queue.sync { | ||||||||||||||||||||||||||||||||||
| if let cached = cachedCodexLBAccounts, | ||||||||||||||||||||||||||||||||||
|
|
@@ -1410,25 +1542,20 @@ final class TokenManager: @unchecked Sendable { | |||||||||||||||||||||||||||||||||
| encryptedAccount.accessTokenEncrypted, | ||||||||||||||||||||||||||||||||||
| key: fernetKey | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| let normalizedAccountId = encryptedAccount.accountId? | ||||||||||||||||||||||||||||||||||
| .trimmingCharacters(in: .whitespacesAndNewlines) | ||||||||||||||||||||||||||||||||||
| let normalizedEmail = encryptedAccount.email? | ||||||||||||||||||||||||||||||||||
| .trimmingCharacters(in: .whitespacesAndNewlines) | ||||||||||||||||||||||||||||||||||
| decodedAccounts.append( | ||||||||||||||||||||||||||||||||||
| OpenAIAuthAccount( | ||||||||||||||||||||||||||||||||||
| makeCodexLBOpenAIAccount( | ||||||||||||||||||||||||||||||||||
| from: encryptedAccount, | ||||||||||||||||||||||||||||||||||
| accessToken: accessToken, | ||||||||||||||||||||||||||||||||||
| accountId: normalizedAccountId?.isEmpty == true ? nil : normalizedAccountId, | ||||||||||||||||||||||||||||||||||
| email: normalizedEmail?.isEmpty == true ? nil : normalizedEmail, | ||||||||||||||||||||||||||||||||||
| authSource: candidate.databaseURL.path, | ||||||||||||||||||||||||||||||||||
| sourceLabels: [openAISourceLabel(for: .codexLB)], | ||||||||||||||||||||||||||||||||||
| source: .codexLB | ||||||||||||||||||||||||||||||||||
| authSourcePath: candidate.databaseURL.path | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| // PII fields (email, account ID) kept at debug level to avoid | ||||||||||||||||||||||||||||||||||
| // leaking personal info in production console logs. | ||||||||||||||||||||||||||||||||||
| logger.debug( | ||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||
| codex-lb account loaded: id=\(normalizedAccountId ?? "nil"), \ | ||||||||||||||||||||||||||||||||||
| \(candidate.databaseURL.path, privacy: .public) codex-lb account loaded: \ | ||||||||||||||||||||||||||||||||||
| id=\(encryptedAccount.accountId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "nil"), \ | ||||||||||||||||||||||||||||||||||
| chatgpt_account_id=\(encryptedAccount.chatGPTAccountId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "nil"), \ | ||||||||||||||||||||||||||||||||||
| email=\(encryptedAccount.email ?? "unknown"), \ | ||||||||||||||||||||||||||||||||||
| plan=\(encryptedAccount.planType ?? "unknown"), \ | ||||||||||||||||||||||||||||||||||
| status=\(encryptedAccount.status ?? "unknown"), \ | ||||||||||||||||||||||||||||||||||
|
|
@@ -1607,6 +1734,7 @@ final class TokenManager: @unchecked Sendable { | |||||||||||||||||||||||||||||||||
| return OpenAIAuthAccount( | ||||||||||||||||||||||||||||||||||
| accessToken: primary.accessToken, | ||||||||||||||||||||||||||||||||||
| accountId: (primaryAccountId?.isEmpty == false) ? primaryAccountId : fallbackAccountId, | ||||||||||||||||||||||||||||||||||
| externalUsageAccountId: normalizedNonEmpty(primary.externalUsageAccountId) ?? normalizedNonEmpty(fallback.externalUsageAccountId), | ||||||||||||||||||||||||||||||||||
| email: (primaryEmail?.isEmpty == false) ? primaryEmail : fallbackEmail, | ||||||||||||||||||||||||||||||||||
| authSource: primary.authSource, | ||||||||||||||||||||||||||||||||||
| sourceLabels: mergedSourceLabels, | ||||||||||||||||||||||||||||||||||
|
|
@@ -2646,6 +2774,7 @@ final class TokenManager: @unchecked Sendable { | |||||||||||||||||||||||||||||||||
| OpenAIAuthAccount( | ||||||||||||||||||||||||||||||||||
| accessToken: access, | ||||||||||||||||||||||||||||||||||
| accountId: auth.openai?.accountId, | ||||||||||||||||||||||||||||||||||
| externalUsageAccountId: nil, | ||||||||||||||||||||||||||||||||||
| email: nil, | ||||||||||||||||||||||||||||||||||
| authSource: authSource, | ||||||||||||||||||||||||||||||||||
| sourceLabels: [openAISourceLabel(for: .opencodeAuth)], | ||||||||||||||||||||||||||||||||||
|
|
@@ -2673,6 +2802,7 @@ final class TokenManager: @unchecked Sendable { | |||||||||||||||||||||||||||||||||
| OpenAIAuthAccount( | ||||||||||||||||||||||||||||||||||
| accessToken: access, | ||||||||||||||||||||||||||||||||||
| accountId: codexAuth.tokens?.accountId, | ||||||||||||||||||||||||||||||||||
| externalUsageAccountId: nil, | ||||||||||||||||||||||||||||||||||
| email: codexEmail, | ||||||||||||||||||||||||||||||||||
| authSource: authSource, | ||||||||||||||||||||||||||||||||||
| sourceLabels: [openAISourceLabel(for: .codexAuth)], | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both branches of the
.externalcase return the exact same expression:Either the
codexLBcheck was supposed to do something different (like returnaccount.externalUsageAccountIdwithout the?? account.accountIdfallback), or the if-statement is dead code and should be removed.If the intent is "codex-lb uses external ID, other sources use regular accountId":
Otherwise just collapse to one line: