fix(opencode): promote bare Codex alias launches to Codex backend (#2066) (#2171)

* fix(opencode): promote bare Codex alias launches to the Codex backend endpoint

* fix(opencode): normalize [1m] tag before Codex alias classification (#2171)

* fix(opencode): centralize [1m] tag normalization inside isCodexAlias (#2171)

* chore: remove pnpm lockfile (repo uses bun)

* fix(provider): share Codex model identity parsing for tagged routes

---------

Co-authored-by: jatmn <the@jat.mn>
This commit is contained in:
Cal
2026-10-25 20:12:22 -07:00
committed by GitHub
co-authored by jatmn
parent 23115eb86d
commit c86014337b
3 changed files with 105 additions and 19 deletions
+5
View File
@@ -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`.
+62
View File
@@ -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 <alias> 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',
+38 -19
View File
@@ -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)