Przeglądaj źródła

feat: add OpenAI Codex provider with OAuth subscription authentication (#10736)

Co-authored-by: Roo Code <[email protected]>
Hannes Rudolph 1 tydzień temu
rodzic
commit
4ebbca08b0
37 zmienionych plików z 2666 dodań i 97 usunięć
  1. 14 0
      packages/types/src/provider-settings.ts
  2. 4 0
      packages/types/src/providers/index.ts
  3. 92 0
      packages/types/src/providers/openai-codex.ts
  4. 3 0
      packages/types/src/vscode-extension-host.ts
  5. 3 0
      src/api/index.ts
  6. 101 0
      src/api/providers/__tests__/openai-codex-native-tool-calls.spec.ts
  7. 1 0
      src/api/providers/index.ts
  8. 1117 0
      src/api/providers/openai-codex.ts
  9. 4 3
      src/core/task/Task.ts
  10. 8 0
      src/core/task/__tests__/Task.spec.ts
  11. 8 0
      src/core/webview/ClineProvider.ts
  12. 39 0
      src/core/webview/webviewMessageHandler.ts
  13. 4 0
      src/extension.ts
  14. 20 1
      src/i18n/locales/ca/common.json
  15. 20 1
      src/i18n/locales/de/common.json
  16. 20 1
      src/i18n/locales/en/common.json
  17. 21 2
      src/i18n/locales/es/common.json
  18. 21 2
      src/i18n/locales/fr/common.json
  19. 21 2
      src/i18n/locales/hi/common.json
  20. 21 2
      src/i18n/locales/id/common.json
  21. 21 2
      src/i18n/locales/it/common.json
  22. 21 2
      src/i18n/locales/ja/common.json
  23. 21 2
      src/i18n/locales/ko/common.json
  24. 21 2
      src/i18n/locales/nl/common.json
  25. 21 2
      src/i18n/locales/pl/common.json
  26. 21 2
      src/i18n/locales/pt-BR/common.json
  27. 21 2
      src/i18n/locales/ru/common.json
  28. 21 2
      src/i18n/locales/tr/common.json
  29. 21 2
      src/i18n/locales/vi/common.json
  30. 21 2
      src/i18n/locales/zh-CN/common.json
  31. 21 2
      src/i18n/locales/zh-TW/common.json
  32. 740 0
      src/integrations/openai-codex/oauth.ts
  33. 75 60
      webview-ui/src/components/settings/ApiOptions.tsx
  34. 3 0
      webview-ui/src/components/settings/constants.ts
  35. 67 0
      webview-ui/src/components/settings/providers/OpenAICodex.tsx
  36. 1 0
      webview-ui/src/components/settings/providers/index.ts
  37. 7 1
      webview-ui/src/components/ui/hooks/useSelectedModel.ts

+ 14 - 0
packages/types/src/provider-settings.ts

@@ -17,6 +17,7 @@ import {
 	ioIntelligenceModels,
 	mistralModels,
 	moonshotModels,
+	openAiCodexModels,
 	openAiNativeModels,
 	qwenCodeModels,
 	sambaNovaModels,
@@ -133,6 +134,7 @@ export const providerNames = [
 	"mistral",
 	"moonshot",
 	"minimax",
+	"openai-codex",
 	"openai-native",
 	"qwen-code",
 	"roo",
@@ -289,6 +291,10 @@ const geminiCliSchema = apiModelIdProviderModelSchema.extend({
 	geminiCliProjectId: z.string().optional(),
 })
 
+const openAiCodexSchema = apiModelIdProviderModelSchema.extend({
+	// No additional settings needed - uses OAuth authentication
+})
+
 const openAiNativeSchema = apiModelIdProviderModelSchema.extend({
 	openAiNativeApiKey: z.string().optional(),
 	openAiNativeBaseUrl: z.string().optional(),
@@ -436,6 +442,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
 	lmStudioSchema.merge(z.object({ apiProvider: z.literal("lmstudio") })),
 	geminiSchema.merge(z.object({ apiProvider: z.literal("gemini") })),
 	geminiCliSchema.merge(z.object({ apiProvider: z.literal("gemini-cli") })),
+	openAiCodexSchema.merge(z.object({ apiProvider: z.literal("openai-codex") })),
 	openAiNativeSchema.merge(z.object({ apiProvider: z.literal("openai-native") })),
 	mistralSchema.merge(z.object({ apiProvider: z.literal("mistral") })),
 	deepSeekSchema.merge(z.object({ apiProvider: z.literal("deepseek") })),
@@ -477,6 +484,7 @@ export const providerSettingsSchema = z.object({
 	...lmStudioSchema.shape,
 	...geminiSchema.shape,
 	...geminiCliSchema.shape,
+	...openAiCodexSchema.shape,
 	...openAiNativeSchema.shape,
 	...mistralSchema.shape,
 	...deepSeekSchema.shape,
@@ -559,6 +567,7 @@ export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
 	openrouter: "openRouterModelId",
 	bedrock: "apiModelId",
 	vertex: "apiModelId",
+	"openai-codex": "apiModelId",
 	"openai-native": "openAiModelId",
 	ollama: "ollamaModelId",
 	lmstudio: "lmStudioModelId",
@@ -684,6 +693,11 @@ export const MODELS_BY_PROVIDER: Record<
 		label: "MiniMax",
 		models: Object.keys(minimaxModels),
 	},
+	"openai-codex": {
+		id: "openai-codex",
+		label: "OpenAI - ChatGPT Plus/Pro",
+		models: Object.keys(openAiCodexModels),
+	},
 	"openai-native": {
 		id: "openai-native",
 		label: "OpenAI",

+ 4 - 0
packages/types/src/providers/index.ts

@@ -18,6 +18,7 @@ export * from "./mistral.js"
 export * from "./moonshot.js"
 export * from "./ollama.js"
 export * from "./openai.js"
+export * from "./openai-codex.js"
 export * from "./openrouter.js"
 export * from "./qwen-code.js"
 export * from "./requesty.js"
@@ -48,6 +49,7 @@ import { ioIntelligenceDefaultModelId } from "./io-intelligence.js"
 import { litellmDefaultModelId } from "./lite-llm.js"
 import { mistralDefaultModelId } from "./mistral.js"
 import { moonshotDefaultModelId } from "./moonshot.js"
+import { openAiCodexDefaultModelId } from "./openai-codex.js"
 import { openRouterDefaultModelId } from "./openrouter.js"
 import { qwenCodeDefaultModelId } from "./qwen-code.js"
 import { requestyDefaultModelId } from "./requesty.js"
@@ -111,6 +113,8 @@ export function getProviderDefaultModelId(
 			return options?.isChina ? mainlandZAiDefaultModelId : internationalZAiDefaultModelId
 		case "openai-native":
 			return "gpt-4o" // Based on openai-native patterns
+		case "openai-codex":
+			return openAiCodexDefaultModelId
 		case "mistral":
 			return mistralDefaultModelId
 		case "openai":

+ 92 - 0
packages/types/src/providers/openai-codex.ts

@@ -0,0 +1,92 @@
+import type { ModelInfo } from "../model.js"
+
+/**
+ * OpenAI Codex Provider
+ *
+ * This provider uses OAuth authentication via ChatGPT Plus/Pro subscription
+ * instead of direct API keys. Requests are routed to the Codex backend at
+ * https://chatgpt.com/backend-api/codex/responses
+ *
+ * Key differences from openai-native:
+ * - Uses OAuth Bearer tokens instead of API keys
+ * - Subscription-based pricing (no per-token costs)
+ * - Limited model subset available
+ * - Custom routing to Codex backend
+ */
+
+export type OpenAiCodexModelId = keyof typeof openAiCodexModels
+
+export const openAiCodexDefaultModelId: OpenAiCodexModelId = "gpt-5.2-codex"
+
+/**
+ * Models available through the Codex OAuth flow.
+ * These models are accessible to ChatGPT Plus/Pro subscribers.
+ * Costs are 0 as they are covered by the subscription.
+ */
+export const openAiCodexModels = {
+	"gpt-5.1-codex-max": {
+		maxTokens: 128000,
+		contextWindow: 400000,
+		supportsNativeTools: true,
+		defaultToolProtocol: "native",
+		includedTools: ["apply_patch"],
+		excludedTools: ["apply_diff", "write_to_file"],
+		supportsImages: true,
+		supportsPromptCache: true,
+		supportsReasoningEffort: ["low", "medium", "high", "xhigh"],
+		reasoningEffort: "xhigh",
+		// Subscription-based: no per-token costs
+		inputPrice: 0,
+		outputPrice: 0,
+		supportsTemperature: false,
+		description: "GPT-5.1 Codex Max: Maximum capability coding model via ChatGPT subscription",
+	},
+	"gpt-5.2-codex": {
+		maxTokens: 128000,
+		contextWindow: 400000,
+		supportsNativeTools: true,
+		defaultToolProtocol: "native",
+		includedTools: ["apply_patch"],
+		excludedTools: ["apply_diff", "write_to_file"],
+		supportsImages: true,
+		supportsPromptCache: true,
+		supportsReasoningEffort: ["low", "medium", "high", "xhigh"],
+		reasoningEffort: "medium",
+		inputPrice: 0,
+		outputPrice: 0,
+		supportsTemperature: false,
+		description: "GPT-5.2 Codex: OpenAI's flagship coding model via ChatGPT subscription",
+	},
+	"gpt-5.1-codex-mini": {
+		maxTokens: 128000,
+		contextWindow: 400000,
+		supportsNativeTools: true,
+		defaultToolProtocol: "native",
+		includedTools: ["apply_patch"],
+		excludedTools: ["apply_diff", "write_to_file"],
+		supportsImages: true,
+		supportsPromptCache: true,
+		supportsReasoningEffort: ["low", "medium", "high"],
+		reasoningEffort: "medium",
+		inputPrice: 0,
+		outputPrice: 0,
+		supportsTemperature: false,
+		description: "GPT-5.1 Codex Mini: Faster version for coding tasks via ChatGPT subscription",
+	},
+	"gpt-5.2": {
+		maxTokens: 128000,
+		contextWindow: 400000,
+		supportsNativeTools: true,
+		defaultToolProtocol: "native",
+		includedTools: ["apply_patch"],
+		excludedTools: ["apply_diff", "write_to_file"],
+		supportsImages: true,
+		supportsPromptCache: true,
+		supportsReasoningEffort: ["none", "low", "medium", "high", "xhigh"],
+		reasoningEffort: "medium",
+		inputPrice: 0,
+		outputPrice: 0,
+		supportsTemperature: false,
+		description: "GPT-5.2: Latest GPT model via ChatGPT subscription",
+	},
+} as const satisfies Record<string, ModelInfo>

+ 3 - 0
packages/types/src/vscode-extension-host.ts

@@ -325,6 +325,7 @@ export type ExtensionState = Pick<
 	taskSyncEnabled: boolean
 	featureRoomoteControlEnabled: boolean
 	claudeCodeIsAuthenticated?: boolean
+	openAiCodexIsAuthenticated?: boolean
 	debug?: boolean
 }
 
@@ -454,6 +455,8 @@ export interface WebviewMessage {
 		| "rooCloudManualUrl"
 		| "claudeCodeSignIn"
 		| "claudeCodeSignOut"
+		| "openAiCodexSignIn"
+		| "openAiCodexSignOut"
 		| "switchOrganization"
 		| "condenseTaskContextRequest"
 		| "requestIndexingStatus"

+ 3 - 0
src/api/index.ts

@@ -13,6 +13,7 @@ import {
 	VertexHandler,
 	AnthropicVertexHandler,
 	OpenAiHandler,
+	OpenAiCodexHandler,
 	LmStudioHandler,
 	GeminiHandler,
 	OpenAiNativeHandler,
@@ -149,6 +150,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
 			return new LmStudioHandler(options)
 		case "gemini":
 			return new GeminiHandler(options)
+		case "openai-codex":
+			return new OpenAiCodexHandler(options)
 		case "openai-native":
 			return new OpenAiNativeHandler(options)
 		case "deepseek":

+ 101 - 0
src/api/providers/__tests__/openai-codex-native-tool-calls.spec.ts

@@ -0,0 +1,101 @@
+// cd src && npx vitest run api/providers/__tests__/openai-codex-native-tool-calls.spec.ts
+
+import { beforeEach, describe, expect, it, vi } from "vitest"
+
+import { OpenAiCodexHandler } from "../openai-codex"
+import type { ApiHandlerOptions } from "../../../shared/api"
+import { NativeToolCallParser } from "../../../core/assistant-message/NativeToolCallParser"
+import { openAiCodexOAuthManager } from "../../../integrations/openai-codex/oauth"
+
+describe("OpenAiCodexHandler native tool calls", () => {
+	let handler: OpenAiCodexHandler
+	let mockOptions: ApiHandlerOptions
+
+	beforeEach(() => {
+		vi.restoreAllMocks()
+		NativeToolCallParser.clearRawChunkState()
+		NativeToolCallParser.clearAllStreamingToolCalls()
+
+		mockOptions = {
+			apiModelId: "gpt-5.2-2025-12-11",
+			// minimal settings; OAuth is mocked below
+		}
+		handler = new OpenAiCodexHandler(mockOptions)
+	})
+
+	it("yields tool_call_partial chunks when API returns function_call-only response", async () => {
+		vi.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token")
+		vi.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test")
+
+		// Mock OpenAI SDK streaming (preferred path).
+		;(handler as any).client = {
+			responses: {
+				create: vi.fn().mockResolvedValue({
+					async *[Symbol.asyncIterator]() {
+						yield {
+							type: "response.output_item.added",
+							item: {
+								type: "function_call",
+								call_id: "call_1",
+								name: "attempt_completion",
+								arguments: "",
+							},
+							output_index: 0,
+						}
+						yield {
+							type: "response.function_call_arguments.delta",
+							delta: '{"result":"hi"}',
+							// Note: intentionally omit call_id + name to simulate tool-call-only streams.
+							item_id: "fc_1",
+							output_index: 0,
+						}
+						yield {
+							type: "response.completed",
+							response: {
+								id: "resp_1",
+								status: "completed",
+								output: [
+									{
+										type: "function_call",
+										call_id: "call_1",
+										name: "attempt_completion",
+										arguments: '{"result":"hi"}',
+									},
+								],
+								usage: { input_tokens: 1, output_tokens: 1 },
+							},
+						}
+					},
+				}),
+			},
+		}
+
+		const stream = handler.createMessage("system", [{ role: "user", content: "hello" } as any], {
+			taskId: "t",
+			toolProtocol: "native",
+			tools: [],
+		})
+
+		const chunks: any[] = []
+		for await (const chunk of stream) {
+			chunks.push(chunk)
+			if (chunk.type === "tool_call_partial") {
+				// Simulate Task.ts behavior so finish_reason handling can emit tool_call_end elsewhere
+				NativeToolCallParser.processRawChunk({
+					index: chunk.index,
+					id: chunk.id,
+					name: chunk.name,
+					arguments: chunk.arguments,
+				})
+			}
+		}
+
+		const toolChunks = chunks.filter((c) => c.type === "tool_call_partial")
+		expect(toolChunks.length).toBeGreaterThan(0)
+		expect(toolChunks[0]).toMatchObject({
+			type: "tool_call_partial",
+			id: "call_1",
+			name: "attempt_completion",
+		})
+	})
+})

+ 1 - 0
src/api/providers/index.ts

@@ -15,6 +15,7 @@ export { IOIntelligenceHandler } from "./io-intelligence"
 export { LiteLLMHandler } from "./lite-llm"
 export { LmStudioHandler } from "./lm-studio"
 export { MistralHandler } from "./mistral"
+export { OpenAiCodexHandler } from "./openai-codex"
 export { OpenAiNativeHandler } from "./openai-native"
 export { OpenAiHandler } from "./openai"
 export { OpenRouterHandler } from "./openrouter"

+ 1117 - 0
src/api/providers/openai-codex.ts

@@ -0,0 +1,1117 @@
+import * as os from "os"
+import { v7 as uuidv7 } from "uuid"
+import { Anthropic } from "@anthropic-ai/sdk"
+import OpenAI from "openai"
+
+import {
+	type ModelInfo,
+	openAiCodexDefaultModelId,
+	OpenAiCodexModelId,
+	openAiCodexModels,
+	type ReasoningEffort,
+	type ReasoningEffortExtended,
+	ApiProviderError,
+} from "@roo-code/types"
+import { TelemetryService } from "@roo-code/telemetry"
+
+import type { ApiHandlerOptions } from "../../shared/api"
+
+import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
+import { getModelParams } from "../transform/model-params"
+
+import { BaseProvider } from "./base-provider"
+import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
+import { isMcpTool } from "../../utils/mcp-name"
+import { openAiCodexOAuthManager } from "../../integrations/openai-codex/oauth"
+import { t } from "../../i18n"
+
+// Get extension version for User-Agent header
+const extensionVersion: string = require("../../package.json").version ?? "unknown"
+
+export type OpenAiCodexModel = ReturnType<OpenAiCodexHandler["getModel"]>
+
+/**
+ * OpenAI Codex base URL for API requests
+ * Per the implementation guide: requests are routed to chatgpt.com/backend-api/codex
+ */
+const CODEX_API_BASE_URL = "https://chatgpt.com/backend-api/codex"
+
+/**
+ * OpenAiCodexHandler - Uses OpenAI Responses API with OAuth authentication
+ *
+ * Key differences from OpenAiNativeHandler:
+ * - Uses OAuth Bearer tokens instead of API keys
+ * - Routes requests to Codex backend (chatgpt.com/backend-api/codex)
+ * - Subscription-based pricing (no per-token costs)
+ * - Limited model subset
+ * - Custom headers for Codex backend
+ */
+export class OpenAiCodexHandler extends BaseProvider implements SingleCompletionHandler {
+	protected options: ApiHandlerOptions
+	private readonly providerName = "OpenAI Codex"
+	private client?: OpenAI
+	// Complete response output array
+	private lastResponseOutput: any[] | undefined
+	// Last top-level response id
+	private lastResponseId: string | undefined
+	// Abort controller for cancelling ongoing requests
+	private abortController?: AbortController
+	// Session ID for the Codex API (persists for the lifetime of the handler)
+	private readonly sessionId: string
+	/**
+	 * Some Codex/Responses streams emit tool-call argument deltas without stable call id/name.
+	 * Track the last observed tool identity from output_item events so we can still
+	 * emit `tool_call_partial` chunks (tool-call-only streams).
+	 */
+	private pendingToolCallId: string | undefined
+	private pendingToolCallName: string | undefined
+
+	// Event types handled by the shared event processor
+	private readonly coreHandledEventTypes = new Set<string>([
+		"response.text.delta",
+		"response.output_text.delta",
+		"response.reasoning.delta",
+		"response.reasoning_text.delta",
+		"response.reasoning_summary.delta",
+		"response.reasoning_summary_text.delta",
+		"response.refusal.delta",
+		"response.output_item.added",
+		"response.output_item.done",
+		"response.done",
+		"response.completed",
+		"response.tool_call_arguments.delta",
+		"response.function_call_arguments.delta",
+		"response.tool_call_arguments.done",
+		"response.function_call_arguments.done",
+	])
+
+	constructor(options: ApiHandlerOptions) {
+		super()
+		this.options = options
+		// Generate a new session ID for standalone handler usage (fallback)
+		this.sessionId = uuidv7()
+	}
+
+	private normalizeUsage(usage: any, model: OpenAiCodexModel): ApiStreamUsageChunk | undefined {
+		if (!usage) return undefined
+
+		const inputDetails = usage.input_tokens_details ?? usage.prompt_tokens_details
+
+		const hasCachedTokens = typeof inputDetails?.cached_tokens === "number"
+		const hasCacheMissTokens = typeof inputDetails?.cache_miss_tokens === "number"
+		const cachedFromDetails = hasCachedTokens ? inputDetails.cached_tokens : 0
+		const missFromDetails = hasCacheMissTokens ? inputDetails.cache_miss_tokens : 0
+
+		let totalInputTokens = usage.input_tokens ?? usage.prompt_tokens ?? 0
+		if (totalInputTokens === 0 && inputDetails && (cachedFromDetails > 0 || missFromDetails > 0)) {
+			totalInputTokens = cachedFromDetails + missFromDetails
+		}
+
+		const totalOutputTokens = usage.output_tokens ?? usage.completion_tokens ?? 0
+		const cacheWriteTokens = usage.cache_creation_input_tokens ?? usage.cache_write_tokens ?? 0
+		const cacheReadTokens =
+			usage.cache_read_input_tokens ?? usage.cache_read_tokens ?? usage.cached_tokens ?? cachedFromDetails ?? 0
+
+		const reasoningTokens =
+			typeof usage.output_tokens_details?.reasoning_tokens === "number"
+				? usage.output_tokens_details.reasoning_tokens
+				: undefined
+
+		// Subscription-based: no per-token costs
+		const out: ApiStreamUsageChunk = {
+			type: "usage",
+			inputTokens: totalInputTokens,
+			outputTokens: totalOutputTokens,
+			cacheWriteTokens,
+			cacheReadTokens,
+			...(typeof reasoningTokens === "number" ? { reasoningTokens } : {}),
+			totalCost: 0, // Subscription-based pricing
+		}
+		return out
+	}
+
+	override async *createMessage(
+		systemPrompt: string,
+		messages: Anthropic.Messages.MessageParam[],
+		metadata?: ApiHandlerCreateMessageMetadata,
+	): ApiStream {
+		const model = this.getModel()
+		yield* this.handleResponsesApiMessage(model, systemPrompt, messages, metadata)
+	}
+
+	private async *handleResponsesApiMessage(
+		model: OpenAiCodexModel,
+		systemPrompt: string,
+		messages: Anthropic.Messages.MessageParam[],
+		metadata?: ApiHandlerCreateMessageMetadata,
+	): ApiStream {
+		// Reset state for this request
+		this.lastResponseOutput = undefined
+		this.lastResponseId = undefined
+		this.pendingToolCallId = undefined
+		this.pendingToolCallName = undefined
+
+		// Get access token from OAuth manager
+		let accessToken = await openAiCodexOAuthManager.getAccessToken()
+		if (!accessToken) {
+			throw new Error(
+				t("common:errors.openAiCodex.notAuthenticated", {
+					defaultValue:
+						"Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow.",
+				}),
+			)
+		}
+
+		// Resolve reasoning effort
+		const reasoningEffort = this.getReasoningEffort(model)
+
+		// Format conversation
+		const formattedInput = this.formatFullConversation(systemPrompt, messages)
+
+		// Build request body
+		// Per the implementation guide: Codex backend may reject some parameters
+		// Notably: max_output_tokens and prompt_cache_retention may be rejected
+		const requestBody = this.buildRequestBody(model, formattedInput, systemPrompt, reasoningEffort, metadata)
+
+		// Make the request with retry on auth failure
+		for (let attempt = 0; attempt < 2; attempt++) {
+			try {
+				yield* this.executeRequest(requestBody, model, accessToken, metadata?.taskId)
+				return
+			} catch (error) {
+				const message = error instanceof Error ? error.message : String(error)
+				const isAuthFailure = /unauthorized|invalid token|not authenticated|authentication|401/i.test(message)
+
+				if (attempt === 0 && isAuthFailure) {
+					// Force refresh the token for retry
+					const refreshed = await openAiCodexOAuthManager.forceRefreshAccessToken()
+					if (!refreshed) {
+						throw new Error(
+							t("common:errors.openAiCodex.notAuthenticated", {
+								defaultValue:
+									"Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow.",
+							}),
+						)
+					}
+					accessToken = refreshed
+					continue
+				}
+				throw error
+			}
+		}
+	}
+
+	private buildRequestBody(
+		model: OpenAiCodexModel,
+		formattedInput: any,
+		systemPrompt: string,
+		reasoningEffort: ReasoningEffortExtended | undefined,
+		metadata?: ApiHandlerCreateMessageMetadata,
+	): any {
+		const ensureAllRequired = (schema: any): any => {
+			if (!schema || typeof schema !== "object" || schema.type !== "object") {
+				return schema
+			}
+
+			const result = { ...schema }
+			if (result.additionalProperties !== false) {
+				result.additionalProperties = false
+			}
+
+			if (result.properties) {
+				const allKeys = Object.keys(result.properties)
+				result.required = allKeys
+
+				const newProps = { ...result.properties }
+				for (const key of allKeys) {
+					const prop = newProps[key]
+					if (prop.type === "object") {
+						newProps[key] = ensureAllRequired(prop)
+					} else if (prop.type === "array" && prop.items?.type === "object") {
+						newProps[key] = {
+							...prop,
+							items: ensureAllRequired(prop.items),
+						}
+					}
+				}
+				result.properties = newProps
+			}
+
+			return result
+		}
+
+		const ensureAdditionalPropertiesFalse = (schema: any): any => {
+			if (!schema || typeof schema !== "object" || schema.type !== "object") {
+				return schema
+			}
+
+			const result = { ...schema }
+			if (result.additionalProperties !== false) {
+				result.additionalProperties = false
+			}
+
+			if (result.properties) {
+				const newProps = { ...result.properties }
+				for (const key of Object.keys(result.properties)) {
+					const prop = newProps[key]
+					if (prop && prop.type === "object") {
+						newProps[key] = ensureAdditionalPropertiesFalse(prop)
+					} else if (prop && prop.type === "array" && prop.items?.type === "object") {
+						newProps[key] = {
+							...prop,
+							items: ensureAdditionalPropertiesFalse(prop.items),
+						}
+					}
+				}
+				result.properties = newProps
+			}
+
+			return result
+		}
+
+		interface ResponsesRequestBody {
+			model: string
+			input: Array<{ role: "user" | "assistant"; content: any[] } | { type: string; content: string }>
+			stream: boolean
+			reasoning?: { effort?: ReasoningEffortExtended; summary?: "auto" }
+			temperature?: number
+			store?: boolean
+			instructions?: string
+			include?: string[]
+			tools?: Array<{
+				type: "function"
+				name: string
+				description?: string
+				parameters?: any
+				strict?: boolean
+			}>
+			tool_choice?: any
+			parallel_tool_calls?: boolean
+		}
+
+		// Per the implementation guide: Codex backend may reject max_output_tokens
+		// and prompt_cache_retention, so we omit them
+		const body: ResponsesRequestBody = {
+			model: model.id,
+			input: formattedInput,
+			stream: true,
+			store: false,
+			instructions: systemPrompt,
+			// Only include encrypted reasoning content when reasoning effort is set
+			...(reasoningEffort ? { include: ["reasoning.encrypted_content"] } : {}),
+			...(reasoningEffort
+				? {
+						reasoning: {
+							...(reasoningEffort ? { effort: reasoningEffort } : {}),
+							summary: "auto" as const,
+						},
+					}
+				: {}),
+			...(metadata?.tools && {
+				tools: metadata.tools
+					.filter((tool) => tool.type === "function")
+					.map((tool) => {
+						const isMcp = isMcpTool(tool.function.name)
+						return {
+							type: "function",
+							name: tool.function.name,
+							description: tool.function.description,
+							parameters: isMcp
+								? ensureAdditionalPropertiesFalse(tool.function.parameters)
+								: ensureAllRequired(tool.function.parameters),
+							strict: !isMcp,
+						}
+					}),
+			}),
+			...(metadata?.tool_choice && { tool_choice: metadata.tool_choice }),
+		}
+
+		// For native tool protocol, control parallel tool calls
+		if (metadata?.toolProtocol === "native") {
+			body.parallel_tool_calls = metadata.parallelToolCalls ?? false
+		}
+
+		return body
+	}
+
+	private async *executeRequest(
+		requestBody: any,
+		model: OpenAiCodexModel,
+		accessToken: string,
+		taskId?: string,
+	): ApiStream {
+		// Create AbortController for cancellation
+		this.abortController = new AbortController()
+
+		try {
+			// Prefer OpenAI SDK streaming (same approach as openai-native) so event handling
+			// is consistent across providers.
+			try {
+				// Get ChatGPT account ID for organization subscriptions
+				const accountId = await openAiCodexOAuthManager.getAccountId()
+
+				// Build Codex-specific headers. Authorization is provided by the SDK apiKey.
+				const codexHeaders: Record<string, string> = {
+					originator: "roo-code",
+					session_id: taskId || this.sessionId,
+					"User-Agent": `roo-code/${extensionVersion} (${os.platform()} ${os.release()}; ${os.arch()}) node/${process.version.slice(1)}`,
+					...(accountId ? { "ChatGPT-Account-Id": accountId } : {}),
+				}
+
+				// Allow tests to inject a client. If none is injected, create one for this request.
+				const client =
+					this.client ??
+					new OpenAI({
+						apiKey: accessToken,
+						baseURL: CODEX_API_BASE_URL,
+						defaultHeaders: codexHeaders,
+					})
+
+				const stream = (await (client as any).responses.create(requestBody, {
+					signal: this.abortController.signal,
+					// If the SDK supports per-request overrides, ensure headers are present.
+					headers: codexHeaders,
+				})) as AsyncIterable<any>
+
+				if (typeof (stream as any)?.[Symbol.asyncIterator] !== "function") {
+					throw new Error(
+						"OpenAI SDK did not return an AsyncIterable for Responses API streaming. Falling back to SSE.",
+					)
+				}
+
+				for await (const event of stream) {
+					if (this.abortController.signal.aborted) {
+						break
+					}
+
+					for await (const outChunk of this.processEvent(event, model)) {
+						yield outChunk
+					}
+				}
+			} catch (_sdkErr) {
+				// Fallback to manual SSE via fetch (Codex backend).
+				yield* this.makeCodexRequest(requestBody, model, accessToken, taskId)
+			}
+		} finally {
+			this.abortController = undefined
+		}
+	}
+
+	private formatFullConversation(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): any {
+		const formattedInput: any[] = []
+
+		for (const message of messages) {
+			// Check if this is a reasoning item
+			if ((message as any).type === "reasoning") {
+				formattedInput.push(message)
+				continue
+			}
+
+			if (message.role === "user") {
+				const content: any[] = []
+				const toolResults: any[] = []
+
+				if (typeof message.content === "string") {
+					content.push({ type: "input_text", text: message.content })
+				} else if (Array.isArray(message.content)) {
+					for (const block of message.content) {
+						if (block.type === "text") {
+							content.push({ type: "input_text", text: block.text })
+						} else if (block.type === "image") {
+							const image = block as Anthropic.Messages.ImageBlockParam
+							const imageUrl = `data:${image.source.media_type};base64,${image.source.data}`
+							content.push({ type: "input_image", image_url: imageUrl })
+						} else if (block.type === "tool_result") {
+							const result =
+								typeof block.content === "string"
+									? block.content
+									: block.content?.map((c) => (c.type === "text" ? c.text : "")).join("") || ""
+							toolResults.push({
+								type: "function_call_output",
+								call_id: block.tool_use_id,
+								output: result,
+							})
+						}
+					}
+				}
+
+				if (content.length > 0) {
+					formattedInput.push({ role: "user", content })
+				}
+
+				if (toolResults.length > 0) {
+					formattedInput.push(...toolResults)
+				}
+			} else if (message.role === "assistant") {
+				const content: any[] = []
+				const toolCalls: any[] = []
+
+				if (typeof message.content === "string") {
+					content.push({ type: "output_text", text: message.content })
+				} else if (Array.isArray(message.content)) {
+					for (const block of message.content) {
+						if (block.type === "text") {
+							content.push({ type: "output_text", text: block.text })
+						} else if (block.type === "tool_use") {
+							toolCalls.push({
+								type: "function_call",
+								call_id: block.id,
+								name: block.name,
+								arguments: JSON.stringify(block.input),
+							})
+						}
+					}
+				}
+
+				if (content.length > 0) {
+					formattedInput.push({ role: "assistant", content })
+				}
+
+				if (toolCalls.length > 0) {
+					formattedInput.push(...toolCalls)
+				}
+			}
+		}
+
+		return formattedInput
+	}
+
+	private async *makeCodexRequest(
+		requestBody: any,
+		model: OpenAiCodexModel,
+		accessToken: string,
+		taskId?: string,
+	): ApiStream {
+		// Per the implementation guide: route to Codex backend with Bearer token
+		const url = `${CODEX_API_BASE_URL}/responses`
+
+		// Get ChatGPT account ID for organization subscriptions
+		const accountId = await openAiCodexOAuthManager.getAccountId()
+
+		// Build headers with required Codex-specific fields
+		const headers: Record<string, string> = {
+			"Content-Type": "application/json",
+			Authorization: `Bearer ${accessToken}`,
+			originator: "roo-code",
+			session_id: taskId || this.sessionId,
+			"User-Agent": `roo-code/${extensionVersion} (${os.platform()} ${os.release()}; ${os.arch()}) node/${process.version.slice(1)}`,
+		}
+
+		// Add ChatGPT-Account-Id if available (required for organization subscriptions)
+		if (accountId) {
+			headers["ChatGPT-Account-Id"] = accountId
+		}
+
+		try {
+			const response = await fetch(url, {
+				method: "POST",
+				headers,
+				body: JSON.stringify(requestBody),
+				signal: this.abortController?.signal,
+			})
+
+			if (!response.ok) {
+				const errorText = await response.text()
+
+				let errorMessage = t("common:errors.api.apiRequestFailed", { status: response.status })
+				let errorDetails = ""
+
+				try {
+					const errorJson = JSON.parse(errorText)
+					if (errorJson.error?.message) {
+						errorDetails = errorJson.error.message
+					} else if (errorJson.message) {
+						errorDetails = errorJson.message
+					} else if (errorJson.detail) {
+						errorDetails = errorJson.detail
+					} else {
+						errorDetails = errorText
+					}
+				} catch {
+					errorDetails = errorText
+				}
+
+				switch (response.status) {
+					case 400:
+						errorMessage = t("common:errors.openAiCodex.invalidRequest")
+						break
+					case 401:
+						errorMessage = t("common:errors.openAiCodex.authenticationFailed")
+						break
+					case 403:
+						errorMessage = t("common:errors.openAiCodex.accessDenied")
+						break
+					case 404:
+						errorMessage = t("common:errors.openAiCodex.endpointNotFound")
+						break
+					case 429:
+						errorMessage = t("common:errors.openAiCodex.rateLimitExceeded")
+						break
+					case 500:
+					case 502:
+					case 503:
+						errorMessage = t("common:errors.openAiCodex.serviceError")
+						break
+					default:
+						errorMessage = t("common:errors.openAiCodex.genericError", { status: response.status })
+				}
+
+				if (errorDetails) {
+					errorMessage += ` - ${errorDetails}`
+				}
+
+				throw new Error(errorMessage)
+			}
+
+			if (!response.body) {
+				throw new Error(t("common:errors.openAiCodex.noResponseBody"))
+			}
+
+			yield* this.handleStreamResponse(response.body, model)
+		} catch (error) {
+			const errorMessage = error instanceof Error ? error.message : String(error)
+			const apiError = new ApiProviderError(errorMessage, this.providerName, model.id, "createMessage")
+			TelemetryService.instance.captureException(apiError)
+
+			if (error instanceof Error) {
+				if (error.message.includes("Codex API")) {
+					throw error
+				}
+				throw new Error(t("common:errors.openAiCodex.connectionFailed", { message: error.message }))
+			}
+			throw new Error(t("common:errors.openAiCodex.unexpectedConnectionError"))
+		}
+	}
+
+	private async *handleStreamResponse(body: ReadableStream<Uint8Array>, model: OpenAiCodexModel): ApiStream {
+		const reader = body.getReader()
+		const decoder = new TextDecoder()
+		let buffer = ""
+		let hasContent = false
+
+		try {
+			while (true) {
+				if (this.abortController?.signal.aborted) {
+					break
+				}
+
+				const { done, value } = await reader.read()
+				if (done) break
+
+				buffer += decoder.decode(value, { stream: true })
+				const lines = buffer.split("\n")
+				buffer = lines.pop() || ""
+
+				for (const line of lines) {
+					if (line.startsWith("data: ")) {
+						const data = line.slice(6).trim()
+						if (data === "[DONE]") {
+							continue
+						}
+
+						try {
+							const parsed = JSON.parse(data)
+
+							// Capture response metadata
+							if (parsed.response?.output && Array.isArray(parsed.response.output)) {
+								this.lastResponseOutput = parsed.response.output
+							}
+							if (parsed.response?.id) {
+								this.lastResponseId = parsed.response.id as string
+							}
+
+							// Delegate standard event types
+							if (parsed?.type && this.coreHandledEventTypes.has(parsed.type)) {
+								// Capture tool call identity from output_item events so we can
+								// emit tool_call_partial for subsequent function_call_arguments.delta events
+								if (
+									parsed.type === "response.output_item.added" ||
+									parsed.type === "response.output_item.done"
+								) {
+									const item = parsed.item
+									if (item && (item.type === "function_call" || item.type === "tool_call")) {
+										const callId = item.call_id || item.tool_call_id || item.id
+										const name = item.name || item.function?.name || item.function_name
+										if (typeof callId === "string" && callId.length > 0) {
+											this.pendingToolCallId = callId
+											this.pendingToolCallName = typeof name === "string" ? name : undefined
+										}
+									}
+								}
+
+								// Some Codex streams only return tool calls (no text). Treat tool output as content.
+								if (
+									parsed.type === "response.function_call_arguments.delta" ||
+									parsed.type === "response.tool_call_arguments.delta" ||
+									parsed.type === "response.output_item.added" ||
+									parsed.type === "response.output_item.done"
+								) {
+									hasContent = true
+								}
+
+								for await (const outChunk of this.processEvent(parsed, model)) {
+									if (outChunk.type === "text" || outChunk.type === "reasoning") {
+										hasContent = true
+									}
+									yield outChunk
+								}
+								continue
+							}
+
+							// Handle complete response
+							if (parsed.response && parsed.response.output && Array.isArray(parsed.response.output)) {
+								for (const outputItem of parsed.response.output) {
+									if (outputItem.type === "text" && outputItem.content) {
+										for (const content of outputItem.content) {
+											if (content.type === "text" && content.text) {
+												hasContent = true
+												yield { type: "text", text: content.text }
+											}
+										}
+									}
+									if (outputItem.type === "reasoning" && Array.isArray(outputItem.summary)) {
+										for (const summary of outputItem.summary) {
+											if (summary?.type === "summary_text" && typeof summary.text === "string") {
+												hasContent = true
+												yield { type: "reasoning", text: summary.text }
+											}
+										}
+									}
+								}
+								if (parsed.response.usage) {
+									const usageData = this.normalizeUsage(parsed.response.usage, model)
+									if (usageData) {
+										yield usageData
+									}
+								}
+							} else if (
+								parsed.type === "response.text.delta" ||
+								parsed.type === "response.output_text.delta"
+							) {
+								if (parsed.delta) {
+									hasContent = true
+									yield { type: "text", text: parsed.delta }
+								}
+							} else if (
+								parsed.type === "response.reasoning.delta" ||
+								parsed.type === "response.reasoning_text.delta"
+							) {
+								if (parsed.delta) {
+									hasContent = true
+									yield { type: "reasoning", text: parsed.delta }
+								}
+							} else if (
+								parsed.type === "response.reasoning_summary.delta" ||
+								parsed.type === "response.reasoning_summary_text.delta"
+							) {
+								if (parsed.delta) {
+									hasContent = true
+									yield { type: "reasoning", text: parsed.delta }
+								}
+							} else if (parsed.type === "response.refusal.delta") {
+								if (parsed.delta) {
+									hasContent = true
+									yield { type: "text", text: `[Refusal] ${parsed.delta}` }
+								}
+							} else if (parsed.type === "response.output_item.added") {
+								if (parsed.item) {
+									if (parsed.item.type === "text" && parsed.item.text) {
+										hasContent = true
+										yield { type: "text", text: parsed.item.text }
+									} else if (parsed.item.type === "reasoning" && parsed.item.text) {
+										hasContent = true
+										yield { type: "reasoning", text: parsed.item.text }
+									} else if (parsed.item.type === "message" && parsed.item.content) {
+										for (const content of parsed.item.content) {
+											if (content.type === "text" && content.text) {
+												hasContent = true
+												yield { type: "text", text: content.text }
+											}
+										}
+									}
+								}
+							} else if (parsed.type === "response.error" || parsed.type === "error") {
+								if (parsed.error || parsed.message) {
+									throw new Error(
+										t("common:errors.openAiCodex.apiError", {
+											message: parsed.error?.message || parsed.message || "Unknown error",
+										}),
+									)
+								}
+							} else if (parsed.type === "response.failed") {
+								if (parsed.error || parsed.message) {
+									throw new Error(
+										t("common:errors.openAiCodex.responseFailed", {
+											message: parsed.error?.message || parsed.message || "Unknown failure",
+										}),
+									)
+								}
+							} else if (parsed.type === "response.completed" || parsed.type === "response.done") {
+								if (parsed.response?.output && Array.isArray(parsed.response.output)) {
+									this.lastResponseOutput = parsed.response.output
+								}
+								if (parsed.response?.id) {
+									this.lastResponseId = parsed.response.id as string
+								}
+
+								if (
+									!hasContent &&
+									parsed.response &&
+									parsed.response.output &&
+									Array.isArray(parsed.response.output)
+								) {
+									for (const outputItem of parsed.response.output) {
+										if (outputItem.type === "message" && outputItem.content) {
+											for (const content of outputItem.content) {
+												if (content.type === "output_text" && content.text) {
+													hasContent = true
+													yield { type: "text", text: content.text }
+												}
+											}
+										}
+										if (outputItem.type === "reasoning" && Array.isArray(outputItem.summary)) {
+											for (const summary of outputItem.summary) {
+												if (
+													summary?.type === "summary_text" &&
+													typeof summary.text === "string"
+												) {
+													hasContent = true
+													yield { type: "reasoning", text: summary.text }
+												}
+											}
+										}
+									}
+								}
+							} else if (parsed.choices?.[0]?.delta?.content) {
+								hasContent = true
+								yield { type: "text", text: parsed.choices[0].delta.content }
+							} else if (
+								parsed.item &&
+								typeof parsed.item.text === "string" &&
+								parsed.item.text.length > 0
+							) {
+								hasContent = true
+								yield { type: "text", text: parsed.item.text }
+							} else if (parsed.usage) {
+								const usageData = this.normalizeUsage(parsed.usage, model)
+								if (usageData) {
+									yield usageData
+								}
+							}
+						} catch (e) {
+							if (!(e instanceof SyntaxError)) {
+								throw e
+							}
+						}
+					} else if (line.trim() && !line.startsWith(":")) {
+						try {
+							const parsed = JSON.parse(line)
+							if (parsed.content || parsed.text || parsed.message) {
+								hasContent = true
+								yield { type: "text", text: parsed.content || parsed.text || parsed.message }
+							}
+						} catch {
+							// Not JSON, ignore
+						}
+					}
+				}
+			}
+		} catch (error) {
+			const errorMessage = error instanceof Error ? error.message : String(error)
+			const apiError = new ApiProviderError(errorMessage, this.providerName, model.id, "createMessage")
+			TelemetryService.instance.captureException(apiError)
+
+			if (error instanceof Error) {
+				throw new Error(t("common:errors.openAiCodex.streamProcessingError", { message: error.message }))
+			}
+			throw new Error(t("common:errors.openAiCodex.unexpectedStreamError"))
+		} finally {
+			reader.releaseLock()
+		}
+	}
+
+	private async *processEvent(event: any, model: OpenAiCodexModel): ApiStream {
+		if (event?.response?.output && Array.isArray(event.response.output)) {
+			this.lastResponseOutput = event.response.output
+		}
+		if (event?.response?.id) {
+			this.lastResponseId = event.response.id as string
+		}
+
+		// Handle text deltas
+		if (event?.type === "response.text.delta" || event?.type === "response.output_text.delta") {
+			if (event?.delta) {
+				yield { type: "text", text: event.delta }
+			}
+			return
+		}
+
+		// Handle reasoning deltas
+		if (
+			event?.type === "response.reasoning.delta" ||
+			event?.type === "response.reasoning_text.delta" ||
+			event?.type === "response.reasoning_summary.delta" ||
+			event?.type === "response.reasoning_summary_text.delta"
+		) {
+			if (event?.delta) {
+				yield { type: "reasoning", text: event.delta }
+			}
+			return
+		}
+
+		// Handle refusal deltas
+		if (event?.type === "response.refusal.delta") {
+			if (event?.delta) {
+				yield { type: "text", text: `[Refusal] ${event.delta}` }
+			}
+			return
+		}
+
+		// Handle tool/function call deltas
+		if (
+			event?.type === "response.tool_call_arguments.delta" ||
+			event?.type === "response.function_call_arguments.delta"
+		) {
+			const callId = event.call_id || event.tool_call_id || event.id || this.pendingToolCallId
+			const name = event.name || event.function_name || this.pendingToolCallName
+			const args = event.delta || event.arguments
+
+			// Codex/Responses may stream tool-call arguments, but these delta events are not guaranteed
+			// to include a stable id/name. Avoid emitting incomplete tool_call_partial chunks because
+			// NativeToolCallParser requires a name to start a call.
+			if (typeof callId === "string" && callId.length > 0 && typeof name === "string" && name.length > 0) {
+				yield {
+					type: "tool_call_partial",
+					index: event.index ?? 0,
+					id: callId,
+					name,
+					arguments: typeof args === "string" ? args : "",
+				}
+			}
+			return
+		}
+
+		// Handle tool/function call completion
+		if (
+			event?.type === "response.tool_call_arguments.done" ||
+			event?.type === "response.function_call_arguments.done"
+		) {
+			return
+		}
+
+		// Handle output item events
+		if (event?.type === "response.output_item.added" || event?.type === "response.output_item.done") {
+			const item = event?.item
+			if (item) {
+				// Capture tool identity so subsequent argument deltas can be attributed.
+				if (item.type === "function_call" || item.type === "tool_call") {
+					const callId = item.call_id || item.tool_call_id || item.id
+					const name = item.name || item.function?.name || item.function_name
+					if (typeof callId === "string" && callId.length > 0) {
+						this.pendingToolCallId = callId
+						this.pendingToolCallName = typeof name === "string" ? name : undefined
+					}
+				}
+
+				if (item.type === "text" && item.text) {
+					yield { type: "text", text: item.text }
+				} else if (item.type === "reasoning" && item.text) {
+					yield { type: "reasoning", text: item.text }
+				} else if (item.type === "message" && Array.isArray(item.content)) {
+					for (const content of item.content) {
+						if ((content?.type === "text" || content?.type === "output_text") && content?.text) {
+							yield { type: "text", text: content.text }
+						}
+					}
+				} else if (
+					(item.type === "function_call" || item.type === "tool_call") &&
+					event.type === "response.output_item.done"
+				) {
+					const callId = item.call_id || item.tool_call_id || item.id
+					if (callId) {
+						const args = item.arguments || item.function?.arguments || item.function_arguments
+						yield {
+							type: "tool_call",
+							id: callId,
+							name: item.name || item.function?.name || item.function_name || "",
+							arguments: typeof args === "string" ? args : "{}",
+						}
+					}
+				}
+			}
+			return
+		}
+
+		// Handle completion events
+		if (event?.type === "response.done" || event?.type === "response.completed") {
+			const usage = event?.response?.usage || event?.usage || undefined
+			const usageData = this.normalizeUsage(usage, model)
+			if (usageData) {
+				yield usageData
+			}
+			return
+		}
+
+		// Fallbacks
+		if (event?.choices?.[0]?.delta?.content) {
+			yield { type: "text", text: event.choices[0].delta.content }
+			return
+		}
+
+		if (event?.usage) {
+			const usageData = this.normalizeUsage(event.usage, model)
+			if (usageData) {
+				yield usageData
+			}
+		}
+	}
+
+	private getReasoningEffort(model: OpenAiCodexModel): ReasoningEffortExtended | undefined {
+		const selected = (this.options.reasoningEffort as any) ?? (model.info.reasoningEffort as any)
+		return selected && selected !== "disable" && selected !== "none" ? (selected as any) : undefined
+	}
+
+	override getModel() {
+		const modelId = this.options.apiModelId
+
+		let id = modelId && modelId in openAiCodexModels ? (modelId as OpenAiCodexModelId) : openAiCodexDefaultModelId
+
+		const info: ModelInfo = openAiCodexModels[id]
+
+		const params = getModelParams({
+			format: "openai",
+			modelId: id,
+			model: info,
+			settings: this.options,
+			defaultTemperature: 0,
+		})
+
+		return { id, info, ...params }
+	}
+
+	getEncryptedContent(): { encrypted_content: string; id?: string } | undefined {
+		if (!this.lastResponseOutput) return undefined
+
+		const reasoningItem = this.lastResponseOutput.find(
+			(item) => item.type === "reasoning" && item.encrypted_content,
+		)
+
+		if (!reasoningItem?.encrypted_content) return undefined
+
+		return {
+			encrypted_content: reasoningItem.encrypted_content,
+			...(reasoningItem.id ? { id: reasoningItem.id } : {}),
+		}
+	}
+
+	getResponseId(): string | undefined {
+		return this.lastResponseId
+	}
+
+	async completePrompt(prompt: string): Promise<string> {
+		this.abortController = new AbortController()
+
+		try {
+			const model = this.getModel()
+
+			// Get access token
+			const accessToken = await openAiCodexOAuthManager.getAccessToken()
+			if (!accessToken) {
+				throw new Error(
+					t("common:errors.openAiCodex.notAuthenticated", {
+						defaultValue:
+							"Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow.",
+					}),
+				)
+			}
+
+			const reasoningEffort = this.getReasoningEffort(model)
+
+			const requestBody: any = {
+				model: model.id,
+				input: [
+					{
+						role: "user",
+						content: [{ type: "input_text", text: prompt }],
+					},
+				],
+				stream: false,
+				store: false,
+				...(reasoningEffort ? { include: ["reasoning.encrypted_content"] } : {}),
+			}
+
+			if (reasoningEffort) {
+				requestBody.reasoning = {
+					effort: reasoningEffort,
+					summary: "auto" as const,
+				}
+			}
+
+			const url = `${CODEX_API_BASE_URL}/responses`
+
+			// Get ChatGPT account ID for organization subscriptions
+			const accountId = await openAiCodexOAuthManager.getAccountId()
+
+			// Build headers with required Codex-specific fields
+			const headers: Record<string, string> = {
+				"Content-Type": "application/json",
+				Authorization: `Bearer ${accessToken}`,
+				originator: "roo-code",
+				session_id: this.sessionId,
+				"User-Agent": `roo-code/${extensionVersion} (${os.platform()} ${os.release()}; ${os.arch()}) node/${process.version.slice(1)}`,
+			}
+
+			// Add ChatGPT-Account-Id if available
+			if (accountId) {
+				headers["ChatGPT-Account-Id"] = accountId
+			}
+
+			const response = await fetch(url, {
+				method: "POST",
+				headers,
+				body: JSON.stringify(requestBody),
+				signal: this.abortController.signal,
+			})
+
+			if (!response.ok) {
+				const errorText = await response.text()
+				throw new Error(
+					t("common:errors.openAiCodex.genericError", { status: response.status }) +
+						(errorText ? `: ${errorText}` : ""),
+				)
+			}
+
+			const responseData = await response.json()
+
+			if (responseData?.output && Array.isArray(responseData.output)) {
+				for (const outputItem of responseData.output) {
+					if (outputItem.type === "message" && outputItem.content) {
+						for (const content of outputItem.content) {
+							if (content.type === "output_text" && content.text) {
+								return content.text
+							}
+						}
+					}
+				}
+			}
+
+			if (responseData?.text) {
+				return responseData.text
+			}
+
+			return ""
+		} catch (error) {
+			const errorModel = this.getModel()
+			const errorMessage = error instanceof Error ? error.message : String(error)
+			const apiError = new ApiProviderError(errorMessage, this.providerName, errorModel.id, "completePrompt")
+			TelemetryService.instance.captureException(apiError)
+
+			if (error instanceof Error) {
+				throw new Error(t("common:errors.openAiCodex.completionError", { message: error.message }))
+			}
+			throw error
+		} finally {
+			this.abortController = undefined
+		}
+	}
+}

+ 4 - 3
src/core/task/Task.ts

@@ -2,6 +2,7 @@ import * as path from "path"
 import * as vscode from "vscode"
 import os from "os"
 import crypto from "crypto"
+import { v7 as uuidv7 } from "uuid"
 import EventEmitter from "events"
 
 import { AskIgnoredError } from "./AskIgnoredError"
@@ -481,7 +482,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 			)
 		}
 
-		this.taskId = historyItem ? historyItem.id : crypto.randomUUID()
+		this.taskId = historyItem ? historyItem.id : uuidv7()
 		this.rootTaskId = historyItem ? historyItem.rootTaskId : rootTask?.taskId
 		this.parentTaskId = historyItem ? historyItem.parentTaskId : parentTask?.taskId
 		this.childTaskId = undefined
@@ -508,7 +509,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 		})
 
 		this.apiConfiguration = apiConfiguration
-		this.api = buildApiHandler(apiConfiguration)
+		this.api = buildApiHandler(this.apiConfiguration)
 		this.autoApprovalHandler = new AutoApprovalHandler()
 
 		this.urlContentFetcher = new UrlContentFetcher(provider.context)
@@ -1547,7 +1548,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 	public updateApiConfiguration(newApiConfiguration: ProviderSettings): void {
 		// Update the configuration and rebuild the API handler
 		this.apiConfiguration = newApiConfiguration
-		this.api = buildApiHandler(newApiConfiguration)
+		this.api = buildApiHandler(this.apiConfiguration)
 
 		// IMPORTANT: Do NOT change the parser based on the new configuration!
 		// The task's tool protocol is locked at creation time and must remain

+ 8 - 0
src/core/task/__tests__/Task.spec.ts

@@ -26,6 +26,14 @@ vi.mock("delay", () => ({
 
 import delay from "delay"
 
+vi.mock("uuid", async (importOriginal) => {
+	const actual = await importOriginal<typeof import("uuid")>()
+	return {
+		...actual,
+		v7: vi.fn(() => "00000000-0000-7000-8000-000000000000"),
+	}
+})
+
 vi.mock("execa", () => ({
 	execa: vi.fn(),
 }))

+ 8 - 0
src/core/webview/ClineProvider.ts

@@ -2193,6 +2193,14 @@ export class ClineProvider
 					return false
 				}
 			})(),
+			openAiCodexIsAuthenticated: await (async () => {
+				try {
+					const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth")
+					return await openAiCodexOAuthManager.isAuthenticated()
+				} catch {
+					return false
+				}
+			})(),
 			debug: vscode.workspace.getConfiguration(Package.name).get<boolean>("debug", false),
 		}
 	}

+ 39 - 0
src/core/webview/webviewMessageHandler.ts

@@ -2385,6 +2385,45 @@ export const webviewMessageHandler = async (
 			}
 			break
 		}
+		case "openAiCodexSignIn": {
+			try {
+				const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth")
+				const authUrl = openAiCodexOAuthManager.startAuthorizationFlow()
+
+				// Open the authorization URL in the browser
+				await vscode.env.openExternal(vscode.Uri.parse(authUrl))
+
+				// Wait for the callback in a separate promise (non-blocking)
+				openAiCodexOAuthManager
+					.waitForCallback()
+					.then(async () => {
+						vscode.window.showInformationMessage("Successfully signed in to OpenAI Codex")
+						await provider.postStateToWebview()
+					})
+					.catch((error) => {
+						provider.log(`OpenAI Codex OAuth callback failed: ${error}`)
+						if (!String(error).includes("timed out")) {
+							vscode.window.showErrorMessage(`OpenAI Codex sign in failed: ${error.message || error}`)
+						}
+					})
+			} catch (error) {
+				provider.log(`OpenAI Codex OAuth failed: ${error}`)
+				vscode.window.showErrorMessage("OpenAI Codex sign in failed.")
+			}
+			break
+		}
+		case "openAiCodexSignOut": {
+			try {
+				const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth")
+				await openAiCodexOAuthManager.clearCredentials()
+				vscode.window.showInformationMessage("Signed out from OpenAI Codex")
+				await provider.postStateToWebview()
+			} catch (error) {
+				provider.log(`OpenAI Codex sign out failed: ${error}`)
+				vscode.window.showErrorMessage("OpenAI Codex sign out failed.")
+			}
+			break
+		}
 		case "rooCloudManualUrl": {
 			try {
 				if (!message.text) {

+ 4 - 0
src/extension.ts

@@ -28,6 +28,7 @@ import { ClineProvider } from "./core/webview/ClineProvider"
 import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
 import { TerminalRegistry } from "./integrations/terminal/TerminalRegistry"
 import { claudeCodeOAuthManager } from "./integrations/claude-code/oauth"
+import { openAiCodexOAuthManager } from "./integrations/openai-codex/oauth"
 import { McpServerManager } from "./services/mcp/McpServerManager"
 import { CodeIndexManager } from "./services/code-index/manager"
 import { MdmService } from "./services/mdm/MdmService"
@@ -104,6 +105,9 @@ export async function activate(context: vscode.ExtensionContext) {
 	// Initialize Claude Code OAuth manager for direct API access.
 	claudeCodeOAuthManager.initialize(context, (message) => outputChannel.appendLine(message))
 
+	// Initialize OpenAI Codex OAuth manager for ChatGPT subscription-based access.
+	openAiCodexOAuthManager.initialize(context, (message) => outputChannel.appendLine(message))
+
 	// Get default commands from configuration.
 	const defaultCommands = vscode.workspace.getConfiguration(Package.name).get<string[]>("allowedCommands") || []
 

+ 20 - 1
src/i18n/locales/ca/common.json

@@ -126,8 +126,27 @@
 		"roo": {
 			"authenticationRequired": "El proveïdor Roo requereix autenticació al núvol. Si us plau, inicieu sessió a Roo Code Cloud."
 		},
+		"openAiCodex": {
+			"notAuthenticated": "No esteu autenticat amb OpenAI Codex. Si us plau, inicieu sessió mitjançant el flux OAuth d'OpenAI Codex.",
+			"invalidRequest": "Sol·licitud no vàlida a l'API de Codex. Si us plau, comproveu els paràmetres d'entrada.",
+			"authenticationFailed": "Ha fallat l'autenticació. Si us plau, torneu a autenticar-vos amb OpenAI Codex.",
+			"accessDenied": "Accés denegat. La vostra subscripció a ChatGPT pot no incloure accés a Codex.",
+			"endpointNotFound": "Punt final de l'API de Codex no trobat.",
+			"rateLimitExceeded": "S'ha superat el límit de velocitat. Si us plau, torneu-ho a provar més tard.",
+			"serviceError": "Error del servei OpenAI Codex. Si us plau, torneu-ho a provar més tard.",
+			"genericError": "Error de l'API de Codex ({{status}})",
+			"noResponseBody": "Error de l'API de Codex: No hi ha cos de resposta",
+			"connectionFailed": "Ha fallat la connexió a l'API de Codex: {{message}}",
+			"unexpectedConnectionError": "Error inesperat en connectar amb l'API de Codex",
+			"apiError": "Error de l'API de Codex: {{message}}",
+			"responseFailed": "La resposta ha fallat: {{message}}",
+			"streamProcessingError": "Error en processar el flux de resposta: {{message}}",
+			"unexpectedStreamError": "Error inesperat en processar el flux de resposta",
+			"completionError": "Error de finalització d'OpenAI Codex: {{message}}"
+		},
 		"api": {
-			"invalidKeyInvalidChars": "La clau API conté caràcters no vàlids."
+			"invalidKeyInvalidChars": "La clau API conté caràcters no vàlids.",
+			"apiRequestFailed": "La sol·licitud API ha fallat ({{status}})"
 		},
 		"manual_url_empty": "Si us plau, introdueix una URL de callback vàlida",
 		"manual_url_no_query": "URL de callback no vàlida: falten paràmetres de consulta",

+ 20 - 1
src/i18n/locales/de/common.json

@@ -123,8 +123,27 @@
 		"roo": {
 			"authenticationRequired": "Roo-Anbieter erfordert Cloud-Authentifizierung. Bitte melde dich bei Roo Code Cloud an."
 		},
+		"openAiCodex": {
+			"notAuthenticated": "Nicht bei OpenAI Codex authentifiziert. Bitte melde dich über den OpenAI Codex OAuth-Flow an.",
+			"invalidRequest": "Ungültige Anfrage an die Codex-API. Bitte überprüfe deine Eingabeparameter.",
+			"authenticationFailed": "Authentifizierung fehlgeschlagen. Bitte authentifiziere dich erneut bei OpenAI Codex.",
+			"accessDenied": "Zugriff verweigert. Dein ChatGPT-Abonnement enthält möglicherweise keinen Codex-Zugang.",
+			"endpointNotFound": "Codex-API-Endpunkt nicht gefunden.",
+			"rateLimitExceeded": "Ratenlimit überschritten. Bitte versuche es später erneut.",
+			"serviceError": "OpenAI Codex Dienstfehler. Bitte versuche es später erneut.",
+			"genericError": "Codex-API-Fehler ({{status}})",
+			"noResponseBody": "Codex-API-Fehler: Kein Antworttext",
+			"connectionFailed": "Verbindung zur Codex-API fehlgeschlagen: {{message}}",
+			"unexpectedConnectionError": "Unerwarteter Fehler beim Verbinden mit der Codex-API",
+			"apiError": "Codex-API-Fehler: {{message}}",
+			"responseFailed": "Antwort fehlgeschlagen: {{message}}",
+			"streamProcessingError": "Fehler beim Verarbeiten des Antwort-Streams: {{message}}",
+			"unexpectedStreamError": "Unerwarteter Fehler beim Verarbeiten des Antwort-Streams",
+			"completionError": "OpenAI Codex Vervollständigungsfehler: {{message}}"
+		},
 		"api": {
-			"invalidKeyInvalidChars": "API-Schlüssel enthält ungültige Zeichen."
+			"invalidKeyInvalidChars": "API-Schlüssel enthält ungültige Zeichen.",
+			"apiRequestFailed": "API-Anfrage fehlgeschlagen ({{status}})"
 		},
 		"manual_url_empty": "Bitte gib eine gültige Callback-URL ein",
 		"manual_url_no_query": "Ungültige Callback-URL: Query-Parameter fehlen",

+ 20 - 1
src/i18n/locales/en/common.json

@@ -123,8 +123,27 @@
 		"roo": {
 			"authenticationRequired": "Roo provider requires cloud authentication. Please sign in to Roo Code Cloud."
 		},
+		"openAiCodex": {
+			"notAuthenticated": "Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow.",
+			"invalidRequest": "Invalid request to Codex API. Please check your input parameters.",
+			"authenticationFailed": "Authentication failed. Please re-authenticate with OpenAI Codex.",
+			"accessDenied": "Access denied. Your ChatGPT subscription may not include Codex access.",
+			"endpointNotFound": "Codex API endpoint not found.",
+			"rateLimitExceeded": "Rate limit exceeded. Please try again later.",
+			"serviceError": "OpenAI Codex service error. Please try again later.",
+			"genericError": "Codex API error ({{status}})",
+			"noResponseBody": "Codex API error: No response body",
+			"connectionFailed": "Failed to connect to Codex API: {{message}}",
+			"unexpectedConnectionError": "Unexpected error connecting to Codex API",
+			"apiError": "Codex API error: {{message}}",
+			"responseFailed": "Response failed: {{message}}",
+			"streamProcessingError": "Error processing response stream: {{message}}",
+			"unexpectedStreamError": "Unexpected error processing response stream",
+			"completionError": "OpenAI Codex completion error: {{message}}"
+		},
 		"api": {
-			"invalidKeyInvalidChars": "API key contains invalid characters."
+			"invalidKeyInvalidChars": "API key contains invalid characters.",
+			"apiRequestFailed": "API request failed ({{status}})"
 		},
 		"manual_url_empty": "Please enter a valid callback URL",
 		"manual_url_no_query": "Invalid callback URL: missing query parameters",

+ 21 - 2
src/i18n/locales/es/common.json

@@ -124,13 +124,32 @@
 			"authenticationRequired": "El proveedor Roo requiere autenticación en la nube. Por favor, inicia sesión en Roo Code Cloud."
 		},
 		"api": {
-			"invalidKeyInvalidChars": "La clave API contiene caracteres inválidos."
+			"invalidKeyInvalidChars": "La clave API contiene caracteres inválidos.",
+			"apiRequestFailed": "La solicitud API falló ({{status}})"
 		},
 		"manual_url_empty": "Por favor, introduce una URL de callback válida",
 		"manual_url_no_query": "URL de callback inválida: faltan parámetros de consulta",
 		"manual_url_missing_params": "URL de callback inválida: faltan parámetros requeridos (code y state)",
 		"manual_url_auth_failed": "Autenticación manual por URL falló",
-		"manual_url_auth_error": "Error de autenticación"
+		"manual_url_auth_error": "Error de autenticación",
+		"openAiCodex": {
+			"notAuthenticated": "Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow.",
+			"invalidRequest": "Invalid request to Codex API. Please check your input parameters.",
+			"authenticationFailed": "Authentication failed. Please re-authenticate with OpenAI Codex.",
+			"accessDenied": "Access denied. Your ChatGPT subscription may not include Codex access.",
+			"endpointNotFound": "Codex API endpoint not found.",
+			"rateLimitExceeded": "Rate limit exceeded. Please try again later.",
+			"serviceError": "OpenAI Codex service error. Please try again later.",
+			"genericError": "Codex API error ({{status}})",
+			"noResponseBody": "Codex API error: No response body",
+			"connectionFailed": "Failed to connect to Codex API: {{message}}",
+			"unexpectedConnectionError": "Unexpected error connecting to Codex API",
+			"apiError": "Codex API error: {{message}}",
+			"responseFailed": "Response failed: {{message}}",
+			"streamProcessingError": "Error processing response stream: {{message}}",
+			"unexpectedStreamError": "Unexpected error processing response stream",
+			"completionError": "OpenAI Codex completion error: {{message}}"
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "No hay contenido de terminal seleccionado",

+ 21 - 2
src/i18n/locales/fr/common.json

@@ -124,13 +124,32 @@
 			"authenticationRequired": "Le fournisseur Roo nécessite une authentification cloud. Veuillez vous connecter à Roo Code Cloud."
 		},
 		"api": {
-			"invalidKeyInvalidChars": "La clé API contient des caractères invalides."
+			"invalidKeyInvalidChars": "La clé API contient des caractères invalides.",
+			"apiRequestFailed": "La requête API a échoué ({{status}})"
 		},
 		"manual_url_empty": "Veuillez entrer une URL de callback valide",
 		"manual_url_no_query": "URL de callback invalide : paramètres de requête manquants",
 		"manual_url_missing_params": "URL de callback invalide : paramètres requis manquants (code et state)",
 		"manual_url_auth_failed": "Authentification par URL manuelle échouée",
-		"manual_url_auth_error": "Échec de l'authentification"
+		"manual_url_auth_error": "Échec de l'authentification",
+		"openAiCodex": {
+			"notAuthenticated": "Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow.",
+			"invalidRequest": "Invalid request to Codex API. Please check your input parameters.",
+			"authenticationFailed": "Authentication failed. Please re-authenticate with OpenAI Codex.",
+			"accessDenied": "Access denied. Your ChatGPT subscription may not include Codex access.",
+			"endpointNotFound": "Codex API endpoint not found.",
+			"rateLimitExceeded": "Rate limit exceeded. Please try again later.",
+			"serviceError": "OpenAI Codex service error. Please try again later.",
+			"genericError": "Codex API error ({{status}})",
+			"noResponseBody": "Codex API error: No response body",
+			"connectionFailed": "Failed to connect to Codex API: {{message}}",
+			"unexpectedConnectionError": "Unexpected error connecting to Codex API",
+			"apiError": "Codex API error: {{message}}",
+			"responseFailed": "Response failed: {{message}}",
+			"streamProcessingError": "Error processing response stream: {{message}}",
+			"unexpectedStreamError": "Unexpected error processing response stream",
+			"completionError": "OpenAI Codex completion error: {{message}}"
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "Aucun contenu de terminal sélectionné",

+ 21 - 2
src/i18n/locales/hi/common.json

@@ -124,13 +124,32 @@
 			"authenticationRequired": "Roo प्रदाता को क्लाउड प्रमाणीकरण की आवश्यकता है। कृपया Roo Code Cloud में साइन इन करें।"
 		},
 		"api": {
-			"invalidKeyInvalidChars": "API कुंजी में अमान्य वर्ण हैं।"
+			"invalidKeyInvalidChars": "API कुंजी में अमान्य वर्ण हैं।",
+			"apiRequestFailed": "API अनुरोध विफल ({{status}})"
 		},
 		"manual_url_empty": "कृपया एक वैध callback URL दर्ज करें",
 		"manual_url_no_query": "अवैध callback URL: क्वेरी पैरामीटर गुम हैं",
 		"manual_url_missing_params": "अवैध callback URL: आवश्यक पैरामीटर गुम हैं (code और state)",
 		"manual_url_auth_failed": "मैनुअल URL प्रमाणीकरण असफल",
-		"manual_url_auth_error": "प्रमाणीकरण असफल"
+		"manual_url_auth_error": "प्रमाणीकरण असफल",
+		"openAiCodex": {
+			"notAuthenticated": "Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow.",
+			"invalidRequest": "Invalid request to Codex API. Please check your input parameters.",
+			"authenticationFailed": "Authentication failed. Please re-authenticate with OpenAI Codex.",
+			"accessDenied": "Access denied. Your ChatGPT subscription may not include Codex access.",
+			"endpointNotFound": "Codex API endpoint not found.",
+			"rateLimitExceeded": "Rate limit exceeded. Please try again later.",
+			"serviceError": "OpenAI Codex service error. Please try again later.",
+			"genericError": "Codex API error ({{status}})",
+			"noResponseBody": "Codex API error: No response body",
+			"connectionFailed": "Failed to connect to Codex API: {{message}}",
+			"unexpectedConnectionError": "Unexpected error connecting to Codex API",
+			"apiError": "Codex API error: {{message}}",
+			"responseFailed": "Response failed: {{message}}",
+			"streamProcessingError": "Error processing response stream: {{message}}",
+			"unexpectedStreamError": "Unexpected error processing response stream",
+			"completionError": "OpenAI Codex completion error: {{message}}"
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "कोई टर्मिनल सामग्री चयनित नहीं",

+ 21 - 2
src/i18n/locales/id/common.json

@@ -124,13 +124,32 @@
 			"authenticationRequired": "Penyedia Roo memerlukan autentikasi cloud. Silakan masuk ke Roo Code Cloud."
 		},
 		"api": {
-			"invalidKeyInvalidChars": "Kunci API mengandung karakter tidak valid."
+			"invalidKeyInvalidChars": "Kunci API mengandung karakter tidak valid.",
+			"apiRequestFailed": "Permintaan API gagal ({{status}})"
 		},
 		"manual_url_empty": "Silakan masukkan URL callback yang valid",
 		"manual_url_no_query": "URL callback tidak valid: parameter query hilang",
 		"manual_url_missing_params": "URL callback tidak valid: parameter yang diperlukan hilang (code dan state)",
 		"manual_url_auth_failed": "Autentikasi URL manual gagal",
-		"manual_url_auth_error": "Autentikasi gagal"
+		"manual_url_auth_error": "Autentikasi gagal",
+		"openAiCodex": {
+			"notAuthenticated": "Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow.",
+			"invalidRequest": "Invalid request to Codex API. Please check your input parameters.",
+			"authenticationFailed": "Authentication failed. Please re-authenticate with OpenAI Codex.",
+			"accessDenied": "Access denied. Your ChatGPT subscription may not include Codex access.",
+			"endpointNotFound": "Codex API endpoint not found.",
+			"rateLimitExceeded": "Rate limit exceeded. Please try again later.",
+			"serviceError": "OpenAI Codex service error. Please try again later.",
+			"genericError": "Codex API error ({{status}})",
+			"noResponseBody": "Codex API error: No response body",
+			"connectionFailed": "Failed to connect to Codex API: {{message}}",
+			"unexpectedConnectionError": "Unexpected error connecting to Codex API",
+			"apiError": "Codex API error: {{message}}",
+			"responseFailed": "Response failed: {{message}}",
+			"streamProcessingError": "Error processing response stream: {{message}}",
+			"unexpectedStreamError": "Unexpected error processing response stream",
+			"completionError": "OpenAI Codex completion error: {{message}}"
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "Tidak ada konten terminal yang dipilih",

+ 21 - 2
src/i18n/locales/it/common.json

@@ -124,13 +124,32 @@
 			"authenticationRequired": "Il provider Roo richiede l'autenticazione cloud. Accedi a Roo Code Cloud."
 		},
 		"api": {
-			"invalidKeyInvalidChars": "La chiave API contiene caratteri non validi."
+			"invalidKeyInvalidChars": "La chiave API contiene caratteri non validi.",
+			"apiRequestFailed": "Richiesta API fallita ({{status}})"
 		},
 		"manual_url_empty": "Inserisci un URL di callback valido",
 		"manual_url_no_query": "URL di callback non valido: parametri di query mancanti",
 		"manual_url_missing_params": "URL di callback non valido: parametri richiesti mancanti (code e state)",
 		"manual_url_auth_failed": "Autenticazione manuale tramite URL fallita",
-		"manual_url_auth_error": "Autenticazione fallita"
+		"manual_url_auth_error": "Autenticazione fallita",
+		"openAiCodex": {
+			"notAuthenticated": "Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow.",
+			"invalidRequest": "Invalid request to Codex API. Please check your input parameters.",
+			"authenticationFailed": "Authentication failed. Please re-authenticate with OpenAI Codex.",
+			"accessDenied": "Access denied. Your ChatGPT subscription may not include Codex access.",
+			"endpointNotFound": "Codex API endpoint not found.",
+			"rateLimitExceeded": "Rate limit exceeded. Please try again later.",
+			"serviceError": "OpenAI Codex service error. Please try again later.",
+			"genericError": "Codex API error ({{status}})",
+			"noResponseBody": "Codex API error: No response body",
+			"connectionFailed": "Failed to connect to Codex API: {{message}}",
+			"unexpectedConnectionError": "Unexpected error connecting to Codex API",
+			"apiError": "Codex API error: {{message}}",
+			"responseFailed": "Response failed: {{message}}",
+			"streamProcessingError": "Error processing response stream: {{message}}",
+			"unexpectedStreamError": "Unexpected error processing response stream",
+			"completionError": "OpenAI Codex completion error: {{message}}"
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "Nessun contenuto del terminale selezionato",

+ 21 - 2
src/i18n/locales/ja/common.json

@@ -124,13 +124,32 @@
 			"authenticationRequired": "Rooプロバイダーはクラウド認証が必要です。Roo Code Cloudにサインインしてください。"
 		},
 		"api": {
-			"invalidKeyInvalidChars": "APIキーに無効な文字が含まれています。"
+			"invalidKeyInvalidChars": "APIキーに無効な文字が含まれています。",
+			"apiRequestFailed": "APIリクエストが失敗しました ({{status}})"
 		},
 		"manual_url_empty": "有効なコールバック URL を入力してください",
 		"manual_url_no_query": "無効なコールバック URL:クエリパラメータがありません",
 		"manual_url_missing_params": "無効なコールバック URL:必要なパラメータ(code と state)がありません",
 		"manual_url_auth_failed": "手動 URL 認証が失敗しました",
-		"manual_url_auth_error": "認証に失敗しました"
+		"manual_url_auth_error": "認証に失敗しました",
+		"openAiCodex": {
+			"notAuthenticated": "Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow.",
+			"invalidRequest": "Invalid request to Codex API. Please check your input parameters.",
+			"authenticationFailed": "Authentication failed. Please re-authenticate with OpenAI Codex.",
+			"accessDenied": "Access denied. Your ChatGPT subscription may not include Codex access.",
+			"endpointNotFound": "Codex API endpoint not found.",
+			"rateLimitExceeded": "Rate limit exceeded. Please try again later.",
+			"serviceError": "OpenAI Codex service error. Please try again later.",
+			"genericError": "Codex API error ({{status}})",
+			"noResponseBody": "Codex API error: No response body",
+			"connectionFailed": "Failed to connect to Codex API: {{message}}",
+			"unexpectedConnectionError": "Unexpected error connecting to Codex API",
+			"apiError": "Codex API error: {{message}}",
+			"responseFailed": "Response failed: {{message}}",
+			"streamProcessingError": "Error processing response stream: {{message}}",
+			"unexpectedStreamError": "Unexpected error processing response stream",
+			"completionError": "OpenAI Codex completion error: {{message}}"
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "選択されたターミナルコンテンツがありません",

+ 21 - 2
src/i18n/locales/ko/common.json

@@ -124,13 +124,32 @@
 			"authenticationRequired": "Roo 제공업체는 클라우드 인증이 필요합니다. Roo Code Cloud에 로그인하세요."
 		},
 		"api": {
-			"invalidKeyInvalidChars": "API 키에 유효하지 않은 문자가 포함되어 있습니다."
+			"invalidKeyInvalidChars": "API 키에 유효하지 않은 문자가 포함되어 있습니다.",
+			"apiRequestFailed": "API 요청 실패 ({{status}})"
 		},
 		"manual_url_empty": "유효한 콜백 URL을 입력하세요",
 		"manual_url_no_query": "유효하지 않은 콜백 URL: 쿼리 매개변수 누락",
 		"manual_url_missing_params": "유효하지 않은 콜백 URL: 필요한 매개변수 누락 (code와 state)",
 		"manual_url_auth_failed": "수동 URL 인증 실패",
-		"manual_url_auth_error": "인증 실패"
+		"manual_url_auth_error": "인증 실패",
+		"openAiCodex": {
+			"notAuthenticated": "Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow.",
+			"invalidRequest": "Invalid request to Codex API. Please check your input parameters.",
+			"authenticationFailed": "Authentication failed. Please re-authenticate with OpenAI Codex.",
+			"accessDenied": "Access denied. Your ChatGPT subscription may not include Codex access.",
+			"endpointNotFound": "Codex API endpoint not found.",
+			"rateLimitExceeded": "Rate limit exceeded. Please try again later.",
+			"serviceError": "OpenAI Codex service error. Please try again later.",
+			"genericError": "Codex API error ({{status}})",
+			"noResponseBody": "Codex API error: No response body",
+			"connectionFailed": "Failed to connect to Codex API: {{message}}",
+			"unexpectedConnectionError": "Unexpected error connecting to Codex API",
+			"apiError": "Codex API error: {{message}}",
+			"responseFailed": "Response failed: {{message}}",
+			"streamProcessingError": "Error processing response stream: {{message}}",
+			"unexpectedStreamError": "Unexpected error processing response stream",
+			"completionError": "OpenAI Codex completion error: {{message}}"
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "선택된 터미널 내용이 없습니다",

+ 21 - 2
src/i18n/locales/nl/common.json

@@ -124,13 +124,32 @@
 			"authenticationRequired": "Roo provider vereist cloud authenticatie. Log in bij Roo Code Cloud."
 		},
 		"api": {
-			"invalidKeyInvalidChars": "API-sleutel bevat ongeldige karakters."
+			"invalidKeyInvalidChars": "API-sleutel bevat ongeldige karakters.",
+			"apiRequestFailed": "API-verzoek mislukt ({{status}})"
 		},
 		"manual_url_empty": "Voer een geldige callback-URL in",
 		"manual_url_no_query": "Ongeldige callback-URL: query-parameters ontbreken",
 		"manual_url_missing_params": "Ongeldige callback-URL: vereiste parameters ontbreken (code en state)",
 		"manual_url_auth_failed": "Handmatige URL-authenticatie mislukt",
-		"manual_url_auth_error": "Authenticatie mislukt"
+		"manual_url_auth_error": "Authenticatie mislukt",
+		"openAiCodex": {
+			"notAuthenticated": "Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow.",
+			"invalidRequest": "Invalid request to Codex API. Please check your input parameters.",
+			"authenticationFailed": "Authentication failed. Please re-authenticate with OpenAI Codex.",
+			"accessDenied": "Access denied. Your ChatGPT subscription may not include Codex access.",
+			"endpointNotFound": "Codex API endpoint not found.",
+			"rateLimitExceeded": "Rate limit exceeded. Please try again later.",
+			"serviceError": "OpenAI Codex service error. Please try again later.",
+			"genericError": "Codex API error ({{status}})",
+			"noResponseBody": "Codex API error: No response body",
+			"connectionFailed": "Failed to connect to Codex API: {{message}}",
+			"unexpectedConnectionError": "Unexpected error connecting to Codex API",
+			"apiError": "Codex API error: {{message}}",
+			"responseFailed": "Response failed: {{message}}",
+			"streamProcessingError": "Error processing response stream: {{message}}",
+			"unexpectedStreamError": "Unexpected error processing response stream",
+			"completionError": "OpenAI Codex completion error: {{message}}"
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "Geen terminalinhoud geselecteerd",

+ 21 - 2
src/i18n/locales/pl/common.json

@@ -124,13 +124,32 @@
 			"authenticationRequired": "Dostawca Roo wymaga uwierzytelnienia w chmurze. Zaloguj się do Roo Code Cloud."
 		},
 		"api": {
-			"invalidKeyInvalidChars": "Klucz API zawiera nieprawidłowe znaki."
+			"invalidKeyInvalidChars": "Klucz API zawiera nieprawidłowe znaki.",
+			"apiRequestFailed": "Żądanie API nie powiodło się ({{status}})"
 		},
 		"manual_url_empty": "Wprowadź prawidłowy URL callback",
 		"manual_url_no_query": "Nieprawidłowy URL callback: brak parametrów zapytania",
 		"manual_url_missing_params": "Nieprawidłowy URL callback: brak wymaganych parametrów (code i state)",
 		"manual_url_auth_failed": "Ręczne uwierzytelnienie URL nie powiodło się",
-		"manual_url_auth_error": "Uwierzytelnienie nie powiodło się"
+		"manual_url_auth_error": "Uwierzytelnienie nie powiodło się",
+		"openAiCodex": {
+			"notAuthenticated": "Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow.",
+			"invalidRequest": "Invalid request to Codex API. Please check your input parameters.",
+			"authenticationFailed": "Authentication failed. Please re-authenticate with OpenAI Codex.",
+			"accessDenied": "Access denied. Your ChatGPT subscription may not include Codex access.",
+			"endpointNotFound": "Codex API endpoint not found.",
+			"rateLimitExceeded": "Rate limit exceeded. Please try again later.",
+			"serviceError": "OpenAI Codex service error. Please try again later.",
+			"genericError": "Codex API error ({{status}})",
+			"noResponseBody": "Codex API error: No response body",
+			"connectionFailed": "Failed to connect to Codex API: {{message}}",
+			"unexpectedConnectionError": "Unexpected error connecting to Codex API",
+			"apiError": "Codex API error: {{message}}",
+			"responseFailed": "Response failed: {{message}}",
+			"streamProcessingError": "Error processing response stream: {{message}}",
+			"unexpectedStreamError": "Unexpected error processing response stream",
+			"completionError": "OpenAI Codex completion error: {{message}}"
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "Nie wybrano zawartości terminala",

+ 21 - 2
src/i18n/locales/pt-BR/common.json

@@ -128,13 +128,32 @@
 			"authenticationRequired": "O provedor Roo requer autenticação na nuvem. Faça login no Roo Code Cloud."
 		},
 		"api": {
-			"invalidKeyInvalidChars": "A chave API contém caracteres inválidos."
+			"invalidKeyInvalidChars": "A chave API contém caracteres inválidos.",
+			"apiRequestFailed": "Solicitação API falhou ({{status}})"
 		},
 		"manual_url_empty": "Por favor, insira uma URL de callback válida",
 		"manual_url_no_query": "URL de callback inválida: parâmetros de consulta ausentes",
 		"manual_url_missing_params": "URL de callback inválida: parâmetros obrigatórios ausentes (code e state)",
 		"manual_url_auth_failed": "Autenticação manual por URL falhou",
-		"manual_url_auth_error": "Falha na autenticação"
+		"manual_url_auth_error": "Falha na autenticação",
+		"openAiCodex": {
+			"notAuthenticated": "Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow.",
+			"invalidRequest": "Invalid request to Codex API. Please check your input parameters.",
+			"authenticationFailed": "Authentication failed. Please re-authenticate with OpenAI Codex.",
+			"accessDenied": "Access denied. Your ChatGPT subscription may not include Codex access.",
+			"endpointNotFound": "Codex API endpoint not found.",
+			"rateLimitExceeded": "Rate limit exceeded. Please try again later.",
+			"serviceError": "OpenAI Codex service error. Please try again later.",
+			"genericError": "Codex API error ({{status}})",
+			"noResponseBody": "Codex API error: No response body",
+			"connectionFailed": "Failed to connect to Codex API: {{message}}",
+			"unexpectedConnectionError": "Unexpected error connecting to Codex API",
+			"apiError": "Codex API error: {{message}}",
+			"responseFailed": "Response failed: {{message}}",
+			"streamProcessingError": "Error processing response stream: {{message}}",
+			"unexpectedStreamError": "Unexpected error processing response stream",
+			"completionError": "OpenAI Codex completion error: {{message}}"
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "Nenhum conteúdo do terminal selecionado",

+ 21 - 2
src/i18n/locales/ru/common.json

@@ -124,13 +124,32 @@
 			"authenticationRequired": "Провайдер Roo требует облачной аутентификации. Войдите в Roo Code Cloud."
 		},
 		"api": {
-			"invalidKeyInvalidChars": "API-ключ содержит недопустимые символы."
+			"invalidKeyInvalidChars": "API-ключ содержит недопустимые символы.",
+			"apiRequestFailed": "Запрос API не удался ({{status}})"
 		},
 		"manual_url_empty": "Введи действительный URL обратного вызова",
 		"manual_url_no_query": "Недействительный URL обратного вызова: отсутствуют параметры запроса",
 		"manual_url_missing_params": "Недействительный URL обратного вызова: отсутствуют обязательные параметры (code и state)",
 		"manual_url_auth_failed": "Ручная аутентификация по URL не удалась",
-		"manual_url_auth_error": "Аутентификация не удалась"
+		"manual_url_auth_error": "Аутентификация не удалась",
+		"openAiCodex": {
+			"notAuthenticated": "Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow.",
+			"invalidRequest": "Invalid request to Codex API. Please check your input parameters.",
+			"authenticationFailed": "Authentication failed. Please re-authenticate with OpenAI Codex.",
+			"accessDenied": "Access denied. Your ChatGPT subscription may not include Codex access.",
+			"endpointNotFound": "Codex API endpoint not found.",
+			"rateLimitExceeded": "Rate limit exceeded. Please try again later.",
+			"serviceError": "OpenAI Codex service error. Please try again later.",
+			"genericError": "Codex API error ({{status}})",
+			"noResponseBody": "Codex API error: No response body",
+			"connectionFailed": "Failed to connect to Codex API: {{message}}",
+			"unexpectedConnectionError": "Unexpected error connecting to Codex API",
+			"apiError": "Codex API error: {{message}}",
+			"responseFailed": "Response failed: {{message}}",
+			"streamProcessingError": "Error processing response stream: {{message}}",
+			"unexpectedStreamError": "Unexpected error processing response stream",
+			"completionError": "OpenAI Codex completion error: {{message}}"
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "Не выбрано содержимое терминала",

+ 21 - 2
src/i18n/locales/tr/common.json

@@ -124,13 +124,32 @@
 			"authenticationRequired": "Roo sağlayıcısı bulut kimlik doğrulaması gerektirir. Lütfen Roo Code Cloud'a giriş yapın."
 		},
 		"api": {
-			"invalidKeyInvalidChars": "API anahtarı geçersiz karakterler içeriyor."
+			"invalidKeyInvalidChars": "API anahtarı geçersiz karakterler içeriyor.",
+			"apiRequestFailed": "API isteği başarısız oldu ({{status}})"
 		},
 		"manual_url_empty": "Lütfen geçerli bir callback URL'si girin",
 		"manual_url_no_query": "Geçersiz callback URL'si: sorgu parametreleri eksik",
 		"manual_url_missing_params": "Geçersiz callback URL'si: gerekli parametreler eksik (code ve state)",
 		"manual_url_auth_failed": "Manuel URL kimlik doğrulama başarısız",
-		"manual_url_auth_error": "Kimlik doğrulama başarısız"
+		"manual_url_auth_error": "Kimlik doğrulama başarısız",
+		"openAiCodex": {
+			"notAuthenticated": "Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow.",
+			"invalidRequest": "Invalid request to Codex API. Please check your input parameters.",
+			"authenticationFailed": "Authentication failed. Please re-authenticate with OpenAI Codex.",
+			"accessDenied": "Access denied. Your ChatGPT subscription may not include Codex access.",
+			"endpointNotFound": "Codex API endpoint not found.",
+			"rateLimitExceeded": "Rate limit exceeded. Please try again later.",
+			"serviceError": "OpenAI Codex service error. Please try again later.",
+			"genericError": "Codex API error ({{status}})",
+			"noResponseBody": "Codex API error: No response body",
+			"connectionFailed": "Failed to connect to Codex API: {{message}}",
+			"unexpectedConnectionError": "Unexpected error connecting to Codex API",
+			"apiError": "Codex API error: {{message}}",
+			"responseFailed": "Response failed: {{message}}",
+			"streamProcessingError": "Error processing response stream: {{message}}",
+			"unexpectedStreamError": "Unexpected error processing response stream",
+			"completionError": "OpenAI Codex completion error: {{message}}"
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "Seçili terminal içeriği yok",

+ 21 - 2
src/i18n/locales/vi/common.json

@@ -124,13 +124,32 @@
 			"authenticationRequired": "Nhà cung cấp Roo yêu cầu xác thực đám mây. Vui lòng đăng nhập vào Roo Code Cloud."
 		},
 		"api": {
-			"invalidKeyInvalidChars": "Khóa API chứa ký tự không hợp lệ."
+			"invalidKeyInvalidChars": "Khóa API chứa ký tự không hợp lệ.",
+			"apiRequestFailed": "Yêu cầu API thất bại ({{status}})"
 		},
 		"manual_url_empty": "Vui lòng nhập URL callback hợp lệ",
 		"manual_url_no_query": "URL callback không hợp lệ: thiếu tham số truy vấn",
 		"manual_url_missing_params": "URL callback không hợp lệ: thiếu tham số bắt buộc (code và state)",
 		"manual_url_auth_failed": "Xác thực URL thủ công thất bại",
-		"manual_url_auth_error": "Xác thực thất bại"
+		"manual_url_auth_error": "Xác thực thất bại",
+		"openAiCodex": {
+			"notAuthenticated": "Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow.",
+			"invalidRequest": "Invalid request to Codex API. Please check your input parameters.",
+			"authenticationFailed": "Authentication failed. Please re-authenticate with OpenAI Codex.",
+			"accessDenied": "Access denied. Your ChatGPT subscription may not include Codex access.",
+			"endpointNotFound": "Codex API endpoint not found.",
+			"rateLimitExceeded": "Rate limit exceeded. Please try again later.",
+			"serviceError": "OpenAI Codex service error. Please try again later.",
+			"genericError": "Codex API error ({{status}})",
+			"noResponseBody": "Codex API error: No response body",
+			"connectionFailed": "Failed to connect to Codex API: {{message}}",
+			"unexpectedConnectionError": "Unexpected error connecting to Codex API",
+			"apiError": "Codex API error: {{message}}",
+			"responseFailed": "Response failed: {{message}}",
+			"streamProcessingError": "Error processing response stream: {{message}}",
+			"unexpectedStreamError": "Unexpected error processing response stream",
+			"completionError": "OpenAI Codex completion error: {{message}}"
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "Không có nội dung terminal được chọn",

+ 21 - 2
src/i18n/locales/zh-CN/common.json

@@ -129,13 +129,32 @@
 			"authenticationRequired": "Roo 提供商需要云认证。请登录 Roo Code Cloud。"
 		},
 		"api": {
-			"invalidKeyInvalidChars": "API 密钥包含无效字符。"
+			"invalidKeyInvalidChars": "API 密钥包含无效字符.",
+			"apiRequestFailed": "API 请求失败 ({{status}})"
 		},
 		"manual_url_empty": "请输入有效的回调 URL",
 		"manual_url_no_query": "无效的回调 URL:缺少查询参数",
 		"manual_url_missing_params": "无效的回调 URL:缺少必需参数(code 和 state)",
 		"manual_url_auth_failed": "手动 URL 身份验证失败",
-		"manual_url_auth_error": "身份验证失败"
+		"manual_url_auth_error": "身份验证失败",
+		"openAiCodex": {
+			"notAuthenticated": "Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow.",
+			"invalidRequest": "Invalid request to Codex API. Please check your input parameters.",
+			"authenticationFailed": "Authentication failed. Please re-authenticate with OpenAI Codex.",
+			"accessDenied": "Access denied. Your ChatGPT subscription may not include Codex access.",
+			"endpointNotFound": "Codex API endpoint not found.",
+			"rateLimitExceeded": "Rate limit exceeded. Please try again later.",
+			"serviceError": "OpenAI Codex service error. Please try again later.",
+			"genericError": "Codex API error ({{status}})",
+			"noResponseBody": "Codex API error: No response body",
+			"connectionFailed": "Failed to connect to Codex API: {{message}}",
+			"unexpectedConnectionError": "Unexpected error connecting to Codex API",
+			"apiError": "Codex API error: {{message}}",
+			"responseFailed": "Response failed: {{message}}",
+			"streamProcessingError": "Error processing response stream: {{message}}",
+			"unexpectedStreamError": "Unexpected error processing response stream",
+			"completionError": "OpenAI Codex completion error: {{message}}"
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "没有选择终端内容",

+ 21 - 2
src/i18n/locales/zh-TW/common.json

@@ -123,14 +123,33 @@
 			"authenticationRequired": "Roo 提供者需要雲端認證。請登入 Roo Code Cloud。"
 		},
 		"api": {
-			"invalidKeyInvalidChars": "API 金鑰包含無效字元。"
+			"invalidKeyInvalidChars": "API 金鑰包含無效字元。",
+			"apiRequestFailed": "API 請求失敗 ({{status}})"
 		},
 		"manual_url_empty": "請輸入有效的回呼 URL",
 		"manual_url_no_query": "無效的回呼 URL:缺少查詢參數",
 		"manual_url_missing_params": "無效的回呼 URL:缺少必要參數(code 和 state)",
 		"manual_url_auth_failed": "手動 URL 身份驗證失敗",
 		"manual_url_auth_error": "身份驗證失敗",
-		"mode_import_failed": "匯入模式失敗:{{error}}"
+		"mode_import_failed": "匯入模式失敗:{{error}}",
+		"openAiCodex": {
+			"notAuthenticated": "Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow.",
+			"invalidRequest": "Invalid request to Codex API. Please check your input parameters.",
+			"authenticationFailed": "Authentication failed. Please re-authenticate with OpenAI Codex.",
+			"accessDenied": "Access denied. Your ChatGPT subscription may not include Codex access.",
+			"endpointNotFound": "Codex API endpoint not found.",
+			"rateLimitExceeded": "Rate limit exceeded. Please try again later.",
+			"serviceError": "OpenAI Codex service error. Please try again later.",
+			"genericError": "Codex API error ({{status}})",
+			"noResponseBody": "Codex API error: No response body",
+			"connectionFailed": "Failed to connect to Codex API: {{message}}",
+			"unexpectedConnectionError": "Unexpected error connecting to Codex API",
+			"apiError": "Codex API error: {{message}}",
+			"responseFailed": "Response failed: {{message}}",
+			"streamProcessingError": "Error processing response stream: {{message}}",
+			"unexpectedStreamError": "Unexpected error processing response stream",
+			"completionError": "OpenAI Codex completion error: {{message}}"
+		}
 	},
 	"warnings": {
 		"no_terminal_content": "沒有選擇終端機內容",

+ 740 - 0
src/integrations/openai-codex/oauth.ts

@@ -0,0 +1,740 @@
+import * as crypto from "crypto"
+import * as http from "http"
+import { URL } from "url"
+import type { ExtensionContext } from "vscode"
+import { z } from "zod"
+
+/**
+ * OpenAI Codex OAuth Configuration
+ *
+ * Based on the OpenAI Codex OAuth implementation guide:
+ * - ISSUER: https://auth.openai.com
+ * - Authorization endpoint: https://auth.openai.com/oauth/authorize
+ * - Token endpoint: https://auth.openai.com/oauth/token
+ * - Fixed callback port: 1455
+ * - Codex-specific params: codex_cli_simplified_flow=true, originator=roo-code
+ */
+export const OPENAI_CODEX_OAUTH_CONFIG = {
+	authorizationEndpoint: "https://auth.openai.com/oauth/authorize",
+	tokenEndpoint: "https://auth.openai.com/oauth/token",
+	clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
+	redirectUri: "http://localhost:1455/auth/callback",
+	scopes: "openid profile email offline_access",
+	callbackPort: 1455,
+} as const
+
+// Token storage key
+const OPENAI_CODEX_CREDENTIALS_KEY = "openai-codex-oauth-credentials"
+
+// Credentials schema
+const openAiCodexCredentialsSchema = z.object({
+	type: z.literal("openai-codex"),
+	access_token: z.string().min(1),
+	refresh_token: z.string().min(1),
+	// expires is in milliseconds since epoch
+	expires: z.number(),
+	email: z.string().optional(),
+	// ChatGPT account ID extracted from JWT claims (for ChatGPT-Account-Id header)
+	accountId: z.string().optional(),
+})
+
+export type OpenAiCodexCredentials = z.infer<typeof openAiCodexCredentialsSchema>
+
+// Token response schema from OpenAI
+const tokenResponseSchema = z.object({
+	access_token: z.string(),
+	refresh_token: z.string().min(1).optional(),
+	id_token: z.string().optional(),
+	expires_in: z.number(),
+	email: z.string().optional(),
+	token_type: z.string().optional(),
+})
+
+/**
+ * JWT claims structure for extracting ChatGPT account ID
+ */
+interface IdTokenClaims {
+	chatgpt_account_id?: string
+	organizations?: Array<{ id: string }>
+	email?: string
+	"https://api.openai.com/auth"?: {
+		chatgpt_account_id?: string
+	}
+}
+
+/**
+ * Parse JWT claims from a token
+ * Returns undefined if the token is invalid or cannot be parsed
+ */
+function parseJwtClaims(token: string): IdTokenClaims | undefined {
+	const parts = token.split(".")
+	if (parts.length !== 3) return undefined
+	try {
+		// Use base64url decoding (Node.js Buffer handles this)
+		const payload = Buffer.from(parts[1], "base64url").toString("utf-8")
+		return JSON.parse(payload) as IdTokenClaims
+	} catch {
+		return undefined
+	}
+}
+
+/**
+ * Extract ChatGPT account ID from JWT claims
+ * Checks multiple locations:
+ * 1. Root-level chatgpt_account_id
+ * 2. Nested under https://api.openai.com/auth
+ * 3. First organization ID
+ */
+function extractAccountIdFromClaims(claims: IdTokenClaims): string | undefined {
+	return (
+		claims.chatgpt_account_id ||
+		claims["https://api.openai.com/auth"]?.chatgpt_account_id ||
+		claims.organizations?.[0]?.id
+	)
+}
+
+/**
+ * Extract ChatGPT account ID from token response
+ * Tries id_token first, then access_token
+ */
+function extractAccountId(tokens: { id_token?: string; access_token: string }): string | undefined {
+	// Try id_token first (more reliable source)
+	if (tokens.id_token) {
+		const claims = parseJwtClaims(tokens.id_token)
+		const accountId = claims && extractAccountIdFromClaims(claims)
+		if (accountId) return accountId
+	}
+	// Fall back to access_token
+	if (tokens.access_token) {
+		const claims = parseJwtClaims(tokens.access_token)
+		return claims ? extractAccountIdFromClaims(claims) : undefined
+	}
+	return undefined
+}
+
+class OpenAiCodexOAuthTokenError extends Error {
+	public readonly status?: number
+	public readonly errorCode?: string
+
+	constructor(message: string, opts?: { status?: number; errorCode?: string }) {
+		super(message)
+		this.name = "OpenAiCodexOAuthTokenError"
+		this.status = opts?.status
+		this.errorCode = opts?.errorCode
+	}
+
+	public isLikelyInvalidGrant(): boolean {
+		if (this.errorCode && /invalid_grant/i.test(this.errorCode)) {
+			return true
+		}
+		if (this.status === 400 || this.status === 401 || this.status === 403) {
+			return /invalid_grant|revoked|expired|invalid refresh/i.test(this.message)
+		}
+		return false
+	}
+}
+
+function parseOAuthErrorDetails(errorText: string): { errorCode?: string; errorMessage?: string } {
+	try {
+		const json: unknown = JSON.parse(errorText)
+		if (!json || typeof json !== "object") {
+			return {}
+		}
+
+		const obj = json as Record<string, unknown>
+		const errorField = obj.error
+
+		const errorCode: string | undefined =
+			typeof errorField === "string"
+				? errorField
+				: errorField &&
+					  typeof errorField === "object" &&
+					  typeof (errorField as Record<string, unknown>).type === "string"
+					? ((errorField as Record<string, unknown>).type as string)
+					: undefined
+
+		const errorDescription = obj.error_description
+		const errorMessageFromError =
+			errorField && typeof errorField === "object" ? (errorField as Record<string, unknown>).message : undefined
+
+		const errorMessage: string | undefined =
+			typeof errorDescription === "string"
+				? errorDescription
+				: typeof errorMessageFromError === "string"
+					? errorMessageFromError
+					: typeof obj.message === "string"
+						? obj.message
+						: undefined
+
+		return { errorCode, errorMessage }
+	} catch {
+		return {}
+	}
+}
+
+/**
+ * Generates a cryptographically random PKCE code verifier
+ * Must be 43-128 characters long using unreserved characters
+ */
+export function generateCodeVerifier(): string {
+	const buffer = crypto.randomBytes(32)
+	return buffer.toString("base64url")
+}
+
+/**
+ * Generates the PKCE code challenge from the verifier using S256 method
+ */
+export function generateCodeChallenge(verifier: string): string {
+	const hash = crypto.createHash("sha256").update(verifier).digest()
+	return hash.toString("base64url")
+}
+
+/**
+ * Generates a random state parameter for CSRF protection
+ */
+export function generateState(): string {
+	return crypto.randomBytes(16).toString("hex")
+}
+
+/**
+ * Builds the authorization URL for OpenAI Codex OAuth flow
+ * Includes Codex-specific parameters per the implementation guide
+ */
+export function buildAuthorizationUrl(codeChallenge: string, state: string): string {
+	const params = new URLSearchParams({
+		client_id: OPENAI_CODEX_OAUTH_CONFIG.clientId,
+		redirect_uri: OPENAI_CODEX_OAUTH_CONFIG.redirectUri,
+		scope: OPENAI_CODEX_OAUTH_CONFIG.scopes,
+		code_challenge: codeChallenge,
+		code_challenge_method: "S256",
+		response_type: "code",
+		state,
+		// Codex-specific parameters
+		codex_cli_simplified_flow: "true",
+		originator: "roo-code",
+	})
+
+	return `${OPENAI_CODEX_OAUTH_CONFIG.authorizationEndpoint}?${params.toString()}`
+}
+
+/**
+ * Exchanges the authorization code for tokens
+ * Important: Uses application/x-www-form-urlencoded (not JSON)
+ * Important: state must NOT be included in token exchange body
+ */
+export async function exchangeCodeForTokens(code: string, codeVerifier: string): Promise<OpenAiCodexCredentials> {
+	// Per the implementation guide: use application/x-www-form-urlencoded
+	// and do NOT include state in the body (OpenAI returns error if included)
+	const body = new URLSearchParams({
+		grant_type: "authorization_code",
+		client_id: OPENAI_CODEX_OAUTH_CONFIG.clientId,
+		code,
+		redirect_uri: OPENAI_CODEX_OAUTH_CONFIG.redirectUri,
+		code_verifier: codeVerifier,
+	})
+
+	const response = await fetch(OPENAI_CODEX_OAUTH_CONFIG.tokenEndpoint, {
+		method: "POST",
+		headers: {
+			"Content-Type": "application/x-www-form-urlencoded",
+		},
+		body: body.toString(),
+		signal: AbortSignal.timeout(30000),
+	})
+
+	if (!response.ok) {
+		const errorText = await response.text()
+		throw new Error(`Token exchange failed: ${response.status} ${response.statusText} - ${errorText}`)
+	}
+
+	const data = await response.json()
+	const tokenResponse = tokenResponseSchema.parse(data)
+
+	if (!tokenResponse.refresh_token) {
+		throw new Error("Token exchange did not return a refresh_token")
+	}
+
+	// Per the implementation guide: expires is in milliseconds since epoch
+	const expiresAt = Date.now() + tokenResponse.expires_in * 1000
+
+	// Extract ChatGPT account ID from JWT claims
+	const accountId = extractAccountId({
+		id_token: tokenResponse.id_token,
+		access_token: tokenResponse.access_token,
+	})
+
+	return {
+		type: "openai-codex",
+		access_token: tokenResponse.access_token,
+		refresh_token: tokenResponse.refresh_token,
+		expires: expiresAt,
+		email: tokenResponse.email,
+		accountId,
+	}
+}
+
+/**
+ * Refreshes the access token using the refresh token
+ * Uses application/x-www-form-urlencoded (not JSON)
+ */
+export async function refreshAccessToken(credentials: OpenAiCodexCredentials): Promise<OpenAiCodexCredentials> {
+	const body = new URLSearchParams({
+		grant_type: "refresh_token",
+		client_id: OPENAI_CODEX_OAUTH_CONFIG.clientId,
+		refresh_token: credentials.refresh_token,
+	})
+
+	const response = await fetch(OPENAI_CODEX_OAUTH_CONFIG.tokenEndpoint, {
+		method: "POST",
+		headers: {
+			"Content-Type": "application/x-www-form-urlencoded",
+		},
+		body: body.toString(),
+		signal: AbortSignal.timeout(30000),
+	})
+
+	if (!response.ok) {
+		const errorText = await response.text()
+		const { errorCode, errorMessage } = parseOAuthErrorDetails(errorText)
+		const details = errorMessage ? errorMessage : errorText
+		throw new OpenAiCodexOAuthTokenError(
+			`Token refresh failed: ${response.status} ${response.statusText}${details ? ` - ${details}` : ""}`,
+			{ status: response.status, errorCode },
+		)
+	}
+
+	const data = await response.json()
+	const tokenResponse = tokenResponseSchema.parse(data)
+
+	// Per the implementation guide: expires is in milliseconds since epoch
+	const expiresAt = Date.now() + tokenResponse.expires_in * 1000
+
+	// Extract new account ID from refreshed tokens, or preserve existing one
+	const newAccountId = extractAccountId({
+		id_token: tokenResponse.id_token,
+		access_token: tokenResponse.access_token,
+	})
+
+	return {
+		type: "openai-codex",
+		access_token: tokenResponse.access_token,
+		refresh_token: tokenResponse.refresh_token ?? credentials.refresh_token,
+		expires: expiresAt,
+		email: tokenResponse.email ?? credentials.email,
+		// Prefer newly extracted accountId, fall back to existing
+		accountId: newAccountId ?? credentials.accountId,
+	}
+}
+
+/**
+ * Checks if the credentials are expired (with 5 minute buffer)
+ * Per the implementation guide: expires is in milliseconds since epoch
+ */
+export function isTokenExpired(credentials: OpenAiCodexCredentials): boolean {
+	const bufferMs = 5 * 60 * 1000 // 5 minutes buffer
+	return Date.now() >= credentials.expires - bufferMs
+}
+
+/**
+ * OpenAiCodexOAuthManager - Handles OAuth flow and token management
+ */
+export class OpenAiCodexOAuthManager {
+	private context: ExtensionContext | null = null
+	private credentials: OpenAiCodexCredentials | null = null
+	private logFn: ((message: string) => void) | null = null
+	private refreshPromise: Promise<OpenAiCodexCredentials> | null = null
+	private pendingAuth: {
+		codeVerifier: string
+		state: string
+		server?: http.Server
+	} | null = null
+
+	private log(message: string): void {
+		if (this.logFn) {
+			this.logFn(message)
+		} else {
+			console.log(message)
+		}
+	}
+
+	private logError(message: string, error?: unknown): void {
+		const details = error instanceof Error ? error.message : error !== undefined ? String(error) : undefined
+		const full = details ? `${message} ${details}` : message
+		this.log(full)
+		console.error(full)
+	}
+
+	/**
+	 * Initialize the OAuth manager with VS Code extension context
+	 */
+	initialize(context: ExtensionContext, logFn?: (message: string) => void): void {
+		this.context = context
+		this.logFn = logFn ?? null
+	}
+
+	/**
+	 * Force a refresh using the stored refresh token even if the access token is not expired.
+	 * Useful when the server invalidates an access token early.
+	 */
+	async forceRefreshAccessToken(): Promise<string | null> {
+		if (!this.credentials) {
+			await this.loadCredentials()
+		}
+
+		if (!this.credentials) {
+			return null
+		}
+
+		try {
+			// De-dupe concurrent refreshes
+			if (!this.refreshPromise) {
+				const prevRefreshToken = this.credentials.refresh_token
+				this.log(`[openai-codex-oauth] Forcing token refresh (expires=${this.credentials.expires})...`)
+				this.refreshPromise = refreshAccessToken(this.credentials).then((newCreds) => {
+					const rotated = newCreds.refresh_token !== prevRefreshToken
+					this.log(
+						`[openai-codex-oauth] Forced refresh response received (expires_in≈${Math.round(
+							(newCreds.expires - Date.now()) / 1000,
+						)}s, refresh_token_rotated=${rotated})`,
+					)
+					return newCreds
+				})
+			}
+
+			const newCredentials = await this.refreshPromise
+			this.refreshPromise = null
+			await this.saveCredentials(newCredentials)
+			this.log(`[openai-codex-oauth] Forced token persisted (expires=${newCredentials.expires})`)
+			return newCredentials.access_token
+		} catch (error) {
+			this.refreshPromise = null
+			this.logError("[openai-codex-oauth] Failed to force refresh token:", error)
+			if (error instanceof OpenAiCodexOAuthTokenError && error.isLikelyInvalidGrant()) {
+				this.log("[openai-codex-oauth] Refresh token appears invalid; clearing stored credentials")
+				await this.clearCredentials()
+			}
+			return null
+		}
+	}
+
+	/**
+	 * Load credentials from storage
+	 */
+	async loadCredentials(): Promise<OpenAiCodexCredentials | null> {
+		if (!this.context) {
+			return null
+		}
+
+		try {
+			const credentialsJson = await this.context.secrets.get(OPENAI_CODEX_CREDENTIALS_KEY)
+			if (!credentialsJson) {
+				return null
+			}
+
+			const parsed = JSON.parse(credentialsJson)
+			this.credentials = openAiCodexCredentialsSchema.parse(parsed)
+			return this.credentials
+		} catch (error) {
+			this.logError("[openai-codex-oauth] Failed to load credentials:", error)
+			return null
+		}
+	}
+
+	/**
+	 * Save credentials to storage
+	 */
+	async saveCredentials(credentials: OpenAiCodexCredentials): Promise<void> {
+		if (!this.context) {
+			throw new Error("OAuth manager not initialized")
+		}
+
+		await this.context.secrets.store(OPENAI_CODEX_CREDENTIALS_KEY, JSON.stringify(credentials))
+		this.credentials = credentials
+	}
+
+	/**
+	 * Clear credentials from storage
+	 */
+	async clearCredentials(): Promise<void> {
+		if (!this.context) {
+			return
+		}
+
+		await this.context.secrets.delete(OPENAI_CODEX_CREDENTIALS_KEY)
+		this.credentials = null
+	}
+
+	/**
+	 * Get a valid access token, refreshing if necessary
+	 */
+	async getAccessToken(): Promise<string | null> {
+		// Try to load credentials if not already loaded
+		if (!this.credentials) {
+			await this.loadCredentials()
+		}
+
+		if (!this.credentials) {
+			return null
+		}
+
+		// Check if token is expired and refresh if needed
+		if (isTokenExpired(this.credentials)) {
+			try {
+				// De-dupe concurrent refreshes
+				if (!this.refreshPromise) {
+					this.log(
+						`[openai-codex-oauth] Access token expired (expires=${this.credentials.expires}). Refreshing...`,
+					)
+					const prevRefreshToken = this.credentials.refresh_token
+					this.refreshPromise = refreshAccessToken(this.credentials).then((newCreds) => {
+						const rotated = newCreds.refresh_token !== prevRefreshToken
+						this.log(
+							`[openai-codex-oauth] Refresh response received (expires_in≈${Math.round(
+								(newCreds.expires - Date.now()) / 1000,
+							)}s, refresh_token_rotated=${rotated})`,
+						)
+						return newCreds
+					})
+				}
+
+				const newCredentials = await this.refreshPromise
+				this.refreshPromise = null
+				await this.saveCredentials(newCredentials)
+				this.log(`[openai-codex-oauth] Token persisted (expires=${newCredentials.expires})`)
+			} catch (error) {
+				this.refreshPromise = null
+				this.logError("[openai-codex-oauth] Failed to refresh token:", error)
+
+				// Only clear secrets when the refresh token is clearly invalid/revoked.
+				if (error instanceof OpenAiCodexOAuthTokenError && error.isLikelyInvalidGrant()) {
+					this.log("[openai-codex-oauth] Refresh token appears invalid; clearing stored credentials")
+					await this.clearCredentials()
+				}
+				return null
+			}
+		}
+
+		return this.credentials.access_token
+	}
+
+	/**
+	 * Get the user's email from credentials
+	 */
+	async getEmail(): Promise<string | null> {
+		if (!this.credentials) {
+			await this.loadCredentials()
+		}
+		return this.credentials?.email || null
+	}
+
+	/**
+	 * Get the ChatGPT account ID from credentials
+	 * Used for the ChatGPT-Account-Id header required by the Codex API
+	 */
+	async getAccountId(): Promise<string | null> {
+		if (!this.credentials) {
+			await this.loadCredentials()
+		}
+		return this.credentials?.accountId || null
+	}
+
+	/**
+	 * Check if the user is authenticated
+	 */
+	async isAuthenticated(): Promise<boolean> {
+		const token = await this.getAccessToken()
+		return token !== null
+	}
+
+	/**
+	 * Start the OAuth authorization flow
+	 * Returns the authorization URL to open in browser
+	 */
+	startAuthorizationFlow(): string {
+		// Cancel any existing authorization flow before starting a new one
+		this.cancelAuthorizationFlow()
+
+		const codeVerifier = generateCodeVerifier()
+		const codeChallenge = generateCodeChallenge(codeVerifier)
+		const state = generateState()
+
+		this.pendingAuth = {
+			codeVerifier,
+			state,
+		}
+
+		return buildAuthorizationUrl(codeChallenge, state)
+	}
+
+	/**
+	 * Start a local server to receive the OAuth callback
+	 * Returns a promise that resolves when authentication is complete
+	 */
+	async waitForCallback(): Promise<OpenAiCodexCredentials> {
+		if (!this.pendingAuth) {
+			throw new Error("No pending authorization flow")
+		}
+
+		// Close any existing server before starting a new one
+		if (this.pendingAuth.server) {
+			try {
+				this.pendingAuth.server.close()
+			} catch {
+				// Ignore errors when closing
+			}
+			this.pendingAuth.server = undefined
+		}
+
+		return new Promise((resolve, reject) => {
+			const server = http.createServer(async (req, res) => {
+				try {
+					const url = new URL(req.url || "", `http://localhost:${OPENAI_CODEX_OAUTH_CONFIG.callbackPort}`)
+
+					if (url.pathname !== "/auth/callback") {
+						res.writeHead(404)
+						res.end("Not Found")
+						return
+					}
+
+					const code = url.searchParams.get("code")
+					const state = url.searchParams.get("state")
+					const error = url.searchParams.get("error")
+
+					if (error) {
+						res.writeHead(400)
+						res.end(`Authentication failed: ${error}`)
+						reject(new Error(`OAuth error: ${error}`))
+						server.close()
+						return
+					}
+
+					if (!code || !state) {
+						res.writeHead(400)
+						res.end("Missing code or state parameter")
+						reject(new Error("Missing code or state parameter"))
+						server.close()
+						return
+					}
+
+					if (state !== this.pendingAuth?.state) {
+						res.writeHead(400)
+						res.end("State mismatch - possible CSRF attack")
+						reject(new Error("State mismatch"))
+						server.close()
+						return
+					}
+
+					try {
+						// Note: state is validated above but not passed to exchangeCodeForTokens
+						// per the implementation guide (OpenAI rejects it)
+						const credentials = await exchangeCodeForTokens(code, this.pendingAuth.codeVerifier)
+
+						await this.saveCredentials(credentials)
+
+						res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" })
+						res.end(`<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Authentication Successful</title>
+<style>
+  body {
+    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 100vh;
+    margin: 0;
+    background: linear-gradient(135deg, #10a37f 0%, #0d8f6f 100%);
+    color: white;
+  }
+  .container {
+    text-align: center;
+    padding: 2rem;
+  }
+  h1 { font-size: 2rem; margin-bottom: 1rem; }
+  p { opacity: 0.9; }
+</style>
+</head>
+<body>
+<div class="container">
+<h1>&#10003; Authentication Successful</h1>
+<p>You can close this window and return to VS Code.</p>
+</div>
+<script>setTimeout(() => window.close(), 3000);</script>
+</body>
+</html>`)
+
+						this.pendingAuth = null
+						server.close()
+						resolve(credentials)
+					} catch (exchangeError) {
+						res.writeHead(500)
+						res.end(`Token exchange failed: ${exchangeError}`)
+						reject(exchangeError)
+						server.close()
+					}
+				} catch (err) {
+					res.writeHead(500)
+					res.end("Internal server error")
+					reject(err)
+					server.close()
+				}
+			})
+
+			server.on("error", (err: NodeJS.ErrnoException) => {
+				this.pendingAuth = null
+				if (err.code === "EADDRINUSE") {
+					reject(
+						new Error(
+							`Port ${OPENAI_CODEX_OAUTH_CONFIG.callbackPort} is already in use. ` +
+								`Please close any other applications using this port and try again.`,
+						),
+					)
+				} else {
+					reject(err)
+				}
+			})
+
+			// Set a timeout for the callback
+			const timeout = setTimeout(
+				() => {
+					server.close()
+					reject(new Error("Authentication timed out"))
+				},
+				5 * 60 * 1000,
+			) // 5 minutes
+
+			server.listen(OPENAI_CODEX_OAUTH_CONFIG.callbackPort, () => {
+				if (this.pendingAuth) {
+					this.pendingAuth.server = server
+				}
+			})
+
+			// Clear timeout when server closes
+			server.on("close", () => {
+				clearTimeout(timeout)
+			})
+		})
+	}
+
+	/**
+	 * Cancel any pending authorization flow
+	 */
+	cancelAuthorizationFlow(): void {
+		if (this.pendingAuth?.server) {
+			this.pendingAuth.server.close()
+		}
+		this.pendingAuth = null
+	}
+
+	/**
+	 * Get the current credentials (for display purposes)
+	 */
+	getCredentials(): OpenAiCodexCredentials | null {
+		return this.credentials
+	}
+}
+
+// Singleton instance
+export const openAiCodexOAuthManager = new OpenAiCodexOAuthManager()

+ 75 - 60
webview-ui/src/components/settings/ApiOptions.tsx

@@ -13,6 +13,7 @@ import {
 	unboundDefaultModelId,
 	litellmDefaultModelId,
 	openAiNativeDefaultModelId,
+	openAiCodexDefaultModelId,
 	anthropicDefaultModelId,
 	doubaoDefaultModelId,
 	claudeCodeDefaultModelId,
@@ -83,6 +84,7 @@ import {
 	Ollama,
 	OpenAI,
 	OpenAICompatible,
+	OpenAICodex,
 	OpenRouter,
 	QwenCode,
 	Requesty,
@@ -138,7 +140,8 @@ const ApiOptions = ({
 	setErrorMessage,
 }: ApiOptionsProps) => {
 	const { t } = useAppTranslation()
-	const { organizationAllowList, cloudIsAuthenticated, claudeCodeIsAuthenticated } = useExtensionState()
+	const { organizationAllowList, cloudIsAuthenticated, claudeCodeIsAuthenticated, openAiCodexIsAuthenticated } =
+		useExtensionState()
 
 	const [customHeaders, setCustomHeaders] = useState<[string, string][]>(() => {
 		const headers = apiConfiguration?.openAiHeaders || {}
@@ -342,6 +345,7 @@ const ApiOptions = ({
 				anthropic: { field: "apiModelId", default: anthropicDefaultModelId },
 				cerebras: { field: "apiModelId", default: cerebrasDefaultModelId },
 				"claude-code": { field: "apiModelId", default: claudeCodeDefaultModelId },
+				"openai-codex": { field: "apiModelId", default: openAiCodexDefaultModelId },
 				"qwen-code": { field: "apiModelId", default: qwenCodeDefaultModelId },
 				"openai-native": { field: "apiModelId", default: openAiNativeDefaultModelId },
 				gemini: { field: "apiModelId", default: geminiDefaultModelId },
@@ -563,6 +567,15 @@ const ApiOptions = ({
 				/>
 			)}
 
+			{selectedProvider === "openai-codex" && (
+				<OpenAICodex
+					apiConfiguration={apiConfiguration}
+					setApiConfigurationField={setApiConfigurationField}
+					simplifySettings={fromWelcomeView}
+					openAiCodexIsAuthenticated={openAiCodexIsAuthenticated}
+				/>
+			)}
+
 			{selectedProvider === "openai-native" && (
 				<OpenAI
 					apiConfiguration={apiConfiguration}
@@ -761,67 +774,69 @@ const ApiOptions = ({
 				<Featherless apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
 			)}
 
-			{/* Skip generic model picker for claude-code since it has its own in ClaudeCode.tsx */}
-			{selectedProviderModels.length > 0 && selectedProvider !== "claude-code" && (
-				<>
-					<div>
-						<label className="block font-medium mb-1">{t("settings:providers.model")}</label>
-						<Select
-							value={selectedModelId === "custom-arn" ? "custom-arn" : selectedModelId}
-							onValueChange={(value) => {
-								setApiConfigurationField("apiModelId", value)
-
-								// Clear custom ARN if not using custom ARN option.
-								if (value !== "custom-arn" && selectedProvider === "bedrock") {
-									setApiConfigurationField("awsCustomArn", "")
-								}
-
-								// Clear reasoning effort when switching models to allow the new model's default to take effect
-								// This is especially important for GPT-5 models which default to "medium"
-								if (selectedProvider === "openai-native") {
-									setApiConfigurationField("reasoningEffort", undefined)
-								}
-							}}>
-							<SelectTrigger className="w-full">
-								<SelectValue placeholder={t("settings:common.select")} />
-							</SelectTrigger>
-							<SelectContent>
-								{selectedProviderModels.map((option) => (
-									<SelectItem key={option.value} value={option.value}>
-										{option.label}
-									</SelectItem>
-								))}
-								{selectedProvider === "bedrock" && (
-									<SelectItem value="custom-arn">{t("settings:labels.useCustomArn")}</SelectItem>
-								)}
-							</SelectContent>
-						</Select>
-					</div>
-
-					{/* Show error if a deprecated model is selected */}
-					{selectedModelInfo?.deprecated && (
-						<ApiErrorMessage errorMessage={t("settings:validation.modelDeprecated")} />
-					)}
+			{/* Skip generic model picker for claude-code/openai-codex since they have their own model pickers */}
+			{selectedProviderModels.length > 0 &&
+				selectedProvider !== "claude-code" &&
+				selectedProvider !== "openai-codex" && (
+					<>
+						<div>
+							<label className="block font-medium mb-1">{t("settings:providers.model")}</label>
+							<Select
+								value={selectedModelId === "custom-arn" ? "custom-arn" : selectedModelId}
+								onValueChange={(value) => {
+									setApiConfigurationField("apiModelId", value)
+
+									// Clear custom ARN if not using custom ARN option.
+									if (value !== "custom-arn" && selectedProvider === "bedrock") {
+										setApiConfigurationField("awsCustomArn", "")
+									}
+
+									// Clear reasoning effort when switching models to allow the new model's default to take effect
+									// This is especially important for GPT-5 models which default to "medium"
+									if (selectedProvider === "openai-native") {
+										setApiConfigurationField("reasoningEffort", undefined)
+									}
+								}}>
+								<SelectTrigger className="w-full">
+									<SelectValue placeholder={t("settings:common.select")} />
+								</SelectTrigger>
+								<SelectContent>
+									{selectedProviderModels.map((option) => (
+										<SelectItem key={option.value} value={option.value}>
+											{option.label}
+										</SelectItem>
+									))}
+									{selectedProvider === "bedrock" && (
+										<SelectItem value="custom-arn">{t("settings:labels.useCustomArn")}</SelectItem>
+									)}
+								</SelectContent>
+							</Select>
+						</div>
+
+						{/* Show error if a deprecated model is selected */}
+						{selectedModelInfo?.deprecated && (
+							<ApiErrorMessage errorMessage={t("settings:validation.modelDeprecated")} />
+						)}
 
-					{selectedProvider === "bedrock" && selectedModelId === "custom-arn" && (
-						<BedrockCustomArn
-							apiConfiguration={apiConfiguration}
-							setApiConfigurationField={setApiConfigurationField}
-						/>
-					)}
+						{selectedProvider === "bedrock" && selectedModelId === "custom-arn" && (
+							<BedrockCustomArn
+								apiConfiguration={apiConfiguration}
+								setApiConfigurationField={setApiConfigurationField}
+							/>
+						)}
 
-					{/* Only show model info if not deprecated */}
-					{!selectedModelInfo?.deprecated && (
-						<ModelInfoView
-							apiProvider={selectedProvider}
-							selectedModelId={selectedModelId}
-							modelInfo={selectedModelInfo}
-							isDescriptionExpanded={isDescriptionExpanded}
-							setIsDescriptionExpanded={setIsDescriptionExpanded}
-						/>
-					)}
-				</>
-			)}
+						{/* Only show model info if not deprecated */}
+						{!selectedModelInfo?.deprecated && (
+							<ModelInfoView
+								apiProvider={selectedProvider}
+								selectedModelId={selectedModelId}
+								modelInfo={selectedModelInfo}
+								isDescriptionExpanded={isDescriptionExpanded}
+								setIsDescriptionExpanded={setIsDescriptionExpanded}
+							/>
+						)}
+					</>
+				)}
 
 			{!fromWelcomeView && (
 				<ThinkingBudget

+ 3 - 0
webview-ui/src/components/settings/constants.ts

@@ -10,6 +10,7 @@ import {
 	geminiModels,
 	mistralModels,
 	openAiNativeModels,
+	openAiCodexModels,
 	qwenCodeModels,
 	vertexModels,
 	xaiModels,
@@ -34,6 +35,7 @@ export const MODELS_BY_PROVIDER: Partial<Record<ProviderName, Record<string, Mod
 	gemini: geminiModels,
 	mistral: mistralModels,
 	"openai-native": openAiNativeModels,
+	"openai-codex": openAiCodexModels,
 	"qwen-code": qwenCodeModels,
 	vertex: vertexModels,
 	xai: xaiModels,
@@ -57,6 +59,7 @@ export const PROVIDERS = [
 	{ value: "deepseek", label: "DeepSeek", proxy: false },
 	{ value: "moonshot", label: "Moonshot", proxy: false },
 	{ value: "openai-native", label: "OpenAI", proxy: false },
+	{ value: "openai-codex", label: "OpenAI - ChatGPT Plus/Pro", proxy: false },
 	{ value: "openai", label: "OpenAI Compatible", proxy: true },
 	{ value: "qwen-code", label: "Qwen Code", proxy: false },
 	{ value: "vertex", label: "GCP Vertex AI", proxy: false },

+ 67 - 0
webview-ui/src/components/settings/providers/OpenAICodex.tsx

@@ -0,0 +1,67 @@
+import React from "react"
+
+import { type ProviderSettings, openAiCodexDefaultModelId, openAiCodexModels } from "@roo-code/types"
+
+import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { Button } from "@src/components/ui"
+import { vscode } from "@src/utils/vscode"
+
+import { ModelPicker } from "../ModelPicker"
+
+interface OpenAICodexProps {
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
+	simplifySettings?: boolean
+	openAiCodexIsAuthenticated?: boolean
+}
+
+export const OpenAICodex: React.FC<OpenAICodexProps> = ({
+	apiConfiguration,
+	setApiConfigurationField,
+	simplifySettings,
+	openAiCodexIsAuthenticated = false,
+}) => {
+	const { t } = useAppTranslation()
+
+	return (
+		<div className="flex flex-col gap-4">
+			{/* Authentication Section */}
+			<div className="flex flex-col gap-2">
+				{openAiCodexIsAuthenticated ? (
+					<div className="flex justify-end">
+						<Button
+							variant="secondary"
+							size="sm"
+							onClick={() => vscode.postMessage({ type: "openAiCodexSignOut" })}>
+							{t("settings:providers.openAiCodex.signOutButton", {
+								defaultValue: "Sign Out",
+							})}
+						</Button>
+					</div>
+				) : (
+					<Button
+						variant="primary"
+						onClick={() => vscode.postMessage({ type: "openAiCodexSignIn" })}
+						className="w-fit">
+						{t("settings:providers.openAiCodex.signInButton", {
+							defaultValue: "Sign in to OpenAI Codex",
+						})}
+					</Button>
+				)}
+			</div>
+
+			{/* Model Picker */}
+			<ModelPicker
+				apiConfiguration={apiConfiguration}
+				setApiConfigurationField={setApiConfigurationField}
+				defaultModelId={openAiCodexDefaultModelId}
+				models={openAiCodexModels}
+				modelIdKey="apiModelId"
+				serviceName="OpenAI - ChatGPT Plus/Pro"
+				serviceUrl="https://chatgpt.com"
+				simplifySettings={simplifySettings}
+				hidePricing
+			/>
+		</div>
+	)
+}

+ 1 - 0
webview-ui/src/components/settings/providers/index.ts

@@ -14,6 +14,7 @@ export { Mistral } from "./Mistral"
 export { Moonshot } from "./Moonshot"
 export { Ollama } from "./Ollama"
 export { OpenAI } from "./OpenAI"
+export { OpenAICodex } from "./OpenAICodex"
 export { OpenAICompatible } from "./OpenAICompatible"
 export { OpenRouter } from "./OpenRouter"
 export { QwenCode } from "./QwenCode"

+ 7 - 1
webview-ui/src/components/ui/hooks/useSelectedModel.ts

@@ -21,6 +21,7 @@ import {
 	vscodeLlmDefaultModelId,
 	claudeCodeModels,
 	normalizeClaudeCodeModelId,
+	openAiCodexModels,
 	sambaNovaModels,
 	doubaoModels,
 	internationalZAiModels,
@@ -381,6 +382,11 @@ function getSelectedModel({
 			const info = qwenCodeModels[id as keyof typeof qwenCodeModels]
 			return { id, info }
 		}
+		case "openai-codex": {
+			const id = apiConfiguration.apiModelId ?? defaultModelId
+			const info = openAiCodexModels[id as keyof typeof openAiCodexModels]
+			return { id, info }
+		}
 		case "vercel-ai-gateway": {
 			const id = getValidatedModelId(
 				apiConfiguration.vercelAiGatewayModelId,
@@ -393,7 +399,7 @@ function getSelectedModel({
 		// case "anthropic":
 		// case "fake-ai":
 		default: {
-			provider satisfies "anthropic" | "gemini-cli" | "qwen-code" | "fake-ai"
+			provider satisfies "anthropic" | "gemini-cli" | "fake-ai"
 			const id = apiConfiguration.apiModelId ?? defaultModelId
 			const baseInfo = anthropicModels[id as keyof typeof anthropicModels]