import { type AuthType, type CredentialStore, InMemoryCredentialStore } from "@earendil-works/pi-ai"; import { describe, expect, it } from "vitest"; import { AuthStorage } from "../src/core/auth-storage.ts"; import { ModelRuntime } from "../src/core/model-runtime.ts"; function authOptions(runtime: ModelRuntime, type?: AuthType) { return runtime .getProviders() .flatMap((provider) => [ ...(!type || type === "oauth" ? provider.auth.oauth ? [{ type: "oauth" as const, provider, method: provider.auth.oauth }] : [] : []), ...(!type || type === "api_key" ? provider.auth.apiKey ? [{ type: "api_key" as const, provider, method: provider.auth.apiKey }] : [] : []), ]); } function testModel(id: string) { return { id, name: id, reasoning: false, input: ["text"] as ("text" | "image")[], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 10000, maxTokens: 1000, }; } describe("ModelRuntime auth options", () => { it("accepts a pi-ai CredentialStore", async () => { const credentials = new InMemoryCredentialStore(); await credentials.modify("anthropic", async () => ({ type: "api_key", key: "stored-key" })); const runtime = await ModelRuntime.create({ credentials, modelsPath: null }); expect((await runtime.getAuth("anthropic"))?.auth.apiKey).toBe("stored-key"); }); it("scopes provider availability reads and records refresh failures", async () => { const base = new InMemoryCredentialStore(); const reads: string[] = []; let failReads = false; const credentials: CredentialStore = { read: async (providerId) => { reads.push(providerId); if (failReads) throw new Error(`read failed for ${providerId}`); return base.read(providerId); }, list: () => base.list(), modify: (providerId, fn) => base.modify(providerId, fn), delete: (providerId) => base.delete(providerId), }; const runtime = await ModelRuntime.create({ credentials, modelsPath: null }); reads.length = 0; await runtime.getAvailable("anthropic"); expect(new Set(reads)).toEqual(new Set(["anthropic"])); failReads = true; await expect(runtime.getAvailable("anthropic")).rejects.toThrow("Credential store read failed for anthropic"); expect(runtime.getError()).toContain("Availability refresh: Credential store read failed for anthropic"); failReads = false; await runtime.getAvailable(); expect(runtime.getError()).toBeUndefined(); }); it("projects provider-owned methods, names, and status", async () => { const runtime = await ModelRuntime.create({ credentials: AuthStorage.inMemory(), modelsPath: null }); const options = authOptions(runtime); expect(options).toEqual( expect.arrayContaining([ expect.objectContaining({ type: "api_key", provider: expect.objectContaining({ id: "amazon-bedrock", name: "Amazon Bedrock" }), method: expect.objectContaining({ name: "AWS credentials or bearer token" }), }), expect.objectContaining({ type: "api_key", provider: expect.objectContaining({ id: "google-vertex", name: "Google Vertex AI" }), method: expect.objectContaining({ name: "Google Cloud credentials" }), }), expect.objectContaining({ type: "oauth", provider: expect.objectContaining({ id: "anthropic", name: "Anthropic" }), }), expect.objectContaining({ type: "api_key", provider: expect.objectContaining({ id: "cloudflare-ai-gateway", name: "Cloudflare AI Gateway" }), }), expect.objectContaining({ type: "api_key", provider: expect.objectContaining({ id: "cloudflare-workers-ai", name: "Cloudflare Workers AI" }), }), ]), ); expect(authOptions(runtime, "api_key").every((option) => option.type === "api_key")).toBe(true); expect(authOptions(runtime, "oauth").every((option) => option.type === "oauth")).toBe(true); expect(options.some((option) => option.provider.id === "openai-codex" && option.type === "api_key")).toBe(false); }); it("attaches the provider's active auth status to every method option", async () => { const runtime = await ModelRuntime.create({ credentials: AuthStorage.inMemory({ anthropic: { type: "oauth", access: "access", refresh: "refresh", expires: Date.now() + 60_000, }, }), modelsPath: null, }); const options = authOptions(runtime).filter((option) => option.provider.id === "anthropic"); expect(options).toHaveLength(2); expect(await runtime.checkAuth("anthropic")).toMatchObject({ type: "oauth" }); }); it("constructs an API key method for an extension API-key provider", async () => { const runtime = await ModelRuntime.create({ credentials: AuthStorage.inMemory(), modelsPath: null }); runtime.registerProvider("extension-api-key", { name: "Extension API Key", baseUrl: "https://example.test/v1", apiKey: "$EXTENSION_TEST_API_KEY", api: "openai-completions", models: [testModel("extension-model")], }); const options = authOptions(runtime).filter((option) => option.provider.id === "extension-api-key"); expect(options).toHaveLength(1); expect(options[0]).toMatchObject({ type: "api_key", provider: { id: "extension-api-key", name: "Extension API Key" }, method: { name: "API key" }, }); expect(options[0]?.method.login).toBeTypeOf("function"); }); it("resolves configured auth from request-scoped environment overrides", async () => { const runtime = await ModelRuntime.create({ credentials: AuthStorage.inMemory(), modelsPath: null }); runtime.registerProvider("request-env-provider", { baseUrl: "https://example.test/v1", apiKey: "$REQUEST_SCOPED_API_KEY", headers: { "x-request-value": "$REQUEST_SCOPED_HEADER" }, api: "openai-completions", models: [testModel("request-env-model")], }); const auth = await runtime.getAuth("request-env-provider", { env: { REQUEST_SCOPED_API_KEY: "request-key", REQUEST_SCOPED_HEADER: "request-header" }, }); expect(auth?.auth).toEqual({ apiKey: "request-key", headers: { "x-request-value": "request-header" } }); }); it("lets an explicit Authorization header override authHeader case-insensitively", async () => { const runtime = await ModelRuntime.create({ credentials: AuthStorage.inMemory(), modelsPath: null }); let capturedHeaders: Record | undefined; runtime.registerProvider("auth-header-provider", { baseUrl: "https://example.test/v1", apiKey: "generated-key", authHeader: true, api: "openai-completions", streamSimple: (_model, _context, options) => { capturedHeaders = options?.headers; throw new Error("captured"); }, models: [testModel("auth-header-model")], }); const model = runtime.getModel("auth-header-provider", "auth-header-model"); expect(model).toBeDefined(); await runtime.completeSimple(model!, { messages: [] }, { headers: { authorization: "Explicit token" } }); expect(capturedHeaders).toEqual({ authorization: "Explicit token" }); }); it("transforms fully assembled headers once without forwarding the transform", async () => { const runtime = await ModelRuntime.create({ credentials: AuthStorage.inMemory(), modelsPath: null }); let capturedHeaders: Record | undefined; let transforms = 0; runtime.registerProvider("header-provider", { baseUrl: "https://example.test/v1", apiKey: "generated-key", authHeader: true, headers: { "x-provider": "provider" }, api: "openai-completions", streamSimple: (_model, _context, options) => { expect(options).not.toHaveProperty("transformHeaders"); capturedHeaders = options?.headers; throw new Error("captured"); }, models: [{ ...testModel("header-model"), headers: { "x-model": "model" } }], }); const model = runtime.getModel("header-provider", "header-model"); expect(model).toBeDefined(); await runtime.completeSimple( model!, { messages: [] }, { headers: { "x-explicit": "explicit" }, transformHeaders: async (headers) => { transforms++; expect(headers).toEqual({ Authorization: "Bearer generated-key", "x-provider": "provider", "x-model": "model", "x-explicit": "explicit", }); return { ...headers, "x-transformed": "yes" }; }, }, ); expect(transforms).toBe(1); expect(capturedHeaders).toEqual({ Authorization: "Bearer generated-key", "x-provider": "provider", "x-model": "model", "x-explicit": "explicit", "x-transformed": "yes", }); }); it("does not fabricate an API key method for an extension OAuth-only provider", async () => { const runtime = await ModelRuntime.create({ credentials: AuthStorage.inMemory(), modelsPath: null }); runtime.registerProvider("extension-oauth", { name: "Extension OAuth", baseUrl: "https://example.test/v1", api: "openai-completions", oauth: { name: "Extension subscription", login: async () => ({ access: "access", refresh: "refresh", expires: Date.now() + 60_000 }), refreshToken: async (credentials) => credentials, getApiKey: (credentials) => credentials.access, }, models: [testModel("extension-model")], }); const options = authOptions(runtime).filter((option) => option.provider.id === "extension-oauth"); expect(options).toHaveLength(1); expect(options[0]).toMatchObject({ type: "oauth", provider: { id: "extension-oauth", name: "Extension OAuth" }, method: { name: "Extension subscription" }, }); }); });