Releases: xhqing/CC-Bridge
Release list
Add Agnes upstream, hybrid multi-provider upstream, and optional upstream proxy; fix continuation thinking-block stripping (CC-side "Content block not found")
Added: Agnes upstream (Agnes AI bridge adapter + framework-level upstream proxy support)
Why: Mapping the hybrid upstream's haiku/sonnet roles to agnes-2.5-flash (the free tier) required Agnes to be a registry upstream, but it had previously been used only as a classifier side-channel. Research (official docs at wiki.agnes-ai.com plus local testing, 2026-09-01) confirmed Agnes offers an official Anthropic-compatible endpoint POST /v1/messages (auth via x-api-key + anthropic-version, supporting streaming, tools, and the Anthropic-format thinking field), so it could be added following the lightweight mimo-bridge pattern with no protocol translation needed.
What:
- New
agnes-bridge/(adapter.js + agnes.env.example): model tables follow the official docs (agnes-2.5-flash: 512K context / 65536 max output, currently free at $0/1M promo pricing; agnes-2.5-pro and pro-beta: 1M / 65536, paid; agnes-2.0-flash and pro-alpha are officially deprecated and excluded).adaptRequestBodyonly clampsmax_tokens(pass-through principle same as MiMo).defaultTarget: agnes-2.5-flash. Registering the row incore/adapter.jsmakes it automatically usable as a hybrid member (AGNES_BASES/AGNES_API_KEY_nsection +MODEL_MAPentries likeagnes:agnes-2.5-flash). - Framework-level upstream proxy support (
UPSTREAM_PROXY, optional config): for overseas upstreams like Agnes that are unreachable or unstable when connected directly, configuring an HTTP proxy routes all outbound requests to upstream endpoints through it. Parsed incore/config.jsand shown byconfig show;core/server.jsbuilds anHttpsProxyAgent(reusing the existing https-proxy-agent dependency) and injects it into both upstream request paths (main forwarding and mid-body continuation), and the startup banner prints anupstream vialine; an invalid proxy URL fails at startup instead of silently falling back to a direct connection. Applies to https endpoints only (production endpoints are all https; http endpoints are local-mock scenarios); zero impact when unset, and no automatic fallback to the shell'sHTTPS_PROXY(the daemon's environment is not a controlled source, and auto-proxying domestic endpoints through it would be harmful); the classifier channel is unaffected (it already has its ownHTTPS_PROXYawareness). - Peripheral sync: CLI HELP title line adds Agnes (the upstream list was already generated dynamically);
package.json(files adds agnes-bridge, description / keywords); main README in both English and Chinese (implemented-upstreams list, available-upstreams table, thinking-level pass-through section, file table); hybrid README and hybrid.env.example member lists add agnes plus aUPSTREAM_PROXYnote (calling out its global nature: once configured, domestic endpoints also go through the proxy);.claude/CLAUDE.md(the AGENTS.md symlink follows) implemented-upstreams list updated. - hybrid.env.example default members and mappings switched to GLM + Agnes dual-active (user decision, 2026-09-01): default mappings for
claude-haiku-4-5andclaude-sonnet-5changed from ds flash / glm-4.6 toagnes-2.5-flash(free lightweight roles), the AGNES section uncommented as a default-active member (so copying the template as-is doesn't hit a "provider not configured" error from MODEL_MAP referencingagnes:while the member section is commented out), and DS / MiMo demoted to commented example blocks; the third pair keeps the "omitted prefix auto-qualification" teaching example (claude-sonnet-5->agnes-2.5-flash). The template's active state was validated through the framework's parseEnv + preprocessEnv startup checks (3 endpoints flattened, 3 mappings, keys bound correctly). - hybrid.env.example mappings expanded to five pairs with the DS section activated (user decision, 2026-09-02): added
claude-opus-4-7->glm:glm-5.3-flashandclaude-opus-4-6->ds:deepseek-v4-flash, with the DS section uncommented as a third default-active member (otherwise copying the template fails startup with provider 'ds' not configured) — the default member set is now GLM + Agnes + DeepSeek, with MiMo left as a commented example. Verified: parseEnv + preprocessEnv pass (4 endpoints flattened: glm-zai / glm-cn / agnes-default / ds-default; all 5 mappings qualified; 4 keys bound correctly).
Known limitation (observed in testing, non-blocking): the Agnes gateway's streaming responses emit no message_delta event (only start / block* / stop — confirmed over multiple test rounds on 2026-09-01, similar to z.ai's earlier nonstandard behavior). The framework's modelUsage injection hooks on message_delta, so it misses for this gateway (Claude Code falls back to its built-in table for the window), and streaming output stats record 0 (non-streaming usage is complete and correct). The gateway outputs thinking blocks by default (even when thinking was not requested; Claude Code handles them fine). The planned patch (detect a missing delta in-stream and synthesize one) is tracked as TODO T18.
Verification: tmp/test-agnes.js end-to-end (bridge from dev source, real endpoint via UPSTREAM_PROXY=http://127.0.0.1:1087) passed all 5 checks: non-streaming 200 + model rewrite (claude-opus-4-8 → agnes-2.5-flash, evidenced by the response model field), forced tool calling (tool_use block + stop_reason=tool_use + correct arguments), streaming SSE event sequence (thinking + text blocks), and the message_delta-absent shape (known-limitation path recorded as expected); tmp/test-agnes-hybrid.js configuration-layer integration passed all 9 checks (AGNES section discovery and flattening, <provider>- endpoint prefixing, cross-section key renumbering and binding, qualified-target qualification, merged window table with qualified keys, routeKeys narrowed to [1,2], provider error when the AGNES section is absent); no regressions in existing tests: tmp/test-hybrid.js 18 checks, tmp/test-modelusage.js (T14 injection) PASS, node --check clean; CLI-level checks confirm agnes is recognized as a valid upstream and hybrid error paths report properly.
Fixed: incomplete thinking-block stripping in mid-body stream continuation (root cause of CC-side "API Error: Content block not found")
Why: On 2026-09-01 the user reported intermittent API Error: Content block not found on the CC side of the production glm bridge. Investigation: the error text appears in neither bridge logs nor upstream responses; decompiling the CC 2.1.226 binary confirmed it is a client-side CC error — the stream reducer throws RangeError("Content block not found") upon receiving a content_block_delta / content_block_stop that references an unknown block (an index that never had a content_block_start). The bridge-side root cause sits in attachContinuationStream (the stripping logic of the mid-body continuation stream): by design it discards thinking blocks re-emitted by the continuation stream, but it only stripped the start event and missed delta and stop — ① the delta stripping condition checked the wrong field names: d.thinking_delta || d.signature_delta looks at fields that don't exist on the delta object (the protocol's discriminant is delta.type, with values "thinking_delta" / "signature_delta"), so the condition was always false and thinking deltas were never stripped; ② the stop branch had no type-based stripping path, so a thinking stop fell into the "remap and forward all subsequent blocks" fallback. Both were forwarded to CC with remapped index nextBlockIndex - 1 — when the break point sits mid tool_use (start withheld in toolBuffer, index already consumed), that index points to a block CC never saw a start for → orphan delta/stop → CC reports "Content block not found". Trigger = mid-body drop (bigmodel.cn gateway ~15s silent RST; the bridge's 12s watchdog proactively continues — 12+ occurrences in production logs on 2026-09-01 alone) + continuation stream leading with a thinking block + break point exactly mid tool_use; the three combined make it intermittent. The 2.15.1 tmp/test-continuation-sse.js regression missed it because its break point was mid text block — the orphan events' remapped index happened to close that half-open block, incidentally avoiding the error — and its client only validated W3C event grouping, without CC-reducer block-reference semantics.
What (core/server.js attachContinuationStream): thinking-block stripping is now consistent across the full trio (start / delta / stop) — a new skipBlockIdx tracks the thinking block currently being stripped: start stripping records its index; the delta condition is fixed to discriminate on delta.type plus index matching as a double guard; the stop branch strips by index match up front and resets. No thinking-block events reach the forwarding path anymore, so the continuation's block-index space stays unpolluted.
Verification: end-to-end via tmp/repro/ (mock upstream + dev-source bridge under isolated config + a strict event checker implementing CC reducer semantics), three scenarios: ① repro (mid-tool_use drop + continuation with thinking): pre-fix, CC sees 3 orphan events (thinking delta ×2 + stop referencing unknown block index=2), reproducing the error; post-fix, the event sequence is legal and the continuation body resumes at the correct index (3); ② control scenarios "break mid text + continuation with thinking" and "mid-tool_use drop + continuation without thinking" both pass post-fix, no regression on the normal continuation path; ③ node --check clean.
Added: hybrid multi-provider upstream (one port serving multiple model providers, routed by model)
Why: previously each upstream needed its own dedicated port / config (glm 8788, ds 8792, mimo 8791…), and switching Claude Code between providers' models meant changing ANTHROPIC_BASE_URL. The user wanted (2026-09-01) a "hybrid bridge": one port whose config holds multiple providers' URLs and k...
Fix tool_use buffered-forwarding SSE event corruption (CC-side JSON Parse error); adapter-based per-target context window injection; add AGENTS.md symlink
Fixed (T15: broken SSE events in tool_use buffered forwarding — root fix for the CC-side "JSON Parse error: Unexpected EOF")
- Why: On the evening of 2026-08-31 the user hit
API Error: JSON Parse error: Unexpected EOFin Claude Code. Investigation confirmed a protocol-level bug introduced by v2.15.0's "buffer tool_use blocks and forward them whole". Reproduced locally end-to-end (fake upstream → bridge → a strict W3C-compliant SSE parser grouping events one by one: 7 events valid, 1 parse failure). Root cause: in the forwarding loop,event:lines and blank-line separators were passed through in real time, while a tool_use block'sdata:lines were withheld untilcontent_block_stopand then flushed in one batch — CC received "N dangling events with no data + multiple data lines joined into a single event". Per the SSE spec, multiple data lines are joined with newlines into one event's data field, soJSON.parse('{start}\n{delta1}\n…\n{stop}')inevitably fails. Trigger condition = the model issues a tool call during a streaming response (the norm in CC agent sessions); the production glm bridge hit it right after loading 2.15.0. The bridge log showed all-200 with no anomalies throughout (every byte was forwarded; the corruption happened at the event-grouping layer, invisible to the bridge) — pre-release testing of 2.15.0 missed it because it only verified byte-level integrity (content / indices / block order), not parsing grouped by SSE events. - What changed (all in
core/server.js):- Main forwarding loop now stages whole events: a new
pendingEventLinemechanism —event:lines are no longer written out immediately; each is held until the same event'sdata:line or blank line arrives and then written in order. Once a tool_use block is recognized (thedata:line ofcontent_block_start), the event line is withheld intotoolBuffertogether with the data lines and inter-event blank lines; after thecontent_block_stopevent is fully received, everything is flushed at once in the upstream's original order. On the non-tool_use path every byte is still written in the original order, byte-for-byte identical to real-time passthrough (only the write timing is deferred to the event boundary). Prefill material extraction now takes only the buffereddata:lines (the buffer also contains event lines and blank lines). - The same fix applied to the continuation stream
attachContinuationStream: whole-block tool_use forwarding (start / deltas / stop) and the dedup-forwarded first text block now emit a trailing blank line after each event — these two spots were also missing the inter-event blank separator in v2.15.0 (they would blow up the same way whenever mid-body continuation triggered).
- Main forwarding loop now stages whole events: a new
- Verification: ①
tmp/repro-toolbuf.js(the 2.15.0 repro script): after the fix, all 11 events valid, 0 parse failures, event-for-event identical to the upstream's original stream; ② a newtmp/test-continuation-sse.jsend-to-end continuation test (fake upstream destroys the body at char 200 to simulate a gateway RST → bridge prefill continuation → strict parser verification): all 15 events valid; whole-block tool_use forwarding, deduped continuation body arrival, and thinking stripping all pass; ③ logs confirm the full continuation chain (continuation #1/3 → continuation stream attached → completed, prefill material contains both a text and a tool_use block).
Changed (T14: context windows moved down into adapters and injected per target; modelUsage filled in by default)
- Why:
CONTEXT_WINDOWwas previously a single global value (unset → modelUsage not injected), so CC could only guess the window from its built-in table — when going through the bridge,ANTHROPIC_BASE_URLis notapi.anthropic.com, and CC degradesclaude-opus-4-8(natively 1M) to a 200K fallback. Measured on 2026-08-30: a long session at 209.8K tokens was rejected by CC's local preflight with "Prompt is too long" (the request was never sent; upstream GLM-5.3's real 1M window would have held it easily; a manual /compact passing at the same size proves it was not an upstream limit). A single global value also cannot adapt to different windows under a multi-pair MODEL_MAP (glm-5.3=1M / glm-4.6=200K). - What changed:
- Each adapter gains a
modelContextWindowtable (official-doc values, verified 2026-08-31):glm-bridge(5.3 / 5.3-Flash / 5.2 = 1M; 5.1 / 5 / 5-Turbo / 5V-Turbo / 4.7 / 4.6 = 200K; the five 4.5-series models = 128K, from the docs.bigmodel.cn model pages),ds-bridge(V4 pro / flash = 1M, from the official pricing page),mimo-bridge(V2.5 / V2.5-Pro = 1M, from Xiaomi's MiMo official docs). Nominal K/M values are converted in decimal (1M=1000000), slightly conservative versus 2^N — better for CC to compact a little early than for preflight to pass and the upstream to reject. Targets not in a table get no window injected (falling back to CC's built-in table, same as older versions). core/server.jsbuildModelUsage()now picks the window per target: priority = explicitCONTEXT_WINDOWconfig (global, backward compatible) > adapter table by target; one entry per mapped pair, with spoof names getting their mapped pair's entry (with multiple pairs, the CLI hits the right window whether it looks up the spoof or the target name). The pairs'contextWindowfield also falls back to the adapter table. Thecore/adapter.jsinterface comment now documents themodelContextWindowfield. Both READMEs' modelUsage-injection wording updated (from "injected when CONTEXT_WINDOW is set" to "injected by default from the adapter's official-doc table; explicit config overrides").
- Each adapter gains a
- Verification:
tmp/test-modelusage.jswith a multi-pair MODEL_MAP (claude-opus-4-8→glm-5.3 / claude-haiku-4-5→glm-4.6) end-to-end — the modelUsage injected in message_delta is{"glm-5.3":{"contextWindow":1000000},"claude-opus-4-8":{…1M},"glm-4.6":{"contextWindow":200000},"claude-haiku-4-5":{…200K}}, each target correct; with an explicitCONTEXT_WINDOW=123456all entries are overridden to 123456 (priority works, backward compatible); tool_use stream regression under modelUsage injection (11 events all green). - Left over: T12 (feedback to Zhipu about the gateway's 15s SSE idle timeout) — the feedback draft is ready (
tmp/zhipu-sse-feedback.md, with three measured runs at 15134/15104/15141ms); submitting the ticket / the user community step is on the user.
Added (AGENTS.md symlink pointing to .claude/CLAUDE.md)
- Why: Newer agent tools such as ZCode use the project-root
AGENTS.mdas the conventional entry for project-level guidance (ZCode does not auto-load.claude/CLAUDE.md), but CC-Bridge's project guide has only ever lived in.claude/CLAUDE.md— ZCode sessions on this project could not read the project rules. Rather than maintaining two copies (which would inevitably drift), a symlink shares the single source of truth. - What changed: a new symlink
AGENTS.md→.claude/CLAUDE.mdat the project root (relative-path link, valid wherever the repo is cloned; git tracks it as a symlink).
Mid-body stream interruption recovery: body-phase drops no longer error out to Claude Code; prefill-based continuation resumes seamlessly from the break point
Mid-body stream interruption recovery: body-phase drops no longer surface an error to Claude Code; a prefill-based continuation resumes seamlessly from the break point.
Changed — mid-body interruption recovery (body-phase drops no longer error out to CC; prefill continuation resumes from the break point)
- Why: On 2026-08-30 the user hit "API Error: Server error mid-response" again. Full-chain investigation established two facts: (1) the upstream gateway (bigmodel.cn) enforces a ~15s application-layer idle timeout on SSE connections — three observed drops measured 15134/15104/15141ms since the last chunk; GLM's long thinking/generation pauses that stay silent beyond 15s get RST. TCP keepalive (15s) cannot prevent this (the gateway watches application-layer bytes, not probes; GLM only pings once at stream start, zero bytes during silent thinking). (2) Decompiling CC 2.1.226 confirmed its mid-stream error-retry policy: once body blocks (text/tool_use) have been produced, any in-stream error event finalizes the partial response (synthesizing end_turn, yielding the error text to the user, no retry this turn); only thinking-only drops with no body output get auto-retried. Hence v2.14.0's SSE-error close-out only rescued thinking-phase drops (18 of 26 drops that day recovered silently); body-phase drops (8) still surfaced an error — already-forwarded partial body cannot be recalled and CC does not retry, so an error was unavoidable.
- What changed (all in
core/server.js):- Continuation main path
continueInterrupted(): on a body-phase drop, instead of sending an error to CC, the already-forwarded body (complete blocks plus the in-flight partial text) is re-sent upstream as an assistant prefill appended to the original conversation (GLM endpoint verified in practice: string/array prefill, thinking enabled, and mid-sentence break points all resume precisely; if the last message is already an assistant — structured-output prefill scenarios — blocks are merged to avoid adjacent double-assistant 400). The continuation stream's body is forwarded under the original message with incrementing content_block indices; message_delta/message_stop close normally — CC receives a protocol-complete message, fully seamless. - Continuation-stream unwrapping
attachContinuationStream(): drops the continuation stream's message_start/ping/thinking blocks (the original message frame already lives on the CC side; repeated thinking adds no value); the first text block is buffered whole, deduplicated viastripRepeatedPrefix()(GLM occasionally repeats the prefill tail or even the whole segment at the continuation head — seen in roughly half of tested scenarios; the algorithm does line-aligned longest-prefix matching over a 4KB window, ignoring single-character coincidences), then forwarded as one new-index block; subsequent text blocks resume real-time per-delta forwarding; tool_use blocks are buffered whole as in the original stream and recorded as prefill material (supports drop-after-drop recovery); if the continuation stream itself drops, it recursively continues within the limit. - Upstream silence watchdog: body-phase drops all follow the "silent 15s → gateway RST" pattern; the bridge now detects a drop after 12s of silence and continues immediately (beating the gateway RST by 3s with controllable logs); armed only after body forwarding has started — thinking-phase drops keep the SSE-error close-out, since CC's thinking-only auto-retry is a verified good path we do not take away.
- 5s SSE comment-line keepalive toward CC (
: keepalive\n\n): while the continuation reconnects or the first text block buffers, CC receives no body bytes; comment lines feed its byte watchdog (default 3 minutes) and stop automatically once real body flow resumes. - tool_use blocks buffered whole before forwarding: half-delivered tool-input JSON is useless to CC (it needs the complete JSON to execute); blocks are now forwarded in one shot after content_block_stop — when a drop hits mid-tool generation the block stays buffered, zero loss to CC.
- Fixes and defenses:
buildHeaderscomputes content-length from the actual outgoing body (the continuation body is longer than the original request; without recomputing, the upstream truncates at the original length and returns 422 json_invalid — caught in end-to-end testing); the SSE forwarding loop gains ansseState.abandonedflag (stop writing the original stream once continuation takes over or error close-out runs, preventing ERR_STREAM_WRITE_AFTER_END — caught in testing); continuation capped at 3 attempts, degrading to SSE-error close-out (back to CC's auto-retry path) when exhausted.
- Continuation main path
- Verified in practice: (1) prefill continuation across three scenarios (string/array/thinking-enabled) resumes precisely; (2) dedup algorithm unit tests 7/7; (3) regression on normal traffic (plain streaming, forced tool_use) — block structure/indices/forwarding unchanged; (4) simulated-drop end-to-end (test hook destroys the upstream stream 200 chars into the body): logs show the full chain
TESTHOOK → continuation #1/3 → ←200 continuation stream attached → continuation done; the client receives[thinking, text (pre-drop half), text (continuation)]— three blocks, indices 0/1/2 contiguous, 654 chars of complete body, normal message_stop close-out, zero error events. - Follow-up: reporting the gateway's 15s SSE idle timeout to Zhipu (the official Anthropic API pings every second precisely to prevent this) is tracked in TODO T12.
Registered TODO (T14: per-target context-window injection in adapters)
- Why registered: investigating QuantStrategistAgent's long-session "Prompt is too long" (209.8K-token context rejected by CC's local pre-check; the request never left the client) exposed a framework gap: for non-official
ANTHROPIC_BASE_URL(host not on theapi.anthropic.comallowlist), CC does not honor claude-opus-4-8's native 1M window (the built-innative_1m:truetable only applies to verifiable channels) and falls back to a 200K floor — while the bridge's modelUsage injection (which tells CC the real window) only fires whenCONTEXT_WINDOWis explicitly configured; the global default of 0 means no injection, and it is a single global value that cannot adapt to different windows across a multi-target MODEL_MAP (glm-5.3=1M / glm-4.6=200K). The upstream GLM-5.3's real 1M window fits 209.8K easily (a manual /compact of the same size succeeded, proving it). - Registered: new TODO T14 (
TODO.md, orange upstream/link section) — each<name>-bridge/adapter.jsgains aMODEL_CONTEXT_WINDOWtable (modeled onMODEL_MAX_TOKENS, per-target official-doc values);buildModelUsage()falls back to the adapter's real window for the request's target whenCONTEXT_WINDOWis not explicitly set. TODO registration only; no code changed this release.
Add glm-5.3-flash entry to MODEL_MAX_TOKENS clamp table
Add glm-5.3-flash entry to MODEL_MAX_TOKENS clamp table
Changed (glm-bridge: add glm-5.3-flash entry to MODEL_MAX_TOKENS)
- Why: A local
glm.envMODEL_MAP pairclaude-opus-4-7->glm-5.3-flashwas added so switching models in Claude Code switches the GLM target (Opus 4.8 = glm-5.3, Opus 4.7 = glm-5.3-flash) with no config change or restart. But theMODEL_MAX_TOKENStable lacked aglm-5.3-flashentry — the clamping logic skips whencap == null, so an over-limitmax_tokenswould be sent upstream as-is and rejected. The official docs (docs.bigmodel.cn GLM-5.3-Flash page) confirm its text parameters match GLM-5.3, with a max output of 128K (131072). - What changed: Added one line to the MODEL_MAX_TOKENS table in
glm-bridge/adapter.js:'glm-5.3-flash': 131072. Table entry only; the clamping logic and passthrough behavior are unchanged. - Verified:
node --checkpassed; end-to-end test after a local bridge restart —claude-opus-4-7requests through the bridge return"model":"glm-5.3-flash"(thinking intact), andclaude-opus-4-8regression still returns"model":"glm-5.3".
v2.14.0
Highlights
- Mid-stream interruption no longer hard-tears the client connection. When the upstream drops a long SSE stream (ECONNRESET mid-response), the bridge now emits an in-protocol
errorevent (overloaded_error) and ends the stream cleanly. Claude Code (≥ 2.1.199) auto-retries the whole turn on in-stream error events, so tasks continue instead of stopping with "Connection closed mid-response" awaiting manual intervention. - Upstream socket keepalive (15s) to keep long-lived streams alive across NAT / load balancers that recycle silent connections.
- Stream-interruption diagnostics: interruption log lines now include time-since-last-chunk and bytes-received, to distinguish idle-timeout kills (server-side recycling during long silent thinking) from active-path resets.
Details
- Write-after-end guards on the SSE relay path (discard late upstream data after an in-stream error has been sent).
- No behavioral change to request forwarding, model rewriting, or key failover.
Request-body passthrough and per-key privacy options
Request-body passthrough and per-key privacy options
Changed (T4: request body switched to full passthrough — removed four strip categories; added per-key privacy option HIDE_USER_ID)
- Why: The T4 design was finalized without a
FIDELITYswitch — passthrough is now the only behavior. All strip/rewrite steps (context_management, Anthropic-specific system blocks,cache_controlstripping plus tools tail re-tagging) were re-examined against a T6 live direct-connection baseline capture and official docs, and none of them held up: endpoints treat unrecognized fields as ignored (stripping only fabricated a signature), and re-tagging the tools tail diverged from the direct-connection shape while being harmful to Zhipu explicit caching.metadata.user_idbecame a per-key privacy option (API_KEY_n_HIDE_USER_ID, passthrough by default, set to 1 to blank it), preserving the ability to keep device identifiers off-network for those who want it without forcing a global deviation from the direct-connection shape. - What changed:
- Bridge adapters:
glm-bridge/adapter.jsandmimo-bridge/adapter.jsrewritten to "passthrough + max_tokens clamp" only (with a "passthrough principle" note in the file header);ds-bridge/adapter.jslikewise dropped the four strip categories while keepingrepairToolSequence(a functional fix) and the clamp.stripCacheControlremoved from all three. core/config.js:collectKeysnow collectsAPI_KEY_n_HIDE_USER_ID;validateKeyAttrsaccepts only 0/1 (invalid values surface in the validate report);KEYSexposes ahideUserIdboolean (unset / 0 = false).core/server.js: insidesend(keyIdx)themetadata.user_idis blanked per the current key'shideUserIdand the body is re-serialized (behavior follows the key across key rotation / failover); request logs add auser_id=hiddenmarker;PROXY_DUMPcomment notes that dumps record the pre-key-processing shape, so the original user_id appearing there is expected.- Five env templates: neutral wording for
API_KEY_n_HIDE_USER_ID(privacy option, passthrough by default) plus commented example lines in glm/ds templates. - Docs: the three bridge READMEs' adaptation tables rewritten as a "passthrough principle" section; main README (EN/ZH) — intro and "What it does" reworded from "Request-body adaptation" to "Request-body passthrough", the multi-key failover section gained a per-key privacy option entry, and Notes gained the boundary sentence "passthrough is a design choice, not a compliance claim"; architecture bullet wording synced.
- Bridge adapters:
- Verified: all three adapters plus config and server modules load cleanly;
hideUserIdparsing and validation unit-tested (set to 1 → true, unset → false, invalid value flagged in validate); live end-to-end with a real key — passthrough config (HIDE unset) returned 200, with the dump showingcache_control(system tail + message blocks) andmetadata.user_idpreserved verbatim and no tools re-tagging (direct-connection shape);HIDE_USER_ID=1config returned 200, logs showeduser_id=hidden, and the forwarded body's user_id was blanked (verified via the re-serialization path in send). - Watch item (acceptance criteria, effective after release): the Zhipu key's
cache(anthropic)hit rate should not drop and should rise (restoring CC's native cache split points); the zai key's hit rate unchanged. - Compatibility: breaking change (request-body shape changed: previously stripped fields are now passed through; no config migration needed —
HIDE_USER_IDis an optional addition); grouped with T11 in this release's behavior-change set. - TODO closure: T4 / T8 archived (T8's README passthrough docs landed with T4 the same day: EN/ZH passthrough wording + per-key privacy option + non-compliance-claim boundary sentence all in place); T7 (request-body rewrite audit tool) dropped by user decision after archiving — after T4 the request body has only five known rewrites, each with a clear reason to exist, so no dedicated audit tool is needed to guard against drift. The fidelity TODO series (T4/T6/T7/T8/T11) is fully closed out.
Changed (T11: thinking-level pinning feature fully retired; thinking fields now passed through verbatim)
- Why: The premise of the 2026-07-07 project — that the CC VS Code extension's effort enum lacked
maxand the bridge had to pin thinking levels — no longer holds (the 2.1.226 extension binary was tested and the enum contains all five levels includingmax; spoof IDclaude-opus-4-8carries themax_effortcapability). Each upstream's official docs now publish the mapping for CC/effortlevels (GLM-5.3: low/medium/high→high, xhigh/max/ultracode→max, default is max; DeepSeek-V4-Flash-0731 and V4-Pro-0813 official mappings match: low→low, medium/high/xhigh→high, max→max, Agent requests auto-max), so the passthrough chain is officially guaranteed. Pinning lost its reason to exist, and writing a top-levelreasoning_effortfield that direct-connection traffic never contains was a fidelity deviation (T6 baseline capture had no such field). User decision: retire the feature, note the mapping tables in the config files, and keep unused fields out of the configs to avoid misleading users. - What changed:
core/config.js:parseModelThinkingemptied into a compatibility no-op (staleMODEL_THINKINGlines in old configs are silently ignored without error); removedTHINK_MAP/THINK_DEFAULTparsing andthinkingError(consumers in validate / show removed too); the two fields remain present as empty / null in the return object purely for downstream read-contract compatibility.core/server.js: removed the two startup lines injectingmodelThinking/thinkingDefaultinto adapters and the banner'sthinking:line; header comment reworded from "GLM thinking normalization" to "request-body adaptation".- Three bridge adapters (glm / ds / mimo): removed the thinking-field write block in
adaptRequestBody(three-field symmetric writes), themapEffortToGLM/mapEffortToDeepSeek/mapEffortToMiMolegacy functions, and thedefaultThinkingexport; rewrite-item comments now read "thinking fields passed through verbatim". core/adapter.js: interface doc comment dropped thedefaultThinkingentry and gained a passthrough note.- Five config templates:
glm.env.example(note: GLM-5.3 official mapping table + "/effort requires a value; choose xhigh or max to always get max" + a 2026-08 version caveat to prevent future mapping changes from misleading),ds.env.example(note: DeepSeek official mapping table + V4-Flash-0731 / V4-Pro-0813 versions agree + thenone→400 warning kept + a hint that xhigh only reaches high on the DS side, unlike GLM),mimo.env.example(note: two-state switch, passthrough means on),kimi/qwenreserved templates (comments synced). TheMODEL_THINKING/MODEL_THINKING_DEFAULTconfig blocks were deleted outright from all templates (kept out of the configs to avoid misleading users). - Docs: three bridge READMEs + main README (EN/ZH) — removed the "pin thinking level per target model" feature entry, the adaptation-table row, and the
MODEL_THINKINGfield docs; the main README's "Per-model thinking level" section was rewritten as "Thinking level passthrough" (with the three upstreams' official mapping tables + version caveat); the quick-start "configure MODEL_THINKING" step became "no configuration needed — /effort forwarded verbatim"; adapter interface docs droppeddefaultThinking; file tables and notes synced.
- Regression review (against CHANGELOG history): ① the "ignore /effort and pin" capability introduced in 2.6.0 is retired along with the feature — the user ruled the capability lost its reason to exist (premise gone + official mapping guarantee), an intentional regression; ② the 2.8.x "DS none=400" config boundary note: with the feature retired the bridge no longer writes
output_config.effort, so the 400 can never be bridge-triggered — the warning survives as an environment hint in ds.env.example to not turn effort off to none on the CC side; ③ 2.7.2 "MiMo MODEL_THINKING validation failure fix" and 2.7.x "OpenAI-path reasoning_effort passthrough": gone with the pinning mechanism, historical entries kept unrewritten; ④ the fidelity TODO (T4) simplified accordingly — faithful mode needs no thinking sub-switch (the former T5 was cancelled with the feature), thinking fields pass through unconditionally. - Compatibility: breaking change (
MODEL_THINKINGconfig no longer takes effect, silently ignored); this release bumps the minor version and the README marks it. Tested: all three adapters and the server load cleanly; production glm.env / ds.env (with stale MODEL_THINKING lines) load without error;adaptRequestBodypasses CC's original request body (thinking adaptive + output_config.effort) through field by field, adding noreasoning_effort.
Changed (glm.env.example now documents the CC /effort → GLM official mapping; thinking-pin removal filed)
- Why: The user verified that the CC VS Code extension (2.1.226) effort enum now matches the CLI (binary tested:
["low","medium","high","xhigh","max"]five levels; spoof IDclaude-opus-4-8carriesmax_effortcapability) — the 2026-07-07 premise "VS Code plugin max unusable (enum lacks max, silently falls back to high), bridge must pin thinking" is gone; and the GLM endpoint's official mapping table (Zhipu docs: low/medium/high→high, xhigh/max/ultracode→max, default level is max) already guarantees max thinking across the/effortpassthrough chain. Pinning lost its reason to exist, and writing a top-levelreasoning_effortfield absent from direct-connection traffic (T6 baseline) was a fidelity deviation. User instruction: just note the effort mapping table in the glm.env config file (no bridge-side pinning; level passthrough + a comment document...
cc-bridge dashboard: local browser panel with the most detailed usage stats
cc-bridge dashboard: local browser panel with the most detailed usage stats
Added — cc-bridge dashboard (local browser panel for detailed usage statistics)
- Why:
cc-bridge statsnow defaults back to terminal text output, with a trailing hint: "For more detailed usage statistics, run cc-bridge dashboard". The dashboard is a general-purpose panel that will host more modules later — usage stats is the first module. - What changed:
- CLI (
bin/cc-bridge.js):cc-bridge statsdefaults to terminal text mode again (aggregated / single-upstream detection unchanged);--text/-tkept as compatibility aliases. Newcc-bridge dashboardcommand (aliases:stats --gui/-g) opens the local browser panel. HELP and both READMEs updated. - Dashboard page (
core/gui.html): 6 overview cards (including "cache-created", which had been recorded but never displayed); a pure-SVG multi-series "usage trend" line chart (per-upstream series, requests / input / output metric switch, auto day-merge beyond 48 buckets, resize-adaptive); detail tables expanded to 3 (by upstream / by key / by model), all with a "cache-created" column; fixed slot-based series colors per upstream (light / dark modes); new dark mode (follows the system). Page is modular (<div class="module">) so future features append as sibling modules; server routes (core/gui.js) refactored into anAPI_ROUTESregistry. - Aggregation layer (
core/stats.js):aggregate()/aggregateWindowFor()now also returntotals/upstreamTotals/series; terminal stats output gains the dashboard hint line. - Fixed a trend-chart empty-render bug (TypeError when an hourly bucket was missing some upstream; the matrix is now zero-filled so lines drop to 0).
- CLI (
Changed — READMEs: "Why a bridge?" rationale and unique-capability documentation
- Why: The main READMEs (EN/CN) described What / How but never Why — the founding premise that "Claude Code only accepts whitelisted model IDs" was nowhere in the docs, and several bridge-only capabilities (classifier routing via
CLASSIFIER_MODE, modelUsage injection, request-body adaptation) were under-documented. Intro examples still said "GLM / Kimi / Qwen", out of sync with the implemented set (glm / ds / mimo). - What changed:
- Main READMEs (EN/CN): new "Why a bridge?" section (before Available upstreams) — explains the opposing
model-field requirements on both sides (CC accepts only whitelisted IDs vs. upstreams accept only real model names, and there is a singlemodelfield), making spoof→target rewriting the only glue; then lists six capabilities plain configuration cannot deliver (multi-key failover, pinned thinking levels, request-body adaptation, classifier routing, modelUsage injection, usage statistics). "What it does" gains three entries: request-body adaptation (stripping CC-specific fields / clamping max_tokens / cache-flag value), security-classifier routing (3× request frequency, ~70% of quota,on/offsemantics), and modelUsage injection (CONTEXT_WINDOW/MAX_OUTPUT_TOKENS— clients show the real context window instead of the spoofed model's). The architecture diagram's modelUsage row now notes "real context window". Intro examples updated to "GLM / DeepSeek / MiMo". glm-bridge/README.md: new section "CC security classifier routing (CLASSIFIER_MODE)" — what the classifier is, why its high frequency burns quota (~70%), anonvsoffbehavior/cost comparison table (on: AGNES free model + primary/backup failover + protocol conversion + system proxy;off: default, local fabricated allow, zero cost, no judgment), and the fallback semantics of returning 502 without forwarding when all AGNES backends fail. "What it does" gains a classifier-routing entry; the config field list gainsCLASSIFIER_MODE.
- Main READMEs (EN/CN): new "Why a bridge?" section (before Available upstreams) — explains the opposing
Changed — TODO/MEMO entries retrofitted with unique numbers
- Why: A new global rule (2026-08-21) requires every TODO/MEMO entry to carry a unique number (T+index / M+index), and existing entries were to be retrofitted in one pass — numbers let the user and AI refer to a specific entry without restating its full text.
- What changed: 3 archived entries in TODO-archive.md numbered T1–T3. Zero body changes (numbers inserted only; no rewriting, reordering, or timestamp changes).
Fixed — TODO/MEMO numbering bolding script truncation incident
- Why: The batch script that bolded entry numbers had a slicing bug that emptied the body of every matched entry (leaving only
- [ ] **Tn**), affecting 1 file and 3 entries. - What changed: Full rebuild from multiple recovery sources — ① git index / HEAD snapshots; ② Claude Code file-history checkpoints; ③ session-transcript replay of historical edits in time order. Entries re-bolded per the rule (Tn / Mn) with bodies verified verbatim against recovery sources; damaged shells archived locally (/tmp/todo-damage-backup/).
Usage stats GUI with time-window query; hourly-bucket stats persistence
Usage stats GUI with time-window query, and hourly-bucket persistence that survives daemon restarts
Added — Usage stats GUI (time-window query + per-dimension views)
cc-bridge stats now opens a local usage dashboard in your browser by default (--text keeps the original terminal view):
- Hourly-bucket persistence (
core/server.js): the stats model switches from per-daemon-process cumulative counters tohours[hk].models/.keyshourly buckets (UTC hour keys, e.g."2026-08-20T04"). Snapshots are loaded on startup, so usage history survives daemon restarts; old v1 snapshots are auto-migrated into a single bucket (totals preserved, granularity coarsened to the hour); 30-day rolling retention (STATS_RETENTION_HOURS). Usage attribution (Anthropic / OpenAI styles, message_start / message_delta dual checkpoints, base fallback estimation) and cache-hit accounting are unchanged. - Time-window aggregation (
core/stats.js):aggregate(fromISO, toISO)merges buckets within the window into two views — by key-name (merged across upstreams, disambiguated on name collisions) and by model (upstream/modellabels).normalizedHours()provides a unified view over v1/v2 snapshots for backward compatibility. - Local GUI server (new
core/gui.js):cc-bridge statsstarts a temporary HTTP server bound to 127.0.0.1 only (port picked fromPROXY_PORT+1, up to 20 tries), protected by a one-time random token in the URL (any missing/wrong token gets 403). It auto-opens the system browser and exits on Ctrl-C. The data APIGET /api/stats?from=&to=reusesaggregate()and works even when the daemon is stopped (reads the snapshot file). - Dashboard page (new
core/gui.html, zero-dependency single file): start/end time pickers (datetime-local, local timezone) + Query button + quick windows (Today / Last 7 days / Last 30 days / All, default Last 7 days); five overview cards (requests / input tokens / cache-hit tokens / cache-hit rate / output tokens); by key-name and by model tables with the same columns as the CLI text view; footer documents attribution and hourly-bucket granularity. - CLI wiring (
bin/cc-bridge.js):cc-bridge statsdefaults to the GUI;--text/-tkeeps the terminal view. HELP and README (EN/CN) updated. - Tested: 11 unit tests for window filtering (v1/v2 compatibility, closed-interval boundaries, one-sided windows, empty window); end-to-end bridged request verifying v1 snapshot migration + new requests landing in new buckets + SIGTERM persisting v2; GUI token checks (valid 200 / missing 403 / wrong 403 / unknown path 403); Chrome headless screenshot confirming page rendering.
Changed — Visitors badge renamed to Visits/day (14d): alt text aligned with the centralized traffic stats label
README badge alt="Visitors" → alt="Visits/day (14d)", matching the badge JSON label on the centralized xhqing traffic-stats side. Alt text only — the endpoint URL, data source, and badge semantics are unchanged.
Fix daemon crash on late upstream errors after response started (ERR_HTTP_HEADERS_SENT)
Fixed
- Fix frequent daemon crashes on late upstream errors after the response has started (
ERR_HTTP_HEADERS_SENTuncaught exception killing the process): Users had long been hit by "the cc-bridge background service dies for no reason and has to be restarted manually" — logs confirmed 27 crashes with the same signature on the glm channel and 9 on the ds channel (2026-07-28 to 2026-08-16), with another occurrence on 2026-08-16 16:57. Root-cause chain: ① On a streaming request the upstream returns 200 immediately, the bridge has already written response headers to the client (writeHeadexecuted) and SSE forwarding has begun; ② tens of seconds later the connection is killed by an RST, and theread ECONNRESETlands in the request-levelactiveUpReq.on('error')handler (instead of the harmless client-side path that only disconnects the client); ③ that handler only checks "is this a transient error" and retries, violating the design intent stated in its own code comment ("the retry window closes once the first upstream response arrives; no more switching after that"); ④ the retry also gets a 200 and callswriteHeada second time on the same client response → Node throwsERR_HTTP_HEADERS_SENTin an async socket-data callback with no catcher → the process exits, the daemon dies, and every in-flight request on the bridge is dropped. Three-layer fix (mutually reinforcing): Primary fix — each request closure now carries aresponseStartedflag, set at the entry ofhandleUpstreamResponse; inactiveUpReq.on('error'), if the response has already started, do not retry — just disconnect the client (a partially forwarded stream cannot be transparently retried anyway; Claude Code resends the request on its own). Defense in depth —handleUpstreamResponsechecksresponseStarted || clientRes.headersSentat entry and drops any late upstream response that arrives after headers were sent, never writing headers twice (catches all variants of late errors). Process-level backstop — registerprocess.on('uncaughtException')/unhandledRejection, log the full stack to the daemon log and keep running (requests on a local proxy are isolated from each other, so continuing is safe), so that any unforeseen bug no longer kills the daemon. After the fix, an upstream connection drop now manifests as "one request dropped (the CC client retries it), daemon alive" instead of "the whole process crashes and needs a manual restart". Verified by reproduction: a mock upstream returning 200 first, thenresetAndDestroy()mid-stream (RST, with the error landing in the request-level handler, byte-for-byte identical to the production logs), and a retry returning 200 — the old build (2.10.0 installed copy) crashes with exactly the production stack, while the fixed build survives the same scenario and its log shows "upstream late error (response started, not retrying)" working as designed; regression tests for 429 / 500 transient retries and non-streaming requests pass 3/3 (retry / failover behavior unchanged). Rationale: occasional upstream drops of long streams are unavoidable (every trigger in glm.log was ECONNRESET / ETIMEDOUT), but amplifying "a dropped stream" into "a dead daemon" was a retry-timing flaw in the bridge and had to be fixed.
Per-key priority, daemon banner fixes, and GLM dual-endpoint rebrand
Per-key priority, daemon banner fixes, and GLM dual-endpoint rebrand
Added
- Per-key priority (
API_KEY_n_PRIORITY— highest-priority key serves traffic first): when a single upstream config carries multiple API keys, each key can setAPI_KEY_n_PRIORITY=<non-negative integer>(higher = used first; unset counts as 0). Implementation:collectKeysincore/config.jscollects the rawKEY_n_PRIORITYvalues,validateKeyAttrsrejects non-integers at startup (API_KEY_n_PRIORITY="…" is not a non-negative integer), andloadConfigsorts keys by priority descending with a stable order (same priority keeps key-number order) — the server's key rotation scans the array in order, so after sortingpickNextKey/ circuit breaking / failback all work unchanged, with zero changes tocore/server.js. Effect: the highest-priority key takes all traffic until it gets circuit-broken on 401/403, then traffic falls to lower-priority keys; 60 s after the breaker expires, traffic automatically switches back to the high-priority key — "primary key first, backup keys only for failover" is now expressed by config, no need to add/remove key lines. If no key setsPRIORITY, sorting degrades to key-number order, identical to previous behavior (backward compatible).cc-bridge config showannotates each key withprio=; theglm/ds/mimoenv.example templates documentAPI_KEY_n_PRIORITYwith primary/backup examples; both READMEs (feature list, config example, multi-key failover section) gain a "Key priority" entry. Motivation: the user wanted control over key usage order (e.g. burn the primary account first, keep backups for failover only); previously this could only be done by renumberingAPI_KEY_nlines, which is painful to maintain when adding accounts. Tested: local mock with two keys (low-priority key numbered first) — requests hit the high-priority key; with the high-priority key returning persistent 401, the breaker switched to the low-priority key and got 200; an invalid PRIORITY value was rejected byvalidate; with no PRIORITY set, order stayed at key-number order.
Fixed
- Daemon banner showed wrong endpoints (still a single
api baseaftercc-bridge restart): 2.9.1 introduced multi-endpointAPI_BASESbut only upgraded the server-process banner (core/server.jslists all endpoints);printBannerincore/daemon.js(output ofrestart/start/claude) still printed the compatibility fieldcfg.API_BASE(= first endpoint URL) — with z.ai + Zhipu dual endpoints configured and only Zhipu keys enabled, the banner still showedapi base : https://api.z.ai/..., misleading the user into thinking traffic went to z.ai (forwarding actually follows each key's binding; logsbase=cnconfirmed Zhipu).printBannernow matches the server banner: multi-endpoint configs print all endpoints asapi bases : zai=… | cn=…, single-endpoint keeps the one-lineapi base; theAPI keysline now lists key names instead of a count (multi-endpoint showsname@endpoint, e.g.zhipu-cn@cn), consistent with the server banner. Motivation: the banner is the first thing users check to judge routing — an endpoint shown that differs from where traffic actually goes directly misleads troubleshooting. - GLM adapter
displayNamewas pinned to(z.ai), inconsistent with multi-endpoint support:displayNameinglm-bridge/adapter.jshad beenGLM-5.3 (z.ai)since 2.9.0; after 2.9.1 added the Zhipu domestic endpoint, the name still claimed a single vendor, and the banner lineupstream : GLM-5.3 (z.ai)was equally misleading when traffic went to Zhipu. Changed toGLM (z.ai / bigmodel.cn)(no pinned model version — the version is on the spoof→target line and the actual endpoints on the api-bases line). All references to the old wording were updated in sync:glm-bridge/README.md(title and body reworded for dual endpoints; config field docs moved from legacyAPI_BASE/ comma-separatedAPI_KEYto theAPI_BASES+API_KEY_ntrio; "z.ai doesn't recognize" wording in the adaptation table changed to "GLM endpoint"), header comment inglm-bridge/adapter.js, both main READMEs (implemented list, upstream table, file table), the implemented list in.claude/CLAUDE.md, and 6 SVGs underassets/demo/that still saidGLM-5.2 (z.ai)/glm-5.2(displayName to dual-endpoint, model 5.2→5.3, version footnote v2.8.1→v2.9.1), with the 5 affected PNGs re-rendered at 2x via rsvg-convert. Motivation: docs and demo images should describe current behavior; pinning an outdated vendor/model version misleads readers about multi-endpoint capability.