4b926a30a2
Bypass the Anthropic SDK streaming parser entirely. Use client.messages.create().asResponse() and decode the SSE stream ourselves with defensive JSON parsing that repairs invalid escape sequences and control characters inside string literals. - Switch from SDK .stream() to .asResponse() + pi-owned SSE decoder - Add repairJson() / parseJsonWithRepair() to json-parse.ts - Add anthropic-sse-parsing.test.ts regression for malformed tool deltas - Update github-copilot-anthropic.test.ts mock to match new call path - Update deprecated claude-3-5-haiku-20241022 refs to claude-haiku-4-5 - Remove stale non-reasoning model test fixes #3175
109 lines
3.5 KiB
TypeScript
109 lines
3.5 KiB
TypeScript
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<string, unknown> | undefined,
|
|
createParams: undefined as Record<string, unknown> | undefined,
|
|
}));
|
|
|
|
vi.mock("@anthropic-ai/sdk", () => {
|
|
function createSseResponse(): Response {
|
|
const body = [
|
|
`event: message_start\ndata: ${JSON.stringify({
|
|
type: "message_start",
|
|
message: {
|
|
id: "msg_test",
|
|
usage: { input_tokens: 10, output_tokens: 0 },
|
|
},
|
|
})}\n`,
|
|
`event: message_delta\ndata: ${JSON.stringify({
|
|
type: "message_delta",
|
|
delta: { stop_reason: "end_turn" },
|
|
usage: { output_tokens: 5 },
|
|
})}\n`,
|
|
].join("\n");
|
|
|
|
return new Response(body, {
|
|
status: 200,
|
|
headers: { "content-type": "text/event-stream" },
|
|
});
|
|
}
|
|
|
|
class FakeAnthropic {
|
|
constructor(opts: Record<string, unknown>) {
|
|
mockState.constructorOpts = opts;
|
|
}
|
|
messages = {
|
|
create: (params: Record<string, unknown>) => {
|
|
mockState.createParams = params;
|
|
return {
|
|
asResponse: async () => createSseResponse(),
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
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<string, string>;
|
|
|
|
// 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.createParams!;
|
|
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<string, string>;
|
|
expect(headers["anthropic-beta"]).toContain("interleaved-thinking-2025-05-14");
|
|
});
|
|
});
|