Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions src/codex/catalog/native-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,19 @@ const NATIVE_OPENAI_CAPABILITY_SOURCES: Readonly<Record<string, string>> = Objec
[NATIVE_DAYBREAK_BLUE_MODEL]: "gpt-5.6-sol",
});

/** Account-scoped native ids that may inherit metadata but never enter the bare allowlist. */
/**
* Native ids whose capability metadata is inherited from another pinned native row.
*
* Membership here is about METADATA INHERITANCE only, and is independent of whether the
* slug is also globally allowlisted in `NATIVE_OPENAI_MODELS`. `gpt-daybreak-blue-latest`
* is now in BOTH: it inherits Sol's capability shape AND ships as a globally supported
* native row (owner decision, devlog 260816_codexrs_multiagent_v2_and_history_perf/011).
*
* The maps that consume the union of these two lists (`PINNED_NATIVE_CAPABILITY_ENTRIES`,
* `UPSTREAM_NATIVE_ENTRIES`) are keyed by slug, so an overlapping id collapses to one
* entry. Catalog row generation iterates `NATIVE_OPENAI_MODELS` alone, so it still emits
* exactly one bare row and one row per account selector.
*/
export const NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS = Object.freeze(
Object.keys(NATIVE_OPENAI_CAPABILITY_SOURCES),
);
Expand All @@ -25,10 +37,28 @@ export function nativeOpenAiCapabilitySourceSlug(slug: string): string {
return NATIVE_OPENAI_CAPABILITY_SOURCES[slug] ?? slug;
}

/** Native OpenAI model ids that this release can route and restore with authoritative metadata. */
/**
* Native OpenAI model ids that this release can route and restore with authoritative metadata.
*
* `gpt-daybreak-blue-latest` is entitlement-gated upstream: it is absent from codex-rs's
* bundled catalog and reaches a client only through an authenticated `/models` response.
* It is listed here by explicit owner decision so the row exists without waiting for an
* observation, because opencodex injects `model_catalog_json` and codex-rs therefore builds
* a `StaticModelsManager` whose refresh is a no-op — an entitled account had no way to
* discover it on a clean install.
*
* Accepted tradeoff: an UNENTITLED account also sees the row. Catalog sync still succeeds;
* selecting the model reaches the canonical OpenAI provider and the backend answers 400
* "model not supported for this account", which is relayed (a bare pooled route may first
* retry one alternate account on that exact body; a selector-qualified route is fixed and
* relays immediately). `disabledModels` hides the row but is NOT a runtime routing denial.
*
* Devlog: 260816_codexrs_multiagent_v2_and_history_perf/011 §4-bis.
*/
export const NATIVE_OPENAI_MODELS = [
"gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex-spark",
"gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna",
NATIVE_DAYBREAK_BLUE_MODEL,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update the Daybreak model documentation

Adding this slug to the global native set directly contradicts docs-site/src/content/docs/guides/codex-app-models.md:26-27 and :42-46, which still state that Daybreak is observation-only, never creates a bare row, and requires an explicit customModels entry. Users will therefore configure an unnecessary duplicate route and misunderstand the entitlement-gated 400 behavior; update the English page and affected translations to document the globally seeded native row and its entitlement limitation.

AGENTS.md reference: src/AGENTS.md:L28-L28

Useful? React with 👍 / 👎.

];

export const SUPPORTED_NATIVE_OPENAI_SLUGS = new Set(NATIVE_OPENAI_MODELS);
21 changes: 16 additions & 5 deletions tests/claude-models-discovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ test("Codex discovery restores account rows for supported natives hidden on disk
}
});

