@earendil-works/pi-ai 0.85.1 → 0.86.1

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 (177) hide show
  1. package/README.md +50 -6
  2. package/dist/api/anthropic-messages.d.ts.map +1 -1
  3. package/dist/api/anthropic-messages.js +130 -86
  4. package/dist/api/anthropic-messages.js.map +1 -1
  5. package/dist/api/azure-openai-responses.d.ts.map +1 -1
  6. package/dist/api/azure-openai-responses.js +17 -5
  7. package/dist/api/azure-openai-responses.js.map +1 -1
  8. package/dist/api/bedrock-converse-stream.d.ts.map +1 -1
  9. package/dist/api/bedrock-converse-stream.js +13 -12
  10. package/dist/api/bedrock-converse-stream.js.map +1 -1
  11. package/dist/api/google-generative-ai.d.ts.map +1 -1
  12. package/dist/api/google-generative-ai.js +21 -73
  13. package/dist/api/google-generative-ai.js.map +1 -1
  14. package/dist/api/google-shared.d.ts +13 -4
  15. package/dist/api/google-shared.d.ts.map +1 -1
  16. package/dist/api/google-shared.js +55 -4
  17. package/dist/api/google-shared.js.map +1 -1
  18. package/dist/api/google-vertex.d.ts.map +1 -1
  19. package/dist/api/google-vertex.js +25 -65
  20. package/dist/api/google-vertex.js.map +1 -1
  21. package/dist/api/mistral-conversations.d.ts.map +1 -1
  22. package/dist/api/mistral-conversations.js +19 -12
  23. package/dist/api/mistral-conversations.js.map +1 -1
  24. package/dist/api/openai-codex-responses.d.ts.map +1 -1
  25. package/dist/api/openai-codex-responses.js +26 -23
  26. package/dist/api/openai-codex-responses.js.map +1 -1
  27. package/dist/api/openai-completions.d.ts +6 -4
  28. package/dist/api/openai-completions.d.ts.map +1 -1
  29. package/dist/api/openai-completions.js +40 -58
  30. package/dist/api/openai-completions.js.map +1 -1
  31. package/dist/api/openai-responses-shared.d.ts +7 -5
  32. package/dist/api/openai-responses-shared.d.ts.map +1 -1
  33. package/dist/api/openai-responses-shared.js +56 -56
  34. package/dist/api/openai-responses-shared.js.map +1 -1
  35. package/dist/api/openai-responses.d.ts.map +1 -1
  36. package/dist/api/openai-responses.js +14 -19
  37. package/dist/api/openai-responses.js.map +1 -1
  38. package/dist/api/pi-messages.d.ts +3 -3
  39. package/dist/api/pi-messages.d.ts.map +1 -1
  40. package/dist/api/pi-messages.js +2 -2
  41. package/dist/api/pi-messages.js.map +1 -1
  42. package/dist/api/simple-options.d.ts +3 -3
  43. package/dist/api/simple-options.d.ts.map +1 -1
  44. package/dist/api/simple-options.js.map +1 -1
  45. package/dist/api/transform-messages.d.ts.map +1 -1
  46. package/dist/api/transform-messages.js +21 -7
  47. package/dist/api/transform-messages.js.map +1 -1
  48. package/dist/auth/oauth/load.d.ts +2 -0
  49. package/dist/auth/oauth/load.d.ts.map +1 -1
  50. package/dist/auth/oauth/load.js +5 -0
  51. package/dist/auth/oauth/load.js.map +1 -1
  52. package/dist/auth/oauth/meta.d.ts +17 -0
  53. package/dist/auth/oauth/meta.d.ts.map +1 -0
  54. package/dist/auth/oauth/meta.js +190 -0
  55. package/dist/auth/oauth/meta.js.map +1 -0
  56. package/dist/bun-oauth.d.ts.map +1 -1
  57. package/dist/bun-oauth.js +2 -0
  58. package/dist/bun-oauth.js.map +1 -1
  59. package/dist/compat.d.ts +3 -3
  60. package/dist/compat.d.ts.map +1 -1
  61. package/dist/compat.js +9 -6
  62. package/dist/compat.js.map +1 -1
  63. package/dist/env-api-keys.d.ts.map +1 -1
  64. package/dist/env-api-keys.js +1 -0
  65. package/dist/env-api-keys.js.map +1 -1
  66. package/dist/image-models.generated.d.ts +30 -0
  67. package/dist/image-models.generated.d.ts.map +1 -1
  68. package/dist/image-models.generated.js +34 -4
  69. package/dist/image-models.generated.js.map +1 -1
  70. package/dist/index.d.ts +2 -1
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +2 -1
  73. package/dist/index.js.map +1 -1
  74. package/dist/models.d.ts +4 -3
  75. package/dist/models.d.ts.map +1 -1
  76. package/dist/models.generated.d.ts +4 -0
  77. package/dist/models.generated.d.ts.map +1 -1
  78. package/dist/models.generated.js +4 -0
  79. package/dist/models.generated.js.map +1 -1
  80. package/dist/models.js +5 -2
  81. package/dist/models.js.map +1 -1
  82. package/dist/providers/all.d.ts.map +1 -1
  83. package/dist/providers/all.js +2 -0
  84. package/dist/providers/all.js.map +1 -1
  85. package/dist/providers/data/.manifest.json +1 -1
  86. package/dist/providers/data/amazon-bedrock.json +1 -1
  87. package/dist/providers/data/anthropic.json +1 -1
  88. package/dist/providers/data/baseten.json +1 -1
  89. package/dist/providers/data/cerebras.json +1 -1
  90. package/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  91. package/dist/providers/data/deepseek.json +1 -1
  92. package/dist/providers/data/fireworks.json +1 -1
  93. package/dist/providers/data/github-copilot.json +1 -1
  94. package/dist/providers/data/google-vertex.json +1 -1
  95. package/dist/providers/data/google.json +1 -1
  96. package/dist/providers/data/huggingface.json +1 -1
  97. package/dist/providers/data/kimi-coding.json +1 -1
  98. package/dist/providers/data/meta.json +1 -0
  99. package/dist/providers/data/mistral.json +1 -1
  100. package/dist/providers/data/moonshotai-cn.json +1 -1
  101. package/dist/providers/data/moonshotai.json +1 -1
  102. package/dist/providers/data/nvidia.json +1 -1
  103. package/dist/providers/data/openai-codex.json +1 -1
  104. package/dist/providers/data/openai.json +1 -1
  105. package/dist/providers/data/opencode-go.json +1 -1
  106. package/dist/providers/data/opencode.json +1 -1
  107. package/dist/providers/data/openrouter.json +1 -1
  108. package/dist/providers/data/qwen-token-plan-cn.json +1 -1
  109. package/dist/providers/data/qwen-token-plan.json +1 -1
  110. package/dist/providers/data/radius.json +1 -0
  111. package/dist/providers/data/together.json +1 -1
  112. package/dist/providers/data/vercel-ai-gateway.json +1 -1
  113. package/dist/providers/data/zai-coding-cn.json +1 -1
  114. package/dist/providers/data/zai.json +1 -1
  115. package/dist/providers/faux.d.ts +2 -2
  116. package/dist/providers/faux.d.ts.map +1 -1
  117. package/dist/providers/faux.js +11 -11
  118. package/dist/providers/faux.js.map +1 -1
  119. package/dist/providers/meta.d.ts +3 -0
  120. package/dist/providers/meta.d.ts.map +1 -0
  121. package/dist/providers/meta.js +24 -0
  122. package/dist/providers/meta.js.map +1 -0
  123. package/dist/providers/meta.models.d.ts +4 -0
  124. package/dist/providers/meta.models.d.ts.map +1 -0
  125. package/dist/providers/meta.models.js +6 -0
  126. package/dist/providers/meta.models.js.map +1 -0
  127. package/dist/providers/opencode-go.d.ts.map +1 -1
  128. package/dist/providers/opencode-go.js +4 -3
  129. package/dist/providers/opencode-go.js.map +1 -1
  130. package/dist/providers/opencode-headers.d.ts +4 -0
  131. package/dist/providers/opencode-headers.d.ts.map +1 -0
  132. package/dist/providers/opencode-headers.js +22 -0
  133. package/dist/providers/opencode-headers.js.map +1 -0
  134. package/dist/providers/opencode.d.ts.map +1 -1
  135. package/dist/providers/opencode.js +5 -4
  136. package/dist/providers/opencode.js.map +1 -1
  137. package/dist/providers/radius.d.ts.map +1 -1
  138. package/dist/providers/radius.js +19 -5
  139. package/dist/providers/radius.js.map +1 -1
  140. package/dist/providers/radius.models.d.ts +4 -0
  141. package/dist/providers/radius.models.d.ts.map +1 -0
  142. package/dist/providers/radius.models.js +6 -0
  143. package/dist/providers/radius.models.js.map +1 -0
  144. package/dist/types.d.ts +98 -27
  145. package/dist/types.d.ts.map +1 -1
  146. package/dist/types.js.map +1 -1
  147. package/dist/utils/diagnostics.d.ts +3 -2
  148. package/dist/utils/diagnostics.d.ts.map +1 -1
  149. package/dist/utils/diagnostics.js.map +1 -1
  150. package/dist/utils/estimate.d.ts +2 -2
  151. package/dist/utils/estimate.d.ts.map +1 -1
  152. package/dist/utils/estimate.js +8 -29
  153. package/dist/utils/estimate.js.map +1 -1
  154. package/dist/utils/event-stream.d.ts.map +1 -1
  155. package/dist/utils/event-stream.js +25 -7
  156. package/dist/utils/event-stream.js.map +1 -1
  157. package/dist/utils/overflow.d.ts +1 -0
  158. package/dist/utils/overflow.d.ts.map +1 -1
  159. package/dist/utils/overflow.js +11 -5
  160. package/dist/utils/overflow.js.map +1 -1
  161. package/dist/utils/retry.d.ts +6 -1
  162. package/dist/utils/retry.d.ts.map +1 -1
  163. package/dist/utils/retry.js +9 -1
  164. package/dist/utils/retry.js.map +1 -1
  165. package/dist/utils/text.d.ts +9 -1
  166. package/dist/utils/text.d.ts.map +1 -1
  167. package/dist/utils/text.js +26 -0
  168. package/dist/utils/text.js.map +1 -1
  169. package/dist/utils/transcript.d.ts +84 -0
  170. package/dist/utils/transcript.d.ts.map +1 -0
  171. package/dist/utils/transcript.js +204 -0
  172. package/dist/utils/transcript.js.map +1 -0
  173. package/package.json +9 -9
  174. package/dist/utils/deferred-tools.d.ts +0 -9
  175. package/dist/utils/deferred-tools.d.ts.map +0 -1
  176. package/dist/utils/deferred-tools.js +0 -36
  177. package/dist/utils/deferred-tools.js.map +0 -1
