TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
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