Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 4ebbca0

Browse files
feat: add OpenAI Codex provider with OAuth subscription authentication (#10736)
Co-authored-by: Roo Code <roomote@roocode.com>
1 parent 739b91e commit 4ebbca0

37 files changed

Lines changed: 2666 additions & 97 deletions

packages/types/src/provider-settings.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
ioIntelligenceModels,
1818
mistralModels,
1919
moonshotModels,
20+
openAiCodexModels,
2021
openAiNativeModels,
2122
qwenCodeModels,
2223
sambaNovaModels,
@@ -133,6 +134,7 @@ export const providerNames = [
133134
"mistral",
134135
"moonshot",
135136
"minimax",
137+
"openai-codex",
136138
"openai-native",
137139
"qwen-code",
138140
"roo",
@@ -289,6 +291,10 @@ const geminiCliSchema = apiModelIdProviderModelSchema.extend({
289291
geminiCliProjectId: z.string().optional(),
290292
})
291293

294+
const openAiCodexSchema = apiModelIdProviderModelSchema.extend({
295+
// No additional settings needed - uses OAuth authentication
296+
})
297+
292298
const openAiNativeSchema = apiModelIdProviderModelSchema.extend({
293299
openAiNativeApiKey: z.string().optional(),
294300
openAiNativeBaseUrl: z.string().optional(),
@@ -436,6 +442,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
436442
lmStudioSchema.merge(z.object({ apiProvider: z.literal("lmstudio") })),
437443
geminiSchema.merge(z.object({ apiProvider: z.literal("gemini") })),
438444
geminiCliSchema.merge(z.object({ apiProvider: z.literal("gemini-cli") })),
445+
openAiCodexSchema.merge(z.object({ apiProvider: z.literal("openai-codex") })),
439446
openAiNativeSchema.merge(z.object({ apiProvider: z.literal("openai-native") })),
440447
mistralSchema.merge(z.object({ apiProvider: z.literal("mistral") })),
441448
deepSeekSchema.merge(z.object({ apiProvider: z.literal("deepseek") })),
@@ -477,6 +484,7 @@ export const providerSettingsSchema = z.object({
477484
...lmStudioSchema.shape,
478485
...geminiSchema.shape,
479486
...geminiCliSchema.shape,
487+
...openAiCodexSchema.shape,
480488
...openAiNativeSchema.shape,
481489
...mistralSchema.shape,
482490
...deepSeekSchema.shape,
@@ -559,6 +567,7 @@ export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
559567
openrouter: "openRouterModelId",
560568
bedrock: "apiModelId",
561569
vertex: "apiModelId",
570+
"openai-codex": "apiModelId",
562571
"openai-native": "openAiModelId",
563572
ollama: "ollamaModelId",
564573
lmstudio: "lmStudioModelId",
@@ -684,6 +693,11 @@ export const MODELS_BY_PROVIDER: Record<
684693
label: "MiniMax",
685694
models: Object.keys(minimaxModels),
686695
},
696+
"openai-codex": {
697+
id: "openai-codex",
698+
label: "OpenAI - ChatGPT Plus/Pro",
699+
models: Object.keys(openAiCodexModels),
700+
},
687701
"openai-native": {
688702
id: "openai-native",
689703
label: "OpenAI",

packages/types/src/providers/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export * from "./mistral.js"
1818
export * from "./moonshot.js"
1919
export * from "./ollama.js"
2020
export * from "./openai.js"
21+
export * from "./openai-codex.js"
2122
export * from "./openrouter.js"
2223
export * from "./qwen-code.js"
2324
export * from "./requesty.js"
@@ -48,6 +49,7 @@ import { ioIntelligenceDefaultModelId } from "./io-intelligence.js"
4849
import { litellmDefaultModelId } from "./lite-llm.js"
4950
import { mistralDefaultModelId } from "./mistral.js"
5051
import { moonshotDefaultModelId } from "./moonshot.js"
52+
import { openAiCodexDefaultModelId } from "./openai-codex.js"
5153
import { openRouterDefaultModelId } from "./openrouter.js"
5254
import { qwenCodeDefaultModelId } from "./qwen-code.js"
5355
import { requestyDefaultModelId } from "./requesty.js"
@@ -111,6 +113,8 @@ export function getProviderDefaultModelId(
111113
return options?.isChina ? mainlandZAiDefaultModelId : internationalZAiDefaultModelId
112114
case "openai-native":
113115
return "gpt-4o" // Based on openai-native patterns
116+
case "openai-codex":
117+
return openAiCodexDefaultModelId
114118
case "mistral":
115119
return mistralDefaultModelId
116120
case "openai":
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import type { ModelInfo } from "../model.js"
2+
3+
/**
4+
* OpenAI Codex Provider
5+
*
6+
* This provider uses OAuth authentication via ChatGPT Plus/Pro subscription
7+
* instead of direct API keys. Requests are routed to the Codex backend at
8+
* https://chatgpt.com/backend-api/codex/responses
9+
*
10+
* Key differences from openai-native:
11+
* - Uses OAuth Bearer tokens instead of API keys
12+
* - Subscription-based pricing (no per-token costs)
13+
* - Limited model subset available
14+
* - Custom routing to Codex backend
15+
*/
16+
17+
export type OpenAiCodexModelId = keyof typeof openAiCodexModels
18+
19+
export const openAiCodexDefaultModelId: OpenAiCodexModelId = "gpt-5.2-codex"
20+
21+
/**
22+
* Models available through the Codex OAuth flow.
23+
* These models are accessible to ChatGPT Plus/Pro subscribers.
24+
* Costs are 0 as they are covered by the subscription.
25+
*/
26+
export const openAiCodexModels = {
27+
"gpt-5.1-codex-max": {
28+
maxTokens: 128000,
29+
contextWindow: 400000,
30+
supportsNativeTools: true,
31+
defaultToolProtocol: "native",
32+
includedTools: ["apply_patch"],
33+
excludedTools: ["apply_diff", "write_to_file"],
34+
supportsImages: true,
35+
supportsPromptCache: true,
36+
supportsReasoningEffort: ["low", "medium", "high", "xhigh"],
37+
reasoningEffort: "xhigh",
38+
// Subscription-based: no per-token costs
39+
inputPrice: 0,
40+
outputPrice: 0,
41+
supportsTemperature: false,
42+
description: "GPT-5.1 Codex Max: Maximum capability coding model via ChatGPT subscription",
43+
},
44+
"gpt-5.2-codex": {
45+
maxTokens: 128000,
46+
contextWindow: 400000,
47+
supportsNativeTools: true,
48+
defaultToolProtocol: "native",
49+
includedTools: ["apply_patch"],
50+
excludedTools: ["apply_diff", "write_to_file"],
51+
supportsImages: true,
52+
supportsPromptCache: true,
53+
supportsReasoningEffort: ["low", "medium", "high", "xhigh"],
54+
reasoningEffort: "medium",
55+
inputPrice: 0,
56+
outputPrice: 0,
57+
supportsTemperature: false,
58+
description: "GPT-5.2 Codex: OpenAI's flagship coding model via ChatGPT subscription",
59+
},
60+
"gpt-5.1-codex-mini": {
61+
maxTokens: 128000,
62+
contextWindow: 400000,
63+
supportsNativeTools: true,
64+
defaultToolProtocol: "native",
65+
includedTools: ["apply_patch"],
66+
excludedTools: ["apply_diff", "write_to_file"],
67+
supportsImages: true,
68+
supportsPromptCache: true,
69+
supportsReasoningEffort: ["low", "medium", "high"],
70+
reasoningEffort: "medium",
71+
inputPrice: 0,
72+
outputPrice: 0,
73+
supportsTemperature: false,
74+
description: "GPT-5.1 Codex Mini: Faster version for coding tasks via ChatGPT subscription",
75+
},
76+
"gpt-5.2": {
77+
maxTokens: 128000,
78+
contextWindow: 400000,
79+
supportsNativeTools: true,
80+
defaultToolProtocol: "native",
81+
includedTools: ["apply_patch"],
82+
excludedTools: ["apply_diff", "write_to_file"],
83+
supportsImages: true,
84+
supportsPromptCache: true,
85+
supportsReasoningEffort: ["none", "low", "medium", "high", "xhigh"],
86+
reasoningEffort: "medium",
87+
inputPrice: 0,
88+
outputPrice: 0,
89+
supportsTemperature: false,
90+
description: "GPT-5.2: Latest GPT model via ChatGPT subscription",
91+
},
92+
} as const satisfies Record<string, ModelInfo>

packages/types/src/vscode-extension-host.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@ export type ExtensionState = Pick<
325325
taskSyncEnabled: boolean
326326
featureRoomoteControlEnabled: boolean
327327
claudeCodeIsAuthenticated?: boolean
328+
openAiCodexIsAuthenticated?: boolean
328329
debug?: boolean
329330
}
330331

@@ -454,6 +455,8 @@ export interface WebviewMessage {
454455
| "rooCloudManualUrl"
455456
| "claudeCodeSignIn"
456457
| "claudeCodeSignOut"
458+
| "openAiCodexSignIn"
459+
| "openAiCodexSignOut"
457460
| "switchOrganization"
458461
| "condenseTaskContextRequest"
459462
| "requestIndexingStatus"

src/api/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
VertexHandler,
1414
AnthropicVertexHandler,
1515
OpenAiHandler,
16+
OpenAiCodexHandler,
1617
LmStudioHandler,
1718
GeminiHandler,
1819
OpenAiNativeHandler,
@@ -149,6 +150,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
149150
return new LmStudioHandler(options)
150151
case "gemini":
151152
return new GeminiHandler(options)
153+
case "openai-codex":
154+
return new OpenAiCodexHandler(options)
152155
case "openai-native":
153156
return new OpenAiNativeHandler(options)
154157
case "deepseek":
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// cd src && npx vitest run api/providers/__tests__/openai-codex-native-tool-calls.spec.ts
2+
3+
import { beforeEach, describe, expect, it, vi } from "vitest"
4+
5+
import { OpenAiCodexHandler } from "../openai-codex"
6+
import type { ApiHandlerOptions } from "../../../shared/api"
7+
import { NativeToolCallParser } from "../../../core/assistant-message/NativeToolCallParser"
8+
import { openAiCodexOAuthManager } from "../../../integrations/openai-codex/oauth"
9+
10+
describe("OpenAiCodexHandler native tool calls", () => {
11+
let handler: OpenAiCodexHandler
12+
let mockOptions: ApiHandlerOptions
13+
14+
beforeEach(() => {
15+
vi.restoreAllMocks()
16+
NativeToolCallParser.clearRawChunkState()
17+
NativeToolCallParser.clearAllStreamingToolCalls()
18+
19+
mockOptions = {
20+
apiModelId: "gpt-5.2-2025-12-11",
21+
// minimal settings; OAuth is mocked below
22+
}
23+
handler = new OpenAiCodexHandler(mockOptions)
24+
})
25+
26+
it("yields tool_call_partial chunks when API returns function_call-only response", async () => {
27+
vi.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token")
28+
vi.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test")
29+
30+
// Mock OpenAI SDK streaming (preferred path).
31+
;(handler as any).client = {
32+
responses: {
33+
create: vi.fn().mockResolvedValue({
34+
async *[Symbol.asyncIterator]() {
35+
yield {
36+
type: "response.output_item.added",
37+
item: {
38+
type: "function_call",
39+
call_id: "call_1",
40+
name: "attempt_completion",
41+
arguments: "",
42+
},
43+
output_index: 0,
44+
}
45+
yield {
46+
type: "response.function_call_arguments.delta",
47+
delta: '{"result":"hi"}',
48+
// Note: intentionally omit call_id + name to simulate tool-call-only streams.
49+
item_id: "fc_1",
50+
output_index: 0,
51+
}
52+
yield {
53+
type: "response.completed",
54+
response: {
55+
id: "resp_1",
56+
status: "completed",
57+
output: [
58+
{
59+
type: "function_call",
60+
call_id: "call_1",
61+
name: "attempt_completion",
62+
arguments: '{"result":"hi"}',
63+
},
64+
],
65+
usage: { input_tokens: 1, output_tokens: 1 },
66+
},
67+
}
68+
},
69+
}),
70+
},
71+
}
72+
73+
const stream = handler.createMessage("system", [{ role: "user", content: "hello" } as any], {
74+
taskId: "t",
75+
toolProtocol: "native",
76+
tools: [],
77+
})
78+
79+
const chunks: any[] = []
80+
for await (const chunk of stream) {
81+
chunks.push(chunk)
82+
if (chunk.type === "tool_call_partial") {
83+
// Simulate Task.ts behavior so finish_reason handling can emit tool_call_end elsewhere
84+
NativeToolCallParser.processRawChunk({
85+
index: chunk.index,
86+
id: chunk.id,
87+
name: chunk.name,
88+
arguments: chunk.arguments,
89+
})
90+
}
91+
}
92+
93+
const toolChunks = chunks.filter((c) => c.type === "tool_call_partial")
94+
expect(toolChunks.length).toBeGreaterThan(0)
95+
expect(toolChunks[0]).toMatchObject({
96+
type: "tool_call_partial",
97+
id: "call_1",
98+
name: "attempt_completion",
99+
})
100+
})
101+
})

src/api/providers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export { IOIntelligenceHandler } from "./io-intelligence"
1515
export { LiteLLMHandler } from "./lite-llm"
1616
export { LmStudioHandler } from "./lm-studio"
1717
export { MistralHandler } from "./mistral"
18+
export { OpenAiCodexHandler } from "./openai-codex"
1819
export { OpenAiNativeHandler } from "./openai-native"
1920
export { OpenAiHandler } from "./openai"
2021
export { OpenRouterHandler } from "./openrouter"

0 commit comments

Comments
 (0)