Files
pi_harness/packages/ai/test/lazy-module-load.test.ts
T
Mario Zechner ba93da9a93 feat(ai): move API implementations to src/api with lazy wrappers (phase 2)
Stream implementations move from src/providers/ to src/api/, renamed by
API id (anthropic.ts -> anthropic-messages.ts, google.ts ->
google-generative-ai.ts, mistral.ts -> mistral-conversations.ts,
amazon-bedrock.ts -> bedrock-converse-stream.ts). Every module now
exports exactly stream/streamSimple; shared helpers move alongside.

New ProviderStreams dispatch contract in types.ts, lazyApi() wrapper in
api/lazy.ts, and one .lazy.ts wrapper per API. Bedrock's wrapper keeps
the node-only variable-specifier import and setBedrockProviderModule()
(now taking ProviderStreams).

providers/register-builtins.ts deleted; interim until the compat
entrypoint lands, builtin api-registry registration lives in stream.ts
and lazy wrappers are exported from the root barrel. Old per-API lazy
exports (streamAnthropic, ...) are gone; package.json subpaths retarget
to dist/api/.
2026-06-10 20:08:59 +02:00

100 lines
2.8 KiB
TypeScript

import { spawnSync } from "node:child_process";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const aiEntryUrl = new URL("../src/index.ts", import.meta.url).href;
const SDK_SPECIFIERS = [
"@anthropic-ai/sdk",
"openai",
"@google/genai",
"@mistralai/mistralai",
"@aws-sdk/client-bedrock-runtime",
] as const;
type ProbeResult = {
loadedSpecifiers: string[];
};
function runProbe(action: string): ProbeResult {
const script = `
import { registerHooks } from "node:module";
const targets = new Set(${JSON.stringify(SDK_SPECIFIERS)});
const loaded = [];
registerHooks({
resolve(specifier, context, nextResolve) {
if (targets.has(specifier)) {
loaded.push(specifier);
}
return nextResolve(specifier, context);
},
});
const mod = await import(${JSON.stringify(aiEntryUrl)});
${action}
console.log(JSON.stringify({ loadedSpecifiers: [...new Set(loaded)] }));
`;
const result = spawnSync(process.execPath, ["--input-type=module", "--eval", script], {
cwd: packageRoot,
encoding: "utf8",
});
if (result.status !== 0) {
throw new Error(`Probe failed (exit ${result.status})\nSTDOUT:\n${result.stdout}\nSTDERR:\n${result.stderr}`);
}
const stdoutLines = result.stdout
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0);
const lastLine = stdoutLines.at(-1);
if (!lastLine) {
throw new Error(`Probe produced no output\nSTDERR:\n${result.stderr}`);
}
return JSON.parse(lastLine) as ProbeResult;
}
describe("lazy provider module loading", () => {
it("does not load provider SDKs when importing the root barrel", () => {
const result = runProbe("");
expect(result.loadedSpecifiers).toEqual([]);
});
it("loads only the Anthropic SDK when streaming through the lazy API wrapper", () => {
const result = runProbe(`
const model = {
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4",
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 8192,
};
const context = { messages: [{ role: "user", content: "hi" }] };
await mod.anthropicMessagesApi().streamSimple(model, context).result();
`);
expect(result.loadedSpecifiers).toEqual(["@anthropic-ai/sdk"]);
});
it("loads only the Anthropic SDK when dispatching through streamSimple", () => {
const result = runProbe(`
const model = mod.getModel("anthropic", "claude-sonnet-4-6");
const context = { messages: [{ role: "user", content: "hi" }] };
await mod.streamSimple(model, context).result();
`);
expect(result.loadedSpecifiers).toEqual(["@anthropic-ai/sdk"]);
});
});