Add Agnes upstream, hybrid multi-provider upstream, and optional upstream proxy; fix continuation thinking-block stripping (CC-side "Content block not found")
LatestAdded: 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 keys at once, with model mappings targeting different providers' models — CC configures a single base URL, and /model switching swaps the provider behind it.
What:
- New
hybrid-bridge/(adapter.js + hybrid.env.example + README.md): hybrid is a "mixed upstream" that combines multiple implemented upstreams behind one port; members are freely chosen (glm / ds / mimo; hybrid cannot nest itself). Config is organized in per-member sections (section prefix = member name uppercased):GLM_BASES/GLM_API_KEY_n(with_NAME/_BASE/_PRIORITY/_HIDE_USER_IDattributes, same semantics as the flat form),DS_BASE/DS_API_KEY_1, etc.;MODEL_MAPtargets carry aprovider:prefix to qualify ownership (claude-opus-4-8->glm:glm-5.3) — with the prefix omitted, exactly one member recognizing the model auto-qualifies it, while zero or multiple matches (ambiguous) fail startup validation. Sections and top-level flat variables (API_BASES/API_KEY_n) cannot be mixed; globals likeCLASSIFIER_*still go top-level. Usage matches other upstreams:cc-bridge hybrid start/config/stats, andset default upstream hybridalso works. - Two new optional adapter hooks in the framework (zero impact on existing upstreams;
core/adapter.jsinterface docs updated accordingly):preprocessEnv(env)— called bycore/config.jsbefore parsing flat variables: hybrid uses it to flatten member sections into standard flat variables —API_BASESendpoint names get a<provider>-prefix (preventing cross-section name collisions), keys are renumbered uniformly across sections, and keys not explicitly bound to an endpoint are bound to their own section's first endpoint (not the global first — that belongs to another member);MODEL_MAPtargets get qualified. After flattening, validation / key rotation / circuit-breaking / stats / daemon / GUI all reuse the existing machinery unmodified. Hook errors are recorded intoproviderConfigErrorand reported by validate (also shown byconfig show), preserving loadConfig's "never throws" contract; reserved unimplemented upstreams (kimi / qwen) skip the hook, without affecting read-only commands like stop / status.routeKeys(target, KEYS)— called bycore/server.jsper request: returns the member's key-index set based on the target's provider prefix; key rotation / circuit-breaking / continuation key candidates all operate within the set — failover stays inside a member and never crosses members (the model doesn't exist at the other provider; crossing only yields a 400); an empty set errors out immediately instead of misrouting to another member.
- The hybrid adapter itself:
adaptRequestBodystrips theprovider:prefix fromobj.modeland delegates to the member adapter (GLM's max_tokens clamp, DeepSeek's tool-sequence fixes, and other per-upstream adaptations still apply);modelContextWindow/modelMaxTokensare merged under qualified keys (glm:glm-5.3) from each member's official doc table, so modelUsage injects each target's true window (glm:glm-5.3=1M / glm:glm-4.6=200K / ds:deepseek-v4-pro=1M…). - Peripheral sync: request-level key narrowing in
core/server.js(pickNextKey/ continuationtryKeys/finalErrorkey-count reporting all follow the narrowed set) + colon sanitization for dump filenames (qualified targets contain:, illegal in Windows filenames); CLI HELP copy adds Hybrid;package.json(files adds hybrid-bridge, description / keywords, version 2.16.0); main README synced in both English and Chinese (available-upstreams table, what-it-can-do, file table, and adding-a-new-upstream sections each gain hybrid content).
Verification: tmp/test-hybrid.js end-to-end (three mock upstreams: GLM dual-endpoint dual-key + DS single-endpoint single-key) passed all 18 checks: config flattening (endpoint prefixing / key renumbering and binding / PAIRS qualification), MODEL_MAP auto-qualification with the provider omitted (glm-4.6 → glm:glm-4.6), spoof routing to the correct member with the upstream receiving the bare model name (prefix stripped) and the correct key, GLM first-key 401 circuit-break switching only to that member's second key while the DS mock received zero requests (no cross-member failover), direct qualified targets (glm:glm-4.6) recognized and routable, unknown models returning HTTP 400 without silent rewriting, modelUsage injection hitting both qualified target and spoof, and all three error paths (no sections / sections mixed with flat variables / MODEL_MAP unknown model) reported with loadConfig never throwing. Legacy regressions: tmp/test-modelusage.js (multi-pair window injection) and tmp/test-continuation-sse.js (tool_use buffered forwarding + continuation SSE event integrity, 15 events all green) show no regression. CLI checks: hybrid config show correctly displays the flattened result (3 endpoints / 3 mapping pairs / 3 key bindings) and the provider error.
Also includes the previously unreleased 2.15.2 changes (2.15.2 never shipped as its own release): merged .claude/rules/cc-bridge-install.md into .claude/CLAUDE.md and removed the .claude/rules/ directory, making the project guide a single self-contained file.