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

Co-authored-by: Roo Code <roomote@roocode.com>
This commit is contained in:
Hannes Rudolph 2026-01-14 21:48:51 -07:00 • committed by GitHub
parent 739b91ec40
commit 4ebbca08b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 2663 additions and 94 deletions

View file

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

View file

@ -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":

View file

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

View file

@ -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"

View file

@ -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":

View file

@ -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",
})
})
})

View file

@ -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"

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

@ -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) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"

View file

@ -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]