@@ -1,6 +1,5 @@
1
1
  import Anthropic from "@anthropic-ai/sdk";
2
2
  import { calculateCost } from "../models.js";
3
- import { splitDeferredTools } from "../utils/deferred-tools.js";
4
3
  import { appendAssistantMessageDiagnostic } from "../utils/diagnostics.js";
5
4
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
6
5
  import { headersToRecord } from "../utils/headers.js";
@@ -9,6 +8,8 @@ import { getPiUserAgent } from "../utils/pi-user-agent.js";
9
8
  import { getProviderEnvValue } from "../utils/provider-env.js";
10
9
  import { retryProviderRequest } from "../utils/provider-retry.js";
11
10
  import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
11
+ import { getSystemMessageText, renderSystemMessageUpdate } from "../utils/text.js";
12
+ import { getCurrentTools, getDeclaredTools, getInitialSystemMessage, hasToolRedefinitions, resolveTranscript, } from "../utils/transcript.js";
12
13
  import { getJsonSchemaToolParameters, resolveJsonSchemaStrictSampling } from "./constrained-sampling.js";
13
14
  import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
14
15
  import { adjustMaxTokensForThinking, buildBaseOptions, clampMaxTokensToContext } from "./simple-options.js";
@@ -114,36 +115,38 @@ const INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14";
114
115
  const SERVER_SIDE_FALLBACK_BETA = "server-side-fallback-2026-07-01";
