maxpool 1.5.29 → 1.5.31

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maxpool",
3
- "version": "1.5.29",
3
+ "version": "1.5.31",
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",
@@ -1609,6 +1609,13 @@ export class AccountManager {
1609
1609
  _isRequestCompatible(account, profile, requestInfo = {}) {
1610
1610
  if (!this._matchesProfile(account, profile)) return false;
1611
1611
 
1612
+ // Kimi (Moonshot) 400s on images Anthropic/GLM accept ("failed to decode
1613
+ // image") — and a provider 400 is TERMINAL (not retried to another account), so
1614
+ // an image request that fell back to Kimi FAILS the whole request. Keep image
1615
+ // requests off Kimi; they still route to GLM (handles images) + OAuth. If those
1616
+ // are all unavailable the request HOLDS/queues (recoverable) rather than 400ing.
1617
+ if (requestInfo.hasImage && account.provider === 'kimi') return false;
1618
+
1612
1619
  const { incompatible, homeProvider } = this._effectiveIncompatible(requestInfo);
1613
1620
  const policy = this._crossProviderFallbackPolicy();
1614
1621
 
package/src/server.js CHANGED
@@ -123,7 +123,7 @@ export function createProxyServer(accountManager, config, hooks = {}) {
123
123
  if (req.method === 'POST' && req.url === '/v1/oauth/token') {
124
124
  const reqId = ++requestCounter;
125
125
  const ctx = { account: '(oauth relay)', status: null };
126
- const accepted = hooks.onRequestStart?.(reqId, { method: req.method, path: req.url });
126
+ const accepted = hooks.onRequestStart?.(reqId, { method: req.method, path: req.url, sessionKey: headerValue(req.headers, 'x-maxpool-session') });
127
127
  if (accepted === false) {
128
128
  res.writeHead(503, {
129
129
  'Content-Type': 'application/json',
@@ -156,7 +156,10 @@ export function createProxyServer(accountManager, config, hooks = {}) {
156
156
 
157
157
  // Track request
158
158
  const reqId = ++requestCounter;
159
- const accepted = hooks.onRequestStart?.(reqId, { method: req.method, path: req.url });
159
+ // sessionKey (from the per-terminal x-maxpool-session header) lets the TUI show
160
+ // how many distinct client sessions the in-flight requests span — so "N in-flight"
161
+ // isn't misread as "N sessions" (subagents of one terminal share one session key).
162
+ const accepted = hooks.onRequestStart?.(reqId, { method: req.method, path: req.url, sessionKey: headerValue(req.headers, 'x-maxpool-session') });
160
163
  if (accepted === false) {
161
164
  res.writeHead(503, {
162
165
  'Content-Type': 'application/json',
@@ -1593,6 +1596,9 @@ function describeRequest(req, body) {
1593
1596
  const origin = detectTranscriptOrigin(json);
1594
1597
  if (origin.anthropicIncompatible) info.anthropicIncompatible = true;
1595
1598
  if (origin.homeProvider) info.homeProvider = origin.homeProvider;
1599
+ // Image content (incl. tool_result-nested screenshots) keeps a request off Kimi
1600
+ // (Moonshot 400s on some images GLM/Anthropic accept, and a 400 is terminal).
1601
+ if (containsImageBlock(json.messages)) info.hasImage = true;
1596
1602
  } catch {
1597
1603
  // Non-JSON requests are rare; body size still gives a useful load signal.
1598
1604
  }
@@ -1679,6 +1685,21 @@ function containsThinkingBlock(value) {
1679
1685
  return false;
1680
1686
  }
1681
1687
 
1688
+ // True if any message carries an `{type:'image'}` content block at ANY nesting
1689
+ // depth — including images nested inside a `tool_result` block (the primary
1690
+ // Claude Code image path: a browser/Playwright tool returning a screenshot). A
1691
+ // shallow 2-level scan misses those. Used to keep image requests off Kimi, whose
1692
+ // decoder 400s on images Anthropic/GLM accept.
1693
+ function containsImageBlock(value) {
1694
+ if (!value) return false;
1695
+ if (Array.isArray(value)) return value.some(containsImageBlock);
1696
+ if (typeof value !== 'object') return false;
1697
+ if (value.type === 'image') return true;
1698
+ if (value.content && containsImageBlock(value.content)) return true;
1699
+ if (value.messages && containsImageBlock(value.messages)) return true;
1700
+ return false;
1701
+ }
1702
+
1682
1703
  function getMaxpoolProfile(headers) {
1683
1704
  // headerValue() handles the x-teamclaude-* legacy fallback.
1684
1705
  const profile = String(headerValue(headers, 'x-maxpool-profile') || 'claude').trim().toLowerCase();
package/src/tui.js CHANGED
@@ -982,8 +982,15 @@ export class TUI {
982
982
 
983
983
  // ── Activity header
984
984
  lines.push('');
985
+ // "N active" reads as "N sessions" — but this is in-flight REQUESTS (Claude Code
986
+ // fans out many parallel subagent requests per session). Label it "in-flight" and
987
+ // show the distinct SESSION count (subagents of one terminal share one session key)
988
+ // so a busy 1-2 sessions firing dozens of requests isn't mistaken for a leak.
985
989
  const ac = this.active.size;
986
- const acTag = ac > 0 ? ` ${cyan(ac + ' active')}` : '';
990
+ const sessions = new Set([...this.active.values()].map(r => r.sessionKey).filter(Boolean)).size;
991
+ const acTag = ac > 0
992
+ ? ` ${cyan(ac + ' in-flight')}${sessions ? dim(` · ${sessions} session${sessions === 1 ? '' : 's'}`) : ''}`
993
+ : '';
987
994
  const aHdr = ` Activity${acTag} `;
988
995
  lines.push(aHdr + dim('─'.repeat(Math.max(1, W - vw(aHdr)))));
989
996