spb/khaelor Public
KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.
TypeScript 82.9%
HTML 14.9%
CSS 1.1%
JavaScript 0.7%
1/**2 * KHAELOR3 * File: tests/anthropic/live.integration.test.ts4 * Description: Live Anthropic API integration — skipped unless KHAELOR_LIVE_TESTS=1 and ANTHROPIC_API_KEY are set.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { describe, expect, it } from "vitest";11import { AnthropicModelClient } from "../../src/anthropic/client.js";12import type { ModelEvent, ModelRequest } from "../../src/anthropic/types.js";1314const API_KEY = process.env["ANTHROPIC_API_KEY"];15const LIVE = process.env["KHAELOR_LIVE_TESTS"] === "1" && API_KEY !== undefined && API_KEY !== "";1617const describeLive = LIVE ? describe : describe.skip;1819const MODEL = "claude-sonnet-5";2021async function collect(iterable: AsyncIterable<ModelEvent>): Promise<ModelEvent[]> {22 const out: ModelEvent[] = [];23 for await (const event of iterable) out.push(event);24 return out;25}2627describeLive("live Anthropic streaming (KHAELOR_LIVE_TESTS=1)", () => {28 it(29 "streams a tiny completion with incremental text deltas and real usage",30 async () => {31 const client = new AnthropicModelClient({ apiKey: API_KEY as string });32 const request: ModelRequest = {33 model: MODEL,34 system: [{ name: "identity", text: "You are a terse assistant. Reply in plain text." }],35 messages: [36 {37 role: "user",38 content: [{ type: "text", text: "Count from 1 to 8, digits separated by spaces." }],39 },40 ],41 tools: [],42 maxOutputTokens: 64,43 thinking: { mode: "disabled" },44 };4546 const events = await collect(client.stream(request));4748 expect(events[0]?.type).toBe("started");49 const deltas = events.filter((e) => e.type === "text-delta");50 // Incremental streaming: text arrives across multiple deltas, not one blob.51 expect(deltas.length).toBeGreaterThanOrEqual(2);5253 const settled = events.find((e) => e.type === "text-block-completed");54 if (settled?.type !== "text-block-completed") throw new Error("no settled text block");55 expect(settled.text).toBe(deltas.map((d) => (d.type === "text-delta" ? d.text : "")).join(""));5657 const completed = events.at(-1);58 if (completed?.type !== "completed") throw new Error("no completed event");59 // Real token counts from the API — never zero for a real request/response.60 expect(completed.usage.inputTokens).toBeGreaterThan(0);61 expect(completed.usage.outputTokens).toBeGreaterThan(0);62 expect(completed.durationMs).toBeGreaterThan(0);63 expect(["end_turn", "max_tokens"]).toContain(completed.stopReason);64 },65 60_000,66 );6768 it(69 "streams a tool call whose input accumulates to valid JSON",70 async () => {71 const client = new AnthropicModelClient({ apiKey: API_KEY as string });72 const request: ModelRequest = {73 model: MODEL,74 system: [{ name: "identity", text: "Answer using the provided tool." }],75 messages: [76 {77 role: "user",78 content: [79 {80 type: "text",81 text: "What is the weather in Paris right now? You must call the get_weather tool.",82 },83 ],84 },85 ],86 tools: [87 {88 name: "get_weather",89 description: "Get the current weather for a city.",90 inputSchema: {91 type: "object",92 properties: { city: { type: "string", description: "City name" } },93 required: ["city"],94 },95 },96 ],97 maxOutputTokens: 96,98 thinking: { mode: "disabled" },99 };100101 const events = await collect(client.stream(request));102103 const started = events.find((e) => e.type === "tool-call-started");104 if (started?.type !== "tool-call-started") throw new Error("no tool-call-started event");105 expect(started.toolName).toBe("get_weather");106107 const completed = events.find((e) => e.type === "tool-call-completed");108 if (completed?.type !== "tool-call-completed") throw new Error("no tool-call-completed event");109 expect(completed.toolUseId).toBe(started.toolUseId);110 // The accumulated input_json_delta chunks parsed into a valid object.111 const input = completed.input as { city?: unknown };112 expect(typeof input).toBe("object");113 expect(typeof input.city).toBe("string");114 expect((input.city as string).toLowerCase()).toContain("paris");115116 const finished = events.at(-1);117 if (finished?.type !== "completed") throw new Error("no completed event");118 expect(finished.stopReason).toBe("tool_use");119 expect(finished.usage.outputTokens).toBeGreaterThan(0);120 },121 60_000,122 );123});124