@earendil-works/pi-ai 0.84.1 → 0.84.3

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 (127) hide show
  1. package/README.md +5 -3
  2. package/dist/api/anthropic-messages.d.ts.map +1 -1
  3. package/dist/api/anthropic-messages.js +40 -12
  4. package/dist/api/anthropic-messages.js.map +1 -1
  5. package/dist/api/azure-openai-responses.d.ts +2 -0
  6. package/dist/api/azure-openai-responses.d.ts.map +1 -1
  7. package/dist/api/azure-openai-responses.js +9 -2
  8. package/dist/api/azure-openai-responses.js.map +1 -1
  9. package/dist/api/bedrock-converse-stream.d.ts.map +1 -1
  10. package/dist/api/bedrock-converse-stream.js +147 -11
  11. package/dist/api/bedrock-converse-stream.js.map +1 -1
  12. package/dist/api/cloudflare-gateway-binding.d.ts +69 -0
  13. package/dist/api/cloudflare-gateway-binding.d.ts.map +1 -0
  14. package/dist/api/cloudflare-gateway-binding.js +159 -0
  15. package/dist/api/cloudflare-gateway-binding.js.map +1 -0
  16. package/dist/api/constrained-sampling.d.ts +3 -0
  17. package/dist/api/constrained-sampling.d.ts.map +1 -1
  18. package/dist/api/constrained-sampling.js +125 -3
  19. package/dist/api/constrained-sampling.js.map +1 -1
  20. package/dist/api/google-generative-ai.d.ts +2 -2
  21. package/dist/api/google-generative-ai.d.ts.map +1 -1
  22. package/dist/api/google-generative-ai.js +24 -16
  23. package/dist/api/google-generative-ai.js.map +1 -1
  24. package/dist/api/google-shared.d.ts +6 -3
  25. package/dist/api/google-shared.d.ts.map +1 -1
  26. package/dist/api/google-shared.js +29 -9
  27. package/dist/api/google-shared.js.map +1 -1
  28. package/dist/api/google-vertex.d.ts +2 -2
  29. package/dist/api/google-vertex.d.ts.map +1 -1
  30. package/dist/api/google-vertex.js +22 -14
  31. package/dist/api/google-vertex.js.map +1 -1
  32. package/dist/api/mistral-conversations.d.ts +1 -1
  33. package/dist/api/mistral-conversations.d.ts.map +1 -1
  34. package/dist/api/mistral-conversations.js +216 -43
  35. package/dist/api/mistral-conversations.js.map +1 -1
  36. package/dist/api/openai-codex-responses.d.ts.map +1 -1
  37. package/dist/api/openai-codex-responses.js +19 -15
  38. package/dist/api/openai-codex-responses.js.map +1 -1
  39. package/dist/api/openai-completions.d.ts +3 -2
  40. package/dist/api/openai-completions.d.ts.map +1 -1
  41. package/dist/api/openai-completions.js +133 -90
  42. package/dist/api/openai-completions.js.map +1 -1
  43. package/dist/api/openai-responses-shared.d.ts +1 -0
  44. package/dist/api/openai-responses-shared.d.ts.map +1 -1
  45. package/dist/api/openai-responses-shared.js +29 -8
  46. package/dist/api/openai-responses-shared.js.map +1 -1
  47. package/dist/api/openai-responses.d.ts.map +1 -1
  48. package/dist/api/openai-responses.js +14 -3
  49. package/dist/api/openai-responses.js.map +1 -1
  50. package/dist/api/pi-messages.d.ts.map +1 -1
  51. package/dist/api/pi-messages.js +1 -1
  52. package/dist/api/pi-messages.js.map +1 -1
  53. package/dist/api/simple-options.d.ts +4 -0
  54. package/dist/api/simple-options.d.ts.map +1 -1
  55. package/dist/api/simple-options.js +17 -10
  56. package/dist/api/simple-options.js.map +1 -1
  57. package/dist/auth/oauth/device-code.d.ts +1 -0
  58. package/dist/auth/oauth/device-code.d.ts.map +1 -1
  59. package/dist/auth/oauth/device-code.js +1 -1
  60. package/dist/auth/oauth/device-code.js.map +1 -1
  61. package/dist/auth/oauth/github-copilot.d.ts.map +1 -1
  62. package/dist/auth/oauth/github-copilot.js +99 -32
  63. package/dist/auth/oauth/github-copilot.js.map +1 -1
  64. package/dist/auth/oauth/kimi-coding.d.ts.map +1 -1
  65. package/dist/auth/oauth/kimi-coding.js +1 -14
  66. package/dist/auth/oauth/kimi-coding.js.map +1 -1
  67. package/dist/image-models.generated.d.ts +45 -0
  68. package/dist/image-models.generated.d.ts.map +1 -1
  69. package/dist/image-models.generated.js +45 -0
  70. package/dist/image-models.generated.js.map +1 -1
  71. package/dist/index.d.ts +1 -1
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js.map +1 -1
  74. package/dist/providers/data/.manifest.json +1 -1
  75. package/dist/providers/data/amazon-bedrock.json +1 -1
  76. package/dist/providers/data/anthropic.json +1 -1
  77. package/dist/providers/data/baseten.json +1 -1
  78. package/dist/providers/data/cerebras.json +1 -1
  79. package/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  80. package/dist/providers/data/cloudflare-workers-ai.json +1 -1
  81. package/dist/providers/data/deepseek.json +1 -1
  82. package/dist/providers/data/fireworks.json +1 -1
  83. package/dist/providers/data/github-copilot.json +1 -1
  84. package/dist/providers/data/google-vertex.json +1 -1
  85. package/dist/providers/data/google.json +1 -1
  86. package/dist/providers/data/huggingface.json +1 -1
  87. package/dist/providers/data/kimi-coding.json +1 -1
  88. package/dist/providers/data/nvidia.json +1 -1
  89. package/dist/providers/data/openai-codex.json +1 -1
  90. package/dist/providers/data/openai.json +1 -1
  91. package/dist/providers/data/opencode-go.json +1 -1
  92. package/dist/providers/data/opencode.json +1 -1
  93. package/dist/providers/data/openrouter.json +1 -1
  94. package/dist/providers/data/qwen-token-plan-cn.json +1 -1
  95. package/dist/providers/data/qwen-token-plan-individual.json +1 -1
  96. package/dist/providers/data/qwen-token-plan.json +1 -1
  97. package/dist/providers/data/together.json +1 -1
  98. package/dist/providers/data/vercel-ai-gateway.json +1 -1
  99. package/dist/providers/data/xai.json +1 -1
  100. package/dist/providers/data/xiaomi-token-plan-ams.json +1 -1
  101. package/dist/providers/data/xiaomi-token-plan-cn.json +1 -1
  102. package/dist/providers/data/xiaomi-token-plan-sgp.json +1 -1
  103. package/dist/providers/data/xiaomi.json +1 -1
  104. package/dist/providers/data/zai-coding-cn.json +1 -1
  105. package/dist/providers/data/zai.json +1 -1
  106. package/dist/providers/xai.d.ts +1 -1
  107. package/dist/providers/xai.d.ts.map +1 -1
  108. package/dist/providers/xai.js +1 -5
  109. package/dist/providers/xai.js.map +1 -1
  110. package/dist/types.d.ts +38 -4
  111. package/dist/types.d.ts.map +1 -1
  112. package/dist/types.js.map +1 -1
  113. package/dist/utils/pi-user-agent.d.ts +2 -0
  114. package/dist/utils/pi-user-agent.d.ts.map +1 -0
  115. package/dist/utils/pi-user-agent.js +12 -0
  116. package/dist/utils/pi-user-agent.js.map +1 -0
  117. package/dist/utils/retry.d.ts.map +1 -1
  118. package/dist/utils/retry.js +1 -0
  119. package/dist/utils/retry.js.map +1 -1
  120. package/dist/utils/sleep.d.ts +2 -0
  121. package/dist/utils/sleep.d.ts.map +1 -0
  122. package/dist/utils/sleep.js +15 -0
  123. package/dist/utils/sleep.js.map +1 -0
  124. package/dist/utils/validation.d.ts.map +1 -1
  125. package/dist/utils/validation.js +34 -0
  126. package/dist/utils/validation.js.map +1 -1
  127. package/package.json +4 -6
