diff --git a/src/codex/catalog/native-models.ts b/src/codex/catalog/native-models.ts index 3fd673f84b..4c63d89ec1 100644 --- a/src/codex/catalog/native-models.ts +++ b/src/codex/catalog/native-models.ts @@ -3,6 +3,9 @@ export const NATIVE_DAYBREAK_BLUE_MODEL = "gpt-daybreak-blue-latest"; /** Native ChatGPT/Codex ids whose availability is proven per authenticated account. */ export const ACCOUNT_GATED_NATIVE_OPENAI_MODELS: ReadonlySet = new Set([ + "gpt-5.6-sol", + "gpt-5.6-terra", + "gpt-5.6-luna", NATIVE_DAYBREAK_BLUE_MODEL, ]); @@ -58,8 +61,8 @@ export function nativeOpenAiCapabilitySourceSlug(slug: string): string { * discover it on a clean install. * * Availability is not static: catalog sync and Pool routing require the account's authenticated - * `/models` roster to contain the slug. An unconfirmed or unentitled account never receives the - * request. `disabledModels` remains the independent user visibility control. + * `/models` roster to contain account-gated slugs. An unconfirmed or unentitled account never + * receives the request. `disabledModels` remains the independent user visibility control. * * Devlog: 260816_codexrs_multiagent_v2_and_history_perf/011 §4-bis. */ diff --git a/tests/agent-task-recovery.test.ts b/tests/agent-task-recovery.test.ts index 44f5fa3913..78647e1075 100644 --- a/tests/agent-task-recovery.test.ts +++ b/tests/agent-task-recovery.test.ts @@ -540,7 +540,7 @@ describe("agent task recovery (opt-in, default off)", () => { const response = await post( routedConfig(), - "gpt-5.6-sol", + "gpt-5.5", encryptedInput(), codexHeaders(), ); diff --git a/tests/bearer-admission-routed-provider.test.ts b/tests/bearer-admission-routed-provider.test.ts index be4449c144..51f1fd99ee 100644 --- a/tests/bearer-admission-routed-provider.test.ts +++ b/tests/bearer-admission-routed-provider.test.ts @@ -57,7 +57,7 @@ function mixedConfig(): OcxConfig { baseUrl: "https://chatgpt.com/backend-api/codex", authMode: "forward", codexAccountMode: "direct", - defaultModel: "gpt-5.6-luna", + defaultModel: "gpt-5.5", }, gateway: { adapter: "openai-chat", @@ -153,7 +153,7 @@ describe("#2132 bearer admission does not require a ChatGPT credential for route const server = startServer(0); try { - const response = await postResponses(server.url, "gpt-5.6-luna"); + const response = await postResponses(server.url, "gpt-5.5"); // This is the #1686 guarantee and it must survive: a native route genuinely needs the // stored credential, so it fails BEFORE any upstream I/O rather than forwarding ours. @@ -175,7 +175,7 @@ describe("#2132 bearer admission does not require a ChatGPT credential for route const server = startServer(0); try { - const response = await postResponses(server.url, "gpt-5.6-luna"); + const response = await postResponses(server.url, "gpt-5.5"); expect(response.status).toBe(200); expect(nativeAuth).toEqual([`Bearer ${stored}`]); @@ -213,7 +213,7 @@ describe("an admission bearer never reaches a canonical ChatGPT transport, whate adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex", authMode: "forward", - defaultModel: "gpt-5.6-luna", + defaultModel: "gpt-5.5", }, }, } as OcxConfig; @@ -225,7 +225,7 @@ describe("an admission bearer never reaches a canonical ChatGPT transport, whate const server = startServer(0); try { - const response = await postResponses(server.url, "mirror/gpt-5.6-luna"); + const response = await postResponses(server.url, "mirror/gpt-5.5"); // Fail-before-I/O is the contract (src/codex/auth-context.ts): the only two acceptable // outcomes for an admission bearer are replaced-with-stored-main, or refused. Reaching @@ -247,7 +247,7 @@ describe("an admission bearer never reaches a canonical ChatGPT transport, whate const server = startServer(0); try { - await postResponses(server.url, "mirror/gpt-5.6-luna"); + await postResponses(server.url, "mirror/gpt-5.5"); expect(nativeAuth.join("|")).not.toContain(ADMISSION_SECRET); for (const sent of nativeAuth) expect(sent).toBe(`Bearer ${stored}`); diff --git a/tests/claude-desktop-native-context.test.ts b/tests/claude-desktop-native-context.test.ts index acc181b384..7c36a0ffe0 100644 --- a/tests/claude-desktop-native-context.test.ts +++ b/tests/claude-desktop-native-context.test.ts @@ -5,6 +5,10 @@ import { join } from "node:path"; import { buildClaudeDesktopState } from "../src/server/management/shared"; import { nativeOpenAiContextWindow, visibleNativeSlugs } from "../src/codex/catalog"; import { generateDesktop3pModels } from "../src/claude/desktop-3p"; +import { + resetCodexModelEntitlementCacheForTests, + seedCodexModelEntitlementsForTests, +} from "../src/codex/model-entitlements"; import type { OcxConfig } from "../src/types"; /** @@ -24,6 +28,8 @@ const config = { } as unknown as OcxConfig; test("buildClaudeDesktopState gives native rows their real context window", async () => { + // Sol/Terra/Luna are account-gated; this test is about window metadata, so confirm them. + seedCodexModelEntitlementsForTests("main", ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]); const home = tempHome(); const prev = process.env.OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR; process.env.OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR = home; @@ -44,6 +50,7 @@ test("buildClaudeDesktopState gives native rows their real context window", asyn if (prev === undefined) delete process.env.OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR; else process.env.OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR = prev; rmSync(home, { recursive: true, force: true }); + resetCodexModelEntitlementCacheForTests(); } }); diff --git a/tests/claude-models-discovery.test.ts b/tests/claude-models-discovery.test.ts index 19f58ac795..0fd20b156f 100644 --- a/tests/claude-models-discovery.test.ts +++ b/tests/claude-models-discovery.test.ts @@ -3,6 +3,9 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { saveConfig } from "../src/config"; +import { + resetCodexModelEntitlementCacheForTests, +} from "../src/codex/model-entitlements"; import { handleManagementAPI } from "../src/server/management-api"; import { startServer } from "../src/server"; import type { OcxConfig } from "../src/types"; @@ -25,6 +28,7 @@ beforeEach(() => { }); afterEach(() => { + resetCodexModelEntitlementCacheForTests(); if (previousHome === undefined) delete process.env.OPENCODEX_HOME; else process.env.OPENCODEX_HOME = previousHome; isolatedCodexHome?.restore(); @@ -159,6 +163,23 @@ test("OpenAI list shape and Codex catalog shape stay unchanged", async () => { }); test("Codex discovery applies the OpenAI context cap to native rows (#1430)", async () => { + writeFileSync(join(isolatedCodexHome!.path, "auth.json"), JSON.stringify({ + tokens: { access_token: "context-cap-access", account_id: "context-cap-account" }, + }), "utf8"); + const originalFetch = globalThis.fetch; + globalThis.fetch = (async (input, init) => { + const request = new Request(input, init); + const url = new URL(request.url); + if (request.method === "GET" && url.pathname === "/backend-api/codex/models") { + const entitled = request.headers.get("authorization") === "Bearer context-cap-access" + && request.headers.get("chatgpt-account-id") === "context-cap-account"; + const slugs = entitled ? ["gpt-5.6-sol"] : []; + return Response.json({ + models: slugs.map(slug => ({ slug, supported_in_api: true, visibility: "list" })), + }); + } + return originalFetch(request); + }) as typeof fetch; const config = configWithStaticModels(); config.providers.openai = { adapter: "openai-responses", @@ -186,6 +207,7 @@ test("Codex discovery applies the OpenAI context cap to native rows (#1430)", as }); } finally { await server.stop(true); + globalThis.fetch = originalFetch; } }); diff --git a/tests/codex-auth-api.test.ts b/tests/codex-auth-api.test.ts index 4bf7876312..c8e552771f 100644 --- a/tests/codex-auth-api.test.ts +++ b/tests/codex-auth-api.test.ts @@ -3485,7 +3485,7 @@ describe("codex-auth API", () => { listOpenAiForwardSidecarCandidates(config), new Headers(), config, - { exactAccount: { accountId: "pool-delete", modelId: "gpt-5.6-sol" } }, + { exactAccount: { accountId: "pool-delete", modelId: "gpt-5.5" } }, ); expect(exactSidecar?.authContext).toMatchObject({ accountId: "pool-delete", diff --git a/tests/codex-auth-context.test.ts b/tests/codex-auth-context.test.ts index 5a3372c137..da4d6cb5d0 100644 --- a/tests/codex-auth-context.test.ts +++ b/tests/codex-auth-context.test.ts @@ -450,7 +450,7 @@ describe("Codex auth context", () => { }); let discoveries = 0; await expect(resolveCodexAuthContext(new Headers(), config(), "pool", { - modelId: "gpt-5.6-sol", + modelId: "gpt-5.5", resolveCodexModelEntitlements: async () => { discoveries += 1; throw new Error("must not run"); @@ -479,7 +479,7 @@ describe("Codex auth context", () => { const exactContext = await resolveCodexAuthContext(headers, cfg, "direct", { accountId: "pool-a", - modelId: "gpt-5.6-sol", + modelId: "gpt-5.5", }); expect(exactContext).toMatchObject({ kind: "pool", @@ -493,7 +493,7 @@ describe("Codex auth context", () => { _codexAccountOverride: { accessToken: "fixed_pool_token", chatgptAccountId: "fixed_pool_acc" }, }); expect(cfg.activeCodexAccountId).toBe("pool-b"); - await expect(resolveCodexAuthContext(headers, cfg, "pool", { modelId: "gpt-5.6-sol" })) + await expect(resolveCodexAuthContext(headers, cfg, "pool", { modelId: "gpt-5.5" })) .resolves.toMatchObject({ kind: "pool", accountId: "pool-b" }); }); @@ -706,7 +706,7 @@ describe("Codex auth context", () => { // The pool path honors the order: pool-b outranks pool-a, so an unbound request // prefers pool-b. - await expect(resolveCodexAuthContext(new Headers(), cfg, "pool", { modelId: "gpt-5.6-sol" })) + await expect(resolveCodexAuthContext(new Headers(), cfg, "pool", { modelId: "gpt-5.5" })) .resolves.toMatchObject({ kind: "pool", accountId: "pool-b" }); // An exact selector names pool-a: it must resolve to pool-a even though pool-b is @@ -714,7 +714,7 @@ describe("Codex auth context", () => { // way to redirect a request that already named its account. await expect(resolveCodexAuthContext(new Headers(), cfg, "pool", { accountId: "pool-a", - modelId: "gpt-5.6-sol", + modelId: "gpt-5.5", })).resolves.toMatchObject({ kind: "pool", accountId: "pool-a", @@ -735,7 +735,7 @@ describe("Codex auth context", () => { await expect(resolveCodexAuthContext(new Headers(), cfg, "pool", { accountId: MAIN_CODEX_ACCOUNT_ID, - modelId: "gpt-5.6-sol", + modelId: "gpt-5.5", })).resolves.toMatchObject({ kind: "main-pool", accountId: MAIN_CODEX_ACCOUNT_ID, @@ -841,7 +841,7 @@ describe("Codex auth context", () => { expect(isCodexAuthContextUsable(captured, cfg)).toBe(true); await expect(resolveCodexAuthContext(new Headers(), cfg, "pool", { accountId: "pool-a", - modelId: "gpt-5.6-sol", + modelId: "gpt-5.5", })).rejects.toThrow("Selected Codex account is unavailable"); expect(cfg.activeCodexAccountId).toBe("pool-a"); await expect(resolveCodexAuthContext(new Headers(), cfg, "pool")) @@ -868,7 +868,7 @@ describe("Codex auth context", () => { await expect(resolveCodexAuthContext(new Headers(), cfg, "pool", { accountId: "pool-a", - modelId: "gpt-5.6-sol", + modelId: "gpt-5.5", })).rejects.toThrow("Selected Codex account needs reauthentication"); expect(cfg.activeCodexAccountId).toBe("pool-b"); }); @@ -885,7 +885,7 @@ describe("Codex auth context", () => { await expect(resolveCodexAuthContext(new Headers(), cfg, "pool", { accountId: "pool-a", - modelId: "gpt-5.6-sol", + modelId: "gpt-5.5", })).rejects.toThrow("Selected Codex account is unavailable"); expect(cfg.activeCodexAccountId).toBe("pool-a"); }); @@ -905,18 +905,18 @@ describe("Codex auth context", () => { recordCodexUpstreamOutcome(cfg, "pool-a", 429, { resetAt: Math.floor((now + 60 * 60_000) / 1_000), now, - modelId: "gpt-5.6-sol", + modelId: "gpt-5.5", fixedAccount: true, }); Date.now = () => now + CODEX_QUOTA_PROBE_INTERVAL_MS; await expect(resolveCodexAuthContext(new Headers(), cfg, "pool", { accountId: "pool-a", - modelId: "gpt-5.6-sol", + modelId: "gpt-5.5", })).rejects.toBeInstanceOf(CodexAccountCooldownError); const ordinaryProbe = await resolveCodexAuthContext(new Headers(), cfg, "pool", { - modelId: "gpt-5.6-sol", + modelId: "gpt-5.5", }); expect(ordinaryProbe).toMatchObject({ kind: "pool", accountId: "pool-a" }); expect(ordinaryProbe.kind === "pool" ? ordinaryProbe.probeLeaseId : undefined).toBeTruthy(); @@ -1079,7 +1079,7 @@ describe("Codex auth context", () => { }); // Spark owns a separate quota, so Terra can use the same account. - await expect(resolveCodexAuthContext(headers, cfg, "pool", { modelId: "gpt-5.6-terra" })) + await expect(resolveCodexAuthContext(headers, cfg, "pool", { modelId: "gpt-5.4" })) .resolves.toMatchObject({ kind: "pool", accountId: "pool-a" }); await expect(resolveCodexAuthContext(headers, cfg, "pool", { modelId: "gpt-5.3-codex-spark" })) .rejects.toBeInstanceOf(CodexAccountCooldownError); @@ -1087,12 +1087,12 @@ describe("Codex auth context", () => { recordCodexUpstreamOutcome(cfg, "pool-a", 429, { now, resetAt, - modelId: "gpt-5.6-terra", + modelId: "gpt-5.4", }); // Terra and Luna stay in the shared native quota group, while Spark keeps // its independent cooldown instead of being overwritten by Terra's 429. - await expect(resolveCodexAuthContext(headers, cfg, "pool", { modelId: "gpt-5.6-luna" })) + await expect(resolveCodexAuthContext(headers, cfg, "pool", { modelId: "gpt-5.4-mini" })) .rejects.toBeInstanceOf(CodexAccountCooldownError); await expect(resolveCodexAuthContext(headers, cfg, "pool", { modelId: "gpt-5.3-codex-spark" })) .rejects.toBeInstanceOf(CodexAccountCooldownError); @@ -1104,7 +1104,7 @@ describe("Codex auth context", () => { retryAfter: "60", modelId: "gpt-5.3-codex-spark", }); - await expect(resolveCodexAuthContext(headers, cfg, "pool", { modelId: "gpt-5.6-terra" })) + await expect(resolveCodexAuthContext(headers, cfg, "pool", { modelId: "gpt-5.4" })) .rejects.toBeInstanceOf(CodexAccountCooldownError); } finally { Date.now = originalNow; @@ -1137,7 +1137,7 @@ describe("Codex auth context", () => { Date.now = () => now; // Establish the shared-scope binding first. The Spark fallback below must // create a second binding rather than replacing this one. - await expect(resolveCodexAuthContext(headers, cfg, "pool", { modelId: "gpt-5.6-terra" })) + await expect(resolveCodexAuthContext(headers, cfg, "pool", { modelId: "gpt-5.4" })) .resolves.toMatchObject({ kind: "pool", accountId: "pool-a" }); recordCodexUpstreamOutcome(cfg, "pool-a", 429, { @@ -1149,7 +1149,7 @@ describe("Codex auth context", () => { await expect(resolveCodexAuthContext(headers, cfg, "pool", { modelId: "gpt-5.3-codex-spark" })) .resolves.toMatchObject({ kind: "pool", accountId: "pool-b" }); expect(cfg.activeCodexAccountId).toBe("pool-a"); - await expect(resolveCodexAuthContext(headers, cfg, "pool", { modelId: "gpt-5.6-terra" })) + await expect(resolveCodexAuthContext(headers, cfg, "pool", { modelId: "gpt-5.4" })) .resolves.toMatchObject({ kind: "pool", accountId: "pool-a" }); // This second Spark request proves routing retained the peer choice for // the Spark affinity instead of relying on an auth-layer substitution. @@ -1182,7 +1182,7 @@ describe("Codex auth context", () => { recordCodexUpstreamOutcome(cfg, "pool-a", 429, { now, resetAt, - modelId: "gpt-5.6-terra", + modelId: "gpt-5.4", }); const probeAt = now + CODEX_QUOTA_PROBE_INTERVAL_MS; @@ -1205,7 +1205,7 @@ describe("Codex auth context", () => { Date.now = () => probeAt + 1; await expect(resolveCodexAuthContext(headers, cfg, "pool", { modelId: "gpt-5.3-codex-spark" })) .resolves.toMatchObject({ kind: "pool", accountId: "pool-a" }); - await expect(resolveCodexAuthContext(headers, cfg, "pool", { modelId: "gpt-5.6-luna" })) + await expect(resolveCodexAuthContext(headers, cfg, "pool", { modelId: "gpt-5.4-mini" })) .resolves.toMatchObject({ kind: "pool", probeQuotaScope: "shared" }); } finally { Date.now = originalNow; diff --git a/tests/codex-catalog-restore.test.ts b/tests/codex-catalog-restore.test.ts index f689d80eb2..253c42f449 100644 --- a/tests/codex-catalog-restore.test.ts +++ b/tests/codex-catalog-restore.test.ts @@ -270,6 +270,8 @@ describe("Codex catalog restore", () => { const r = runScript(codexHome, opencodexHome, ` const { syncCatalogModels } = require("./src/codex/catalog"); + const { seedCodexModelEntitlementsForTests } = require("./src/codex/model-entitlements"); + seedCodexModelEntitlementsForTests("__main__", ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]); (async () => { const result = await syncCatalogModels({ port: 10100, @@ -291,6 +293,9 @@ describe("Codex catalog restore", () => { test("sync advertises documented Codex-native additions omitted by the bundled catalog", () => { const catalogPath = join(codexHome, "catalog.json"); writeFileSync(join(codexHome, "config.toml"), 'model_catalog_json = "catalog.json"\n', "utf8"); + writeFileSync(join(codexHome, "auth.json"), JSON.stringify({ + tokens: { access_token: "catalog-main-access", account_id: "catalog-main-account" }, + }), "utf8"); writeFileSync(catalogPath, JSON.stringify({ models: [ { @@ -314,6 +319,19 @@ describe("Codex catalog restore", () => { const r = runScript(codexHome, opencodexHome, ` const { syncCatalogModels } = require("./src/codex/catalog"); + globalThis.fetch = async (input, init) => { + const request = new Request(input, init); + const url = new URL(request.url); + if (request.method !== "GET" || url.pathname !== "/backend-api/codex/models") { + throw new Error("unexpected fetch: " + request.method + " " + url.href); + } + const entitled = request.headers.get("authorization") === "Bearer catalog-main-access" + && request.headers.get("chatgpt-account-id") === "catalog-main-account"; + const slugs = entitled ? ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"] : []; + return Response.json({ + models: slugs.map(slug => ({ slug, supported_in_api: true, visibility: "list" })), + }); + }; (async () => { const result = await syncCatalogModels({ port: 10100, diff --git a/tests/codex-catalog-sync-hardening.test.ts b/tests/codex-catalog-sync-hardening.test.ts index 29d84ff661..c00e96923d 100644 --- a/tests/codex-catalog-sync-hardening.test.ts +++ b/tests/codex-catalog-sync-hardening.test.ts @@ -108,7 +108,7 @@ describe("Codex catalog sync hardening", () => { if (existsSync(opencodexHome)) rmSync(opencodexHome, { recursive: true, force: true }); }); - test("Gap B: drops legacy OpenAI-family natives but keeps supported + user natives", () => { + test("Gap B: drops legacy and unentitled account-gated natives but keeps supported + user natives", () => { const catalogPath = join(codexHome, "catalog.json"); writeFileSync(join(codexHome, "config.toml"), 'model_catalog_json = "catalog.json"\n', "utf8"); writeFileSync(catalogPath, JSON.stringify({ @@ -138,9 +138,11 @@ describe("Codex catalog sync hardening", () => { expect(slugs).toContain("gpt-5.4"); expect(slugs).toContain("gpt-5.4-mini"); expect(slugs).toContain("gpt-5.3-codex-spark"); - expect(slugs).toContain("gpt-5.6-sol"); - expect(slugs).toContain("gpt-5.6-terra"); - expect(slugs).toContain("gpt-5.6-luna"); + // This isolated fixture has no authenticated ChatGPT roster, so account-gated + // native models must fail closed rather than remain selectable. + expect(slugs).not.toContain("gpt-5.6-sol"); + expect(slugs).not.toContain("gpt-5.6-terra"); + expect(slugs).not.toContain("gpt-5.6-luna"); expect(slugs).toContain("user-native"); // genuine user native preserved expect(slugs).not.toContain("gpt-5.3-codex"); // legacy dropped expect(slugs).not.toContain("gpt-5.2"); // legacy dropped @@ -153,8 +155,8 @@ describe("Codex catalog sync hardening", () => { writeFileSync(catalogPath, JSON.stringify({ models: [ { - ...nativeEntry("gpt-5.6-sol", 0), - display_name: "Original Sol", + ...nativeEntry("gpt-5.5", 0), + display_name: "Original GPT-5.5", comp_hash: "native-sol-hash", base_instructions: "Native Sol instructions", model_messages: { instructions_template: "Native Sol instructions" }, @@ -178,17 +180,17 @@ describe("Codex catalog sync hardening", () => { adapter: "openai-chat", baseUrl: "https://api.example.test/v1", liveModels: false, - models: ["codex/gpt-5.6-sol"] + models: ["codex/gpt-5.5"] } }, codexAccounts: [{ id: "stored-team-account", isMain: false }], codexAccountNamespaces: { team: "stored-team-account" }, combos: { "nova-sol": { - alias: "gpt-5.6-sol", + alias: "gpt-5.5", nativeAlias: true, - displayName: "Nova Sol", - targets: [{ provider: "Nova1", model: "codex/gpt-5.6-sol" }] + displayName: "Nova GPT-5.5", + targets: [{ provider: "Nova1", model: "codex/gpt-5.5" }] } } }; @@ -205,13 +207,13 @@ describe("Codex catalog sync hardening", () => { tool_mode?: string | null; opencodex_catalog_kind?: string; }>; - expect(rows.filter(row => row.slug === "gpt-5.6-sol")).toEqual([ + expect(rows.filter(row => row.slug === "gpt-5.5")).toEqual([ expect.objectContaining({ - display_name: "Nova Sol", + display_name: "Nova GPT-5.5", opencodex_catalog_kind: "combo-native-alias-v1", }), ]); - expect(rows.find(row => row.slug === "team/gpt-5.6-sol")).toMatchObject({ + expect(rows.find(row => row.slug === "team/gpt-5.5")).toMatchObject({ comp_hash: "native-sol-hash", base_instructions: "Native Sol instructions", model_messages: { instructions_template: "Native Sol instructions" }, diff --git a/tests/codex-catalog.test.ts b/tests/codex-catalog.test.ts index bf5656779b..2cfdfa1ce5 100644 --- a/tests/codex-catalog.test.ts +++ b/tests/codex-catalog.test.ts @@ -18,6 +18,7 @@ import { cursorModelReasoningEfforts, } from "../src/adapters/cursor/discovery"; import { getModelMetadata, resolveMetadataProvider } from "../src/generated/model-metadata"; +import { resetCodexModelEntitlementCacheForTests, seedCodexModelEntitlementsForTests } from "../src/codex/model-entitlements"; import { clearModelCache, getProviderDiscoveryStatus, @@ -57,6 +58,7 @@ afterEach(() => { globalThis.fetch = originalFetch; clearModelCache(); resetOpenAiApiCatalogWarningStateForTests(); + resetCodexModelEntitlementCacheForTests(); }); function normalizedCombo( @@ -1258,7 +1260,9 @@ describe("combo catalog capability intersection", () => { // The "openai" provider uses forward-auth (Codex login passthrough) — fetchProviderModels // returns [] for it, so native slugs only surface through nativeOpenAiSlugs(). Before the // fix, memberByKey never contained openai/, so combos with a native-openai target were - // silently dropped from the catalog. + // silently dropped from the catalog. Sol is account-gated now, so the combo's native member + // needs a confirmed roster to be visible at all. + seedCodexModelEntitlementsForTests("main", ["gpt-5.6-sol"]); globalThis.fetch = (() => { throw new Error("forward providers must not fetch /models"); }) as typeof fetch; const config: OcxConfig = { port: 10100, diff --git a/tests/codex-convergence-account-selectors.test.ts b/tests/codex-convergence-account-selectors.test.ts index a0e367091b..9206aaa9e4 100644 --- a/tests/codex-convergence-account-selectors.test.ts +++ b/tests/codex-convergence-account-selectors.test.ts @@ -46,7 +46,7 @@ import { markModelsFetchFailure } from "../src/codex/model-cache"; import { legacyCustomModelCatalogSlugs } from "../src/codex/custom-model-catalog-migration"; import { resetCodexModelEntitlementCacheForTests } from "../src/codex/model-entitlements"; import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../src/codex/catalog/native-models"; -import { saveCodexAccountCredential } from "../src/codex/account-store"; +import { removeCodexAccountCredential, saveCodexAccountCredential } from "../src/codex/account-store"; // The canonical-bytes case spawns real syncs and runs ~2.5s in isolation, on this // tree and on a clean baseline alike. That is half of bun's 5s default, but full @@ -62,6 +62,16 @@ let catalogPath = ""; let previousCodexHome: string | undefined; let previousOpencodexHome: string | undefined; let previousCodexCliPath: string | undefined; +let previousFetch: typeof fetch; +let modelRostersByChatgptAccount: Map; + +const GPT56_NATIVE_MODELS = ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"] as const; + +function grantGpt56NativeModels(...chatgptAccountIds: string[]): void { + for (const accountId of chatgptAccountIds) { + modelRostersByChatgptAccount.set(accountId, GPT56_NATIVE_MODELS); + } +} function nativeEntry(visibility = "list"): RawEntry { return { @@ -265,12 +275,38 @@ beforeEach(() => { mkdirSync(opencodexHome); process.env.CODEX_HOME = codexHome; process.env.OPENCODEX_HOME = opencodexHome; + previousFetch = globalThis.fetch; + modelRostersByChatgptAccount = new Map(); + writeFileSync(join(codexHome, "auth.json"), JSON.stringify({ + tokens: { access_token: "main-token", account_id: "main-chatgpt-account" }, + })); + saveCodexAccountCredential("side-account-id", { + accessToken: "side-token", + refreshToken: "side-refresh", + expiresAt: Date.now() + 5 * 60_000, + chatgptAccountId: "side-chatgpt-account", + }); + globalThis.fetch = (async (input, init) => { + const url = new URL(typeof input === "string" ? input : input instanceof URL ? input : input.url); + if (url.hostname === "chatgpt.com" && url.pathname.endsWith("/models")) { + const accountId = new Headers(init?.headers).get("chatgpt-account-id") ?? ""; + return Response.json({ + models: (modelRostersByChatgptAccount.get(accountId) ?? []).map(slug => ({ + slug, + supported_in_api: true, + visibility: "list", + })), + }); + } + return previousFetch(input, init); + }) as typeof fetch; resetCatalogRuntimeStateForTests(); resetCodexRuntimeResolveCacheForTests(); resetCodexModelEntitlementCacheForTests(); }); afterEach(() => { + globalThis.fetch = previousFetch; const identity = resolveEffectiveUserIdentity(); const serializationDb = resolveCodexCatalogSerializationDatabasePath(identity, codexHome); for (const suffix of ["", "-journal", "-wal", "-shm"]) { @@ -287,6 +323,7 @@ afterEach(() => { }); test("convergence renders account-qualified rows and preserves only non-generated foreign rows", async () => { + grantGpt56NativeModels("main-chatgpt-account", "side-chatgpt-account"); writeCatalog([ nativeEntry(), accountEntry("stale-selector"), @@ -326,6 +363,7 @@ test("convergence renders account-qualified rows and preserves only non-generate }); test("convergence preserves one configured soft budget on bare and account-native rows", async () => { + grantGpt56NativeModels("main-chatgpt-account", "side-chatgpt-account"); writeCatalog([nativeEntry()]); const nextConfig = config(true); nextConfig.providers.openai!.modelAutoCompactTokenLimits = { "gpt-5.6-sol": 120_000 }; @@ -341,6 +379,7 @@ test("convergence preserves one configured soft budget on bare and account-nativ }); test("disabling the picker removes generated rows, restores bare rows, and retains foreign rows", async () => { + grantGpt56NativeModels("main-chatgpt-account", "side-chatgpt-account"); writeCatalog([ nativeEntry("hide"), accountEntry("desktop"), @@ -377,6 +416,7 @@ test("convergence drops unsupported bare native rows and never qualifies them", test("convergence projects the observed Daybreak row onto its selector and one bare row", async () => { writeCatalog([nativeEntry()]); + removeCodexAccountCredential("side-account-id"); writeFileSync(join(codexHome, "auth.json"), JSON.stringify({ tokens: { access_token: "main-token", account_id: "main-chatgpt-account" }, })); @@ -394,24 +434,8 @@ test("convergence projects the observed Daybreak row onto its selector and one b }], }, null, 2) + "\n"); - const originalFetch = globalThis.fetch; - globalThis.fetch = (async (input, init) => { - const url = new URL(typeof input === "string" ? input : input instanceof URL ? input : input.url); - if (url.hostname === "chatgpt.com" && url.pathname.endsWith("/models")) { - return Response.json({ models: [{ - slug: "gpt-daybreak-blue-latest", - supported_in_api: true, - visibility: "list", - }] }); - } - return originalFetch(input, init); - }) as typeof fetch; - let catalog: RawCatalog; - try { - catalog = await convergeCatalog(config(true)); - } finally { - globalThis.fetch = originalFetch; - } + modelRostersByChatgptAccount.set("main-chatgpt-account", ["gpt-daybreak-blue-latest"]); + const catalog = await convergeCatalog(config(true)); const models = catalog.models ?? []; const daybreak = models.find(entry => entry.slug === "desktop/gpt-daybreak-blue-latest"); expect(daybreak).toMatchObject({ @@ -447,28 +471,14 @@ test("Direct convergence does not borrow a Pool-only Daybreak grant for the bare chatgptAccountId: "side-chatgpt-account", }); - const originalFetch = globalThis.fetch; - globalThis.fetch = (async (input, init) => { - const url = new URL(typeof input === "string" ? input : input instanceof URL ? input : input.url); - if (url.hostname === "chatgpt.com" && url.pathname.endsWith("/models")) { - const accountId = new Headers(init?.headers).get("chatgpt-account-id"); - return Response.json({ models: [ - { slug: "gpt-5.6-sol", supported_in_api: true, visibility: "list" }, - ...(accountId === "side-chatgpt-account" - ? [{ slug: "gpt-daybreak-blue-latest", supported_in_api: true, visibility: "list" }] - : []), - ] }); - } - return originalFetch(input, init); - }) as typeof fetch; + modelRostersByChatgptAccount.set("main-chatgpt-account", ["gpt-5.6-sol"]); + modelRostersByChatgptAccount.set( + "side-chatgpt-account", + ["gpt-5.6-sol", "gpt-daybreak-blue-latest"], + ); const directConfig = config(true); directConfig.providers.openai!.codexAccountMode = "direct"; - let catalog: RawCatalog; - try { - catalog = await convergeCatalog(directConfig); - } finally { - globalThis.fetch = originalFetch; - } + const catalog = await convergeCatalog(directConfig); const models = catalog.models ?? []; expect(models.filter(entry => entry.slug === "gpt-daybreak-blue-latest")).toHaveLength(0); @@ -621,7 +631,7 @@ test("degraded preservation still honors explicit routed visibility policy", asy expect(models.some(entry => entry.slug === "offline/unselected-old")).toBe(false); }); -test("custom-catalog convergence reports network degradation without a fallback notice", async () => { + test("custom-catalog convergence reports network degradation without a fallback notice", async () => { catalogPath = join(codexHome, "custom-catalog.json"); writeFileSync( join(codexHome, "config.toml"), @@ -630,6 +640,9 @@ test("custom-catalog convergence reports network degradation without a fallback primeCodexRuntimeFixture(); writeCatalog([nativeEntry(), generatedRoutedEntry("offline/old-live")]); const nextConfig = config(false); + nextConfig.codexAccounts = []; + nextConfig.codexAccountNamespaces = {}; + rmSync(join(codexHome, "auth.json"), { force: true }); nextConfig.providers.offline = { adapter: "openai-chat", baseUrl: "https://offline.example.test/v1", @@ -720,6 +733,9 @@ test("OAuth admission degradation is auth-only and does not masquerade as a netw primeCodexRuntimeFixture(); writeCatalog([nativeEntry(), generatedRoutedEntry("offline/old-live")]); const nextConfig = config(false); + nextConfig.codexAccounts = []; + nextConfig.codexAccountNamespaces = {}; + rmSync(join(codexHome, "auth.json"), { force: true }); nextConfig.providers.offline = { adapter: "openai-chat", baseUrl: "https://offline.example.test/v1", @@ -770,6 +786,7 @@ test("disabled-provider selections cannot delete a foreign row in either writer" }); test("convergence clamps native, routed, and account rows to observed runtime support", async () => { + grantGpt56NativeModels("main-chatgpt-account", "side-chatgpt-account"); seedObservedRuntimeSupport(); writeCatalog([nativeEntry()]); const nextConfig = config(true); @@ -808,6 +825,7 @@ test("convergence clamps native, routed, and account rows to observed runtime su }); test("generated account rows silently win freshly gathered provider collisions", async () => { + grantGpt56NativeModels("main-chatgpt-account", "side-chatgpt-account"); writeCatalog([nativeEntry()]); const nextConfig = config(true); nextConfig.providers.team = { @@ -890,20 +908,26 @@ test("retained sync and convergence produce identical canonical bytes in either if (ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug)) expect(slugs).not.toContain(slug); else expect(slugs).toContain(slug); } + for (const entry of models.filter(entry => ( + entry.opencodex_catalog_kind === CODEX_ACCOUNT_BOUND_CATALOG_KIND + ))) { + const baseSlug = entry.slug?.slice(entry.slug.indexOf("/") + 1); + expect(ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(baseSlug ?? "")).toBe(false); + } expect(slugs).not.toContain("gpt-legacy-unsupported"); expect(slugs).toContain("user-native"); - expect(models.find(entry => entry.slug === "gpt-5.6-sol")?.visibility) + expect(models.find(entry => entry.slug === "gpt-5.5")?.visibility) .toBe(pickerEnabled ? "hide" : "list"); expect(models.some(entry => ( entry.opencodex_catalog_kind === CODEX_ACCOUNT_BOUND_CATALOG_KIND ))).toBe(pickerEnabled); if (pickerEnabled) { - expect(models.find(entry => entry.slug === "desktop/gpt-5.6-sol")).toMatchObject({ - display_name: "desktop / 5.6 Sol", + expect(models.find(entry => entry.slug === "desktop/gpt-5.5")).toMatchObject({ + display_name: "desktop / 5.5", opencodex_catalog_kind: CODEX_ACCOUNT_BOUND_CATALOG_KIND, }); - expect(models.find(entry => entry.slug === "team/gpt-5.6-sol")).toMatchObject({ - display_name: "team / 5.6 Sol", + expect(models.find(entry => entry.slug === "team/gpt-5.5")).toMatchObject({ + display_name: "team / 5.5", opencodex_catalog_kind: CODEX_ACCOUNT_BOUND_CATALOG_KIND, }); } @@ -1040,6 +1064,7 @@ test("convergence refuses a combo shadow when every backup target is present but }); test("both writers restore pristine native priorities after featured-model transitions", async () => { + grantGpt56NativeModels("main-chatgpt-account", "side-chatgpt-account"); primeCodexRuntimeFixture(); catalogPath = join(codexHome, "custom-catalog.json"); writeFileSync( diff --git a/tests/codex-envkey-admission-substitution.test.ts b/tests/codex-envkey-admission-substitution.test.ts index 55b1b2a63a..307c5f667c 100644 --- a/tests/codex-envkey-admission-substitution.test.ts +++ b/tests/codex-envkey-admission-substitution.test.ts @@ -48,7 +48,7 @@ function directConfig(): OcxConfig { baseUrl: "https://chatgpt.com/backend-api/codex", authMode: "forward", codexAccountMode: "direct", - defaultModel: "gpt-5.6-luna", + defaultModel: "gpt-5.5", }, }, apiKeys: [ @@ -105,7 +105,7 @@ async function postResponses(url: string | URL, authorization: string): Promise< return originalFetch(new URL("/v1/responses", url), { method: "POST", headers: { "content-type": "application/json", authorization }, - body: JSON.stringify({ model: "gpt-5.6-luna", input: "hi", stream: false }), + body: JSON.stringify({ model: "gpt-5.5", input: "hi", stream: false }), }); } @@ -113,7 +113,7 @@ async function postCompact(url: string | URL, authorization: string): Promise { fetcher: (async (_input, init) => { const accountId = new Headers(init?.headers).get("chatgpt-account-id"); return accountId === "chatgpt-main" - ? roster("gpt-5.6-sol", DAYBREAK) - : roster("gpt-5.6-sol"); + ? roster(SOL, LUNA, DAYBREAK) + : roster(SOL, TERRA); }) as typeof fetch, now: 1_000, }); expect([...entitledCodexAccountIdsForModel(snapshot, DAYBREAK)!]).toEqual(["main"]); - expect([...availableAccountGatedNativeModels(snapshot)]).toEqual([DAYBREAK]); - expect(entitledCodexAccountIdsForModel(snapshot, "gpt-5.6-sol")).toBeUndefined(); + expect([...entitledCodexAccountIdsForModel(snapshot, SOL)!]).toEqual(["main", "secondary"]); + expect([...entitledCodexAccountIdsForModel(snapshot, TERRA)!]).toEqual(["secondary"]); + expect([...entitledCodexAccountIdsForModel(snapshot, LUNA)!]).toEqual(["main"]); + expect([...availableAccountGatedNativeModels(snapshot)]).toEqual([SOL, TERRA, LUNA, DAYBREAK]); }); test("fails closed when an account roster cannot be confirmed", async () => { diff --git a/tests/codex-refresh.test.ts b/tests/codex-refresh.test.ts index a234b7a401..7b5833383c 100644 --- a/tests/codex-refresh.test.ts +++ b/tests/codex-refresh.test.ts @@ -159,8 +159,10 @@ describe("Codex catalog refresh", () => { expect(result.path).toBe(join(realpathSync.native(home.codexHome), "nested", "catalog.json")); expect(result.catalogWritten).toBe(true); expect(after).not.toBe(before); - expect(rewritten.models[0].slug).toBe("gpt-5.6-sol"); - expect(rewritten.models[0].display_name).toBe("GPT-5.6-Sol"); + // The fixture seeds gated Sol rows, but this isolated home has no authenticated + // roster, so sync drops them and the first surviving row is gpt-5.5. + expect(rewritten.models[0].slug).toBe("gpt-5.5"); + expect(rewritten.models[0].display_name).toBe("gpt-5.5"); expect(rewritten.models[0].context_window).toBeGreaterThan(0); } finally { home.restore(); diff --git a/tests/grok-models-effort-list.test.ts b/tests/grok-models-effort-list.test.ts index 3e0a353662..e4e0d33d95 100644 --- a/tests/grok-models-effort-list.test.ts +++ b/tests/grok-models-effort-list.test.ts @@ -3,6 +3,10 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { saveConfig } from "../src/config"; +import { + resetCodexModelEntitlementCacheForTests, + seedCodexModelEntitlementsForTests, +} from "../src/codex/model-entitlements"; import { startServer } from "../src/server"; import type { OcxConfig } from "../src/types"; import { SERVER_BUDGET_MS } from "./helpers/test-budget"; @@ -44,6 +48,7 @@ beforeEach(() => { }); afterEach(() => { + resetCodexModelEntitlementCacheForTests(); if (previousHome === undefined) delete process.env.OPENCODEX_HOME; else process.env.OPENCODEX_HOME = previousHome; if (testHome) rmSync(testHome, { recursive: true, force: true }); @@ -52,6 +57,7 @@ afterEach(() => { describe("raw /v1/models list reasoning-effort advertisement (Grok Build discovery)", () => { test("routed models with configured tiers advertise the Grok reasoning catalog shape", async () => { + seedCodexModelEntitlementsForTests("main", ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]); const config = effortConfig(); config.providers.openai = { adapter: "openai-responses", diff --git a/tests/grok-sync.test.ts b/tests/grok-sync.test.ts index d1593a949a..0ed57bcfb9 100644 --- a/tests/grok-sync.test.ts +++ b/tests/grok-sync.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "bun:test"; +import { afterEach, describe, expect, test } from "bun:test"; import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -6,10 +6,16 @@ import { injectGrokConfig } from "../src/grok/inject"; import { syncGrokConfig } from "../src/grok/sync"; import { nativeOpenAiContextWindow, visibleNativeSlugs } from "../src/codex/catalog"; import type { CatalogModel } from "../src/codex/catalog"; +import { + resetCodexModelEntitlementCacheForTests, + seedCodexModelEntitlementsForTests, +} from "../src/codex/model-entitlements"; import type { OcxConfig } from "../src/types"; const baseConfig = { port: 10100, defaultProvider: "openai", providers: {} } as unknown as OcxConfig; +afterEach(() => resetCodexModelEntitlementCacheForTests()); + function tempGrokHome(): { root: string; grokHome: string } { const root = mkdtempSync(join(tmpdir(), "ocx-grok-sync-")); const grokHome = join(root, ".grok"); @@ -44,6 +50,7 @@ describe("syncGrokConfig", () => { // and Grok fell back to its own 200k default — understating gpt-5.6-sol, which is 372k. The // window comes from the same accessor the dashboard uses, so the two surfaces agree. test("native slugs carry their real context window, not Grok's 200k default", async () => { + seedCodexModelEntitlementsForTests("main", ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]); const { root, grokHome } = tempGrokHome(); try { const result = await syncGrokConfig(10190, baseConfig, { grokHome }, { diff --git a/tests/helpers/native-main-owner-child.ts b/tests/helpers/native-main-owner-child.ts index 26d261ec0b..b70db3dde0 100644 --- a/tests/helpers/native-main-owner-child.ts +++ b/tests/helpers/native-main-owner-child.ts @@ -183,7 +183,7 @@ async function request(port: number, kind: string): Promise<{ status: number; te const response = await fetch(`http://127.0.0.1:${port}/v1/responses`, { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify({ model: "gpt-5.6-sol", input: kind, stream: false }), + body: JSON.stringify({ model: "gpt-5.5", input: kind, stream: false }), }); return { status: response.status, text: await response.text() }; } diff --git a/tests/issue-452-empty-503.test.ts b/tests/issue-452-empty-503.test.ts index 53ad74ff6e..7eac48b321 100644 --- a/tests/issue-452-empty-503.test.ts +++ b/tests/issue-452-empty-503.test.ts @@ -249,7 +249,7 @@ describe("passthrough empty 503 (#452)", () => { const response = await originalGlobalFetch(new URL("/v1/responses", serverUrl), { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer inbound-token" }, - body: JSON.stringify({ model: "gpt-5.6-sol", input: "hi", stream: false }), + body: JSON.stringify({ model: "gpt-5.5", input: "hi", stream: false }), }); expect(response.status).toBe(503); const text = await response.text(); @@ -275,7 +275,7 @@ describe("passthrough empty 503 (#452)", () => { const response = await originalGlobalFetch(new URL("/v1/responses", serverUrl), { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer inbound-token" }, - body: JSON.stringify({ model: "gpt-5.6-sol", input: "hi", stream: false }), + body: JSON.stringify({ model: "gpt-5.5", input: "hi", stream: false }), }); expect(response.status).toBe(418); expect(response.headers.get("content-type")).toContain("application/json"); @@ -297,7 +297,7 @@ describe("passthrough empty 503 (#452)", () => { const response = await originalGlobalFetch(new URL("/v1/responses", serverUrl), { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer inbound-token" }, - body: JSON.stringify({ model: "gpt-5.6-sol", input: "hi", stream: false }), + body: JSON.stringify({ model: "gpt-5.5", input: "hi", stream: false }), }); expect(response.status).toBe(status); expect(response.headers.get("content-type")).toContain("application/json"); @@ -317,7 +317,7 @@ describe("passthrough empty 503 (#452)", () => { const response = await originalGlobalFetch(new URL("/v1/responses", serverUrl), { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer inbound-token" }, - body: JSON.stringify({ model: "gpt-5.6-sol", input: "hi", stream: false }), + body: JSON.stringify({ model: "gpt-5.5", input: "hi", stream: false }), }); expect(response.status).toBe(503); expect(response.headers.get("retry-after")).toBeNull(); @@ -333,7 +333,7 @@ describe("passthrough empty 503 (#452)", () => { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer inbound-token" }, body: JSON.stringify({ - model: "gpt-5.6-sol", + model: "gpt-5.5", messages: [{ role: "user", content: "hi" }], stream: false, }), diff --git a/tests/issue-702-expired-replay-state.test.ts b/tests/issue-702-expired-replay-state.test.ts index 239d305746..5a618c4be7 100644 --- a/tests/issue-702-expired-replay-state.test.ts +++ b/tests/issue-702-expired-replay-state.test.ts @@ -93,7 +93,7 @@ function completedSse(responseId: string, text: string): string { response: { id: responseId, status: "completed", - model: "gpt-5.6-sol", + model: "gpt-5.5", output: [item], }, })}`, @@ -165,7 +165,7 @@ async function runForwardScenario( method: "POST", headers: requestHeaders, body: JSON.stringify({ - model: "gpt-5.6-sol", + model: "gpt-5.5", input: [inputMessage(HISTORICAL_USER_SENTINEL)], stream: true, store: false, @@ -192,7 +192,7 @@ async function runForwardScenario( method: "POST", headers: { ...requestHeaders, ...resumeHeaders }, body: JSON.stringify({ - model: "gpt-5.6-sol", + model: "gpt-5.5", previous_response_id: FIRST_RESPONSE_ID, input: [inputMessage(CURRENT_USER_SENTINEL)], stream: true, @@ -249,7 +249,7 @@ describe("Issue #702 expired forward replay state", () => { const responseId = "resp_issue_702_missing_spill"; setResponseStateByteCapForTests(1_024); rememberResponseState( - { model: "openai/gpt-5.6-sol", input: "x".repeat(8_000), store: false }, + { model: "openai/gpt-5.5", input: "x".repeat(8_000), store: false }, { id: responseId, status: "completed", output: [{ role: "assistant", content: "done" }] }, undefined, { force: true }, @@ -265,7 +265,7 @@ describe("Issue #702 expired forward replay state", () => { throw new Error("upstream must not be called"); }) as typeof fetch; const routeClasses: Array<{ config: OcxConfig; model: string }> = [ - { config: forwardConfig(), model: "gpt-5.6-sol" }, + { config: forwardConfig(), model: "gpt-5.5" }, { config: { port: 0, @@ -278,11 +278,11 @@ describe("Issue #702 expired forward replay state", () => { baseUrl: "https://runtime.us-east-1.kiro.dev", authMode: "key", apiKey: "synthetic-token", - models: ["gpt-5.6-sol"], + models: ["gpt-5.5"], }, }, } as OcxConfig, - model: "kiro-test/gpt-5.6-sol", + model: "kiro-test/gpt-5.5", }, { config: { @@ -296,11 +296,11 @@ describe("Issue #702 expired forward replay state", () => { baseUrl: "https://api.openai.com/v1", authMode: "key", apiKey: "provider-key", - models: ["gpt-5.6-sol"], + models: ["gpt-5.5"], }, }, } as OcxConfig, - model: "test-openai/gpt-5.6-sol", + model: "test-openai/gpt-5.5", }, ]; @@ -418,7 +418,7 @@ describe("Issue #702 expired forward replay state", () => { baseUrl: `${upstream.url.toString().replace(/\/$/, "")}/v1`, allowPrivateNetwork: true, apiKey: "provider-key", - defaultModel: "gpt-5.6-sol", + defaultModel: "gpt-5.5", }, }, } as OcxConfig); @@ -428,7 +428,7 @@ describe("Issue #702 expired forward replay state", () => { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ - model: "test-openai/gpt-5.6-sol", + model: "test-openai/gpt-5.5", previous_response_id: "resp_upstream_native_state", input: [inputMessage(CURRENT_USER_SENTINEL)], stream: true, diff --git a/tests/management-client-config-route.test.ts b/tests/management-client-config-route.test.ts index baee2a34d2..0df9ba44b8 100644 --- a/tests/management-client-config-route.test.ts +++ b/tests/management-client-config-route.test.ts @@ -1,5 +1,9 @@ -import { describe, expect, test } from "bun:test"; +import { afterEach, describe, expect, test } from "bun:test"; import { join } from "node:path"; +import { + resetCodexModelEntitlementCacheForTests, + seedCodexModelEntitlementsForTests, +} from "../src/codex/model-entitlements"; import { handleManagementAPI } from "../src/server/management-api"; import { OPENCODE_API_KEY_ENV, @@ -24,6 +28,8 @@ import { catalogConvergenceFactory } from "./helpers/catalog-convergence"; */ const REAL_LOOKING_KEY = "ocx_live_9f3c7a2b41d84e6fa05c8e17b3d92764"; +afterEach(() => resetCodexModelEntitlementCacheForTests()); + interface ClientConfigEnvelope { client: string; filename: string; @@ -190,6 +196,7 @@ describe("GET /api/client-config", () => { }, 15_000); test("DSH response keeps management reasoning metadata in the rc.6 model map", async () => { + seedCodexModelEntitlementsForTests("main", ["gpt-5.6-luna"]); const response = await clientConfigApi(baseConfig(), "?client=dsh"); expect(response.status).toBe(200); const body = await response.json() as ClientConfigEnvelope; diff --git a/tests/native-model-toggle.test.ts b/tests/native-model-toggle.test.ts index b8d153db6c..fcf42e07dc 100644 --- a/tests/native-model-toggle.test.ts +++ b/tests/native-model-toggle.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, test } from "bun:test"; +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { accountBoundNativeOpenAiSlugs, accountBoundNativeDisplayName, @@ -31,6 +31,13 @@ import { afterEach(() => resetCodexModelEntitlementCacheForTests()); +// Most of this file exercises visibility/window mechanics on Sol/Terra/Luna rows. They are +// account-gated now, so give them a confirmed main roster up front; the two gating-specific +// tests below reset the cache to assert the unconfirmed baseline first. +beforeEach(() => { + seedCodexModelEntitlementsForTests("main", ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]); +}); + function makeConfig(overrides: Partial = {}): OcxConfig { return { port: 10100, providers: {}, defaultProvider: "openai", ...overrides } as OcxConfig; } @@ -71,16 +78,23 @@ describe("native GPT model toggles (bare slugs in disabledModels)", () => { }); test("nativeModelRows hides account-gated ids until an authenticated roster confirms them", () => { + resetCodexModelEntitlementCacheForTests(); const rows = nativeModelRows({ disabledModels: ["gpt-5.6-sol"] }); expect(rows.map(r => r.slug)).toEqual( NATIVE_OPENAI_MODELS.filter(slug => !ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug)), ); - expect(rows.find(r => r.slug === "gpt-5.6-sol")?.disabled).toBe(true); - expect(rows.find(r => r.slug === "gpt-5.5")?.disabled).toBe(false); + + seedCodexModelEntitlementsForTests( + "main", + ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-daybreak-blue-latest"], + ); + const confirmed = nativeModelRows({ disabledModels: ["gpt-5.6-sol"] }); + expect(confirmed.map(r => r.slug)).toEqual(NATIVE_OPENAI_MODELS); + expect(confirmed.find(r => r.slug === "gpt-5.6-sol")?.disabled).toBe(true); + expect(confirmed.find(r => r.slug === "gpt-5.5")?.disabled).toBe(false); // Known context metadata rides along for the dashboard. - expect(rows.find(r => r.slug === "gpt-5.6-sol")?.contextWindow).toBe(272_000); + expect(confirmed.find(r => r.slug === "gpt-5.6-sol")?.contextWindow).toBe(272_000); - seedCodexModelEntitlementsForTests("main", ["gpt-daybreak-blue-latest"]); expect(nativeModelRows({ disabledModels: [] }).map(row => row.slug)) .toContain("gpt-daybreak-blue-latest"); }); @@ -648,6 +662,7 @@ describe("native GPT model toggles (bare slugs in disabledModels)", () => { }); test("management API surfaces: /api/models leads with native rows; subagent available drops disabled bare slugs", async () => { + resetCodexModelEntitlementCacheForTests(); const config = makeConfig({ disabledModels: ["gpt-5.6-sol"] }); const modelsRes = await handleManagementAPI( @@ -658,7 +673,16 @@ describe("native GPT model toggles (bare slugs in disabledModels)", () => { expect(nativeRows.map(r => r.namespaced)).toEqual( NATIVE_OPENAI_MODELS.filter(slug => !ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug)), ); - expect(nativeRows.find(r => r.namespaced === "gpt-5.6-sol")?.disabled).toBe(true); + + // A confirmed roster makes the gated rows selectable again; a bare disable still wins. + seedCodexModelEntitlementsForTests("main", ["gpt-5.6-sol"]); + const confirmedRes = await handleManagementAPI( + new Request("http://localhost/api/models"), new URL("http://localhost/api/models"), config, + ); + const confirmedRows = (await confirmedRes!.json() as Array<{ namespaced: string; native?: boolean; disabled: boolean }>) + .filter(r => r.native); + expect(confirmedRows.map(r => r.namespaced)).toContain("gpt-5.6-sol"); + expect(confirmedRows.find(r => r.namespaced === "gpt-5.6-sol")?.disabled).toBe(true); // Native rows lead the response so the GUI pins the group first. expect(rows[0]?.native).toBe(true); diff --git a/tests/native-profile-crash-boundaries.test.ts b/tests/native-profile-crash-boundaries.test.ts index 66bc308936..d54a85495f 100644 --- a/tests/native-profile-crash-boundaries.test.ts +++ b/tests/native-profile-crash-boundaries.test.ts @@ -184,7 +184,7 @@ function spawnStartup( } async function mainRequest(port: number) { - return fetch(`http://127.0.0.1:${port}/v1/responses`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ model: "gpt-5.6-sol", input: "crash recovery", stream: false }) }); + return fetch(`http://127.0.0.1:${port}/v1/responses`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ model: "gpt-5.5", input: "crash recovery", stream: false }) }); } const boundaries: Array<{ diff --git a/tests/native-profile-startup.test.ts b/tests/native-profile-startup.test.ts index 247f2df7fa..921796a6fd 100644 --- a/tests/native-profile-startup.test.ts +++ b/tests/native-profile-startup.test.ts @@ -298,7 +298,7 @@ async function mainRequest(port: number): Promise { return fetch(`http://127.0.0.1:${port}/v1/responses`, { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify({ model: "gpt-5.6-sol", input: "startup gate", stream: false }), + body: JSON.stringify({ model: "gpt-5.5", input: "startup gate", stream: false }), }); } diff --git a/tests/openai-provider-option-e2e.test.ts b/tests/openai-provider-option-e2e.test.ts index e680675418..104119a9ba 100644 --- a/tests/openai-provider-option-e2e.test.ts +++ b/tests/openai-provider-option-e2e.test.ts @@ -176,6 +176,19 @@ describe("OpenAI provider-option integration spine", () => { rate_limit: { secondary_window: { used_percent: isAdded ? 10 : 90 } }, }); } + if (request.method === "GET" && url.pathname === "/backend-api/codex/models") { + const authorization = request.headers.get("authorization"); + const accountId = request.headers.get("chatgpt-account-id"); + const explicitlyEntitled = accountId === "fixture-main-account" + || accountId === "fixture-pool-account" + || authorization === "Bearer fixture-caller-main"; + const slugs = explicitlyEntitled + ? ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"] + : []; + return Response.json({ + models: slugs.map(slug => ({ slug, supported_in_api: true, visibility: "list" })), + }); + } const upstreamTuple = `${request.method} ${url.href}`; if (!upstreamTuples.has(upstreamTuple)) { throw new Error(`deny-by-default fetch blocked: ${upstreamTuple}`); @@ -214,6 +227,7 @@ describe("OpenAI provider-option integration spine", () => { websocketRegistry, requestLog, catalog, + modelEntitlements, serverModule, mainAccount, sidecar, @@ -227,6 +241,7 @@ describe("OpenAI provider-option integration spine", () => { import("../src/codex/websocket-registry"), import("../src/server/request-log"), import("../src/codex/catalog"), + import("../src/codex/model-entitlements"), import("../src/server"), import("../src/codex/main-account"), import("../src/providers/openai-sidecar"), @@ -235,6 +250,7 @@ describe("OpenAI provider-option integration spine", () => { resets.push( requestLog.clearRequestLogsForTests, catalog.resetCatalogRuntimeStateForTests, + modelEntitlements.resetCodexModelEntitlementCacheForTests, routing.clearThreadAccountMap, routing.clearCodexUpstreamHealth, authApi.clearAccountQuota, diff --git a/tests/responses-account-label.test.ts b/tests/responses-account-label.test.ts index ebb0f76636..f17b58b18c 100644 --- a/tests/responses-account-label.test.ts +++ b/tests/responses-account-label.test.ts @@ -48,7 +48,7 @@ function request(): Request { return new Request("http://localhost/v1/responses", { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify({ model: "gpt-5.6-sol", input: "hello", stream: false }), + body: JSON.stringify({ model: "gpt-5.5", input: "hello", stream: false }), }); } diff --git a/tests/responses-compaction-routing.test.ts b/tests/responses-compaction-routing.test.ts index 6ec6473503..10d45dda23 100644 --- a/tests/responses-compaction-routing.test.ts +++ b/tests/responses-compaction-routing.test.ts @@ -232,11 +232,11 @@ describe("Codex auth-context error parity (#2392)", () => { ]; function regularAuthRequest(): Request { - return compactionRequest({ model: "gpt-5.6-sol", input: "hello", stream: false }); + return compactionRequest({ model: "gpt-5.5", input: "hello", stream: false }); } function compactAuthRequest(): Request { - return compactionRequest(baseCompactionBody({ model: "gpt-5.6-sol" })); + return compactionRequest(baseCompactionBody({ model: "gpt-5.5" })); } test.each(cases)("maps $label identically on regular and compact Responses", async testCase => { @@ -393,7 +393,16 @@ describe("native Codex pool compaction", () => { expiresAt: Date.now() + 300_000, chatgptAccountId: "pool_acc", }); - globalThis.fetch = (async () => { + globalThis.fetch = (async (input, init) => { + const request = new Request(input, init); + const url = new URL(request.url); + if (request.method === "GET" && url.pathname === "/backend-api/codex/models") { + const accountId = request.headers.get("chatgpt-account-id"); + const slugs = accountId === "pool_acc" ? ["gpt-5.6-terra"] : []; + return Response.json({ + models: slugs.map(slug => ({ slug, supported_in_api: true, visibility: "list" })), + }); + } if (sparkPhase) { return Response.json({ error: { message: "Spark quota exhausted" } }, { status: 429, @@ -907,7 +916,7 @@ describe("compact alternate-account attempt (#913)", () => { }) as typeof fetch; const res = await handleResponsesCompact( - compactionRequest(baseCompactionBody({ model: "side/gpt-5.6-sol" })), + compactionRequest(baseCompactionBody({ model: "side/gpt-5.5" })), config, { model: "", provider: "" }, ); @@ -1191,7 +1200,7 @@ describe("compact alternate-account attempt (#913)", () => { const request = new Request("http://localhost/v1/responses", { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify({ model: "gpt-5.6-sol", input: "hello", stream: false }), + body: JSON.stringify({ model: "gpt-5.5", input: "hello", stream: false }), }); await expect(handleResponses(request, config, { model: "", provider: "" })) .rejects.toThrow("synthetic build failure"); @@ -1216,7 +1225,7 @@ describe("compact alternate-account attempt (#913)", () => { const request = () => new Request("http://localhost/v1/responses", { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify({ model: "gpt-5.6-sol", input: "hello", stream: false }), + body: JSON.stringify({ model: "gpt-5.5", input: "hello", stream: false }), }); const authSpy = spyOn(authContextModule, "resolveCodexAuthContext"); try { diff --git a/tests/server-auth.test.ts b/tests/server-auth.test.ts index de2de9fc42..401fcde4af 100644 --- a/tests/server-auth.test.ts +++ b/tests/server-auth.test.ts @@ -150,7 +150,7 @@ afterEach(() => { if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true }); }); -const POOL_RETRY_MODEL = "gpt-5.6-sol"; +const POOL_RETRY_MODEL = "gpt-5.5"; function unsupportedModelBody(model = POOL_RETRY_MODEL): string { return JSON.stringify({ diff --git a/tests/server-combo-failover-e2e.test.ts b/tests/server-combo-failover-e2e.test.ts index 96348e7401..bfcced0f35 100644 --- a/tests/server-combo-failover-e2e.test.ts +++ b/tests/server-combo-failover-e2e.test.ts @@ -1123,7 +1123,7 @@ describe("server combo failover 030 activation matrix", () => { }, }, [ { provider: "openai", model: "gpt-5.3-codex-spark" }, - { provider: "openai", model: "gpt-5.6-terra" }, + { provider: "openai", model: "gpt-5.5" }, ]); config.codexAccounts = [{ id: rawAccountId, @@ -1150,13 +1150,13 @@ describe("server combo failover 030 activation matrix", () => { headers: { "x-codex-primary-reset-at": String(resetAt) }, }); } - return Response.json(responsesSuccess("Terra fallback", "gpt-5.6-terra")); + return Response.json(responsesSuccess("Shared-native fallback", "gpt-5.5")); }; const response = await post(config); expect(response.status).toBe(200); expect(upstreamCalls).toBe(2); - expect(await response.json()).toMatchObject({ model: "gpt-5.6-terra" }); + expect(await response.json()).toMatchObject({ model: "gpt-5.5" }); }); test("keeps a failed estimate on A without overwriting B reported usage", async () => { diff --git a/tests/subagent-fallback-handle-responses.test.ts b/tests/subagent-fallback-handle-responses.test.ts index 937b0aadeb..288786f2d6 100644 --- a/tests/subagent-fallback-handle-responses.test.ts +++ b/tests/subagent-fallback-handle-responses.test.ts @@ -27,6 +27,9 @@ import { resetSubagentModelFallbackStateForTests, setSubagentQuotaPrimeForTests, } from "../src/codex/subagent-model-fallback"; +import { + resetCodexModelEntitlementCacheForTests, +} from "../src/codex/model-entitlements"; import { resolveCodexAuthContext, type CodexAuthContext } from "../src/codex/auth-context"; import { handleResponses } from "../src/server/responses"; import { isEagerRelaySseResponse } from "../src/server/relay"; @@ -52,6 +55,9 @@ beforeEach(() => { clearCodexUpstreamHealth(); clearAccountQuota(); resetSubagentModelFallbackStateForTests(); + // Gated-native negative rosters are cached process-wide for 15s; a real-network + // miss in one test must not fail-closed the next test's entitlement lookups. + resetCodexModelEntitlementCacheForTests(); }); afterEach(() => { @@ -77,6 +83,7 @@ function fernetFixture(ciphertextBytes = 16): string { } const FERNET_TASK = fernetFixture(); +const GPT56_NATIVE_MODELS = ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"] as const; function encryptedAgentInput(): unknown[] { return [{ @@ -148,21 +155,57 @@ function installPoolCredential(accountId: string, chatgptAccountId: string, now: }); } +function isCodexModelsFetch(input: unknown): boolean { + try { + const url = new URL(String(input)); + return url.hostname === "chatgpt.com" && url.pathname.endsWith("/models"); + } catch { + return false; + } +} + +function codexRosterResponse(slugs: readonly string[]): Response { + return Response.json({ + models: slugs.map(slug => ({ + slug, supported_in_api: true, visibility: "list", + })), + }); +} + +function codexRosterKey(headers: Headers): string { + return headers.get("chatgpt-account-id") ?? headers.get("authorization") ?? ""; +} + +function installCodexRosterMock(rostersByCredential: Readonly>): void { + globalThis.fetch = (async (input, init) => { + if (isCodexModelsFetch(input)) { + const credential = codexRosterKey(new Headers(init?.headers)); + return codexRosterResponse(rostersByCredential[credential] ?? []); + } + return originalFetch(input, init); + }) as typeof fetch; +} + function mockUpstream(capture: { urls: string[]; bodies: string[]; auths: Array; -}): void { +}, rostersByCredential: Readonly> = {}): void { globalThis.fetch = (async (input, init) => { - capture.urls.push(String(input)); - capture.bodies.push(typeof init?.body === "string" ? init.body : ""); const headers = new Headers(init?.headers); + if (isCodexModelsFetch(input)) { + const credential = codexRosterKey(headers); + return codexRosterResponse(rostersByCredential[credential] ?? []); + } + const body = typeof init?.body === "string" ? init.body : ""; + capture.urls.push(String(input)); + capture.bodies.push(body); capture.auths.push(headers.get("authorization")); return Response.json({ id: "resp_test", object: "response", status: "completed", - model: "gpt-5.6-sol", + model: (JSON.parse(body) as { model?: string }).model, output: [], usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }, }); @@ -231,7 +274,7 @@ describe("subagent fallback without primary auth cooldown failure", () => { }) as typeof fetch; const response = await postSpawn(cfg, { - model: "side/gpt-5.6-sol", + model: "side/gpt-5.5", input: readableAgentInput(), stream: false, }); @@ -244,7 +287,7 @@ describe("subagent fallback without primary auth cooldown failure", () => { expect(urls.length).toBeGreaterThan(0); expect(urls.every(url => url.includes("chatgpt.com/backend-api/codex"))).toBe(true); expect(new Set(accounts)).toEqual(new Set(["pool_acc"])); - expect(new Set(models)).toEqual(new Set(["gpt-5.6-sol"])); + expect(new Set(models)).toEqual(new Set(["gpt-5.5"])); }); test("cooled primary with no probe lease selects healthy routed fallback", async () => { @@ -264,7 +307,7 @@ describe("subagent fallback without primary auth cooldown failure", () => { const response = await postSpawn( cfg, - { model: "gpt-5.6-sol", input: readableAgentInput(), stream: false }, + { model: "gpt-5.5", input: readableAgentInput(), stream: false }, { onCodexAuthContextResolved: (ctx) => authPublications.push(ctx) }, ); @@ -294,7 +337,7 @@ describe("subagent fallback without primary auth cooldown failure", () => { }) as typeof fetch; const response = await postSpawn(cfg, { - model: "gpt-5.6-sol", + model: "gpt-5.5", input: readableAgentInput(), stream: false, }); @@ -317,7 +360,7 @@ describe("subagent fallback without primary auth cooldown failure", () => { const resetAt = Math.floor((now + 4 * 24 * 60 * 60_000) / 1000); recordCodexUpstreamOutcome(cfg, "pool-a", 429, { resetAt, now }); const { noteSubagentModelFailure } = await import("../src/codex/subagent-model-fallback"); - noteSubagentModelFailure("gpt-5.6-sol", "429", cfg, "pool-a"); + noteSubagentModelFailure("gpt-5.5", "429", cfg, "pool-a"); const probeAt = now + CODEX_QUOTA_PROBE_INTERVAL_MS; Date.now = () => probeAt; @@ -329,7 +372,7 @@ describe("subagent fallback without primary auth cooldown failure", () => { const response = await postSpawn( cfg, - { model: "gpt-5.6-sol", input: readableAgentInput(), stream: false }, + { model: "gpt-5.5", input: readableAgentInput(), stream: false }, { onCodexAuthContextResolved: (ctx) => { authPublications.push(ctx); @@ -543,7 +586,7 @@ describe("subagent fallback final-route normalization", () => { noteSubagentModelFailure("grok-4.5", "429", cfg); const capture = { urls: [] as string[], bodies: [] as string[], auths: [] as Array }; - mockUpstream(capture); + mockUpstream(capture, { "Bearer caller-codex-token": ["gpt-5.6-terra"] }); const response = await postSpawn(cfg, { model: "xai/grok-4.5", @@ -576,13 +619,13 @@ describe("subagent fallback final-route normalization", () => { subagentModelFallback: ["xai/grok-4.5"], }); const { noteSubagentModelFailure } = await import("../src/codex/subagent-model-fallback"); - noteSubagentModelFailure("gpt-5.6-sol", "rate limit exceeded", cfg); + noteSubagentModelFailure("gpt-5.5", "rate limit exceeded", cfg); const capture = { urls: [] as string[], bodies: [] as string[], auths: [] as Array }; mockUpstream(capture); const response = await postSpawn(cfg, { - model: "gpt-5.6-sol", + model: "gpt-5.5", input: readableAgentInput(), stream: false, reasoning: { effort: "max" }, @@ -618,7 +661,7 @@ describe("subagent fallback final-route normalization", () => { }); const capture = { urls: [] as string[], bodies: [] as string[], auths: [] as Array }; - mockUpstream(capture); + mockUpstream(capture, { "Bearer caller-codex-token": ["gpt-5.6-terra"] }); const response = await postSpawn(cfg, { model: "xai/grok-4.5", @@ -653,13 +696,13 @@ describe("subagent fallback final-route normalization", () => { subagentModelFallback: ["xai/grok-4.5"], }); const { noteSubagentModelFailure } = await import("../src/codex/subagent-model-fallback"); - noteSubagentModelFailure("gpt-5.6-sol", "rate limit exceeded", cfg); + noteSubagentModelFailure("gpt-5.5", "rate limit exceeded", cfg); const capture = { urls: [] as string[], bodies: [] as string[], auths: [] as Array }; mockUpstream(capture); const response = await postSpawn(cfg, { - model: "gpt-5.6-sol", + model: "gpt-5.5", input: readableAgentInput(), stream: false, }); @@ -674,6 +717,12 @@ describe("native fallback account preview", () => { const now = 1_800_000_000_000; Date.now = () => now; installPoolCredential("pool-a", "pool_acc_a", now); + // Sol/Terra/Luna are account-gated; grant them only to the accounts this + // preview test configured instead of giving every discovery caller a roster. + installCodexRosterMock({ + pool_acc_a: GPT56_NATIVE_MODELS, + pool_acc_b: GPT56_NATIVE_MODELS, + }); installPoolCredential("pool-b", "pool_acc_b", now); const cfg = poolNativePlusRoutedConfig({ defaultProvider: "xai", @@ -731,6 +780,10 @@ describe("native fallback account preview", () => { const now = 1_800_000_000_000; Date.now = () => now; installPoolCredential("pool-a", "pool_acc_a", now); + installCodexRosterMock({ + pool_acc_a: GPT56_NATIVE_MODELS, + pool_acc_b: GPT56_NATIVE_MODELS, + }); installPoolCredential("pool-b", "pool_acc_b", now); const cfg = poolNativePlusRoutedConfig({ defaultProvider: "xai", @@ -779,6 +832,10 @@ describe("native fallback account preview", () => { const now = 1_800_000_000_000; Date.now = () => now; installPoolCredential("pool-a", "pool_acc_a", now); + installCodexRosterMock({ + pool_acc_a: GPT56_NATIVE_MODELS, + pool_acc_b: GPT56_NATIVE_MODELS, + }); installPoolCredential("pool-b", "pool_acc_b", now); const cfg = poolNativePlusRoutedConfig({ defaultProvider: "xai", @@ -818,7 +875,10 @@ describe("native fallback account preview", () => { let finalAuth: CodexAuthContext | undefined; const capture = { urls: [] as string[], bodies: [] as string[], auths: [] as Array }; - mockUpstream(capture); + mockUpstream(capture, { + pool_acc_a: GPT56_NATIVE_MODELS, + pool_acc_b: GPT56_NATIVE_MODELS, + }); const response = await postSpawn( cfg, @@ -836,6 +896,10 @@ describe("native fallback account preview", () => { const now = 1_800_000_000_000; Date.now = () => now; installPoolCredential("pool-a", "pool_acc_a", now); + installCodexRosterMock({ + pool_acc_a: GPT56_NATIVE_MODELS, + pool_acc_b: GPT56_NATIVE_MODELS, + }); installPoolCredential("pool-b", "pool_acc_b", now); const cfg = poolNativePlusRoutedConfig({ defaultProvider: "xai", @@ -885,6 +949,10 @@ describe("native fallback account preview", () => { const now = 1_800_000_000_000; Date.now = () => now; installPoolCredential("pool-a", "pool_acc_a", now); + installCodexRosterMock({ + pool_acc_a: GPT56_NATIVE_MODELS, + pool_acc_b: GPT56_NATIVE_MODELS, + }); installPoolCredential("pool-b", "pool_acc_b", now); const cfg = poolNativePlusRoutedConfig({ activeCodexAccountId: "pool-a", @@ -1050,7 +1118,7 @@ describe("native passthrough terminal finalization", () => { try { const response = await postSpawn( cfg, - { model: "gpt-5.6-sol", input: readableAgentInput(), stream: true }, + { model: "gpt-5.5", input: readableAgentInput(), stream: true }, { onNativePassthroughTerminal: (status) => terminals.push(status), }, @@ -1060,7 +1128,7 @@ describe("native passthrough terminal finalization", () => { await Bun.sleep(20); return { terminals, - healthBlocked: isModelHealthBlocked("gpt-5.6-sol", cfg, "pool-a"), + healthBlocked: isModelHealthBlocked("gpt-5.5", cfg, "pool-a"), responseText, }; } finally { @@ -1119,7 +1187,7 @@ describe("darwin explicit eager-relay path selection", () => { mockSseUpstream(completedSse); return postSpawn( poolNativePlusRoutedConfig({ streamMode, activeCodexAccountId: "pool-a" }), - { model: "gpt-5.6-sol", input: readableAgentInput(), stream: true }, + { model: "gpt-5.5", input: readableAgentInput(), stream: true }, ); } diff --git a/tests/vision-reasoning-contract.test.ts b/tests/vision-reasoning-contract.test.ts index 813e462b1d..0627422d92 100644 --- a/tests/vision-reasoning-contract.test.ts +++ b/tests/vision-reasoning-contract.test.ts @@ -5,6 +5,10 @@ import { join } from "node:path"; import { handleConfigCommand } from "../src/cli/config-command"; import { handleManagementAPI } from "../src/server/management-api"; import { listManagementModelRows } from "../src/server/management/model-rows"; +import { + resetCodexModelEntitlementCacheForTests, + seedCodexModelEntitlementsForTests, +} from "../src/codex/model-entitlements"; import type { OcxConfig } from "../src/types"; import { resolveOpenAiVisionModel } from "../src/vision"; import { ManagementRequest as Request } from "./helpers/management-auth"; @@ -54,6 +58,9 @@ function validCliConfig(visionSidecar: Record): Record { test("native management rows expose vision-safe reasoning ladders", async () => { + // This contract is about the effort ladders themselves; Sol/Luna are account-gated, + // so confirm a roster or their rows would be filtered before the ladder is read. + seedCodexModelEntitlementsForTests("main", ["gpt-5.6-sol", "gpt-5.6-luna"]); const config: OcxConfig = { port: 10100, defaultProvider: "none", providers: {} }; const rows = await listManagementModelRows(config); const efforts = (id: string) => (rows.find(row => row.native === true && row.id === id) as @@ -209,6 +216,7 @@ describe("vision reasoning capability contracts", () => { if (previousHome === undefined) delete process.env.OPENCODEX_HOME; else process.env.OPENCODEX_HOME = previousHome; rmSync(isolatedHome, { recursive: true, force: true }); + resetCodexModelEntitlementCacheForTests(); } }); }); diff --git a/tests/ws-upstream.test.ts b/tests/ws-upstream.test.ts index 2d7eba9a38..c16060c9f5 100644 --- a/tests/ws-upstream.test.ts +++ b/tests/ws-upstream.test.ts @@ -48,7 +48,7 @@ function streamingInit(body: Record = {}): RequestInit { return { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer test" }, - body: JSON.stringify({ model: "gpt-5.6-luna", stream: true, ...body }), + body: JSON.stringify({ model: "gpt-5.5", stream: true, ...body }), }; } @@ -109,7 +109,7 @@ describe("shouldUseCodexWsUpstream", () => { // Non-streaming turns keep HTTP: the WS path only speaks the event protocol. expect(shouldUseCodexWsUpstream(CODEX_URL, { method: "POST", - body: JSON.stringify({ model: "gpt-5.6-luna" }), + body: JSON.stringify({ model: "gpt-5.5" }), })).toBe(false); expect(shouldUseCodexWsUpstream(CODEX_URL, { method: "GET" })).toBe(false); expect(shouldUseCodexWsUpstream("https://api.openai.com/v1/responses", streamingInit())).toBe(false); @@ -121,12 +121,12 @@ describe("shouldUseCodexWsUpstream", () => { // Nested stream:true must not flip the transport. expect(shouldUseCodexWsUpstream(CODEX_URL, { method: "POST", - body: JSON.stringify({ model: "gpt-5.6-luna", metadata: { stream: true } }), + body: JSON.stringify({ model: "gpt-5.5", metadata: { stream: true } }), })).toBe(false); // Whitespace-formatted JSON still routes. expect(shouldUseCodexWsUpstream(CODEX_URL, { method: "POST", - body: "{\n \"model\": \"gpt-5.6-luna\",\n \"stream\" : true\n}", + body: "{\n \"model\": \"gpt-5.5\",\n \"stream\" : true\n}", })).toBe(true); // Non-boolean stream values stay on HTTP. expect(shouldUseCodexWsUpstream(CODEX_URL, { @@ -264,7 +264,7 @@ describe("handleResponses Codex WS relay selection", () => { return new Request("http://localhost/v1/responses", { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer test" }, - body: JSON.stringify({ model: "gpt-5.6-luna", input: "hello", stream: true }), + body: JSON.stringify({ model: "gpt-5.5", input: "hello", stream: true }), }); }