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.
224 lines
6.3 KiB
TypeScript
224 lines
6.3 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const googleGenAiMock = vi.hoisted(() => ({
|
|
constructorCalls: [] as Array<Record<string, unknown>>,
|
|
}));
|
|
|
|
vi.mock("@google/genai", () => {
|
|
class GoogleGenAI {
|
|
models = {
|
|
generateContentStream: async function* () {
|
|
yield {
|
|
responseId: "vertex-response-id",
|
|
candidates: [
|
|
{
|
|
content: { parts: [{ text: "ok" }] },
|
|
finishReason: "STOP",
|
|
},
|
|
],
|
|
usageMetadata: {
|
|
promptTokenCount: 1,
|
|
candidatesTokenCount: 1,
|
|
totalTokenCount: 2,
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
constructor(config: Record<string, unknown>) {
|
|
googleGenAiMock.constructorCalls.push(config);
|
|
}
|
|
}
|
|
|
|
return {
|
|
GoogleGenAI,
|
|
ResourceScope: {
|
|
COLLECTION: "COLLECTION",
|
|
},
|
|
ThinkingLevel: {
|
|
THINKING_LEVEL_UNSPECIFIED: "THINKING_LEVEL_UNSPECIFIED",
|
|
MINIMAL: "MINIMAL",
|
|
LOW: "LOW",
|
|
MEDIUM: "MEDIUM",
|
|
HIGH: "HIGH",
|
|
},
|
|
};
|
|
});
|
|
|
|
import { stream as streamGoogleVertex } from "../src/api/google-vertex.ts";
|
|
import { getModel } from "../src/compat.ts";
|
|
import type { Context, Model } from "../src/types.ts";
|
|
|
|
const model = getModel("google-vertex", "gemini-3-flash-preview");
|
|
const context: Context = {
|
|
messages: [{ role: "user", content: "hello", timestamp: Date.now() }],
|
|
};
|
|
|
|
const originalGoogleCloudApiKey = process.env.GOOGLE_CLOUD_API_KEY;
|
|
|
|
beforeEach(() => {
|
|
googleGenAiMock.constructorCalls.length = 0;
|
|
delete process.env.GOOGLE_CLOUD_API_KEY;
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (originalGoogleCloudApiKey === undefined) {
|
|
delete process.env.GOOGLE_CLOUD_API_KEY;
|
|
} else {
|
|
process.env.GOOGLE_CLOUD_API_KEY = originalGoogleCloudApiKey;
|
|
}
|
|
});
|
|
|
|
describe("google-vertex api key resolution", () => {
|
|
it("falls back to ADC when options.apiKey is a placeholder marker", async () => {
|
|
const stream = streamGoogleVertex(model, context, {
|
|
apiKey: "<authenticated>",
|
|
project: "test-project",
|
|
location: "us-central1",
|
|
});
|
|
|
|
await stream.result();
|
|
|
|
expect(googleGenAiMock.constructorCalls).toHaveLength(1);
|
|
expect(googleGenAiMock.constructorCalls[0]).toMatchObject({
|
|
vertexai: true,
|
|
project: "test-project",
|
|
location: "us-central1",
|
|
apiVersion: "v1",
|
|
});
|
|
expect(googleGenAiMock.constructorCalls[0]).not.toHaveProperty("apiKey");
|
|
});
|
|
|
|
it("falls back to ADC when options.apiKey is the gcp-vertex-credentials marker", async () => {
|
|
const stream = streamGoogleVertex(model, context, {
|
|
apiKey: "gcp-vertex-credentials",
|
|
project: "test-project",
|
|
location: "us-central1",
|
|
});
|
|
|
|
await stream.result();
|
|
|
|
expect(googleGenAiMock.constructorCalls).toHaveLength(1);
|
|
expect(googleGenAiMock.constructorCalls[0]).toMatchObject({
|
|
vertexai: true,
|
|
project: "test-project",
|
|
location: "us-central1",
|
|
apiVersion: "v1",
|
|
});
|
|
expect(googleGenAiMock.constructorCalls[0]).not.toHaveProperty("apiKey");
|
|
});
|
|
|
|
it("falls back to ADC when GOOGLE_CLOUD_API_KEY is a placeholder marker", async () => {
|
|
process.env.GOOGLE_CLOUD_API_KEY = "<authenticated>";
|
|
|
|
const stream = streamGoogleVertex(model, context, {
|
|
project: "test-project",
|
|
location: "us-central1",
|
|
});
|
|
|
|
await stream.result();
|
|
|
|
expect(googleGenAiMock.constructorCalls).toHaveLength(1);
|
|
expect(googleGenAiMock.constructorCalls[0]).toMatchObject({
|
|
vertexai: true,
|
|
project: "test-project",
|
|
location: "us-central1",
|
|
apiVersion: "v1",
|
|
});
|
|
expect(googleGenAiMock.constructorCalls[0]).not.toHaveProperty("apiKey");
|
|
});
|
|
|
|
it("still uses the API key client for real API keys", async () => {
|
|
const stream = streamGoogleVertex(model, context, {
|
|
apiKey: "AIzaSyExampleRealisticLookingApiKey123456",
|
|
});
|
|
|
|
await stream.result();
|
|
|
|
expect(googleGenAiMock.constructorCalls).toHaveLength(1);
|
|
expect(googleGenAiMock.constructorCalls[0]).toMatchObject({
|
|
vertexai: true,
|
|
apiKey: "AIzaSyExampleRealisticLookingApiKey123456",
|
|
apiVersion: "v1",
|
|
});
|
|
expect(googleGenAiMock.constructorCalls[0]).not.toHaveProperty("project");
|
|
expect(googleGenAiMock.constructorCalls[0]).not.toHaveProperty("location");
|
|
});
|
|
|
|
it("does not forward generated Vertex base URL placeholders", async () => {
|
|
const stream = streamGoogleVertex(model, context, {
|
|
project: "test-project",
|
|
location: "us-central1",
|
|
});
|
|
|
|
await stream.result();
|
|
|
|
expect(googleGenAiMock.constructorCalls).toHaveLength(1);
|
|
expect(googleGenAiMock.constructorCalls[0]?.httpOptions).toBeUndefined();
|
|
});
|
|
|
|
it("forwards custom baseUrl to the ADC client", async () => {
|
|
const customModel: Model<"google-vertex"> = { ...model, baseUrl: "https://proxy.example.com" };
|
|
const stream = streamGoogleVertex(customModel, context, {
|
|
project: "test-project",
|
|
location: "us-central1",
|
|
});
|
|
|
|
await stream.result();
|
|
|
|
expect(googleGenAiMock.constructorCalls).toHaveLength(1);
|
|
expect(googleGenAiMock.constructorCalls[0]).toMatchObject({
|
|
vertexai: true,
|
|
project: "test-project",
|
|
location: "us-central1",
|
|
apiVersion: "v1",
|
|
httpOptions: {
|
|
baseUrl: "https://proxy.example.com",
|
|
baseUrlResourceScope: "COLLECTION",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("forwards custom baseUrl to the API key client", async () => {
|
|
const customModel: Model<"google-vertex"> = { ...model, baseUrl: "https://proxy.example.com" };
|
|
const stream = streamGoogleVertex(customModel, context, {
|
|
apiKey: "AIzaSyExampleRealisticLookingApiKey123456",
|
|
});
|
|
|
|
await stream.result();
|
|
|
|
expect(googleGenAiMock.constructorCalls).toHaveLength(1);
|
|
expect(googleGenAiMock.constructorCalls[0]).toMatchObject({
|
|
vertexai: true,
|
|
apiKey: "AIzaSyExampleRealisticLookingApiKey123456",
|
|
apiVersion: "v1",
|
|
httpOptions: {
|
|
baseUrl: "https://proxy.example.com",
|
|
baseUrlResourceScope: "COLLECTION",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("does not append apiVersion when custom baseUrl already includes one", async () => {
|
|
const customModel: Model<"google-vertex"> = {
|
|
...model,
|
|
baseUrl: "https://proxy.example.com/v1/projects/test-project/locations/global",
|
|
};
|
|
const stream = streamGoogleVertex(customModel, context, {
|
|
project: "test-project",
|
|
location: "us-central1",
|
|
});
|
|
|
|
await stream.result();
|
|
|
|
expect(googleGenAiMock.constructorCalls).toHaveLength(1);
|
|
expect(googleGenAiMock.constructorCalls[0]).toMatchObject({
|
|
httpOptions: {
|
|
baseUrl: "https://proxy.example.com/v1/projects/test-project/locations/global",
|
|
baseUrlResourceScope: "COLLECTION",
|
|
apiVersion: "",
|
|
},
|
|
});
|
|
});
|
|
});
|