У владельца сервер 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>
12 KiB
Задание 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.
- Адаптер: OpenAI-совместимый
POST {base_url}/chat/completions. Ключ необязателен: локальные серверы обычно его не требуют. Если ключа нет — не выдумывать заголовок авторизации. - Обнаружение моделей:
GET {base_url}/models. Это штатный способ, и он избавляет от той боли, что была сagy models. Список кладётся в тот жеModelDiscoveryService. health_check: запрос к{base_url}/modelsс коротким таймаутом. Локальная сеть быстрая, но сервер может быть выключен — отказ должен быть быстрым и внятным, а не висеть.- Профили и слоты: добавить
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. Аудит вторым проходом
Для проверяющего:
- Ни одного выдуманного значения. В прошлом раунде в веб-мастере нашлись захардкоженные коды устройства
GRK-7842иCDX-9104, а тест требовал наличия неверного адреса, то есть защищал выдумку. Здесь особый риск: модель локальная, соблазн «подставить разумное» велик. - Запустить то, что написано. Адаптер должен быть проверен реальным вызовом к серверу владельца, а не только тестом с заглушкой.
- Отказ при выключенном сервере. Проверить, что Hub не виснет и не роняет маршрутизацию, если 192.168.1.81 недоступен. Это домашний сервер, он будет выключаться.
- Побочные изменения в файлах, которых задание не касалось, — объяснить каждое.
- Пропущенный пункт назвать пропущенным.
Ограничения
- Параллельно идёт 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не создавать.
Критерии приёмки
- Ветка в
origin,git statusчист. - В отчёте приведён вывод разведки с сервера владельца.
- Провайдер
localработает: реальный вызов к серверу возвращает ответ модели — приложить. - Обнаружение моделей через
/modelsнаполняет кэш; список виден в выборе модели. find_free_slot("local")возвращает существующий профиль; миграция добавляет слоты в существующую конфигурацию.- Отсутствие квоты показано отдельным состоянием, отличимым от «данные не пришли».
- При выключенном сервере Hub не виснет, маршрутизация уходит к следующему профилю; проверено.
- Мастер показывает настоящий список моделей при проверке подключения; при отказе — конкретную причину.
- Адрес сервера нигде не зашит.
ruff check .чисто; релизный гейт не ухудшен.- Отчёт:
START_HEAD,FINAL_HEAD,origin/main,git status,X passed / Y skipped / Z failed. Наmainсейчас 375 passed, 2 skipped.
Главное
У владельца реально работает один провайдер из пяти, и запаса нет: если у Antigravity кончатся квоты, переключаться некуда. Локальная модель этот запас даёт — она бесплатна и не исчерпывается. Ради этого задание и делается.
Порядок сдачи
Передать точный FINAL_COMMIT_SHA. Сдано только после появления коммита в origin.