Repository navigation
feat(server): opt-in stripOverageHeaders drops per-org billing headers - #478
Merged
MagicalTux merged 4 commits intoSep 29, 2026
Merged
Conversation
When upstream refuses a WebSocket handshake with a plain response, relayUpgrade writes the refusal head to the client by hand. It joined the surviving header lines into one string and wrapped it in fixed CRLFs, so an empty header set produced a malformed head. On a refusal whose only headers are Connection and Content-Length (both filtered) the client got HTTP/1.1 403 Forbidden\r\n\r\nConnection: close\r\n\r\n an empty head followed by "Connection: close" as body bytes. The refusal writer now builds the head from a line array (status line, header lines, Connection: close) joined with CRLF and ended by a single empty line. The 101 writer is changed to the same serialization for consistency. Its empty case is latent: Node only emits 'upgrade' when the response carries Upgrade and Connection, so that writer always has header lines today, and only a future transformation that removed every header would expose it. Tests assert the exact bytes for a refusal whose headers all drop and for a 101, reading until the proxy closes the client socket.
Anthropic responses carry the serving organization's billing state in anthropic-ratelimit-unified-overage-* and anthropic-ratelimit-unified-upgrade-paths: whether extra usage is enabled, why it is not, whether it is in use, and what the org could upgrade to. In a pool whose accounts belong to several organizations, each response carries the headers of whichever org served it, and Claude Code caches them as its own org's state. One response from an org with extra usage disabled can make the client report "no usage credits", block a model the pool still has plan quota for, or show a consent dialog offering to enable paid overage on the wrong org. `stripOverageHeaders: true` removes that family from every upstream response whose HTTP headers TeamClaude parses: forwardRequest, the client-credential relay (relayStream), the token relay (relayRaw), the WebSocket handshake (relayUpgrade, both the 101 and a refused handshake, on the main listener and the MITM listener) and the absolute-form relay (relayHttpForward). Blind CONNECT tunnels and bytes after a completed upgrade are not parsed and stay untouched. The plan-quota headers (5h, 7d and 7d_oi status, utilization and reset, the overall status, representative-claim, fallback) always pass through. updateQuota receives the unfiltered headers and does not persist the overage family. The flag is sampled from the live config when each request is dispatched, so POST /teamclaude/reload applies an edit to subsequent requests. Default false: with the key unset nothing changes for existing deployments. config.example.json and docs/configuration.md list it. Tests cover each path with the flag on (stripped, plan headers kept, quota accounting on the originals) and with it unset or false (forwarded unchanged), the MITM upgrade listener through a terminated CONNECT tunnel, and a reload subprocess test that flips the key on disk false, true, false and removes it, asserting the next /v1/messages response each time.
…ltering The MITM CONNECT/TLS setup in overage-headers.test.js now rejects on a premature close, an error or a 10 s timeout and destroys both sockets, and teardown destroys every accepted socket, since closeAllConnections() skips sockets handed to an 'upgrade' or 'connect' listener. The readiness and reload fetches in reload-strip-overage-headers.test.js get AbortSignal.timeout, as the /v1/messages fetch already had, so a child that accepts but never answers cannot hold the test past its deadline. A new regression test flips stripOverageHeaders while the first attempt is in flight and triggers the in-stream failover hop: the hop keeps the value the request was dispatched with, and the next request sees the new one. The relayRaw comment no longer says "no header rewriting": it names the dropped connection-specific and framing headers and the optional billing header filter.
# Conflicts: # src/server.js
Merged
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
lifrary
added a commit
to lifrary/teamclaude
that referenced
this pull request
Sep 29, 2026
Takes KarpelesLab/teamclaude up to ba01b4f while keeping the fork's routing safeguards (403 cooldown without parking, send-failure fail-over, outbound content-length, the single predispatch wait budget, cappedMessage, the overload slot release, the dead-refresh-token guard and refresh retry, the session home wait from d7810f1, and unranked-priority semantics). Adopted from upstream, among others: per-conversation session pins (KarpelesLab#438), 401 fail-over without parking (KarpelesLab#439, KarpelesLab#473), real quota-reset retry-after and candidate counts (KarpelesLab#429, KarpelesLab#408), a headerless 429 retry (KarpelesLab#431), fail-over on a failing 200 stream (KarpelesLab#470), per-account egress proxies (KarpelesLab#441), extra-usage fallback (KarpelesLab#427), maxSpend (KarpelesLab#466), stripOverageHeaders (KarpelesLab#478), keep-alive that outlives the client pool (KarpelesLab#411), a dead terminal not killing the proxy (KarpelesLab#410), console resolution per call (KarpelesLab#432), config reload and sync fixes (KarpelesLab#465, KarpelesLab#415), and the dashboard, TUI and Codex work since 1.1.20. Integration fixes the merge needed beyond conflict hunks: - upstream request-path code that referenced upstream-only locals (sx, route, ctx.tried) rewritten for the fork's forwardRequest - resolveSwitchThreshold was declared twice after a clean auto-merge - the fork's warm-up probe now forwards a routed account through its own proxy instead of sending its credential direct (new regression test) - a routing failure during a token refresh arms the routing hold instead of parking the account; a pinned request may use an account on routing hold - the headerless-429 branch no longer writes after headers were sent - canonical-state allowlists widened for upstream's new quota fields; saves use exportState() - the fork's home wait keys on the conversation pin like selection does Tests adapted where the fork deliberately differs (warm-up probe on by default, account-anchored TUI cursor, coordinator-built Prober and Warmer, unranked priority, per-conversation pin keys, fail-over-only 401, the wait budget instead of inline waits), each with an in-file note. Full suite 2919/2919, lint, typecheck and the strict ratchet (1411 vs 1465) pass. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.
Closes #476. Builds on #477, whose commit comes first here until it lands: with the flag on, a refused handshake left with only overage headers hits the empty-head case it fixes.
The gap
Responses carry the billing state of the organization that served them (
anthropic-ratelimit-unified-overage-*andanthropic-ratelimit-unified-upgrade-paths). On Claude Code 2.1.283, the disabled-reason header updates the client's cached billing reason. In a pool spanning several organizations, a cachedorg_level_disabledfrom another org keeps the Fable 5.1 consent dialog up with plan quota left. Reading the client code shows that the dialog's re-enable action calls billing endpoints for the client's own organization; that action was not exercised. Observed behavior, client-code findings and proposed two-org reproduction steps are in #476.What changed
stripOverageHeaders, defaultfalse, insrc/config.js,config.example.jsonanddocs/configuration.md.src/server.js:isOverageHeader(name)(theoverage-prefix plusupgrade-paths, case-insensitive) andshouldStripOverageHeaders(config)(=== true). The filter runs on the client-bound copy only, inforwardRequest(afterupdateQuota, which still sees the originals),relayStream,relayRaw(/v1/oauth/token),relayUpgrade(101 and refused handshake) andrelayHttpForward(absolute-form relay). Blind CONNECT tunnels stay opaque. Plan-quota headers pass through.relayUpgradereads it from its options object,forwardRequestfrom the request context, the other three from an optional trailing argument that defaults tofalse.src/mitm.jspasses it torelayUpgrade.src/index.jscopies it on reload (=== true), soPOST /teamclaude/reloadapplies an edit and a removed key restoresfalse.With the key unset or
false, responses are unchanged apart from the CRLF fix.Tests
test/overage-headers.test.js, 20 tests.isOverageHeadermatches the overage family andupgrade-pathsand no plan-quota header. With the flag on, the headers are gone on/v1/messages(withupdateQuotabooking from the originals),relayStream,relayRaw,relayUpgradedirectly, the main listener's upgrade path (101 and 403), the MITM upgrade path through a terminated CONNECT tunnel (101) and the absolute-form relay, plus exact bytes for a refused handshake whose headers all drop. Passthrough is checked across those relay paths using unset or false configurations. A stream failover test changes the flag while the first attempt is in flight: the hop keeps the value the request was dispatched with, and the next request sees the new one. The absolute-form and MITM tests useallowLoopbackForward(proxy), astest/http-forward-proxy.test.jsdoes.test/reload-strip-overage-headers.test.js, 1 test: the real server as a subprocess, as intest/reload-event-logging-blocklist.test.js. It edits the key throughfalse,true,false,trueand removed, reloads after each edit and checks the next/v1/messagesresponse.The CI checks pass locally:
npm test(2542 passing on Node 20.19.6, 22.21.1 and 24.12.0),npm run lint,npm run typecheck, andnpm run typecheck:strict -- --base master(1703 strict diagnostics, same asmaster).Not covered: the MITM HTTP/2 path and a refused handshake on the MITM listener;
relayStreamwith an explicitfalseand the absolute-form relay unset; a request in flight across aPOST /teamclaude/reload(the failover test changes the shared config object directly); byte equivalence with the flag off outside the two exact-byte handshake cases (the rest compare header values).Left alone
forwardRequeststill writes the full upstream header set, stripped names included, to the per-request log when request logging is on.notifyRunningServerinsrc/index.jsstill lists onlyeventLoggingandblockedModelsamong the settings reload picks up.teamclaude statusdoes not show the flag.nullafter ordinary traffic, because the client caches a missing header asnull; other client versions may differ.Default
false. When the serving organization's billing state matches the client's organization, stripping hides legitimate billing information. Mixed-org operators opt in with"stripOverageHeaders": trueand a reload. If you would rather strip by default, I can switch it.