Skip to content

fix(third-party): strip content block types a custom upstream rejects - #451

Merged
MagicalTux merged 2 commits into
KarpelesLab:masterfrom
lexfrei:fix/strip-content-block-types
Sep 25, 2026
Merged

MagicalTux merged 2 commits into
KarpelesLab:masterfrom
lexfrei:fix/strip-content-block-types

Conversation

@lexfrei

@lexfrei lexfrei commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

A conversation routed to a strict Anthropic-compatible upstream can get stuck on a 400 for good, and this adds an opt-in way to strip the content blocks that cause it.

Claude Code announces a tool that becomes available after the first request with a content block of its own type. The blocks are tool_addition and tool_removal. They belong to the mid-conversation-tool-changes-2026-07-01 beta. It happens for example when an MCP connector finishes connecting mid-conversation. Anthropic accepts them. Kimi's coding endpoint answers 400 invalid_request_error: "Invalid request Error" and gives no hint about the cause. The block stays in the history. The first request of a session works, the second fails, and the session never recovers.

It reproduces without the proxy. A request whose system-role message has one text block returns 200, and the same message plus one tool_addition block returns 400. Removing the referenced tool definitions from tools does not help. The proxy forwards the body unchanged, so nothing is broken here. It is still the place where per-account rewrites for such upstreams already live.

The strip is opt-in per account, like the existing cache_control one. It is a second nested form of stripRequestFields: ["content.tool_addition", "content.tool_removal"]. Nothing is dropped without an entry. Claude Code puts the message's cache breakpoint on its last block, which is one of the blocks being dropped, so the breakpoint moves to the last block that stays. Without that the prefix cache hit would be lost on every following turn.

One thing I am not sure about is the default. I kept it opt-in to match cache_control.<subfield>. The alternative is to apply it to every non-Anthropic upstream automatically, the way thread continues are refused. I can switch if you prefer that.

There are unit tests for the strip and a wiring test through the body rewrite. The full suite, lint and typecheck pass. I also resumed a session that was stuck on the 400 through the patched proxy: without the entry it got 400 with three blocks forwarded, with the entry 200 and none forwarded.

lexfrei and others added 2 commits September 21, 2026 20:59
Claude Code announces a tool that appears after the first request with
a tool_addition or tool_removal content block. Anthropic accepts it; a
strict Anthropic-compatible upstream answers a non-retryable 400, and
because the block stays in the history every later turn of that
conversation fails with it, so the session never recovers.

stripRequestFields gains a second nested form, content.<block type>,
with the same per-account opt-in as the cache_control one. A cache
breakpoint carried by a dropped block moves to the last block left in
the message, since losing it would cost the prefix cache hit on every
following turn, and a message left without content is dropped.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
@MagicalTux
MagicalTux merged commit 4897b91 into KarpelesLab:master Sep 25, 2026
5 checks passed
@MagicalTux MagicalTux mentioned this pull request Sep 29, 2026
MagicalTux added a commit that referenced this pull request Sep 29, 2026
Thirty-two commits since 1.1.21. Two change routing on an existing
config without an opt-in (#480, #481); the rest is opt-in, additive, or
display.

Behaviour changes
  #481 an API-key account's 401 is a cooldown, not a permanent `error`:
       1 min, then 5, 15 and 60 for every further rejection with no success
       in between; any 2xx/3xx resets it. The request still fails over and
       the client never sees the 401. OAuth accounts are unchanged
  #480 with session distribution on, requests carrying no session id stay
       within the top priority tier, so a fallback gateway no longer answers
       Claude Code's bootstrap and connector calls
  #470 a 200 whose SSE stream reports a provider failure before any output
       (`server_is_overloaded`, `response.failed`) fails over once, like a
       status-shaped failure would
  #465 a reload removes running accounts whose config entry is gone from
       disk, so `teamclaude remove` from another shell takes effect at once
  #460 `import` refuses an account whose token upstream has definitively
       rejected (401/403), even with `--name`; a 5xx or timeout still imports

Rename
  #483 the project is being renamed to TeamRouter (#72). This release accepts
       the new name everywhere the old one is read and changes nothing an
       install has on disk: `teamrouter` runs the same CLI, every
       `TEAMCLAUDE_*` variable is also read as `TEAMROUTER_*` (which wins when
       both are set), every `/teamclaude/…` control route also answers at
       `/teamrouter/…`, and `~/.config/teamrouter.json` is used when it exists

Features
  #441 per-account egress proxy (`accounts[].routing`: http, socks4/4a,
       socks5/5h) for refresh, probes and requests; `login --routing`,
       `teamclaude routing set/show/clear`, a connection check before it is
       relied on, and a short hold when the proxy is unreachable
  #427 `accounts[].allowExtraUsage: true` lets a paid extra-usage account
       serve once every account is past its threshold, instead of a 429
  #466 `accounts[].maxSpend`, a money cap judged against the month-to-date
       extra-usage spend upstream reports; the TUI shows what an account
       has billed
  #436 `autoRedeemResets` spends a free Codex rate-limit reset credit when
       the Codex pool runs dry (off by default)
  #482 `advisorEligibility: "strict" | "prefer"`; when the advisor model
       narrows selection to a subset of the fleet the log says so, and status
       carries the reading (`advisorNarrowing`)
  #478 `stripOverageHeaders` drops another org's per-organization billing
       headers from responses, for a pool spanning several orgs (#476)
  #471 `quota.unified5hSeenAt` / `unified7dSeenAt` in status: when upstream
       last stated each shared window
  #446 client and dimension usage for the last 5h and 24h in status and the
       dashboard, resumed across restarts
  #458 #459 #461 the dashboard sets the switch threshold, enables/disables and
       reprioritizes an account, and has a light theme remembered per browser
  #464 `l` in the TUI signs an account in `error` in again from the dashboard
  #457 status records which Codex limit meters each model
       (`quota.codexModelLimits`)
  #442 `quotaBarPercent` drops the percentage beside a TUI bar's countdown
  #451 `stripRequestFields` takes `content.<block type>` to drop content
       blocks a strict Anthropic-compatible upstream rejects
  #469 `proxy.mcp` schemas declare their item types, the write audit line
       records what happened, and the write queue has a depth (#447–#450)

Fixes
  #477 a refused WebSocket handshake whose headers all drop is relayed as a
       well-formed head instead of a blank line and body bytes
  #474 two members of one ChatGPT workspace are told apart by user id, so a
       second `login --codex` no longer replaces the first
  #469 a Codex Responses stream with no Content-Type is relayed as a stream
       and booked; thread repair on the global upstream; TUI settings and
       status gaps; a hint when a local login would have served
  #463 a Codex row with no session window draws one wide weekly bar

Tests
  #484 #485 #486 the suite asserts behaviour, not the scheduler: wall-clock
       upper bounds are gone, and subprocess tests spawn the server through
       `test-helpers/spawn-server.js`, which verifies the server it reached by
       `server.pid` (new in status) instead of trusting a port

Tooling
  #452 #453 #454 #455 docker workflow actions bumped
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants