@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.
Files changed (113) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/NOTICE.md +4 -5
  3. package/README.md +6 -4
  4. package/dist/api/anthropic-messages.d.ts.map +1 -1
  5. package/dist/api/anthropic-messages.js +55 -25
  6. package/dist/api/anthropic-messages.js.map +1 -1
  7. package/dist/api/azure-openai-responses.d.ts +2 -0
  8. package/dist/api/azure-openai-responses.d.ts.map +1 -1
  9. package/dist/api/azure-openai-responses.js +9 -2
  10. package/dist/api/azure-openai-responses.js.map +1 -1
  11. package/dist/api/bedrock-converse-stream.d.ts.map +1 -1
  12. package/dist/api/bedrock-converse-stream.js +132 -8
  13. package/dist/api/bedrock-converse-stream.js.map +1 -1
  14. package/dist/api/github-copilot-headers.d.ts +22 -1
  15. package/dist/api/github-copilot-headers.d.ts.map +1 -1
  16. package/dist/api/github-copilot-headers.js +30 -0
  17. package/dist/api/github-copilot-headers.js.map +1 -1
  18. package/dist/api/google-generative-ai.d.ts +2 -2
  19. package/dist/api/google-generative-ai.d.ts.map +1 -1
  20. package/dist/api/google-generative-ai.js +28 -18
  21. package/dist/api/google-generative-ai.js.map +1 -1
  22. package/dist/api/google-shared.d.ts +5 -2
  23. package/dist/api/google-shared.d.ts.map +1 -1
  24. package/dist/api/google-shared.js +16 -0
  25. package/dist/api/google-shared.js.map +1 -1
  26. package/dist/api/google-vertex.d.ts +2 -2
  27. package/dist/api/google-vertex.d.ts.map +1 -1
  28. package/dist/api/google-vertex.js +15 -11
  29. package/dist/api/google-vertex.js.map +1 -1
  30. package/dist/api/mistral-conversations.d.ts.map +1 -1
  31. package/dist/api/mistral-conversations.js +15 -5
  32. package/dist/api/mistral-conversations.js.map +1 -1
  33. package/dist/api/openai-codex-responses.d.ts.map +1 -1
  34. package/dist/api/openai-codex-responses.js +15 -6
  35. package/dist/api/openai-codex-responses.js.map +1 -1
  36. package/dist/api/openai-completions.d.ts +3 -2
  37. package/dist/api/openai-completions.d.ts.map +1 -1
  38. package/dist/api/openai-completions.js +142 -94
  39. package/dist/api/openai-completions.js.map +1 -1
  40. package/dist/api/openai-responses.d.ts.map +1 -1
  41. package/dist/api/openai-responses.js +20 -10
  42. package/dist/api/openai-responses.js.map +1 -1
  43. package/dist/api/pi-messages.d.ts.map +1 -1
  44. package/dist/api/pi-messages.js +23 -5
  45. package/dist/api/pi-messages.js.map +1 -1
  46. package/dist/api/simple-options.d.ts +4 -0
  47. package/dist/api/simple-options.d.ts.map +1 -1
  48. package/dist/api/simple-options.js +18 -10
  49. package/dist/api/simple-options.js.map +1 -1
  50. package/dist/auth/oauth/device-code.d.ts +1 -0
  51. package/dist/auth/oauth/device-code.d.ts.map +1 -1
  52. package/dist/auth/oauth/device-code.js +1 -1
  53. package/dist/auth/oauth/device-code.js.map +1 -1
  54. package/dist/auth/oauth/github-copilot.d.ts.map +1 -1
  55. package/dist/auth/oauth/github-copilot.js +98 -34
  56. package/dist/auth/oauth/github-copilot.js.map +1 -1
  57. package/dist/auth/oauth/kimi-coding.d.ts.map +1 -1
  58. package/dist/auth/oauth/kimi-coding.js +1 -14
  59. package/dist/auth/oauth/kimi-coding.js.map +1 -1
  60. package/dist/index.d.ts +2 -1
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +1 -0
  63. package/dist/index.js.map +1 -1
  64. package/dist/providers/data/.manifest.json +1 -1
  65. package/dist/providers/data/amazon-bedrock.json +1 -1
  66. package/dist/providers/data/anthropic.json +1 -1
  67. package/dist/providers/data/baseten.json +1 -1
  68. package/dist/providers/data/cerebras.json +1 -1
  69. package/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  70. package/dist/providers/data/cloudflare-workers-ai.json +1 -1
  71. package/dist/providers/data/github-copilot.json +1 -1
  72. package/dist/providers/data/google-vertex.json +1 -1
  73. package/dist/providers/data/google.json +1 -1
  74. package/dist/providers/data/huggingface.json +1 -1
  75. package/dist/providers/data/nvidia.json +1 -1
  76. package/dist/providers/data/opencode-go.json +1 -1
  77. package/dist/providers/data/opencode.json +1 -1
  78. package/dist/providers/data/openrouter.json +1 -1
  79. package/dist/providers/data/qwen-token-plan-cn.json +1 -1
  80. package/dist/providers/data/qwen-token-plan-individual.json +1 -1
  81. package/dist/providers/data/qwen-token-plan.json +1 -1
  82. package/dist/providers/data/together.json +1 -1
  83. package/dist/providers/data/vercel-ai-gateway.json +1 -1
  84. package/dist/providers/data/xai.json +1 -1
  85. package/dist/providers/data/xiaomi-token-plan-ams.json +1 -1
  86. package/dist/providers/data/xiaomi-token-plan-cn.json +1 -1
  87. package/dist/providers/data/xiaomi-token-plan-sgp.json +1 -1
  88. package/dist/providers/data/xiaomi.json +1 -1
  89. package/dist/providers/data/zai-coding-cn.json +1 -1
  90. package/dist/providers/data/zai.json +1 -1
  91. package/dist/providers/github-copilot-env.d.ts +36 -0
  92. package/dist/providers/github-copilot-env.d.ts.map +1 -0
  93. package/dist/providers/github-copilot-env.js +94 -0
  94. package/dist/providers/github-copilot-env.js.map +1 -0
  95. package/dist/providers/xai.d.ts +1 -1
  96. package/dist/providers/xai.d.ts.map +1 -1
  97. package/dist/providers/xai.js +1 -5
  98. package/dist/providers/xai.js.map +1 -1
  99. package/dist/types.d.ts +43 -5
  100. package/dist/types.d.ts.map +1 -1
  101. package/dist/types.js.map +1 -1
  102. package/dist/utils/retry.d.ts.map +1 -1
  103. package/dist/utils/retry.js +14 -0
  104. package/dist/utils/retry.js.map +1 -1
  105. package/dist/utils/sleep.d.ts +2 -0
  106. package/dist/utils/sleep.d.ts.map +1 -0
  107. package/dist/utils/sleep.js +15 -0
  108. package/dist/utils/sleep.js.map +1 -0
  109. package/dist/utils/stream-deadline.d.ts +74 -0
  110. package/dist/utils/stream-deadline.d.ts.map +1 -0
  111. package/dist/utils/stream-deadline.js +196 -0
  112. package/dist/utils/stream-deadline.js.map +1 -0
  113. 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, clampReasoning, MIN_ANSWER_TOKENS } from "./simple-options.js";
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 isEncryptedReasoningDetail(detail) {
83
- if (typeof detail !== "object" || detail === null) {
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
- const candidate = detail;
87
- return (candidate.type === "reasoning.encrypted" &&
88
- typeof candidate.id === "string" &&
89
- candidate.id.length > 0 &&
90
- typeof candidate.data === "string" &&
91
- candidate.data.length > 0);
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
- ...(options?.signal ? { signal: options.signal } : {}),
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: options?.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 (isEncryptedReasoningDetail(detail)) {
411
- const serializedDetail = JSON.stringify(detail);
412
- const matchingToolCall = toolCallBlocksById.get(detail.id);
413
- if (matchingToolCall) {
414
- matchingToolCall.thoughtSignature = serializedDetail;
415
- }
416
- else {
417
- pendingReasoningDetailsByToolCallId.set(detail.id, serializedDetail);
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 = buildBaseOptions(model, context, options, options?.apiKey);
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
- // vLLM caps reasoning with a top-level thinking_token_budget. Independent of
668
- // thinkingFormat: the same server can serve zai, qwen or chat-template models.
669
- // Reasoning and the answer share max_tokens here, so an uncapped reasoning
670
- // phase can consume the whole response and leave no answer and no tool call.
671
- if (compat.supportsThinkingTokenBudget && options?.reasoningEffort && model.reasoning) {
672
- const level = clampReasoning(options.reasoningEffort);
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 buildChatTemplateValues(model, options, values) {
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 nonEmptyThinkingBlocks = msg.content
898
- .filter(isThinkingContentBlock)
899
- .filter((block) => block.thinking.trim().length > 0);
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
- // Use the signature from the first thinking block if available (for llama.cpp server + gpt-oss)
918
- let signature = nonEmptyThinkingBlocks[0].thinkingSignature;
919
- if (model.provider === "opencode-go" && signature === "reasoning") {
920
- signature = "reasoning_content";
921
- }
922
- if (signature && signature.length > 0) {
923
- assistantMsg[signature] = nonEmptyThinkingBlocks.map((block) => block.thinking).join("\n");
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
- const reasoningDetails = toolCalls
959
- .filter((tc) => tc.thoughtSignature)
960
- .map((tc) => {
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). OpenAI does not document or emit cache_write_tokens, but
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,