@earendil-works/pi-ai 0.84.2 → 0.84.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/api/anthropic-messages.d.ts.map +1 -1
- package/dist/api/anthropic-messages.js +34 -19
- package/dist/api/anthropic-messages.js.map +1 -1
- package/dist/api/azure-openai-responses.d.ts +2 -0
- package/dist/api/azure-openai-responses.d.ts.map +1 -1
- package/dist/api/azure-openai-responses.js +9 -2
- package/dist/api/azure-openai-responses.js.map +1 -1
- package/dist/api/bedrock-converse-stream.d.ts.map +1 -1
- package/dist/api/bedrock-converse-stream.js +131 -7
- package/dist/api/bedrock-converse-stream.js.map +1 -1
- package/dist/api/google-generative-ai.d.ts +2 -2
- package/dist/api/google-generative-ai.d.ts.map +1 -1
- package/dist/api/google-generative-ai.js +17 -13
- package/dist/api/google-generative-ai.js.map +1 -1
- package/dist/api/google-shared.d.ts +5 -2
- package/dist/api/google-shared.d.ts.map +1 -1
- package/dist/api/google-shared.js +16 -0
- package/dist/api/google-shared.js.map +1 -1
- package/dist/api/google-vertex.d.ts +2 -2
- package/dist/api/google-vertex.d.ts.map +1 -1
- package/dist/api/google-vertex.js +15 -11
- package/dist/api/google-vertex.js.map +1 -1
- package/dist/api/mistral-conversations.d.ts.map +1 -1
- package/dist/api/mistral-conversations.js +7 -2
- 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 +4 -1
- package/dist/api/openai-codex-responses.js.map +1 -1
- package/dist/api/openai-completions.d.ts +3 -2
- package/dist/api/openai-completions.d.ts.map +1 -1
- package/dist/api/openai-completions.js +160 -86
- package/dist/api/openai-completions.js.map +1 -1
- package/dist/api/openai-responses.d.ts.map +1 -1
- package/dist/api/openai-responses.js +6 -2
- package/dist/api/openai-responses.js.map +1 -1
- package/dist/api/pi-messages.d.ts.map +1 -1
- package/dist/api/pi-messages.js +1 -1
- package/dist/api/pi-messages.js.map +1 -1
- package/dist/api/simple-options.d.ts +4 -0
- package/dist/api/simple-options.d.ts.map +1 -1
- package/dist/api/simple-options.js +17 -10
- package/dist/api/simple-options.js.map +1 -1
- package/dist/auth/oauth/device-code.d.ts +1 -0
- package/dist/auth/oauth/device-code.d.ts.map +1 -1
- package/dist/auth/oauth/device-code.js +1 -1
- package/dist/auth/oauth/device-code.js.map +1 -1
- package/dist/auth/oauth/github-copilot.d.ts.map +1 -1
- package/dist/auth/oauth/github-copilot.js +98 -34
- package/dist/auth/oauth/github-copilot.js.map +1 -1
- package/dist/auth/oauth/kimi-coding.d.ts.map +1 -1
- package/dist/auth/oauth/kimi-coding.js +1 -14
- package/dist/auth/oauth/kimi-coding.js.map +1 -1
- package/dist/image-models.generated.d.ts +75 -0
- package/dist/image-models.generated.d.ts.map +1 -1
- package/dist/image-models.generated.js +75 -0
- package/dist/image-models.generated.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/providers/cloudflare-ai-gateway.d.ts +3 -1
- package/dist/providers/cloudflare-ai-gateway.d.ts.map +1 -1
- package/dist/providers/cloudflare-ai-gateway.js.map +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/azure-openai-responses.json +1 -1
- package/dist/providers/data/baseten.json +1 -1
- package/dist/providers/data/cerebras.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/deepseek.json +1 -1
- package/dist/providers/data/fireworks.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/minimax-cn.json +1 -1
- package/dist/providers/data/minimax.json +1 -1
- package/dist/providers/data/mistral.json +1 -1
- package/dist/providers/data/nvidia.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/qwen-token-plan-cn.json +1 -1
- package/dist/providers/data/qwen-token-plan-individual.json +1 -1
- package/dist/providers/data/qwen-token-plan.json +1 -1
- package/dist/providers/data/together.json +1 -1
- package/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/dist/providers/data/xai.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/data/zai-coding-cn.json +1 -1
- package/dist/providers/data/zai.json +1 -1
- package/dist/providers/xai.d.ts +1 -1
- package/dist/providers/xai.d.ts.map +1 -1
- package/dist/providers/xai.js +1 -5
- package/dist/providers/xai.js.map +1 -1
- package/dist/types.d.ts +29 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/sleep.d.ts +2 -0
- package/dist/utils/sleep.d.ts.map +1 -0
- package/dist/utils/sleep.js +15 -0
- package/dist/utils/sleep.js.map +1 -0
- package/package.json +3 -4
|
@@ -5,13 +5,14 @@ import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
|
|
5
5
|
import { shortHash } from "../utils/hash.js";
|
|
6
6
|
import { headersToRecord } from "../utils/headers.js";
|
|
7
7
|
import { parseStreamingJson } from "../utils/json-parse.js";
|
|
8
|
+
import { getPiUserAgent } from "../utils/pi-user-agent.js";
|
|
8
9
|
import { getProviderEnvValue } from "../utils/provider-env.js";
|
|
9
10
|
import { retryProviderRequest } from "../utils/provider-retry.js";
|
|
10
11
|
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
|
11
12
|
import { appendGrammarToolInputJsonDelta, createGrammarToolInputProperties, getGrammarToolInput, getJsonSchemaToolParameters, resolveGrammarConstrainedSampling, resolveJsonSchemaStrictSampling, } from "./constrained-sampling.js";
|
|
12
13
|
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
|
|
13
14
|
import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.js";
|
|
14
|
-
import { buildBaseOptions,
|
|
15
|
+
import { buildBaseOptions, clampThinkingBudgetToAnswerRoom, thinkingBudgetForLevel } from "./simple-options.js";
|
|
15
16
|
import { transformMessages } from "./transform-messages.js";
|
|
16
17
|
/**
|
|
17
18
|
* Check if conversation messages contain tool calls or tool results.
|
|
@@ -79,16 +80,81 @@ function isToolCallBlock(block) {
|
|
|
79
80
|
function isImageContentBlock(block) {
|
|
80
81
|
return block.type === "image";
|
|
81
82
|
}
|
|
82
|
-
function
|
|
83
|
-
|
|
83
|
+
function isReasoningDetailObject(detail) {
|
|
84
|
+
return typeof detail === "object" && detail !== null && !Array.isArray(detail);
|
|
85
|
+
}
|
|
86
|
+
function hasValidCommonReasoningDetailFields(candidate) {
|
|
87
|
+
return ((candidate.id === undefined || candidate.id === null || typeof candidate.id === "string") &&
|
|
88
|
+
(candidate.format === undefined || typeof candidate.format === "string") &&
|
|
89
|
+
(candidate.index === undefined || typeof candidate.index === "number"));
|
|
90
|
+
}
|
|
91
|
+
function isOpenAIReasoningDetail(detail) {
|
|
92
|
+
if (!isReasoningDetailObject(detail) || !hasValidCommonReasoningDetailFields(detail)) {
|
|
84
93
|
return false;
|
|
85
94
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
95
|
+
switch (detail.type) {
|
|
96
|
+
case "reasoning.summary":
|
|
97
|
+
return typeof detail.summary === "string";
|
|
98
|
+
case "reasoning.encrypted":
|
|
99
|
+
return typeof detail.data === "string";
|
|
100
|
+
case "reasoning.text":
|
|
101
|
+
return (typeof detail.text === "string" &&
|
|
102
|
+
(detail.signature === undefined || detail.signature === null || typeof detail.signature === "string"));
|
|
103
|
+
default:
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function parseOpenAIReasoningDetails(signature) {
|
|
108
|
+
if (!signature)
|
|
109
|
+
return undefined;
|
|
110
|
+
try {
|
|
111
|
+
const parsed = JSON.parse(signature);
|
|
112
|
+
return Array.isArray(parsed) && parsed.length > 0 && parsed.every(isOpenAIReasoningDetail) ? parsed : undefined;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function parseLegacyEncryptedReasoningDetail(signature) {
|
|
119
|
+
if (!signature)
|
|
120
|
+
return undefined;
|
|
121
|
+
try {
|
|
122
|
+
const parsed = JSON.parse(signature);
|
|
123
|
+
return isOpenAIReasoningDetail(parsed) &&
|
|
124
|
+
parsed.type === "reasoning.encrypted" &&
|
|
125
|
+
typeof parsed.id === "string" &&
|
|
126
|
+
parsed.id.length > 0 &&
|
|
127
|
+
parsed.data.length > 0
|
|
128
|
+
? parsed
|
|
129
|
+
: undefined;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function fillMissingCommonReasoningDetailFields(target, source) {
|
|
136
|
+
target.id ??= source.id;
|
|
137
|
+
target.format ||= source.format;
|
|
138
|
+
target.index ??= source.index;
|
|
139
|
+
}
|
|
140
|
+
function appendOpenAIReasoningDetail(details, detail) {
|
|
141
|
+
const lastDetail = details[details.length - 1];
|
|
142
|
+
if (detail.type === "reasoning.text" && lastDetail?.type === "reasoning.text") {
|
|
143
|
+
lastDetail.text += detail.text;
|
|
144
|
+
lastDetail.signature ||= detail.signature;
|
|
145
|
+
fillMissingCommonReasoningDetailFields(lastDetail, detail);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (detail.type === "reasoning.summary" && lastDetail?.type === "reasoning.summary") {
|
|
149
|
+
lastDetail.summary += detail.summary;
|
|
150
|
+
fillMissingCommonReasoningDetailFields(lastDetail, detail);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
details.push({ ...detail });
|
|
154
|
+
}
|
|
155
|
+
const OPENAI_COMPLETIONS_REASONING_FIELDS = ["reasoning", "reasoning_content", "reasoning_text"];
|
|
156
|
+
function isOpenAICompletionsReasoningField(field) {
|
|
157
|
+
return OPENAI_COMPLETIONS_REASONING_FIELDS.includes(field);
|
|
92
158
|
}
|
|
93
159
|
function resolveCacheRetention(cacheRetention, env) {
|
|
94
160
|
if (cacheRetention) {
|
|
@@ -119,6 +185,14 @@ export const stream = (model, context, options) => {
|
|
|
119
185
|
stopReason: "pending",
|
|
120
186
|
timestamp: Date.now(),
|
|
121
187
|
};
|
|
188
|
+
// `reasoning_details` are replay metadata, not user-visible stream deltas.
|
|
189
|
+
// Keep them in memory during streaming and serialize once when the block is finalized.
|
|
190
|
+
let streamedReasoningDetails;
|
|
191
|
+
const applyStreamedReasoningDetails = (block) => {
|
|
192
|
+
if (streamedReasoningDetails !== undefined) {
|
|
193
|
+
block.thinkingSignature = JSON.stringify(streamedReasoningDetails);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
122
196
|
try {
|
|
123
197
|
const apiKey = getClientApiKey(model.provider, options?.apiKey, options?.headers);
|
|
124
198
|
const compat = getCompat(model);
|
|
@@ -148,7 +222,6 @@ export const stream = (model, context, options) => {
|
|
|
148
222
|
let hasFinishReason = false;
|
|
149
223
|
const toolCallBlocksByIndex = new Map();
|
|
150
224
|
const toolCallBlocksById = new Map();
|
|
151
|
-
const pendingReasoningDetailsByToolCallId = new Map();
|
|
152
225
|
const blocks = output.content;
|
|
153
226
|
const getContentIndex = (block) => blocks.indexOf(block);
|
|
154
227
|
const getCustomToolCallInput = (block) => {
|
|
@@ -180,6 +253,7 @@ export const stream = (model, context, options) => {
|
|
|
180
253
|
});
|
|
181
254
|
}
|
|
182
255
|
else if (block.type === "thinking") {
|
|
256
|
+
applyStreamedReasoningDetails(block);
|
|
183
257
|
stream.push({
|
|
184
258
|
type: "thinking_end",
|
|
185
259
|
contentIndex,
|
|
@@ -235,16 +309,6 @@ export const stream = (model, context, options) => {
|
|
|
235
309
|
}
|
|
236
310
|
return thinkingBlock;
|
|
237
311
|
};
|
|
238
|
-
const applyPendingReasoningDetail = (block) => {
|
|
239
|
-
if (!block.id) {
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
const pendingReasoningDetail = pendingReasoningDetailsByToolCallId.get(block.id);
|
|
243
|
-
if (pendingReasoningDetail) {
|
|
244
|
-
block.thoughtSignature = pendingReasoningDetail;
|
|
245
|
-
pendingReasoningDetailsByToolCallId.delete(block.id);
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
312
|
const ensureToolCallBlock = (toolCall) => {
|
|
249
313
|
const streamIndex = typeof toolCall.index === "number" ? toolCall.index : undefined;
|
|
250
314
|
const name = toolCall.function?.name ?? toolCall.custom?.name ?? "";
|
|
@@ -300,7 +364,6 @@ export const stream = (model, context, options) => {
|
|
|
300
364
|
};
|
|
301
365
|
delete block.partialArgs;
|
|
302
366
|
}
|
|
303
|
-
applyPendingReasoningDetail(block);
|
|
304
367
|
return block;
|
|
305
368
|
};
|
|
306
369
|
for await (const chunk of openaiStream) {
|
|
@@ -407,16 +470,14 @@ export const stream = (model, context, options) => {
|
|
|
407
470
|
const reasoningDetails = choice.delta.reasoning_details;
|
|
408
471
|
if (Array.isArray(reasoningDetails)) {
|
|
409
472
|
for (const detail of reasoningDetails) {
|
|
410
|
-
if (
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}
|
|
419
|
-
}
|
|
473
|
+
if (!isOpenAIReasoningDetail(detail))
|
|
474
|
+
continue;
|
|
475
|
+
ensureThinkingBlock("");
|
|
476
|
+
streamedReasoningDetails ??= [];
|
|
477
|
+
// Keep provider replay data in the existing signature slot. OpenRouter streams
|
|
478
|
+
// reasoning_details as deltas: consecutive text/summary deltas are merged into
|
|
479
|
+
// logical entries, while encrypted entries remain opaque and discrete.
|
|
480
|
+
appendOpenAIReasoningDetail(streamedReasoningDetails, detail);
|
|
420
481
|
}
|
|
421
482
|
}
|
|
422
483
|
}
|
|
@@ -444,6 +505,9 @@ export const stream = (model, context, options) => {
|
|
|
444
505
|
}
|
|
445
506
|
catch (error) {
|
|
446
507
|
for (const block of output.content) {
|
|
508
|
+
if (block.type === "thinking") {
|
|
509
|
+
applyStreamedReasoningDetails(block);
|
|
510
|
+
}
|
|
447
511
|
delete block.index;
|
|
448
512
|
// Streaming scratch buffers are only used during parsing; never persist them.
|
|
449
513
|
delete block.partialArgs;
|
|
@@ -468,19 +532,20 @@ export const stream = (model, context, options) => {
|
|
|
468
532
|
};
|
|
469
533
|
export const streamSimple = (model, context, options) => {
|
|
470
534
|
getClientApiKey(model.provider, options?.apiKey, options?.headers);
|
|
471
|
-
const base =
|
|
535
|
+
const base = {
|
|
536
|
+
...buildBaseOptions(model, context, options, options?.apiKey),
|
|
537
|
+
toolChoice: options?.toolChoice,
|
|
538
|
+
};
|
|
472
539
|
const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
|
|
473
540
|
const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
|
|
474
|
-
const toolChoice = options?.toolChoice;
|
|
475
541
|
return stream(model, context, {
|
|
476
542
|
...base,
|
|
477
543
|
reasoningEffort,
|
|
478
|
-
toolChoice,
|
|
479
544
|
thinkingBudgets: options?.thinkingBudgets,
|
|
480
545
|
});
|
|
481
546
|
};
|
|
482
547
|
function createClient(model, context, apiKey, optionsHeaders, fetch, sessionId, compat = getCompat(model)) {
|
|
483
|
-
const headers = { ...model.headers };
|
|
548
|
+
const headers = { "User-Agent": getPiUserAgent(), ...model.headers };
|
|
484
549
|
if (model.provider === "github-copilot") {
|
|
485
550
|
const hasImages = hasCopilotVisionInput(context.messages);
|
|
486
551
|
const copilotHeaders = buildCopilotDynamicHeaders({
|
|
@@ -561,6 +626,8 @@ function buildParams(model, context, options, compat = getCompat(model), cacheRe
|
|
|
561
626
|
if (options?.toolChoice) {
|
|
562
627
|
params.tool_choice = options.toolChoice;
|
|
563
628
|
}
|
|
629
|
+
const thinkingTokenBudgetField = resolveThinkingTokenBudgetField(compat);
|
|
630
|
+
const thinkingBudget = resolveClampedThinkingBudget(model, options, params);
|
|
564
631
|
if (compat.thinkingFormat === "zai" && model.reasoning) {
|
|
565
632
|
const zaiParams = params;
|
|
566
633
|
zaiParams.thinking = options?.reasoningEffort ? { type: "enabled", clear_thinking: false } : { type: "disabled" };
|
|
@@ -588,14 +655,14 @@ function buildParams(model, context, options, compat = getCompat(model), cacheRe
|
|
|
588
655
|
};
|
|
589
656
|
}
|
|
590
657
|
else if (compat.thinkingFormat === "chat-template" && model.reasoning) {
|
|
591
|
-
const chatTemplateKwargs = buildChatTemplateValues(model, options, compat.chatTemplateKwargs);
|
|
658
|
+
const chatTemplateKwargs = buildChatTemplateValues(model, options, compat.chatTemplateKwargs, thinkingBudget);
|
|
592
659
|
if (chatTemplateKwargs) {
|
|
593
660
|
params.chat_template_kwargs = chatTemplateKwargs;
|
|
594
661
|
}
|
|
595
662
|
}
|
|
596
663
|
else if (compat.thinkingFormat === "baseten" && model.reasoning) {
|
|
597
664
|
const basetenParams = params;
|
|
598
|
-
const chatTemplateArgs = buildChatTemplateValues(model, options, compat.chatTemplateArgs);
|
|
665
|
+
const chatTemplateArgs = buildChatTemplateValues(model, options, compat.chatTemplateArgs, thinkingBudget);
|
|
599
666
|
if (chatTemplateArgs) {
|
|
600
667
|
basetenParams.chat_template_args = chatTemplateArgs;
|
|
601
668
|
}
|
|
@@ -664,25 +731,12 @@ function buildParams(model, context, options, compat = getCompat(model), cacheRe
|
|
|
664
731
|
params.reasoning_effort = offValue;
|
|
665
732
|
}
|
|
666
733
|
}
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
//
|
|
671
|
-
if (
|
|
672
|
-
|
|
673
|
-
const budgets = {
|
|
674
|
-
minimal: 1024,
|
|
675
|
-
low: 2048,
|
|
676
|
-
medium: 8192,
|
|
677
|
-
high: 16384,
|
|
678
|
-
...options.thinkingBudgets,
|
|
679
|
-
};
|
|
680
|
-
const ceiling = params.max_tokens ?? params.max_completion_tokens ?? model.maxTokens;
|
|
681
|
-
// Always leave room for the answer, otherwise the budget recreates the bug it prevents.
|
|
682
|
-
const budget = Math.min(budgets[level], Math.max(0, ceiling - MIN_ANSWER_TOKENS));
|
|
683
|
-
if (budget > 0) {
|
|
684
|
-
params.thinking_token_budget = budget;
|
|
685
|
-
}
|
|
734
|
+
// Cap reasoning with a top-level budget field. Independent of thinkingFormat: the
|
|
735
|
+
// same server can serve zai, qwen or chat-template models. Reasoning and the answer
|
|
736
|
+
// share max_tokens here, so an uncapped reasoning phase can consume the whole
|
|
737
|
+
// response and leave no answer and no tool call.
|
|
738
|
+
if (thinkingTokenBudgetField && thinkingBudget !== undefined) {
|
|
739
|
+
Object.assign(params, { [thinkingTokenBudgetField]: thinkingBudget });
|
|
686
740
|
}
|
|
687
741
|
// OpenRouter provider routing preferences
|
|
688
742
|
if (model.compat?.openRouterRouting) {
|
|
@@ -706,17 +760,31 @@ function buildParams(model, context, options, compat = getCompat(model), cacheRe
|
|
|
706
760
|
}
|
|
707
761
|
return params;
|
|
708
762
|
}
|
|
709
|
-
function
|
|
763
|
+
function resolveThinkingTokenBudgetField(compat) {
|
|
764
|
+
if (compat.thinkingTokenBudgetField)
|
|
765
|
+
return compat.thinkingTokenBudgetField;
|
|
766
|
+
if (compat.supportsThinkingTokenBudget)
|
|
767
|
+
return "thinking_token_budget";
|
|
768
|
+
return undefined;
|
|
769
|
+
}
|
|
770
|
+
function resolveClampedThinkingBudget(model, options, params) {
|
|
771
|
+
if (!options?.reasoningEffort || !model.reasoning)
|
|
772
|
+
return undefined;
|
|
773
|
+
const ceiling = params.max_tokens ?? params.max_completion_tokens ?? model.maxTokens;
|
|
774
|
+
const budget = clampThinkingBudgetToAnswerRoom(thinkingBudgetForLevel(options.reasoningEffort, options.thinkingBudgets), ceiling);
|
|
775
|
+
return budget > 0 ? budget : undefined;
|
|
776
|
+
}
|
|
777
|
+
function buildChatTemplateValues(model, options, values, thinkingBudget) {
|
|
710
778
|
const resolvedValues = {};
|
|
711
779
|
for (const [key, value] of Object.entries(values)) {
|
|
712
|
-
const resolved = resolveChatTemplateKwargValue(model, options, value);
|
|
780
|
+
const resolved = resolveChatTemplateKwargValue(model, options, value, thinkingBudget);
|
|
713
781
|
if (resolved !== undefined) {
|
|
714
782
|
resolvedValues[key] = resolved;
|
|
715
783
|
}
|
|
716
784
|
}
|
|
717
785
|
return Object.keys(resolvedValues).length > 0 ? resolvedValues : undefined;
|
|
718
786
|
}
|
|
719
|
-
function resolveChatTemplateKwargValue(model, options, value) {
|
|
787
|
+
function resolveChatTemplateKwargValue(model, options, value, thinkingBudget) {
|
|
720
788
|
if (typeof value !== "object" || value === null) {
|
|
721
789
|
return value;
|
|
722
790
|
}
|
|
@@ -727,6 +795,9 @@ function resolveChatTemplateKwargValue(model, options, value) {
|
|
|
727
795
|
if (value.$var === "thinking.enabled") {
|
|
728
796
|
return !!reasoningEffort;
|
|
729
797
|
}
|
|
798
|
+
if (value.$var === "thinking.budget") {
|
|
799
|
+
return thinkingBudget;
|
|
800
|
+
}
|
|
730
801
|
const mappedValue = reasoningEffort ? model.thinkingLevelMap?.[reasoningEffort] : model.thinkingLevelMap?.off;
|
|
731
802
|
return mappedValue === undefined ? reasoningEffort : typeof mappedValue === "string" ? mappedValue : undefined;
|
|
732
803
|
}
|
|
@@ -894,9 +965,16 @@ export function convertMessages(model, context, compat, options) {
|
|
|
894
965
|
text: sanitizeSurrogates(block.text),
|
|
895
966
|
}));
|
|
896
967
|
const assistantText = assistantTextParts.map((part) => part.text).join("");
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
968
|
+
const thinkingBlocks = msg.content.filter(isThinkingContentBlock);
|
|
969
|
+
const toolCalls = msg.content.filter(isToolCallBlock);
|
|
970
|
+
const signedReasoningDetails = thinkingBlocks
|
|
971
|
+
.map((block) => parseOpenAIReasoningDetails(block.thinkingSignature))
|
|
972
|
+
.find((details) => details !== undefined);
|
|
973
|
+
const legacyReasoningDetails = toolCalls
|
|
974
|
+
.map((toolCall) => parseLegacyEncryptedReasoningDetail(toolCall.thoughtSignature))
|
|
975
|
+
.filter((detail) => detail !== undefined);
|
|
976
|
+
const preservedReasoningDetails = signedReasoningDetails ?? (legacyReasoningDetails.length > 0 ? legacyReasoningDetails : undefined);
|
|
977
|
+
const nonEmptyThinkingBlocks = thinkingBlocks.filter((block) => block.thinking.trim().length > 0);
|
|
900
978
|
if (nonEmptyThinkingBlocks.length > 0) {
|
|
901
979
|
if (compat.requiresThinkingAsText) {
|
|
902
980
|
// Convert thinking blocks to plain text (no tags to avoid model mimicking them)
|
|
@@ -914,13 +992,16 @@ export function convertMessages(model, context, compat, options) {
|
|
|
914
992
|
if (assistantText.length > 0) {
|
|
915
993
|
assistantMsg.content = assistantText;
|
|
916
994
|
}
|
|
917
|
-
//
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
signature =
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
995
|
+
// reasoning_details is the structured alternative to a raw reasoning field.
|
|
996
|
+
if (!preservedReasoningDetails) {
|
|
997
|
+
// Use the signature from the first thinking block if available (for llama.cpp server + gpt-oss)
|
|
998
|
+
let signature = nonEmptyThinkingBlocks[0].thinkingSignature;
|
|
999
|
+
if (model.provider === "opencode-go" && signature === "reasoning") {
|
|
1000
|
+
signature = "reasoning_content";
|
|
1001
|
+
}
|
|
1002
|
+
if (signature && isOpenAICompletionsReasoningField(signature)) {
|
|
1003
|
+
assistantMsg[signature] = nonEmptyThinkingBlocks.map((block) => block.thinking).join("\n");
|
|
1004
|
+
}
|
|
924
1005
|
}
|
|
925
1006
|
}
|
|
926
1007
|
}
|
|
@@ -932,7 +1013,6 @@ export function convertMessages(model, context, compat, options) {
|
|
|
932
1013
|
// output, producing recursive nesting like [{'type':'text','text':'[{...}]'}].
|
|
933
1014
|
assistantMsg.content = assistantText;
|
|
934
1015
|
}
|
|
935
|
-
const toolCalls = msg.content.filter(isToolCallBlock);
|
|
936
1016
|
if (toolCalls.length > 0) {
|
|
937
1017
|
assistantMsg.tool_calls = toolCalls.map((tc) => {
|
|
938
1018
|
const customInputProperty = options?.grammarToolInputProperties?.get(tc.name);
|
|
@@ -955,20 +1035,9 @@ export function convertMessages(model, context, compat, options) {
|
|
|
955
1035
|
},
|
|
956
1036
|
};
|
|
957
1037
|
});
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
try {
|
|
962
|
-
return JSON.parse(tc.thoughtSignature);
|
|
963
|
-
}
|
|
964
|
-
catch {
|
|
965
|
-
return null;
|
|
966
|
-
}
|
|
967
|
-
})
|
|
968
|
-
.filter(Boolean);
|
|
969
|
-
if (reasoningDetails.length > 0) {
|
|
970
|
-
assistantMsg.reasoning_details = reasoningDetails;
|
|
971
|
-
}
|
|
1038
|
+
}
|
|
1039
|
+
if (preservedReasoningDetails) {
|
|
1040
|
+
assistantMsg.reasoning_details = preservedReasoningDetails;
|
|
972
1041
|
}
|
|
973
1042
|
if (compat.requiresReasoningContentOnAssistantMessages &&
|
|
974
1043
|
model.reasoning &&
|
|
@@ -1105,10 +1174,13 @@ function convertTools(tools, compat) {
|
|
|
1105
1174
|
}
|
|
1106
1175
|
function parseChunkUsage(rawUsage, model) {
|
|
1107
1176
|
const promptTokens = rawUsage.prompt_tokens || 0;
|
|
1108
|
-
const cacheReadTokens = rawUsage.prompt_tokens_details?.cached_tokens ?? rawUsage.prompt_cache_hit_tokens ?? 0;
|
|
1177
|
+
const cacheReadTokens = rawUsage.prompt_tokens_details?.cached_tokens ?? rawUsage.prompt_cache_hit_tokens ?? rawUsage.cached_tokens ?? 0;
|
|
1109
1178
|
const cacheWriteTokens = rawUsage.prompt_tokens_details?.cache_write_tokens || 0;
|
|
1110
1179
|
// Follow documented OpenAI/OpenRouter semantics: cached_tokens is cache-read
|
|
1111
|
-
// tokens (hits).
|
|
1180
|
+
// tokens (hits). Providers disagree on placement: OpenAI/OpenRouter use
|
|
1181
|
+
// prompt_tokens_details.cached_tokens, DeepSeek uses prompt_cache_hit_tokens,
|
|
1182
|
+
// and Kimi documents top-level usage.cached_tokens on the final usage chunk.
|
|
1183
|
+
// OpenAI does not document or emit cache_write_tokens, but
|
|
1112
1184
|
// OpenRouter-compatible providers can include it as a separate write count.
|
|
1113
1185
|
// OpenRouter's own provider/tests affirm the separate mapping:
|
|
1114
1186
|
// https://github.com/OpenRouterTeam/ai-sdk-provider/pull/409
|
|
@@ -1227,6 +1299,7 @@ function detectCompat(model) {
|
|
|
1227
1299
|
chatTemplateArgs: {},
|
|
1228
1300
|
zaiToolStream: false,
|
|
1229
1301
|
supportsThinkingTokenBudget: false,
|
|
1302
|
+
thinkingTokenBudgetField: undefined,
|
|
1230
1303
|
supportsStrictMode: !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia,
|
|
1231
1304
|
supportsOpenAIGrammarTools: false,
|
|
1232
1305
|
cacheControlFormat,
|
|
@@ -1267,6 +1340,7 @@ function getCompat(model) {
|
|
|
1267
1340
|
chatTemplateArgs: model.compat.chatTemplateArgs ?? detected.chatTemplateArgs,
|
|
1268
1341
|
zaiToolStream: model.compat.zaiToolStream ?? detected.zaiToolStream,
|
|
1269
1342
|
supportsThinkingTokenBudget: model.compat.supportsThinkingTokenBudget ?? detected.supportsThinkingTokenBudget,
|
|
1343
|
+
thinkingTokenBudgetField: model.compat.thinkingTokenBudgetField ?? detected.thinkingTokenBudgetField,
|
|
1270
1344
|
supportsStrictMode: model.compat.supportsStrictMode ?? detected.supportsStrictMode,
|
|
1271
1345
|
supportsOpenAIGrammarTools: model.compat.supportsOpenAIGrammarTools ?? detected.supportsOpenAIGrammarTools,
|
|
1272
1346
|
cacheControlFormat: model.compat.cacheControlFormat ?? detected.cacheControlFormat,
|