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%
8.6 KB · 132 lines typescript
Raw Blame History
1import { describe, it, expect } from "vitest";2import { humanizeChatError, retryPhrase, secondsLeft } from "@/lib/chat/humanize-error";3import { splitStreamingMarkdown, normalizeMath, endsInsideFence, extensionForLanguage } from "@/lib/chat/markdown-blocks";4import { deprecationNotice, suggestReplacement, largerContextModel } from "@/lib/chat/deprecation";5import { chatRequestSchema, chatAdoptSchema } from "@/lib/chat/schemas";6import { toStoredError, stripJsonFence } from "@/lib/chat/service";7import type { PolyModel } from "@/lib/client/types";89const caps = { text: true, vision: false, audioInput: false, audioOutput: false, imageGeneration: false, video: false, reasoning: false, tools: true, structuredOutput: true, streaming: true, files: false, webSearch: false };10const mk = (key: string, extra: Partial<PolyModel> = {}): PolyModel => ({ key, id: key.split("/")[1], provider: key.split("/")[0] as PolyModel["provider"], displayName: key.split("/")[1], capabilities: caps, parameters: {}, status: "active", pricing: { inputPerMillion: 1, outputPerMillion: 2 }, ...extra });1112describe("humanizeChatError", () => {13  it("writes provider-specific rate limit copy with the retry delay", () => {14    const h = humanizeChatError({ code: "RATE_LIMITED", provider: "openai", retryAfterMs: 18_000, retryable: true, message: "429" });15    expect(h.title).toBe("OpenAI rate limit reached.");16    expect(h.description).toContain("Retry in 18 seconds.");17    expect(h.canRetry).toBe(true);18    expect(h.retryAfterMs).toBe(18_000);19  });20  it("points invalid keys and quota problems to the Providers settings", () => {21    expect(humanizeChatError({ code: "INVALID_API_KEY", provider: "anthropic" }).suggestProviders).toBe(true);22    expect(humanizeChatError({ code: "INSUFFICIENT_CREDITS", provider: "gemini" }).title).toBe("Google Gemini quota exhausted.");23    expect(humanizeChatError({ code: "INVALID_API_KEY", provider: "anthropic" }).canRetry).toBe(false);24  });25  it("covers outage, context, parameter and timeout codes", () => {26    expect(humanizeChatError({ code: "PROVIDER_UNAVAILABLE", provider: "xai" }).title).toBe("xAI is having an outage.");27    expect(humanizeChatError({ code: "CONTEXT_TOO_LONG" }).suggestSwitch).toBe(true);28    expect(humanizeChatError({ code: "INVALID_PARAMETER" }).description).toMatch(/Reset the model configuration/);29    expect(humanizeChatError({ code: "REQUEST_TIMEOUT", provider: "mistral" }).title).toBe("Mistral timed out.");30  });31  it("falls back for unknown codes and keeps diagnostics", () => {32    const h = humanizeChatError({ code: "WEIRD", provider: "cerebras", providerCode: "x_y", status: 418, message: "teapot" }, { requestId: "req_1" });33    expect(h.title).toBe("Cerebras returned an unexpected error.");34    expect(h.details).toMatchObject({ code: "WEIRD", providerCode: "x_y", status: 418, requestId: "req_1", raw: "teapot" });35  });36  it("formats retry phrases and countdowns", () => {37    expect(retryPhrase(null)).toBe("Retry in a moment.");38    expect(retryPhrase(1000)).toBe("Retry in 1 second.");39    expect(retryPhrase(90_000)).toMatch(/about 2 minutes/);40    expect(secondsLeft(5000, 1000, 2000)).toBe(4);41    expect(secondsLeft(5000, 1000, 7000)).toBeNull();42    expect(secondsLeft(null, 0, 0)).toBeNull();43  });44});4546describe("splitStreamingMarkdown", () => {47  it("cuts completed paragraphs and keeps the tail live", () => {48    const { blocks, tail } = splitStreamingMarkdown("Para one.\n\nPara two.\n\nPara thr");49    expect(blocks).toEqual(["Para one.", "Para two."]);50    expect(tail).toBe("Para thr");51  });52  it("never cuts inside a code fence", () => {53    const { blocks, tail } = splitStreamingMarkdown("Intro\n\n```ts\nconst a = 1;\n\nconst b = 2;\n");54    expect(blocks).toEqual(["Intro"]);55    expect(tail).toContain("const b = 2;");56    expect(endsInsideFence("```\nx")).toBe(true);57    expect(endsInsideFence("```\nx\n```")).toBe(false);58  });59  it("keeps loose lists, quotes and indented continuations together", () => {60    expect(splitStreamingMarkdown("1. a\n\n2. b\n\n3. c").blocks).toEqual([]);61    expect(splitStreamingMarkdown("> q1\n\n> q2\n\nnext").blocks).toEqual(["> q1\n\n> q2"]);62    expect(splitStreamingMarkdown("- item\n\n    continued\n\nAfter").blocks).toEqual(["- item\n\n    continued"]);63  });64  it("round-trips: blocks + tail reproduce the text modulo blank lines", () => {65    const text = "# Title\n\nSome text.\n\n| a | b |\n|---|---|\n| 1 | 2 |\n\nEnd";66    const { blocks, tail } = splitStreamingMarkdown(text);67    expect([...blocks, tail].join("\n\n")).toBe(text);68  });69  it("honours a minimum tail length", () => {70    const { blocks, tail } = splitStreamingMarkdown("A.\n\nB.\n\nC", { minTailChars: 4 });71    expect(blocks).toEqual(["A."]);72    expect(tail).toBe("B.\n\nC");73  });74});7576describe("normalizeMath", () => {77  it("rewrites LaTeX delimiters outside code", () => {78    expect(normalizeMath("Euler: \\(e^{i\\pi}+1=0\\) and \\[\\int_0^1 x\\,dx\\]")).toBe("Euler: $e^{i\\pi}+1=0$ and $$\n\\int_0^1 x\\,dx\n$$");79    expect(normalizeMath("`\\(keep\\)` and ```\n\\[keep\\]\n```")).toBe("`\\(keep\\)` and ```\n\\[keep\\]\n```");80    expect(normalizeMath("plain")).toBe("plain");81  });82  it("maps languages to file extensions", () => {83    expect(extensionForLanguage("typescript")).toBe("ts");84    expect(extensionForLanguage("unknownlang")).toBe("txt");85  });86});8788describe("deprecation helpers", () => {89  const now = Date.UTC(2026, 8, 11);90  it("detects deprecated and soon-retiring models", () => {91    expect(deprecationNotice(mk("openai/a"), now)).toBeNull();92    expect(deprecationNotice(mk("openai/a", { status: "deprecated" }), now)?.kind).toBe("deprecated");93    expect(deprecationNotice(mk("openai/a", { metadata: { shutdownDate: "2026-10-01" } }), now)?.kind).toBe("retiring");94    expect(deprecationNotice(mk("openai/a", { metadata: { shutdownDate: "2027-10-01" } }), now)).toBeNull();95  });96  it("suggests the newest same-provider active model, same family first", () => {97    const old = mk("openai/gpt-4o", { status: "deprecated", family: "gpt-4" });98    const models = [old, mk("anthropic/claude", { metadata: { sortWeight: 99 } }), mk("openai/gpt-5", { family: "gpt-5", metadata: { sortWeight: 50 } }), mk("openai/gpt-4.1", { family: "gpt-4", metadata: { sortWeight: 10 } }), mk("openai/o1", { status: "deprecated" })];99    expect(suggestReplacement(old, models)?.key).toBe("openai/gpt-4.1");100    expect(suggestReplacement(mk("openai/x", { status: "deprecated" }), models)?.key).toBe("openai/gpt-5");101    expect(suggestReplacement(old, models, new Set(["anthropic"]))).toBeNull();102  });103  it("finds a larger-context model", () => {104    const cur = mk("openai/small", { limits: { contextTokens: 128_000 } });105    const models = [cur, mk("openai/big", { limits: { contextTokens: 400_000 } }), mk("gemini/huge", { limits: { contextTokens: 1_000_000 } })];106    expect(largerContextModel(cur, models, new Set(["openai", "gemini"]))?.key).toBe("openai/big");107    expect(largerContextModel(cur, models, new Set(["gemini"]))?.key).toBe("gemini/huge");108    expect(largerContextModel(mk("gemini/huge", { limits: { contextTokens: 1_000_000 } }), models, new Set(["openai", "gemini"]))).toBeNull();109  });110});111112describe("chat schemas", () => {113  it("accepts ephemeral requests with history and projectId", () => {114    const parsed = chatRequestSchema.parse({ modelKey: "openai/gpt-5", message: { text: "hi" }, ephemeral: true, history: [{ role: "user", content: "a" }, { role: "assistant", content: "b" }], projectId: "prj_1" });115    expect(parsed.ephemeral).toBe(true);116    expect(parsed.history?.length).toBe(2);117    expect(parsed.action).toBe("send");118  });119  it("validates adopt payloads", () => {120    expect(chatAdoptSchema.safeParse({ modelKey: "openai/gpt-5", conversationId: "cnv_1", content: "answer" }).success).toBe(true);121    expect(chatAdoptSchema.safeParse({ modelKey: "openai/gpt-5", arenaResponseId: "arr_1", userText: "prompt" }).success).toBe(true);122    expect(chatAdoptSchema.safeParse({ modelKey: "openai/gpt-5", conversationId: "cnv_1" }).success).toBe(false);123    expect(chatAdoptSchema.safeParse({ modelKey: "openai/gpt-5", content: "x" }).success).toBe(false);124  });125  it("stores safe error diagnostics", () => {126    const e = toStoredError({ code: "RATE_LIMITED", message: "429 Too Many Requests", provider: "openai", status: 429, retryable: true, retryAfterMs: 18_000, providerCode: "rate_limit_exceeded" });127    expect(e).toMatchObject({ code: "RATE_LIMITED", provider: "openai", status: 429, retryAfterMs: 18_000, providerCode: "rate_limit_exceeded", detail: "429 Too Many Requests" });128    expect(e.message).toMatch(/rate-limiting/);129    expect(stripJsonFence("```json\n{\"a\":1}\n```")).toBe('{"a":1}');130  });131});132