maxpool 1.5.15 → 1.5.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -194,7 +194,9 @@ Provider fallback credentials can be supplied per Claude Code process with `ANTH
194
194
 
195
195
  `x-maxpool-session: <id>` enables session affinity. With this header, the first request for a Claude Code process is routed by the adaptive load balancer, then later requests from the same process keep using that home account while it remains available. If the home account is rate-limited, exhausted, in cooldown, or removed, the session temporarily uses another eligible route. When the home account becomes available again, the session returns to it. For the `all` profile, a Claude session that had to spill onto GLM/Kimi moves back to Claude once a Claude account frees up — **as long as it produced no provider-format tool calls while there**.
196
196
 
197
- **Cross-provider session pinning (`all` profile).** Maxpool classifies each session's home provider from its transcript: Anthropic tool-use ids are `toolu_`/`srvtoolu_`, GLM (z.ai) emits `call_`, Kimi (Moonshot) emits `tool_`. A session whose replayed history contains any non-Anthropic tool-use id (e.g. resuming a `cc glm` session under `cc all`) is **pinned to GLM/Kimi** and never routed to a Claude account — Anthropic rejects a non-`srvtoolu_` `server_tool_use.id` with a 400, so that direction is a hard incompatibility, not a preference. The reverse (a Claude session spilling onto GLM/Kimi) is safe. This is governed by `scheduler.crossProviderFallbackPolicy` (`never` | `when-exhausted` (default) | `always`), cyclable live in the TUI Routing menu (`m` → `f`): `never` = strict same-family pin, `when-exhausted` = cross only once the home family is exhausted, `always` = providers peer with Claude for a Claude session. A GLM/Kimi-origin session is never sent to Claude under any policy. (Note: Claude Code itself may warn that a resumed `glm-*` model "could not be restored" and fall back to `claude-opus-4-8`; that is a client-side session-store message — Maxpool still routes the session to GLM/Kimi correctly by its transcript.)
197
+ **Cross-provider fallback + compatibility (`all` profile).** Claude, GLM (z.ai), and Kimi (Moonshot) interoperate for ordinary sessions — a regular tool-use id (`call_`/`tool_`) passes Anthropic's loose validation, so a Kimi or GLM session runs fine on Claude and a Claude session spills onto GLM/Kimi when Claude is unavailable. `scheduler.crossProviderFallbackPolicy` controls it, cyclable live in the TUI Routing menu (`m` → `f`): `never` = strict same-family (Claude→Claude, GLM→GLM, Kimi→Kimi), `when-exhausted` (default) = cross only once the home family is exhausted, `always` = providers peer with Claude.
198
+
199
+ The one hard incompatibility Maxpool guards is a **`server_tool_use` id** that isn't `srvtoolu_` — Anthropic 400s that on replay (e.g. a `cc glm` session that used a server tool, resumed under `cc all`). Maxpool predicts that case ahead (pins the session to GLM/Kimi) and, for anything it can't predict (a rejected thinking signature), **self-heals**: the 400 is pre-stream, so Maxpool latches the session provider-only and transparently retries on GLM/Kimi — the client never sees the error. (Note: Claude Code may warn a resumed `glm-*` model "could not be restored" and fall back to `claude-opus-4-8`; that's a client-side message — routing is unaffected.)
198
200
 
199
201
  Provider rows do not use Claude Max session/week bars unless the provider returns compatible quota headers. For GLM/Kimi, Maxpool always tracks operational telemetry (`Act`, `OK`, `Fail`, `Last`) and also parses common `x-ratelimit-*` / `ratelimit-*` headers if present.
200
202
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maxpool",
3
- "version": "1.5.15",
3
+ "version": "1.5.16",
4
4
  "description": "Multi-account Claude Code proxy with adaptive, rate-aware load balancing across Claude accounts",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -1034,16 +1034,16 @@ export class AccountManager {
1034
1034
  _migrationSafeForRequest(requestInfo = {}) {
1035
1035
  // Fail closed on any body we couldn't fully scan (non-JSON / parse error).
1036
1036
  if (requestInfo.bodyThinkingScanned !== true) return false;
1037
- // A foreign (GLM/Kimi) session is provider-pinned; the only cross it could make
1038
- // is GLM↔Kimi, whose mismatched thinking formats risk a reasoning-loop — keep it
1039
- // on its bound provider rather than rebalancing.
1040
- if (this._effectiveOrigin(requestInfo).class === 'foreign') return false;
1037
+ // An Anthropic-incompatible (provider-pinned) session's only possible cross is
1038
+ // GLM↔Kimi, whose mismatched thinking formats risk a reasoning-loop — keep it on
1039
+ // its bound provider rather than rebalancing.
1040
+ if (this._effectiveIncompatible(requestInfo).incompatible) return false;
1041
1041
  // A signed-thinking request is migration-safe when cross-account thinking
1042
1042
  // migration is enabled: the signature is content/model integrity, not account-
1043
- // bound, and the rebalance candidate filter (_matchesRequest → _isRequestCompatible)
1044
- // already bars providers for thinking, so every eligible target is a Claude
1045
- // account. When the flag is off, keep the conservative bar (never migrate signed
1046
- // thinking) until the revert-to-issuer fail-safe lands (#16).
1043
+ // bound. The rebalance candidate loop (_shouldRebalanceBoundSession) additionally
1044
+ // skips PROVIDER targets for a signed-thinking request, so every migration target
1045
+ // stays a Claude account (a signed block isn't shuttled to a provider mid-session).
1046
+ // When the flag is off, keep the conservative bar (never migrate signed thinking).
1047
1047
  if (requestInfo.requiresAnthropicThinkingIntegrity === true) {
1048
1048
  return this.scheduler.crossAccountThinkingMigration === true;
1049
1049
  }
@@ -1084,6 +1084,10 @@ export class AccountManager {
1084
1084
  for (const account of this.accounts) {
1085
1085
  if (account.index === bound.index) continue;
1086
1086
  if (excludedIndexes.has(account.index)) continue;
1087
+ // Keep a signed-thinking session's live migration on Claude accounts only —
1088
+ // don't shuttle an Anthropic-signed block onto a provider mid-session even
1089
+ // though _isRequestCompatible now allows providers for thinking under policy.
1090
+ if (requestInfo.requiresAnthropicThinkingIntegrity === true && account.type === 'provider') continue;
1087
1091
  if (!this._matchesRequest(account, profile, requestInfo)) continue;
1088
1092
  // Genuinely-healthy alternatives only (normal/soft/unknown weekly + model headroom).
1089
1093
  if (!this._isAvailable(account, { allowWeeklyReserve: false, allowWeeklyCritical: false, model: requestInfo.model })) continue;
@@ -1493,7 +1497,7 @@ export class AccountManager {
1493
1497
  const base = Number.isFinite(account.priority) ? account.priority : 0;
1494
1498
  if (account.type === 'provider'
1495
1499
  && this._crossProviderFallbackPolicy() === 'always'
1496
- && this._effectiveOrigin(requestInfo).class !== 'foreign') {
1500
+ && !this._effectiveIncompatible(requestInfo).incompatible) {
1497
1501
  return 0;
1498
1502
  }
1499
1503
  return base;
@@ -1506,53 +1510,45 @@ export class AccountManager {
1506
1510
  return true;
1507
1511
  }
1508
1512
 
1509
- // Effective home-provider origin for a request: the request's own transcript
1510
- // classification, OR a sticky 'foreign' latch on the session (which never
1511
- // downgrades — once a GLM/Kimi tool-use id is seen, later no-tool follow-up turns
1512
- // of that session stay provider-pinned). Both the selector AND the retry oracle
1513
- // read this so they never disagree (foreign-barred here == foreign-barred there).
1514
- _effectiveOrigin(requestInfo = {}) {
1513
+ // Effective Anthropic-incompatibility for a request: the request's own transcript
1514
+ // verdict, OR a sticky latch on the session — set once a foreign server_tool_use
1515
+ // id is seen, or once Anthropic REJECTED the transcript on replay (react-and-heal
1516
+ // in server.js). Never downgrades, so a later no-tool follow-up turn stays
1517
+ // provider-pinned. Both the selector AND the retry oracle read this so they never
1518
+ // disagree. homeProvider is a SOFT hint (first foreign id shape) for 'never' only.
1519
+ _effectiveIncompatible(requestInfo = {}) {
1515
1520
  const sticky = requestInfo.sessionKey ? this.sessionPolicies.get(requestInfo.sessionKey) : null;
1516
- if (sticky?.originClass === 'foreign') {
1517
- return { class: 'foreign', provider: sticky.originProvider || requestInfo.originProvider || null };
1518
- }
1519
- if (requestInfo.originClass === 'foreign') {
1520
- return { class: 'foreign', provider: requestInfo.originProvider || null };
1521
- }
1522
1521
  return {
1523
- class: requestInfo.originClass || sticky?.originClass || null,
1524
- provider: requestInfo.originProvider || sticky?.originProvider || null,
1522
+ incompatible: Boolean(requestInfo.anthropicIncompatible || sticky?.anthropicIncompatible),
1523
+ homeProvider: requestInfo.homeProvider || sticky?.homeProvider || null,
1525
1524
  };
1526
1525
  }
1527
1526
 
1528
1527
  _isRequestCompatible(account, profile, requestInfo = {}) {
1529
1528
  if (!this._matchesProfile(account, profile)) return false;
1530
1529
 
1531
- const origin = this._effectiveOrigin(requestInfo);
1530
+ const { incompatible, homeProvider } = this._effectiveIncompatible(requestInfo);
1531
+ const policy = this._crossProviderFallbackPolicy();
1532
1532
 
1533
- if (origin.class === 'foreign') {
1534
- // HARD, policy-INDEPENDENT: a GLM/Kimi transcript can never replay to an
1535
- // Anthropic account (400 on the non-`srvtoolu_` id). Provider accounts only.
1536
- // This branch also SUPERSEDES the Anthropic-thinking bar below — a foreign
1537
- // session's (unsigned) thinking must go BACK to its provider, never be
1538
- // force-pinned to Claude (the misroute that caused the reported 400).
1533
+ if (incompatible) {
1534
+ // The transcript can't replay to Claude (a foreign server_tool_use id, or
1535
+ // content Anthropic rejected on replay) — provider accounts ONLY, regardless
1536
+ // of policy. Providers are lenient and accept each other's ids (GLM↔Kimi is
1537
+ // fine); 'never' pins to the detected home provider when known.
1539
1538
  if (account.type !== 'provider') return false;
1540
- // 'never' pins to the ORIGIN provider family when known (GLM→GLM, Kimi→Kimi).
1541
- // 'when-exhausted'/'always' let the sibling provider serve too (priority 10<20
1542
- // keeps it a fallback). Unknown origin provider ⇒ any provider allowed.
1543
- if (this._crossProviderFallbackPolicy() === 'never'
1544
- && origin.provider && account.provider !== origin.provider) return false;
1539
+ if (policy === 'never' && homeProvider && account.provider !== homeProvider) return false;
1545
1540
  return true;
1546
1541
  }
1547
1542
 
1548
- // Anthropic-origin or unknown: oauth (Claude) is always eligible. Provider
1549
- // fallback (Claude→GLM/Kimi, the SAFE direction) is policy-gated and still
1550
- // barred for signed thinking (an Anthropic signature can't be validated by a
1551
- // provider). 'never' = Claude session uses Claude only.
1552
- if (account.type === 'provider') {
1553
- if (this._crossProviderFallbackPolicy() === 'never') return false;
1554
- if (this._requiresAnthropicThinkingIntegrity(requestInfo)) return false;
1555
- }
1543
+ // Compatible session — includes Kimi and GLM-without-server-tools, whose regular
1544
+ // tool_use ids pass Anthropic's loose validation, AND ordinary Claude sessions.
1545
+ // Claude is eligible + preferred (priority 0). Provider fallback is the SAFE
1546
+ // direction (lenient providers accept Anthropic ids/signatures) and is
1547
+ // policy-gated ONLY: 'never' keeps a Claude session on Claude; 'when-exhausted'
1548
+ // lets providers serve as a priority-fallback; 'always' peers them
1549
+ // (_effectivePriority). Signed thinking no longer bars providers here — but its
1550
+ // live MIGRATION stays Claude-only (see the rebalance guard).
1551
+ if (account.type === 'provider' && policy === 'never') return false;
1556
1552
  return true;
1557
1553
  }
1558
1554
 
@@ -1561,33 +1557,34 @@ export class AccountManager {
1561
1557
  if (requestInfo.requiresAnthropicThinkingIntegrity) {
1562
1558
  this.markSessionThinkingProtected(requestInfo.sessionKey, requestInfo.model);
1563
1559
  }
1564
- if (requestInfo.originClass === 'foreign') {
1565
- this.markSessionOrigin(requestInfo.sessionKey, 'foreign', requestInfo.originProvider);
1560
+ if (requestInfo.anthropicIncompatible) {
1561
+ this.markSessionIncompatible(requestInfo.sessionKey, requestInfo.homeProvider);
1566
1562
  }
1567
1563
  }
1568
1564
 
1569
- // Latch a session's foreign (GLM/Kimi) origin. Only 'foreign' is sticky — an
1570
- // Anthropic classification never bars anything, and never-downgrade keeps a
1571
- // provider-pinned session correct across a later no-tool follow-up turn.
1572
- markSessionOrigin(sessionKey, originClass, originProvider = null) {
1573
- if (!sessionKey || originClass !== 'foreign') return;
1565
+ // Latch a session as Anthropic-incompatible (a foreign server_tool_use id, or a
1566
+ // transcript Anthropic rejected on replay). Sticky + never-downgrades so the
1567
+ // session stays provider-pinned across later follow-up turns.
1568
+ markSessionIncompatible(sessionKey, homeProvider = null) {
1569
+ if (!sessionKey) return;
1574
1570
  const existing = this.sessionPolicies.get(sessionKey) || {};
1575
- if (existing.originClass !== 'foreign') {
1576
- console.log(`[Maxpool] Session "${sessionKey}" is ${originProvider || 'provider'}-origin (foreign transcript) — pinned to provider accounts`);
1571
+ if (!existing.anthropicIncompatible) {
1572
+ console.log(`[Maxpool] Session "${sessionKey}" is Anthropic-incompatible (${homeProvider || 'provider'} transcript) — pinned to GLM/Kimi`);
1577
1573
  }
1578
1574
  this.sessionPolicies.set(sessionKey, {
1579
1575
  ...existing,
1580
- originClass: 'foreign',
1581
- originProvider: existing.originProvider || originProvider || null,
1576
+ anthropicIncompatible: true,
1577
+ homeProvider: existing.homeProvider || homeProvider || null,
1582
1578
  });
1583
1579
  }
1584
1580
 
1581
+ // Marks a session as containing Anthropic signed thinking. This no longer bars
1582
+ // provider fallback (a lenient provider accepts an Anthropic signature) — it only
1583
+ // keeps the session's live cross-account MIGRATION on Claude (the rebalance guard),
1584
+ // so a signed block isn't needlessly shuttled to a provider mid-session.
1585
1585
  markSessionThinkingProtected(sessionKey, model = null) {
1586
1586
  if (!sessionKey) return;
1587
1587
  const existing = this.sessionPolicies.get(sessionKey) || {};
1588
- if (!existing.requiresAnthropicThinkingIntegrity) {
1589
- console.log(`[Maxpool] Session "${sessionKey}" contains Anthropic signed thinking; provider fallback disabled`);
1590
- }
1591
1588
  this.sessionPolicies.set(sessionKey, {
1592
1589
  ...existing,
1593
1590
  requiresAnthropicThinkingIntegrity: true,
@@ -2416,7 +2413,7 @@ export class AccountManager {
2416
2413
  sessions: {
2417
2414
  stickyBindings: this.sessionBindings.size,
2418
2415
  thinkingProtected: [...this.sessionPolicies.values()].filter(p => p.requiresAnthropicThinkingIntegrity).length,
2419
- foreignPinned: [...this.sessionPolicies.values()].filter(p => p.originClass === 'foreign').length,
2416
+ providerPinned: [...this.sessionPolicies.values()].filter(p => p.anthropicIncompatible).length,
2420
2417
  },
2421
2418
  };
2422
2419
  }
package/src/server.js CHANGED
@@ -736,8 +736,14 @@ async function forwardRequest(
736
736
 
737
737
  if (upstreamRes.status >= 400 && upstreamRes.status < 500) {
738
738
  const errorBody = await readErrorBody(upstreamRes);
739
+ // A transcript a lenient provider (GLM/Kimi) produced can be rejected by
740
+ // Anthropic on replay (a non-srvtoolu_ server_tool_use id, or a thinking
741
+ // signature it can't validate). Detect it on an Anthropic account so we can
742
+ // self-heal onto a provider instead of surfacing the 400.
743
+ const anthropicIncompat = account.type !== 'provider' && isAnthropicIncompatBody(errorBody);
739
744
  const errorType = errorBody.includes('Invalid `signature` in `thinking` block')
740
745
  ? 'invalid_thinking_signature'
746
+ : anthropicIncompat ? 'anthropic_incompatible_transcript'
741
747
  : `HTTP ${upstreamRes.status}`;
742
748
  accountManager.releaseAccount(lease, { status: upstreamRes.status, error: errorType });
743
749
 
@@ -768,6 +774,24 @@ async function forwardRequest(
768
774
  }
769
775
  }
770
776
 
777
+ // React-and-heal: this transcript can't run on Claude (foreign server_tool_use
778
+ // id / thinking Anthropic can't validate). The 400 is pre-stream and the body
779
+ // is buffered, so latch the session Anthropic-incompatible (sticky → once per
780
+ // session) and retry PROVIDER-only, rather than surfacing the 400. Only worth
781
+ // it when a provider can actually serve it (profile=all with a GLM/Kimi token).
782
+ const providerAvailable = accountManager.accounts?.some(a => a.type === 'provider' && a.enabled !== false);
783
+ if (anthropicIncompat && requestInfo.sessionKey && providerAvailable
784
+ && canRetryBufferedBody && retryCount + 1 < maxAttempts && !res.headersSent) {
785
+ accountManager.markSessionIncompatible?.(requestInfo.sessionKey, requestInfo.homeProvider);
786
+ excludedIndexes.add(account.index);
787
+ console.log(`[Maxpool] Anthropic rejected this transcript (server_tool_use/thinking); pinning session to GLM/Kimi and retrying`);
788
+ return forwardRequest(
789
+ req, res, body, accountManager, upstream, retryCount + 1, hooks, reqId, ctx, logDir,
790
+ retryConfig, queueConfig, { ...requestInfo, anthropicIncompatible: true },
791
+ canRetryBufferedBody, canQueueBufferedBody, excludedIndexes,
792
+ );
793
+ }
794
+
771
795
  ctx.status = upstreamRes.status;
772
796
  sendErrorBody(res, requestInfo, upstreamRes.status, errorBody, upstreamRes.headers);
773
797
  return;
@@ -987,15 +1011,15 @@ function computeQueueWindowMs({
987
1011
  }
988
1012
 
989
1013
  function unavailableMessage(accountManager, requestInfo = {}, retryAfter, willRecoverSoon = true) {
990
- const origin = accountManager._effectiveOrigin?.(requestInfo) || { class: null, provider: null };
1014
+ const incompat = accountManager._effectiveIncompatible?.(requestInfo) || { incompatible: false, homeProvider: null };
991
1015
 
992
- // A foreign (GLM/Kimi)-origin session is pinned to provider accounts — Anthropic
993
- // would 400 on its tool-use ids. "All Claude at limit" would be the wrong story
994
- // (the Claude accounts are irrelevant to it); say what actually blocks it.
995
- if (origin.class === 'foreign') {
996
- const fam = origin.provider === 'zai' ? 'GLM' : origin.provider === 'kimi' ? 'Kimi' : 'GLM/Kimi';
1016
+ // An Anthropic-incompatible session is pinned to provider accounts — Anthropic
1017
+ // 400s on its server_tool_use id / foreign thinking. "All Claude at limit" would
1018
+ // be the wrong story (Claude is irrelevant to it); say what actually blocks it.
1019
+ if (incompat.incompatible) {
1020
+ const fam = incompat.homeProvider === 'zai' ? 'GLM' : incompat.homeProvider === 'kimi' ? 'Kimi' : 'GLM/Kimi';
997
1021
  const eta = Number.isFinite(retryAfter) && retryAfter > 0 ? ` Retry in ${retryAfter}s.` : '';
998
- return `This session started on ${fam}, so only GLM/Kimi can serve it (a GLM/Kimi transcript can't run on Claude — Anthropic rejects its tool-call ids). No GLM/Kimi provider is available right now.${eta} Check the x-maxpool-zai-token / x-maxpool-kimi-token headers, or resume this session with 'cc glm'/'cc kimi'.`;
1022
+ return `This session's transcript can only run on GLM/Kimi — Claude rejects its server-tool ids/thinking on replay. No GLM/Kimi provider is available right now.${eta} Check the x-maxpool-zai-token / x-maxpool-kimi-token headers, or resume with 'cc ${incompat.homeProvider === 'kimi' ? 'kimi' : 'glm'}'.`;
999
1023
  }
