hermes-hub/agents/inbox/2026-08-24-A25-local-llm-provider.md
Hermes Team 992d0c574c docs(task): A25 — локальная модель как провайдер Hub
У владельца сервер 192.168.1.81 с локальной LLM; хочет использовать её
как субагента. Ценность: локальная модель не имеет квоты и не стоит
денег — идеальный последний резерв. Сейчас у него реально работает один
провайдер из пяти, запаса нет.

Половина работы уже есть: RouterProfileConfig.custom_base_url
существует, deepseek_adapter — готовый образец OpenAI-совместимого
вызова, а профиль через provider: custom у него уже настроен в Hermes.

Первым пунктом — разведка, а не код: ревьюер снаружи увидел открытыми
только 22 и 3000 (Rocket.Chat), порт Ollama закрыт. Что именно слушает,
выясняет владелец на сервере: доступа по SSH нет, вход по ключу не
настроен, пароли не используются.

Отдельным пунктом — честность про квоту: у локальной модели её нет как
понятия, и показывать Н/Д рядом с процентами других провайдеров нельзя,
это читается как «данные не пришли».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 10:34:13 +07:00

137 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Задание A25: локальная модель как провайдер Hub
## Дата поступления
2026-08-24
## База
Проверочный HEAD на момент выдачи: **`f757639`**.
## Ветка
`antigravity/local-llm-provider`
## Порядок исполнения
Два прохода: **Flash** реализует, **Pro** проводит аудит. Идёт параллельно с **A24** (перестройка навигации) — границы по файлам ниже.
---
## Порядок работы с git
```
cd <каталог репозитория>; git fetch origin --prune; git status
git checkout main; git pull --ff-only origin main
git checkout -b antigravity/local-llm-provider
git commit -m "..." <- сначала коммит
git push -u origin antigravity/local-llm-provider
```
В `main` напрямую не пушить. В конце — push и проверка `git log --oneline -1`, `git status` чистый.
---
## Задача
У владельца есть сервер **192.168.1.81** с локальной LLM. Там же стоят Hermes, Docker, git. Он хочет использовать локальную модель как субагента — то есть как обычного провайдера Hub, наравне с Antigravity и Codex.
Ценность очевидна: локальная модель **не имеет квоты и не стоит денег**. Для ролей вроде `fast` и `research` это идеальный резерв, который никогда не исчерпается. Сейчас у владельца реально работает один провайдер из пяти, и запаса нет.
## Что уже есть в проекте
Не начинайте с нуля, половина сделана:
- **`RouterProfileConfig.custom_base_url`** — поле для произвольного адреса уже существует (`router_config.py:22`), сохраняется и загружается;
- **`deepseek_adapter.py`** — готовый образец OpenAI-совместимого адаптера: берёт `profile.custom_base_url`, зовёт `{base_url}/chat/completions`. Локальные серверы (Ollama, vLLM, LM Studio, llama.cpp) выставляют тот же интерфейс;
- у владельца в Hermes уже настроен профиль `deepseek` через `provider: custom, endpoint: https://limitdeckai.ru/v1` — то есть путь проверен на практике.
## P0-1. Разведка — сначала выяснить, что за сервер
**Не угадывайте.** Прежде чем писать адаптер, определите, что именно слушает на 192.168.1.81 и по какому интерфейсу.
Ревьюер проверил снаружи: **пинг проходит, открыты порты 22 и 3000**. Порт 3000 — это Rocket.Chat, к моделям отношения не имеет. Порт Ollama по умолчанию (11434) **закрыт снаружи** — значит сервер слушает только localhost либо использует другой порт.
Попросите владельца выполнить **на сервере** и приложите вывод в отчёт:
```
ss -tlnp | grep -E '11434|8000|8080|1234|5000|5001'
curl -s localhost:11434/api/tags | head -20
curl -s localhost:8000/v1/models | head -20
docker ps --format '{{.Names}}\t{{.Ports}}\t{{.Image}}'
```
От результата зависит: адрес, порт, наличие `/v1/models`, нужен ли ключ, и слушает ли сервис только localhost — в последнем случае Hub с другой машины до него не достучится без правки конфигурации сервера.
Ревьюер не имеет доступа по SSH (вход по ключу не настроен, пароли не используются), поэтому эту часть выполняет владелец.
## P0-2. Провайдер локальных моделей
Добавить провайдера — предлагается ключ **`local`** — по образцу `deepseek_adapter`.
1. **Адаптер**: OpenAI-совместимый `POST {base_url}/chat/completions`. Ключ необязателен: локальные серверы обычно его не требуют. Если ключа нет — не выдумывать заголовок авторизации.
2. **Обнаружение моделей**: `GET {base_url}/models`. Это штатный способ, и он избавляет от той боли, что была с `agy models`. Список кладётся в тот же `ModelDiscoveryService`.
3. **`health_check`**: запрос к `{base_url}/models` с коротким таймаутом. Локальная сеть быстрая, но сервер может быть выключен — отказ должен быть быстрым и внятным, а не висеть.
4. **Профили и слоты**: добавить `local-1`, `local-2`, `local-3` во встроенные умолчания, чтобы миграция из A9 довела их до существующих конфигураций. Проверить, что `find_free_slot("local")` возвращает существующий профиль.
## P0-3. Квоты: у локальной модели их нет, и это надо сказать прямо
Самое важное для честности продукта.
У локальной модели **нет квоты** — не «квота неизвестна», не ноль, а её не существует как понятия. Показывать `Н/Д` рядом с процентами других провайдеров будет читаться как «данные не пришли».
Требуется отдельное состояние: **«Без ограничений»** или равнозначное, с пояснением, что это локальная модель. `is_loading` при этом `false`, `unavailable_reason` — не про ошибку, а про природу провайдера.
Границы всё же есть, и их стоит показать, если сервер их отдаёт: длина контекста, число одновременных запросов. Не отдаёт — не выдумывать.
## P0-4. Мастер подключения
Добавить локального провайдера в мастер. Поля: **адрес сервера** (например `http://192.168.1.81:11434/v1`), необязательный ключ, кнопка проверки.
Проверка должна быть настоящей: запрос к `/models`, показ найденных моделей. Не «сохранено», а «сервер ответил, доступно N моделей» с их перечислением.
При недоступности — конкретная причина: не отвечает, отвечает не тем, требует ключа. **Не «ошибка подключения» без пояснения** — этот урок уже оплачен кодом 12 и «terminated unexpectedly».
## P0-5. Роль по умолчанию
Предложить владельцу, куда поставить локальную модель в цепочках, и обосновать. Разумно — последним резервом у `fast` и `research`: она бесплатна и не исчерпывается, значит подходит как последняя линия, когда платные квоты кончились.
**Не менять цепочки владельца молча.** Предложить в отчёте, решение за ним.
## P0-6. Аудит вторым проходом
Для проверяющего:
1. **Ни одного выдуманного значения.** В прошлом раунде в веб-мастере нашлись захардкоженные коды устройства `GRK-7842` и `CDX-9104`, а тест **требовал** наличия неверного адреса, то есть защищал выдумку. Здесь особый риск: модель локальная, соблазн «подставить разумное» велик.
2. **Запустить то, что написано.** Адаптер должен быть проверен реальным вызовом к серверу владельца, а не только тестом с заглушкой.
3. **Отказ при выключенном сервере.** Проверить, что Hub не виснет и не роняет маршрутизацию, если 192.168.1.81 недоступен. Это домашний сервер, он будет выключаться.
4. **Побочные изменения** в файлах, которых задание не касалось, — объяснить каждое.
5. **Пропущенный пункт назвать пропущенным.**
---
## Ограничения
- Параллельно идёт **A24** (перестройка навигации веба). Его зона: `router/web/static/**`. **Туда не заходить**, кроме добавления локального провайдера в мастер — эту правку согласовать в отчёте.
- Ваши файлы: `adapters/**`, `router_config.py`, `auto_assigner.py`, `quota_collector.py`, `model_discovery*`, `router/ui/add_account_wizard.py`, соответствующие тесты.
- Адрес локального сервера — **настройка, а не константа**. Ни `192.168.1.81`, ни порт в коде не зашивать.
- Отсутствие квоты показывать как отдельное состояние, а не как отсутствие данных.
- Тег `v0.1.1` не создавать.
## Критерии приёмки
1. Ветка в `origin`, `git status` чист.
2. В отчёте приведён вывод разведки с сервера владельца.
3. Провайдер `local` работает: реальный вызов к серверу возвращает ответ модели — **приложить**.
4. Обнаружение моделей через `/models` наполняет кэш; список виден в выборе модели.
5. `find_free_slot("local")` возвращает существующий профиль; миграция добавляет слоты в существующую конфигурацию.
6. Отсутствие квоты показано отдельным состоянием, отличимым от «данные не пришли».
7. При выключенном сервере Hub не виснет, маршрутизация уходит к следующему профилю; проверено.
8. Мастер показывает настоящий список моделей при проверке подключения; при отказе — конкретную причину.
9. Адрес сервера нигде не зашит.
10. `ruff check .` чисто; релизный гейт не ухудшен.
11. Отчёт: `START_HEAD`, `FINAL_HEAD`, `origin/main`, `git status`, `X passed / Y skipped / Z failed`. На `main` сейчас 375 passed, 2 skipped.
## Главное
У владельца реально работает один провайдер из пяти, и запаса нет: если у Antigravity кончатся квоты, переключаться некуда. Локальная модель этот запас даёт — она бесплатна и не исчерпывается. Ради этого задание и делается.
## Порядок сдачи
Передать точный `FINAL_COMMIT_SHA`. Сдано только после появления коммита в `origin`.