SPB Git

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%
4.6 KB · 124 lines typescript
Raw Blame History
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