@sammorrowdrums/mcpi-ai 0.85.1 → 0.85.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 (62) hide show
  1. package/dist/api/anthropic-messages.d.ts +3 -1
  2. package/dist/api/anthropic-messages.d.ts.map +1 -1
  3. package/dist/api/anthropic-messages.js +270 -59
  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 +2 -0
  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 +2 -0
  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 +2 -0
  13. package/dist/api/google-generative-ai.js.map +1 -1
  14. package/dist/api/google-vertex.d.ts.map +1 -1
  15. package/dist/api/google-vertex.js +2 -0
  16. package/dist/api/google-vertex.js.map +1 -1
  17. package/dist/api/mistral-conversations.d.ts.map +1 -1
  18. package/dist/api/mistral-conversations.js +2 -0
  19. package/dist/api/mistral-conversations.js.map +1 -1
  20. package/dist/api/openai-codex-responses.d.ts.map +1 -1
  21. package/dist/api/openai-codex-responses.js +12 -2
  22. package/dist/api/openai-codex-responses.js.map +1 -1
  23. package/dist/api/openai-completions.d.ts +3 -1
  24. package/dist/api/openai-completions.d.ts.map +1 -1
  25. package/dist/api/openai-completions.js +23 -16
  26. package/dist/api/openai-completions.js.map +1 -1
  27. package/dist/api/openai-responses.d.ts.map +1 -1
  28. package/dist/api/openai-responses.js +12 -2
  29. package/dist/api/openai-responses.js.map +1 -1
  30. package/dist/api/provider-replay.d.ts +17 -0
  31. package/dist/api/provider-replay.d.ts.map +1 -0
  32. package/dist/api/provider-replay.js +2 -0
  33. package/dist/api/provider-replay.js.map +1 -0
  34. package/dist/api/transform-messages.d.ts.map +1 -1
  35. package/dist/api/transform-messages.js +5 -1
  36. package/dist/api/transform-messages.js.map +1 -1
  37. package/dist/image-models.generated.d.ts +30 -0
  38. package/dist/image-models.generated.d.ts.map +1 -1
  39. package/dist/image-models.generated.js +30 -0
  40. package/dist/image-models.generated.js.map +1 -1
  41. package/dist/providers/data/.manifest.json +1 -1
  42. package/dist/providers/data/amazon-bedrock.json +1 -1
  43. package/dist/providers/data/baseten.json +1 -1
  44. package/dist/providers/data/fireworks.json +1 -1
  45. package/dist/providers/data/github-copilot.json +1 -1
  46. package/dist/providers/data/huggingface.json +1 -1
  47. package/dist/providers/data/kimi-coding.json +1 -1
  48. package/dist/providers/data/nvidia.json +1 -1
  49. package/dist/providers/data/opencode-go.json +1 -1
  50. package/dist/providers/data/openrouter.json +1 -1
  51. package/dist/providers/data/qwen-token-plan-cn.json +1 -1
  52. package/dist/providers/data/qwen-token-plan.json +1 -1
  53. package/dist/providers/data/together.json +1 -1
  54. package/dist/providers/data/vercel-ai-gateway.json +1 -1
  55. package/dist/types.d.ts +25 -0
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/types.js.map +1 -1
  58. package/dist/utils/deferred-tools.d.ts +63 -4
  59. package/dist/utils/deferred-tools.d.ts.map +1 -1
  60. package/dist/utils/deferred-tools.js +111 -12
  61. package/dist/utils/deferred-tools.js.map +1 -1
  62. package/package.json +2 -2
@@ -1,6 +1,7 @@
1
1
  import Anthropic from "@anthropic-ai/sdk";
2
2
  import { calculateCost } from "../models.js";
3
- import { splitDeferredTools } from "../utils/deferred-tools.js";
3
+ import { appendDeferredToolsUnsupportedDiagnostic, splitDeferredTools } from "../utils/deferred-tools.js";
4
+ import { appendAssistantMessageDiagnostic, createAssistantMessageDiagnostic } from "../utils/diagnostics.js";
4
5
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
5
6
  import { headersToRecord } from "../utils/headers.js";
