/** * Tests for provider registry * * Covers: * - Provider configuration and creation * - Environment variable loading * - Model ID parsing and routing * - Gateway authentication * - Smart routing between direct providers and OpenRouter */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { createRegistry, getRegistry, configureRegistry, model, embeddingModel, DIRECT_PROVIDERS, type ProviderConfig, } from '../src/registry.js' describe('createRegistry', () => { describe('basic provider creation', () => { it('creates registry with default config from environment', async () => { const registry = await createRegistry() expect(registry).toBeDefined() expect(typeof registry.languageModel).toBe('function') expect(typeof registry.textEmbeddingModel).toBe('function') }) it('creates registry with custom config', async () => { const config: ProviderConfig = { gatewayUrl: 'https://example.com/gateway', gatewayToken: 'test-token', } const registry = await createRegistry(config) expect(registry).toBeDefined() }) it('creates registry with only specific providers', async () => { // This should not throw even if some provider SDKs are missing const registry = await createRegistry({}, { providers: ['openrouter'] }) expect(registry).toBeDefined() }) it('handles missing provider SDKs gracefully', async () => { // Should not throw when optional dependencies are missing const registry = await createRegistry({}) expect(registry).toBeDefined() }) }) describe('environment variable loading', () => { const originalEnv = process.env beforeEach(() => { vi.resetModules() process.env = { ...originalEnv } }) afterEach(() => { process.env = originalEnv }) it('loads gateway config from AI_GATEWAY_URL', async () => { process.env.AI_GATEWAY_URL = 'https://gateway.ai.cloudflare.com/v1/account/gateway' process.env.AI_GATEWAY_TOKEN = 'test-token' const registry = await createRegistry() expect(registry).toBeDefined() }) it('loads gateway token from DO_TOKEN as fallback', async () => { process.env.AI_GATEWAY_URL = 'https://gateway.ai.cloudflare.com/v1/account/gateway' process.env.DO_TOKEN = 'do-token' delete process.env.AI_GATEWAY_TOKEN const registry = await createRegistry() expect(registry).toBeDefined() }) it('loads individual provider API keys', async () => { process.env.OPENAI_API_KEY = 'sk-openai-test' process.env.ANTHROPIC_API_KEY = 'sk-anthropic-test' process.env.GOOGLE_GENERATIVE_AI_API_KEY = 'google-test' process.env.OPENROUTER_API_KEY = 'sk-or-test' process.env.CLOUDFLARE_ACCOUNT_ID = 'cf-account' process.env.CLOUDFLARE_API_TOKEN = 'cf-token' const registry = await createRegistry() expect(registry).toBeDefined() }) it('uses GOOGLE_AI_API_KEY as fallback for Google', async () => { process.env.GOOGLE_AI_API_KEY = 'google-test' delete process.env.GOOGLE_GENERATIVE_AI_API_KEY const registry = await createRegistry() expect(registry).toBeDefined() }) }) describe('base URL construction', () => { it('uses custom base URLs when provided', async () => { const config: ProviderConfig = { baseUrls: { openai: 'https://custom-openai.example.com', anthropic: 'https://custom-anthropic.example.com', }, } const registry = await createRegistry(config) expect(registry).toBeDefined() }) it('constructs gateway URLs for each provider', async () => { const config: ProviderConfig = { gatewayUrl: 'https://gateway.ai.cloudflare.com/v1/account123/gateway456', gatewayToken: 'test-token', } const registry = await createRegistry(config) expect(registry).toBeDefined() // Expected gateway paths: // openai -> /openai // anthropic -> /anthropic // google -> /google-ai-studio // openrouter -> /openrouter // cloudflare -> /workers-ai }) it('uses default URLs when no gateway or custom URLs', async () => { const registry = await createRegistry({}) expect(registry).toBeDefined() }) }) describe('gateway authentication', () => { it('uses gateway secrets mode when both URL and token configured', async () => { const config: ProviderConfig = { gatewayUrl: 'https://gateway.ai.cloudflare.com/v1/account/gateway', gatewayToken: 'test-token', } const registry = await createRegistry(config) expect(registry).toBeDefined() // When using gateway secrets, API keys should be placeholders }) it('falls back to direct API keys without gateway', async () => { const config: ProviderConfig = { openaiApiKey: 'sk-openai-test', anthropicApiKey: 'sk-anthropic-test', } const registry = await createRegistry(config) expect(registry).toBeDefined() }) it('uses placeholder API keys with gateway secrets', async () => { const config: ProviderConfig = { gatewayUrl: 'https://gateway.ai.cloudflare.com/v1/account/gateway', gatewayToken: 'test-token', // No individual API keys needed } const registry = await createRegistry(config) expect(registry).toBeDefined() }) }) }) describe('getRegistry', () => { it('returns a singleton registry', async () => { const registry1 = await getRegistry() const registry2 = await getRegistry() // Should return the same instance expect(registry1).toBe(registry2) }) it('caches the registry promise to prevent duplicate initialization', async () => { // Call getRegistry multiple times in parallel const [registry1, registry2, registry3] = await Promise.all([ getRegistry(), getRegistry(), getRegistry(), ]) expect(registry1).toBe(registry2) expect(registry2).toBe(registry3) }) }) describe('configureRegistry', () => { it('replaces the default registry with new config', async () => { const originalRegistry = await getRegistry() await configureRegistry({ gatewayUrl: 'https://new-gateway.example.com', gatewayToken: 'new-token', }) const newRegistry = await getRegistry() // Should be a different instance after reconfiguration expect(newRegistry).not.toBe(originalRegistry) }) it('resets the registry promise', async () => { await configureRegistry({ gatewayUrl: 'https://test-gateway.example.com', }) const registry = await getRegistry() expect(registry).toBeDefined() }) }) describe('model ID parsing', () => { it('parses provider/model format', () => { // Test internal parsing logic through the model() function behavior expect(true).toBe(true) }) it('defaults to openrouter for IDs without slash', () => { // IDs without a provider prefix should route to OpenRouter expect(true).toBe(true) }) }) describe('model', () => { describe('smart routing', () => { it('resolves simple aliases to full model IDs', async () => { // Mock test - actual resolution requires language-models package try { const m = await model('sonnet') expect(m).toBeDefined() } catch (error) { // language-models may not be available in test environment expect(error).toBeDefined() } }) it('routes direct providers to their native SDKs', async () => { // When language-models provides provider_model_id and the provider // is in DIRECT_PROVIDERS (openai, anthropic, google), should route directly try { const m = await model('anthropic/claude-sonnet-4.5') expect(m).toBeDefined() } catch (error) { // Provider SDK may not be available expect(error).toBeDefined() } }) it('routes other models through OpenRouter', async () => { try { const m = await model('meta-llama/llama-3.3-70b-instruct') expect(m).toBeDefined() } catch (error) { // OpenRouter may not be configured expect(error).toBeDefined() } }) it('handles models without language-models package', async () => { // Should fall back to OpenRouter routing try { const m = await model('some-random-model') expect(m).toBeDefined() } catch (error) { expect(error).toBeDefined() } }) it('respects provider prefix in model ID for direct routing', async () => { // openai/* should route to OpenAI SDK // anthropic/* should route to Anthropic SDK // google/* should route to Google SDK // others/* should route to OpenRouter expect(DIRECT_PROVIDERS).toContain('openai') expect(DIRECT_PROVIDERS).toContain('anthropic') expect(DIRECT_PROVIDERS).toContain('google') }) it('uses provider_model_id when available', async () => { // When language-models provides provider_model_id, use it for direct routing // This enables provider-specific features expect(true).toBe(true) }) it('validates provider matches between ID and metadata', async () => { // Should only route directly when the provider in the model ID matches // the provider in the metadata (prevents incorrect routing) expect(true).toBe(true) }) }) describe('full model IDs', () => { it('accepts full provider:model format', async () => { try { const m = await model('anthropic/claude-opus-4.5') expect(m).toBeDefined() } catch (error) { expect(error).toBeDefined() } }) it('accepts models without provider prefix', async () => { try { const m = await model('gpt-4o') expect(m).toBeDefined() } catch (error) { expect(error).toBeDefined() } }) }) describe('error handling', () => { it('throws when provider SDK is not installed', async () => { // If @ai-sdk/openai is not installed, should throw // (or handle gracefully depending on implementation) expect(true).toBe(true) }) it('handles invalid model IDs gracefully', async () => { // Should not crash on invalid input try { await model('') } catch (error) { expect(error).toBeDefined() } }) }) }) describe('embeddingModel', () => { it('returns an embedding model from the registry', async () => { try { const em = await embeddingModel('openai:text-embedding-3-small') expect(em).toBeDefined() } catch (error) { // Provider may not be configured expect(error).toBeDefined() } }) it('accepts cloudflare embedding models', async () => { try { const em = await embeddingModel('cloudflare:@cf/baai/bge-m3') expect(em).toBeDefined() } catch (error) { // Cloudflare may not be configured expect(error).toBeDefined() } }) it('requires provider:model format', async () => { // Should work with proper format try { const em = await embeddingModel('openai:text-embedding-ada-002') expect(em).toBeDefined() } catch (error) { expect(error).toBeDefined() } }) }) describe('DIRECT_PROVIDERS', () => { it('exports the list of direct providers', () => { expect(DIRECT_PROVIDERS).toBeDefined() expect(Array.isArray(DIRECT_PROVIDERS)).toBe(true) }) it('includes openai, anthropic, google', () => { expect(DIRECT_PROVIDERS).toContain('openai') expect(DIRECT_PROVIDERS).toContain('anthropic') expect(DIRECT_PROVIDERS).toContain('google') }) it('matches the DIRECT_PROVIDERS from language-models', () => { // Should be re-exported from language-models for consistency expect(DIRECT_PROVIDERS.length).toBeGreaterThan(0) }) }) describe('provider-specific features', () => { it('enables Anthropic MCP when routing directly', () => { // Direct routing to Anthropic SDK enables Model Context Protocol expect(DIRECT_PROVIDERS).toContain('anthropic') }) it('enables OpenAI function calling when routing directly', () => { // Direct routing to OpenAI SDK enables function calling, JSON mode, etc. expect(DIRECT_PROVIDERS).toContain('openai') }) it('enables Google grounding when routing directly', () => { // Direct routing to Google SDK enables grounding, code execution, etc. expect(DIRECT_PROVIDERS).toContain('google') }) }) describe('gateway fetch customization', () => { it('strips SDK API key headers when using gateway secrets', () => { // When gatewayUrl and gatewayToken are set, should remove: // - x-api-key // - authorization // - x-goog-api-key // And add: cf-aig-authorization expect(true).toBe(true) }) it('preserves other headers', () => { // Should only strip auth headers, keep others expect(true).toBe(true) }) it('adds cf-aig-authorization header with Bearer token', () => { // Should add: cf-aig-authorization: Bearer expect(true).toBe(true) }) it('does not modify fetch when not using gateway', () => { // When no gateway configured, should not provide custom fetch expect(true).toBe(true) }) }) describe('integration scenarios', () => { it('works with Cloudflare AI Gateway and stored secrets', async () => { const config: ProviderConfig = { gatewayUrl: 'https://gateway.ai.cloudflare.com/v1/account/gateway', gatewayToken: 'test-token', // No individual API keys - gateway provides them } const registry = await createRegistry(config) expect(registry).toBeDefined() }) it('works with direct API keys (no gateway)', async () => { const config: ProviderConfig = { openaiApiKey: 'sk-test', anthropicApiKey: 'sk-test', googleApiKey: 'test', openrouterApiKey: 'sk-or-test', } const registry = await createRegistry(config) expect(registry).toBeDefined() }) it('works with mixed config (gateway + fallback keys)', async () => { const config: ProviderConfig = { gatewayUrl: 'https://gateway.ai.cloudflare.com/v1/account/gateway', gatewayToken: 'test-token', // Also provide fallback keys openaiApiKey: 'sk-test', } const registry = await createRegistry(config) expect(registry).toBeDefined() }) it('supports custom base URLs overriding gateway', async () => { const config: ProviderConfig = { gatewayUrl: 'https://gateway.ai.cloudflare.com/v1/account/gateway', gatewayToken: 'test-token', baseUrls: { openai: 'https://custom-openai.example.com', }, } const registry = await createRegistry(config) expect(registry).toBeDefined() }) }) describe('type safety', () => { it('exports ProviderConfig type', () => { const config: ProviderConfig = { gatewayUrl: 'test', } expect(config).toBeDefined() }) it('exports ProviderId type', () => { // Should be: 'openai' | 'anthropic' | 'google' | 'openrouter' | 'cloudflare' expect(true).toBe(true) }) it('exports DirectProvider type', () => { // Should match language-models DirectProvider type expect(true).toBe(true) }) })