maxpool 1.21.2 → 1.21.4

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/server.js +78 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maxpool",
3
- "version": "1.21.2",
3
+ "version": "1.21.4",
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",
package/src/server.js CHANGED
@@ -1045,6 +1045,20 @@ async function forwardRequest(
1045
1045
  // '^srvtoolu_…'`. Repairable by converting the pair to text (verified 200 OK) —
1046
1046
  // it used to fall through to a PERMANENT provider pin.
1047
1047
  || /server_tool_use\.id: String should match pattern/i.test(errorBody));
1048
+ // ORDERING REJECTION (2026-09-23): a mid-conversation `system` message sitting where
1049
+ // the API no longer accepts it. Anthropic has now stated the rule twice, differently:
1050
+ // Aug: "must precede an 'assistant' message or end the array"
1051
+ // Sep: "must follow a 'user' message or an 'assistant' message ending in a server
1052
+ // tool result; the directive-only form (content: []) is accepted at any position"
1053
+ // The CLI legitimately emits mid-conversation system messages (mid-conversation-system
1054
+ // beta: compaction boundaries, injected reminders) at messages.NNN deep in history, so
1055
+ // this fires on ordinary long sessions. Repairable WITHOUT dropping anything: convert
1056
+ // the offending system to the directive-only form the API accepts at any position
1057
+ // (content: [], text moved into output_config) — a shape-preserving transformation,
1058
+ // not the orphaning re-anchor.
1059
+ const isOrderingRejection = account.type !== 'provider'
1060
+ && upstreamRes.status === 400
1061
+ && /role 'system' must (follow|precede)/i.test(errorBody);
1048
1062
  // LOG THE ACTUAL REASON. Previously a 4xx recorded only "HTTP 400" and the upstream
1049
1063
  // message was never written anywhere, so a whole class of failures (e.g. a rejected
1050
1064
  // effort level breaking every web search) was invisible in the log — you could not
