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
This commit is contained in:
roboomp
2026-07-14 20:47:46 +00:00
parent 51ab2fcf23
commit 5ec402fc10
6 changed files with 79 additions and 7 deletions
@@ -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",