diff --git a/apps/desktop/src/main/codex-oauth-ipc.test.ts b/apps/desktop/src/main/codex-oauth-ipc.test.ts index d1661070..ec270807 100644 --- a/apps/desktop/src/main/codex-oauth-ipc.test.ts +++ b/apps/desktop/src/main/codex-oauth-ipc.test.ts @@ -117,6 +117,15 @@ function makeIdToken(payload: Record): string { return `${header}.${body}.sig`; } +const EXPECTED_CHATGPT_CODEX_MODELS = [ + 'gpt-5.5', + 'gpt-5.4', + 'gpt-5.4-mini', + 'gpt-5.3-codex', + 'gpt-5.3-codex-spark', + 'gpt-5.2', +]; + describe('codex-oauth:v1:status', () => { it('returns loggedIn: false when no token file is present', async () => { await register(); @@ -192,7 +201,8 @@ describe('codex-oauth:v1:login', () => { name: 'ChatGPT 订阅', wire: 'openai-codex-responses', baseUrl: 'https://chatgpt.com/backend-api', - defaultModel: 'gpt-5.3-codex', + defaultModel: 'gpt-5.5', + modelsHint: EXPECTED_CHATGPT_CODEX_MODELS, requiresApiKey: false, }); @@ -252,7 +262,7 @@ describe('codex-oauth:v1:login', () => { await handlers.get('codex-oauth:v1:login')?.(); expect(fakeCachedConfig?.activeProvider).toBe('chatgpt-codex'); - expect(fakeCachedConfig?.activeModel).toBe('gpt-5.3-codex'); + expect(fakeCachedConfig?.activeModel).toBe('gpt-5.5'); }); it('leaves active provider alone when one is already set and valid', async () => { @@ -394,10 +404,10 @@ describe('codex-oauth:v1:logout', () => { }); describe('migrateStaleCodexEntryIfNeeded', () => { - it('rewrites stale codex entry with current wire + baseUrl', async () => { + it('rewrites stale codex entry with current wire, baseUrl, and model hints', async () => { fakeCachedConfig = { activeProvider: 'chatgpt-codex', - activeModel: 'gpt-5.3-codex', + activeModel: 'gpt-5.1-codex-max', secrets: {}, providers: { 'chatgpt-codex': { @@ -408,7 +418,7 @@ describe('migrateStaleCodexEntryIfNeeded', () => { wire: 'openai-responses', baseUrl: 'https://chatgpt.com/backend-api/codex', defaultModel: 'gpt-5.3-codex', - modelsHint: ['gpt-5.3-codex'], + modelsHint: ['gpt-5.4', 'gpt-5.1-codex-max', 'gpt-5.1'], requiresApiKey: false, }, }, @@ -420,6 +430,9 @@ describe('migrateStaleCodexEntryIfNeeded', () => { const rewritten = fakeCachedConfig?.providers['chatgpt-codex'] as Record; expect(rewritten['wire']).toBe('openai-codex-responses'); expect(rewritten['baseUrl']).toBe('https://chatgpt.com/backend-api'); + expect(rewritten['defaultModel']).toBe('gpt-5.5'); + expect(rewritten['modelsHint']).toEqual(EXPECTED_CHATGPT_CODEX_MODELS); + expect(fakeCachedConfig?.activeModel).toBe('gpt-5.5'); expect(writeConfigMock).toHaveBeenCalledTimes(1); }); @@ -435,7 +448,8 @@ describe('migrateStaleCodexEntryIfNeeded', () => { builtin: false, wire: 'openai-codex-responses', baseUrl: 'https://chatgpt.com/backend-api', - defaultModel: 'gpt-5.3-codex', + defaultModel: 'gpt-5.5', + modelsHint: EXPECTED_CHATGPT_CODEX_MODELS, requiresApiKey: false, }, }, diff --git a/apps/desktop/src/main/codex-oauth-ipc.ts b/apps/desktop/src/main/codex-oauth-ipc.ts index d25ee43d..6c33376b 100644 --- a/apps/desktop/src/main/codex-oauth-ipc.ts +++ b/apps/desktop/src/main/codex-oauth-ipc.ts @@ -38,6 +38,16 @@ export interface CodexOAuthStatus { // avoid a module cycle with `provider-settings`. export { CHATGPT_CODEX_PROVIDER_ID }; +const CHATGPT_CODEX_DEFAULT_MODEL = 'gpt-5.5'; +const CHATGPT_CODEX_MODELS = [ + CHATGPT_CODEX_DEFAULT_MODEL, + 'gpt-5.4', + 'gpt-5.4-mini', + 'gpt-5.3-codex', + 'gpt-5.3-codex-spark', + 'gpt-5.2', +]; + const CHATGPT_CODEX_PROVIDER: ProviderEntry = { id: CHATGPT_CODEX_PROVIDER_ID, name: 'ChatGPT 订阅', @@ -47,22 +57,11 @@ const CHATGPT_CODEX_PROVIDER: ProviderEntry = { // we store the bare base. Do not add `/codex` here — it'd produce // `/codex/codex/responses`. baseUrl: 'https://chatgpt.com/backend-api', - defaultModel: 'gpt-5.3-codex', - // Ordered strongest-first by pricing tier / recency, so the UI model - // picker surfaces the best choice at the top. `defaultModel` is the - // codex-specialized flagship rather than gpt-5.4 because the - // codex-trained family produces more reliable artifact output today. - modelsHint: [ - 'gpt-5.4', - 'gpt-5.3-codex', - 'gpt-5.3-codex-spark', - 'gpt-5.2-codex', - 'gpt-5.2', - 'gpt-5.1-codex-max', - 'gpt-5.1', - 'gpt-5.4-mini', - 'gpt-5.1-codex-mini', - ], + defaultModel: CHATGPT_CODEX_DEFAULT_MODEL, + // Static hint for the keyless ChatGPT/Codex OAuth path. This endpoint has + // no usable /models discovery flow, so keep the list aligned with the + // official Codex model picker and ordered strongest-first. + modelsHint: CHATGPT_CODEX_MODELS, requiresApiKey: false, capabilities: { supportsKeyless: true, @@ -277,12 +276,24 @@ async function runLogout(): Promise { return { loggedIn: false, email: null, accountId: null, expiresAt: null }; } +function sameModelHints(input: string[] | undefined): boolean { + return ( + input !== undefined && + input.length === CHATGPT_CODEX_MODELS.length && + input.every((model, index) => model === CHATGPT_CODEX_MODELS[index]) + ); +} + +function isKnownChatgptCodexModel(modelId: string): boolean { + return CHATGPT_CODEX_MODELS.includes(modelId); +} + /** * One-shot boot migration for feat-branch testers: if an older build wrote * `chatgpt-codex` with Phase 1's stale `wire`/`baseUrl`, overwrite with the - * Phase 2 canonical values so the first generate after upgrade works without - * requiring a manual re-login. No-op when the entry is absent or already - * canonical. Safe to call on every boot — writes only when state diverges. + * current canonical values so the first generate after upgrade works without + * requiring a manual re-login. This also refreshes the static model hint list + * when ChatGPT subscription model availability changes. * * The public card used to ship disabled, so this migration only fires for * users who ran the experimental branch directly; zero writes on fresh @@ -292,16 +303,36 @@ export async function migrateStaleCodexEntryIfNeeded(): Promise { const cfg = getCachedConfig(); const entry = cfg?.providers[CHATGPT_CODEX_PROVIDER_ID]; if (cfg === null || entry === undefined) return; - const isStale = - entry.wire !== CHATGPT_CODEX_PROVIDER.wire || entry.baseUrl !== CHATGPT_CODEX_PROVIDER.baseUrl; - if (!isStale) return; - await persistProviderMutation((providers) => { - providers[CHATGPT_CODEX_PROVIDER_ID] = { ...CHATGPT_CODEX_PROVIDER }; - return providers; + + const providerIsStale = + entry.wire !== CHATGPT_CODEX_PROVIDER.wire || + entry.baseUrl !== CHATGPT_CODEX_PROVIDER.baseUrl || + entry.defaultModel !== CHATGPT_CODEX_PROVIDER.defaultModel || + !sameModelHints(entry.modelsHint); + const activeModelIsStale = + cfg.activeProvider === CHATGPT_CODEX_PROVIDER_ID && !isKnownChatgptCodexModel(cfg.activeModel); + + if (!providerIsStale && !activeModelIsStale) return; + + const nextProviders = { + ...cfg.providers, + [CHATGPT_CODEX_PROVIDER_ID]: { ...CHATGPT_CODEX_PROVIDER }, + }; + const next: Config = hydrateConfig({ + version: 3, + activeProvider: cfg.activeProvider, + activeModel: activeModelIsStale ? CHATGPT_CODEX_DEFAULT_MODEL : cfg.activeModel, + secrets: cfg.secrets, + providers: nextProviders, + ...(cfg.designSystem !== undefined ? { designSystem: cfg.designSystem } : {}), }); + await writeConfig(next); + setCachedConfig(next); logger.info('codex.oauth.migrate.stale_entry_rewritten', { previousWire: entry.wire, previousBaseUrl: entry.baseUrl, + previousDefaultModel: entry.defaultModel, + activeModelReset: activeModelIsStale, }); } diff --git a/packages/i18n/src/locales/en.json b/packages/i18n/src/locales/en.json index f2d002a7..7db67986 100644 --- a/packages/i18n/src/locales/en.json +++ b/packages/i18n/src/locales/en.json @@ -332,7 +332,7 @@ "sectionTitle": "API Providers", "chatgptLogin": { "title": "Sign in with ChatGPT subscription", - "description": "Use your ChatGPT Plus / Pro / Team plan quota to call Codex models (gpt-5.3-codex and friends) — no API key needed.", + "description": "Use your ChatGPT Plus / Pro / Team plan quota to call Codex models (gpt-5.5 and friends) — no API key needed.", "signIn": "Sign in with ChatGPT", "inProgress": "Browser opened. Complete the authorization and we'll return automatically…", "loggedInBadge": "Signed in with ChatGPT", diff --git a/packages/i18n/src/locales/es.json b/packages/i18n/src/locales/es.json index 467899ea..a0a191c9 100644 --- a/packages/i18n/src/locales/es.json +++ b/packages/i18n/src/locales/es.json @@ -280,7 +280,7 @@ "sectionTitle": "Proveedores de API", "chatgptLogin": { "title": "Iniciar sesión con suscripción de ChatGPT", - "description": "Usa tu cuota del plan ChatGPT Plus / Pro / Team para llamar a modelos Codex (gpt-5.3-codex y similares) — no se necesita clave API.", + "description": "Usa tu cuota del plan ChatGPT Plus / Pro / Team para llamar a modelos Codex (gpt-5.5 y similares) — no se necesita clave API.", "signIn": "Iniciar sesión con ChatGPT", "inProgress": "Navegador abierto. Completa la autorización y regresaremos automáticamente…", "loggedInBadge": "Sesión iniciada con ChatGPT", diff --git a/packages/i18n/src/locales/pt-BR.json b/packages/i18n/src/locales/pt-BR.json index 96b9926b..bb644e5e 100644 --- a/packages/i18n/src/locales/pt-BR.json +++ b/packages/i18n/src/locales/pt-BR.json @@ -252,7 +252,7 @@ "sectionTitle": "Provedores de API", "chatgptLogin": { "title": "Entrar com assinatura do ChatGPT", - "description": "Use a cota do seu plano ChatGPT Plus / Pro / Team para chamar modelos Codex (gpt-5.3-codex e afins) — sem precisar de chave de API.", + "description": "Use a cota do seu plano ChatGPT Plus / Pro / Team para chamar modelos Codex (gpt-5.5 e afins) — sem precisar de chave de API.", "signIn": "Entrar com ChatGPT", "inProgress": "Navegador aberto. Conclua a autorização e voltamos automaticamente…", "loggedInBadge": "Conectado com ChatGPT", diff --git a/packages/i18n/src/locales/zh-CN.json b/packages/i18n/src/locales/zh-CN.json index f6251254..57e62734 100644 --- a/packages/i18n/src/locales/zh-CN.json +++ b/packages/i18n/src/locales/zh-CN.json @@ -332,7 +332,7 @@ "sectionTitle": "API 服务", "chatgptLogin": { "title": "用 ChatGPT 订阅登录", - "description": "直接用你的 ChatGPT Plus / Pro / Team 订阅额度调用 Codex 模型(gpt-5.3-codex 等),无需 API key。", + "description": "直接用你的 ChatGPT Plus / Pro / Team 订阅额度调用 Codex 模型(gpt-5.5 等),无需 API key。", "signIn": "用 ChatGPT 订阅登录", "inProgress": "已打开浏览器,完成授权后自动返回…", "loggedInBadge": "已登录 ChatGPT",