mirror of
https://github.com/earendil-works/pi.git
synced 2026-06-18 15:54:04 +08:00
8a0903ebf2
The root barrel is now core-only and side-effect free: types, createModels/createProvider, auth substrate, lazyStream/lazyApi, faux, utils. Generated catalogs, api-registry, env-api-keys, images, global stream functions, and per-API lazy wrappers leave the root. New @earendil-works/pi-ai/compat preserves the old surface verbatim as a strict superset of the root: api-dispatch stream/complete with env key injection, the builtin registration side effect (skip-if-present so it cannot clobber earlier overrides), deprecated getModel/getModels/ getProviders aliases of the new getBuiltin* reads in providers/all, lazy api wrappers + setBedrockProviderModule, and image generation. Compat dies with the coding-agent ModelManager migration. Packaging: exports map gains ./compat, ./providers/*, ./api/*; sideEffects array lists only the effectful modules. Old-global imports across agent/coding-agent/examples and pi-ai tests switch to /compat (path-only; compat is a superset). The coding-agent extension loader resolves the pi-ai ROOT specifier to compat, so existing user extensions using the old global API keep working at runtime until compat is removed. vitest configs alias /compat to src; browser smoke imports old globals from /compat.
108 lines
3.4 KiB
TypeScript
108 lines
3.4 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { stream as streamAnthropic } from "../src/api/anthropic-messages.ts";
|
|
import { getModel } from "../src/compat.ts";
|
|
import type { Context } from "../src/types.ts";
|
|
|
|
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.6");
|
|
expect(model.api).toBe("anthropic-messages");
|
|
|
|
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.6");
|
|
expect(params.stream).toBe(true);
|
|
expect(params.max_tokens).toBe(model.maxTokens);
|
|
expect(Array.isArray(params.messages)).toBe(true);
|
|
});
|
|
|
|
it("omits interleaved-thinking beta for adaptive-thinking models", async () => {
|
|
const model = getModel("github-copilot", "claude-sonnet-4.6");
|
|
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"] ?? "").not.toContain("interleaved-thinking-2025-05-14");
|
|
});
|
|
});
|