SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
4.8 KB · 82 lines typescript
Raw Blame History
1import { describe, it, expect } from "vitest";2import { computeSavings, coversCapabilities, type ModelUsageAgg } from "@/lib/usage/savings";3import type { PolyModel, ModelCapabilities } from "@/lib/ai/core/types";45const caps = (over: Partial<ModelCapabilities> = {}): ModelCapabilities => ({ text: true, vision: true, audioInput: false, audioOutput: false, imageGeneration: false, video: false, reasoning: true, tools: true, structuredOutput: true, streaming: true, files: true, webSearch: false, ...over });67function model(key: string, input: number, output: number, extra: Partial<PolyModel> = {}): PolyModel {8  const [provider, id] = key.split("/") as [PolyModel["provider"], string];9  return { key, id, provider, displayName: id, family: "Claude", capabilities: caps(), parameters: {}, status: "active", pricing: { inputPerMillion: input, outputPerMillion: output, cachedInputPerMillion: input / 10 }, ...extra };10}1112const registry = new Map<string, PolyModel>(13  [14    model("anthropic/claude-opus-4", 15, 75, { displayName: "Claude Opus 4" }),15    model("anthropic/claude-sonnet-4", 3, 15, { displayName: "Claude Sonnet 4" }),16    model("anthropic/claude-haiku-4", 0.8, 4, { displayName: "Claude Haiku 4", capabilities: caps({ files: false }) }),17    model("anthropic/claude-old", 1, 5, { status: "deprecated" }),18    model("openai/gpt-x", 2, 8, { family: "GPT", capabilities: caps() }),19  ].map((m) => [m.key, m]),20);2122const opusRow: ModelUsageAgg = { modelKey: "anthropic/claude-opus-4", provider: "anthropic", requests: 40, inputTokens: 1_000_000, outputTokens: 200_000, cachedTokens: 0, reasoningTokens: 0, costUsd: 30 };2324describe("savings opportunities", () => {25  it("picks the closest cheaper sibling on the same provider, not blindly the cheapest", () => {26    const [opp] = computeSavings([opusRow], registry);27    expect(opp).toBeDefined();28    expect(opp.toModelKey).toBe("anthropic/claude-sonnet-4"); // Haiku is cheaper but Sonnet is the next tier and still saves > 40 %29    // Opus: 1M × $15 + 0.2M × $75 = $30 ; Sonnet: $3 + $3 = $630    expect(opp.currentCostUsd).toBeCloseTo(30, 4);31    expect(opp.alternativeCostUsd).toBeCloseTo(6, 4);32    expect(opp.savingsUsd).toBeCloseTo(24, 4);33    expect(opp.savingsPct).toBe(80);34    expect(opp.headline).toContain("Claude Opus 4");35    expect(opp.headline).toContain("Claude Sonnet 4");36    expect(opp.headline).toMatch(/\$24\.00/);37  });3839  it("re-prices cached input at the cached rate", () => {40    const row = { ...opusRow, cachedTokens: 500_000 };41    const [opp] = computeSavings([row], registry);42    // Opus: 0.5M × 15 + 0.5M × 1.5 + 0.2M × 75 = 7.5 + 0.75 + 15 = 23.2543    expect(opp.currentCostUsd).toBeCloseTo(23.25, 4);44  });4546  it("never proposes a sibling that lacks a capability the source has", () => {47    const only = new Map(registry);48    only.delete("anthropic/claude-sonnet-4"); // leaves Haiku (no files) as the only cheaper sibling49    expect(computeSavings([opusRow], only)).toEqual([]);50    expect(coversCapabilities(registry.get("anthropic/claude-haiku-4")!, registry.get("anthropic/claude-opus-4")!)).toBe(false);51    expect(coversCapabilities(registry.get("anthropic/claude-sonnet-4")!, registry.get("anthropic/claude-opus-4")!)).toBe(true);52  });5354  it("ignores deprecated siblings, other providers, unpriced sources and tiny amounts", () => {55    const sonnetRow: ModelUsageAgg = { ...opusRow, modelKey: "anthropic/claude-sonnet-4", costUsd: 6 };56    const noHaiku = new Map(registry);57    noHaiku.delete("anthropic/claude-haiku-4");58    expect(computeSavings([sonnetRow], noHaiku)).toEqual([]); // only deprecated claude-old and OpenAI remain59    const unpriced = new Map(registry);60    unpriced.set("anthropic/claude-opus-4", { ...registry.get("anthropic/claude-opus-4")!, pricing: null });61    expect(computeSavings([opusRow], unpriced)).toEqual([]);62    const tiny: ModelUsageAgg = { ...opusRow, inputTokens: 100, outputTokens: 10, costUsd: 0.002 };63    expect(computeSavings([tiny], registry)).toEqual([]);64  });6566  it("requires the sibling's context window to fit the average prompt", () => {67    const small = new Map(registry);68    small.set("anthropic/claude-sonnet-4", { ...registry.get("anthropic/claude-sonnet-4")!, limits: { contextTokens: 8_000 } });69    small.delete("anthropic/claude-haiku-4");70    const row = { ...opusRow, requests: 10 }; // 100K tokens per request71    expect(computeSavings([row], small)).toEqual([]);72  });7374  it("returns the top 3 sorted by savings", () => {75    const rows: ModelUsageAgg[] = ["a", "b", "c", "d"].map((s, i) => ({ ...opusRow, modelKey: `anthropic/claude-opus-4`, requests: 10, inputTokens: (i + 1) * 100_000, outputTokens: 0, costUsd: 1 }));76    const out = computeSavings(rows, registry, { top: 3 });77    expect(out).toHaveLength(3);78    expect(out[0].savingsUsd).toBeGreaterThanOrEqual(out[1].savingsUsd);79    expect(out[1].savingsUsd).toBeGreaterThanOrEqual(out[2].savingsUsd);80  });81});82