115
116
  const MID_CONVERSATION_OUTPUT_CONFIG_BETA = "mid-conversation-output-config-2026-07-01";
116
117
  const THINKING_BINDING_CONTROLS_BETA = "thinking-binding-controls-2026-08-01";
118
+ const MID_CONVERSATION_TOOL_CHANGES_BETA = "mid-conversation-tool-changes-2026-07-01";
119
+ /**
120
+ * Stable deferred tool declared whenever native tool changes are in use. Anthropic adds
121
+ * hidden prompt scaffolding as soon as any tool has `defer_loading`; declaring this
122
+ * placeholder from the first request keeps that scaffolding in the cached prefix, so the
123
+ * first real late tool does not invalidate the cache (measured: full miss without it).
124
+ * It is never activated and the model cannot see it.
125
+ */
126
+ const DEFERRED_TOOL_PLACEHOLDER = {
127
+ name: "__pi_deferred_placeholder__",
128
+ description: "Reserved placeholder. Never available. Never call this.",
129
+ input_schema: { type: "object", properties: {}, required: [] },
130
+ defer_loading: true,
131
+ };
117
132
  function shouldUseServerSideFallbackBeta(model) {
118
133
  return (model.compat?.allowedFallbackModels?.length ?? 0) > 0;
119
134
  }
