mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-07 02:58:15 +00:00
feat: add OpenAI Codex provider with OAuth subscription authentication (#10736)
Co-authored-by: Roo Code <roomote@roocode.com>
This commit is contained in:
parent
739b91ec40
commit
4ebbca08b0
37 changed files with 2663 additions and 94 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export * from "./mistral.js"
|
|||
export * from "./moonshot.js"
|
||||
export * from "./ollama.js"
|
||||
export * from "./openai.js"
|
||||
export * from "./openai-codex.js"
|
||||
export * from "./openrouter.js"
|
||||
export * from "./qwen-code.js"
|
||||
export * from "./requesty.js"
|
||||
|
|
@ -48,6 +49,7 @@ import { ioIntelligenceDefaultModelId } from "./io-intelligence.js"
|
|||
import { litellmDefaultModelId } from "./lite-llm.js"
|
||||
import { mistralDefaultModelId } from "./mistral.js"
|
||||
import { moonshotDefaultModelId } from "./moonshot.js"
|
||||
import { openAiCodexDefaultModelId } from "./openai-codex.js"
|
||||
import { openRouterDefaultModelId } from "./openrouter.js"
|
||||
import { qwenCodeDefaultModelId } from "./qwen-code.js"
|
||||
import { requestyDefaultModelId } from "./requesty.js"
|
||||
|
|
@ -111,6 +113,8 @@ export function getProviderDefaultModelId(
|
|||
return options?.isChina ? mainlandZAiDefaultModelId : internationalZAiDefaultModelId
|
||||
case "openai-native":
|
||||
return "gpt-4o" // Based on openai-native patterns
|
||||
case "openai-codex":
|
||||
return openAiCodexDefaultModelId
|
||||
case "mistral":
|
||||
return mistralDefaultModelId
|
||||
case "openai":
|
||||
|
|
|
|||
92
packages/types/src/providers/openai-codex.ts
Normal file
92
packages/types/src/providers/openai-codex.ts
Normal 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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -15,6 +15,7 @@ export { IOIntelligenceHandler } from "./io-intelligence"
|
|||
export { LiteLLMHandler } from "./lite-llm"
|
||||
export { LmStudioHandler } from "./lm-studio"
|
||||
export { MistralHandler } from "./mistral"
|
||||
export { OpenAiCodexHandler } from "./openai-codex"
|
||||
export { OpenAiNativeHandler } from "./openai-native"
|
||||
export { OpenAiHandler } from "./openai"
|
||||
export { OpenRouterHandler } from "./openrouter"
|
||||
|
|
|
|||
1117
src/api/providers/openai-codex.ts
Normal file
1117
src/api/providers/openai-codex.ts
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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") || []
|
||||
|
||||
|
|
|
|||
21
src/i18n/locales/ca/common.json
generated
21
src/i18n/locales/ca/common.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
21
src/i18n/locales/de/common.json
generated
21
src/i18n/locales/de/common.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
23
src/i18n/locales/es/common.json
generated
23
src/i18n/locales/es/common.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
23
src/i18n/locales/fr/common.json
generated
23
src/i18n/locales/fr/common.json
generated
|
|
@ -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é",
|
||||
|
|
|
|||
23
src/i18n/locales/hi/common.json
generated
23
src/i18n/locales/hi/common.json
generated
|
|
@ -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": "कोई टर्मिनल सामग्री चयनित नहीं",
|
||||
|
|
|
|||
23
src/i18n/locales/id/common.json
generated
23
src/i18n/locales/id/common.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
23
src/i18n/locales/it/common.json
generated
23
src/i18n/locales/it/common.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
23
src/i18n/locales/ja/common.json
generated
23
src/i18n/locales/ja/common.json
generated
|
|
@ -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": "選択されたターミナルコンテンツがありません",
|
||||
|
|
|
|||
23
src/i18n/locales/ko/common.json
generated
23
src/i18n/locales/ko/common.json
generated
|
|
@ -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": "선택된 터미널 내용이 없습니다",
|
||||
|
|
|
|||
23
src/i18n/locales/nl/common.json
generated
23
src/i18n/locales/nl/common.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
23
src/i18n/locales/pl/common.json
generated
23
src/i18n/locales/pl/common.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
23
src/i18n/locales/pt-BR/common.json
generated
23
src/i18n/locales/pt-BR/common.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
23
src/i18n/locales/ru/common.json
generated
23
src/i18n/locales/ru/common.json
generated
|
|
@ -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": "Не выбрано содержимое терминала",
|
||||
|
|
|
|||
23
src/i18n/locales/tr/common.json
generated
23
src/i18n/locales/tr/common.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
23
src/i18n/locales/vi/common.json
generated
23
src/i18n/locales/vi/common.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
23
src/i18n/locales/zh-CN/common.json
generated
23
src/i18n/locales/zh-CN/common.json
generated
|
|
@ -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": "没有选择终端内容",
|
||||
|
|
|
|||
23
src/i18n/locales/zh-TW/common.json
generated
23
src/i18n/locales/zh-TW/common.json
generated
|
|
@ -123,14 +123,33 @@
|
|||
"authenticationRequired": "Roo 提供者需要雲端認證。請登入 Roo Code Cloud。"
|
||||
},
|
||||
"api": {
|
||||
"invalidKeyInvalidChars": "API 金鑰包含無效字元。"
|
||||
"invalidKeyInvalidChars": "API 金鑰包含無效字元。",
|
||||
"apiRequestFailed": "API 請求失敗 ({{status}})"
|
||||
},
|
||||
"manual_url_empty": "請輸入有效的回呼 URL",
|
||||
"manual_url_no_query": "無效的回呼 URL:缺少查詢參數",
|
||||
"manual_url_missing_params": "無效的回呼 URL:缺少必要參數(code 和 state)",
|
||||
"manual_url_auth_failed": "手動 URL 身份驗證失敗",
|
||||
"manual_url_auth_error": "身份驗證失敗",
|
||||
"mode_import_failed": "匯入模式失敗:{{error}}"
|
||||
"mode_import_failed": "匯入模式失敗:{{error}}",
|
||||
"openAiCodex": {
|
||||
"notAuthenticated": "Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow.",
|
||||
"invalidRequest": "Invalid request to Codex API. Please check your input parameters.",
|
||||
"authenticationFailed": "Authentication failed. Please re-authenticate with OpenAI Codex.",
|
||||
"accessDenied": "Access denied. Your ChatGPT subscription may not include Codex access.",
|
||||
"endpointNotFound": "Codex API endpoint not found.",
|
||||
"rateLimitExceeded": "Rate limit exceeded. Please try again later.",
|
||||
"serviceError": "OpenAI Codex service error. Please try again later.",
|
||||
"genericError": "Codex API error ({{status}})",
|
||||
"noResponseBody": "Codex API error: No response body",
|
||||
"connectionFailed": "Failed to connect to Codex API: {{message}}",
|
||||
"unexpectedConnectionError": "Unexpected error connecting to Codex API",
|
||||
"apiError": "Codex API error: {{message}}",
|
||||
"responseFailed": "Response failed: {{message}}",
|
||||
"streamProcessingError": "Error processing response stream: {{message}}",
|
||||
"unexpectedStreamError": "Unexpected error processing response stream",
|
||||
"completionError": "OpenAI Codex completion error: {{message}}"
|
||||
}
|
||||
},
|
||||
"warnings": {
|
||||
"no_terminal_content": "沒有選擇終端機內容",
|
||||
|
|
|
|||
740
src/integrations/openai-codex/oauth.ts
Normal file
740
src/integrations/openai-codex/oauth.ts
Normal 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>✓ 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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
geminiModels,
|
||||
mistralModels,
|
||||
openAiNativeModels,
|
||||
openAiCodexModels,
|
||||
qwenCodeModels,
|
||||
vertexModels,
|
||||
xaiModels,
|
||||
|
|
@ -34,6 +35,7 @@ export const MODELS_BY_PROVIDER: Partial<Record<ProviderName, Record<string, Mod
|
|||
gemini: geminiModels,
|
||||
mistral: mistralModels,
|
||||
"openai-native": openAiNativeModels,
|
||||
"openai-codex": openAiCodexModels,
|
||||
"qwen-code": qwenCodeModels,
|
||||
vertex: vertexModels,
|
||||
xai: xaiModels,
|
||||
|
|
@ -57,6 +59,7 @@ export const PROVIDERS = [
|
|||
{ value: "deepseek", label: "DeepSeek", proxy: false },
|
||||
{ value: "moonshot", label: "Moonshot", proxy: false },
|
||||
{ value: "openai-native", label: "OpenAI", proxy: false },
|
||||
{ value: "openai-codex", label: "OpenAI - ChatGPT Plus/Pro", proxy: false },
|
||||
{ value: "openai", label: "OpenAI Compatible", proxy: true },
|
||||
{ value: "qwen-code", label: "Qwen Code", proxy: false },
|
||||
{ value: "vertex", label: "GCP Vertex AI", proxy: false },
|
||||
|
|
|
|||
67
webview-ui/src/components/settings/providers/OpenAICodex.tsx
Normal file
67
webview-ui/src/components/settings/providers/OpenAICodex.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue