Владелец дал доступ по ключу. На 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>
160 lines
14 KiB
Markdown
160 lines
14 KiB
Markdown
# Задание 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`.
|