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%
9.9 KB · 162 lines typescript
Raw Blame History
1import { describe, it, expect } from "vitest";2import { filterSettings, snapEffort } from "@/lib/ai/core/normalize";3import { estimateCost, tokensPerSecond } from "@/lib/ai/core/pricing";4import { codeFromStatus, refineByMessage, parseRetryAfter, normalizeGenericError } from "@/lib/ai/core/errors";5import { StreamAccumulator, safeJson } from "@/lib/ai/core/stream-utils";6import { modelKey, parseModelKey, type PolyModel } from "@/lib/ai/core/types";7import { normalizeAnthropicModel } from "@/lib/ai/providers/anthropic";8import { normalizeOpenAIModel } from "@/lib/ai/providers/openai";9import { normalizeXaiModel } from "@/lib/ai/providers/xai";10import { deriveTitle } from "@/lib/chat/service";1112const model: PolyModel = {13  key: "openai/gpt-5.5",14  id: "gpt-5.5",15  provider: "openai",16  displayName: "GPT-5.5",17  capabilities: { text: true, vision: true, audioInput: false, audioOutput: false, imageGeneration: false, video: false, reasoning: true, tools: true, structuredOutput: true, streaming: true, files: true, webSearch: true },18  limits: { contextTokens: 1_050_000, maxOutputTokens: 128_000 },19  parameters: { temperature: true, topP: true, maxTokens: true, stop: false, seed: false, reasoningEffort: true, reasoningEffortLevels: ["none", "low", "medium", "high", "xhigh"], temperatureRange: { min: 0, max: 2 } },20  status: "active",21  pricing: { inputPerMillion: 5, cachedInputPerMillion: 0.5, outputPerMillion: 30 },22};2324describe("parameter normalization", () => {25  it("drops unsupported parameters and clamps ranges", () => {26    const { settings, dropped } = filterSettings({ temperature: 3, seed: 42, stop: ["x"], topK: 5, maxTokens: 999_999, reasoningEffort: "max", verbosity: "low" }, model);27    expect(settings.temperature).toBe(2);28    expect(settings.seed).toBeUndefined();29    expect(settings.stop).toBeUndefined();30    expect(settings.topK).toBeUndefined();31    expect(settings.maxTokens).toBe(128_000);32    expect(settings.reasoningEffort).toBe("xhigh"); // snapped to the closest accepted level33    expect(settings.verbosity).toBeUndefined();34    expect(dropped.map((d) => d.name).sort()).toEqual(["seed", "stop", "topK", "verbosity"]);35  });36  it("snaps effort levels", () => {37    expect(snapEffort("minimal", ["low", "medium", "high"])).toBe("low");38    expect(snapEffort("max", ["low", "medium", "high"])).toBe("high");39    expect(snapEffort("none", ["low", "medium"])).toBe("low");40  });41  it("passes everything through when no model sheet is available", () => {42    const { settings } = filterSettings({ temperature: 0.3, seed: 1 }, undefined);43    expect(settings).toEqual({ temperature: 0.3, seed: 1 });44  });45});4647describe("pricing", () => {48  it("estimates cost with cached tokens and long-context tiers", () => {49    const c = estimateCost({ inputTokens: 1_000_000, outputTokens: 100_000, cachedInputTokens: 500_000 }, model.pricing);50    expect(c.known).toBe(true);51    expect(c.totalUsd).toBeCloseTo(0.5 * 5 + 0.5 * 0.5 + 0.1 * 30, 6);52    const lc = estimateCost({ inputTokens: 300_000, outputTokens: 1000 }, { inputPerMillion: 2, outputPerMillion: 6, longContext: { thresholdTokens: 200_000, inputPerMillion: 4, outputPerMillion: 12 } });53    expect(lc.totalUsd).toBeCloseTo(0.3 * 4 + 0.001 * 12, 6);54    expect(estimateCost({ inputTokens: 10, outputTokens: 10 }, null).known).toBe(false);55    expect(tokensPerSecond(500, 5000, 1000)).toBe(125);56  });57});5859describe("error normalization", () => {60  it("maps statuses and refines by message", () => {61    expect(codeFromStatus(401)).toBe("INVALID_API_KEY");62    expect(codeFromStatus(429)).toBe("RATE_LIMITED");63    expect(codeFromStatus(503)).toBe("PROVIDER_UNAVAILABLE");64    expect(refineByMessage("INVALID_PARAMETER", "This model's maximum context length is 128000 tokens")).toBe("CONTEXT_TOO_LONG");65    expect(refineByMessage("INVALID_PARAMETER", "You exceeded your current quota, please check your plan and billing details")).toBe("INSUFFICIENT_CREDITS");66    expect(refineByMessage("UNKNOWN_PROVIDER_ERROR", "Incorrect API key provided")).toBe("INVALID_API_KEY");67    expect(parseRetryAfter(new Headers({ "retry-after": "2" }))).toBe(2000);68    expect(parseRetryAfter({ "retry-after-ms": "750" })).toBe(750);69    const e = normalizeGenericError("xai", Object.assign(new Error("Rate limit"), { status: 429, headers: { "retry-after": "1" } }));70    expect(e.code).toBe("RATE_LIMITED");71    expect(e.retryable).toBe(true);72    expect(e.retryAfterMs).toBe(1000);73    const abort = normalizeGenericError("openai", Object.assign(new Error("aborted"), { name: "AbortError" }));74    expect(abort.code).toBe("CANCELLED");75    expect(abort.retryable).toBe(false);76  });77  it("never leaks key material in messages", () => {78    const e = normalizeGenericError("openai", Object.assign(new Error("bad key sk-proj-abcdefghijklmnopqrstuvwxyz0123456789"), { status: 401 }));79    expect(e.message).toBe("Invalid API key");80    const e2 = normalizeGenericError("openai", Object.assign(new Error("oops sk-proj-abcdefghijklmnopqrstuvwxyz0123456789"), { status: 500 }));81    expect(e2.message).not.toContain("abcdefghijklmnop");82  });83});8485describe("stream accumulator", () => {86  it("rebuilds text, reasoning, tool calls and usage", () => {87    const acc = new StreamAccumulator();88    acc.push({ type: "start", id: "r1", model: "m" });89    acc.push({ type: "reasoning-delta", text: "think " });90    acc.push({ type: "text-delta", text: "Hello" });91    acc.push({ type: "text-delta", text: " world" });92    acc.push({ type: "tool-start", id: "c1", name: "calc" });93    acc.push({ type: "tool-delta", id: "c1", argumentsDelta: '{"expression":' });94    acc.push({ type: "tool-delta", id: "c1", argumentsDelta: '"1+1"}' });95    acc.push({ type: "tool-end", id: "c1", name: "calc", arguments: {}, argumentsText: "" });96    acc.push({ type: "usage", usage: { inputTokens: 10, outputTokens: 5 } });97    acc.push({ type: "finish", reason: "tool-calls" });98    const r = acc.toResponse("openai", "m");99    expect(r.text).toBe("Hello world");100    expect(r.reasoning).toBe("think ");101    expect(r.toolCalls).toEqual([{ type: "tool-call", id: "c1", name: "calc", arguments: { expression: "1+1" }, argumentsText: '{"expression":"1+1"}', providerData: undefined }]);102    expect(r.usage?.inputTokens).toBe(10);103    expect(r.finishReason).toBe("tool-calls");104    expect(safeJson("not json")).toEqual({ _raw: "not json" });105  });106});107108describe("model normalization", () => {109  it("normalizes Anthropic Models API entries", () => {110    const m = normalizeAnthropicModel({111      id: "claude-opus-5",112      display_name: "Claude Opus 5",113      created_at: "2026-04-01T00:00:00Z",114      type: "model",115      max_input_tokens: 1_000_000,116      max_tokens: 128_000,117      capabilities: { thinking: { supported: true, types: { adaptive: { supported: true }, enabled: { supported: false } } }, effort: { supported: true, low: { supported: true }, medium: { supported: true }, high: { supported: true }, xhigh: { supported: true }, max: { supported: true } }, image_input: { supported: true }, pdf_input: { supported: true }, structured_outputs: { supported: true } },118    } as never);119    expect(m.key).toBe("anthropic/claude-opus-5");120    expect(m.parameters.temperature).toBe(false); // sampling removed on adaptive-only generations121    expect(m.parameters.reasoningEffortLevels).toEqual(["none", "low", "medium", "high", "xhigh", "max"]);122    expect(m.pricing?.inputPerMillion).toBe(5);123    const haiku = normalizeAnthropicModel({ id: "claude-haiku-4-5-20251001", display_name: "Claude Haiku 4.5", created_at: "", type: "model", max_input_tokens: 200_000, max_tokens: 64_000, capabilities: { thinking: { supported: true, types: { adaptive: { supported: false }, enabled: { supported: true } } }, effort: { supported: false } } } as never);124    expect(haiku.parameters.temperature).toBe(true);125    expect(haiku.parameters.thinkingBudget).toBe(true);126    expect(haiku.parameters.reasoningEffort).toBe(false);127    expect(haiku.pricing?.inputPerMillion).toBe(1);128  });129  it("filters OpenAI non-chat models and merges the catalog", () => {130    expect(normalizeOpenAIModel({ id: "gpt-4o-mini-tts", object: "model", created: 0, owned_by: "openai" })).toBeNull();131    expect(normalizeOpenAIModel({ id: "text-embedding-3-large", object: "model", created: 0, owned_by: "openai" })).toBeNull();132    expect(normalizeOpenAIModel({ id: "gpt-4", object: "model", created: 0, owned_by: "openai", shutdown_date: "2020-01-01" } as never)).toBeNull();133    const m = normalizeOpenAIModel({ id: "gpt-5.5", object: "model", created: 0, owned_by: "openai" })!;134    expect(m.capabilities.reasoning).toBe(true);135    expect(m.parameters.seed).toBe(false);136    expect(m.parameters.reasoningEffortLevels).toContain("none");137    expect(m.pricing?.outputPerMillion).toBe(30);138    expect(m.metadata?.samplingMode).toBe("conditional");139  });140  it("converts xAI price units (cents per 100M tokens)", () => {141    const m = normalizeXaiModel({ id: "grok-4.6", input_modalities: ["text", "image"], output_modalities: ["text"], prompt_text_token_price: 20000, cached_prompt_text_token_price: 5000, completion_text_token_price: 60000, long_context_threshold: 200000, prompt_text_token_price_long_context: 40000, completion_text_token_price_long_context: 120000, aliases: [] });142    expect(m.pricing?.inputPerMillion).toBe(2);143    expect(m.pricing?.outputPerMillion).toBe(6);144    expect(m.pricing?.longContext?.inputPerMillion).toBe(4);145    expect(m.parameters.frequencyPenalty).toBe(false);146    expect(m.limits?.contextTokens).toBe(500_000);147  });148  it("model keys round-trip", () => {149    expect(parseModelKey(modelKey("gemini", "gemini-3.8-flash"))).toEqual({ provider: "gemini", id: "gemini-3.8-flash" });150    expect(parseModelKey("nope")).toBeNull();151    expect(parseModelKey("foo/bar")).toBeNull();152  });153});154155describe("titles", () => {156  it("derives a clean title from the first message", () => {157    expect(deriveTitle("# Hello **world**! How are you? More text.")).toBe("Hello world!");158    expect(deriveTitle("   ")).toBeUndefined();159    expect(deriveTitle("a".repeat(100))!.length).toBeLessThanOrEqual(64);160  });161});162