6
7
  import { parseJsonWithRepair, parseStreamingJson } from "../utils/json-parse.js";
@@ -119,18 +120,68 @@ function getAnthropicCompat(model) {
119
120
  supportsTemperature: model.compat?.supportsTemperature ?? true,
120
121
  allowEmptySignature: model.compat?.allowEmptySignature ?? false,
121
122
  supportsStrictTools: model.compat?.supportsStrictTools ?? false,
122
- supportsToolReferences: model.compat?.supportsToolReferences ?? defaultSupportsToolReferences(model),
123
+ supportsToolReferences: resolveSupportsToolReferences(model),
124
+ // On by default wherever deferral works: Anthropic defines `defer_loading` as "only
125
+ // loaded when returned via tool_reference from tool search", so withholding a schema
126
+ // without offering search leaves no way to hand it back. Verified against the Copilot
127
+ // gateway on every id in `GITHUB_COPILOT_TOOL_REFERENCE_MODEL_IDS` routed through this
128
+ // API, each answering with `server_tool_use` -> `tool_search_tool_result` -> `tool_use`.
129
+ // Only consulted when `supportsToolReferences` is on, which gates deferral itself.
130
+ supportsToolSearch: model.compat?.supportsToolSearch ?? true,
123
131
  };
124
132
  }
125
133
  /**
126
- * Default for `supportsToolReferences`: first-party Anthropic models except
127
- * Haiku (rejects client-side tool_reference blocks) and models that predate
128
- * tool search (Claude 3.x, Opus/Sonnet 4.0, Opus 4.1).
134
+ * Models whose deferred-tool support was withdrawn after the endpoint rejected
135
+ * `defer_loading` or `tool_reference`. Process-lifetime only, and only reachable
136
+ * for endpoints that claim the capability without implementing it, since built-in
137
+ * catalog metadata is probe-verified.
138
+ */
139
+ const toolReferencesRejectedByEndpoint = new Set();
140
+ function toolReferenceEndpointKey(model) {
141
+ return `${model.provider}\u0000${model.id}\u0000${model.baseUrl}`;
142
+ }
143
+ /** Record a hard endpoint rejection so this process stops sending deferred tools. */
144
+ export function disableToolReferencesForEndpoint(model) {
145
+ toolReferencesRejectedByEndpoint.add(toolReferenceEndpointKey(model));
146
+ }
147
+ function resolveSupportsToolReferences(model) {
148
+ if (toolReferencesRejectedByEndpoint.has(toolReferenceEndpointKey(model)))
149
+ return false;
150
+ return model.compat?.supportsToolReferences ?? defaultSupportsToolReferences(model);
151
+ }
152
+ /**
153
+ * Anthropic's server-side tool search, offered whenever a request carries deferred tools.
154
+ *
155
+ * BM25 rather than the regex variant: a regex that matches nothing returns no references
156
+ * and leaves the tool hidden, which is the failure this exists to prevent, while BM25
157
+ * always ranks the catalog and returns the closest matches. It stays outside the deferred
158
+ * set so the model can always see that searching is possible.
159
+ */
160
+ const TOOL_SEARCH_TOOL = {
161
+ name: "tool_search_tool_bm25",
162
+ type: "tool_search_tool_bm25_20251119",
163
+ };
164
+ /**
165
+ * Fallback for `anthropic-messages` models with no explicit capability metadata.
166
+ *
167
+ * First-party Anthropic ships dated snapshots of the same model
168
+ * (`claude-opus-4-6-20260101`), so support is derived from the family version
169
+ * rather than an exact-id allowlist: Sonnet, Opus, and Fable at 4.5 or newer,
170
+ * excluding Haiku, which rejects client-side `tool_reference` blocks.
171
+ *
172
+ * Every other provider returns false and must opt in through
173
+ * `compat.supportsToolReferences`. Gateways that reuse Claude ids do not
174
+ * necessarily implement the protocol, and the generated catalog carries
175
+ * probe-verified metadata for the ones that do (see
176
+ * `GITHUB_COPILOT_TOOL_REFERENCE_MODEL_IDS` in `scripts/generate-models.ts`).
129
177
  */
