-
Notifications
You must be signed in to change notification settings - Fork 0
feat: support OpenAI Codex OAuth subscription flow #73
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
Changes from all commits
f2fb3c5
8c0377d
f0ce670
b0504bd
8a0c35e
e654942
5088b5d
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 |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |
| import dev.aceclaw.llm.anthropic.AnthropicClient; | ||
| import dev.aceclaw.llm.openai.CopilotRoutingClient; | ||
| import dev.aceclaw.llm.openai.CopilotTokenProvider; | ||
| import dev.aceclaw.llm.openai.OpenAiCodexTokenProvider; | ||
| import dev.aceclaw.llm.openai.OpenAICompatClient; | ||
| import dev.aceclaw.llm.openai.OpenAIRoutingClient; | ||
| import dev.aceclaw.llm.openai.OpenAIResponsesClient; | ||
|
|
@@ -26,6 +27,7 @@ public final class LlmClientFactory { | |
| /** Default base URLs for known providers (without trailing slash). */ | ||
| private static final Map<String, String> DEFAULT_BASE_URLS = Map.of( | ||
| "openai", "https://api.openai.com", | ||
| "openai-codex", "https://chatgpt.com/backend-api/codex", | ||
| "groq", "https://api.groq.com/openai", | ||
| "together", "https://api.together.xyz", | ||
| "mistral", "https://api.mistral.ai", | ||
|
|
@@ -36,6 +38,7 @@ public final class LlmClientFactory { | |
| /** Default model identifiers for known providers. */ | ||
| private static final Map<String, String> DEFAULT_MODELS = Map.of( | ||
| "openai", "gpt-4o", | ||
| "openai-codex", "gpt-5-codex", | ||
| "groq", "llama-3.3-70b-versatile", | ||
| "together", "meta-llama/Llama-3.3-70B-Instruct-Turbo", | ||
| "mistral", "mistral-large-latest", | ||
|
|
@@ -46,6 +49,7 @@ public final class LlmClientFactory { | |
| /** Providers that support image input. */ | ||
| private static final Map<String, ProviderCapabilities> PROVIDER_CAPABILITIES = Map.of( | ||
| "openai", ProviderCapabilities.OPENAI, | ||
| "openai-codex", ProviderCapabilities.CODEX, | ||
| "groq", ProviderCapabilities.OPENAI_COMPAT, | ||
| "together", ProviderCapabilities.OPENAI_COMPAT, | ||
| "mistral", ProviderCapabilities.OPENAI_COMPAT, | ||
|
|
@@ -92,6 +96,7 @@ public static LlmClient create(String provider, String apiKey, | |
| case "anthropic" -> createAnthropicClient(apiKey, refreshToken, baseUrl); | ||
| case "copilot" -> createCopilotClient(apiKey, baseUrl, model); | ||
| case "openai" -> createOpenAiClient(apiKey, baseUrl, model); | ||
| case "openai-codex" -> createOpenAiCodexClient(apiKey, baseUrl, model); | ||
| case "groq", "together", "mistral", "ollama" -> { | ||
| String resolvedBaseUrl = baseUrl != null ? baseUrl : DEFAULT_BASE_URLS.get(provider); | ||
| String resolvedModel = model != null ? model : DEFAULT_MODELS.getOrDefault(provider, "gpt-4o"); | ||
|
|
@@ -101,7 +106,7 @@ public static LlmClient create(String provider, String apiKey, | |
| } | ||
| default -> throw new IllegalArgumentException( | ||
| "Unknown provider: " + provider | ||
| + ". Supported: anthropic, openai, groq, together, mistral, copilot, ollama"); | ||
| + ". Supported: anthropic, openai, openai-codex, groq, together, mistral, copilot, ollama"); | ||
| }; | ||
| } | ||
|
|
||
|
|
@@ -150,6 +155,21 @@ private static LlmClient createOpenAiClient(String apiKey, String baseUrl, Strin | |
| return new OpenAIRoutingClient(chatClient, responsesClient, resolvedModel); | ||
| } | ||
|
|
||
| private static LlmClient createOpenAiCodexClient(String apiKey, String baseUrl, String model) { | ||
| var tokenProvider = new OpenAiCodexTokenProvider(apiKey); | ||
| String resolvedBaseUrl = baseUrl != null ? baseUrl : DEFAULT_BASE_URLS.get("openai-codex"); | ||
| String resolvedModel = model != null ? model : DEFAULT_MODELS.getOrDefault("openai-codex", "gpt-5-codex"); | ||
|
|
||
| var chatClient = new OpenAICompatClient( | ||
| tokenProvider, resolvedBaseUrl, "/v1/chat/completions", | ||
| "openai-codex", resolvedModel, ProviderCapabilities.OPENAI, Map.of()); | ||
| var responsesClient = new OpenAIResponsesClient( | ||
| tokenProvider, resolvedBaseUrl, "/v1/responses", | ||
| "openai-codex", resolvedModel, ProviderCapabilities.CODEX, Map.of()); | ||
|
|
||
| return new OpenAIRoutingClient(chatClient, responsesClient, resolvedModel, "openai-codex"); | ||
| } | ||
|
Comment on lines
+158
to
+171
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. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check for backend-api/codex references and related endpoint usage
rg -n "backend-api/codex" --type java -C 3
rg -n "codex" -i --type java | grep -E "endpoint|path|url|/v1" | head -20Repository: xinhuagu/AceClaw Length of output: 945 🌐 Web query:
💡 Result: Codex doesn’t have a separate “Codex API” with unique paths; Codex clients (e.g., Codex CLI) call the Responses API. The relevant documented endpoint paths are: This is the standard endpoint Codex CLI uses when authenticated with an API key. [1] This is the ChatGPT “backend-api” path used by Codex CLI when you log in with ChatGPT (it’s referenced in OpenAI’s Codex agent loop write-up, not as a general public API reference). [1] Additional related API path docs (model pages list supported endpoints) include: (plus Sources: OpenAI “Unrolling the Codex agent loop” (Jan 23, 2026) [1]; OpenAI model docs showing supported endpoints for Codex-capable models [2]. Fix incorrect endpoint paths for ChatGPT backend-api Codex client. The base URL 🤖 Prompt for AI Agents |
||
|
|
||
| private static LlmClient createAnthropicClient(String apiKey, String refreshToken, String baseUrl) { | ||
| if (apiKey != null && apiKey.startsWith("sk-ant-oat") && refreshToken != null) { | ||
| if (baseUrl != null) { | ||
|
|
||
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.
🧩 Analysis chain
🏁 Script executed:
Repository: xinhuagu/AceClaw
Length of output: 1604
🏁 Script executed:
Repository: xinhuagu/AceClaw
Length of output: 50372
🌐 Web query:
Jackson ObjectMapper readTree null return behavior JsonNode💡 Result:
ObjectMapper.readTree(...)(andObjectCodec#readTree(JsonParser)) returns Javanullonly when there is no JSON content to read—i.e., the parser is at end-of-input / no more tokens. It does not return Javanullfor a JSON literalnull; JSONnullis represented as a non-nullJsonNodewherenode.isNull()istrue(aNullNode). [1][2]This commonly shows up in custom deserializers when you do:
After the default deserializer has consumed the JSON, the parser has “no more content accessible”, so
readTree(p)returnsnull. [3][4]How to handle it (typical fixes):
JsonParsertwice), e.g. viaTreeTraversingParser. [4]TokenBufferand create two parsers from it (one for default deserialization, one for tree access). [4]Rule of thumb: if you need both “default POJO deserialization” and “tree access”, you must buffer (tree or token buffer) because a
JsonParseris forward-only. [4]Sources:
[1] Jackson
ObjectMapper#readTreeJavadoc (returnnullon no content; JSONnull=>JsonNode.isNull())[2]
ObjectMapper.readValue()returns null only for JSON literalnull(Stack Overflow)[3]
ObjectMapper.readTree(JsonParser)implementation notes: returnsnullif no more content accessible[4] “Use JsonParser again after deserialization” solution: read tree first / use
TreeTraversingParserorTokenBuffer(Stack Overflow)Add null check after
readTree()to prevent NPE on empty or malformed auth.json.If the auth file is empty,
mapper.readTree()returns null, causing an uncaughtNullPointerExceptionattree.path(). Additionally, checktree.isNull()to handle JSONnullcontent gracefully.Proposed fix
try { var mapper = new ObjectMapper(); var tree = mapper.readTree(CODEX_AUTH_FILE.toFile()); + if (tree == null || tree.isNull()) { + log.warn("Codex auth file {} is empty or null JSON", CODEX_AUTH_FILE); + return; + } String token = null; var tokens = tree.path("tokens");🤖 Prompt for AI Agents