# Liho LLM — developer documentation > Liho LLM API v1. Self-hosted deployment; configure providers and model pricing before use. Base URL: https://llm.liho.dev/v1 Authentication: Authorization: Bearer $LIHO_LLM_API_KEY Endpoints: POST /v1/chat/completions; GET /v1/models, /v1/key, /v1/credits, /v1/generation; /v1/keys (management key) Navigation: /#docs/ # Быстрый старт Route: /#docs/quickstart Подключите AI к своему приложению через один API. Выбирайте модель под задачу и используйте привычные инструменты. ## Подготовьте пространство Создайте API-ключ: Заведите отдельный ключ для проекта: выберите срок действия и лимит расходов — на всё время или со сбросом каждый день, неделю или месяц. Сохраните ключ на сервере. Пополните баланс: Запросы оплачиваются по фактическому использованию токенов. Доступного баланса должно хватать на резерв. Выберите модель: Скопируйте model ID из каталога. Цена, размер контекста и возможности зависят от выбранной модели. ## Настройте окружение Сохраните ключ и базовый URL в переменных окружения серверного приложения. Base URL уже включает /v1 — добавлять этот префикс второй раз не нужно. Предварительная спецификация: Базовый адрес API — https://llm.liho.dev/v1. Нужны настроенная модель, действующий ключ и положительный баланс. ```Shell export LIHO_LLM_API_KEY="YOUR_API_KEY" export LIHO_LLM_BASE_URL="https://llm.liho.dev/v1" ``` ## Установите клиент Для cURL и HTTP / fetch отдельный SDK не нужен. Для Python или TypeScript выберите команду своего стека. ```Python python -m pip install openai ``` ```Node.js npm install openai ``` ## Отправьте первый запрос Отправьте обычный HTTP-запрос или используйте совместимый клиент. Примеры Python и TypeScript выполняются на сервере и используют пакет openai. ```cURL curl "$LIHO_LLM_BASE_URL/chat/completions" \ -H "Authorization: Bearer $LIHO_LLM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-4.1-mini", "messages": [ { "role": "user", "content": "Объясни API gateway в одном предложении." } ], "max_tokens": 256 }' ``` ```Python import os from openai import OpenAI client = OpenAI( base_url=os.environ["LIHO_LLM_BASE_URL"], api_key=os.environ["LIHO_LLM_API_KEY"], ) response = client.chat.completions.create( model="openai/gpt-4.1-mini", messages=[{"role": "user", "content": "Что такое API gateway?"}], max_tokens=256, ) print(response.choices[0].message.content) ``` ```TypeScript import OpenAI from 'openai'; const baseURL = process.env.LIHO_LLM_BASE_URL; const apiKey = process.env.LIHO_LLM_API_KEY; if (!baseURL || !apiKey) throw new Error("Set Liho LLM environment variables"); const client = new OpenAI({ baseURL, apiKey }); const response = await client.chat.completions.create({ model: 'openai/gpt-4.1-mini', messages: [{ role: 'user', content: 'Что такое API gateway?' }], max_tokens: 256, }); console.log(response.choices[0]?.message.content); ``` ```HTTP / fetch const response = await fetch( process.env.LIHO_LLM_BASE_URL + '/chat/completions', { method: 'POST', headers: { Authorization: 'Bearer ' + process.env.LIHO_LLM_API_KEY, 'Content-Type': 'application/json', }, body: JSON.stringify({ "model": "openai/gpt-4.1-mini", "messages": [ { "role": "user", "content": "Объясни API gateway в одном предложении." } ], "max_tokens": 256 }), }, ); if (!response.ok) throw new Error(await response.text()); console.log(await response.json()); ``` ## Получите ответ Текст находится в choices[0].message.content, а usage содержит расход токенов. Пример ответа ниже иллюстративный. ```JSON { "id": "9b1f7c2e-4d5a-4e8b-a1c3-2f6d8e0b7a91", "object": "chat.completion", "created": 1792148400, "model": "openai/gpt-4.1-mini", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "API gateway принимает запросы и направляет их к нужному сервису." }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 24, "completion_tokens": 21, "total_tokens": 45, "prompt_tokens_details": { "cached_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0 }, "cost": 0.004752 } } ``` ## Что дальше Добавьте потоковую выдачу для чата, задайте лимиты для проекта и обработайте ошибки. Если переходите с OpenRouter, начните с руководства по миграции. # Переход с OpenRouter Route: /#docs/migration Сохраните знакомый Chat Completions API и явно проверьте особенности вашей интеграции. ## Замените URL и ключ Для клиента OpenAI SDK измените base URL и API-ключ. Публичные model ID сверяйте с каталогом Liho LLM: названия, aliases и доступность не обязаны совпадать. ```Python client = OpenAI( base_url=os.environ["LIHO_LLM_BASE_URL"], api_key=os.environ["LIHO_LLM_API_KEY"], ) ``` ```TypeScript const baseURL = process.env.LIHO_LLM_BASE_URL; const apiKey = process.env.LIHO_LLM_API_KEY; if (!baseURL || !apiKey) throw new Error("Set Liho LLM environment variables"); const client = new OpenAI({ baseURL, apiKey }); ``` ## Проверьте совместимость В интеграции | В Liho LLM OpenAI SDK · chat.completions | Проектируется совместимость с базовым текстовым Chat Completions OpenRouter /api/v1 | Base URL Liho LLM заканчивается на /v1 provider, models, transforms, plugins | Расширения OpenRouter не входят в публичный контракт MVP @openrouter/sdk, @openrouter/agent | Их совместимость с Liho LLM не заявляется; используйте HTTP или OpenAI SDK HTTP-Referer, X-Title | Необязательны; приложение видно в логах кабинета /api/v1/key, /credits, /generation, /keys | Те же ответы по /v1/…; суммы — в рублях, поле currency: RUB Сохранённые цены и model aliases | Повторно проверьте в каталоге Liho LLM ## Проверьте интеграцию Обычный ответ: Проверьте текст, finish_reason и usage на небольшой задаче. Streaming: Проверьте delta, финальный usage, пустой choices и завершение потока. Ошибки и бюджет: Проверьте отозванный ключ, недостаток средств и достижение лимита до переключения production-трафика. Совместимый формат не означает одинаковый продукт: Доступные модели, capabilities, маршрутизация, расширения API и правила оплаты проверяются отдельно. Полная совместимость с OpenRouter не заявляется. # Аутентификация Route: /#docs/authentication Каждый API-запрос авторизуется ключом Liho LLM через стандартный Bearer-заголовок. ## Заголовок авторизации Используйте ключ из кабинета Liho LLM. Upstream-ключи OpenAI, Anthropic и других провайдеров здесь не принимаются. ```HTTP Authorization: Bearer YOUR_LIHO_LLM_API_KEY Content-Type: application/json ``` ## Жизненный цикл ключа Создание: Полный ключ отображается один раз. Для каждого окружения используйте свой ключ. Лимиты: Лимит ключа ограничивает расходы за всё время или за день, неделю или месяц (сброс в 00:00 UTC), RPM — частоту запросов, срок действия — время работы ключа. Баланс аккаунта ограничивает расходы всегда. Ротация: Создайте новый ключ, замените секрет в приложении, убедитесь в работоспособности и отзовите старый. ## Хранение секрета Неверный или отозванный ключ возвращает 401. Блокировка аккаунта и политика доступа проверяются до вызова модели. Только на сервере: Не добавляйте ключ в VITE_*, NEXT_PUBLIC_*, публичный репозиторий, URL или браузерный JavaScript. Вызывайте Liho LLM из своего backend. ## Вход desktop-приложения liho-cowork-desktop выдаёт Cowork доступ к данным подключения своих серверов после явного browser consent; liho-cowork-server предназначен для VPS и разрешает только обычный inference. Старые liho-cowork/liho ключи не получают directory scope. Все legacy desktop ключи требуют повторного входа; имена ключей не используются для определения прав. Public client ID не является attestation приложения. Desktop-клиенты (например, Liho Cowork) получают ключ через device-flow с PKCE, не касаясь пароля: приложение открывает браузер на странице кабинета, человек входит и разрешает доступ (POST /api/auth/device с сессией кабинета), затем код из loopback-перенаправления обменивается на ключ. POST /v1/auth/device/exchange — без авторизации. Тело: {code, code_verifier} — одноразовый код из callback и PKCE-верификатор (RFC 7636, 43–128 символов), base64url-SHA-256 от которого передавался в запросе согласия. Ответ 200: {key, key_id, user: {name, email}}; ключ — обычный ключ инференса «Liho Cowork — » (RPM по умолчанию, без лимита и срока действия), виден и отзываем в кабинете. Код действует 10 минут и расходуется в одной транзакции: повторный обмен тем же кодом или неверный verifier — 400 «Invalid or expired device code»; превышение квоты ключей (100 активных) — 409. Лимит — 20 запросов в минуту на IP-адрес, общий с регистрацией и входом. Выход из desktop-приложения — DELETE /v1/key. # Chat completions Route: /#docs/chat-completions Передайте историю сообщений и получите следующий ответ модели в OpenAI-совместимом формате. ## Тело запроса Поле | Тип | Описание model | string · required | Публичный ID из каталога Liho LLM route | string | Единственное значение "fallback"; принимается для совместимости с OpenRouter, маршрут не меняет messages | array · required | 1–2048 сообщений: system, developer, user, assistant, tool max_tokens | integer · required | Верхняя граница выходных токенов, включая reasoning; можно передать как max_completion_tokens stream | boolean | false — JSON; true — поток SSE temperature | number | 0..2; отбрасывается у reasoning-моделей и при включённом reasoning_effort top_p | number | 0..1 presence_penalty, frequency_penalty | number | −2..2 stream_options.include_usage | boolean | Принимается; usage в потоке приходит всегда tools | array | До 128 функций {type: "function", function: {name, description, parameters, strict}}; для моделей с tools в supported_parameters tool_choice | string | object | none, auto, required или {type: "function", function: {name}} parallel_tool_calls | boolean | Разрешить несколько вызовов за ход response_format | object | text, json_object (в сообщениях должно встречаться слово JSON) или json_schema с именованной схемой — что поддерживает модель stop | string | array | До 4 стоп-последовательностей, если модель поддерживает stop seed | integer | Для воспроизводимости, если модель поддерживает reasoning_effort | string | Одно из reasoning_efforts модели в GET /v1/models (none, minimal, low, medium, high, xhigh) reasoning | object | Форма OpenRouter: effort задаёт reasoning_effort, enabled: false — none, enabled: true без effort — medium. max_tokens и exclude не поддерживаются (400) user | string | ID конечного пользователя, до 256 байт metadata | object | Принимается (до 16 пар), провайдеру не передаётся ```JSON { "model": "openai/gpt-4.1-mini", "messages": [ { "role": "user", "content": "Объясни API gateway в одном предложении." } ], "max_tokens": 256 } ``` ## Сообщения Роли: system и developer (передаётся провайдеру как system), user, assistant и tool. content — строка, массив частей или null у assistant, который только вызывает инструменты. user может передавать части text и image_url, если у модели image в input_modalities: base64 data URL (png, jpeg, gif, webp), байты которого соответствуют типу, до 64 изображений на запрос. https-ссылки на изображения отклоняются: скачайте изображение и передайте его inline. Текст не может быть пустым; у assistant нужен content или tool_calls, а результат tool может быть пустым. assistant с tool_calls и следующие за ним сообщения tool с tool_call_id из этого вызова образуют один шаг агента. Сообщение tool без предшествующего вызова отклоняется до резервирования. Не отправляйте чувствительные данные, которые не хотите передавать выбранному провайдеру. Что не поддерживается: Аудио и файлы в content отклоняются с ошибкой 400, в которой назван тип части. cache_control принимается и отбрасывается: кеширование промпта по подсказке клиента не включается. Embeddings и Responses API пока не поддерживаются. ```JSON { "messages": [ { "role": "user", "content": "What does main.go do?" }, { "role": "assistant", "content": null, "tool_calls": [ { "id": "call_1", "type": "function", "function": { "name": "read_file", "arguments": "{\"path\":\"main.go\"}" } } ] }, { "role": "tool", "tool_call_id": "call_1", "content": "package main\n…" } ] } ``` ## Успешный ответ finish_reason описывает причину завершения. id — идентификатор генерации: он совпадает с заголовком X-Request-ID и подходит для GET /v1/generation. usage.cost — фактическое списание в рублях (без части, покрытой бесплатными токенами), рассчитанное до отправки ответа. usage.prompt_tokens_details.cached_tokens и completion_tokens_details.reasoning_tokens входят в prompt_tokens и completion_tokens соответственно. Если расчёт ещё не завершён (например, usage выше резерва и ждёт проверки), cost отсутствует. ```JSON { "id": "9b1f7c2e-4d5a-4e8b-a1c3-2f6d8e0b7a91", "object": "chat.completion", "created": 1792148400, "model": "openai/gpt-4.1-mini", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "API gateway принимает запросы и направляет их к нужному сервису." }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 24, "completion_tokens": 21, "total_tokens": 45, "prompt_tokens_details": { "cached_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0 }, "cost": 0.004752 } } ``` ## Атрибуция приложения Заголовки необязательны, как в OpenRouter. Приложение видно в логах кабинета и в GET /v1/generation; на тариф и маршрут оно не влияет. Заголовок | Назначение HTTP-Referer | URL вашего приложения; сохраняется без query и fragment X-Title (или X-OpenRouter-Title) | Название приложения, до 100 символов # Каталог моделей Route: /#docs/models Получите список разрешённых для вашего аккаунта model ID. ## Запрос Подойдёт любой действующий ключ Liho LLM — клиентский или ключ управления. В списке показываются публичные модели; внутренние имена маршрутов и секреты провайдеров не возвращаются. ```cURL curl "$LIHO_LLM_BASE_URL/models" \ -H "Authorization: Bearer $LIHO_LLM_API_KEY" ``` ## Формат ответа Это пример структуры, а не актуальный перечень моделей. Формат повторяет каталог OpenRouter и сохраняет OpenAI-поля object и owned_by. pricing — розничные цены в рублях за один токен строками десятичных дробей (currency: RUB, наценка включена): 0.00033 означает 330 ₽ за 1M токенов. input_cache_read — цена кэшированных токенов промпта (без отдельной цены равна prompt), рассуждения тарифицируются по цене completion. У модели может быть несколько endpoint-ов с разными ценами. pricing показывает минимальную цену каждого класса токенов среди доступных endpoint-ов («от»), context_length — наибольший контекст. Запрос оплачивается по ценам endpoint-а, который его обслужил: это значение приходит в usage.cost и в GET /v1/generation. Цены каждого endpoint-а — в GET /api/v1/models/{author}/{slug}/endpoints. supported_parameters перечисляет поля запроса, которые модель учитывает (в терминах OpenRouter), architecture.input_modalities показывает, принимает ли она изображения, а reasoning_efforts — допустимые значения reasoning_effort. ```JSON { "object": "list", "data": [ { "id": "anthropic/claude-sonnet-4.5", "object": "model", "owned_by": "anthropic", "created": 1792108800, "name": "Claude Sonnet 4.5", "canonical_slug": "anthropic/claude-sonnet-4.5", "description": "Agentic coding model with tools and vision", "context_length": 200000, "architecture": { "modality": "text+image->text", "input_modalities": [ "text", "image" ], "output_modalities": [ "text" ], "tokenizer": "Claude", "instruct_type": null }, "pricing": { "prompt": "0.00033", "completion": "0.00165", "input_cache_read": "0.000033", "request": "0", "image": "0", "web_search": "0", "internal_reasoning": "0", "currency": "RUB" }, "top_provider": { "context_length": 200000, "max_completion_tokens": 64000, "is_moderated": false }, "per_request_limits": null, "supported_parameters": [ "max_tokens", "max_completion_tokens", "temperature", "top_p", "seed", "user", "stop", "tools", "tool_choice", "parallel_tool_calls", "response_format", "structured_outputs", "reasoning_effort", "reasoning" ], "reasoning_efforts": [ "none", "low", "medium", "high" ] } ] } ``` ## Как выбрать модель Критерий | На что смотреть Задача | Качество на ваших текстовых запросах Стоимость | Раздельные цены input, кешированного input и output Задержка | Время до первого токена и полного ответа Контекст | Размер истории вместе с резервом output Доступность | Модель должна быть включена в каталоге # Аккаунт и статистика Route: /#docs/account Агентам и IDE-клиентам: лимиты ключа, баланс, статистика генерации и управление ключами — в формате OpenRouter. ## Запросы Подходит любой активный ключ, в том числе ключ управления. Суммы — числа в рублях с точностью до микрорубля, поле currency всегда RUB. Лимит — 300 запросов в минуту на ключ, отдельно от RPM инференса. ```cURL curl "$LIHO_LLM_BASE_URL/key" -H "Authorization: Bearer $LIHO_LLM_API_KEY" curl "$LIHO_LLM_BASE_URL/credits" -H "Authorization: Bearer $LIHO_LLM_API_KEY" curl "$LIHO_LLM_BASE_URL/generation?id=9b1f7c2e-4d5a-4e8b-a1c3-2f6d8e0b7a91" \ -H "Authorization: Bearer $LIHO_LLM_API_KEY" ``` ## GET /v1/key Поле | Смысл label, name | Маскированный префикс и имя ключа limit, limit_reset | Лимит расходов за период (daily, weekly, monthly) или null limit_remaining | Остаток лимита за вычетом расходов периода и текущих резервов usage, usage_daily/weekly/monthly | Списания деньгами за всё время и за день, неделю, месяц (UTC) is_free_tier, free_tokens | Аккаунт ещё не пополнялся; доступные бесплатные токены rate_limit | RPM ключа: requests за interval ```JSON { "data": { "id": "5c0e8a52-1f3b-4c7d-9e2a-6b4d8f1a3c57", "name": "ci-agent", "label": "lh_AbCdEfG…wxyz", "disabled": false, "limit": 1500, "limit_remaining": 1342.25, "limit_reset": "monthly", "usage": 4210.5, "usage_daily": 12.4, "usage_weekly": 96.1, "usage_monthly": 157.75, "rate_limit": { "requests": 60, "interval": "60s" }, "expires_at": null, "created_at": "2026-10-01T09:00:00Z", "is_management_key": false, "is_provisioning_key": false, "is_free_tier": false, "free_tokens": 0, "currency": "RUB" } } ``` ## DELETE /v1/key Отзывает ключ, которым авторизован сам запрос; история сохраняется. Ответ: {"deleted": true}. Так Liho Cowork выходит из аккаунта: приложение удаляет локальный секрет и отзывает ключ на сервере одним запросом. Авторизация — тем же ключом. Management-ключ получает 403: он отзывается в кабинете или через DELETE /v1/keys/{id} другим ключом управления. ```cURL curl -X DELETE "$LIHO_LLM_BASE_URL/key" -H "Authorization: Bearer $LIHO_LLM_API_KEY" ``` ## GET /v1/credits total_credits − total_usage = balance, как в OpenRouter. held — сумма, зарезервированная выполняющимися запросами; available = balance − held. ```JSON { "data": { "total_credits": 10000, "total_usage": 4210.5, "balance": 5789.5, "held": 12.3, "available": 5777.2, "free_tokens": 0, "currency": "RUB" } } ``` ## GET /v1/generation?id= id — значение id из ответа или X-Request-ID. Видны только генерации своего аккаунта; чужой или неизвестный id — 404, неверный формат — 400. latency — время до первого токена (для обычного ответа — полная длительность), generation_time — полная длительность, обе в миллисекундах. total_cost равен null, пока запрос не рассчитан (state ≠ settled); после расчёта это списание по ценам endpoint-а, который обслужил запрос (endpoint — его маршрут, fallback — позиция в цепочке, 0 — основной). ```JSON { "data": { "id": "9b1f7c2e-4d5a-4e8b-a1c3-2f6d8e0b7a91", "model": "openai/gpt-4.1-mini", "served_model": "gpt-4.1-mini-2025-04-14", "provider_name": "openai", "endpoint": "openai/gpt-4.1-mini", "fallback": 0, "created_at": "2026-10-18T09:12:03Z", "state": "settled", "streamed": true, "cancelled": false, "tokens_prompt": 24, "tokens_completion": 21, "native_tokens_prompt": 24, "native_tokens_completion": 21, "native_tokens_cached": 0, "native_tokens_reasoning": 0, "total_cost": 0.004752, "usage": 0.004752, "free_tokens_used": 0, "is_byok": false, "latency": 412, "generation_time": 1630, "finish_reason": "stop", "native_finish_reason": "stop", "origin": "https://agent.example", "app_title": "My Agent", "key_id": "5c0e8a52-1f3b-4c7d-9e2a-6b4d8f1a3c57", "currency": "RUB" } } ``` ## Ключи управления: /v1/keys Ключ управления создаётся в кабинете (флажок «Ключ управления»). Он не вызывает модели (403) и управляет только ключами инференса своего аккаунта: GET /v1/keys, POST /v1/keys, GET, PATCH и DELETE /v1/keys/{id}. limit — число в рублях (до 6 знаков после точки) или null, limit_reset — daily, weekly, monthly или null, rpm — 1…1000, expires_at — RFC 3339 только при создании. PATCH меняет только переданные поля; disabled: true отзывает ключ навсегда. Новый секрет возвращается один раз в поле key. Храните ключ управления отдельно: Он может выпускать и отзывать ключи аккаунта. Держите его только в системе provisioning и не передавайте агентам. ```POST curl -X POST "$LIHO_LLM_BASE_URL/keys" \ -H "Authorization: Bearer $LIHO_LLM_MANAGEMENT_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"ci-agent","limit":1500,"limit_reset":"monthly"}' ``` ```GET curl "$LIHO_LLM_BASE_URL/keys?limit=50&offset=0" \ -H "Authorization: Bearer $LIHO_LLM_MANAGEMENT_KEY" ``` ```PATCH curl -X PATCH "$LIHO_LLM_BASE_URL/keys/$KEY_ID" \ -H "Authorization: Bearer $LIHO_LLM_MANAGEMENT_KEY" \ -H "Content-Type: application/json" \ -d '{"limit":null,"limit_reset":null}' ``` ```DELETE curl -X DELETE "$LIHO_LLM_BASE_URL/keys/$KEY_ID" \ -H "Authorization: Bearer $LIHO_LLM_MANAGEMENT_KEY" ``` # Параметры запроса Route: /#docs/parameters Передавайте только параметры, которые поддерживают выбранная модель и публичный контракт. ## Контекст и output max_tokens (или max_completion_tokens) обязателен: он ограничивает ответ вместе с reasoning-токенами и целиком входит в предварительное резервирование. Вход резервируется по консервативной оценке без токенизатора: цифры и знаки препинания — токен на байт, слова — примерно токен на две буквы, кириллица — токен на символ, плюс 32 токена на сообщение, 64 на инструмент и вызов, граница модели на изображение (наибольший счёт токенов одного изображения) за каждое изображение и 1024 на запрос. Для кода и текста оценка в 2–2,5 раза выше реального счёта, поэтому 500 КБ истории агента резервируют около 340 тысяч входных токенов; резерв освобождается при списании фактического usage. Для проверки контекста те же части пересчитываются к типичному размеру (около 1,2 реального счёта для кода и текста, не ниже него для JSON и идентификаторов). Если он вместе с max_tokens не помещается в контекст модели, запрос отклоняется с 400 «…exceed the model’s maximum context length…» до резервирования — агенты, которые реагируют на такую ошибку, сократят историю сами. ## Возможности модели Каждая модель публикует, что она поддерживает, в GET /v1/models. Запрос, которому нужно то, чего у модели нет, отклоняется с 400 до резервирования, а не падает у провайдера: изображения для текстовой модели, tools, stop, тип response_format или значение reasoning_effort вне списка модели, изображение больше лимита модели (например, 5 МБ base64 у Claude). Некоторые поля вместо этого отбрасываются, потому что семантика OpenAI позволяет модели их игнорировать: temperature, top_p и штрафы — у reasoning-моделей и при включённом reasoning_effort; reasoning_effort: "none" — у модели без reasoning; tool_choice и parallel_tool_calls — без tools. Claude с включённым reasoning_effort (кроме none) требует max_tokens не меньше 1025: бюджет размышлений — минимум 1024 токена и должен быть меньше max_tokens. max_tokens всегда ограничивает размышления и ответ вместе. ## Параметры генерации Параметр | Рекомендация temperature | Используйте поддерживаемое моделью значение; reasoning-модели могут ограничивать настройку max_tokens | Задайте небольшой предел для первого теста и увеличивайте под задачу stream | Выберите true для интерактивного интерфейса stream_options | Передавайте вместе со stream: true для получения usage ## Неподдержанные поля Неизвестные поля на любом уровне (cache_control, citations, usage, transforms и т. п.) не передаются провайдеру: они отбрасываются, а заголовок ответа X-Liho-Ignored-Fields перечисляет их пути без индексов, например messages[].cache_control. Проверяйте этот заголовок, если параметр не подействовал. Опции, молчаливый пропуск которых изменил бы маршрут, цену или форму ответа, отклоняются с 400 с названием поля: fallbacks, preset, service_tier, web_search_options, plugins, logprobs, top_logprobs, logit_bias, modalities, audio, prediction, functions, function_call. Значение, которое ничего не запрашивает (null, false, 0, пустое, service_tier: "auto"), отбрасывается как неизвестное поле; исключение — web_search_options: {}, который включает поиск и отклоняется. Неизвестное поле внутри provider или reasoning тоже отклоняется. Тело запроса — до 32 МиБ (413 сверх этого), остальные эндпоинты принимают до 1 МиБ. Принимаются, но не передаются: metadata, n = 1 и поля, которые OpenAI SDK возвращает в сообщении ассистента (refusal, annotations, reasoning_content), проверяются и отбрасываются. Это позволяет добавлять ответ модели в историю без очистки. # Streaming Route: /#docs/streaming Показывайте ответ по мере генерации с помощью Server-Sent Events. ## Включите потоковую выдачу Создайте client из быстрого старта, затем отправьте stream: true. SDK разбирает SSE-фреймы; приложение получает отдельные delta-фрагменты. ```Python stream = client.chat.completions.create( model="openai/gpt-4.1-mini", messages=[{"role": "user", "content": "Расскажи о SSE"}], max_tokens=512, stream=True, stream_options={"include_usage": True}, ) for chunk in stream: if chunk.usage: print("\nUsage:", chunk.usage.total_tokens) if chunk.choices: print(chunk.choices[0].delta.content or "", end="", flush=True) ``` ```TypeScript const stream = await client.chat.completions.create({ model: 'openai/gpt-4.1-mini', messages: [{ role: 'user', content: 'Расскажи о SSE' }], max_tokens: 512, stream: true, stream_options: { include_usage: true }, }); for await (const chunk of stream) { if (chunk.usage) console.log('Usage:', chunk.usage.total_tokens); process.stdout.write(chunk.choices[0]?.delta?.content ?? ''); } ``` ```cURL curl -N "$LIHO_LLM_BASE_URL/chat/completions" \ -H "Authorization: Bearer $LIHO_LLM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-4.1-mini", "messages": [{"role": "user", "content": "Расскажи о SSE"}], "max_tokens": 512, "stream": true, "stream_options": {"include_usage": true} }' ``` ## События и usage Выше показаны сокращённые фреймы. Не считайте каждый сетевой read отдельным событием: фрейм может прийти частями. Если реализуете парсер сами, учитывайте пустые строки, комментарии SSE и UTF-8 границы. Обрабатывайте usage независимо от choices: финальный accounting-фрейм может не содержать текста и иметь пустой массив choices. JSON, SSE, trailing usage и fallback проверяются интеграционными тестами на Bifrost v2.3.0. Финальный фрейм с usage шлюз придерживает до расчёта: списание фиксируется в базе, и фрейм уходит с usage.cost и деталями токенов прямо перед data: [DONE]. Это добавляет к концу потока одну транзакцию базы данных. Если поток оборвался, фрейм с usage приходит без cost и без [DONE]. ```SSE data: {"id":"9b1f7c2e-4d5a-4e8b-a1c3-2f6d8e0b7a91","choices":[{"index":0,"delta":{"content":"Привет"}}]} data: {"id":"9b1f7c2e-4d5a-4e8b-a1c3-2f6d8e0b7a91","choices":[],"usage":{"prompt_tokens":12,"completion_tokens":1,"total_tokens":13,"prompt_tokens_details":{"cached_tokens":0},"completion_tokens_details":{"reasoning_tokens":0},"cost":0.000704}} data: [DONE] ``` ## Ошибки и остановка До начала SSE ошибка возвращается обычным JSON. После начала ответа HTTP-статус уже нельзя изменить: приложение должно отслеживать ошибку потока и отсутствие нормального завершения. Обрыв соединения не гарантирует остановку генерации у провайдера и нулевую стоимость. Если итоговый usage неизвестен, расчёт запроса может оставаться в состоянии уточнения. Не запускайте автоматически второй платный запрос. # Агенты и tools Route: /#docs/agents Подключайте coding-агентов и OpenAI-клиенты с вызовом инструментов, длинной историей и параллельными потоками. ## Цикл вызова инструментов Передайте tools. Если модель решила вызвать инструмент, ответ приходит с finish_reason: "tool_calls" и message.tool_calls. Выполните вызовы, добавьте в историю ответ ассистента и по сообщению tool на каждый tool_call_id, затем повторите запрос. Каждый запрос цикла резервируется и списывается отдельно по фактическому usage. ```Python import json tools = [{ "type": "function", "function": { "name": "read_file", "description": "Read a file from the workspace", "parameters": { "type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"], }, }, }] messages = [{"role": "user", "content": "What does main.go do?"}] while True: response = client.chat.completions.create( model="openai/gpt-4.1-mini", messages=messages, tools=tools, max_completion_tokens=4096, ) message = response.choices[0].message messages.append(message) # echoed refusal/annotations are accepted if response.choices[0].finish_reason != "tool_calls": print(message.content) break for call in message.tool_calls: path = json.loads(call.function.arguments)["path"] with open(path) as f: messages.append({"role": "tool", "tool_call_id": call.id, "content": f.read()}) ``` ```TypeScript import { readFile } from 'node:fs/promises'; const tools: OpenAI.ChatCompletionTool[] = [{ type: 'function', function: { name: 'read_file', description: 'Read a file from the workspace', parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] }, }, }]; const messages: OpenAI.ChatCompletionMessageParam[] = [ { role: 'user', content: 'What does main.go do?' }, ]; for (;;) { const response = await client.chat.completions.create({ model: 'openai/gpt-4.1-mini', messages, tools, max_completion_tokens: 4096, }); const choice = response.choices[0]!; messages.push(choice.message); if (choice.finish_reason !== 'tool_calls') { console.log(choice.message.content); break; } for (const call of choice.message.tool_calls ?? []) { if (call.type !== 'function') continue; const { path } = JSON.parse(call.function.arguments); messages.push({ role: 'tool', tool_call_id: call.id, content: await readFile(path, 'utf8') }); } } ``` ## Tool calls в потоке В SSE имя функции приходит в первом фрагменте, аргументы — частями в delta.tool_calls[].function.arguments с тем же index. Склейте их до события с finish_reason: "tool_calls". Финальный фрагмент несёт usage. ```SSE data: {"choices":[{"index":0,"delta":{"role":"assistant","tool_calls":[{"index":0,"id":"call_1","type":"function","function":{"name":"read_file","arguments":""}}]},"finish_reason":null}]} data: {"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"path\":\"main.go\"}"}}]},"finish_reason":null}]} data: {"choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":812,"completion_tokens":19,"total_tokens":831}} data: [DONE] ``` ## Настройка клиентов Используйте провайдер «OpenAI Compatible» с base URL $LIHO_LLM_BASE_URL и ключом lh_…. Явно задайте лимит выходных токенов: многие агенты по умолчанию не отправляют max_tokens, а Liho LLM требует его для резервирования. Клиент | Где задать лимит выхода Cline | Model configuration → Max output tokens Roo Code | Include max output tokens и значение в настройках модели Aider | extra_params: max_tokens в .aider.model.settings.yml OpenHands | max_output_tokens в настройках LLM OpenAI SDK | max_tokens или max_completion_tokens в каждом запросе ## Codex CLI Опишите провайдера Liho в ~/.codex/config.toml, ключ — в переменной окружения из env_key. Модель указывается по ID из каталога. ```~/.codex/config.toml model = "gpt-6-sol" model_provider = "liho" [model_providers.liho] name = "Liho LLM" base_url = "https://llm.liho.dev/v1" env_key = "LIHO_API_KEY" wire_api = "chat" ``` ```shell export LIHO_API_KEY="lh_..." codex ``` ## Cursor, Cline и Roo Code Cursor: Settings → Models → OpenAI API Key — вставьте ключ lh_… и включите Override OpenAI Base URL с адресом Liho, затем добавьте модели по их ID. Cline и Roo Code: провайдер «OpenAI Compatible», base URL https://llm.liho.dev/v1, ключ lh_… и ID модели из каталога. Не забудьте лимит выходных токенов из таблицы выше. ```Base URL https://llm.liho.dev/v1 ``` ```API Key lh_... ``` ```Model ID gpt-5.6-sol ``` ## OpenWebUI Admin Panel → Settings → Connections → OpenAI API: укажите адрес и ключ Liho, после проверки модели появятся в списке чата. ```OpenAI API URL https://llm.liho.dev/v1 ``` ```API Key lh_... ``` ## Claude Code Claude Code говорит по Anthropic-протоколу (/v1/messages), который Liho пока не принимает. Подключайте его через адаптер claude-code-router: он переводит Anthropic-запросы в OpenAI-совместимый чат Liho. ```npm i -g @musistudio/claude-code-router ccr code ``` ```~/.claude-code-router/config.json { "Providers": [ { "name": "liho", "api_base_url": "https://llm.liho.dev/v1/chat/completions", "api_key": "lh_...", "models": [ "gpt-6-sol", "claude-opus-4-8", "claude-sonnet-5" ] } ], "Router": { "default": "liho,gpt-6-sol" } } ``` ## Лимиты запроса Что | Предел Тело запроса | 32 МиБ Сообщений | 2048 Инструментов | 128; имя [A-Za-z0-9_-]{1,64}, описание и JSON Schema до 64 КиБ tool_calls в сообщении | 128; tool_call_id до 256 байт Частей content в сообщении | 256 Изображений в запросе | 64 base64 data URL до 20 МиБ каждый (у некоторых моделей меньше) stop | 4 последовательности до 256 байт Параллельные потоки: Параллельные запросы одного ключа ограничены RPM ключа, и каждый держит свой резерв. Если баланса не хватает на сумму резервов, новый запрос получит 402 — уменьшите max_tokens или число одновременных потоков. # Маршрутизация и fallback Route: /#docs/routing Несколько провайдеров на модель, fallback между моделями и оплата по фактическому провайдеру. ## Как выбирается провайдер Модель может обслуживаться несколькими провайдерами (endpoint-ами) со своими ценами, лимитами и политикой данных. Для каждого запроса платформа строит упорядоченный список endpoint-ов и передаёт его во внутренний Bifrost: первый — основной, остальные — fallback внутри одного вызова. Ключи провайдеров не выдаются клиенту. Без предпочтений нагрузка распределяется как в OpenRouter: endpoint-ы с недавними сбоями идут последними, остальные — в случайном порядке с весом 1/цена², поэтому самый дешёвый получает большую часть запросов. Запрос резервируется по самой высокой цене среди endpoint-ов, которые могут его обслужить, а списывается по цене провайдера, который ответил. В логах видны этот провайдер, маршрут и позиция в цепочке («резервный №N»). ## Предпочтения provider Поле | Значение models | Fallback-модели после model, по порядку (до 8) route | Только "fallback"; поле принимается для совместимости с OpenRouter и на выбор провайдера не влияет provider.order | Провайдеры, которые пробуются первыми, в этом порядке provider.only / ignore | Разрешённые / исключённые провайдеры или маршруты provider.allow_fallbacks | false — только провайдеры из order или только первый endpoint provider.sort | price, throughput или latency (медианы за 30 минут) provider.max_price | Предельная цена в рублях за 1M токенов: prompt, completion provider.data_collection | deny — исключить провайдеров, которые могут хранить запросы provider.zdr | true — только zero data retention provider.require_parameters | true — пропустить модель, которая отбросила бы параметры сэмплинга запроса (возможности задаются на модель) provider.quantizations | Допустимые квантизации, например ["fp8","bf16"] :nitro / :floor | Суффикс model: сортировка по скорости / по цене ```JSON { "model": "openai/gpt-4.1-mini", "models": ["anthropic/claude-sonnet"], "provider": { "order": ["openai"], "allow_fallbacks": true, "sort": "price", "max_price": { "prompt": "300", "completion": "1200" }, "data_collection": "deny" }, "messages": [{ "role": "user", "content": "Ping" }], "max_tokens": 256 } ``` ## Поведение fallback Ситуация | Ожидаемое поведение Провайдер вернул временную ошибку | Gateway применяет разрешённую политику retries Основной endpoint недоступен | Тот же вызов переходит к следующему endpoint-у списка Все endpoint-ы модели недоступны | Пробуется следующая модель из models Часть SSE уже отправлена | Не смешиваем ответ с повторной генерацией другого маршрута Предпочтения исключили все endpoint-ы | 404 до резервирования; деньги не блокируются ## Наблюдаемость GET /api/v1/models/{author}/{slug}/endpoints возвращает endpoint-ы модели в формате OpenRouter: цены в рублях за токен, лимиты, квантизацию, политику данных и измерения шлюза — uptime за 30 минут и сутки, задержку до первого токена и скорость генерации (p50–p99). Автоматический выбор модели (openrouter/auto) не поддерживается: шлюз не оценивает качество ответа и не подменяет модель за клиента. # Лимиты и повторы Route: /#docs/rate-limits Управляйте нагрузкой и не создавайте дубли платных запросов. ## Какие лимиты действуют Ограничение | Что контролирует RPM ключа | Число запросов за минуту, по умолчанию 300 Лимит ключа | Расходы за всё время или за день, неделю, месяц; сброс в 00:00 UTC Срок действия ключа | После даты истечения ключ получает 401 «API key expired» Баланс аккаунта | Доступные средства за вычетом резерва Контекст модели | Допустимый размер input и output ## Заголовки лимитов Заголовки приходят на каждый ответ /v1, прошедший проверку лимита. Окно скользящее: ёмкость возвращается постепенно в течение следующей минуты, поэтому Retry-After остаётся 60 секунд. Эндпоинты аккаунта (/v1/key, /v1/credits, /v1/generation, /v1/keys) считаются отдельно от RPM ключа: 300 запросов в минуту. Заголовок | Значение X-RateLimit-Limit | Лимит запросов в минуту для этого ключа и эндпоинта X-RateLimit-Remaining | Сколько запросов окно ещё пропустит X-RateLimit-Reset | Конец текущей минуты, Unix-время в миллисекундах ## Когда повторять запрос При 429 сначала выясните причину: временный rate limit и исчерпанный денежный бюджет требуют разных действий. Повторяйте с экспоненциальной задержкой и jitter только временные ошибки; если сервер предоставил Retry-After, учитывайте его. После сетевого таймаута результат может быть неизвестен. Не считайте повтор идемпотентным: он способен создать ещё одну генерацию и отдельный расход. Передайте уникальный Idempotency-Key (до 100 байтов). Повтор на том же API-ключе возвращает 409 и не запускает генерацию заново. Тело ответа не сохраняется для повторной выдачи; итог смотрите в истории запросов. # Токены и оплата Route: /#docs/billing Стоимость складывается из входящих (новых и кешированных) и исходящих токенов по тарифу выбранной модели. ## Расчёт стоимости Цены примера вымышленные, указаны в RUB за 1 млн токенов. Публичный тариф уже включает наценку. Значения usage и тарифный snapshot определяют окончательный расчёт; правила округления закрепляются в серверном контракте. cached_tokens — часть промпта, которую провайдер взял из своего кеша (usage.prompt_tokens_details.cached_tokens). Она оплачивается по цене кешированного входа из каталога. Если у модели нет отдельной цены кеша, она равна цене входа. Длинный неизменный префикс (системный промпт, инструкции агента, описание инструментов) чаще попадает в кеш. ```Формула Стоимость = ((input_tokens − cached_tokens) × input_price + cached_tokens × cached_input_price + output_tokens × output_price) / 1 000 000 Пример: 1 000 input, из них 800 из кеша 200 × 44 ₽ + 800 × 4,4 ₽ + 500 output × 176 ₽ Итого: 0,10032 ₽ (без кеша — 0,132 ₽) ``` ## Резервирование До запроса: Платформа проверяет бюджет и атомарно резервирует сумму на выполнение. После usage: Резерв заменяется фактической стоимостью, неиспользованный остаток снова доступен. Если результат неизвестен: Запрос остаётся на сверке до определения usage. Резерв не освобождается только из-за закрытия вкладки. ## Контроль расходов Используйте отдельные ключи для проектов, задавайте бюджет и max_tokens. Финансовая история показывает зачисления и списания; usage-запросы раскрываются отдельно в истории запросов. # Обработка ошибок Route: /#docs/errors Разделяйте ошибки параметров, доступа, бюджета и upstream. ## Формат ошибки Формат совместим с OpenRouter и OpenAI SDK: code — HTTP-статус числом, type — liho_error, metadata.request_id — идентификатор запроса из X-Request-ID. Обрабатывайте ошибки по статусу, а не по тексту message. Сырые upstream-ошибки клиенту не передаются. ```JSON { "error": { "code": 402, "message": "insufficient available balance", "type": "liho_error", "metadata": { "request_id": "9b1f7c2e-4d5a-4e8b-a1c3-2f6d8e0b7a91" } } } ``` ## HTTP-статусы Статус | Причина | Что делать 400 | Неверные параметры | Исправить запрос 404 | Модель недоступна | Проверить ID и каталог 409 | Повтор Idempotency-Key | Не запускать повторную генерацию; проверить историю 401 | Неверный, отозванный или истёкший ключ | Проверить секрет и срок действия 403 | Доступ запрещён | Проверить аккаунт и политику 402 | Недостаточно доступных средств | Пополнить баланс или уменьшить max_tokens 429 | Превышен RPM | Учитывать Retry-After; денежные лимиты возвращают 402 502 / 503 | Ошибка upstream или временная недоступность | Проверить результат перед повтором ## Диагностика Сохраняйте доступный ID запроса, модель, время и код ошибки. Для сверки используйте историю запросов. Не записывайте API-ключи и полные сообщения пользователей в обычные application logs. # Безопасность и данные Route: /#docs/privacy Понимайте, где проходит запрос и какие данные получает модель. ## Путь запроса Ваши сообщения передаются выбранному upstream для выполнения запроса. Собственный API-ключ Liho LLM авторизует приложение; upstream-секреты остаются внутри платформы. ```Архитектура Ваш backend → Liho LLM → внутренний Gateway → LLM-провайдер ``` ## Логи и хранение Сервис хранит метаданные использования и финансовый журнал без тел prompt/response. Это не означает отсутствие хранения у upstream: его политика проверяется отдельно. Промпты и ответы не сохраняются в финансовой истории и логах Liho LLM. Гарантии ZDR, регионального хранения и специальные договорные условия зависят от upstream-провайдера. Не переносите ожидания от другого провайдера без проверки. ## Перед подключением Секреты: Храните ключ на сервере, разделяйте окружения и регулярно ротируйте credentials. Контроль данных: Исключите из сообщений ненужные персональные данные и внутренние секреты. Проверка политики: Согласуйте правила upstream, хранения, логирования и доступные модели до отправки production-данных.