Skip to content

fix(route): honor a custom table's wire on runtime routes - #7020

Merged
Hmbown merged 2 commits into
codewhale-hq:mainfrom
asto18089:up/custom-wire-runtime-route
Oct 18, 2026
Merged

Hmbown merged 2 commits into
codewhale-hq:mainfrom
asto18089:up/custom-wire-runtime-route

Conversation

@asto18089

@asto18089 asto18089 commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

What

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 CodewhaleClient::from_candidate bound Chat regardless of the table's dialect. The ambient spawn-time client (provider_wire_format_for_config) and the capability reporter did honor the override — so a host that resolves routes per turn silently POSTed {base}/chat/completions while its config said Responses, and a cross-model rebind rebuilt the client around the static protocol, downgrading the transport mid-session.

  • RouteRequest gains wire_override, honored only for ProviderKind::Custom (built-ins keep their descriptor policy; a Chat override cannot demote a fixed-Responses builtin either). The resolver binds the Custom gate once so the protocol stamp cannot desync from it.
  • The tui route layer populates the override from the resolved identity's scoped config — the same table provider_wire_format_for_config reads — so receipts, preflight, readiness, and the per-turn client all see one protocol. The app-server /v1/chat/completions pass-through threads the same override, so a Responses-bound table fails closed at its Chat-only guard instead of silently receiving a chat body.
  • The client-internal model re-resolution pins the transport's own wire for Custom clients, so an engine-internal model switch cannot downgrade it. Same-table per-request switches that bypass rebind still fail closed at the protocol-mismatch guard (unchanged behavior, no silent downgrade).
  • The per-config dialect parse moves into the config crate next to the field it reads (wire_dialect_override plus the two alias predicates), replacing the byte-identical private copies in the client and capability readers so one alias list cannot drift. A misspelled dialect keeps degrading to the static Chat policy, now with a warning naming the table.

Tests

  • Resolver: the override mints the wire-true candidate (Responses and Anthropic) even on a relay base URL with no first-party catalog facts resurrected; builtins ignore it; absent/chat dialects keep the static policy.
  • Route layer: a wire = "responses" custom table mints a Responses runtime route; a typo'd dialect degrades to Chat.
  • Shared parse: canonical alias sets, case/separator normalization, explicit-chat arm, unknown degrade.
  • cargo test -p codewhale-config --locked 177 passed; app-server suite 15 passed; targeted tui filters green; clippy -D warnings clean (config, app-server, tui lib).

Ported from the Pinvou/CodeWhale fork (#79), rescoped: the fork's encrypted-reasoning capture widening already landed upstream (all Responses routes that send include capture now).

Refs #6997

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
`CodewhaleClient::from_candidate` bound Chat regardless of the table's
dialect. The ambient spawn-time client (`provider_wire_format_for_config`)
and the capability reporter *did* honor the override, so a host that
resolves routes per turn silently POSTed `{base}/chat/completions` while
its config said Responses — and a cross-model rebind rebuilt the client
around the static protocol, downgrading the transport mid-session.

`RouteRequest` gains a `wire_override` honored only for
`ProviderKind::Custom` (built-ins keep their descriptor policy; a Chat
override cannot demote a fixed-Responses builtin either). The resolver
binds the Custom gate once so the protocol stamp cannot desync from it.
The tui route layer populates the override from the resolved identity's
scoped config — the same table `provider_wire_format_for_config` reads —
and the /provider picker's browsing receipts thread the row's own dialect,
so receipts, readiness, preflight, and the per-turn client all see one
protocol. The app-server `/v1/chat/completions` pass-through threads the
same override so a Responses-bound table fails closed at its Chat-only
guard instead of silently receiving a chat body. The client-internal model
re-resolution pins the transport's own wire for Custom clients, so an
engine-internal model switch cannot downgrade it; same-table per-request
switches that bypass rebind still fail closed at the protocol-mismatch
guard.

The per-config dialect parse moves into the config crate next to the field
it reads (`wire_dialect_override` plus the two alias predicates), replacing
the byte-identical private copies in the client, capability reader, and
dual-wire resolver so one alias list cannot drift. A misspelled dialect
keeps degrading to the static Chat policy, now with a warning naming the
table.

Pinned by resolver-level tests (override mints the wire, builtins ignore
it, unknown dialects degrade), the route-layer test asserting a
`wire = "responses"` table mints a Responses runtime route, and the shared
parse's alias/case/chat-arm tests.

Signed-off-by: asto <asto18089@126.com>
@asto18089
asto18089 requested a review from Hmbown as a code owner October 18, 2026 02:06

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@github-actions github-actions Bot added the contribution-gate Author not yet in .github/APPROVED_CONTRIBUTORS; a maintainer grants access with /lgtm label Oct 18, 2026
@Hmbown
Hmbown merged commit 53e4e5a into codewhale-hq:main Oct 18, 2026
27 of 28 checks passed
@Hmbown Hmbown mentioned this pull request Oct 18, 2026
1 of 2 tasks
@Hmbown

Hmbown commented Oct 18, 2026

Copy link
Copy Markdown
Collaborator

🤖 This reply is from Claude Opus 5.5, an AI agent working on behalf of the Codewhale maintainers. It is posted through @Hmbown's account, but Hunter did not write it.

Merged in 53e4e5a. Thank you @asto18089. Giving RouteRequest a wire channel fixes a bug I had misread on #6997: a named table's wire held only for the startup client, and every turn's re-installed route went back to Chat. I corrected my reply there and credited this PR. Not in a release yet.

— Written by Claude Opus 5.5 (AI agent) on behalf of the maintainers.

Hmbown pushed a commit that referenced this pull request Oct 18, 2026
Adds [Unreleased] entries for #7014, #7015, #7017, #7018, #7019 and #7020,
crediting @asto18089 (and @zhuowp for the fork ports). Each merged without
a changelog line. The entry for #7020 names its behavior change: the
app-server's legacy [providers.custom] table now refuses a non-Chat wire
with provider_wire_format_unsupported instead of forwarding a Chat body.

Refs #7014
Refs #7015
Refs #7017
Refs #7018
Refs #7019
Refs #7020
Refs #6997

Verified: sync-changelog --check up to date; web check-facts OK;
check-contributor-credit "Every contributor in the window is credited on
all three surfaces".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBKbhx19BRbZtvWK8rVKjz
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contribution-gate Author not yet in .github/APPROVED_CONTRIBUTORS; a maintainer grants access with /lgtm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants