/** * KHAELOR * File: tests/anthropic/live.integration.test.ts * Description: Live Anthropic API integration — skipped unless KHAELOR_LIVE_TESTS=1 and ANTHROPIC_API_KEY are set. * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { describe, expect, it } from "vitest"; import { AnthropicModelClient } from "../../src/anthropic/client.js"; import type { ModelEvent, ModelRequest } from "../../src/anthropic/types.js"; const API_KEY = process.env["ANTHROPIC_API_KEY"]; const LIVE = process.env["KHAELOR_LIVE_TESTS"] === "1" && API_KEY !== undefined && API_KEY !== ""; const describeLive = LIVE ? describe : describe.skip; const MODEL = "claude-sonnet-5"; async function collect(iterable: AsyncIterable): Promise { const out: ModelEvent[] = []; for await (const event of iterable) out.push(event); return out; } describeLive("live Anthropic streaming (KHAELOR_LIVE_TESTS=1)", () => { it( "streams a tiny completion with incremental text deltas and real usage", async () => { const client = new AnthropicModelClient({ apiKey: API_KEY as string }); const request: ModelRequest = { model: MODEL, system: [{ name: "identity", text: "You are a terse assistant. Reply in plain text." }], messages: [ { role: "user", content: [{ type: "text", text: "Count from 1 to 8, digits separated by spaces." }], }, ], tools: [], maxOutputTokens: 64, thinking: { mode: "disabled" }, }; const events = await collect(client.stream(request)); expect(events[0]?.type).toBe("started"); const deltas = events.filter((e) => e.type === "text-delta"); // Incremental streaming: text arrives across multiple deltas, not one blob. expect(deltas.length).toBeGreaterThanOrEqual(2); const settled = events.find((e) => e.type === "text-block-completed"); if (settled?.type !== "text-block-completed") throw new Error("no settled text block"); expect(settled.text).toBe(deltas.map((d) => (d.type === "text-delta" ? d.text : "")).join("")); const completed = events.at(-1); if (completed?.type !== "completed") throw new Error("no completed event"); // Real token counts from the API — never zero for a real request/response. expect(completed.usage.inputTokens).toBeGreaterThan(0); expect(completed.usage.outputTokens).toBeGreaterThan(0); expect(completed.durationMs).toBeGreaterThan(0); expect(["end_turn", "max_tokens"]).toContain(completed.stopReason); }, 60_000, ); it( "streams a tool call whose input accumulates to valid JSON", async () => { const client = new AnthropicModelClient({ apiKey: API_KEY as string }); const request: ModelRequest = { model: MODEL, system: [{ name: "identity", text: "Answer using the provided tool." }], messages: [ { role: "user", content: [ { type: "text", text: "What is the weather in Paris right now? You must call the get_weather tool.", }, ], }, ], tools: [ { name: "get_weather", description: "Get the current weather for a city.", inputSchema: { type: "object", properties: { city: { type: "string", description: "City name" } }, required: ["city"], }, }, ], maxOutputTokens: 96, thinking: { mode: "disabled" }, }; const events = await collect(client.stream(request)); const started = events.find((e) => e.type === "tool-call-started"); if (started?.type !== "tool-call-started") throw new Error("no tool-call-started event"); expect(started.toolName).toBe("get_weather"); const completed = events.find((e) => e.type === "tool-call-completed"); if (completed?.type !== "tool-call-completed") throw new Error("no tool-call-completed event"); expect(completed.toolUseId).toBe(started.toolUseId); // The accumulated input_json_delta chunks parsed into a valid object. const input = completed.input as { city?: unknown }; expect(typeof input).toBe("object"); expect(typeof input.city).toBe("string"); expect((input.city as string).toLowerCase()).toContain("paris"); const finished = events.at(-1); if (finished?.type !== "completed") throw new Error("no completed event"); expect(finished.stopReason).toBe("tool_use"); expect(finished.usage.outputTokens).toBeGreaterThan(0); }, 60_000, ); });