120
135
  function getAnthropicCompat(model) {
136
+ const isOpenRouter = model.provider === "openrouter" || model.baseUrl.includes("openrouter.ai");
121
137
  return {
122
138
  supportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? true,
123
139
  supportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? true,
124
- sendSessionAffinityHeaders: model.compat?.sendSessionAffinityHeaders ?? false,
140
+ sendSessionAffinityHeaders: model.compat?.sendSessionAffinityHeaders ?? isOpenRouter,
141
+ sessionAffinityFormat: model.compat?.sessionAffinityFormat ?? (isOpenRouter ? "openrouter" : undefined),
125
142
  supportsCacheControlOnTools: model.compat?.supportsCacheControlOnTools ?? true,
126
143
  supportsTemperature: model.compat?.supportsTemperature ?? true,
127
144
  allowEmptySignature: model.compat?.allowEmptySignature ?? false,
128
145
  supportsStrictTools: model.compat?.supportsStrictTools ?? false,
129
- supportsToolReferences: model.compat?.supportsToolReferences ?? defaultSupportsToolReferences(model),
146
+ supportsMidConvoSystemMessages: model.compat?.supportsMidConvoSystemMessages ?? false,
147
+ supportsMidConvoToolChanges: model.compat?.supportsMidConvoToolChanges ?? false,
130
148
  };
131
149
  }
