Files
vasyanskandClaude Opus 5.5 d7d02267e2 Добавь HTTP-клиент, каталог моделей и шесть read-only инструментов MCP
Клиент — замороженный объект с семью методами GET, без универсального
запроса и тела; денежные поля проверяются на строковый тип (fail-closed).
Каталог джойнит /api/models и /v1/models: модальность и рубли из первого,
USD и доступность ключу из второго. Инструменты: list_models, pick_model,
estimate_cost, get_balance, get_usage, list_api_keys. Точка входа stdio
уводит console.log/info в stderr первым импортом.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 10:59:06 +07:00

106 lines
4.8 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<ApiModelsResponse>("api-models");
const v1 = fixture<V1ModelsResponse>("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 <T>(v: T): Promise<T> => {
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);
});