Заведи каркас пакета и утилитный слой MCP-сервера

package.json, tsconfig, eslint, генерация типов из OpenAPI-спеки,
модули config/redact/errors/money/sanitize/cache с тестами и
обезличенные фикстуры живых ответов API openrusrouter.ru.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
vasyanskandClaude Opus 5.5 committed 2026-10-24 10:48:00 +07:00
commit ade6eb82df
29 files changed
+11350

No files matched your search

+42
View File
@@ -0,0 +1,42 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { createTtlCache } from "../src/cache.js";
test("значение живёт ttl и потом перезагружается", async () => {
let t = 1_000;
let calls = 0;
const get = createTtlCache(100, async () => ++calls, () => t);
assert.equal(await get(), 1);
t += 99;
assert.equal(await get(), 1);
t += 1;
assert.equal(await get(), 2);
assert.equal(calls, 2);
});
test("single-flight: один loader на N параллельных get()", async () => {
let calls = 0;
let release!: (v: string) => void;
const get = createTtlCache(1_000, () => {
calls++;
return new Promise<string>((r) => {
release = r;
});
});
const all = Promise.all(Array.from({ length: 10 }, () => get()));
release("ok");
assert.deepEqual(await all, Array(10).fill("ok"));
assert.equal(calls, 1);
});
test("ошибка не кэшируется", async () => {
let calls = 0;
const get = createTtlCache(1_000, async () => {
calls++;
if (calls === 1) throw new Error("fail");
return "ok";
});
await assert.rejects(get(), /fail/);
assert.equal(await get(), "ok");
assert.equal(calls, 2);
});
+72
View File
@@ -0,0 +1,72 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { ConfigError, DEFAULT_BASE_URL, parseConfig } from "../src/config.js";
const KEY = "orr_live_TestKey0123456789abcdefABCDEFGHIJ";
test("нет ключа — ConfigError без значений переменных", () => {
assert.throws(
() => parseConfig({ OPENRUSROUTER_BASE_URL: "https://secret-host.example/x" }),
(err: unknown) => {
assert.ok(err instanceof ConfigError);
assert.match(err.message, /OPENRUSROUTER_API_KEY/);
assert.ok(!err.message.includes("secret-host"));
assert.ok(!err.message.includes("${"));
return true;
},
);
assert.throws(() => parseConfig({ OPENRUSROUTER_API_KEY: " " }), ConfigError);
});
test("по умолчанию — прод, объект заморожен", () => {
const c = parseConfig({ OPENRUSROUTER_API_KEY: KEY });
assert.equal(c.baseUrl, DEFAULT_BASE_URL);
assert.equal(c.apiKey, KEY);
assert.ok(Object.isFrozen(c));
});
test("http на внешний хост — отказ", () => {
assert.throws(
() => parseConfig({ OPENRUSROUTER_API_KEY: KEY, OPENRUSROUTER_BASE_URL: "http://example.com" }),
ConfigError,
);
});
test("userinfo в адресе — отказ, и пароль не попадает в сообщение", () => {
assert.throws(
() =>
parseConfig({
OPENRUSROUTER_API_KEY: KEY,
OPENRUSROUTER_BASE_URL: "https://user:hunter2@openrusrouter.ru",
}),
(err: unknown) => {
assert.ok(err instanceof ConfigError);
assert.ok(!err.message.includes("hunter2"));
return true;
},
);
});
test("мусор и чужие протоколы — отказ", () => {
for (const url of ["not a url", "ftp://openrusrouter.ru", "https://openrusrouter.ru/?k=1"]) {
assert.throws(
() => parseConfig({ OPENRUSROUTER_API_KEY: KEY, OPENRUSROUTER_BASE_URL: url }),
ConfigError,
url,
);
}
});
test("https на любой хост и http на localhost — ок, хвостовой / срезан", () => {
const ok: Array<[string, string]> = [
["https://any-host.example/", "https://any-host.example"],
["https://any-host.example/prefix/", "https://any-host.example/prefix"],
["http://localhost:1234", "http://localhost:1234"],
["http://127.0.0.1:8080/", "http://127.0.0.1:8080"],
["http://[::1]:8080", "http://[::1]:8080"],
];
for (const [input, expected] of ok) {
const c = parseConfig({ OPENRUSROUTER_API_KEY: KEY, OPENRUSROUTER_BASE_URL: input });
assert.equal(c.baseUrl, expected);
}
});
+1
View File
@@ -0,0 +1 @@
{"balance":"1234.56789012","referral_balance":"0"}
+43
View File
@@ -0,0 +1,43 @@
{
"keys": [
{
"id": "6f1c2a9e-4b7d-4e21-9a3c-1d2e3f405a6b",
"name": "my-agent",
"prefix": "orr_live_Ab3dEf",
"last_used_at": "2026-10-24T03:39:57.228771Z",
"created_at": "2026-10-23T02:00:45.11982Z",
"revoked_at": null,
"spend_window": "day",
"spend_limit_rub": "200",
"spend_used": "12.5784961",
"spend_resets_at": "2026-10-25T00:00:00Z",
"allowed_models": null
},
{
"id": "a8b9c0d1-e2f3-4a5b-8c6d-7e8f90a1b2c3",
"name": "restricted-bot",
"prefix": "orr_live_Gh5iJk",
"last_used_at": "2026-10-20T01:36:06.837887Z",
"created_at": "2026-09-22T09:07:45.316979Z",
"revoked_at": null,
"spend_window": "day",
"spend_limit_rub": "100",
"spend_used": "0",
"spend_resets_at": "2026-10-25T00:00:00Z",
"allowed_models": ["openai/gpt-4o-mini"]
},
{
"id": "0d9e8f7a-6b5c-4d3e-a2f1-0b9c8d7e6f5a",
"name": "ci-runner",
"prefix": "orr_live_Lm7nOp",
"last_used_at": "2026-10-02T13:11:04.90802Z",
"created_at": "2026-10-02T02:37:36.886285Z",
"revoked_at": null,
"spend_window": "day",
"spend_limit_rub": "1000",
"spend_used": "0",
"spend_resets_at": "2026-10-25T00:00:00Z",
"allowed_models": null
}
]
}
+254
View File
@@ -0,0 +1,254 @@
{
"data": [
{
"id": "aion-labs/aion-3.0-mini",
"name": "AionLabs: Aion-3.0-Mini",
"description": "Aion-3.0 Mini is a multi-model roleplaying and storytelling system from AionLabs, built on the DeepSeek family of models. It uses a collaborative generation process in which multiple specialized models each...",
"context_length": 131072,
"price_prompt_rub_1m": "79.91392973",
"price_completion_rub_1m": "159.82785945",
"is_free": false,
"modality": "text",
"input_modalities": [
"text"
],
"price_image_rub": "0"
},
{
"id": "anthropic/claude-sonnet-5.5",
"name": "Anthropic: Claude Sonnet 5.5",
"description": "Claude Sonnet 5.5 is Anthropic's Sonnet-class model for well-scoped everyday work, succeeding Claude Sonnet 5 as a direct upgrade. It is especially strong at building features, fixing bugs, and producing...",
"context_length": 1000000,
"price_prompt_rub_1m": "228.3255135",
"price_completion_rub_1m": "1141.6275675",
"is_free": false,
"modality": "text",
"input_modalities": [
"text",
"image",
"file"
],
"price_image_rub": "0"
},
{
"id": "assemblyai/universal-3-5-pro",
"name": "AssemblyAI: Universal-3.5 Pro",
"description": "Universal-3.5 Pro is AssemblyAI's speech-to-text model served through its Sync API, returning a complete transcript with word-level timestamps in a single synchronous response for audio up to 20 minutes. It...",
"context_length": 0,
"price_prompt_rub_1m": "9513.55925707",
"price_completion_rub_1m": "0",
"is_free": false,
"modality": "transcription",
"input_modalities": [
"audio"
],
"price_image_rub": "0"
},
{
"id": "baai/bge-m3",
"name": "BAAI: bge-m3",
"description": "The bge-m3 embedding model encodes sentences, paragraphs, and long documents into a 1024-dimensional dense vector space, delivering high-quality semantic embeddings optimized for multilingual retrieval, semantic search, and large-context applications.",
"context_length": 8194,
"price_prompt_rub_1m": "1.14162757",
"price_completion_rub_1m": "0",
"is_free": false,
"modality": "embedding",
"input_modalities": [
"text"
],
"price_image_rub": "0"
},
{
"id": "black-forest-labs/flux-3-image",
"name": "Black Forest Labs: FLUX 3 Image",
"description": "FLUX.3 Image is Black Forest Labs' flagship image generation and editing model. It handles text-to-image and multi-reference editing with up to 10 input images, and renders at fixed resolution tiers...",
"context_length": 0,
"price_prompt_rub_1m": "0",
"price_completion_rub_1m": "0",
"is_free": false,
"modality": "image",
"input_modalities": [
"text",
"image"
],
"price_image_rub": "0"
},
{
"id": "bytedance-seed/seedream-4.5",
"name": "ByteDance Seed: Seedream 4.5",
"description": "Seedream 4.5 is the latest in-house image generation model developed by ByteDance. Compared with Seedream 4.0, it delivers comprehensive improvements, especially in editing consistency, including better preservation of subject details,...",
"context_length": 0,
"price_prompt_rub_1m": "0",
"price_completion_rub_1m": "0",
"is_free": false,
"modality": "image",
"input_modalities": [
"image",
"text"
],
"price_image_rub": "4.56651027",
"price_image_unit": "image"
},
{
"id": "deepseek/deepseek-v4-flash",
"name": "DeepSeek: DeepSeek V4 Flash 0423",
"description": "DeepSeek V4 Flash is an efficiency-optimized Mixture-of-Experts model from DeepSeek with 284B total parameters and 13B activated parameters, supporting a 1M-token context window. It is designed for fast inference and...",
"context_length": 1048576,
"price_prompt_rub_1m": "10.38881086",
"price_completion_rub_1m": "20.77762173",
"is_free": false,
"modality": "text",
"input_modalities": [
"text"
],
"price_image_rub": "0"
},
{
"id": "elevenlabs/eleven-flash-v2",
"name": "ElevenLabs: Eleven Flash v2",
"description": "Eleven Flash v2 is an English-only, ultra-low-latency text-to-speech model from ElevenLabs. It is suited for conversational and real-time English use cases, and has a 30,000-character request limit.",
"context_length": 0,
"price_prompt_rub_1m": "4566.51027",
"price_completion_rub_1m": "0",
"is_free": false,
"modality": "speech",
"input_modalities": [
"text"
],
"price_image_rub": "0"
},
{
"id": "elevenlabs/eleven-multilingual-v2",
"name": "ElevenLabs: Eleven Multilingual v2",
"description": "Eleven Multilingual v2 is a text-to-speech model from ElevenLabs. It is suited for lifelike, consistent long-form narration such as voice-overs and audiobooks, supports 29 languages, and has a 10,000-character request...",
"context_length": 0,
"price_prompt_rub_1m": "9133.02054",
"price_completion_rub_1m": "0",
"is_free": false,
"modality": "speech",
"input_modalities": [
"text"
],
"price_image_rub": "0"
},
{
"id": "google/gemini-2.5-flash-image",
"name": "Google: Nano Banana (Gemini 2.5 Flash Image)",
"description": "Gemini 2.5 Flash Image, a.k.a. \"Nano Banana,\" is now generally available. It is a state of the art image generation model with contextual understanding. It is capable of image generation,...",
"context_length": 0,
"price_prompt_rub_1m": "0",
"price_completion_rub_1m": "0",
"is_free": false,
"modality": "image",
"input_modalities": [
"image",
"text"
],
"price_image_rub": "3424.8827025",
"price_image_unit": "token"
},
{
"id": "nvidia/nemotron-3.5-embed-1b:free",
"name": "NVIDIA: Nemotron 3.5 Embed 1B (free)",
"description": "Nemotron 3.5 Embed 1B is a multimodal embedding model from NVIDIA for multilingual retrieval over text and document page images (tables, charts, infographics). Queries are text and documents can be...",
"context_length": 32768,
"price_prompt_rub_1m": "0",
"price_completion_rub_1m": "0",
"is_free": true,
"modality": "embedding",
"input_modalities": [
"text",
"image"
],
"price_image_rub": "0"
},
{
"id": "nvidia/nemotron-3.5-super-vl:free",
"name": "NVIDIA: Nemotron 3.5 Super VL (free)",
"description": "NVIDIA Nemotron 3.5 Super VL is an open multimodal mixture-of-experts model from NVIDIA, with 12B active parameters out of 120B total. It takes text, images, documents, GUI screenshots, and video...",
"context_length": 1000000,
"price_prompt_rub_1m": "0",
"price_completion_rub_1m": "0",
"is_free": true,
"modality": "text",
"input_modalities": [
"text",
"image",
"video"
],
"price_image_rub": "0"
},
{
"id": "openai/gpt-6-luna",
"name": "OpenAI: GPT-6 Luna",
"description": "GPT-6 Luna is the fast, cost-efficient model in OpenAI's GPT-6 series, positioned below GPT-6 Sol. It is suited for high-volume and latency-sensitive workloads such as chat, classification, and lightweight agentic...",
"context_length": 1050000,
"price_prompt_rub_1m": "11.41627568",
"price_completion_rub_1m": "57.08137838",
"is_free": false,
"modality": "text",
"input_modalities": [
"file",
"image",
"text"
],
"price_image_rub": "0"
},
{
"id": "openai/whisper-1",
"name": "OpenAI: Whisper 1",
"description": "Whisper is OpenAI's open-source automatic speech recognition model, available via API as `whisper-1`. It supports transcription and translation across 50+ languages from audio files up to 25 MB. Accepts formats...",
"context_length": 0,
"price_prompt_rub_1m": "11416.275675",
"price_completion_rub_1m": "0",
"is_free": false,
"modality": "transcription",
"input_modalities": [
"audio"
],
"price_image_rub": "0"
},
{
"id": "qwen/qwen3-coder-30b-a3b-instruct",
"name": "Qwen: Qwen3 Coder 30B A3B Instruct",
"description": "Qwen3-Coder-30B-A3B-Instruct is a 30.5B parameter Mixture-of-Experts (MoE) model with 128 experts (8 active per forward pass), designed for advanced code generation, repository-scale understanding, and agentic tool use. Built on the...",
"context_length": 262144,
"price_prompt_rub_1m": "7.99139297",
"price_completion_rub_1m": "31.96557189",
"is_free": false,
"modality": "text",
"input_modalities": [
"text"
],
"price_image_rub": "0"
},
{
"id": "typesafe/jev-1.13.0",
"name": "Jev 1.13",
"description": "Отвечает не текстом, а калиброванной вероятностью на типизированный вопрос о поданном тексте: да/нет, выбор из вариантов, оценка по шкале. Маршрут POST /v1/systemone — это не чат, поля messages у него нет. Основной язык модели — английский, остальные языки поддержаны заметно хуже: проверяйте качество на своей выборке. Известные ограничения провайдера: модель не считает, не сравнивает даты, не решает задач, требующих числовой точности, читает инструкции буквально и не генерирует текст.",
"context_length": 64000,
"price_prompt_rub_1m": "4.79483578",
"price_completion_rub_1m": "0",
"is_free": false,
"modality": "decision",
"input_modalities": [
"text"
],
"price_image_rub": "0"
},
{
"id": "typesafe/jev-latest",
"name": "Jev (последняя версия)",
"description": "Плавающий алиас: провайдер обслуживает им текущую версию Jev. Цена совпадает с закреплённой версией. Отвечает не текстом, а калиброванной вероятностью на типизированный вопрос о поданном тексте: да/нет, выбор из вариантов, оценка по шкале. Маршрут POST /v1/systemone — это не чат, поля messages у него нет. Основной язык модели — английский, остальные языки поддержаны заметно хуже: проверяйте качество на своей выборке. Известные ограничения провайдера: модель не считает, не сравнивает даты, не решает задач, требующих числовой точности, читает инструкции буквально и не генерирует текст.",
"context_length": 64000,
"price_prompt_rub_1m": "4.79483578",
"price_completion_rub_1m": "0",
"is_free": false,
"modality": "decision",
"input_modalities": [
"text"
],
"price_image_rub": "0"
}
]
}
+1
View File
@@ -0,0 +1 @@
{"data":[{"model":"deepseek/deepseek-v4-flash","requests":5,"tokens_prompt":51230,"tokens_cached":0,"tokens_completion":11874,"amount_rub":"0.8124417"},{"model":"elevenlabs/eleven-flash-v2","requests":3,"tokens_prompt":15,"tokens_cached":0,"tokens_completion":0,"amount_rub":"0.18412076"},{"model":"openai/whisper-1","requests":2,"tokens_prompt":0,"tokens_cached":0,"tokens_completion":0,"amount_rub":"0.24310558"},{"model":"openai/gpt-6-luna","requests":2,"tokens_prompt":1022,"tokens_cached":256,"tokens_completion":611,"amount_rub":"0.05120934"},{"model":"qwen/qwen3-coder-30b-a3b-instruct","requests":1,"tokens_prompt":301,"tokens_cached":0,"tokens_completion":38,"amount_rub":"0.00371284"}],"failed_events":0,"group_by":"model","from":"2026-10-17T03:39:57Z","to":"2026-10-24T03:39:57Z"}
+72
View File
@@ -0,0 +1,72 @@
{
"models": [
{
"id": "aion-labs/aion-3.0-mini",
"name": "AionLabs: Aion-3.0-Mini"
},
{
"id": "anthropic/claude-sonnet-5.5",
"name": "Anthropic: Claude Sonnet 5.5"
},
{
"id": "assemblyai/universal-3-5-pro",
"name": "AssemblyAI: Universal-3.5 Pro"
},
{
"id": "baai/bge-m3",
"name": "BAAI: bge-m3"
},
{
"id": "black-forest-labs/flux-3-image",
"name": "Black Forest Labs: FLUX 3 Image"
},
{
"id": "bytedance-seed/seedream-4.5",
"name": "ByteDance Seed: Seedream 4.5"
},
{
"id": "deepseek/deepseek-v4-flash",
"name": "DeepSeek: DeepSeek V4 Flash 0423"
},
{
"id": "elevenlabs/eleven-flash-v2",
"name": "ElevenLabs: Eleven Flash v2"
},
{
"id": "elevenlabs/eleven-multilingual-v2",
"name": "ElevenLabs: Eleven Multilingual v2"
},
{
"id": "google/gemini-2.5-flash-image",
"name": "Google: Nano Banana (Gemini 2.5 Flash Image)"
},
{
"id": "nvidia/nemotron-3.5-embed-1b:free",
"name": "NVIDIA: Nemotron 3.5 Embed 1B (free)"
},
{
"id": "nvidia/nemotron-3.5-super-vl:free",
"name": "NVIDIA: Nemotron 3.5 Super VL (free)"
},
{
"id": "openai/gpt-6-luna",
"name": "OpenAI: GPT-6 Luna"
},
{
"id": "openai/whisper-1",
"name": "OpenAI: Whisper 1"
},
{
"id": "qwen/qwen3-coder-30b-a3b-instruct",
"name": "Qwen: Qwen3 Coder 30B A3B Instruct"
},
{
"id": "typesafe/jev-1.13.0",
"name": "Jev 1.13"
},
{
"id": "typesafe/jev-latest",
"name": "Jev (последняя версия)"
}
]
}
+1
View File
@@ -0,0 +1 @@
{"data":{"label":"my-agent","balance":"187.42150390","account_balance":"1234.56789012","limit":"200","limit_remaining":"187.42150390","usage":"12.57849610","limit_window":"day","limit_resets_at":"2026-10-25T00:00:00Z"}}
+436
View File
@@ -0,0 +1,436 @@
{
"object": "list",
"data": [
{
"id": "aion-labs/aion-3.0-mini",
"object": "model",
"created": 1792811344,
"owned_by": "aion-labs",
"name": "AionLabs: Aion-3.0-Mini",
"description": "Aion-3.0 Mini is a multi-model roleplaying and storytelling system from AionLabs, built on the DeepSeek family of models. It uses a collaborative generation process in which multiple specialized models each...",
"context_length": 131072,
"architecture": {
"input_modalities": [
"text"
],
"output_modalities": [
"text"
],
"modality": "text->text"
},
"pricing": {
"prompt": "0.00000099225",
"completion": "0.0000019845",
"request": "0",
"image": "0",
"input_cache_read": "0.00000025515"
},
"top_provider": {
"context_length": 131072,
"is_moderated": false
}
},
{
"id": "anthropic/claude-sonnet-5.5",
"object": "model",
"created": 1792811344,
"owned_by": "anthropic",
"name": "Anthropic: Claude Sonnet 5.5",
"description": "Claude Sonnet 5.5 is Anthropic's Sonnet-class model for well-scoped everyday work, succeeding Claude Sonnet 5 as a direct upgrade. It is especially strong at building features, fixing bugs, and producing...",
"context_length": 1000000,
"architecture": {
"input_modalities": [
"text",
"image",
"file"
],
"output_modalities": [
"text"
],
"modality": "text+image+file->text"
},
"pricing": {
"prompt": "0.000002835",
"completion": "0.000014175",
"request": "0",
"image": "0",
"input_cache_read": "0.00000014175"
},
"top_provider": {
"context_length": 1000000,
"is_moderated": false
}
},
{
"id": "assemblyai/universal-3-5-pro",
"object": "model",
"created": 1792811344,
"owned_by": "assemblyai",
"name": "AssemblyAI: Universal-3.5 Pro",
"description": "Universal-3.5 Pro is AssemblyAI's speech-to-text model served through its Sync API, returning a complete transcript with word-level timestamps in a single synchronous response for audio up to 20 minutes. It...",
"architecture": {
"input_modalities": [
"audio"
],
"output_modalities": [
"text"
],
"modality": "audio->text"
},
"top_provider": {
"is_moderated": false
}
},
{
"id": "baai/bge-m3",
"object": "model",
"created": 1792811344,
"owned_by": "baai",
"name": "BAAI: bge-m3",
"description": "The bge-m3 embedding model encodes sentences, paragraphs, and long documents into a 1024-dimensional dense vector space, delivering high-quality semantic embeddings optimized for multilingual retrieval, semantic search, and large-context applications.",
"context_length": 8194,
"architecture": {
"input_modalities": [
"text"
],
"output_modalities": [
"embeddings"
],
"modality": "text->embeddings"
},
"pricing": {
"prompt": "0.000000014175",
"completion": "0",
"request": "0",
"image": "0"
},
"top_provider": {
"context_length": 8194,
"is_moderated": false
}
},
{
"id": "black-forest-labs/flux-3-image",
"object": "model",
"created": 1792811344,
"owned_by": "black-forest-labs",
"name": "Black Forest Labs: FLUX 3 Image",
"description": "FLUX.3 Image is Black Forest Labs' flagship image generation and editing model. It handles text-to-image and multi-reference editing with up to 10 input images, and renders at fixed resolution tiers...",
"architecture": {
"input_modalities": [
"text"
],
"output_modalities": [
"image"
],
"modality": "text->image"
},
"top_provider": {
"is_moderated": false
}
},
{
"id": "bytedance-seed/seedream-4.5",
"object": "model",
"created": 1792811344,
"owned_by": "bytedance-seed",
"name": "ByteDance Seed: Seedream 4.5",
"description": "Seedream 4.5 is the latest in-house image generation model developed by ByteDance. Compared with Seedream 4.0, it delivers comprehensive improvements, especially in editing consistency, including better preservation of subject details,...",
"architecture": {
"input_modalities": [
"text"
],
"output_modalities": [
"image"
],
"modality": "text->image"
},
"top_provider": {
"is_moderated": false
}
},
{
"id": "deepseek/deepseek-v4-flash",
"object": "model",
"created": 1792811344,
"owned_by": "deepseek",
"name": "DeepSeek: DeepSeek V4 Flash 0423",
"description": "DeepSeek V4 Flash is an efficiency-optimized Mixture-of-Experts model from DeepSeek with 284B total parameters and 13B activated parameters, supporting a 1M-token context window. It is designed for fast inference and...",
"context_length": 1048576,
"architecture": {
"input_modalities": [
"text"
],
"output_modalities": [
"text"
],
"modality": "text->text"
},
"pricing": {
"prompt": "0.0000001289925",
"completion": "0.000000257985",
"request": "0",
"image": "0",
"input_cache_read": "0.0000000257985"
},
"top_provider": {
"context_length": 1048576,
"is_moderated": false
}
},
{
"id": "elevenlabs/eleven-flash-v2",
"object": "model",
"created": 1792811344,
"owned_by": "elevenlabs",
"name": "ElevenLabs: Eleven Flash v2",
"description": "Eleven Flash v2 is an English-only, ultra-low-latency text-to-speech model from ElevenLabs. It is suited for conversational and real-time English use cases, and has a 30,000-character request limit.",
"architecture": {
"input_modalities": [
"text"
],
"output_modalities": [
"speech"
],
"modality": "text->speech"
},
"top_provider": {
"is_moderated": false
}
},
{
"id": "elevenlabs/eleven-multilingual-v2",
"object": "model",
"created": 1792811344,
"owned_by": "elevenlabs",
"name": "ElevenLabs: Eleven Multilingual v2",
"description": "Eleven Multilingual v2 is a text-to-speech model from ElevenLabs. It is suited for lifelike, consistent long-form narration such as voice-overs and audiobooks, supports 29 languages, and has a 10,000-character request...",
"architecture": {
"input_modalities": [
"text"
],
"output_modalities": [
"speech"
],
"modality": "text->speech"
},
"top_provider": {
"is_moderated": false
}
},
{
"id": "google/gemini-2.5-flash-image",
"object": "model",
"created": 1792811344,
"owned_by": "google",
"name": "Google: Nano Banana (Gemini 2.5 Flash Image)",
"description": "Gemini 2.5 Flash Image, a.k.a. \"Nano Banana,\" is now generally available. It is a state of the art image generation model with contextual understanding. It is capable of image generation,...",
"architecture": {
"input_modalities": [
"text"
],
"output_modalities": [
"image"
],
"modality": "text->image"
},
"top_provider": {
"is_moderated": false
}
},
{
"id": "nvidia/nemotron-3.5-embed-1b:free",
"object": "model",
"created": 1792811344,
"owned_by": "nvidia",
"name": "NVIDIA: Nemotron 3.5 Embed 1B (free)",
"description": "Nemotron 3.5 Embed 1B is a multimodal embedding model from NVIDIA for multilingual retrieval over text and document page images (tables, charts, infographics). Queries are text and documents can be...",
"context_length": 32768,
"architecture": {
"input_modalities": [
"text",
"image"
],
"output_modalities": [
"embeddings"
],
"modality": "text+image->embeddings"
},
"pricing": {
"prompt": "0",
"completion": "0",
"request": "0",
"image": "0"
},
"top_provider": {
"context_length": 32768,
"is_moderated": false
}
},
{
"id": "nvidia/nemotron-3.5-super-vl:free",
"object": "model",
"created": 1792811344,
"owned_by": "nvidia",
"name": "NVIDIA: Nemotron 3.5 Super VL (free)",
"description": "NVIDIA Nemotron 3.5 Super VL is an open multimodal mixture-of-experts model from NVIDIA, with 12B active parameters out of 120B total. It takes text, images, documents, GUI screenshots, and video...",
"context_length": 1000000,
"architecture": {
"input_modalities": [
"text",
"image",
"video"
],
"output_modalities": [
"text"
],
"modality": "text+image+video->text"
},
"pricing": {
"prompt": "0",
"completion": "0",
"request": "0",
"image": "0"
},
"top_provider": {
"context_length": 1000000,
"is_moderated": false
}
},
{
"id": "openai/gpt-6-luna",
"object": "model",
"created": 1792811344,
"owned_by": "openai",
"name": "OpenAI: GPT-6 Luna",
"description": "GPT-6 Luna is the fast, cost-efficient model in OpenAI's GPT-6 series, positioned below GPT-6 Sol. It is suited for high-volume and latency-sensitive workloads such as chat, classification, and lightweight agentic...",
"context_length": 1050000,
"architecture": {
"input_modalities": [
"file",
"image",
"text"
],
"output_modalities": [
"text"
],
"modality": "file+image+text->text"
},
"pricing": {
"prompt": "0.00000014175",
"completion": "0.00000070875",
"request": "0",
"image": "0",
"input_cache_read": "0.000000014175"
},
"top_provider": {
"context_length": 1050000,
"is_moderated": false
}
},
{
"id": "openai/whisper-1",
"object": "model",
"created": 1792811344,
"owned_by": "openai",
"name": "OpenAI: Whisper 1",
"description": "Whisper is OpenAI's open-source automatic speech recognition model, available via API as `whisper-1`. It supports transcription and translation across 50+ languages from audio files up to 25 MB. Accepts formats...",
"architecture": {
"input_modalities": [
"audio"
],
"output_modalities": [
"text"
],
"modality": "audio->text"
},
"top_provider": {
"is_moderated": false
}
},
{
"id": "qwen/qwen3-coder-30b-a3b-instruct",
"object": "model",
"created": 1792811344,
"owned_by": "qwen",
"name": "Qwen: Qwen3 Coder 30B A3B Instruct",
"description": "Qwen3-Coder-30B-A3B-Instruct is a 30.5B parameter Mixture-of-Experts (MoE) model with 128 experts (8 active per forward pass), designed for advanced code generation, repository-scale understanding, and agentic tool use. Built on the...",
"context_length": 262144,
"architecture": {
"input_modalities": [
"text"
],
"output_modalities": [
"text"
],
"modality": "text->text"
},
"pricing": {
"prompt": "0.000000099225",
"completion": "0.0000003969",
"request": "0",
"image": "0"
},
"top_provider": {
"context_length": 262144,
"is_moderated": false
}
},
{
"id": "typesafe/jev-1.13.0",
"object": "model",
"created": 1789984193,
"owned_by": "typesafe",
"name": "Jev 1.13",
"description": "Отвечает не текстом, а калиброванной вероятностью на типизированный вопрос о поданном тексте: да/нет, выбор из вариантов, оценка по шкале. Маршрут POST /v1/systemone — это не чат, поля messages у него нет. Основной язык модели — английский, остальные языки поддержаны заметно хуже: проверяйте качество на своей выборке. Известные ограничения провайдера: модель не считает, не сравнивает даты, не решает задач, требующих числовой точности, читает инструкции буквально и не генерирует текст.",
"context_length": 64000,
"architecture": {
"input_modalities": [
"text"
],
"output_modalities": [
"text"
],
"modality": "text->text"
},
"pricing": {
"prompt": "0.000000059535",
"completion": "0",
"request": "0",
"image": "0"
},
"top_provider": {
"context_length": 64000,
"is_moderated": false
}
},
{
"id": "typesafe/jev-latest",
"object": "model",
"created": 1789984193,
"owned_by": "typesafe",
"name": "Jev (последняя версия)",
"description": "Плавающий алиас: провайдер обслуживает им текущую версию Jev. Цена совпадает с закреплённой версией. Отвечает не текстом, а калиброванной вероятностью на типизированный вопрос о поданном тексте: да/нет, выбор из вариантов, оценка по шкале. Маршрут POST /v1/systemone — это не чат, поля messages у него нет. Основной язык модели — английский, остальные языки поддержаны заметно хуже: проверяйте качество на своей выборке. Известные ограничения провайдера: модель не считает, не сравнивает даты, не решает задач, требующих числовой точности, читает инструкции буквально и не генерирует текст.",
"context_length": 64000,
"architecture": {
"input_modalities": [
"text"
],
"output_modalities": [
"text"
],
"modality": "text->text"
},
"pricing": {
"prompt": "0.000000059535",
"completion": "0",
"request": "0",
"image": "0"
},
"top_provider": {
"context_length": 64000,
"is_moderated": false
}
}
]
}
+61
View File
@@ -0,0 +1,61 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import {
add,
compare,
divPow10,
mul,
mulByBigInt,
parseDecimal,
round,
toFixed,
toString,
} from "../src/money.js";
const d = parseDecimal;
test("0.1 + 0.2 = 0.3", () => {
assert.equal(toString(add(d("0.1"), d("0.2"))), "0.3");
});
test("мелкая цена × много токенов без потери точности", () => {
assert.equal(toString(mulByBigInt(d("0.00000012"), 50_000_000n)), "6");
assert.equal(toString(mulByBigInt(d("0.0000001289925"), 123_456_789n)), "15.9249998550825");
// цена за 1M токенов
assert.equal(toString(divPow10(d("79.91392973"), 6)), "0.00007991392973");
assert.equal(toString(mul(d("1.5"), d("0.25"))), "0.375");
assert.equal(toString(mul(d("-1.5"), d("0.2"))), "-0.3");
});
test("round HALF UP — от нуля на .5", () => {
assert.equal(toString(round(d("0.125"), 2)), "0.13");
assert.equal(toString(round(d("-0.125"), 2)), "-0.13");
assert.equal(toString(round(d("0.124999"), 2)), "0.12");
assert.equal(toString(round(d("2.5"), 0)), "3");
assert.equal(toString(round(d("-2.5"), 0)), "-3");
assert.equal(toString(round(d("0.0005"), 3)), "0.001");
assert.equal(toString(round(d("-0.0004"), 3)), "0");
assert.equal(toFixed(d("1.005"), 2), "1.01");
assert.equal(toFixed(d("7"), 2), "7.00");
assert.equal(toFixed(d("-0.001"), 2), "0.00");
});
test("parseDecimal строгий", () => {
assert.throws(() => parseDecimal(0.1), TypeError);
for (const bad of ["1e-7", "", "1,5", " 1", "1.", ".5", "+1", "--1", "NaN"]) {
assert.throws(() => parseDecimal(bad), SyntaxError, bad);
}
assert.equal(toString(d("-12.3400")), "-12.34");
});
test("compare и канонический toString для нулей и отрицательных", () => {
assert.equal(compare(d("0"), d("0.0")), 0);
assert.equal(compare(d("0.00"), d("-0")), 0);
for (const z of ["0", "0.0", "0.00", "-0", "-0.000"]) {
assert.equal(toString(d(z)), "0", z);
}
assert.equal(compare(d("-1.5"), d("-1.49")), -1);
assert.equal(compare(d("10"), d("9.999")), 1);
assert.equal(toString(d("-0.050")), "-0.05");
assert.equal(toString(d("100")), "100");
});
+72
View File
@@ -0,0 +1,72 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { redactError, redactString } from "../src/redact.js";
import { ApiError, toToolErrorResult } from "../src/errors.js";
const KEY = "orr_live_AbCdEf0123456789GhIjKlMnOpQrStUv";
function assertClean(serialized: string): void {
assert.ok(!serialized.includes(KEY), serialized);
assert.ok(!serialized.includes(`Bearer ${KEY}`), serialized);
assert.ok(!serialized.includes(KEY.slice(9)), serialized);
}
test("redactString маскирует Bearer и ключи", () => {
const out = redactString(`Authorization: Bearer ${KEY}; ключ=${KEY}; x${KEY}`);
assertClean(out);
assert.match(out, /Bearer \[REDACTED\]/);
});
test("ключ в message", () => {
const e = new Error(`request failed for ${KEY}`);
assertClean(JSON.stringify(redactError(e)));
});
test("ключ в cause.message не протекает", () => {
const e = new Error("outer", { cause: new Error(`inner Bearer ${KEY}`) });
const r = redactError(e);
assertClean(JSON.stringify(r));
assert.equal(r.message, "outer");
});
test("headers/config/request/stack не копируются", () => {
const e = Object.assign(new Error("boom"), {
status: 401,
code: "invalid_key",
path: "/api/balance",
headers: { Authorization: `Bearer ${KEY}` },
config: { headers: { Authorization: `Bearer ${KEY}` } },
request: { auth: KEY },
});
e.stack = `Error: ${KEY}\n at x`;
const r = redactError(e);
assertClean(JSON.stringify(r));
assert.deepEqual(Object.keys(r).sort(), ["code", "message", "name", "path", "status"]);
assert.equal(r.status, 401);
});
test("произвольные значения", () => {
assertClean(JSON.stringify(redactError(`string ${KEY}`)));
assertClean(JSON.stringify(redactError({ message: `obj ${KEY}`, headers: { a: KEY } })));
assert.equal(redactError(null).message, "неизвестная ошибка");
assert.equal(redactError(42).message, "неизвестная ошибка");
});
test("toToolErrorResult маппит статусы и редактирует текст", () => {
const cases: Array<[number, RegExp]> = [
[401, /недействителен или отозван/],
[403, /запрещено/],
[429, /лимит частоты/],
[503, /временно недоступен/],
];
for (const [status, re] of cases) {
const r = toToolErrorResult(new ApiError(status, "/api/balance", `oops ${KEY}`));
assert.equal(r.isError, true);
assert.match(r.content[0].text, re);
assertClean(JSON.stringify(r));
}
const other = toToolErrorResult(new ApiError(400, "/api/usage", `bad Bearer ${KEY}`));
assert.match(other.content[0].text, /bad Bearer \[REDACTED\]/);
assertClean(JSON.stringify(other));
assertClean(JSON.stringify(toToolErrorResult(new Error(`plain ${KEY}`))));
});
+20
View File
@@ -0,0 +1,20 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { cleanText } from "../src/sanitize.js";
test("убирает управляющие, bidi и zero-width символы", () => {
assert.equal(cleanText("a\u0000b\u0007c\u009Fd\u007Fe"), "abcde");
assert.equal(cleanText("safe\u202Etxt.exe\u202C"), "safetxt.exe");
assert.equal(cleanText("x\u2066y\u2069z\u200Bw\uFEFF"), "xyzw");
});
test("схлопывает пробелы и переводы строк", () => {
assert.equal(cleanText(" a \t b\r\n\r\n\n c "), "a b\nc");
});
test("обрезает по max с многоточием, не рвёт суррогатные пары", () => {
assert.equal(cleanText("abcdef", 4), "abc…");
assert.equal(cleanText("abcd", 4), "abcd");
assert.equal(cleanText("😀😀😀😀😀", 3), "😀😀…");
assert.equal(Array.from(cleanText("x".repeat(1000))).length, 500);
});