@@ -1174,6 +1188,28 @@ async function forwardRequest(
1174
1188
  // rewrite it saves nothing; it just guarantees the 400 surfaces. Reported
1175
1189
  // 2026-08-10: "history too large to rewrite automatically" on a session the strip
1176
1190
  // would have fixed in 20ms. The retry it schedules re-checks the SHRUNK size.
1191
+ // ORDERING RECOVERY (2026-09-23): convert the offending system message(s) to the
1192
+ // directive-only form and retry on the SAME account — the request is now valid by
1193
+ // the API's own stated rule, so no failover is needed and the user never sees the
1194
+ // 400. One-shot per request via its own flag so a second ordering 400 (a rule we
1195
+ // have not modeled) still surfaces honestly instead of looping.
1196
+ if (isOrderingRejection && !requestInfo.orderingRepaired && canRepairBody) {
1197
+ const coord = /messages\.(\d+)/.exec(errorBody);
1198
+ const { messages: fixedMessages, converted } = directiveOnlySystemMessages(
1199
+ JSON.parse(body.toString('utf8')).messages ?? [],
1200
+ coord ? Number(coord[1]) : -1);
1201
+ if (converted > 0) {
1202
+ const json = JSON.parse(body.toString('utf8'));
1203
+ json.messages = fixedMessages;
1204
+ const fixedBody = Buffer.from(JSON.stringify(json));
1205
+ console.log(`[Maxpool] Recovering session on Claude: converted ${converted} mis-positioned system message(s) to directive-only form`);
1206
+ return forwardRequest(
1207
+ req, res, fixedBody, accountManager, upstream, retryCount + 1, hooks, reqId, ctx, logDir,
1208
+ retryConfig, queueConfig, { ...requestInfo, orderingRepaired: true, repairCount: repairCount + 1 },
1209
+ fixedBody.length <= retryConfig.maxRetryBufferBytes, canQueueBufferedBody, excludedIndexes,
1210
+ );
1211
+ }
1212
+ }
1177
1213
  if (isSignatureRejection && !requestInfo.thinkingStripped && canRepairBody) {
1178
1214
  console.log(`[Maxpool] Anthropic rejected a block: ${describeRejectedBlock(body, errorBody)}`);
1179
1215
  const { body: cleanBody, removed, converted } = stripForeignThinkingBlocks(body);
@@ -1809,7 +1845,7 @@ function isContextLengthError(errorBody) {
1809
1845
  return /exceeded model token limit|maximum context length|context length exceeded|context window (?:size )?(?:exceeded|too)|prompt is too long|input is too long|reduce the length of|too many (?:input )?tokens|request too large/i.test(errorBody);
1810
1846
  }
1811
1847
 
1812
- export const __serverTest = { rewriteBodyForAccount, sanitizeBlocksForProvider, reanchorOrphanedSystemMessages, unavailableMessage, computeQueueWindowMs, isRetriableUpstreamStatus, classifyEffortRejection, repairEffort, isCapacitySignalStatus, isStrippableThinkingBlock, stripForeignThinkingBlocks, parseRejectedBlockPath, stripRejectedBlockClass, peekRejectedBlockType, describeRejectedBlock, headerValue, getMaxpoolProfile, ensureQueueHeartbeat, clearQueueHeartbeat, commitStreamGraceHeartbeat, describeRequest, classifyRateLimit, detectTranscriptOrigin, isAnthropicIncompatBody, isContextLengthError, isProviderParamRejection, describeBodyShape, streamResponse, startIdleRequestReaper, normalizeModelEcho, classifyHeldStreamPrefix };
1848
+ export const __serverTest = { directiveOnlySystemMessages, rewriteBodyForAccount, sanitizeBlocksForProvider, reanchorOrphanedSystemMessages, unavailableMessage, computeQueueWindowMs, isRetriableUpstreamStatus, classifyEffortRejection, repairEffort, isCapacitySignalStatus, isStrippableThinkingBlock, stripForeignThinkingBlocks, parseRejectedBlockPath, stripRejectedBlockClass, peekRejectedBlockType, describeRejectedBlock, headerValue, getMaxpoolProfile, ensureQueueHeartbeat, clearQueueHeartbeat, commitStreamGraceHeartbeat, describeRequest, classifyRateLimit, detectTranscriptOrigin, isAnthropicIncompatBody, isContextLengthError, isProviderParamRejection, describeBodyShape, streamResponse, startIdleRequestReaper, normalizeModelEcho, classifyHeldStreamPrefix };
1813
1849
 
1814
1850
  async function readErrorBody(upstreamRes, limitBytes = 64 * 1024) {
1815
1851
  if (!upstreamRes.body) return '';
@@ -2083,6 +2119,41 @@ function describeRejectedBlock(body, errorBody) {
2083
2119
  * Idempotent: a second run finds no violation, so the latched re-strip cannot grow the
2084
2120
  * transcript turn after turn.
2085
2121
  */
2122
+ /** ORDERING REPAIR (2026-09-23): convert an illegally-positioned mid-conversation
2123
+ * system message into the directive-only form the API accepts ANYWHERE — content: []
2124
+ * with the original text preserved in output_config. Shape-preserving (nothing dropped,
2125
+ * nothing orphaned), idempotent (a directive-only system is already legal), and driven
2126
+ * by the upstream's own coordinate when it names one, else all violating systems.
2127
+ * Returns { messages, converted }.
2128
+ */
2129
+ function directiveOnlySystemMessages(messages, coordIndex = -1) {
2130
+ let converted = 0;
2131
+ // Under the Sep rule, a system is legal when preceded by nothing (start), by a user
2132
+ // message, or by an assistant ENDING IN a server tool result; and always when it is
2133
+ // already directive-only. Everything else is a violation.
2134
+ const violates = (i) => {
2135
+ const m = messages[i];
2136
+ if (m?.role !== 'system') return false;
2137
+ if (Array.isArray(m.content) && m.content.length === 0) return false; // directive-only
2138
+ if (i === 0) return false; // start-of-array — governed by first-message rules, not this
2139
+ const prev = messages[i - 1];
2140
+ if (prev?.role === 'user') return false;
2141
+ if (prev?.role === 'assistant' && Array.isArray(prev.content) && prev.content.length
2142
+ && prev.content[prev.content.length - 1]?.type === 'server_tool_result') return false;
2143
+ return true;
2144
+ };
2145
+ const out = messages.map((m, i) => {
2146
+ if (coordIndex >= 0 ? i !== coordIndex : !violates(i)) return m;
2147
+ const text = (Array.isArray(m.content) ? m.content : [])
2148
+ .map(b => (typeof b?.text === 'string' ? b.text : ''))
2149
+ .filter(Boolean).join('\n');
2150
+ converted++;
2151
+ // output_config shape per the API's own 400 text: directive-only system.
2152
+ return { role: 'system', content: [], output_config: { directives: text } };
2153
+ });
2154
+ return { messages: out, converted };
2155
+ }
2156
+
2086
2157
  function reanchorOrphanedSystemMessages(messages) {
2087
2158
  let inserted = 0;
2088
2159
  const out = [];
@@ -3266,8 +3337,13 @@ async function streamResponse(webStream, res, status, responseHeaders, accountIn
3266
3337
  modelEchoBuffer = modelEchoBuffer ? [...modelEchoBuffer, value] : [value];
3267
3338
  const s = decoder.decode(concatUint8(modelEchoBuffer));
3268
3339
  if (s.includes('"model"') && s.includes('\n\n')) {
3340
+ // \s* — providers serialize SSE JSON with spaces ("model": "glm-5.3"),
3341
+ // Anthropic compact ("model":"…"). The tight form shipped 2026-08-31 and never
3342
+ // matched a single real z.ai byte (all 1,546 glm rows in this very session
3343
+ // leaked through it; fixture JSON was hand-written compact, so tests stayed
3344
+ // green while production leaked. 2026-09-23.
3269
3345
  const normalized = s.replace(
3270
- /("model":")[^"]+(")/,
3346
+ /("model"\s*:\s*")[^"]+(")/,
3271
3347
  `$1${requestInfo.model.replace(/["\\]/g, '\\$&')}$2`,
3272
3348
  );
3273
3349
  out = Buffer.from(normalized, 'utf8');