SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
4.8 KB · 102 lines typescript
Raw Blame History
1import { describe, expect, it, vi, beforeEach } from 'vitest';2import { z } from 'zod';3import { estimateUsd, priceFor, addUsage, ZERO_USAGE } from './pricing.js';4import { ModelRouter } from './router.js';5import { AiNotConfiguredError, type ModelProvider, type Role } from './types.js';6import { recentCosts, setCostSink, recordCost } from './costs.js';7import { identifyCollectible, IdentificationSchema } from './helpers/identify.js';89describe('pricing', () => {10  it('estimates usd from the price table', () => {11    expect(priceFor('claude-opus-5')?.input).toBe(5);12    expect(estimateUsd('claude-haiku-4-5', { inputTokens: 1_000_000, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 })).toBe(1);13    expect(estimateUsd('claude-opus-5', { inputTokens: 0, outputTokens: 1000, cacheReadTokens: 1_000_000, cacheWriteTokens: 0 })).toBeCloseTo(0.025 + 0.5, 5);14    expect(estimateUsd('unknown-local-model', { ...ZERO_USAGE, inputTokens: 5000 })).toBe(0);15    expect(addUsage(ZERO_USAGE, { inputTokens: 1, outputTokens: 2, cacheReadTokens: 3, cacheWriteTokens: 4 }).cacheWriteTokens).toBe(4);16  });17});1819describe('router', () => {20  it('reports not configured without keys', () => {21    const r = new ModelRouter({ persistCosts: false });22    expect(r.configured).toBe(false);23    expect(() => r.providerFor('research')).toThrow(AiNotConfiguredError);24    expect(r.table().every((t) => t.provider === null)).toBe(true);25  });26  it('routes roles and honours overrides', () => {27    const r = new ModelRouter({ anthropicApiKey: 'sk-test', openaiApiKey: 'sk-openai', overrides: { research: 'anthropic:claude-opus-5', classify: 'claude-haiku-4-5', embed: 'openai:text-embedding-3-small' }, persistCosts: false });28    expect(r.providerFor('research').id).toBe('anthropic');29    expect(r.modelFor('research')).toBe('claude-opus-5');30    expect(r.modelFor('normalize')).toBe('claude-haiku-4-5');31    expect(r.providerFor('embed').id).toBe('openai');32    expect(r.modelFor('vision')).toBe('claude-opus-5');33    const table = r.table();34    expect(table.find((t) => t.role === 'embed')?.provider).toBe('openai');35  });36  it('embeddings unavailable with Anthropic only', () => {37    const r = new ModelRouter({ anthropicApiKey: 'sk-test', persistCosts: false });38    expect(r.supports('embed')).toBe(false);39    expect(() => r.providerFor('embed')).toThrow(/embeddings/);40  });41});4243describe('cost sink', () => {44  beforeEach(() => setCostSink(null));45  it('records and buffers events', async () => {46    const sink = vi.fn();47    setCostSink(sink);48    await recordCost({ provider: 'anthropic', model: 'claude-haiku-4-5', role: 'classify', usage: ZERO_USAGE, usdEst: 0.001 });49    expect(sink).toHaveBeenCalledTimes(1);50    expect(recentCosts().at(-1)?.role).toBe('classify');51  });52});5354function fakeProvider(data: unknown): ModelProvider {55  return {56    id: 'fake',57    supports: () => true,58    modelFor: () => 'fake-model',59    complete: async () => ({ text: '', model: 'fake-model', provider: 'fake', usage: ZERO_USAGE, usdEst: 0, stopReason: 'end_turn', refusal: null }),60    stream: async function* () {},61    runTools: async function* () {},62    extract: async <T,>(_role: Role, req: { schema: z.ZodType<T> }) => {63      const parsed = req.schema.parse(data);64      return { data: parsed, confidence: (parsed as { confidence?: number }).confidence ?? 1, model: 'fake-model', provider: 'fake', usage: ZERO_USAGE, usdEst: 0 };65    },66    vision: async <T,>(req: { schema: z.ZodType<T> }) => ({ data: req.schema.parse(data), confidence: 0.9, model: 'fake-model', provider: 'fake', usage: ZERO_USAGE, usdEst: 0 }),67    embed: async () => ({ vectors: [], model: 'fake', provider: 'fake', dimensions: 0, usdEst: 0 }),68  };69}7071describe('identifyCollectible', () => {72  it('validates schema and rejects unknown categories', async () => {73    const guess = IdentificationSchema.parse({74      categorySlug: 'not_a_real_slug',75      name: 'Charizard',76      brand: null,77      franchise: 'Pokémon',78      set: 'Base Set',79      number: '4/102',80      year: 1999,81      variant: '1st Edition Holo',82      language: 'English',83      grader: 'psa',84      grade: '10',85      certificationNumber: null,86      conditionNotes: null,87      likelyGradeRange: null,88      confidence: 0.82,89      rationale: 'Holo foil, 1st edition stamp visible',90      searchQueries: ['Charizard Base Set 1st Edition 4/102'],91      warnings: [],92    });93    const res = await identifyCollectible({ text: '1999 Pokemon Base Set Charizard 1st Edition PSA 10', provider: fakeProvider(guess) });94    expect(res.data.categorySlug).toBeNull();95    expect(res.data.warnings[0]).toMatch(/unknown category/);96    expect(res.confidence).toBeCloseTo(0.82);97  });98  it('requires input', async () => {99    await expect(identifyCollectible({ provider: fakeProvider({}) })).rejects.toThrow(/provide images or text/);100  });101});102