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>
This commit is contained in:
Hermes Team 2026-08-24 10:34:13 +07:00
parent f757639b7e
commit 992d0c574c

View file

@ -0,0 +1,137 @@
# Задание 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`.