LM Studio API

SkillAI & models

Reference for the LM Studio HTTP API (local model server, usually port 1234). Use it when you need to programmatically manage models in LM Studio - see what's loaded, load or unload a model, set context length and parallelism, disable model thinking - as well as for diagnostics: mo

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the LM Studio API skill

What this skill tells your AI

The instructions your AI receives, as published by desko77/cursor-1c-skills in skills/lmstudio-api/SKILL.md and read by ahel’s review.

Проверено вживую на LM Studio 0.4.x, 2026-08-07, локальный сервер на порту 1234 (AMD Strix Halo, 128 ГБ единой памяти). Замеры в конце - на qwen3-vl-30b-a3b-instruct.

Три поверхности API

ПоверхностьПутьНазначение
OpenAI-совместимая/v1/*инференс: chat/completions; models отдает только КАТАЛОГ скачанных
Нативная v0 (устаревшая)/api/v0/*models с полем state; управления моделями нет
Нативная v1/api/v1/*с LM Studio 0.4.0, рекомендуемая: инференс + управление моделями

Anthropic-совместимые эндпоинты тоже заявлены в доках, не проверялись.

Эндпоинты v1

POST /api/v1/chat (НЕ /api/v1/chat/completions)
GET /api/v1/models
POST /api/v1/models/load
POST /api/v1/models/unload
POST /api/v1/models/download
GET /api/v1/models/download/status

load: обязателен model - точный ключ модели. Опционально context_length, eval_batch_size, flash_attention, num_experts, offload_kv_cache_to_gpu, echo_load_config. Всегда ставить echo_load_config: true - ответ вернет фактически примененный конфиг, и сразу видно, что сервер проигнорировал.

Ответ: {"type", "instance_id", "load_time_seconds", "status": "loaded", "load_config": {...}}. Вызов БЛОКИРУЮЩИЙ - возвращается по факту загрузки, отдельно опрашивать готовность не нужно.

unload: обязателен instance_id, НЕ имя модели. Берется из loaded_instances[].id ответа GET /api/v1/models.

GET /api/v1/models отдает {"models":[...]} с полями key, type, publisher, architecture, quantization, size_bytes, max_context_length, capabilities, loaded_instances[]. У инстанса - id и config с фактическими context_length, parallel, flash_attention, eval_batch_size, num_experts и прочим. Пустой loaded_instances = модель не загружена.

Инференс: два эндпоинта, и они НЕ равнозначны

/v1/chat/completions (OpenAI)/api/v1/chat (родной)
поле вводаmessagesinput
схемалишние ключи проглатываетстрогая, лишний ключ -> 400 unrecognized_keys
управление reasoningНЕТ (см. ниже)reasoning первым классом
ответchoices[].message.contentoutput[].content
статистикаusagestats c reasoning_output_tokens

Тело родного вызова и ответ:

{"model": "...", "input": "<весь промпт строкой>", "reasoning": "off"}

{"model_instance_id": "...",
 "output": [{"type": "message", "content": "Париж"}],
 "stats": {"input_tokens": 22, "total_output_tokens": 4, "reasoning_output_tokens": 0,
 "tokens_per_second": 64.8, "time_to_first_token_seconds": 1.79}}

Как ВЫКЛЮЧИТЬ размышления (thinking / reasoning)

chat_template_kwargs: {"enable_thinking": false} НЕ РАБОТАЕТ. Для GGUF линейки qwen3.x в LM Studio этот параметр не прокидывается - зарегистрированный баг (lmstudio-ai/lmstudio-bug-tracker issue #1990). Замер на qwen/qwen3.6-35b-a3b: с флагом и без него результат идентичен - весь бюджет max_tokens уходит в размышления, finish_reason=length, content пуст, ответа нет вообще.

Это опасно вдвойне: многие клиенты при пустом content подставляют reasoning_content. Тогда в парсер уезжает текст размышлений вместо ответа, и задача со строгим форматом (JSON) не падает с ошибкой, а тихо возвращает мусор.

Рабочий способ - параметр reasoning на /api/v1/chat:

{"model": "qwen/qwen3.6-35b-a3b", "input": "...", "reasoning": "off"}

Тот же запрос через родной эндпоинт: reasoning_output_tokens: 0, чистый ответ в content, 5.8 с против полного провала на OpenAI-совместимом пути.

Допустимые значения зависят от МОДЕЛИ. Схема эндпоинта принимает off | low | medium | high | on, но конкретная модель может поддерживать лишь часть: qwen3.6 отвечает 400 Reasoning setting 'low' is not supported by model ... Supported settings: 'off', 'on'. Источник истины - capabilities.reasoning.allowed_options в GET /api/v1/models. Модель без блока reasoning в capabilities (варианты Instruct, например qwen3-vl-30b-a3b-instruct) не думает в принципе и параметра не требует.

Практический вывод: не отбраковывать reasoning-модели из-за "неотключаемого thinking" - через родной эндпоинт они полностью управляемы. Отбраковка оправдана, только если клиент намертво привязан к OpenAI-совместимому пути.

Грабли протокола

Неизвестный путь отдает HTTP 200, а не 404. Тело при этом {"error":"Unexpected endpoint or method. (GET /path)"}. Код ответа НЕ доказывает существование маршрута - читать тело обязательно.

Несоответствие метода отдает 404, а не 405. GET по POST-эндпоинту выглядит как "маршрута нет". Зондировать наличие маршрута GET-ом бесполезно и приводит к ложному выводу. Правильно: POST с пустым телом - валидатор вернет 400 со схемой (Missing required field 'model'), и это доказывает, что маршрут есть.

/api/v0/models не отдает loaded_context_length у незагруженной модели - поля просто нет в объекте. Идиома x.get("loaded_context_length") or x.get("max_context_length") молча подставит архитектурный потолок (262144 вместо реальных 32768) и обрушит расчет бюджета. Гейтить строго по state == "loaded" (v0) или по непустому loaded_instances (v1).

load может вернуть 500 на крупной модели, хотя загрузка при этом состоится. Замер: модель на 37 ГБ отдала HTTP 500 через 138 секунд (клиентский таймаут был 900 - обрывал не клиент), а через интерфейс та же модель загрузилась штатно. Похоже на внутренний предел сервера на длительность загрузки. Следствие для кода: после неуспешного load не объявлять модель недоступной сразу - перечитать GET /api/v1/models и посмотреть loaded_instances, загрузка могла завершиться уже после ответа с ошибкой. Иначе уйдешь в фолбэк на модели, которая через полминуты будет готова.

Идентификатор модели должен быть точным, включая префикс публикатора. Вызов qwen3.6-35b-a3b вместо qwen/qwen3.6-35b-a3b заставит LM Studio считать это другой моделью и загрузить дубликат.

WebSocket-namespace SDK доступны по сети: /llm, /system, /embedding, /files, /repository, /diagnostics - рукопожатие проходит с удаленной машины. На этом канале работают lms CLI и SDK (lmstudio-python, lmstudio-js). Для управления моделями он больше не нужен - хватает REST v1.

Параллельность и контекст

Unified KV cache: слоты параллелизма делят ОДНО окно контекста. Действует параллельность * (промпт + max_tokens) <= context, а НЕ промпт + max_tokens <= context. Игнорирование дает HTTP 400 Context size has been exceeded.

Планировать бюджет только от ФАКТИЧЕСКОГО context_length загруженного инстанса. Считать по паспортному max_context_length нельзя: разрыв бывает восьмикратным.

Серверный потолок одновременных предсказаний - поле parallel в конфиге инстанса, оно же Max Concurrent Predictions в интерфейсе. Оно ЗАДАЕТСЯ через load, хотя в документации эндпоинта не указано - проверено: {"model": ..., "context_length": 65536, "parallel": 8} применяется, эхо конфига и интерфейс показывают 8. То есть менять его руками в GUI не нужно, профиль загрузки полностью задается из кода.

Больше слотов не значит быстрее. Замер на 30B (Strix Halo): при параллельности 4 и 8 агрегированная пропускная способность одинакова (46.0 и 47.4 ток/с) - железо насыщается уже на четырех, лишние слоты только размазывают ту же полосу. Хуже того, при отправке всех запросов разом стена равна САМОМУ ДОЛГОМУ ответу, тогда как очередь на меньшем числе слотов сглаживает разброс. Сравнивая режимы, нормируй на фактически сгенерированные токены: при ненулевой температуре один и тот же вход дает разный объем вывода (наблюдалось расхождение 27% между прогонами), и сравнение по "стене" без нормировки врет.

Холодный старт: параллельные запросы в незагруженную модель отбиваются

Замер на 30B, 4 одновременных запроса в выгруженную модель:

JIT (без явной загрузки) успешно 1 из 4, три отказа HTTP 500 за 0.0-0.1 с
явный load, затем те же 4 успешно 4 из 4, отказов нет

Первый запрос инициирует JIT-загрузку, остальные сервер отбивает, пока модель грузится.

Тело пятисотки - generic HTML Node, без JSON и без кода ошибки:

<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Error</title></head>
<body><pre>Internal Server Error</pre></body></html>

Отличить "модель еще грузится" от настоящего сбоя по тексту НЕВОЗМОЖНО. Единственный надежный признак - время ответа: отказ неготовности приходит за доли секунды, тогда как настоящий запрос к 30B идет десятки секунд. Правило: HTTP 500 быстрее секунды трактовать как неготовность - убедиться в загрузке и повторить, в счетчик сбоев не засчитывать.

Правильный порядок работы с моделью:

1. GET /api/v1/models - есть ли в каталоге, есть ли loaded_instances
2. загружена -> взять context_length из конфига инстанса
3. не загружена -> POST /api/v1/models/load (блокирующий, ждет сам)
4. загрузка не удалась -> вот теперь модель действительно недоступна
5. бюджет считать от фактического context_length
6. первый запрос отправить в одиночку, потом выходить на параллельность

Жизненный цикл модели: грузить один раз на всю работу

Загрузка стоит десятки секунд, выгрузка - две. Любая перезагрузка между стадиями или между файлами серии - чистая потеря времени.

Начало работы (всей серии, не одного файла):
 модель уже загружена -> ИСПОЛЬЗОВАТЬ КАК ЕСТЬ, бюджет считать от ЕЕ контекста
 не загружена -> load один раз, запомнить instance_id как свой
Во время обработки:
 не выгружать и не перезагружать ничего - ни между стадиями, ни между файлами
Конец:
 РАБОЧУЮ модель конвейера ОСТАВИТЬ загруженной, TTL уберет сам
 РАЗОВУЮ модель из бенчмарка ВЫГРУЗИТЬ СРАЗУ - см. ниже
 чужие инстансы не трогать никогда

Исключение из "оставить загруженными": модель, поднятая под разовый замер. Правило держать инстанс теплым написано для рабочих моделей конвейера, где перезагрузка дорога и повторится. К модели, которую подняли один раз ради сравнения и отвергли, оно не относится: она занимает память до истечения TTL и деградирует всех соседей.

Цена ошибки измерена 07.08.2026 на общем боксе. qwen3.6-35b-a3b осталась после ночного теста в Q8_0 с context_length 262144 и parallel 4 - веса 35 ГБ плюс KV-кэш под четверть миллиона токенов на четыре слота. Соседняя gemma, обслуживающая интерактивный голосовой ввод, отвечала 34.5 с на 8 токенов; после выгрузки лишней модели - 12.3 с на холодном KV и 0.2 с на теплом. Деградация в сотню раз держалась часами и выглядела как "сервер тормозит", а не как чей-то забытый инстанс.

Отсюда два следствия. Первое: закончил замер - выгрузи свой инстанс тем же ходом, не откладывая на TTL. Второе: грузя модель под замер, задавать context_length явно, по реальной нужде замера. Дефолт берет паспортный максимум (у MoE это сотни тысяч токенов), и KV-кэш съедает больше, чем сами веса.

Подстраивать себя под модель, а не модель под себя. Если модель уже загружена с "неудобным" контекстом - считать параллельность от него, а не перезагружать ради круглого числа. Сохраненный конфиг мог быть выставлен осознанно (наблюдалось context_length: 50176 у 32B - число не круглое и явно не дефолтное), и модель может обслуживать чужую задачу. Перезагрузка оправдана, только если существующего окна не хватает даже на ОДИН запрос, и делать ее молча нельзя.

Несколько крупных моделей спокойно живут рядом при достаточной памяти: 30B + 26B + 32B заняли около 65 ГБ из 128 и работали одновременно. Значит "свопов" между стадиями конвейера может не быть вовсе - проверять со-резидентность до того, как городить оптимизацию порядка стадий.

Правило на общем сервере

Сервер моделей может обслуживать не только текущую задачу (типовой случай - параллельно работающий голосовой ввод на своей модели).

Выгружать разрешено только те инстансы, которые загрузил ты сам в этом прогоне. Все, что застали загруженным, не трогать. Перед unload сверять instance_id со своим списком, а не искать модель по имени.

Авто-вытеснение LM Studio этим правилом не управляется: загрузка своей модели в принципе может выбить чужую без явного unload. На боксе с большой памятью почти не грозит - на 128 ГБ 30B и 26B держались одновременно, вытеснения не было. На тесной машине проверять состояние чужих моделей ПОСЛЕ своей загрузки и сообщать пользователю, если что-то выгрузилось.

Замеры (qwen3-vl-30b-a3b-instruct, Q6_K, Strix Halo 128 ГБ)

загрузка 30B (ctx 32768) 29-39 с выгрузка 1.8-3.3 с
загрузка 32B (ctx 50176) 16-19 с
контекст при загрузке 32768 (паспортный max 262144)

Зрение по кадрам-скриншотам, prompt_tokens ПОСТОЯНЕН для всех кадров одного видео (зависит от разрешения, не от содержимого - наблюдалось 1196 и на разреженном, и на плотном экране):

редкий кадр (список участников) completion 63-216, латентность 19-27 с
плотный кадр 1С completion 560-4904, латентность 45-144 с
4 параллельно, редкие кадры 6 с на кадр амортизированно
4 параллельно, плотные кадры 25.5 с на кадр, ~46 ток/с агрегированно

Текстовая задача (маппинг спикеров, вход ~7200 токенов, выход ~80):

qwen2.5-32b-instruct (плотная) 78.5 с
qwen3-vl-30b-a3b-instruct (MoE) 21.8 с
qwen/qwen3.6-35b-a3b, reasoning=off 5.8 с
qwen/qwen3.6-35b-a3b через OpenAI-путь провал: весь бюджет в reasoning, ответа нет

Со-резидентность: три модели одновременно (30B ctx 32768 + 35B ctx 262144 + 26B ctx 32000), суммарно порядка 80 ГБ на 128 ГБ единой памяти - работают без вытеснения. Footprint сильно зависит от ВЫБРАННОГО контекста, а не только от веса: 35B весом 35.2 ГБ при окне 262144 занимает 37.8 ГБ. Грузя модель сам, задавай окно под задачу, а не паспортный максимум.

Выводы для планирования. Латентность одного запроса и пропускная способность расходятся вчетверо - оценивать стоимость стадии по латентности значит завысить в разы. И MoE-модели на текстовых задачах дают кратный выигрыш над плотными при сопоставимом размере.

Signals

GitHub stars
57
Forks
13
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
lmstudio-api-desko77
Source
github.com/desko77/cursor-1c-skills