@earendil-works/pi-ai 1.1.0 → 1.3.0
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/README.md +5 -34
- package/dist/api/anthropic-messages.d.ts.map +1 -1
- package/dist/api/anthropic-messages.js +120 -74
- package/dist/api/anthropic-messages.js.map +1 -1
- package/dist/api/bedrock-converse-stream.js +16 -18
- 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 +2 -0
- package/dist/api/google-shared.js.map +1 -1
- package/dist/api/mistral-conversations.d.ts.map +1 -1
- package/dist/api/mistral-conversations.js +31 -15
- package/dist/api/mistral-conversations.js.map +1 -1
- package/dist/api/openai-codex-responses.d.ts.map +1 -1
- package/dist/api/openai-codex-responses.js +2 -1
- package/dist/api/openai-codex-responses.js.map +1 -1
- package/dist/api/openai-completions.d.ts.map +1 -1
- package/dist/api/openai-completions.js +29 -27
- package/dist/api/openai-completions.js.map +1 -1
- package/dist/api/openai-responses-shared.d.ts +10 -0
- package/dist/api/openai-responses-shared.d.ts.map +1 -1
- package/dist/api/openai-responses-shared.js +15 -2
- 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 +21 -4
- package/dist/api/openai-responses.js.map +1 -1
- package/dist/api/transform-messages.d.ts +16 -1
- package/dist/api/transform-messages.d.ts.map +1 -1
- package/dist/api/transform-messages.js +28 -15
- package/dist/api/transform-messages.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/models.d.ts +5 -0
- package/dist/models.d.ts.map +1 -1
- package/dist/models.js +1 -0
- package/dist/models.js.map +1 -1
- package/dist/providers/compat-schema.d.ts +341 -0
- package/dist/providers/compat-schema.d.ts.map +1 -0
- package/dist/providers/compat-schema.js +383 -0
- package/dist/providers/compat-schema.js.map +1 -0
- 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/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/google.json +1 -1
- package/dist/providers/data/huggingface.json +1 -1
- package/dist/providers/data/kimi-coding.json +1 -1
- package/dist/providers/data/meta.json +1 -1
- package/dist/providers/data/mistral.json +1 -1
- package/dist/providers/data/nvidia.json +1 -1
- package/dist/providers/data/openai-codex.json +1 -1
- package/dist/providers/data/openai.json +1 -1
- package/dist/providers/data/opencode-go.json +1 -1
- package/dist/providers/data/opencode.json +1 -1
- package/dist/providers/data/openrouter.json +1 -1
- package/dist/providers/data/radius.json +1 -1
- package/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/dist/providers/data/xiaomi-token-plan-ams.json +1 -1
- package/dist/providers/data/xiaomi-token-plan-cn.json +1 -1
- package/dist/providers/data/xiaomi-token-plan-sgp.json +1 -1
- package/dist/providers/data/xiaomi.json +1 -1
- package/dist/providers/kimi-coding.d.ts +1 -1
- package/dist/providers/kimi-coding.js +3 -3
- package/dist/providers/kimi-coding.js.map +1 -1
- package/dist/providers/model-schema.d.ts +91 -0
- package/dist/providers/model-schema.d.ts.map +1 -0
- package/dist/providers/model-schema.js +55 -0
- package/dist/providers/model-schema.js.map +1 -0
- package/dist/providers/opencode-go.d.ts +3 -1
- package/dist/providers/opencode-go.d.ts.map +1 -1
- package/dist/providers/opencode-go.js +2 -0
- package/dist/providers/opencode-go.js.map +1 -1
- package/dist/providers/openrouter-api.d.ts +40 -0
- package/dist/providers/openrouter-api.d.ts.map +1 -0
- package/dist/providers/openrouter-api.js +41 -0
- package/dist/providers/openrouter-api.js.map +1 -0
- package/dist/providers/openrouter.d.ts +11 -1
- package/dist/providers/openrouter.d.ts.map +1 -1
- package/dist/providers/openrouter.js +52 -1
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/types.d.ts +10 -303
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/json-parse.d.ts +10 -0
- package/dist/utils/json-parse.d.ts.map +1 -1
- package/dist/utils/json-parse.js +26 -1
- package/dist/utils/json-parse.js.map +1 -1
- package/dist/utils/retry.d.ts.map +1 -1
- package/dist/utils/retry.js +8 -3
- package/dist/utils/retry.js.map +1 -1
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +3 -0
- package/dist/utils/validation.js.map +1 -1
- package/package.json +2 -2
- package/dist/utils/assistant-message-frame.d.ts +0 -77
- package/dist/utils/assistant-message-frame.d.ts.map +0 -1
- package/dist/utils/assistant-message-frame.js +0 -425
- package/dist/utils/assistant-message-frame.js.map +0 -1
package/README.md
CHANGED
|
@@ -26,7 +26,6 @@ Unified LLM API with provider collections, automatic auth resolution, token and
|
|
|
26
26
|
- [Streaming Tool Calls with Partial JSON](#streaming-tool-calls-with-partial-json)
|
|
27
27
|
- [Validating Tool Arguments](#validating-tool-arguments)
|
|
28
28
|
- [Complete Event Reference](#complete-event-reference)
|
|
29
|
-
- [Compact Assistant Message Frames](#compact-assistant-message-frames)
|
|
30
29
|
- [Image Input](#image-input)
|
|
31
30
|
- [Image Generation](#image-generation)
|
|
32
31
|
- [Classification](#classification)
|
|
@@ -723,34 +722,6 @@ All streaming events emitted during assistant message generation:
|
|
|
723
722
|
|
|
724
723
|
Streaming events for different content blocks are not guaranteed to be contiguous. Providers may emit deltas for text, thinking, and tool calls in the same upstream chunk, and pi may surface corresponding events interleaved, for example `text_start`, `text_delta`, `toolcall_start`, `text_delta`, `toolcall_delta`. Consumers must use `contentIndex` to associate each delta/end event with its block and must not assume that a block's `*_start`/`*_delta`/`*_end` sequence is uninterrupted by events for other blocks.
|
|
725
724
|
|
|
726
|
-
### Compact Assistant Message Frames
|
|
727
|
-
|
|
728
|
-
`AssistantMessageFrameEncoder` converts one stream into compact, persistable `AssistantMessageFrame` values. Create one encoder per stream and feed it every event in order. The encoder understands that `partial` is live: a block-start event consumed after the provider has already queued later deltas snapshots the current block once, and covered queued text/thinking deltas produce no duplicate frame. It retains only per-open-block counters plus, temporarily, the raw prefix needed to synchronize an already-advanced tool call. It never clones the growing full partial per token.
|
|
729
|
-
|
|
730
|
-
The start frame contains message metadata with empty content. Text and thinking frames store each generated character at most once before the authoritative end frame. Tool calls that were already advanced when their start event was consumed use one compact JSON checkpoint before ordinary deltas resume. Terminal `done` and `error` events produce no frame because final message settlement is separate. A pre-generation `error` therefore produces no frames.
|
|
731
|
-
|
|
732
|
-
`reduceAssistantMessageFrames()` is the canonical pure reducer. It reconstructs text, thinking, and tool-call arguments, including interleaved blocks identified by `contentIndex`, and rejects malformed sequences. It performs a single pass over the iterable and returns `undefined` when there is no start frame. End frames replace blocks with the provider's authoritative completed content and metadata. The reducer does not validate tool arguments against a TypeBox schema; call `validateToolCall` before execution.
|
|
733
|
-
|
|
734
|
-
```typescript
|
|
735
|
-
import {
|
|
736
|
-
AssistantMessageFrameEncoder,
|
|
737
|
-
reduceAssistantMessageFrames,
|
|
738
|
-
type AssistantMessageFrame,
|
|
739
|
-
} from '@earendil-works/pi-ai';
|
|
740
|
-
|
|
741
|
-
const encoder = new AssistantMessageFrameEncoder();
|
|
742
|
-
const frames: AssistantMessageFrame[] = [];
|
|
743
|
-
for await (const event of s) {
|
|
744
|
-
const frame = encoder.encode(event);
|
|
745
|
-
if (frame) frames.push(frame);
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
const reconstructedPartial = reduceAssistantMessageFrames(frames);
|
|
749
|
-
const finalMessage = await s.result(); // Persist terminal settlement separately.
|
|
750
|
-
```
|
|
751
|
-
|
|
752
|
-
An encoder rejects duplicate starts, updates before start, `done` before start, events after a terminal event, duplicate block starts, and block-kind mismatches. An `error` before start is valid and returns no frame.
|
|
753
|
-
|
|
754
725
|
## Image Input
|
|
755
726
|
|
|
756
727
|
Models with vision capabilities can process images. You can check if a model supports images via the `input` property. If you pass images to a non-vision model, they are silently ignored.
|
|
@@ -1527,13 +1498,13 @@ Notes:
|
|
|
1527
1498
|
|
|
1528
1499
|
## Cross-Provider Handoffs
|
|
1529
1500
|
|
|
1530
|
-
The library supports seamless handoffs between different LLM providers within the same conversation. This allows you to switch models mid-conversation while preserving context, including
|
|
1501
|
+
The library supports seamless handoffs between different LLM providers within the same conversation. This allows you to switch models mid-conversation while preserving context, including text, tool calls, and tool results.
|
|
1531
1502
|
|
|
1532
1503
|
When messages from one provider are sent to a different provider, the library automatically transforms them for compatibility:
|
|
1533
1504
|
|
|
1534
1505
|
- **User and tool result messages** are passed through unchanged
|
|
1535
|
-
- **Assistant messages from the same provider
|
|
1536
|
-
- **Assistant messages from different
|
|
1506
|
+
- **Assistant messages from the same provider, API, and model** are preserved as-is, including thinking blocks
|
|
1507
|
+
- **Assistant messages from a different model** have their thinking blocks dropped. Signatures and encrypted reasoning only work with the model that produced them, and readable thinking sent as plain text makes the new model treat it as part of the reply
|
|
1537
1508
|
- **Tool calls and regular text** are preserved unchanged
|
|
1538
1509
|
|
|
1539
1510
|
```typescript
|
|
@@ -1554,7 +1525,7 @@ const claude = models.getModel('anthropic', 'claude-sonnet-4-5')!;
|
|
|
1554
1525
|
context.messages.push({ role: 'user', content: 'What is 25 * 18?', timestamp: Date.now() });
|
|
1555
1526
|
context.messages.push(await models.completeSimple(claude, context, { reasoning: 'medium' }));
|
|
1556
1527
|
|
|
1557
|
-
// Switch to GPT-5 - it
|
|
1528
|
+
// Switch to GPT-5 - it sees Claude's answer, but not Claude's thinking
|
|
1558
1529
|
const gpt5 = models.getModel('openai', 'gpt-5-mini')!;
|
|
1559
1530
|
context.messages.push({ role: 'user', content: 'Is that calculation correct?', timestamp: Date.now() });
|
|
1560
1531
|
context.messages.push(await models.complete(gpt5, context));
|
|
@@ -1565,7 +1536,7 @@ context.messages.push({ role: 'user', content: 'What was the original question?'
|
|
|
1565
1536
|
const geminiResponse = await models.complete(gemini, context);
|
|
1566
1537
|
```
|
|
1567
1538
|
|
|
1568
|
-
All providers can handle messages from other providers — text, tool calls and results (including images), thinking blocks (
|
|
1539
|
+
All providers can handle messages from other providers — text, tool calls and results (including images), thinking blocks (dropped when the model changes), and aborted messages with partial content. This enables flexible workflows: start with a fast model, switch to a more capable one for complex reasoning, or maintain continuity across provider outages.
|
|
1569
1540
|
|
|
1570
1541
|
## System Messages
|
|
1571
1542
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"anthropic-messages.d.ts","sourceRoot":"","sources":["../../src/api/anthropic-messages.ts"],"names":[],"mappings":"AAAA,OAAO,SAAiC,MAAM,mBAAmB,CAAC;AAoBlE,OAAO,KAAK,EASX,mBAAmB,EAEnB,cAAc,EACd,aAAa,EAMb,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"anthropic-messages.d.ts","sourceRoot":"","sources":["../../src/api/anthropic-messages.ts"],"names":[],"mappings":"AAAA,OAAO,SAAiC,MAAM,mBAAmB,CAAC;AAoBlE,OAAO,KAAK,EASX,mBAAmB,EAEnB,cAAc,EACd,aAAa,EAMb,MAAM,aAAa,CAAC;AAuJrB,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;AAE1E,MAAM,MAAM,wBAAwB,GAAG,YAAY,GAAG,SAAS,CAAC;AA2ChE,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;AAwRD,eAAO,MAAM,MAAM,EAAE,cAAc,CAAC,oBAAoB,EAAE,gBAAgB,CA0WzE,CAAC;AA2BF,eAAO,MAAM,YAAY,EAAE,cAAc,CAAC,oBAAoB,EAAE,mBAAmB,CAgDlF,CAAC"}
|
|
@@ -4,7 +4,7 @@ import { calculateCost } from "../models.js";
|
|
|
4
4
|
import { appendAssistantMessageDiagnostic } from "../utils/diagnostics.js";
|
|
5
5
|
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
|
6
6
|
import { headersToRecord } from "../utils/headers.js";
|
|
7
|
-
import { parseJsonWithRepair, parseStreamingJson } from "../utils/json-parse.js";
|
|
7
|
+
import { parseJsonWithRepair, parseStreamingJson, parseToolCallArguments } from "../utils/json-parse.js";
|
|
8
8
|
import { getPiUserAgent } from "../utils/pi-user-agent.js";
|
|
9
9
|
import { getProviderEnvValue } from "../utils/provider-env.js";
|
|
10
10
|
import { retryProviderRequest } from "../utils/provider-retry.js";
|
|
@@ -14,7 +14,7 @@ import { getCurrentTools, getInitialSystemMessage, resolveTranscript, } from "..
|
|
|
14
14
|
import { getJsonSchemaToolParameters, resolveJsonSchemaStrictSampling, } from "./constrained-sampling.js";
|
|
15
15
|
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
|
|
16
16
|
import { adjustMaxTokensForThinking, buildBaseOptions, clampMaxTokensToContext } from "./simple-options.js";
|
|
17
|
-
import { transformMessages } from "./transform-messages.js";
|
|
17
|
+
import { splitErrorToolResultImages, transformMessages } from "./transform-messages.js";
|
|
18
18
|
/**
|
|
19
19
|
* Resolve cache retention preference.
|
|
20
20
|
* Defaults to "short" and uses PI_CACHE_RETENTION for backward compatibility.
|
|
@@ -75,6 +75,16 @@ const fromClaudeCodeName = (name, tools) => {
|
|
|
75
75
|
}
|
|
76
76
|
return name;
|
|
77
77
|
};
|
|
78
|
+
function convertImageBlock(image) {
|
|
79
|
+
return {
|
|
80
|
+
type: "image",
|
|
81
|
+
source: {
|
|
82
|
+
type: "base64",
|
|
83
|
+
media_type: image.mimeType,
|
|
84
|
+
data: image.data,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
78
88
|
/**
|
|
79
89
|
* Convert content blocks to Anthropic API format
|
|
80
90
|
*/
|
|
@@ -92,14 +102,7 @@ function convertContentBlocks(content) {
|
|
|
92
102
|
text: sanitizeSurrogates(block.text),
|
|
93
103
|
};
|
|
94
104
|
}
|
|
95
|
-
return
|
|
96
|
-
type: "image",
|
|
97
|
-
source: {
|
|
98
|
-
type: "base64",
|
|
99
|
-
media_type: block.mimeType,
|
|
100
|
-
data: block.data,
|
|
101
|
-
},
|
|
102
|
-
};
|
|
105
|
+
return convertImageBlock(block);
|
|
103
106
|
});
|
|
104
107
|
// If only images (no text), add placeholder text block
|
|
105
108
|
const hasText = blocks.some((b) => b.type === "text");
|
|
@@ -378,14 +381,12 @@ export const stream = (model, context, options) => {
|
|
|
378
381
|
const normalizedContext = resolveTranscript(context, getAnthropicCompat(model).supportsMidConvoSystemMessages);
|
|
379
382
|
const currentTools = getCurrentTools(normalizedContext.messages);
|
|
380
383
|
(async () => {
|
|
381
|
-
const providerThinkingLevel = model.compat?.supportsMidConvoEffort ? (options?.effort ?? "high") : undefined;
|
|
382
384
|
const output = {
|
|
383
385
|
role: "assistant",
|
|
384
386
|
content: [],
|
|
385
387
|
api: model.api,
|
|
386
388
|
provider: model.provider,
|
|
387
389
|
model: model.id,
|
|
388
|
-
...(providerThinkingLevel === undefined ? {} : { providerThinkingLevel }),
|
|
389
390
|
usage: {
|
|
390
391
|
input: 0,
|
|
391
392
|
output: 0,
|
|
@@ -425,21 +426,55 @@ export const stream = (model, context, options) => {
|
|
|
425
426
|
client = created.client;
|
|
426
427
|
isOAuth = created.isOAuthToken;
|
|
427
428
|
}
|
|
428
|
-
let params = buildParams(model, normalizedContext, isOAuth, options);
|
|
429
|
-
const nextParams = await options?.onPayload?.(params, model);
|
|
430
|
-
if (nextParams !== undefined) {
|
|
431
|
-
params = { ...nextParams, stream: true };
|
|
432
|
-
}
|
|
433
429
|
const requestOptions = {
|
|
434
430
|
...(options?.signal ? { signal: options.signal } : {}),
|
|
435
431
|
...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
|
|
436
432
|
maxRetries: 0,
|
|
437
433
|
};
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
434
|
+
let disablePreferredStrictTools = false;
|
|
435
|
+
let response;
|
|
436
|
+
for (;;) {
|
|
437
|
+
options?.signal?.throwIfAborted();
|
|
438
|
+
const built = buildParams(model, normalizedContext, isOAuth, options, disablePreferredStrictTools);
|
|
439
|
+
if (built.providerThinkingLevel)
|
|
440
|
+
output.providerThinkingLevel = built.providerThinkingLevel;
|
|
441
|
+
let params = built.params;
|
|
442
|
+
const nextParams = await options?.onPayload?.(params, model);
|
|
443
|
+
if (nextParams !== undefined) {
|
|
444
|
+
params = { ...nextParams, stream: true };
|
|
445
|
+
}
|
|
446
|
+
try {
|
|
447
|
+
response = await retryProviderRequest(() => client.beta.messages.create(params, requestOptions).asResponse(), {
|
|
448
|
+
maxRetries: options?.maxRetries,
|
|
449
|
+
maxRetryDelayMs: options?.maxRetryDelayMs,
|
|
450
|
+
signal: options?.signal,
|
|
451
|
+
});
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
// Only downgrade a rejected request, never an already-started stream.
|
|
456
|
+
if (options?.signal?.aborted ||
|
|
457
|
+
disablePreferredStrictTools ||
|
|
458
|
+
!built.hasPreferredStrictTools ||
|
|
459
|
+
!(error instanceof Anthropic.APIError) ||
|
|
460
|
+
error.status !== 400 ||
|
|
461
|
+
error.type !== "invalid_request_error" ||
|
|
462
|
+
![
|
|
463
|
+
"The compiled grammar is too large",
|
|
464
|
+
"Too many strict tools (",
|
|
465
|
+
"Schemas contains too many parameters with union types (",
|
|
466
|
+
].some((message) => error.message.includes(message))) {
|
|
467
|
+
throw error;
|
|
468
|
+
}
|
|
469
|
+
disablePreferredStrictTools = true;
|
|
470
|
+
appendAssistantMessageDiagnostic(output, {
|
|
471
|
+
type: "anthropic_strict_tools_fallback",
|
|
472
|
+
timestamp: Date.now(),
|
|
473
|
+
error: { message: error.message },
|
|
474
|
+
details: { reason: "strict_tool_limits_exceeded" },
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
443
478
|
await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
|
|
444
479
|
stream.push({ type: "start", partial: output });
|
|
445
480
|
const blocks = output.content;
|
|
@@ -593,7 +628,7 @@ export const stream = (model, context, options) => {
|
|
|
593
628
|
});
|
|
594
629
|
}
|
|
595
630
|
else if (block.type === "toolCall") {
|
|
596
|
-
block
|
|
631
|
+
Object.assign(block, parseToolCallArguments(block.partialJson));
|
|
597
632
|
// Finalize in-place and strip the scratch buffer so replay only
|
|
598
633
|
// carries parsed arguments.
|
|
599
634
|
delete block.partialJson;
|
|
@@ -855,7 +890,7 @@ function getBetaFeatures(model, context, isOAuthToken, nativeToolChanges, option
|
|
|
855
890
|
features.push(INLINE_TOOLS_BETA);
|
|
856
891
|
return [...new Set(features)];
|
|
857
892
|
}
|
|
858
|
-
function buildParams(model, context, isOAuthToken, options) {
|
|
893
|
+
function buildParams(model, context, isOAuthToken, options, disablePreferredStrictTools = false) {
|
|
859
894
|
const { cacheControl } = getCacheControl(model, options?.cacheRetention, options?.env);
|
|
860
895
|
const compat = getAnthropicCompat(model);
|
|
861
896
|
const initialSystemMessage = getInitialSystemMessage(context.messages);
|
|
@@ -866,18 +901,28 @@ function buildParams(model, context, isOAuthToken, options) {
|
|
|
866
901
|
// by value in a `tool_addition` block, which also expresses same-name redefinitions.
|
|
867
902
|
// Anthropic rejects a tool list where every tool is deferred, so there must be an initial
|
|
868
903
|
// active tool to anchor the placeholder. Otherwise the current tool list is sent.
|
|
904
|
+
let hasPreferredStrictTools = false;
|
|
905
|
+
const convertToolDefinitions = (tools, toolCacheControl) => {
|
|
906
|
+
const definitions = convertTools(tools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, toolCacheControl, disablePreferredStrictTools);
|
|
907
|
+
if (definitions.some((definition, index) => {
|
|
908
|
+
const config = tools[index].constrainedSampling;
|
|
909
|
+
return definition.strict === true && config && config.type === "json_schema" && config.strict === "prefer";
|
|
910
|
+
})) {
|
|
911
|
+
hasPreferredStrictTools = true;
|
|
912
|
+
}
|
|
913
|
+
return definitions;
|
|
914
|
+
};
|
|
869
915
|
const initialTools = initialSystemMessage?.toolsAdded ?? [];
|
|
870
916
|
const nativeToolChanges = compat.supportsMidConvoSystemMessages && compat.supportsMidConvoToolChanges && initialTools.length > 0;
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
917
|
+
const thinkingDisabled = model.reasoning && options?.thinkingEnabled === false && model.thinkingLevelMap?.off !== null;
|
|
918
|
+
// Effort cannot change between messages while thinking is disabled, so "off" sends no effort markers.
|
|
919
|
+
const effortMessages = model.compat?.supportsMidConvoEffort === true && !thinkingDisabled;
|
|
920
|
+
const converted = convertMessages(conversationMessages, isOAuthToken, cacheControl, compat.allowEmptySignature, effortMessages ? model.provider : undefined, nativeToolChanges ? convertToolDefinitions : undefined);
|
|
874
921
|
const activeEffort = options?.effort ?? "high";
|
|
875
922
|
const betaFeatures = getBetaFeatures(model, context, isOAuthToken, nativeToolChanges, options);
|
|
876
923
|
const params = {
|
|
877
924
|
model: model.id,
|
|
878
|
-
messages:
|
|
879
|
-
? insertThinkingLevelMessages(converted, activeEffort)
|
|
880
|
-
: converted.messages,
|
|
925
|
+
messages: effortMessages ? insertThinkingLevelMessages(converted, activeEffort) : converted.messages,
|
|
881
926
|
max_tokens: options?.maxTokens ?? model.maxTokens,
|
|
882
927
|
stream: true,
|
|
883
928
|
...(betaFeatures.length > 0 ? { betas: betaFeatures } : {}),
|
|
@@ -922,20 +967,22 @@ function buildParams(model, context, isOAuthToken, options) {
|
|
|
922
967
|
// placeholder. The list never changes afterwards: later tools are defined by value in
|
|
923
968
|
// `tool_addition` blocks and withdrawn by `tool_removal`, so the cached prefix survives
|
|
924
969
|
// every tool change.
|
|
925
|
-
params.tools = [
|
|
926
|
-
...convertTools(initialTools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, toolCacheControl),
|
|
927
|
-
DEFERRED_TOOL_PLACEHOLDER,
|
|
928
|
-
];
|
|
970
|
+
params.tools = [...convertToolDefinitions(initialTools, toolCacheControl), DEFERRED_TOOL_PLACEHOLDER];
|
|
929
971
|
}
|
|
930
972
|
else {
|
|
931
973
|
const tools = getCurrentTools(context.messages);
|
|
932
974
|
if (tools.length > 0) {
|
|
933
|
-
params.tools =
|
|
975
|
+
params.tools = convertToolDefinitions(tools, toolCacheControl);
|
|
934
976
|
}
|
|
935
977
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
978
|
+
if (thinkingDisabled) {
|
|
979
|
+
// No effort is sent, so the model default applies. Anthropic rejects `block_binding`
|
|
980
|
+
// with disabled thinking.
|
|
981
|
+
params.thinking = { type: "disabled" };
|
|
982
|
+
}
|
|
983
|
+
else if (effortMessages) {
|
|
984
|
+
// Managed effort models use adaptive thinking so prefix mismatches can
|
|
985
|
+
// be dropped instead of surfacing as persistent 400 responses.
|
|
939
986
|
params.thinking = {
|
|
940
987
|
type: "adaptive",
|
|
941
988
|
display: options?.thinkingDisplay ?? "summarized",
|
|
@@ -943,29 +990,24 @@ function buildParams(model, context, isOAuthToken, options) {
|
|
|
943
990
|
};
|
|
944
991
|
params.output_config = { effort: "high" };
|
|
945
992
|
}
|
|
946
|
-
else if (model.reasoning) {
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
params.output_config = { effort: options.effort };
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
else {
|
|
959
|
-
// Budget-based thinking for older models
|
|
960
|
-
params.thinking = {
|
|
961
|
-
type: "enabled",
|
|
962
|
-
budget_tokens: options.thinkingBudgetTokens || 1024,
|
|
963
|
-
display,
|
|
964
|
-
};
|
|
993
|
+
else if (model.reasoning && options?.thinkingEnabled) {
|
|
994
|
+
// Default to "summarized" so Opus 4.7 and Mythos Preview behave like
|
|
995
|
+
// older Claude 4 models (whose API default is also "summarized").
|
|
996
|
+
const display = options.thinkingDisplay ?? "summarized";
|
|
997
|
+
if (model.compat?.forceAdaptiveThinking === true) {
|
|
998
|
+
// Adaptive thinking: Claude decides when and how much to think.
|
|
999
|
+
params.thinking = { type: "adaptive", display };
|
|
1000
|
+
if (options.effort) {
|
|
1001
|
+
params.output_config = { effort: options.effort };
|
|
965
1002
|
}
|
|
966
1003
|
}
|
|
967
|
-
else
|
|
968
|
-
|
|
1004
|
+
else {
|
|
1005
|
+
// Budget-based thinking for older models
|
|
1006
|
+
params.thinking = {
|
|
1007
|
+
type: "enabled",
|
|
1008
|
+
budget_tokens: options.thinkingBudgetTokens || 1024,
|
|
1009
|
+
display,
|
|
1010
|
+
};
|
|
969
1011
|
}
|
|
970
1012
|
}
|
|
971
1013
|
if (options?.metadata) {
|
|
@@ -986,17 +1028,17 @@ function buildParams(model, context, isOAuthToken, options) {
|
|
|
986
1028
|
if (allowedFallbackModels && allowedFallbackModels.length > 0) {
|
|
987
1029
|
params.fallbacks = allowedFallbackModels.map((fallback) => ({ model: fallback.model }));
|
|
988
1030
|
}
|
|
989
|
-
return params;
|
|
1031
|
+
return { params, hasPreferredStrictTools, providerThinkingLevel: effortMessages ? activeEffort : undefined };
|
|
990
1032
|
}
|
|
991
1033
|
// Normalize tool call IDs to match Anthropic's required pattern and length
|
|
992
1034
|
function normalizeToolCallId(id) {
|
|
993
1035
|
return id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
|
|
994
1036
|
}
|
|
995
|
-
function convertToolResult(msg) {
|
|
1037
|
+
function convertToolResult(msg, content = msg.content) {
|
|
996
1038
|
return {
|
|
997
1039
|
type: "tool_result",
|
|
998
1040
|
tool_use_id: msg.toolCallId,
|
|
999
|
-
content: convertContentBlocks(
|
|
1041
|
+
content: convertContentBlocks(content),
|
|
1000
1042
|
is_error: msg.isError,
|
|
1001
1043
|
};
|
|
1002
1044
|
}
|
|
@@ -1061,14 +1103,7 @@ convertToolDefinitions) {
|
|
|
1061
1103
|
};
|
|
1062
1104
|
}
|
|
1063
1105
|
else {
|
|
1064
|
-
return
|
|
1065
|
-
type: "image",
|
|
1066
|
-
source: {
|
|
1067
|
-
type: "base64",
|
|
1068
|
-
media_type: item.mimeType,
|
|
1069
|
-
data: item.data,
|
|
1070
|
-
},
|
|
1071
|
-
};
|
|
1106
|
+
return convertImageBlock(item);
|
|
1072
1107
|
}
|
|
1073
1108
|
});
|
|
1074
1109
|
const filteredBlocks = blocks.filter((b) => {
|
|
@@ -1159,16 +1194,24 @@ convertToolDefinitions) {
|
|
|
1159
1194
|
else if (msg.role === "toolResult") {
|
|
1160
1195
|
// Collect all consecutive toolResult messages, needed for z.ai Anthropic endpoint.
|
|
1161
1196
|
const toolResults = [];
|
|
1197
|
+
// Anthropic rejects error tool results that contain anything but text, so their images follow
|
|
1198
|
+
// all tool results of the turn, which must come first in the user message (#10794).
|
|
1199
|
+
const errorImages = [];
|
|
1162
1200
|
let j = i;
|
|
1163
1201
|
while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") {
|
|
1164
|
-
|
|
1202
|
+
const toolMsg = transformedMessages[j];
|
|
1203
|
+
const split = splitErrorToolResultImages(toolMsg);
|
|
1204
|
+
toolResults.push(convertToolResult(toolMsg, split?.content));
|
|
1205
|
+
if (split) {
|
|
1206
|
+
errorImages.push({ type: "text", text: split.caption }, ...split.images.map(convertImageBlock));
|
|
1207
|
+
}
|
|
1165
1208
|
j++;
|
|
1166
1209
|
}
|
|
1167
1210
|
// Skip the messages we've already processed.
|
|
1168
1211
|
i = j - 1;
|
|
1169
1212
|
params.push({
|
|
1170
1213
|
role: "user",
|
|
1171
|
-
content: toolResults,
|
|
1214
|
+
content: [...toolResults, ...errorImages],
|
|
1172
1215
|
});
|
|
1173
1216
|
}
|
|
1174
1217
|
}
|
|
@@ -1255,11 +1298,14 @@ const isAnthropicStrictUnsupportedKeyword = (key, value) => {
|
|
|
1255
1298
|
return typeof value !== "string" || !ANTHROPIC_STRICT_STRING_FORMATS.has(value);
|
|
1256
1299
|
return false;
|
|
1257
1300
|
};
|
|
1258
|
-
function convertTools(tools, isOAuthToken, supportsEagerToolInputStreaming, supportsStrictTools, cacheControl) {
|
|
1301
|
+
function convertTools(tools, isOAuthToken, supportsEagerToolInputStreaming, supportsStrictTools, cacheControl, disablePreferredStrictTools = false) {
|
|
1259
1302
|
if (!tools)
|
|
1260
1303
|
return [];
|
|
1261
1304
|
return tools.map((tool, index) => {
|
|
1262
|
-
const
|
|
1305
|
+
const config = tool.constrainedSampling;
|
|
1306
|
+
const strict = disablePreferredStrictTools && config && config.type === "json_schema" && config.strict === "prefer"
|
|
1307
|
+
? undefined
|
|
1308
|
+
: resolveJsonSchemaStrictSampling(tool, supportsStrictTools, isAnthropicStrictUnsupportedKeyword);
|
|
1263
1309
|
const parameters = getJsonSchemaToolParameters(tool, strict);
|
|
1264
1310
|
const schema = parameters;
|
|
1265
1311
|
const legacyInputSchema = {
|