Введение
В своих прошлых материалах я уже рассказывал о запуске крупных MoE-моделей на ограниченном железе через llama-server и о сравнительных экспериментах с 35B, 120B и 176B-классами моделей. Практика показала, что llama.cpp давно перестал быть просто «запускатором GGUF»: сегодня это полноценный рантайм с OpenAI-совместимым API, поддержкой инструментов, Jinja-шаблонов, спекулятивного декодирования и гибкого CPU/GPU-offload. Единственное, чего ему не хватало для повседневной работы, — удобной обвязки, которая избавила бы от ручного набора десятка флагов при каждом старте.
Именно поэтому у меня появился небольшой лаунчер на Go: llama-runner, который брал на себя типовые сценарии: сам раскрывал ~ в абсолютные пути, проверял наличие бинарника и модели, собирал аргументы llama-server, пробрасывал логи и корректно гасил дочерние процессы по Ctrl+C. Программа успешно решала свою задачу, пока я не наткнулся на статью «70 токенов в секунду в Qwen3.8-27B на RTX 3090», где помимо тонкой настройки llama-server подробно разбиралась связка локального сервера с агентной оболочкой Claude Code. Идея показалась мне логичным развитием собственного инструмента: зачем запускать сервер и клиент двумя командами в разных терминалах, если это можно сделать одним вызовом?
Так родилась вторая версия llama-runner, о которой и пойдёт речь.
Что именно требовалось сделать
Задача формулировалась просто: llama-runner должен по умолчанию запускать llama-server, дожидаться его готовности и стартовать Claude Code с уже настроенными переменными окружения. Так, чтобы пользователь видел перед собой интерфейс агента, а не возился с адресами и портами. При этом хотелось сохранить и «старое» поведение: если запуск идёт только ради сервера (например, для доступа через WebUI), Claude Code поднимать не нужно.
Формально это вылилось в четыре изменения:
Флаг
-server, это режим «только сервер», полностью совпадающий с прежним поведением.Флаг
-aliasс автоподстановкой значенияlocal, если он не задан явно и не активен-server.Ожидание готовности
llama-serverчерез опрос/healthс настраиваемым таймаутом.Запуск Claude Code с переменными
ANTHROPIC_BASE_URL,ANTHROPIC_AUTH_TOKEN,ANTHROPIC_API_KEYиANTHROPIC_DEFAULT_*_MODEL.
Звучит несложно, но в процессе всплыли три нетривиальные проблемы, каждая из которых заслуживает отдельного абзаца.
Проблема первая: имя модели
Claude Code как известно, клиент, изначально рассчитанный на модели Anthropic, и по умолчанию он запрашивает у сервера claude-sonnet-4 или claude-haiku-4. Локальный llama-server о таких именах ничего не знает и отвечает 404 model not found. Возможных решений два: либо глобально прописать нужные переменные в ~/.bashrc, либо заставить лаунчер подставлять имя модели автоматически.
Я выбрал второй путь, но с оговоркой. Флаг -alias пробрасывается в llama-server как --alias <name> и одновременно устанавливается в три переменные: ANTHROPIC_DEFAULT_SONNET_MODEL, ANTHROPIC_DEFAULT_HAIKU_MODEL и ANTHROPIC_DEFAULT_OPUS_MODEL. Клиент и сервер договариваются об имени без участия пользователя. Чтобы не заставлять людей каждый раз вводить -alias, я предусмотрел автоподстановку значения local: если пользователь не задал флаг явно и не запустил лаунчер в режиме -server, alias выставляется автоматически. Проверка «задал ли пользователь флаг» делается через flag.Visit, так можно отличить «не задан» от «задан пустой строкой» (последнее означает сознательный отказ от алиаса).
Проблема вторая: логи сервера ломают TUI Claude Code
Это оказалось самым неприятным сюрпризом. Claude Code терминальное приложение с альтернативным экраном, невидимым курсором и постоянной перерисовкой ANSI-последовательностями. Как только llama-server пишет что-то в тот же терминал (а он пишет довольно много: метрики слотов, timing, предупреждения), интерфейс агента начинает «размазываться»: текст накладывается друг на друга, курсор прыгает, ввод глючит.
Решение этой проблемы заключается в том, что бы перенаправлять stdout и stderr сервера в файл, когда мы работаем в паре с Claude Code. В llama-runner появились флаги -log-file (по умолчанию ~/.cache/llama-runner/llama-server.log) и -quiet-server (по умолчанию true). В режиме -server логи, как и раньше, идут в терминал и там это никому не мешает. Туда же уходят логи mcp-search-server, если он запущен через -webui-mcp-proxy.
Проблема третья: шаблон чата Qwen и системное сообщение
Самая коварная ошибка. После запуска Claude Code в логах llama-server начал появляться 500 с текстом:
Error: Jinja Exception: System message must be at the beginning.
Как выяснилось, причина в стандартном Jinja-шаблоне Qwen 3.8, который строго требует, чтобы системное сообщение было первым в диалоге. Claude Code, когда делает повторную попытку после сбоя, отправляет системное сообщение уже не первым и шаблон падает, клиент "ретраит", цикл повторяется. В логе это выглядит как бесконечная серия W srv operator(): got exception.
Лечится подменой шаблона. Сообщество давно поддерживает исправленные версии, например, в репозитории froggeric/Qwen-Fixed-Chat-Templates на Hugging Face. Шаблон лежит в корне репозитория под именем chat_template.jinja, и скачивается одной командой через curl:
curl -fL -o ~/GGUF/qwen-fixed-chat-template.jinja \ https://huggingface.co/froggeric/Qwen-Fixed-Chat-Templates/resolve/main/chat_template.jinja
Я специально кладу файл в ~/GGUF туда же, где лежат сами модели. Это не обязательно, но удобно: все связанные с моделями артефакты в одном месте, и путь к шаблону совпадает с дефолтом лаунчера. Флаг -f заставляет curl завершиться с ошибкой, если сервер вернёт не 200, а, например, HTML-страницу редиректа или заглушку блокировки.
После скачивания обязательно убедитесь, что получен именно Jinja-шаблон, а не что-то другое. Достаточно взглянуть на первые строки (пример актуален для моей конфигурации):
head -20 ~/GGUF/qwen-fixed-chat-template.jinja
Ожидаемый вывод, это конструкции вида {%- if tools %} и {{- '<|im_start|>system\n' }}. Если вместо этого вы видите <!DOCTYPE html> или JSON с полем error, значит скачалась заглушка, необходимо перепроверить URL и повторить загрузку. Полезно также сразу убедиться, что в файле нет строки, из-за которой падал сервер:
grep -n "System message must be at the beginning" ~/GGUF/qwen-fixed-chat-template.jinja
Пустой вывод grep это хороший знак: это именно исправленный шаблон, в котором проверка ослаблена. Если строка найдётся, значит скачался не тот файл (например, из другой ветки репозитория), и стоит поискать альтернативу (шаблон от Qwen2.5 или Qwen3-Coder).
В llama-runner появился флаг -chat-template с дефолтом ~/GGUF/qwen-fixed-chat-template.jinja. То есть после скачивания в указанное место вам больше ничего не нужно передавать вручную: лаунчер сам подставит --chat-template-file в аргументы llama-server. Если же файл лежит в другом каталоге или вы экспериментируете с несколькими шаблонами, путь можно переопределить:
./llama-runner -chat-template ~/models/another-template.jinja
Проверка существования файла сделана мягкой. Если по указанному пути шаблона нет, llama-runner выведет предупреждение вида предупреждение: файл шаблона "..." не найден, запускаю без --chat-template-file и продолжит работу, при этом сервер стартует со встроенным шаблоном модели. Это удобно, когда вы пробуете разные модели и не хотите каждый раз править конфигурацию: несовместимость шаблона с конкретной моделью видна сразу по ошибке 500 в логе, но сам лаунчер из-за отсутствия файла не падает. Сознательно отказаться от кастомного шаблона можно и явно, передав пустое значение:
./llama-runner -chat-template ""
Такой запуск полностью игнорирует и дефолт, и переменные окружения, и передаёт серверу управление шаблоном «как есть».
Что умеет новая версия
Собрав всё вместе, я получил инструмент со следующей логикой.
Режим по умолчанию (сервер + Claude Code).
./llama-runner
Лаунчер запускает llama-server с моделью из ~/GGUF, подставляет --alias local, подхватывает исправленный шаблон, ждёт готовности через /health, выставляет переменные окружения и стартует claude. Логи сервера уходят в ~/.cache/llama-runner/llama-server.log. Когда пользователь выходит из Claude Code (любым способом — /exit, Ctrl+C, EOF), лаунчер шлёт SIGINT серверу и дожидается его завершения. Порт не остаётся занятым «висящим» процессом.
Режим только сервера (старое поведение).
./llama-runner -server
Логи идут в терминал, Claude Code не запускается, поведение полностью совпадает с первой версией программы. Удобно, когда вы работаете через встроенный WebUI llama-server или подключаете собственный клиент.
Гибкая настройка.
./llama-runner -alias qwen3-coder -claude-dir ~/my-project ./llama-runner -model ~/GGUF/other.gguf -alias other ./llama-runner -chat-template "" # без кастомного шаблона ./llama-runner -v # показать команду без запуска
Помимо этого, есть флаги для контекста (-c), размеров батча (-b, -ub), flash attention (-fa), числа параллельных слотов (--parallel), режима инструментов (-tools), Jinja-шаблонов (-jinja), включения mmap с ленивым режимом (-lm-lazy) и MCP-прокси (-webui-mcp-proxy). В режиме прокси лаунчер дополнительно поднимает mcp-search-server на порту :8090 , который осуществляет поиск необходимой информации в интернете и корректно гасит его при выходе. Код моего "поискового" MCP сервера.
Приглашение к использованию
Код открыт и доступен по адресу: https://github.com/SkalaSkalolaz/llama-runner/
Проект распространяется свободно, без каких-либо ограничений на использование. Если у вас Ubuntu или другой Linux, есть локальная модель в GGUF и вы давно хотели попробовать агентную работу с Claude Code поверх llama-server , берите и пользуйтесь. Сборка тривиальная:
git clone https://github.com/SkalaSkalolaz/llama-runner/ cd server go build -o llama-runner . ./llama-runner -h
Особенно рекомендую тем, кто, как и я, экспериментирует с MoE-моделями на ограниченной VRAM: связка llama-server + Claude Code + Qwen3.8-35B-A3B даёт вполне рабочую агентную среду без облаков, API-ключей и ежемесячных платежей. Если найдёте баги, присылайте issue или пул-реквесты - проект живой, и я готов его развивать дальше.
Одна оговорка: llama-server с флагом --tools all это серьёзная штука. Если вы открываете порт не только на 127.0.0.1, ограничьте режим инструментов через -tools до нужного подмножества. В остальном - запускайте, экспериментируйте и делитесь результатами.

