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
Closed
fix(providers): Copilot mixed-wire routing, DeepSeek service_tier + reasoning replay, Claude 4.6/4.7 1M windows#889lidge-jun wants to merge 28 commits into
lidge-jun wants to merge 28 commits into
Conversation
…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.
This was referenced Aug 2, 2026
This was referenced Aug 3, 2026
This was referenced Aug 11, 2026
Closed
Closed
This was referenced Aug 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-copilotpreset's provider-wideopenai-chatadapter breaks Responses-only models:gpt-5.4rejects/chat/completionsfor 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 registrymodelWireDefaults— the precedence mechanism already on the tree (hard pin → explicitmodelAdapters→ registry default → provider adapter). Chat-served models are untouched;gpt-5.4-nanostays out (no field report) as a documentedmodelAdaptersopt-in. #746's sampling/credential-replay parts are NOT included here — they remain a separate parity/security unit.2. DeepSeek:
service_tiercapability gate + reasoning replay (supersedes #860; local half of #875)service_tieris an OpenAI-only Responses parameter, but fast mode injected it for every Responses provider. A provider-levelsupportsServiceTiercapability now gates it after the final route is settled, adopting fix(responses): gate service tiers by provider capability #860's reviewed fail-closed semantics: canonicalopenai/openai-apikeykeep fast-mode behavior,deepseek/volcengine-agent-planstrip, unclassified providers fail closed unless explicitly opted in. Stripping also clearsoptions.serviceTierso logging never mislabels a removed tier.sanitizeReasoningInputContent()blanked reasoningcontentfor EVERY Responses provider — a rule only the ChatGPT native backend needs. DeepSeek's Responses API accepts plaintext reasoning replay, so providers flaggedpreserveResponsesReasoningContentkeep it (ocxr1envelopes 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).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_WINDOWSomittedclaude-opus-4-7,claude-opus-4-6,claude-sonnet-4-6— they advertisedmax_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
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 livehandleResponsespayload proof.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).bun run typecheckexit 0,bun run test7043 pass / 0 fail (484 files, verified on a clean clone on a separate host),bun run privacy:scanpassed.Docs
docs-sitereference (supportsServiceTier,preserveResponsesReasoningContent, updatedmodelAdaptersrow) and guides (Copilot routing precedence, capability-gated fast tier) in EN + ko/ja/zh-cn/ru.Base note
Local
devis currently ahead oforigin/dev(parallel sessions' unpushed commits); this branch is based on localdevtip, so the PR diff includes those commits untildevis pushed. The 15 wt3 commits are the tip of the branch.🤖 Generated with Codex
Summary by CodeRabbit
New Features
Documentation
Bug Fixes