@bastani/pi-ai 0.9.14 → 0.9.15
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/CHANGELOG.md +72 -0
- package/NOTICE.md +4 -5
- package/README.md +6 -4
- package/dist/api/anthropic-messages.d.ts.map +1 -1
- package/dist/api/anthropic-messages.js +55 -25
- 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 +132 -8
- package/dist/api/bedrock-converse-stream.js.map +1 -1
- package/dist/api/github-copilot-headers.d.ts +22 -1
- package/dist/api/github-copilot-headers.d.ts.map +1 -1
- package/dist/api/github-copilot-headers.js +30 -0
- package/dist/api/github-copilot-headers.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 +28 -18
- 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 +15 -5
- 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 +15 -6
- 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 +142 -94
- 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 +20 -10
- 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 +23 -5
- 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 +18 -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/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.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/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/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/nvidia.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/github-copilot-env.d.ts +36 -0
- package/dist/providers/github-copilot-env.d.ts.map +1 -0
- package/dist/providers/github-copilot-env.js +94 -0
- package/dist/providers/github-copilot-env.js.map +1 -0
- 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 +43 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/retry.d.ts.map +1 -1
- package/dist/utils/retry.js +14 -0
- package/dist/utils/retry.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/dist/utils/stream-deadline.d.ts +74 -0
- package/dist/utils/stream-deadline.d.ts.map +1 -0
- package/dist/utils/stream-deadline.js +196 -0
- package/dist/utils/stream-deadline.js.map +1 -0
- package/package.json +118 -120
|
@@ -5,13 +5,15 @@ 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";
|
|
12
|
+
import { createStreamDeadline, withStreamDeadline } from "../utils/stream-deadline.js";
|
|
11
13
|
import { appendGrammarToolInputJsonDelta, createGrammarToolInputProperties, getGrammarToolInput, getJsonSchemaToolParameters, resolveGrammarConstrainedSampling, resolveJsonSchemaStrictSampling, } from "./constrained-sampling.js";
|
|
12
|
-
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
|
|
14
|
+
import { buildCopilotDynamicHeaders, hasCopilotVisionInput, preserveCopilotIntegrationHeader, } from "./github-copilot-headers.js";
|
|
13
15
|
import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.js";
|
|
14
|
-
import { buildBaseOptions,
|
|
16
|
+
import { buildBaseOptions, clampThinkingBudgetToAnswerRoom, thinkingBudgetForLevel } from "./simple-options.js";
|
|
15
17
|
import { transformMessages } from "./transform-messages.js";
|
|
16
18
|
/**
|
|
17
19
|
* Check if conversation messages contain tool calls or tool results.
|
|
@@ -79,16 +81,61 @@ function isToolCallBlock(block) {
|
|
|
79
81
|
function isImageContentBlock(block) {
|
|
80
82
|
return block.type === "image";
|
|
81
83
|
}
|
|
82
|
-
function
|
|
83
|
-
|
|
84
|
+
function isReasoningDetailObject(detail) {
|
|
85
|
+
return typeof detail === "object" && detail !== null && !Array.isArray(detail);
|
|
86
|
+
}
|
|
87
|
+
function hasValidCommonReasoningDetailFields(candidate) {
|
|
88
|
+
return ((candidate.id === undefined || candidate.id === null || typeof candidate.id === "string") &&
|
|
89
|
+
(candidate.format === undefined || typeof candidate.format === "string") &&
|
|
90
|
+
(candidate.index === undefined || typeof candidate.index === "number"));
|
|
91
|
+
}
|
|
92
|
+
function isOpenAIReasoningDetail(detail) {
|
|
93
|
+
if (!isReasoningDetailObject(detail) || !hasValidCommonReasoningDetailFields(detail)) {
|
|
84
94
|
return false;
|
|
85
95
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
switch (detail.type) {
|
|
97
|
+
case "reasoning.summary":
|
|
98
|
+
return typeof detail.summary === "string";
|
|
99
|
+
case "reasoning.encrypted":
|
|
100
|
+
return typeof detail.data === "string";
|
|
101
|
+
case "reasoning.text":
|
|
102
|
+
return (typeof detail.text === "string" &&
|
|
103
|
+
(detail.signature === undefined || detail.signature === null || typeof detail.signature === "string"));
|
|
104
|
+
default:
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function parseOpenAIReasoningDetails(signature) {
|
|
109
|
+
if (!signature)
|
|
110
|
+
return undefined;
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(signature);
|
|
113
|
+
return Array.isArray(parsed) && parsed.length > 0 && parsed.every(isOpenAIReasoningDetail) ? parsed : undefined;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function parseLegacyEncryptedReasoningDetail(signature) {
|
|
120
|
+
if (!signature)
|
|
121
|
+
return undefined;
|
|
122
|
+
try {
|
|
123
|
+
const parsed = JSON.parse(signature);
|
|
124
|
+
return isOpenAIReasoningDetail(parsed) &&
|
|
125
|
+
parsed.type === "reasoning.encrypted" &&
|
|
126
|
+
typeof parsed.id === "string" &&
|
|
127
|
+
parsed.id.length > 0 &&
|
|
128
|
+
parsed.data.length > 0
|
|
129
|
+
? parsed
|
|
130
|
+
: undefined;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const OPENAI_COMPLETIONS_REASONING_FIELDS = ["reasoning", "reasoning_content", "reasoning_text"];
|
|
137
|
+
function isOpenAICompletionsReasoningField(field) {
|
|
138
|
+
return OPENAI_COMPLETIONS_REASONING_FIELDS.includes(field);
|
|
92
139
|
}
|
|
93
140
|
function resolveCacheRetention(cacheRetention, env) {
|
|
94
141
|
if (cacheRetention) {
|
|
@@ -119,27 +166,28 @@ export const stream = (model, context, options) => {
|
|
|
119
166
|
stopReason: "pending",
|
|
120
167
|
timestamp: Date.now(),
|
|
121
168
|
};
|
|
169
|
+
const streamDeadline = createStreamDeadline(options?.streamDeadlineMs, options?.signal);
|
|
122
170
|
try {
|
|
123
171
|
const apiKey = getClientApiKey(model.provider, options?.apiKey, options?.headers);
|
|
124
172
|
const compat = getCompat(model);
|
|
125
173
|
const grammarToolInputProperties = createGrammarToolInputProperties(context.tools, compat.supportsOpenAIGrammarTools);
|
|
126
174
|
const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env);
|
|
127
175
|
const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
|
|
128
|
-
const client = createClient(model, context, apiKey, options?.headers, options?.fetch, cacheSessionId, compat);
|
|
176
|
+
const client = createClient(model, context, apiKey, options?.headers, options?.fetch, cacheSessionId, compat, options?.apiKey);
|
|
129
177
|
let params = buildParams(model, context, options, compat, cacheRetention, grammarToolInputProperties);
|
|
130
178
|
const nextParams = await options?.onPayload?.(params, model);
|
|
131
179
|
if (nextParams !== undefined) {
|
|
132
180
|
params = nextParams;
|
|
133
181
|
}
|
|
134
182
|
const requestOptions = {
|
|
135
|
-
...(
|
|
183
|
+
...(streamDeadline.signal ? { signal: streamDeadline.signal } : {}),
|
|
136
184
|
...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
|
|
137
185
|
maxRetries: 0,
|
|
138
186
|
};
|
|
139
187
|
const { data: openaiStream, response } = await retryProviderRequest(() => client.chat.completions.create(params, requestOptions).withResponse(), {
|
|
140
188
|
maxRetries: options?.maxRetries,
|
|
141
189
|
maxRetryDelayMs: options?.maxRetryDelayMs,
|
|
142
|
-
signal:
|
|
190
|
+
signal: streamDeadline.signal,
|
|
143
191
|
});
|
|
144
192
|
await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
|
|
145
193
|
stream.push({ type: "start", partial: output });
|
|
@@ -148,7 +196,6 @@ export const stream = (model, context, options) => {
|
|
|
148
196
|
let hasFinishReason = false;
|
|
149
197
|
const toolCallBlocksByIndex = new Map();
|
|
150
198
|
const toolCallBlocksById = new Map();
|
|
151
|
-
const pendingReasoningDetailsByToolCallId = new Map();
|
|
152
199
|
const blocks = output.content;
|
|
153
200
|
const getContentIndex = (block) => blocks.indexOf(block);
|
|
154
201
|
const getCustomToolCallInput = (block) => {
|
|
@@ -235,16 +282,6 @@ export const stream = (model, context, options) => {
|
|
|
235
282
|
}
|
|
236
283
|
return thinkingBlock;
|
|
237
284
|
};
|
|
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
285
|
const ensureToolCallBlock = (toolCall) => {
|
|
249
286
|
const streamIndex = typeof toolCall.index === "number" ? toolCall.index : undefined;
|
|
250
287
|
const name = toolCall.function?.name ?? toolCall.custom?.name ?? "";
|
|
@@ -300,10 +337,9 @@ export const stream = (model, context, options) => {
|
|
|
300
337
|
};
|
|
301
338
|
delete block.partialArgs;
|
|
302
339
|
}
|
|
303
|
-
applyPendingReasoningDetail(block);
|
|
304
340
|
return block;
|
|
305
341
|
};
|
|
306
|
-
for await (const chunk of openaiStream) {
|
|
342
|
+
for await (const chunk of withStreamDeadline(openaiStream, streamDeadline.deadlineMs, streamDeadline.abort)) {
|
|
307
343
|
if (!chunk || typeof chunk !== "object")
|
|
308
344
|
continue;
|
|
309
345
|
// OpenAI documents ChatCompletionChunk.id as the unique chat completion identifier,
|
|
@@ -407,16 +443,14 @@ export const stream = (model, context, options) => {
|
|
|
407
443
|
const reasoningDetails = choice.delta.reasoning_details;
|
|
408
444
|
if (Array.isArray(reasoningDetails)) {
|
|
409
445
|
for (const detail of reasoningDetails) {
|
|
410
|
-
if (
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}
|
|
419
|
-
}
|
|
446
|
+
if (!isOpenAIReasoningDetail(detail))
|
|
447
|
+
continue;
|
|
448
|
+
const block = ensureThinkingBlock("");
|
|
449
|
+
const preservedDetails = parseOpenAIReasoningDetails(block.thinkingSignature) ?? [];
|
|
450
|
+
preservedDetails.push(detail);
|
|
451
|
+
// Keep provider replay data in the existing signature slot. OpenRouter
|
|
452
|
+
// requires the complete reasoning_details sequence in its original order.
|
|
453
|
+
block.thinkingSignature = JSON.stringify(preservedDetails);
|
|
420
454
|
}
|
|
421
455
|
}
|
|
422
456
|
}
|
|
@@ -463,30 +497,35 @@ export const stream = (model, context, options) => {
|
|
|
463
497
|
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
464
498
|
stream.end();
|
|
465
499
|
}
|
|
500
|
+
finally {
|
|
501
|
+
streamDeadline.cleanup();
|
|
502
|
+
}
|
|
466
503
|
})();
|
|
467
504
|
return stream;
|
|
468
505
|
};
|
|
469
506
|
export const streamSimple = (model, context, options) => {
|
|
470
507
|
getClientApiKey(model.provider, options?.apiKey, options?.headers);
|
|
471
|
-
const base =
|
|
508
|
+
const base = {
|
|
509
|
+
...buildBaseOptions(model, context, options, options?.apiKey),
|
|
510
|
+
toolChoice: options?.toolChoice,
|
|
511
|
+
};
|
|
472
512
|
const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
|
|
473
513
|
const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
|
|
474
|
-
const toolChoice = options?.toolChoice;
|
|
475
514
|
return stream(model, context, {
|
|
476
515
|
...base,
|
|
477
516
|
reasoningEffort,
|
|
478
|
-
toolChoice,
|
|
479
517
|
thinkingBudgets: options?.thinkingBudgets,
|
|
480
518
|
});
|
|
481
519
|
};
|
|
482
|
-
function createClient(model, context, apiKey, optionsHeaders, fetch, sessionId, compat = getCompat(model)) {
|
|
483
|
-
const headers = { ...model.headers };
|
|
520
|
+
function createClient(model, context, apiKey, optionsHeaders, fetch, sessionId, compat = getCompat(model), copilotApiKey) {
|
|
521
|
+
const headers = { "User-Agent": getPiUserAgent(), ...model.headers };
|
|
484
522
|
if (model.provider === "github-copilot") {
|
|
485
523
|
const hasImages = hasCopilotVisionInput(context.messages);
|
|
486
|
-
const copilotHeaders = buildCopilotDynamicHeaders({
|
|
524
|
+
const copilotHeaders = preserveCopilotIntegrationHeader(model.headers, buildCopilotDynamicHeaders({
|
|
487
525
|
messages: context.messages,
|
|
488
526
|
hasImages,
|
|
489
|
-
|
|
527
|
+
apiKey: copilotApiKey,
|
|
528
|
+
}));
|
|
490
529
|
Object.assign(headers, copilotHeaders);
|
|
491
530
|
}
|
|
492
531
|
if (sessionId && compat.sendSessionAffinityHeaders) {
|
|
@@ -561,6 +600,8 @@ function buildParams(model, context, options, compat = getCompat(model), cacheRe
|
|
|
561
600
|
if (options?.toolChoice) {
|
|
562
601
|
params.tool_choice = options.toolChoice;
|
|
563
602
|
}
|
|
603
|
+
const thinkingTokenBudgetField = resolveThinkingTokenBudgetField(compat);
|
|
604
|
+
const thinkingBudget = resolveClampedThinkingBudget(model, options, params);
|
|
564
605
|
if (compat.thinkingFormat === "zai" && model.reasoning) {
|
|
565
606
|
const zaiParams = params;
|
|
566
607
|
zaiParams.thinking = options?.reasoningEffort ? { type: "enabled", clear_thinking: false } : { type: "disabled" };
|
|
@@ -588,14 +629,14 @@ function buildParams(model, context, options, compat = getCompat(model), cacheRe
|
|
|
588
629
|
};
|
|
589
630
|
}
|
|
590
631
|
else if (compat.thinkingFormat === "chat-template" && model.reasoning) {
|
|
591
|
-
const chatTemplateKwargs = buildChatTemplateValues(model, options, compat.chatTemplateKwargs);
|
|
632
|
+
const chatTemplateKwargs = buildChatTemplateValues(model, options, compat.chatTemplateKwargs, thinkingBudget);
|
|
592
633
|
if (chatTemplateKwargs) {
|
|
593
634
|
params.chat_template_kwargs = chatTemplateKwargs;
|
|
594
635
|
}
|
|
595
636
|
}
|
|
596
637
|
else if (compat.thinkingFormat === "baseten" && model.reasoning) {
|
|
597
638
|
const basetenParams = params;
|
|
598
|
-
const chatTemplateArgs = buildChatTemplateValues(model, options, compat.chatTemplateArgs);
|
|
639
|
+
const chatTemplateArgs = buildChatTemplateValues(model, options, compat.chatTemplateArgs, thinkingBudget);
|
|
599
640
|
if (chatTemplateArgs) {
|
|
600
641
|
basetenParams.chat_template_args = chatTemplateArgs;
|
|
601
642
|
}
|
|
@@ -664,25 +705,12 @@ function buildParams(model, context, options, compat = getCompat(model), cacheRe
|
|
|
664
705
|
params.reasoning_effort = offValue;
|
|
665
706
|
}
|
|
666
707
|
}
|
|
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
|
-
}
|
|
708
|
+
// Cap reasoning with a top-level budget field. Independent of thinkingFormat: the
|
|
709
|
+
// same server can serve zai, qwen or chat-template models. Reasoning and the answer
|
|
710
|
+
// share max_tokens here, so an uncapped reasoning phase can consume the whole
|
|
711
|
+
// response and leave no answer and no tool call.
|
|
712
|
+
if (thinkingTokenBudgetField && thinkingBudget !== undefined) {
|
|
713
|
+
Object.assign(params, { [thinkingTokenBudgetField]: thinkingBudget });
|
|
686
714
|
}
|
|
687
715
|
// OpenRouter provider routing preferences
|
|
688
716
|
if (model.compat?.openRouterRouting) {
|
|
@@ -706,17 +734,31 @@ function buildParams(model, context, options, compat = getCompat(model), cacheRe
|
|
|
706
734
|
}
|
|
707
735
|
return params;
|
|
708
736
|
}
|
|
709
|
-
function
|
|
737
|
+
function resolveThinkingTokenBudgetField(compat) {
|
|
738
|
+
if (compat.thinkingTokenBudgetField)
|
|
739
|
+
return compat.thinkingTokenBudgetField;
|
|
740
|
+
if (compat.supportsThinkingTokenBudget)
|
|
741
|
+
return "thinking_token_budget";
|
|
742
|
+
return undefined;
|
|
743
|
+
}
|
|
744
|
+
function resolveClampedThinkingBudget(model, options, params) {
|
|
745
|
+
if (!options?.reasoningEffort || !model.reasoning)
|
|
746
|
+
return undefined;
|
|
747
|
+
const ceiling = params.max_tokens ?? params.max_completion_tokens ?? model.maxTokens;
|
|
748
|
+
const budget = clampThinkingBudgetToAnswerRoom(thinkingBudgetForLevel(options.reasoningEffort, options.thinkingBudgets), ceiling);
|
|
749
|
+
return budget > 0 ? budget : undefined;
|
|
750
|
+
}
|
|
751
|
+
function buildChatTemplateValues(model, options, values, thinkingBudget) {
|
|
710
752
|
const resolvedValues = {};
|
|
711
753
|
for (const [key, value] of Object.entries(values)) {
|
|
712
|
-
const resolved = resolveChatTemplateKwargValue(model, options, value);
|
|
754
|
+
const resolved = resolveChatTemplateKwargValue(model, options, value, thinkingBudget);
|
|
713
755
|
if (resolved !== undefined) {
|
|
714
756
|
resolvedValues[key] = resolved;
|
|
715
757
|
}
|
|
716
758
|
}
|
|
717
759
|
return Object.keys(resolvedValues).length > 0 ? resolvedValues : undefined;
|
|
718
760
|
}
|
|
719
|
-
function resolveChatTemplateKwargValue(model, options, value) {
|
|
761
|
+
function resolveChatTemplateKwargValue(model, options, value, thinkingBudget) {
|
|
720
762
|
if (typeof value !== "object" || value === null) {
|
|
721
763
|
return value;
|
|
722
764
|
}
|
|
@@ -727,6 +769,9 @@ function resolveChatTemplateKwargValue(model, options, value) {
|
|
|
727
769
|
if (value.$var === "thinking.enabled") {
|
|
728
770
|
return !!reasoningEffort;
|
|
729
771
|
}
|
|
772
|
+
if (value.$var === "thinking.budget") {
|
|
773
|
+
return thinkingBudget;
|
|
774
|
+
}
|
|
730
775
|
const mappedValue = reasoningEffort ? model.thinkingLevelMap?.[reasoningEffort] : model.thinkingLevelMap?.off;
|
|
731
776
|
return mappedValue === undefined ? reasoningEffort : typeof mappedValue === "string" ? mappedValue : undefined;
|
|
732
777
|
}
|
|
@@ -894,9 +939,16 @@ export function convertMessages(model, context, compat, options) {
|
|
|
894
939
|
text: sanitizeSurrogates(block.text),
|
|
895
940
|
}));
|
|
896
941
|
const assistantText = assistantTextParts.map((part) => part.text).join("");
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
942
|
+
const thinkingBlocks = msg.content.filter(isThinkingContentBlock);
|
|
943
|
+
const toolCalls = msg.content.filter(isToolCallBlock);
|
|
944
|
+
const signedReasoningDetails = thinkingBlocks
|
|
945
|
+
.map((block) => parseOpenAIReasoningDetails(block.thinkingSignature))
|
|
946
|
+
.find((details) => details !== undefined);
|
|
947
|
+
const legacyReasoningDetails = toolCalls
|
|
948
|
+
.map((toolCall) => parseLegacyEncryptedReasoningDetail(toolCall.thoughtSignature))
|
|
949
|
+
.filter((detail) => detail !== undefined);
|
|
950
|
+
const preservedReasoningDetails = signedReasoningDetails ?? (legacyReasoningDetails.length > 0 ? legacyReasoningDetails : undefined);
|
|
951
|
+
const nonEmptyThinkingBlocks = thinkingBlocks.filter((block) => block.thinking.trim().length > 0);
|
|
900
952
|
if (nonEmptyThinkingBlocks.length > 0) {
|
|
901
953
|
if (compat.requiresThinkingAsText) {
|
|
902
954
|
// Convert thinking blocks to plain text (no tags to avoid model mimicking them)
|
|
@@ -914,13 +966,16 @@ export function convertMessages(model, context, compat, options) {
|
|
|
914
966
|
if (assistantText.length > 0) {
|
|
915
967
|
assistantMsg.content = assistantText;
|
|
916
968
|
}
|
|
917
|
-
//
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
signature =
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
969
|
+
// reasoning_details is the structured alternative to a raw reasoning field.
|
|
970
|
+
if (!preservedReasoningDetails) {
|
|
971
|
+
// Use the signature from the first thinking block if available (for llama.cpp server + gpt-oss)
|
|
972
|
+
let signature = nonEmptyThinkingBlocks[0].thinkingSignature;
|
|
973
|
+
if (model.provider === "opencode-go" && signature === "reasoning") {
|
|
974
|
+
signature = "reasoning_content";
|
|
975
|
+
}
|
|
976
|
+
if (signature && isOpenAICompletionsReasoningField(signature)) {
|
|
977
|
+
assistantMsg[signature] = nonEmptyThinkingBlocks.map((block) => block.thinking).join("\n");
|
|
978
|
+
}
|
|
924
979
|
}
|
|
925
980
|
}
|
|
926
981
|
}
|
|
@@ -932,7 +987,6 @@ export function convertMessages(model, context, compat, options) {
|
|
|
932
987
|
// output, producing recursive nesting like [{'type':'text','text':'[{...}]'}].
|
|
933
988
|
assistantMsg.content = assistantText;
|
|
934
989
|
}
|
|
935
|
-
const toolCalls = msg.content.filter(isToolCallBlock);
|
|
936
990
|
if (toolCalls.length > 0) {
|
|
937
991
|
assistantMsg.tool_calls = toolCalls.map((tc) => {
|
|
938
992
|
const customInputProperty = options?.grammarToolInputProperties?.get(tc.name);
|
|
@@ -955,20 +1009,9 @@ export function convertMessages(model, context, compat, options) {
|
|
|
955
1009
|
},
|
|
956
1010
|
};
|
|
957
1011
|
});
|
|
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
|
-
}
|
|
1012
|
+
}
|
|
1013
|
+
if (preservedReasoningDetails) {
|
|
1014
|
+
assistantMsg.reasoning_details = preservedReasoningDetails;
|
|
972
1015
|
}
|
|
973
1016
|
if (compat.requiresReasoningContentOnAssistantMessages &&
|
|
974
1017
|
model.reasoning &&
|
|
@@ -1105,10 +1148,13 @@ function convertTools(tools, compat) {
|
|
|
1105
1148
|
}
|
|
1106
1149
|
function parseChunkUsage(rawUsage, model) {
|
|
1107
1150
|
const promptTokens = rawUsage.prompt_tokens || 0;
|
|
1108
|
-
const cacheReadTokens = rawUsage.prompt_tokens_details?.cached_tokens ?? rawUsage.prompt_cache_hit_tokens ?? 0;
|
|
1151
|
+
const cacheReadTokens = rawUsage.prompt_tokens_details?.cached_tokens ?? rawUsage.prompt_cache_hit_tokens ?? rawUsage.cached_tokens ?? 0;
|
|
1109
1152
|
const cacheWriteTokens = rawUsage.prompt_tokens_details?.cache_write_tokens || 0;
|
|
1110
1153
|
// Follow documented OpenAI/OpenRouter semantics: cached_tokens is cache-read
|
|
1111
|
-
// tokens (hits).
|
|
1154
|
+
// tokens (hits). Providers disagree on placement: OpenAI/OpenRouter use
|
|
1155
|
+
// prompt_tokens_details.cached_tokens, DeepSeek uses prompt_cache_hit_tokens,
|
|
1156
|
+
// and Kimi documents top-level usage.cached_tokens on the final usage chunk.
|
|
1157
|
+
// OpenAI does not document or emit cache_write_tokens, but
|
|
1112
1158
|
// OpenRouter-compatible providers can include it as a separate write count.
|
|
1113
1159
|
// OpenRouter's own provider/tests affirm the separate mapping:
|
|
1114
1160
|
// https://github.com/OpenRouterTeam/ai-sdk-provider/pull/409
|
|
@@ -1227,6 +1273,7 @@ function detectCompat(model) {
|
|
|
1227
1273
|
chatTemplateArgs: {},
|
|
1228
1274
|
zaiToolStream: false,
|
|
1229
1275
|
supportsThinkingTokenBudget: false,
|
|
1276
|
+
thinkingTokenBudgetField: undefined,
|
|
1230
1277
|
supportsStrictMode: !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia,
|
|
1231
1278
|
supportsOpenAIGrammarTools: false,
|
|
1232
1279
|
cacheControlFormat,
|
|
@@ -1267,6 +1314,7 @@ function getCompat(model) {
|
|
|
1267
1314
|
chatTemplateArgs: model.compat.chatTemplateArgs ?? detected.chatTemplateArgs,
|
|
1268
1315
|
zaiToolStream: model.compat.zaiToolStream ?? detected.zaiToolStream,
|
|
1269
1316
|
supportsThinkingTokenBudget: model.compat.supportsThinkingTokenBudget ?? detected.supportsThinkingTokenBudget,
|
|
1317
|
+
thinkingTokenBudgetField: model.compat.thinkingTokenBudgetField ?? detected.thinkingTokenBudgetField,
|
|
1270
1318
|
supportsStrictMode: model.compat.supportsStrictMode ?? detected.supportsStrictMode,
|
|
1271
1319
|
supportsOpenAIGrammarTools: model.compat.supportsOpenAIGrammarTools ?? detected.supportsOpenAIGrammarTools,
|
|
1272
1320
|
cacheControlFormat: model.compat.cacheControlFormat ?? detected.cacheControlFormat,
|