import { assert, assertEquals, assertThrows } from "@std/assert"; import { buildAdapter, clientOptionsFor, ProviderManager } from "./manager.ts"; import { OpenAIAdapter } from "./openai.ts"; import { AnthropicAdapter } from "./anthropic.ts"; import { GeminiAdapter } from "./gemini.ts"; import { CohereAdapter } from "./cohere.ts"; import { BedrockAdapter } from "./bedrock.ts"; import { VertexAdapter } from "./vertex.ts"; import { OPENAI_COMPAT_BASE_URLS } from "./openai_compat.ts"; import { ProviderClient } from "./client.ts"; import { GatewayError } from "../../core/src/mod.ts"; import { ProviderRegistry } from "../../contracts/src/mod.ts"; import type { ProviderAccountConfig, ProviderName, } from "../../contracts/src/mod.ts"; function mockFetch( handler: (input: RequestInfo | URL, init?: RequestInit) => Response, ): typeof fetch { return (input, init) => Promise.resolve(handler(input, init)); } function hasMethod(adapter: unknown, name: string): boolean { return typeof (adapter as Record)[name] === "function"; } function account( overrides: Partial & { id: string }, ): ProviderAccountConfig { return { type: "openai", enabled: true, models: ["gpt-4o"], priority: 0, apiKey: "k", ...overrides, }; } Deno.test("resolve routes provider-prefixed models and strips the prefix", () => { const manager = new ProviderManager([ account({ id: "openai" }), account({ id: "anthropic", type: "anthropic", models: ["claude-x"] }), ]); const target = manager.resolve("anthropic/claude-x"); assertEquals(target.providerId, "anthropic"); assertEquals(target.model, "claude-x"); }); Deno.test("global proxy is rejected when native HTTP clients are unavailable", () => { const manager = new ProviderManager( [], undefined, undefined, undefined, () => false, ); const error = assertThrows( () => manager.configureGlobalProxy({ proxyUrl: "http://proxy.internal", noProxy: [], }), GatewayError, ); assertEquals(error.status, 409); assertEquals(manager.getGlobalProxy(), undefined); }); Deno.test("provider noProxy overrides the selected global proxy policy", () => { const globalProxy = { proxyUrl: "http://proxy.internal", noProxy: [".global"], }; assertEquals( clientOptionsFor( account({ id: "scoped", proxy: { noProxy: [".provider"] } }), globalProxy, ).noProxy, [".provider"], ); assertEquals( clientOptionsFor(account({ id: "fallback" }), globalProxy).noProxy, [".global"], ); }); Deno.test("resolve uses the default provider for unprefixed models", () => { const manager = new ProviderManager( [account({ id: "openai" }), account({ id: "backup" })], "backup", ); const target = manager.resolve("gpt-4o"); assertEquals(target.providerId, "backup"); assertEquals(target.model, "gpt-4o"); }); Deno.test("resolve uses the single enabled account when unambiguous", () => { const manager = new ProviderManager([ account({ id: "solo" }), account({ id: "off", enabled: false }), ]); assertEquals(manager.resolve("gpt-4o").providerId, "solo"); }); Deno.test("resolve fails loud when unroutable or empty", () => { // Two accounts, neither advertises the requested model: ambiguous. const ambiguous = new ProviderManager([ account({ id: "a" }), account({ id: "b" }), ]); assertThrows(() => ambiguous.resolve("mystery-model"), GatewayError); const empty = new ProviderManager([]); const err = assertThrows(() => empty.resolve("gpt-4o"), GatewayError); assertEquals(err.status, 503); }); Deno.test("resolve load-balances round-robin across advertising accounts", () => { const manager = new ProviderManager([ account({ id: "a" }), account({ id: "b" }), ]); const picks = [ manager.resolve("gpt-4o").providerId, manager.resolve("gpt-4o").providerId, manager.resolve("gpt-4o").providerId, manager.resolve("gpt-4o").providerId, ]; // alternates between the two equal-priority accounts assertEquals(new Set(picks).size, 2); assertEquals(picks[0] !== picks[1], true); assertEquals(picks[0], picks[2]); }); Deno.test("resolve prefers lower priority tiers in the pool", () => { const manager = new ProviderManager([ account({ id: "backup", priority: 10 }), account({ id: "main", priority: 0 }), ]); assertEquals(manager.resolve("gpt-4o").providerId, "main"); assertEquals(manager.resolve("gpt-4o").providerId, "main"); }); Deno.test("resolveChain alternates equal-priority primaries across calls", () => { const manager = new ProviderManager([ account({ id: "a" }), account({ id: "b" }), ]); const first = manager.resolveChain("gpt-4o"); const second = manager.resolveChain("gpt-4o"); const third = manager.resolveChain("gpt-4o"); // One request advances the rotation exactly once, so consecutive // resolveChain calls alternate the primary between the two accounts. assert(first[0].providerId !== second[0].providerId); assertEquals(first[0].providerId, third[0].providerId); // Failover still covers the other account. assertEquals( new Set(first.map((t) => t.providerId)), new Set(["a", "b"]), ); }); Deno.test("resolveChain appends automatic failover targets from the pool", () => { const manager = new ProviderManager([ account({ id: "a" }), account({ id: "b" }), account({ id: "c", models: ["other-model"] }), ], "a"); const chain = manager.resolveChain("gpt-4o"); assertEquals(chain[0].providerId, "a"); // b advertises the same model -> auto failover; c does not -> excluded assertEquals(chain.map((t) => t.providerId).includes("b"), true); assertEquals(chain.map((t) => t.providerId).includes("c"), false); }); Deno.test("resolve rejects disabled providers", () => { const manager = new ProviderManager([account({ id: "x", enabled: false })]); const err = assertThrows(() => manager.resolve("x/gpt-4o"), GatewayError); assertEquals(err.status, 400); }); Deno.test("resolveChain appends valid fallbacks and skips unknown ones", () => { const manager = new ProviderManager([ account({ id: "a" }), account({ id: "b", models: ["other"] }), ]); const chain = manager.resolveChain("a/gpt-4o", [ { provider: "b" }, { provider: "missing" }, { provider: "b", model: "override" }, ]); assertEquals(chain.map((t) => `${t.providerId}:${t.model}`), [ "a:gpt-4o", "b:gpt-4o", "b:override", ]); }); Deno.test("models aggregates enabled catalogs with prefixed ids", () => { const manager = new ProviderManager([ account({ id: "openai", models: ["gpt-4o", "gpt-4o-mini"] }), account({ id: "off", enabled: false, models: ["hidden"] }), ]); assertEquals(manager.models().map((m) => m.id), [ "openai/gpt-4o", "openai/gpt-4o-mini", ]); }); Deno.test("listPublic never exposes API keys", () => { const manager = new ProviderManager([ account({ id: "openai", apiKey: "sk-secret" }), ]); const pub = manager.listPublic()[0] as unknown as Record; assertEquals(pub.hasApiKey, true); assertEquals("apiKey" in pub, false); }); Deno.test("buildAdapter maps openai-compatible to the OpenAI adapter", () => { const adapter = buildAdapter(account({ id: "openai-compatible", type: "openai-compatible", baseUrl: "https://compat.example/v1", })); assert(adapter instanceof OpenAIAdapter); }); Deno.test("buildAdapter maps anthropic-compatible to the Anthropic adapter", () => { const adapter = buildAdapter(account({ id: "anthropic-compatible", type: "anthropic-compatible", baseUrl: "https://anthropic.example", })); assert(adapter instanceof AnthropicAdapter); }); Deno.test("buildAdapter maps lmstudio to the OpenAI adapter with a local default base URL", () => { const adapter = buildAdapter(account({ id: "lmstudio", type: "lmstudio" })); assert(adapter instanceof OpenAIAdapter); // lmstudio needs no explicit case: the default branch supplies its base URL. assertEquals(OPENAI_COMPAT_BASE_URLS.lmstudio, "http://localhost:1234/v1"); }); Deno.test("ProviderRegistry advertises the three generic providers", () => { for ( const name of [ "openai-compatible", "anthropic-compatible", "lmstudio", ] as const ) { const cap = ProviderRegistry[name]; assert(cap, `missing registry entry: ${name}`); assertEquals(cap.supportsStreaming, true); assertEquals(cap.supportsTools, true); } assertEquals(ProviderRegistry["openai-compatible"].authRequirements, [ "OPENAI_COMPAT_BASE_URL", ]); assertEquals(ProviderRegistry["anthropic-compatible"].authRequirements, [ "ANTHROPIC_COMPAT_BASE_URL", ]); assertEquals(ProviderRegistry.lmstudio.authRequirements, []); }); Deno.test("registry capability flags match the resolved adapter surface (no lying flags)", () => { const build = (type: ProviderName) => buildAdapter(account({ id: type, type })); // Gemini: embeddings + countTokens + native Imagen image generation. The // supportsImages flag is truthful via generateImage (not rawProxy). const gemini = build("gemini"); assert(gemini instanceof GeminiAdapter); assertEquals(ProviderRegistry.gemini.supportsEmbeddings, true); assert(hasMethod(gemini, "embeddings")); assert(hasMethod(gemini, "countTokens")); assertEquals(ProviderRegistry.gemini.supportsImages, true); assert(hasMethod(gemini, "generateImage")); // Gemini audio (TTS/STT) and files/batches ride a translating rawProxy. assertEquals(ProviderRegistry.gemini.supportsAudio, true); assertEquals(ProviderRegistry.gemini.supportsFiles, true); assert(hasMethod(gemini, "rawProxy")); // Anthropic files/batches ride a translating rawProxy. const anthropic = build("anthropic"); assertEquals(ProviderRegistry.anthropic.supportsFiles, true); assert(hasMethod(anthropic, "rawProxy")); // Bedrock files (S3 emulation) + batches (Model Invocation Jobs). const bedrockAdapter = build("bedrock"); assertEquals(ProviderRegistry.bedrock.supportsFiles, true); assert(hasMethod(bedrockAdapter, "rawProxy")); // HuggingFace images are a typed generateImage; audio rides rawProxy. const huggingface = build("huggingface"); assertEquals(ProviderRegistry.huggingface.supportsImages, true); assert(hasMethod(huggingface, "generateImage")); assertEquals(ProviderRegistry.huggingface.supportsAudio, true); assert(hasMethod(huggingface, "rawProxy")); // Mistral transcription is an OpenAI-wire passthrough (groq precedent). const mistral = build("mistral"); assert(mistral instanceof OpenAIAdapter); assertEquals(ProviderRegistry.mistral.supportsAudio, true); assert(hasMethod(mistral, "rawProxy")); // Vertex embeddings + countTokens + native Imagen image generation. const vertex = build("vertex"); assert(vertex instanceof VertexAdapter); assertEquals(ProviderRegistry.vertex.supportsEmbeddings, true); assert(hasMethod(vertex, "embeddings")); assert(hasMethod(vertex, "countTokens")); assertEquals(ProviderRegistry.vertex.supportsImages, true); assert(hasMethod(vertex, "generateImage")); // sgl embeddings ride the OpenAI adapter's /embeddings surface. const sgl = build("sgl"); assert(sgl instanceof OpenAIAdapter); assertEquals(ProviderRegistry.sgl.supportsEmbeddings, true); assert(hasMethod(sgl, "embeddings")); // nebius images ride the OpenAI adapter's rawProxy image-generation path. const nebius = build("nebius"); assert(nebius instanceof OpenAIAdapter); assertEquals(ProviderRegistry.nebius.supportsImages, true); assert(hasMethod(nebius, "rawProxy")); // Tool-forwarding OpenAI-wire vendors resolve to the OpenAI adapter, which // forwards `tools` verbatim. for (const type of ["perplexity", "sgl", "parasail"] as const) { assertEquals(ProviderRegistry[type].supportsTools, true); assert(build(type) instanceof OpenAIAdapter); } // Cohere + Bedrock translate tools inside their own adapters. assertEquals(ProviderRegistry.cohere.supportsTools, true); assert(build("cohere") instanceof CohereAdapter); assertEquals(ProviderRegistry.bedrock.supportsTools, true); const bedrock = build("bedrock"); assert(bedrock instanceof BedrockAdapter); // Bedrock streaming is live via the converse-stream eventstream decoder. assertEquals(ProviderRegistry.bedrock.supportsStreaming, true); // Bedrock embeddings ride the native InvokeModel (:invoke) surface. assertEquals(ProviderRegistry.bedrock.supportsEmbeddings, true); assert(hasMethod(bedrock, "embeddings")); }); Deno.test("buildAdapter enables native count_tokens only for the openai account", async () => { // Real OpenAI: native /responses/input_tokens counter. let openaiHit = false; const openaiClient = new ProviderClient( { maxRetries: 0 }, mockFetch(() => { openaiHit = true; return new Response(JSON.stringify({ input_tokens: 3 }), { headers: { "Content-Type": "application/json" }, }); }), ); const openai = buildAdapter( account({ id: "openai", type: "openai", baseUrl: "http://mock/v1" }), openaiClient, ) as OpenAIAdapter; const native = await openai.countTokens!({ model: "gpt-4o", messages: [{ role: "user", content: "hi" }], }); assert(openaiHit); assertEquals(native.estimated, false); // groq reuses the OpenAI adapter but must NOT call a native endpoint. let groqHit = false; const groqClient = new ProviderClient( { maxRetries: 0 }, mockFetch(() => { groqHit = true; return new Response("nope", { status: 500 }); }), ); const groq = buildAdapter( account({ id: "groq", type: "groq", baseUrl: "http://mock/v1" }), groqClient, ) as OpenAIAdapter; const estimate = await groq.countTokens!({ model: "mixtral", messages: [{ role: "user", content: "12345678" }], }); assertEquals(groqHit, false); assertEquals(estimate, { input_tokens: 2, estimated: true }); }); Deno.test("listPublic replaces proxyUrl with a hasProxy flag", () => { const manager = new ProviderManager([ account({ id: "proxied", proxyUrl: "http://user:pass@proxy.local:8888" }), account({ id: "direct" }), ]); try { const pub = Object.fromEntries( manager.listPublic().map(( p, ) => [p.id, p as unknown as Record]), ); assertEquals("proxyUrl" in pub.proxied, false); assertEquals(pub.proxied.hasProxy, true); assertEquals(pub.direct.hasProxy, false); } finally { // remove() closes the proxied account's native HTTP client. manager.remove("proxied"); } });