From 128c6482a992d36ac2532c12efcfa508db3ea1c1 Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Wed, 14 Jan 2026 17:29:33 -0700 Subject: [PATCH 01/13] feat: add OpenAI Codex provider with OAuth subscription authentication Add support for OpenAI Codex models (GPT-5.x) via ChatGPT Plus/Pro subscription: - Add openai-codex provider type and model definitions - Implement OAuth authentication flow with token management - Create OpenAiCodexHandler routing to Codex backend API - Add settings UI component for sign-in/sign-out - Include tests for native tool call handling Enables subscription-based access to GPT-5.1-codex-max, GPT-5.2-codex, GPT-5.1-codex-mini, and GPT-5.2 models without per-token costs. --- packages/types/src/provider-settings.ts | 14 + packages/types/src/providers/index.ts | 4 + packages/types/src/providers/openai-codex.ts | 92 ++ packages/types/src/vscode-extension-host.ts | 3 + src/api/index.ts | 3 + .../openai-codex-native-tool-calls.spec.ts | 101 ++ src/api/providers/index.ts | 1 + src/api/providers/openai-codex.ts | 1101 +++++++++++++++++ src/core/webview/ClineProvider.ts | 8 + src/core/webview/webviewMessageHandler.ts | 39 + src/extension.ts | 4 + src/integrations/openai-codex/oauth.ts | 741 +++++++++++ .../src/components/settings/ApiOptions.tsx | 135 +- .../src/components/settings/constants.ts | 3 + .../settings/providers/OpenAICodex.tsx | 67 + .../components/settings/providers/index.ts | 1 + .../components/ui/hooks/useSelectedModel.ts | 8 +- 17 files changed, 2264 insertions(+), 61 deletions(-) create mode 100644 packages/types/src/providers/openai-codex.ts create mode 100644 src/api/providers/__tests__/openai-codex-native-tool-calls.spec.ts create mode 100644 src/api/providers/openai-codex.ts create mode 100644 src/integrations/openai-codex/oauth.ts create mode 100644 webview-ui/src/components/settings/providers/OpenAICodex.tsx diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index d5880cd3b13..457252e7fe6 100644 --- a/packages/types/src/provider-settings.ts +++ b/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 = { 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", diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index a08d673e221..3c6741fcd84 100644 --- a/packages/types/src/providers/index.ts +++ b/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": diff --git a/packages/types/src/providers/openai-codex.ts b/packages/types/src/providers/openai-codex.ts new file mode 100644 index 00000000000..70171311023 --- /dev/null +++ b/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: ["none", "low", "medium", "high", "xhigh"], + reasoningEffort: "high", + // 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: ["none", "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 diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index c0bb8726d7d..bce6c993bc7 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/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" diff --git a/src/api/index.ts b/src/api/index.ts index 456a99f74ed..4dfe1e2ecb4 100644 --- a/src/api/index.ts +++ b/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": diff --git a/src/api/providers/__tests__/openai-codex-native-tool-calls.spec.ts b/src/api/providers/__tests__/openai-codex-native-tool-calls.spec.ts new file mode 100644 index 00000000000..c7c4a48fc3d --- /dev/null +++ b/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", + }) + }) +}) diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index fe9388962f0..1e0ae50c9d2 100644 --- a/src/api/providers/index.ts +++ b/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" diff --git a/src/api/providers/openai-codex.ts b/src/api/providers/openai-codex.ts new file mode 100644 index 00000000000..fa2658d9f0d --- /dev/null +++ b/src/api/providers/openai-codex.ts @@ -0,0 +1,1101 @@ +import * as crypto from "crypto" +import * as os from "os" +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 = require("../../../package.json").version ?? "unknown" + +export type OpenAiCodexModel = ReturnType + +/** + * 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([ + "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 unique session ID for this handler instance + // Format: ses_ + this.sessionId = `ses_${crypto.randomBytes(16).toString("hex")}` + } + + 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) + 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): 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 (per OpenCode implementation) + const accountId = await openAiCodexOAuthManager.getAccountId() + + // Build Codex-specific headers. Authorization is provided by the SDK apiKey. + const codexHeaders: Record = { + originator: "roocode", + session_id: 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 + + 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) + } + } 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): 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 (per OpenCode implementation) + const accountId = await openAiCodexOAuthManager.getAccountId() + + // Build headers with required Codex-specific fields + const headers: Record = { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + originator: "roocode", + 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 (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 = `OpenAI Codex API request failed (${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 = "Invalid request to Codex API. Please check your input parameters." + break + case 401: + errorMessage = "Authentication failed. Please re-authenticate with OpenAI Codex." + break + case 403: + errorMessage = "Access denied. Your ChatGPT subscription may not include Codex access." + break + case 404: + errorMessage = "Codex API endpoint not found." + break + case 429: + errorMessage = "Rate limit exceeded. Please try again later." + break + case 500: + case 502: + case 503: + errorMessage = "OpenAI Codex service error. Please try again later." + break + default: + errorMessage = `Codex API error (${response.status})` + } + + if (errorDetails) { + errorMessage += ` - ${errorDetails}` + } + + throw new Error(errorMessage) + } + + if (!response.body) { + throw new Error("Codex API error: No response body") + } + + 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(`Failed to connect to Codex API: ${error.message}`) + } + throw new Error(`Unexpected error connecting to Codex API`) + } + } + + private async *handleStreamResponse(body: ReadableStream, 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( + `Codex API error: ${parsed.error?.message || parsed.message || "Unknown error"}`, + ) + } + } else if (parsed.type === "response.failed") { + if (parsed.error || parsed.message) { + throw new Error( + `Response failed: ${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(`Error processing response stream: ${error.message}`) + } + throw new Error("Unexpected error processing response stream") + } 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 { + 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 = { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + originator: "roocode", + 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(`Codex API error (${response.status}): ${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(`OpenAI Codex completion error: ${error.message}`) + } + throw error + } finally { + this.abortController = undefined + } + } +} diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 64b044bb352..274ee60179d 100644 --- a/src/core/webview/ClineProvider.ts +++ b/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("debug", false), } } diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 5d3c9e01522..ddd97dffd50 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/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) { diff --git a/src/extension.ts b/src/extension.ts index 76f02af6de2..c12f223f954 100644 --- a/src/extension.ts +++ b/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("allowedCommands") || [] diff --git a/src/integrations/openai-codex/oauth.ts b/src/integrations/openai-codex/oauth.ts new file mode 100644 index 00000000000..02d4ff477a8 --- /dev/null +++ b/src/integrations/openai-codex/oauth.ts @@ -0,0 +1,741 @@ +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 OpenCode implementation guide (PR #7537): + * - 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=roocode + */ +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 (per OpenCode refactor) + 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 + +// 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 + * Based on OpenCode implementation + */ +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 per OpenCode implementation: + * 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 + const errorField = obj.error + + const errorCode: string | undefined = + typeof errorField === "string" + ? errorField + : errorField && + typeof errorField === "object" && + typeof (errorField as Record).type === "string" + ? ((errorField as Record).type as string) + : undefined + + const errorDescription = obj.error_description + const errorMessageFromError = + errorField && typeof errorField === "object" ? (errorField as Record).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 (per OpenCode implementation guide) + codex_cli_simplified_flow: "true", + originator: "roocode", + }) + + 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 { + // 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 (per OpenCode implementation) + 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 { + 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 | 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 { + 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 { + 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 { + 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 { + 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 { + // 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 { + 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 { + if (!this.credentials) { + await this.loadCredentials() + } + return this.credentials?.accountId || null + } + + /** + * Check if the user is authenticated + */ + async isAuthenticated(): Promise { + 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 { + 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(` + + + +Authentication Successful + + + +
+

✓ Authentication Successful

+

You can close this window and return to VS Code.

+
+ + +`) + + 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() diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 8e2c117e7c0..1012b73263f 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/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" && ( + + )} + {selectedProvider === "openai-native" && ( )} - {/* Skip generic model picker for claude-code since it has its own in ClaudeCode.tsx */} - {selectedProviderModels.length > 0 && selectedProvider !== "claude-code" && ( - <> -
- - -
- - {/* Show error if a deprecated model is selected */} - {selectedModelInfo?.deprecated && ( - - )} + {/* 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" && ( + <> +
+ + +
+ + {/* Show error if a deprecated model is selected */} + {selectedModelInfo?.deprecated && ( + + )} - {selectedProvider === "bedrock" && selectedModelId === "custom-arn" && ( - - )} + {selectedProvider === "bedrock" && selectedModelId === "custom-arn" && ( + + )} - {/* Only show model info if not deprecated */} - {!selectedModelInfo?.deprecated && ( - - )} - - )} + {/* Only show model info if not deprecated */} + {!selectedModelInfo?.deprecated && ( + + )} + + )} {!fromWelcomeView && ( void + simplifySettings?: boolean + openAiCodexIsAuthenticated?: boolean +} + +export const OpenAICodex: React.FC = ({ + apiConfiguration, + setApiConfigurationField, + simplifySettings, + openAiCodexIsAuthenticated = false, +}) => { + const { t } = useAppTranslation() + + return ( +
+ {/* Authentication Section */} +
+ {openAiCodexIsAuthenticated ? ( +
+ +
+ ) : ( + + )} +
+ + {/* Model Picker */} + +
+ ) +} diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index 0dd722a5220..e28cc257706 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/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" diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 65be3e21fe4..5788d38d912 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/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] From d33f2a84a1f58bc07038961e311db043549e6981 Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Wed, 14 Jan 2026 17:33:57 -0700 Subject: [PATCH 02/13] fix: correct originator header to 'roo-code' and fix version path --- src/api/providers/openai-codex.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/api/providers/openai-codex.ts b/src/api/providers/openai-codex.ts index fa2658d9f0d..bd85973223c 100644 --- a/src/api/providers/openai-codex.ts +++ b/src/api/providers/openai-codex.ts @@ -26,7 +26,7 @@ import { openAiCodexOAuthManager } from "../../integrations/openai-codex/oauth" import { t } from "../../i18n" // Get extension version for User-Agent header -const extensionVersion = require("../../../package.json").version ?? "unknown" +const extensionVersion: string = require("../../package.json").version ?? "unknown" export type OpenAiCodexModel = ReturnType @@ -348,7 +348,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion // Build Codex-specific headers. Authorization is provided by the SDK apiKey. const codexHeaders: Record = { - originator: "roocode", + originator: "roo-code", session_id: this.sessionId, "User-Agent": `roo-code/${extensionVersion} (${os.platform()} ${os.release()}; ${os.arch()}) node/${process.version.slice(1)}`, ...(accountId ? { "ChatGPT-Account-Id": accountId } : {}), @@ -483,7 +483,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion const headers: Record = { "Content-Type": "application/json", Authorization: `Bearer ${accessToken}`, - originator: "roocode", + originator: "roo-code", session_id: this.sessionId, "User-Agent": `roo-code/${extensionVersion} (${os.platform()} ${os.release()}; ${os.arch()}) node/${process.version.slice(1)}`, } @@ -1043,7 +1043,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion const headers: Record = { "Content-Type": "application/json", Authorization: `Bearer ${accessToken}`, - originator: "roocode", + originator: "roo-code", session_id: this.sessionId, "User-Agent": `roo-code/${extensionVersion} (${os.platform()} ${os.release()}; ${os.arch()}) node/${process.version.slice(1)}`, } From df483344033d45ebd8bca300aaf2388d2769791b Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Wed, 14 Jan 2026 17:45:52 -0700 Subject: [PATCH 03/13] refactor: use UUIDv7 for session_id without prefix --- src/api/providers/openai-codex.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/api/providers/openai-codex.ts b/src/api/providers/openai-codex.ts index bd85973223c..6d5c5dd2e71 100644 --- a/src/api/providers/openai-codex.ts +++ b/src/api/providers/openai-codex.ts @@ -1,5 +1,5 @@ -import * as crypto from "crypto" import * as os from "os" +import { v7 as uuidv7 } from "uuid" import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" @@ -88,9 +88,8 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion constructor(options: ApiHandlerOptions) { super() this.options = options - // Generate a unique session ID for this handler instance - // Format: ses_ - this.sessionId = `ses_${crypto.randomBytes(16).toString("hex")}` + // Generate a unique session ID for this handler instance using UUIDv7 + this.sessionId = uuidv7() } private normalizeUsage(usage: any, model: OpenAiCodexModel): ApiStreamUsageChunk | undefined { From ab48cf2083010321b4c051c9a3f24a064d1bf69b Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Wed, 14 Jan 2026 17:58:49 -0700 Subject: [PATCH 04/13] fix(openai-codex): keep session_id stable across profile switches --- packages/types/src/provider-settings.ts | 2 ++ src/api/providers/openai-codex.ts | 9 +++++---- src/core/task/Task.ts | 23 +++++++++++++++++++---- src/integrations/openai-codex/oauth.ts | 13 ++++++------- 4 files changed, 32 insertions(+), 15 deletions(-) diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 457252e7fe6..cb0e23c63b3 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -293,6 +293,8 @@ const geminiCliSchema = apiModelIdProviderModelSchema.extend({ const openAiCodexSchema = apiModelIdProviderModelSchema.extend({ // No additional settings needed - uses OAuth authentication + // Session ID for tracking conversations (generated by Task, passed through to handler) + openAiCodexSessionId: z.string().optional(), }) const openAiNativeSchema = apiModelIdProviderModelSchema.extend({ diff --git a/src/api/providers/openai-codex.ts b/src/api/providers/openai-codex.ts index 6d5c5dd2e71..afded610d3c 100644 --- a/src/api/providers/openai-codex.ts +++ b/src/api/providers/openai-codex.ts @@ -88,8 +88,9 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion constructor(options: ApiHandlerOptions) { super() this.options = options - // Generate a unique session ID for this handler instance using UUIDv7 - this.sessionId = uuidv7() + // Use session ID from options if provided (stable across handler rebuilds within a Task), + // otherwise generate a new one (for standalone handler usage) + this.sessionId = options.openAiCodexSessionId ?? uuidv7() } private normalizeUsage(usage: any, model: OpenAiCodexModel): ApiStreamUsageChunk | undefined { @@ -342,7 +343,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion // Prefer OpenAI SDK streaming (same approach as openai-native) so event handling // is consistent across providers. try { - // Get ChatGPT account ID for organization subscriptions (per OpenCode implementation) + // Get ChatGPT account ID for organization subscriptions const accountId = await openAiCodexOAuthManager.getAccountId() // Build Codex-specific headers. Authorization is provided by the SDK apiKey. @@ -475,7 +476,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion // 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 (per OpenCode implementation) + // Get ChatGPT account ID for organization subscriptions const accountId = await openAiCodexOAuthManager.getAccountId() // Build headers with required Codex-specific fields diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index b39c2f9b368..10534d59d47 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -314,6 +314,19 @@ export class Task extends EventEmitter implements TaskLike { api: ApiHandler private static lastGlobalApiRequestTime?: number private autoApprovalHandler: AutoApprovalHandler + // Stable session ID for OpenAI Codex provider (persists for the lifetime of this task) + private readonly openAiCodexSessionId: string + + private withOpenAiCodexSessionId(config: ProviderSettings): ProviderSettings { + if (config.apiProvider !== "openai-codex") { + return config + } + + return { + ...config, + openAiCodexSessionId: this.openAiCodexSessionId, + } + } /** * Reset the global API request timestamp. This should only be used for testing. @@ -482,6 +495,8 @@ export class Task extends EventEmitter implements TaskLike { } this.taskId = historyItem ? historyItem.id : crypto.randomUUID() + // Use taskId as the stable Codex session_id so it persists across profile switches and restarts. + this.openAiCodexSessionId = this.taskId this.rootTaskId = historyItem ? historyItem.rootTaskId : rootTask?.taskId this.parentTaskId = historyItem ? historyItem.parentTaskId : parentTask?.taskId this.childTaskId = undefined @@ -507,8 +522,8 @@ export class Task extends EventEmitter implements TaskLike { console.error("Failed to initialize RooIgnoreController:", error) }) - this.apiConfiguration = apiConfiguration - this.api = buildApiHandler(apiConfiguration) + this.apiConfiguration = this.withOpenAiCodexSessionId(apiConfiguration) + this.api = buildApiHandler(this.apiConfiguration) this.autoApprovalHandler = new AutoApprovalHandler() this.urlContentFetcher = new UrlContentFetcher(provider.context) @@ -1546,8 +1561,8 @@ export class Task extends EventEmitter implements TaskLike { */ public updateApiConfiguration(newApiConfiguration: ProviderSettings): void { // Update the configuration and rebuild the API handler - this.apiConfiguration = newApiConfiguration - this.api = buildApiHandler(newApiConfiguration) + this.apiConfiguration = this.withOpenAiCodexSessionId(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 diff --git a/src/integrations/openai-codex/oauth.ts b/src/integrations/openai-codex/oauth.ts index 02d4ff477a8..9f1ae5dcebc 100644 --- a/src/integrations/openai-codex/oauth.ts +++ b/src/integrations/openai-codex/oauth.ts @@ -7,7 +7,7 @@ import { z } from "zod" /** * OpenAI Codex OAuth Configuration * - * Based on the OpenCode implementation guide (PR #7537): + * 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 @@ -31,7 +31,7 @@ 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 (per OpenCode refactor) + // 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) @@ -52,7 +52,6 @@ const tokenResponseSchema = z.object({ /** * JWT claims structure for extracting ChatGPT account ID - * Based on OpenCode implementation */ interface IdTokenClaims { chatgpt_account_id?: string @@ -81,7 +80,7 @@ function parseJwtClaims(token: string): IdTokenClaims | undefined { /** * Extract ChatGPT account ID from JWT claims - * Checks multiple locations per OpenCode implementation: + * Checks multiple locations: * 1. Root-level chatgpt_account_id * 2. Nested under https://api.openai.com/auth * 3. First organization ID @@ -210,9 +209,9 @@ export function buildAuthorizationUrl(codeChallenge: string, state: string): str code_challenge_method: "S256", response_type: "code", state, - // Codex-specific parameters (per OpenCode implementation guide) + // Codex-specific parameters codex_cli_simplified_flow: "true", - originator: "roocode", + originator: "roo-code", }) return `${OPENAI_CODEX_OAUTH_CONFIG.authorizationEndpoint}?${params.toString()}` @@ -258,7 +257,7 @@ export async function exchangeCodeForTokens(code: string, codeVerifier: string): // 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 (per OpenCode implementation) + // Extract ChatGPT account ID from JWT claims const accountId = extractAccountId({ id_token: tokenResponse.id_token, access_token: tokenResponse.access_token, From fb8e9da3b369640e13238f8ecfab14820d7a8a37 Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Wed, 14 Jan 2026 18:12:18 -0700 Subject: [PATCH 05/13] Use UUIDv7 for new task IDs --- src/core/task/Task.ts | 3 ++- src/core/task/__tests__/Task.spec.ts | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 10534d59d47..f771d4a4069 100644 --- a/src/core/task/Task.ts +++ b/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" @@ -494,7 +495,7 @@ export class Task extends EventEmitter implements TaskLike { ) } - this.taskId = historyItem ? historyItem.id : crypto.randomUUID() + this.taskId = historyItem ? historyItem.id : uuidv7() // Use taskId as the stable Codex session_id so it persists across profile switches and restarts. this.openAiCodexSessionId = this.taskId this.rootTaskId = historyItem ? historyItem.rootTaskId : rootTask?.taskId diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index 5b7346d49da..8a82524b15a 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/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() + return { + ...actual, + v7: vi.fn(() => "00000000-0000-7000-8000-000000000000"), + } +}) + vi.mock("execa", () => ({ execa: vi.fn(), })) From 39edc8e192c36f84a8d58223276fbeed3b5b423c Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Wed, 14 Jan 2026 18:32:23 -0700 Subject: [PATCH 06/13] docs: fix stale originator comment in OAuth config Update comment to match actual implementation: 'roo-code' not 'roocode' --- src/integrations/openai-codex/oauth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/integrations/openai-codex/oauth.ts b/src/integrations/openai-codex/oauth.ts index 9f1ae5dcebc..0cae6a41640 100644 --- a/src/integrations/openai-codex/oauth.ts +++ b/src/integrations/openai-codex/oauth.ts @@ -12,7 +12,7 @@ import { z } from "zod" * - 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=roocode + * - Codex-specific params: codex_cli_simplified_flow=true, originator=roo-code */ export const OPENAI_CODEX_OAUTH_CONFIG = { authorizationEndpoint: "https://auth.openai.com/oauth/authorize", From cb433e4ee816a581f67547c3c67d525d7f9019a6 Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Wed, 14 Jan 2026 18:41:35 -0700 Subject: [PATCH 07/13] fix: align supportsReasoningEffort and reasoningEffort with openai-native models - gpt-5.1-codex-max: remove 'none' from supportsReasoningEffort, change reasoningEffort to 'xhigh' - gpt-5.2-codex: remove 'none' from supportsReasoningEffort --- packages/types/src/providers/openai-codex.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/types/src/providers/openai-codex.ts b/packages/types/src/providers/openai-codex.ts index 70171311023..e9cf5e170c2 100644 --- a/packages/types/src/providers/openai-codex.ts +++ b/packages/types/src/providers/openai-codex.ts @@ -33,8 +33,8 @@ export const openAiCodexModels = { excludedTools: ["apply_diff", "write_to_file"], supportsImages: true, supportsPromptCache: true, - supportsReasoningEffort: ["none", "low", "medium", "high", "xhigh"], - reasoningEffort: "high", + supportsReasoningEffort: ["low", "medium", "high", "xhigh"], + reasoningEffort: "xhigh", // Subscription-based: no per-token costs inputPrice: 0, outputPrice: 0, @@ -50,7 +50,7 @@ export const openAiCodexModels = { excludedTools: ["apply_diff", "write_to_file"], supportsImages: true, supportsPromptCache: true, - supportsReasoningEffort: ["none", "low", "medium", "high", "xhigh"], + supportsReasoningEffort: ["low", "medium", "high", "xhigh"], reasoningEffort: "medium", inputPrice: 0, outputPrice: 0, From 534b87e7f8ddaf878414e70525b6ac1a86ea98ff Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Wed, 14 Jan 2026 20:37:46 -0700 Subject: [PATCH 08/13] Refactor Task.ts to remove OpenAI Codex specific logic and use taskId from metadata --- src/api/providers/openai-codex.ts | 22 ++++++++++++++++------ src/core/task/Task.ts | 19 ++----------------- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/src/api/providers/openai-codex.ts b/src/api/providers/openai-codex.ts index afded610d3c..afbda193a21 100644 --- a/src/api/providers/openai-codex.ts +++ b/src/api/providers/openai-codex.ts @@ -177,7 +177,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion // Make the request with retry on auth failure for (let attempt = 0; attempt < 2; attempt++) { try { - yield* this.executeRequest(requestBody, model, accessToken) + yield* this.executeRequest(requestBody, model, accessToken, metadata?.taskId) return } catch (error) { const message = error instanceof Error ? error.message : String(error) @@ -335,7 +335,12 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion return body } - private async *executeRequest(requestBody: any, model: OpenAiCodexModel, accessToken: string): ApiStream { + private async *executeRequest( + requestBody: any, + model: OpenAiCodexModel, + accessToken: string, + taskId?: string, + ): ApiStream { // Create AbortController for cancellation this.abortController = new AbortController() @@ -349,7 +354,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion // Build Codex-specific headers. Authorization is provided by the SDK apiKey. const codexHeaders: Record = { originator: "roo-code", - session_id: this.sessionId, + 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 } : {}), } @@ -386,7 +391,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion } } catch (_sdkErr) { // Fallback to manual SSE via fetch (Codex backend). - yield* this.makeCodexRequest(requestBody, model, accessToken) + yield* this.makeCodexRequest(requestBody, model, accessToken, taskId) } } finally { this.abortController = undefined @@ -472,7 +477,12 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion return formattedInput } - private async *makeCodexRequest(requestBody: any, model: OpenAiCodexModel, accessToken: string): ApiStream { + 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` @@ -484,7 +494,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion "Content-Type": "application/json", Authorization: `Bearer ${accessToken}`, originator: "roo-code", - session_id: this.sessionId, + session_id: taskId || this.sessionId, "User-Agent": `roo-code/${extensionVersion} (${os.platform()} ${os.release()}; ${os.arch()}) node/${process.version.slice(1)}`, } diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index f771d4a4069..af7aed86d58 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -315,19 +315,6 @@ export class Task extends EventEmitter implements TaskLike { api: ApiHandler private static lastGlobalApiRequestTime?: number private autoApprovalHandler: AutoApprovalHandler - // Stable session ID for OpenAI Codex provider (persists for the lifetime of this task) - private readonly openAiCodexSessionId: string - - private withOpenAiCodexSessionId(config: ProviderSettings): ProviderSettings { - if (config.apiProvider !== "openai-codex") { - return config - } - - return { - ...config, - openAiCodexSessionId: this.openAiCodexSessionId, - } - } /** * Reset the global API request timestamp. This should only be used for testing. @@ -496,8 +483,6 @@ export class Task extends EventEmitter implements TaskLike { } this.taskId = historyItem ? historyItem.id : uuidv7() - // Use taskId as the stable Codex session_id so it persists across profile switches and restarts. - this.openAiCodexSessionId = this.taskId this.rootTaskId = historyItem ? historyItem.rootTaskId : rootTask?.taskId this.parentTaskId = historyItem ? historyItem.parentTaskId : parentTask?.taskId this.childTaskId = undefined @@ -523,7 +508,7 @@ export class Task extends EventEmitter implements TaskLike { console.error("Failed to initialize RooIgnoreController:", error) }) - this.apiConfiguration = this.withOpenAiCodexSessionId(apiConfiguration) + this.apiConfiguration = apiConfiguration this.api = buildApiHandler(this.apiConfiguration) this.autoApprovalHandler = new AutoApprovalHandler() @@ -1562,7 +1547,7 @@ export class Task extends EventEmitter implements TaskLike { */ public updateApiConfiguration(newApiConfiguration: ProviderSettings): void { // Update the configuration and rebuild the API handler - this.apiConfiguration = this.withOpenAiCodexSessionId(newApiConfiguration) + this.apiConfiguration = newApiConfiguration this.api = buildApiHandler(this.apiConfiguration) // IMPORTANT: Do NOT change the parser based on the new configuration! From cc0a7d581e4aa5484b8d5d06b34f31a0f15b0917 Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Wed, 14 Jan 2026 20:45:19 -0700 Subject: [PATCH 09/13] Internationalize OpenAI Codex error message --- src/api/providers/openai-codex.ts | 2 +- src/i18n/locales/en/common.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/api/providers/openai-codex.ts b/src/api/providers/openai-codex.ts index afbda193a21..360a7971311 100644 --- a/src/api/providers/openai-codex.ts +++ b/src/api/providers/openai-codex.ts @@ -514,7 +514,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion if (!response.ok) { const errorText = await response.text() - let errorMessage = `OpenAI Codex API request failed (${response.status})` + let errorMessage = t("common:api.apiRequestFailed", { status: response.status }) let errorDetails = "" try { diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 2d17e13feab..d35945fcfeb 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -124,7 +124,8 @@ "authenticationRequired": "Roo provider requires cloud authentication. Please sign in to Roo Code Cloud." }, "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", From 7fff9b128150a02a623e4ec4641f88da5392293d Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Wed, 14 Jan 2026 20:54:47 -0700 Subject: [PATCH 10/13] Add apiRequestFailed translation to all locales --- src/i18n/locales/ca/common.json | 3 ++- src/i18n/locales/de/common.json | 3 ++- src/i18n/locales/es/common.json | 3 ++- src/i18n/locales/fr/common.json | 3 ++- src/i18n/locales/hi/common.json | 3 ++- src/i18n/locales/id/common.json | 3 ++- src/i18n/locales/it/common.json | 3 ++- src/i18n/locales/ja/common.json | 3 ++- src/i18n/locales/ko/common.json | 3 ++- src/i18n/locales/nl/common.json | 3 ++- src/i18n/locales/pl/common.json | 3 ++- src/i18n/locales/pt-BR/common.json | 3 ++- src/i18n/locales/ru/common.json | 3 ++- src/i18n/locales/tr/common.json | 3 ++- src/i18n/locales/vi/common.json | 3 ++- src/i18n/locales/zh-CN/common.json | 3 ++- src/i18n/locales/zh-TW/common.json | 3 ++- 17 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index ecc60195bd5..3fde0310769 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -127,7 +127,8 @@ "authenticationRequired": "El proveïdor Roo requereix autenticació al núvol. Si us plau, inicieu sessió a Roo Code Cloud." }, "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", diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 73c8213ce00..eafb87700b6 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -124,7 +124,8 @@ "authenticationRequired": "Roo-Anbieter erfordert Cloud-Authentifizierung. Bitte melde dich bei Roo Code Cloud an." }, "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", diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 9cc23f5bb1c..67142c5a7b2 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -124,7 +124,8 @@ "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", diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index ce991866451..120fad3549e 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -124,7 +124,8 @@ "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", diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index 9cf2456f7c3..fdb80e5bc79 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -124,7 +124,8 @@ "authenticationRequired": "Roo प्रदाता को क्लाउड प्रमाणीकरण की आवश्यकता है। कृपया Roo Code Cloud में साइन इन करें।" }, "api": { - "invalidKeyInvalidChars": "API कुंजी में अमान्य वर्ण हैं।" + "invalidKeyInvalidChars": "API कुंजी में अमान्य वर्ण हैं।", + "apiRequestFailed": "API अनुरोध विफल ({{status}})" }, "manual_url_empty": "कृपया एक वैध callback URL दर्ज करें", "manual_url_no_query": "अवैध callback URL: क्वेरी पैरामीटर गुम हैं", diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index 295f843b8a6..7dadba487ff 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -124,7 +124,8 @@ "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", diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index 237fc4faa30..968c18574c9 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -124,7 +124,8 @@ "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", diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index a2da92cfac8..98bb19f82ac 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -124,7 +124,8 @@ "authenticationRequired": "Rooプロバイダーはクラウド認証が必要です。Roo Code Cloudにサインインしてください。" }, "api": { - "invalidKeyInvalidChars": "APIキーに無効な文字が含まれています。" + "invalidKeyInvalidChars": "APIキーに無効な文字が含まれています。", + "apiRequestFailed": "APIリクエストが失敗しました ({{status}})" }, "manual_url_empty": "有効なコールバック URL を入力してください", "manual_url_no_query": "無効なコールバック URL:クエリパラメータがありません", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index d2d696510dc..5bd404d880e 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -124,7 +124,8 @@ "authenticationRequired": "Roo 제공업체는 클라우드 인증이 필요합니다. Roo Code Cloud에 로그인하세요." }, "api": { - "invalidKeyInvalidChars": "API 키에 유효하지 않은 문자가 포함되어 있습니다." + "invalidKeyInvalidChars": "API 키에 유효하지 않은 문자가 포함되어 있습니다.", + "apiRequestFailed": "API 요청 실패 ({{status}})" }, "manual_url_empty": "유효한 콜백 URL을 입력하세요", "manual_url_no_query": "유효하지 않은 콜백 URL: 쿼리 매개변수 누락", diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index 91a3a94401d..80e0e6ab8f7 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -124,7 +124,8 @@ "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", diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index 7247573cd37..83063f1045a 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -124,7 +124,8 @@ "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", diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index bf53a9764bc..053e2498489 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -128,7 +128,8 @@ "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", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index dedbe8450e9..860d8d82460 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -124,7 +124,8 @@ "authenticationRequired": "Провайдер Roo требует облачной аутентификации. Войдите в Roo Code Cloud." }, "api": { - "invalidKeyInvalidChars": "API-ключ содержит недопустимые символы." + "invalidKeyInvalidChars": "API-ключ содержит недопустимые символы.", + "apiRequestFailed": "Запрос API не удался ({{status}})" }, "manual_url_empty": "Введи действительный URL обратного вызова", "manual_url_no_query": "Недействительный URL обратного вызова: отсутствуют параметры запроса", diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 4cbb45210dc..177d82158cc 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -124,7 +124,8 @@ "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", diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index 659b0ad0afe..04e5c5b3681 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -124,7 +124,8 @@ "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", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index fcc054d8a7d..fe75ea9bf68 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -129,7 +129,8 @@ "authenticationRequired": "Roo 提供商需要云认证。请登录 Roo Code Cloud。" }, "api": { - "invalidKeyInvalidChars": "API 密钥包含无效字符。" + "invalidKeyInvalidChars": "API 密钥包含无效字符.", + "apiRequestFailed": "API 请求失败 ({{status}})" }, "manual_url_empty": "请输入有效的回调 URL", "manual_url_no_query": "无效的回调 URL:缺少查询参数", diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index b8a6fc42a1b..b676d9f5cc8 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -123,7 +123,8 @@ "authenticationRequired": "Roo 提供者需要雲端認證。請登入 Roo Code Cloud。" }, "api": { - "invalidKeyInvalidChars": "API 金鑰包含無效字元。" + "invalidKeyInvalidChars": "API 金鑰包含無效字元。", + "apiRequestFailed": "API 請求失敗 ({{status}})" }, "manual_url_empty": "請輸入有效的回呼 URL", "manual_url_no_query": "無效的回呼 URL:缺少查詢參數", From 6a9a0cfc521fd29ebe346987bc39a09619353763 Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Wed, 14 Jan 2026 21:03:18 -0700 Subject: [PATCH 11/13] Remove openAiCodexSessionId from ProviderSettings and OpenAiCodexHandler constructor --- packages/types/src/provider-settings.ts | 2 -- src/api/providers/openai-codex.ts | 5 ++--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index cb0e23c63b3..457252e7fe6 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -293,8 +293,6 @@ const geminiCliSchema = apiModelIdProviderModelSchema.extend({ const openAiCodexSchema = apiModelIdProviderModelSchema.extend({ // No additional settings needed - uses OAuth authentication - // Session ID for tracking conversations (generated by Task, passed through to handler) - openAiCodexSessionId: z.string().optional(), }) const openAiNativeSchema = apiModelIdProviderModelSchema.extend({ diff --git a/src/api/providers/openai-codex.ts b/src/api/providers/openai-codex.ts index 360a7971311..ce58d013168 100644 --- a/src/api/providers/openai-codex.ts +++ b/src/api/providers/openai-codex.ts @@ -88,9 +88,8 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion constructor(options: ApiHandlerOptions) { super() this.options = options - // Use session ID from options if provided (stable across handler rebuilds within a Task), - // otherwise generate a new one (for standalone handler usage) - this.sessionId = options.openAiCodexSessionId ?? uuidv7() + // Generate a new session ID for standalone handler usage (fallback) + this.sessionId = uuidv7() } private normalizeUsage(usage: any, model: OpenAiCodexModel): ApiStreamUsageChunk | undefined { From 1f9fe18f9de476142109fca1fc22290b57b3ac8d Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Wed, 14 Jan 2026 21:08:47 -0700 Subject: [PATCH 12/13] fix: update i18n key for api request failure to match common.json structure --- src/api/providers/openai-codex.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/providers/openai-codex.ts b/src/api/providers/openai-codex.ts index ce58d013168..08bb95b081e 100644 --- a/src/api/providers/openai-codex.ts +++ b/src/api/providers/openai-codex.ts @@ -513,7 +513,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion if (!response.ok) { const errorText = await response.text() - let errorMessage = t("common:api.apiRequestFailed", { status: response.status }) + let errorMessage = t("common:errors.api.apiRequestFailed", { status: response.status }) let errorDetails = "" try { From 8d3abcf8b243d8544a2c29f7a74f520d83d4f8ea Mon Sep 17 00:00:00 2001 From: Roo Code Date: Thu, 15 Jan 2026 04:29:56 +0000 Subject: [PATCH 13/13] i18n: translate hardcoded error strings in OpenAI Codex provider - Add openAiCodex error translation keys to all locale files - Replace hardcoded English error strings in openai-codex.ts with i18n t() calls - Includes translations for: invalidRequest, authenticationFailed, accessDenied, endpointNotFound, rateLimitExceeded, serviceError, genericError, noResponseBody, connectionFailed, unexpectedConnectionError, apiError, responseFailed, streamProcessingError, unexpectedStreamError, completionError --- src/api/providers/openai-codex.ts | 39 ++++++++++++++++++------------ src/i18n/locales/ca/common.json | 18 ++++++++++++++ src/i18n/locales/de/common.json | 18 ++++++++++++++ src/i18n/locales/en/common.json | 18 ++++++++++++++ src/i18n/locales/es/common.json | 20 ++++++++++++++- src/i18n/locales/fr/common.json | 20 ++++++++++++++- src/i18n/locales/hi/common.json | 20 ++++++++++++++- src/i18n/locales/id/common.json | 20 ++++++++++++++- src/i18n/locales/it/common.json | 20 ++++++++++++++- src/i18n/locales/ja/common.json | 20 ++++++++++++++- src/i18n/locales/ko/common.json | 20 ++++++++++++++- src/i18n/locales/nl/common.json | 20 ++++++++++++++- src/i18n/locales/pl/common.json | 20 ++++++++++++++- src/i18n/locales/pt-BR/common.json | 20 ++++++++++++++- src/i18n/locales/ru/common.json | 20 ++++++++++++++- src/i18n/locales/tr/common.json | 20 ++++++++++++++- src/i18n/locales/vi/common.json | 20 ++++++++++++++- src/i18n/locales/zh-CN/common.json | 20 ++++++++++++++- src/i18n/locales/zh-TW/common.json | 20 ++++++++++++++- 19 files changed, 362 insertions(+), 31 deletions(-) diff --git a/src/api/providers/openai-codex.ts b/src/api/providers/openai-codex.ts index 08bb95b081e..490b5baaae4 100644 --- a/src/api/providers/openai-codex.ts +++ b/src/api/providers/openai-codex.ts @@ -533,27 +533,27 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion switch (response.status) { case 400: - errorMessage = "Invalid request to Codex API. Please check your input parameters." + errorMessage = t("common:errors.openAiCodex.invalidRequest") break case 401: - errorMessage = "Authentication failed. Please re-authenticate with OpenAI Codex." + errorMessage = t("common:errors.openAiCodex.authenticationFailed") break case 403: - errorMessage = "Access denied. Your ChatGPT subscription may not include Codex access." + errorMessage = t("common:errors.openAiCodex.accessDenied") break case 404: - errorMessage = "Codex API endpoint not found." + errorMessage = t("common:errors.openAiCodex.endpointNotFound") break case 429: - errorMessage = "Rate limit exceeded. Please try again later." + errorMessage = t("common:errors.openAiCodex.rateLimitExceeded") break case 500: case 502: case 503: - errorMessage = "OpenAI Codex service error. Please try again later." + errorMessage = t("common:errors.openAiCodex.serviceError") break default: - errorMessage = `Codex API error (${response.status})` + errorMessage = t("common:errors.openAiCodex.genericError", { status: response.status }) } if (errorDetails) { @@ -564,7 +564,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion } if (!response.body) { - throw new Error("Codex API error: No response body") + throw new Error(t("common:errors.openAiCodex.noResponseBody")) } yield* this.handleStreamResponse(response.body, model) @@ -577,9 +577,9 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion if (error.message.includes("Codex API")) { throw error } - throw new Error(`Failed to connect to Codex API: ${error.message}`) + throw new Error(t("common:errors.openAiCodex.connectionFailed", { message: error.message })) } - throw new Error(`Unexpected error connecting to Codex API`) + throw new Error(t("common:errors.openAiCodex.unexpectedConnectionError")) } } @@ -733,13 +733,17 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion } else if (parsed.type === "response.error" || parsed.type === "error") { if (parsed.error || parsed.message) { throw new Error( - `Codex API error: ${parsed.error?.message || parsed.message || "Unknown 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( - `Response failed: ${parsed.error?.message || parsed.message || "Unknown failure"}`, + t("common:errors.openAiCodex.responseFailed", { + message: parsed.error?.message || parsed.message || "Unknown failure", + }), ) } } else if (parsed.type === "response.completed" || parsed.type === "response.done") { @@ -818,9 +822,9 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion TelemetryService.instance.captureException(apiError) if (error instanceof Error) { - throw new Error(`Error processing response stream: ${error.message}`) + throw new Error(t("common:errors.openAiCodex.streamProcessingError", { message: error.message })) } - throw new Error("Unexpected error processing response stream") + throw new Error(t("common:errors.openAiCodex.unexpectedStreamError")) } finally { reader.releaseLock() } @@ -1071,7 +1075,10 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion if (!response.ok) { const errorText = await response.text() - throw new Error(`Codex API error (${response.status}): ${errorText}`) + throw new Error( + t("common:errors.openAiCodex.genericError", { status: response.status }) + + (errorText ? `: ${errorText}` : ""), + ) } const responseData = await response.json() @@ -1100,7 +1107,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion TelemetryService.instance.captureException(apiError) if (error instanceof Error) { - throw new Error(`OpenAI Codex completion error: ${error.message}`) + throw new Error(t("common:errors.openAiCodex.completionError", { message: error.message })) } throw error } finally { diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index 3fde0310769..321a1aa3a06 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -126,6 +126,24 @@ "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.", "apiRequestFailed": "La sol·licitud API ha fallat ({{status}})" diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index eafb87700b6..0611cf889af 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -123,6 +123,24 @@ "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.", "apiRequestFailed": "API-Anfrage fehlgeschlagen ({{status}})" diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index d35945fcfeb..90c409feb76 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -123,6 +123,24 @@ "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.", "apiRequestFailed": "API request failed ({{status}})" diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 67142c5a7b2..d0a086173e9 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -131,7 +131,25 @@ "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", diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index 120fad3549e..58350ef02ba 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -131,7 +131,25 @@ "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é", diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index fdb80e5bc79..33277c71623 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -131,7 +131,25 @@ "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": "कोई टर्मिनल सामग्री चयनित नहीं", diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index 7dadba487ff..c10532beef9 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -131,7 +131,25 @@ "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", diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index 968c18574c9..a75dccd3875 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -131,7 +131,25 @@ "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", diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index 98bb19f82ac..b378f00b03f 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -131,7 +131,25 @@ "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": "選択されたターミナルコンテンツがありません", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index 5bd404d880e..e7afdceabce 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -131,7 +131,25 @@ "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": "선택된 터미널 내용이 없습니다", diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index 80e0e6ab8f7..889cd4b3ab6 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -131,7 +131,25 @@ "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", diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index 83063f1045a..faa4e9ed3a5 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -131,7 +131,25 @@ "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", diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index 053e2498489..f41a379acbc 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -135,7 +135,25 @@ "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", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 860d8d82460..751637f19e0 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -131,7 +131,25 @@ "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": "Не выбрано содержимое терминала", diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 177d82158cc..7b2ac152a9b 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -131,7 +131,25 @@ "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", diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index 04e5c5b3681..0d88ba07808 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -131,7 +131,25 @@ "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", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index fe75ea9bf68..133b3de0794 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -136,7 +136,25 @@ "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": "没有选择终端内容", diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index b676d9f5cc8..8039f203b62 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -131,7 +131,25 @@ "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": "沒有選擇終端機內容",