From fed5d3d9512697f4c26a986e85f2472ffc56d237 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 9 Jul 2026 23:12:02 -0700 Subject: [PATCH] FN-7759: fix GPT-5.6 codex models missing from model dropdown Ensure GPT-5.6 codex models (luna, sol, terra) actually surface in the codex model picker by validating the openai-codex supplemental merge against the real pi-coding-agent ModelRegistry instead of only a mocked one. - Harden openai-models.ts supplemental merge logic against real ModelRegistry auth filtering, registerProvider full-replacement, and OAuth provider validation - Add @earendil-works/pi-coding-agent devDependency to @fusion/core for real-registry testing - Add regression test (openai-models.test.ts) exercising the real registry path - Extend register-model-routes-openai-codex-supplemental.test.ts dashboard test coverage - Update docs/settings-reference.md - Add changeset fn-7759-codex-gpt-5-6-dropdown.md (patch, fix) Files changed: .changeset/fn-7759-codex-gpt-5-6-dropdown.md | 7 ++ docs/settings-reference.md | 2 +- packages/core/package.json | 1 + packages/core/src/__tests__/openai-models.test.ts | 68 +++++++++++++ packages/core/src/openai-models.ts | 107 ++++++++++++++++----- ...-model-routes-openai-codex-supplemental.test.ts | 77 +++++++++++++-- pnpm-lock.yaml | 68 +++++++++++++ 7 files changed, 300 insertions(+), 30 deletions(-) Fusion-Task-Id: FN-7759 Fusion-Task-Lineage: 4c03838f-b8d8-4f6f-ae55-c3356c1c3176 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-7759-codex-gpt-5-6-dropdown.md | 7 ++ docs/settings-reference.md | 2 +- packages/core/package.json | 1 + .../core/src/__tests__/openai-models.test.ts | 68 +++++++++++ packages/core/src/openai-models.ts | 107 ++++++++++++++---- ...l-routes-openai-codex-supplemental.test.ts | 77 +++++++++++-- pnpm-lock.yaml | 68 +++++++++++ 7 files changed, 300 insertions(+), 30 deletions(-) create mode 100644 .changeset/fn-7759-codex-gpt-5-6-dropdown.md diff --git a/.changeset/fn-7759-codex-gpt-5-6-dropdown.md b/.changeset/fn-7759-codex-gpt-5-6-dropdown.md new file mode 100644 index 0000000000..c3d68e4d44 --- /dev/null +++ b/.changeset/fn-7759-codex-gpt-5-6-dropdown.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: GPT-5.6 codex models (luna, sol, terra) now actually appear in the codex model picker. +category: fix +dev: Prior fixes (FN-7742/7745/7754) validated the openai-codex supplemental merge only against a mocked ModelRegistry, so gpt-5.6-luna/sol/terra could fail to reach the picker through the real pi-coding-agent registry (getAvailable() auth filtering + registerProvider full-replacement + OAuth provider validation) and/or the /api/models configuredProviders filter. This closes that gap and adds a real-registry regression test. diff --git a/docs/settings-reference.md b/docs/settings-reference.md index 93c609c77f..102d19282f 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -977,7 +977,7 @@ When the Cursor Runtime plugin (`fusion-plugin-cursor-runtime`) is installed and When the Grok Runtime plugin (`fusion-plugin-grok-runtime`) is installed and the `useGrokCli` toggle is enabled (Settings → Authentication), Grok CLI-discovered models (`grok models`) are surfaced additively in `/api/models` under the `grok-cli` provider — id/name derived from the discovered model id/label. This surfacing is fetched through a short-TTL, single-flight cache so the model picker never spawns `grok` on every request; a missing/failed/unavailable Grok CLI binary simply yields zero `grok-cli` rows without affecting other providers. Disabling `useGrokCli` hides all `grok-cli` rows. Unlike Cursor (OAuth/session auth), Grok direct-endpoint auth is API-key based, but CLI-routed execution lets the `grok` binary use any auth source it supports; the Settings card still surfaces Fusion-visible key detection only as an informational hint. -The three GPT-5.6 codenamed OpenAI Codex variants (`gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`) are additively surfaced under the `openai-codex` provider (FN-7745/FN-7754, mirroring the Anthropic/Z.ai supplemental-merge pattern above) so they appear both in dashboard `/api/models` and the engine/pi `createFnAgent` registry-seeding surface whenever `openai-codex` is configured — deduped against any pinned pi-ai catalog row that already carries one of the ids. +The three GPT-5.6 codenamed OpenAI Codex variants (`gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`) are additively surfaced under the `openai-codex` provider (FN-7745/FN-7754/FN-7759, mirroring the Anthropic/Z.ai supplemental-merge pattern above) so they appear both in dashboard `/api/models` and the engine/pi `createFnAgent` registry-seeding surface whenever `openai-codex` is configured — deduped against any pinned pi-ai catalog row that already carries one of the ids. FN-7759 specifically keeps the supplemental registration compatible with the real pi-coding-agent `ModelRegistry` by preserving the OpenAI Codex OAuth provider during dynamic full-provider replacement, so legacy catalogs without native 5.6 rows still survive `getAvailable()` auth filtering and remain executable. ### Planning model diff --git a/packages/core/package.json b/packages/core/package.json index 0f7ebc17a6..77f489acc4 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -39,6 +39,7 @@ "test": "vitest run --silent=passed-only --reporter=dot" }, "devDependencies": { + "@earendil-works/pi-coding-agent": "^0.80.6", "@types/dockerode": "^3.3.41", "@types/node": "^25.5.0", "@vitest/coverage-v8": "^4.1.0", diff --git a/packages/core/src/__tests__/openai-models.test.ts b/packages/core/src/__tests__/openai-models.test.ts index 86768d4a6a..f3a9b9ca89 100644 --- a/packages/core/src/__tests__/openai-models.test.ts +++ b/packages/core/src/__tests__/openai-models.test.ts @@ -1,3 +1,4 @@ +import { ModelRegistry } from "@earendil-works/pi-coding-agent"; import { describe, expect, it } from "vitest"; import { GPT_5_6_LUNA_MODEL_ID, @@ -9,6 +10,42 @@ import { } from "../openai-models.js"; const EXPECTED_IDS = [GPT_5_6_LUNA_MODEL_ID, GPT_5_6_SOL_MODEL_ID, GPT_5_6_TERRA_MODEL_ID]; +const BUILT_IN_CODEX_IDS = ["gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini", "gpt-5.5"]; +const OPENAI_CODEX_OAUTH_CREDENTIAL = { + refresh: "test-refresh-token", + access: "test-access-token", + expires: Date.now() + 60_000, +}; +const OPENAI_CODEX_OAUTH_PROVIDER = { + id: OPENAI_CODEX_PROVIDER_ID, + name: "ChatGPT Plus/Pro (Codex Subscription)", + login: async () => OPENAI_CODEX_OAUTH_CREDENTIAL, + refreshToken: async () => OPENAI_CODEX_OAUTH_CREDENTIAL, + getApiKey: () => OPENAI_CODEX_OAUTH_CREDENTIAL.access, +}; + +function createOpenAiCodexAuthStorage() { + const credential = { + type: "oauth", + access: "test-access-token", + refresh: "test-refresh-token", + expires: Date.now() + 60_000, + }; + + return { + getOAuthProviders: () => [OPENAI_CODEX_OAUTH_PROVIDER], + get: (providerId: string) => providerId === OPENAI_CODEX_PROVIDER_ID ? credential : undefined, + hasAuth: (providerId: string) => providerId === OPENAI_CODEX_PROVIDER_ID, + getProviderEnv: () => ({}), + getApiKey: async (providerId: string) => providerId === OPENAI_CODEX_PROVIDER_ID ? credential.access : undefined, + }; +} + +function createRealRegistryWithLegacyCodexCatalog() { + const registry = ModelRegistry.inMemory(createOpenAiCodexAuthStorage() as never) as unknown as ModelRegistry & { models: Array> }; + registry.models = registry.models.filter((model) => model.provider !== OPENAI_CODEX_PROVIDER_ID || !EXPECTED_IDS.includes(String(model.id))); + return registry; +} describe("SUPPLEMENTAL_OPENAI_CODEX_PROVIDER_REGISTRATION", () => { it("targets the openai-codex-responses API and ChatGPT backend baseUrl", () => { @@ -131,4 +168,35 @@ describe("mergeSupplementalOpenAiCodexModels", () => { expect(() => mergeSupplementalOpenAiCodexModels(registry)).not.toThrow(); expect(registeredProviders.has(OPENAI_CODEX_PROVIDER_ID)).toBe(true); }); + + it("surfaces GPT-5.6 rows through the real ModelRegistry auth and full-replacement path", () => { + const registry = createRealRegistryWithLegacyCodexCatalog(); + const beforeIds = registry.getAvailable() + .filter((model) => model.provider === OPENAI_CODEX_PROVIDER_ID) + .map((model) => model.id); + expect(beforeIds).not.toEqual(expect.arrayContaining(EXPECTED_IDS)); + expect(beforeIds).toEqual(expect.arrayContaining(BUILT_IN_CODEX_IDS)); + + const warnings: string[] = []; + mergeSupplementalOpenAiCodexModels(registry, (message) => warnings.push(message)); + + expect(warnings).toEqual([]); + const codexRows = registry.getAvailable().filter((model) => model.provider === OPENAI_CODEX_PROVIDER_ID); + const codexIds = codexRows.map((model) => model.id); + expect(codexIds).toEqual(expect.arrayContaining([...BUILT_IN_CODEX_IDS, ...EXPECTED_IDS])); + expect(new Set(codexIds).size).toBe(codexIds.length); + + for (const id of EXPECTED_IDS) { + const row = codexRows.find((model) => model.id === id); + expect(row).toMatchObject({ + provider: OPENAI_CODEX_PROVIDER_ID, + api: "openai-codex-responses", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + contextWindow: 372_000, + maxTokens: 128_000, + }); + expect(row?.thinkingLevelMap).toMatchObject({ xhigh: "xhigh", max: "max", minimal: "low" }); + } + }); }); diff --git a/packages/core/src/openai-models.ts b/packages/core/src/openai-models.ts index 6755b9bb1d..4283f3d611 100644 --- a/packages/core/src/openai-models.ts +++ b/packages/core/src/openai-models.ts @@ -5,16 +5,44 @@ export const GPT_5_6_LUNA_MODEL_ID = "gpt-5.6-luna"; export const GPT_5_6_SOL_MODEL_ID = "gpt-5.6-sol"; export const GPT_5_6_TERRA_MODEL_ID = "gpt-5.6-terra"; +interface OpenAiCodexCostTier { + inputTokensAbove: number; + input: number; + output: number; + cacheRead: number; + cacheWrite: number; +} + +interface OpenAiCodexOAuthCredentials { + refresh: string; + access: string; + expires: number; + [key: string]: unknown; +} + +interface OpenAiCodexOAuthProviderRegistration { + id?: unknown; + name: string; + login: (...args: never[]) => Promise; + refreshToken: (credentials: OpenAiCodexOAuthCredentials) => Promise; + getApiKey: (credentials: OpenAiCodexOAuthCredentials) => string; + usesCallbackServer?: boolean; +} + interface OpenAiCodexModelRegistration { id: string; name: string; + api?: "openai-codex-responses"; + baseUrl?: string; reasoning: boolean; + thinkingLevelMap?: Record; input: OpenAiCodexModelInput[]; cost: { input: number; output: number; cacheRead: number; cacheWrite: number; + tiers?: OpenAiCodexCostTier[]; }; contextWindow: number; maxTokens: number; @@ -25,6 +53,7 @@ export interface OpenAiCodexProviderRegistration { name: string; baseUrl: string; apiKey?: string; + oauth?: OpenAiCodexOAuthProviderRegistration; api: "openai-codex-responses"; models: OpenAiCodexModelRegistration[]; } @@ -38,13 +67,19 @@ export interface OpenAiCodexProviderRegistration { * did not carry the three GPT-5.6 codenamed ids under "openai-codex", so no picker * surfaced them. Mirror the SUPPLEMENTAL_ANTHROPIC_PROVIDER_REGISTRATION seam * (anthropic-models.ts) to additively register them: if a later pi-ai bump already - * carries an id (confirmed true as of the pinned 0.80.5 used by this task), the merge - * below is a dedupe-safe no-op — the existing catalog row always wins, never displaced - * or duplicated. Field shape (api/baseUrl) copied verbatim from the pinned catalog's - * openai-codex.models.js entries; apiKey is intentionally omitted because the real - * "openai-codex" provider authenticates via ChatGPT Plus/Pro OAuth, not an env-var API - * key — the merge preserves whatever auth the provider was already registered with - * (see mergeSupplementalOpenAiCodexModels's `...registeredProvider` override below). + * carries an id, the merge below is a dedupe-safe no-op — the existing catalog row + * always wins, never displaced or duplicated. Field shape (api/baseUrl) is copied + * from the pinned catalog's openai-codex.models.js entries. + * + * FNXC:ModelCatalog 2026-07-09-23:55: + * FN-7759: the real pi-coding-agent ModelRegistry rejects dynamic providers that + * define models without `apiKey` or `oauth`, and `registerProvider` full-replaces + * the provider's rows before getAvailable() runs. Prior mock-only tests missed that + * the supplemental OpenAI Codex registration could be logged-and-dropped on installs + * whose pinned catalog lacked 5.6. Keep model fields aligned with the real Codex + * catalog (per-model api/baseUrl/thinking levels/pricing) and carry the built-in + * OAuth provider object into the dynamic registration so validation, getAvailable() + * auth filtering, and execution-time OAuth auth treatment stay identical to 5.3/5.4/5.5. */ export const SUPPLEMENTAL_OPENAI_CODEX_PROVIDER_REGISTRATION: OpenAiCodexProviderRegistration = { name: "OpenAI Codex", @@ -54,53 +89,67 @@ export const SUPPLEMENTAL_OPENAI_CODEX_PROVIDER_REGISTRATION: OpenAiCodexProvide { id: GPT_5_6_LUNA_MODEL_ID, name: "GPT-5.6 Luna", + api: "openai-codex-responses", + baseUrl: "https://chatgpt.com/backend-api", reasoning: true, + thinkingLevelMap: { xhigh: "xhigh", max: "max", minimal: "low" }, input: ["text", "image"], cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, + input: 1, + output: 6, + cacheRead: 0.1, cacheWrite: 1.25, + tiers: [{ inputTokensAbove: 272_000, input: 2, output: 9, cacheRead: 0.2, cacheWrite: 2.5 }], }, - contextWindow: 272_000, + contextWindow: 372_000, maxTokens: 128_000, }, { id: GPT_5_6_SOL_MODEL_ID, name: "GPT-5.6 Sol", + api: "openai-codex-responses", + baseUrl: "https://chatgpt.com/backend-api", reasoning: true, + thinkingLevelMap: { xhigh: "xhigh", max: "max", minimal: "low" }, input: ["text", "image"], cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 1.25, + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 6.25, + tiers: [{ inputTokensAbove: 272_000, input: 10, output: 45, cacheRead: 1, cacheWrite: 12.5 }], }, - contextWindow: 272_000, + contextWindow: 372_000, maxTokens: 128_000, }, { id: GPT_5_6_TERRA_MODEL_ID, name: "GPT-5.6 Terra", + api: "openai-codex-responses", + baseUrl: "https://chatgpt.com/backend-api", reasoning: true, + thinkingLevelMap: { xhigh: "xhigh", max: "max", minimal: "low" }, input: ["text", "image"], cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 1.25, + input: 2.5, + output: 15, + cacheRead: 0.25, + cacheWrite: 3.125, + tiers: [{ inputTokensAbove: 272_000, input: 5, output: 22.5, cacheRead: 0.5, cacheWrite: 6.25 }], }, - contextWindow: 272_000, + contextWindow: 372_000, maxTokens: 128_000, }, ], }; -type OpenAiCodexModelLike = Partial> & { +type OpenAiCodexModelLike = Partial> & { id: string; + api?: string; name?: unknown; provider?: string; compat?: unknown; + thinkingLevelMap?: unknown; }; interface OpenAiCodexModelRegistryLike { @@ -110,14 +159,25 @@ interface OpenAiCodexModelRegistryLike { type RegistryWithProviderState = OpenAiCodexModelRegistryLike & { registeredProviders?: Map>; + authStorage?: { getOAuthProviders?: () => OpenAiCodexOAuthProviderRegistration[] }; }; +function getOpenAiCodexOAuthProvider(registryWithState: RegistryWithProviderState, registeredProvider: Partial | undefined): OpenAiCodexOAuthProviderRegistration | undefined { + return registeredProvider?.oauth + ?? registryWithState.authStorage?.getOAuthProviders?.().find((provider) => provider.id === OPENAI_CODEX_PROVIDER_ID); +} + function toOpenAiCodexModelRegistration(model: OpenAiCodexModelLike): OpenAiCodexModelRegistration { const supplemental = SUPPLEMENTAL_OPENAI_CODEX_PROVIDER_REGISTRATION.models.find((entry) => entry.id === model.id); return { id: model.id, name: String(model.name ?? supplemental?.name ?? model.id), + api: model.api === "openai-codex-responses" ? model.api : supplemental?.api, + baseUrl: model.baseUrl ?? supplemental?.baseUrl, reasoning: model.reasoning ?? supplemental?.reasoning ?? false, + thinkingLevelMap: typeof model.thinkingLevelMap === "object" && model.thinkingLevelMap !== null + ? { ...(model.thinkingLevelMap as Record) } + : supplemental?.thinkingLevelMap ? { ...supplemental.thinkingLevelMap } : undefined, input: Array.isArray(model.input) ? model.input as OpenAiCodexModelInput[] : supplemental?.input ?? ["text"], cost: model.cost ?? supplemental?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: Number(model.contextWindow ?? supplemental?.contextWindow ?? 0), @@ -154,9 +214,12 @@ export function mergeSupplementalOpenAiCodexModels( if (missingModels.length === 0) return; + const oauth = getOpenAiCodexOAuthProvider(registryWithState, registeredProvider); + modelRegistry.registerProvider(OPENAI_CODEX_PROVIDER_ID, { ...cloneOpenAiCodexProviderRegistration(SUPPLEMENTAL_OPENAI_CODEX_PROVIDER_REGISTRATION), ...registeredProvider, + oauth, models: [...currentModels, ...missingModels.map((model) => toOpenAiCodexModelRegistration(model))], }); } catch (error) { diff --git a/packages/dashboard/src/__tests__/register-model-routes-openai-codex-supplemental.test.ts b/packages/dashboard/src/__tests__/register-model-routes-openai-codex-supplemental.test.ts index 5de080886d..1ff9921f08 100644 --- a/packages/dashboard/src/__tests__/register-model-routes-openai-codex-supplemental.test.ts +++ b/packages/dashboard/src/__tests__/register-model-routes-openai-codex-supplemental.test.ts @@ -9,11 +9,51 @@ registry lacking these ids would never surface them even with openai-codex confi this suite encodes that failing-before/passing-after contract plus dedupe and the configuredProviders allow-list gate. */ +import { ModelRegistry } from "@earendil-works/pi-coding-agent"; import type { Router } from "express"; import { describe, expect, it, vi } from "vitest"; import { registerModelRoutes } from "../routes/register-model-routes.js"; const GPT_5_6_IDS = ["gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"]; +const BUILT_IN_CODEX_IDS = ["gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini", "gpt-5.5"]; +const OPENAI_CODEX_OAUTH_CREDENTIAL = { + refresh: "test-refresh-token", + access: "test-access-token", + expires: Date.now() + 60_000, +}; +const OPENAI_CODEX_OAUTH_PROVIDER = { + id: "openai-codex", + name: "ChatGPT Plus/Pro (Codex Subscription)", + login: async () => OPENAI_CODEX_OAUTH_CREDENTIAL, + refreshToken: async () => OPENAI_CODEX_OAUTH_CREDENTIAL, + getApiKey: () => OPENAI_CODEX_OAUTH_CREDENTIAL.access, +}; + +function createOpenAiCodexAuthStorage(openAiCodexConfigured: boolean) { + const credential = { + type: "oauth", + access: "test-access-token", + refresh: "test-refresh-token", + expires: Date.now() + 60_000, + }; + + return { + reload: vi.fn(), + getOAuthProviders: vi.fn(() => [OPENAI_CODEX_OAUTH_PROVIDER]), + getApiKeyProviders: vi.fn(() => []), + get: vi.fn((providerId: string) => openAiCodexConfigured && providerId === "openai-codex" ? credential : undefined), + hasAuth: vi.fn((providerId: string) => openAiCodexConfigured && providerId === "openai-codex"), + hasApiKey: vi.fn(() => false), + getProviderEnv: vi.fn(() => ({})), + getApiKey: vi.fn(async (providerId: string) => openAiCodexConfigured && providerId === "openai-codex" ? credential.access : undefined), + }; +} + +function createRealModelRegistryWithLegacyCodexCatalog(authStorage: ReturnType) { + const registry = ModelRegistry.inMemory(authStorage as never) as unknown as ModelRegistry & { models: Array> }; + registry.models = registry.models.filter((model) => model.provider !== "openai-codex" || !GPT_5_6_IDS.includes(String(model.id))); + return registry; +} interface FakeOpenAiCodexModel { id: string; @@ -61,7 +101,7 @@ function createFakeModelRegistry(initialOpenAiCodexModels: FakeOpenAiCodexModel[ }; } -function createRouterHarness(modelRegistry: ReturnType, options: { openAiCodexConfigured: boolean }) { +function createRouterHarness(modelRegistry: ReturnType | ModelRegistry, options: { openAiCodexConfigured: boolean }, authStorage = createOpenAiCodexAuthStorage(options.openAiCodexConfigured)) { const getHandlers = new Map void }) => Promise>(); const router = { get: vi.fn((path: string, handler: (req: unknown, res: { json: (body: unknown) => void }) => Promise) => { @@ -76,12 +116,6 @@ function createRouterHarness(modelRegistry: ReturnType ({ warn: vi.fn() })) }; - const authStorage = { - reload: vi.fn(), - getOAuthProviders: vi.fn(() => (options.openAiCodexConfigured ? [{ id: "openai-codex", name: "OpenAI Codex" }] : [])), - hasAuth: vi.fn((providerId: string) => options.openAiCodexConfigured && providerId === "openai-codex"), - }; - registerModelRoutes({ router, store: store as never, @@ -140,4 +174,33 @@ describe("FN-7745: GPT-5.6 codenamed OpenAI Codex variants — /api/models", () const response = await callModels(handler); expect(response.models.some((m) => m.provider === "openai-codex")).toBe(false); }); + + it("drives the real ModelRegistry path from supplemental merge through /api/models filtering", async () => { + const authStorage = createOpenAiCodexAuthStorage(true); + const modelRegistry = createRealModelRegistryWithLegacyCodexCatalog(authStorage); + const beforeIds = modelRegistry.getAvailable() + .filter((model) => model.provider === "openai-codex") + .map((model) => model.id); + expect(beforeIds).not.toEqual(expect.arrayContaining(GPT_5_6_IDS)); + expect(beforeIds).toEqual(expect.arrayContaining(BUILT_IN_CODEX_IDS)); + + const handler = createRouterHarness(modelRegistry, { openAiCodexConfigured: true }, authStorage); + + const response = await callModels(handler); + const codexRows = response.models.filter((m) => m.provider === "openai-codex"); + const codexIds = codexRows.map((m) => m.id); + expect(codexIds).toEqual(expect.arrayContaining([...BUILT_IN_CODEX_IDS, ...GPT_5_6_IDS])); + + const keys = response.models.map((m) => `${m.provider}/${m.id}`); + expect(new Set(keys).size).toBe(keys.length); + }); + + it("keeps real-registry GPT-5.6 rows gated when openai-codex auth is absent", async () => { + const authStorage = createOpenAiCodexAuthStorage(false); + const modelRegistry = createRealModelRegistryWithLegacyCodexCatalog(authStorage); + const handler = createRouterHarness(modelRegistry, { openAiCodexConfigured: false }, authStorage); + + const response = await callModels(handler); + expect(response.models.some((m) => m.provider === "openai-codex")).toBe(false); + }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 74222b1a86..f0fbe00278 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -182,6 +182,9 @@ importers: specifier: ^2.8.3 version: 2.8.3 devDependencies: + '@earendil-works/pi-coding-agent': + specifier: ^0.80.6 + version: 0.80.6 '@types/dockerode': specifier: ^3.3.41 version: 3.3.47 @@ -8523,6 +8526,20 @@ snapshots: - ws - zod + '@earendil-works/pi-agent-core@0.80.6': + dependencies: + '@earendil-works/pi-ai': 0.80.6 + ignore: 7.0.5 + typebox: 1.1.38 + yaml: 2.9.0 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + '@earendil-works/pi-agent-core@0.80.6(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)': dependencies: '@earendil-works/pi-ai': 0.80.6(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76) @@ -8666,6 +8683,27 @@ snapshots: - ws - zod + '@earendil-works/pi-ai@0.80.6': + dependencies: + '@anthropic-ai/sdk': 0.91.1 + '@aws-sdk/client-bedrock-runtime': 3.1048.0 + '@google/genai': 1.52.0 + '@mistralai/mistralai': 2.2.6(@opentelemetry/api@1.9.0) + '@opentelemetry/api': 1.9.0 + '@smithy/node-http-handler': 4.7.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + openai: 6.26.0 + partial-json: 0.1.7 + typebox: 1.1.38 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + '@earendil-works/pi-ai@0.80.6(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)': dependencies: '@anthropic-ai/sdk': 0.91.1(zod@3.25.76) @@ -8875,6 +8913,36 @@ snapshots: - ws - zod + '@earendil-works/pi-coding-agent@0.80.6': + dependencies: + '@earendil-works/pi-agent-core': 0.80.6 + '@earendil-works/pi-ai': 0.80.6 + '@earendil-works/pi-tui': 0.80.6 + '@silvia-odwyer/photon-node': 0.3.4 + chalk: 5.6.2 + cross-spawn: 7.0.6 + diff: 8.0.4 + glob: 13.0.6 + highlight.js: 10.7.3 + hosted-git-info: 9.0.3 + ignore: 7.0.5 + jiti: 2.7.0 + minimatch: 10.2.5 + proper-lockfile: 4.1.2 + semver: 7.8.0 + typebox: 1.1.38 + undici: 8.5.0 + yaml: 2.9.0 + optionalDependencies: + '@mariozechner/clipboard': 0.3.9 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + '@earendil-works/pi-coding-agent@0.80.6(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)': dependencies: '@earendil-works/pi-agent-core': 0.80.6(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)