import { describe, expect, it, vi } from "vitest"; import { getModel } from "../src/models.js"; import type { Context } from "../src/types.js"; const mockState = vi.hoisted(() => ({ constructorOpts: undefined as Record | undefined, streamParams: undefined as Record | undefined, })); vi.mock("@anthropic-ai/sdk", () => { const fakeStream = { async *[Symbol.asyncIterator]() { yield { type: "message_start", message: { usage: { input_tokens: 10, output_tokens: 0 }, }, }; yield { type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 5 }, }; }, finalMessage: async () => ({ usage: { input_tokens: 10, output_tokens: 5, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 }, }), }; class FakeAnthropic { constructor(opts: Record) { mockState.constructorOpts = opts; } messages = { stream: (params: Record) => { mockState.streamParams = params; return fakeStream; }, }; } return { default: FakeAnthropic }; }); describe("Copilot Claude via Anthropic Messages", () => { const context: Context = { systemPrompt: "You are a helpful assistant.", messages: [{ role: "user", content: "Hello", timestamp: Date.now() }], }; it("uses Bearer auth, Copilot headers, and valid Anthropic Messages payload", async () => { const model = getModel("github-copilot", "claude-sonnet-4"); expect(model.api).toBe("anthropic-messages"); const { streamAnthropic } = await import("../src/providers/anthropic.js"); const s = streamAnthropic(model, context, { apiKey: "tid_copilot_session_test_token" }); for await (const event of s) { if (event.type === "error") break; } const opts = mockState.constructorOpts!; expect(opts).toBeDefined(); // Auth: apiKey null, authToken for Bearer expect(opts.apiKey).toBeNull(); expect(opts.authToken).toBe("tid_copilot_session_test_token"); const headers = opts.defaultHeaders as Record; // Copilot static headers from model.headers expect(headers["User-Agent"]).toContain("GitHubCopilotChat"); expect(headers["Copilot-Integration-Id"]).toBe("vscode-chat"); // Dynamic headers expect(headers["X-Initiator"]).toBe("user"); expect(headers["Openai-Intent"]).toBe("conversation-edits"); // No fine-grained-tool-streaming (Copilot doesn't support it) const beta = headers["anthropic-beta"] ?? ""; expect(beta).not.toContain("fine-grained-tool-streaming"); // Payload is valid Anthropic Messages format const params = mockState.streamParams!; expect(params.model).toBe("claude-sonnet-4"); expect(params.stream).toBe(true); expect(params.max_tokens).toBeGreaterThan(0); expect(Array.isArray(params.messages)).toBe(true); }); it("includes interleaved-thinking beta when reasoning is enabled", async () => { const model = getModel("github-copilot", "claude-sonnet-4"); const { streamAnthropic } = await import("../src/providers/anthropic.js"); const s = streamAnthropic(model, context, { apiKey: "tid_copilot_session_test_token", interleavedThinking: true, }); for await (const event of s) { if (event.type === "error") break; } const headers = mockState.constructorOpts!.defaultHeaders as Record; expect(headers["anthropic-beta"]).toContain("interleaved-thinking-2025-05-14"); }); });