Заведи каркас пакета и утилитный слой 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:
29 files changed
+11350
No files matched your search
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"balance":"1234.56789012","referral_balance":"0"}
|
||||
Vendored
+43
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+254
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+72
@@ -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 (последняя версия)"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+1
@@ -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"}}
|
||||
Vendored
+436
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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}`))));
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
Reference in new issue
Block a user