import { describe, it, expect, vi } from 'vitest'; import { LLMProviderAdapter } from '../src/proxymodel/llm-adapter.js'; import { ProviderRegistry } from '../src/providers/registry.js'; import type { LlmProvider, CompletionResult } from '../src/providers/types.js'; function mockProvider(name: string, response = 'mock response'): LlmProvider { return { name, complete: vi.fn().mockResolvedValue({ content: response, toolCalls: [], usage: { promptTokens: 10, completionTokens: 5, totalTokens: 15 }, finishReason: 'stop', } satisfies CompletionResult), listModels: vi.fn().mockResolvedValue([]), isAvailable: vi.fn().mockResolvedValue(true), }; } function failingProvider(name: string, error = 'connection refused'): LlmProvider { return { name, complete: vi.fn().mockRejectedValue(new Error(error)), listModels: vi.fn().mockResolvedValue([]), isAvailable: vi.fn().mockResolvedValue(true), }; } describe('LLMProviderAdapter', () => { it('available() returns true when a provider is registered', () => { const registry = new ProviderRegistry(); registry.register(mockProvider('test')); registry.assignTier('test', 'fast'); const adapter = new LLMProviderAdapter(registry); expect(adapter.available()).toBe(true); }); it('available() returns false when no provider is registered', () => { const registry = new ProviderRegistry(); const adapter = new LLMProviderAdapter(registry); expect(adapter.available()).toBe(false); }); it('complete() sends prompt as user message', async () => { const provider = mockProvider('test'); const registry = new ProviderRegistry(); registry.register(provider); registry.assignTier('test', 'fast'); const adapter = new LLMProviderAdapter(registry); const result = await adapter.complete('summarize this'); expect(result).toBe('mock response'); expect(provider.complete).toHaveBeenCalledWith({ messages: [{ role: 'user', content: 'summarize this' }], temperature: 0, }); }); it('complete() includes system message when provided', async () => { const provider = mockProvider('test'); const registry = new ProviderRegistry(); registry.register(provider); registry.assignTier('test', 'fast'); const adapter = new LLMProviderAdapter(registry); await adapter.complete('summarize', { system: 'You are a summarizer', maxTokens: 200 }); expect(provider.complete).toHaveBeenCalledWith({ messages: [ { role: 'system', content: 'You are a summarizer' }, { role: 'user', content: 'summarize' }, ], maxTokens: 200, temperature: 0, }); }); it('complete() throws when no provider available', async () => { const registry = new ProviderRegistry(); const adapter = new LLMProviderAdapter(registry); await expect(adapter.complete('test')).rejects.toThrow('No LLM provider available'); }); // --- Failover tests --- it('falls back to next provider in same tier on failure', async () => { const failing = failingProvider('vllm-local', 'vLLM startup timed out'); const working = mockProvider('anthropic', 'anthropic response'); const registry = new ProviderRegistry(); registry.register(failing); registry.register(working); registry.assignTier('vllm-local', 'fast'); registry.assignTier('anthropic', 'fast'); const adapter = new LLMProviderAdapter(registry); const result = await adapter.complete('test'); expect(result).toBe('anthropic response'); expect(failing.complete).toHaveBeenCalledOnce(); expect(working.complete).toHaveBeenCalledOnce(); }); it('falls back cross-tier when all fast providers fail', async () => { const fastFail = failingProvider('vllm-local'); const heavy = mockProvider('gemini', 'gemini response'); const registry = new ProviderRegistry(); registry.register(fastFail); registry.register(heavy); registry.assignTier('vllm-local', 'fast'); registry.assignTier('gemini', 'heavy'); const adapter = new LLMProviderAdapter(registry); const result = await adapter.complete('test'); expect(result).toBe('gemini response'); expect(fastFail.complete).toHaveBeenCalledOnce(); expect(heavy.complete).toHaveBeenCalledOnce(); }); it('throws last error when all providers fail', async () => { const fail1 = failingProvider('vllm', 'vLLM down'); const fail2 = failingProvider('anthropic', 'rate limited'); const registry = new ProviderRegistry(); registry.register(fail1); registry.register(fail2); registry.assignTier('vllm', 'fast'); registry.assignTier('anthropic', 'heavy'); const adapter = new LLMProviderAdapter(registry); await expect(adapter.complete('test')).rejects.toThrow('rate limited'); expect(fail1.complete).toHaveBeenCalledOnce(); expect(fail2.complete).toHaveBeenCalledOnce(); }); it('does not retry provider that already succeeded', async () => { const fast = mockProvider('fast-provider', 'fast result'); const heavy = mockProvider('heavy-provider', 'heavy result'); const registry = new ProviderRegistry(); registry.register(fast); registry.register(heavy); registry.assignTier('fast-provider', 'fast'); registry.assignTier('heavy-provider', 'heavy'); const adapter = new LLMProviderAdapter(registry); const result = await adapter.complete('test'); expect(result).toBe('fast result'); expect(fast.complete).toHaveBeenCalledOnce(); expect(heavy.complete).not.toHaveBeenCalled(); }); it('prefers named provider but falls back on failure', async () => { const named = failingProvider('preferred', 'preferred down'); const fallback = mockProvider('fallback', 'fallback response'); const registry = new ProviderRegistry(); registry.register(named); registry.register(fallback); registry.assignTier('preferred', 'fast'); registry.assignTier('fallback', 'fast'); const adapter = new LLMProviderAdapter(registry, 'preferred'); const result = await adapter.complete('test'); expect(result).toBe('fallback response'); }); });