Skip to content

fix(providers): Copilot mixed-wire routing, DeepSeek service_tier + reasoning replay, Claude 4.6/4.7 1M windows - #889

Closed
lidge-jun wants to merge 28 commits into
devfrom
codex/wt3-exec
Closed

fix(providers): Copilot mixed-wire routing, DeepSeek service_tier + reasoning replay, Claude 4.6/4.7 1M windows#889
lidge-jun wants to merge 28 commits into
devfrom
codex/wt3-exec

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Summary

Three confirmed must-fix provider-wire bugs, one consolidated branch (wt3 lane of the bugfix campaign; unit docs in devlog/_plan/260802_wt3_provider_wire/). Each bug landed as its own PABCD cycle with an independent audit gate.

1. Copilot mixed-wire routing (closes #748, supersedes the routing half of #746)

The github-copilot preset's provider-wide openai-chat adapter breaks Responses-only models: gpt-5.4 rejects /chat/completions for real Codex-agent traffic (function tools + reasoning), verified externally (litellm#23332, JetBrains LLM-29711, #748 field runs). The fix declares seven verified models (gpt-5.3-codex, gpt-5.4, gpt-5.4-mini, gpt-5.5, gpt-5.6-luna, gpt-5.6-sol, gpt-5.6-terra) as registry modelWireDefaults — the precedence mechanism already on the tree (hard pin → explicit modelAdapters → registry default → provider adapter). Chat-served models are untouched; gpt-5.4-nano stays out (no field report) as a documented modelAdapters opt-in. #746's sampling/credential-replay parts are NOT included here — they remain a separate parity/security unit.

2. DeepSeek: service_tier capability gate + reasoning replay (supersedes #860; local half of #875)

  • service_tier is an OpenAI-only Responses parameter, but fast mode injected it for every Responses provider. A provider-level supportsServiceTier capability now gates it after the final route is settled, adopting fix(responses): gate service tiers by provider capability #860's reviewed fail-closed semantics: canonical openai/openai-apikey keep fast-mode behavior, deepseek/volcengine-agent-plan strip, unclassified providers fail closed unless explicitly opted in. Stripping also clears options.serviceTier so logging never mislabels a removed tier.
  • Root cause of [Bug] DeepSeek V4 Flash Responses route stalls after tool calls #875's local half: sanitizeReasoningInputContent() blanked reasoning content for EVERY Responses provider — a rule only the ChatGPT native backend needs. DeepSeek's Responses API accepts plaintext reasoning replay, so providers flagged preserveResponsesReasoningContent keep it (ocxr1 envelopes still stripped). [Bug] DeepSeek V4 Flash Responses route stalls after tool calls #875 stays open: the "no follow-up request sent at all" observation has no local explanation (triage comment posted).
  • Both capabilities are registry-only (never seeded into saved config), so the management API's canonical-seed comparison is untouched; enrichProviderFromRegistry + router backfill supply them without overriding explicit config.

3. Claude 4.6/4.7 1M context windows (supersedes #839 and #854 as one fix)

ANTHROPIC_MODEL_CONTEXT_WINDOWS omitted claude-opus-4-7, claude-opus-4-6, claude-sonnet-4-6 — they advertised max_input_tokens: null, emitted no [1m] picker row, and Claude Code accounted them at 200k. All three are documented at 1M by Anthropic (2026-02-05 / 2026-04-16 / 2026-02-17; no beta header required per the official context-window contract). Also ports #854's second half: generated subagent defs now mark [1m] only on authoritative ≥1M windows (a 372K route was previously written [1m] with no compaction pairing), preserving genuine routed [1m] ids and provider caps. Generated jawcode metadata and the stale generator override are aligned.

Tests

  • New: tests/github-copilot-wire-defaults.test.ts (22), tests/service-tier-capability.test.ts (12), tests/deepseek-reasoning-replay.test.ts (6) — incl. captured-upstream-URL and live handleResponses payload proof.
  • Modified: tests/claude-agents-inject.test.ts (372k contract + 5 regressions), tests/codex-catalog.test.ts (1M contract), tests/provider-registry-parity.test.ts (all three windows).
  • Fresh gates on this head: bun run typecheck exit 0, bun run test 7043 pass / 0 fail (484 files, verified on a clean clone on a separate host), bun run privacy:scan passed.

Docs

docs-site reference (supportsServiceTier, preserveResponsesReasoningContent, updated modelAdapters row) and guides (Copilot routing precedence, capability-gated fast tier) in EN + ko/ja/zh-cn/ru.

Base note

Local dev is currently ahead of origin/dev (parallel sessions' unpushed commits); this branch is based on local dev tip, so the PR diff includes those commits until dev is pushed. The 15 wt3 commits are the tip of the branch.

🤖 Generated with Codex

Summary by CodeRabbit

  • New Features

    • Added capability-aware fast-tier handling for compatible routed providers, with explicit opt-in for custom gateways.
    • Added GitHub Copilot routing defaults for selected GPT-5 models.
    • Added support for preserving plaintext reasoning content during compatible Responses replay.
    • Added Claude context-window metadata updates, including expanded 1M-token support.
    • Added foundational Codex prompt-layer management and validation.
  • Documentation

    • Updated provider configuration and routing guidance across supported locales.
  • Bug Fixes

    • Improved context-marker handling for routed and subagent models.

…dence

Fourteen documents: the ask decomposed, four read-only research lanes against
upstream 2b5bdcf67 and dev f9b9440, a UX design, seven implementation phases,
and a deferred Theme stub.

Two findings reshape the build. model_instructions_file replaces the entire base
prompt rather than adding to it, so custom layers compose into
developer_instructions instead. And the Logs tab the ask names as the model uses
exclusive hash-persisted tabpanels, not the SectionTabs scroll-spy strip that
shares the name.

The hard constraint - layers that must never be disableable - is now a proven
set rather than a guess: base/model instructions, model-switch, AGENTS.md,
realtime, plugins, and non-Skills extension layers have no include_ key anywhere
in the schema.
…eference)

Survey unit for applying client integrations as an on/off switch from the
management API: cc-switch additive/exclusive toggle mechanics, per-client
requirements for Hermes/OpenClaw/Kimi Code/Gajae Code, and design options
with a five-state read-back model and risk register. Research only; no
production code.
An independent audit returned FAIL on twelve counts. The central architecture
survived - model_instructions_file replaces the base prompt, so custom layers
belong in developer_instructions - but the persistence protocol would have
corrupted user config.

Fencing layer bodies inside a TOML multiline string was the worst of it. A body
containing triple quotes terminates the value and a backslash is an escape, so
arbitrary prose could produce a file Codex cannot parse at all. The sidecar JSON
made it worse by requiring two-way reconciliation that no rule actually defined.

Storage is now one owned JSON file as the single source of truth, with
developer_instructions as a generated projection written through a real TOML
serializer and verified by a reparse. No fences, so no text can collide with a
delimiter. Revision hashes and a write-order contract close the concurrency hole
that atomic rename never covered.

The layer inventory was also wrong: it listed Plugins as impossible to disable
while session/mod.rs checks Feature::Plugins. A single cannot-be-turned-off list
is replaced by five explicit classes, exported once from WP1 and consumed by
route and GUI alike, with a partition test. The API derives its refusals from
that inventory rather than a hand-maintained deny-list.

Two overclaims are retracted. effective becomes defaultedUserValue because
opencodex reads one config layer out of eight. The linter's size rule drops its
citation of the 32 KiB AGENTS.md budget, which governs project-doc loading and
never constrained developer_instructions.

Phases re-sliced to remove four forward dependencies, and the missing-config
state no longer disables the switch that was supposed to create the file.
Round 2 of the audit rejected the smol-toml plan on six counts. A live probe on
this machine settled it harder than the audit could: Bun 1.3.14's TOML.parse
transposes \t and \f, rejects \u0007, and does not trim the newline after an
opening triple quote. Codex parses with Rust toml_edit. So a verify-by-reparse
design would have checked our encoding against a parser with known-transposed
escapes and reported success on a file Codex might read differently.

The fix is to stop needing a parser. Bodies accept printable Unicode, spaces and
newlines; tabs normalize to four spaces and CRLF to LF; control characters are
refused. Within that set the escaping is three unambiguous rules, and
verification becomes a byte comparison. No production dependency is added, which
also retires the unreviewed dependency the audit flagged - it was BSD-3-Clause
rather than MIT, absent from the lockfile, and its parse() never exposed the
source spans the plan claimed.

The transaction is rebuilt around a journal. config.toml is written first now,
so a failed request leaves the source of truth untouched rather than committing
JSON and returning an error. Reads never write: drift is reported as state and
resolved by an explicit POST, because an HTTP GET must not modify a user's
config. A missing store with a live owned projection is no longer treated as an
empty store - that would have erased the active prompt on the next write.

Refusing an externally authored developer_instructions is no longer a dead end:
an adopt flow shows the raw line, offers a copy, and imports it on confirmation.

Also corrected: the assembly table now uses the five-class vocabulary instead of
contradicting section 4, two citations the audit judged overstated are narrowed
to what the source actually shows, and 003 no longer mandates an effective field
that 010 and 005 deliberately defer.
Round 3 confirmed the Bun.TOML measurements independently and accepted the
restricted encoder, then found the recovery algorithm destructive. The rule was
'if either target differs from the post-image, rewrite both'. Crash after the
first write, let the user or Codex edit config.toml, and recovery overwrites
their edit with a stale post-image.

Recovery now classifies each target against both recorded hashes and refuses to
write any file matching neither - that file belongs to someone else. One
unrecognised target aborts the whole recovery with recovery_required rather than
rolling forward into a state nobody intended.

The commit point was also mixing two models: journal-rename-is-commit alongside
a rollback that claimed a failed request changes nothing. The journal is now
prepared intent, commit is both targets matching the post-image, and rollback is
the only failure path. Durability adds parent-directory fsync with a documented
Windows fallback.

Stale-lock breaking could delete a successor's lock and admit two writers.
Takeover is now an atomic rename to a token-quarantined name that exactly one
contender can win, and release deletes only a lock whose token still matches.

Adoption imported the raw TOML source line as the body, so a value containing an
escaped newline would have become twelve literal characters. It now decodes
through the inverse of our own encoder, accepting only the three escapes we
emit, and previews both the source line and the decoded body. Malformed
marker-owned lines get the same flow instead of being a permanent lockout, and
store-missing repair is described as what it is: salvage of one concatenated
string, with the losses listed and a backup written first.

Character validation is defined over Unicode scalar values, rejects unpaired
surrogates and C1 controls, measures caps in UTF-8 bytes after normalization,
and reports code-point positions. One golden fixture parsed by real toml_edit
now backs the claim that byte equality means Rust reads what we intended.
Campaign preparation (docs-only): five units under devlog/_plan/260802_wtN_*
with 000 research + 010 implementation roadmaps, claim ledgers verified by a
lunasearch fan-out (Anthropic 1M windows, Copilot mixed-wire, DeepSeek
service_tier, WHATWG extension origins, POSIX rename-over-symlink).

wt1 update-path: PR #871, issue #879 (star-prompt deferral leakage), #557 optional
wt2 zero-leak: PRs #840 #841 #843 #844 #845 #847 (tracker #820)
wt3 provider-wire: PRs #746 #860 #839/#854, issue #875 triage, #616/#837 optional
wt4 server-config: PRs #850 (CORS origin confusion), #869 (symlink destruction)
wt5 windows-service: PRs #868, #861 (issue #848)
…tract

Round 4 found the transaction still holding both models at once: journal
existence was called the commit point while rollback claimed a failed request
changes nothing. The journal is now unambiguously prepared intent - commit is
deleting it after both targets verify against the post-image - so a journal
found on disk always means the transaction never committed, and recovery rolls
back. The one exception is not a roll-forward: when both targets already match
the post-image the writes had finished and only the commit step was missing.

An unparseable journal previously implied restoring a pre-image that lives
inside the same damaged document. The journal is now a checksummed envelope and
any failure of that checksum is recovery_required with nothing read and nothing
written.

The pre-rename guard only rehashed config.toml, leaving the store overwritable
by a third party between compare and write; each target now re-verifies its own
bytes immediately before its own rename, and rollback applies the same
pre/post/neither classification. Post-write verification compares complete bytes
instead of looking for our two lines, so another writer changing an unrelated
key can no longer be reported as success.

WP1's exported contract was missing drift, adopt, repair, salvage, and recovery
entirely, so WP2 could not have been built from it. All are declared now, with
read-only preview DTOs, and every filesystem mutation stays in WP1.

Modes were specified for config.toml alone. The store, journal, salvage backup,
and lock all carry prompt bodies or metadata and are now 0600 at creation rather
than by later chmod. Salvage backups use exclusive creation with a random suffix
and abort the operation if they cannot be made durable.

Also: the 001 assembly table fix from the previous round never landed on that
block - the audit was right that the committed file still carried ALWAYS-ON. It
is corrected here, along with the last locked/features array references.
GO-WITH-FIXES (5 blockers) all folded: CORS fix retargeted to
src/server/auth-cors.ts isExtraAllowedOrigin with full origin_rejected
coverage instruction; ANTHROPIC_MODEL_CONTEXT_WINDOWS correctly assigned
to registry.ts:217; wt2/wt3 shared-file coordination named in both units;
atomicWriteFile caller audit switched to grep-at-P with oauth/store.ts
named explicitly.
…ds on

Round 5 opened core/src/mcp.rs and disproved two of my drafts at once:

    let plugins_available =
        selected_plugin_available || !loaded_plugins.capability_summaries().is_empty();

Feature::Plugins feeds plugins_config_input and therefore the right operand,
but selected_plugin_available is an independent OR path that can make the
section emit regardless. The flag influences emission; it does not gate it. The
citation both earlier drafts leaned on, session/mod.rs:3422-3430, gates
recommended plugin candidates rather than this section. Plugins is now
runtime-conditional in the table, in class D, and out of the feature list, with
the residual question recorded as UNKNOWN rather than asserted.

Three other corrections. A stale paragraph still told recovery to restore from
the pre-image of a journal that had failed its own checksum - exactly the
impossible path the envelope section rejects - and it is deleted. Adoption now
states its five ordered steps and previews the post-normalization body, because
previewing decoded text while committing normalized text would show one string
and save another. WP2's repair contract gains owned-malformed, which it omitted
entirely, and stops calling store recovery reconstruction when 010 defines it as
lossy salvage.

Also enumerated the lock interleaving the audit asked for - A quarantines, B
acquires, A's wx fails and A retries without touching B's lock - and made
Windows write-through an explicit WP1 acceptance gate rather than an assumption:
establish what Bun exposes, or fail closed as recovery_required with a test on
that branch.
The stale UX paragraph promised a built-in dialog showing rendered prompt text
and an effective value. Neither exists: Codex exposes no API for rendered layer
bodies, and opencodex reads one config layer out of eight. 005 now says what the
dialog actually shows and names the two things it does not.

SalvagePreview returned a backupPath, which either reserves a name during a
read-only preview or promises one that exclusive creation may refuse at commit.
It returns backupDir now; the real path is created during the confirmed
mutation and comes back in the WriteResult.

Adoption tests 26a-26g enumerate the pipeline the prose already required:
normalization, control rejection with position, post-normalization overflow,
composed overflow, and the property that matters most - the previewed body is
byte-identical to the committed one.

The Windows durability gate stays open by design. It is an acceptance criterion
for WP1, not something a document can settle.
…ollback

UX design spec for merging the API/Claude/Grok pages into one Integrations
tab: hash-routed sub-tabs, ops-hero with install detection and capability-
aware switches, per-client settings, and a two-level rollback contract
(operation journal + restore preflight). Design only; no components.
First slice of WP1: the canonical layer inventory plus the encoding contract.
No file writes yet.

The encoder exists because Bun.TOML cannot verify our work. Measured on 1.3.14,
its parser transposes \t and \f, rejects \u0007, and does not trim the newline
after an opening triple quote. Codex parses with Rust toml_edit, so a
verify-by-reparse loop could report success on a file Codex reads differently.

So bodies are restricted instead: tabs become four spaces, CRLF becomes LF, and
control characters, DEL, C1, and unpaired surrogates are refused with a
code-point position. Within that set the escaping is three total rules and the
emitted line is checked against a hand-written grammar that shares no code with
the encoder. The decoder is deliberately narrow - it accepts only the three
escapes we emit and refuses \t, \f, \b, \r and \uXXXX rather than guessing.

The inventory classifies every layer into exactly one of five classes, and the
tests assert the partition is total and disjoint so the API can derive
switchability from it instead of a hand-maintained deny-list. Plugins is
runtime-conditional: core/src/mcp.rs:200 ORs selected_plugin_available with the
loaded summaries, so [features] plugins feeds only the right operand.

The surrogate guard was driven red once to prove the test is not vacuous.
The scanner flags concrete home paths, and three example snippets carried one.
Nothing sensitive was in them, but the gate is the gate.
…d mechanisms

wp-a: modelWireDefaults on the github-copilot registry entry (mechanism
already on tree: hard pin > modelAdapters > registry defaults > provider
adapter); conservative 6-model set, nano/sol lead-only via modelAdapters.
wp-b: adopt PR #860's capability file map + #875 root cause found —
sanitizeReasoningInputContent blanks plaintext reasoning for every
Responses provider (openai-responses.ts:35, called :1027); scope it.
wp-c: registry.ts:217 map + authoritative-window [1m] predicate.
1: docs target corrected to reference/configuration/providers.md + locales
2: exact seed before/after array, defaultModel unchanged, nano/sol excluded
3: evidence table moved to 000 (LEXICO-SPLIT-01)
4: per-model pi.dev provenance + two-leg selection rule (sol/nano out)
…ve seed policy

1: gpt-5.6-sol meets the two-leg rule identically to luna/terra (#748 field
report + pi.dev Responses declaration + JetBrains conditional chat failure);
excluding it was an inconsistent rule application. nano stays out (no field
report leg).
2: seed policy made explicitly additive (no removals); gpt-5-mini added as a
verified chat model so the chat regression fixture is honest; scenario 4
corrected.
…ecedence fixture

claim ledger and selection reference now say seven built-in / sol verified;
scenario 3 uses gpt-5.4-nano or gpt-5-mini for the opt-in direction since
sol is itself a default and cannot prove override precedence.
github-copilot fronts a mixed-wire catalog: gpt-5.3-codex, gpt-5.4,
gpt-5.4-mini, gpt-5.5, gpt-5.6-luna, gpt-5.6-sol, gpt-5.6-terra reject
/chat/completions for real Codex-agent traffic (function tools +
reasoning). Declare them as registry modelWireDefaults so every inbound
wire rides Responses while the provider-wide adapter stays openai-chat
for the chat-served catalog. Explicit modelAdapters still win in both
directions; gpt-5.4-nano stays out (no field report) as a documented
override example.

Evidence: issue #748 field runs, pi.dev wire declarations,
litellm#23332 (gpt-5.4), JetBrains LLM-29711 (gpt-5.6-sol).
Tests: tests/github-copilot-wire-defaults.test.ts — 22 cases incl.
captured-upstream-URL replay proof on all three inbound wires.

Consolidates the routing half of PR #746 (its sampling/credential-replay
half remains a separate parity/security unit). Closes #748.
…s escape hatch

reference/configuration/providers.md (EN + ko/ja/zh-cn/ru): the modelAdapters
row now names the built-in Copilot Responses-only defaults and the opt-in
path for models without one (gpt-5.4-nano example). guides/providers.md
gains the routing-precedence note (pin > modelAdapters > registry default
> provider adapter).
…ntics

adopts #860's reviewed fail-closed semantics (undefined strips; explicit
config is the escape hatch); names preserveResponsesReasoningContent as the
reasoning-replay flag following the statelessResponses flow; maps every
edit point on the current tree.
1: research survey moved to 000 (LEXICO-SPLIT-01)
2: guides/codex-app-models.md x5 locales added to docs file map
3: options.serviceTier clearing required + tested
4: ocxr1-strip-under-preservation negative case added
5: comment wording calibrated (accepted replay = verified; required = inference)
…epSeek reasoning replay

Two DeepSeek wire fixes, one capability flow:

1. service_tier is an OpenAI-only Responses parameter, but fast mode
   injected it for every Responses provider. A provider-level
   supportsServiceTier capability now gates it after the final route is
   settled: canonical openai/openai-apikey keep fast-mode inject/remove
   (unset fast mode preserves a caller value); deepseek and
   volcengine-agent-plan strip it; unclassified providers fail closed
   unless explicitly opted in. Stripping also clears
   options.serviceTier so logging never mislabels a removed tier.
   Adopts PR #860's reviewed fail-closed semantics.

2. sanitizeReasoningInputContent blanked reasoning content for EVERY
   Responses provider — a rule only the ChatGPT native backend needs.
   DeepSeek's Responses API accepts plaintext reasoning replay, so
   providers flagged preserveResponsesReasoningContent keep it
   (ocxr1 envelopes are still stripped). Fixes the local half of #875:
   continuations after tool calls no longer reach DeepSeek with emptied
   reasoning items.

Both fields flow registry -> providerConfigSeed -> enrichProviderFromRegistry
-> router backfill without overriding explicit config.
Tests: tests/service-tier-capability.test.ts + tests/deepseek-reasoning-replay.test.ts
(18 cases incl. live handleResponses payload capture).
…ent reference rows, capability-gated fast-tier guides

reference/configuration/providers.md (EN/ko/ja/zh-cn/ru): rows for the two
new provider fields. guides/codex-app-models.md (all five locales): the
blanket 'routed non-OpenAI models strip service-tier metadata' wording is
now the capability-gated fail-closed behavior with the explicit opt-in —
closing #860's open docs review issue.
Seeding them via providerConfigSeed broke the management API's canonical
openai seed comparison (exact key set) — 4 management-provider-validation
failures. Follow the modelWireDefaults philosophy instead: the registry
holds the defaults, providerConfigSeed stays free of them so an explicit
user value stays distinguishable, and enrichProviderFromRegistry +
the router backfill supply them from the entry directly.
…ts-inject + generator override

desktop-profile and model-info guards already landed; the open halves are
the registry map, #854's withSubagentContextMarker port, and the stale
jawcode generator override contradicting its own committed output.
…ts + full regression set

my stale check misread the bedrock rows: the anthropic section of the
generated metadata still pins sonnet-4-6 at 200k with a catalog test
blessing it; file map now includes the generated rows, codex-catalog
test, registry-parity assertions, and #854's complete test set
(modified 372k test + five additions incl incomplete-metadata branch).
…tative [1m] in generated profiles

Two halves, consolidated from PRs #839 and #854:

1. ANTHROPIC_MODEL_CONTEXT_WINDOWS omitted claude-opus-4-7, claude-opus-4-6,
   and claude-sonnet-4-6 although all three ship in ANTHROPIC_MODELS —
   they advertised max_input_tokens null, emitted no [1m] picker row, and
   Claude Code accounted them at its 200k default. All three are
   documented at 1M by Anthropic (Opus 4.6 2026-02-05, Opus 4.7
   2026-04-16, Sonnet 4.6 2026-02-17). The generated jawcode metadata and
   its 200k-pinned catalog test move with the registry; the stale
   CONTEXT_WINDOW_OVERRIDES generator pin is removed.

2. Generated subagent defs marked [1m] with the MAIN-SESSION auto-context
   predicate, so a 372K route was written [1m] into generated profiles —
   accounted at 1M with no compaction pairing in the subagent. Generated
   defs now mark only authoritative >=1M windows, strip inherited unsafe
   markers to bare, preserve genuine routed [1m] ids (kimi/k3[1m]) and
   provider caps, and keep selectors with unknown windows as-was.
   Main-session env-slot marking is unchanged.

Tests: 372k roster test corrected + five new regressions (catalog-derived
1M markers, routed [1m] preservation, 350K cap unmarking, marker-case
precedence, incomplete-metadata preservation), catalog 1M contract,
registry parity assertions for all three windows.
@lidge-jun lidge-jun closed this Aug 2, 2026
@lidge-jun
lidge-jun deleted the codex/wt3-exec branch August 3, 2026 10:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant