Вышла версия LocalAI 4.9.0, спустя тринадцать дней и 146 объединенных pull-реквестов. В этот раз новых движков нет. Работа была сосредоточена на улучшении существующих функций LocalAI, включая исправление безопасности, с которым стоит ознакомиться перед обновлением.
Полный список изменений доступен в примечаниях. В этом посте рассматриваются части, которые влияют на повседневную работу, с указанием номеров pull-реквестов, чтобы вы могли изучить изменения в коде.
Прочитайте это перед обновлением: маршруты теперь закрыты по умолчанию
До этого релиза промежуточное ПО (middleware) аутентификации решало, что защищать, сопоставляя префиксы путей. Маршрут, путь которого не был в этом списке, считался публичным.
Это было приемлемо до тех пор, пока у маршрута не появлялся псевдоним. /mcp/chat/completions, /moderations, /models, /backends и /import-model существовали без префикса, который искал классификатор, поэтому глобальная аутентификация к ним не применялась. Любой маршрут, добавленный позже, по умолчанию наследовал ту же проблему, так как отсутствие в списке защищенных префиксов — это состояние, к которому легко прийти случайно.
Теперь промежуточное ПО учитывает методы и закрывает доступ по умолчанию. Маршрут является публичным только в том случае, если его метод и путь присутствуют в явном реестре. Публичными остаются только необходимые элементы: инструкции API, GET-маршруты Swagger, хорошо известный документ LocalAI, а также потоки здоровья, входа в систему, OAuth, SPA, активов, брендинга и начальной загрузки токена регистрации узла. Покрытие всего маршрутизатора подтверждается тестами, поэтому новый маршрут не может стать публичным из-за того, что о нем забыли (#11602).
Если вы используете аутентификацию через базу данных или настроенные устаревшие ключи API, изменятся две вещи. /version теперь требует учетные данные. То же самое касается URL-адресов, которые LocalAI возвращает для сгенерированного аудио, изображений, видео и 3D-активов, что означает, что клиент, который получал эти URL анонимно, должен отправлять ключ. Встроенные развертывания по-прежнему могут открывать узкие префиксы через ApplicationConfig.PathWithoutAuth, а флаги исключения устаревших GET-запросов по-прежнему существуют как явные переопределения совместимости.
Спасибо Наору Яакову за сообщение об этом классе уязвимостей обхода защиты.
Чат теперь может сжимать свою историю
Длинный разговор в конечном итоге перестает помещаться в контекстное окно, и обычный ответ — отбросить самые старые сообщения. Этот релиз позволяет вместо этого их сжимать.
Сжатие включается для каждой модели отдельно через блок compression в конфигурации модели. Когда оно включено, старые завершенные диалоги прогоняются через выбранную вами модель LocalAI и заменяются результатом перед выводом. По умолчанию оно выключено, и для моделей, где оно не настроено, ничего не меняется (#11556).
Некоторые вещи намеренно оставлены без изменений. Начальные системные промпты и промпты разработчика сохраняются, поэтому промпт безопасности не будет удален при суммаризации. Самые новые сообщения сохраняются. Пары вызовов инструментов и их результатов остаются целыми, поэтому сжатая история никогда не содержит вызов, результат которого был потерян. Сжатие выполняется после фильтрации PII и после внедрения промптов Assistant и MCP, включая последующие итерации MCP, поэтому сжатие видит промпт, который был бы отправлен на самом деле. Схемы инструментов и запас, необходимый для завершения, учитываются с консервативным ограничением токенов в автономном режиме.
Ответы без потоковой передачи содержат метаданные сжатия, потоковые ответы содержат их в трейлере использования, а события сжатия, коэффициенты и длительность экспортируются как метрики. Конфигурация прокси-сервера облака отклоняет сжатие, так как LocalAI не может безопасно перезаписать непрозрачную полезную нагрузку провайдера; режим перевода работает. Если сжатие завершается неудачно во время уже начатого потока, вы получите ошибку SSE внутри канала, за которой последует [DONE], вместо обрезанного ответа.
Модели и бэкенды теперь имеют по одной странице
У моделей была галерея и отдельная поверхность управления в разделе Host. У бэкендов был вложенный вид Host для установленных бинарных файлов. Это означало, что местоположение ресурса зависело от того, ищете ли вы его или управляете им, а общие действия находились на уровень глубже, чем нужно.
Теперь /app/models содержит разделы Explore и Installed, а /app/backends — Catalog и Installed. Оба сохраняют состояние поиска, фильтрации и выбора в URL, поэтому отфильтрованный вид — это ссылка, которую можно отправить кому угодно. Бэкенды сохраняют свои варианты, сборки для разработки и область действия целевого узла. Explore предлагает действия по установке Open и Manage с учетом возможностей, в то время как деструктивные элементы управления остаются в Installed, где вы уже просматриваете то, что вам принадлежит. Обзор работы считывает емкость хоста из общего опросчика сводок, который у него уже был, а вложенный пункт назначения Host удален (#11548).
API не изменился. Существующие закладки /app/manage по-прежнему работают через перенаправление, которое сохраняет состояние запроса модели и бэкенда.
Форма импорта скрывала ответ за шевроном
Страница импорта получила новую палитру, но сохранила старую компоновку: столбец шириной 760 пикселей, содержащий поле URI, руководство по форматам, десять чипов модальности, девять полей предпочтений, повторитель «ключ-значение» и редактор YAML, при этом основная кнопка была отделена от формы, которую она отправляла.
Две из проблем были явными ошибками. Кнопка «Импорт» вообще не имела className, поэтому она переходила к кнопке пользовательского агента и отображалась в системном стиле с неправильным шрифтом и без рамки фокуса. Рядом с ней class="btn btn-primary fas fa-save fa-upload" устанавливал Font Awesome в качестве семейства шрифтов самой кнопки, которое наследовал текст метки, в то время как два класса иконок боролись за один ::before.
Переработка переходит к более широкой компоновке и помещает справочник форматов рядом со столбцом работы. Этот справочник отвечает на единственный вопрос, который есть у администратора-новичка: что разрешено вставлять, и раньше он находился за шевроном, который по умолчанию был закрыт. При ширине менее 1024 пикселей он сворачивается в раскрывающийся список и остается доступным. Поле источника является главным и содержит собственную кнопку «Импорт», что позволило удалить скрытую кнопку отправки, которая существовала только потому, что реальное действие находилось вне <form>. Простой и расширенный режимы исчезли: они были идентичны примерно на 80%, и их сохранение означало переключение режима, ключ localStorage и диалог «Сохранить/Отменить», чьей единственной задачей была защита состояния, которое переключение скрыло бы. Что действительно отличается, так это тип ввода, поэтому теперь это две вкладки: источник или YAML (#11461).
Ошибка со строкой классов не была единичной. Последующая проверка обнаружила восемь элементов управления заголовками на семи страницах, где классы двух или трех элементов были объединены в одну строку (#11462).
Модель на 35 ГБ, которая выглядела постоянно сломанной
На фронтенде с двумя репликами загрузка GGUF объемом 35,7 ГБ на недавно добавленный рабочий узел Jetson Thor делала модель недоступной для загрузки с места оператора. Каждая повторная попытка в пользовательском интерфейсе воспроизводила это. При этом промежуточная стадия (staging) все время проходила нормально.
Реплика A установила консультативную блокировку для каждой модели и начала передачу, которая занимала около двадцати минут. Реплика B получила запрос на ту же модель, заблокировалась на pg_advisory_lock и была завершена через шестьдесят секунд из-за statement_timeout для роли localai:
Здесь объединились две ошибки. Route обернул всю холодную загрузку в advisorylock.WithLockCtx, поэтому блокировка удерживалась во время установки бэкенда, многогигабайтного стейджинга и загрузки чекпоинта. Блокировка существует для того, чтобы мешать двум репликам загружать одну и ту же модель одновременно — на принятие этого решения уходят миллисекунды. При этом WithLockCtx переопределял lock_timeout, но не statement_timeout, хотя оба прерывают один и тот же блокирующий вызов. Из-за этого шестидесятисекундный таймаут стал латентной проблемой для каждой advisory-блокировки в кодовой базе, а не только для этой.
Теперь холодные загрузки выполняются как долговечные задачи, поэтому блокировка захватывается и освобождается вокруг проверки дедупликации, а не во время передачи (#11514).
vllm-cpp теперь собирается для карт, отличных от Blackwell
CUDA-образы vllm-cpp собирались для 120a;121a на amd64 и только для 121a на arm64 из десяти архитектур, для которых собирается собственный релизный архив vllm.cpp.
Неподдерживаемая карта не работает медленнее — она падает при первом запросе с ошибкой «no kernel image is available for execution on the device» («для устройства недоступен образ ядра для выполнения»), спустя много времени после того, как установка бэкендов в local-ai отчиталась об успехе vllm-cpp. Эта проблема была обнаружена на узле Jetson Thor, где бэкенд был установлен, но ничего не мог обслуживать.
Разделение зависит от наличия кремния. Jetson работает только на arm64: 87 для Orin и 110 для Thor. Десктопный 120a работает только на amd64. 90a и 100a поддерживаются на обеих архитектурах из-за компонентов SBSA. Это добавляет поддержку A100, A10 и 3090, L4, 4090 и RTX 6000 Ada, H100 и H200, B200, B300, Jetson Orin и Jetson Thor. Защита CUDA 13 теперь также охватывает ветку arm64, а Triton-AOT остается включенным (#11512).
Видео со звуком и TTS в матрице llama.cpp
В vllm-cpp появилась поддержка видеомодальности. В C ABI vllm.cpp появился срез для видео в v12, и когда конфигурация модели объявляет набор чекпоинтов MiniMax-H3, Load открывает видеодвижок вместо текстового, а GenerateVideo обслуживает существующий эндпоинт /video в LocalAI. Видео и аудио генерируются совместно, поэтому MP4 возвращается с реальной дорожкой AAC вместо тишины, а если в промпте запрашивается речь, модель синхронизирует движения губ с ней (#11424).
Есть две детали, о которых стоит знать, если вы решите разобраться. H3 — это не каталог моделей: DiT, текстовый энкодер и два VAE являются отдельными артефактами, поэтому parameters.model указывает на DiT, а остальная часть набора перечисляется в options:. При этом раздел DiT должен быть явно объявлен, а не определяться автоматически, поскольку комьюнити-квантования удаляют метаданные релиза, а DiT-модели FL2VA и Ref2VA имеют идентичную структуру на диске. Передача эталонной кондиции в FL2VA DiT приводит к рендерингу в течение нескольких часов и возврату цветной решетки, поэтому такая комбинация отклоняется до вызова движка. Эндпоинты галереи minimax-h3-fl2va-q4 и minimax-h3-ref2va-q4 устанавливают эти два набора.
Кроме того, Qwen3-TTS теперь работает через бэкенд llama-cpp, что добавляет синтез речи на ту же матрицу ускорителей, которая уже используется для генерации текста: CUDA, ROCm, SYCL, Vulkan, Metal и L4T с использованием собственной конвертации GGUF из апстрима. Это реализовано как слотовая задача SERVER_TASK_TYPE_TTS, а не как обработчик gRPC, управляющий циклом генерации, поскольку server_context владеет llama_context и запускает планировщик слотов в своем собственном потоке, а обработчик со своим собственным циклом создал бы состояние гонки. Серверная обвязка в апстриме все еще находится на стадии драфта, поэтому она поставляется в виде патча, который будет удален при слиянии ggml-org/llama.cpp#26603. Эндпоинты в галерее: qwen3-tts-llamacpp и qwen3-tts-llamacpp-q4. Существующий бэкенд qwen3-tts-cpp остается без изменений, так что они работают параллельно (#11392).
Остальное кратко
HTTP-контроль допущенных запросов (admission control) теперь работает в масштабе всего процесса, а не для каждого бэкенда по отдельности, что также предотвращает бесконечный рост списка трейсов. Кроме того, выполняющиеся операции бэкенда теперь отображаются во время их полета со ссылками прямо на их логи (#11560).
PII-мидлваре теперь может восстанавливать то, что она маскировала. При включенном параметре pii.reverse_in_response замаскированное значение становится уникальным детерминированным псевдонимом в рамках запроса: два адреса отображаются как EMAIL_001 и EMAIL_002 там, где раньше было два одинаковых маркера [REDACTED:EMAIL], и каждый из них восстанавливается, если бэкенд возвращает его обратно. Восстановление корректно обрабатывает SSE-токены, разделенные между записями ответов. Карта подставок локальна для запроса и никогда не записывается на диск, а необратимая маскировка по-прежнему используется по умолчанию (#11272).
Материализация снепшотов Hugging Face теперь скачивает файлы параллельно до заданного лимита, поэтому репозиторий с большим количеством шардов больше не обрабатывается по одному файлу за раз. Параллельно загружаются только целые файлы, но никогда не их части, поэтому возобновление .partial и верификация SHA для каждого файла работают точно так же, как и раньше, а два вызывающих кода вне конвейера артефактов сохраняют свою последовательную очередность через обертку с лимитом в единицу (#11162).
KNN стал роутером запросов первого уровня, а не просто кэшем для классификатора. classifier: knn голосует на основе сходства по размеченным примерам промптов, поэтому ему не нужна модель классификатора, а знания о метках хранятся в корпусе, который вы наполняете через API администратора. Записи ниже knn.similarity_threshold не могут участвовать в голосовании, и если ни одна не проходит порог, роутер возвращается к резервному поведению и не угадывает, при этом nearest_similarity в любом случае фиксируется в решении. Корпус сохраняется в виде одного файла JSONL на один роутер по пути <data path>/router-corpus, а записи, сохраненные под другой моделью эмбеддингов, пересчитывают эмбеддинги при загрузке (#10652).
Реальное время WebRTC теперь можно закрепить за одним портом UDP с помощью переменной LOCALAI_WEBRTC_UDP_PORT, поэтому для контейнера или файрвола требуется одно правило вместо диапазона (#11436). Последующее исправление устранило проблему взаимодействия между двумя настройками. Переменная LOCALAI_WEBRTC_ICE_INTERFACES молча игнорировалась всякий раз, когда был задан порт, поскольку мультиплексор с подстановочными знаками заставляет pion самостоятельно перечислять интерфейсы без фильтрации. На хосте с мостами Docker браузеру передавались адреса вроде 172.18.0.1 и им подобные, происходило подключение по подходящей паре, а затем соединение обрывалось, когда проверки согласия ICE завершались неудачей на остальных интерфейсах (#11466).
В двух macOS-бэкендах отсутствовала поддержка Metal. В stablediffusion-ggml флаги Metal были привязаны к переменной OS=Darwin, которую раннер Darwin не определяет, поэтому BUILD_TYPE=metal создавал сборку без встроенной библиотеки (#11531). parakeet-cpp вообще никогда не передавал BUILD_TYPE=metal в PARAKEET_GGML_METAL; на M1 Air выполнение того же пятиминутного семпла сократилось с 82,57 секунд до 50,18 секунд при идентичном на уровне байтов выходе (#11492).
При падении бэкенда теперь сообщается причина. Неожиданный выход во время выполнения раньше логировал свой вывод ошибок (stderr) только на уровне debug, поэтому при стандартном уровне логирования операторы видели код выхода и ничего более; теперь последняя непустая строка stderr добавляется к предупреждению (#11532), а неудачная проверка готовности gRPC сохраняет код выхода процесса вместе с последними 4 КиБ диагностических данных (#11447). MCP-серверы уровня моделей больше не исчезают из Chat при сбое обнаружения, что раньше скрывало проблемы с доступностью DNS контейнера и VPN; они остаются в списке в виде строк ошибок с прикрепленной причиной (#11495). Несоответствие контрольных сумм после скачивания теперь рассматривается как временный сбой передачи и повторяется, поэтому устаревший ответ CDN больше не приводит к сбою установки, при этом не верифицированные байты по-прежнему отклоняются (#11536).
Веб-интерфейс теперь поддерживает еще два языка: португальский (Бразилия) — полный перевод всех 14 пространств имен (#11427), а также индонезийский для строк администратора, медиа и навигации (#11493).
Количество записей в галерее увеличилось с 1622 до 1707. Появились модели Qwen3.8 в версиях 9B, 27B, Ridge и small, а также Gemma 4 agentic и Scotoma 2, DeepSeek V4 Pro 0813, Ling 3.0 Flash, Nemotron 3.5 Lightning 30B, Tess 4 27B, Ornith 1.0 и 1.5, LFM2.5 в версиях 230M и VL 1.6B, HunyuanOCR, OvisOCR2, Higgs Audio v3 и первое семейство моделей для геномики. В этом релизе приняли участие десять человек, трое из которых — впервые.
Чтобы обновиться, выполните pull образа localai/localai:latest или запустите скрипт установки повторно. Если вы используете API-ключи, сначала ознакомьтесь с разделом аутентификации выше. Полный список изменений содержит все, что не вошло в эту публикацию.