132
- /**
133
- * Default for `supportsToolReferences`: first-party Anthropic models except
134
- * Haiku (rejects client-side tool_reference blocks) and models that predate
135
- * tool search (Claude 3.x, Opus/Sonnet 4.0, Opus 4.1).
136
- */
137
- function defaultSupportsToolReferences(model) {
138
- if (model.provider !== "anthropic" || model.id.includes("haiku"))
139
- return false;
140
- const version = model.id.match(/^claude-(?:opus|sonnet|fable)-(\d+)(?:-(\d+))?(?:-|$)/);
141
- if (!version)
142
- return false;
143
- const major = Number(version[1]);
144
- const minor = version[2] && version[2].length < 8 ? Number(version[2]) : 0;
145
- return major > 4 || (major === 4 && minor >= 5);
146
- }
147
150
  function mergeHeaders(...headerSources) {
148
151
  const merged = {};
149
152
  for (const headers of headerSources) {
@@ -329,6 +332,8 @@ async function* iterateAnthropicEvents(response, signal) {
329
332
  }
330
333
  export const stream = (model, context, options) => {
331
334
  const stream = new AssistantMessageEventStream();
335
+ const normalizedContext = resolveTranscript(context, getAnthropicCompat(model).supportsMidConvoSystemMessages);
336
+ const currentTools = getCurrentTools(normalizedContext.messages);
332
337
  (async () => {
333
338
  const providerThinkingLevel = model.compat?.supportsMidConvoEffort ? (options?.effort ?? "high") : undefined;
334
339
  const output = {
@@ -363,9 +368,9 @@ export const stream = (model, context, options) => {
363
368
  assertRequestAuth(model.provider, apiKey, options?.headers);
364
369
  let copilotDynamicHeaders;
365
370
  if (model.provider === "github-copilot") {
366
- const hasImages = hasCopilotVisionInput(context.messages);
371
+ const hasImages = hasCopilotVisionInput(normalizedContext.messages);
367
372
  copilotDynamicHeaders = buildCopilotDynamicHeaders({
368
- messages: context.messages,
373
+ messages: normalizedContext.messages,
369
374
  hasImages,
370
375
  });
371
376
  }
@@ -375,7 +380,7 @@ export const stream = (model, context, options) => {
375
380
  client = created.client;
376
381
  isOAuth = created.isOAuthToken;
377
382
  }
378
- let params = buildParams(model, context, isOAuth, options);
383
+ let params = buildParams(model, normalizedContext, isOAuth, options);
379
384
  const nextParams = await options?.onPayload?.(params, model);
380
385
  if (nextParams !== undefined) {
381
386
  params = { ...nextParams, stream: true };
@@ -399,11 +404,13 @@ export const stream = (model, context, options) => {
399
404
  const transformations = event.message.input_transformations;
400
405
  if (Array.isArray(transformations))
401
406
  inputTransformations = transformations;
402
- output.model = event.message.model;
403
- const fallbackCost = output.model === model.id
407
+ const responseModel = event.message.model;
408
+ if (responseModel !== model.id)
409
+ output.responseModel = responseModel;
410
+ const fallbackCost = responseModel === model.id
404
411
  ? undefined
405
- : model.compat?.allowedFallbackModels?.find((fallback) => fallback.provider === model.provider && fallback.model === output.model)?.cost;
406
- usageModel = fallbackCost ? { ...model, id: output.model, cost: fallbackCost } : model;
412
+ : model.compat?.allowedFallbackModels?.find((fallback) => fallback.provider === model.provider && fallback.model === responseModel)?.cost;
413
+ usageModel = fallbackCost ? { ...model, id: responseModel, cost: fallbackCost } : model;
407
414
  // Capture initial token usage from message_start event
408
415
  // This ensures we have input token counts even if the stream is aborted early
409
416
  output.usage.input = event.message.usage.input_tokens || 0;
@@ -458,7 +465,7 @@ export const stream = (model, context, options) => {
458
465
  type: "toolCall",
459
466
  id: event.content_block.id,
460
467
  name: isOAuth
461
- ? fromClaudeCodeName(event.content_block.name, context.tools)
468
+ ? fromClaudeCodeName(event.content_block.name, currentTools)
462
469
  : event.content_block.name,
463
470
  arguments: event.content_block.input ?? {},
464
471
  partialJson: "",
@@ -722,7 +729,12 @@ function createClient(model, apiKey, optionsHeaders, fetch, dynamicHeaders, sess
722
729
  return { client, isOAuthToken: true };
723
730
  }
724
731
  // API key or header-owned auth.
725
- const sessionAffinityHeaders = sessionId && getAnthropicCompat(model).sendSessionAffinityHeaders ? { "x-session-affinity": sessionId } : {};
732
+ const compat = getAnthropicCompat(model);
733
+ const sessionAffinityHeaders = {};
734
+ if (sessionId && compat.sendSessionAffinityHeaders) {
735
+ const header = compat.sessionAffinityFormat === "openrouter" ? "x-session-id" : "x-session-affinity";
736
+ sessionAffinityHeaders[header] = sessionId;
737
+ }
726
738
  const defaultHeaders = mergeClientHeaders({
727
739
  accept: "application/json",
728
740
  "anthropic-dangerous-direct-browser-access": "true",
@@ -737,7 +749,7 @@ function createClient(model, apiKey, optionsHeaders, fetch, dynamicHeaders, sess
737
749
  });
738
750
  return { client, isOAuthToken: false };
739
751
  }
740
- function getBetaFeatures(model, context, isOAuthToken, options) {
752
+ function getBetaFeatures(model, context, isOAuthToken, nativeToolChanges, options) {
741
753
  let configuredFeatures;
742
754
  for (const headers of [model.headers, options?.headers]) {
743
755
  for (const [name, value] of Object.entries(headers ?? {})) {
@@ -771,24 +783,28 @@ function getBetaFeatures(model, context, isOAuthToken, options) {
771
783
  if (model.compat?.supportsMidConvoEffort === true) {
772
784
  features.push(MID_CONVERSATION_OUTPUT_CONFIG_BETA, THINKING_BINDING_CONTROLS_BETA);
773
785
  }
786
+ if (nativeToolChanges)
787
+ features.push(MID_CONVERSATION_TOOL_CHANGES_BETA);
774
788
  return [...new Set(features)];
775
789
  }
776
790
  function buildParams(model, context, isOAuthToken, options) {
777
791
  const { cacheControl } = getCacheControl(model, options?.cacheRetention, options?.env);
778
792
  const compat = getAnthropicCompat(model);
793
+ const initialSystemMessage = getInitialSystemMessage(context.messages);
794
+ const initialSystemText = initialSystemMessage ? getSystemMessageText(initialSystemMessage) : "";
779
795
  const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
780
- const normalizeToolName = isOAuthToken ? toClaudeCodeName : (name) => name;
781
- const toolPlacement = splitDeferredTools({ ...context, messages: transformedMessages }, compat.supportsToolReferences, normalizeToolName);
782
- let immediateTools = toolPlacement.immediate;
783
- let deferredTools = [...toolPlacement.deferred.values()];
784
- if (immediateTools.length === 0 && deferredTools.length > 0) {
785
- immediateTools = deferredTools;
786
- deferredTools = [];
787
- }
788
- const deferredToolNames = new Set(deferredTools.map((tool) => normalizeToolName(tool.name)));
789
- const converted = convertMessages(transformedMessages, isOAuthToken, cacheControl, compat.allowEmptySignature, deferredToolNames, normalizeToolName, model.compat?.supportsMidConvoEffort === true ? model.provider : undefined);
796
+ const conversationMessages = initialSystemMessage ? transformedMessages.slice(1) : transformedMessages;
797
+ // Native tool changes reference tools by name, so a redefined name cannot be expressed,
798
+ // and Anthropic rejects a tool list where every tool is deferred, so there must be an
799
+ // initial active tool to anchor the deferred ones. Otherwise the current tool list is sent.
800
+ const initialTools = initialSystemMessage?.toolsAdded ?? [];
801
+ const nativeToolChanges = compat.supportsMidConvoSystemMessages &&
802
+ compat.supportsMidConvoToolChanges &&
803
+ initialTools.length > 0 &&
804
+ !hasToolRedefinitions(context.messages);
805
+ const converted = convertMessages(conversationMessages, isOAuthToken, cacheControl, compat.allowEmptySignature, model.compat?.supportsMidConvoEffort === true ? model.provider : undefined, nativeToolChanges);
790
806
  const activeEffort = options?.effort ?? "high";
791
- const betaFeatures = getBetaFeatures(model, context, isOAuthToken, options);
807
+ const betaFeatures = getBetaFeatures(model, context, isOAuthToken, nativeToolChanges, options);
792
808
  const params = {
793
809
  model: model.id,
794
810
  messages: model.compat?.supportsMidConvoEffort === true
@@ -807,20 +823,20 @@ function buildParams(model, context, isOAuthToken, options) {
807
823
  ...(cacheControl ? { cache_control: cacheControl } : {}),
808
824
  },
809
825
  ];
810
- if (context.systemPrompt) {
826
+ if (initialSystemText) {
811
827
  params.system.push({
812
828
  type: "text",
813
- text: sanitizeSurrogates(context.systemPrompt),
829
+ text: sanitizeSurrogates(initialSystemText),
814
830
  ...(cacheControl ? { cache_control: cacheControl } : {}),
815
831
  });
816
832
  }
817
833
  }
818
- else if (context.systemPrompt) {
834
+ else if (initialSystemText) {
819
835
  // Add cache control to system prompt for non-OAuth tokens
820
836
  params.system = [
821
837
  {
822
838
  type: "text",
823
- text: sanitizeSurrogates(context.systemPrompt),
839
+ text: sanitizeSurrogates(initialSystemText),
824
840
  ...(cacheControl ? { cache_control: cacheControl } : {}),
825
841
  },
826
842
  ];
@@ -832,12 +848,26 @@ function buildParams(model, context, isOAuthToken, options) {
832
848
  compat.supportsTemperature) {
833
849
  params.temperature = options.temperature;
834
850
  }
835
- if (immediateTools.length > 0 || deferredTools.length > 0) {
851
+ const toolCacheControl = compat.supportsCacheControlOnTools ? cacheControl : undefined;
852
+ if (nativeToolChanges) {
853
+ // Initial tools stay active with the cache breakpoint on the last one. Every later
854
+ // declaration is deferred and only surfaced by its `tool_addition` block; removed
855
+ // tools stay declared and are withdrawn by `tool_removal`. The request-level list
856
+ // therefore only grows, keeping the cached prefix intact across tool changes.
857
+ const initialNames = new Set(initialTools.map((tool) => tool.name));
858
+ const laterTools = getDeclaredTools(context.messages).filter((tool) => !initialNames.has(tool.name));
836
859
  params.tools = [
837
- ...convertTools(immediateTools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, compat.supportsCacheControlOnTools ? cacheControl : undefined),
838
- ...convertTools(deferredTools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, undefined, true),
860
+ ...convertTools(initialTools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, toolCacheControl),
861
+ DEFERRED_TOOL_PLACEHOLDER,
862
+ ...convertTools(laterTools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools).map((tool) => ({ ...tool, defer_loading: true })),
839
863
  ];
840
864
  }
865
+ else {
866
+ const tools = getCurrentTools(context.messages);
867
+ if (tools.length > 0) {
868
+ params.tools = convertTools(tools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, toolCacheControl);
869
+ }
870
+ }
841
871
  // Managed effort models always use adaptive thinking so prefix mismatches can
842
872
  // be dropped instead of surfacing as persistent 400 responses.
843
873
  if (model.compat?.supportsMidConvoEffort === true) {
@@ -897,41 +927,54 @@ function buildParams(model, context, isOAuthToken, options) {
897
927
  function normalizeToolCallId(id) {
898
928
  return id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
899
929
  }
900
- function convertToolResult(msg, isOAuthToken, deferredToolNames, loadedToolNames, normalizeToolName) {
901
- const references = [];
902
- for (const name of msg.addedToolNames ?? []) {
903
- const normalizedName = normalizeToolName(name);
904
- if (!deferredToolNames.has(normalizedName) || loadedToolNames.has(normalizedName))
905
- continue;
906
- loadedToolNames.add(normalizedName);
907
- references.push({
908
- type: "tool_reference",
909
- tool_name: isOAuthToken ? toClaudeCodeName(name) : name,
910
- });
911
- }
912
- const convertedContent = convertContentBlocks(msg.content);
913
- // Anthropic rejects tool references mixed with ordinary tool-result content.
930
+ function convertToolResult(msg) {
914
931
  return {
915
- toolResult: {
916
- type: "tool_result",
917
- tool_use_id: msg.toolCallId,
918
- content: references.length > 0 ? references : convertedContent,
919
- is_error: msg.isError,
920
- },
921
- siblingContent: references.length === 0
922
- ? []
923
- : typeof convertedContent === "string"
924
- ? [{ type: "text", text: convertedContent }]
925
- : convertedContent,
932
+ type: "tool_result",
933
+ tool_use_id: msg.toolCallId,
934
+ content: convertContentBlocks(msg.content),
935
+ is_error: msg.isError,
926
936
  };
927
937
  }
928
- function convertMessages(transformedMessages, isOAuthToken, cacheControl, allowEmptySignature = false, deferredToolNames = new Set(), normalizeToolName = (name) => name, managedProvider) {
938
+ function convertMessages(transformedMessages, isOAuthToken, cacheControl, allowEmptySignature = false, managedProvider, nativeToolChanges = false) {
929
939
  const params = [];
930
940
  const assistantLevels = new Map();
931
- const loadedToolNames = new Set();
941
+ // Later system messages are held back and emitted directly before the next assistant
942
+ // message (or at the end of the transcript). Anthropic requires `tool_result` blocks to
943
+ // immediately follow their `tool_use`, so a system message between them is rejected; this
944
+ // also mirrors where the managed-effort system messages are inserted. As a result an
945
+ // update placed before a user message in the transcript lands after it on the wire.
946
+ const pendingSystemMessages = [];
947
+ const flushPendingSystemMessages = () => {
948
+ params.push(...pendingSystemMessages);
949
+ pendingSystemMessages.length = 0;
950
+ };
932
951
  for (let i = 0; i < transformedMessages.length; i++) {
933
952
  const msg = transformedMessages[i];
934
- if (msg.role === "user") {
953
+ if (msg.role === "system") {
954
+ // Later system messages only reach this point when the model accepts them natively;
955
+ // otherwise the transcript was collapsed into the leading message before conversion.
956
+ const text = renderSystemMessageUpdate(msg);
957
+ const blocks = [];
958
+ if (text.length > 0)
959
+ blocks.push({ type: "text", text: sanitizeSurrogates(text) });
960
+ if (nativeToolChanges) {
961
+ for (const tool of msg.toolsRemoved ?? []) {
962
+ blocks.push({
963
+ type: "tool_removal",
964
+ tool: { type: "tool_reference", name: isOAuthToken ? toClaudeCodeName(tool.name) : tool.name },
965
+ });
966
+ }
967
+ for (const tool of msg.toolsAdded ?? []) {
968
+ blocks.push({
969
+ type: "tool_addition",
970
+ tool: { type: "tool_reference", name: isOAuthToken ? toClaudeCodeName(tool.name) : tool.name },
971
+ });
972
+ }
973
+ }
974
+ if (blocks.length > 0)
975
+ pendingSystemMessages.push({ role: "system", content: blocks });
976
+ }
977
+ else if (msg.role === "user") {
935
978
  if (typeof msg.content === "string") {
936
979
  if (msg.content.trim().length > 0) {
937
980
  params.push({
@@ -974,6 +1017,7 @@ function convertMessages(transformedMessages, isOAuthToken, cacheControl, allowE
974
1017
  }
975
1018
  }
976
1019
  else if (msg.role === "assistant") {
1020
+ flushPendingSystemMessages();
977
1021
  const blocks = [];
978
1022
  for (const block of msg.content) {
979
1023
  if (block.type === "text") {
@@ -1046,31 +1090,32 @@ function convertMessages(transformedMessages, isOAuthToken, cacheControl, allowE
1046
1090
  else if (msg.role === "toolResult") {
1047
1091
  // Collect all consecutive toolResult messages, needed for z.ai Anthropic endpoint.
1048
1092
  const toolResults = [];
1049
- const siblingContent = [];
1050
1093
  let j = i;
1051
1094
  while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") {
1052
- const converted = convertToolResult(transformedMessages[j], isOAuthToken, deferredToolNames, loadedToolNames, normalizeToolName);
1053
- toolResults.push(converted.toolResult);
1054
- siblingContent.push(...converted.siblingContent);
1095
+ toolResults.push(convertToolResult(transformedMessages[j]));
1055
1096
  j++;
1056
1097
  }
1057
1098
  // Skip the messages we've already processed.
1058
1099
  i = j - 1;
1059
- // Displaced reference-bearing results must follow every tool_result block.
1060
1100
  params.push({
1061
1101
  role: "user",
1062
- content: [...toolResults, ...siblingContent],
1102
+ content: toolResults,
1063
1103
  });
1064
1104
  }
1065
1105
  }
1066
- // Add cache_control to the last user message to cache conversation history
1106
+ flushPendingSystemMessages();
1107
+ // Add cache_control to the last user or system message to cache conversation history
1067
1108
  if (cacheControl && params.length > 0) {
1068
1109
  const lastMessage = params[params.length - 1];
1069
- if (lastMessage.role === "user") {
1110
+ if (lastMessage.role === "user" || lastMessage.role === "system") {
1070
1111
  if (Array.isArray(lastMessage.content)) {
1071
1112
  const lastBlock = lastMessage.content[lastMessage.content.length - 1];
1072
1113
  if (lastBlock &&
1073
- (lastBlock.type === "text" || lastBlock.type === "image" || lastBlock.type === "tool_result")) {
1114
+ (lastBlock.type === "text" ||
1115
+ lastBlock.type === "image" ||
1116
+ lastBlock.type === "tool_result" ||
1117
+ lastBlock.type === "tool_addition" ||
1118
+ lastBlock.type === "tool_removal")) {
1074
1119
  lastBlock.cache_control = cacheControl;
1075
1120
  }
1076
1121
  }
@@ -1103,9 +1148,9 @@ function insertThinkingLevelMessages(converted, activeEffort) {
1103
1148
  return messages;
1104
1149
  }
1105
1150
  function shouldUseFineGrainedToolStreamingBeta(model, context) {
1106
- return !!context.tools?.length && !getAnthropicCompat(model).supportsEagerToolInputStreaming;
1151
+ return getCurrentTools(context.messages).length > 0 && !getAnthropicCompat(model).supportsEagerToolInputStreaming;
1107
1152
  }
1108
- function convertTools(tools, isOAuthToken, supportsEagerToolInputStreaming, supportsStrictTools, cacheControl, deferLoading = false) {
1153
+ function convertTools(tools, isOAuthToken, supportsEagerToolInputStreaming, supportsStrictTools, cacheControl) {
1109
1154
  if (!tools)
1110
1155
  return [];
1111
1156
  return tools.map((tool, index) => {
@@ -1129,7 +1174,6 @@ function convertTools(tools, isOAuthToken, supportsEagerToolInputStreaming, supp
1129
1174
  ...(supportsEagerToolInputStreaming ? { eager_input_streaming: true } : {}),
1130
1175
  ...(strict === true ? { strict: true } : {}),
1131
1176
  input_schema: inputSchema,
1132
- ...(deferLoading ? { defer_loading: true } : {}),
1133
1177
  ...(cacheControl && index === tools.length - 1 ? { cache_control: cacheControl } : {}),
1134
1178
  };
1135
1179
  });