@code-yeongyu/senpi-ai 2026.7.25-2 → 2026.7.28
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.d.ts +1 -0
- package/dist/api/anthropic-messages.d.ts.map +1 -1
- package/dist/api/anthropic-messages.js +296 -66
- package/dist/api/anthropic-messages.js.map +1 -1
- package/dist/api/azure-openai-responses.js +4 -2
- package/dist/api/azure-openai-responses.js.map +1 -1
- package/dist/api/bedrock-converse-stream.d.ts +1 -0
- package/dist/api/bedrock-converse-stream.d.ts.map +1 -1
- package/dist/api/bedrock-converse-stream.js +3 -47
- package/dist/api/bedrock-converse-stream.js.map +1 -1
- package/dist/api/google-shared.d.ts.map +1 -1
- package/dist/api/google-shared.js +4 -3
- package/dist/api/google-shared.js.map +1 -1
- package/dist/api/openai-codex-responses/reasoning.d.ts +14 -0
- package/dist/api/openai-codex-responses/reasoning.d.ts.map +1 -0
- package/dist/api/openai-codex-responses/reasoning.js +27 -0
- package/dist/api/openai-codex-responses/reasoning.js.map +1 -0
- package/dist/api/openai-codex-responses.d.ts +2 -1
- package/dist/api/openai-codex-responses.d.ts.map +1 -1
- package/dist/api/openai-codex-responses.js +4 -12
- package/dist/api/openai-codex-responses.js.map +1 -1
- package/dist/api/openai-completions.d.ts +3 -8
- package/dist/api/openai-completions.d.ts.map +1 -1
- package/dist/api/openai-completions.js +8 -122
- package/dist/api/openai-completions.js.map +1 -1
- package/dist/api/openai-responses-shared.d.ts +1 -0
- package/dist/api/openai-responses-shared.d.ts.map +1 -1
- package/dist/api/openai-responses-shared.js +67 -31
- package/dist/api/openai-responses-shared.js.map +1 -1
- package/dist/api/openai-responses.d.ts.map +1 -1
- package/dist/api/openai-responses.js +2 -1
- package/dist/api/openai-responses.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/model.d.ts +7 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js.map +1 -1
- package/dist/oauth.d.ts +2 -1
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +2 -1
- package/dist/oauth.js.map +1 -1
- package/dist/openai-responses-compat.d.ts +24 -0
- package/dist/openai-responses-compat.d.ts.map +1 -0
- package/dist/openai-responses-compat.js +2 -0
- package/dist/openai-responses-compat.js.map +1 -0
- package/dist/providers/data/.manifest.json +1 -1
- package/dist/providers/data/cloudflare-ai-gateway.json +1 -1
- package/dist/providers/data/fireworks.json +1 -1
- package/dist/providers/data/huggingface.json +1 -1
- package/dist/providers/data/nvidia.json +1 -1
- package/dist/providers/data/openai.json +1 -1
- package/dist/providers/data/opencode.json +1 -1
- package/dist/providers/data/openrouter.json +1 -1
- package/dist/providers/data/together.json +1 -1
- package/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/dist/types.d.ts +8 -22
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/prompt-cache-ttl.d.ts +32 -0
- package/dist/utils/prompt-cache-ttl.d.ts.map +1 -0
- package/dist/utils/prompt-cache-ttl.js +294 -0
- package/dist/utils/prompt-cache-ttl.js.map +1 -0
- package/dist/utils/provider-retry.d.ts +5 -0
- package/dist/utils/provider-retry.d.ts.map +1 -1
- package/dist/utils/provider-retry.js +46 -1
- package/dist/utils/provider-retry.js.map +1 -1
- package/dist/utils/retry.d.ts +7 -0
- package/dist/utils/retry.d.ts.map +1 -1
- package/dist/utils/retry.js +24 -1
- package/dist/utils/retry.js.map +1 -1
- package/dist/utils/stop-details.d.ts +1 -1
- package/dist/utils/stop-details.d.ts.map +1 -1
- package/dist/utils/stop-details.js +7 -3
- package/dist/utils/stop-details.js.map +1 -1
- package/dist/utils/tool-call-id.d.ts +9 -0
- package/dist/utils/tool-call-id.d.ts.map +1 -0
- package/dist/utils/tool-call-id.js +16 -0
- package/dist/utils/tool-call-id.js.map +1 -0
- package/package.json +2 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Anthropic from "@anthropic-ai/sdk";
|
|
2
2
|
import type { SimpleStreamOptions, StreamFunction, StreamOptions } from "../types.ts";
|
|
3
|
+
export { getAnthropicCompat } from "../utils/prompt-cache-ttl.ts";
|
|
3
4
|
export type AnthropicEffort = "low" | "medium" | "high" | "xhigh" | "max";
|
|
4
5
|
export type AnthropicThinkingDisplay = "summarized" | "omitted";
|
|
5
6
|
export interface AnthropicOptions extends StreamOptions {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"anthropic-messages.d.ts","sourceRoot":"","sources":["../../src/api/anthropic-messages.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAU1C,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"anthropic-messages.d.ts","sourceRoot":"","sources":["../../src/api/anthropic-messages.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAU1C,OAAO,KAAK,EAWX,mBAAmB,EAEnB,cAAc,EACd,aAAa,EAOb,MAAM,aAAa,CAAC;AA+BrB,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AA6JlE,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;AAE1E,MAAM,MAAM,wBAAwB,GAAG,YAAY,GAAG,SAAS,CAAC;AAoEhE,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACtD;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,wBAAwB,CAAC;IAC3C;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;CACnB;AA8zBD,eAAO,MAAM,MAAM,EAAE,cAAc,CAAC,oBAAoB,EAAE,gBAAgB,CAqYzE,CAAC;AA0FF,eAAO,MAAM,YAAY,EAAE,cAAc,CAAC,oBAAoB,EAAE,mBAAmB,CA2ClF,CAAC"}
|
|
@@ -6,16 +6,19 @@ import { splitDeferredTools } from "../utils/deferred-tools.js";
|
|
|
6
6
|
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
|
7
7
|
import { headersToRecord, providerHeadersToRecord } from "../utils/headers.js";
|
|
8
8
|
import { parseJsonWithRepair, parseStreamingJson } from "../utils/json-parse.js";
|
|
9
|
+
import { getAnthropicCompat, isAnthropicApiBaseUrl } from "../utils/prompt-cache-ttl.js";
|
|
9
10
|
import { getProviderEnvValue } from "../utils/provider-env.js";
|
|
10
11
|
import { retryProviderRequest } from "../utils/provider-retry.js";
|
|
11
12
|
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
|
12
13
|
import { applyServerFallbackAbort, parseServerFallbackReceipt, parseStickyFallbackReceipt, } from "../utils/server-fallback-receipt.js";
|
|
14
|
+
import { normalizeToolCallId } from "../utils/tool-call-id.js";
|
|
13
15
|
import { isForcedToolChoiceUnsupportedError, omitToolChoiceParam } from "../utils/tool-choice-fallback.js";
|
|
14
16
|
import { resolveCloudflareBaseUrl } from "./cloudflare.js";
|
|
15
17
|
import { resolveJsonSchemaStrictSampling } from "./constrained-sampling.js";
|
|
16
18
|
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
|
|
17
19
|
import { ANTHROPIC_RESERVED_BODY_KEYS, adjustMaxTokensForThinking, buildBaseOptions, clampMaxTokensToContext, } from "./simple-options.js";
|
|
18
20
|
import { transformMessages } from "./transform-messages.js";
|
|
21
|
+
export { getAnthropicCompat } from "../utils/prompt-cache-ttl.js";
|
|
19
22
|
/**
|
|
20
23
|
* Resolve cache retention preference.
|
|
21
24
|
* Defaults to the provided fallback and uses PI_CACHE_RETENTION for backward compatibility.
|
|
@@ -32,14 +35,6 @@ function resolveCacheRetention(cacheRetention, env, fallback = "short") {
|
|
|
32
35
|
}
|
|
33
36
|
return fallback;
|
|
34
37
|
}
|
|
35
|
-
function isAnthropicApiBaseUrl(baseUrl) {
|
|
36
|
-
try {
|
|
37
|
-
return new URL(baseUrl).hostname === "api.anthropic.com";
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
38
|
function getCacheControl(model, cacheRetention, env) {
|
|
44
39
|
const retention = resolveCacheRetention(cacheRetention, env, "long");
|
|
45
40
|
if (retention === "none") {
|
|
@@ -167,7 +162,6 @@ const NATIVE_XHIGH_EFFORT_MODEL_MARKERS = [
|
|
|
167
162
|
* gateway rows carry no generated compat, so the family fact has to live here as well.
|
|
168
163
|
*/
|
|
169
164
|
const DISABLED_THINKING_REJECTING_MODEL_MARKERS = ["fable-5", "mythos-5"];
|
|
170
|
-
const CLAUDE_FABLE_OR_MYTHOS_MODEL_ID = /^claude-(?:fable|mythos)(?:-|$)/i;
|
|
171
165
|
const UNSUPPORTED_NATIVE_COMPUTER_TOOL_MODEL_MARKERS = [
|
|
172
166
|
"opus-4-6",
|
|
173
167
|
"opus-4.6",
|
|
@@ -190,47 +184,6 @@ function isInvalidUnsignedThinkingSignatureError(error) {
|
|
|
190
184
|
error instanceof Error &&
|
|
191
185
|
/Invalid signature in thinking block/i.test(error.message));
|
|
192
186
|
}
|
|
193
|
-
function getAnthropicCompat(model) {
|
|
194
|
-
// Auto-detect session affinity and cache control support from provider
|
|
195
|
-
const isFireworks = model.provider === "fireworks";
|
|
196
|
-
const isCloudflareAiGatewayAnthropic = model.provider === "cloudflare-ai-gateway" && model.baseUrl.includes("anthropic");
|
|
197
|
-
const isXiaomi = model.provider === "xiaomi" || model.provider.startsWith("xiaomi-token-plan-");
|
|
198
|
-
return {
|
|
199
|
-
supportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? !isFireworks,
|
|
200
|
-
supportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? !isFireworks,
|
|
201
|
-
sendSessionAffinityHeaders: model.compat?.sendSessionAffinityHeaders ?? !!(isFireworks || isCloudflareAiGatewayAnthropic),
|
|
202
|
-
supportsCacheControlOnTools: model.compat?.supportsCacheControlOnTools ?? !isFireworks,
|
|
203
|
-
supportsDisabledThinking: model.compat?.supportsDisabledThinking ?? !isXiaomi,
|
|
204
|
-
supportsTemperature: model.compat?.supportsTemperature ?? true,
|
|
205
|
-
supportsToolChoice: model.compat?.supportsToolChoice ?? true,
|
|
206
|
-
supportsForcedToolChoice: model.compat?.supportsForcedToolChoice ?? !CLAUDE_FABLE_OR_MYTHOS_MODEL_ID.test(model.id),
|
|
207
|
-
allowEmptySignature: model.compat?.allowEmptySignature ?? false,
|
|
208
|
-
unsignedThinkingReplay: model.compat?.unsignedThinkingReplay ?? (model.compat?.allowEmptySignature ? "empty-signature" : "text"),
|
|
209
|
-
supportsStrictTools: model.compat?.supportsStrictTools ?? false,
|
|
210
|
-
supportsToolReferences: model.compat?.supportsToolReferences ?? defaultSupportsToolReferences(model),
|
|
211
|
-
// Default: first-party Anthropic only. Anthropic-compatible providers
|
|
212
|
-
// (kimi-coding, fireworks, copilot, gateways) may execute the server-side
|
|
213
|
-
// search but reject the replayed server_tool_use / web_search_tool_result
|
|
214
|
-
// blocks on the next request (kimi-coding 400s with `tool_call_id is not
|
|
215
|
-
// found`).
|
|
216
|
-
supportsWebSearch: model.compat?.supportsWebSearch ?? isAnthropicApiBaseUrl(model.baseUrl),
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Default for `supportsToolReferences`: first-party Anthropic models except
|
|
221
|
-
* Haiku (rejects client-side tool_reference blocks) and models that predate
|
|
222
|
-
* tool search (Claude 3.x, Opus/Sonnet 4.0, Opus 4.1).
|
|
223
|
-
*/
|
|
224
|
-
function defaultSupportsToolReferences(model) {
|
|
225
|
-
if (model.provider !== "anthropic" || model.id.includes("haiku"))
|
|
226
|
-
return false;
|
|
227
|
-
const version = model.id.match(/^claude-(?:opus|sonnet|fable)-(\d+)(?:-(\d+))?(?:-|$)/);
|
|
228
|
-
if (!version)
|
|
229
|
-
return false;
|
|
230
|
-
const major = Number(version[1]);
|
|
231
|
-
const minor = version[2] && version[2].length < 8 ? Number(version[2]) : 0;
|
|
232
|
-
return major > 4 || (major === 4 && minor >= 5);
|
|
233
|
-
}
|
|
234
187
|
function mergeHeaders(...headerSources) {
|
|
235
188
|
const merged = {};
|
|
236
189
|
for (const headers of headerSources) {
|
|
@@ -309,6 +262,97 @@ function isAnthropicWebSearchReplayBlock(raw) {
|
|
|
309
262
|
return true;
|
|
310
263
|
return raw.type === "server_tool_use" && raw.name === "web_search";
|
|
311
264
|
}
|
|
265
|
+
// The user turn a pending server tool can survive is one that carries only
|
|
266
|
+
// tool results. Anything else the wire would emit — real text, an image —
|
|
267
|
+
// closes the assistant turn (the API then 400s the still-open use). A blank
|
|
268
|
+
// message serializes to nothing, so it closes nothing.
|
|
269
|
+
function userMessageClosesServerTurn(message) {
|
|
270
|
+
if (typeof message.content === "string")
|
|
271
|
+
return message.content.trim().length > 0;
|
|
272
|
+
return message.content.some((block) => (block.type === "text" && block.text.trim().length > 0) || block.type === "image");
|
|
273
|
+
}
|
|
274
|
+
function collectProviderNativeToolPairing(messages, model, deferredToolNames, normalizeToolName, discardedFallbackToolCallIds) {
|
|
275
|
+
const resolvedUseIds = new Set();
|
|
276
|
+
const validResultIds = new Set();
|
|
277
|
+
// Uses whose turn can still be resumed. A same-model assistant resets the
|
|
278
|
+
// set: its own result blocks may answer the prior pending uses, and any use
|
|
279
|
+
// it leaves pending becomes the open set. Anything that closes the turn —
|
|
280
|
+
// user text, a tool result whose deferred-tool references serialize sibling
|
|
281
|
+
// text after the results, or another model's assistant — empties it.
|
|
282
|
+
let pendingUseIds = new Set();
|
|
283
|
+
const loadedReferenceNames = new Set();
|
|
284
|
+
for (const message of messages) {
|
|
285
|
+
if (message.role === "assistant") {
|
|
286
|
+
const priorUseIds = pendingUseIds;
|
|
287
|
+
pendingUseIds = new Set();
|
|
288
|
+
if (!isSameAnthropicModel(message, model))
|
|
289
|
+
continue;
|
|
290
|
+
for (const block of message.content) {
|
|
291
|
+
if (block.type !== "providerNative" || !isRecord(block.raw))
|
|
292
|
+
continue;
|
|
293
|
+
const raw = block.raw;
|
|
294
|
+
if (typeof raw.type !== "string" || !REPLAYABLE_ANTHROPIC_PROVIDER_NATIVE_TYPES.has(raw.type))
|
|
295
|
+
continue;
|
|
296
|
+
if (isProviderNativeToolUseBlock(raw) && typeof raw.id === "string")
|
|
297
|
+
pendingUseIds.add(raw.id);
|
|
298
|
+
}
|
|
299
|
+
for (const block of message.content) {
|
|
300
|
+
if (block.type !== "providerNative" || !isRecord(block.raw))
|
|
301
|
+
continue;
|
|
302
|
+
const raw = block.raw;
|
|
303
|
+
if (typeof raw.type !== "string" || !REPLAYABLE_ANTHROPIC_PROVIDER_NATIVE_TYPES.has(raw.type))
|
|
304
|
+
continue;
|
|
305
|
+
const toolUseId = raw.tool_use_id;
|
|
306
|
+
if (typeof toolUseId !== "string")
|
|
307
|
+
continue;
|
|
308
|
+
if (priorUseIds.has(toolUseId) || pendingUseIds.has(toolUseId)) {
|
|
309
|
+
resolvedUseIds.add(toolUseId);
|
|
310
|
+
validResultIds.add(toolUseId);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (message.role === "toolResult") {
|
|
316
|
+
// Conversion drops a discarded pre-fallback result without touching
|
|
317
|
+
// loadedToolNames, so it must not load the name here either.
|
|
318
|
+
if (discardedFallbackToolCallIds.has(message.toolCallId))
|
|
319
|
+
continue;
|
|
320
|
+
// convertToolResult emits sibling text after the tool_result blocks only
|
|
321
|
+
// for names that survive the deferred/loaded filter, so only those names
|
|
322
|
+
// close the turn; a stale or already-loaded name leaves the result plain
|
|
323
|
+
// and the pending use resumable.
|
|
324
|
+
let emitsReferences = false;
|
|
325
|
+
for (const name of message.addedToolNames ?? []) {
|
|
326
|
+
const normalizedName = normalizeToolName(name);
|
|
327
|
+
if (!deferredToolNames.has(normalizedName) || loadedReferenceNames.has(normalizedName))
|
|
328
|
+
continue;
|
|
329
|
+
loadedReferenceNames.add(normalizedName);
|
|
330
|
+
emitsReferences = true;
|
|
331
|
+
}
|
|
332
|
+
if (emitsReferences)
|
|
333
|
+
pendingUseIds = new Set();
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (message.role === "user" && userMessageClosesServerTurn(message)) {
|
|
337
|
+
pendingUseIds = new Set();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// Whatever is still pending at the end of history belongs to the last
|
|
341
|
+
// assistant message and can still resume, so it is live rather than unpaired.
|
|
342
|
+
return { resolvedUseIds, liveUseIds: pendingUseIds, validResultIds };
|
|
343
|
+
}
|
|
344
|
+
// True for a server-tool block whose counterpart can never arrive: a use that
|
|
345
|
+
// is neither answered nor resumable, or a result whose use is nowhere.
|
|
346
|
+
// Blocks that pair nothing (`fallback`, `container_upload`) are never unpaired.
|
|
347
|
+
function isUnpairedProviderNativeToolBlock(raw, pairing) {
|
|
348
|
+
if (!isRecord(raw))
|
|
349
|
+
return false;
|
|
350
|
+
if (isProviderNativeToolUseBlock(raw)) {
|
|
351
|
+
return typeof raw.id !== "string" || !(pairing.resolvedUseIds.has(raw.id) || pairing.liveUseIds.has(raw.id));
|
|
352
|
+
}
|
|
353
|
+
const toolUseId = raw.tool_use_id;
|
|
354
|
+
return typeof toolUseId === "string" && !pairing.validResultIds.has(toolUseId);
|
|
355
|
+
}
|
|
312
356
|
// tool_use ids referenced by server-tool result blocks in content[0, boundary).
|
|
313
357
|
// A pre-boundary `server_tool_use` whose id is absent here is unpaired — the
|
|
314
358
|
// fallback interrupted the declined attempt before its result arrived — so
|
|
@@ -477,6 +521,164 @@ function sanitizeUnsupportedNativeTools(model, params) {
|
|
|
477
521
|
}
|
|
478
522
|
return changed ? sanitized : params;
|
|
479
523
|
}
|
|
524
|
+
/**
|
|
525
|
+
* Anthropic validates that every tool referenced by the message history is
|
|
526
|
+
* available in the same request — defined in `tools` or discovered through a
|
|
527
|
+
* `tool_reference` block — and rejects the whole request otherwise
|
|
528
|
+
* ("Tool reference '<name>' not found in available tools"). Sessions outlive
|
|
529
|
+
* their tools: an MCP server can be absent after a resume, an extension can
|
|
530
|
+
* stop registering a tool, or a payload hook can strip a definition while the
|
|
531
|
+
* history still carries the call. Demote those references to plain text so
|
|
532
|
+
* the turn can proceed; the matching tool_result is demoted in lockstep so no
|
|
533
|
+
* orphan pairing error replaces the original one.
|
|
534
|
+
*/
|
|
535
|
+
function demoteUnavailableToolReferences(params) {
|
|
536
|
+
const messages = params.messages;
|
|
537
|
+
if (!Array.isArray(messages) || messages.length === 0)
|
|
538
|
+
return params;
|
|
539
|
+
const definedNames = new Set();
|
|
540
|
+
if (Array.isArray(params.tools)) {
|
|
541
|
+
for (const tool of params.tools) {
|
|
542
|
+
if (isRecord(tool) && typeof tool.name === "string")
|
|
543
|
+
definedNames.add(tool.name);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
// `tool_reference` blocks — emitted for deferred tools or replayed from a
|
|
547
|
+
// server-side tool search — make their targets available without a
|
|
548
|
+
// non-deferred definition.
|
|
549
|
+
const discoveredNames = new Set();
|
|
550
|
+
collectToolReferenceNames(messages, discoveredNames);
|
|
551
|
+
const demotedCallNames = new Map();
|
|
552
|
+
for (const message of messages) {
|
|
553
|
+
if (message.role !== "assistant" || !Array.isArray(message.content))
|
|
554
|
+
continue;
|
|
555
|
+
for (const block of message.content) {
|
|
556
|
+
if (isRecord(block) &&
|
|
557
|
+
block.type === "tool_use" &&
|
|
558
|
+
typeof block.name === "string" &&
|
|
559
|
+
!definedNames.has(block.name) &&
|
|
560
|
+
!discoveredNames.has(block.name)) {
|
|
561
|
+
demotedCallNames.set(block.id, block.name);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
// A `tool_reference` without its definition 400s the same way.
|
|
566
|
+
const danglingReferenceNames = new Set();
|
|
567
|
+
for (const name of discoveredNames) {
|
|
568
|
+
if (!definedNames.has(name))
|
|
569
|
+
danglingReferenceNames.add(name);
|
|
570
|
+
}
|
|
571
|
+
if (demotedCallNames.size === 0 && danglingReferenceNames.size === 0)
|
|
572
|
+
return params;
|
|
573
|
+
let changed = false;
|
|
574
|
+
const rewrittenMessages = [];
|
|
575
|
+
for (const message of messages) {
|
|
576
|
+
if (!Array.isArray(message.content)) {
|
|
577
|
+
rewrittenMessages.push(message);
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
let messageChanged = false;
|
|
581
|
+
const content = [];
|
|
582
|
+
for (const block of message.content) {
|
|
583
|
+
if (message.role === "assistant" && isRecord(block) && block.type === "tool_use") {
|
|
584
|
+
const demotedName = demotedCallNames.get(block.id);
|
|
585
|
+
if (demotedName !== undefined) {
|
|
586
|
+
messageChanged = true;
|
|
587
|
+
content.push({ type: "text", text: demotedToolCallText(demotedName, block.input) });
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (isRecord(block) && block.type === "tool_result") {
|
|
592
|
+
const demotedName = demotedCallNames.get(block.tool_use_id);
|
|
593
|
+
if (demotedName !== undefined) {
|
|
594
|
+
messageChanged = true;
|
|
595
|
+
content.push({ type: "text", text: demotedToolResultText(demotedName, block.content) });
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
if (danglingReferenceNames.size > 0 && Array.isArray(block.content)) {
|
|
599
|
+
const kept = [];
|
|
600
|
+
const omitted = [];
|
|
601
|
+
for (const item of block.content) {
|
|
602
|
+
if (isRecord(item) &&
|
|
603
|
+
item.type === "tool_reference" &&
|
|
604
|
+
typeof item.tool_name === "string" &&
|
|
605
|
+
danglingReferenceNames.has(item.tool_name)) {
|
|
606
|
+
omitted.push(item.tool_name);
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
kept.push(item);
|
|
610
|
+
}
|
|
611
|
+
if (omitted.length > 0) {
|
|
612
|
+
messageChanged = true;
|
|
613
|
+
const nextContent = kept.length > 0
|
|
614
|
+
? kept
|
|
615
|
+
: [{ type: "text", text: `Tool reference unavailable: ${[...new Set(omitted)].join(", ")}` }];
|
|
616
|
+
content.push({ ...block, content: nextContent });
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
content.push(block);
|
|
622
|
+
}
|
|
623
|
+
if (content.length === 0) {
|
|
624
|
+
changed = true;
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
if (messageChanged) {
|
|
628
|
+
changed = true;
|
|
629
|
+
rewrittenMessages.push({ ...message, content });
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
rewrittenMessages.push(message);
|
|
633
|
+
}
|
|
634
|
+
if (!changed)
|
|
635
|
+
return params;
|
|
636
|
+
return { ...params, messages: rewrittenMessages };
|
|
637
|
+
}
|
|
638
|
+
function collectToolReferenceNames(value, names) {
|
|
639
|
+
if (Array.isArray(value)) {
|
|
640
|
+
for (const item of value)
|
|
641
|
+
collectToolReferenceNames(item, names);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (!isRecord(value))
|
|
645
|
+
return;
|
|
646
|
+
if (value.type === "tool_reference" && typeof value.tool_name === "string")
|
|
647
|
+
names.add(value.tool_name);
|
|
648
|
+
for (const nested of Object.values(value))
|
|
649
|
+
collectToolReferenceNames(nested, names);
|
|
650
|
+
}
|
|
651
|
+
function demotedToolCallText(name, input) {
|
|
652
|
+
let serializedInput;
|
|
653
|
+
try {
|
|
654
|
+
serializedInput = JSON.stringify(input ?? {});
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
serializedInput = "{}";
|
|
658
|
+
}
|
|
659
|
+
return `[Called tool "${name}" (no longer available in this session) with input: ${serializedInput}]`;
|
|
660
|
+
}
|
|
661
|
+
function demotedToolResultText(name, content) {
|
|
662
|
+
return `[Result of unavailable tool "${name}": ${toolResultText(content)}]`;
|
|
663
|
+
}
|
|
664
|
+
function toolResultText(content) {
|
|
665
|
+
if (typeof content === "string")
|
|
666
|
+
return content;
|
|
667
|
+
if (Array.isArray(content)) {
|
|
668
|
+
const parts = [];
|
|
669
|
+
for (const item of content) {
|
|
670
|
+
if (!isRecord(item))
|
|
671
|
+
continue;
|
|
672
|
+
if (item.type === "text" && typeof item.text === "string")
|
|
673
|
+
parts.push(item.text);
|
|
674
|
+
else if (typeof item.type === "string")
|
|
675
|
+
parts.push(`[${item.type}]`);
|
|
676
|
+
}
|
|
677
|
+
if (parts.length > 0)
|
|
678
|
+
return parts.join("\n");
|
|
679
|
+
}
|
|
680
|
+
return "Tool output unavailable.";
|
|
681
|
+
}
|
|
480
682
|
function sanitizeAdaptiveThinkingPayload(model, params, options) {
|
|
481
683
|
if (!supportsAdaptiveThinking(model)) {
|
|
482
684
|
return params;
|
|
@@ -725,6 +927,7 @@ export const stream = (model, context, options) => {
|
|
|
725
927
|
}
|
|
726
928
|
params = sanitizeAdaptiveThinkingPayload(model, params, options);
|
|
727
929
|
params = sanitizeUnsupportedNativeTools(model, params);
|
|
930
|
+
params = demoteUnavailableToolReferences(params);
|
|
728
931
|
const payloadRequestMetadata = extractPayloadRequestMetadata(params);
|
|
729
932
|
params = payloadRequestMetadata.params;
|
|
730
933
|
const requestOptions = {
|
|
@@ -1055,6 +1258,18 @@ function cannotDisableThinking(model, compat) {
|
|
|
1055
1258
|
return true;
|
|
1056
1259
|
return matchesModelMarker(model, DISABLED_THINKING_REJECTING_MODEL_MARKERS);
|
|
1057
1260
|
}
|
|
1261
|
+
function disableThinkingForRequest(params, model, compat) {
|
|
1262
|
+
// A degraded/disabled turn must not retain the caller's higher effort.
|
|
1263
|
+
delete params.output_config;
|
|
1264
|
+
if (cannotDisableThinking(model, compat)) {
|
|
1265
|
+
delete params.thinking;
|
|
1266
|
+
if (supportsAdaptiveThinking(model)) {
|
|
1267
|
+
params.output_config = { effort: "low" };
|
|
1268
|
+
}
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
params.thinking = { type: "disabled" };
|
|
1272
|
+
}
|
|
1058
1273
|
function supportsAdaptiveThinking(model) {
|
|
1059
1274
|
if (model.compat?.forceAdaptiveThinking !== undefined) {
|
|
1060
1275
|
return model.compat.forceAdaptiveThinking;
|
|
@@ -1288,20 +1503,18 @@ function buildParams(model, context, isOAuthToken, options, unsignedThinkingRepl
|
|
|
1288
1503
|
}
|
|
1289
1504
|
}
|
|
1290
1505
|
else if (options?.thinkingEnabled === false) {
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
// effort: the request stays valid and reasoning stays at the documented minimum.
|
|
1296
|
-
if (supportsAdaptiveThinking(model)) {
|
|
1297
|
-
params.output_config = { effort: "low" };
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
else {
|
|
1301
|
-
params.thinking = { type: "disabled" };
|
|
1302
|
-
}
|
|
1506
|
+
// Some adaptive families reject `thinking.type: "disabled"` and default
|
|
1507
|
+
// to reasoning when omitted. Keep their request valid at the cheapest
|
|
1508
|
+
// legal effort; use an explicit disabled block everywhere else.
|
|
1509
|
+
disableThinkingForRequest(params, model, compat);
|
|
1303
1510
|
}
|
|
1304
1511
|
}
|
|
1512
|
+
// Anthropic rejects a thinking-enabled request whose final assistant turn
|
|
1513
|
+
// contains tool_use but does not begin with a thinking block. Cross-model
|
|
1514
|
+
// histories lose their signed thinking blocks (demoted to text or dropped),
|
|
1515
|
+
// so degrade thinking for this request instead of failing every turn.
|
|
1516
|
+
if (params.thinking && params.thinking.type !== "disabled" && finalAssistantTurnStartsWithToolUse(params.messages))
|
|
1517
|
+
disableThinkingForRequest(params, model, compat);
|
|
1305
1518
|
if (options?.metadata) {
|
|
1306
1519
|
const userId = options.metadata.user_id;
|
|
1307
1520
|
if (typeof userId === "string") {
|
|
@@ -1331,9 +1544,24 @@ function applyExtraBodyToAnthropicParams(params, extraBody) {
|
|
|
1331
1544
|
Object.defineProperty(params, key, { value, writable: true, enumerable: true, configurable: true });
|
|
1332
1545
|
}
|
|
1333
1546
|
}
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1547
|
+
/**
|
|
1548
|
+
* Whether the final assistant turn contains tool_use without a leading
|
|
1549
|
+
* thinking/redacted_thinking block. Anthropic requires thinking-enabled
|
|
1550
|
+
* requests to start that turn with a thinking block.
|
|
1551
|
+
*/
|
|
1552
|
+
function finalAssistantTurnStartsWithToolUse(messages) {
|
|
1553
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
1554
|
+
const message = messages[index];
|
|
1555
|
+
if (message.role !== "assistant")
|
|
1556
|
+
continue;
|
|
1557
|
+
if (!Array.isArray(message.content) || message.content.length === 0)
|
|
1558
|
+
return false;
|
|
1559
|
+
const first = message.content[0];
|
|
1560
|
+
if (first.type === "thinking" || first.type === "redacted_thinking")
|
|
1561
|
+
return false;
|
|
1562
|
+
return message.content.some((block) => block.type === "tool_use");
|
|
1563
|
+
}
|
|
1564
|
+
return false;
|
|
1337
1565
|
}
|
|
1338
1566
|
function convertToolResult(msg, isOAuthToken, deferredToolNames, loadedToolNames, normalizeToolName) {
|
|
1339
1567
|
const references = [];
|
|
@@ -1372,6 +1600,7 @@ function convertMessages(transformedMessages, model, isOAuthToken, cacheControl,
|
|
|
1372
1600
|
// assistant turn below; drop their tool_results in lockstep so none dangle.
|
|
1373
1601
|
const discardedFallbackToolCallIds = collectDiscardedFallbackToolCallIds(transformedMessages, model);
|
|
1374
1602
|
const rejectsNativeWebSearchReplay = !getAnthropicCompat(model).supportsWebSearch;
|
|
1603
|
+
const providerNativeToolPairing = collectProviderNativeToolPairing(transformedMessages, model, deferredToolNames, normalizeToolName, discardedFallbackToolCallIds);
|
|
1375
1604
|
for (let i = 0; i < transformedMessages.length; i++) {
|
|
1376
1605
|
const msg = transformedMessages[i];
|
|
1377
1606
|
if (msg.role === "user") {
|
|
@@ -1505,7 +1734,8 @@ function convertMessages(transformedMessages, model, isOAuthToken, cacheControl,
|
|
|
1505
1734
|
else if (block.type === "providerNative") {
|
|
1506
1735
|
if (isSameModel &&
|
|
1507
1736
|
isReplayableAnthropicProviderNativeBlock(block.raw) &&
|
|
1508
|
-
!(rejectsNativeWebSearchReplay && isAnthropicWebSearchReplayBlock(block.raw))
|
|
1737
|
+
!(rejectsNativeWebSearchReplay && isAnthropicWebSearchReplayBlock(block.raw)) &&
|
|
1738
|
+
!isUnpairedProviderNativeToolBlock(block.raw, providerNativeToolPairing)) {
|
|
1509
1739
|
blocks.push(block.raw);
|
|
1510
1740
|
}
|
|
1511
1741
|
}
|