import { test } from "node:test"; import assert from "node:assert/strict"; import { buildCatalog, joinCatalog } from "../src/catalog.js"; import type { ApiModelsResponse, Client, V1ModelsResponse } from "../src/client.js"; import { byId, fixture, fixtureCatalog } from "./helpers.js"; const api = fixture("api-models"); const v1 = fixture("v1-models"); test("transcription и speech: пометка вместо цены, токенные цены null", () => { const items = fixtureCatalog(); for (const [id, note] of [ ["openai/whisper-1", "тарифицируется по факту распознавания"], ["assemblyai/universal-3-5-pro", "тарифицируется по факту распознавания"], ["elevenlabs/eleven-flash-v2", "тарифицируется по факту синтеза"], ] as const) { const m = byId(items, id); assert.equal(m.price_note, note, id); assert.equal(m.price_prompt_rub_1m, null, id); assert.equal(m.price_completion_rub_1m, null, id); assert.equal(m.context_length, null, id); } }); test("image: цена с единицей на русском; без единицы — «цена уточняется»", () => { const items = fixtureCatalog(); const seedream = byId(items, "bytedance-seed/seedream-4.5"); assert.equal(seedream.price_note, "4.56651027 ₽ за изображение"); assert.equal(seedream.price_image_unit, "image"); assert.equal(seedream.price_prompt_rub_1m, null); const gemini = byId(items, "google/gemini-2.5-flash-image"); // единица token у бэкенда — цена за 1M токенов, не за один assert.equal(gemini.price_note, "3424.8827025 ₽ за 1M токенов"); const flux = byId(items, "black-forest-labs/flux-3-image"); assert.equal(flux.price_note, "цена уточняется"); assert.equal(flux.price_image_rub, null); assert.equal(flux.price_image_unit, null); }); test("decision: модальность из /api/models, completion-цена null", () => { const items = fixtureCatalog(); const jev = byId(items, "typesafe/jev-1.13.0"); assert.equal(jev.modality, "decision"); // /v1/models называет её text->text assert.equal(jev.price_completion_rub_1m, null); assert.equal(jev.price_prompt_rub_1m, "4.79483578"); assert.equal(jev.price_note, null); }); test("text: рублёвые и USD-цены, vendor, allowed_for_this_key", () => { const items = fixtureCatalog(); assert.equal(items.length, api.data.length); assert.ok(items.every((m) => m.allowed_for_this_key)); const ds = byId(items, "deepseek/deepseek-v4-flash"); assert.equal(ds.vendor, "deepseek"); assert.equal(ds.price_prompt_rub_1m, "10.38881086"); assert.deepEqual(ds.usd, { prompt: "0.0000001289925", completion: "0.000000257985", input_cache_read: "0.0000000257985", }); assert.equal(byId(items, "qwen/qwen3-coder-30b-a3b-instruct").usd?.input_cache_read, null); assert.equal(byId(items, "openai/whisper-1").usd, null); assert.equal(ds.price_note, null); }); test("модель только в /api/models — недоступна ключу; только в /v1/models — исключена с одной строкой в лог", () => { const logs: string[] = []; const v1Restricted = v1.data.filter((m) => m.id !== "anthropic/claude-sonnet-5.5"); const extraV1 = [...v1Restricted, { id: "ghost/model-a" }, { id: "ghost/model-b" }]; const items = joinCatalog(api.data, extraV1, (m) => logs.push(m)); assert.equal(byId(items, "anthropic/claude-sonnet-5.5").allowed_for_this_key, false); assert.equal(byId(items, "anthropic/claude-sonnet-5.5").usd, null); assert.equal(items.find((m) => m.id.startsWith("ghost/")), undefined); assert.equal(logs.length, 1); assert.match(logs[0] ?? "", /2 моделей разошлись/); }); test("описания чистятся от управляющих и bidi-символов", () => { const items = joinCatalog( [{ ...(api.data[0] as ApiModelsResponse["data"][number]), name: "Evil‮name", description: "a\u0007b​c" }], v1.data, () => undefined, ); assert.equal(items[0]?.name, "Evilname"); assert.equal(items[0]?.description, "abc"); }); test("buildCatalog тянет оба листинга параллельно", async () => { let inflight = 0; let maxInflight = 0; const track = async (v: T): Promise => { inflight += 1; maxInflight = Math.max(maxInflight, inflight); await new Promise((r) => setTimeout(r, 5)); inflight -= 1; return v; }; const client = { getApiModels: () => track(api), getV1Models: () => track(v1), } as unknown as Client; const items = await buildCatalog(client); assert.equal(items.length, api.data.length); assert.equal(maxInflight, 2); });