import type Anthropic from "@anthropic-ai/sdk"; import { Type } from "typebox"; import { describe, expect, it } from "vitest"; import { getModel } from "../src/models.ts"; import { streamAnthropic } from "../src/providers/anthropic.ts"; import type { Context, ToolCall } from "../src/types.ts"; 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" }, }); } const minimalAnthropicEvents = [ { 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: "text", text: "" }, }), }, { event: "content_block_delta", data: JSON.stringify({ type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "Hello" }, }), }, { 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: "end_turn" }, 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" }), }, ]; 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", }); }); it("ignores unknown SSE events after message_stop", async () => { const model = getModel("anthropic", "claude-haiku-4-5"); const context: Context = { messages: [{ role: "user", content: "Say hello.", timestamp: Date.now() }], }; const response = createSseResponse([ ...minimalAnthropicEvents, { event: "done", data: "[DONE]" }, { event: "proxy.stats", data: "not json" }, ]); const stream = streamAnthropic(model, context, { client: createFakeAnthropicClient(response), }); const result = await stream.result(); expect(result.stopReason).toBe("stop"); expect(result.errorMessage).toBeUndefined(); expect(result.content).toEqual([{ type: "text", text: "Hello" }]); }); });