import { createServer, type IncomingMessage } from "node:http";
import type { AddressInfo } from "node:net";
import { Type } from "typebox";
import { describe, expect, it } from "vitest";
import { getModel } from "../src/models.js";
import { type AnthropicOptions, streamAnthropic } from "../src/providers/anthropic.js";
import { streamSimple } from "../src/stream.js";
import type { Context, Model, SimpleStreamOptions, Tool } from "../src/types.js";
interface AnthropicThinkingPayload {
thinking?: { type: string; budget_tokens?: number; display?: string };
output_config?: { effort?: string };
temperature?: number;
}
function makePayloadCaptureContext(): Context {
return {
messages: [{ role: "user", content: "Hello", timestamp: Date.now() }],
};
}
async function capturePayload(
model: Model<"anthropic-messages">,
options?: SimpleStreamOptions,
): Promise<AnthropicThinkingPayload> {
let capturedPayload: AnthropicThinkingPayload | undefined;
const payloadCaptureModel: Model<"anthropic-messages"> = {
...model,
baseUrl: "http://127.0.0.1:9",
};
const s = streamSimple(payloadCaptureModel, makePayloadCaptureContext(), {
...options,
apiKey: "fake-key",
onPayload: (payload) => {
capturedPayload = payload as AnthropicThinkingPayload;
return payload;
},
});
await s.result();
if (!capturedPayload) {
throw new Error("Expected payload to be captured before request failure");
}
return capturedPayload;
}
describe("Anthropic thinking disable payload", () => {
it("sends thinking.type=disabled for budget-based reasoning models when thinking is off", async () => {
const payload = await capturePayload(getModel("anthropic", "claude-sonnet-4-5"));
expect(payload.thinking).toEqual({ type: "disabled" });
expect(payload.output_config).toBeUndefined();
});
it("sends thinking.type=disabled for adaptive reasoning models when thinking is off", async () => {
const payload = await capturePayload(getModel("anthropic", "claude-opus-4-6"));
expect(payload.thinking).toEqual({ type: "disabled" });
expect(payload.output_config).toBeUndefined();
});
it("sends thinking.type=disabled for Claude Opus 4.7 when thinking is off", async () => {
const payload = await capturePayload(getModel("anthropic", "claude-opus-4-7"));
expect(payload.thinking).toEqual({ type: "disabled" });
expect(payload.output_config).toBeUndefined();
});
it("uses adaptive thinking for Claude Opus 4.7 when reasoning is enabled", async () => {
const payload = await capturePayload(getModel("anthropic", "claude-opus-4-7"), { reasoning: "high" });
expect(payload.thinking).toEqual({ type: "adaptive", display: "summarized" });
expect(payload.output_config).toEqual({ effort: "high" });
});
it("maps xhigh reasoning to effort=xhigh for Claude Opus 4.7", async () => {
const payload = await capturePayload(getModel("anthropic", "claude-opus-4-7"), { reasoning: "xhigh" });
expect(payload.thinking).toEqual({ type: "adaptive", display: "summarized" });
expect(payload.output_config).toEqual({ effort: "xhigh" });
});
it("maps max reasoning to effort=max for Claude Opus 4.7", async () => {
const payload = await capturePayload(getModel("anthropic", "claude-opus-4-7"), { reasoning: "max" });
expect(payload.thinking).toEqual({ type: "adaptive", display: "summarized" });
expect(payload.output_config).toEqual({ effort: "max" });
});
it("maps max reasoning to effort=max for Claude Opus 4.6 (no native xhigh)", async () => {
const payload = await capturePayload(getModel("anthropic", "claude-opus-4-6"), { reasoning: "max" });
expect(payload.thinking).toEqual({ type: "adaptive", display: "summarized" });
expect(payload.output_config).toEqual({ effort: "max" });
});
it("clamps xhigh reasoning to effort=max for Claude Opus 4.6 (no native xhigh)", async () => {
const payload = await capturePayload(getModel("anthropic", "claude-opus-4-6"), { reasoning: "xhigh" });
expect(payload.output_config).toEqual({ effort: "max" });
});
it("maps max reasoning to effort=max for Claude Sonnet 4.6 (no native xhigh)", async () => {
const payload = await capturePayload(getModel("anthropic", "claude-sonnet-4-6"), { reasoning: "max" });
expect(payload.thinking).toEqual({ type: "adaptive", display: "summarized" });
expect(payload.output_config).toEqual({ effort: "max" });
});
it("omits the thinking param for Claude Fable 5 when reasoning is off (explicit disabled is a 400)", async () => {
const payload = await capturePayload(getModel("anthropic", "claude-fable-5"));
expect(payload.thinking).toBeUndefined();
expect(payload.output_config).toBeUndefined();
});
it("drops temperature for Claude Fable 5 (sampling params are rejected)", async () => {
const payload = await capturePayload(getModel("anthropic", "claude-fable-5"), { temperature: 0.5 });
expect(payload.temperature).toBeUndefined();
expect(payload.thinking).toBeUndefined();
});
it("uses adaptive thinking with effort=xhigh for Claude Fable 5", async () => {
const payload = await capturePayload(getModel("anthropic", "claude-fable-5"), { reasoning: "xhigh" });
expect(payload.thinking).toEqual({ type: "adaptive", display: "summarized" });
expect(payload.output_config).toEqual({ effort: "xhigh" });
});
it("maps max reasoning to effort=max for Claude Fable 5", async () => {
const payload = await capturePayload(getModel("anthropic", "claude-fable-5"), { reasoning: "max" });
expect(payload.thinking).toEqual({ type: "adaptive", display: "summarized" });
expect(payload.output_config).toEqual({ effort: "max" });
});
});
interface CapturedRequest {
headers: IncomingMessage["headers"];
body: Record<string, unknown>;
}
async function captureAnthropicRequest(
model: Model<"anthropic-messages">,
context: Context,
options: AnthropicOptions,
): Promise<CapturedRequest> {
let capturedRequest: CapturedRequest | undefined;
const server = createServer(async (request, response) => {
const chunks: Buffer[] = [];
for await (const chunk of request) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
capturedRequest = {
headers: request.headers,
body: JSON.parse(Buffer.concat(chunks).toString("utf8")) as Record<string, unknown>,
};
response.writeHead(200, { "content-type": "text/event-stream" });
response.end();
});
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const { port } = server.address() as AddressInfo;
try {
const s = streamAnthropic({ ...model, baseUrl: `http://127.0.0.1:${port}` }, context, options);
for await (const event of s) {
if (event.type === "done" || event.type === "error") break;
}
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
if (!capturedRequest) throw new Error("Anthropic request was not captured");
return capturedRequest;
}
function toolsOf(body: Record<string, unknown>): Array<Record<string, unknown>> {
return (body.tools ?? []) as Array<Record<string, unknown>>;
}
function tool(name: string): Tool {
return { name, description: `Tool ${name}`, parameters: Type.Object({ value: Type.String() }) };
}
const toolContext: Context = {
messages: [{ role: "user", content: "Use the tool", timestamp: 1 }],
tools: [tool("lookup")],
};
describe("Anthropic request wire contract", () => {
const testModel: Model<"anthropic-messages"> = {
...getModel("anthropic", "claude-opus-4-7"),
provider: "test-anthropic",
};
it.each([
{
name: "sends per-tool eager_input_streaming by default",
compat: undefined,
context: toolContext,
eager: true,
beta: undefined,
},
{
name: "uses the legacy fine-grained beta when eager tool input streaming is disabled",
compat: { supportsEagerToolInputStreaming: false },
context: toolContext,
eager: undefined,
beta: "fine-grained-tool-streaming-2025-05-14",
},
{
name: "omits the legacy fine-grained beta when there are no tools",
compat: { supportsEagerToolInputStreaming: false },
context: { messages: toolContext.messages } as Context,
eager: undefined,
beta: undefined,
},
])("$name", async ({ compat, context, eager, beta }) => {
const request = await captureAnthropicRequest({ ...testModel, compat }, context, {
apiKey: "test-key",
cacheRetention: "none",
});
expect(toolsOf(request.body)[0]?.eager_input_streaming).toBe(eager);
expect(request.headers["anthropic-beta"]).toBe(beta);
});
it("renames user tools to their Claude Code casing only for OAuth tokens", async () => {
const context: Context = {
messages: [{ role: "user", content: "Use the tools", timestamp: 1 }],
tools: [tool("todowrite"), tool("find"), tool("my_custom_tool")],
};
const oauth = await captureAnthropicRequest(getModel("anthropic", "claude-sonnet-4-6"), context, {
apiKey: "sk-ant-oat-fake-token",
cacheRetention: "none",
});
expect(toolsOf(oauth.body).map((entry) => entry.name)).toEqual(["TodoWrite", "find", "my_custom_tool"]);
const apiKey = await captureAnthropicRequest(getModel("anthropic", "claude-sonnet-4-6"), context, {
apiKey: "sk-ant-api-fake-token",
cacheRetention: "none",
});
expect(toolsOf(apiKey.body).map((entry) => entry.name)).toEqual(["todowrite", "find", "my_custom_tool"]);
});
it("sends Copilot bearer auth, Copilot headers, and a valid Anthropic Messages payload", async () => {
const model = getModel("github-copilot", "claude-sonnet-4.6");
expect(model.api).toBe("anthropic-messages");
const request = await captureAnthropicRequest(
model as Model<"anthropic-messages">,
{ systemPrompt: "You are a helpful assistant.", messages: [{ role: "user", content: "Hello", timestamp: 1 }] },
{ apiKey: "tid_copilot_session_test_token" },
);
expect(request.headers.authorization).toBe("Bearer tid_copilot_session_test_token");
expect(request.headers["x-api-key"]).toBeUndefined();
expect(request.headers["user-agent"]).toContain("GitHubCopilotChat");
expect(request.headers["copilot-integration-id"]).toBe("vscode-chat");
expect(request.headers["x-initiator"]).toBe("user");
expect(request.headers["openai-intent"]).toBe("conversation-edits");
expect(request.headers["anthropic-beta"] ?? "").not.toContain("fine-grained-tool-streaming");
expect(request.body.model).toBe("claude-sonnet-4.6");
expect(request.body.stream).toBe(true);
expect(request.body.max_tokens as number).toBeGreaterThan(0);
expect(Array.isArray(request.body.messages)).toBe(true);
});
it("includes the interleaved-thinking beta for non-adaptive Copilot Claude models", async () => {
const request = await captureAnthropicRequest(
getModel("github-copilot", "claude-haiku-4.5") as Model<"anthropic-messages">,
{ messages: [{ role: "user", content: "Hello", timestamp: 1 }] },
{ apiKey: "tid_copilot_session_test_token", interleavedThinking: true },
);
expect(request.headers["anthropic-beta"]).toContain("interleaved-thinking-2025-05-14");
});
});