mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(copilot): prevent invalid Claude tool IDs from causing retries (#117789)
* fix(copilot): normalize Claude tool IDs before requests * fix(copilot): preserve typed payload and stream contracts * fix(copilot): preserve unique paired Anthropic tool IDs
This commit is contained in:
committed by
GitHub
parent
fc0371bbe7
commit
de6bd7d1bc
@@ -1,5 +1,8 @@
|
||||
// Github Copilot tests cover stream plugin behavior.
|
||||
import type { Context } from "openclaw/plugin-sdk/llm";
|
||||
import { createServer } from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
|
||||
import { streamSimple, type Context, type Model } from "openclaw/plugin-sdk/llm";
|
||||
import { buildCopilotIdeHeaders } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { COPILOT_RUNTIME_INTEGRATION_ID } from "./runtime-identity.js";
|
||||
@@ -39,6 +42,386 @@ function buildExpectedCopilotHeaders(
|
||||
}
|
||||
|
||||
describe("wrapCopilotAnthropicStream", () => {
|
||||
it("normalizes Copilot Claude wire tool IDs without mutating the persisted transcript", () => {
|
||||
const model = {
|
||||
provider: "github-copilot",
|
||||
api: "anthropic-messages",
|
||||
id: "claude-sonnet-4.6",
|
||||
} as never;
|
||||
const sourceIds = [
|
||||
"toolu_native_123",
|
||||
"already-valid_id-456",
|
||||
"pipe|value",
|
||||
"dot.value",
|
||||
"colon:value",
|
||||
"slash/value",
|
||||
"space value",
|
||||
"functions.read:0",
|
||||
`toolu_${"x".repeat(80)}`,
|
||||
];
|
||||
const messages = [
|
||||
{ role: "user", content: "Use each tool" },
|
||||
{
|
||||
role: "assistant",
|
||||
provider: "github-copilot",
|
||||
api: "anthropic-messages",
|
||||
model: "claude-sonnet-4.6",
|
||||
content: [
|
||||
{ type: "thinking", thinking: "private", thinkingSignature: "signature" },
|
||||
...sourceIds.map((id) => ({ type: "toolCall", id, name: "read", arguments: {} })),
|
||||
],
|
||||
},
|
||||
...sourceIds.map((id) => ({
|
||||
role: "toolResult",
|
||||
toolCallId: id,
|
||||
toolName: "read",
|
||||
content: [{ type: "text", text: `result for ${id}` }],
|
||||
})),
|
||||
] as Context["messages"];
|
||||
const persistedTranscript = structuredClone(messages);
|
||||
let observedPayload: { messages: Array<{ role: string; content: unknown }> } | undefined;
|
||||
const baseStreamFn = vi.fn<StreamFn>((streamModel, context, options) => {
|
||||
const payload = {
|
||||
messages: context.messages.map((message) => {
|
||||
if (message.role === "toolResult") {
|
||||
return {
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: message.toolCallId,
|
||||
content: message.content,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
if (message.role !== "assistant") {
|
||||
return { role: message.role, content: message.content };
|
||||
}
|
||||
return {
|
||||
role: "assistant",
|
||||
content: message.content.map((block) =>
|
||||
block.type === "toolCall"
|
||||
? { type: "tool_use", id: block.id, name: block.name, input: block.arguments }
|
||||
: block,
|
||||
),
|
||||
};
|
||||
}),
|
||||
};
|
||||
options?.onPayload?.(payload, streamModel);
|
||||
observedPayload = payload;
|
||||
return { async *[Symbol.asyncIterator]() {} } as never;
|
||||
});
|
||||
|
||||
void requireStreamFn(wrapCopilotAnthropicStream(baseStreamFn))(model, { messages }, {});
|
||||
|
||||
const outbound = observedPayload?.messages ?? [];
|
||||
const assistant = outbound.find((message) => message.role === "assistant");
|
||||
const assistantBlocks = Array.isArray(assistant?.content) ? assistant.content : [];
|
||||
const outboundIds = assistantBlocks
|
||||
.filter((block): block is { type: "tool_use"; id: string } => block.type === "tool_use")
|
||||
.map((block) => block.id);
|
||||
const resultIds = outbound.flatMap((message) =>
|
||||
Array.isArray(message.content)
|
||||
? message.content
|
||||
.filter(
|
||||
(block): block is { type: "tool_result"; tool_use_id: string } =>
|
||||
block.type === "tool_result",
|
||||
)
|
||||
.map((block) => block.tool_use_id)
|
||||
: [],
|
||||
);
|
||||
|
||||
expect(outboundIds).toEqual([
|
||||
"toolu_native_123",
|
||||
"already-valid_id-456",
|
||||
"pipe_value",
|
||||
"dot_value",
|
||||
"colon_value",
|
||||
"slash_value",
|
||||
"space_value",
|
||||
"functions_read_0",
|
||||
`toolu_${"x".repeat(58)}`,
|
||||
]);
|
||||
expect(resultIds).toEqual(outboundIds);
|
||||
expect(assistantBlocks.some((block) => block.type === "thinking")).toBe(false);
|
||||
expect(messages).toEqual(persistedTranscript);
|
||||
});
|
||||
|
||||
it("uniquely pairs colliding wire IDs, preserves valid IDs, and remains idempotent", () => {
|
||||
const longPrefix = "x".repeat(64);
|
||||
const sourceIds = [
|
||||
"a.b",
|
||||
"a:b",
|
||||
"a_b",
|
||||
"a_b_2",
|
||||
"a.b",
|
||||
"native_id",
|
||||
"native_id",
|
||||
"native_id_2",
|
||||
`${longPrefix}first`,
|
||||
`${longPrefix}second`,
|
||||
longPrefix,
|
||||
];
|
||||
const expectedIds = [
|
||||
"a_b_3",
|
||||
"a_b_4",
|
||||
"a_b",
|
||||
"a_b_2",
|
||||
"a_b_5",
|
||||
"native_id",
|
||||
"native_id_3",
|
||||
"native_id_2",
|
||||
`${"x".repeat(62)}_2`,
|
||||
`${"x".repeat(62)}_3`,
|
||||
longPrefix,
|
||||
];
|
||||
const toolUseBlocks = sourceIds.map((id) => ({
|
||||
type: "tool_use",
|
||||
id,
|
||||
name: "read",
|
||||
input: {},
|
||||
}));
|
||||
const toolResultBlocks = [...sourceIds, "a.b"].map((tool_use_id) => ({
|
||||
type: "tool_result",
|
||||
tool_use_id,
|
||||
content: "done",
|
||||
}));
|
||||
const payload = {
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: toolUseBlocks,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: toolResultBlocks,
|
||||
},
|
||||
],
|
||||
};
|
||||
const baseStreamFn = vi.fn<StreamFn>((model, _context, options) => {
|
||||
options?.onPayload?.(payload, model);
|
||||
const patchedOnce = structuredClone(payload);
|
||||
options?.onPayload?.(payload, model);
|
||||
expect(payload).toEqual(patchedOnce);
|
||||
return { async *[Symbol.asyncIterator]() {} } as never;
|
||||
});
|
||||
|
||||
void requireStreamFn(wrapCopilotAnthropicStream(baseStreamFn))(
|
||||
{ provider: "github-copilot", api: "anthropic-messages", id: "claude-sonnet-4.6" } as never,
|
||||
{ messages: [{ role: "user", content: "hi" }] } as never,
|
||||
{},
|
||||
);
|
||||
|
||||
const toolUseIds = toolUseBlocks.map((block) => block.id);
|
||||
const toolResultIds = toolResultBlocks.map((block) => block.tool_use_id);
|
||||
expect(toolUseIds).toEqual(expectedIds);
|
||||
expect(toolResultIds).toEqual([...expectedIds, "a_b_5"]);
|
||||
expect(new Set(toolUseIds).size).toBe(expectedIds.length);
|
||||
expect(toolUseIds.every((id) => /^[a-zA-Z0-9_-]{1,64}$/.test(id))).toBe(true);
|
||||
});
|
||||
|
||||
it.each(["sync", "async", "sync in-place", "async in-place"] as const)(
|
||||
"normalizes Copilot Claude payloads returned by a %s caller hook",
|
||||
async (hookType) => {
|
||||
let returnedPayload: unknown;
|
||||
const baseStreamFn = vi.fn(async (model, _context, options) => {
|
||||
const initialPayload = { messages: [{ role: "user", content: "initial request" }] };
|
||||
const replacement = await options?.onPayload?.(initialPayload, model);
|
||||
returnedPayload = replacement ?? initialPayload;
|
||||
return { async *[Symbol.asyncIterator]() {} } as never;
|
||||
});
|
||||
const replacement = {
|
||||
messages: [
|
||||
{ role: "system", content: "replacement system prompt" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "thinking", thinking: "private" },
|
||||
{ type: "tool_use", id: "functions.read:0", name: "read", input: {} },
|
||||
{ type: "tool_use", id: "a.b", name: "read", input: {} },
|
||||
{ type: "tool_use", id: "a:b", name: "read", input: {} },
|
||||
{ type: "tool_use", id: "a_b", name: "read", input: {} },
|
||||
{ type: "tool_use", id: "a.b", name: "read", input: {} },
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: ["functions.read:0", "a.b", "a:b", "a_b", "a.b"].map((tool_use_id) => ({
|
||||
type: "tool_result",
|
||||
tool_use_id,
|
||||
content: "done",
|
||||
})),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await requireStreamFn(wrapCopilotAnthropicStream(baseStreamFn))(
|
||||
{ provider: "github-copilot", api: "anthropic-messages", id: "claude-sonnet-4.6" } as never,
|
||||
{ messages: [{ role: "user", content: "hi" }] } as never,
|
||||
{
|
||||
onPayload: (payload) => {
|
||||
const inPlace = hookType.endsWith("in-place");
|
||||
if (inPlace && payload && typeof payload === "object") {
|
||||
Object.assign(payload, replacement);
|
||||
}
|
||||
const result = inPlace ? undefined : replacement;
|
||||
return hookType.startsWith("async") ? Promise.resolve(result) : result;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(returnedPayload).toEqual({
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "replacement system prompt",
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: ["functions_read_0", "a_b_2", "a_b_3", "a_b", "a_b_4"].map((id) => ({
|
||||
type: "tool_use",
|
||||
id,
|
||||
name: "read",
|
||||
input: {},
|
||||
})),
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: ["functions_read_0", "a_b_2", "a_b_3", "a_b", "a_b_4"].map((tool_use_id) => ({
|
||||
type: "tool_result",
|
||||
tool_use_id,
|
||||
content: "done",
|
||||
})),
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("sends uniquely paired IDs through the actual Anthropic SDK and loopback HTTP", async () => {
|
||||
const requests: Array<{
|
||||
path: string;
|
||||
messages: Array<{ role: string; content: Array<Record<string, unknown>> | string }>;
|
||||
}> = [];
|
||||
const server = createServer((request, response) => {
|
||||
let body = "";
|
||||
request.setEncoding("utf8");
|
||||
request.on("data", (chunk: string) => {
|
||||
body += chunk;
|
||||
});
|
||||
request.on("end", () => {
|
||||
const payload = JSON.parse(body) as { messages: (typeof requests)[number]["messages"] };
|
||||
requests.push({ path: request.url ?? "", messages: payload.messages });
|
||||
response.writeHead(200, { "content-type": "text/event-stream" });
|
||||
response.write(
|
||||
"event: message_start\ndata: " +
|
||||
JSON.stringify({
|
||||
type: "message_start",
|
||||
message: { id: "msg_loopback", usage: { input_tokens: 1, output_tokens: 0 } },
|
||||
}) +
|
||||
"\n\nevent: message_stop\ndata: " +
|
||||
JSON.stringify({ type: "message_stop" }) +
|
||||
"\n\n",
|
||||
);
|
||||
response.end();
|
||||
});
|
||||
});
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
server.off("error", reject);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
const address = server.address() as AddressInfo;
|
||||
const longPrefix = "x".repeat(64);
|
||||
const sourceIds = ["a.b", "a:b", "a_b", `${longPrefix}first`, `${longPrefix}second`];
|
||||
const model = {
|
||||
id: "claude-sonnet-4-6",
|
||||
name: "Claude Sonnet 4.6",
|
||||
provider: "github-copilot",
|
||||
api: "anthropic-messages",
|
||||
baseUrl: `http://127.0.0.1:${address.port}`,
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 200_000,
|
||||
maxTokens: 4_096,
|
||||
} satisfies Model<"anthropic-messages">;
|
||||
const context = {
|
||||
messages: [
|
||||
{ role: "user", content: "Use each tool", timestamp: 1 },
|
||||
{
|
||||
role: "assistant",
|
||||
provider: "github-copilot",
|
||||
api: "anthropic-messages",
|
||||
model: model.id,
|
||||
content: sourceIds.map((id) => ({
|
||||
type: "toolCall" as const,
|
||||
id,
|
||||
name: "read",
|
||||
arguments: {},
|
||||
})),
|
||||
usage: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 0,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
},
|
||||
stopReason: "toolUse",
|
||||
timestamp: 2,
|
||||
},
|
||||
...sourceIds.map((toolCallId, index) => ({
|
||||
role: "toolResult" as const,
|
||||
toolCallId,
|
||||
toolName: "read",
|
||||
content: [{ type: "text" as const, text: `result ${index}` }],
|
||||
isError: false,
|
||||
timestamp: index + 3,
|
||||
})),
|
||||
],
|
||||
} satisfies Context;
|
||||
|
||||
try {
|
||||
const stream = await requireStreamFn(wrapCopilotAnthropicStream(streamSimple))(
|
||||
model,
|
||||
context,
|
||||
{ apiKey: "copilot-token", maxRetries: 0 },
|
||||
);
|
||||
const result = await stream.result();
|
||||
expect(result.stopReason).toBe("stop");
|
||||
} finally {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
});
|
||||
}
|
||||
|
||||
expect(requests).toHaveLength(1);
|
||||
expect(requests[0]?.path).toBe("/v1/messages");
|
||||
const wireBlocks = requests[0]?.messages.flatMap((message) =>
|
||||
Array.isArray(message.content) ? message.content : [],
|
||||
);
|
||||
const toolUseIds = wireBlocks
|
||||
?.filter((block) => block.type === "tool_use")
|
||||
.map((block) => block.id);
|
||||
const toolResultIds = wireBlocks
|
||||
?.filter((block) => block.type === "tool_result")
|
||||
.map((block) => block.tool_use_id);
|
||||
const expectedIds = ["a_b_2", "a_b_3", "a_b", longPrefix, `${"x".repeat(62)}_2`];
|
||||
expect(toolUseIds).toEqual(expectedIds);
|
||||
expect(toolResultIds).toEqual(expectedIds);
|
||||
expect(new Set(toolUseIds).size).toBe(expectedIds.length);
|
||||
});
|
||||
|
||||
it("adds Copilot headers, strips thinking replay, and marks cache for Claude payloads", () => {
|
||||
const payloads: Array<{
|
||||
messages: Array<Record<string, unknown>>;
|
||||
@@ -173,6 +556,39 @@ describe("wrapCopilotAnthropicStream", () => {
|
||||
expect(baseStreamFn.mock.calls).toEqual([[model, context, options]]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ provider: "anthropic", id: "claude-sonnet-4-6", toolId: "toolu_native_123" },
|
||||
{ provider: "kimi", id: "k2p5", toolId: "functions.read:0" },
|
||||
])("does not patch unrelated $provider Anthropic streams", (model) => {
|
||||
const payload = {
|
||||
messages: [
|
||||
{ role: "assistant", content: [{ type: "tool_use", id: model.toolId }] },
|
||||
{ role: "user", content: [{ type: "tool_result", tool_use_id: model.toolId }] },
|
||||
],
|
||||
};
|
||||
const baseStreamFn = vi.fn((streamModel, _context, streamOptions) => {
|
||||
streamOptions?.onPayload?.(payload, streamModel);
|
||||
return { async *[Symbol.asyncIterator]() {} } as never;
|
||||
});
|
||||
const wrapped = requireStreamFn(wrapCopilotAnthropicStream(baseStreamFn));
|
||||
const streamModel = {
|
||||
provider: model.provider,
|
||||
id: model.id,
|
||||
api: "anthropic-messages",
|
||||
} as never;
|
||||
const context = { messages: [{ role: "user", content: "hi" }] } as never;
|
||||
const options = { headers: { Existing: "1" }, onPayload: vi.fn() };
|
||||
|
||||
void wrapped(streamModel, context, options as never);
|
||||
|
||||
expect(baseStreamFn.mock.calls).toEqual([[streamModel, context, options]]);
|
||||
expect(payload.messages[0]?.content[0]).toEqual({ type: "tool_use", id: model.toolId });
|
||||
expect(payload.messages[1]?.content[0]).toEqual({
|
||||
type: "tool_result",
|
||||
tool_use_id: model.toolId,
|
||||
});
|
||||
});
|
||||
|
||||
it("adds Copilot headers, sanitizes reasoning replay, and rewrites message IDs before payload send", () => {
|
||||
const reasoningId = Buffer.from(`reasoning-${"x".repeat(24)}`).toString("base64");
|
||||
const overlongReasoningId = `5PX6gLHXT5wE+Y2tPmUV4gn+${"B".repeat(384)}`;
|
||||
|
||||
@@ -57,14 +57,18 @@ function buildCopilotDynamicHeaders(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function patchOnPayloadResult(result: unknown): unknown {
|
||||
function patchOnPayloadResult(
|
||||
result: unknown,
|
||||
patchPayload: (payload: unknown) => unknown = sanitizeCopilotReplayResponsePayload,
|
||||
fallbackPayload?: unknown,
|
||||
): unknown {
|
||||
if (result && typeof result === "object" && "then" in result) {
|
||||
return Promise.resolve(result).then((next) => {
|
||||
sanitizeCopilotReplayResponsePayload(next);
|
||||
patchPayload(next === undefined ? fallbackPayload : next);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
sanitizeCopilotReplayResponsePayload(result);
|
||||
patchPayload(result === undefined ? fallbackPayload : result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -81,9 +85,102 @@ function buildCopilotRequestHeaders(
|
||||
};
|
||||
}
|
||||
|
||||
type CopilotAnthropicToolBlock = {
|
||||
record: Record<string, unknown>;
|
||||
idKey: "id" | "tool_use_id";
|
||||
rawId: string;
|
||||
};
|
||||
|
||||
function normalizeCopilotAnthropicToolIds(messages: unknown[]): void {
|
||||
const blocks: CopilotAnthropicToolBlock[] = [];
|
||||
for (const message of messages) {
|
||||
if (!message || typeof message !== "object") {
|
||||
continue;
|
||||
}
|
||||
const content = (message as { content?: unknown }).content;
|
||||
if (!Array.isArray(content)) {
|
||||
continue;
|
||||
}
|
||||
for (const block of content) {
|
||||
if (!block || typeof block !== "object") {
|
||||
continue;
|
||||
}
|
||||
const record = block as Record<string, unknown>;
|
||||
const idKey =
|
||||
record.type === "tool_use" ? "id" : record.type === "tool_result" ? "tool_use_id" : null;
|
||||
const rawId = idKey ? record[idKey] : undefined;
|
||||
if (idKey && typeof rawId === "string") {
|
||||
blocks.push({ record, idKey, rawId });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reserve valid IDs globally so an earlier invalid call cannot steal the ID
|
||||
// of a later native call; replaying this payload patch must also be stable.
|
||||
const validId = /^[a-zA-Z0-9_-]{1,64}$/;
|
||||
const reserved = new Set(
|
||||
blocks
|
||||
.filter((block) => block.idKey === "id" && validId.test(block.rawId))
|
||||
.map((block) => block.rawId),
|
||||
);
|
||||
const used = new Set(reserved);
|
||||
const claimedValid = new Set<string>();
|
||||
const pendingByRawId = new Map<string, string[]>();
|
||||
const lastResolvedByRawId = new Map<string, string>();
|
||||
|
||||
const allocate = (rawId: string): string => {
|
||||
if (validId.test(rawId) && !claimedValid.has(rawId)) {
|
||||
claimedValid.add(rawId);
|
||||
return rawId;
|
||||
}
|
||||
|
||||
const base = rawId.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64) || "tool";
|
||||
if (!used.has(base)) {
|
||||
used.add(base);
|
||||
return base;
|
||||
}
|
||||
|
||||
for (let occurrence = 2; ; occurrence += 1) {
|
||||
const suffix = `_${occurrence}`;
|
||||
const candidate = `${base.slice(0, 64 - suffix.length)}${suffix}`;
|
||||
if (!used.has(candidate)) {
|
||||
used.add(candidate);
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const block of blocks) {
|
||||
if (block.idKey === "id") {
|
||||
const wireId = allocate(block.rawId);
|
||||
const pending = pendingByRawId.get(block.rawId);
|
||||
if (pending) {
|
||||
pending.push(wireId);
|
||||
} else {
|
||||
pendingByRawId.set(block.rawId, [wireId]);
|
||||
}
|
||||
block.record.id = wireId;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Upstream projection can collapse distinct raw calls to the same string;
|
||||
// consume occurrences in order so each result answers its own tool call.
|
||||
const pending = pendingByRawId.get(block.rawId);
|
||||
const wireId =
|
||||
pending?.shift() ?? lastResolvedByRawId.get(block.rawId) ?? allocate(block.rawId);
|
||||
if (pending?.length === 0) {
|
||||
pendingByRawId.delete(block.rawId);
|
||||
}
|
||||
lastResolvedByRawId.set(block.rawId, wireId);
|
||||
block.record.tool_use_id = wireId;
|
||||
}
|
||||
}
|
||||
|
||||
function patchCopilotAnthropicPayload(payload: Record<string, unknown>): void {
|
||||
if (Array.isArray(payload.messages)) {
|
||||
payload.messages = stripCopilotAssistantThinkingMessages(payload.messages);
|
||||
const messages = stripCopilotAssistantThinkingMessages(payload.messages);
|
||||
payload.messages = messages;
|
||||
normalizeCopilotAnthropicToolIds(messages);
|
||||
}
|
||||
applyAnthropicEphemeralCacheControlMarkers(payload);
|
||||
}
|
||||
@@ -100,6 +197,7 @@ export function wrapCopilotAnthropicStream(
|
||||
return underlying(model, context, options);
|
||||
}
|
||||
|
||||
const originalOnPayload = options?.onPayload;
|
||||
return streamWithPayloadPatch(
|
||||
underlying,
|
||||
model,
|
||||
@@ -107,6 +205,16 @@ export function wrapCopilotAnthropicStream(
|
||||
{
|
||||
...options,
|
||||
headers: buildCopilotRequestHeaders(context, options?.headers),
|
||||
onPayload: (payload, payloadModel) =>
|
||||
patchOnPayloadResult(
|
||||
originalOnPayload?.(payload, payloadModel),
|
||||
(replacement) => {
|
||||
if (replacement && typeof replacement === "object") {
|
||||
patchCopilotAnthropicPayload(replacement as Record<string, unknown>);
|
||||
}
|
||||
},
|
||||
payload,
|
||||
),
|
||||
},
|
||||
patchCopilotAnthropicPayload,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user