/** * llmindex.io — OpenRouter client unit tests (mocked fetch) * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * License: Proprietary — © Simon-Pierre Boucher, all rights reserved */ import { describe, expect, it, vi } from 'vitest'; import { OpenRouterClient, OpenRouterError } from './client'; const okBody = { id: 'gen-1', model: 'test/model', choices: [{ index: 0, message: { role: 'assistant', content: 'ANSWER: 42' }, finish_reason: 'stop' }], usage: { prompt_tokens: 100, completion_tokens: 50, total_tokens: 150 }, }; function jsonResponse(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' }, }); } describe('OpenRouterClient', () => { it('throws without an API key', () => { const prev = process.env.OPENROUTER_API_KEY; delete process.env.OPENROUTER_API_KEY; expect(() => new OpenRouterClient()).toThrow(OpenRouterError); if (prev) process.env.OPENROUTER_API_KEY = prev; }); it('sends required headers and computes cost from pricing', async () => { const fetchFn = vi.fn().mockResolvedValue(jsonResponse(okBody)); const client = new OpenRouterClient({ apiKey: 'test-key', fetchFn, backoffBaseMs: 1 }); const result = await client.chat( { model: 'test/model', messages: [{ role: 'user', content: 'hi' }], temperature: 0 }, { promptPerM: 3, completionPerM: 15 }, ); const [url, init] = fetchFn.mock.calls[0]!; expect(url).toBe('https://openrouter.ai/api/v1/chat/completions'); expect(init.headers.Authorization).toBe('Bearer test-key'); expect(init.headers['HTTP-Referer']).toBe('https://www.llmindex.io'); expect(init.headers['X-Title']).toBe('LLM Index'); expect(result.text).toBe('ANSWER: 42'); // 100 tok × $3/1M + 50 tok × $15/1M expect(result.costUsd).toBeCloseTo(0.00105, 8); expect(result.latencyMs).toBeGreaterThanOrEqual(0); expect(result.requestParams.temperature).toBe(0); }); it('retries on 429 then succeeds', async () => { const fetchFn = vi .fn() .mockResolvedValueOnce(jsonResponse({ error: 'rate limited' }, 429)) .mockResolvedValueOnce(jsonResponse(okBody)); const client = new OpenRouterClient({ apiKey: 'test-key', fetchFn, backoffBaseMs: 1 }); const result = await client.chat({ model: 'test/model', messages: [] }); expect(fetchFn).toHaveBeenCalledTimes(2); expect(result.text).toBe('ANSWER: 42'); }); it('does not retry on 400 and surfaces the error', async () => { const fetchFn = vi.fn().mockResolvedValue(jsonResponse({ error: 'bad request' }, 400)); const client = new OpenRouterClient({ apiKey: 'test-key', fetchFn, backoffBaseMs: 1 }); await expect(client.chat({ model: 'test/model', messages: [] })).rejects.toThrow( OpenRouterError, ); expect(fetchFn).toHaveBeenCalledTimes(1); }); it('gives up after 5 retries on 500s', async () => { const fetchFn = vi.fn().mockResolvedValue(jsonResponse({ error: 'boom' }, 500)); const client = new OpenRouterClient({ apiKey: 'test-key', fetchFn, backoffBaseMs: 1 }); await expect(client.chat({ model: 'test/model', messages: [] })).rejects.toThrow( /after 6 attempt/, ); expect(fetchFn).toHaveBeenCalledTimes(6); }); it('lists models', async () => { const fetchFn = vi.fn().mockResolvedValue( jsonResponse({ data: [ { id: 'prov/model-x', name: 'Model X', context_length: 200000, pricing: { prompt: '0.000003', completion: '0.000015' }, }, ], }), ); const client = new OpenRouterClient({ apiKey: 'test-key', fetchFn }); const models = await client.listModels(); expect(models[0]?.id).toBe('prov/model-x'); }); });