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 +3 -1
- package/package.json +1 -1
- package/src/account-manager.js +55 -58
- package/src/server.js +80 -40
- package/src/tui.js +5 -0
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
|
|
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
package/src/account-manager.js
CHANGED
|
@@ -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
|
-
//
|
|
1038
|
-
//
|
|
1039
|
-
//
|
|
1040
|
-
if (this.
|
|
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
|
|
1044
|
-
//
|
|
1045
|
-
//
|
|
1046
|
-
//
|
|
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.
|
|
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
|
|
1510
|
-
//
|
|
1511
|
-
//
|
|
1512
|
-
//
|
|
1513
|
-
//
|
|
1514
|
-
|
|
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
|
-
|
|
1524
|
-
|
|
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
|
|
1530
|
+
const { incompatible, homeProvider } = this._effectiveIncompatible(requestInfo);
|
|
1531
|
+
const policy = this._crossProviderFallbackPolicy();
|
|
1532
1532
|
|
|
1533
|
-
if (
|
|
1534
|
-
//
|
|
1535
|
-
// Anthropic
|
|
1536
|
-
//
|
|
1537
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
1549
|
-
//
|
|
1550
|
-
//
|
|
1551
|
-
//
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
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.
|
|
1565
|
-
this.
|
|
1560
|
+
if (requestInfo.anthropicIncompatible) {
|
|
1561
|
+
this.markSessionIncompatible(requestInfo.sessionKey, requestInfo.homeProvider);
|
|
1566
1562
|
}
|
|
1567
1563
|
}
|
|
1568
1564
|
|
|
1569
|
-
// Latch a session
|
|
1570
|
-
// Anthropic
|
|
1571
|
-
// provider-pinned
|
|
1572
|
-
|
|
1573
|
-
if (!sessionKey
|
|
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.
|
|
1576
|
-
console.log(`[Maxpool] Session "${sessionKey}" is ${
|
|
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
|
-
|
|
1581
|
-
|
|
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
|
-
|
|
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
|
|
1014
|
+
const incompat = accountManager._effectiveIncompatible?.(requestInfo) || { incompatible: false, homeProvider: null };
|
|
991
1015
|
|
|
992
|
-
//
|
|
993
|
-
//
|
|
994
|
-
//
|
|
995
|
-
if (
|
|
996
|
-
const fam =
|
|
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
|
|
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
|
-
//
|
|
1493
|
-
//
|
|
1494
|
-
//
|
|
1495
|
-
//
|
|
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.
|
|
1500
|
-
if (origin.
|
|
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
|
-
//
|
|
1521
|
-
//
|
|
1522
|
-
//
|
|
1523
|
-
//
|
|
1524
|
-
//
|
|
1525
|
-
//
|
|
1526
|
-
//
|
|
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 {
|
|
1529
|
-
let
|
|
1530
|
-
let
|
|
1531
|
-
const
|
|
1532
|
-
|
|
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 (
|
|
1535
|
-
if (Array.isArray(value)) { for (const v of value) { visit(v); if (
|
|
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 (
|
|
1539
|
-
|
|
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
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
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)))
|