SPB Git

spb/llmindex Public

The discriminative, contamination-resistant, fully transparent LLM ranking — updated live.

TypeScript 77.9% TeX 15.2% Python 3.7% SQL 1.4% JavaScript 1.1% Shell 0.5%
3.8 KB · 98 lines typescript
Raw Blame History
1/**2 * llmindex.io — OpenRouter client unit tests (mocked fetch)3 * Author:  Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * License: Proprietary — © Simon-Pierre Boucher, all rights reserved6 */7import { describe, expect, it, vi } from 'vitest';8import { OpenRouterClient, OpenRouterError } from './client';910const okBody = {11  id: 'gen-1',12  model: 'test/model',13  choices: [{ index: 0, message: { role: 'assistant', content: 'ANSWER: 42' }, finish_reason: 'stop' }],14  usage: { prompt_tokens: 100, completion_tokens: 50, total_tokens: 150 },15};1617function jsonResponse(body: unknown, status = 200): Response {18  return new Response(JSON.stringify(body), {19    status,20    headers: { 'Content-Type': 'application/json' },21  });22}2324describe('OpenRouterClient', () => {25  it('throws without an API key', () => {26    const prev = process.env.OPENROUTER_API_KEY;27    delete process.env.OPENROUTER_API_KEY;28    expect(() => new OpenRouterClient()).toThrow(OpenRouterError);29    if (prev) process.env.OPENROUTER_API_KEY = prev;30  });3132  it('sends required headers and computes cost from pricing', async () => {33    const fetchFn = vi.fn().mockResolvedValue(jsonResponse(okBody));34    const client = new OpenRouterClient({ apiKey: 'test-key', fetchFn, backoffBaseMs: 1 });35    const result = await client.chat(36      { model: 'test/model', messages: [{ role: 'user', content: 'hi' }], temperature: 0 },37      { promptPerM: 3, completionPerM: 15 },38    );39    const [url, init] = fetchFn.mock.calls[0]!;40    expect(url).toBe('https://openrouter.ai/api/v1/chat/completions');41    expect(init.headers.Authorization).toBe('Bearer test-key');42    expect(init.headers['HTTP-Referer']).toBe('https://www.llmindex.io');43    expect(init.headers['X-Title']).toBe('LLM Index');44    expect(result.text).toBe('ANSWER: 42');45    // 100 tok × $3/1M + 50 tok × $15/1M46    expect(result.costUsd).toBeCloseTo(0.00105, 8);47    expect(result.latencyMs).toBeGreaterThanOrEqual(0);48    expect(result.requestParams.temperature).toBe(0);49  });5051  it('retries on 429 then succeeds', async () => {52    const fetchFn = vi53      .fn()54      .mockResolvedValueOnce(jsonResponse({ error: 'rate limited' }, 429))55      .mockResolvedValueOnce(jsonResponse(okBody));56    const client = new OpenRouterClient({ apiKey: 'test-key', fetchFn, backoffBaseMs: 1 });57    const result = await client.chat({ model: 'test/model', messages: [] });58    expect(fetchFn).toHaveBeenCalledTimes(2);59    expect(result.text).toBe('ANSWER: 42');60  });6162  it('does not retry on 400 and surfaces the error', async () => {63    const fetchFn = vi.fn().mockResolvedValue(jsonResponse({ error: 'bad request' }, 400));64    const client = new OpenRouterClient({ apiKey: 'test-key', fetchFn, backoffBaseMs: 1 });65    await expect(client.chat({ model: 'test/model', messages: [] })).rejects.toThrow(66      OpenRouterError,67    );68    expect(fetchFn).toHaveBeenCalledTimes(1);69  });7071  it('gives up after 5 retries on 500s', async () => {72    const fetchFn = vi.fn().mockResolvedValue(jsonResponse({ error: 'boom' }, 500));73    const client = new OpenRouterClient({ apiKey: 'test-key', fetchFn, backoffBaseMs: 1 });74    await expect(client.chat({ model: 'test/model', messages: [] })).rejects.toThrow(75      /after 6 attempt/,76    );77    expect(fetchFn).toHaveBeenCalledTimes(6);78  });7980  it('lists models', async () => {81    const fetchFn = vi.fn().mockResolvedValue(82      jsonResponse({83        data: [84          {85            id: 'prov/model-x',86            name: 'Model X',87            context_length: 200000,88            pricing: { prompt: '0.000003', completion: '0.000015' },89          },90        ],91      }),92    );93    const client = new OpenRouterClient({ apiKey: 'test-key', fetchFn });94    const models = await client.listModels();95    expect(models[0]?.id).toBe('prov/model-x');96  });97});98