From 51ab2fcf23314e11c4137192498bc393222ddd23 Mon Sep 17 00:00:00 2001 From: roboomp Date: Tue, 14 Jul 2026 19:17:23 +0000 Subject: [PATCH 1/2] fix(catalog): made codex discovery authoritative Replaced stale bundled OpenAI Codex entries after successful account-scoped discovery in both runtime resolution and catalog generation. Fixes #5364 --- packages/catalog/CHANGELOG.md | 4 + packages/catalog/scripts/generate-models.ts | 11 +- packages/catalog/src/models.json | 816 +++++++++++++++--- .../catalog/src/provider-models/special.ts | 1 + packages/catalog/test/codex-discovery.test.ts | 37 + .../src/prompts/system/tan-context-switch.md | 2 +- .../agent-session-prune-persistence.test.ts | 4 +- 7 files changed, 766 insertions(+), 109 deletions(-) diff --git a/packages/catalog/CHANGELOG.md b/packages/catalog/CHANGELOG.md index 21be8f50c..62437ba3f 100644 --- a/packages/catalog/CHANGELOG.md +++ b/packages/catalog/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed OpenAI Codex discovery to replace stale bundled models with the authenticated account catalog, preventing unsupported models from remaining selectable. ([#5364](https://github.com/can1357/oh-my-pi/issues/5364)) + ## [16.4.3] - 2026-07-11 ### Fixed diff --git a/packages/catalog/scripts/generate-models.ts b/packages/catalog/scripts/generate-models.ts index 1d9df8b78..423142517 100644 --- a/packages/catalog/scripts/generate-models.ts +++ b/packages/catalog/scripts/generate-models.ts @@ -524,19 +524,25 @@ async function generateModels() { allModels.push(...buildFireworksFastSeed()); const specialDiscoverySources = [ - { label: "Antigravity", fetch: fetchAntigravityModels }, - { label: "Codex", fetch: fetchCodexDiscoveryModels }, + { label: "Antigravity", providerId: "google-antigravity", authoritative: false, fetch: fetchAntigravityModels }, + { label: "Codex", providerId: "openai-codex", authoritative: true, fetch: fetchCodexDiscoveryModels }, ] as const; const specialDiscoveries = await Promise.all( specialDiscoverySources.map(async source => ({ label: source.label, + providerId: source.providerId, + authoritative: source.authoritative, models: await source.fetch(), })), ); + const authoritativeSpecialDiscoveryProviders = new Set(); for (const discovery of specialDiscoveries) { if (discovery.models.length > 0) { console.log(`Added ${discovery.models.length} models from ${discovery.label} discovery`); allModels.push(...discovery.models); + if (discovery.authoritative) { + authoritativeSpecialDiscoveryProviders.add(discovery.providerId); + } } } @@ -563,6 +569,7 @@ async function generateModels() { !DISCOVERY_ONLY_PROVIDERS.has(model.provider) && !RETIRED_PROVIDERS.has(model.provider) && !authoritativeCatalogProviders.has(model.provider) && + !authoritativeSpecialDiscoveryProviders.has(model.provider) && !modelsDevSnapshotExcludedProviders.has(model.provider) ) { allModels.push(model); diff --git a/packages/catalog/src/models.json b/packages/catalog/src/models.json index 35377218c..8e689b13f 100644 --- a/packages/catalog/src/models.json +++ b/packages/catalog/src/models.json @@ -9684,6 +9684,96 @@ }, "contextPromotionTarget": "amazon-bedrock/openai.gpt-5.4" }, + "openai.gpt-5.6-luna": { + "id": "openai.gpt-5.6-luna", + "name": "GPT-5.6 Luna", + "api": "bedrock-converse-stream", + "provider": "amazon-bedrock", + "baseUrl": "https://bedrock-runtime.us-east-1.amazonaws.com", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 1, + "output": 6, + "cacheRead": 0.1, + "cacheWrite": 1.25 + }, + "contextWindow": 272000, + "maxTokens": 128000, + "thinking": { + "mode": "budget", + "efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ] + } + }, + "openai.gpt-5.6-sol": { + "id": "openai.gpt-5.6-sol", + "name": "GPT-5.6 Sol", + "api": "bedrock-converse-stream", + "provider": "amazon-bedrock", + "baseUrl": "https://bedrock-runtime.us-east-1.amazonaws.com", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 5, + "output": 30, + "cacheRead": 0.5, + "cacheWrite": 6.25 + }, + "contextWindow": 272000, + "maxTokens": 128000, + "thinking": { + "mode": "budget", + "efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ] + } + }, + "openai.gpt-5.6-terra": { + "id": "openai.gpt-5.6-terra", + "name": "GPT-5.6 Terra", + "api": "bedrock-converse-stream", + "provider": "amazon-bedrock", + "baseUrl": "https://bedrock-runtime.us-east-1.amazonaws.com", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 2.5, + "output": 15, + "cacheRead": 0.25, + "cacheWrite": 3.125 + }, + "contextWindow": 272000, + "maxTokens": 128000, + "thinking": { + "mode": "budget", + "efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ] + } + }, "openai.gpt-oss-120b": { "id": "openai.gpt-oss-120b", "name": "gpt-oss-120b", @@ -12294,6 +12384,126 @@ }, "contextPromotionTarget": "azure/gpt-5.4" }, + "gpt-5.6-luna": { + "id": "gpt-5.6-luna", + "name": "GPT-5.6 Luna", + "api": "azure-openai-responses", + "provider": "azure", + "baseUrl": "", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 1, + "output": 6, + "cacheRead": 0.1, + "cacheWrite": 0 + }, + "contextWindow": 1050000, + "maxTokens": 128000, + "thinking": { + "mode": "effort", + "efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ] + } + }, + "gpt-5.6-sol": { + "id": "gpt-5.6-sol", + "name": "GPT-5.6 Sol", + "api": "azure-openai-responses", + "provider": "azure", + "baseUrl": "", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 5, + "output": 30, + "cacheRead": 0.5, + "cacheWrite": 0 + }, + "contextWindow": 1050000, + "maxTokens": 128000, + "thinking": { + "mode": "effort", + "efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ] + } + }, + "gpt-5.6-terra": { + "id": "gpt-5.6-terra", + "name": "GPT-5.6 Terra", + "api": "azure-openai-responses", + "provider": "azure", + "baseUrl": "", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 2.5, + "output": 15, + "cacheRead": 0.25, + "cacheWrite": 0 + }, + "contextWindow": 1050000, + "maxTokens": 128000, + "thinking": { + "mode": "effort", + "efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ] + } + }, + "gpt-chat-latest": { + "id": "gpt-chat-latest", + "name": "GPT Chat Latest", + "api": "azure-openai-responses", + "provider": "azure", + "baseUrl": "", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 5, + "output": 30, + "cacheRead": 0.5, + "cacheWrite": 0 + }, + "contextWindow": 128000, + "maxTokens": 16384, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + } + }, "o1": { "id": "o1", "name": "o1", @@ -12913,7 +13123,7 @@ "cost": { "input": 2.25, "output": 2.75, - "cacheRead": 0, + "cacheRead": 2.25, "cacheWrite": 0 }, "contextWindow": 131072, @@ -13725,6 +13935,96 @@ }, "contextPromotionTarget": "cloudflare-ai-gateway/openai/gpt-5.4" }, + "openai/gpt-5.6-luna": { + "id": "openai/gpt-5.6-luna", + "name": "GPT-5.6 Luna", + "api": "anthropic-messages", + "provider": "cloudflare-ai-gateway", + "baseUrl": "https://gateway.ai.cloudflare.com/v1///anthropic", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 1, + "output": 6, + "cacheRead": 0.1, + "cacheWrite": 0 + }, + "contextWindow": 1050000, + "maxTokens": 128000, + "thinking": { + "mode": "budget", + "efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ] + } + }, + "openai/gpt-5.6-sol": { + "id": "openai/gpt-5.6-sol", + "name": "GPT-5.6 Sol", + "api": "anthropic-messages", + "provider": "cloudflare-ai-gateway", + "baseUrl": "https://gateway.ai.cloudflare.com/v1///anthropic", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 5, + "output": 30, + "cacheRead": 0.5, + "cacheWrite": 0 + }, + "contextWindow": 1050000, + "maxTokens": 128000, + "thinking": { + "mode": "budget", + "efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ] + } + }, + "openai/gpt-5.6-terra": { + "id": "openai/gpt-5.6-terra", + "name": "GPT-5.6 Terra", + "api": "anthropic-messages", + "provider": "cloudflare-ai-gateway", + "baseUrl": "https://gateway.ai.cloudflare.com/v1///anthropic", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 2.5, + "output": 15, + "cacheRead": 0.25, + "cacheWrite": 0 + }, + "contextWindow": 1050000, + "maxTokens": 128000, + "thinking": { + "mode": "budget", + "efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ] + } + }, "openai/o1": { "id": "openai/o1", "name": "o1", @@ -13996,6 +14296,35 @@ "xhigh" ] } + }, + "workers-ai/@cf/zai-org/glm-5.2": { + "id": "workers-ai/@cf/zai-org/glm-5.2", + "name": "Glm 5.2", + "api": "anthropic-messages", + "provider": "cloudflare-ai-gateway", + "baseUrl": "https://gateway.ai.cloudflare.com/v1///anthropic", + "reasoning": true, + "input": [ + "text" + ], + "cost": { + "input": 1.4, + "output": 4.4, + "cacheRead": 0.26, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 262144, + "thinking": { + "mode": "budget", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + } } }, "coreweave": { @@ -27805,6 +28134,25 @@ "contextWindow": null, "maxTokens": null }, + "kwaipilot/kat-coder-air-v2.5": { + "id": "kwaipilot/kat-coder-air-v2.5", + "name": "KAT-Coder-Air V2.5", + "api": "openai-completions", + "provider": "kilo", + "baseUrl": "https://api.kilo.ai/api/gateway", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": null, + "maxTokens": null + }, "kwaipilot/kat-coder-pro": { "id": "kwaipilot/kat-coder-pro", "name": "KAT-Coder-Pro V1", @@ -27843,6 +28191,25 @@ "contextWindow": 256000, "maxTokens": 80000 }, + "kwaipilot/kat-coder-pro-v2.5": { + "id": "kwaipilot/kat-coder-pro-v2.5", + "name": "KAT-Coder-Pro V2.5", + "api": "openai-completions", + "provider": "kilo", + "baseUrl": "https://api.kilo.ai/api/gateway", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": null, + "maxTokens": null + }, "liquid/lfm-2-24b-a2b": { "id": "liquid/lfm-2-24b-a2b", "name": "LFM2-24B-A2B", @@ -28244,6 +28611,25 @@ "contextWindow": null, "maxTokens": null }, + "meta/muse-spark-1.1": { + "id": "meta/muse-spark-1.1", + "name": "Muse Spark 1.1", + "api": "openai-completions", + "provider": "kilo", + "baseUrl": "https://api.kilo.ai/api/gateway", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 1048576, + "maxTokens": 1048576 + }, "microsoft/phi-4": { "id": "microsoft/phi-4", "name": "Phi 4", @@ -31042,9 +31428,10 @@ "api": "openai-completions", "provider": "kilo", "baseUrl": "https://api.kilo.ai/api/gateway", - "reasoning": false, + "reasoning": true, "input": [ - "text" + "text", + "image" ], "cost": { "input": 0, @@ -31053,7 +31440,17 @@ "cacheWrite": 0 }, "contextWindow": 372000, - "maxTokens": 128000 + "maxTokens": 128000, + "thinking": { + "mode": "effort", + "efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ] + } }, "openai/gpt-5.6-luna-pro": { "id": "openai/gpt-5.6-luna-pro", @@ -31076,13 +31473,14 @@ }, "openai/gpt-5.6-sol": { "id": "openai/gpt-5.6-sol", - "name": "GPT-5.6 Sol (new)", + "name": "GPT-5.6 Sol", "api": "openai-completions", "provider": "kilo", "baseUrl": "https://api.kilo.ai/api/gateway", - "reasoning": false, + "reasoning": true, "input": [ - "text" + "text", + "image" ], "cost": { "input": 0, @@ -31091,7 +31489,17 @@ "cacheWrite": 0 }, "contextWindow": 372000, - "maxTokens": 128000 + "maxTokens": 128000, + "thinking": { + "mode": "effort", + "efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ] + } }, "openai/gpt-5.6-sol-pro": { "id": "openai/gpt-5.6-sol-pro", @@ -31114,13 +31522,14 @@ }, "openai/gpt-5.6-terra": { "id": "openai/gpt-5.6-terra", - "name": "GPT-5.6 Terra (new)", + "name": "GPT-5.6 Terra", "api": "openai-completions", "provider": "kilo", "baseUrl": "https://api.kilo.ai/api/gateway", - "reasoning": false, + "reasoning": true, "input": [ - "text" + "text", + "image" ], "cost": { "input": 0, @@ -31129,7 +31538,17 @@ "cacheWrite": 0 }, "contextWindow": 372000, - "maxTokens": 128000 + "maxTokens": 128000, + "thinking": { + "mode": "effort", + "efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ] + } }, "openai/gpt-5.6-terra-pro": { "id": "openai/gpt-5.6-terra-pro", @@ -33827,6 +34246,25 @@ ] } }, + "stealth/gpt-5.6-sol": { + "id": "stealth/gpt-5.6-sol", + "name": "GPT-5.6 Sol", + "api": "openai-completions", + "provider": "kilo", + "baseUrl": "https://api.kilo.ai/api/gateway", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 1050000, + "maxTokens": 128000 + }, "stealth/qwen3.6-plus": { "id": "stealth/qwen3.6-plus", "name": "Qwen3.6 Plus", @@ -53109,13 +53547,14 @@ }, "x-ai/grok-4.5": { "id": "x-ai/grok-4.5", - "name": "x-ai/grok-4.5", + "name": "Grok 4.5", "api": "openai-completions", "provider": "nanogpt", "baseUrl": "https://nano-gpt.com/api/v1", - "reasoning": false, + "reasoning": true, "input": [ - "text" + "text", + "image" ], "cost": { "input": 0, @@ -53124,7 +53563,17 @@ "cacheWrite": 0 }, "contextWindow": 500000, - "maxTokens": 500000 + "maxTokens": 500000, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + } }, "x-ai/grok-build-0.1": { "id": "x-ai/grok-build-0.1", @@ -56520,6 +56969,37 @@ "maxTokens": 32000, "supportsTools": true }, + "xai/grok-4.5": { + "id": "xai/grok-4.5", + "name": "xai/grok-4.5", + "api": "openai-completions", + "provider": "novita", + "baseUrl": "https://api.novita.ai/openai/v1", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 2, + "output": 6, + "cacheRead": 0.5, + "cacheWrite": 0 + }, + "contextWindow": 500000, + "maxTokens": 500000, + "supportsTools": true, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + } + }, "xiaomimimo/mimo-v2.5": { "id": "xiaomimimo/mimo-v2.5", "name": "XiaomiMiMo/MiMo-V2.5", @@ -68074,8 +68554,8 @@ "text" ], "cost": { - "input": 0.21, - "output": 0.7899999999999999, + "input": 0.25, + "output": 0.95, "cacheRead": 0.13, "cacheWrite": 0 }, @@ -68249,13 +68729,13 @@ "text" ], "cost": { - "input": 0.08399999999999999, - "output": 0.16799999999999998, - "cacheRead": 0.016800000000000002, + "input": 0.09, + "output": 0.18, + "cacheRead": 0.018, "cacheWrite": 0 }, "contextWindow": 1048576, - "maxTokens": 384000, + "maxTokens": 65536, "thinking": { "mode": "effort", "efforts": [ @@ -68930,13 +69410,13 @@ "image" ], "cost": { - "input": 0.12, + "input": 0.06, "output": 0.35, "cacheRead": 0.09, "cacheWrite": 0 }, "contextWindow": 262144, - "maxTokens": 262144, + "maxTokens": 8192, "thinking": { "mode": "effort", "efforts": [ @@ -68965,7 +69445,7 @@ "cacheWrite": 0 }, "contextWindow": 262144, - "maxTokens": 8192, + "maxTokens": 32768, "thinking": { "mode": "effort", "efforts": [ @@ -69193,6 +69673,25 @@ ] } }, + "kwaipilot/kat-coder-air-v2.5": { + "id": "kwaipilot/kat-coder-air-v2.5", + "name": "KAT-Coder-Air V2.5", + "api": "openrouter", + "provider": "openrouter", + "baseUrl": "https://openrouter.ai/api/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0.15, + "output": 0.6, + "cacheRead": 0.03, + "cacheWrite": 0 + }, + "contextWindow": 256000, + "maxTokens": 80000 + }, "kwaipilot/kat-coder-pro": { "id": "kwaipilot/kat-coder-pro", "name": "KAT-Coder-Pro V1", @@ -69231,6 +69730,25 @@ "contextWindow": 256000, "maxTokens": 80000 }, + "kwaipilot/kat-coder-pro-v2.5": { + "id": "kwaipilot/kat-coder-pro-v2.5", + "name": "KAT-Coder-Pro V2.5", + "api": "openrouter", + "provider": "openrouter", + "baseUrl": "https://openrouter.ai/api/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0.74, + "output": 2.96, + "cacheRead": 0.15, + "cacheWrite": 0 + }, + "contextWindow": 256000, + "maxTokens": 80000 + }, "liquid/lfm-2.5-1.2b-thinking:free": { "id": "liquid/lfm-2.5-1.2b-thinking:free", "name": "LFM2.5-1.2B-Thinking (free)", @@ -69405,8 +69923,8 @@ "image" ], "cost": { - "input": 0.15, - "output": 0.6, + "input": 0.19999999999999998, + "output": 0.7999999999999999, "cacheRead": 0, "cacheWrite": 0 }, @@ -70342,9 +70860,9 @@ "image" ], "cost": { - "input": 0.72, + "input": 0.719, "output": 3.49, - "cacheRead": 0.159, + "cacheRead": 0.149, "cacheWrite": 0 }, "contextWindow": 262144, @@ -71382,7 +71900,7 @@ "cost": { "input": 0.049999999999999996, "output": 0.39999999999999997, - "cacheRead": 0.01, + "cacheRead": 0.005, "cacheWrite": 0 }, "contextWindow": 400000, @@ -71440,7 +71958,7 @@ "cost": { "input": 1.25, "output": 10, - "cacheRead": 0.13, + "cacheRead": 0.125, "cacheWrite": 0 }, "contextWindow": 400000, @@ -72150,8 +72668,8 @@ "text" ], "cost": { - "input": 0.036, - "output": 0.18, + "input": 0.03, + "output": 0.15, "cacheRead": 0, "cacheWrite": 0 }, @@ -73178,7 +73696,7 @@ ], "cost": { "input": 0.09, - "output": 0.09999999999999999, + "output": 0.55, "cacheRead": 0, "cacheWrite": 0 }, @@ -74064,13 +74582,13 @@ "image" ], "cost": { - "input": 0.28500000000000003, + "input": 0.28900000000000003, "output": 2.4, "cacheRead": 0.15, "cacheWrite": 0 }, "contextWindow": 262144, - "maxTokens": 262140, + "maxTokens": 131072, "thinking": { "mode": "effort", "efforts": [ @@ -75452,12 +75970,12 @@ ], "cost": { "input": 0.43, - "output": 1.74, + "output": 1.75, "cacheRead": 0.08, "cacheWrite": 0 }, - "contextWindow": 202752, - "maxTokens": 131072, + "contextWindow": 200000, + "maxTokens": 16384, "thinking": { "mode": "effort", "efforts": [ @@ -75667,13 +76185,13 @@ "text" ], "cost": { - "input": 0.42, - "output": 1.32, - "cacheRead": 0.078, + "input": 0.9099999999999999, + "output": 2.8600000000000003, + "cacheRead": 0.16899999999999998, "cacheWrite": 0 }, "contextWindow": 1048576, - "maxTokens": 131072, + "maxTokens": 128000, "thinking": { "mode": "effort", "efforts": [ @@ -75870,7 +76388,7 @@ "synthetic": { "hf:MiniMaxAI/MiniMax-M3": { "id": "hf:MiniMaxAI/MiniMax-M3", - "name": "MiniMaxAI/MiniMax-M3", + "name": "MiniMax-M3", "api": "openai-completions", "provider": "synthetic", "baseUrl": "https://api.synthetic.new/openai/v1", @@ -75885,7 +76403,7 @@ "cacheRead": 0.6, "cacheWrite": 0 }, - "contextWindow": 262144, + "contextWindow": 524288, "maxTokens": 65536, "thinking": { "mode": "effort", @@ -75900,7 +76418,7 @@ }, "hf:moonshotai/Kimi-K2.7-Code": { "id": "hf:moonshotai/Kimi-K2.7-Code", - "name": "moonshotai/Kimi-K2.7-Code", + "name": "Kimi K2.7 Code", "api": "openai-completions", "provider": "synthetic", "baseUrl": "https://api.synthetic.new/openai/v1", @@ -75930,7 +76448,7 @@ }, "hf:nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4": { "id": "hf:nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4", - "name": "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4", + "name": "Nemotron 3 Super 120B A12B", "api": "openai-completions", "provider": "synthetic", "baseUrl": "https://api.synthetic.new/openai/v1", @@ -75959,7 +76477,7 @@ }, "hf:openai/gpt-oss-120b": { "id": "hf:openai/gpt-oss-120b", - "name": "openai/gpt-oss-120b", + "name": "GPT OSS 120B", "api": "openai-completions", "provider": "synthetic", "baseUrl": "https://api.synthetic.new/openai/v1", @@ -75986,7 +76504,7 @@ }, "hf:Qwen/Qwen3.6-27B": { "id": "hf:Qwen/Qwen3.6-27B", - "name": "Qwen/Qwen3.6-27B", + "name": "Qwen3.6 27B", "api": "openai-completions", "provider": "synthetic", "baseUrl": "https://api.synthetic.new/openai/v1", @@ -76015,7 +76533,7 @@ }, "hf:zai-org/GLM-4.7-Flash": { "id": "hf:zai-org/GLM-4.7-Flash", - "name": "zai-org/GLM-4.7-Flash", + "name": "GLM-4.7-Flash", "api": "openai-completions", "provider": "synthetic", "baseUrl": "https://api.synthetic.new/openai/v1", @@ -76044,7 +76562,7 @@ }, "hf:zai-org/GLM-5.2": { "id": "hf:zai-org/GLM-5.2", - "name": "zai-org/GLM-5.2", + "name": "GLM-5.2", "api": "openai-completions", "provider": "synthetic", "baseUrl": "https://api.synthetic.new/openai/v1", @@ -76983,6 +77501,38 @@ "escapeBuiltinToolNames": true } }, + "umans-deepseek-v4-pro-dspark": { + "id": "umans-deepseek-v4-pro-dspark", + "name": "Umans DeepSeek V4 Pro DSpark (experimental)", + "api": "anthropic-messages", + "provider": "umans", + "baseUrl": "https://api.code.umans.ai", + "reasoning": true, + "thinking": { + "mode": "budget", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + }, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 393216, + "maxTokens": 131071, + "compat": { + "escapeBuiltinToolNames": true + } + }, "umans-flash": { "id": "umans-flash", "name": "Umans Flash", @@ -80352,7 +80902,7 @@ "cacheWrite": 0 }, "contextWindow": 200000, - "maxTokens": 24000, + "maxTokens": 80000, "thinking": { "mode": "effort", "efforts": [ @@ -81315,7 +81865,7 @@ "cacheWrite": 18.75 }, "contextWindow": 200000, - "maxTokens": 32000, + "maxTokens": 8192, "thinking": { "mode": "budget", "efforts": [ @@ -81496,7 +82046,7 @@ "cacheWrite": 3.75 }, "contextWindow": 1000000, - "maxTokens": 64000, + "maxTokens": 8192, "thinking": { "mode": "budget", "efforts": [ @@ -81813,12 +82363,12 @@ "text" ], "cost": { - "input": 0.6, - "output": 1.7, - "cacheRead": 0.28, + "input": 0.25, + "output": 0.95, + "cacheRead": 0.13, "cacheWrite": 0 }, - "contextWindow": 128000, + "contextWindow": 163840, "maxTokens": 128000, "thinking": { "mode": "budget", @@ -82468,6 +83018,35 @@ ] } }, + "kwaipilot/kat-coder-air-v2.5": { + "id": "kwaipilot/kat-coder-air-v2.5", + "name": "Kat Coder Air V2.5", + "api": "anthropic-messages", + "provider": "vercel-ai-gateway", + "baseUrl": "https://ai-gateway.vercel.sh", + "reasoning": true, + "input": [ + "text" + ], + "cost": { + "input": 0.15, + "output": 0.6, + "cacheRead": 0.03, + "cacheWrite": 0 + }, + "contextWindow": 256000, + "maxTokens": 80000, + "thinking": { + "mode": "budget", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + } + }, "kwaipilot/kat-coder-pro-v1": { "id": "kwaipilot/kat-coder-pro-v1", "name": "KAT-Coder-Pro V1", @@ -82506,6 +83085,35 @@ "contextWindow": 256000, "maxTokens": 256000 }, + "kwaipilot/kat-coder-pro-v2.5": { + "id": "kwaipilot/kat-coder-pro-v2.5", + "name": "Kat Coder Pro V2.5", + "api": "anthropic-messages", + "provider": "vercel-ai-gateway", + "baseUrl": "https://ai-gateway.vercel.sh", + "reasoning": true, + "input": [ + "text" + ], + "cost": { + "input": 0.74, + "output": 2.96, + "cacheRead": 0.15, + "cacheWrite": 0 + }, + "contextWindow": 256000, + "maxTokens": 80000, + "thinking": { + "mode": "budget", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + } + }, "meituan/longcat-flash-chat": { "id": "meituan/longcat-flash-chat", "name": "LongCat Flash Chat", @@ -84556,7 +85164,7 @@ }, "openai/gpt-5.6-luna": { "id": "openai/gpt-5.6-luna", - "name": "GPT 5.6 Luna", + "name": "GPT-5.6 Luna", "api": "anthropic-messages", "provider": "vercel-ai-gateway", "baseUrl": "https://ai-gateway.vercel.sh", @@ -84586,7 +85194,7 @@ }, "openai/gpt-5.6-sol": { "id": "openai/gpt-5.6-sol", - "name": "GPT 5.6 Sol", + "name": "GPT-5.6 Sol", "api": "anthropic-messages", "provider": "vercel-ai-gateway", "baseUrl": "https://ai-gateway.vercel.sh", @@ -84616,7 +85224,7 @@ }, "openai/gpt-5.6-terra": { "id": "openai/gpt-5.6-terra", - "name": "GPT 5.6 Terra", + "name": "GPT-5.6 Terra", "api": "anthropic-messages", "provider": "vercel-ai-gateway", "baseUrl": "https://ai-gateway.vercel.sh", @@ -86132,9 +86740,9 @@ "text" ], "cost": { - "input": 3, - "output": 10.25, - "cacheRead": 0.5, + "input": 2.0999999999999996, + "output": 6.6000000000000005, + "cacheRead": 0.21, "cacheWrite": 0 }, "contextWindow": 1000000, @@ -87299,9 +87907,9 @@ }, "includeEncryptedReasoning": false, "filterReasoningHistory": true, + "supportsImageDetailOriginal": false, "omitReasoningEffort": true, - "supportsReasoningEffort": false, - "supportsImageDetailOriginal": false + "supportsReasoningEffort": false } }, "grok-4.20-0309-reasoning": { @@ -87329,9 +87937,9 @@ }, "includeEncryptedReasoning": false, "filterReasoningHistory": true, + "supportsImageDetailOriginal": false, "omitReasoningEffort": true, - "supportsReasoningEffort": false, - "supportsImageDetailOriginal": false + "supportsReasoningEffort": false } }, "grok-4.20-multi-agent-0309": { @@ -87352,6 +87960,16 @@ }, "contextWindow": 2000000, "maxTokens": 2000000, + "compat": { + "reasoningEffortMap": { + "minimal": "low" + }, + "includeEncryptedReasoning": false, + "filterReasoningHistory": true, + "supportsImageDetailOriginal": false, + "omitReasoningEffort": false, + "supportsReasoningEffort": true + }, "thinking": { "mode": "effort", "efforts": [ @@ -87364,16 +87982,6 @@ "effortMap": { "minimal": "low" } - }, - "compat": { - "reasoningEffortMap": { - "minimal": "low" - }, - "includeEncryptedReasoning": false, - "filterReasoningHistory": true, - "omitReasoningEffort": false, - "supportsReasoningEffort": true, - "supportsImageDetailOriginal": false } }, "grok-4.3": { @@ -87395,6 +88003,16 @@ }, "contextWindow": 1000000, "maxTokens": 1000000, + "compat": { + "reasoningEffortMap": { + "minimal": "low" + }, + "includeEncryptedReasoning": false, + "filterReasoningHistory": true, + "supportsImageDetailOriginal": false, + "omitReasoningEffort": false, + "supportsReasoningEffort": true + }, "thinking": { "mode": "effort", "efforts": [ @@ -87407,16 +88025,6 @@ "effortMap": { "minimal": "low" } - }, - "compat": { - "reasoningEffortMap": { - "minimal": "low" - }, - "includeEncryptedReasoning": false, - "filterReasoningHistory": true, - "omitReasoningEffort": false, - "supportsReasoningEffort": true, - "supportsImageDetailOriginal": false } }, "grok-4.5": { @@ -87438,6 +88046,16 @@ }, "contextWindow": 500000, "maxTokens": 500000, + "compat": { + "reasoningEffortMap": { + "minimal": "low" + }, + "includeEncryptedReasoning": false, + "filterReasoningHistory": true, + "supportsImageDetailOriginal": false, + "omitReasoningEffort": false, + "supportsReasoningEffort": true + }, "thinking": { "mode": "effort", "efforts": [ @@ -87450,16 +88068,6 @@ "effortMap": { "minimal": "low" } - }, - "compat": { - "reasoningEffortMap": { - "minimal": "low" - }, - "includeEncryptedReasoning": false, - "filterReasoningHistory": true, - "omitReasoningEffort": false, - "supportsReasoningEffort": true, - "supportsImageDetailOriginal": false } }, "grok-build": { @@ -87487,9 +88095,9 @@ }, "includeEncryptedReasoning": false, "filterReasoningHistory": true, + "supportsImageDetailOriginal": false, "omitReasoningEffort": true, - "supportsReasoningEffort": false, - "supportsImageDetailOriginal": false + "supportsReasoningEffort": false } }, "grok-build-0.1": { @@ -87517,9 +88125,9 @@ }, "includeEncryptedReasoning": false, "filterReasoningHistory": true, + "supportsImageDetailOriginal": false, "omitReasoningEffort": true, - "supportsReasoningEffort": false, - "supportsImageDetailOriginal": false + "supportsReasoningEffort": false } }, "grok-composer-2.5-fast": { @@ -87546,9 +88154,9 @@ }, "includeEncryptedReasoning": false, "filterReasoningHistory": true, + "supportsImageDetailOriginal": false, "omitReasoningEffort": true, - "supportsReasoningEffort": false, - "supportsImageDetailOriginal": false + "supportsReasoningEffort": false } } }, diff --git a/packages/catalog/src/provider-models/special.ts b/packages/catalog/src/provider-models/special.ts index be8329bb7..2bd937130 100644 --- a/packages/catalog/src/provider-models/special.ts +++ b/packages/catalog/src/provider-models/special.ts @@ -21,6 +21,7 @@ export function openaiCodexModelManagerOptions( const { accessToken, accountId, clientVersion } = config; return { providerId: "openai-codex", + dynamicModelsAuthoritative: true, ...(accessToken ? { fetchDynamicModels: async () => { diff --git a/packages/catalog/test/codex-discovery.test.ts b/packages/catalog/test/codex-discovery.test.ts index b647ac895..73f11d54f 100644 --- a/packages/catalog/test/codex-discovery.test.ts +++ b/packages/catalog/test/codex-discovery.test.ts @@ -7,6 +7,7 @@ import { buildModel } from "@oh-my-pi/pi-catalog/build"; import { fetchCodexModels } from "@oh-my-pi/pi-catalog/discovery/codex"; import { writeModelCache } from "@oh-my-pi/pi-catalog/model-cache"; import { resolveProviderModels } from "@oh-my-pi/pi-catalog/model-manager"; +import { openaiCodexModelManagerOptions } from "@oh-my-pi/pi-catalog/provider-models/special"; import type { ModelSpec } from "@oh-my-pi/pi-catalog/types"; describe("Codex model discovery", () => { @@ -100,6 +101,42 @@ describe("Codex model discovery", () => { expect(legacy?.useResponsesLite).toBeUndefined(); }); + it("uses the discovered account catalog as authoritative", async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "pi-catalog-codex-authoritative-")); + const staticOnlyModel: ModelSpec<"openai-codex-responses"> = { + id: "unsupported-static", + name: "Unsupported static model", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 272_000, + maxTokens: 128_000, + }; + const discoveredModel: ModelSpec<"openai-codex-responses"> = { + ...staticOnlyModel, + id: "account-supported", + name: "Account-supported model", + }; + try { + const result = await resolveProviderModels( + { + ...openaiCodexModelManagerOptions(), + staticModels: [staticOnlyModel], + cacheDbPath: path.join(tempDir, "models.db"), + fetchDynamicModels: async () => [discoveredModel], + }, + "online", + ); + + expect(result.models.map(model => model.id)).toEqual(["account-supported"]); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); + it("ignores pre-V2 Codex discovery cache rows", async () => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "pi-catalog-codex-v7-cache-")); const dbPath = path.join(tempDir, "models.db"); diff --git a/packages/coding-agent/src/prompts/system/tan-context-switch.md b/packages/coding-agent/src/prompts/system/tan-context-switch.md index 55468b15a..88cd57291 100644 --- a/packages/coding-agent/src/prompts/system/tan-context-switch.md +++ b/packages/coding-agent/src/prompts/system/tan-context-switch.md @@ -1,5 +1,5 @@ -The conversation above belongs to your parent session. +The conversation above belongs to your parent session. You are a fork created solely to handle the user's request below. Your parent agent is still working on the original task — that responsibility is diff --git a/packages/coding-agent/test/agent-session-prune-persistence.test.ts b/packages/coding-agent/test/agent-session-prune-persistence.test.ts index 3b78c3657..438f0de4e 100644 --- a/packages/coding-agent/test/agent-session-prune-persistence.test.ts +++ b/packages/coding-agent/test/agent-session-prune-persistence.test.ts @@ -114,7 +114,7 @@ describe("AgentSession per-turn prune persistence", () => { const message = session.agent.state.messages.find( candidate => candidate.role === "toolResult" && candidate.toolCallId === BIG_CALL_ID, ); - if (!message || message.role !== "toolResult" || !Array.isArray(message.content)) { + if (message?.role !== "toolResult" || !Array.isArray(message.content)) { throw new Error("Expected the seeded tool result in live agent state"); } const text = message.content.find(block => block.type === "text"); @@ -156,7 +156,7 @@ describe("AgentSession per-turn prune persistence", () => { const rebuilt = reloaded .buildSessionContext() .messages.find(candidate => candidate.role === "toolResult" && candidate.toolCallId === BIG_CALL_ID); - if (!rebuilt || rebuilt.role !== "toolResult" || !Array.isArray(rebuilt.content)) { + if (rebuilt?.role !== "toolResult" || !Array.isArray(rebuilt.content)) { throw new Error("Expected the seeded tool result in the from-disk rebuild"); } const rebuiltText = rebuilt.content.find(block => block.type === "text"); From 5ec402fc10ad486981a48745ef86bf16fc1ca825 Mon Sep 17 00:00:00 2001 From: roboomp Date: Tue, 14 Jul 2026 20:47:46 +0000 Subject: [PATCH 2/2] fix(catalog): force codex refresh for authoritative pruning Built-in discovery skipped the OAuth refresh whenever a fresh authoritative cache existed, so an openai-codex user with an expired access token never got the model manager constructed and stale bundled models (e.g. gpt-5.4-nano) stayed selectable for the full cache TTL. Force the refresh for authoritative providers and forward the registry fetch through the Codex manager so discovery honors the configured transport. Fixes #5364 --- packages/catalog/CHANGELOG.md | 1 + packages/catalog/src/discovery/codex.ts | 4 +- .../catalog/src/provider-models/special.ts | 5 +- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/config/model-registry.ts | 28 +++++++++-- .../coding-agent/test/model-discovery.test.ts | 47 +++++++++++++++++++ 6 files changed, 79 insertions(+), 7 deletions(-) diff --git a/packages/catalog/CHANGELOG.md b/packages/catalog/CHANGELOG.md index 62437ba3f..2017e588b 100644 --- a/packages/catalog/CHANGELOG.md +++ b/packages/catalog/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - Fixed OpenAI Codex discovery to replace stale bundled models with the authenticated account catalog, preventing unsupported models from remaining selectable. ([#5364](https://github.com/can1357/oh-my-pi/issues/5364)) +- Fixed OpenAI Codex discovery ignoring the caller-supplied `fetch`, so it always hit the global network instead of the configured (proxy/extra-CA/test) fetch. ([#5364](https://github.com/can1357/oh-my-pi/issues/5364)) ## [16.4.3] - 2026-07-11 diff --git a/packages/catalog/src/discovery/codex.ts b/packages/catalog/src/discovery/codex.ts index 33dad7763..a9031458e 100644 --- a/packages/catalog/src/discovery/codex.ts +++ b/packages/catalog/src/discovery/codex.ts @@ -1,5 +1,5 @@ import { type } from "arktype"; -import type { ModelSpec } from "../types"; +import type { FetchImpl, ModelSpec } from "../types"; import { discoveryFetch } from "../utils"; import { CODEX_BASE_URL, CODEX_CLIENT_VERSION, OPENAI_HEADER_VALUES, OPENAI_HEADERS } from "../wire/codex"; @@ -60,7 +60,7 @@ export interface CodexModelDiscoveryOptions { /** Abort signal for network request cancellation. */ signal?: AbortSignal; /** Optional fetch implementation override for tests. */ - fetchFn?: typeof fetch; + fetchFn?: FetchImpl; } /** diff --git a/packages/catalog/src/provider-models/special.ts b/packages/catalog/src/provider-models/special.ts index 2bd937130..e747f6de6 100644 --- a/packages/catalog/src/provider-models/special.ts +++ b/packages/catalog/src/provider-models/special.ts @@ -13,19 +13,20 @@ export interface OpenAICodexModelManagerConfig { accessToken?: string; accountId?: string; clientVersion?: string; + fetch?: FetchImpl; } export function openaiCodexModelManagerOptions( config: OpenAICodexModelManagerConfig = {}, ): ModelManagerOptions<"openai-codex-responses"> { - const { accessToken, accountId, clientVersion } = config; + const { accessToken, accountId, clientVersion, fetch } = config; return { providerId: "openai-codex", dynamicModelsAuthoritative: true, ...(accessToken ? { fetchDynamicModels: async () => { - const result = await fetchCodexModels({ accessToken, accountId, clientVersion }); + const result = await fetchCodexModels({ accessToken, accountId, clientVersion, fetchFn: fetch }); return result?.models ?? null; }, } diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 9fe2494f3..75d6ef564 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -43,6 +43,7 @@ - Fixed launch tool rendering stacking a stale pending header over a bare `✓ Launch` line and raw text: the tool now uses a merged registry renderer with one per-op status header (op, target, `state · pid · uptime` meta), stripped log cursor suffixes, capped collapsed log/list previews, and a launch tool glyph - Fixed confusing launch start/wait results when readiness timed out with the log pattern already matched (readiness needs log AND port): the result printed a contradictory `Ready: ` next to `Readiness timed out` without naming the failing condition. Daemon snapshots now carry the unmet conditions (`readyPending`), and start/wait results state exactly what never happened (e.g. `port 3100 on 127.0.0.1 never accepted connections`); the TUI shows a `waiting on port` badge on starting daemons - Fixed the in-process `stat` builtin mangling BSD-style invocations like `stat -f "%Sm %N" file` (macOS muscle memory): GNU `-f` means `--file-system`, so the format string was treated as a file operand — printing filesystem info for the real operands and erroring with `cannot read file system information for '%Sm %N'`. A `-f` whose format value contains `%` is now detected as BSD syntax and translated to the GNU equivalent (`%Sm`→`%y`, `%N`→`%n`, `%z`→`%s`, epoch/`S`-form times, owner/group/permission and `H`/`L` sub-field directives, `-L`/`-n`/`-q`/`-F` flag clusters, with `%n`/`%t` as literal newline/tab); directives with no GNU counterpart fail with a clear `unsupported BSD format directive` error +- Fixed authoritative providers (e.g. `openai-codex`) keeping unsupported bundled models selectable when a fresh model cache and an expired OAuth token coincided: built-in discovery now forces the OAuth refresh so the provider's model manager is constructed and prunes stale bundled entries (e.g. `gpt-5.4-nano`) instead of waiting out the cache TTL. ([#5364](https://github.com/can1357/oh-my-pi/issues/5364)) - Fixed the remaining GNU-flavored shell builtins that broke under macOS/BSD muscle memory, using the same unambiguous-detection approach as the `stat` fix (only invocations that are invalid or nonsensical under GNU semantics are reinterpreted; unsupported BSD forms fail loudly instead of producing wrong output): `date -r ` formats the epoch when no such file exists (GNU `-r FILE` mtime preserved), signed `date -v±N` adjustments translate to `-d` relative dates and `-j` is accepted (`-j -f` strptime parse mode and field-set `-v` error clearly); `sed -i '' 's/…/…/' file` drops the BSD empty backup-suffix token instead of treating it as the script; `mktemp -t prefix` without X's creates `$TMPDIR/prefix.XXXXXXXXXX` (the GNU `too few X's` error path); `tail -r` reverses input by delegating to `tac` (with `-n`/`-c`/`-f` combinations erroring clearly); `find -E` maps to `-regextype posix-extended` ahead of the expression; `base64 -D` decodes as an alias of `-d`; and `ln -sfh` works via a `-h` alias of `--no-dereference` (clap's `-h` help short is dropped to match real GNU/BSD ln; `--help` unchanged) ## [16.4.8] - 2026-07-12 diff --git a/packages/coding-agent/src/config/model-registry.ts b/packages/coding-agent/src/config/model-registry.ts index 7fb1491b6..976c89b60 100644 --- a/packages/coding-agent/src/config/model-registry.ts +++ b/packages/coding-agent/src/config/model-registry.ts @@ -1592,6 +1592,7 @@ export class ModelRegistry { providerId: string, strategy: ModelRefreshStrategy, cacheProviderId: string, + authoritative: boolean, ): Promise { const peekedKey = await this.#peekApiKeyForProvider(providerId); if (isAuthenticated(peekedKey) || strategy === "offline") { @@ -1601,7 +1602,13 @@ export class ModelRegistry { if (oauthCredentials.length === 0) { return peekedKey; } - if (strategy === "online-if-uncached") { + // Authoritative providers prune bundled models only when their manager is + // actually constructed, which needs an authenticated key. A fresh cache does + // not let us skip the refresh here: with an expired OAuth token peekedKey is + // undefined, the manager is never added, and stale bundled models survive the + // full cache TTL. So only take the no-refresh shortcut for non-authoritative + // providers, whose bundled models stay visible regardless. + if (strategy === "online-if-uncached" && !authoritative) { // Mirror shouldFetchRemoteSources: built-in managers use the catalog's // default TTL, so only refresh when the manager will actually fetch. const cache = readModelCache( @@ -1633,11 +1640,13 @@ export class ModelRegistry { ): Promise[]> { const specialProviderDescriptors: Array<{ providerId: string; + authoritative: boolean; resolveKey: (value: string | undefined) => string | undefined; createOptions: (key: string) => ModelManagerOptions; }> = [ { providerId: "google-antigravity", + authoritative: false, resolveKey: extractGoogleOAuthToken, createOptions: oauthToken => googleAntigravityModelManagerOptions({ @@ -1648,6 +1657,7 @@ export class ModelRegistry { }, { providerId: "google-gemini-cli", + authoritative: false, resolveKey: extractGoogleOAuthToken, createOptions: oauthToken => googleGeminiCliModelManagerOptions({ @@ -1658,12 +1668,14 @@ export class ModelRegistry { }, { providerId: "openai-codex", + authoritative: true, resolveKey: value => value, createOptions: accessToken => { const accountId = resolveOAuthAccountIdForAccessToken(this.authStorage, "openai-codex", accessToken); return openaiCodexModelManagerOptions({ accessToken, accountId, + fetch: this.#fetch, }); }, }, @@ -1688,12 +1700,22 @@ export class ModelRegistry { const cacheProviderId = descriptor.createModelManagerOptions({ baseUrl: discoveryBaseUrl, fetch: this.#fetch }) .cacheProviderId ?? descriptor.providerId; - return this.#resolveBuiltInDiscoveryApiKey(descriptor.providerId, strategy, cacheProviderId); + return this.#resolveBuiltInDiscoveryApiKey( + descriptor.providerId, + strategy, + cacheProviderId, + descriptor.dynamicModelsAuthoritative ?? false, + ); }), ); const specialKeys = await Promise.all( enabledSpecialProviderDescriptors.map(descriptor => - this.#resolveBuiltInDiscoveryApiKey(descriptor.providerId, strategy, descriptor.providerId), + this.#resolveBuiltInDiscoveryApiKey( + descriptor.providerId, + strategy, + descriptor.providerId, + descriptor.authoritative, + ), ), ); const options: ModelManagerOptions[] = []; diff --git a/packages/coding-agent/test/model-discovery.test.ts b/packages/coding-agent/test/model-discovery.test.ts index d700be529..eb7d4492f 100644 --- a/packages/coding-agent/test/model-discovery.test.ts +++ b/packages/coding-agent/test/model-discovery.test.ts @@ -279,6 +279,53 @@ describe("ModelRegistry runtime discovery", () => { expect(authStorage.getOAuthCredential("anthropic")?.access).toBe("sk-ant-oat-expired-anthropic"); }); + test("online-if-uncached refreshes expired OAuth for authoritative providers even when the cache is fresh", async () => { + // Regression for #5364: openai-codex is authoritative, so its bundled + // models are pruned only when the manager is actually constructed — which + // needs an authenticated key. With an expired OAuth token peekApiKey + // returns undefined; the fresh-cache shortcut must NOT skip the refresh, or + // the manager is never added and unsupported bundled ids (gpt-5.4-nano) + // remain selectable for the whole cache TTL. + const { refreshCalls } = await useAuthStorageWithRefreshTracker(); + await authStorage.set("openai-codex", { + type: "oauth", + access: "expired-openai-codex", + refresh: "refresh-openai-codex", + expires: Date.now() - 60_000, + }); + // Fresh + authoritative, but written against no static fingerprint so the + // constructed manager still performs the account-scoped fetch. + writeModelCache("openai-codex", Date.now() - 60_000, [], true, "", cacheDbPath); + let modelListCalls = 0; + const fetchMock: FetchImpl = async (input, init) => { + const url = String(input); + if (url.startsWith("https://chatgpt.com/backend-api") && url.includes("/models")) { + modelListCalls++; + expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer fresh-openai-codex"); + return Response.json({ + models: [ + { + slug: "gpt-5.6-terra", + display_name: "GPT-5.6 Terra", + context_window: 372_000, + supported_in_api: true, + input_modalities: ["text", "image"], + }, + ], + }); + } + throw new Error(`Unexpected URL: ${url}`); + }; + const registry = new ModelRegistry(authStorage, modelsJsonPath, { fetch: fetchMock }); + + await registry.refreshProvider("openai-codex", "online-if-uncached"); + + expect(refreshCalls).toEqual(["openai-codex"]); + expect(modelListCalls).toBe(1); + expect(registry.find("openai-codex", "gpt-5.6-terra")).toBeDefined(); + expect(registry.find("openai-codex", "gpt-5.4-nano")).toBeUndefined(); + }); + test("configured discovery suppresses built-in special OAuth discovery", async () => { await authStorage.set("google-gemini-cli", { type: "oauth",