test("Codex discovery preserves an observed account-only native id exactly", async () => {
test("Codex discovery exposes the observed native as a selector row plus one global bare row", async () => {
const config = configWithStaticModels();
config.providers.openai = {
adapter: "openai-responses",
Expand Down Expand Up @@ -378,7 +378,9 @@ test("Codex discovery preserves an observed account-only native id exactly", asy
const plain = await fetch(new URL("/v1/models", server.url))
.then(response => response.json()) as { data: Array<{ id: string }> };
expect(plain.data).toContainEqual(expect.objectContaining({ id: "team/gpt-daybreak-blue-latest" }));
expect(plain.data.some(model => model.id === "gpt-daybreak-blue-latest")).toBe(false);
// Daybreak is globally allowlisted (owner decision, devlog 260816_.../011), so the bare
// id is now discoverable too, exactly once.
expect(plain.data.filter(model => model.id === "gpt-daybreak-blue-latest")).toHaveLength(1);

const managementUrl = new URL("http://localhost/api/models");
const managementResponse = await handleManagementAPI(
Expand All @@ -387,11 +389,15 @@ test("Codex discovery preserves an observed account-only native id exactly", asy
config,
);
const management = await managementResponse!.json() as Array<{ id: string; native?: boolean }>;
// Once Daybreak is globally allowlisted the management surface reports it under its
// GLOBAL bare identity rather than as an account-qualified discovery row
// (model-rows.ts:59 / metadata.ts:243). Exactly one row, and no selector duplicate.
expect(management).toContainEqual(expect.objectContaining({
id: "team/gpt-daybreak-blue-latest",
id: "gpt-daybreak-blue-latest",
native: true,
}));
expect(management.some(model => model.id === "gpt-daybreak-blue-latest")).toBe(false);
expect(management.filter(model => model.id === "gpt-daybreak-blue-latest")).toHaveLength(1);
expect(management.some(model => model.id === "team/gpt-daybreak-blue-latest")).toBe(false);

const catalog = await fetch(new URL("/v1/models?client_version=1.0.0", server.url))
.then(response => response.json()) as { models: Array<{ slug: string; visibility?: string }> };
Expand All @@ -403,8 +409,13 @@ test("Codex discovery preserves an observed account-only native id exactly", asy
const anthropic = await fetch(new URL("/v1/models?flavor=anthropic&ids=cli", server.url), {
headers: { "anthropic-version": "2023-06-01" },
}).then(response => response.json()) as { data: Array<{ id: string }> };
expect(anthropic.data.some(model => model.id === claudeCodeNativeAlias("team/gpt-daybreak-blue-latest"))).toBe(true);
// Claude discovery advertises only rows visible in the Codex catalog. The global bare
// Daybreak row is synthesized as visibility "hide" (asserted above), and the
// account-qualified projection is no longer produced now that the slug is globally
// allowlisted, so neither identity reaches the Anthropic surface. Verified empirically:
// the filtered id list contained gpt-5.5 only.
expect(anthropic.data.some(model => model.id === claudeCodeNativeAlias("gpt-daybreak-blue-latest"))).toBe(false);
expect(anthropic.data.some(model => model.id === claudeCodeNativeAlias("team/gpt-daybreak-blue-latest"))).toBe(false);
} finally {
await server.stop(true);
}
Expand Down
9 changes: 7 additions & 2 deletions tests/codex-catalog-sync-hardening.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,9 @@ describe("Codex catalog sync hardening", () => {
use_responses_lite: true,
supports_parallel_tool_calls: true,
});
expect(rows.some(row => row.slug === "gpt-daybreak-blue-latest")).toBe(false);
// Daybreak is globally allowlisted now (owner decision, devlog 260816_.../011),
// so the bare row IS expected — exactly once — alongside the account-qualified row.
expect(rows.filter(row => row.slug === "gpt-daybreak-blue-latest")).toHaveLength(1);
});

test("explicit Codex-forward Daybreak survives sync with Sol metadata while account picker is off", () => {
Expand Down Expand Up @@ -477,8 +479,11 @@ describe("Codex catalog sync hardening", () => {
opencodex_catalog_kind: "custom-model-v1",
});
expect(daybreak?.base_instructions).toContain("powered by the gpt-daybreak-blue-latest");
expect(rows.some(row => row.slug === "gpt-daybreak-blue-latest")).toBe(false);
// The global native row exists (owner decision); the explicit Codex-forward custom row
// above is a separate identity and must not collapse into it.
expect(rows.filter(row => row.slug === "gpt-daybreak-blue-latest")).toHaveLength(1);
expect(rows.some(row => row.slug === "main/gpt-daybreak-blue-latest")).toBe(false);
// The separately billed API-key alias must still never reach the Codex surface.
expect(rows.some(row => row.slug === "openai-apikey/daybreak-blue-latest")).toBe(false);
});

Expand Down
17 changes: 13 additions & 4 deletions tests/codex-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2793,7 +2793,11 @@ describe("Codex catalog routed normalization", () => {
expect(nativeOpenAiContextWindow("gpt-5.4", 272_000)).toBe(272_000);
});

test("account-scoped Daybreak Blue inherits Sol capabilities without expanding the bare allowlist", () => {
// Owner decision (devlog 260816_.../011 §4-bis): Daybreak Blue is now a GLOBALLY
// allowlisted native, so a bare row IS expected. It still inherits Sol's capability
// shape, and the overlap between NATIVE_OPENAI_MODELS and
// NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS must not duplicate any row.
test("Daybreak Blue inherits Sol capabilities and ships one bare row plus one row per selector", () => {
expect(NATIVE_DAYBREAK_BLUE_MODEL).toBe("gpt-daybreak-blue-latest");
expect(nativeOpenAiCapabilitySourceSlug(NATIVE_DAYBREAK_BLUE_MODEL)).toBe("gpt-5.6-sol");
expect(nativeOpenAiContextWindow(NATIVE_DAYBREAK_BLUE_MODEL)).toBe(372_000);
Expand Down Expand Up @@ -2821,6 +2825,7 @@ describe("Codex catalog routed normalization", () => {
expect((source?.model_messages as { instructions_template?: string })?.instructions_template)
.toContain("powered by the gpt-daybreak-blue-latest");

// NATIVE_OPENAI_MODELS already contains the slug; passing it again would double it.
const projected = buildCatalogEntries(
nativeTemplate(),
NATIVE_OPENAI_MODELS,
Expand All @@ -2833,8 +2838,8 @@ describe("Codex catalog routed normalization", () => {
new Set(),
new Set(),
undefined,
[...NATIVE_OPENAI_MODELS, NATIVE_DAYBREAK_BLUE_MODEL],
new Map([["main", [...NATIVE_OPENAI_MODELS, NATIVE_DAYBREAK_BLUE_MODEL]]]),
[...NATIVE_OPENAI_MODELS],
new Map([["main", [...NATIVE_OPENAI_MODELS]]]),
);
const daybreak = projected.find(entry => entry.slug === `main/${NATIVE_DAYBREAK_BLUE_MODEL}`);
const sol = projected.find(entry => entry.slug === "gpt-5.6-sol");
Expand All @@ -2849,7 +2854,11 @@ describe("Codex catalog routed normalization", () => {
supports_parallel_tool_calls: sol?.supports_parallel_tool_calls,
input_modalities: sol?.input_modalities,
});
expect(projected.some(entry => entry.slug === NATIVE_DAYBREAK_BLUE_MODEL)).toBe(false);
// The bare row now exists (owner decision) and appears exactly once, proving the
// two-list overlap does not duplicate it.
expect(projected.filter(entry => entry.slug === NATIVE_DAYBREAK_BLUE_MODEL)).toHaveLength(1);
expect(projected.filter(entry => entry.slug === `main/${NATIVE_DAYBREAK_BLUE_MODEL}`)).toHaveLength(1);
// The separately billed API-key alias must still never appear on the Codex surface.
expect(projected.some(entry => entry.slug === "daybreak-blue-latest")).toBe(false);
});

Expand Down
10 changes: 7 additions & 3 deletions tests/codex-convergence-account-selectors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ test("convergence drops unsupported bare native rows and never qualifies them",
))).toBe(true);
});

test("convergence preserves an observed account-only native id without creating a bare row", async () => {
test("convergence projects the observed Daybreak row onto its selector and one bare row", async () => {
writeCatalog([nativeEntry()]);
writeFileSync(join(codexHome, "models_cache.json"), JSON.stringify({
models: [{
Expand Down Expand Up @@ -388,8 +388,12 @@ test("convergence preserves an observed account-only native id without creating
});
expect((daybreak?.supported_reasoning_levels as Array<{ effort: string }>).map(level => level.effort))
.toEqual(["low", "medium", "high", "xhigh", "max", "ultra"]);
expect(models.some(entry => entry.slug === "team/gpt-daybreak-blue-latest")).toBe(false);
expect(models.some(entry => entry.slug === "gpt-daybreak-blue-latest")).toBe(false);
// Daybreak is globally allowlisted (owner decision, devlog 260816_.../011). A global
// native is seeded onto EVERY visible selector, not only the one that observed it, and the
// bare row now exists. Each must appear exactly once despite the observation also present.
expect(models.filter(entry => entry.slug === "team/gpt-daybreak-blue-latest")).toHaveLength(1);
expect(models.filter(entry => entry.slug === "desktop/gpt-daybreak-blue-latest")).toHaveLength(1);
expect(models.filter(entry => entry.slug === "gpt-daybreak-blue-latest")).toHaveLength(1);
});

test("convergence preserves unrelated foreign rows alongside fresh configured provider rows", async () => {
Expand Down
6 changes: 4 additions & 2 deletions tests/codex-models-cache-invalidate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ describe("invalidateCodexModelsCache write gate (#476 / #518)", () => {
}, null, 2) + "\n");
writeFileSync(join(codexHome, "models_cache.json"), JSON.stringify({
models: [{
slug: "gpt-daybreak-blue-latest",
// gpt-daybreak-blue-latest is a KNOWN global native now (devlog 260816_.../011),
// so it can no longer stand in for an unknown observed id.
slug: "gpt-future-unlisted",
visibility: "list",
supported_in_api: true,
shell_type: "shell_command",
Expand All @@ -75,7 +77,7 @@ describe("invalidateCodexModelsCache write gate (#476 / #518)", () => {
const cache = JSON.parse(readFileSync(join(codexHome, "models_cache.json"), "utf8")) as {
models: Array<Record<string, unknown>>;
};
expect(cache.models.find(model => model.slug === "gpt-daybreak-blue-latest")).toMatchObject({
expect(cache.models.find(model => model.slug === "gpt-future-unlisted")).toMatchObject({
visibility: "hide",
opencodex_account_observed_native: true,
});
Expand Down
46 changes: 35 additions & 11 deletions tests/native-model-toggle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,15 +142,18 @@ describe("native GPT model toggles (bare slugs in disabledModels)", () => {
expect(entries.every(entry => Number.isInteger(entry.priority))).toBe(true);
});

// gpt-daybreak-blue-latest is now a GLOBALLY allowlisted native (owner decision, devlog
// 260816_.../011), so it is no longer an "unknown observed id" and cannot stand in for one
// here. gpt-future-unlisted plays that role instead; the invariant under test is unchanged.
test("observed account-only native ids stay qualified and do not expand the bare set", () => {
const observedEntries = [
{ ...nativeTemplate(), slug: "gpt-daybreak-blue-latest", visibility: "list", supported_in_api: true },
{ ...nativeTemplate(), slug: "gpt-hidden-daybreak", visibility: "hide", supported_in_api: true },
{ ...nativeTemplate(), slug: "gpt-future-unlisted", visibility: "list", supported_in_api: true },
{ ...nativeTemplate(), slug: "gpt-hidden-future", visibility: "hide", supported_in_api: true },
{ ...nativeTemplate(), slug: "gpt-not-an-api-model", visibility: "list", supported_in_api: false },
{ ...nativeTemplate(), slug: "provider/gpt-daybreak-blue-latest", visibility: "list", supported_in_api: true },
{ ...nativeTemplate(), slug: "provider/gpt-future-unlisted", visibility: "list", supported_in_api: true },
];
expect(accountBoundNativeOpenAiSlugs(observedEntries)).toContain("gpt-daybreak-blue-latest");
expect(accountBoundNativeOpenAiSlugs(observedEntries)).not.toContain("gpt-hidden-daybreak");
expect(accountBoundNativeOpenAiSlugs(observedEntries)).toContain("gpt-future-unlisted");
expect(accountBoundNativeOpenAiSlugs(observedEntries)).not.toContain("gpt-hidden-future");
expect(accountBoundNativeOpenAiSlugs(observedEntries)).not.toContain("gpt-not-an-api-model");

const entries = buildCatalogEntries(
Expand All @@ -167,25 +170,46 @@ describe("native GPT model toggles (bare slugs in disabledModels)", () => {
undefined,
accountBoundNativeOpenAiSlugs(observedEntries),
);
expect(entries.find(entry => entry.slug === "gpt-daybreak-blue-latest")).toBeUndefined();
expect(entries.find(entry => entry.slug === "team/gpt-daybreak-blue-latest")).toMatchObject({
expect(entries.find(entry => entry.slug === "gpt-future-unlisted")).toBeUndefined();
expect(entries.find(entry => entry.slug === "team/gpt-future-unlisted")).toMatchObject({
opencodex_catalog_kind: CODEX_ACCOUNT_BOUND_CATALOG_KIND,
visibility: "list",
});

expect(observedAccountBoundNativeEntries([{
...nativeTemplate(),
slug: "gpt-daybreak-blue-latest",
slug: "gpt-future-unlisted",
visibility: "hide",
supported_in_api: true,
opencodex_account_observed_native: true,
}])).toHaveLength(1);
expect(observedAccountBoundNativeOpenAiSlugs(observedEntries)).toEqual(["gpt-daybreak-blue-latest"]);
expect(observedAccountBoundNativeOpenAiSlugs(observedEntries)).toEqual(["gpt-future-unlisted"]);
});

test("gpt-daybreak-blue-latest ships as a global native row without an observation", () => {
const entries = buildCatalogEntries(
nativeTemplate(),
[...NATIVE_OPENAI_MODELS],
[],
[],
false,
"default",
new Set(),
[],
new Set(),
new Set(),
);
const bare = entries.filter(entry => entry.slug === "gpt-daybreak-blue-latest");
// Exactly one row: the slug sits in BOTH NATIVE_OPENAI_MODELS and
// NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS, and that overlap must not duplicate it.
expect(bare).toHaveLength(1);
// Capability is inherited from gpt-5.6-sol, so it is a recursive-capable v2 delegate.
expect(bare[0]?.multi_agent_version).toBe("v2");
});

test("a minimal hand-edited cache row is ignored", () => {
const handEdited = [{ slug: "gpt-daybreak-blue-latest", visibility: "list", supported_in_api: true }];
expect(accountBoundNativeOpenAiSlugs(handEdited)).not.toContain("gpt-daybreak-blue-latest");
const handEdited = [{ slug: "gpt-future-unlisted", visibility: "list", supported_in_api: true }];
expect(accountBoundNativeOpenAiSlugs(handEdited)).not.toContain("gpt-future-unlisted");
expect(observedAccountBoundNativeEntries(handEdited)).toEqual([]);
});

Expand Down
Loading