Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions apps/desktop/src/main/codex-oauth-ipc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,15 @@ function makeIdToken(payload: Record<string, unknown>): 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();
Expand Down Expand Up @@ -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,
});

Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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': {
Expand All @@ -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,
},
},
Expand All @@ -420,6 +430,9 @@ describe('migrateStaleCodexEntryIfNeeded', () => {
const rewritten = fakeCachedConfig?.providers['chatgpt-codex'] as Record<string, unknown>;
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);
});

Expand All @@ -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,
},
},
Expand Down
81 changes: 56 additions & 25 deletions apps/desktop/src/main/codex-oauth-ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 订阅',
Expand All @@ -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,
Expand Down Expand Up @@ -277,12 +276,24 @@ async function runLogout(): Promise<CodexOAuthStatus> {
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
Expand All @@ -292,16 +303,36 @@ export async function migrateStaleCodexEntryIfNeeded(): Promise<void> {
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,
});
}

Expand Down
2 changes: 1 addition & 1 deletion packages/i18n/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/i18n/src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/i18n/src/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/i18n/src/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading