diff --git a/devlog/_plan/260912_codex_spark_retirement/000_plan.md b/devlog/_plan/260912_codex_spark_retirement/000_plan.md new file mode 100644 index 0000000000..2bb5726958 --- /dev/null +++ b/devlog/_plan/260912_codex_spark_retirement/000_plan.md @@ -0,0 +1,28 @@ +# Spark retirement preparation + +Prepare an unmerged draft PR for removing GPT-5.3-Codex-Spark after its announced retirement. Tibo's September 11 announcement says next week, without an exact cutoff. Deployment timing stays with the maintainer; this branch contains the future removal and must not be merged now. + +## Loop contract +- Archetype: satisfy-spec; trigger: user requested complete Spark removal PR including usage quota and UI. +- Goal: one cohesive retirement patch covering membership, quota/route semantics and settings/UI. +- Class: C3 with C4 care for quota isolation, persisted evidence and management settings. +- Non-goals: no merge/auto-merge/deploy/live config changes; no historical usage deletion; no Meta Muse Spark or vendor snapshot retirement. +- Verifier: exact-head Cross-platform CI and independent source review. Local product tests/typecheck/build/install NOT RUN by user instruction. CI workflow runs the relevant Bun tests and gates. UI evidence must be a genuine rendered screenshot, not a waiver invented by the agent. +- Stop: correct reviewed Draft PR, complete head-specific CI with no failures or pending required jobs, remains unmerged. +- Artifact: this unit, 001_source.md, 010_implementation.md, 020_review.md, 030_delivery.md. +- Outcomes: DONE means draft delivery, not retirement deployed. Source-date uncertainty must stay explicit. No invented budget. +- Delegation: inherited model/effort only; independent read-only scout/auditor and three disjoint implementation lanes. Main coordinates, reviews and delivers. Native architect role is not exposed; ordinary inherited read-only source review is recorded as such, not claimed to be that registered role. +- Escalation: investigate actual correctness conflicts; reclaim remaining work only after inspecting failed agent state. No shared role configuration edits. + +## Single work-phase +One P-A-B-C-D cycle for a single user-visible outcome. Backend, API and UI are subtasks in dependency order; no partial retirement can be considered complete alone. This avoids the previous task's retrospective phase attestations. + +## Decisions +D1: remove Spark native membership AND add the exact slug to the existing retired set. Preserve unknown/new account-native observations. +D2: remove Spark-only quota collection, routing scope and show/hide preference. A narrow tombstone for old persisted Spark quota rows is allowed; such evidence must neither display nor affect shared/Reserve capacity. Do not delete generic custom windows. +D3: reject or ignore retired Spark response quota evidence so it cannot be reclassified as shared account quota. +D4: preserve shared/Reserve affinity, account selection, probe leases and cooldown isolation. Replace old Spark positive-control tests with existing Reserve/shared coverage or real retirement negatives, not blind slug substitutions. +D5: remove Spark compatibility branches where exclusive to Codex Spark; preserve general Responses Lite, namespace scrubbing and other-provider behavior. +D6: remove GUI toggle/state/i18n/CSS and settings API/type/schema field together. Old config documents remain loadable; a legacy-only settings update must not revive the switch. +D7: preserve historical costs/benchmark data/vendor slugs and maintained general fixtures; document each remaining active-source Spark match. + diff --git a/devlog/_plan/260912_codex_spark_retirement/001_source.md b/devlog/_plan/260912_codex_spark_retirement/001_source.md new file mode 100644 index 0000000000..fc7749d0e5 --- /dev/null +++ b/devlog/_plan/260912_codex_spark_retirement/001_source.md @@ -0,0 +1,11 @@ +# Retirement announcement + +Source: https://x.com/thsottiaux/status/2098300998968357218 + +Author: Tibo (@thsottiaux). Posted 2026-09-11 at 06:41:57 UTC (15:41 KST). + +Verified with Aside's signed-in twitter.search and by opening the exact post in Aside repl. The post states: “Next week we’ll be retiring GPT-5.3-Codex-Spark.” + +The post gives no exact retirement date or time. The calendar week following the post is September 14–20, 2026; that range is an interpretation, not an announced cutoff. No time-triggered production switch or automatic merge is authorized. + +User direction: prepare the complete removal PR, leave the current service alone, no merge. New subagents inherit the main session; no explicit model override. The prior no-local-suite and no-verify-push instructions continue. diff --git a/devlog/_plan/260912_codex_spark_retirement/010_implementation.md b/devlog/_plan/260912_codex_spark_retirement/010_implementation.md new file mode 100644 index 0000000000..0dada0992c --- /dev/null +++ b/devlog/_plan/260912_codex_spark_retirement/010_implementation.md @@ -0,0 +1,43 @@ +# Implementation map + +## Lane A — native catalog and compatibility +MODIFY src/codex/catalog/{native-models,metadata,parsing,sync,effort}.ts: +- NATIVE_OPENAI_MODELS and DOCUMENTED_NATIVE_OPENAI_ADDITIONS lose gpt-5.3-codex-spark. +- RETIRED_NATIVE_OPENAI_MODELS gains the exact slug; remove 100k Spark context override and exclusive tier/lite exceptions. +- Verify full-shaped bare, cache, persisted account-selector-v1 Spark rows cannot reappear in assembly/restore; unrelated future native still admitted. +MODIFY src/adapters/openai-responses.ts and src/responses/hosted-tool-policy.ts to remove exclusive Spark request/tool/lite flags. Retain shared response normalization. Adjust misleading src/server/responses-self-named-namespace-scrub.ts comment only, not generic scrub logic. +Tests owned by A: catalog/metadata/effort/visibility/convergence/restore/selector tests; tests/claude-integration, tests/clients, tests/responses, tests/routing. Do not change quota/routing core tests owned by B. Keep adequate positive native controls after deleting Spark-specific behavior. + +## Lane B — quota, routing, settings +MODIFY src/codex/{quota,routing,auth-context,auth-api}.ts, src/providers/quota.ts, src/server/management/config-routes.ts, src/config.ts, src/types/config.ts. +Before: CodexQuotaScope = shared|spark|reserve, Spark WHAM additional rate limits and response header windows parsed, preference showCodexSparkQuota gates UI projection. +After: shared|reserve scope only; no Spark collection/active affinity/probe family. Ignore Spark-specific response evidence; filter legacy Spark windows before presence/capacity/DTO use. Remove setting read/write/rollback/type/schema contract while accepting old config without dropping unrelated fields. Generic customWindows and Reserve detection remain. +Trace scope generation -> maps/cache persistence -> auth error labels -> main claim recovery -> provider quota DTO. Tests owned by B: codex-{quota*,routing,pool*,auth-api,auth-context,cooldown*}, main-quota-evidence-validation, reserve-quota-scope, codex-spark-visibility; tests/server; tests/providers/provider-quota. Replace obsolete Spark tests with retired-evidence negatives and Reserve isolation. +If deleting a test file adjust scripts/test-layout/layout.json and tests/fixtures/test-layout-expected.json; prefer repurposing existing files. + +## Lane C — UI and documentation +MODIFY gui/src/components/CodexAccountPool.tsx, codex-account-pool-main-card.tsx: delete sparkVisible/sparkBusy/onToggleSpark and showCodexSparkQuota wiring; preserve other actions, feedback and busy state. +MODIFY gui/src/styles.css to remove only Spark styles and revise actual action layout comments; remove codexAuth.sparkQuota* keys across all locales. Update gui/tests, notably main-account-hard-lock-setting. +MODIFY current docs/, docs-site (all locales) and structure/ current ownership docs. Describe no Spark quota setting or native offering in this branch; retain historical decisions and benchmark records. Structure docs must describe shared/Reserve and retired-window suppression accurately. Main owns devlog unit only. +Capture real UI evidence using an already available environment or remote-built artifact, with synthetic fixture data. No local product build/tests/install and no mutation of the live service. + +## Verification scenarios +1. Seed full-shaped retired native rows bare and account-qualified, run assembly/sync twice: absent both times; keep a future unknown native positive control. +2. Feed WHAM codex_bengalfox and Spark labels with shared/Reserve limits: Spark custom windows absent, ordinary limits unmodified. +3. Feed old persisted custom windows and retired response headers: no resurrection and no shared cooldown contamination. +4. Old showCodexSparkQuota config loads safely; API never advertises or accepts re-enabling it. +5. Shared and Reserve independent quota/cooldown/affinity tests still prove independence. +6. Native catalog tests retain reasoning/context/ordering positive coverage using live native models, not deleted assertions. +7. DOM/rendered account panel has no Spark switch/bars and surviving controls still work; authentic screenshot in PR. +8. Hosted CI is complete for the final PR head; no pending/failed jobs masked by shell pipelines or pass-count scripts. + +## Source review refinements (before Build) +- Quota tombstone lives before ingestion/hydration and presence/observation, not only DTO display. quota.ts hydration currently accepts stored customWindows (626); partial updates preserve them (338). Sanitize both and direct-provider DTO paths. +- Retired Spark model-derived response headers and reset-derived 429 outcomes are ignored before shared quota health/recovery. Preserve true Retry-After account throttle and credential/transport failures. +- Keep generic WebSocket family normalization in src/server/responses/codex-ws-metadata.ts. It is not Spark-specific merely because fixtures mention codex_bengalfox. +- Generic custom-window reset observation in src/quota/window-mapping.ts survives; filter retired evidence before it sees the snapshot. +- Config schema passthrough makes an old setting inert. No destructive config migration or removal of historical observer/usage stores. +- Ownership correction: B exclusively owns tests/routing/, tests/responses/responses-compaction-routing.test.ts, ws-upstream.test.ts and responses-account-label.test.ts. A owns the remaining Responses tests. B also owns src/codex/subagent-model-fallback.ts and Responses core edits only if tracing requires them; report such expansion. This is explicit rather than overlapping broad directory ownership. +- Known retirement guard is local catalog/evidence policy (tier E1, runtime executing surface). Manually typed model ids may still pass generic routing; this work removes advertised native membership and model-specific support, not a universal request denylist. No stronger security enforcement is claimed. +- UI proof: download the exact-head dashboard-preview CI artifact (build-commit and build-gui-tree stamps) and render against synthetic fixture API responses. No local product build/test/install or live service modification. + diff --git a/devlog/_plan/260912_codex_spark_retirement/020_review.md b/devlog/_plan/260912_codex_spark_retirement/020_review.md new file mode 100644 index 0000000000..b6dd92fe4c --- /dev/null +++ b/devlog/_plan/260912_codex_spark_retirement/020_review.md @@ -0,0 +1,7 @@ +# Plan audit + +Source reviewer: inherited native agent Beauvoir. D1-D7 aligned; quota tombstones must precede storage/presence/reset observation, Spark 429 needs exclusion from shared model-derived cooldowns, and broad test ownership required disambiguation. Accepted into 010. + +Independent A auditor: inherited native agent Hegel. Final result: “PASS — the amended plan is sufficient for the A gate. No required corrections remain.” Its final line was “VERDICT: PASS”. The review specifically confirmed ingestion/hydration tombstones, reset-derived quota versus Retry-After, generic WS/reset preservation, legacy config compatibility, disjoint test scope and CI-artifact screenshot proof. + +Both reviews were read-only; no local product checks ran. Registered architect role is unavailable in this host schema; these are ordinary independent source reviews, with shared model-family inheritance requested by the user. diff --git a/devlog/_plan/260912_codex_spark_retirement/030_delivery.md b/devlog/_plan/260912_codex_spark_retirement/030_delivery.md new file mode 100644 index 0000000000..43440bceec --- /dev/null +++ b/devlog/_plan/260912_codex_spark_retirement/030_delivery.md @@ -0,0 +1,34 @@ +# Verification and delivery record + +Status: Draft PR #4334 prepared for the announced retirement. NO MERGE, auto-merge, release, deployment or live service/config change is authorized. + +## Source +Primary source verified through Aside: https://x.com/thsottiaux/status/2098300998968357218, Tibo, 2026-09-11 06:41:57 UTC. “Next week” is the timing given; no exact cutoff was announced. + +## Implemented behavior +Spark is absent from native membership, observations and restore outputs. Exact retired rows cannot re-enter from a full-shaped bare/cache/account-selector row; future unknown native observations still work. Spark-specific adapter/Lite/tool compatibility branches are removed while generic mechanisms remain. + +Dedicated Spark quota collection, routing scope and preference are removed. Old Spark windows are tombstoned before presence/ingestion/hydration/reset observations and Direct/Pool projections. Spark-only quota is unknown (null), not spare shared capacity. Retired-model response headers and reset-derived 429 evidence do not change shared quota state; actual Retry-After/account credential/transport handling and shared/Reserve isolation remain. + +The old settings field is inert in persisted passthrough config and cannot reactivate the removed control. UI state, API calls, toggle, CSS and translations are removed. Historical costs, usage, benchmark rows, generic unknown families and other providers including Muse Spark are preserved. + +## Independent reviews +All native agents were dispatched with model/effort overrides omitted, inheriting the main session as explicitly requested. +- A plan review: Hegel PASS after source-review refinements. +- Catalog/compatibility post-build review: Kant PASS, then corrected two missed test assumptions after CI failures. +- Quota/security/UI contract review: Zeno PASS on 8398f2abb7..0a0fd2a225, no concrete blockers. + +## CI repair +First full remote workflow run 34671771774 at 0a0fd2a225 found test fixture failures: +1. Two-pass retirement regression serialized complete duplicated instructions/catalog/cache rows to captured stdout. The failure wrapper discarded spawn error/signal. Full snapshots now go to the fixture directory with the same complete comparisons, plus preserved subprocess diagnostics. Capture overflow is the supported static diagnosis; the original log did not retain enough subprocess evidence to claim it proven. +2. Astra without long-window opt-in emits context/max 272000 and compaction 244800. The test mistakenly asserted the raw pin's 872000 ceiling. + +No production change was needed for those failures. Re-run on the repaired final head is required; prior-head green or pending jobs are not substitutes. + +## UI evidence +Source dashboard artifact: run 34671771774, artifact 10291125984, commit 0a0fd2a225614fb0c80ad3e125dfa7243879c87b, GUI tree 08717a046c2b2550c8ed08499511e5edc6aa2801. +The built artifact is served unchanged with synthetic fixture API/bootstrap data on loopback; no local product build and no live proxy are used. Parent visually inspected desktop and mobile captures: no Spark switch or quota rows, surviving main/pool quota and actions visible. Fixture request ledger supplies interaction evidence. Screenshot commits do not change the GUI tree; artifact provenance must match that tree on final delivery. + +## Limits and completion gate +Local product tests/typecheck/build/install: NOT RUN by user instruction. Only static source/diff inspection and rendered remote-built fixture UI ran locally. Exact final-head hosted CI and unmerged Draft PR state are checked by .tmp/spark-retirement/check_remote.py; failures/pending checks return nonzero. The final chat report carries the final SHA/run result because writing it back into this file would create another untested head. + diff --git a/docs-site/src/content/docs/fr/guides/codex-app-models.md b/docs-site/src/content/docs/fr/guides/codex-app-models.md index 10dce00320..28fa0ee8a3 100644 --- a/docs-site/src/content/docs/fr/guides/codex-app-models.md +++ b/docs-site/src/content/docs/fr/guides/codex-app-models.md @@ -130,7 +130,7 @@ service OpenAI. ## Couverture stable actuelle des modèles -L'ensemble natif de secours comprend `gpt-5.5`, `gpt-5.3-codex-spark` et GPT-5.6 +L'ensemble natif de secours comprend `gpt-5.5` et GPT-5.6 Sol/Terra/Luna. Pour la famille GPT-5.5, opencodex conserve les entrées dynamiques plus riches du catalogue Codex installé et ne synthétise qu'une entrée manquante. L'instantané amont fourni n'est employé que pour GPT-5.6, auquel il apporte l'identité et les métadonnées réelles de chaque modèle plutôt qu'une diff --git a/docs-site/src/content/docs/fr/guides/providers.md b/docs-site/src/content/docs/fr/guides/providers.md index fd696660b7..5594f6e436 100644 --- a/docs-site/src/content/docs/fr/guides/providers.md +++ b/docs-site/src/content/docs/fr/guides/providers.md @@ -196,9 +196,9 @@ de récupération strict, déterminé par `Retry-After`, par les en-têtes `rese plafond prévu — ou par un bref délai de repli par défaut. Les comptes soumis à un délai `Retry-After` explicite ne sont pas sondés avant son expiration. Les délais calculés à partir des informations de réinitialisation peuvent bénéficier d'une autorisation de sondage cadencée, afin de détecter la reprise sans submerger le -fournisseur. Pour les modèles natifs, ces délais préservent également les groupes de quotas indépendants connus : -`gpt-5.3-codex-spark` n'empêche pas le même compte d'essayer le quota partagé de GPT-5.6 Terra/Luna, tandis -que les modèles de ce groupe partagé continuent de se protéger mutuellement. Les délais `Retry-After` explicites +fournisseur. Pour les modèles natifs, ces délais séparent le quota partagé (dont GPT-5.6 Terra/Luna) +de `gpt-reserve`. Les modèles du groupe partagé continuent de se protéger mutuellement ; +une requête ordinaire réussie ne lève pas le délai de Reserve. Les délais `Retry-After` explicites et les délais par défaut s'appliquent toujours à l'ensemble du compte. **Affinité de session.** L'affinité entre le fil Codex et le compte est locale au processus — uniquement en mémoire et diff --git a/docs-site/src/content/docs/fr/reference/cli/lifecycle.md b/docs-site/src/content/docs/fr/reference/cli/lifecycle.md index d87aa4895a..2624d6388e 100644 --- a/docs-site/src/content/docs/fr/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/fr/reference/cli/lifecycle.md @@ -40,6 +40,11 @@ Vérifie de manière idempotente qu’un proxy d’arrière-plan est actif, puis Rétablit le fonctionnement natif de Codex **sans arrêter** le proxy : les lignes de configuration injectées et les entrées routées du catalogue sont supprimées, de sorte qu’une invocation simple de `codex` utilise de nouveau Codex directement. `eject` est un alias de `restore`. +Le catalogue restauré exclut les modèles natifs retirés, dont `gpt-5.3-codex-spark`, +que leurs identifiants soient nus ou qualifiés par un compte de confiance. Cette règle +s’applique avec ou sans sauvegarde ; la sauvegarde originale et les anciens choix de modèles +enregistrés par l’utilisateur sont conservés. + Ajoutez `back` à l’une ou l’autre forme pour rediriger une invocation simple de `codex` vers un proxy déjà actif, sans modifier le cycle de vie du proxy : ```bash 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 f99fbc5fd8..8219dd2281 100644 --- a/docs-site/src/content/docs/guides/codex-app-models.md +++ b/docs-site/src/content/docs/guides/codex-app-models.md @@ -188,7 +188,7 @@ including OpenAI service-tier metadata. ## Current stable model coverage -The native fallback set includes `gpt-5.5`, `gpt-5.3-codex-spark`, and GPT-5.6 Sol/Terra/Luna. +The native fallback set includes `gpt-5.5` and GPT-5.6 Sol/Terra/Luna. For the GPT-5.5 family, opencodex preserves the installed Codex catalog's richer live entries and only synthesizes a missing entry. The bundled upstream snapshot is used only for GPT-5.6, where it supplies the real per-model identity and diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index 892dc045d9..b6aada80a6 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -313,9 +313,9 @@ Terminal refresh failures mark the account as needing reauthentication instead o `Retry-After`, quota `reset` headers (capped), or a short default backoff. Accounts on an explicit `Retry-After` cooldown are not probed early; reset-derived cooldowns may receive a paced probe lease so recovery can be detected without flooding the provider. Reset-derived native-model cooldowns -also preserve known independent quota groups: `gpt-5.3-codex-spark` does not prevent the same account -from trying the shared GPT-5.6 Terra/Luna quota, while models in that shared group still protect one -another. Explicit `Retry-After` and default cooldowns always remain account-wide. +keep shared native quota (including GPT-5.6 Terra/Luna) separate from `gpt-reserve`. +Models in the shared group still protect one another; an ordinary success cannot clear a Reserve cooldown. +Explicit `Retry-After` and default cooldowns always remain account-wide. **Session affinity.** Codex thread→account affinity is process-local (in-memory only; not persisted across proxy restarts). On credential failures (`401` / `403`) the account is quarantined for diff --git a/docs-site/src/content/docs/ja/guides/codex-app-models.md b/docs-site/src/content/docs/ja/guides/codex-app-models.md index 13f9b6517b..fa3bc09fcb 100644 --- a/docs-site/src/content/docs/ja/guides/codex-app-models.md +++ b/docs-site/src/content/docs/ja/guides/codex-app-models.md @@ -48,7 +48,7 @@ visibility = "list" ## 現在の安定したモデルの範囲 -ネイティブ フォールバック セットには、`gpt-5.5`、`gpt-5.3-codex-spark`、および GPT-5.6 Sol/Terra/Luna が含まれます。 GPT-5.5 ファミリの場合、opencodex は、インストールされている Codex カタログの豊富なライブ エントリを保存し、欠落しているエントリのみを合成します。バンドルされたアップストリーム スナップショットは GPT-5.6 でのみ使用され、古いテンプレートの近似値の代わりに実際のモデルごとの ID とメタデータが提供されます。 +ネイティブ フォールバック セットには、`gpt-5.5` および GPT-5.6 Sol/Terra/Luna が含まれます。 GPT-5.5 ファミリの場合、opencodex は、インストールされている Codex カタログの豊富なライブ エントリを保存し、欠落しているエントリのみを合成します。バンドルされたアップストリーム スナップショットは GPT-5.6 でのみ使用され、古いテンプレートの近似値の代わりに実際のモデルごとの ID とメタデータが提供されます。 |ルート |ピッカー ID とカタログのメタデータ | | --- | --- | diff --git a/docs-site/src/content/docs/ja/reference/cli/lifecycle.md b/docs-site/src/content/docs/ja/reference/cli/lifecycle.md index 9f8612a2c6..39cec05483 100644 --- a/docs-site/src/content/docs/ja/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ja/reference/cli/lifecycle.md @@ -40,6 +40,10 @@ ocx start --port 8080 プロキシを停止せずに**ネイティブ Codex を復元します。挿入された設定行とルーティングされたカタログ エントリを削除し、プレーンな `codex` が再びネイティブに動作するようにします。 `eject` は `restore` の別名です。 +復元後のカタログでは、`gpt-5.3-codex-spark` など提供終了したネイティブモデルの bare ID と +信頼済みのアカウント修飾エントリを除外します。バックアップの有無にかかわらず適用され、 +元のバックアップとユーザーが保存した過去のモデル選択設定は保持します。 + プロキシのライフサイクルを変更せずに、既に実行されているプロキシでプレーン `codex` を再指定するには、`back` をどちらかのスペルに渡します。 ```bash 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 23dfd131c6..0a40e9eacf 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 @@ -97,8 +97,7 @@ visibility = "list" ## 현재 안정 모델 범위 -네이티브 폴백 목록에는 `gpt-5.5`, -`gpt-5.3-codex-spark`, 그리고 GPT-5.6 Sol/Terra/Luna가 들어 있습니다. GPT-5.5 계열은 설치된 +네이티브 폴백 목록에는 `gpt-5.5`와 GPT-5.6 Sol/Terra/Luna가 들어 있습니다. GPT-5.5 계열은 설치된 Codex 카탈로그의 더 풍부한 실시간 항목을 보존하고, 빠진 항목만 합성합니다. 번들 업스트림 스냅샷은 GPT-5.6에만 사용합니다. 오래된 템플릿으로 근사하지 않고 모델별 실제 식별 정보와 메타데이터를 제공하기 위해서입니다. diff --git a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md index 7f9d741a37..f8b5dc6579 100644 --- a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md @@ -65,6 +65,10 @@ stop/start 대체 동작 없이 안전하게 실패합니다. 소유권을 확 프록시를 중지하지 않고 기본 Codex를 **복원**합니다. 주입된 설정 줄과 라우팅된 카탈로그 항목을 제거하므로 일반 `codex`가 다시 네이티브로 동작합니다. `eject`는 `restore`의 별칭입니다. +복원된 카탈로그에서는 `gpt-5.3-codex-spark` 등 지원이 종료된 네이티브 모델의 bare id와 +신뢰된 계정 한정 항목을 제외합니다. 카탈로그 백업 유무와 관계없이 적용되며, 원본 백업과 +사용자가 저장한 과거 모델 선택 설정은 보존합니다. + 저장된 저널에 해당 파일의 주입 상태 해시가 없으면, 변경된 설정 파일을 덮어쓰는 대신 복원 실패를 보고합니다. 현재 파일과 저널은 검토용으로 보존됩니다. [해시 없는 저널의 복구 규칙](/guides/codex-integration/#recovery-without-injection-hashes)을 참고하세요. diff --git a/docs-site/src/content/docs/ko/reference/management-api.md b/docs-site/src/content/docs/ko/reference/management-api.md index b806e10d17..b2c7951e89 100644 --- a/docs-site/src/content/docs/ko/reference/management-api.md +++ b/docs-site/src/content/docs/ko/reference/management-api.md @@ -270,7 +270,7 @@ OpenAI도 같은 규칙을 따르며, 스위치를 켠다고 별도의 922k 모 조회, 불완전하거나 소진된 usage, 신원이 바뀐 계정, 더 최근의 quota 실패로는 복구하지 않습니다. 오래된 main usage 응답은 더 최근에 반영한 관측을 덮어쓰지 않습니다. credential 갱신을 거쳤다면 해당 인증에서 이어진 갱신인지 확인되어야 하며, 외부에서 교체된 credential은 같은 계정이어도 -복구 근거가 되지 않습니다. 명시적 `Retry-After`, Spark/Reserve 쿨다운, pause·pin·선택 +복구 근거가 되지 않습니다. 명시적 `Retry-After`, Reserve 쿨다운, pause·pin·선택 설정도 보존됩니다. `already_redeemed`와 저장된 결과 재생은 새 reset을 증명하지 않습니다. `reset` 또는 `already_redeemed`가 확인된 뒤 usage 조회가 실패하거나 바쁘더라도 소비 응답은 diff --git a/docs-site/src/content/docs/reference/cli/lifecycle.md b/docs-site/src/content/docs/reference/cli/lifecycle.md index bc0c03c57f..ea1f0295e9 100644 --- a/docs-site/src/content/docs/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/reference/cli/lifecycle.md @@ -77,6 +77,10 @@ Idempotently ensure a background proxy is running, then sync its live model cata Restore native Codex **without** stopping the proxy — strips the injected config lines and routed catalog entries so plain `codex` works natively again. `eject` is an alias of `restore`. +Restored catalog output excludes retired native models, including `gpt-5.3-codex-spark`, +whether stored as bare ids or trusted account-qualified rows. This applies with or without +a catalog backup; the original backup and historical user-selected configuration are preserved. + Restoration reports failure instead of replacing changed configuration files when a saved journal lacks the corresponding injection hashes. The current files and journal remain available for review; see [recovery without injection hashes](/guides/codex-integration/#recovery-without-injection-hashes). diff --git a/docs-site/src/content/docs/reference/management-api.md b/docs-site/src/content/docs/reference/management-api.md index 7fee801715..47509393f4 100644 --- a/docs-site/src/content/docs/reference/management-api.md +++ b/docs-site/src/content/docs/reference/management-api.md @@ -488,7 +488,7 @@ started before the reset, incomplete or exhausted usage, a changed account, and quota failure do not qualify. Older main-account usage responses cannot replace a newer published observation. If usage needs credential refresh, recovery requires that refresh's confirmed lineage; an externally replaced credential does not qualify merely because it -belongs to the same account. Explicit `Retry-After`, Spark/Reserve cooldowns, pause +belongs to the same account. Explicit `Retry-After`, Reserve cooldowns, pause settings, pins and the selected account are preserved. `already_redeemed` and durable replay do not prove a new reset and do not gain this recovery behavior. diff --git a/docs-site/src/content/docs/ru/guides/codex-app-models.md b/docs-site/src/content/docs/ru/guides/codex-app-models.md index 5f8c458184..3445dc08c0 100644 --- a/docs-site/src/content/docs/ru/guides/codex-app-models.md +++ b/docs-site/src/content/docs/ru/guides/codex-app-models.md @@ -77,8 +77,7 @@ visibility = "list" ## Текущее покрытие стабильных моделей -Нативный fallback-набор включает `gpt-5.5`, -`gpt-5.3-codex-spark` и GPT-5.6 Sol/Terra/Luna. Для семейства GPT-5.5 opencodex сохраняет +Нативный fallback-набор включает `gpt-5.5` и GPT-5.6 Sol/Terra/Luna. Для семейства GPT-5.5 opencodex сохраняет более богатые живые записи установленного каталога Codex и синтезирует только отсутствующую запись. Bundled upstream-snapshot используется только для GPT-5.6, где он даёт настоящую per-model identity и метаданные вместо приближения по старому шаблону. diff --git a/docs-site/src/content/docs/ru/reference/cli/lifecycle.md b/docs-site/src/content/docs/ru/reference/cli/lifecycle.md index e72d956bcd..cbe330bf71 100644 --- a/docs-site/src/content/docs/ru/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ru/reference/cli/lifecycle.md @@ -59,6 +59,10 @@ ocx start --port 8080 маршрутизируемые записи каталога, чтобы обычный `codex` снова работал нативно. `eject` — alias команды `restore`. +Восстановленный каталог исключает снятые нативные модели, включая `gpt-5.3-codex-spark`, +как с bare ID, так и с доверенным префиксом аккаунта. Это действует с резервной копией и без неё; +исходная копия и сохранённые пользователем прежние настройки выбора моделей остаются нетронутыми. + Передайте `back`, чтобы любая из этих форм снова направила обычный `codex` на уже запущенный прокси, не меняя жизненный цикл самого прокси: diff --git a/docs-site/src/content/docs/tr/guides/codex-app-models.md b/docs-site/src/content/docs/tr/guides/codex-app-models.md index d26343e9eb..2743b075ed 100644 --- a/docs-site/src/content/docs/tr/guides/codex-app-models.md +++ b/docs-site/src/content/docs/tr/guides/codex-app-models.md @@ -144,8 +144,7 @@ yerel yetenekleri kaldırır. ## Mevcut kararlı model kapsamı -Yerel geri dönüş kümesi `gpt-5.5`, -`gpt-5.3-codex-spark` ve GPT-5.6 Sol/Terra/Luna modellerini içerir. GPT-5.5 +Yerel geri dönüş kümesi `gpt-5.5` ve GPT-5.6 Sol/Terra/Luna modellerini içerir. GPT-5.5 ailesi için opencodex, kurulu Codex kataloğunun daha zengin canlı girdilerini korur ve yalnızca eksik bir girdiyi sentezler. Paketlenmiş yukarı akış anlık görüntüsü yalnızca eski şablon yaklaşımı yerine gerçek model başına kimliği ve diff --git a/docs-site/src/content/docs/tr/guides/providers.md b/docs-site/src/content/docs/tr/guides/providers.md index e4bd33bb6f..003d309293 100644 --- a/docs-site/src/content/docs/tr/guides/providers.md +++ b/docs-site/src/content/docs/tr/guides/providers.md @@ -220,11 +220,10 @@ varsayılan geri çekilmeden sabit bir soğuma süresi ayarlar. Açık bir `Retry-After` soğuma süresindeki hesaplar erken araştırılmaz; sıfırlamadan türetilen soğuma süreleri, sağlayıcıyı boğmadan kurtarmanın algılanabilmesi için tempolu bir araştırma kiralama süresi alabilir. Sıfırlamadan türetilen yerel -model soğuma süreleri bilinen bağımsız kota gruplarını da korur: -`gpt-5.3-codex-spark`, aynı hesabın paylaşılan GPT-5.6 Terra/Luna kotasını -denemesini engellemezken, bu paylaşılan gruptaki modeller yine de birbirini -korur. Açık `Retry-After` ve varsayılan soğuma süreleri her zaman hesap -genelinde kalır. +model soğuma süreleri, paylaşılan yerel kotayı (GPT-5.6 Terra/Luna dahil) +`gpt-reserve` kotasından ayrı tutar. Paylaşılan gruptaki modeller birbirini +korur; sıradan bir isteğin başarısı Reserve soğuma süresini kaldırmaz. +Açık `Retry-After` ve varsayılan soğuma süreleri her zaman hesap genelinde kalır. **Oturum bağlılığı.** Codex iş parçacığı→hesap bağlılığı işleme özeldir (yalnızca bellek içindedir; proxy yeniden başlatmalarında kalıcı değildir). diff --git a/docs-site/src/content/docs/tr/reference/cli/lifecycle.md b/docs-site/src/content/docs/tr/reference/cli/lifecycle.md index 0aa50bfebf..33d13c543b 100644 --- a/docs-site/src/content/docs/tr/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/tr/reference/cli/lifecycle.md @@ -65,6 +65,10 @@ Proxy'yi **durdurmadan** yerel Codex'i geri yükleyin — enjekte edilen yapılandırma satırlarını ve yönlendirilen katalog girdilerini kaldırır, böylece düz `codex` tekrar yerel olarak çalışır. `eject`, `restore`'un bir takma adıdır. +Geri yüklenen katalog, `gpt-5.3-codex-spark` dahil kullanımdan kaldırılan yerel modellerin +yalın kimliklerini ve güvenilir hesap önekli girdilerini dışarıda bırakır. Katalog yedeği olsa da +olmasa da bu kural geçerlidir; özgün yedek ve kullanıcının geçmiş model seçimleri korunur. + Proxy yaşam döngüsünü değiştirmeden düz `codex`'i zaten çalışan bir proxy'ye yeniden yönlendirmek için her iki yazıma da `back` iletin: 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 d47a080ac5..d48a5457dd 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 @@ -61,7 +61,7 @@ visibility = "list" ## 当前稳定模型覆盖 -原生回退集合包含 `gpt-5.5`、`gpt-5.3-codex-spark` 以及 GPT-5.6 Sol/Terra/Luna。对于 GPT-5.5 家族,opencodex 会保留已安装 Codex 目录中更丰富的实时条目,只在缺失时才合成条目。内置的上游快照只用于 GPT-5.6,因为它提供的是每个模型真实的身份和元数据,而不是较旧模板的近似版本。 +原生回退集合包含 `gpt-5.5` 以及 GPT-5.6 Sol/Terra/Luna。对于 GPT-5.5 家族,opencodex 会保留已安装 Codex 目录中更丰富的实时条目,只在缺失时才合成条目。内置的上游快照只用于 GPT-5.6,因为它提供的是每个模型真实的身份和元数据,而不是较旧模板的近似版本。 | 路由 | 选择器 id 与目录元数据 | | --- | --- | diff --git a/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md b/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md index 541485dbf4..1b1d98e178 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md @@ -40,6 +40,9 @@ ocx start --port 8080 在**不停止代理**的情况下恢复原生 Codex——移除注入的配置行和路由后的目录条目,让普通 `codex` 重新以原生方式工作。`eject` 是 `restore` 的别名。 +恢复后的目录会排除已退役的原生模型,包括 `gpt-5.3-codex-spark` 的裸 ID 和可信的账户限定条目。 +无论是否存在目录备份,此规则均适用;原始备份和用户保存的历史模型选择配置保持不变。 + 在任一命令后附加 `back`,即可在不改变代理生命周期的前提下,把普通 `codex` 重新指向一个已经在运行的代理: ```bash diff --git a/docs-site/src/content/docs/zh-tw/guides/codex-app-models.md b/docs-site/src/content/docs/zh-tw/guides/codex-app-models.md index efe0dfb6ee..6c2d7eb9bd 100644 --- a/docs-site/src/content/docs/zh-tw/guides/codex-app-models.md +++ b/docs-site/src/content/docs/zh-tw/guides/codex-app-models.md @@ -80,8 +80,7 @@ visibility = "list" ## 目前穩定模型涵蓋範圍 -原生回退列表包含 `gpt-5.5`、 -`gpt-5.3-codex-spark` 以及 GPT-5.6 Sol/Terra/Luna。對於 GPT-5.5 系列,opencodex 會 +原生回退列表包含 `gpt-5.5` 以及 GPT-5.6 Sol/Terra/Luna。對於 GPT-5.5 系列,opencodex 會 保留已安裝 Codex 目錄中資訊更完整的即時條目,僅在條目缺失時才合成。內建的上游快照只用於 GPT-5.6,以便提供每個模型真實的身份和後設資料,而不是套用舊模板近似生成。 diff --git a/docs-site/src/content/docs/zh-tw/guides/providers.md b/docs-site/src/content/docs/zh-tw/guides/providers.md index 72d2ae214a..5bb74c0bb0 100644 --- a/docs-site/src/content/docs/zh-tw/guides/providers.md +++ b/docs-site/src/content/docs/zh-tw/guides/providers.md @@ -170,8 +170,8 @@ opencodex 協調 token refresh 與 Codex pool 路由,避免並行請求競爭 **Cooldown(Codex pool)。** 上游 `429`/quota response 會依 `Retry-After`、quota `reset` header (有上限)或短預設 backoff 設定 hard cooldown。明確 `Retry-After` cooldown 中的帳號不會被提前 probe; reset 衍生 cooldown 可能取得節流後的 probe lease,在不淹沒 provider 的情況下偵測恢復。由 reset 衍生的 -native-model cooldown 也會保留已知獨立 quota group:`gpt-5.3-codex-spark` 不會阻止同一帳號嘗試共享的 -GPT-5.6 Terra/Luna quota,而共享群組內的模型仍會互相保護。明確 `Retry-After` 與預設 cooldown 始終為 +native-model cooldown 會將共享原生 quota(含 GPT-5.6 Terra/Luna)與 `gpt-reserve` 分開。 +共享群組內的模型仍會互相保護;一般請求成功不會清除 Reserve cooldown。明確 `Retry-After` 與預設 cooldown 始終為 account-wide。 **Session affinity。** Codex thread→account affinity 只存在目前 process 記憶體,不會跨 proxy restart diff --git a/docs-site/src/content/docs/zh-tw/reference/cli/lifecycle.md b/docs-site/src/content/docs/zh-tw/reference/cli/lifecycle.md index 0eaf6c75f9..727de9bb54 100644 --- a/docs-site/src/content/docs/zh-tw/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/zh-tw/reference/cli/lifecycle.md @@ -39,6 +39,9 @@ ocx start --port 8080 在不停止代理的情況下還原原生 Codex——剝除注入的設定行與路由目錄項目,使普通 `codex` 再次以原生方式運作。`eject` 是 `restore` 的別名。 +還原後的目錄會排除已退役的原生模型,包括 `gpt-5.3-codex-spark` 的裸 ID 與可信的帳號限定項目。 +無論是否有目錄備份,此規則皆適用;原始備份與使用者儲存的歷史模型選擇設定保持不變。 + 對任一拼法傳入 `back` 可在不變更代理生命週期的情況下,將普通 `codex` 重新指向已在執行的代理: ```bash diff --git a/docs/pr-assets/codex-spark-retirement/README.md b/docs/pr-assets/codex-spark-retirement/README.md new file mode 100644 index 0000000000..1e28f9a8d0 --- /dev/null +++ b/docs/pr-assets/codex-spark-retirement/README.md @@ -0,0 +1,21 @@ +# Codex Spark retirement UI evidence + +Rendered from Cross-platform CI run **34671771774**, artifact **10291125984**: + +- Build commit: `0a0fd2a225614fb0c80ad3e125dfa7243879c87b` +- GUI tree: `08717a046c2b2550c8ed08499511e5edc6aa2801` +- Dashboard route: `/#codex-set`, with synthetic local API data only. +- Desktop CSS viewport: 1440 × 1100; mobile: 390 × 844. PNGs retain native capture resolution. + +Each screenshot was visually inspected before copying; none was retouched. + +| Image | Observed state | +| --- | --- | +| [desktop.png](desktop.png) | Main and backup account cards, remaining controls, no Spark switch. | +| [desktop-refreshed.png](desktop-refreshed.png) | Refresh completed with success feedback. | +| [desktop-paused.png](desktop-paused.png) | Bulk pause affected only the exhausted backup; Resume remains available. | +| [mobile.png](mobile.png) | Responsive account panel after successful resume, without horizontal document overflow. | + +The browser console was empty and fixture requests returned HTTP 200. This is UI evidence, +not live-account quota or entitlement validation. On mobile, long account/limit text wraps. +No live proxy, product build or local test suite was used. The fixture server was stopped after capture. diff --git a/docs/pr-assets/codex-spark-retirement/desktop-paused.png b/docs/pr-assets/codex-spark-retirement/desktop-paused.png new file mode 100644 index 0000000000..f6fd82c8f4 Binary files /dev/null and b/docs/pr-assets/codex-spark-retirement/desktop-paused.png differ diff --git a/docs/pr-assets/codex-spark-retirement/desktop-refreshed.png b/docs/pr-assets/codex-spark-retirement/desktop-refreshed.png new file mode 100644 index 0000000000..7bceaae550 Binary files /dev/null and b/docs/pr-assets/codex-spark-retirement/desktop-refreshed.png differ diff --git a/docs/pr-assets/codex-spark-retirement/desktop.png b/docs/pr-assets/codex-spark-retirement/desktop.png new file mode 100644 index 0000000000..f70ca316a3 Binary files /dev/null and b/docs/pr-assets/codex-spark-retirement/desktop.png differ diff --git a/docs/pr-assets/codex-spark-retirement/mobile.png b/docs/pr-assets/codex-spark-retirement/mobile.png new file mode 100644 index 0000000000..c94107ea8d Binary files /dev/null and b/docs/pr-assets/codex-spark-retirement/mobile.png differ diff --git a/gui/src/components/CodexAccountPool.tsx b/gui/src/components/CodexAccountPool.tsx index 56a38fbd1b..c7e006be6a 100644 --- a/gui/src/components/CodexAccountPool.tsx +++ b/gui/src/components/CodexAccountPool.tsx @@ -127,10 +127,6 @@ export default function CodexAccountPool({ apiBase, accountModeState = null, ban setQuotaBusyScope(null); setQuotaFeedback(null); } - // undefined until /api/settings answers: the switch must not render a guessed position and - // then visibly correct itself a moment later. - const [sparkVisible, setSparkVisible] = useState(undefined); - const [sparkBusy, setSparkBusy] = useState(false); const [resetPopup, setResetPopup] = useState(null); const [resetConfirm, setResetConfirm] = useState(false); const [redeeming, setRedeeming] = useState(false); @@ -363,12 +359,10 @@ export default function CodexAccountPool({ apiBase, accountModeState = null, ban fetch(`${apiBase}/api/settings`, { signal: read.signal }) .then(response => { if (!response.ok) throw new Error("read"); return response.json(); }) .then((payload: { - showCodexSparkQuota?: unknown; codexQuotaAutoRefresh?: QuotaAutoRefreshSettings; } | null) => { if (abort.signal.aborted) return; if (!payload) throw new Error("read"); - if (typeof payload.showCodexSparkQuota === "boolean") setSparkVisible(payload.showCodexSparkQuota); if (quotaAutoRefreshMutationRevisionRef.current === mutationRevision) { setQuotaState({ apiBase, revision: quotaReadRevision, settings: readQuotaActivationSettings(payload), error: false }); setQuotaBusyScope(null); @@ -391,34 +385,6 @@ export default function CodexAccountPool({ apiBase, accountModeState = null, ban }; }, [apiBase, quotaReadRevision]); - const toggleSpark = async () => { - if (sparkBusy || sparkVisible === undefined) return; - const requested = !sparkVisible; - setSparkBusy(true); - // Optimistic, then reconciled against what the server confirms — the same shape the account - // picker toggle uses, so a rejected write visibly snaps back instead of lying. - setSparkVisible(requested); - try { - const response = await fetch(`${apiBase}/api/settings`, { - method: "PUT", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ showCodexSparkQuota: requested }), - }); - if (!response.ok) throw new Error("save"); - const payload = await response.json() as { showCodexSparkQuota?: unknown }; - const confirmed = typeof payload.showCodexSparkQuota === "boolean" ? payload.showCodexSparkQuota : requested; - setSparkVisible(confirmed); - showActionFeedback(t(confirmed ? "codexAuth.sparkQuotaShown" : "codexAuth.sparkQuotaHidden"), "ok"); - await load(true); - } catch { - setSparkVisible(!requested); - showActionFeedback(t("codexAuth.sparkQuotaFailed"), "err"); - } finally { - setSparkBusy(false); - } - }; - - const pauseExhausted = async () => { const result = await controller.pauseExhaustedAccounts(); if (!result.ok && result.reason === "busy") return; @@ -485,17 +451,13 @@ export default function CodexAccountPool({ apiBase, accountModeState = null, ban pauseBusy={pauseBusy} onRefresh={() => { void refreshQuotas(); }} onPauseExhausted={() => { void pauseExhausted(); }} - sparkVisible={sparkVisible} - sparkBusy={sparkBusy} - onToggleSpark={() => { void toggleSpark(); }} /> {banner} {/* - Relocated out of the page head: with two accounts the head carried a title, a - status line, a toggle and two buttons on one row, and the actions sat above the - cards they act on. They belong next to the accounts. + The standalone page places pause/refresh below the mode banner, next to the + accounts they act on. The page head retains its title and feedback region. */} {!embedded && ( void; onPauseExhausted: () => void; - /** undefined until the preference has loaded, so the switch never renders a guessed state. */ - sparkVisible?: boolean; - sparkBusy?: boolean; - onToggleSpark?: () => void; }) { return (
{actionFeedback ?? ""} - {sparkVisible !== undefined && onToggleSpark && ( - - {t("codexAuth.sparkQuota")} - - - )} - {/* - The two account-scoped actions used to live here, beside the page title. On the - standalone page that put four controls plus a heading on one row, and the actions - sat far above the account cards they act on. They render in - CodexAccountPoolActions below instead. The embedded surface keeps them inline, - because there is no title row there to crowd. - */} + {/* The standalone pause/refresh row sits next to the account cards. Embedded + surfaces keep those actions beside feedback because there is no page title. */} {embedded && ( = { "codexAuth.openaiPresetUnavailable": "OpenAI-Anbieter-Preset ist nicht verfügbar.", "codexAuth.openProviders": "Anbieter öffnen", "codexAuth.add": "Hinzufügen", - "codexAuth.sparkQuota": "Codex-Spark-Kontingent", - "codexAuth.sparkQuotaHint": "Zeigt das GPT-5.3-Codex-Spark-Wochenfenster auf Kontokarten. Standardmäßig ausgeblendet, da es nur für ein Modell gilt.", - "codexAuth.sparkQuotaShown": "Codex-Spark-Kontingent wird angezeigt", - "codexAuth.sparkQuotaHidden": "Codex-Spark-Kontingent ausgeblendet", - "codexAuth.sparkQuotaFailed": "Codex-Spark-Kontingent konnte nicht geändert werden", "codexAuth.refreshQuota": "Kontingente aktualisieren", "codexAuth.ultraFastTitle": "Ultra-Fast-Diensttarif", "codexAuth.mainHardLockTitle": "Hauptkonto bei 99 % sperren", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index c892f87f59..02c6277633 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -1912,11 +1912,6 @@ export const en = { "codexAuth.openaiPresetUnavailable": "OpenAI provider preset is unavailable.", "codexAuth.openProviders": "Open Providers", "codexAuth.add": "Add", - "codexAuth.sparkQuota": "Codex Spark quota", - "codexAuth.sparkQuotaHint": "Show the GPT-5.3-Codex-Spark weekly window on account cards. Hidden by default because it applies to one model only.", - "codexAuth.sparkQuotaShown": "Codex Spark quota shown", - "codexAuth.sparkQuotaHidden": "Codex Spark quota hidden", - "codexAuth.sparkQuotaFailed": "Could not change the Codex Spark quota setting", "codexAuth.refreshQuota": "Refresh quotas", "codexAuth.ultraFastTitle": "Ultra Fast service tier", "codexAuth.mainHardLockTitle": "Block main account at 99%", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 4598823f78..dd2744ff00 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -1844,11 +1844,6 @@ export const fr: Record = { "codexAuth.openaiPresetUnavailable": "Le préréglage du fournisseur OpenAI est indisponible.", "codexAuth.openProviders": "Ouvrir Fournisseurs", "codexAuth.add": "Ajouter", - "codexAuth.sparkQuota": "Quota Codex Spark", - "codexAuth.sparkQuotaHint": "Affiche la fenêtre hebdomadaire GPT-5.3-Codex-Spark sur les cartes de compte. Masquée par défaut car elle ne concerne qu'un seul modèle.", - "codexAuth.sparkQuotaShown": "Quota Codex Spark affiché", - "codexAuth.sparkQuotaHidden": "Quota Codex Spark masqué", - "codexAuth.sparkQuotaFailed": "Impossible de modifier le réglage du quota Codex Spark", "codexAuth.refreshQuota": "Actualiser les quotas", "codexAuth.ultraFastTitle": "Niveau de service Ultra Fast", "codexAuth.mainHardLockTitle": "Bloquer le compte principal à 99 %", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index f9c1ff9102..e06aeb5e86 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -1769,11 +1769,6 @@ export const ja: Record = { "codexAuth.openaiPresetUnavailable": "OpenAI プロバイダーのプリセットを利用できません。", "codexAuth.openProviders": "プロバイダーを開く", "codexAuth.add": "追加", - "codexAuth.sparkQuota": "Codex Spark 使用量", - "codexAuth.sparkQuotaHint": "アカウントカードに GPT-5.3-Codex-Spark の週次枠を表示します。対象が 1 モデルのみのため既定は非表示です。", - "codexAuth.sparkQuotaShown": "Codex Spark 使用量を表示しました", - "codexAuth.sparkQuotaHidden": "Codex Spark 使用量を非表示にしました", - "codexAuth.sparkQuotaFailed": "Codex Spark 使用量の設定を変更できませんでした", "codexAuth.refreshQuota": "クォータを更新", "codexAuth.ultraFastTitle": "Ultra Fast サービスティア", "codexAuth.mainHardLockTitle": "メインアカウントを99%で停止", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 6ead1fb54c..7bbeeccd98 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -1362,11 +1362,6 @@ export const ko: Record = { "codexAuth.openaiPresetUnavailable": "OpenAI 공급자 프리셋을 사용할 수 없습니다.", "codexAuth.openProviders": "프로바이더 열기", "codexAuth.add": "추가", - "codexAuth.sparkQuota": "Codex Spark 할당량", - "codexAuth.sparkQuotaHint": "계정 카드에 GPT-5.3-Codex-Spark 주간 창을 표시합니다. 모델 하나에만 적용되므로 기본값은 숨김입니다.", - "codexAuth.sparkQuotaShown": "Codex Spark 할당량을 표시합니다", - "codexAuth.sparkQuotaHidden": "Codex Spark 할당량을 숨겼습니다", - "codexAuth.sparkQuotaFailed": "Codex Spark 할당량 설정을 바꾸지 못했습니다", "codexAuth.refreshQuota": "할당량 새로고침", "codexAuth.ultraFastTitle": "Ultra Fast 서비스 티어", "codexAuth.mainHardLockTitle": "메인 계정 99% 차단", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index aa093090c2..ea75a1f74f 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -1839,11 +1839,6 @@ export const ru: Record = { "codexAuth.openaiPresetUnavailable": "Пресет провайдера OpenAI недоступен.", "codexAuth.openProviders": "Открыть провайдеров", "codexAuth.add": "Добавить", - "codexAuth.sparkQuota": "Квота Codex Spark", - "codexAuth.sparkQuotaHint": "Показывать недельное окно GPT-5.3-Codex-Spark на карточках аккаунтов. По умолчанию скрыто: оно относится лишь к одной модели.", - "codexAuth.sparkQuotaShown": "Квота Codex Spark показана", - "codexAuth.sparkQuotaHidden": "Квота Codex Spark скрыта", - "codexAuth.sparkQuotaFailed": "Не удалось изменить настройку квоты Codex Spark", "codexAuth.refreshQuota": "Обновить квоты", "codexAuth.ultraFastTitle": "Уровень обслуживания Ultra Fast", "codexAuth.mainHardLockTitle": "Блокировать основной аккаунт при 99%", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 78b572a477..3a2ca09210 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -1869,11 +1869,6 @@ export const tr: Record = { "codexAuth.openaiPresetUnavailable": "Ayar kullanılamıyor.", "codexAuth.openProviders": "Sağlayıcıları Aç", "codexAuth.add": "Ekle", - "codexAuth.sparkQuota": "Codex Spark kotası", - "codexAuth.sparkQuotaHint": "Hesap kartlarında GPT-5.3-Codex-Spark haftalık penceresini gösterir. Yalnızca tek bir modeli kapsadığı için varsayılan olarak gizlidir.", - "codexAuth.sparkQuotaShown": "Codex Spark kotası gösteriliyor", - "codexAuth.sparkQuotaHidden": "Codex Spark kotası gizlendi", - "codexAuth.sparkQuotaFailed": "Codex Spark kotası ayarı değiştirilemedi", "codexAuth.refreshQuota": "Kotaları yenile", "codexAuth.ultraFastTitle": "Ultra Fast hizmet katmanı", "codexAuth.mainHardLockTitle": "Ana hesabı %99’da durdur", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 0dfa0cd8b7..91c871c1e3 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -1427,11 +1427,6 @@ export const zhTW: Record = { "codexAuth.openaiPresetUnavailable": "OpenAI 供應商預設不可用。", "codexAuth.openProviders": "開啟供應商", "codexAuth.add": "新增", - "codexAuth.sparkQuota": "Codex Spark 配額", - "codexAuth.sparkQuotaHint": "在帳號卡片上顯示 GPT-5.3-Codex-Spark 週視窗。預設隱藏,因為只適用於單一模型。", - "codexAuth.sparkQuotaShown": "已顯示 Codex Spark 配額", - "codexAuth.sparkQuotaHidden": "已隱藏 Codex Spark 配額", - "codexAuth.sparkQuotaFailed": "無法變更 Codex Spark 配額設定", "codexAuth.refreshQuota": "重新整理額度", "codexAuth.ultraFastTitle": "Ultra Fast 服務層級", "codexAuth.mainHardLockTitle": "主帳戶用量達 99% 時阻擋請求", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index e30cca65a6..a47bd5e2fd 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -1343,11 +1343,6 @@ export const zh: Record = { "codexAuth.openaiPresetUnavailable": "OpenAI 提供方预设不可用。", "codexAuth.openProviders": "打开提供商", "codexAuth.add": "添加", - "codexAuth.sparkQuota": "Codex Spark 配额", - "codexAuth.sparkQuotaHint": "在账户卡片上显示 GPT-5.3-Codex-Spark 周窗口。默认隐藏,因为它只适用于一个模型。", - "codexAuth.sparkQuotaShown": "已显示 Codex Spark 配额", - "codexAuth.sparkQuotaHidden": "已隐藏 Codex Spark 配额", - "codexAuth.sparkQuotaFailed": "无法更改 Codex Spark 配额设置", "codexAuth.refreshQuota": "刷新额度", "codexAuth.ultraFastTitle": "Ultra Fast 服务层级", "codexAuth.mainHardLockTitle": "主账户用量达 99% 时阻止请求", diff --git a/gui/src/styles.css b/gui/src/styles.css index 0899d77859..352d1c53a2 100644 --- a/gui/src/styles.css +++ b/gui/src/styles.css @@ -1798,14 +1798,8 @@ dialog.modal-overlay::backdrop { z-index: 1; } -/* The action cluster is four nowrap items on one line — feedback, the Spark switch, - and two labelled buttons. Narrow the window and that line wants ~577px inside a - 437px column, and because `.page-head` is a plain nowrap flex row the surplus did - not wrap or scroll: the trailing "Refresh quotas" button rendered ~140px past its - own container and was sliced by the viewport edge. Both axes wrap now — the - cluster drops below the title first, then breaks internally if it still does not - fit — so every control stays reachable at any width. Wrapping is inert while the - line fits, so the wide layout is byte-identical. */ +/* Let feedback wrap below the standalone page title when a translated message is + wider than the available row. Account actions use their own wrapping row below. */ .codex-auth-page-head { align-items: flex-start; flex-wrap: wrap; row-gap: 8px; } .codex-auth-page-head__actions { display: flex; @@ -1818,19 +1812,6 @@ dialog.modal-overlay::backdrop { /* Once the cluster owns its own line the title no longer competes for width, so it takes the full row rather than wrapping "Codex Auth" onto two lines. */ .codex-auth-page-head > .page-title { flex: 1 1 auto; } -/* Spark visibility switch: a labelled toggle, not a bare knob. An unlabelled switch sitting - between two labelled buttons is a guessing game, and this one changes what every card in the - page renders. The label carries the meaning; the toggle carries the state. */ -.codex-auth-spark-toggle { - display: inline-flex; - align-items: center; - gap: 8px; - white-space: nowrap; -} -.codex-auth-spark-toggle__label { - font-size: 12px; - color: var(--muted); -} /* The pause/refresh pair after it moved out of the page head. Right-aligned so it still reads as chrome for the account list below rather than a heading of its own, and it wraps because both labels grow substantially in several locales. */ diff --git a/gui/tests/codex-set-actions-relocation.test.ts b/gui/tests/codex-set-actions-relocation.test.ts index 30e40a5ef4..fce588af89 100644 --- a/gui/tests/codex-set-actions-relocation.test.ts +++ b/gui/tests/codex-set-actions-relocation.test.ts @@ -1,10 +1,8 @@ /** * Where the Codex Set account actions live. * - * "한도 도달 계정 일시 중지" and "할당량 새로고침" used to sit in the page head beside the - * title and the Spark toggle — four controls and a heading on one row, with the actions - * far above the account cards they operate on. They render in their own row below the - * account-mode banner now. + * Pause-exhausted and refresh render in their own row below the account-mode banner, + * next to the account cards they operate on. The page head retains title and feedback. * * The embedded surface is deliberately excluded: in the Providers workspace the same * component renders a bare `.row` with no title, so there is nothing to crowd and the @@ -23,8 +21,8 @@ const headStart = src.indexOf("export function CodexAccountPoolPageHead"); const head = src.slice(headStart, src.indexOf("export function CodexAccountPoolActionButtons", headStart)); test("the standalone page head no longer renders the two action buttons inline", () => { - // The head keeps the title, the feedback region and the Spark toggle; the pause and - // refresh labels are reached through the shared component only. + // The head keeps the title and feedback region; pause and refresh labels are + // reached through the shared component only. expect(head).not.toContain('t("codexAuth.pauseExhausted")'); expect(head).not.toContain('t("codexAuth.refreshQuota")'); }); diff --git a/gui/tests/codex-set-page-head-wrap.test.ts b/gui/tests/codex-set-page-head-wrap.test.ts index 8a6afd2255..239e327180 100644 --- a/gui/tests/codex-set-page-head-wrap.test.ts +++ b/gui/tests/codex-set-page-head-wrap.test.ts @@ -3,12 +3,8 @@ import { expect, test } from "bun:test"; /** * The Codex Set page head must be able to wrap. * - * Its action cluster is four nowrap items — the Spark switch, two labelled - * buttons, and the feedback slot. While the head was a single nowrap flex row, - * a narrow viewport pushed the trailing button past its own container, and - * `overflow-x: hidden` on html/body turned that into a clip rather than a - * scrollbar: measured at 850px, "Refresh quotas" ran to x=944 against a - * container ending at 804. + * The standalone head holds a title and feedback slot. Long translated feedback + * must wrap below the title without clipping; pause/refresh have a separate row. * * Source-text assertions, not measurements: happy-dom performs no layout, so a * getBoundingClientRect() here returns zeros and would prove nothing. The diff --git a/gui/tests/main-account-hard-lock-setting.test.tsx b/gui/tests/main-account-hard-lock-setting.test.tsx index 4eb1129c54..26e87ba177 100644 --- a/gui/tests/main-account-hard-lock-setting.test.tsx +++ b/gui/tests/main-account-hard-lock-setting.test.tsx @@ -294,7 +294,7 @@ test("same-page manage opens Advanced; save refreshes the one injected account c const url = new URL(String(input)); if (url.pathname === "/api/settings") { if (init?.method === "PUT") enabled = JSON.parse(String(init.body)).codexMainAccountHardLock; - return response({ ok: true, ...settings(enabled, enabled ? "blocked" : "off"), showCodexSparkQuota: false, codexAccountPickerEnabled: false }); + return response({ ok: true, ...settings(enabled, enabled ? "blocked" : "off"), codexAccountPickerEnabled: false }); } if (url.pathname === "/api/codex-auth/accounts") { accountReads++; @@ -335,7 +335,7 @@ test("late proxy A PUT cannot reload A or replace proxy B's parent-owned account expect(JSON.parse(String(init.body))).toEqual({ codexMainAccountHardLock: false }); return pendingPut.promise; } - return response({ ...settings(enabled, state), showCodexSparkQuota: false, codexAccountPickerEnabled: false }); + return response({ ...settings(enabled, state), codexAccountPickerEnabled: false }); } if (url.pathname === "/api/codex-auth/accounts") return response({ accounts: [{ ...mainAccount(state), email: isA ? "proxy-a@example.test" : "proxy-b@example.test" }], @@ -374,7 +374,7 @@ test("collapsing Advanced within the same proxy still refreshes the owner after const url = new URL(String(input)); if (url.pathname === "/api/settings") { if (init?.method === "PUT") return pendingPut.promise; - return response({ ...settings(enabled, enabled ? "blocked" : "off"), showCodexSparkQuota: false, codexAccountPickerEnabled: false }); + return response({ ...settings(enabled, enabled ? "blocked" : "off"), codexAccountPickerEnabled: false }); } if (url.pathname === "/api/codex-auth/accounts") { accountReads++; @@ -396,3 +396,51 @@ test("collapsing Advanced within the same proxy still refreshes the owner after expect(accountReads).toBe(2); expect(host.querySelector(".codex-main-hard-lock-status")).toBeNull(); }); + +test.each([false, true])("retired Spark setting (legacy response: %s) cannot restore a control; account actions remain usable", async legacy => { + const refreshed = deferred(); + const writes: { path: string; body: unknown }[] = []; + const host = await mount((async (input, init) => { + const url = new URL(String(input)); + if (init?.method && init.method !== "GET") { + writes.push({ path: url.pathname, body: init.body ? JSON.parse(String(init.body)) : null }); + } + if (url.pathname === "/api/settings") return response({ + ...settings(false), codexAccountPickerEnabled: false, codexQuotaAutoRefresh: {}, + ...(legacy ? { showCodexSparkQuota: true } : {}), + }); + if (url.pathname === "/api/codex-auth/accounts/refresh") return refreshed.promise; + if (url.pathname === "/api/codex-auth/accounts/pause-exhausted") { + return response({ ok: true, pausedAccountIds: [], pausedCount: 0 }); + } + if (url.pathname === "/api/codex-auth/accounts") return response({ accounts: [mainAccount("off")] }); + if (url.pathname === "/api/codex-auth/active") return response({ activeCodexAccountId: "__main__", autoSwitchThreshold: 80, accountPoolStrategy: "quota", accountPoolStickyLimit: 1 }); + if (url.pathname === "/api/config") return response({ providers: {} }); + return response({}); + }) as typeof fetch, ); + + expect(host.querySelector('[aria-label="Codex Spark quota"]')).toBeNull(); + expect(host.querySelector(".codex-auth-spark-toggle")).toBeNull(); + expect(host.textContent).not.toContain("Spark"); + expect(host.querySelector("#codex-account-priority-__main__")).not.toBeNull(); + expect(button(host, '[aria-label="Pause"]').disabled).toBe(false); + const actions = host.querySelectorAll(".codex-auth-actions-row button"); + expect(actions).toHaveLength(2); + expect(actions[0]!.textContent).toContain("Pause exhausted"); + expect(actions[1]!.textContent).toContain("Refresh quotas"); + await click(actions[1]!); + expect(actions[0]!.disabled).toBe(true); + expect(actions[1]!.disabled).toBe(true); + await act(async () => { refreshed.resolve(response({ accounts: [mainAccount("off")] })); await flush(); }); + expect(actions[0]!.disabled).toBe(false); + expect(actions[1]!.disabled).toBe(false); + expect(host.querySelector(".codex-auth-page-head__feedback.is-ok")).not.toBeNull(); + await click(actions[0]!); + expect(writes).toEqual([ + { path: "/api/codex-auth/accounts/refresh", body: null }, + { path: "/api/codex-auth/accounts/pause-exhausted", body: null }, + ]); + await click(button(host, ".codex-auth-advanced__toggle")); + expect(toggle(host).disabled).toBe(false); + expect(host.textContent).not.toContain("Spark"); +}); diff --git a/src/adapters/openai-responses.ts b/src/adapters/openai-responses.ts index 5a5d370faf..79b8597df3 100644 --- a/src/adapters/openai-responses.ts +++ b/src/adapters/openai-responses.ts @@ -334,24 +334,6 @@ function scrubOcxCompactionItems( return changed ? { ...body, input } : body; } -/** - * Strip unsupported `reasoning` sub-parameters for native slugs that reject them (e.g. Spark). - * codex-rs injects `reasoning.context` and `reasoning.summary` based on catalog flags; Spark's - * backend rejects both. The catalog fix prevents `use_responses_lite` from being set, but this - * is a defense-in-depth guard so stale on-disk catalogs don't break until the user runs `ocx sync`. - */ -function stripUnsupportedReasoningParams(body: unknown): unknown { - if (!isPlainObject(body)) return body; - const model = typeof body.model === "string" ? body.model : ""; - if (!model.includes("codex-spark")) return body; - if (!isPlainObject(body.reasoning)) return body; - const reasoning = body.reasoning as Record; - // Spark supports reasoning.effort but rejects context, summary, and generate_summary. - const { context: _ctx, summary: _sum, generate_summary: _gs, ...rest } = reasoning; - if (_ctx === undefined && _sum === undefined && _gs === undefined) return body; - return { ...body, reasoning: Object.keys(rest).length > 0 ? rest : undefined }; -} - /** * GPT-5.6 retired the legacy 24-hour retention field, and the ChatGPT backend 400s the whole * request when that field is present (issue #2092). @@ -472,157 +454,10 @@ function normalizeConfiguredReasoningSummaryDelivery( }; } -/** - * Comprehensive Spark compatibility layer. codex-rs emits five tool types (function, - * namespace, tool_search, web_search, custom) plus extensions (defer_loading, - * parallel_tool_calls, tool_search_call/output items). Spark's serving path only - * supports flat function tools and hosted web_search. This function: - * - Flattens MCP-style namespace tools → promotes inner functions to top level. The reserved - * `functions` group is kept as a group (#3217): Codex 0.147+ sends every ordinary client tool - * inside it on Responses Lite, the backend accepts the group as-is, and flattening it changes - * what the backend answers with — a `custom_tool_call` carrying `namespace: "exec"`, which - * codex-rs concatenates into the unroutable `execexec`. Traced on a live proxy: with the - * group intact the same backend returns the bare `exec` call and the turn completes. - * - Drops unsupported tool types (tool_search, custom) - * - Strips defer_loading from function tools - * - Strips namespace from input items - * - Drops tool_search_call/tool_search_output input items - * - Sets parallel_tool_calls to false - */ -function stripSparkCompatibility(body: unknown): unknown { - if (!isPlainObject(body)) return body; - const model = typeof body.model === "string" ? body.model : ""; - if (!model.includes("codex-spark")) return body; - - let changed = false; - - const SPARK_SAFE_TOOL_TYPES = new Set(["function", "web_search", "web_search_preview"]); - // Inside the reserved group Codex sends freeform `custom` tools (code-mode `exec`) and the - // backend accepts them there; the top-level "drop custom" rule stays for flattened groups. - const SPARK_SAFE_FUNCTIONS_GROUP_CHILD_TYPES = new Set(["function", "custom"]); - const filterSparkFunctionsGroup = (group: Record): Record | undefined => { - if (!Array.isArray(group.tools)) return undefined; - let groupChanged = false; - const children: unknown[] = []; - for (const child of group.tools) { - if (!isPlainObject(child) || typeof child.type !== "string" || !SPARK_SAFE_FUNCTIONS_GROUP_CHILD_TYPES.has(child.type)) { - groupChanged = true; - continue; - } - if (child.type === "function" && "defer_loading" in child) { - const { defer_loading: _, ...rest } = child; - groupChanged = true; - children.push(rest); - continue; - } - children.push(child); - } - if (children.length === 0) return undefined; - return groupChanged ? { ...group, tools: children } : group; - }; - - let tools = body.tools; - if (Array.isArray(tools)) { - const flattened: unknown[] = []; - for (const t of tools) { - if (isPlainObject(t) && t.type === "namespace" && t.name === SPARK_RESERVED_FUNCTIONS_NAMESPACE) { - const kept = filterSparkFunctionsGroup(t); - if (kept !== t) changed = true; - if (kept) flattened.push(kept); - } else if (isPlainObject(t) && t.type === "namespace") { - changed = true; - if (Array.isArray(t.tools)) { - for (const inner of t.tools) flattened.push(inner); - } - } else if (isPlainObject(t) && typeof t.type === "string" && !SPARK_SAFE_TOOL_TYPES.has(t.type)) { - changed = true; - } else { - flattened.push(t); - } - } - // Strip defer_loading from promoted/remaining function tools. - tools = flattened.map(t => { - if (isPlainObject(t) && t.type === "function" && "defer_loading" in t) { - const { defer_loading: _, ...rest } = t; - changed = true; - return rest; - } - return t; - }); - } - - // Clean input items: strip namespace, drop tool_search_call/tool_search_output. - const SPARK_UNSUPPORTED_INPUT_TYPES = new Set([ - "tool_search_call", "tool_search_output", - "custom_tool_call", "custom_tool_call_output", - ]); - let input = body.input; - if (Array.isArray(input)) { - const cleaned: unknown[] = []; - for (const item of input) { - if (isPlainObject(item) && typeof item.type === "string" && SPARK_UNSUPPORTED_INPUT_TYPES.has(item.type)) { - changed = true; - continue; - } - // Process additional_tools items: filter their inner tools array the same way. - if (isPlainObject(item) && item.type === "additional_tools" && Array.isArray(item.tools)) { - const innerTools = item.tools as unknown[]; - const filteredInner: unknown[] = []; - for (const t of innerTools) { - if (isPlainObject(t) && t.type === "namespace" && t.name === SPARK_RESERVED_FUNCTIONS_NAMESPACE) { - const kept = filterSparkFunctionsGroup(t); - if (kept !== t) changed = true; - if (kept) filteredInner.push(kept); - } else if (isPlainObject(t) && t.type === "namespace") { - changed = true; - if (Array.isArray(t.tools)) { - for (const fn of t.tools) filteredInner.push(fn); - } - } else if (isPlainObject(t) && typeof t.type === "string" && !SPARK_SAFE_TOOL_TYPES.has(t.type)) { - changed = true; // drop custom, tool_search, etc. - } else { - filteredInner.push(t); - } - } - // Strip defer_loading from remaining function tools. - const cleanedInner = filteredInner.map(t => { - if (isPlainObject(t) && t.type === "function" && "defer_loading" in t) { - const { defer_loading: _, ...rest } = t; - changed = true; - return rest; - } - return t; - }); - cleaned.push({ ...item, tools: cleanedInner }); - continue; - } - if (isPlainObject(item) && "namespace" in item) { - const { namespace: _, ...rest } = item; - changed = true; - cleaned.push(rest); - } else { - cleaned.push(item); - } - } - if (changed) input = cleaned; - } - - // Force parallel_tool_calls off for Spark. - const extraOverrides: Record = {}; - if (body.parallel_tool_calls === true) { extraOverrides.parallel_tool_calls = false; changed = true; } - - return changed - ? { ...body, ...(tools !== body.tools ? { tools } : {}), ...(input !== body.input ? { input } : {}), ...extraOverrides } - : body; -} - function isPlainObject(v: unknown): v is Record { return !!v && typeof v === "object" && !Array.isArray(v); } -/** Codex's reserved client-tool group on Responses Lite; carries no wire prefix. */ -const SPARK_RESERVED_FUNCTIONS_NAMESPACE = "functions"; - /** * Apply the routed provider's real effort ladder to an existing Responses reasoning field. * Native forward requests keep the server-owned native clamp; unknown third-party ladders stay @@ -868,20 +703,6 @@ function promoteClientLoadedTools(body: unknown): unknown { const MAX_RESPONSES_CALL_ID_LENGTH = 64; -/** - * Whether the outgoing body still delivers tools through the responses-lite shape. - * - * Lite carries the client catalog as an `additional_tools` input item; the non-Lite wire shape - * expects top-level `tools`. Anything that flips the Lite advertisement has to agree with the - * shape actually being sent, or the destination silently loses the tool surface. - */ -function bodyCarriesLiteToolShape(body: Record): boolean { - if (!Array.isArray(body.input)) return false; - return body.input.some(item => - isPlainObject(item) && item.type === "additional_tools" - && Array.isArray(item.tools) && item.tools.length > 0 - ); -} const REPAIRED_CALL_ID_PREFIX = "call_ocx_"; const REPAIRED_CALL_ID_DIGEST_LENGTH = MAX_RESPONSES_CALL_ID_LENGTH - REPAIRED_CALL_ID_PREFIX.length; @@ -2517,26 +2338,22 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): } const threadServingIdentityChanged = parsed._stripReasoningEncryptedContent === true; const sanitizedBody = normalizeToolSchemas( - stripSparkCompatibility( - stripUnsupportedReasoningParams( - stripItemIdsWhenUnstored( - stripInvalidItemIds( - stripUnsupportedHostedTools( - sanitizeReasoningInputContent( - scrubOcxCompactionItems( - outBody, - destinationDecodesNativeCompactionBlob(provider), - threadServingIdentityChanged, - ), - { - preserveRawReasoningContent: provider.preserveResponsesReasoningContent === true, - dropNullContentChannel: !isOpenAiOperatedResponsesDestination(provider), - stripEncryptedContent: threadServingIdentityChanged, - }, - ), - provider, + stripItemIdsWhenUnstored( + stripInvalidItemIds( + stripUnsupportedHostedTools( + sanitizeReasoningInputContent( + scrubOcxCompactionItems( + outBody, + destinationDecodesNativeCompactionBlob(provider), + threadServingIdentityChanged, ), + { + preserveRawReasoningContent: provider.preserveResponsesReasoningContent === true, + dropNullContentChannel: !isOpenAiOperatedResponsesDestination(provider), + stripEncryptedContent: threadServingIdentityChanged, + }, ), + provider, ), ), ), @@ -2561,23 +2378,6 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): ? { ...(unnormalizedBody as Record), model: stripBracketedModelSuffix((unnormalizedBody as { model: string }).model) } : unnormalizedBody; if (isCanonicalOpenAiForwardProvider(provider)) { - // Select Spark's Lite compatibility from the final wire model, including aliases, and - // let the BODY decide it. The header also overrides native WS metadata downstream, so a - // forwarded or statically configured value must never contradict the shape being sent. - // - // The synchronized catalog keeps `use_responses_lite: true` for Spark precisely because - // it selects tool delivery (`input[].additional_tools` instead of top-level `tools`), and - // stripSparkCompatibility filters that group in place rather than promoting it. So a - // Lite-shaped body is pinned back ON — otherwise an inherited `false` advertises non-Lite - // while the tools exist only in the Lite shape, and Spark loses the tool surface. Only a - // body with no Lite tool group is downgraded, which is what the stream fix needs. - if (isPlainObject(finalBody) && finalBody.model === "gpt-5.3-codex-spark") { - const liteShaped = bodyCarriesLiteToolShape(finalBody); - for (const name of Object.keys(headers)) { - if (name.toLowerCase() === CODEX_RESPONSES_LITE_HEADER) delete headers[name]; - } - headers[CODEX_RESPONSES_LITE_HEADER] = liteShaped ? "true" : "false"; - } const routingHeaders = new Headers(headers); applyCodexRoutingHint(routingHeaders, finalBody); // Static headers may use mixed casing. Remove every stale spelling diff --git a/src/codex/auth-api.ts b/src/codex/auth-api.ts index 70c7273c5d..8f92f2c8c8 100644 --- a/src/codex/auth-api.ts +++ b/src/codex/auth-api.ts @@ -87,6 +87,7 @@ import { parseUsageQuota, setAccountQuotaFromParsed, updateAccountQuota, + withoutRetiredCodexQuota, type StoredAccountQuota, type WhamUsageResponse, } from "./quota"; @@ -251,49 +252,11 @@ function codexAccountPersistenceConflict( : undefined; } -/** - * The exact labels `parseUsageQuota` emits for the Codex Spark windows (quota.ts). - * Matching on the label rather than on "is a custom window" is load-bearing: the same array - * carries Cursor's First-party models / API usage, Anthropic's Fable / Opus / Sonnet, - * Antigravity's Gem / Cla, Kimi's subscription credits and a dozen dynamic provider meters. - */ -const CODEX_SPARK_WINDOW_LABELS = new Set([ - "GPT-5.3-Codex-Spark 5h", - "GPT-5.3-Codex-Spark Weekly", -]); - -/** - * Drop the Spark window unless the operator asked for it (default hidden). - * - * Applied at the DTO boundary, never at parse or cache time: custom windows participate in - * quota-presence checks, snapshot reconciliation and capacity aggregation, so removing Spark - * upstream of this point would change routing state rather than display. - * - * Both GUI surfaces funnel through here — the Codex Auth rows directly, and /api/provider-quotas - * via listCodexAuthAccountsSnapshot — so one filter covers both. Filtering only one would leave - * the other still rendering the row the operator switched off. - */ -export function withSparkVisibility | StoredAccountQuota | null>( - quota: T, -): T { - if (!quota?.customWindows?.length) return quota; - if (loadConfig().showCodexSparkQuota === true) return quota; - const kept = quota.customWindows.filter(window => !CODEX_SPARK_WINDOW_LABELS.has(window.label)); - if (kept.length === quota.customWindows.length) return quota; - // An empty list is dropped rather than serialized: an absent field and an empty array should - // not be two different ways of saying "no custom windows" on the wire. - const next = { ...quota } as Record; - if (kept.length > 0) next.customWindows = kept; - else delete next.customWindows; - return next as T; -} - - function quotaForPlan | StoredAccountQuota | null>( quota: T, plan: unknown, -): T { - const visible = withSparkVisibility(quota); +): T | null { + const visible = withoutRetiredCodexQuota(quota); if (!visible || !isThirtyDayOnlyCodexPlan(plan)) return visible; const quotaWindows = visible; return { @@ -1749,7 +1712,7 @@ export async function runCodexCooldownRecoveryProbes(config: OcxConfig, now = Da try { const result = await fetchPoolAccountQuota(claim.accountId, true, account.plan); // Defence in depth: independent scopes are already excluded at the claim site. - // Generic WHAM must never clear Spark or Reserve even if claim selection changes. + // Generic WHAM must never clear Reserve even if claim selection changes. const recovered = (claim.scope === undefined || claim.scope === "shared") && isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? account.plan); settleCodexQuotaRecoveryProbe(claim, recovered, { @@ -2093,9 +2056,9 @@ export async function listCodexAuthAccountsSnapshot( hasCredential: hasMainCredential, needsReauth: mainNeedsReauth, ...(mainReauthReason !== undefined ? { reauthReason: mainReauthReason } : {}), - quota: mainInfo.quota ? { - ...quotaForPlan(mainQuotaWithCarriedResetCredits(mainInfo.quota), mainInfo.plan), - } : null, + quota: mainInfo.quota + ? quotaForPlan(mainQuotaWithCarriedResetCredits(mainInfo.quota), mainInfo.plan) + : null, ...oauthAccountHealthFields("codex", MAIN_CODEX_ACCOUNT_ID, mainHealth), }; return { diff --git a/src/codex/auth-context.ts b/src/codex/auth-context.ts index 5bf023565e..12a1dd6e17 100644 --- a/src/codex/auth-context.ts +++ b/src/codex/auth-context.ts @@ -561,7 +561,7 @@ export function cooldownErrorMessage(err: CodexAccountCooldownError, accountSele if (err instanceof CodexMainAccountHardLockError || err instanceof CodexReserveUnavailableError) return err.message; const until = new Date(err.cooldownUntil).toISOString(); const scopeLabels: Record = { - spark: "Spark quota", shared: "shared native quota", reserve: "Reserve quota", + shared: "shared native quota", reserve: "Reserve quota", }; const scope = err.quotaScope ? scopeLabels[err.quotaScope] : null; const selected = accountSelector diff --git a/src/codex/catalog/effort.ts b/src/codex/catalog/effort.ts index 7aa0f006dc..e3909263c0 100644 --- a/src/codex/catalog/effort.ts +++ b/src/codex/catalog/effort.ts @@ -60,7 +60,7 @@ export function nativeEffortClamp(slug: string, effort: string | undefined): str if (levels.length === 0) { // Not snapshot-covered. gpt-5.6 natives have a REAL max rung (ensureGpt56ReasoningLevels // restores it even off-snapshot) -> never clamp. Every other bare native (gpt-5.5, - // 5.3-codex-spark, a retired slug a client still asks for, and future old-ladder slugs) + // a retired slug a client still asks for, and future old-ladder slugs) // really stops at xhigh — the ChatGPT backend error names exactly none..xhigh — so clamp // the synthetic top tier. return isGpt56NativeSlug(slug) ? null : "xhigh"; diff --git a/src/codex/catalog/metadata.ts b/src/codex/catalog/metadata.ts index 0b16e7d7bb..8ae7bde58a 100644 --- a/src/codex/catalog/metadata.ts +++ b/src/codex/catalog/metadata.ts @@ -72,7 +72,6 @@ export { } from "./native-models"; export const DOCUMENTED_NATIVE_OPENAI_ADDITIONS = [ - "gpt-5.3-codex-spark", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", // The shipped pin also backfills older installed Codex catalogs that predate Astra. NATIVE_GPT6_ASTRA_MODEL, @@ -163,7 +162,6 @@ const NATIVE_GPT56_FAMILY = new Set([ export const NATIVE_OPENAI_CONTEXT_OVERRIDES: Record = { "gpt-5.5": { contextWindow: 272_000, maxContextWindow: 272_000 }, - "gpt-5.3-codex-spark": { contextWindow: 100_000, maxContextWindow: 100_000 }, "gpt-5.6-sol": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_MAX_INPUT_TOKENS, maxInputTokens: NATIVE_GPT56_MAX_INPUT_TOKENS }, "gpt-5.6-terra": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_MAX_INPUT_TOKENS, maxInputTokens: NATIVE_GPT56_MAX_INPUT_TOKENS }, "gpt-5.6-luna": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_MAX_INPUT_TOKENS, maxInputTokens: NATIVE_GPT56_MAX_INPUT_TOKENS }, @@ -378,8 +376,7 @@ export function nativeInputModalities(slug: string): string[] { if (Array.isArray(upstream?.input_modalities) && upstream!.input_modalities!.length > 0) { return [...upstream!.input_modalities as string[]]; } - // gpt-5.3-codex-spark is not in the upstream snapshot; all supported natives are - // text+image capable, so default to the family baseline rather than text-only. + // Without a pinned row, retain the native family modality baseline. return ["text", "image"]; } @@ -393,7 +390,7 @@ export function nativeReasoningEfforts(slug: string): string[] { // include ultra while Luna intentionally ends at max. return levels.flatMap(l => typeof l.effort === "string" ? [l.effort] : []); } - // gpt-5.3-codex-spark is not in upstream snapshot — use the standard old-ladder default. + // Without a pinned row, use the standard old-ladder default. return ["low", "medium", "high", "xhigh"]; } @@ -854,7 +851,7 @@ function catalogNativeSlugs(): string[] { } export function listCatalogNativeSlugs(): string[] { - // Ensure documented additions (e.g. gpt-5.3-codex-spark) appear even when the bundled catalog + // Ensure documented additions (e.g. gpt-6-astra) appear even when the bundled catalog // predates the slug — mirrors nativeOpenAiSlugs() which already merges them for /v1/models. return unique([...catalogNativeSlugs(), ...DOCUMENTED_NATIVE_OPENAI_ADDITIONS]); } diff --git a/src/codex/catalog/native-models.ts b/src/codex/catalog/native-models.ts index c7bee6d1f8..cdcdaa2db2 100644 --- a/src/codex/catalog/native-models.ts +++ b/src/codex/catalog/native-models.ts @@ -155,7 +155,7 @@ export function nativeOpenAiAliasPresentation(slug: string): { displayName: stri * Devlog: 260816_codexrs_multiagent_v2_and_history_perf/011 §4-bis. */ export const NATIVE_OPENAI_MODELS = [ - "gpt-5.5", "gpt-5.3-codex-spark", + "gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", NATIVE_DAYBREAK_BLUE_MODEL, NATIVE_GPT6_ASTRA_MODEL, @@ -178,7 +178,7 @@ export const SUPPORTED_NATIVE_OPENAI_SLUGS = new Set(NATIVE_OPENAI_MODELS); * thing that makes a slug retired is upstream withdrawing it. */ export const RETIRED_NATIVE_OPENAI_MODELS: ReadonlySet = new Set([ - "gpt-5.4", "gpt-5.4-mini", + "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex-spark", ]); /** @@ -192,8 +192,8 @@ export const RETIRED_NATIVE_OPENAI_MODELS: ReadonlySet = new Set([ * flipped false — letting a drain silently rewrite the operator's configured subagent model. * * It is an explicit list rather than `SUPPORTED_NATIVE_OPENAI_SLUGS`, which would have widened - * the sentinel to `gpt-5.5` and `gpt-5.3-codex-spark` as well. Those - * models were never covered, and widening would turn "fell back and answered" into a + * the sentinel to `gpt-5.5` as well. That + * model was never covered, and widening would turn "fell back and answered" into a * maintenance error for the most commonly configured fallback slug in the repo. Membership is * the set the drain behaviour was actually reasoned about: the account-gated natives plus the * flagships that just left that set. diff --git a/src/codex/catalog/parsing.ts b/src/codex/catalog/parsing.ts index 06747a8553..e1629a0ed5 100644 --- a/src/codex/catalog/parsing.ts +++ b/src/codex/catalog/parsing.ts @@ -337,15 +337,6 @@ export function findSupportedNativeTemplate(catalog: RawCatalog | null): RawEntr ) ?? null; } -/** - * Native OpenAI slugs that do NOT support the Fast (priority) service tier. - * Upstream may advertise service_tiers for these models, but the tier is not - * actually available — strip it so the Codex UI does not offer a dead toggle. - */ -const NO_FAST_TIER_NATIVE_SLUGS = new Set([ - "gpt-5.3-codex-spark", -]); - /** Does this row already carry an `ultrafast` tier the operator put there themselves? */ /** * Read the opt-in from the live config, ONCE per catalog build. @@ -438,14 +429,6 @@ export function normalizeServiceTiers(entry: RawEntry): RawEntry { ? { ...tier, description: "2x speed, increased usage" } : tier, ); } - // Strip service tiers for models that do not actually support the Fast tier. - if (typeof entry.slug === "string" && NO_FAST_TIER_NATIVE_SLUGS.has(entry.slug)) { - delete entry.service_tier; - delete entry.service_tiers; - delete entry.default_service_tier; - delete entry.additional_speed_tiers; - return entry; - } // Codex stores the user-facing config spelling as "fast", but the catalog/request // service tier id is "priority" in current codex-rs. Keep legacy catalogs working. if (entry.service_tier === "fast") entry.service_tier = "priority"; @@ -517,9 +500,8 @@ export function applyNativeOpenAiContextOverride(entry: RawEntry, limits?: Nativ } } // providerContextCaps.openai is a ceiling for native OpenAI rows regardless of where the - // advertised window came from (#1430): preserved rows without a hardcoded override (e.g. - // gpt-5.3-codex-spark) must stay under the cap too, and auto-compaction follows the capped - // window. + // advertised window came from (#1430): preserved rows without a hardcoded override + // must stay under the cap too, and auto-compaction follows the capped window. // The per-model window narrows the same rows for the same reason. const currentContext = typeof entry.context_window === "number" ? entry.context_window : undefined; const cappedContext = narrowNativeMaxContextWindow(nativeSlug, currentContext, limits); diff --git a/src/codex/catalog/sync.ts b/src/codex/catalog/sync.ts index bb0a778993..edf41e74c9 100644 --- a/src/codex/catalog/sync.ts +++ b/src/codex/catalog/sync.ts @@ -49,7 +49,7 @@ import { codexAccountLogLabel, fallbackCodexAccountLogLabel } from "../account-l import { CODEX_CUSTOM_MODEL_CATALOG_KIND, CODEX_PROVIDER_MODEL_CATALOG_KIND, activeCodexModelsCachePath, applyCatalogMetadata, applyMultiAgentMode, applyNativeOpenAiContextOverride, applyRoutedCodexToolMode, catalogBackupPathFor, catalogHasRoutedEntries, catalogModelSlug, ensureStrictCatalogFields, findNativeTemplate, findSupportedNativeTemplate, isDefaultCatalogPath, isRoutedModelCompatibilityExcluded, legacyCatalogBackupPath, normalizeRoutedCatalogEntry, normalizeServiceTiers, readCatalog, readCatalogBackup, readCodexCatalogPath, readCodexCatalogPathForHome, readConfiguredAutoReviewModel, readNativeBaseline } from "./parsing"; import type { CatalogModel, MultiAgentMode, RawCatalog, RawEntry } from "./parsing"; -import { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, applyNativeVisibility, CODEX_NATIVE_ALIAS_CATALOG_KIND, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, isNativeAliasCatalogEntry, isUnsupportedOpenAiNativeSlug, NATIVE_OPENAI_MODELS, nativeContextLimits, observedAccountBoundNativeEntries, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, shouldUpgradeToUpstreamEntry, SUPPORTED_NATIVE_OPENAI_SLUGS, upstreamNativeEntry, type NativeContextLimitsInput } from "./metadata"; +import { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, applyNativeVisibility, CODEX_NATIVE_ALIAS_CATALOG_KIND, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, isNativeAliasCatalogEntry, isUnsupportedOpenAiNativeSlug, NATIVE_OPENAI_MODELS, RETIRED_NATIVE_OPENAI_MODELS, nativeContextLimits, observedAccountBoundNativeEntries, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, shouldUpgradeToUpstreamEntry, SUPPORTED_NATIVE_OPENAI_SLUGS, upstreamNativeEntry, type NativeContextLimitsInput } from "./metadata"; import { bundledCatalogCacheState, loadBundledCodexCatalog, @@ -261,7 +261,7 @@ export function finishUpstreamNativeEntry(clone: RawEntry, priority: number, con if (priority !== 9) clone.priority = priority; applyNativeOpenAiContextOverride(clone, contextCap); // GPT-5.6 natives keep their exact upstream ladders (e.g. luna has max but no ultra). - // Older natives (gpt-5.5 / 5.3-codex-spark) get mock max + ultra + // Older natives (gpt-5.5) get mock max + ultra // (wire-clamped to xhigh). Ultra is always advertised regardless of v2 toggle. if (!isGpt56NativeSlug(String(clone.slug ?? ""))) ensureUltraReasoningLevel(clone); return ensureStrictCatalogFields(normalizeServiceTiers(clone)); @@ -400,15 +400,10 @@ export function deriveEntry( applyNativeOpenAiContextOverride(e, contextCap); if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(e); else ensureUltraReasoningLevel(e); - // Non-5.6 natives (5.5, 5.4, 5.4-mini, spark) do not support responses-lite; - // the template may carry the flag from a 5.6 entry — strip it so codex-rs does - // not inject reasoning.context: "all_turns" for models that reject it. - if (!isGpt56NativeSlug(slug)) { - // Spark NEEDS use_responses_lite: true — it controls the tool delivery format - // (AdditionalTools in input vs top-level tools). The reasoning params that - // use_responses_lite triggers (context: "all_turns", summary) are stripped - // separately in the passthrough adapter (stripUnsupportedReasoningParams). - if (!slug.includes("codex-spark")) delete e.use_responses_lite; + // Older natives do not support Responses Lite. A newer template must not enable + // reasoning.context or WebSockets on those models. + if (!isGpt56NativeSlug(slug)) { + delete e.use_responses_lite; delete e.supports_websockets; } } @@ -2576,18 +2571,22 @@ export function restoreCodexCatalogWithPermit( const replacementVisibility = visibleAccountReplacementNatives(catalog.models, disabledModels); const backup = readCatalogBackup(catalogPath); if (backup && Array.isArray(backup.models)) { - const removed = (catalog.models ?? []).filter(m => typeof m.slug === "string" && m.slug.includes("/")).length; + const removed = (catalog.models ?? []).filter(m => typeof m.slug === "string" + && (m.slug.includes("/") || RETIRED_NATIVE_OPENAI_MODELS.has(m.slug))).length; const backupSlugs = new Set(backup.models.flatMap(m => typeof m.slug === "string" ? [m.slug] : [])); const userNativeAdditions = restoreAccountHiddenBareNatives( (catalog.models ?? []).filter(m => typeof m.slug === "string" && !m.slug.includes("/") && !backupSlugs.has(m.slug) + && !RETIRED_NATIVE_OPENAI_MODELS.has(m.slug) ), replacementVisibility, disabledModels, ); const restored = { ...backup, - models: [...backup.models, ...userNativeAdditions], + // A pristine backup predates retirement; it must not revive withdrawn native rows. + models: [...backup.models.filter(m => typeof m.slug !== "string" + || !RETIRED_NATIVE_OPENAI_MODELS.has(trustedAccountBoundNativeCatalogSlug(m) ?? m.slug)), ...userNativeAdditions], }; replaceActiveCodexCatalog(permit, owningCodexHome, { path: catalogPath, @@ -2597,7 +2596,8 @@ export function restoreCodexCatalogWithPermit( } const before = catalog.models.length; const native = restoreAccountHiddenBareNatives( - catalog.models.filter(m => !(typeof m.slug === "string" && m.slug.includes("/"))), + catalog.models.filter(m => !(typeof m.slug === "string" + && (m.slug.includes("/") || RETIRED_NATIVE_OPENAI_MODELS.has(m.slug)))), replacementVisibility, disabledModels, ); diff --git a/src/codex/quota.ts b/src/codex/quota.ts index f7bff7dc0e..01cfe485c0 100644 --- a/src/codex/quota.ts +++ b/src/codex/quota.ts @@ -274,7 +274,9 @@ export function setAccountQuotaFromParsed( policyQuota: Omit | null = quota, historyEvidence?: QuotaObservationEvidence, ): void { - if (!quota) return; + quota = withoutRetiredCodexQuota(quota); + policyQuota = withoutRetiredCodexQuota(policyQuota); + if (!quota || (!snapshotHasUsage(quota) && quota.resetCredits === undefined)) return; if (!mayCommitAccountQuota(accountId, writerGeneration)) return; const isMain = accountId === MAIN_CODEX_ACCOUNT_ID; if (isMain && mainWriter && !isMainQuotaWriterLive(mainWriter)) return; @@ -317,6 +319,8 @@ function mergeAccountQuota( updatedAt: number, policyEvidence = false, ): StoredAccountQuota { + quota = withoutRetiredCodexQuota(quota) ?? {}; + existing = withoutRetiredCodexQuota(existing ?? null) ?? undefined; const next: StoredAccountQuota = { updatedAt }; const creditsOnly = quota.resetCredits !== undefined && !snapshotHasUsage(quota); @@ -425,7 +429,9 @@ function notifyCodexQuotaSnapshot(accountId: string, next: StoredAccountQuota): // Copy before the boundary: `next` is the live map value and the following write mutates // it, so an observation that read it after awaiting could see a later snapshot than the // one it was called for. - const snapshot = { ...next }; + const retained = withoutRetiredCodexQuota(next); + if (!retained) return; + const snapshot = { ...retained }; pendingObservation = pendingObservation .then(async () => { const observer = await import("../quota/reset-observer"); @@ -449,21 +455,25 @@ export function flushQuotaObservationsForTests(): Promise { return pendingObservation; } -/** Wire marker shared by Spark-family models, whose upstream limit family is model-specific. */ -const SPARK_MODEL_MARKER = "codex-spark"; -/** - * Custom-window label for the Spark 5h window. The WHAM parser and the response-header path - * must write the SAME label so a header refresh replaces the WHAM reading instead of doubling it. - */ -const SPARK_SHORT_WINDOW_LABEL = "GPT-5.3-Codex-Spark 5h"; -const SPARK_WEEKLY_WINDOW_LABEL = "GPT-5.3-Codex-Spark Weekly"; +/** Exact retired model evidence, including account/provider-qualified selectors. */ +export function isRetiredCodexSparkModel(modelId: string | undefined): boolean { + return modelId?.trim().toLowerCase().split("/").at(-1) === "gpt-5.3-codex-spark"; +} -/** True when the routed model belongs to the Spark family, which carries its own rate limit. */ -function isCodexSparkModel(modelId: string | undefined): boolean { - return typeof modelId === "string" && modelId.includes(SPARK_MODEL_MARKER); +/** Tombstone old cache/DTO labels without changing unrelated custom windows or stores. */ +export function withoutRetiredCodexQuota | null>(quota: T): T | null { + if (!quota?.customWindows?.length) return quota; + const kept = quota.customWindows.filter(window => + window.label !== "GPT-5.3-Codex-Spark 5h" && window.label !== "GPT-5.3-Codex-Spark Weekly"); + if (kept.length === quota.customWindows.length) return quota; + const next = { ...quota }; + if (kept.length > 0) next.customWindows = kept; + else delete next.customWindows; + return snapshotHasUsage(next) || next.resetCredits !== undefined ? next as T : null; } export function parseUpstreamQuotaHeaders(headers: Headers, options?: { modelId?: string }): Omit | null { + if (isRetiredCodexSparkModel(options?.modelId)) return null; const primaryRaw = headers.get("x-codex-primary-used-percent"); const secondaryRaw = headers.get("x-codex-secondary-used-percent"); const tertiaryRaw = headers.get("x-codex-tertiary-used-percent"); @@ -486,10 +496,6 @@ export function parseUpstreamQuotaHeaders(headers: Headers, options?: { modelId? // it into weeklyPercent both discards the real weekly reading and leaves the account looking // exhausted long after the burst window resets. Duration decides, exactly as parseUsageQuota // already does for the WHAM payload — the two parsers must not disagree about the same data. - // One more attribution layer (#4122): on a Spark-family model response the sub-day primary is - // the MODEL-SPECIFIC limit, not an account window. Filing it as the account short tuple made - // one pool account display a 5h bar its identically-limited peers did not have, and fed a - // model limit to the account-policy readers (main-account hard lock, five-hour auto-refresh). const primaryIsShort = isExplicitShortWindowMinutes(primaryWindowMinutes); if (primaryIsMonthly) { @@ -506,21 +512,10 @@ export function parseUpstreamQuotaHeaders(headers: Headers, options?: { modelId? if (secondaryResetAt !== undefined) quota.weeklyResetAt = secondaryResetAt; } } else if (primaryIsShort) { - if (isCodexSparkModel(options?.modelId)) { - if (primaryPercent !== undefined) { - const sparkWindow: { label: string; percent: number; resetAt?: number } = { - label: SPARK_SHORT_WINDOW_LABEL, - percent: primaryPercent, - }; - if (primaryResetAt !== undefined) sparkWindow.resetAt = primaryResetAt; - quota.customWindows = [sparkWindow]; - } - } else { - if (primaryPercent !== undefined) quota.shortPercent = primaryPercent; - if (primaryResetAt !== undefined) quota.shortResetAt = primaryResetAt; - const minutes = windowMinutes_(primaryWindowMinutes); - if (minutes !== undefined) quota.shortWindowSeconds = Math.round(minutes * 60); - } + if (primaryPercent !== undefined) quota.shortPercent = primaryPercent; + if (primaryResetAt !== undefined) quota.shortResetAt = primaryResetAt; + const minutes = windowMinutes_(primaryWindowMinutes); + if (minutes !== undefined) quota.shortWindowSeconds = Math.round(minutes * 60); // The burst window vacates the primary slot, so the weekly reading is the secondary — which // is where it was all along. Without this the true weekly value is silently dropped. if (secondaryPercent !== undefined) { @@ -558,26 +553,9 @@ export function applyAccountQuotaFromUpstreamHeaders( const policyQuota = [ "x-codex-primary-used-percent", "x-codex-secondary-used-percent", "x-codex-tertiary-used-percent", ].some(name => isInvalidPolicyUsagePercent(headers.get(name))) ? null : filterMainPolicyMonthlyQuota(quota); - // A header-observed Spark window is a partial update against the WHAM-recorded custom windows: - // merge by label so the weekly Spark entry survives, and hydrate first so the first call in a - // process does not merge against an empty map. The merged list goes only to the legacy - // snapshot — the identity-bound policy evidence keeps exactly what this response said. - let legacyQuota = quota; - if (quota.customWindows !== undefined) { - hydrateAccountQuotasFromDisk(); - const existing = accountQuota.get(accountId)?.customWindows; - if (existing !== undefined) { - const incoming = new Map(quota.customWindows.map(window => [window.label, window])); - const merged = existing.map(window => incoming.get(window.label) ?? window); - for (const window of quota.customWindows) { - if (!existing.some(entry => entry.label === window.label)) merged.push(window); - } - legacyQuota = { ...quota, customWindows: merged }; - } - } const validHistory = !["x-codex-primary-used-percent", "x-codex-secondary-used-percent", "x-codex-tertiary-used-percent"] .some(name => isInvalidPolicyUsagePercent(headers.get(name))); - setAccountQuotaFromParsed(accountId, legacyQuota, writerGeneration, mainWriter, policyQuota, + setAccountQuotaFromParsed(accountId, quota, writerGeneration, mainWriter, policyQuota, options?.poolWriter && validHistory ? { writer: options.poolWriter, observedAt: Date.now(), source: "response-header", raw: quota } : undefined); } @@ -682,7 +660,9 @@ function hydrateAccountQuotasFromDisk(): void { for (const [accountId, quota] of Object.entries(parsed.quotas)) { if (!quota || typeof quota !== "object" || typeof quota.updatedAt !== "number") continue; if (now - quota.updatedAt > QUOTA_DISK_MAX_AGE_MS) continue; - if (!accountQuota.has(accountId)) accountQuota.set(accountId, quota); + const retained = withoutRetiredCodexQuota(quota); + if (!retained) continue; + if (!accountQuota.has(accountId)) accountQuota.set(accountId, retained); } } catch { // Corrupt/missing cache must never block routing or the dashboard. @@ -819,9 +799,6 @@ export function parseUsageQuota(data: WhamUsageResponse): Omit { - const name = String(additional.limit_name ?? "").toLowerCase(); - const feature = String(additional.metered_feature ?? "").toLowerCase(); - return feature === "codex_bengalfox" || name.includes("gpt-5.3-codex-spark"); - }); - const sparkWindows = [spark?.rate_limit?.primary_window, spark?.rate_limit?.secondary_window] - .filter((window): window is WhamUsageWindow => !!window); - const sparkShort = sparkWindows.find(window => { - const percent = normalizeUsagePercent(window.used_percent); - return percent !== undefined && isExplicitShortWindow(window); - }); - const sparkWeekly = sparkWindows.find(window => { - const percent = normalizeUsagePercent(window.used_percent); - const seconds = window.limit_window_seconds; - return percent !== undefined - && !isExplicitShortWindow(window) - && !isExplicitMonthlyWindow(window) - && (seconds === undefined || seconds >= WEEKLY_WINDOW_MIN_SECONDS); - }); - const sparkCustomWindows: Array<{ label: string; percent: number; resetAt?: number }> = []; - for (const [label, window] of [ - [SPARK_SHORT_WINDOW_LABEL, sparkShort], - [SPARK_WEEKLY_WINDOW_LABEL, sparkWeekly], - ] as const) { - const percent = normalizeUsagePercent(window?.used_percent); - if (percent === undefined) continue; - const sparkWindow: { label: string; percent: number; resetAt?: number } = { label, percent }; - const resetAt = normalizeResetAt(window?.reset_at); - if (resetAt !== undefined) sparkWindow.resetAt = resetAt; - sparkCustomWindows.push(sparkWindow); - } - if (sparkCustomWindows.length > 0) quota.customWindows = sparkCustomWindows; if (resetCredits !== undefined) quota.resetCredits = resetCredits; return hasKnownQuotaValue(quota) || resetCredits !== undefined ? quota : null; @@ -950,13 +895,10 @@ function historyWindows(quota: Omit): QuotaHist ...(window === "monthly" && quota.monthlyIsPrimaryWindow ? { monthlyIsPrimaryWindow: true } : {}), }); } - for (const [label, window] of [[SPARK_SHORT_WINDOW_LABEL, "short"], [SPARK_WEEKLY_WINDOW_LABEL, "weekly"]] as const) { - const raw = quota.customWindows?.find(row => row.label === label); - if (!raw || !Number.isFinite(raw.percent) || raw.percent < 0 || raw.percent > 100) continue; - windows.push({ family: "spark", window, usedPercent: raw.percent, - ...(typeof raw.resetAt === "number" && Number.isFinite(raw.resetAt) && raw.resetAt >= 0 ? { resetAtMs: resetAtToMs(raw.resetAt) } : {}), - }); - } + // No observation path emits retired Spark custom windows any more: the header parser refuses the + // retired model and the WHAM parser no longer derives its family, so recording one here could only + // resurrect evidence `withoutRetiredCodexQuota` tombstones on the way in. The `spark` family stays + // in the history schema so samples already persisted by older builds still parse. return windows; } diff --git a/src/codex/routing.ts b/src/codex/routing.ts index 0ec207f8a6..ae6e5d792a 100644 --- a/src/codex/routing.ts +++ b/src/codex/routing.ts @@ -18,7 +18,13 @@ import { seedPoolRotationAccount, selectPriorityTier, } from "./pool-rotation"; -import { CODEX_EXHAUSTED_USAGE_PERCENT, CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota, resetAtToMs } from "./quota"; +import { + CODEX_EXHAUSTED_USAGE_PERCENT, + CODEX_UNKNOWN_USAGE_SCORE, + getAccountQuota, + isRetiredCodexSparkModel, + resetAtToMs, +} from "./quota"; import { codexPlanKey, isThirtyDayOnlyCodexPlan } from "./plan"; import { MAIN_CODEX_ACCOUNT_ID, @@ -199,7 +205,7 @@ export type CodexCooldownSource = "retry-after" | "reset-derived" | "default"; * Add a new explicit group here only when its independent upstream quota is * confirmed, so shared limits never receive cross-model bypasses. */ -export type CodexQuotaScope = "shared" | "spark" | "reserve"; +export type CodexQuotaScope = "shared" | "reserve"; export type CodexQuotaRecoveryProbeClaim = { accountId: string; @@ -218,7 +224,7 @@ export type CodexQuotaRecoveryProbeProof = { /** * Requests without a resolved native model retain the historic one-account-per- * thread behavior. Requests with a known quota scope get an independent - * affinity so a Spark failover cannot displace the same thread's Terra/Luna + * affinity so a Reserve failover cannot displace the same thread's Terra/Luna * account (and vice versa). */ type BaseThreadAffinityScope = CodexQuotaScope | "legacy"; @@ -233,7 +239,6 @@ function isModelDetourAffinityScope(scope: ThreadAffinityScope): scope is ModelD } const NATIVE_MODEL_QUOTA_SCOPES: Readonly> = { - "gpt-5.3-codex-spark": "spark", [NATIVE_RESERVE_MODEL]: "reserve", }; @@ -688,7 +693,7 @@ export function claimDueCodexQuotaRecoveryProbes( { scope: undefined, health: upstreamHealth.get(account.id) }, ...[...(quotaScopedHealth.get(account.id) ?? [])].map(([scope, health]) => ({ scope, health })), ].filter((entry): entry is { scope?: CodexQuotaScope; health: CodexUpstreamHealth } => - // Generic WHAM evidence can recover only ordinary quota, never Spark or Reserve. + // Generic WHAM evidence can recover only ordinary quota, never Reserve. // Do not spend this account's one claim per pass on an independent scope and // delay the shared scope that the response can actually recover. (entry.scope === undefined || entry.scope === "shared") @@ -2693,6 +2698,9 @@ export function recordCodexUpstreamOutcome( if (writerGeneration < lastReconciledGeneration && !liveHealthAccountIds.has(accountId)) return; const now = meta.now ?? Date.now(); const outcomeClass = classifyCodexUpstreamOutcome(outcome, meta.denial); + // Reject retired quota evidence before stale-credential cleanup or any shared mutation. + if (outcomeClass === "quota" && isRetiredCodexSparkModel(meta.modelId) + && computeQuotaCooldown(meta).source === "reset-derived") return; const quotaScope = codexQuotaScopeForModel(meta.modelId); /* * Spend a stale credential failure BEFORE any branch reads health (#2892 gap 4 review). @@ -2864,7 +2872,7 @@ export function recordCodexUpstreamOutcome( const { until, source } = computeQuotaCooldown(meta); // A reset timestamp is an advisory quota-window announcement. When the // selected native model belongs to a confirmed independent group, preserve - // it there so a different group (Spark versus the shared native quota) can + // it there so a different group (Reserve versus the shared native quota) can // still reach upstream. Explicit Retry-After/default 429s remain account-wide. if (source === "reset-derived" && quotaScope) { const prior = scopedHealthFor(accountId, quotaScope); @@ -2889,7 +2897,7 @@ export function recordCodexUpstreamOutcome( }); // The shared native scope is the existing account-wide native behavior: // threads must leave it and new requests should prefer an eligible account. - // Spark remains isolated so a same-account Terra/Luna combo fallback can run. + // Reserve remains isolated so a same-account Terra/Luna combo fallback can run. if (quotaScope === "shared" && !meta.fixedAccount) { clearThreadAccountMapForAccount(accountId); notePoolRotationFailure(POOL_KEY_CODEX, accountId); diff --git a/src/config.ts b/src/config.ts index 9f518dbcd0..4633860e79 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1338,9 +1338,6 @@ const configSchema = z.object({ // A malformed hand edit must degrade to false without discarding providers, accounts, // or the exact selector map. Live writes remain strict. codexAccountPickerEnabled: z.boolean().optional().catch(false), - // Same degrade-not-reject rule: a malformed hand edit hides Spark rather than discarding the - // whole config. Hidden is also the default, so `catch(false)` and the default agree. - showCodexSparkQuota: z.boolean().optional().catch(false), resetCreditAutoRedeem: z.object({ enabled: z.boolean().optional(), leadTimeMinutes: z.number().int().min(1).max(60).optional(), diff --git a/src/providers/quota.ts b/src/providers/quota.ts index dfe887b5e2..1ab17a3603 100644 --- a/src/providers/quota.ts +++ b/src/providers/quota.ts @@ -3,9 +3,8 @@ import { effectiveCodexAuthAccountId, fetchMainAccountInfoSnapshot, listCodexAuthAccountsSnapshot, - withSparkVisibility, } from "../codex/auth-api"; -import type { StoredAccountQuota } from "../codex/quota"; +import { withoutRetiredCodexQuota, type StoredAccountQuota } from "../codex/quota"; import { isMainAccountIdentityGenerationLive } from "../codex/main-account-cache"; import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account"; import { codexPlanKey } from "../codex/plan"; @@ -224,12 +223,10 @@ function providerQuotaFromCodexQuota( quota: StoredAccountQuota | Omit | null | undefined, ): CodexCapacityQuota | null { if (!quota) return null; - // Every Codex-sourced provider report funnels through here — the pooled path via - // listCodexAuthAccountsSnapshot and the `direct` path via fetchMainAccountInfoSnapshot, which - // never touches the Codex Auth DTO. Applying the Spark preference at this one point is what - // stops the row surviving on /api/provider-quotas after the operator switched it off. - quota = withSparkVisibility(quota ?? null) ?? quota; - return { + // Direct snapshots bypass account DTOs; sanitize here as well as at ingestion. + quota = withoutRetiredCodexQuota(quota); + if (!quota) return null; + const projected: CodexCapacityQuota = { ...(quota.shortPercent !== undefined ? { fiveHourPercent: quota.shortPercent } : {}), ...(quota.shortResetAt !== undefined ? { fiveHourResetAt: quota.shortResetAt } : {}), ...(quota.weeklyPercent !== undefined ? { weeklyPercent: quota.weeklyPercent } : {}), @@ -239,6 +236,7 @@ function providerQuotaFromCodexQuota( ...(quota.customWindows !== undefined ? { customWindows: quota.customWindows } : {}), updatedAt: "updatedAt" in quota ? quota.updatedAt : Date.now(), }; + return hasQuotaRows(projected) ? projected : null; } /** Hash only presentation-relevant state; account ids and email addresses never enter the key. */ diff --git a/src/responses/hosted-tool-policy.ts b/src/responses/hosted-tool-policy.ts index e9a99c2493..8502e6b4fc 100644 --- a/src/responses/hosted-tool-policy.ts +++ b/src/responses/hosted-tool-policy.ts @@ -3,7 +3,6 @@ const UNSUPPORTED_HOSTED_TOOLS: ReadonlyArray<{ match: (model: string, baseUrl?: string) => boolean; tools: ReadonlySet; }> = [ - { match: model => model.includes("codex-spark"), tools: new Set(["image_generation", "tool_search"]) }, { match: (model, baseUrl) => model === "grok-4.6" && baseUrl?.replace(/\/+$/, "") === "https://opencode.ai/zen/go/v1", diff --git a/src/server/management/config-routes.ts b/src/server/management/config-routes.ts index 0390ea8d76..03d549a24a 100644 --- a/src/server/management/config-routes.ts +++ b/src/server/management/config-routes.ts @@ -316,9 +316,6 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise