Choosing a Protocol¶
LM Studio speaks several APIs on one port, and runs whatever model you have downloaded behind all of them. This gem registers a protocol for each:
| API | Endpoints | Protocols |
|---|---|---|
| OpenAI | POST /v1/chat/completions, POST /v1/responses, GET /v1/models, POST /v1/embeddings |
:chat_completions (default), :responses |
| Anthropic | POST /v1/messages |
:anthropic |
| Native REST | v0: POST /api/v0/chat/completions, GET /api/v0/models v1: POST /api/v1/chat, GET /api/v1/models, POST /api/v1/models/load, /unload, /download |
:native_v0, :native_chat |
| Decisions | POST /v1/decisions, POST /v1/systemone |
:decisions (default for judgments), :system_one |
The first three APIs carry the five chat protocols, selected with
lms_protocol. The
decision protocols serve only judgments and are selected separately with
lms_judgment_protocol — see Configuration.
Two choices, not one¶
The protocol and the model each decide part of the outcome
The protocol decides what is expressible. with_schema is honored on
:chat_completions and :native_v0 and silently ignored on :responses
and :anthropic — same model, same schema, prose where you expected JSON.
Reasoning is spelled none/minimal/low/medium/high/xhigh on the OpenAI
endpoints and off/low/medium/high/xhigh/on on the native one.
That much is consistent for every model, and this gem papers over what it
can.
The model decides whether it honors what the protocol accepted, and no
provider code can fix that. LM Studio forwards tool_choice: "required"
for anything — qwen3 obeys it, gpt-oss ignores it. It constrains generation
with a grammar built from your schema, so every model returns
structurally valid JSON — and gpt-oss returns
{"name":"analysis","age":0}, which parses cleanly and means nothing.
This is the price of LM Studio's reach: it is all things to all models. Test against the models you actually intend to ship on, rather than assuming a capability carries across either axis.
The chat protocols compared¶
Stay on the default unless you need something only another column offers.
:chat_completions |
:responses |
:anthropic |
:native_v0 |
:native_chat |
|
|---|---|---|---|---|---|
| Streaming | yes | yes | yes | yes | yes |
Client-side tools (with_tools) |
yes | yes | yes | yes | no — raises; use MCP integrations |
Structured output (with_schema) |
yes | ignored, silently | ignored, silently | yes | no — raises |
| Inference stats | no | no | no | yes, via raw |
yes, via tokens |
| LM Studio request extras | yes | yes | yes | yes | sampling settings only — see Native REST API |
| Server-stored conversations | no | no | no | no | yes |
| Reasoning control | yes | yes | yes | yes | yes |
| Reasoning text returned | yes | yes | yes | yes | yes |
Checked against LM Studio 0.4.24+1; the :anthropic reasoning rows and the
decision endpoints against 0.4.26. The cells that say the server ignores
something describe observed behavior, not anything the gem enforces.
Where a protocol ignores something silently rather than raising, its page says
so — those are the cases that cost you an afternoon. Example
15_protocol_matrix.rb runs the with_schema row against your own models.
"Reasoning control" means with_thinking reaches the server. Whether a given
model reasons at all is a separate question — see Reasoning.
Selecting one¶
RubyLLM.configure { |config| config.lms_protocol = :anthropic } # process-wide
RubyLLM.context { |config| config.lms_protocol = :native_v0 } # one context
.chat(model: 'qwen/qwen3-4b', provider: :lms)
RubyLLM.chat(model: 'qwen/qwen3-4b', provider: :lms, # one chat
protocol: :native_chat, assume_model_exists: true)
Whatever lms_protocol says, embeddings go to /v1/embeddings and judgments
go to a decision endpoint.