4b926a30a2
Bypass the Anthropic SDK streaming parser entirely. Use client.messages.create().asResponse() and decode the SSE stream ourselves with defensive JSON parsing that repairs invalid escape sequences and control characters inside string literals. - Switch from SDK .stream() to .asResponse() + pi-owned SSE decoder - Add repairJson() / parseJsonWithRepair() to json-parse.ts - Add anthropic-sse-parsing.test.ts regression for malformed tool deltas - Update github-copilot-anthropic.test.ts mock to match new call path - Update deprecated claude-3-5-haiku-20241022 refs to claude-haiku-4-5 - Remove stale non-reasoning model test fixes #3175
114 lines
3.0 KiB
TypeScript
114 lines
3.0 KiB
TypeScript
import type Anthropic from "@anthropic-ai/sdk";
|
|
import { Type } from "@sinclair/typebox";
|
|
import { describe, expect, it } from "vitest";
|
|
import { getModel } from "../src/models.js";
|
|
import { streamAnthropic } from "../src/providers/anthropic.js";
|
|
import type { Context, ToolCall } from "../src/types.js";
|
|
|
|
function createSseResponse(events: Array<{ event: string; data: string }>): Response {
|
|
const body = events.map(({ event, data }) => `event: ${event}\ndata: ${data}\n`).join("\n");
|
|
return new Response(body, {
|
|
status: 200,
|
|
headers: { "content-type": "text/event-stream" },
|
|
});
|
|
}
|
|
|
|
function createFakeAnthropicClient(response: Response): Anthropic {
|
|
return {
|
|
messages: {
|
|
create: () => ({
|
|
asResponse: async () => response,
|
|
}),
|
|
},
|
|
} as unknown as Anthropic;
|
|
}
|
|
|
|
describe("Anthropic raw SSE parsing", () => {
|
|
it("repairs malformed SSE JSON and malformed streamed tool JSON", async () => {
|
|
const model = getModel("anthropic", "claude-haiku-4-5");
|
|
const context: Context = {
|
|
messages: [{ role: "user", content: "Use the edit tool.", timestamp: Date.now() }],
|
|
tools: [
|
|
{
|
|
name: "edit",
|
|
description: "Edit a file.",
|
|
parameters: Type.Object({
|
|
path: Type.String(),
|
|
text: Type.String(),
|
|
}),
|
|
},
|
|
],
|
|
};
|
|
|
|
const malformedToolJsonDelta = String.raw`{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"{\"path\":\"A\H\",\"text\":\"col1 col2\"}"}}`;
|
|
|
|
const response = createSseResponse([
|
|
{
|
|
event: "message_start",
|
|
data: JSON.stringify({
|
|
type: "message_start",
|
|
message: {
|
|
id: "msg_test",
|
|
usage: {
|
|
input_tokens: 12,
|
|
output_tokens: 0,
|
|
cache_read_input_tokens: 0,
|
|
cache_creation_input_tokens: 0,
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
{
|
|
event: "content_block_start",
|
|
data: JSON.stringify({
|
|
type: "content_block_start",
|
|
index: 0,
|
|
content_block: {
|
|
type: "tool_use",
|
|
id: "toolu_test",
|
|
name: "edit",
|
|
input: {},
|
|
},
|
|
}),
|
|
},
|
|
{ event: "content_block_delta", data: malformedToolJsonDelta },
|
|
{
|
|
event: "content_block_stop",
|
|
data: JSON.stringify({ type: "content_block_stop", index: 0 }),
|
|
},
|
|
{
|
|
event: "message_delta",
|
|
data: JSON.stringify({
|
|
type: "message_delta",
|
|
delta: { stop_reason: "tool_use" },
|
|
usage: {
|
|
input_tokens: 12,
|
|
output_tokens: 5,
|
|
cache_read_input_tokens: 0,
|
|
cache_creation_input_tokens: 0,
|
|
},
|
|
}),
|
|
},
|
|
{
|
|
event: "message_stop",
|
|
data: JSON.stringify({ type: "message_stop" }),
|
|
},
|
|
]);
|
|
|
|
const stream = streamAnthropic(model, context, {
|
|
client: createFakeAnthropicClient(response),
|
|
});
|
|
const result = await stream.result();
|
|
|
|
expect(result.stopReason).toBe("toolUse");
|
|
expect(result.errorMessage).toBeUndefined();
|
|
|
|
const toolCall = result.content.find((block): block is ToolCall => block.type === "toolCall");
|
|
expect(toolCall).toBeDefined();
|
|
expect(toolCall?.arguments).toEqual({
|
|
path: "A\\H",
|
|
text: "col1\tcol2",
|
|
});
|
|
});
|
|
});
|