maxpool 1.5.29 → 1.5.30

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.30",
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
@@ -1593,6 +1593,9 @@ function describeRequest(req, body) {
1593
1593
  const origin = detectTranscriptOrigin(json);
1594
1594
  if (origin.anthropicIncompatible) info.anthropicIncompatible = true;
1595
1595
  if (origin.homeProvider) info.homeProvider = origin.homeProvider;
1596
+ // Image content (incl. tool_result-nested screenshots) keeps a request off Kimi
1597
+ // (Moonshot 400s on some images GLM/Anthropic accept, and a 400 is terminal).
1598
+ if (containsImageBlock(json.messages)) info.hasImage = true;
1596
1599
  } catch {
1597
1600
  // Non-JSON requests are rare; body size still gives a useful load signal.
1598
1601
  }
@@ -1679,6 +1682,21 @@ function containsThinkingBlock(value) {
1679
1682
  return false;
1680
1683
  }
1681
1684
 
1685
+ // True if any message carries an `{type:'image'}` content block at ANY nesting
1686
+ // depth — including images nested inside a `tool_result` block (the primary
1687
+ // Claude Code image path: a browser/Playwright tool returning a screenshot). A
1688
+ // shallow 2-level scan misses those. Used to keep image requests off Kimi, whose
1689
+ // decoder 400s on images Anthropic/GLM accept.
1690
+ function containsImageBlock(value) {
1691
+ if (!value) return false;
1692
+ if (Array.isArray(value)) return value.some(containsImageBlock);
1693
+ if (typeof value !== 'object') return false;
1694
+ if (value.type === 'image') return true;
1695
+ if (value.content && containsImageBlock(value.content)) return true;
1696
+ if (value.messages && containsImageBlock(value.messages)) return true;
1697
+ return false;
1698
+ }
1699
+
1682
1700
  function getMaxpoolProfile(headers) {
1683
1701
  // headerValue() handles the x-teamclaude-* legacy fallback.
1684
1702
  const profile = String(headerValue(headers, 'x-maxpool-profile') || 'claude').trim().toLowerCase();