@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.
- package/dist/api/anthropic-messages.d.ts +3 -1
- package/dist/api/anthropic-messages.d.ts.map +1 -1
- package/dist/api/anthropic-messages.js +270 -59
- package/dist/api/anthropic-messages.js.map +1 -1
- package/dist/api/azure-openai-responses.d.ts.map +1 -1
- package/dist/api/azure-openai-responses.js +2 -0
- package/dist/api/azure-openai-responses.js.map +1 -1
- package/dist/api/bedrock-converse-stream.d.ts.map +1 -1
- package/dist/api/bedrock-converse-stream.js +2 -0
- package/dist/api/bedrock-converse-stream.js.map +1 -1
- package/dist/api/google-generative-ai.d.ts.map +1 -1
- package/dist/api/google-generative-ai.js +2 -0
- package/dist/api/google-generative-ai.js.map +1 -1
- package/dist/api/google-vertex.d.ts.map +1 -1
- package/dist/api/google-vertex.js +2 -0
- package/dist/api/google-vertex.js.map +1 -1
- package/dist/api/mistral-conversations.d.ts.map +1 -1
- package/dist/api/mistral-conversations.js +2 -0
- package/dist/api/mistral-conversations.js.map +1 -1
- package/dist/api/openai-codex-responses.d.ts.map +1 -1
- package/dist/api/openai-codex-responses.js +12 -2
- package/dist/api/openai-codex-responses.js.map +1 -1
- package/dist/api/openai-completions.d.ts +3 -1
- package/dist/api/openai-completions.d.ts.map +1 -1
- package/dist/api/openai-completions.js +23 -16
- package/dist/api/openai-completions.js.map +1 -1
- package/dist/api/openai-responses.d.ts.map +1 -1
- package/dist/api/openai-responses.js +12 -2
- package/dist/api/openai-responses.js.map +1 -1
- package/dist/api/provider-replay.d.ts +17 -0
- package/dist/api/provider-replay.d.ts.map +1 -0
- package/dist/api/provider-replay.js +2 -0
- package/dist/api/provider-replay.js.map +1 -0
- package/dist/api/transform-messages.d.ts.map +1 -1
- package/dist/api/transform-messages.js +5 -1
- package/dist/api/transform-messages.js.map +1 -1
- package/dist/image-models.generated.d.ts +30 -0
- package/dist/image-models.generated.d.ts.map +1 -1
- package/dist/image-models.generated.js +30 -0
- package/dist/image-models.generated.js.map +1 -1
- package/dist/providers/data/.manifest.json +1 -1
- package/dist/providers/data/amazon-bedrock.json +1 -1
- package/dist/providers/data/baseten.json +1 -1
- package/dist/providers/data/fireworks.json +1 -1
- package/dist/providers/data/github-copilot.json +1 -1
- package/dist/providers/data/huggingface.json +1 -1
- package/dist/providers/data/kimi-coding.json +1 -1
- package/dist/providers/data/nvidia.json +1 -1
- package/dist/providers/data/opencode-go.json +1 -1
- package/dist/providers/data/openrouter.json +1 -1
- package/dist/providers/data/qwen-token-plan-cn.json +1 -1
- package/dist/providers/data/qwen-token-plan.json +1 -1
- package/dist/providers/data/together.json +1 -1
- package/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/dist/types.d.ts +25 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/deferred-tools.d.ts +63 -4
- package/dist/utils/deferred-tools.d.ts.map +1 -1
- package/dist/utils/deferred-tools.js +111 -12
- package/dist/utils/deferred-tools.js.map +1 -1
- 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:
|
|
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
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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
|
|
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
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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: "
|
|
913
|
-
|
|
1108
|
+
type: "text",
|
|
1109
|
+
text: sanitizeSurrogates(block.text),
|
|
914
1110
|
});
|
|
915
|
-
continue;
|
|
916
1111
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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:
|
|
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: "
|
|
939
|
-
|
|
940
|
-
|
|
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
|
|
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++;
|