diff --git a/.github/pr-assets/native-gpt56-1m-context-mode.png b/.github/pr-assets/native-gpt56-1m-context-mode.png new file mode 100644 index 0000000000..f546ad62c9 Binary files /dev/null and b/.github/pr-assets/native-gpt56-1m-context-mode.png differ 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 053fba1669..d1bc580a44 100644 --- a/docs-site/src/content/docs/guides/codex-app-models.md +++ b/docs-site/src/content/docs/guides/codex-app-models.md @@ -167,7 +167,7 @@ metadata instead of an older-template approximation. | Route | Picker ids and catalog metadata | | --- | --- | -| 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 922,000-token catalog window. | +| 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`. Default mode keeps the 272,000-token catalog window. The Models card's explicit 1M mode preserves that catalog default, raises only these three rows' `max_context_window` to 1,000,000, and synchronizes Codex's 1,000,000 / 900,000 root opt-in. The pre-existing context cap can still opt into or cap the measured 922,000 workflow. | | 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. | | 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 (922,000 context; 829,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) | diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index d666ca972d..3fbbe077be 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -97,6 +97,7 @@ differing backup and rewrites known legacy namespaced selected ids to bare ids. | `modelVercelGatewayRouting?` | `Record` | Exact model-id overrides that replace the provider-wide Vercel AI Gateway preference. | | `authMode?` | `"key" \| "forward" \| "oauth" \| "local"` | Authentication mode (default `key`). OAuth/subscription credentials are stored outside `config.json`; `local` is limited to providers whose registry entry permits it. | | `codexAccountMode?` | `"pool" \| "direct"` | Canonical `openai` only; defaults to Pool. Direct bypasses pool state. | +| `codexNativeContextMode?` | `"default" \| "1m"` | Canonical Codex-login `openai` only. `default` (and an omitted field) preserves the existing 272k default plus the optional 922k context-cap workflow. `1m` is the explicit Codex GPT-5.6 Sol/Terra/Luna opt-in: sync writes `model_context_window = 1000000` and `model_auto_compact_token_limit = 900000`, and raises only those three catalog maxima to 1,000,000. Routed providers, GPT-5.5, GPT-5.4, Daybreak aliases, and `effective_context_window_percent` are unchanged. | | `refreshPolicy?` | `"proactive" \| "lazy-only" \| "disabled"` | Override this OAuth provider's Token Guardian policy. | | `reasoningEfforts?` | `string[]` | Provider-wide Codex reasoning labels to advertise and send. For `google`-adapter providers, a configured ladder also asserts `thinkingLevel` capability: direct and Vertex non-image requests send the selected effort as `generationConfig.thinkingConfig.thinkingLevel`, while Cloud Code Assist uses its envelope-specific path. | | `modelReasoningEfforts?` | `Record` | Per-model labels. An empty list hides effort control. As with `reasoningEfforts`, each configured `google`-adapter ladder asserts `thinkingLevel` capability; direct and Vertex non-image requests use the flat Gemini path, while Cloud Code Assist sends it under its request envelope. | diff --git a/docs-site/src/content/docs/zh-cn/guides/codex-app-models.md b/docs-site/src/content/docs/zh-cn/guides/codex-app-models.md index 7709de7721..18c23332a0 100644 --- a/docs-site/src/content/docs/zh-cn/guides/codex-app-models.md +++ b/docs-site/src/content/docs/zh-cn/guides/codex-app-models.md @@ -65,7 +65,7 @@ visibility = "list" | 路由 | 选择器 id 与目录元数据 | | --- | --- | -| Codex 登录(账户限定的选择器行未启用) | 显示 `gpt-5.6-sol`、`gpt-5.6-terra`、`gpt-5.6-luna` 等裸原生 id,并按 `codexAccountMode` 使用 Pool 或 Direct。GPT-5.6 行使用 922,000-token 目录窗口。 | +| Codex 登录(账户限定的选择器行未启用) | 显示 `gpt-5.6-sol`、`gpt-5.6-terra`、`gpt-5.6-luna` 等裸原生 id,并按 `codexAccountMode` 使用 Pool 或 Direct。默认模式保留 272,000-token 目录窗口。Models 卡片中的显式 1M 模式不改变该默认值,只把这三个模型的 `max_context_window` 提升为 1,000,000,并同步 Codex 的 1,000,000 / 900,000 root opt-in。原有上下文 cap 仍可启用或限制实测的 922,000 流程。 | | Codex 登录(账户限定的选择器行已启用且存在有效 selector) | 为每个有效 selector 与受支持原生模型的组合显示 `/` 行。每行只使用映射账户,裸原生行会从选择器中隐藏。原生 metadata 与 context window 会保留。 | | OpenAI(API key) | 恰好八个命名空间行:`gpt-5.5`、`gpt-5.6`、Sol/Terra/Luna,以及三个 `*-pro` 虚拟 id(八个条目均为 1,050,000 context / 922,000 max input) | | OpenRouter | `openrouter/openai/gpt-5.6-sol`、`openrouter/openai/gpt-5.6-terra`、`openrouter/openai/gpt-5.6-luna`(922,000) | diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md b/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md index 754e77e220..6b18e86fb7 100644 --- a/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md +++ b/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md @@ -82,6 +82,7 @@ selector,而不是分配一个新名称。 | `vercelGatewayRouting?` | `VercelGatewayRouting` | 默认的 Vercel AI Gateway `order`、`only` 和 `sort`(`"cost"` \| `"ttft"` \| `"tps"`)偏好;仅对使用 `openai-chat` 的规范 Vercel AI Gateway 有效。 | | `authMode?` | `"key" \| "forward" \| "oauth" \| "local"` | 身份验证模式(默认 `key`)。OAuth/订阅凭据存放在 `config.json` 之外;`local` 仅限注册表条目允许它的提供者。 | | `codexAccountMode?` | `"pool" \| "direct"` | 仅适用于规范的 `openai`;默认是 Pool。Direct 会绕过池状态。 | +| `codexNativeContextMode?` | `"default" \| "1m"` | 仅适用于使用 Codex 登录的规范 `openai`。`default`(以及省略该字段)保留现有 272k 默认行为和可选的 922k 上下文上限流程。`1m` 是 GPT-5.6 Sol/Terra/Luna 的显式 Codex opt-in:同步会写入 `model_context_window = 1000000` 与 `model_auto_compact_token_limit = 900000`,且只把这三个模型的目录最大值提升至 1,000,000。路由提供商、GPT-5.5、GPT-5.4、Daybreak alias 以及 `effective_context_window_percent` 均不受影响。 | | `refreshPolicy?` | `"proactive" \| "lazy-only" \| "disabled"` | 覆盖该 OAuth 提供者的 Token Guardian 策略。 | | `reasoningEfforts?` | `string[]` | 要向外暴露并发送的、提供者级 Codex 推理标签。 | | `modelReasoningEfforts?` | `Record` | 按模型设置的标签。空列表会隐藏 effort 控件。 | diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 5278690349..48c4c0c90d 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -486,6 +486,14 @@ export const de: Record = { "models.subtitle": "Steuere, welche Modelle Codex sieht — natives GPT-Passthrough und geroutete Anbieter, nach Anbieter gruppiert (Kopfzeile zum Einklappen anklicken). Ausgeblendete Modelle fehlen in Katalog und Auswahl, bleiben aber per genauer ID aufrufbar. Änderungen gelten bei der nächsten Codex-Runde — opencodex invalidiert Codex 5-Minuten-Modell-Cache, kein Neustart nötig.", "models.nativeGroupLabel": "OpenAI nativ", "models.nativeHint": "Passthrough-Modelle verwenden die unter Anbieter gewählte Pool- oder Direkt-Option. Ausblenden entfernt sie aus der Codex-Auswahl (Katalogeintrag bleibt, Reaktivierung stellt exakt wieder her). Ein hier hinzugefügtes Modell wird als gerouteter `openai/`-Selektor registriert, nicht als neue reine Passthrough-ID.", + "models.nativeContextModeTitle": "GPT-5.6-Kontextmodus", + "models.nativeContextModeDefault": "Standard", + "models.nativeContextModeOneMillion": "1M", + "models.nativeContextModeDefaultHint": "Verwendet die von Codex empfohlene Standard-Kontextkonfiguration.", + "models.nativeContextModeOneMillionHint": "Aktiviert den Codex-1M-Kontextmodus für native GPT-5.6 Sol, Terra und Luna.", + "models.nativeContextModeExperimental": "Die maximale Verfügbarkeit hängt vom Konto und OpenAI-Rollout ab. Bei Ablehnung zu großer Anfragen auf Standard zurückschalten. Neue Sitzungen verwenden die synchronisierte Einstellung; einen veralteten Codex-App-Server bei Hinweis neu starten.", + "models.nativeContextModeApplied": "GPT-5.6-Kontextmodus synchronisiert. Neue Codex-Sitzungen verwenden ihn.", + "models.nativeContextModeSaveFailed": "Der GPT-5.6-Kontextmodus konnte nicht angewendet werden; die vorherige Einstellung wurde wiederhergestellt.", "models.active": "{active}/{total} sichtbar", "models.workspace.providers": "Anbieter", "models.workspace.allProviders": "Alle Anbieter", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index ab6b652c69..7045085134 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -511,6 +511,14 @@ export const en = { "models.subtitle": "Toggle which models Codex sees — native GPT passthrough and routed providers, grouped by provider (click a header to collapse). Hidden models stay off the catalog + model picker but remain directly callable by exact id. Changes apply on the next Codex turn — opencodex invalidates Codex's 5-min model cache so no restart is needed.", "models.nativeGroupLabel": "OpenAI native", "models.nativeHint": "Passthrough models use the Pool or Direct account option selected on Providers. Toggling one off hides it from the Codex picker (the catalog entry is kept, so re-enabling restores it exactly). Adding a model here registers a routed `openai/` selector, not a new bare passthrough id.", + "models.nativeContextModeTitle": "GPT-5.6 context mode", + "models.nativeContextModeDefault": "Default", + "models.nativeContextModeOneMillion": "1M", + "models.nativeContextModeDefaultHint": "Use Codex's recommended default context configuration.", + "models.nativeContextModeOneMillionHint": "Enable Codex's 1M context mode for native GPT-5.6 Sol, Terra, and Luna.", + "models.nativeContextModeExperimental": "Maximum availability depends on your account and OpenAI rollout. If an oversized request is rejected, switch back to Default. New sessions use the synced setting; restart a stale Codex app-server if prompted.", + "models.nativeContextModeApplied": "GPT-5.6 context mode synced. New Codex sessions will use it.", + "models.nativeContextModeSaveFailed": "Could not apply the GPT-5.6 context mode; the previous setting was restored.", "models.active": "{active}/{total} visible", "models.workspace.providers": "Providers", "models.workspace.allProviders": "All providers", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 5b1809f394..1dfeb264f4 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -496,6 +496,14 @@ export const fr: Record = { "models.subtitle": "Choisissez les modèles visibles par Codex — accès direct aux GPT natifs et fournisseurs routés, regroupés par fournisseur (cliquez sur un en-tête pour le réduire). Les modèles masqués sont retirés du catalogue et du sélecteur, mais restent directement accessibles par leur identifiant exact. Les modifications s’appliquent au prochain tour Codex — opencodex invalide le cache de modèles de Codex de 5 min, sans nécessiter de redémarrage.", "models.nativeGroupLabel": "OpenAI natif", "models.nativeHint": "Les modèles en accès direct utilisent l’option de compte Groupe ou Direct sélectionnée dans Fournisseurs. La désactivation d’un modèle le masque dans le sélecteur Codex (son entrée de catalogue est conservée afin que sa réactivation la restaure à l’identique). Ajouter un modèle ici enregistre un sélecteur routé `openai/`, et non un nouvel identifiant passthrough brut.", + "models.nativeContextModeTitle": "Mode de contexte GPT-5.6", + "models.nativeContextModeDefault": "Par défaut", + "models.nativeContextModeOneMillion": "1 M", + "models.nativeContextModeDefaultHint": "Utilise la configuration de contexte par défaut recommandée par Codex.", + "models.nativeContextModeOneMillionHint": "Active le mode de contexte Codex 1M pour GPT-5.6 Sol, Terra et Luna natifs.", + "models.nativeContextModeExperimental": "La disponibilité maximale dépend du compte et du déploiement OpenAI. Revenez au mode par défaut si une requête trop longue est refusée. Les nouvelles sessions utilisent le réglage synchronisé ; redémarrez un app-server Codex obsolète si demandé.", + "models.nativeContextModeApplied": "Mode de contexte GPT-5.6 synchronisé. Les nouvelles sessions Codex l’utiliseront.", + "models.nativeContextModeSaveFailed": "Impossible d’appliquer le mode de contexte GPT-5.6 ; le réglage précédent a été restauré.", "models.active": "{active}/{total} visibles", "models.workspace.providers": "Fournisseurs", "models.workspace.allProviders": "Tous les fournisseurs", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index d1bfb6dfcd..ab51971f33 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -494,6 +494,14 @@ export const ja: Record = { "models.subtitle": "Codex に表示するモデルを切り替えます — ネイティブ GPT パススルーとルーティングプロバイダー、プロバイダー別(ヘッダーをクリックで折りたたみ)。非表示モデルはカタログとピッカーから外れますが、正確な id での直接呼び出しは可能です。変更は次回の Codex ターンで適用 — opencodex は Codex の 5 分間モデルキャッシュを無効化するので再起動は不要です。", "models.nativeGroupLabel": "OpenAI ネイティブ", "models.nativeHint": "パススルーモデルはプロバイダーで選択したプールまたはダイレクトアカウントオプションを使用します。一つオフにすると Codex ピッカーから隠します(カタログエントリは保持されるので、再有効化で正確に復元されます)。 ここでモデルを追加すると、bare passthrough id ではなくルーティングされた `openai/` セレクタとして登録されます。", + "models.nativeContextModeTitle": "GPT-5.6 コンテキストモード", + "models.nativeContextModeDefault": "デフォルト", + "models.nativeContextModeOneMillion": "1M", + "models.nativeContextModeDefaultHint": "Codex 推奨のデフォルトコンテキスト設定を使用します。", + "models.nativeContextModeOneMillionHint": "ネイティブ GPT-5.6 Sol、Terra、Luna で Codex 1M コンテキストモードを有効にします。", + "models.nativeContextModeExperimental": "利用可能な最大値はアカウントと OpenAI のロールアウトに依存します。長すぎるリクエストが拒否された場合はデフォルトに戻してください。新しいセッションには同期済み設定が適用され、古い Codex app-server の再起動を求められる場合があります。", + "models.nativeContextModeApplied": "GPT-5.6 コンテキストモードを同期しました。新しい Codex セッションに適用されます。", + "models.nativeContextModeSaveFailed": "GPT-5.6 コンテキストモードを適用できなかったため、以前の設定に戻しました。", "models.active": "{active}/{total} 表示中", "models.workspace.providers": "プロバイダー", "models.workspace.allProviders": "すべてのプロバイダー", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 532b798018..a63931e56c 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -497,6 +497,14 @@ export const ko: Record = { "models.subtitle": "Codex가 보는 모델을 켜고 끕니다 — 네이티브 GPT passthrough와 라우팅된 모델을 프로바이더별로 묶어 보여줍니다(헤더를 클릭하면 접힘). 숨긴 모델은 카탈로그와 선택기에서 빠지지만 정확한 id로 직접 호출할 수 있습니다. 변경 사항은 다음 Codex 턴에 적용됩니다 — opencodex가 Codex의 5분 모델 캐시를 무효화하므로 재시작이 필요 없습니다.", "models.nativeGroupLabel": "OpenAI 네이티브", "models.nativeHint": "프로바이더에서 선택한 풀 또는 직접 계정 옵션으로 서빙되는 passthrough 모델입니다. 끄면 Codex 선택기에서 숨겨지고, 카탈로그 항목은 유지되므로 다시 켜면 그대로 복원됩니다. 여기서 모델을 추가하면 bare passthrough id가 아니라 라우팅된 `openai/` selector로 등록됩니다.", + "models.nativeContextModeTitle": "GPT-5.6 컨텍스트 모드", + "models.nativeContextModeDefault": "기본값", + "models.nativeContextModeOneMillion": "1M", + "models.nativeContextModeDefaultHint": "Codex가 권장하는 기본 컨텍스트 구성을 사용합니다.", + "models.nativeContextModeOneMillionHint": "네이티브 GPT-5.6 Sol, Terra, Luna에 Codex 1M 컨텍스트 모드를 활성화합니다.", + "models.nativeContextModeExperimental": "최대 사용 가능 범위는 계정 및 OpenAI 롤아웃에 따라 달라집니다. 너무 긴 요청이 거부되면 기본값으로 전환하세요. 새 세션은 동기화된 설정을 사용하며, 오래된 Codex app-server는 안내에 따라 다시 시작하세요.", + "models.nativeContextModeApplied": "GPT-5.6 컨텍스트 모드를 동기화했습니다. 새 Codex 세션에서 사용됩니다.", + "models.nativeContextModeSaveFailed": "GPT-5.6 컨텍스트 모드를 적용하지 못해 이전 설정으로 복원했습니다.", "models.active": "{active}/{total} 표시", "models.workspace.providers": "프로바이더", "models.workspace.allProviders": "모든 프로바이더", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 28f30646bd..8fa37f21f1 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -499,6 +499,14 @@ export const ru: Record = { "models.subtitle": "Управляйте тем, какие модели видит Codex — нативные GPT (сквозной проброс) и модели маршрутизируемых провайдеров, сгруппированные по провайдеру (нажмите на заголовок, чтобы свернуть группу). Скрытые модели исчезают из каталога и селектора, но остаются вызываемыми по точному id. Изменения применяются на следующем ходе Codex — opencodex сбрасывает 5-минутный кэш моделей Codex, поэтому перезапуск не требуется.", "models.nativeGroupLabel": "Нативные OpenAI", "models.nativeHint": "Модели сквозного проброса используют режим аккаунта (пул или прямое подключение), выбранный на странице «Провайдеры». Отключение модели скрывает её из селектора Codex (запись в каталоге сохраняется, поэтому при повторном включении она восстанавливается в точности). Добавление модели здесь регистрирует маршрутизируемый селектор `openai/`, а не новый «голый» passthrough-идентификатор.", + "models.nativeContextModeTitle": "Режим контекста GPT-5.6", + "models.nativeContextModeDefault": "По умолчанию", + "models.nativeContextModeOneMillion": "1M", + "models.nativeContextModeDefaultHint": "Использует рекомендованную Codex конфигурацию контекста по умолчанию.", + "models.nativeContextModeOneMillionHint": "Включает режим контекста Codex 1M для нативных GPT-5.6 Sol, Terra и Luna.", + "models.nativeContextModeExperimental": "Максимальная доступность зависит от аккаунта и развёртывания OpenAI. Если слишком длинный запрос отклонён, вернитесь к режиму по умолчанию. Новые сессии используют синхронизированную настройку; при предупреждении перезапустите устаревший Codex app-server.", + "models.nativeContextModeApplied": "Режим контекста GPT-5.6 синхронизирован. Он будет использоваться в новых сессиях Codex.", + "models.nativeContextModeSaveFailed": "Не удалось применить режим контекста GPT-5.6; восстановлена предыдущая настройка.", "models.active": "{active}/{total} видимо", "models.workspace.providers": "Провайдеры", "models.workspace.allProviders": "Все провайдеры", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 232900f51e..5bb09ae53e 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -502,6 +502,14 @@ export const tr: Record = { "models.subtitle": "Codex'in göreceği modelleri açıp kapatın.", "models.nativeGroupLabel": "Yerel OpenAI", "models.nativeHint": "Doğrudan geçiş modelleri Sağlayıcılar bölümünde seçilen Havuz veya Doğrudan seçeneğini kullanır. Buradan model eklemek, yeni bir düz passthrough kimliği değil, yönlendirilmiş bir `openai/` seçici kaydeder.", + "models.nativeContextModeTitle": "GPT-5.6 bağlam modu", + "models.nativeContextModeDefault": "Varsayılan", + "models.nativeContextModeOneMillion": "1M", + "models.nativeContextModeDefaultHint": "Codex'in önerilen varsayılan bağlam yapılandırmasını kullanır.", + "models.nativeContextModeOneMillionHint": "Yerel GPT-5.6 Sol, Terra ve Luna için Codex 1M bağlam modunu etkinleştirir.", + "models.nativeContextModeExperimental": "Azami kullanılabilirlik hesaba ve OpenAI dağıtımına bağlıdır. Aşırı uzun istek reddedilirse Varsayılan'a dönün. Yeni oturumlar eşitlenen ayarı kullanır; istenirse eski Codex app-server'ı yeniden başlatın.", + "models.nativeContextModeApplied": "GPT-5.6 bağlam modu eşitlendi. Yeni Codex oturumları bunu kullanacak.", + "models.nativeContextModeSaveFailed": "GPT-5.6 bağlam modu uygulanamadı; önceki ayar geri yüklendi.", "models.active": "{active}/{total} görünür", "models.workspace.providers": "Sağlayıcılar", "models.workspace.allProviders": "Tüm sağlayıcılar", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index b491a7bc94..f97c100c07 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -378,6 +378,14 @@ export const zhTW: Record = { "models.subtitle": "開關 Codex 可見的模型 — 原生 GPT passthrough 與已路由模型按供應商分組(點選標題可摺疊)。已停用的模型會從目錄和模型選擇器中隱藏。更改在下一個 Codex 回合生效 — opencodex 會使 Codex 的 5 分鐘模型快取失效,因此無需重新啟動。", "models.nativeGroupLabel": "OpenAI 原生", "models.nativeHint": "Passthrough 模型使用在供應商頁面選擇的帳號池或直連選項。關閉後會從 Codex 選擇器中隱藏(目錄條目保留,重新開啟即可完整恢復)。 在此新增模型會註冊為路由的 `openai/` 選擇器,而非新的裸 passthrough id。", + "models.nativeContextModeTitle": "GPT-5.6 上下文模式", + "models.nativeContextModeDefault": "預設", + "models.nativeContextModeOneMillion": "100 萬", + "models.nativeContextModeDefaultHint": "使用 Codex 官方建議的預設上下文設定。", + "models.nativeContextModeOneMillionHint": "為原生 GPT-5.6 Sol、Terra 與 Luna 啟用 Codex 1M 上下文模式。", + "models.nativeContextModeExperimental": "實際最大可用上下文取決於帳號與 OpenAI 後端 rollout。若超長請求遭拒,請切回預設。新工作階段會使用同步後的設定;若提示 app-server 已過期,請重新啟動 Codex。", + "models.nativeContextModeApplied": "GPT-5.6 上下文模式已同步,新 Codex 工作階段將使用此設定。", + "models.nativeContextModeSaveFailed": "無法套用 GPT-5.6 上下文模式;已還原先前設定。", "models.active": "{active}/{total} 已啟用", "models.workspace.providers": "供應商", "models.workspace.allProviders": "所有供應商", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 189a61fa20..b4b202de73 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -494,6 +494,14 @@ export const zh: Record = { "models.subtitle": "开关 Codex 可见的模型 — 原生 GPT passthrough 与已路由模型按提供方分组(点击标题可折叠)。隐藏的模型不会出现在目录和模型选择器中,但仍可按精确 id 直接调用。更改在下一个 Codex 回合生效 — opencodex 会使 Codex 的 5 分钟模型缓存失效,因此无需重启。", "models.nativeGroupLabel": "OpenAI 原生", "models.nativeHint": "Passthrough 模型使用在提供方页面选择的账户池或直连选项。关闭后会从 Codex 选择器中隐藏(目录条目保留,重新开启即可完整恢复)。 在此添加模型将注册为路由的 `openai/` 选择器,而不是新的裸 passthrough id。", + "models.nativeContextModeTitle": "GPT-5.6 上下文模式", + "models.nativeContextModeDefault": "默认", + "models.nativeContextModeOneMillion": "1M", + "models.nativeContextModeDefaultHint": "使用 Codex 官方推荐的默认上下文配置。", + "models.nativeContextModeOneMillionHint": "为原生 GPT-5.6 Sol、Terra 和 Luna 启用 Codex 1M 上下文模式。", + "models.nativeContextModeExperimental": "实际最大可用上下文取决于账户与 OpenAI 后端 rollout。若超长请求被拒绝,请切回默认。新会话会使用同步后的设置;如提示 app-server 已过期,请重启 Codex。", + "models.nativeContextModeApplied": "GPT-5.6 上下文模式已同步,新 Codex 会话将使用该设置。", + "models.nativeContextModeSaveFailed": "无法应用 GPT-5.6 上下文模式;已恢复此前设置。", "models.active": "{active}/{total} 可见", "models.workspace.providers": "提供方", "models.workspace.allProviders": "所有提供方", diff --git a/gui/src/models-groups.ts b/gui/src/models-groups.ts index a8d6ddc69c..5616edeb9f 100644 --- a/gui/src/models-groups.ts +++ b/gui/src/models-groups.ts @@ -28,6 +28,7 @@ export interface ConfiguredProviderSummary { models?: string[]; contextWindow?: number; modelContextWindows?: Record; + codexNativeContextMode?: "default" | "1m"; discovery?: ProviderDiscoverySummary; entitlement?: ProviderEntitlementSummary; } @@ -49,6 +50,7 @@ export interface ProviderModelGroup { configuredModels: string[]; contextWindow?: number; modelContextWindows?: Record; + codexNativeContextMode?: "default" | "1m"; discovery?: ProviderDiscoverySummary; entitlement?: ProviderEntitlementSummary; } @@ -86,6 +88,7 @@ export function buildProviderModelGroups section hidden. @@ -734,7 +736,7 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(enabled && nativeGroup - ? { provider, enabled, value: NATIVE_GPT56_OPT_IN_WINDOW } + ? { provider, enabled, value: NATIVE_GPT56_SAFE_WINDOW } : { provider, enabled }), }); try { @@ -754,6 +756,38 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; busyRef.current = false; } }; + const saveNativeContextMode = async (mode: "default" | "1m") => { + if (nativeContextModeBusyRef.current) return; + nativeContextModeBusyRef.current = true; + setNativeContextModeSaving(true); + setStatus(""); + try { + const response = await fetch(`${apiBase}/api/providers?name=openai`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ codexNativeContextMode: mode }), + }); + await readJsonOrThrow<{ success: boolean; codexNativeContextMode: "default" | "1m" }>( + response, + t("models.nativeContextModeSaveFailed"), + ); + setProviders(current => current.map(provider => provider.name === "openai" + ? { ...provider, codexNativeContextMode: mode } + : provider)); + publishFeedback(true, t("models.nativeContextModeApplied")); + await load(true); + reloadAppServerState(); + } catch (error) { + await load(true); + publishFeedback( + false, + error instanceof Error ? error.message : t("models.nativeContextModeSaveFailed"), + ); + } finally { + nativeContextModeBusyRef.current = false; + setNativeContextModeSaving(false); + } + }; const toggleCollapse = (p: string) => { setCollapsed(prev => { const n = new Set(prev); @@ -1194,6 +1228,10 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; const capOptions = group.nativeProviderGroup ? NATIVE_CAP_OPTIONS : CAP_OPTIONS; const capOptionSet = group.nativeProviderGroup ? NATIVE_CAP_OPTION_SET : CAP_OPTION_SET; const discoveryFailure = liveModels && discovery?.status === "failed" ? discovery : undefined; + const nativeOneMillionAvailable = nativeProviderGroup && rows.some(row => + row.id === "gpt-5.6-sol" || row.id === "gpt-5.6-terra" || row.id === "gpt-5.6-luna" + ); + const nativeContextMode = group.codexNativeContextMode ?? "default"; const q = (search[provider] ?? "").trim().toLowerCase(); const filtered = q ? rows.filter(m => m.id.toLowerCase().includes(q)) : rows; // Display-only: enabled models float to the top of each provider group so they @@ -1399,6 +1437,43 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; {!isCollapsed && (
{nativeProviderGroup &&

{t("models.nativeHint")}

} + {nativeOneMillionAvailable && ( +
+
+ {t("models.nativeContextModeTitle")} + + {nativeContextMode === "1m" + ? t("models.nativeContextModeOneMillionHint") + : t("models.nativeContextModeDefaultHint")} + + {t("models.nativeContextModeExperimental")} +
+
+ {(["default", "1m"] as const).map(mode => ( + + ))} +
+ {nativeContextModeSaving && ( + {t("common.saving")} + )} +
+ )} {!nativeProviderGroup && modelDiscovery && (
{t("models.newPolicyProvider")} diff --git a/gui/src/pages/models-shared.ts b/gui/src/pages/models-shared.ts index 6e1f463db7..717d6f5368 100644 --- a/gui/src/pages/models-shared.ts +++ b/gui/src/pages/models-shared.ts @@ -85,8 +85,8 @@ export const CAP_OPTION_SET = new Set(CAP_OPTIONS); * Anything else goes through "Custom". */ export const NATIVE_GPT56_DEFAULT_WINDOW = 272_000; -export const NATIVE_GPT56_OPT_IN_WINDOW = 922_000; -export const NATIVE_CAP_OPTIONS = [NATIVE_GPT56_DEFAULT_WINDOW, 372_000, NATIVE_GPT56_OPT_IN_WINDOW]; +export const NATIVE_GPT56_SAFE_WINDOW = 922_000; +export const NATIVE_CAP_OPTIONS = [NATIVE_GPT56_DEFAULT_WINDOW, 372_000, NATIVE_GPT56_SAFE_WINDOW]; export const NATIVE_CAP_OPTION_SET = new Set(NATIVE_CAP_OPTIONS); export const CUSTOM_OPTION = "custom"; export const THREAD_OPTIONS = [4, 8, 16, 32, 64, 128, 256, 500, 1000]; diff --git a/gui/tests/models-native-context-mode.test.tsx b/gui/tests/models-native-context-mode.test.tsx new file mode 100644 index 0000000000..367be3bb58 --- /dev/null +++ b/gui/tests/models-native-context-mode.test.tsx @@ -0,0 +1,162 @@ +import { afterEach, beforeEach, expect, test } from "bun:test"; +import { Window } from "happy-dom"; +import { act } from "react"; +import type { Root } from "react-dom/client"; +import { clearClientResourceStoresForTests } from "../src/client-resource"; +import { LanguageProvider } from "../src/i18n/provider"; +import Models from "../src/pages/Models"; + +const globals = [ + "document", "window", "navigator", "localStorage", "sessionStorage", + "IS_REACT_ACT_ENVIRONMENT", "setInterval", "clearInterval", "fetch", +] as const; +let previousGlobals: Record<(typeof globals)[number], PropertyDescriptor | undefined>; +let testWindow: Window; +let container: HTMLElement; +let root: Root | null = null; +let serverMode: "default" | "1m"; +let failSave: boolean; +let releaseSave: (() => void) | null; +const patches: Array> = []; + +const models = [ + { provider: "openai", id: "gpt-5.6-sol", namespaced: "gpt-5.6-sol", native: true, disabled: false }, + { provider: "openrouter", id: "gpt-5.6-sol", namespaced: "openrouter/gpt-5.6-sol", native: false, disabled: false }, +]; + +function providers() { + return [ + { name: "openai", authMode: "forward", codexNativeContextMode: serverMode }, + { name: "openrouter", authMode: "api-key" }, + ]; +} + +async function flush(): Promise { + await act(async () => { + await new Promise(resolve => testWindow.setTimeout(resolve, 0)); + await Promise.resolve(); + }); +} + +beforeEach(() => { + previousGlobals = Object.fromEntries( + globals.map(key => [key, Object.getOwnPropertyDescriptor(globalThis, key)]), + ) as typeof previousGlobals; + root = null; + serverMode = "default"; + failSave = false; + releaseSave = null; + patches.length = 0; + testWindow = new Window({ url: "http://localhost/" }); + Object.defineProperties(globalThis, { + document: { configurable: true, value: testWindow.document }, + window: { configurable: true, value: testWindow }, + navigator: { configurable: true, value: testWindow.navigator }, + localStorage: { configurable: true, value: testWindow.localStorage }, + sessionStorage: { configurable: true, value: testWindow.sessionStorage }, + IS_REACT_ACT_ENVIRONMENT: { configurable: true, value: true }, + setInterval: { configurable: true, value: () => 1 }, + clearInterval: { configurable: true, value: () => {} }, + }); + testWindow.localStorage.setItem("ocx-models-collapsed:v2", JSON.stringify([])); + testWindow.sessionStorage.setItem("ocx.models.catalog.v1:http://localhost", JSON.stringify({ + models, + providers: providers(), + selectedModels: {}, + disabled: [], + contextCaps: {}, + contextCapValue: 350_000, + })); + globalThis.fetch = (async (input, init) => { + const url = String(input); + const pathname = new URL(url, "http://localhost").pathname; + if (pathname === "/api/models") return Response.json(models); + if (pathname === "/api/providers" && init?.method === "PATCH") { + const body = JSON.parse(String(init.body)) as Record; + patches.push(body); + if (releaseSave !== null) await new Promise(resolve => { releaseSave = resolve; }); + if (failSave) return Response.json({ error: "synthetic mode failure" }, { status: 500 }); + serverMode = body.codexNativeContextMode as "default" | "1m"; + return Response.json({ success: true, codexNativeContextMode: serverMode }); + } + if (pathname === "/api/providers") return Response.json(providers()); + if (pathname === "/api/selected-models") return Response.json({ selected: {} }); + if (pathname === "/api/provider-context-caps") return Response.json({ caps: {} }); + if (pathname === "/api/combos") return Response.json({ combos: [] }); + if (pathname === "/api/shadow-call-settings") return Response.json({ enabled: false, model: "" }); + if (pathname === "/api/v2") return Response.json({ enabled: false, agentsMaxThreadsConflict: false, multiAgentMode: "default" }); + return new Response(null, { status: 404 }); + }) as typeof fetch; + container = testWindow.document.createElement("div"); + testWindow.document.body.appendChild(container as never); +}); + +afterEach(async () => { + clearClientResourceStoresForTests(); + if (root) await act(async () => { root!.unmount(); }); + root = null; + testWindow.close(); + for (const key of globals) { + const descriptor = previousGlobals[key]; + if (descriptor) Object.defineProperty(globalThis, key, descriptor); + else Reflect.deleteProperty(globalThis, key); + } +}); + +test("native GPT-5.6 shows Default / 1M while routed providers do not", async () => { + const { createRoot } = await import("react-dom/client"); + await act(async () => { + root = createRoot(container); + root.render(); + }); + await flush(); + + const controls = container.querySelectorAll("[data-testid=native-gpt56-context-mode]"); + expect(controls.length).toBe(1); // routed openrouter row never gets the native control + const control = () => container.querySelector("[data-testid=native-gpt56-context-mode]")!; + const button = (label: string) => [...control().querySelectorAll("button")] + .find(item => item.textContent === label)!; + expect(button("Default").getAttribute("aria-checked")).toBe("true"); + expect(button("1M").getAttribute("aria-checked")).toBe("false"); +}); + +test("native context save uses an isolated PATCH, disables both choices, and reloads truth on failure", async () => { + const { createRoot } = await import("react-dom/client"); + await act(async () => { + root = createRoot(container); + root.render(); + }); + await flush(); + + const control = () => container.querySelector("[data-testid=native-gpt56-context-mode]")!; + const button = (label: string) => [...control().querySelectorAll("button")] + .find(item => item.textContent === label)!; + + releaseSave = () => {}; // hold the first PATCH so the pending UI can be asserted + act(() => { button("1M").click(); }); + await Promise.resolve(); + expect(patches).toEqual([{ codexNativeContextMode: "1m" }]); + expect(button("Default").disabled).toBe(true); + expect(button("1M").disabled).toBe(true); + button("Default").click(); + expect(patches.length).toBe(1); // disabled controls and the single-flight ref block repeats + + const settle = releaseSave; + expect(settle).not.toBeNull(); + releaseSave = null; + settle!(); + await flush(); + expect(button("1M").getAttribute("aria-checked")).toBe("true"); + expect(container.textContent).toContain("context mode synced"); + + failSave = true; + act(() => { button("Default").click(); }); + await flush(); + expect(patches).toEqual([ + { codexNativeContextMode: "1m" }, + { codexNativeContextMode: "default" }, + ]); + expect(button("1M").getAttribute("aria-checked")).toBe("true"); + expect(button("Default").getAttribute("aria-checked")).toBe("false"); + expect(container.textContent).toContain("synthetic mode failure"); +}); diff --git a/gui/tests/models-native-group-controls.test.ts b/gui/tests/models-native-group-controls.test.ts index e54190ea93..015db5ccc8 100644 --- a/gui/tests/models-native-group-controls.test.ts +++ b/gui/tests/models-native-group-controls.test.ts @@ -33,6 +33,14 @@ test("the native provider group carries the additive entitlement diagnostic", () expect(groups[0]!.entitlement).toEqual(entitlement); }); +test("the native provider group carries the persisted GPT-5.6 context mode", () => { + const groups = buildProviderModelGroups( + [nativeRow("gpt-5.6-sol")], + [{ name: "openai", codexNativeContextMode: "1m" }], + ); + expect(groups[0]!.codexNativeContextMode).toBe("1m"); +}); + test("the native card keeps sorting first once a custom row joins it", () => { const groups = buildProviderModelGroups( [{ provider: "anthropic", id: "opus", native: false }, nativeRow("gpt-5.6-sol"), customRow("gpt-5.4")], diff --git a/src/codex/catalog.ts b/src/codex/catalog.ts index ce7eaf1615..67c3de342f 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_DAYBREAK_BLUE_MODEL, NATIVE_GPT56_CONTEXT_WINDOW, NATIVE_GPT56_MAX_INPUT_TOKENS, NATIVE_GPT56_OPT_IN_CONTEXT_WINDOW, NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS, NATIVE_OPENAI_MODELS, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, isNativeAliasCatalogEntry, isNativeOpenAiCapabilityAliasModel, nativeContextLimits, nativeOpenAiCapabilitySourceSlug, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, observedAccountBoundNativeEntries, observedAccountBoundNativeOpenAiSlugs, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs, nativeInputModalities, nativeReasoningEfforts, nativeDefaultReasoningEffort, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, type NativeContextLimits, type NativeContextLimitsInput } from "./catalog/metadata"; +export { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, CODEX_NATIVE_ALIAS_CATALOG_KIND, NATIVE_DAYBREAK_BLUE_MODEL, NATIVE_GPT56_CONTEXT_WINDOW, NATIVE_GPT56_MAX_INPUT_TOKENS, NATIVE_GPT56_OPT_IN_CONTEXT_WINDOW, NATIVE_GPT56_ONE_MILLION_CONTEXT_WINDOW, NATIVE_GPT56_ONE_MILLION_MODELS, NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS, NATIVE_OPENAI_MODELS, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, isNativeAliasCatalogEntry, isNativeOpenAiCapabilityAliasModel, nativeContextLimits, nativeOpenAiCapabilitySourceSlug, nativeOpenAiContextWindow, nativeOpenAiMaxContextWindow, nativeOpenAiMaxInputTokens, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, observedAccountBoundNativeEntries, observedAccountBoundNativeOpenAiSlugs, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs, nativeInputModalities, nativeReasoningEfforts, nativeDefaultReasoningEffort, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, type NativeContextLimits, type NativeContextLimitsInput } 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/metadata.ts b/src/codex/catalog/metadata.ts index e797f59d2b..e200614aa1 100644 --- a/src/codex/catalog/metadata.ts +++ b/src/codex/catalog/metadata.ts @@ -49,6 +49,8 @@ import { } from "./native-models"; import { cachedAvailableAccountGatedNativeModels } from "../model-entitlements"; import { MAIN_CODEX_ACCOUNT_ID } from "../main-account"; +import { NATIVE_GPT56_ONE_MILLION_CONTEXT_WINDOW } from "../native-context-mode"; +export { NATIVE_GPT56_ONE_MILLION_CONTEXT_WINDOW } from "../native-context-mode"; export { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds"; export { NATIVE_DAYBREAK_BLUE_MODEL, @@ -138,9 +140,16 @@ export const NATIVE_GPT56_CONTEXT_WINDOW = 272_000; */ export const NATIVE_GPT56_MAX_INPUT_TOKENS = 922_000; -/** User-facing 1M opt-in: the largest window the native 5.6 family may advertise. */ +/** Legacy measured/safe operating-window opt-in used by the provider context-cap controls. */ export const NATIVE_GPT56_OPT_IN_CONTEXT_WINDOW = NATIVE_GPT56_MAX_INPUT_TOKENS; +/** Exact public model allowlist for the official 1M mode. Capability aliases are excluded. */ +export const NATIVE_GPT56_ONE_MILLION_MODELS: ReadonlySet = new Set([ + "gpt-5.6-sol", + "gpt-5.6-terra", + "gpt-5.6-luna", +]); + const NATIVE_GPT56_FAMILY = new Set([ "gpt-5.6-sol", "gpt-5.6-terra", @@ -207,6 +216,8 @@ export interface NativeContextLimits { readonly modelWindows?: Readonly>; /** `providers.openai.modelAutoCompactTokenLimits` — soft, lowering-only budgets. */ readonly modelAutoCompactTokenLimits?: Readonly>; + /** Official root Codex override, valid only on the canonical forward provider. */ + readonly nativeContextMode?: "default" | "1m"; } export type NativeContextLimitsInput = NativeContextLimits | number | undefined; @@ -242,6 +253,10 @@ export function nativeContextLimits( ...(positiveInt(provider?.contextWindow) !== undefined ? { providerWindow: provider!.contextWindow } : {}), ...(Object.keys(modelWindows).length > 0 ? { modelWindows } : {}), ...(Object.keys(modelAutoCompactTokenLimits).length > 0 ? { modelAutoCompactTokenLimits } : {}), + ...(provider?.codexNativeContextMode === "1m" + && isCanonicalOpenAiForwardProvider(provider) + ? { nativeContextMode: "1m" as const } + : {}), }; } @@ -258,7 +273,7 @@ function narrowToLimits(raw: number | undefined, slug: string, input: NativeCont return overlay !== undefined && cap !== undefined ? Math.min(window, cap) : window; } const narrowed = overlay === undefined ? raw : Math.min(raw, overlay); - // 922k is the GPT-5.6 1M opt-in, not a request to shrink gpt-5.4's 1M window. + // 922k is the legacy measured GPT-5.6 preset, not a request to shrink gpt-5.4's 1M window. if (cap === NATIVE_GPT56_MAX_INPUT_TOKENS) return narrowed; return applyProviderContextCap(narrowed, cap) ?? narrowed; } @@ -271,6 +286,30 @@ export function nativeOpenAiContextWindow(slug: string, limits?: NativeContextLi return narrowToLimits(raw, slug, limits); } +/** + * Catalog ceiling for a native model. The official 1M mode raises only the exact public + * Sol/Terra/Luna allowlist and deliberately leaves their ordinary `context_window` alone. + * Existing explicit context overlays and provider caps remain lowering constraints. + */ +export function nativeOpenAiMaxContextWindow( + slug: string, + input?: NativeContextLimitsInput, +): number | undefined { + const limits = asLimits(input); + const configured = NATIVE_OPENAI_CONTEXT_OVERRIDES[slug]?.maxContextWindow; + if (configured === undefined) return undefined; + if (limits.nativeContextMode !== "1m" || !NATIVE_GPT56_ONE_MILLION_MODELS.has(slug)) { + const contextWindow = nativeOpenAiContextWindow(slug, limits); + return contextWindow === undefined ? configured : Math.min(configured, contextWindow); + } + const overlay = positiveInt(limits.modelWindows?.[slug]) ?? positiveInt(limits.providerWindow); + const cap = positiveInt(limits.cap); + let maximum = NATIVE_GPT56_ONE_MILLION_CONTEXT_WINDOW; + if (overlay !== undefined) maximum = Math.min(maximum, overlay); + if (cap !== undefined) maximum = Math.min(maximum, cap); + return maximum; +} + /** * Largest input a native slug accepts, or undefined when no separate limit is known * (the caller then falls back to the context window). diff --git a/src/codex/catalog/parsing.ts b/src/codex/catalog/parsing.ts index de195b9abc..282fb81a7e 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, isNativeOpenAiCapabilityAliasModel, nativeMultiAgentVersion, nativeOpenAiAutoCompactTokenLimit, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, type NativeContextLimitsInput } from "./metadata"; +import { NATIVE_OPENAI_CONTEXT_OVERRIDES, SUPPORTED_NATIVE_OPENAI_SLUGS, UPSTREAM_NATIVE_ENTRIES, isNativeOpenAiCapabilityAliasModel, nativeMultiAgentVersion, nativeOpenAiAutoCompactTokenLimit, nativeOpenAiContextWindow, nativeOpenAiMaxContextWindow, nativeOpenAiMaxInputTokens, type NativeContextLimitsInput } from "./metadata"; import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget"; import { trustedAccountBoundNativeCatalogSlug } from "./account-models"; import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds"; @@ -376,7 +376,7 @@ export function applyNativeOpenAiContextOverride(entry: RawEntry, limits?: Nativ entry.context_window = contextWindow; } if (typeof override.maxContextWindow === "number") { - const maxContextWindow = narrowNativeMaxContextWindow(nativeSlug, override.maxContextWindow, limits); + const maxContextWindow = nativeOpenAiMaxContextWindow(nativeSlug, limits); entry.max_context_window = maxContextWindow; } } @@ -389,10 +389,12 @@ export function applyNativeOpenAiContextOverride(entry: RawEntry, limits?: Nativ if (cappedContext !== currentContext && typeof cappedContext === "number") { entry.context_window = cappedContext; } - const currentMax = typeof entry.max_context_window === "number" ? entry.max_context_window : undefined; - const cappedMax = narrowNativeMaxContextWindow(nativeSlug, currentMax, limits); - if (cappedMax !== currentMax) { - entry.max_context_window = cappedMax; + if (typeof override?.maxContextWindow !== "number") { + const currentMax = typeof entry.max_context_window === "number" ? entry.max_context_window : undefined; + const cappedMax = narrowNativeMaxContextWindow(nativeSlug, currentMax, limits); + if (cappedMax !== currentMax) { + entry.max_context_window = cappedMax; + } } const effectiveContext = typeof entry.context_window === "number" && entry.context_window > 0 ? entry.context_window diff --git a/src/codex/inject.ts b/src/codex/inject.ts index 72be578784..2d8b3bfb79 100644 --- a/src/codex/inject.ts +++ b/src/codex/inject.ts @@ -73,6 +73,8 @@ import { transformManagedSubagentDefaults, type ManagedSubagentDefaults, } from "./subagent-defaults"; +import { transformManagedNativeContextMode } from "./native-context-mode"; +import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers"; import type { OcxConfig } from "../types"; // Ownership predicates live in `./injected-marker` so `journal.ts` can reach them @@ -450,6 +452,12 @@ export function stripRootContextWindowOverrides(content: string): string { .join("\n"); } +function configuredNativeOneMillionContext(config: OcxConfig | undefined): boolean { + const provider = config?.providers?.openai; + return provider?.codexNativeContextMode === "1m" + && isCanonicalOpenAiForwardProvider(provider); +} + function stripRootRoutedModel(content: string): string { const lines = content.split("\n"); const firstTable = lines.findIndex((l) => /^\s*\[/.test(l)); @@ -696,6 +704,13 @@ export async function injectCodexConfig( const rawContent = readFileSync(CODEX_CONFIG_PATH, "utf-8"); const activeProvider = externalCodexModelProvider(rawContent); if (activeProvider) { + if (configuredNativeOneMillionContext(config)) { + return { + success: false, + message: + `Codex config injection refused: native GPT-5.6 1M mode requires the built-in openai provider, but ${tomlString(activeProvider)} owns config.toml. No files were changed.`, + }; + } // A launcher may have journaled before the provider manager took ownership. Never let shutdown // replay that stale snapshot over externally managed config. if (!options.validateOnly) removeJournal(); @@ -735,7 +750,19 @@ export async function injectCodexConfig( `No files were changed; inspect ${CODEX_CONFIG_PATH}.`, }; } - const baselineContent = nativeDefaultsBaseline.content; + const nativeContextBaseline = transformManagedNativeContextMode( + nativeDefaultsBaseline.content, + false, + ); + if (!nativeContextBaseline.ok) { + return { + success: false, + message: + `Codex config injection refused: existing OpenCodex-managed GPT-5.6 context settings are ambiguous: ${nativeContextBaseline.error}. ` + + `No files were changed; inspect ${CODEX_CONFIG_PATH}.`, + }; + } + const baselineContent = nativeContextBaseline.content; /* * The journal write used to happen HERE, before the transforms. It now happens @@ -807,6 +834,27 @@ export async function injectCodexConfig( keptUserBaseUrl = result.keptUserBaseUrl; } + const desiredNativeOneMillionContext = configuredNativeOneMillionContext(config); + if (desiredNativeOneMillionContext && (legacyMode || keptUserBaseUrl)) { + return { + success: false, + message: + "Codex config injection refused: native GPT-5.6 1M mode requires canonical OpenAI passthrough routing with no user-owned root openai_base_url. No files were changed.", + }; + } + const managedNativeContext = transformManagedNativeContextMode( + content, + desiredNativeOneMillionContext, + ); + if (!managedNativeContext.ok) { + return { + success: false, + message: + `Codex config injection refused: GPT-5.6 1M settings could not be managed safely: ${managedNativeContext.error}. No files were changed.`, + }; + } + content = managedNativeContext.content; + const desiredSubagentDefaults = configuredManagedSubagentDefaults(config); const routingOwnershipWarning = keptUserBaseUrl && desiredSubagentDefaults @@ -1197,6 +1245,7 @@ function removeOcxSection(content: string): string { interface StripOpencodexConfigResult { content: string; managedDefaultsError: string | null; + managedNativeContextError: string | null; } /** @@ -1233,10 +1282,13 @@ function stripOpencodexConfigResult( if (hadRootOcxProvider || hadInjectedBaseUrl) out = stripRootRoutedModel(out); const managedDefaults = transformManagedSubagentDefaults(out, null); if (managedDefaults.ok) out = managedDefaults.content; + const managedNativeContext = transformManagedNativeContextMode(out, false); + if (managedNativeContext.ok) out = managedNativeContext.content; out = stripOpencodexCatalogPath(out); return { content: out.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n", managedDefaultsError: !managedDefaults.ok ? managedDefaults.error : null, + managedNativeContextError: !managedNativeContext.ok ? managedNativeContext.error : null, }; } @@ -1294,6 +1346,14 @@ export function removeCodexConfig( "The ambiguous marker and adjacent value were preserved; inspect $CODEX_HOME/config.toml before using native Codex.", }; } + if (stripped.managedNativeContextError) { + return { + success: false, + message: + `${removedMessage} Native GPT-5.6 context settings could not be safely removed: ${stripped.managedNativeContextError}. ` + + "The ambiguous marker and adjacent value were preserved; inspect $CODEX_HOME/config.toml before using native Codex.", + }; + } return { success: true, message: removedMessage, diff --git a/src/codex/native-context-mode.ts b/src/codex/native-context-mode.ts new file mode 100644 index 0000000000..5c8cc6dbff --- /dev/null +++ b/src/codex/native-context-mode.ts @@ -0,0 +1,116 @@ +/** Root Codex settings published for the explicit GPT-5.6 1M opt-in. */ +export const NATIVE_GPT56_ONE_MILLION_CONTEXT_WINDOW = 1_000_000; +export const NATIVE_GPT56_ONE_MILLION_AUTO_COMPACT_LIMIT = 900_000; +export const MANAGED_NATIVE_CONTEXT_MARKER = "# Managed by opencodex: native GPT-5.6 1M context"; + +const TARGETS = { + model_context_window: NATIVE_GPT56_ONE_MILLION_CONTEXT_WINDOW, + model_auto_compact_token_limit: NATIVE_GPT56_ONE_MILLION_AUTO_COMPACT_LIMIT, +} as const; + +type TargetKey = keyof typeof TARGETS; + +export type ManagedNativeContextTransformResult = + | { ok: true; changed: boolean; content: string } + | { ok: false; changed: false; content: string; error: string }; + +function fail(content: string, error: string): ManagedNativeContextTransformResult { + return { ok: false, changed: false, content, error }; +} + +function targetAssignment(line: string): { key: TargetKey; value: number } | null { + const match = line.match(/^\s*(model_context_window|model_auto_compact_token_limit)\s*=\s*(\d+)\s*(?:#.*)?$/); + if (!match) return null; + return { key: match[1] as TargetKey, value: Number(match[2]) }; +} + +function rootNumericValue( + lines: readonly string[], + key: TargetKey, +): { ok: true; value: number | undefined } | { ok: false; error: string } { + const firstTable = lines.findIndex(line => /^\s*\[/.test(line)); + const root = lines.slice(0, firstTable === -1 ? lines.length : firstTable); + const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const assignment = new RegExp(`^\\s*(?:${escaped}|"${escaped}"|'${escaped}')\\s*=`); + const matches = root.filter(line => assignment.test(line)); + if (matches.length > 1) return { ok: false, error: `duplicate user-owned ${key} cannot be updated safely` }; + if (matches.length === 0) return { ok: true, value: undefined }; + try { + const parsed = Bun.TOML.parse(matches[0]!) as Record; + const value = parsed[key]; + if (typeof value !== "number" || !Number.isSafeInteger(value)) { + return { ok: false, error: `user-owned ${key} must be an integer` }; + } + return { ok: true, value }; + } catch { + return { ok: false, error: `user-owned ${key} is not valid TOML` }; + } +} + +/** + * Add or remove only marker-owned root settings. User-owned compaction policy is never + * overwritten: an exact 900k value can satisfy the mode as-is, while any other value makes + * enable fail closed. Callers retain the historical root context-window stripping separately. + */ +export function transformManagedNativeContextMode( + input: string, + enabled: boolean, +): ManagedNativeContextTransformResult { + const eol = input.includes("\r\n") ? "\r\n" : "\n"; + const content = input.replace(/\r\n/g, "\n"); + const lines = content.split("\n"); + const firstTable = lines.findIndex(line => /^\s*\[/.test(line)); + const rootEnd = firstTable === -1 ? lines.length : firstTable; + const owned = new Map(); + + for (let index = 0; index < lines.length; index += 1) { + if (lines[index]!.trim() !== MANAGED_NATIVE_CONTEXT_MARKER) continue; + if (index >= rootEnd) return fail(input, "managed native context marker is not at the TOML root"); + const assignment = targetAssignment(lines[index + 1] ?? ""); + if (!assignment || index + 1 >= rootEnd) { + return fail(input, "orphaned managed native context marker cannot be updated safely"); + } + if (owned.has(assignment.key)) { + return fail(input, `duplicate managed ${assignment.key} cannot be updated safely`); + } + if (assignment.value !== TARGETS[assignment.key]) { + return fail(input, `managed ${assignment.key} has an unexpected value`); + } + owned.set(assignment.key, index); + index += 1; + } + + const removals = [...owned.values()].sort((a, b) => b - a); + for (const index of removals) lines.splice(index, 2); + + if (enabled) { + const nextFirstTable = lines.findIndex(line => /^\s*\[/.test(line)); + const userContext = rootNumericValue(lines, "model_context_window"); + if (!userContext.ok) return fail(input, userContext.error); + if (userContext.value !== undefined) { + return fail(input, "user-owned model_context_window must be normalized before enabling 1M mode"); + } + const userCompact = rootNumericValue(lines, "model_auto_compact_token_limit"); + if (!userCompact.ok) return fail(input, userCompact.error); + if (userCompact.value !== undefined + && userCompact.value !== NATIVE_GPT56_ONE_MILLION_AUTO_COMPACT_LIMIT) { + return fail(input, "user-owned model_auto_compact_token_limit conflicts with the 1M mode"); + } + + const block = [ + MANAGED_NATIVE_CONTEXT_MARKER, + `model_context_window = ${NATIVE_GPT56_ONE_MILLION_CONTEXT_WINDOW}`, + ...(userCompact.value === NATIVE_GPT56_ONE_MILLION_AUTO_COMPACT_LIMIT ? [] : [ + MANAGED_NATIVE_CONTEXT_MARKER, + `model_auto_compact_token_limit = ${NATIVE_GPT56_ONE_MILLION_AUTO_COMPACT_LIMIT}`, + ]), + ]; + let insertAt = nextFirstTable === -1 ? lines.length : nextFirstTable; + while (insertAt > 0 && lines[insertAt - 1]!.trim() === "") insertAt -= 1; + lines.splice(insertAt, 0, ...block); + } + + let output = lines.join("\n"); + if (eol === "\r\n") output = output.replace(/\n/g, "\r\n"); + return { ok: true, changed: output !== input, content: output }; +} diff --git a/src/config.ts b/src/config.ts index 11d88af91d..435d4439fd 100644 --- a/src/config.ts +++ b/src/config.ts @@ -531,6 +531,7 @@ const providerConfigSchema = z.object({ retryOn429: retryOn429PolicySchema.optional(), transientRetryOn5xx: transientRetryOn5xxPolicySchema.optional(), codexAccountMode: z.enum(["pool", "direct"]).optional(), + codexNativeContextMode: z.enum(["default", "1m"]).optional(), // Validated rather than passed through: this schema ends in `.passthrough()`, so an // undeclared key survives verbatim. A misspelled `codexToolMode` therefore used to be // accepted, persisted, and then silently resolved to the `code_mode_only` default — the @@ -1258,6 +1259,20 @@ const configSchema = z.object({ }); } } + if (Object.hasOwn(provider, "codexNativeContextMode") && provider.codexNativeContextMode !== undefined) { + const canonicalOpenAiShape = name === "openai" + && provider.adapter === "openai-responses" + && (provider as { authMode?: unknown }).authMode === "forward" + && typeof provider.baseUrl === "string" + && provider.baseUrl.replace(/\/+$/, "") === "https://chatgpt.com/backend-api/codex"; + if (!canonicalOpenAiShape) { + ctx.addIssue({ + code: "custom", + path: ["providers", redactSecretString(name), "codexNativeContextMode"], + message: "codexNativeContextMode is valid only on the canonical built-in openai provider", + }); + } + } } if (!hasOwnProvider(config.providers, config.defaultProvider)) { ctx.addIssue({ diff --git a/src/server/auth-cors.ts b/src/server/auth-cors.ts index 0174794c6a..cd89009468 100644 --- a/src/server/auth-cors.ts +++ b/src/server/auth-cors.ts @@ -26,6 +26,7 @@ import { redactSecretString } from "../lib/redact"; import { effectiveGoogleMode, getProviderRegistryEntry, providerCodexAccountMode, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../providers/registry"; import { providerConfigSeed } from "../providers/derive"; import type { OcxConfig, OcxProviderConfig } from "../types"; +import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers"; import { openRouterRoutingConfigError } from "../providers/openrouter-routing"; import { modelAutoCompactTokenLimitsConfigError } from "../providers/auto-compact-budget"; import { vercelGatewayRoutingConfigError } from "../providers/vercel-gateway-routing"; @@ -565,6 +566,12 @@ export function providerManagementConfigError(name: unknown, provider: unknown): } if (seed) seed.codexAccountMode = raw.codexAccountMode; const canonicalCandidate = { ...raw }; + if (Object.hasOwn(raw, "codexNativeContextMode") + && raw.codexNativeContextMode !== "default" + && raw.codexNativeContextMode !== "1m") { + return "provider openai codexNativeContextMode must be default or 1m"; + } + delete canonicalCandidate.codexNativeContextMode; delete canonicalCandidate.responsesSnapshotRepair; // modelCosts is a user-owned display overlay, not part of the canonical // forward seed; it is validated separately below (providerModelCostsConfigError). @@ -591,8 +598,8 @@ export function providerManagementConfigError(name: unknown, provider: unknown): if (!canonical) { return `provider ${name} must equal the canonical built-in provider seed`; } - } else if (Object.hasOwn(raw, "codexAccountMode")) { - return `provider ${name} must not include codexAccountMode`; + } else if (Object.hasOwn(raw, "codexAccountMode") || Object.hasOwn(raw, "codexNativeContextMode")) { + return `provider ${name} must not include Codex-native settings`; } const typed = provider as unknown as OcxProviderConfig; const baseUrlError = providerBaseUrlConfigError(typed.baseUrl); @@ -780,6 +787,9 @@ export function safeConfigDTO(config: OcxConfig): unknown { if (typeof registryNote === "string" && registryNote.trim()) dto.note = registryNote; const codexAccountMode = providerCodexAccountMode(name, provider); if (codexAccountMode) dto.codexAccountMode = codexAccountMode; + if (name === "openai" && isCanonicalOpenAiForwardProvider(provider)) { + dto.codexNativeContextMode = provider.codexNativeContextMode ?? "default"; + } providers[name] = dto; } return { diff --git a/src/server/management/context.ts b/src/server/management/context.ts index 6703477556..d16d7859c5 100644 --- a/src/server/management/context.ts +++ b/src/server/management/context.ts @@ -12,6 +12,7 @@ import type { removeDesktop3pStandardPivot, writeDesktop3pConfig } from "../../c import type { probeClaudeDesktopPolicy } from "../../claude/desktop-policy"; import type { RuntimePortState } from "../../config/process-state"; import type { CatalogDisposition, ConvergeCodex } from "../../codex/convergence-types"; +import type { syncModelsToCodex } from "../../codex/sync"; import type { performCodexRestart, readCodexAppServerState, @@ -23,6 +24,8 @@ export interface ManagementApiDeps { toggleCodexMultiAgentV2?: (enabled: boolean) => void; toggleDefaultModeRequestUserInput?: (enabled: boolean) => void; createManagementConvergeCodex?: (config: Readonly) => ConvergeCodex; + /** Full native sync seam for atomic settings that span config.json, catalog, and config.toml. */ + syncModelsToCodex?: typeof syncModelsToCodex; /** Test-only destination for best-effort Claude agent-definition sync. */ claudeAgentConfigDir?: string; /** Startup-health seam keeps route tests from launching platform probes. */ diff --git a/src/server/management/provider-routes.ts b/src/server/management/provider-routes.ts index d71397de8f..84ae17fb00 100644 --- a/src/server/management/provider-routes.ts +++ b/src/server/management/provider-routes.ts @@ -473,6 +473,9 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise { + config.providers.openai = { ...config.providers.openai!, codexNativeContextMode: mode }; + save(config); + }); + reconcileLiveStateStores(); + const { clearModelCache } = await import("../../codex/model-cache"); + clearModelCache("openai"); + + const sync = deps.syncModelsToCodex ?? (await import("../../codex/sync")).syncModelsToCodex; + const runtimePort = deps.readRuntimePort?.(process.pid)?.port ?? config.port; + let result: Awaited>; + try { + result = await sync(runtimePort, config, null); + } catch { + result = { + status: "applied", + ok: false, + added: 0, + catalogPath: null, + catalogExists: false, + catalogWritten: false, + cacheSynced: false, + message: "Codex sync failed before the GPT-5.6 context mode could be applied.", + }; + } + if (result.status === "applied" && result.ok) { + const { attachStaleAppServerHint } = await import("../../codex/app-server-processes"); + return jsonResponse({ + success: true, + name: "openai", + codexNativeContextMode: mode, + sync: attachStaleAppServerHint(result), + }); + } + + withConfigMutationLockSync(() => { + const current = { ...config.providers.openai! }; + if (previousPresent) current.codexNativeContextMode = previousMode; + else delete current.codexNativeContextMode; + config.providers.openai = current; + save(config); + }); + reconcileLiveStateStores(); + clearModelCache("openai"); + let rollbackOk = false; + try { + const rollback = await sync(runtimePort, config, null); + rollbackOk = rollback.status === "applied" && rollback.ok; + } catch { + rollbackOk = false; + } + const status = result.status === "refused" || result.status === "skipped" || result.status === "catalog-only" + ? 409 + : 500; + return jsonResponse({ + error: result.message, + codexNativeContextMode: previousMode ?? "default", + rolledBack: true, + rollbackSyncOk: rollbackOk, + }, status); + } + // codexAccountMode keeps its dedicated side-effect path (quota cache clear, thread map // clear, pool prime) and is mutually exclusive with every other patch field. if (hasMode) { diff --git a/src/types.ts b/src/types.ts index 71171957aa..021d747a27 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,7 +16,12 @@ export { toolChoiceToolPredicate, } from "./types/tools"; -export type { UpstreamHttpVersion, ReasoningSummaryDelivery, CodexAccountMode } from "./types/wire"; +export type { + UpstreamHttpVersion, + ReasoningSummaryDelivery, + CodexAccountMode, + CodexNativeContextMode, +} from "./types/wire"; export { UPSTREAM_HTTP_VERSION_VALUES, REASONING_SUMMARY_DELIVERY_VALUES, diff --git a/src/types/provider.ts b/src/types/provider.ts index 5584238df6..b9071dbe28 100644 --- a/src/types/provider.ts +++ b/src/types/provider.ts @@ -1,4 +1,9 @@ -import type { UpstreamHttpVersion, ReasoningSummaryDelivery, CodexAccountMode } from "./wire"; +import type { + UpstreamHttpVersion, + ReasoningSummaryDelivery, + CodexAccountMode, + CodexNativeContextMode, +} from "./wire"; /** * Per-provider proactive-refresh policy. The guardian only ever touches a provider whose EFFECTIVE @@ -290,6 +295,8 @@ export interface OcxProviderConfig { * failover engine; "direct" pins the caller's main Codex login and never touches pool state. */ codexAccountMode?: CodexAccountMode; + /** Official Codex context mode for the canonical native GPT-5.6 family. */ + codexNativeContextMode?: CodexNativeContextMode; apiKey?: string; /** * Key-auth header style for Anthropic-compatible providers. diff --git a/src/types/wire.ts b/src/types/wire.ts index 0800bca428..a5f58c7a1d 100644 --- a/src/types/wire.ts +++ b/src/types/wire.ts @@ -25,6 +25,9 @@ export type ReasoningSummaryDelivery = typeof REASONING_SUMMARY_DELIVERY_VALUES[ /** Trusted runtime ownership for Codex-account credentials. Never persisted per provider. */ export type CodexAccountMode = "direct" | "pool"; +/** User-selected context policy for canonical Codex-login GPT-5.6 models. */ +export type CodexNativeContextMode = "default" | "1m"; + export const OPENAI_PROVIDER_TIER_VERSION = 2 as const; /** diff --git a/structure/08_openai-provider-tiers.md b/structure/08_openai-provider-tiers.md index ab15088fb4..a4825b9960 100644 --- a/structure/08_openai-provider-tiers.md +++ b/structure/08_openai-provider-tiers.md @@ -210,15 +210,20 @@ preserving a stale one would block every later migration. account, and an unavailable discovery temporarily hides the model rather than guessing. - The two GPT-5.6 surfaces advertise different windows on purpose. API rows use 1,050,000 context with 922,000 max input. Codex-login rows default to the live catalog 272,000 - (auto-compact 244,800) and only rise to 922,000 / 829,800 when the user turns the 1M - switch on. + (auto-compact 244,800). The existing provider context-cap workflow can raise that operating + window to the measured 922,000 / 829,800 mode. Separately, the Models card exposes the official + `Default | 1M` Codex opt-in for exactly Sol/Terra/Luna. That mode keeps the catalog + `context_window` at its normal default, raises only `max_context_window` to 1,000,000, and + marker-manages root `model_context_window = 1000000` plus + `model_auto_compact_token_limit = 900000`. The ceiling is the same on both — probing a real Codex-login account accepted 921,508 input tokens and refused 922,013 with `context_length_exceeded` on Sol, Terra and Luna alike, matching the 922,000 the API surface already declared. A Codex-login `context_window` is a spending budget, not a label: Codex fills `context_window * effective_context_window_percent` (95% by default, codex-rs `turn_context.rs`). Advertising 1,050,000 there spent 997,500 and - blew past the ceiling. The 922,000 opt-in yields a 875,900-token budget and keeps ~46k of + blew past the measured ceiling. The older 922,000 operating mode yields an 875,900-token budget + and keeps ~46k of headroom. Evidence: `devlog/_plan/260817_native_gpt56_1m_context/001_measurement_evidence.md` and `014_final_922k_with_margin.md`. - `*-pro` selected ids rewrite to the base wire id with `reasoning.mode: "pro"`; request logs, diff --git a/tests/codex-inject-integration.test.ts b/tests/codex-inject-integration.test.ts index 9f7157eb5b..f22dc00d02 100644 --- a/tests/codex-inject-integration.test.ts +++ b/tests/codex-inject-integration.test.ts @@ -47,6 +47,22 @@ function runRestore(codexHome: string, ocxHome: string): { stdout: string; statu return { stdout: result.stdout?.trim() ?? "", status: result.status ?? 1 }; } +function nativeContextConfig(mode: "default" | "1m"): string { + return JSON.stringify({ + port: 10100, + defaultProvider: "openai", + providers: { + openai: { + adapter: "openai-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", + authMode: "forward", + codexAccountMode: "pool", + codexNativeContextMode: mode, + }, + }, + }); +} + describe("injectCodexConfig integration (Design B)", () => { let codexHome: string; let ocxHome: string; @@ -130,6 +146,68 @@ describe("injectCodexConfig integration (Design B)", () => { expect(second).toBe(first); }); + test("native GPT-5.6 1M mode injects exact root settings and Default removes only owned settings", () => { + writeFileSync(join(codexHome, "config.toml"), [ + 'model = "gpt-5.6-sol"', + 'model_reasoning_effort = "high"', + "", + "[features]", + "fast_mode = true", + "", + "[mcp_servers.example]", + 'command = "example"', + "", + "[agents]", + "max_threads = 4", + "", + "[plugins.example]", + "enabled = true", + "", + "[model_providers.user-owned]", + 'name = "User owned"', + 'base_url = "https://example.invalid/v1"', + "", + ].join("\n"), "utf8"); + + expect(runInject(codexHome, ocxHome, nativeContextConfig("1m")).status).toBe(0); + const enabled = readFileSync(join(codexHome, "config.toml"), "utf8"); + expect(enabled.match(/^model_context_window = 1000000$/gm)?.length).toBe(1); + expect(enabled.match(/^model_auto_compact_token_limit = 900000$/gm)?.length).toBe(1); + expect(() => Bun.TOML.parse(enabled)).not.toThrow(); + + expect(runInject(codexHome, ocxHome, nativeContextConfig("1m")).status).toBe(0); + expect(readFileSync(join(codexHome, "config.toml"), "utf8")).toBe(enabled); + + expect(runInject(codexHome, ocxHome, nativeContextConfig("default")).status).toBe(0); + const disabled = readFileSync(join(codexHome, "config.toml"), "utf8"); + expect(disabled).not.toContain("model_context_window = 1000000"); + expect(disabled).not.toContain("model_auto_compact_token_limit = 900000"); + expect(disabled).toContain('model = "gpt-5.6-sol"'); + expect(disabled).toContain('model_reasoning_effort = "high"'); + expect(disabled).toContain("fast_mode = true"); + expect(disabled).toContain("[mcp_servers.example]"); + expect(disabled).toContain("[agents]"); + expect(disabled).toContain("[plugins.example]"); + expect(disabled).toContain("[model_providers.user-owned]"); + }); + + test("a conflicting user compaction limit refuses 1M mode without changing config", () => { + const original = [ + 'model = "gpt-5.6-sol"', + "model_auto_compact_token_limit = 120000", + "", + "[features]", + "fast_mode = true", + "", + ].join("\n"); + writeFileSync(join(codexHome, "config.toml"), original, "utf8"); + + const result = runInject(codexHome, ocxHome, nativeContextConfig("1m")); + expect(result.status).toBe(0); + expect(JSON.parse(result.stdout).success).toBe(false); + expect(readFileSync(join(codexHome, "config.toml"), "utf8")).toBe(original); + }); + test.each([ 'model_catalog_json = "custom-catalog.json" # user catalog', '"model_catalog_json" = "custom-catalog.json" # user catalog', diff --git a/tests/codex-inject.test.ts b/tests/codex-inject.test.ts index 1d9b86a00f..578859a40e 100644 --- a/tests/codex-inject.test.ts +++ b/tests/codex-inject.test.ts @@ -15,8 +15,68 @@ import { MANAGED_AGENTS_TABLE_MARKER, MANAGED_SUBAGENT_DEFAULT_MARKER, } from "../src/codex/subagent-defaults"; +import { + MANAGED_NATIVE_CONTEXT_MARKER, + transformManagedNativeContextMode, +} from "../src/codex/native-context-mode"; describe("Codex config injection", () => { + test("managed GPT-5.6 1M settings are exact, idempotent, and reversible", () => { + const original = [ + 'model = "gpt-5.6-sol"', + "model_reasoning_effort = \"high\"", + "", + "[features]", + "fast_mode = true", + "", + "[mcp_servers.example]", + 'command = "example"', + "", + ].join("\n"); + const enabled = transformManagedNativeContextMode(original, true); + expect(enabled.ok).toBe(true); + if (!enabled.ok) return; + expect(enabled.content.match(/^model_context_window = 1000000$/gm)?.length).toBe(1); + expect(enabled.content.match(/^model_auto_compact_token_limit = 900000$/gm)?.length).toBe(1); + expect(enabled.content.match(new RegExp(MANAGED_NATIVE_CONTEXT_MARKER, "g"))?.length).toBe(2); + expect(enabled.content).toContain("[mcp_servers.example]"); + + const repeated = transformManagedNativeContextMode(enabled.content, true); + expect(repeated).toEqual({ ok: true, changed: false, content: enabled.content }); + + const disabled = transformManagedNativeContextMode(enabled.content, false); + expect(disabled.ok).toBe(true); + if (!disabled.ok) return; + expect(disabled.content).not.toContain(MANAGED_NATIVE_CONTEXT_MARKER); + expect(disabled.content).not.toContain("model_context_window = 1000000"); + expect(disabled.content).not.toContain("model_auto_compact_token_limit = 900000"); + expect(disabled.content).toContain('model = "gpt-5.6-sol"'); + expect(disabled.content).toContain("[mcp_servers.example]"); + }); + + test("1M settings preserve an exact user compaction value and reject a conflicting one", () => { + const exact = transformManagedNativeContextMode( + "\"model_auto_compact_token_limit\" = 900_000\n[features]\nfast_mode = true\n", + true, + ); + expect(exact.ok).toBe(true); + if (exact.ok) { + expect(exact.content).toContain('"model_auto_compact_token_limit" = 900_000'); + expect(exact.content.match(new RegExp(MANAGED_NATIVE_CONTEXT_MARKER, "g"))?.length).toBe(1); + const removed = transformManagedNativeContextMode(exact.content, false); + expect(removed.ok).toBe(true); + if (removed.ok) expect(removed.content).toContain('"model_auto_compact_token_limit" = 900_000'); + } + + const conflict = "model_auto_compact_token_limit = 120000\n[features]\nfast_mode = true\n"; + expect(transformManagedNativeContextMode(conflict, true)).toEqual({ + ok: false, + changed: false, + content: conflict, + error: "user-owned model_auto_compact_token_limit conflicts with the 1M mode", + }); + }); + test("omits provider-level Responses WebSocket support by default", () => { const block = buildProviderTableBlock(10100); diff --git a/tests/management-provider-validation.test.ts b/tests/management-provider-validation.test.ts index 68e4c6010d..d00736d711 100644 --- a/tests/management-provider-validation.test.ts +++ b/tests/management-provider-validation.test.ts @@ -2914,6 +2914,97 @@ describe("provider management validation", () => { }); }); + test("native GPT-5.6 context mode persists only after full sync and rolls back on failure", async () => { + if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true }); + mkdirSync(TEST_DIR, { recursive: true }); + process.env.OPENCODEX_HOME = TEST_DIR; + const liveConfig: OcxConfig = { + port: 10100, + hostname: "127.0.0.1", + defaultProvider: "openai", + openaiProviderTierVersion: 2, + providers: { + openai: { ...canonicalDirect, codexNativeContextMode: "default" }, + extra: { adapter: "openai-chat", baseUrl: "https://extra.example.test/v1" }, + }, + }; + saveConfig(liveConfig); + const seenModes: string[] = []; + let failNext = false; + const deps = { + createManagementConvergeCodex: catalogConvergenceFactory(), + syncModelsToCodex: async (_port: number | undefined, current: OcxConfig) => { + seenModes.push(current.providers.openai?.codexNativeContextMode ?? "default"); + if (failNext) { + failNext = false; + return { + status: "applied", + ok: false, + added: 0, + catalogPath: null, + catalogExists: false, + catalogWritten: false, + cacheSynced: false, + message: "synthetic sync failure", + } as const; + } + return { + status: "applied", + ok: true, + added: 0, + catalogPath: null, + catalogExists: true, + catalogWritten: true, + cacheSynced: true, + message: "synced", + } as const; + }, + }; + const patch = async (name: string, mode: unknown) => { + const req = new Request(`http://127.0.0.1/api/providers?name=${name}`, { + method: "PATCH", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ codexNativeContextMode: mode }), + }); + return handleManagementAPI(req, new URL(req.url), liveConfig, deps); + }; + + expect((await patch("extra", "1m"))?.status).toBe(400); + expect((await patch("openai", "invalid"))?.status).toBe(400); + const enabled = await patch("openai", "1m"); + expect(enabled?.status).toBe(200); + expect(await enabled?.json()).toMatchObject({ + success: true, + codexNativeContextMode: "1m", + }); + expect(liveConfig.providers.openai?.codexNativeContextMode).toBe("1m"); + expect(loadConfig().providers.openai?.codexNativeContextMode).toBe("1m"); + + const listedRequest = new Request("http://127.0.0.1/api/providers"); + const listedResponse = await handleManagementAPI( + listedRequest, + new URL(listedRequest.url), + liveConfig, + deps, + ); + const listed = await listedResponse!.json() as Array>; + expect(listed.find(row => row.name === "openai")?.codexNativeContextMode).toBe("1m"); + expect(listed.find(row => row.name === "extra")).not.toHaveProperty("codexNativeContextMode"); + + failNext = true; + const failed = await patch("openai", "default"); + expect(failed?.status).toBe(500); + expect(await failed?.json()).toMatchObject({ + error: "synthetic sync failure", + codexNativeContextMode: "1m", + rolledBack: true, + rollbackSyncOk: true, + }); + expect(liveConfig.providers.openai?.codexNativeContextMode).toBe("1m"); + expect(loadConfig().providers.openai?.codexNativeContextMode).toBe("1m"); + expect(seenModes).toEqual(["1m", "default", "1m"]); + }); + test("xAI Responses opt-in reports mixed state and atomically normalizes both model adapters", async () => { if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true }); mkdirSync(TEST_DIR, { recursive: true }); diff --git a/tests/native-model-toggle.test.ts b/tests/native-model-toggle.test.ts index 37ec607585..1db572e4c5 100644 --- a/tests/native-model-toggle.test.ts +++ b/tests/native-model-toggle.test.ts @@ -175,6 +175,85 @@ describe("native GPT model toggles (bare slugs in disabledModels)", () => { expect(entry.auto_compact_token_limit).toBe(120_000); }); + test("official 1M mode raises only the exact native GPT-5.6 catalog maximum", () => { + const limits = nativeContextLimits({ + providers: { + openai: { + adapter: "openai-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", + authMode: "forward", + codexNativeContextMode: "1m", + }, + }, + } as never); + for (const slug of ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]) { + const entry: Record = { + slug, + context_window: 272_000, + max_context_window: 922_000, + effective_context_window_percent: 95, + }; + applyNativeOpenAiContextOverride(entry as never, limits); + expect(entry).toMatchObject({ + context_window: 272_000, + max_context_window: 1_000_000, + effective_context_window_percent: 95, + }); + } + + for (const slug of ["gpt-5.5", "gpt-daybreak-blue-latest"]) { + const entry: Record = { + slug, + context_window: 272_000, + max_context_window: 922_000, + effective_context_window_percent: 95, + }; + applyNativeOpenAiContextOverride(entry as never, limits); + expect(entry.max_context_window).toBe(272_000); + } + + const gpt54 = { + slug: "gpt-5.4", + context_window: 1_000_000, + max_context_window: 1_000_000, + effective_context_window_percent: 95, + }; + const gpt54Default = { ...gpt54 }; + applyNativeOpenAiContextOverride(gpt54Default as never, nativeContextLimits({} as never)); + applyNativeOpenAiContextOverride(gpt54 as never, limits); + expect(gpt54).toEqual(gpt54Default); + + const routed = { + slug: "openrouter/gpt-5.6-sol", + context_window: 128_000, + max_context_window: 128_000, + effective_context_window_percent: 95, + }; + applyNativeOpenAiContextOverride(routed as never, limits); + expect(routed).toEqual({ + slug: "openrouter/gpt-5.6-sol", + context_window: 128_000, + max_context_window: 128_000, + effective_context_window_percent: 95, + }); + }); + + test("1M mode is ignored on a non-canonical provider named openai", () => { + const limits = nativeContextLimits({ + providers: { + openai: { + adapter: "openai-responses", + baseUrl: "https://example.invalid/v1", + authMode: "forward", + codexNativeContextMode: "1m", + }, + }, + } as never); + const entry: Record = { slug: "gpt-5.6-sol", context_window: 272_000 }; + applyNativeOpenAiContextOverride(entry as never, limits); + expect(entry.max_context_window).toBe(272_000); + }); + test("the on-disk catalog preserves a lower retained native compaction threshold", () => { const retained = { slug: "gpt-5.4-mini", @@ -231,7 +310,7 @@ describe("native GPT model toggles (bare slugs in disabledModels)", () => { expect(gpt55?.maxInputTokens).toBeUndefined(); }); - test("the native 1M switch raises the Codex 272k default up to the measured ceiling", () => { + test("the native provider cap raises the Codex 272k default up to the measured ceiling", () => { const raised = nativeModelRows({ providerContextCaps: { openai: 922_000 } }); expect(raised.find(r => r.slug === "gpt-5.6-sol")).toMatchObject({ contextWindow: 922_000, @@ -794,9 +873,9 @@ describe("#2574 a stale on-disk row is what a subagent reads", () => { * missing is any assertion that the WRITTEN row matches what the resolver would produce. */ test("a raised cap opts the family into the wider window, and the row follows", () => { - // The 1M opt-in is expressed as a raised providerContextCaps.openai, not a per-model - // window. With the default cap the family stays at 272k; raising it past the opt-in - // threshold is what makes 922k the correct width. + // The legacy measured-window path is expressed as a raised providerContextCaps.openai, + // not a per-model window. With the default cap the family stays at 272k; raising it past + // the measured threshold is what makes 922k the correct width. const optedIn = nativeContextLimits({ providerContextCaps: { openai: 1_050_000 } } as never); expect(nativeOpenAiContextWindow("gpt-5.6-sol", optedIn)).toBe(NATIVE_GPT56_OPT_IN_CONTEXT_WINDOW); diff --git a/tests/subagent-context-staleness.test.ts b/tests/subagent-context-staleness.test.ts index bd8351e5f8..b6589d63df 100644 --- a/tests/subagent-context-staleness.test.ts +++ b/tests/subagent-context-staleness.test.ts @@ -40,8 +40,15 @@ function writeConfig(): void { writeFileSync(join(home, "config.json"), JSON.stringify({ port: 10100, defaultProvider: "openai", - providers: { openai: { adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex", authMode: "forward" } }, - // The 1M opt-in is a raised provider cap, not a per-model window. + providers: { openai: { + adapter: "openai-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", + authMode: "forward", + // The current-config re-derive must carry the new maximum even when the file predates it. + codexNativeContextMode: "1m", + } }, + // Keep the pre-existing measured 922k operating-window path active too: the official 1M + // mode changes the catalog maximum, while this cap continues to choose the ordinary window. providerContextCaps: { openai: 1_050_000 }, })); } @@ -79,4 +86,3 @@ describe("#2574 a subagent does not inherit a stale catalog width", () => { expect(Math.floor(NATIVE_GPT56_OPT_IN_CONTEXT_WINDOW * 0.95)).toBe(875_900); }); }); -