130
178
  function defaultSupportsToolReferences(model) {
131
179
  if (model.provider !== "anthropic" || model.id.includes("haiku"))
132
180
  return false;
133
- const version = model.id.match(/^claude-(?:opus|sonnet|fable)-(\d+)(?:-(\d+))?(?:-|$)/);
181
+ // Minor versions appear as `claude-opus-4-6` and `claude-opus-4.8` depending on
182
+ // the catalog, so accept either separator. A trailing 8-digit group is a release
183
+ // date (`claude-sonnet-4-20250514`), not a minor version.
184
+ const version = model.id.match(/^claude-(?:opus|sonnet|fable)-(\d+)(?:[-.](\d+))?(?:[-.]|$)/);
134
185
  if (!version)
135
186
  return false;
136
187
  const major = Number(version[1]);
@@ -328,6 +379,41 @@ async function* iterateAnthropicEvents(response, signal) {
328
379
  throw new Error("Anthropic stream ended before message_stop");
329
380
  }
330
381
  }
382
+ /**
383
+ * Signals that the endpoint does not implement the deferred-tool protocol at all:
384
+ * the field is rejected as unknown, or the content block type is unrecognized.
385
+ */
386
+ const DEFERRED_TOOL_CAPABILITY_REJECTION = /extra inputs are not permitted|not permitted|unsupported|not supported|unrecognized|unexpected (?:field|keyword|argument|property)|unknown (?:field|parameter|argument|property)|does not match any of the expected|invalid[^.]{0,32}\btype\b/i;
387
+ /**
388
+ * Signals that the endpoint *does* implement references and could not resolve one.
389
+ * That is a client-side activation or naming bug, not a capability gap, so it must
390
+ * surface rather than silently downgrade the request.
391
+ */
392
+ const TOOL_REFERENCE_RESOLUTION_FAILURE = /not found|does not exist|no such|unresolved|unknown tool\b/i;
393
+ /**
394
+ * True only for an unambiguous rejection of the deferred-tool protocol itself:
395
+ * a 400 naming the `defer_loading` field, the `tool_reference` block type, or the
396
+ * `tool_search_tool_*` server tool, *and* describing it as unknown, extra, or
397
+ * unsupported.
398
+ *
399
+ * The Copilot gateway words these as `Extra inputs are not permitted:
400
+ * tools.1.defer_loading` and as an unrecognized content block type.
401
+ *
402
+ * `Tool reference 'x' not found in available tools` is deliberately excluded. It
403
+ * proves the endpoint implements references, so downgrading would mask a client
404
+ * activation or tool-naming bug behind a silent capability loss.
405
+ */
406
+ function isDeferredToolRejection(error) {
407
+ if (!(error instanceof Error))
408
+ return false;
409
+ if (error.status !== 400)
410
+ return false;
411
+ if (!/\b(?:defer_loading|tool_reference|tool_search_tool\w*)\b/.test(error.message))
412
+ return false;
413
+ if (TOOL_REFERENCE_RESOLUTION_FAILURE.test(error.message))
414
+ return false;
415
+ return DEFERRED_TOOL_CAPABILITY_REJECTION.test(error.message);
416
+ }
331
417
  export const stream = (model, context, options) => {
332
418
  const stream = new AssistantMessageEventStream();
333
419
  (async () => {
@@ -372,7 +458,9 @@ export const stream = (model, context, options) => {
372
458
  client = created.client;
373
459
  isOAuth = created.isOAuthToken;
374
460
  }
375
- let params = buildParams(model, context, isOAuth, options);
461
+ let built = buildParams(model, context, isOAuth, options);
462
+ appendDeferredToolsUnsupportedDiagnostic(output, model, built.unsupportedDeferredToolNames);
463
+ let params = built.params;
376
464
  const nextParams = await options?.onPayload?.(params, model);
377
465
  if (nextParams !== undefined) {
378
466
  params = nextParams;
@@ -382,14 +470,40 @@ export const stream = (model, context, options) => {
382
470
  ...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
383
471
  maxRetries: 0,
384
472
  };
385
- const response = await retryProviderRequest(() => client.messages.create({ ...params, stream: true }, requestOptions).asResponse(), {
473
+ const sendRequest = (body) => retryProviderRequest(() => client.messages.create({ ...body, stream: true }, requestOptions).asResponse(), {
386
474
  maxRetries: options?.maxRetries,
387
475
  maxRetryDelayMs: options?.maxRetryDelayMs,
388
476
  signal: options?.signal,
389
477
  });
478
+ let response;
479
+ try {
480
+ response = await sendRequest(params);
481
+ }
482
+ catch (error) {
483
+ // Only a 400 that names the deferred-tool fields is treated as a
484
+ // capability mismatch. Every other failure, including other 400s,
485
+ // propagates with the provider's exact message.
486
+ if (built.deferredToolNames.length === 0 || !isDeferredToolRejection(error))
487
+ throw error;
488
+ disableToolReferencesForEndpoint(model);
489
+ appendAssistantMessageDiagnostic(output, createAssistantMessageDiagnostic("deferred_tools_rejected", error, {
490
+ provider: model.provider,
491
+ model: model.id,
492
+ deferredTools: built.deferredToolNames,
493
+ }));
494
+ built = buildParams(model, context, isOAuth, options);
495
+ appendDeferredToolsUnsupportedDiagnostic(output, model, built.unsupportedDeferredToolNames);
496
+ params = built.params;
497
+ const retryParams = await options?.onPayload?.(params, model);
498
+ if (retryParams !== undefined) {
499
+ params = retryParams;
500
+ }
501
+ response = await sendRequest(params);
502
+ }
390
503
  await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
391
504
  stream.push({ type: "start", partial: output });
392
505
  const blocks = output.content;
506
+ const replayBlocks = [];
393
507
  for await (const event of iterateAnthropicEvents(response, options?.signal)) {
394
508
  if (event.type === "message_start") {
395
509
  output.responseId = event.message.id;
@@ -406,6 +520,11 @@ export const stream = (model, context, options) => {
406
520
  calculateCost(model, output.usage);
407
521
  }
408
522
  else if (event.type === "content_block_start") {
523
+ replayBlocks.push({
524
+ index: event.index,
525
+ block: event.content_block,
526
+ partialJson: "",
527
+ });
409
528
  if (event.content_block.type === "text") {
410
529
  const block = {
411
530
  type: "text",
@@ -452,6 +571,31 @@ export const stream = (model, context, options) => {
452
571
  }
453
572
  }
454
573
  else if (event.type === "content_block_delta") {
574
+ const replayBlock = replayBlocks.find((block) => block.index === event.index);
575
+ if (replayBlock) {
576
+ if (event.delta.type === "text_delta") {
577
+ const text = replayBlock.block.text;
578
+ replayBlock.block.text = `${typeof text === "string" ? text : ""}${event.delta.text}`;
579
+ }
580
+ else if (event.delta.type === "thinking_delta") {
581
+ const thinking = replayBlock.block.thinking;
582
+ replayBlock.block.thinking = `${typeof thinking === "string" ? thinking : ""}${event.delta.thinking}`;
583
+ }
584
+ else if (event.delta.type === "signature_delta") {
585
+ const signature = replayBlock.block.signature;
586
+ replayBlock.block.signature = `${typeof signature === "string" ? signature : ""}${event.delta.signature}`;
587
+ }
588
+ else if (event.delta.type === "input_json_delta") {
589
+ replayBlock.partialJson += event.delta.partial_json;
590
+ replayBlock.block.input = parseStreamingJson(replayBlock.partialJson);
591
+ }
592
+ else if (event.delta.type === "citations_delta") {
593
+ const citations = replayBlock.block.citations;
594
+ replayBlock.block.citations = Array.isArray(citations)
595
+ ? [...citations, event.delta.citation]
596
+ : [event.delta.citation];
597
+ }
598
+ }
455
599
  if (event.delta.type === "text_delta") {
456
600
  const index = blocks.findIndex((b) => b.index === event.index);
457
601
  const block = blocks[index];
@@ -502,6 +646,10 @@ export const stream = (model, context, options) => {
502
646
  }
503
647
  }
504
648
  else if (event.type === "content_block_stop") {
649
+ const replayBlock = replayBlocks.find((block) => block.index === event.index);
650
+ if (replayBlock?.partialJson) {
651
+ replayBlock.block.input = parseStreamingJson(replayBlock.partialJson);
652
+ }
505
653
  const index = blocks.findIndex((b) => b.index === event.index);
506
654
  const block = blocks[index];
507
655
  if (block) {
@@ -584,6 +732,17 @@ export const stream = (model, context, options) => {
584
732
  if (output.stopReason === "aborted" || output.stopReason === "error") {
585
733
  throw new Error(output.errorMessage || "An unknown error occurred");
586
734
  }
735
+ const providerContent = replayBlocks
736
+ .sort((left, right) => left.index - right.index)
737
+ .map((block) => block.block);
738
+ if (providerContent.some((block) => typeof block === "object" && block !== null && !Array.isArray(block) && block.type !== "text")) {
739
+ output.providerReplay = {
740
+ provider: model.provider,
741
+ api: model.api,
742
+ model: model.id,
743
+ content: providerContent,
744
+ };
745
+ }
587
746
  stream.push({ type: "done", reason: output.stopReason, message: output });
588
747
  stream.end();
589
748
  }
@@ -718,13 +877,22 @@ function buildParams(model, context, isOAuthToken, options) {
718
877
  const compat = getAnthropicCompat(model);
719
878
  const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
720
879
  const normalizeToolName = isOAuthToken ? toClaudeCodeName : (name) => name;
721
- const toolPlacement = splitDeferredTools({ ...context, messages: transformedMessages }, compat.supportsToolReferences, normalizeToolName);
722
- let immediateTools = toolPlacement.immediate;
723
- let deferredTools = [...toolPlacement.deferred.values()];
724
- if (immediateTools.length === 0 && deferredTools.length > 0) {
725
- immediateTools = deferredTools;
726
- deferredTools = [];
727
- }
880
+ const deferralContext = { ...context, messages: transformedMessages };
881
+ const toolPlacement = splitDeferredTools(deferralContext, {
882
+ enabled: compat.supportsToolReferences,
883
+ // Without server-side search, the only load point is a `tool_reference` in a tool
884
+ // result, so a tool registered with `deferred: true` that no result introduces would
885
+ // stay hidden for the whole session. Report those instead of losing them.
886
+ registrationDeferral: compat.supportsToolSearch,
887
+ // The all-deferred safety floor exists so the model always has somewhere to start. The
888
+ // search tool below is somewhere to start, so a tool set that is entirely deferred --
889
+ // the all-MCP-proxy shape that gains the most here -- stays deferred rather than being
890
+ // expanded wholesale.
891
+ providesToolSearch: compat.supportsToolSearch,
892
+ normalizeName: normalizeToolName,
893
+ });
894
+ const immediateTools = toolPlacement.immediate;
895
+ const deferredTools = [...toolPlacement.deferred.values()];
728
896
  const deferredToolNames = new Set(deferredTools.map((tool) => normalizeToolName(tool.name)));
729
897
  const params = {
730
898
  model: model.id,
@@ -768,6 +936,11 @@ function buildParams(model, context, isOAuthToken, options) {
768
936
  ...convertTools(immediateTools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, compat.supportsCacheControlOnTools ? cacheControl : undefined),
769
937
  ...convertTools(deferredTools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, undefined, true),
770
938
  ];
939
+ // Appended last so it sits outside the cached prefix, which ends at the
940
+ // `cache_control` breakpoint on the final immediate tool.
941
+ if (deferredTools.length > 0 && compat.supportsToolSearch) {
942
+ params.tools.push(TOOL_SEARCH_TOOL);
943
+ }
771
944
  }
772
945
  // Configure thinking mode: adaptive, budget-based, or explicitly disabled.
773
946
  if (model.reasoning) {
@@ -813,7 +986,11 @@ function buildParams(model, context, isOAuthToken, options) {
813
986
  params.tool_choice = options.toolChoice;
814
987
  }
815
988
  }
816
- return params;
989
+ return {
990
+ params,
991
+ deferredToolNames: [...deferredToolNames],
992
+ unsupportedDeferredToolNames: toolPlacement.unsupported,
993
+ };
817
994
  }
818
995
  // Normalize tool call IDs to match Anthropic's required pattern and length
819
996
  function normalizeToolCallId(id) {
@@ -850,6 +1027,7 @@ function convertToolResult(msg, isOAuthToken, deferredToolNames, loadedToolNames
850
1027
  function convertMessages(transformedMessages, isOAuthToken, cacheControl, allowEmptySignature = false, deferredToolNames = new Set(), normalizeToolName = (name) => name) {
851
1028
  const params = [];
852
1029
  const loadedToolNames = new Set();
1030
+ const providerToolCallIds = new Map();
853
1031
  for (let i = 0; i < transformedMessages.length; i++) {
854
1032
  const msg = transformedMessages[i];
855
1033
  if (msg.role === "user") {
@@ -896,59 +1074,88 @@ function convertMessages(transformedMessages, isOAuthToken, cacheControl, allowE
896
1074
  }
897
1075
  else if (msg.role === "assistant") {
898
1076
  const blocks = [];
899
- for (const block of msg.content) {
900
- if (block.type === "text") {
901
- if (block.text.trim().length === 0)
902
- continue;
903
- blocks.push({
904
- type: "text",
905
- text: sanitizeSurrogates(block.text),
906
- });
1077
+ const providerReplay = msg.providerReplay;
1078
+ const canReplayProviderContent = providerReplay?.provider === msg.provider &&
1079
+ providerReplay.api === msg.api &&
1080
+ providerReplay.model === msg.model &&
1081
+ providerReplay.api === "anthropic-messages";
1082
+ if (canReplayProviderContent) {
1083
+ const replayBlocks = structuredClone(providerReplay.content);
1084
+ const replayToolCalls = replayBlocks.filter((block) => block.type === "tool_use");
1085
+ const localToolCalls = msg.content.filter((block) => block.type === "toolCall");
1086
+ for (let index = 0; index < Math.min(localToolCalls.length, replayToolCalls.length); index++) {
1087
+ providerToolCallIds.set(localToolCalls[index].id, replayToolCalls[index].id);
907
1088
  }
908
- else if (block.type === "thinking") {
909
- // Redacted thinking: pass the opaque payload back as redacted_thinking
910
- if (block.redacted) {
1089
+ blocks.push(...replayBlocks);
1090
+ }
1091
+ else {
1092
+ for (let index = 0; index < msg.content.length - 1; index++) {
1093
+ const current = msg.content[index];
1094
+ const next = msg.content[index + 1];
1095
+ if (current.type === "thinking" &&
1096
+ next.type === "thinking" &&
1097
+ current.thinkingSignature &&
1098
+ next.thinkingSignature &&
1099
+ msg.content.slice(index + 2).some((block) => block.type === "toolCall")) {
1100
+ throw new Error("Cannot safely replay this Anthropic assistant message: it contains adjacent signed thinking blocks but no provider-native replay data. mcpi 0.85.2 could drop native server-tool blocks between those thoughts. Start a new session or branch before this turn, or start a new session with thinking disabled.");
1101
+ }
1102
+ }
1103
+ for (const block of msg.content) {
1104
+ if (block.type === "text") {
1105
+ if (block.text.trim().length === 0)
1106
+ continue;
911
1107
  blocks.push({
912
- type: "redacted_thinking",
913
- data: block.thinkingSignature,
1108
+ type: "text",
1109
+ text: sanitizeSurrogates(block.text),
914
1110
  });
915
- continue;
916
1111
  }
917
- const thinkingSignature = block.thinkingSignature;
918
- const hasThinkingSignature = !!thinkingSignature && thinkingSignature.trim().length > 0;
919
- if (block.thinking.trim().length === 0 && !hasThinkingSignature)
920
- continue;
921
- // If thinking signature is missing/empty (e.g., from aborted stream),
922
- // convert to plain text for Anthropic. Some compatible providers emit
923
- // and accept empty signatures, so let marked models preserve the block.
924
- if (!hasThinkingSignature) {
925
- blocks.push(allowEmptySignature
926
- ? {
1112
+ else if (block.type === "thinking") {
1113
+ // Redacted thinking: pass the opaque payload back as redacted_thinking
1114
+ if (block.redacted) {
1115
+ blocks.push({
1116
+ type: "redacted_thinking",
1117
+ data: block.thinkingSignature,
1118
+ });
1119
+ continue;
1120
+ }
1121
+ const thinkingSignature = block.thinkingSignature;
1122
+ const hasThinkingSignature = !!thinkingSignature && thinkingSignature.trim().length > 0;
1123
+ if (block.thinking.trim().length === 0 && !hasThinkingSignature)
1124
+ continue;
1125
+ // If thinking signature is missing/empty (e.g., from aborted stream),
1126
+ // convert to plain text for Anthropic. Some compatible providers emit
1127
+ // and accept empty signatures, so let marked models preserve the block.
1128
+ if (!hasThinkingSignature) {
1129
+ blocks.push(allowEmptySignature
1130
+ ? {
1131
+ type: "thinking",
1132
+ thinking: sanitizeSurrogates(block.thinking),
1133
+ signature: "",
1134
+ }
1135
+ : {
1136
+ type: "text",
1137
+ text: sanitizeSurrogates(block.thinking),
1138
+ });
1139
+ }
1140
+ else {
1141
+ // Signatures authenticate the exact thinking text. Never sanitize
1142
+ // or otherwise rewrite a signed block during replay.
1143
+ blocks.push({
927
1144
  type: "thinking",
928
- thinking: sanitizeSurrogates(block.thinking),
929
- signature: "",
930
- }
931
- : {
932
- type: "text",
933
- text: sanitizeSurrogates(block.thinking),
1145
+ thinking: block.thinking,
1146
+ signature: thinkingSignature,
934
1147
  });
1148
+ }
935
1149
  }
936
- else {
1150
+ else if (block.type === "toolCall") {
937
1151
  blocks.push({
938
- type: "thinking",
939
- thinking: sanitizeSurrogates(block.thinking),
940
- signature: thinkingSignature,
1152
+ type: "tool_use",
1153
+ id: block.id,
1154
+ name: isOAuthToken ? toClaudeCodeName(block.name) : block.name,
1155
+ input: block.arguments ?? {},
941
1156
  });
942
1157
  }
943
1158
  }
944
- else if (block.type === "toolCall") {
945
- blocks.push({
946
- type: "tool_use",
947
- id: block.id,
948
- name: isOAuthToken ? toClaudeCodeName(block.name) : block.name,
949
- input: block.arguments ?? {},
950
- });
951
- }
952
1159
  }
953
1160
  if (blocks.length === 0)
954
1161
  continue;
@@ -963,7 +1170,11 @@ function convertMessages(transformedMessages, isOAuthToken, cacheControl, allowE
963
1170
  const siblingContent = [];
964
1171
  let j = i;
965
1172
  while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") {
966
- const converted = convertToolResult(transformedMessages[j], isOAuthToken, deferredToolNames, loadedToolNames, normalizeToolName);
1173
+ const toolResult = transformedMessages[j];
1174
+ const converted = convertToolResult({
1175
+ ...toolResult,
1176
+ toolCallId: providerToolCallIds.get(toolResult.toolCallId) ?? toolResult.toolCallId,
1177
+ }, isOAuthToken, deferredToolNames, loadedToolNames, normalizeToolName);
967
1178
  toolResults.push(converted.toolResult);
968
1179
  siblingContent.push(...converted.siblingContent);
969
1180
  j++;