hermes-hub/agents/inbox/2026-08-24-A25-local-llm-provider.md
Hermes Team bad24ff6aa docs(task): A25 — разведка выполнена, задание построено на фактах с сервера
Владелец дал доступ по ключу. На 192.168.1.81 работают два сервера
llama.cpp, оба OpenAI-совместимые, проверено запросами:

  8081  Qwen3.8-27B-Q4_K_M   reasoning on, контекст 65536, -ngl 99
  8082  Qwen3-4B-Instruct    reasoning off, «compressor»

GET /v1/models и POST /v1/chat/completions отвечают 200 на обоих.
Видеокарта Tesla V100-32GB, занято 28.7 из 32.7 ГБ.

Три следствия внесены в задание как определяющие реализацию:
- оба слушают только 127.0.0.1, снаружи недоступны; варианты решения
  предложены владельцу, выбор за ним;
- у обоих --parallel 1, то есть один запрос за раз: лизы Hub обязаны
  ограничивать локального провайдера, иначе роли заблокируют друг друга;
- памяти видеокарты на третью модель нет, слотов ровно два.

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

160 lines
14 KiB
Markdown
Raw Permalink 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` работают **два сервера llama.cpp**, оба OpenAI-совместимые:
| Порт | Модель | Настройки |
|---|---|---|
| **8081** | `Qwen3.8-27B-Q4_K_M.gguf` | `-ngl 99`, контекст 65536, flash-attn, KV-кэш q8_0, **reasoning on** (бюджет 4096, формат deepseek), temp 0.2, top-p 0.9 |
| **8082** | `Qwen3-4B-Instruct-2507-Q4_K_M.gguf` | то же, но **reasoning off**; в имени каталога — «compressor» |
Проверено запросами:
```
GET /v1/models -> список моделей, формат llama.cpp
POST /v1/chat/completions -> 200 (на обоих портах)
```
Видеокарта: **Tesla V100-PCIE-32GB, занято 28.7 ГБ из 32.7**.
### Три следствия, которые определяют реализацию
1. **Оба сервера слушают только `127.0.0.1`.** Снаружи, в том числе с машины владельца под Windows, они недоступны. Пока это не решено, провайдер работать не будет. Решение выбирает владелец, вариант в отчёт:
- перезапустить llama.cpp с `--host 0.0.0.0` — модель станет доступна всем в домашней сети;
- держать Hub на самом сервере — там уже стоят Hermes и Hub, и `127.0.0.1` доступен напрямую;
- проброс по SSH — не годится для службы, туннель придётся держать поднятым.
2. **У обоих серверов `--parallel 1`.** Это значит **один запрос за раз**. Второй встанет в очередь и будет ждать. Для маршрутизации это принципиально: лизы Hub обязаны ограничивать локального провайдера одним одновременным вызовом, иначе роли начнут блокировать друг друга и таймауты пойдут лавиной.
3. **Память видеокарты почти исчерпана** — 28.7 из 32.7 ГБ. Третью модель не поднять, и это не проблема Hub, а факт, который надо учитывать: локальный провайдер даёт ровно две модели.
### Что это значит для ролей
Набор напрашивается сам:
- **27B с reasoning** — тяжёлые роли: `reviewer`, `coder-secondary`. Модель рассуждающая, с большим контекстом;
- **4B** — `fast`: быстрые вспомогательные вызовы, ради чего она и поднята.
Предложить владельцу, не менять цепочки молча.
## 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` во встроенные умолчания — ровно по числу поднятых моделей; третью на этой видеокарте не запустить, чтобы миграция из A9 довела их до существующих конфигураций. Проверить, что `find_free_slot("local")` возвращает существующий профиль.
## P0-3. Квоты: у локальной модели их нет, и это надо сказать прямо
Самое важное для честности продукта.
У локальной модели **нет квоты** — не «квота неизвестна», не ноль, а её не существует как понятия. Показывать `Н/Д` рядом с процентами других провайдеров будет читаться как «данные не пришли».
Требуется отдельное состояние: **«Без ограничений»** или равнозначное, с пояснением, что это локальная модель. `is_loading` при этом `false`, `unavailable_reason` — не про ошибку, а про природу провайдера.
Границы всё же есть, и их стоит показать, если сервер их отдаёт: длина контекста, число одновременных запросов. Не отдаёт — не выдумывать.
## P0-4. Мастер подключения
Добавить локального провайдера в мастер. Поля: **адрес сервера** (у владельца это `http://192.168.1.81:8081/v1` и `:8082/v1`), необязательный ключ, кнопка проверки.
Проверка должна быть настоящей: запрос к `/models`, показ найденных моделей. Не «сохранено», а «сервер ответил, доступно N моделей» с их перечислением.
При недоступности — конкретная причина: не отвечает, отвечает не тем, требует ключа. **Не «ошибка подключения» без пояснения** — этот урок уже оплачен кодом 12 и «terminated unexpectedly».
## P0-5. Роль по умолчанию
Предложить владельцу, куда поставить обе модели, и обосновать. Разумно исходить из их назначения: 27B с включённым reasoning — последним резервом у `reviewer` и `coder-secondary`, 4B — у `fast`. Обе бесплатны и не исчерпываются, значит годятся как последняя линия, когда платные квоты кончились.
Помнить про `--parallel 1`: ставить одну и ту же локальную модель резервом сразу нескольким ролям опасно — при одновременной нагрузке они выстроятся в очередь друг за другом.
**Не менять цепочки владельца молча.** Предложить в отчёте, решение за ним.
## 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`.