@@ -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
- import { appendGrammarToolInputJsonDelta, createGrammarToolInputProperties, getGrammarToolInput, resolveGrammarConstrainedSampling, resolveJsonSchemaStrictSampling, } from "./constrained-sampling.js";
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, clampReasoning, MIN_ANSWER_TOKENS } from "./simple-options.js";
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,61 @@ function isToolCallBlock(block) {
79
80
  function isImageContentBlock(block) {
80
81
  return block.type === "image";
81
82
  }
82
- function isEncryptedReasoningDetail(detail) {
83
- if (typeof detail !== "object" || detail === null) {
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
- 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);
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
+ const OPENAI_COMPLETIONS_REASONING_FIELDS = ["reasoning", "reasoning_content", "reasoning_text"];
136
+ function isOpenAICompletionsReasoningField(field) {
137
+ return OPENAI_COMPLETIONS_REASONING_FIELDS.includes(field);
92
138
  }
93
139
  function resolveCacheRetention(cacheRetention, env) {
94
140
  if (cacheRetention) {
@@ -148,7 +194,6 @@ export const stream = (model, context, options) => {
148
194
  let hasFinishReason = false;
149
195
  const toolCallBlocksByIndex = new Map();
150
196
  const toolCallBlocksById = new Map();
151
- const pendingReasoningDetailsByToolCallId = new Map();
152
197
  const blocks = output.content;
153
198
  const getContentIndex = (block) => blocks.indexOf(block);
154
199
  const getCustomToolCallInput = (block) => {
@@ -235,16 +280,6 @@ export const stream = (model, context, options) => {
235
280
  }
236
281
  return thinkingBlock;
237
282
  };
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
283
  const ensureToolCallBlock = (toolCall) => {
249
284
  const streamIndex = typeof toolCall.index === "number" ? toolCall.index : undefined;
250
285
  const name = toolCall.function?.name ?? toolCall.custom?.name ?? "";
@@ -300,7 +335,6 @@ export const stream = (model, context, options) => {
300
335
  };
301
336
  delete block.partialArgs;
302
337
  }
303
- applyPendingReasoningDetail(block);
304
338
  return block;
305
339
  };
306
340
  for await (const chunk of openaiStream) {
@@ -407,16 +441,14 @@ export const stream = (model, context, options) => {
407
441
  const reasoningDetails = choice.delta.reasoning_details;
408
442
  if (Array.isArray(reasoningDetails)) {
409
443
  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
- }
444
+ if (!isOpenAIReasoningDetail(detail))
445
+ continue;
446
+ const block = ensureThinkingBlock("");
447
+ const preservedDetails = parseOpenAIReasoningDetails(block.thinkingSignature) ?? [];
448
+ preservedDetails.push(detail);
449
+ // Keep provider replay data in the existing signature slot. OpenRouter
450
+ // requires the complete reasoning_details sequence in its original order.
451
+ block.thinkingSignature = JSON.stringify(preservedDetails);
420
452
  }
421
453
  }
422
454
  }
@@ -468,19 +500,20 @@ export const stream = (model, context, options) => {
468
500
  };
469
501
  export const streamSimple = (model, context, options) => {
470
502
  getClientApiKey(model.provider, options?.apiKey, options?.headers);
471
- const base = buildBaseOptions(model, context, options, options?.apiKey);
503
+ const base = {
504
+ ...buildBaseOptions(model, context, options, options?.apiKey),
505
+ toolChoice: options?.toolChoice,
506
+ };
472
507
  const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
473
508
  const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
474
- const toolChoice = options?.toolChoice;
475
509
  return stream(model, context, {
476
510
  ...base,
477
511
  reasoningEffort,
478
- toolChoice,
479
512
  thinkingBudgets: options?.thinkingBudgets,
480
513
  });
481
514
  };
482
515
  function createClient(model, context, apiKey, optionsHeaders, fetch, sessionId, compat = getCompat(model)) {
483
- const headers = { ...model.headers };
516
+ const headers = { "User-Agent": getPiUserAgent(), ...model.headers };
484
517
  if (model.provider === "github-copilot") {
485
518
  const hasImages = hasCopilotVisionInput(context.messages);
486
519
  const copilotHeaders = buildCopilotDynamicHeaders({
@@ -561,6 +594,8 @@ function buildParams(model, context, options, compat = getCompat(model), cacheRe
561
594
  if (options?.toolChoice) {
562
595
  params.tool_choice = options.toolChoice;
563
596
  }
597
+ const thinkingTokenBudgetField = resolveThinkingTokenBudgetField(compat);
598
+ const thinkingBudget = resolveClampedThinkingBudget(model, options, params);
564
599
  if (compat.thinkingFormat === "zai" && model.reasoning) {
565
600
  const zaiParams = params;
566
601
  zaiParams.thinking = options?.reasoningEffort ? { type: "enabled", clear_thinking: false } : { type: "disabled" };
@@ -588,14 +623,14 @@ function buildParams(model, context, options, compat = getCompat(model), cacheRe
588
623
  };
589
624
  }
590
625
  else if (compat.thinkingFormat === "chat-template" && model.reasoning) {
591
- const chatTemplateKwargs = buildChatTemplateValues(model, options, compat.chatTemplateKwargs);
626
+ const chatTemplateKwargs = buildChatTemplateValues(model, options, compat.chatTemplateKwargs, thinkingBudget);
592
627
  if (chatTemplateKwargs) {
593
628
  params.chat_template_kwargs = chatTemplateKwargs;
594
629
  }
595
630
  }
596
631
  else if (compat.thinkingFormat === "baseten" && model.reasoning) {
597
632
  const basetenParams = params;
598
- const chatTemplateArgs = buildChatTemplateValues(model, options, compat.chatTemplateArgs);
633
+ const chatTemplateArgs = buildChatTemplateValues(model, options, compat.chatTemplateArgs, thinkingBudget);
599
634
  if (chatTemplateArgs) {
600
635
  basetenParams.chat_template_args = chatTemplateArgs;
601
636
  }
@@ -664,25 +699,12 @@ function buildParams(model, context, options, compat = getCompat(model), cacheRe
664
699
  params.reasoning_effort = offValue;
665
700
  }
666
701
  }
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
- }
702
+ // Cap reasoning with a top-level budget field. Independent of thinkingFormat: the
703
+ // same server can serve zai, qwen or chat-template models. Reasoning and the answer
704
+ // share max_tokens here, so an uncapped reasoning phase can consume the whole
705
+ // response and leave no answer and no tool call.
706
+ if (thinkingTokenBudgetField && thinkingBudget !== undefined) {
707
+ Object.assign(params, { [thinkingTokenBudgetField]: thinkingBudget });
686
708
  }
687
709
  // OpenRouter provider routing preferences
688
710
  if (model.compat?.openRouterRouting) {
@@ -706,17 +728,31 @@ function buildParams(model, context, options, compat = getCompat(model), cacheRe
706
728
  }
707
729
  return params;
708
730
  }
709
- function buildChatTemplateValues(model, options, values) {
731
+ function resolveThinkingTokenBudgetField(compat) {
732
+ if (compat.thinkingTokenBudgetField)
733
+ return compat.thinkingTokenBudgetField;
734
+ if (compat.supportsThinkingTokenBudget)
735
+ return "thinking_token_budget";
736
+ return undefined;
737
+ }
738
+ function resolveClampedThinkingBudget(model, options, params) {
739
+ if (!options?.reasoningEffort || !model.reasoning)
740
+ return undefined;
741
+ const ceiling = params.max_tokens ?? params.max_completion_tokens ?? model.maxTokens;
742
+ const budget = clampThinkingBudgetToAnswerRoom(thinkingBudgetForLevel(options.reasoningEffort, options.thinkingBudgets), ceiling);
743
+ return budget > 0 ? budget : undefined;
744
+ }
745
+ function buildChatTemplateValues(model, options, values, thinkingBudget) {
710
746
  const resolvedValues = {};
711
747
  for (const [key, value] of Object.entries(values)) {
712
- const resolved = resolveChatTemplateKwargValue(model, options, value);
748
+ const resolved = resolveChatTemplateKwargValue(model, options, value, thinkingBudget);
713
749
  if (resolved !== undefined) {
714
750
  resolvedValues[key] = resolved;
715
751
  }
716
752
  }
717
753
  return Object.keys(resolvedValues).length > 0 ? resolvedValues : undefined;
718
754
  }
719
- function resolveChatTemplateKwargValue(model, options, value) {
755
+ function resolveChatTemplateKwargValue(model, options, value, thinkingBudget) {
720
756
  if (typeof value !== "object" || value === null) {
721
757
  return value;
722
758
  }
@@ -727,6 +763,9 @@ function resolveChatTemplateKwargValue(model, options, value) {
727
763
  if (value.$var === "thinking.enabled") {
728
764
  return !!reasoningEffort;
729
765
  }
766
+ if (value.$var === "thinking.budget") {
767
+ return thinkingBudget;
768
+ }
730
769
  const mappedValue = reasoningEffort ? model.thinkingLevelMap?.[reasoningEffort] : model.thinkingLevelMap?.off;
731
770
  return mappedValue === undefined ? reasoningEffort : typeof mappedValue === "string" ? mappedValue : undefined;
732
771
  }
@@ -894,9 +933,16 @@ export function convertMessages(model, context, compat, options) {
894
933
  text: sanitizeSurrogates(block.text),
895
934
  }));
896
935
  const assistantText = assistantTextParts.map((part) => part.text).join("");
897
- const nonEmptyThinkingBlocks = msg.content
898
- .filter(isThinkingContentBlock)
899
- .filter((block) => block.thinking.trim().length > 0);
936
+ const thinkingBlocks = msg.content.filter(isThinkingContentBlock);
937
+ const toolCalls = msg.content.filter(isToolCallBlock);
938
+ const signedReasoningDetails = thinkingBlocks
939
+ .map((block) => parseOpenAIReasoningDetails(block.thinkingSignature))
940
+ .find((details) => details !== undefined);
941
+ const legacyReasoningDetails = toolCalls
942
+ .map((toolCall) => parseLegacyEncryptedReasoningDetail(toolCall.thoughtSignature))
943
+ .filter((detail) => detail !== undefined);
944
+ const preservedReasoningDetails = signedReasoningDetails ?? (legacyReasoningDetails.length > 0 ? legacyReasoningDetails : undefined);
945
+ const nonEmptyThinkingBlocks = thinkingBlocks.filter((block) => block.thinking.trim().length > 0);
900
946
  if (nonEmptyThinkingBlocks.length > 0) {
901
947
  if (compat.requiresThinkingAsText) {
902
948
  // Convert thinking blocks to plain text (no tags to avoid model mimicking them)
@@ -914,13 +960,16 @@ export function convertMessages(model, context, compat, options) {
914
960
  if (assistantText.length > 0) {
915
961
  assistantMsg.content = assistantText;
916
962
  }
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");
963
+ // reasoning_details is the structured alternative to a raw reasoning field.
964
+ if (!preservedReasoningDetails) {
965
+ // Use the signature from the first thinking block if available (for llama.cpp server + gpt-oss)
966
+ let signature = nonEmptyThinkingBlocks[0].thinkingSignature;
967
+ if (model.provider === "opencode-go" && signature === "reasoning") {
968
+ signature = "reasoning_content";
969
+ }
970
+ if (signature && isOpenAICompletionsReasoningField(signature)) {
971
+ assistantMsg[signature] = nonEmptyThinkingBlocks.map((block) => block.thinking).join("\n");
972
+ }
924
973
  }
925
974
  }
926
975
  }
@@ -932,7 +981,6 @@ export function convertMessages(model, context, compat, options) {
932
981
  // output, producing recursive nesting like [{'type':'text','text':'[{...}]'}].
933
982
  assistantMsg.content = assistantText;
934
983
  }
935
- const toolCalls = msg.content.filter(isToolCallBlock);
936
984
  if (toolCalls.length > 0) {
937
985
  assistantMsg.tool_calls = toolCalls.map((tc) => {
938
986
  const customInputProperty = options?.grammarToolInputProperties?.get(tc.name);
@@ -955,20 +1003,9 @@ export function convertMessages(model, context, compat, options) {
955
1003
  },
956
1004
  };
957
1005
  });
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
- }
1006
+ }
1007
+ if (preservedReasoningDetails) {
1008
+ assistantMsg.reasoning_details = preservedReasoningDetails;
972
1009
  }
973
1010
  if (compat.requiresReasoningContentOnAssistantMessages &&
974
1011
  model.reasoning &&
@@ -1096,7 +1133,7 @@ function convertTools(tools, compat) {
1096
1133
  function: {
1097
1134
  name: tool.name,
1098
1135
  description: tool.description,
1099
- parameters: tool.parameters, // TypeBox already generates JSON Schema
1136
+ parameters: getJsonSchemaToolParameters(tool, strict),
1100
1137
  // Only include strict if provider supports it. Some reject unknown fields.
1101
1138
  ...(compat.supportsStrictMode !== false && { strict: strict ?? false }),
1102
1139
  },
@@ -1105,10 +1142,13 @@ function convertTools(tools, compat) {
1105
1142
  }
1106
1143
  function parseChunkUsage(rawUsage, model) {
1107
1144
  const promptTokens = rawUsage.prompt_tokens || 0;
1108
- const cacheReadTokens = rawUsage.prompt_tokens_details?.cached_tokens ?? rawUsage.prompt_cache_hit_tokens ?? 0;
1145
+ const cacheReadTokens = rawUsage.prompt_tokens_details?.cached_tokens ?? rawUsage.prompt_cache_hit_tokens ?? rawUsage.cached_tokens ?? 0;
1109
1146
  const cacheWriteTokens = rawUsage.prompt_tokens_details?.cache_write_tokens || 0;
1110
1147
  // Follow documented OpenAI/OpenRouter semantics: cached_tokens is cache-read
1111
- // tokens (hits). OpenAI does not document or emit cache_write_tokens, but
1148
+ // tokens (hits). Providers disagree on placement: OpenAI/OpenRouter use
1149
+ // prompt_tokens_details.cached_tokens, DeepSeek uses prompt_cache_hit_tokens,
1150
+ // and Kimi documents top-level usage.cached_tokens on the final usage chunk.
1151
+ // OpenAI does not document or emit cache_write_tokens, but
1112
1152
  // OpenRouter-compatible providers can include it as a separate write count.
1113
1153
  // OpenRouter's own provider/tests affirm the separate mapping:
1114
1154
  // https://github.com/OpenRouterTeam/ai-sdk-provider/pull/409
@@ -1172,6 +1212,7 @@ function detectCompat(model) {
1172
1212
  const isCloudflareAiGateway = provider === "cloudflare-ai-gateway" || baseUrl.includes("gateway.ai.cloudflare.com");
1173
1213
  const isNvidia = provider === "nvidia" || baseUrl.includes("integrate.api.nvidia.com");
1174
1214
  const isAntLing = provider === "ant-ling" || baseUrl.includes("api.ant-ling.com");
1215
+ const isDeepSeek = provider === "deepseek" || baseUrl.toLowerCase().includes("deepseek.com");
1175
1216
  const isNonStandard = isNvidia ||
1176
1217
  provider === "cerebras" ||
1177
1218
  baseUrl.includes("cerebras.ai") ||
@@ -1179,7 +1220,7 @@ function detectCompat(model) {
1179
1220
  baseUrl.includes("api.x.ai") ||
1180
1221
  isTogether ||
1181
1222
  baseUrl.includes("chutes.ai") ||
1182
- baseUrl.includes("deepseek.com") ||
1223
+ isDeepSeek ||
1183
1224
  isZai ||
1184
1225
  isMoonshot ||
1185
1226
  provider === "opencode" ||
@@ -1188,6 +1229,7 @@ function detectCompat(model) {
1188
1229
  isCloudflareAiGateway ||
1189
1230
  isAntLing;
1190
1231
  const useMaxTokens = baseUrl.includes("chutes.ai") ||
1232
+ isDeepSeek ||
1191
1233
  isMoonshot ||
1192
1234
  isCloudflareAiGateway ||
1193
1235
  isTogether ||
@@ -1195,7 +1237,6 @@ function detectCompat(model) {
1195
1237
  isAntLing ||
1196
1238
  isZai;
1197
1239
  const isGrok = provider === "xai" || baseUrl.includes("api.x.ai");
1198
- const isDeepSeek = provider === "deepseek" || baseUrl.includes("deepseek.com");
1199
1240
  const isOpenRouterDeveloperRoleModel = isOpenRouter && (model.id.startsWith("anthropic/") || model.id.startsWith("openai/"));
1200
1241
  const cacheControlFormat = provider === "openrouter" && model.id.startsWith("anthropic/") ? "anthropic" : undefined;
1201
1242
  return {
@@ -1226,6 +1267,7 @@ function detectCompat(model) {
1226
1267
  chatTemplateArgs: {},
1227
1268
  zaiToolStream: false,
1228
1269
  supportsThinkingTokenBudget: false,
1270
+ thinkingTokenBudgetField: undefined,
1229
1271
  supportsStrictMode: !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia,
1230
1272
  supportsOpenAIGrammarTools: false,
1231
1273
  cacheControlFormat,
@@ -1266,6 +1308,7 @@ function getCompat(model) {
1266
1308
  chatTemplateArgs: model.compat.chatTemplateArgs ?? detected.chatTemplateArgs,
1267
1309
  zaiToolStream: model.compat.zaiToolStream ?? detected.zaiToolStream,
1268
1310
  supportsThinkingTokenBudget: model.compat.supportsThinkingTokenBudget ?? detected.supportsThinkingTokenBudget,
1311
+ thinkingTokenBudgetField: model.compat.thinkingTokenBudgetField ?? detected.thinkingTokenBudgetField,
1269
1312
  supportsStrictMode: model.compat.supportsStrictMode ?? detected.supportsStrictMode,
1270
1313
  supportsOpenAIGrammarTools: model.compat.supportsOpenAIGrammarTools ?? detected.supportsOpenAIGrammarTools,
1271
1314
  cacheControlFormat: model.compat.cacheControlFormat ?? detected.cacheControlFormat,