From 2ff758c7293c3c53b874c27b837658043dda0bbe Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Thu, 13 Aug 2026 19:02:48 +0900 Subject: [PATCH] fix: preserve Daybreak Blue Codex capabilities --- .../content/docs/guides/codex-app-models.md | 38 ++++- .../docs/ko/guides/codex-app-models.md | 38 ++++- src/codex/catalog.ts | 2 +- src/codex/catalog/effort.ts | 3 +- src/codex/catalog/metadata.ts | 74 +++++++-- src/codex/catalog/native-models.ts | 27 +++ src/codex/catalog/parsing.ts | 19 ++- src/codex/catalog/provider-fetch.ts | 52 +++++- src/codex/catalog/sync.ts | 28 +++- structure/03_catalog-and-subagents.md | 4 + structure/08_openai-provider-tiers.md | 7 + tests/codex-catalog-sync-hardening.test.ts | 79 ++++++++- tests/codex-catalog.test.ts | 156 +++++++++++++++++- ...odex-convergence-account-selectors.test.ts | 13 +- tests/codex-routing.test.ts | 53 ++++++ 15 files changed, 551 insertions(+), 42 deletions(-) diff --git a/docs-site/src/content/docs/guides/codex-app-models.md b/docs-site/src/content/docs/guides/codex-app-models.md index 70ea33de92..d84cbef49e 100644 --- a/docs-site/src/content/docs/guides/codex-app-models.md +++ b/docs-site/src/content/docs/guides/codex-app-models.md @@ -23,17 +23,41 @@ the bare or API-key model list. The row is matched on the field shape a real cat which filters malformed entries — it does not prove the id came from an upstream response, since the cache is a user-owned file. See [Exact Codex account selectors](/reference/configuration/routing/#exact-codex-account-selectors). +`gpt-daybreak-blue-latest` follows that observation-only rule for account-qualified rows and is not +added to the bare native allowlist. A separate, explicit `customModels` entry can expose the same +wire id as `openai/gpt-daybreak-blue-latest` through the canonical Codex-login forward provider: + +```json +{ + "customModels": [ + { + "id": "daybreak-codex-forward", + "provider": "openai", + "modelId": "gpt-daybreak-blue-latest" + } + ] +} +``` + +Only that exact provider, endpoint, and model id receive the pinned Sol capability snapshot: +372,000 context, 334,800 automatic compaction, the native reasoning ladder, and native Codex tool +metadata. The request still sends `gpt-daybreak-blue-latest`; opencodex does not rewrite it to Sol, +does not create a bare row, and does not grant account entitlement. The separately billed +`openai-apikey/daybreak-blue-latest` API row is a different route and its 1,050,000 / 922,000 limits +are never copied into the Codex-login row. + When the `codexAccountNamespaces` map is empty, account-qualified picker rows are off. If `codexAccountPickerEnabled` is omitted with a non-empty map, they are treated as enabled for backward compatibility. Set it to `false` to hide generated qualified rows and restore bare native rows in the picker without deleting mappings or disabling exact `/` routing. -API GPT-5.6 entries use +API GPT-5.6 and Daybreak entries use 1,050,000 context / 922,000 max input, and `*-pro` picker ids resolve to the base wire model with `reasoning.mode: "pro"` while logs, usage, and picker state keep the virtual id. -The API catalog is fixed to exactly eight ids: `gpt-5.5`, `gpt-5.6`, Sol/Terra/Luna, and their -three Pro virtual ids; there is no generic `gpt-5.6-pro` alias. +The API catalog is fixed to exactly ten ids: `gpt-5.5`, `gpt-5.6`, Sol/Terra/Luna, their three Pro +virtual ids, `daybreak-red-latest`, and `daybreak-blue-latest`; there is no generic +`gpt-5.6-pro` alias. Compact requests keep the selected tier but send the base model without a reasoning object. Select the credential route represented by the picker id. Change Pool/Direct on the Providers page; @@ -43,6 +67,9 @@ Select the credential route represented by the picker id. Change Pool/Direct on gpt-5.6-sol # bare Codex-login route via Pool or Direct /gpt-5.6-sol # stored Codex account mapped by that selector openai-apikey/gpt-5.6-sol # API key +openai/gpt-daybreak-blue-latest # explicit Codex-forward custom row (372,000) +/gpt-daybreak-blue-latest # observed account-qualified native id, when available +openai-apikey/daybreak-blue-latest # separate API-key route (1,050,000 / 922,000) ``` Fresh installs and configs with no saved mode default to Pool. Current configs use marker 2 and @@ -102,13 +129,16 @@ metadata instead of an older-template approximation. | --- | --- | | Codex login (account-qualified rows disabled) | Bare native ids such as `gpt-5.6-sol`, `gpt-5.6-terra`, and `gpt-5.6-luna`; Pool or Direct is selected through `codexAccountMode`. GPT-5.6 rows use a 372,000-token catalog window. | | Codex login (account-qualified rows enabled with eligible selectors) | One `/` row per eligible selector and supported native model; each row uses only its mapped account, and bare native rows are hidden from the picker. Native metadata and context windows are preserved. | -| OpenAI (API key) | Exactly eight namespaced rows: `gpt-5.5`, `gpt-5.6`, Sol/Terra/Luna, and the three `*-pro` virtual ids (1,050,000 context; 922,000 max input for all eight) | +| Codex login (explicit Daybreak forward row) | `openai/gpt-daybreak-blue-latest` only when the exact `customModels` row is configured on the canonical `openai` provider. It keeps the Daybreak wire id and uses the pinned Sol capability snapshot (372,000 context; 334,800 automatic compaction). | +| OpenAI (API key) | Exactly ten namespaced rows: `gpt-5.5`, `gpt-5.6`, Sol/Terra/Luna, the three `*-pro` virtual ids, and the two Daybreak aliases (1,050,000 context; 922,000 max input for all ten) | | OpenRouter | `openrouter/openai/gpt-5.6-sol`, `openrouter/openai/gpt-5.6-terra`, `openrouter/openai/gpt-5.6-luna` (1,050,000) | | Cursor | Static fallback includes `cursor/gpt-5.6-sol`, `cursor/gpt-5.6-terra`, and `cursor/gpt-5.6-luna` (1,000,000), plus `cursor/grok-4.5` and `cursor/grok-4.5-fast` (500,000); live account discovery decides which remain visible. | | xAI | Live discovery is authoritative; the fallback catalog defaults to `xai/grok-4.5` with a 500,000-token window and `low` / `medium` / `high` reasoning controls. | The pinned GPT-5.6 entries preserve the exact upstream ladder. Sol and Terra expose `low` through `ultra`; Luna stops at `max`. Sol defaults to `low`, while Terra and Luna default to `medium`. +The explicit Codex-forward Daybreak Blue row inherits Sol's ladder and default without changing its +wire identity. `ultra` is a client-facing choice for maximum reasoning plus proactive delegation and reaches the backend as `max`. A picker entry only means the catalog is ready: the connected account or API key must still be entitled to use that model. diff --git a/docs-site/src/content/docs/ko/guides/codex-app-models.md b/docs-site/src/content/docs/ko/guides/codex-app-models.md index b6f31f1b5f..0e89e554df 100644 --- a/docs-site/src/content/docs/ko/guides/codex-app-models.md +++ b/docs-site/src/content/docs/ko/guides/codex-app-models.md @@ -18,15 +18,38 @@ opencodex는 매핑된 계정별로 `/` 행을 다른 계정으로 전환하지 않고 요청이 실패합니다. 자세한 내용은 [명시적 Codex 계정 selector](/reference/configuration/routing/#exact-codex-account-selectors)를 참고하세요. +계정 한정 행에서 `gpt-daybreak-blue-latest`는 계정 카탈로그에 관측됐을 때만 보존되며 bare native +allowlist에는 추가되지 않습니다. 이와 별개로 canonical Codex 로그인 forward provider에 다음과 같은 +명시적 `customModels` 항목을 두면 같은 wire id를 `openai/gpt-daybreak-blue-latest`로 노출할 수 있습니다. + +```json +{ + "customModels": [ + { + "id": "daybreak-codex-forward", + "provider": "openai", + "modelId": "gpt-daybreak-blue-latest" + } + ] +} +``` + +정확히 이 provider, endpoint, model id 조합만 고정된 Sol capability snapshot을 상속합니다. 컨텍스트는 +372,000, 자동 압축점은 334,800이며 native reasoning ladder와 Codex tool metadata도 보존됩니다. 요청의 +wire id는 계속 `gpt-daybreak-blue-latest`이고 Sol로 다시 쓰지 않으며 bare 행이나 계정 사용 권한을 만들지 +않습니다. 별도 과금 경로인 `openai-apikey/daybreak-blue-latest`의 1,050,000 / 922,000 한도는 Codex 로그인 +행으로 복사되지 않습니다. + `codexAccountNamespaces` map이 비어 있으면 계정 한정 선택기 행은 꺼집니다. 비어 있지 않은 map에서 `codexAccountPickerEnabled`를 생략하면 이전 버전과의 호환성을 위해 활성화된 것으로 취급됩니다. `false`로 설정하면 매핑을 삭제하거나 명시적 `/` 라우팅을 비활성화하지 않은 채 생성된 qualified 행을 숨기고 선택기에 bare native 행을 복원합니다. -API GPT-5.6 항목은 context 1,050,000 / max input 922,000을 +API GPT-5.6과 Daybreak 항목은 context 1,050,000 / max input 922,000을 쓰고, `*-pro` picker id는 로그, 사용량, picker 상태에는 가상 id를 유지한 채 wire에서는 base model과 -`reasoning.mode: "pro"`로 풀립니다. API 카탈로그는 `gpt-5.5`, `gpt-5.6`, Sol/Terra/Luna, 그리고 세 개의 -Pro 가상 id까지 정확히 여덟 개로 고정되어 있으며, 일반적인 `gpt-5.6-pro` 별칭은 없습니다. Compact 요청은 +`reasoning.mode: "pro"`로 풀립니다. API 카탈로그는 `gpt-5.5`, `gpt-5.6`, Sol/Terra/Luna, 세 개의 +Pro 가상 id, `daybreak-red-latest`, `daybreak-blue-latest`까지 정확히 열 개로 고정되어 있으며, +일반적인 `gpt-5.6-pro` 별칭은 없습니다. Compact 요청은 선택한 tier를 유지하되 reasoning 객체 없이 base model만 보냅니다. 선택기 id로 credential 경로를 명시적으로 선택하세요. Pool/Direct는 Providers 페이지에서 바꾸며, @@ -36,6 +59,9 @@ Pro 가상 id까지 정확히 여덟 개로 고정되어 있으며, 일반적인 gpt-5.6-sol # Pool 또는 Direct를 통한 bare Codex 로그인 경로 /gpt-5.6-sol # 해당 selector에 매핑된 저장된 Codex 계정 openai-apikey/gpt-5.6-sol # API key +openai/gpt-daybreak-blue-latest # 명시적 Codex-forward custom 행 (372,000) +/gpt-daybreak-blue-latest # 사용 가능할 때 관측되는 계정 한정 native id +openai-apikey/daybreak-blue-latest # 별도 API-key 경로 (1,050,000 / 922,000) ``` 새로 설치한 환경과 저장된 모드가 없는 설정은 Pool이 기본값입니다. 현재 설정은 마커 2를 사용하고, @@ -81,13 +107,15 @@ GPT-5.6에만 사용합니다. 오래된 템플릿으로 근사하지 않고 모 | --- | --- | | Codex 로그인(계정 한정 선택기 행 비활성) | `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna` 같은 bare native id를 표시하고 `codexAccountMode`에 따라 Pool 또는 Direct를 사용합니다. GPT-5.6 행의 카탈로그 창은 372,000토큰입니다. | | Codex 로그인(계정 한정 선택기 행 활성, 유효한 selector 있음) | 유효한 selector와 지원되는 native model의 각 조합마다 `/` 행을 표시합니다. 각 행은 매핑된 계정만 사용하며 bare native 행은 선택기에서 숨깁니다. Native metadata와 context window는 보존됩니다. | -| OpenAI(API key) | 정확히 여덟 개의 네임스페이스 행: `gpt-5.5`, `gpt-5.6`, Sol/Terra/Luna, 그리고 세 개의 `*-pro` 가상 id (모두 컨텍스트 1,050,000; 최대 입력 922,000) | +| Codex 로그인(명시적 Daybreak forward 행) | canonical `openai` provider에 정확한 `customModels` 항목이 있을 때만 `openai/gpt-daybreak-blue-latest`를 표시합니다. Daybreak wire id를 유지하고 고정된 Sol capability snapshot(컨텍스트 372,000; 자동 압축점 334,800)을 사용합니다. | +| OpenAI(API key) | 정확히 열 개의 네임스페이스 행: `gpt-5.5`, `gpt-5.6`, Sol/Terra/Luna, 세 개의 `*-pro` 가상 id, 두 Daybreak 별칭 (모두 컨텍스트 1,050,000; 최대 입력 922,000) | | OpenRouter | `openrouter/openai/gpt-5.6-sol`, `openrouter/openai/gpt-5.6-terra`, `openrouter/openai/gpt-5.6-luna` (1,050,000) | | Cursor | 정적 폴백에는 `cursor/gpt-5.6-sol`, `cursor/gpt-5.6-terra`, `cursor/gpt-5.6-luna` (1,000,000)와 `cursor/grok-4.5`, `cursor/grok-4.5-fast` (500,000)가 들어갑니다. 실시간 계정 탐색이 어떤 항목을 계속 보일지 정합니다. | | xAI | 실시간 탐색이 기준입니다. 폴백 카탈로그의 기본값은 `xai/grok-4.5`이고, 컨텍스트 500,000과 `low` / `medium` / `high` 추론 제어를 제공합니다. | 고정된 GPT-5.6 항목은 업스트림 ladder를 그대로 보존합니다. Sol과 Terra는 `low`부터 `ultra`까지 노출하고, -Luna는 `max`에서 멈춥니다. Sol의 기본값은 `low`이고, Terra와 Luna의 기본값은 `medium`입니다. `ultra`는 +Luna는 `max`에서 멈춥니다. Sol의 기본값은 `low`이고, Terra와 Luna의 기본값은 `medium`입니다. 명시적 +Codex-forward Daybreak Blue 행도 wire id를 바꾸지 않은 채 Sol의 ladder와 기본값을 상속합니다. `ultra`는 최대 reasoning과 선제적 delegation을 묶은 클라이언트 선택지이며 백엔드에는 `max`로 전달됩니다. picker 항목이 보인다는 것은 카탈로그가 준비됐다는 뜻일 뿐입니다. 연결된 계정이나 API key에 실제 사용 권한이 있어야 합니다. diff --git a/src/codex/catalog.ts b/src/codex/catalog.ts index f597108b4f..453dfc54e3 100644 --- a/src/codex/catalog.ts +++ b/src/codex/catalog.ts @@ -2,7 +2,7 @@ // Public surface preserved exactly; importers keep using "src/codex/catalog". export { isMediaGenerationModelId, shouldExposeRoutedModel, readCodexCatalogPath, readCatalog, normalizeRoutedCatalogEntry, catalogModelSlug, filterSupportedNativeSlugs, catalogModelSupportsReasoningSummaries } from "./catalog/parsing"; export type { CatalogModel, MultiAgentMode } from "./catalog/parsing"; -export { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, CODEX_NATIVE_ALIAS_CATALOG_KIND, NATIVE_OPENAI_MODELS, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, isNativeAliasCatalogEntry, nativeOpenAiContextWindow, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, observedAccountBoundNativeEntries, observedAccountBoundNativeOpenAiSlugs, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs, nativeInputModalities, nativeReasoningEfforts, nativeDefaultReasoningEffort, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi } from "./catalog/metadata"; +export { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, CODEX_NATIVE_ALIAS_CATALOG_KIND, NATIVE_DAYBREAK_BLUE_MODEL, NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS, NATIVE_OPENAI_MODELS, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, isNativeAliasCatalogEntry, isNativeOpenAiCapabilityAliasModel, nativeOpenAiCapabilitySourceSlug, nativeOpenAiContextWindow, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, observedAccountBoundNativeEntries, observedAccountBoundNativeOpenAiSlugs, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs, nativeInputModalities, nativeReasoningEfforts, nativeDefaultReasoningEffort, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi } from "./catalog/metadata"; export { isSpawnableCodexCandidate, codexExecInvocation, loadBundledCodexCatalog, materializeBundledCodexCatalog, loadCatalogTemplate } from "./catalog/bundled"; export { nativeEffortClamp, shouldApplyNativeEffortClamp, catalogModelEfforts, codexSupportedReasoningEfforts, clampedDefaultEffort, clampEntryToCodexSupportedEfforts, clampCatalogModelsToCodexSupport } from "./catalog/effort"; export { applyProviderConfigHints, isDatedVariantId, filterCatalogVisibleModels, gatherRoutedModels, clearGatherRoutedModelsInflight, augmentRoutedModelsWithRegistryOpenAiApiRows, augmentRoutedModelsWithMetadata, resolveComboCatalogMember, configuredComboTargetModelsByProvider } from "./catalog/provider-fetch"; diff --git a/src/codex/catalog/effort.ts b/src/codex/catalog/effort.ts index 3a7f823d5c..b1ac6b540d 100644 --- a/src/codex/catalog/effort.ts +++ b/src/codex/catalog/effort.ts @@ -34,6 +34,7 @@ import upstreamModelsSnapshot from "../data/upstream-models.json"; import { readCatalog, readCodexCatalogPath } from "./parsing"; import type { CatalogModel, RawEntry } from "./parsing"; import { UPSTREAM_NATIVE_ENTRIES } from "./metadata"; +import { nativeOpenAiCapabilitySourceSlug } from "./native-models"; import { loadBundledCodexCatalog } from "./bundled"; import type { BundledCatalogDeps, ReadonlyRawCatalog } from "./bundled"; import { deriveEntry } from "./sync"; @@ -180,7 +181,7 @@ export function applyReasoningLevels( } export function isGpt56NativeSlug(slug: string): boolean { - return !slug.includes("/") && slug.startsWith("gpt-5.6-"); + return !slug.includes("/") && nativeOpenAiCapabilitySourceSlug(slug).startsWith("gpt-5.6-"); } export function ensureGpt56ReasoningLevels(entry: RawEntry): void { diff --git a/src/codex/catalog/metadata.ts b/src/codex/catalog/metadata.ts index 7285213fa7..ef7d17a815 100644 --- a/src/codex/catalog/metadata.ts +++ b/src/codex/catalog/metadata.ts @@ -15,7 +15,7 @@ import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../.. import { getProviderRegistryEntry } from "../../providers/registry"; import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap"; import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec"; -import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity"; +import { identifyRoutedModel } from "../../adapters/identity"; import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery"; import { fetchCursorUsableModels } from "../../adapters/cursor/live-models"; import { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers"; @@ -37,9 +37,23 @@ import type { RawEntry } from "./parsing"; import { readCurrentCatalogOrCache, readCurrentCodexCatalog, readCurrentCodexModelsCache, unique } from "./bundled"; import { trustedAccountBoundNativeCatalogSlug, visibleCodexAccountSelectors } from "./account-models"; import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds"; -import { NATIVE_OPENAI_MODELS, SUPPORTED_NATIVE_OPENAI_SLUGS } from "./native-models"; +import { + NATIVE_DAYBREAK_BLUE_MODEL, + NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS, + NATIVE_OPENAI_MODELS, + SUPPORTED_NATIVE_OPENAI_SLUGS, + isNativeOpenAiCapabilityAliasModel, + nativeOpenAiCapabilitySourceSlug, +} from "./native-models"; export { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds"; -export { NATIVE_OPENAI_MODELS, SUPPORTED_NATIVE_OPENAI_SLUGS } from "./native-models"; +export { + NATIVE_DAYBREAK_BLUE_MODEL, + NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS, + NATIVE_OPENAI_MODELS, + SUPPORTED_NATIVE_OPENAI_SLUGS, + isNativeOpenAiCapabilityAliasModel, + nativeOpenAiCapabilitySourceSlug, +} from "./native-models"; export const DOCUMENTED_NATIVE_OPENAI_ADDITIONS = [ "gpt-5.3-codex-spark", @@ -94,18 +108,28 @@ export const NATIVE_OPENAI_CONTEXT_OVERRIDES: Record = new Map( + ((upstreamModelsSnapshot as unknown as { models?: RawEntry[] }).models ?? []) + .flatMap(model => typeof model.slug === "string" ? [[model.slug, model] as const] : []), +); + +function pinnedNativeCapabilityEntry(slug: string): RawEntry | undefined { + return PINNED_UPSTREAM_MODELS.get(nativeOpenAiCapabilitySourceSlug(slug)); +} + /** * Pinned capability metadata is safe to use as a fallback for every supported native model. * Keep it separate from UPSTREAM_NATIVE_ENTRIES: that narrower map also authorizes replacing * persisted native rows during sync, which is currently intentional only for the GPT-5.6 family. */ const PINNED_NATIVE_CAPABILITY_ENTRIES: Map = new Map( - ((upstreamModelsSnapshot as unknown as { models?: RawEntry[] }).models ?? []) - .filter(m => typeof m.slug === "string" - && SUPPORTED_NATIVE_OPENAI_SLUGS.has(m.slug as string)) - .map(m => [m.slug as string, m]), + [...NATIVE_OPENAI_MODELS, ...NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS].flatMap(slug => { + const entry = pinnedNativeCapabilityEntry(slug); + return entry ? [[slug, entry] as const] : []; + }), ); export function nativeOpenAiContextWindow(slug: string, contextCap?: number): number | undefined { @@ -259,12 +283,38 @@ export function applyNativeVisibility( return entries; } +function upstreamNativeEntryForSlug(slug: string): RawEntry | undefined { + const sourceSlug = nativeOpenAiCapabilitySourceSlug(slug); + if (!sourceSlug.startsWith("gpt-5.6-")) return undefined; + const source = PINNED_UPSTREAM_MODELS.get(sourceSlug); + if (!source) return undefined; + if (slug === sourceSlug) return source; + + const alias = structuredClone(source) as RawEntry; + alias.slug = slug; + alias.display_name = "Daybreak Blue"; + alias.description = "Frontier general-purpose model with safeguards for defensive cybersecurity work."; + if (typeof alias.base_instructions === "string") { + alias.base_instructions = identifyRoutedModel(alias.base_instructions, slug); + } + if (alias.model_messages && typeof alias.model_messages === "object" && !Array.isArray(alias.model_messages)) { + const modelMessages = alias.model_messages as Record; + if (typeof modelMessages.instructions_template === "string") { + alias.model_messages = { + ...modelMessages, + instructions_template: identifyRoutedModel(modelMessages.instructions_template, slug), + }; + } + } + delete alias.availability_nux; + return alias; +} + export const UPSTREAM_NATIVE_ENTRIES: Map = new Map( - ((upstreamModelsSnapshot as unknown as { models?: RawEntry[] }).models ?? []) - .filter(m => typeof m.slug === "string" - && SUPPORTED_NATIVE_OPENAI_SLUGS.has(m.slug as string) - && (m.slug as string).startsWith("gpt-5.6-")) - .map(m => [m.slug as string, m]), + [...NATIVE_OPENAI_MODELS, ...NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS].flatMap(slug => { + const entry = upstreamNativeEntryForSlug(slug); + return entry ? [[slug, entry] as const] : []; + }), ); export function upstreamNativeEntry(slug: string): RawEntry | null { diff --git a/src/codex/catalog/native-models.ts b/src/codex/catalog/native-models.ts index b4076ef62d..d05d2a0da1 100644 --- a/src/codex/catalog/native-models.ts +++ b/src/codex/catalog/native-models.ts @@ -1,3 +1,30 @@ +/** ChatGPT/Codex wire id observed for the account-native Daybreak Blue surface. */ +export const NATIVE_DAYBREAK_BLUE_MODEL = "gpt-daybreak-blue-latest"; + +/** + * Account-native aliases whose Codex capabilities track another pinned native row. + * + * This is catalog metadata inheritance only. Routing always preserves the requested + * wire id, so the ChatGPT/Codex `gpt-daybreak-*` surface never collapses into the + * separately billed API-key `daybreak-*-latest` surface or into `gpt-5.6-sol`. + */ +const NATIVE_OPENAI_CAPABILITY_SOURCES: Readonly> = Object.freeze({ + [NATIVE_DAYBREAK_BLUE_MODEL]: "gpt-5.6-sol", +}); + +/** Account-scoped native ids that may inherit metadata but never enter the bare allowlist. */ +export const NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS = Object.freeze( + Object.keys(NATIVE_OPENAI_CAPABILITY_SOURCES), +); + +export function isNativeOpenAiCapabilityAliasModel(slug: string): boolean { + return Object.hasOwn(NATIVE_OPENAI_CAPABILITY_SOURCES, slug); +} + +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. */ export const NATIVE_OPENAI_MODELS = [ "gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex-spark", diff --git a/src/codex/catalog/parsing.ts b/src/codex/catalog/parsing.ts index 442d1378dc..bf1d165181 100644 --- a/src/codex/catalog/parsing.ts +++ b/src/codex/catalog/parsing.ts @@ -31,7 +31,7 @@ import { redactSecretString } from "../../lib/redact"; import upstreamModelsSnapshot from "../data/upstream-models.json"; -import { NATIVE_OPENAI_CONTEXT_OVERRIDES, SUPPORTED_NATIVE_OPENAI_SLUGS, UPSTREAM_NATIVE_ENTRIES, nativeMultiAgentVersion } from "./metadata"; +import { NATIVE_OPENAI_CONTEXT_OVERRIDES, SUPPORTED_NATIVE_OPENAI_SLUGS, UPSTREAM_NATIVE_ENTRIES, isNativeOpenAiCapabilityAliasModel, nativeMultiAgentVersion } from "./metadata"; import { trustedAccountBoundNativeCatalogSlug } from "./account-models"; import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds"; @@ -116,6 +116,11 @@ export interface CatalogModel { inputModalities?: string[]; /** Provider opted into parallel tool calls (OcxProviderConfig.parallelToolCalls). */ parallelToolCalls?: boolean; + /** + * This routed row is an explicitly configured account-native alias on the canonical ChatGPT + * forward provider. It may inherit pinned native Codex metadata without changing its wire id. + */ + codexForwardNativeCapabilityAlias?: boolean; /** Whether Codex may send Responses text.verbosity for this routed model. */ supportsVerbosity?: boolean; supportsReasoningSummaries?: boolean; @@ -359,9 +364,19 @@ export function applyMultiAgentMode(entries: RawEntry[], mode: MultiAgentMode, v for (const entry of entries) { const slug = typeof entry.slug === "string" ? entry.slug : ""; const nativeAlias = entry.opencodex_catalog_kind === CODEX_NATIVE_ALIAS_CATALOG_KIND; + const routedNativeSlug = slug.startsWith(`${OPENAI_CODEX_PROVIDER_ID}/`) + ? slug.slice(OPENAI_CODEX_PROVIDER_ID.length + 1) + : ""; + const codexForwardCapabilityAlias = entry.opencodex_catalog_kind === CODEX_CUSTOM_MODEL_CATALOG_KIND + && entry.use_responses_lite === true + && isNativeOpenAiCapabilityAliasModel(routedNativeSlug) + ? routedNativeSlug + : undefined; const upstreamPin = nativeAlias ? nativeMultiAgentVersion(slug) - : UPSTREAM_NATIVE_ENTRIES.get(trustedAccountBoundNativeCatalogSlug(entry) ?? slug)?.multi_agent_version; + : codexForwardCapabilityAlias + ? nativeMultiAgentVersion(codexForwardCapabilityAlias) + : UPSTREAM_NATIVE_ENTRIES.get(trustedAccountBoundNativeCatalogSlug(entry) ?? slug)?.multi_agent_version; if (typeof upstreamPin === "string") { entry.multi_agent_version = upstreamPin; } else if (v2FeatureEnabled) { diff --git a/src/codex/catalog/provider-fetch.ts b/src/codex/catalog/provider-fetch.ts index a723fe0878..2b5137f932 100644 --- a/src/codex/catalog/provider-fetch.ts +++ b/src/codex/catalog/provider-fetch.ts @@ -69,7 +69,7 @@ import { createAdmissionGate, ResourceAdmissionError, type AdmissionMetrics } fr import { CODEX_CUSTOM_MODEL_CATALOG_KIND, JAWCODE_CATALOG_AUGMENT_PROVIDERS, catalogModelSlug, shouldExposeRoutedModel } from "./parsing"; import type { CatalogModel } from "./parsing"; -import { disabledNativeSlugs, hasComboTargets, nativeDefaultReasoningEffort, nativeInputModalities, nativeOpenAiContextWindow, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata"; +import { disabledNativeSlugs, hasComboTargets, isNativeOpenAiCapabilityAliasModel, nativeDefaultReasoningEffort, nativeInputModalities, nativeOpenAiContextWindow, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata"; import { deriveComboCatalogModel, normalizedOpenAiApiSignature, openAiApiCollisionWarnings, replaceLastComboCatalogOmissions, warnUncataloguedComboOnce } from "./aggregation"; import type { ComboCatalogOmission } from "./aggregation"; import type { CatalogGatherProviderAuthEvidence } from "./filesystem-evidence"; @@ -209,6 +209,15 @@ interface GatherInflightEntry { readonly promise: Promise; } +function withCanonicalOpenAiForwardAuthDefault( + name: string, + provider: OcxProviderConfig, +): OcxProviderConfig { + if (name !== OPENAI_CODEX_PROVIDER_ID || provider.authMode !== undefined) return provider; + const candidate = { ...provider, authMode: "forward" as const }; + return isCanonicalOpenAiForwardProvider(candidate) ? candidate : provider; +} + const gatherInflight = new Map(); const CATALOG_GATHER_AUTHORITY_KEY = randomBytes(32); const REQUEST_CREDENTIAL_SENTINEL = `ocx-catalog-credential-${randomBytes(16).toString("hex")}`; @@ -390,7 +399,7 @@ function captureProviderGather( authResolver: ModelsAuthResolver, retainConfiguredModelIds?: ReadonlySet, ): CapturedProviderGather { - const enriched = detachedClone(configured); + const enriched = detachedClone(withCanonicalOpenAiForwardAuthDefault(name, configured)); enrichProviderFromRegistry(name, enriched); const provider = recursivelyFreeze(enriched); const observedAuth = authResolver.kind === "observed" @@ -1714,16 +1723,49 @@ async function gatherRoutedModelsUncached( const replacedByRoutedSlug = new Map(all.map(model => [routedSlug(model.provider, model.id), model])); const customModels = (config.customModels ?? []).map(cm => { const rawProvider = config.providers[cm.provider]; + // Registry routing backfills an omitted authMode on the built-in OpenAI provider to + // forward. Keep the catalog projection on the same contract while still failing closed + // for every explicit non-forward mode and every non-canonical endpoint. + const providerForCanonicalCheck = rawProvider + ? withCanonicalOpenAiForwardAuthDefault(cm.provider, rawProvider) + : undefined; + const codexForwardNativeCapabilityAlias = cm.provider === OPENAI_CODEX_PROVIDER_ID + && providerForCanonicalCheck !== undefined + && isCanonicalOpenAiForwardProvider(providerForCanonicalCheck) + && isNativeOpenAiCapabilityAliasModel(cm.modelId); + const nativeAliasContextWindow = codexForwardNativeCapabilityAlias + ? nativeOpenAiContextWindow(cm.modelId, providerContextCap(config, OPENAI_CODEX_PROVIDER_ID)) + : undefined; + const customContextWindow = cm.contextWindow + ? nativeAliasContextWindow !== undefined + ? Math.min(cm.contextWindow, nativeAliasContextWindow) + : cm.contextWindow + : nativeAliasContextWindow; + const nativeAliasDefaultEffort = codexForwardNativeCapabilityAlias + ? nativeDefaultReasoningEffort(cm.modelId) + : undefined; const supportsReasoningSummaries = configuredReasoningSummarySupport(rawProvider, cm.modelId); const base: CatalogModel = { id: cm.modelId, provider: cm.provider, catalogKind: CODEX_CUSTOM_MODEL_CATALOG_KIND, // Display-only label: never feeds routing (customModels are keyed by routedSlug below). - ...(cm.displayName ? { displayName: cm.displayName } : {}), - ...(cm.contextWindow ? { contextWindow: cm.contextWindow } : {}), - ...(cm.inputModalities ? { inputModalities: cm.inputModalities } : {}), + ...(cm.displayName + ? { displayName: cm.displayName } + : codexForwardNativeCapabilityAlias ? { displayName: "Daybreak Blue" } : {}), + ...(customContextWindow !== undefined ? { contextWindow: customContextWindow } : {}), + ...(cm.inputModalities + ? { inputModalities: cm.inputModalities } + : codexForwardNativeCapabilityAlias ? { inputModalities: nativeInputModalities(cm.modelId) } : {}), ...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}), + ...(codexForwardNativeCapabilityAlias + ? { + codexForwardNativeCapabilityAlias: true, + reasoningEfforts: nativeReasoningEfforts(cm.modelId), + parallelToolCalls: nativeParallelToolCalls(cm.modelId), + ...(nativeAliasDefaultEffort ? { defaultReasoningEffort: nativeAliasDefaultEffort } : {}), + } + : {}), }; // #962: the dedupe below drops the provider-derived row this custom row replaces. Inherit that // row's provider capability metadata (reasoning ladder, default effort, parallel tool calls, diff --git a/src/codex/catalog/sync.ts b/src/codex/catalog/sync.ts index 5afa06c106..e3a9e56892 100644 --- a/src/codex/catalog/sync.ts +++ b/src/codex/catalog/sync.ts @@ -248,6 +248,9 @@ export function deriveEntry( contextCap?: number, ): RawEntry { const preserveExact = isExactComboCatalogModel(model, exactComboSlugs); + const codexForwardNativeCapabilityAlias = model?.codexForwardNativeCapabilityAlias === true + ? upstreamNativeEntry(model.id) + : null; const isRouted = model !== undefined; if (!isRouted && !slug.includes("/")) { // Supported native slug covered by the upstream snapshot: use the REAL entry (exact @@ -256,8 +259,8 @@ export function deriveEntry( const upstream = upstreamNativeEntry(slug); if (upstream) return finishUpstreamNativeEntry(upstream, priority, contextCap); } - if (template) { - const e = JSON.parse(JSON.stringify(template)) as RawEntry; + if (template || codexForwardNativeCapabilityAlias) { + const e = JSON.parse(JSON.stringify(codexForwardNativeCapabilityAlias ?? template)) as RawEntry; e.slug = slug; e.display_name = routedDisplayName(slug); e.description = desc; @@ -272,9 +275,11 @@ export function deriveEntry( // window when /models omits context metadata (#992). Known metadata // restores exact values below; otherwise the strict-fields fallback // supplies the conservative 128k triple. - delete e.context_window; - delete e.max_context_window; - delete e.auto_compact_token_limit; + if (!codexForwardNativeCapabilityAlias) { + delete e.context_window; + delete e.max_context_window; + delete e.auto_compact_token_limit; + } // Native id for identity text + metadata lookups — the slug may be an encoded // alias (`provider/vendor-model`); the model object carries the native id. const modelName = model?.id ?? slug.slice(slug.indexOf("/") + 1); @@ -283,8 +288,17 @@ export function deriveEntry( // (leaking that into base_instructions is a non-first-party signature → ToS risk). e.base_instructions = identifyRoutedModel(e.base_instructions, modelName); } - applyReasoningLevels(e, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExact); - normalizeRoutedCatalogEntry(e, model?.parallelToolCalls === true); + applyReasoningLevels( + e, + model?.reasoningEfforts, + model?.defaultReasoningEffort, + preserveExact || codexForwardNativeCapabilityAlias !== null, + ); + // This exact provider/model pair is the ChatGPT/Codex forward surface. Keep the pinned + // native tool/search/responses-lite contract while preserving the routed slug and wire id. + if (!codexForwardNativeCapabilityAlias) { + normalizeRoutedCatalogEntry(e, model?.parallelToolCalls === true); + } if (model) applyCatalogMetadata(e, model.provider, model.id, model.contextCap); applyCatalogModelMetadata(e, model); if (model?.catalogKind) e.opencodex_catalog_kind = model.catalogKind; diff --git a/structure/03_catalog-and-subagents.md b/structure/03_catalog-and-subagents.md index 54bfaaf1cb..d37f686965 100644 --- a/structure/03_catalog-and-subagents.md +++ b/structure/03_catalog-and-subagents.md @@ -7,6 +7,10 @@ - preserves native OpenAI entries from the live catalog or static fallback, and emits gpt-5.6 natives from the pinned upstream models.json snapshot (`src/codex/data/upstream-models.json` — exact per-slug ladders: luna has no ultra); +- upgrades either an observed selector-qualified `*/gpt-daybreak-blue-latest` account row or an + explicitly configured canonical `openai/gpt-daybreak-blue-latest` Codex-forward row from the + pinned Sol capability metadata while preserving its selector and Daybreak wire identity; + this never expands the bare/API-key model lists or rewrites the wire model to `gpt-5.6-sol`; - clones a native template for routed `provider/model` entries; - forces strict Codex catalog fields required by the current parser; - hides `disabledModels` without blocking direct routing (routed provider ids are excluded; diff --git a/structure/08_openai-provider-tiers.md b/structure/08_openai-provider-tiers.md index 5a42416ba1..243c6678e5 100644 --- a/structure/08_openai-provider-tiers.md +++ b/structure/08_openai-provider-tiers.md @@ -68,7 +68,10 @@ view exactly when it is doing the most work — suppressing every higher tier. ```text gpt-5.6-sol # openai; Pool or Direct follows the provider option +main/gpt-daybreak-blue-latest # openai; observed account-native Daybreak, Sol capability metadata +openai/gpt-daybreak-blue-latest # Codex forward; explicit Daybreak row with Sol native metadata openai-apikey/gpt-5.6-sol # OpenAI API key +openai-apikey/daybreak-blue-latest # API Daybreak alias; separate approval/provisioning openai-apikey/gpt-5.6-sol-pro # API Pro virtual model ``` @@ -107,6 +110,10 @@ migration. change catalog, selected, requested, or wire model identity. - `openai-apikey` exposes namespaced API rows. Its trusted catalog contains `gpt-5.5`, `gpt-5.6`, Sol/Terra/Luna, and the three corresponding Pro variants. No generic `gpt-5.6-pro` alias exists. +- The selector-qualified account-native `*/gpt-daybreak-blue-latest` and API-key + `daybreak-blue-latest` are distinct wire surfaces. An observed native row follows the pinned Sol + capability metadata, but routing strips only the account selector and keeps + `gpt-daybreak-blue-latest` byte-for-byte; it never expands the bare list or substitutes Sol. - API GPT-5.6 rows use 1,050,000 context tokens and 922,000 max input tokens. Codex-login rows keep the native 372,000-token contract. - `*-pro` selected ids rewrite to the base wire id with `reasoning.mode: "pro"`; request logs, diff --git a/tests/codex-catalog-sync-hardening.test.ts b/tests/codex-catalog-sync-hardening.test.ts index 0617407273..ed48d5b855 100644 --- a/tests/codex-catalog-sync-hardening.test.ts +++ b/tests/codex-catalog-sync-hardening.test.ts @@ -13,9 +13,22 @@ function runScript( script: string, extraEnv: Record = {}, ): { stdout: string; status: number; stderr: string } { + // The suite preload redirects USERPROFILE, but .NET's Windows known-folder lookup then returns + // an empty LocalApplicationData path. Catalog serialization intentionally resolves its lock + // namespace from that OS API rather than environment variables, so restore only the real + // profile for this child. CODEX_HOME and OPENCODEX_HOME remain explicit test sandboxes. + const windowsIdentityEnv = process.platform === "win32" && process.env.OCX_REAL_HOME + ? { USERPROFILE: process.env.OCX_REAL_HOME } + : {}; const result = spawnSync(process.execPath, ["--eval", script], { cwd: repoRoot, - env: { ...process.env, CODEX_HOME: codexHome, OPENCODEX_HOME: opencodexHome, ...extraEnv }, + env: { + ...process.env, + ...windowsIdentityEnv, + CODEX_HOME: codexHome, + OPENCODEX_HOME: opencodexHome, + ...extraEnv, + }, encoding: "utf8", }); return { stdout: result.stdout?.trim() ?? "", stderr: result.stderr ?? "", status: result.status ?? 1 }; @@ -404,9 +417,69 @@ describe("Codex catalog sync hardening", () => { `); expect(r.status).toBe(0); - const rows = JSON.parse(readFileSync(catalogPath, "utf8")).models as Array<{ slug: string }>; - expect(rows.some(row => row.slug === "team/gpt-daybreak-blue-latest")).toBe(true); + const rows = JSON.parse(readFileSync(catalogPath, "utf8")).models as Array>; + expect(rows.find(row => row.slug === "team/gpt-daybreak-blue-latest")).toMatchObject({ + context_window: 372_000, + max_context_window: 372_000, + auto_compact_token_limit: 334_800, + comp_hash: "3000", + tool_mode: "code_mode_only", + use_responses_lite: true, + supports_parallel_tool_calls: true, + }); + expect(rows.some(row => row.slug === "gpt-daybreak-blue-latest")).toBe(false); + }); + + test("explicit Codex-forward Daybreak survives sync with Sol metadata while account picker is off", () => { + const catalogPath = join(codexHome, "catalog.json"); + writeFileSync(join(codexHome, "config.toml"), 'model_catalog_json = "catalog.json"\n', "utf8"); + writeFileSync(catalogPath, JSON.stringify({ + models: [nativeEntry("gpt-5.5", 0)], + }, null, 2) + "\n"); + + const r = runScript(codexHome, opencodexHome, ` + const { syncCatalogModels } = require("./src/codex/catalog"); + syncCatalogModels({ + providers: { + openai: { + adapter: "openai-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", + authMode: "forward", + codexAccountMode: "pool" + } + }, + defaultProvider: "openai", + codexAccountPickerEnabled: false, + codexAccountNamespaces: { main: "@main" }, + customModels: [{ + id: "daybreak-codex-forward", + provider: "openai", + modelId: "gpt-daybreak-blue-latest", + contextWindow: 1050000 + }] + }).then(res => console.log(JSON.stringify(res))); + `); + expect(r.status).toBe(0); + + const rows = JSON.parse(readFileSync(catalogPath, "utf8")).models as Array>; + const daybreak = rows.find(row => row.slug === "openai/gpt-daybreak-blue-latest"); + expect(daybreak).toMatchObject({ + display_name: "Daybreak Blue", + context_window: 372_000, + max_context_window: 372_000, + auto_compact_token_limit: 334_800, + comp_hash: "3000", + tool_mode: "code_mode_only", + use_responses_lite: true, + supports_parallel_tool_calls: true, + supports_search_tool: true, + multi_agent_version: "v2", + 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); + expect(rows.some(row => row.slug === "main/gpt-daybreak-blue-latest")).toBe(false); + expect(rows.some(row => row.slug === "openai-apikey/daybreak-blue-latest")).toBe(false); }); test("a live provider row shadowed by an account selector warns once per runtime generation", () => { diff --git a/tests/codex-catalog.test.ts b/tests/codex-catalog.test.ts index d3e0c431d5..27833dd4ca 100644 --- a/tests/codex-catalog.test.ts +++ b/tests/codex-catalog.test.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect, spyOn, test } from "bun:test"; import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { applyNativeVisibility, augmentRoutedModelsWithMetadata, augmentRoutedModelsWithRegistryOpenAiApiRows, buildCatalogEntries, buildComboCatalogOmission, catalogModelSlug, clampCatalogModelsToCodexSupport, clampEntryToCodexSupportedEfforts, clampedDefaultEffort, CODEX_ACCOUNT_BOUND_CATALOG_KIND, CODEX_NATIVE_ALIAS_CATALOG_KIND, comboCatalogOmissionReason, deriveComboCatalogModel, exactComboCatalogSlugs, filterCatalogVisibleModels, filterSupportedNativeSlugs, gatherRoutedModels as gatherRoutedModelsDirect, isDatedVariantId, isMediaGenerationModelId, loadBundledCodexCatalog, materializeBundledCodexCatalog, mergeCatalogEntriesForSync, NATIVE_OPENAI_MODELS, nativeOpenAiContextWindow, normalizeRoutedCatalogEntry, resetCatalogRuntimeStateForTests, resetOpenAiApiCatalogWarningStateForTests, resolveComboCatalogMember, shouldExposeRoutedModel } from "../src/codex/catalog"; +import { applyNativeVisibility, augmentRoutedModelsWithMetadata, augmentRoutedModelsWithRegistryOpenAiApiRows, buildCatalogEntries, buildComboCatalogOmission, catalogModelSlug, clampCatalogModelsToCodexSupport, clampEntryToCodexSupportedEfforts, clampedDefaultEffort, CODEX_ACCOUNT_BOUND_CATALOG_KIND, CODEX_NATIVE_ALIAS_CATALOG_KIND, comboCatalogOmissionReason, deriveComboCatalogModel, exactComboCatalogSlugs, filterCatalogVisibleModels, filterSupportedNativeSlugs, gatherRoutedModels as gatherRoutedModelsDirect, isDatedVariantId, isMediaGenerationModelId, loadBundledCodexCatalog, materializeBundledCodexCatalog, mergeCatalogEntriesForSync, NATIVE_DAYBREAK_BLUE_MODEL, NATIVE_OPENAI_MODELS, nativeDefaultReasoningEffort, nativeInputModalities, nativeOpenAiCapabilitySourceSlug, nativeOpenAiContextWindow, nativeReasoningEfforts, normalizeRoutedCatalogEntry, resetCatalogRuntimeStateForTests, resetOpenAiApiCatalogWarningStateForTests, resolveComboCatalogMember, shouldExposeRoutedModel, upstreamNativeEntry } from "../src/codex/catalog"; import { CODEX_CUSTOM_MODEL_CATALOG_KIND, CODEX_PROVIDER_MODEL_CATALOG_KIND, @@ -2519,6 +2519,160 @@ 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", () => { + 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); + expect(nativeInputModalities(NATIVE_DAYBREAK_BLUE_MODEL)).toEqual(["text", "image"]); + expect(nativeReasoningEfforts(NATIVE_DAYBREAK_BLUE_MODEL)) + .toEqual(["low", "medium", "high", "xhigh", "max", "ultra"]); + expect(nativeDefaultReasoningEffort(NATIVE_DAYBREAK_BLUE_MODEL)).toBe("low"); + + const source = upstreamNativeEntry(NATIVE_DAYBREAK_BLUE_MODEL); + expect(source).toMatchObject({ + slug: NATIVE_DAYBREAK_BLUE_MODEL, + display_name: "Daybreak Blue", + context_window: 372_000, + max_context_window: 372_000, + comp_hash: "3000", + tool_mode: "code_mode_only", + use_responses_lite: true, + supports_parallel_tool_calls: true, + supports_search_tool: true, + multi_agent_version: "v2", + }); + expect(source).not.toHaveProperty("availability_nux"); + expect(source?.base_instructions).toContain("powered by the gpt-daybreak-blue-latest"); + expect(source?.base_instructions).not.toContain("based on GPT-5"); + expect((source?.model_messages as { instructions_template?: string })?.instructions_template) + .toContain("powered by the gpt-daybreak-blue-latest"); + + const projected = buildCatalogEntries( + nativeTemplate(), + NATIVE_OPENAI_MODELS, + [], + undefined, + false, + "default", + new Set(), + ["main"], + new Set(), + new Set(), + undefined, + [...NATIVE_OPENAI_MODELS, NATIVE_DAYBREAK_BLUE_MODEL], + new Map([["main", [...NATIVE_OPENAI_MODELS, NATIVE_DAYBREAK_BLUE_MODEL]]]), + ); + const daybreak = projected.find(entry => entry.slug === `main/${NATIVE_DAYBREAK_BLUE_MODEL}`); + const sol = projected.find(entry => entry.slug === "gpt-5.6-sol"); + expect(daybreak).toBeDefined(); + expect(daybreak?.auto_compact_token_limit).toBe(334_800); + expect(daybreak).toMatchObject({ + context_window: sol?.context_window, + max_context_window: sol?.max_context_window, + comp_hash: sol?.comp_hash, + tool_mode: sol?.tool_mode, + use_responses_lite: sol?.use_responses_lite, + 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); + expect(projected.some(entry => entry.slug === "daybreak-blue-latest")).toBe(false); + }); + + test("configured ChatGPT-forward Daybreak gets Sol native metadata without API-key crossover", async () => { + globalThis.fetch = (() => { throw new Error("forward providers must not fetch /models"); }) as typeof fetch; + const forwardConfig: OcxConfig = { + port: 10100, + defaultProvider: "openai", + providers: { + openai: { + adapter: "openai-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", + // The built-in OpenAI provider defaults an omitted authMode to forward. + codexAccountMode: "pool", + }, + }, + codexAccountPickerEnabled: false, + codexAccountNamespaces: { main: "@main" }, + customModels: [{ + id: "daybreak-codex-forward", + provider: "openai", + modelId: NATIVE_DAYBREAK_BLUE_MODEL, + // An API-sized user value may lower neither the installed Codex native contract nor + // accidentally collapse this row into the separately billed API-key surface. + contextWindow: 1_050_000, + }], + }; + + const models = await gatherRoutedModels(forwardConfig); + const model = models.find(row => row.provider === "openai" && row.id === NATIVE_DAYBREAK_BLUE_MODEL); + expect(model).toMatchObject({ + id: NATIVE_DAYBREAK_BLUE_MODEL, + provider: "openai", + displayName: "Daybreak Blue", + catalogKind: CODEX_CUSTOM_MODEL_CATALOG_KIND, + codexForwardNativeCapabilityAlias: true, + contextWindow: 372_000, + inputModalities: ["text", "image"], + reasoningEfforts: ["low", "medium", "high", "xhigh", "max", "ultra"], + defaultReasoningEffort: "low", + parallelToolCalls: true, + }); + + const entries = buildCatalogEntries(nativeTemplate(), [], models); + const daybreak = entries.find(entry => entry.slug === `openai/${NATIVE_DAYBREAK_BLUE_MODEL}`); + expect(daybreak).toMatchObject({ + slug: `openai/${NATIVE_DAYBREAK_BLUE_MODEL}`, + display_name: "Daybreak Blue", + context_window: 372_000, + max_context_window: 372_000, + auto_compact_token_limit: 334_800, + comp_hash: "3000", + tool_mode: "code_mode_only", + use_responses_lite: true, + supports_parallel_tool_calls: true, + supports_search_tool: true, + multi_agent_version: "v2", + opencodex_catalog_kind: CODEX_CUSTOM_MODEL_CATALOG_KIND, + }); + expect(daybreak?.base_instructions).toContain("powered by the gpt-daybreak-blue-latest"); + expect(daybreak?.model_messages).toBeDefined(); + expect(entries.some(entry => entry.slug === NATIVE_DAYBREAK_BLUE_MODEL)).toBe(false); + expect(entries.some(entry => entry.slug === `main/${NATIVE_DAYBREAK_BLUE_MODEL}`)).toBe(false); + + const apiRows = augmentRoutedModelsWithRegistryOpenAiApiRows([], openAiApiCatalogConfig()); + expect(apiRows.find(row => row.provider === "openai-apikey" && row.id === "daybreak-blue-latest")) + .toMatchObject({ contextWindow: 1_050_000, maxInputTokens: 922_000 }); + }); + + test("Daybreak metadata inheritance rejects noncanonical providers", async () => { + const models = await gatherRoutedModels({ + port: 10100, + defaultProvider: "openai", + providers: { + openai: { + adapter: "openai-responses", + baseUrl: "https://example.invalid/v1", + authMode: "key", + liveModels: false, + }, + }, + customModels: [{ + id: "not-codex-forward", + provider: "openai", + modelId: NATIVE_DAYBREAK_BLUE_MODEL, + }], + }); + const model = models.find(row => row.provider === "openai" && row.id === NATIVE_DAYBREAK_BLUE_MODEL); + expect(model?.codexForwardNativeCapabilityAlias).toBeUndefined(); + const row = buildCatalogEntries(nativeTemplate(), [], models) + .find(entry => entry.slug === `openai/${NATIVE_DAYBREAK_BLUE_MODEL}`); + expect(row?.context_window).toBe(128_000); + expect(row?.use_responses_lite).toBeUndefined(); + expect(row?.supports_search_tool).toBe(true); + expect(row?.multi_agent_version).toBeUndefined(); + }); + test("catalog sync upgrades fallback-quality gpt-5.6 entries but preserves genuine ones", () => { // Fallback-quality: display_name stamped with the bare slug (ocx synthesis signature), // wrong ladder (ultra on luna) left by an older ocx version. diff --git a/tests/codex-convergence-account-selectors.test.ts b/tests/codex-convergence-account-selectors.test.ts index 12fa07e235..c5536cea35 100644 --- a/tests/codex-convergence-account-selectors.test.ts +++ b/tests/codex-convergence-account-selectors.test.ts @@ -373,10 +373,21 @@ test("convergence preserves an observed account-only native id without creating const catalog = await convergeCatalog(config(true)); const models = catalog.models ?? []; - expect(models.find(entry => entry.slug === "desktop/gpt-daybreak-blue-latest")).toMatchObject({ + const daybreak = models.find(entry => entry.slug === "desktop/gpt-daybreak-blue-latest"); + expect(daybreak).toMatchObject({ visibility: "list", opencodex_catalog_kind: CODEX_ACCOUNT_BOUND_CATALOG_KIND, + context_window: 372_000, + max_context_window: 372_000, + auto_compact_token_limit: 334_800, + comp_hash: "3000", + tool_mode: "code_mode_only", + use_responses_lite: true, + supports_parallel_tool_calls: true, + multi_agent_version: "v2", }); + 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); }); diff --git a/tests/codex-routing.test.ts b/tests/codex-routing.test.ts index 162697bd98..1c9a65360f 100644 --- a/tests/codex-routing.test.ts +++ b/tests/codex-routing.test.ts @@ -255,6 +255,59 @@ describe("codex routing", () => { expect(resolveCodexAccountForThread("after-success", config)).toBe("a"); }); + test("routes account-scoped Daybreak Blue through the exact main account without rewriting its wire id", () => { + const config = makeConfig({ + providers: { + openai: { + adapter: "openai-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", + authMode: "forward", + }, + }, + defaultProvider: "openai", + codexAccountNamespaces: { main: "@main" }, + }); + + expect(routeModel(config, "main/gpt-daybreak-blue-latest")).toMatchObject({ + providerName: "openai", + modelId: "gpt-daybreak-blue-latest", + routeKind: "explicit-account", + routeReason: "account-namespace", + codexAccountMode: "pool", + codexAccountNamespace: "main", + codexAccountId: MAIN_CODEX_ACCOUNT_ID, + routeDecision: { + requestedModel: "main/gpt-daybreak-blue-latest", + selected: { model: "gpt-daybreak-blue-latest", accountRef: "main" }, + }, + }); + }); + + test("routes the configured Codex-forward Daybreak selector without API alias rewriting", () => { + const config = makeConfig({ + providers: { + openai: { + adapter: "openai-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", + authMode: "forward", + }, + }, + defaultProvider: "openai", + customModels: [{ + id: "daybreak-codex-forward", + provider: "openai", + modelId: "gpt-daybreak-blue-latest", + }], + }); + + expect(routeModel(config, "openai/gpt-daybreak-blue-latest")).toMatchObject({ + providerName: "openai", + modelId: "gpt-daybreak-blue-latest", + routeKind: "explicit-provider", + routeReason: "explicit-provider-namespace", + }); + }); + test("paused main account is excluded even when it is the active and lowest-usage candidate", () => { writeFileSync(join(TEST_DIR, "auth.json"), JSON.stringify({ tokens: { access_token: "main-access", account_id: "main-chatgpt-id" },