add: codex api support#1243
Conversation
📝 WalkthroughWalkthroughThis PR adds complete Codex Responses API provider support by implementing model mapping, a streaming HTTP client, query orchestration, provider routing, settings schema updates, model selection helpers, and user-facing CLI/OAuth configuration flows, enabling users to switch between Anthropic, OpenAI, and Codex providers via environment variables or CLI command. ChangesCodex Provider Integration
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/services/api/codex/index.ts`:
- Around line 105-107: Replace the loose any types: change contentBlocks from
Record<number, any> to a safer type (e.g., Record<number, unknown> or
Map<number, unknown> depending on how it's used) and give partialMessage a
concrete type (for example AssistantMessage | undefined or unknown or a defined
PartialAssistantMessage interface) instead of any; update any downstream code
that accesses contentBlocks or partialMessage to narrow/cast the unknown safely
(using type guards or explicit mapping) so the compiler knows the actual shape
when using symbols contentBlocks and partialMessage.
- Around line 191-192: Replace the unsafe "as any" casts by giving `usage` a
proper type that matches what `calculateUSDCost` and `addToTotalSessionCost`
expect; update the `usage` variable declaration (or the function signatures) to
use that concrete type (e.g., `Usage`, `OpenAIUsage`, or your internal
`ModelUsage`), import/define that type and then call
`calculateUSDCost(codexModel, usage)` and `addToTotalSessionCost(costUSD, usage,
options.model)` without `as any`; if only one function needs a different shape,
add a small adapter/mapper function (e.g., `toExpectedUsage(usage)`) to convert
the current `usage` to the required type rather than casting.
- Around line 117-185: The loop over adaptedStream uses (event as any)
everywhere; define a discriminated union for the stream events (e.g.,
MessageStartEvent | ContentBlockStartEvent | ContentBlockDeltaEvent |
ContentBlockStopEvent | MessageDeltaEvent | MessageStopEvent), update the
for-await signature to use that union, and replace all (event as any).X with
strongly typed access (event.message, event.index, event.delta, event.usage,
etc.). Add minimal type-guard helpers where needed (e.g.,
isContentBlockDelta(event): event is ContentBlockDeltaEvent) to narrow variants
in switch cases, and if you must coerce due to upstream types use the approved
double-assertion (as unknown as YourEventType) only at the stream boundary; keep
rest of code references (partialMessage, contentBlocks, updateOpenAIUsage,
normalizeContentFromAPI, AssistantMessage, randomUUID) unchanged.
In `@src/utils/managedEnvConstants.ts`:
- Around line 83-101: PROVIDER_MANAGED_ENV_VARS is missing the new flag
CLAUDE_CODE_USE_CODEX which getAPIProvider() uses to decide Codex routing,
allowing settings-sourced env to override host-managed routing; add the string
'CLAUDE_CODE_USE_CODEX' to the PROVIDER_MANAGED_ENV_VARS array in
src/utils/managedEnvConstants.ts so host-managed mode will treat that key as
provider-managed and prevent settings from forcing Codex selection in
getAPIProvider().
In `@src/utils/model/__tests__/codexModelOptions.test.ts`:
- Around line 36-39: The test "uses CODEX_MODEL as the default model when no
explicit model is selected" is not environment-isolated; modify it to save and
restore any affected env vars (at minimum OPENAI_AUTH_MODE and CODEX_AUTH_MODE)
or explicitly clear them for the test, then restore originals after the test
finishes; keep the existing process.env.CLAUDE_CODE_USE_CODEX and
process.env.CODEX_MODEL assignments but wrap them with backing up original
values (e.g., const prev = process.env.OPENAI_AUTH_MODE;
process.env.OPENAI_AUTH_MODE = ''; ... ) and restore
(process.env.OPENAI_AUTH_MODE = prev) in a finally or afterEach so the test does
not depend on ambient runner environment.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8e226cfa-5d7d-4d6d-a96a-9376807b5ccc
📒 Files selected for processing (21)
packages/@ant/model-provider/src/index.tspackages/@ant/model-provider/src/providers/codex/__tests__/modelMapping.test.tspackages/@ant/model-provider/src/providers/codex/modelMapping.tssrc/commands/logout/logout.tsxsrc/commands/provider.tssrc/components/ConsoleOAuthFlow.tsxsrc/services/api/claude.tssrc/services/api/codex/__tests__/client.test.tssrc/services/api/codex/client.tssrc/services/api/codex/index.tssrc/services/api/openai/responsesAdapter.tssrc/utils/managedEnvConstants.tssrc/utils/model/__tests__/codexModelOptions.test.tssrc/utils/model/__tests__/providers.test.tssrc/utils/model/chatgptModels.tssrc/utils/model/configs.tssrc/utils/model/model.tssrc/utils/model/modelOptions.tssrc/utils/model/providers.tssrc/utils/settings/types.tssrc/utils/status.tsx
| const contentBlocks: Record<number, any> = {} | ||
| const collectedMessages: AssistantMessage[] = [] | ||
| let partialMessage: any |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Avoid as any in production code.
Variables contentBlocks and partialMessage use any typing. Per coding guidelines, production code should use specific types or Record<string, unknown> for objects with unknown structure.
- const contentBlocks: Record<number, any> = {}
+ const contentBlocks: Record<number, Record<string, unknown>> = {}
const collectedMessages: AssistantMessage[] = []
- let partialMessage: any
+ let partialMessage: Record<string, unknown> | undefined📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const contentBlocks: Record<number, any> = {} | |
| const collectedMessages: AssistantMessage[] = [] | |
| let partialMessage: any | |
| const contentBlocks: Record<number, Record<string, unknown>> = {} | |
| const collectedMessages: AssistantMessage[] = [] | |
| let partialMessage: Record<string, unknown> | undefined |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/services/api/codex/index.ts` around lines 105 - 107, Replace the loose
any types: change contentBlocks from Record<number, any> to a safer type (e.g.,
Record<number, unknown> or Map<number, unknown> depending on how it's used) and
give partialMessage a concrete type (for example AssistantMessage | undefined or
unknown or a defined PartialAssistantMessage interface) instead of any; update
any downstream code that accesses contentBlocks or partialMessage to narrow/cast
the unknown safely (using type guards or explicit mapping) so the compiler knows
the actual shape when using symbols contentBlocks and partialMessage.
| for await (const event of adaptedStream) { | ||
| switch (event.type) { | ||
| case 'message_start': { | ||
| partialMessage = (event as any).message | ||
| ttftMs = Date.now() - start | ||
| if ((event as any).message?.usage) { | ||
| usage = updateOpenAIUsage(usage, (event as any).message.usage) | ||
| } | ||
| break | ||
| } | ||
| case 'content_block_start': { | ||
| const idx = (event as any).index | ||
| const cb = (event as any).content_block | ||
| if (cb.type === 'tool_use') { | ||
| contentBlocks[idx] = { ...cb, input: '' } | ||
| } else if (cb.type === 'text') { | ||
| contentBlocks[idx] = { ...cb, text: '' } | ||
| } else if (cb.type === 'thinking') { | ||
| contentBlocks[idx] = { ...cb, thinking: '', signature: '' } | ||
| } else { | ||
| contentBlocks[idx] = { ...cb } | ||
| } | ||
| break | ||
| } | ||
| case 'content_block_delta': { | ||
| const idx = (event as any).index | ||
| const delta = (event as any).delta | ||
| const block = contentBlocks[idx] | ||
| if (!block) break | ||
| if (delta.type === 'text_delta') { | ||
| block.text = (block.text || '') + delta.text | ||
| } else if (delta.type === 'input_json_delta') { | ||
| block.input = (block.input || '') + delta.partial_json | ||
| } else if (delta.type === 'thinking_delta') { | ||
| block.thinking = (block.thinking || '') + delta.thinking | ||
| } else if (delta.type === 'signature_delta') { | ||
| block.signature = delta.signature | ||
| } | ||
| break | ||
| } | ||
| case 'content_block_stop': { | ||
| const idx = (event as any).index | ||
| const block = contentBlocks[idx] | ||
| if (!block || !partialMessage) break | ||
|
|
||
| const m: AssistantMessage = { | ||
| message: { | ||
| ...partialMessage, | ||
| content: normalizeContentFromAPI([block], tools, options.agentId), | ||
| }, | ||
| requestId: undefined, | ||
| type: 'assistant', | ||
| uuid: randomUUID(), | ||
| timestamp: new Date().toISOString(), | ||
| } | ||
| collectedMessages.push(m) | ||
| yield m | ||
| break | ||
| } | ||
| case 'message_delta': { | ||
| const deltaUsage = (event as any).usage | ||
| if (deltaUsage) { | ||
| usage = updateOpenAIUsage(usage, deltaUsage) | ||
| } | ||
| break | ||
| } | ||
| case 'message_stop': | ||
| break | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift
Replace as any with typed event handling.
The event loop uses (event as any) repeatedly to access event properties. This violates the coding guideline prohibiting as any in production code. Define a discriminated union type or use type guards for the stream events.
♻️ Suggested approach
+// Define or import proper event types
+type StreamEventWithIndex = { type: string; index?: number; content_block?: Record<string, unknown>; delta?: Record<string, unknown>; message?: Record<string, unknown>; usage?: Record<string, unknown> }
for await (const event of adaptedStream) {
+ const typedEvent = event as StreamEventWithIndex
switch (event.type) {
case 'message_start': {
- partialMessage = (event as any).message
+ partialMessage = typedEvent.message
ttftMs = Date.now() - start
- if ((event as any).message?.usage) {
- usage = updateOpenAIUsage(usage, (event as any).message.usage)
+ if (typedEvent.message?.usage) {
+ usage = updateOpenAIUsage(usage, typedEvent.message.usage as typeof usage)
}
break
}
case 'content_block_start': {
- const idx = (event as any).index
- const cb = (event as any).content_block
+ const idx = typedEvent.index ?? 0
+ const cb = typedEvent.content_block ?? {}
// ... rest of the block
}
// ... similar changes for other casesAs per coding guidelines: "Prohibit as any type assertions in production code; in test files, as any is permitted for mock data. Use as unknown as SpecificType double assertion or interface supplementation when type mismatches occur"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/services/api/codex/index.ts` around lines 117 - 185, The loop over
adaptedStream uses (event as any) everywhere; define a discriminated union for
the stream events (e.g., MessageStartEvent | ContentBlockStartEvent |
ContentBlockDeltaEvent | ContentBlockStopEvent | MessageDeltaEvent |
MessageStopEvent), update the for-await signature to use that union, and replace
all (event as any).X with strongly typed access (event.message, event.index,
event.delta, event.usage, etc.). Add minimal type-guard helpers where needed
(e.g., isContentBlockDelta(event): event is ContentBlockDeltaEvent) to narrow
variants in switch cases, and if you must coerce due to upstream types use the
approved double-assertion (as unknown as YourEventType) only at the stream
boundary; keep rest of code references (partialMessage, contentBlocks,
updateOpenAIUsage, normalizeContentFromAPI, AssistantMessage, randomUUID)
unchanged.
| const costUSD = calculateUSDCost(codexModel, usage as any) | ||
| addToTotalSessionCost(costUSD, usage as any, options.model) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Avoid as any for usage object.
The usage as any casts should use proper typing.
- const costUSD = calculateUSDCost(codexModel, usage as any)
- addToTotalSessionCost(costUSD, usage as any, options.model)
+ const costUSD = calculateUSDCost(codexModel, usage as unknown as Parameters<typeof calculateUSDCost>[1])
+ addToTotalSessionCost(costUSD, usage as unknown as Parameters<typeof addToTotalSessionCost>[1], options.model)Alternatively, define the usage type to match what these functions expect.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const costUSD = calculateUSDCost(codexModel, usage as any) | |
| addToTotalSessionCost(costUSD, usage as any, options.model) | |
| const costUSD = calculateUSDCost(codexModel, usage as unknown as Parameters<typeof calculateUSDCost>[1]) | |
| addToTotalSessionCost(costUSD, usage as unknown as Parameters<typeof addToTotalSessionCost>[1], options.model) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/services/api/codex/index.ts` around lines 191 - 192, Replace the unsafe
"as any" casts by giving `usage` a proper type that matches what
`calculateUSDCost` and `addToTotalSessionCost` expect; update the `usage`
variable declaration (or the function signatures) to use that concrete type
(e.g., `Usage`, `OpenAIUsage`, or your internal `ModelUsage`), import/define
that type and then call `calculateUSDCost(codexModel, usage)` and
`addToTotalSessionCost(costUSD, usage, options.model)` without `as any`; if only
one function needs a different shape, add a small adapter/mapper function (e.g.,
`toExpectedUsage(usage)`) to convert the current `usage` to the required type
rather than casting.
| // Codex provider specific | ||
| 'CODEX_AUTH_MODE', | ||
| 'CODEX_API_KEY', | ||
| 'CODEX_BASE_URL', | ||
| 'CODEX_RESPONSES_URL', | ||
| 'CODEX_MODEL', | ||
| 'CODEX_DEFAULT_HAIKU_MODEL', | ||
| 'CODEX_DEFAULT_HAIKU_MODEL_DESCRIPTION', | ||
| 'CODEX_DEFAULT_HAIKU_MODEL_NAME', | ||
| 'CODEX_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES', | ||
| 'CODEX_DEFAULT_OPUS_MODEL', | ||
| 'CODEX_DEFAULT_OPUS_MODEL_DESCRIPTION', | ||
| 'CODEX_DEFAULT_OPUS_MODEL_NAME', | ||
| 'CODEX_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES', | ||
| 'CODEX_DEFAULT_SONNET_MODEL', | ||
| 'CODEX_DEFAULT_SONNET_MODEL_DESCRIPTION', | ||
| 'CODEX_DEFAULT_SONNET_MODEL_NAME', | ||
| 'CODEX_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES', | ||
| 'CODEX_SMALL_FAST_MODEL', |
There was a problem hiding this comment.
Add CLAUDE_CODE_USE_CODEX to provider-managed routing env keys.
getAPIProvider() now routes on CLAUDE_CODE_USE_CODEX, but this key is not in PROVIDER_MANAGED_ENV_VARS. In host-managed mode, that lets settings-sourced env still force Codex selection and bypass host routing intent.
Suggested patch
const PROVIDER_MANAGED_ENV_VARS = new Set([
// The flag itself — settings can't unset it once the host set it
'CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST',
// Provider selection
'CLAUDE_CODE_USE_BEDROCK',
'CLAUDE_CODE_USE_VERTEX',
'CLAUDE_CODE_USE_FOUNDRY',
'CLAUDE_CODE_USE_GEMINI',
+ 'CLAUDE_CODE_USE_CODEX',🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/utils/managedEnvConstants.ts` around lines 83 - 101,
PROVIDER_MANAGED_ENV_VARS is missing the new flag CLAUDE_CODE_USE_CODEX which
getAPIProvider() uses to decide Codex routing, allowing settings-sourced env to
override host-managed routing; add the string 'CLAUDE_CODE_USE_CODEX' to the
PROVIDER_MANAGED_ENV_VARS array in src/utils/managedEnvConstants.ts so
host-managed mode will treat that key as provider-managed and prevent settings
from forcing Codex selection in getAPIProvider().
| test('uses CODEX_MODEL as the default model when no explicit model is selected', () => { | ||
| process.env.CLAUDE_CODE_USE_CODEX = '1' | ||
| process.env.CODEX_MODEL = 'deepseek-v4-pro[1m]' | ||
|
|
There was a problem hiding this comment.
Make this test env-isolated like the first one.
Line 36-39 sets Codex env, but unlike Line 27-28 it doesn’t clear OPENAI_AUTH_MODE / CODEX_AUTH_MODE. This can make behavior depend on ambient runner env.
Proposed patch
test('uses CODEX_MODEL as the default model when no explicit model is selected', () => {
process.env.CLAUDE_CODE_USE_CODEX = '1'
process.env.CODEX_MODEL = 'deepseek-v4-pro[1m]'
+ delete process.env.OPENAI_AUTH_MODE
+ delete process.env.CODEX_AUTH_MODE
expect(getDefaultSonnetModel()).toBe('deepseek-v4-pro[1m]')
})🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/utils/model/__tests__/codexModelOptions.test.ts` around lines 36 - 39,
The test "uses CODEX_MODEL as the default model when no explicit model is
selected" is not environment-isolated; modify it to save and restore any
affected env vars (at minimum OPENAI_AUTH_MODE and CODEX_AUTH_MODE) or
explicitly clear them for the test, then restore originals after the test
finishes; keep the existing process.env.CLAUDE_CODE_USE_CODEX and
process.env.CODEX_MODEL assignments but wrap them with backing up original
values (e.g., const prev = process.env.OPENAI_AUTH_MODE;
process.env.OPENAI_AUTH_MODE = ''; ... ) and restore
(process.env.OPENAI_AUTH_MODE = prev) in a finally or afterEach so the test does
not depend on ambient runner environment.
增加了对于 codex api 格式的支持(/v1/responses)
使用:
/provider codex切换成 codex 格式/login选择 codex正常设置即可
Summary by CodeRabbit
New Features
Tests