10 KiB
llm-web-proxy
Goal
Кастомный LLM-провайдер (локальный, Windows) для pi и Claude Code: web-чаты вместо API. Основа — код omniroute (executor'ы + трансляторы), вытащенный и завёрнутый в минимальный сервер. Решает боль текущих комбо omniroute: quality validation режет стримы, 503 chat_admission_busy, оверкилл Next.js+sqlite+дашборд ради 2 комбо.
Заказчик: vitya. Сессия дизайна 2026-08-22 (.wiki/concepts/omniroute-combo-deepseek-web-tuning.md).
Почему (контекст)
- pi ходит в omniroute (localhost:20128) → combo
deepseek-v4-flash-web/deepseek-v4-pro-web→ провайдерdeepseek-web(chat.deepseek.com через userToken, 3 аккаунта: megan/yulia/vitya). - Веб-чат DeepSeek не имеет нативного function calling — omniroute эмулирует:
tools[]→ текстовый контракт<tool>{"name":"...","arguments":{...}}</tool>в промпт → парсит ответ обратно вtool_calls. - Существующий
llm-router-failover-proxy(служба, порт 8787) — не подходит: пасsthrough для API-провайдеров (routerai/официальный DS), нет web-протокола, нет эмуляции тулов, нет/v1/messages. - Диагноз вылетов в omniroute: (1)
system_messageoverride режет скилы pi, (2)tool_filter_regexрежет тулы, (3) search-режим генерит DEEP_SEARCH-мусор, (4) quality validation (isStreamingUpstreamError) режет стрим наtype:error, (5) 503 = capacity веб-чата. Всё, кроме 503, — следствие оверкилла; в кастомном провайдере этих слоёв просто не будет.
Архитектура
pi ── /v1/chat/completions (OpenAI) ──┐
Claude Code ── /v1/messages (Anthropic) ──┴──▶ llm-web-proxy (localhost:8788)
│ 1. detect format (openai | claude)
│ 2. round-robin: combo → account (конфиг)
│ 3. web executor: auth(PoW) → session → SSE
│ 4. эмуляция тулов: <tool>-контракт → tool_calls
│ 5. ретраи/cooldown на 429/5xx/timeout
▼
chat.deepseek.com (и др. web-чаты)
Комбо (конфиг, один на провайдера — НЕ смешивать)
deepseek-flash-web→ deepseek-web аккаунты (megan/yulia/vitya), модельdeepseek-v4-flash-think(search ВЫКЛЮЧЕН — он ломал тулы)deepseek-pro-web→ те же аккаунты,deepseek-v4-pro- позже:
qwen-web,gemini-web,kimi-web,grok-web— отдельные комбо, свои executors
Что берём из omniroute (готовое, работает)
Из docker-контейнера omniroute (/app/), образ diegosouzapw/omniroute:latest:
open-sse/executors/deepseek-web.ts(1147 строк) — auth userToken→accessToken, PoW-челлендж, создание/переиспользование сессий, SSE-парсинг фрагментов (THINK/ANSWER), эмуляция тулов. Взять целиком, удалить лишнее (persistSession уже есть).open-sse/translator/deepseekWebTools.ts(486 строк) +open-sse/translator/webTools.ts— парсеры зоопарка<tool>-форматов. Взять целиком.open-sse/translator/request/claude-to-openai.ts+open-sse/translator/response/openai-to-claude.ts— конвертация Anthropic ⇄ OpenAI (для Claude Code).- Зависимости:
@omniroute/open-sseимеет свои импорты — вытаскивать вместе с нужными модулями (streamHelpers,tokenAccountingи т.п.) либо скопировать файлы в свой пакет и починить импорты. Решение исполнителя, но НЕ переписывать логику.
Что пишем сами (мало)
src/server.js— HTTP:POST /v1/chat/completions(OpenAI) +POST /v1/messages(Anthropic, с SSE keepaliveevent: ping— Claude Code рвёт соединение при долгом промпте) +GET /health.src/router.js— round-robin по аккаунтам + cooldown/circuit (паттерн llm-router-proxysrc/router.js— уже написан, работает).config/web-providers.json— комбо → аккаунты → модель. Ключи НЕ в конфиге: userToken'ы из~/.pi/agent/auth.jsonили отдельного файла (формат на усмотрение, gitignore).- Логирование — первоклассное требование (см. ниже).
Логирование (обязательно, для диагностики агентом)
Файл-лог, одна строка = одно событие, grep-парсится: [TS] [TAG] event=... key=value.
| Событие | Что пишем | Зачем |
|---|---|---|
req |
alias, аккаунт, модель, msgs, tools, stream | какой аккаунт крутит round-robin |
tool-prompt |
tools=N, bytes контракта | контракт раздулся → модель теряет формат |
raw-reply |
первые ~500 символов сырого ответа модели | видно <tool>-блоки, DEEP_SEARCH-мусор, reasoning, пустоту — корень 90% проблем |
parse |
calls=N, имена тулов, аргументы валидны | парсер нашёл/потерял вызовы, arguments:{} |
upstream |
статус, код (401/429/503), PoW, таймаут, ретрай N | лимиты, баны |
session |
created/reused/deleted, persist | эффективность persistSession |
stream |
complete/truncated/error, байты, длительность | обрыв стрима |
failover |
упавший апстрим → куда ушёл | round-robin честен |
Правила: тела user-сообщений НЕ логируем (только размеры); raw-reply обрезанный; ротация (.1, .2); пишем в stdout + файл (winsw).
Требования
- OpenAI-совместимый
/v1/chat/completions— pi подключается одним провайдером в~/.pi/agent/models.json. - Anthropic-совместимый
/v1/messages— Claude Code подключается черезANTHROPIC_BASE_URL=http://127.0.0.1:8788(+ свой API-ключ, любой). - Tool-calls работают в обоих форматах: несколько последовательных инструмент-раундов (как pi вызывает тулы; Claude Code —
tool_useблоки). - thinking/reasoning deepseek не ломается (reasoning_content для pi,
thinking-блоки для Claude Code). - Failover: 429/5xx/таймаут/сеть → следующий аккаунт в комбо; cooldown + circuit-breaker на упавший аккаунт.
- persistSession: true — переиспользование сессии (меньше 503 chat_admission_busy).
- БЕЗ quality validation — этого слоя в omniroute нет (это и была боль).
- Логирование — по спеке выше; агент должен по логу восстанавливать цепочку запроса.
- Конфиг — JSON: комбо → упорядоченные аккаунты (baseUrl/executor, userToken-ref, model, timeout), router-параметры.
- Тесты: TDD — round-robin, failover, парсинг
<tool>(валид/зоопарк форматов), claude⇄openai конвертация, стриминг.
Acceptance
- pi →
/v1/chat/completions(model=deepseek-flash-web): стриминг, tool-calls в несколько раундов, reasoning не ломается. - Claude Code →
/v1/messages: подключается, тулы (tool_use) работают, SSE keepalive не даёт оборвать соединение. - Failover доказан тестом: аккаунт недоступен (503/таймаут) → запрос обслуживает следующий; cooldown работает.
- Лог: по
raw-reply+parseвидно каждый tool-call модели; тела пользователя отсутствуют. - Инструкция pi-интеграции (models.json) + Claude Code (env) + запуск/остановка (winsw-сервис, паттерн llm-router).
- Деплой: локально, Windows, winsw-сервис
llm-web-proxy, живой smoke.
Деплой
Локально на Windows: Node-процесс + winsw-сервис (паттерн agents-task-runner / llm-router уже обкатан). Ключи не уходят с машины. Порт 8788 (не конфликтует с llm-router :8787).
Обязательные скилы — вызвать до начала работы
- invoke
tdd-criteria— до написания кода - invoke
using-tasks— статус задачи - invoke
project-discipline— коммиты/пуши - invoke
using-wikiпосле закрытия — обновить концепт
TDD: да — failover/приоритет/парсинг/конвертация это чистые юнит-кейсы; тест-харнесс обязателен (мок-апстрим). Разрешения: интерны: да | автопуш: да Weight: needs-claude Notify: OpeItcLoc03/admin