maxpool 1.21.2 → 1.21.3
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 +1 -1
- package/src/server.js +72 -1
package/package.json
CHANGED
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 = [];
|