diff --git a/docs/advanced-setup.md b/docs/advanced-setup.md index 8ed917fae..8cb3af410 100644 --- a/docs/advanced-setup.md +++ b/docs/advanced-setup.md @@ -51,6 +51,11 @@ export OPENAI_MODEL=gpt-4o `codexplan` maps to GPT-5.6 Sol on the Codex backend with high reasoning. `codexspark` maps to GPT-5.3 Codex Spark for faster loops. +Full Codex aliases such as `gpt-5.6-sol` also use the Codex backend when no +base URL is configured. To use the direct OpenAI API with those model names, +set `OPENAI_BASE_URL=https://api.openai.com/v1` explicitly. The `[1m]` context +tag preserves the same route, including when combined with a reasoning query. + If you use the in-app provider wizard, choose `Codex OAuth` to open ChatGPT sign-in in your browser and let OpenClaude store Codex credentials securely. If you already use the Codex CLI, OpenClaude reads `~/.codex/auth.json` automatically. You can also point it elsewhere with `CODEX_AUTH_JSON_PATH` or override the token directly with `CODEX_API_KEY`. diff --git a/src/services/api/providerConfig.test.ts b/src/services/api/providerConfig.test.ts index ade244531..76a3b5867 100644 --- a/src/services/api/providerConfig.test.ts +++ b/src/services/api/providerConfig.test.ts @@ -461,6 +461,68 @@ test('resolveProviderRequest resolves the GPT-5.6 family Codex aliases', () => { expect(bare.reasoning).toEqual({ effort: 'high' }) }) +test('resolveProviderRequest pairs bare Codex-alias launches with the Codex backend endpoint', () => { + // --provider openai --model with no base URL anywhere resolves + // through OPENAI_MODEL; the codex_responses transport must not end up + // paired with the public OpenAI fallback URL (#2066). + for (const model of ['gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.5']) { + const request = resolveProviderRequest({ + processEnv: { CLAUDE_CODE_USE_OPENAI: '1', OPENAI_MODEL: model }, + }) + expect(request.requestedModel).toBe(model) + expect(request.transport).toBe('codex_responses') + expect(request.baseUrl).toBe('https://chatgpt.com/backend-api/codex') + } +}) + +test('resolveProviderRequest keeps an explicit default base URL on the direct route for Codex aliases', () => { + const request = resolveProviderRequest({ + processEnv: { + CLAUDE_CODE_USE_OPENAI: '1', + OPENAI_BASE_URL: 'https://api.openai.com/v1', + OPENAI_MODEL: 'gpt-5.6-terra', + }, + }) + + expect(request.transport).toBe('responses') + expect(request.baseUrl).toBe('https://api.openai.com/v1') +}) + +test('resolveProviderRequest promotes [1m]-tagged Codex aliases identically to untagged ones', () => { + for (const baseUrl of [undefined, 'https://api.openai.com/v1', 'https://gateway.example/v1']) { + for (const model of ['gpt-5.6-sol', 'gpt-5.5', 'codexplan', 'codexspark']) { + const processEnv = { + CLAUDE_CODE_USE_OPENAI: '1', + OPENAI_BASE_URL: baseUrl, + OPENAI_MODEL: `${model}?reasoning=medium`, + } + const untagged = resolveProviderRequest({ processEnv }) + for (const taggedModel of [`${model}[1m]?reasoning=medium`, `${model}?reasoning=medium[1m]`]) { + const request = resolveProviderRequest({ + processEnv: { ...processEnv, OPENAI_MODEL: taggedModel }, + }) + expect(request.requestedModel).toBe(taggedModel) + expect(request.baseUrl).toBe(untagged.baseUrl) + expect(request.transport).toBe(untagged.transport) + expect(request.resolvedModel).toBe(untagged.resolvedModel) + expect(request.reasoning).toEqual({ effort: 'medium' }) + } + const tagged = resolveProviderRequest({ + processEnv: { ...processEnv, OPENAI_MODEL: `${model}[1m]` }, + }) + const plain = resolveProviderRequest({ + processEnv: { ...processEnv, OPENAI_MODEL: model }, + }) + expect(tagged).toMatchObject({ + baseUrl: plain.baseUrl, + transport: plain.transport, + resolvedModel: plain.resolvedModel, + reasoning: plain.reasoning, + }) + } + } +}) + test('resolveProviderRequest keeps the implicit Codex fallback on Sol with high reasoning', () => { expect(resolveProviderRequest({ processEnv: {} })).toMatchObject({ requestedModel: 'codexplan', diff --git a/src/services/api/providerConfig.ts b/src/services/api/providerConfig.ts index 886622d66..d0b4b2ec5 100644 --- a/src/services/api/providerConfig.ts +++ b/src/services/api/providerConfig.ts @@ -391,7 +391,11 @@ export function parseOpenAICompatibleApiFormat( return undefined } -function parseModelDescriptor(model: string): ModelDescriptor { +function parseModelIdentity(model: string): { + raw: string + baseModel: string + query: string | undefined +} { // A trailing [1m] suffix is the client-side 1M-context opt-in (see // has1mContext) — never part of the wire model id or the ?query syntax. // Strip it before parsing so tagged aliases keep their mapping and effort @@ -400,14 +404,25 @@ function parseModelDescriptor(model: string): ModelDescriptor { // on the base id (`gpt-5.5[1m]`); both forms are handled. const trimmed = model.trim().replace(/\[1m]$/i, '').trim() const queryIndex = trimmed.indexOf('?') - if (queryIndex === -1) { - const alias = trimmed.toLowerCase() as CodexAlias + return { + raw: trimmed, + baseModel: (queryIndex === -1 ? trimmed : trimmed.slice(0, queryIndex)) + .trim() + .replace(/\[1m]$/i, ''), + query: queryIndex === -1 ? undefined : trimmed.slice(queryIndex + 1), + } +} + +function parseModelDescriptor(model: string): ModelDescriptor { + const { raw, baseModel, query } = parseModelIdentity(model) + if (query === undefined) { + const alias = baseModel.toLowerCase() as CodexAlias const aliasConfig = Object.hasOwn(CODEX_ALIAS_MODELS, alias) ? CODEX_ALIAS_MODELS[alias] : undefined if (aliasConfig) { return { - raw: trimmed, + raw, baseModel: aliasConfig.model, reasoning: aliasConfig.reasoningEffort ? { effort: aliasConfig.reasoningEffort } @@ -416,16 +431,12 @@ function parseModelDescriptor(model: string): ModelDescriptor { } } return { - raw: trimmed, - baseModel: trimmed, + raw, + baseModel, } } - const baseModel = trimmed - .slice(0, queryIndex) - .trim() - .replace(/\[1m]$/i, '') - const params = new URLSearchParams(trimmed.slice(queryIndex + 1)) + const params = new URLSearchParams(query) const alias = baseModel.toLowerCase() as CodexAlias const aliasConfig = Object.hasOwn(CODEX_ALIAS_MODELS, alias) ? CODEX_ALIAS_MODELS[alias] @@ -440,7 +451,7 @@ function parseModelDescriptor(model: string): ModelDescriptor { const thinking = parseThinkingType(params.get('thinking') ?? undefined) return { - raw: trimmed, + raw, baseModel: resolvedBaseModel, reasoning: typeof reasoning === 'string' ? { effort: reasoning } : reasoning, reasoningFromAlias: @@ -450,15 +461,13 @@ function parseModelDescriptor(model: string): ModelDescriptor { } export function isCodexAlias(model: string): boolean { - const normalized = model.trim().toLowerCase() - const base = normalized.split('?', 1)[0] ?? normalized - return Object.hasOwn(CODEX_ALIAS_MODELS, base) + const { baseModel } = parseModelIdentity(model) + return Object.hasOwn(CODEX_ALIAS_MODELS, baseModel.toLowerCase()) } function isOpenAICodexShortcutAlias(model: string): boolean { - const normalized = model.trim().toLowerCase() - const base = normalized.split('?', 1)[0] ?? normalized - return OPENAI_CODEX_SHORTCUT_ALIASES.has(base) + const { baseModel } = parseModelIdentity(model) + return OPENAI_CODEX_SHORTCUT_ALIASES.has(baseModel.toLowerCase()) } export function shouldUseCodexTransport( @@ -1082,9 +1091,19 @@ export function resolveProviderRequest(options?: { descriptor.baseModel === envResolvedCodexModel const isCodexAliasModel = isOpenAICodexShortcutAlias(requestedModel) || requestedMatchesEnvCodexShortcut + // A non-shortcut Codex alias launched with no endpoint identity anywhere + // still selects the codex_responses transport (shouldUseCodexTransport + // treats every alias as Codex once the base URL is absent), leaving it + // paired with the public OpenAI fallback URL (#2066). Promote bare-alias + // launches to the Codex backend so transport and baseUrl describe one + // route. An explicit base URL — even set to the literal default value — + // still pins the direct OpenAI route. + const isBareCodexAliasLaunch = + !isGithubMode && !isCodexAliasModel && !rawBaseUrl && isCodexAlias(requestedModel) const hasUserSetBaseUrl = rawBaseUrl && rawBaseUrl !== DEFAULT_OPENAI_BASE_URL const finalBaseUrlRaw = - !isGithubMode && isCodexAliasModel && !hasUserSetBaseUrl + !isGithubMode && + ((isCodexAliasModel && !hasUserSetBaseUrl) || isBareCodexAliasLaunch) ? DEFAULT_CODEX_BASE_URL : rawBaseUrl const finalBaseUrl = normalizeGitlawbOpengatewayBaseUrl(finalBaseUrlRaw)