1000
1024
 
1001
1025
  const thinking = requestInfo.requiresAnthropicThinkingIntegrity
@@ -1021,7 +1045,7 @@ function unavailableMessage(accountManager, requestInfo = {}, retryAfter, willRe
1021
1045
  return `All ${n} accounts exhausted. Retry in ${retryAfter}s.`;
1022
1046
  }
1023
1047
 
1024
- export const __serverTest = { unavailableMessage, computeQueueWindowMs, isRetriableUpstreamStatus, headerValue, getMaxpoolProfile, ensureQueueHeartbeat, clearQueueHeartbeat, describeRequest, classifyRateLimit, detectTranscriptOrigin };
1048
+ export const __serverTest = { unavailableMessage, computeQueueWindowMs, isRetriableUpstreamStatus, headerValue, getMaxpoolProfile, ensureQueueHeartbeat, clearQueueHeartbeat, describeRequest, classifyRateLimit, detectTranscriptOrigin, isAnthropicIncompatBody };
1025
1049
 
1026
1050
  async function readErrorBody(upstreamRes, limitBytes = 64 * 1024) {
1027
1051
  if (!upstreamRes.body) return '';
@@ -1489,15 +1513,13 @@ function describeRequest(req, body) {
1489
1513
  // rebalancing); an unparsed body leaves this false → treated as NOT safe
1490
1514
  // (fail-closed) so we never replay a signed thinking block to a new account.
1491
1515
  info.bodyThinkingScanned = true;
1492
- // Home-provider origin of the resumed transcript (from tool-use id shapes).
1493
- // A 'foreign' (GLM/Kimi) session can NEVER be replayed to an Anthropic account
1494
- // (Anthropic 400s on a non-srvtoolu_ server_tool_use id) — routing pins it to
1495
- // provider accounts. A missing originClass = no tool blocks yet (ambiguous) OR
1496
- // an unparsed body; both fail OPEN in routing (never stranded), and stickiness
1497
- // (once a foreign id is seen) keeps a latched session pinned regardless.
1516
+ // Whether the resumed transcript can replay to a Claude account. A session with
1517
+ // a foreign server_tool_use id CANNOT (Anthropic 400s) → routing pins it to
1518
+ // providers. Everything else stays Claude-eligible (Kimi/GLM without server-tools
1519
+ // replay fine); a rare rejected-thinking 400 self-heals via react-and-heal.
1498
1520
  const origin = detectTranscriptOrigin(json);
1499
- if (origin.class) info.originClass = origin.class;
1500
- if (origin.provider) info.originProvider = origin.provider;
1521
+ if (origin.anthropicIncompatible) info.anthropicIncompatible = true;
1522
+ if (origin.homeProvider) info.homeProvider = origin.homeProvider;
1501
1523
  } catch {
1502
1524
  // Non-JSON requests are rare; body size still gives a useful load signal.
1503
1525
  }
@@ -1517,39 +1539,57 @@ function requiresAnthropicThinkingIntegrity(json) {
1517
1539
  // anything NOT matching this is treated as foreign (fail-closed classification).
1518
1540
  const ANTHROPIC_TOOL_ID = /^(toolu|srvtoolu)_/;
1519
1541
 
1520
- // Classify a resumed transcript's HOME PROVIDER from the tool-use id shapes in its
1521
- // replayed `messages`. Returns { class: 'foreign' | 'anthropic' | null, provider:
1522
- // 'zai' | 'kimi' | null }. ANY non-Anthropic tool-use / server_tool_use / tool_result
1523
- // id ⇒ 'foreign' (that session can never be replayed to Anthropic). 'anthropic' if
1524
- // only Anthropic ids seen; null if no tool blocks at all (ambiguous — a fresh session
1525
- // with no incompatible history yet). The request MODEL is NOT used — Claude Code
1526
- // rewrites it to claude-opus-4-8 on resume, so only the transcript is trustworthy.
1542
+ // Decide whether a resumed transcript can replay to an Anthropic (Claude) account,
1543
+ // from the tool-use id shapes in its `messages`. Returns { anthropicIncompatible,
1544
+ // homeProvider }.
1545
+ // anthropicIncompatible = the transcript has a `server_tool_use` id NOT matching
1546
+ // ^srvtoolu_ — the ONE DETERMINISTIC incompatibility (Anthropic 400s on replay,
1547
+ // the reported bug). Anthropic validates a client `tool_use.id` LOOSELY
1548
+ // (^[a-zA-Z0-9_-]+$), so GLM `call_…` / Kimi `tool_…` client ids PASS and a
1549
+ // Kimi/GLM session without server-tools is FINE on Claude — we do NOT predict
1550
+ // those (a rejected thinking signature, if it ever happens, self-heals via the
1551
+ // 4xx react-and-heal in forwardRequest). server_tool_use is rare (GLM ~15%,
1552
+ // Kimi 0%), so most sessions are compatible.
1553
+ // homeProvider = the first foreign tool-use id shape (call_→zai, tool_→kimi), a
1554
+ // SOFT hint for the 'never' same-family preference only; ambiguous/none → null.
1555
+ // The request MODEL is not used — Claude Code rewrites it to opus on resume.
1527
1556
  function detectTranscriptOrigin(json) {
1528
- if (!json || typeof json !== 'object') return { class: null, provider: null };
1529
- let sawAnthropic = false;
1530
- let foreignProvider = null; // 'zai' | 'kimi' | 'foreign'
1531
- const classifyForeign = id =>
1532
- id.startsWith('call_') ? 'zai' : id.startsWith('tool_') ? 'kimi' : 'foreign';
1557
+ if (!json || typeof json !== 'object') return { anthropicIncompatible: false, homeProvider: null };
1558
+ let incompatible = false;
1559
+ let homeProvider = null;
1560
+ const fam = id => id.startsWith('call_') ? 'zai' : id.startsWith('tool_') ? 'kimi' : null;
1561
+ const noteForeign = id => { if (!homeProvider) homeProvider = fam(id); };
1533
1562
  const visit = value => {
1534
- if (foreignProvider) return; // one foreign id is decisive — early-exit the walk
1535
- if (Array.isArray(value)) { for (const v of value) { visit(v); if (foreignProvider) return; } return; }
1563
+ if (incompatible) return; // server_tool_use is decisive — stop
1564
+ if (Array.isArray(value)) { for (const v of value) { visit(v); if (incompatible) return; } return; }
1536
1565
  if (!value || typeof value !== 'object') return;
1537
1566
  const t = value.type;
1538
- if ((t === 'tool_use' || t === 'server_tool_use') && typeof value.id === 'string') {
1539
- if (ANTHROPIC_TOOL_ID.test(value.id)) sawAnthropic = true;
1540
- else { foreignProvider = classifyForeign(value.id); return; }
1541
- }
1542
- if (t === 'tool_result' && typeof value.tool_use_id === 'string') {
1543
- if (ANTHROPIC_TOOL_ID.test(value.tool_use_id)) sawAnthropic = true;
1544
- else { foreignProvider = classifyForeign(value.tool_use_id); return; }
1567
+ if (t === 'server_tool_use' && typeof value.id === 'string' && !ANTHROPIC_TOOL_ID.test(value.id)) {
1568
+ incompatible = true; noteForeign(value.id); return;
1545
1569
  }
1570
+ if (t === 'tool_use' && typeof value.id === 'string' && !ANTHROPIC_TOOL_ID.test(value.id)) noteForeign(value.id);
1571
+ if (t === 'tool_result' && typeof value.tool_use_id === 'string' && !ANTHROPIC_TOOL_ID.test(value.tool_use_id)) noteForeign(value.tool_use_id);
1546
1572
  if (value.content) visit(value.content);
1547
1573
  if (value.messages) visit(value.messages);
1548
1574
  };
1549
1575
  visit(json.messages);
1550
- if (foreignProvider) return { class: 'foreign', provider: foreignProvider === 'foreign' ? null : foreignProvider };
1551
- if (sawAnthropic) return { class: 'anthropic', provider: null };
1552
- return { class: null, provider: null };
1576
+ return { anthropicIncompatible: incompatible, homeProvider };
1577
+ }
1578
+
1579
+ // Does this upstream 4xx error body indicate the transcript is un-replayable to
1580
+ // Anthropic (a lenient GLM/Kimi provider produced content Anthropic rejects on
1581
+ // replay)? Matches the three known shapes: a non-srvtoolu_ server_tool_use id, or a
1582
+ // thinking-block signature Anthropic can't validate (invalid OR missing). Used to
1583
+ // self-heal onto a provider instead of surfacing the 400.
1584
+ function isAnthropicIncompatBody(body) {
1585
+ if (!body) return false;
1586
+ // The deterministic server-tool-id 400, the known invalid-signature error, or a
1587
+ // thinking-block signature VALIDATION error (missing/invalid) — the last gated on
1588
+ // a validation verb so a user message merely echoing "thinking"/"signature" can't
1589
+ // false-latch the session provider-pinned for life.
1590
+ return /srvtoolu_|server_tool_use/.test(body)
1591
+ || /invalid `signature` in `thinking`/i.test(body)
1592
+ || (/thinking/.test(body) && /signature/.test(body) && /(should match|required|invalid|expected|must )/i.test(body));
1553
1593
  }
1554
1594
 
1555
1595
  function containsThinkingBlock(value) {
package/src/tui.js CHANGED
@@ -864,6 +864,11 @@ export class TUI {
864
864
  lines.push(yellow(' No accounts configured. Press [a] to add one.'));
865
865
  } else {
866
866
  lines.push('');
867
+ // Column legend — the per-row numbers are otherwise cryptic. Ses/Wk are the
868
+ // two quota bars; Now is live concurrency; 15m/1h are recent throughput.
869
+ if (W >= 88) {
870
+ lines.push(' ' + dim('Ses/Wk = 5h/7d quota (used% · resets-in) · Now = in-flight (weight) · 15m/1h = requests served (avg latency · Nf=fails)'));
871
+ }
867
872
  const showBoth = W >= 70;
868
873
  const bw = showBoth
869
874
  ? Math.max(5, Math.min(20, Math.floor((W - 56) / 2)))