Conversation
A named [providers.<name>] table with wire = "responses" (or "anthropic") minted a Chat Completions candidate: RouteRequest had no wire channel, so candidate.protocol() came from Custom's backward-compatible static policy and every per-turn client built by DeepSeekClient::from_candidate bound Chat regardless of the table's dialect. The ambient spawn-time client (DeepSeekClient::new) and provider_capability_with_wire honored the override, so hosts that resolve routes per turn - the Hmbown#3384 architecture - silently talked to {base}/chat/completions while their config said Responses. RouteRequest gains a wire_override honored only for ProviderKind::Custom (built-ins keep their descriptor policy); the tui route layer populates it from the active table's dialect (custom_wire_override_for, the same source provider_wire_format_for_config reads), and the client-internal rebind/request/limit resolutions pin the transport's own wire so an engine-internal model switch cannot downgrade it. The minted candidate is now wire-true end to end: receipts, preflight, and the per-turn client all read the same protocol. Pinvou PR Hmbown#625 routes catalog GPT models through such a table and hit exactly this gap (three review rounds missed it because no test pinned the wire on the runtime path). Signed-off-by: asto <asto18089@126.com>
Encrypted reasoning-item capture was gated on the Codex provider, so a wire = "responses" Custom table streamed reasoning items that were never persisted and replayed id-less function_call continuations without their paired reasoning items - the per-round reasoning-continuity loss the OpenAI cookbook documents for store:false. Widen the capture gate to every Responses route that sends include: ["reasoning.encrypted_content"] (Codex plus Custom tables; DeepSeek and Concentrate stay excluded with their own contracts). The replay gate already matches the captured provider tag, so Custom states replay unchanged; both directions are pinned for the Custom route. Signed-off-by: asto <asto18089@126.com>
Bumps the CodeWhale gitlink to the T9 candidate head (98d4709b905ca5a5abce42da4a2b322c4a88d7b5, Pinvou/CodeWhale#79): the runtime-route resolver now honors a custom table's wire dialect, so the responses-wire routing this PR builds in build_dt_config actually drives the /responses client on every turn, with encrypted reasoning capture/replay on the route. Candidate-period registration: fork-guard EXPECTED_HEAD/COMMITS point at the candidate (50 commits above upstream), six T9 fingerprints pin the engine behaviors, and both registers document the topic with the re-pin obligation - once #79 squash-merges into pinvou3-clean, the landing wave re-pins the public maintenance-branch head and recycles the candidate constants. Signed-off-by: asto18089 <asto18089@126.com> Signed-off-by: asto <asto18089@126.com>
Fresh review of
|
asto18089
left a comment
There was a problem hiding this comment.
Companion note from the fresh paired review of Pinvou/pinvou-agent#625 (9 lanes, head 98d4709b9 verified against base 61cb769be). The core mechanism holds up end-to-end: the wiremock gate drives the real per-turn construction (resolve_runtime_route → from_candidate → create_message_stream), the dialect is single-sourced through provider_wire_dialect, legacy behavior with wire_override: None reduces byte-identically to the old path, and capture/replay widening is order-safe and compaction-safe. One required item and a few follow-ups from the engine lanes:
Required — app-server ingress stays wire-blind for the tables this PR introduces. crates/app-server/src/chat_completions.rs:115-123 resolves with wire_override: None; Custom is Fixed(Chat), so route.protocol() is Chat and the provider_wire_format_unsupported guard at :351-361 can never fire for a wire = "responses" table — the chat body goes to {base}/chat/completions (:257) and fails with an opaque upstream 404 instead of the clean rejection the guard promises. Either thread the override (or read the table and fail closed) here, or declare the boundary in the fork register + PR body with a tracked follow-up.
Follow-ups (non-blocking, evidence-checked):
custom_wire_override_for(config)reads the unscoped config on the identity-scoped path (route_runtime.rs:805-814vsroute_configscoped at:793) — a cache-replay/validation resolution for a non-current custom identity reads the globally-selected table's wire; fix is passing&route_config.- Replay is endpoint-blind across Custom routes (tag+model only,
responses.rs:821-825): a base-url edit re-sends gateway A'sencrypted_contentto gateway B. Surfaced 400, provider-encrypted blob — consider endpoint identity in the tag. - Two guards are unpinned: deleting the
wire_format == Responseshalf of the capture gate (responses.rs:174-177) passes the suite, and the missing/emptyencrypted_contentfilter (responses.rs:506) survives mutation. - A malformed
wirevalue silently degrades to Chat — no validation warning, no test. - The wire-alias matchers now exist in two byte-identical copies (
config.rs:9705/9721vsclient.rs:1826/1842); alias drift would split ambient vs candidate wire. provider_readiness::route_is_valid_for_modelpasseswire_override: Nonefor Custom — outcome-identical today, but it validates a protocol candidate that is not the per-turn one.
Nit: stray #[allow(clippy::too_many_arguments)] sits above the doc comment of the one-arg custom_wire_override_for (route_runtime.rs:451), copied from the many-arg neighbor below.
Review round 1 of #79 (B1, S1): the wire override was read from the ambient `Config` while `resolve_runtime_route_for_identity*` resolves the endpoint from the identity-scoped clone, so on every pinned turn path (per-thread routing, `reload_config`, fleet pins, session restore) the override could name a different `[providers.<name>]` table than the one supplying the endpoint: a pinned responses table rode Chat, and an ambient responses table wired Chat-only relays for `/responses` (a regression against the static-policy base for multi-table setups). The override now comes from the scoped clone and is computed only for `ProviderKind::Custom`. The per-config `wire` dialect parse moves into the config crate next to the field it reads (`wire_dialect_override` plus the alias predicates), so the tui wire-format/capability readers share one alias list with the resolver instead of byte-identical private copies. The app-server `/v1/chat/completions` pass-through threads the same override from the `provider_cfg` that supplies its endpoint, so a `wire = "responses"` table now fails closed at the handler's ChatCompletions-only guard instead of silently receiving a chat body. Also pins the explicit `chat` dialect arm and the `messages` endpoint key in tests, and drops a copy-pasted `#[allow(clippy::too_many_arguments)]` on the one-argument helper. Signed-off-by: asto <asto18089@126.com>
Review round 1 of #79 (S2, S3): every named Custom table shares the `custom` provider slug, so Custom-tagged opaque reasoning state replayed onto any table resolving the same model id — table A's encrypted reasoning rode table B's wire after a provider switch. The replay gate now compares an endpoint-scoped tag (`custom/<table>` from the client's frozen provider identity); no released build mints the old bare tag, so nothing stops replaying. Capture also widens to the OpenCode Zen Responses roster, which sends `include: ["reasoning.encrypted_content"]` and `store: false` but never captured — the same unpaired-function_call replay loss this PR fixed for Custom tables. Include and capture now cover the same route set by construction, and the turn-path pin expressions collapse into `pinned_wire_override` / `reasoning_provider_tag` helpers. Signed-off-by: asto <asto18089@126.com>
Round-1 fixes pushed —
|
Round-1 review residue: the resolver's Custom wire channel was gated by two `provider_kind == Custom` checks forty lines apart - the endpoint-key remap and the protocol selection - so a future edit to one gate could silently desync the key from the protocol. The override is now bound once above both consumers. The new field is also spelled `RequestProtocol` per the route module's naming convention (same alias; request-side wire shapes are spelled that way there). Test pins: the override mints the wire even on the descriptor's loopback placeholder base URL; a wire-true custom candidate still carries default capabilities and UnknownOrStale pricing (no first-party fact resurrection); a Chat override cannot demote a Responses-descriptor builtin (OpenCode Zen); and Codex-minted opaque reasoning state never replays onto a Custom route. The protocol-mismatch tail of `rebound_for_model_protocol` now documents that the pinned override makes it unreachable for Custom clients. Signed-off-by: asto <asto18089@126.com>
Round-2 fixes pushed —
|
asto18089
left a comment
There was a problem hiding this comment.
Rebase follow-up: cd281909e lands the remaining round-1 residue on top of the 747329d57/2871dfff9/633ba6b9e wave — my earlier working tree had parallel implementations of B1/S1/S2, so it was dropped in favor of this wave's designs (identity-scoped tag and config-crate dialect parse included) and only the genuinely missing pieces were ported:
forkguard_custom_chat_stream_does_not_capture_encrypted_reasoning— a Chat-wire Custom table never mints opaque state even when a Responses-shaped stream reacheshandle_responses_stream; deleting the wire half of the capture gate fails exactly this pin.forkguard_custom_responses_capture_tolerates_missing_or_empty_encrypted_content— missing/emptyencrypted_contentis skipped while the stream keeps flowing and both blocks close; deleting the empty-content filter fails exactly this pin.forkguard_named_table_unrecognized_wire_keeps_the_chat_default— a typo'd dialect degrades to the legacy Chat policy at both the override reader and the runtime candidate (pinswire_dialect_override's silent-degrade contract).route_is_valid_for_model(preflight) now validates the table's own dialect, so receipts/preflight see the same protocol candidate the turn path mints — outcome-identical today, but protocol-conditional validation can no longer drift from the turn path.
Verified on the rebased head: tui full suite 11932 pass / 0 fail (the four known runtime-dir contention flakes pass serially), config 642/0, core 82/0, app-server 102/0, clippy/fmt clean. The parent PR Hmbown#625 gitlink and its T9 register/fingerprints (14) are rebased onto this head.
Bumps the CodeWhale gitlink to the T9 candidate head (98d4709b905ca5a5abce42da4a2b322c4a88d7b5, Pinvou/CodeWhale#79): the runtime-route resolver now honors a custom table's wire dialect, so the responses-wire routing this PR builds in build_dt_config actually drives the /responses client on every turn, with encrypted reasoning capture/replay on the route. Candidate-period registration: fork-guard EXPECTED_HEAD/COMMITS point at the candidate (50 commits above upstream), six T9 fingerprints pin the engine behaviors, and both registers document the topic with the re-pin obligation - once #79 squash-merges into pinvou3-clean, the landing wave re-pins the public maintenance-branch head and recycles the candidate constants. Signed-off-by: asto18089 <asto18089@126.com> Signed-off-by: asto <asto18089@126.com>
Round-1 residue pins for the custom wire channel, rebased onto the identity-scoped override work: - A Chat-wire Custom table must not mint opaque reasoning state even when a Responses-shaped stream reaches handle_responses_stream - the capture gate keys on the transport's wire, not the event shape. Deleting the wire half of the gate fails exactly this pin. - Missing or empty encrypted_content must not be captured (an empty blob would poison every later turn) while the stream keeps flowing and both reasoning blocks close. Deleting the empty-content filter fails exactly this pin. - A typo'd wire = "respones" dialect degrades to the legacy Chat default at both the override reader and the runtime candidate, so the silent-degrade contract of the shared dialect parser stays deliberate. - route_is_valid_for_model now validates the wire the per-turn route would mint: the dialect of the same table provider_config_for resolves, which also supplies the validated base URL. Outcome-identical today (Custom route validation is protocol-independent), but any future protocol-conditional validation would otherwise silently drift from the turn path. Signed-off-by: asto <asto18089@126.com>
The endpoint-scoped replay key compared the provider tag, api shape, and model - but the tag only pins the named Custom table's NAME, not the URL behind it. A table whose base_url is edited between sessions (proxy to vendor-direct, relay swap) replayed the old endpoint's encrypted blobs to the new one, producing sticky 400s whose cause was invisible. OpaqueReasoningState gains a serde-default endpoint fingerprint (the catalog's base_url_fingerprint of the client's frozen base URL). Capture mints it; the replay gate requires it to match, while fingerprint-less states from fixed-endpoint providers and pre-existing sessions keep replaying. The custom capture/replay/turn tests pin capture binding, mismatch drop, and legacy replay. Signed-off-by: asto <asto18089@126.com>
The capture gate enumerated its providers positively (Codex, Zen, Custom Responses) while the body builder's include gate was a negative list (not DeepSeek, not Concentrate). They coincided only through the current provider roster: a future Responses-wire builtin would silently start sending include: ["reasoning.encrypted_content"] without capturing, re-creating the unpaired function_call replay loss on multi-turn tool continuations. Both gates now derive from one shared predicate (responses_route_sends_encrypted_reasoning_include), so the lockstep the PR body claims holds by construction. The transport-wire half stays in the capture gate; the current route set is unchanged. Signed-off-by: asto <asto18089@126.com>
A typo'd wire value ("respones") degraded silently to the default Chat
Completions policy on every surface, so a user who typo'd the one string
that switches their endpoint's protocol saw an opaque wrong-wire failure
at request time with no diagnostic anywhere. wire_dialect_override now
logs a warning for non-empty unrecognized values; recognized explicit
chat spellings (chat, openai, ...) stay silent, the parse stays total,
and the degrade contract is unchanged.
Also pins the canonical alias sets, the normalization (case, whitespace,
underscore/hyphen), and the degrade behavior with direct unit tests in
the crate that owns them — coverage was previously indirect via the tui.
Signed-off-by: asto <asto18089@126.com>
The /provider picker resolved rows through the config-free candidate wrapper, which pins the static Chat policy — so after the runtime fix a wire = "responses" row bound Responses per turn while the picker still displayed Chat Completions as its supported protocol. The wrapper now takes the wire override explicitly; the picker passes the dialect of the same row-scoped table that supplied its base URL, and the session-state and model-switch receipts thread the ambient table's dialect the same way. Callers that never consume candidate.protocol() (unpinned child admission, the /model receipt) pass None with that reason stated. Also corrects three inaccurate comments left by earlier rounds: the two-pass resolver note (the wire override rides both passes), the readiness claim (it reads the ambient selection, the same table as its base URL — identity-pinned tables are the route layer's job), and the app-server claim (this ingress reads the literal [providers.custom] field, not the named-table map), plus the ProviderConfigToml::wire field doc, which still described only the built-in dual-wire vendors. Signed-off-by: asto <asto18089@126.com>
Three seams could regress silently because nothing bound them: - config: the runtime receipt's wire_override (deleting it changed no receipt a test asserted) - the new test resolves responses/anthropic/ chat tables through resolve_runtime_options and pins protocol plus endpoint key. - tui: route_is_valid_for_model's dialect threading now has a resolution pin for a wire table. The bool interface cannot observe protocol directly (validation is protocol-independent); the resolver and receipt pins above carry the protocol assertions. - app-server: the ChatCompletions-only guard is now posted through the real router and asserted as a 400 provider_wire_format_unsupported. This replaces non_chat_completions_provider_rejected, which built a struct and asserted a field against itself without invoking the handler. Signed-off-by: asto <asto18089@126.com>
Two coverage gaps the audit found: - Capture was tested at the client and replay at the pure builder, so an asymmetric edit to the tag or endpoint fingerprint derivation would only be caught by luck. The new seam test captures off a real stream on the per-turn client, places the state into history the way the turn loop commits it, and asserts turn 2's prepared request body leads with the paired reasoning item. - wire = "anthropic" custom tables were pinned only at the candidate level, one layer short of the wire. The new transport test drives resolve_runtime_route -> from_candidate against a loopback mock and asserts the POST path, x-api-key/anthropic-version headers, and the verbatim model. Also builds the custom responses turn-path client from the resolved route's identity-scoped config instead of the ambient config, mirroring the production install path - in a two-table setup the ambient table would freeze the wrong identity onto the pinned endpoint. Signed-off-by: asto <asto18089@126.com>
Signed-off-by: asto <asto18089@126.com>
cd28190 to
ffd897f
Compare
Round-3 fresh audit complete — fixes pushed (
|
JensenChen28
left a comment
There was a problem hiding this comment.
当前 head 的必需门禁均已通过,round-3 的 endpoint fingerprint 方向也正确,但仍有一个跨 endpoint 重放缺口,需要修复后再批准。
[P1] fingerprint 缺失时,Custom opaque reasoning state 仍会无条件重放到当前 endpoint。 crates/tui/src/client/responses.rs:880-883 把 state.endpoint == None 判为匹配;对应测试还明确固定了该行为。这样,升级前已保存的 custom/<table> state 在用户修改同一 table 的 base_url 后,会把旧 endpoint 产生的 encrypted_content 发给新 endpoint。table tag 只能固定名称,无法证明旧 state 来自当前 URL,因此这正好绕过本轮新增的 endpoint 边界。
请对 Custom state 采用 fail-closed 迁移:缺少 fingerprint 时不重放;如需兼容固定 endpoint provider,可只为其保留 None => true。同时把回归测试改为断言 fingerprint-less Custom state 不进入请求。这样既保留固定 provider 的旧会话兼容,也不会把旧网关的 opaque payload 发给重新指向的网关。
验证范围:复核了 cd281909e..ffd897f04 的新增提交、capture/include 共用谓词、capture/replay fingerprint 流向和相关回归测试;current-head required checks 全绿。未在本机重跑完整测试套件。
Round-4 fresh audit — no blockers, no majors; head
|
Review on this PR flagged the legacy-state arm of the endpoint gate: state.endpoint == None matched unconditionally, so a Custom-table state saved before the endpoint field existed kept replaying after the table's base_url was edited — exactly the cross-endpoint leak the fingerprint was added to close, and one that bricks the old session against the new endpoint (the undecryptable blob stays in history and keeps matching). Custom tags (the legacy root 'custom' and named 'custom/<table>') are the only tags whose endpoint can move under a stable tag, so they now fail closed: no proof of origin, no replay. Built-in providers have fixed catalog URLs, so their legacy states provably came from the only endpoint the tag ever had and keep replaying. The upgrade cost on Custom is one turn of reasoning continuity; fresh captures carry the fingerprint and replay resumes. Pins: the legacy Custom state assertion is flipped to require a reasoning-free request body, the legacy-root 'custom' tag gets the same pin, and a fixed-endpoint (Codex) legacy state pins the carve-out side. Signed-off-by: asto <asto18089@126.com>
|
感谢评审,P1 成立,已在 修复内容:replay 闸的 legacy 分支从 测试:按评审要求把 legacy Custom 断言翻转为「请求体不得含 reasoning item」(变异验证:把谓词砍回 验证:tui 11948 / core 82 全绿(一次 正文已加 Round-4 段并标注 round-3 中被本修复取代的「legacy keeps replaying」表述。 |
JensenChen28
left a comment
There was a problem hiding this comment.
当前修复已让 fingerprint-less Custom state 正确 fail closed,原始跨 table/base_url 缺口在 Custom 路径上已解决。但同一判断把所有非 Custom tag 当作固定 endpoint,而配置层明确允许内置 provider 使用 custom endpoint,因此仍有同类跨 endpoint 重放缺口。必需 check 也仍在运行。
| // fixed-endpoint provider's URL cannot have | ||
| // changed and its old sessions keep replaying. | ||
| let endpoint_matches = match &state.endpoint { | ||
| None => !is_custom_reasoning_tag(&state.provider), |
There was a problem hiding this comment.
[P1] None => !is_custom_reasoning_tag(...) 仍会把旧 state 发往已改址的内置 provider。 内置 tag 并不等于固定 URL:配置支持 OPENAI_BASE_URL、OPENAI_CODEX_BASE_URL,Config::provider_uses_custom_endpoint 也明确处理这些路由;OpenaiCodex 构造器甚至按 custom endpoint 切换凭据。于是 fingerprint-less openai-codex(或其他 Responses provider)state 在 base URL 改成另一个网关后仍返回 true,测试还把该行为固定为兼容性。请根据当前请求是否使用可变/custom endpoint 决定 legacy policy,而不是从 provider tag 推断;无法证明 URL 固定时应 fail closed。至少补一条 OpenAI Codex custom-base-url 改址的回归,断言旧无 fingerprint state 不进入请求。
Summary
A named
[providers.<name>]table withwire = "responses"never reached the wire that actually serves a turn.RouteRequesthad no wire channel, socandidate.protocol()came fromCustom's backward-compatible static policy (WirePolicy::Fixed(ChatCompletions)), and every per-turn client built byDeepSeekClient::from_candidatebound Chat — while the ambient spawn-time client (DeepSeekClient::new) andprovider_capability_with_wirehonored the override. Under the Hmbown#3384 host-resolved-route architecture (everyOp::SendMessagecarries a runtime-resolved route andinstall_resolved_runtime_routerebuilds the client viafrom_candidate), a config that said Responses talked to{base}/chat/completionsin practice.Changes:
RouteRequest.wire_override(config crate) — honored only forProviderKind::Custom; built-ins keep their descriptor policy even if a stray override is set. The minted candidate carries the wire-true protocol and endpoint key (responses/messages/chat), so receipts, preflight, and the per-turn client binding all read the same fact.custom_wire_override_for(config)reads the active table's dialect (the sameprovider_wire_dialectsourceprovider_wire_format_for_configuses) and threads it throughresolve_runtime_route*, taken from the identity-scoped config so a pinned turn path (per-thread routing,reload_config, fleet pins, session restore) reads the pinned table's wire, never the ambient selection's. Client-internal re-resolutions (rebound_for_model_protocol, per-request routing, limit lookups) pin the transport's own wire so an engine-internal model switch cannot downgrade an endpoint-scoped client.include: ["reasoning.encrypted_content"]: the Codex OAuth backend, OpenCode Zen's Responses roster, andwire = "responses"Custom tables (DeepSeek's plainreasoning_textand Concentrate stay excluded). Captured state is tagged endpoint-scoped (custom/<table>for Custom, from the client's frozen provider identity), and the replay gate requires that exact tag plus api shape plus model, so table A's encrypted reasoning never replays onto table B. Without capture, a Custom/Zen Responses route streamed reasoning items that were never persisted, and multi-turn tool continuations replayedfunction_callitems without their paired reasoning items.Consumer census: the
wiredialect parse lives once, in the config crate (wire_dialect_overridenext to theProviderConfigToml::wirefield it reads).provider_wire_format_for_config(ambient client),provider_capability_with_wire, the resolver, and the app-server/v1/chat/completionspass-through all consume it, so include/capture/resolution agree by construction; awire = "responses"table reaching the pass-through fails closed at its ChatCompletions-only guard instead of silently receiving a chat body. The config-freeresolve_route_candidate*wrappers keep the static policy (validation/display only; no client construction).Tests
custom_wire_override_mints_a_wire_true_candidate/wire_override_is_ignored_for_builtin_kinds(config): override → Responses/Messages candidates + endpoint keys (including the explicitchatarm); default stays Chat; built-ins immune.forkguard_named_table_wire_{responses,anthropic,without_wire}*(tui route_runtime): named-table dialect → runtime candidate protocol — the exact regression Pinvou PR feat(permissions): external_directory gate + broader-pattern always-allow (#411 #412) Hmbown/Codewhale#625 hit (three review rounds missed it because nothing pinned the wire on the runtime path).forkguard_identity_pinned_route_reads_the_pinned_tables_wire/forkguard_ambient_wire_override_does_not_leak_onto_pinned_chat_tables(tui route_runtime): the two cross-table directions onresolve_runtime_route_for_identity— the pinned table's wire+endpoint must agree even when the ambient selection is a different table.custom_table_wire_override_reaches_the_app_route(app-server): the pass-through resolves the table's Responses wire (the handler guard then rejects, fail closed).forkguard_custom_responses_route_turn_client_posts_to_the_responses_endpoint(client): full turn-path construction (resolve_runtime_route→from_candidate) against a loopback wiremock asserting the POST path is/v1/responseswithstore:false, theinclude, and the verbatim model — the path-level pin.forkguard_custom_responses_stream_captures_encrypted_reasoning_as_opaque_state/forkguard_custom_responses_replays_only_exact_model_opaque_reasoning_state: capture yields acustom/<table>-tagged opaque state with the wire model; replay includes it on exact table+model match and drops it on table switch or model switch.opencode_zen_responses_stream_captures_encrypted_reasoning(client): the Zen roster captures encrypted reasoning the same way, taggedopencode-zen.forkguard_custom_chat_stream_does_not_capture_encrypted_reasoning(client): a Chat-wire Custom table never mints opaque state even when a Responses-shaped stream reaches the handler — the capture gate keys on the transport's wire, not the event shape (mutation-checked: deleting the wire half fails exactly this pin).forkguard_custom_responses_capture_tolerates_missing_or_empty_encrypted_content(client): missing or emptyencrypted_contentis not captured and the stream keeps flowing (mutation-checked against the empty-content filter).forkguard_named_table_unrecognized_wire_keeps_the_chat_default(tui route_runtime): a typo'd dialect (wire = "respones") degrades to the legacy Chat policy at both the override reader and the runtime candidate.route_is_valid_for_model(preflight) validates the table's own dialect (provider_readiness), so receipts/preflight see the same protocol candidate the turn path mints.Review history
Round 1 review (2026-09-29, first comment) raised 1 blocker + 4 should-fixes; all are addressed by
747329d5+2871dfff(identity-scoped override read; canonical config-crate dialect parse shared by app-server; endpoint-scoped Custom replay tag; Zen capture; predicate dedup), and the follow-up round633ba6b9binds the resolver'sCustomwire gate once (the endpoint-key remap and the protocol selection can no longer drift apart), spells the request fieldRequestProtocolper the route module's naming convention, pins the override's remaining edges (placeholder base URL, no capability/pricing resurrection, no Zen demotion, no foreign-provider replay), and documents why therebound_for_model_protocolmismatch tail is unreachable for Custom clients.cd281909ecloses the remaining round-1 residue: the three edge pins above plus the preflight wire threading.Out of scope
Two adjacent gaps reproduce on the pristine base and stay untouched here — both are pre-existing and would smuggle unrelated behavior changes into this PR: the legacy
Modelstudio*Anthropiccapability-report mismatch inprovider_capability_with_wire(the doctor claims Chat while the client speaks Messages; outside the Custom class this PR targets), and the prompt-suggestion gate that still keys on the static chat-completions predicate rather than the resolved route's protocol. The stray blank-line churn in somewire_override: None,test literals is cosmetic and left as-is.Verification notes
cargo test -p codewhale-config -p codewhale-tui -p codewhale-app-server --lib: all green in isolation; under full-suite local parallel load pre-existingremote_controltests flake (they pass in isolation on both this branch and the pristine base61cb769be), so local-machine load flake, not this change. CI is the authority.cargo clippy --workspace --all-features --locked -- -D warnings -A clippy::uninlined_format_argsclean;cargo fmt --checkclean.No-Issue: the gap was surfaced by the parent review (Pinvou/pinvou-agent#625, round 4); the config-wire feature itself ships upstream via Hmbown#1519 - no issue tracks it in this repository.
Round-3 fresh audit + fixes (2026-09-30)
A fresh eight-domain audit of the round-2 head (
cd281909e) verified every earlier fix genuine (identity-scoped override, endpoint-scoped replay, single parse, gate bound once; mutation checks confirmed the pins can fail) and found no P1. It did find one behavioral hole plus a cluster of should-fixes, all addressed on this branch (new headffd897f04):2a4bd2f61) — the tag pins the table name, not the URL behind it: a table whosebase_urlis edited between sessions replayed the old endpoint's encrypted blobs to the new one.OpaqueReasoningStategains a serde-defaultendpointfingerprint (catalogbase_url_fingerprintof the client's frozen base URL); capture mints it, the replay gate requires a match, and fingerprint-less legacy states keep replaying (the legacy arm was tightened to fail-closed on Custom in the round-4 review fix below).7d01b8684) — the capture gate enumerated providers positively while the builder's include gate was a negative list; a future Responses-wire builtin would silently ship include-without-capture. Both now derive from one shared predicate.8c34f2323) —wire_dialect_overridenow logs a warning on unrecognized non-empty values (recognized chat spellings stay silent; the parse stays total), and the canonical alias sets/normalization/degrade contract gained direct unit tests in the config crate./providerdisplay diverge (02826611c) — picker rows resolved through the config-free wrapper and still showed Chat for awire = "responses"row. The wrapper now takes the override explicitly; the picker, session-state, and model-switch receipts thread their table's dialect, while callers that never consumecandidate.protocol()passNonewith that reason stated. Also corrects the two-pass resolver comment, the readiness comment (it reads the ambient selection — the same table as its base URL), the app-server comment (this ingress reads the literal[providers.custom]field), and theProviderConfigToml::wirefield doc.1cae5e776) — the runtime receipt'swire_override(protocol + endpoint key for responses/anthropic/chat tables throughresolve_runtime_options), a resolution pin forroute_is_valid_for_modelwith a wire table, and the app-server ChatCompletions-only guard posted through the real router asserting 400provider_wire_format_unsupported(replacing the tautologicalnon_chat_completions_provider_rejected, which asserted a struct field against itself).7f4364d2f) — a capture-to-replay seam test through the real per-turn client (capture off a live stream → state into turn-loop-placed history → turn 2's prepared body leads with the paired reasoning item), and awire = "anthropic"transport test (resolve →from_candidate→ POST{base}/v1/messageswithx-api-key/anthropic-version). The responses turn-path test now builds its client from the resolved route's identity-scoped config, mirroring the production install path.Round-3 residue, deliberately not addressed here: session-history growth of encrypted blobs (compaction caps thinking text but not
state; stripping or capping state risks re-creating the unpaired-replay failure and deserves its own design pass), the app-server pass-through's pre-existing named-table blindness (endpoint, not just wire — changing which table serves the ingress is a behavior change beyond this PR), and the separate built-in vendorwire_prefers_anthropiclist in configlib.rs(a distinct dialect space; folding it into the Custom parse would silently widen built-in alias acceptance).Verification on
ffd897f04:cargo test -p codewhale-config -p codewhale-tui -p codewhale-app-server -p codewhale-core --libgreen locally with CI'sRUST_MIN_STACK(config 645 / core 82 / app-server 101 / tui 11947, 0 failed);cargo clippy --workspace --all-features --locked -- -D warnings -A clippy::uninlined_format_argsclean;cargo fmt --checkclean. CI is the authority.Round-4 review fix (2026-09-30)
The review on
ffd897f04confirmed the round-3 direction and caught the one remaining hole — the legacy arm of the endpoint gate — fixed in57ae408d9:state.endpoint == Nonearm matched every legacy (pre-fingerprint) state, so acustom/<table>state saved before this PR kept riding the table's wire after the table'sbase_urlwas edited: precisely the cross-endpoint replay the fingerprint was added to close, and one that bricks the old session against the new endpoint (the undecryptable blob stays in history and keeps matching tag/api/model). Custom tags (customandcustom/<table>) are the only tags whose endpoint can move under a stable tag, so they now fail closed: no proof of origin, no replay. Built-in providers have fixed catalog URLs, so their legacy states provably came from the only endpoint the tag ever had and keep replaying (reviewer-suggested carve-out). The upgrade cost on Custom is one turn of reasoning continuity; fresh captures carry the fingerprint and replay resumes.customtag gets the same pin, and a fixed-endpoint (Codex) legacy state pins the carve-out side.Verification on
57ae408d9:cargo test -p codewhale-tui --lib11948 passed / 0 failed andcargo test -p codewhale-core --lib82 passed / 0 failed (one pre-existingremote_controlload flake failed once under full-suite parallel load and passes on rerun, same as documented for earlier rounds);cargo clippy --workspace --all-features --locked -- -D warnings -A clippy::uninlined_format_argsclean;cargo fmt --checkclean. CI is the authority.Pairing
Parent side: Pinvou/pinvou-agent#625 (app bridge + named table + catalog) pins this branch as the candidate head and carries the fork-register entry; re-pin to
pinvou3-cleanafter this PR merges (the register will need the new candidate head57ae408d9after this round).