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) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7759-codex-gpt-5-6-dropdown.md
Normal file
7
.changeset/fn-7759-codex-gpt-5-6-dropdown.md
Normal file
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<Record<string, unknown>> };
|
||||
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" });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<OpenAiCodexOAuthCredentials>;
|
||||
refreshToken: (credentials: OpenAiCodexOAuthCredentials) => Promise<OpenAiCodexOAuthCredentials>;
|
||||
getApiKey: (credentials: OpenAiCodexOAuthCredentials) => string;
|
||||
usesCallbackServer?: boolean;
|
||||
}
|
||||
|
||||
interface OpenAiCodexModelRegistration {
|
||||
id: string;
|
||||
name: string;
|
||||
api?: "openai-codex-responses";
|
||||
baseUrl?: string;
|
||||
reasoning: boolean;
|
||||
thinkingLevelMap?: Record<string, string | null>;
|
||||
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<Omit<OpenAiCodexModelRegistration, "name" | "compat">> & {
|
||||
type OpenAiCodexModelLike = Partial<Omit<OpenAiCodexModelRegistration, "name" | "api" | "compat" | "thinkingLevelMap">> & {
|
||||
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<string, Partial<OpenAiCodexProviderRegistration>>;
|
||||
authStorage?: { getOAuthProviders?: () => OpenAiCodexOAuthProviderRegistration[] };
|
||||
};
|
||||
|
||||
function getOpenAiCodexOAuthProvider(registryWithState: RegistryWithProviderState, registeredProvider: Partial<OpenAiCodexProviderRegistration> | 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<string, string | null>) }
|
||||
: 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) {
|
||||
|
||||
@@ -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<typeof createOpenAiCodexAuthStorage>) {
|
||||
const registry = ModelRegistry.inMemory(authStorage as never) as unknown as ModelRegistry & { models: Array<Record<string, unknown>> };
|
||||
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<typeof createFakeModelRegistry>, options: { openAiCodexConfigured: boolean }) {
|
||||
function createRouterHarness(modelRegistry: ReturnType<typeof createFakeModelRegistry> | ModelRegistry, options: { openAiCodexConfigured: boolean }, authStorage = createOpenAiCodexAuthStorage(options.openAiCodexConfigured)) {
|
||||
const getHandlers = new Map<string, (req: unknown, res: { json: (body: unknown) => void }) => Promise<void>>();
|
||||
const router = {
|
||||
get: vi.fn((path: string, handler: (req: unknown, res: { json: (body: unknown) => void }) => Promise<void>) => {
|
||||
@@ -76,12 +116,6 @@ function createRouterHarness(modelRegistry: ReturnType<typeof createFakeModelReg
|
||||
|
||||
const runtimeLogger = { child: vi.fn(() => ({ 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);
|
||||
});
|
||||
});
|
||||
|
||||
68
pnpm-lock.yaml
generated
68
pnpm-lock.yaml
generated
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user