@code-yeongyu/senpi-ai 2026.10.10 → 2026.10.12
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/dist/api/anthropic-messages.js +148 -72
- package/dist/api/anthropic-native-replay-blocks.d.ts +4 -0
- package/dist/api/anthropic-native-replay-blocks.js +28 -0
- package/dist/api/anthropic-tool-references.js +24 -18
- package/dist/api/anthropic-web-search-expiry.d.ts +10 -0
- package/dist/api/anthropic-web-search-expiry.js +104 -0
- package/dist/api/bedrock-converse-stream.js +5 -1
- package/dist/api/google-shared.js +101 -21
- package/dist/api/lazy.js +13 -0
- package/dist/api/mistral-conversations.js +18 -3
- package/dist/api/openai-codex-responses.js +2 -2
- package/dist/api/openai-completions.js +26 -2
- package/dist/api/openai-responses-allowed-tools.d.ts +33 -0
- package/dist/api/openai-responses-allowed-tools.js +108 -0
- package/dist/api/openai-responses-shared.js +19 -0
- package/dist/api/openai-responses.js +48 -59
- package/dist/auth/oauth/anthropic.js +4 -3
- package/dist/auth/oauth/chatgpt-subscription.js +3 -2
- package/dist/auth/oauth/cursor.js +3 -2
- package/dist/auth/oauth/devin-token.js +2 -1
- package/dist/auth/oauth/github-copilot.js +3 -2
- package/dist/auth/oauth/kimi-coding.js +5 -4
- package/dist/auth/oauth/openai-chatgpt.js +2 -1
- package/dist/auth/oauth/openrouter.js +2 -1
- package/dist/auth/oauth/radius.js +5 -5
- package/dist/auth/oauth/xai.js +7 -4
- package/dist/auth/oauth-refresh.js +10 -0
- package/dist/auth/resolve.js +5 -0
- package/dist/model.d.ts +6 -2
- package/dist/openai-responses-compat.d.ts +7 -2
- package/dist/openai-responses-compat.js +15 -2
- package/dist/providers/all.d.ts +1 -1
- package/dist/providers/data/.manifest.json +1 -1
- package/dist/providers/data/amazon-bedrock.json +1 -1
- package/dist/providers/data/anthropic.json +1 -1
- package/dist/providers/data/baseten.json +1 -1
- package/dist/providers/data/chatgpt-subscription.json +1 -1
- package/dist/providers/data/cloudflare-ai-gateway.json +1 -1
- package/dist/providers/data/cloudflare-workers-ai.json +1 -1
- package/dist/providers/data/github-copilot.json +1 -1
- package/dist/providers/data/google-vertex.json +1 -1
- package/dist/providers/data/huggingface.json +1 -1
- package/dist/providers/data/mistral.json +1 -1
- package/dist/providers/data/moonshotai-cn.json +1 -1
- package/dist/providers/data/moonshotai.json +1 -1
- package/dist/providers/data/opencode-go.json +1 -1
- package/dist/providers/data/opencode.json +1 -1
- package/dist/providers/data/opengateway.json +1 -1
- package/dist/providers/data/openrouter.json +1 -1
- package/dist/providers/data/radius.json +1 -1
- package/dist/providers/data/venice.json +1 -1
- package/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/dist/providers/kimi-coding.models.d.ts +1 -1
- package/dist/providers/kimi-coding.models.js +1 -1
- package/dist/providers/opengateway-refresh.js +2 -1
- package/dist/providers/xai.js +7 -1
- package/dist/types.d.ts +25 -2
- package/dist/utils/oauth-refresh-error.d.ts +16 -0
- package/dist/utils/oauth-refresh-error.js +67 -0
- package/dist/utils/prompt-cache-ttl.d.ts +2 -1
- package/dist/utils/prompt-cache-ttl.js +4 -1
- package/dist/utils/provider-failure-description.d.ts +3 -1
- package/dist/utils/provider-failure-description.js +8 -0
- package/dist/utils/retry.d.ts +6 -0
- package/dist/utils/retry.js +10 -0
- package/dist/utils/tool-choice-fallback.d.ts +11 -0
- package/dist/utils/tool-choice-fallback.js +69 -2
- package/package.json +3 -3
|
@@ -27,8 +27,10 @@ import { normalizeToolCallId } from "../utils/tool-call-id.js";
|
|
|
27
27
|
import { sendWithForcedToolChoiceFallback } from "../utils/tool-choice-fallback.js";
|
|
28
28
|
import { resolveRootObjectSchema } from "../utils/tool-schema-compat.js";
|
|
29
29
|
import { getCurrentTools, getDeclaredTools, getInitialSystemMessage, hasToolRedefinitions, normalizeContext, resolveTranscript, } from "../utils/transcript.js";
|
|
30
|
+
import { isAnthropicWebSearchReplayBlock, isReplayableAnthropicProviderNativeBlock, } from "./anthropic-native-replay-blocks.js";
|
|
30
31
|
import { sanitizeAnthropicToolPairs } from "./anthropic-tool-pairs.js";
|
|
31
32
|
import { demoteToolReferenceReplay, demoteUnavailableToolReferences } from "./anthropic-tool-references.js";
|
|
33
|
+
import { demoteExpiredWebSearchReplay, expiredWebSearchReplayIds, isExpiredWebSearchContentError, } from "./anthropic-web-search-expiry.js";
|
|
32
34
|
import { resolveCloudflareBaseUrl } from "./cloudflare.js";
|
|
33
35
|
import { getJsonSchemaToolParameters, resolveJsonSchemaStrictSampling, } from "./constrained-sampling.js";
|
|
34
36
|
import { withGitHubCopilotFailureNote } from "./github-copilot-errors.js";
|
|
@@ -71,7 +73,7 @@ function getCacheControl(model, cacheRetention, env) {
|
|
|
71
73
|
// The bundled Claude Code version and the floor of the advertised `claude-cli/<version>`
|
|
72
74
|
// (see utils/claude-code-version.ts). Keep this exact declaration: a downstream installer
|
|
73
75
|
// (oh-my-openagent) rewrites it byte-for-byte in the installed dist and bundle.
|
|
74
|
-
const claudeCodeVersion = "2.1.
|
|
76
|
+
const claudeCodeVersion = "2.1.292";
|
|
75
77
|
// Claude Code 2.x tool names (canonical casing)
|
|
76
78
|
// Source: https://cchistory.mariozechner.at/data/prompts-2.1.11.md
|
|
77
79
|
// To update: https://github.com/badlogic/cchistory
|
|
@@ -164,6 +166,7 @@ const ADAPTIVE_THINKING_MODEL_MARKERS = [
|
|
|
164
166
|
"opus-5",
|
|
165
167
|
"sonnet-4-6",
|
|
166
168
|
"sonnet-5",
|
|
169
|
+
"haiku-5-5",
|
|
167
170
|
"fable-5",
|
|
168
171
|
"mythos-5",
|
|
169
172
|
];
|
|
@@ -176,14 +179,20 @@ const NATIVE_XHIGH_EFFORT_MODEL_MARKERS = [
|
|
|
176
179
|
"opus-4-8",
|
|
177
180
|
"opus-5",
|
|
178
181
|
"sonnet-5",
|
|
182
|
+
"haiku-5-5",
|
|
179
183
|
"fable-5",
|
|
180
184
|
"mythos-5",
|
|
181
185
|
];
|
|
182
186
|
/**
|
|
183
|
-
*
|
|
184
|
-
* `"thinking.type.disabled" is not supported for this model`)
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
+
* Families senpi never sends `thinking: {type: "disabled"}` to. Fable 5, Opus 5.5 and Sonnet 5.5 reject it
|
|
188
|
+
* outright (verified live 400: `"thinking.type.disabled" is not supported for this model`); Mythos 5 is
|
|
189
|
+
* listed by family with Fable. Claude Haiku 5.5 is listed by choice, not because it rejects `disabled`: its
|
|
190
|
+
* effort docs accept `disabled` at effort `high` or below (400 only at `xhigh` / `max`) and reject a
|
|
191
|
+
* differing per-message effort while disabled, so a real thinking-off needs marker handling first
|
|
192
|
+
* (senpi#2927). Until then a thinking-off turn sends no `thinking` and effort `low`, which is valid on all of
|
|
193
|
+
* them. The generated catalog also encodes this as `compat.supportsDisabledThinking: false`, but
|
|
194
|
+
* `models.json` entries and third-party gateway rows carry no generated compat, so the family fact has to
|
|
195
|
+
* live here as well.
|
|
187
196
|
*/
|
|
188
197
|
const DISABLED_THINKING_REJECTING_MODEL_MARKERS = [
|
|
189
198
|
"fable-5",
|
|
@@ -192,6 +201,8 @@ const DISABLED_THINKING_REJECTING_MODEL_MARKERS = [
|
|
|
192
201
|
"opus-5.5",
|
|
193
202
|
"sonnet-5-5",
|
|
194
203
|
"sonnet-5.5",
|
|
204
|
+
"haiku-5-5",
|
|
205
|
+
"haiku-5.5",
|
|
195
206
|
];
|
|
196
207
|
const UNSUPPORTED_NATIVE_COMPUTER_TOOL_MODEL_MARKERS = [
|
|
197
208
|
"opus-4-6",
|
|
@@ -234,6 +245,8 @@ function supportsMidConvoToolChanges(model) {
|
|
|
234
245
|
const unsignedThinkingTextReplayFallbacks = new Set();
|
|
235
246
|
// Endpoints that rejected a replayed tool_reference for one conversation (senpi #2568).
|
|
236
247
|
const toolReferenceReplayFallbacks = new Set();
|
|
248
|
+
// `${fallbackKey}\u0000${tool_use_id}` of web search results whose encrypted payload expired (senpi #3201).
|
|
249
|
+
const expiredWebSearchReplays = new Set();
|
|
237
250
|
registerSessionResourceCleanup((sessionId) => {
|
|
238
251
|
// One small entry per (session, base URL, model) that ever hit the fallback;
|
|
239
252
|
// drop the session's entries at teardown so long-lived hosts do not collect
|
|
@@ -241,10 +254,15 @@ registerSessionResourceCleanup((sessionId) => {
|
|
|
241
254
|
if (sessionId === undefined) {
|
|
242
255
|
unsignedThinkingTextReplayFallbacks.clear();
|
|
243
256
|
toolReferenceReplayFallbacks.clear();
|
|
257
|
+
expiredWebSearchReplays.clear();
|
|
244
258
|
return;
|
|
245
259
|
}
|
|
246
260
|
const prefix = `${sessionId}\u0000`;
|
|
247
|
-
for (const fallbacks of [
|
|
261
|
+
for (const fallbacks of [
|
|
262
|
+
unsignedThinkingTextReplayFallbacks,
|
|
263
|
+
toolReferenceReplayFallbacks,
|
|
264
|
+
expiredWebSearchReplays,
|
|
265
|
+
]) {
|
|
248
266
|
for (const key of fallbacks) {
|
|
249
267
|
if (key.startsWith(prefix))
|
|
250
268
|
fallbacks.delete(key);
|
|
@@ -346,26 +364,6 @@ let federationClient;
|
|
|
346
364
|
function isRecord(value) {
|
|
347
365
|
return typeof value === "object" && value !== null;
|
|
348
366
|
}
|
|
349
|
-
const REPLAYABLE_ANTHROPIC_PROVIDER_NATIVE_TYPES = new Set([
|
|
350
|
-
"server_tool_use",
|
|
351
|
-
"web_search_tool_result",
|
|
352
|
-
"web_fetch_tool_result",
|
|
353
|
-
"code_execution_tool_result",
|
|
354
|
-
"bash_code_execution_tool_result",
|
|
355
|
-
"text_editor_code_execution_tool_result",
|
|
356
|
-
"tool_search_tool_result",
|
|
357
|
-
"container_upload",
|
|
358
|
-
// The server-side fallback marker (`fallback`, server-side-fallback-2026-06-01
|
|
359
|
-
// beta) is deliberately absent: the Messages API rejects it as an *input* tag
|
|
360
|
-
// ("Input tag 'fallback' found using 'type' does not match any of the expected
|
|
361
|
-
// tags"), so replaying it 400s every subsequent same-model request. The stored
|
|
362
|
-
// marker is audit metadata and still drives declined-attempt pruning in
|
|
363
|
-
// convertMessages (see lastAnthropicFallbackBoundary and
|
|
364
|
-
// collectDiscardedFallbackToolCallIds); blocks after it replay verbatim.
|
|
365
|
-
]);
|
|
366
|
-
function isReplayableAnthropicProviderNativeBlock(raw) {
|
|
367
|
-
return isRecord(raw) && typeof raw.type === "string" && REPLAYABLE_ANTHROPIC_PROVIDER_NATIVE_TYPES.has(raw.type);
|
|
368
|
-
}
|
|
369
367
|
function isSameAnthropicModel(message, model) {
|
|
370
368
|
return message.provider === model.provider && message.api === model.api && message.model === model.id;
|
|
371
369
|
}
|
|
@@ -399,17 +397,6 @@ function isAnthropicServerToolUseBlock(raw) {
|
|
|
399
397
|
function isProviderNativeToolUseBlock(raw) {
|
|
400
398
|
return isRecord(raw) && (raw.type === "server_tool_use" || raw.type === "mcp_tool_use");
|
|
401
399
|
}
|
|
402
|
-
// Endpoints without `supportsWebSearch` reject replayed web-search server-tool
|
|
403
|
-
// blocks (kimi-coding 400s with `tool_call_id is not found`), wedging every
|
|
404
|
-
// subsequent request of the session. Dropping the pair loses the searched
|
|
405
|
-
// context but keeps the conversation usable.
|
|
406
|
-
function isAnthropicWebSearchReplayBlock(raw) {
|
|
407
|
-
if (!isRecord(raw))
|
|
408
|
-
return false;
|
|
409
|
-
if (raw.type === "web_search_tool_result")
|
|
410
|
-
return true;
|
|
411
|
-
return raw.type === "server_tool_use" && raw.name === "web_search";
|
|
412
|
-
}
|
|
413
400
|
// The user turn a pending server tool can survive is one that carries only
|
|
414
401
|
// tool results. Anything else the wire would emit — real text, an image —
|
|
415
402
|
// closes the assistant turn (the API then 400s the still-open use). A blank
|
|
@@ -439,7 +426,7 @@ function collectProviderNativeToolPairing(messages, model, deferredToolNames, no
|
|
|
439
426
|
if (block.type !== "providerNative" || !isRecord(block.raw))
|
|
440
427
|
continue;
|
|
441
428
|
const raw = block.raw;
|
|
442
|
-
if (
|
|
429
|
+
if (!isReplayableAnthropicProviderNativeBlock(raw))
|
|
443
430
|
continue;
|
|
444
431
|
if (isProviderNativeToolUseBlock(raw) && typeof raw.id === "string")
|
|
445
432
|
pendingUseIds.add(raw.id);
|
|
@@ -448,7 +435,7 @@ function collectProviderNativeToolPairing(messages, model, deferredToolNames, no
|
|
|
448
435
|
if (block.type !== "providerNative" || !isRecord(block.raw))
|
|
449
436
|
continue;
|
|
450
437
|
const raw = block.raw;
|
|
451
|
-
if (
|
|
438
|
+
if (!isReplayableAnthropicProviderNativeBlock(raw))
|
|
452
439
|
continue;
|
|
453
440
|
const toolUseId = raw.tool_use_id;
|
|
454
441
|
if (typeof toolUseId !== "string")
|
|
@@ -922,7 +909,7 @@ export const stream = (model, context, options) => {
|
|
|
922
909
|
const normalizedContext = resolveTranscript(context, supportsMidConvoSystemMessages(model));
|
|
923
910
|
const currentTools = getCurrentTools(normalizedContext.messages);
|
|
924
911
|
(async () => {
|
|
925
|
-
const providerThinkingLevel = model
|
|
912
|
+
const providerThinkingLevel = managedEffortForRequest(model, options);
|
|
926
913
|
const output = {
|
|
927
914
|
role: "assistant",
|
|
928
915
|
content: [],
|
|
@@ -972,7 +959,7 @@ export const stream = (model, context, options) => {
|
|
|
972
959
|
const cacheRetention = resolveCacheRetention(options?.cacheRetention ?? model.cacheRetention, options?.env, "short");
|
|
973
960
|
const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
|
|
974
961
|
openClient = () => {
|
|
975
|
-
const created = createClient(model, apiKey, options?.interleavedThinking ?? true, shouldUseFineGrainedToolStreamingBeta(model, normalizedContext), options?.refusalFallbacks !== undefined, optionsHeaders, options?.fetch, copilotDynamicHeaders, cacheSessionId, options?.env, federation);
|
|
962
|
+
const created = createClient(model, apiKey, options?.interleavedThinking ?? true, shouldUseFineGrainedToolStreamingBeta(model, normalizedContext), options?.refusalFallbacks !== undefined && configuredBetaHeader(model, options) !== null, optionsHeaders, options?.fetch, copilotDynamicHeaders, cacheSessionId, options?.env, federation);
|
|
976
963
|
client = created.client;
|
|
977
964
|
isOAuth = created.isOAuthToken;
|
|
978
965
|
advertisedClaudeCodeVersion = created.claudeCodeVersion;
|
|
@@ -1007,6 +994,15 @@ export const stream = (model, context, options) => {
|
|
|
1007
994
|
: getAnthropicCompat(model).unsignedThinkingReplay;
|
|
1008
995
|
let demoteReferenceReplay = fallbackKey !== undefined && toolReferenceReplayFallbacks.has(fallbackKey);
|
|
1009
996
|
let requestReplaysReferences = false;
|
|
997
|
+
const expiredSearchPrefix = fallbackKey === undefined ? undefined : `${fallbackKey}\u0000`;
|
|
998
|
+
const expiredSearchIds = new Set();
|
|
999
|
+
if (expiredSearchPrefix !== undefined) {
|
|
1000
|
+
for (const key of expiredWebSearchReplays) {
|
|
1001
|
+
if (key.startsWith(expiredSearchPrefix))
|
|
1002
|
+
expiredSearchIds.add(key.slice(expiredSearchPrefix.length));
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
let lastSentParams;
|
|
1010
1006
|
const createRequest = async () => {
|
|
1011
1007
|
let params = buildParams(model, normalizedContext, isOAuth, options, unsignedThinkingReplay);
|
|
1012
1008
|
const nextParams = await options?.onPayload?.(params, model);
|
|
@@ -1020,6 +1016,8 @@ export const stream = (model, context, options) => {
|
|
|
1020
1016
|
requestReplaysReferences = withoutReferenceReplay !== params;
|
|
1021
1017
|
if (demoteReferenceReplay)
|
|
1022
1018
|
params = withoutReferenceReplay;
|
|
1019
|
+
params = demoteExpiredWebSearchReplay(params, expiredSearchIds);
|
|
1020
|
+
lastSentParams = params;
|
|
1023
1021
|
params = sanitizeAnthropicToolPairs(params);
|
|
1024
1022
|
const payloadRequestMetadata = extractPayloadRequestMetadata(params);
|
|
1025
1023
|
params = payloadRequestMetadata.params;
|
|
@@ -1044,6 +1042,28 @@ export const stream = (model, context, options) => {
|
|
|
1044
1042
|
});
|
|
1045
1043
|
return { params: sent.params, response: sent.result };
|
|
1046
1044
|
};
|
|
1045
|
+
// True when the error names web search payloads this request had not demoted yet.
|
|
1046
|
+
const markNewlyExpiredSearches = (error) => {
|
|
1047
|
+
if (!isExpiredWebSearchContentError(error) || lastSentParams === undefined)
|
|
1048
|
+
return false;
|
|
1049
|
+
const newlyExpired = expiredWebSearchReplayIds(lastSentParams, error).filter((id) => !expiredSearchIds.has(id));
|
|
1050
|
+
for (const id of newlyExpired) {
|
|
1051
|
+
expiredSearchIds.add(id);
|
|
1052
|
+
if (expiredSearchPrefix !== undefined)
|
|
1053
|
+
expiredWebSearchReplays.add(expiredSearchPrefix + id);
|
|
1054
|
+
}
|
|
1055
|
+
return newlyExpired.length > 0;
|
|
1056
|
+
};
|
|
1057
|
+
const createRequestDemotingExpiredSearches = async () => {
|
|
1058
|
+
try {
|
|
1059
|
+
return await createRequest();
|
|
1060
|
+
}
|
|
1061
|
+
catch (error) {
|
|
1062
|
+
if (markNewlyExpiredSearches(error))
|
|
1063
|
+
return createRequestDemotingExpiredSearches();
|
|
1064
|
+
throw error;
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1047
1067
|
let requestOutcome;
|
|
1048
1068
|
try {
|
|
1049
1069
|
requestOutcome = await retryProviderRequest(async () => {
|
|
@@ -1063,6 +1083,8 @@ export const stream = (model, context, options) => {
|
|
|
1063
1083
|
toolReferenceReplayFallbacks.add(fallbackKey);
|
|
1064
1084
|
return createRequest();
|
|
1065
1085
|
}
|
|
1086
|
+
if (markNewlyExpiredSearches(error))
|
|
1087
|
+
return createRequestDemotingExpiredSearches();
|
|
1066
1088
|
if (await retryWithNewerClaudeCode(error)) {
|
|
1067
1089
|
try {
|
|
1068
1090
|
return await createRequest();
|
|
@@ -1459,6 +1481,25 @@ function cannotDisableThinking(model, compat) {
|
|
|
1459
1481
|
return true;
|
|
1460
1482
|
return matchesModelMarker(model, DISABLED_THINKING_REJECTING_MODEL_MARKERS);
|
|
1461
1483
|
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Effort a per-message-effort request runs at, which is also the active marker and the level recorded
|
|
1486
|
+
* on the reply: the caller's effort with thinking on, `low` for a thinking-off turn on a family that
|
|
1487
|
+
* cannot disable thinking, and none when thinking is actually disabled (senpi#2912).
|
|
1488
|
+
*/
|
|
1489
|
+
function managedEffortForRequest(model, options) {
|
|
1490
|
+
if (model.compat?.supportsMidConvoEffort !== true)
|
|
1491
|
+
return undefined;
|
|
1492
|
+
if (options?.thinkingEnabled === false) {
|
|
1493
|
+
return cannotDisableThinking(model, getAnthropicCompat(model)) ? "low" : undefined;
|
|
1494
|
+
}
|
|
1495
|
+
return options?.effort ?? "high";
|
|
1496
|
+
}
|
|
1497
|
+
function isEffortMarker(message) {
|
|
1498
|
+
return (message.role === "system" &&
|
|
1499
|
+
Array.isArray(message.content) &&
|
|
1500
|
+
message.content.length === 0 &&
|
|
1501
|
+
message.output_config?.effort !== undefined);
|
|
1502
|
+
}
|
|
1462
1503
|
function disableThinkingForRequest(params, model, compat) {
|
|
1463
1504
|
// A degraded/disabled turn must not retain the caller's higher effort.
|
|
1464
1505
|
delete params.output_config;
|
|
@@ -1470,6 +1511,8 @@ function disableThinkingForRequest(params, model, compat) {
|
|
|
1470
1511
|
return;
|
|
1471
1512
|
}
|
|
1472
1513
|
params.thinking = { type: "disabled" };
|
|
1514
|
+
// Per-message effort is rejected alongside disabled thinking (senpi#1399).
|
|
1515
|
+
params.messages = params.messages.filter((message) => !isEffortMarker(message));
|
|
1473
1516
|
}
|
|
1474
1517
|
function supportsAdaptiveThinking(model) {
|
|
1475
1518
|
if (model.compat?.forceAdaptiveThinking !== undefined) {
|
|
@@ -1665,27 +1708,46 @@ export function buildAnthropicWarmPromptCacheParams(model, context, options) {
|
|
|
1665
1708
|
max_tokens: 0,
|
|
1666
1709
|
});
|
|
1667
1710
|
}
|
|
1668
|
-
function
|
|
1669
|
-
let
|
|
1711
|
+
function configuredBetaHeader(model, options) {
|
|
1712
|
+
let configured;
|
|
1670
1713
|
for (const headers of [model.headers, options?.headers]) {
|
|
1671
1714
|
for (const [name, value] of Object.entries(headers ?? {})) {
|
|
1672
1715
|
if (name.toLowerCase() === "anthropic-beta")
|
|
1673
|
-
|
|
1716
|
+
configured = value;
|
|
1674
1717
|
}
|
|
1675
1718
|
}
|
|
1676
|
-
|
|
1719
|
+
return configured;
|
|
1720
|
+
}
|
|
1721
|
+
function getBetaFeatures(model, context, isOAuthToken, shape, options) {
|
|
1722
|
+
const configured = configuredBetaHeader(model, options);
|
|
1723
|
+
if (configured === null)
|
|
1677
1724
|
return [];
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
...new Set(configuredFeatures
|
|
1681
|
-
.split(",")
|
|
1682
|
-
.map((feature) => feature.trim())
|
|
1683
|
-
.filter(Boolean)),
|
|
1684
|
-
];
|
|
1685
|
-
}
|
|
1686
|
-
const features = [];
|
|
1725
|
+
// Betas the request body depends on: the API rejects the request without them.
|
|
1726
|
+
const required = [];
|
|
1687
1727
|
if (isOAuthToken)
|
|
1688
|
-
|
|
1728
|
+
required.push("claude-code-20250219", "oauth-2025-04-20");
|
|
1729
|
+
if (shape.midConvoEffort)
|
|
1730
|
+
required.push(MID_CONVERSATION_OUTPUT_CONFIG_BETA);
|
|
1731
|
+
if (shape.thinkingBinding)
|
|
1732
|
+
required.push(THINKING_BINDING_CONTROLS_BETA);
|
|
1733
|
+
if (shape.nativeToolChanges)
|
|
1734
|
+
required.push(MID_CONVERSATION_TOOL_CHANGES_BETA);
|
|
1735
|
+
if (shape.refusalFallbacks)
|
|
1736
|
+
required.push(SERVER_SIDE_FALLBACK_BETA);
|
|
1737
|
+
// A configured header owns the optional betas and is merged with the required ones, never replacing them: a
|
|
1738
|
+
// proxy route's own header used to drop the effort beta, and Claude 5.5 models then 400ed (senpi#2957).
|
|
1739
|
+
// Interleaved thinking is stripped on adaptive models here too, as it is from the client headers.
|
|
1740
|
+
const optional = configured === undefined
|
|
1741
|
+
? defaultOptionalBetas(model, context, options)
|
|
1742
|
+
: configured
|
|
1743
|
+
.split(",")
|
|
1744
|
+
.map((feature) => feature.trim())
|
|
1745
|
+
.filter(Boolean)
|
|
1746
|
+
.filter((feature) => !(feature === INTERLEAVED_THINKING_BETA && supportsAdaptiveThinking(model)));
|
|
1747
|
+
return [...new Set([...optional, ...required])];
|
|
1748
|
+
}
|
|
1749
|
+
function defaultOptionalBetas(model, context, options) {
|
|
1750
|
+
const features = [];
|
|
1689
1751
|
if (shouldUseFineGrainedToolStreamingBeta(model, context))
|
|
1690
1752
|
features.push(FINE_GRAINED_TOOL_STREAMING_BETA);
|
|
1691
1753
|
if (model.reasoning &&
|
|
@@ -1696,12 +1758,7 @@ function getBetaFeatures(model, context, isOAuthToken, nativeToolChanges, option
|
|
|
1696
1758
|
}
|
|
1697
1759
|
if (shouldUseServerSideFallbackBeta(model))
|
|
1698
1760
|
features.push(SERVER_SIDE_FALLBACK_BETA);
|
|
1699
|
-
|
|
1700
|
-
features.push(MID_CONVERSATION_OUTPUT_CONFIG_BETA, THINKING_BINDING_CONTROLS_BETA);
|
|
1701
|
-
}
|
|
1702
|
-
if (nativeToolChanges)
|
|
1703
|
-
features.push(MID_CONVERSATION_TOOL_CHANGES_BETA);
|
|
1704
|
-
return [...new Set(features)];
|
|
1761
|
+
return features;
|
|
1705
1762
|
}
|
|
1706
1763
|
function buildParams(model, context, isOAuthToken, options, unsignedThinkingReplay = getAnthropicCompat(model).unsignedThinkingReplay) {
|
|
1707
1764
|
const compat = getAnthropicCompat(model);
|
|
@@ -1718,7 +1775,16 @@ function buildParams(model, context, isOAuthToken, options, unsignedThinkingRepl
|
|
|
1718
1775
|
// and Anthropic rejects a tool list where every tool is deferred, so there must be an
|
|
1719
1776
|
// initial active tool to anchor the deferred ones. Otherwise the current tool list is sent.
|
|
1720
1777
|
const initialTools = initialSystemMessage?.toolsAdded ?? [];
|
|
1721
|
-
|
|
1778
|
+
// A configured `anthropic-beta: null` asks for no betas at all, so the request takes a shape that needs none:
|
|
1779
|
+
// the current tool list instead of native tool changes, top-level effort instead of per-message markers, and
|
|
1780
|
+
// no server-side fallbacks (senpi#2957). OAuth still needs its identity betas; getBetaFeatures names them.
|
|
1781
|
+
const betasSuppressed = configuredBetaHeader(model, options) === null;
|
|
1782
|
+
if (betasSuppressed && isOAuthToken) {
|
|
1783
|
+
throw new Error(`The anthropic-beta header is set to null, but ${model.provider}/${model.id} is called with an OAuth token, which needs the claude-code-20250219 and oauth-2025-04-20 betas. Remove the null, or list the betas you want instead; senpi adds the ones a request needs.`);
|
|
1784
|
+
}
|
|
1785
|
+
const midConvoEffort = model.compat?.supportsMidConvoEffort === true && !betasSuppressed;
|
|
1786
|
+
const nativeToolChanges = !betasSuppressed &&
|
|
1787
|
+
supportsMidConvoSystemMessages(model) &&
|
|
1722
1788
|
supportsMidConvoToolChanges(model) &&
|
|
1723
1789
|
initialTools.length > 0 &&
|
|
1724
1790
|
!hasToolRedefinitions(context.messages);
|
|
@@ -1741,15 +1807,21 @@ function buildParams(model, context, isOAuthToken, options, unsignedThinkingRepl
|
|
|
1741
1807
|
deferredTools = [];
|
|
1742
1808
|
}
|
|
1743
1809
|
const deferredToolNames = new Set(deferredTools.map((tool) => normalizeToolName(tool.name)));
|
|
1744
|
-
const activeEffort = options
|
|
1745
|
-
const
|
|
1746
|
-
const
|
|
1747
|
-
|
|
1810
|
+
const activeEffort = managedEffortForRequest(model, options) ?? "high";
|
|
1811
|
+
const sendFallbacks = options?.refusalFallbacks !== undefined && !betasSuppressed;
|
|
1812
|
+
const betaFeatures = getBetaFeatures(model, context, isOAuthToken, {
|
|
1813
|
+
midConvoEffort,
|
|
1814
|
+
thinkingBinding: midConvoEffort && options?.thinkingEnabled !== false,
|
|
1815
|
+
nativeToolChanges,
|
|
1816
|
+
refusalFallbacks: sendFallbacks,
|
|
1817
|
+
}, options);
|
|
1818
|
+
const convertedMessages = convertMessages(conversationMessages, model, isOAuthToken, cacheControl, unsignedThinkingReplay, deferredToolNames, normalizeToolName, midConvoEffort ? model.provider : undefined, nativeToolChanges);
|
|
1819
|
+
const messages = midConvoEffort
|
|
1748
1820
|
? insertThinkingLevelMessages(convertedMessages, activeEffort)
|
|
1749
1821
|
: convertedMessages.messages;
|
|
1750
1822
|
// Effort markers are appended after history; re-run the fork's final-user cache checkpoint
|
|
1751
1823
|
// against the actual last user/tool-result message so the marker cannot displace cache_control.
|
|
1752
|
-
if (
|
|
1824
|
+
if (midConvoEffort && cacheControl) {
|
|
1753
1825
|
for (let index = messages.length - 1; index >= 0; index--) {
|
|
1754
1826
|
const message = messages[index];
|
|
1755
1827
|
// A native system update after the last turn already holds the checkpoint;
|
|
@@ -1826,9 +1898,10 @@ function buildParams(model, context, isOAuthToken, options, unsignedThinkingRepl
|
|
|
1826
1898
|
];
|
|
1827
1899
|
}
|
|
1828
1900
|
// Managed effort models always use adaptive thinking so prefix mismatches can
|
|
1829
|
-
// be dropped instead of surfacing as persistent 400 responses.
|
|
1830
|
-
//
|
|
1831
|
-
|
|
1901
|
+
// be dropped instead of surfacing as persistent 400 responses. A thinking-off turn
|
|
1902
|
+
// skips this branch: `disableThinkingForRequest` sends `disabled` where the family
|
|
1903
|
+
// accepts it and pins effort `low` where it does not (Opus/Sonnet/Haiku 5.5, Fable, Mythos).
|
|
1904
|
+
if (midConvoEffort && options?.thinkingEnabled !== false) {
|
|
1832
1905
|
params.thinking = {
|
|
1833
1906
|
type: "adaptive",
|
|
1834
1907
|
display: options?.thinkingDisplay ?? "summarized",
|
|
@@ -1897,7 +1970,7 @@ function buildParams(model, context, isOAuthToken, options, unsignedThinkingRepl
|
|
|
1897
1970
|
}
|
|
1898
1971
|
}
|
|
1899
1972
|
applyExtraBodyToAnthropicParams(params, options?.extraBody);
|
|
1900
|
-
if (options?.refusalFallbacks !== undefined) {
|
|
1973
|
+
if (sendFallbacks && options?.refusalFallbacks !== undefined) {
|
|
1901
1974
|
// AnthropicRefusalFallback models a readonly list; the SDK's BetaFallbacksParam
|
|
1902
1975
|
// is mutable, so copy rather than widen the fork's public type.
|
|
1903
1976
|
params.fallbacks = options.refusalFallbacks === "default" ? "default" : [...options.refusalFallbacks];
|
|
@@ -2070,7 +2143,6 @@ function convertMessages(transformedMessages, model, isOAuthToken, cacheControl,
|
|
|
2070
2143
|
}
|
|
2071
2144
|
}
|
|
2072
2145
|
else if (msg.role === "assistant") {
|
|
2073
|
-
flushPendingSystemMessages();
|
|
2074
2146
|
const blocks = [];
|
|
2075
2147
|
const isSameModel = isSameAnthropicModel(msg, model);
|
|
2076
2148
|
// Blocks before the final fallback marker are the declined attempt; the
|
|
@@ -2156,8 +2228,12 @@ function convertMessages(transformedMessages, model, isOAuthToken, cacheControl,
|
|
|
2156
2228
|
}
|
|
2157
2229
|
}
|
|
2158
2230
|
}
|
|
2231
|
+
// Pending system messages are emitted only in front of an assistant param that is actually sent: a reply
|
|
2232
|
+
// that converts to no blocks is skipped, and flushing for it would leave a `system` param directly before
|
|
2233
|
+
// the next `user` turn, which Anthropic rejects on every later request (senpi#2864).
|
|
2159
2234
|
if (blocks.length === 0)
|
|
2160
2235
|
continue;
|
|
2236
|
+
flushPendingSystemMessages();
|
|
2161
2237
|
const messageIndex = params.length;
|
|
2162
2238
|
params.push({
|
|
2163
2239
|
role: "assistant",
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { BetaContentBlockParam } from "@anthropic-ai/sdk/resources/beta/messages/messages.js";
|
|
2
|
+
export declare function isReplayableAnthropicProviderNativeBlock(raw: unknown): raw is BetaContentBlockParam;
|
|
3
|
+
export declare function isAnthropicWebSearchReplayBlock(raw: unknown): boolean;
|
|
4
|
+
//# sourceMappingURL=anthropic-native-replay-blocks.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const replayableTypes = new Set([
|
|
2
|
+
"server_tool_use",
|
|
3
|
+
"web_search_tool_result",
|
|
4
|
+
// GLM's Anthropic-compatible coding endpoint returns this server-result shape.
|
|
5
|
+
// Pairing still requires a preceding same-model server_tool_use.
|
|
6
|
+
"tool_result",
|
|
7
|
+
"web_fetch_tool_result",
|
|
8
|
+
"code_execution_tool_result",
|
|
9
|
+
"bash_code_execution_tool_result",
|
|
10
|
+
"text_editor_code_execution_tool_result",
|
|
11
|
+
"tool_search_tool_result",
|
|
12
|
+
"container_upload",
|
|
13
|
+
// Stored fallback markers are audit metadata, not valid Messages input tags.
|
|
14
|
+
]);
|
|
15
|
+
function isRecord(value) {
|
|
16
|
+
return value !== null && typeof value === "object";
|
|
17
|
+
}
|
|
18
|
+
export function isReplayableAnthropicProviderNativeBlock(raw) {
|
|
19
|
+
return isRecord(raw) && typeof raw.type === "string" && replayableTypes.has(raw.type);
|
|
20
|
+
}
|
|
21
|
+
export function isAnthropicWebSearchReplayBlock(raw) {
|
|
22
|
+
if (!isRecord(raw))
|
|
23
|
+
return false;
|
|
24
|
+
if (raw.type === "web_search_tool_result" || raw.type === "tool_result")
|
|
25
|
+
return true;
|
|
26
|
+
return raw.type === "server_tool_use" && raw.name === "web_search";
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=anthropic-native-replay-blocks.js.map
|
|
@@ -73,6 +73,27 @@ export function demoteUnavailableToolReferences(params) {
|
|
|
73
73
|
}
|
|
74
74
|
let changed = false;
|
|
75
75
|
const availableToolNames = [...available];
|
|
76
|
+
// A native search pair whose every reference stopped resolving is demoted as a
|
|
77
|
+
// unit: the result decides, and its `server_tool_use` follows. The pair can span
|
|
78
|
+
// two assistant messages (a deferred server tool resumes in the continuation), so
|
|
79
|
+
// the decision is collected over the whole request before any message is rewritten.
|
|
80
|
+
const droppedSearchUseIds = new Set();
|
|
81
|
+
const droppedSearchNames = new Map();
|
|
82
|
+
for (const message of messages) {
|
|
83
|
+
if (message.role !== "assistant" || !Array.isArray(message.content))
|
|
84
|
+
continue;
|
|
85
|
+
for (const block of message.content) {
|
|
86
|
+
if (!isNativeToolSearchResultBlock(block))
|
|
87
|
+
continue;
|
|
88
|
+
const names = block.content.tool_references
|
|
89
|
+
.filter((item) => isRecord(item) && item.type === "tool_reference")
|
|
90
|
+
.map((item) => (typeof item.tool_name === "string" ? item.tool_name : ""));
|
|
91
|
+
if (names.length > 0 && names.every((name) => resolve(name) === undefined)) {
|
|
92
|
+
droppedSearchUseIds.add(block.tool_use_id);
|
|
93
|
+
droppedSearchNames.set(block.tool_use_id, names);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
76
97
|
const seenDemotedCallNames = new Set();
|
|
77
98
|
const rewrittenMessages = [];
|
|
78
99
|
for (const message of messages) {
|
|
@@ -81,23 +102,6 @@ export function demoteUnavailableToolReferences(params) {
|
|
|
81
102
|
continue;
|
|
82
103
|
}
|
|
83
104
|
let messageChanged = false;
|
|
84
|
-
// A native search pair whose every reference stopped resolving is demoted
|
|
85
|
-
// as a unit: the result decides, and its `server_tool_use` follows.
|
|
86
|
-
const droppedSearchUseIds = new Set();
|
|
87
|
-
const droppedSearchNames = new Map();
|
|
88
|
-
if (message.role === "assistant") {
|
|
89
|
-
for (const block of message.content) {
|
|
90
|
-
if (!isNativeToolSearchResultBlock(block))
|
|
91
|
-
continue;
|
|
92
|
-
const names = block.content.tool_references
|
|
93
|
-
.filter((item) => isRecord(item) && item.type === "tool_reference")
|
|
94
|
-
.map((item) => (typeof item.tool_name === "string" ? item.tool_name : ""));
|
|
95
|
-
if (names.length > 0 && names.every((name) => resolve(name) === undefined)) {
|
|
96
|
-
droppedSearchUseIds.add(block.tool_use_id);
|
|
97
|
-
droppedSearchNames.set(block.tool_use_id, names);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
105
|
const content = [];
|
|
102
106
|
for (const block of message.content) {
|
|
103
107
|
if (message.role === "assistant" && isRecord(block) && block.type === "tool_use") {
|
|
@@ -168,7 +172,9 @@ export function demoteUnavailableToolReferences(params) {
|
|
|
168
172
|
}
|
|
169
173
|
content.push(block);
|
|
170
174
|
}
|
|
171
|
-
|
|
175
|
+
// Only a message this pass emptied is dropped; a message that arrived empty (a per-message
|
|
176
|
+
// effort marker carries `content: []` by design) passes through untouched (senpi#2912).
|
|
177
|
+
if (messageChanged && content.length === 0) {
|
|
172
178
|
changed = true;
|
|
173
179
|
continue;
|
|
174
180
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { MessageCreateParamsStreaming } from "@anthropic-ai/sdk/resources/beta/messages/messages.js";
|
|
2
|
+
export declare function isExpiredWebSearchContentError(error: unknown): error is Error;
|
|
3
|
+
/**
|
|
4
|
+
* The `tool_use_id`s of the web search results the rejected request replayed
|
|
5
|
+
* at or before the message the API named. Without a parsable index every
|
|
6
|
+
* replayed result counts as expired.
|
|
7
|
+
*/
|
|
8
|
+
export declare function expiredWebSearchReplayIds(params: MessageCreateParamsStreaming, error: Error): string[];
|
|
9
|
+
export declare function demoteExpiredWebSearchReplay(params: MessageCreateParamsStreaming, expired: ReadonlySet<string>): MessageCreateParamsStreaming;
|
|
10
|
+
//# sourceMappingURL=anthropic-web-search-expiry.d.ts.map
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The opaque `encrypted_content` of a native web search result is only valid
|
|
3
|
+
* for the account that ran the search (senpi #3201). Same-model replay keeps
|
|
4
|
+
* the payload byte-for-byte (#208), so once a request is served by another
|
|
5
|
+
* account (credential rotation, a multi-account gateway) every later request
|
|
6
|
+
* on that model fails with "messages.N.content.0: Invalid `encrypted_content` in
|
|
7
|
+
* `search_result` block" and the session only moves on through a model
|
|
8
|
+
* fallback. Stripping the field is not an option: the API requires it (#150).
|
|
9
|
+
*
|
|
10
|
+
* Recovery demotes expired search pairs to plain text: the `server_tool_use`
|
|
11
|
+
* becomes a note naming its query and the `web_search_tool_result` a list of
|
|
12
|
+
* the result titles and URLs. Only pairs whose result sits at or before the
|
|
13
|
+
* rejected message are demoted, because a search is never fresher than a
|
|
14
|
+
* later one; newer results keep replaying verbatim.
|
|
15
|
+
*/
|
|
16
|
+
const EXPIRED_SEARCH_CONTENT = /Invalid `encrypted_content` in `search_result` block/i;
|
|
17
|
+
const REJECTED_MESSAGE_INDEX = /messages\.(\d+)\.content\.\d+: Invalid `encrypted_content`/i;
|
|
18
|
+
function isRecord(value) {
|
|
19
|
+
return typeof value === "object" && value !== null;
|
|
20
|
+
}
|
|
21
|
+
export function isExpiredWebSearchContentError(error) {
|
|
22
|
+
return (error instanceof Error &&
|
|
23
|
+
"status" in error &&
|
|
24
|
+
error.status === 400 &&
|
|
25
|
+
EXPIRED_SEARCH_CONTENT.test(error.message));
|
|
26
|
+
}
|
|
27
|
+
function webSearchResultIds(message) {
|
|
28
|
+
if (message.role !== "assistant" || !Array.isArray(message.content))
|
|
29
|
+
return [];
|
|
30
|
+
const ids = [];
|
|
31
|
+
for (const block of message.content) {
|
|
32
|
+
if (isRecord(block) && block.type === "web_search_tool_result" && typeof block.tool_use_id === "string") {
|
|
33
|
+
ids.push(block.tool_use_id);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return ids;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The `tool_use_id`s of the web search results the rejected request replayed
|
|
40
|
+
* at or before the message the API named. Without a parsable index every
|
|
41
|
+
* replayed result counts as expired.
|
|
42
|
+
*/
|
|
43
|
+
export function expiredWebSearchReplayIds(params, error) {
|
|
44
|
+
const match = REJECTED_MESSAGE_INDEX.exec(error.message);
|
|
45
|
+
const lastIndex = match?.[1] === undefined ? params.messages.length - 1 : Number(match[1]);
|
|
46
|
+
return params.messages.slice(0, lastIndex + 1).flatMap(webSearchResultIds);
|
|
47
|
+
}
|
|
48
|
+
function searchQuery(block) {
|
|
49
|
+
const input = block.input;
|
|
50
|
+
return isRecord(input) && typeof input.query === "string" ? input.query : undefined;
|
|
51
|
+
}
|
|
52
|
+
function demotedResultText(block) {
|
|
53
|
+
const results = Array.isArray(block.content) ? block.content : [];
|
|
54
|
+
const lines = [];
|
|
55
|
+
for (const result of results) {
|
|
56
|
+
if (!isRecord(result) || result.type !== "web_search_result")
|
|
57
|
+
continue;
|
|
58
|
+
const title = typeof result.title === "string" ? result.title : "";
|
|
59
|
+
const url = typeof result.url === "string" ? result.url : "";
|
|
60
|
+
lines.push(`- ${title} (${url})`);
|
|
61
|
+
}
|
|
62
|
+
if (lines.length === 0)
|
|
63
|
+
return "Web search returned no results.";
|
|
64
|
+
return ["Web search results (page contents no longer available):", ...lines].join("\n");
|
|
65
|
+
}
|
|
66
|
+
function demoteBlock(block, expired) {
|
|
67
|
+
if (!isRecord(block))
|
|
68
|
+
return undefined;
|
|
69
|
+
if (block.type === "server_tool_use" && block.name === "web_search" && typeof block.id === "string") {
|
|
70
|
+
if (!expired.has(block.id))
|
|
71
|
+
return undefined;
|
|
72
|
+
const query = searchQuery(block);
|
|
73
|
+
return { type: "text", text: query === undefined ? "Searched the web." : `Searched the web for: ${query}` };
|
|
74
|
+
}
|
|
75
|
+
if (block.type === "web_search_tool_result" && typeof block.tool_use_id === "string") {
|
|
76
|
+
if (!expired.has(block.tool_use_id))
|
|
77
|
+
return undefined;
|
|
78
|
+
return { type: "text", text: demotedResultText(block) };
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
export function demoteExpiredWebSearchReplay(params, expired) {
|
|
83
|
+
if (expired.size === 0)
|
|
84
|
+
return params;
|
|
85
|
+
let changed = false;
|
|
86
|
+
const messages = params.messages.map((message) => {
|
|
87
|
+
if (message.role !== "assistant" || !Array.isArray(message.content))
|
|
88
|
+
return message;
|
|
89
|
+
let messageChanged = false;
|
|
90
|
+
const content = message.content.map((block) => {
|
|
91
|
+
const demoted = demoteBlock(block, expired);
|
|
92
|
+
if (demoted === undefined)
|
|
93
|
+
return block;
|
|
94
|
+
messageChanged = true;
|
|
95
|
+
return demoted;
|
|
96
|
+
});
|
|
97
|
+
if (!messageChanged)
|
|
98
|
+
return message;
|
|
99
|
+
changed = true;
|
|
100
|
+
return { ...message, content };
|
|
101
|
+
});
|
|
102
|
+
return changed ? { ...params, messages } : params;
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=anthropic-web-search-expiry.js.map
|