import { expect, test } from "bun:test"; import type { ChatMessage } from "@stc/contracts"; import { createAnthropicProvider } from "../src/providers/anthropic"; import { collect, fakeFetch, sseBody } from "./helpers"; const ok = (events: [string | null, unknown][]) => () => new Response(sseBody(events), { headers: { "content-type": "text/event-stream" } }); const textTurn: [string, unknown][] = [ ["message_start", { type: "message_start", message: { id: "m1", content: [] } }], ["content_block_start", { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } }], ["ping", { type: "ping" }], ["content_block_delta", { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "Hello " } }], ["content_block_delta", { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "world" } }], ["content_block_stop", { type: "content_block_stop", index: 0 }], ["message_delta", { type: "message_delta", delta: { stop_reason: "end_turn" } }], ["message_stop", { type: "message_stop" }], ]; const toolTurn: [string, unknown][] = [ ["message_start", { type: "message_start", message: { id: "m2", content: [] } }], ["content_block_start", { type: "content_block_start", index: 0, content_block: { type: "thinking", thinking: "", signature: "" } }], ["content_block_delta", { type: "content_block_delta", index: 0, delta: { type: "thinking_delta", thinking: "" } }], ["content_block_delta", { type: "content_block_delta", index: 0, delta: { type: "signature_delta", signature: "SIG-abc" } }], ["content_block_stop", { type: "content_block_stop", index: 0 }], ["content_block_start", { type: "content_block_start", index: 1, content_block: { type: "text", text: "" } }], ["content_block_delta", { type: "content_block_delta", index: 1, delta: { type: "text_delta", text: "Let me look." } }], ["content_block_stop", { type: "content_block_stop", index: 1 }], ["content_block_start", { type: "content_block_start", index: 2, content_block: { type: "tool_use", id: "toolu_1", name: "search_course", input: {} } }], ["content_block_delta", { type: "content_block_delta", index: 2, delta: { type: "input_json_delta", partial_json: "{\"que" } }], ["content_block_delta", { type: "content_block_delta", index: 2, delta: { type: "input_json_delta", partial_json: "ry\": \"add\"}" } }], ["content_block_stop", { type: "content_block_stop", index: 2 }], ["message_delta", { type: "message_delta", delta: { stop_reason: "tool_use" } }], ["message_stop", { type: "message_stop" }], ]; test("streams text and sends a well-formed request", async () => { const ff = fakeFetch([ok(textTurn)]); const p = createAnthropicProvider({ apiKey: "sk-test", fetch: ff.fetch }); const msgs: ChatMessage[] = [{ role: "system", content: "You are kind." }, { role: "user", content: "Hi" }]; const r = await collect(p.chat(msgs, { tools: [{ name: "search_course", description: "Search", inputSchema: { type: "object", properties: {} } }] })); expect(r.text).toBe("Hello world"); const c = ff.calls[0]!; expect(c.url).toBe("https://api.anthropic.com/v1/messages"); const h = new Headers(c.init.headers); expect(h.get("x-api-key")).toBe("sk-test"); expect(h.get("anthropic-version")).toBe("2023-06-01"); expect(c.body.model).toBe("claude-opus-5-5"); expect(c.body.stream).toBe(true); expect(c.body.system).toBe("You are kind."); expect(c.body.messages).toEqual([{ role: "user", content: [{ type: "text", text: "Hi" }] }]); expect(c.body.tools).toEqual([{ name: "search_course", description: "Search", input_schema: { type: "object", properties: {} } }]); expect(c.body.thinking).toBeUndefined(); expect(c.body.temperature).toBeUndefined(); expect(c.body.tool_choice).toBeUndefined(); expect(c.body.fallbacks).toBe("default"); expect(h.get("anthropic-beta")).toBe("server-side-fallback-2026-07-01"); }); test("assembles tool input and replays thinking blocks unchanged on the next call", async () => { const ff = fakeFetch([ok(toolTurn), ok(textTurn)]); const p = createAnthropicProvider({ apiKey: "k", fetch: ff.fetch }); const msgs: ChatMessage[] = [{ role: "user", content: "How do I add?" }]; const first = await collect(p.chat(msgs, {})); expect(first.text).toBe("Let me look."); expect(first.toolCalls).toEqual([{ id: "toolu_1", name: "search_course", input: { query: "add" } }]); msgs.push({ role: "assistant", content: "Let me look." }); msgs.push({ role: "assistant", content: JSON.stringify({ query: "add" }), toolCallId: "toolu_1", name: "search_course" }); msgs.push({ role: "tool", content: "lessons/04-exercise.md: ...", toolCallId: "toolu_1", name: "search_course" }); await collect(p.chat(msgs, {})); const sent = ff.calls[1]!.body.messages; expect(sent).toHaveLength(3); expect(sent[1]).toEqual({ role: "assistant", content: [ { type: "thinking", thinking: "", signature: "SIG-abc" }, { type: "text", text: "Let me look." }, { type: "tool_use", id: "toolu_1", name: "search_course", input: { query: "add" } }, ], }); expect(sent[2]).toEqual({ role: "user", content: [{ type: "tool_result", tool_use_id: "toolu_1", content: "lessons/04-exercise.md: ..." }] }); }); test("tool calls it did not produce are mapped from the message fields", async () => { const ff = fakeFetch([ok(textTurn)]); const p = createAnthropicProvider({ apiKey: "k", fetch: ff.fetch }); await collect(p.chat([ { role: "user", content: "q" }, { role: "assistant", content: "{\"x\":1}", toolCallId: "t9", name: "list_notes" }, { role: "tool", content: "none", toolCallId: "t9" }, ], {})); expect(ff.calls[0]!.body.messages[1]).toEqual({ role: "assistant", content: [{ type: "tool_use", id: "t9", name: "list_notes", input: { x: 1 } }] }); }); test("errors carry the API message", async () => { const ff = fakeFetch([() => Response.json({ type: "error", error: { type: "authentication_error", message: "invalid x-api-key" } }, { status: 401 })]); const p = createAnthropicProvider({ apiKey: "bad", fetch: ff.fetch }); await expect(collect(p.chat([{ role: "user", content: "x" }], {}))).rejects.toThrow(/invalid x-api-key/); const ff2 = fakeFetch([ok([["error", { type: "error", error: { type: "overloaded_error", message: "Overloaded" } }]])]); await expect(collect(createAnthropicProvider({ apiKey: "k", fetch: ff2.fetch }).chat([{ role: "user", content: "x" }], {}))).rejects.toThrow(/Overloaded/); }); test("a refusal yields a plain sentence", async () => { const ff = fakeFetch([ok([["message_start", { type: "message_start", message: { content: [] } }], ["message_delta", { type: "message_delta", delta: { stop_reason: "refusal" } }], ["message_stop", { type: "message_stop" }]])]); const r = await collect(createAnthropicProvider({ apiKey: "k", fetch: ff.fetch }).chat([{ role: "user", content: "x" }], {})); expect(r.text).toContain("declined"); }); test("custom base URL, model and effort", async () => { const ff = fakeFetch([ok(textTurn)]); await collect(createAnthropicProvider({ apiKey: "k", baseUrl: "http://proxy.local/", model: "claude-sonnet-5-5", effort: "high", fetch: ff.fetch }).chat([{ role: "user", content: "x" }], {})); expect(ff.calls[0]!.url).toBe("http://proxy.local/v1/messages"); expect(ff.calls[0]!.body.model).toBe("claude-sonnet-5-5"); expect(ff.calls[0]!.body.output_config).toEqual({ effort: "high" }); expect(ff.calls[0]!.body.fallbacks).toBeUndefined(); });