本文档说明独立适配器 litellm_lmstudio_adapter.py 从启动到接收外部请求并处理返回的完整工作流程。
这个适配器解决的是一个很具体的问题:
LM Studio 同时提供了 OpenAI 兼容接口 和 原生接口LM Studio 原生 /api/v1/chat 支持真实可用的 reasoning: "on" | "off"LM Studio 的 OpenAI 兼容 /v1/chat/completions 对某些模型不一定会真正按请求关闭或开启 thinking所以这个适配器的思路是:
flowchart LR
accTitle: 适配器整体请求路径
accDescr: 从 LiteLLM 或其他 OpenAI 兼容客户端发起请求,经适配器路由到 LM Studio 原生接口或兼容接口,再返回结果。
caller["客户端 / LiteLLM<br/>发起 OpenAI 兼容请求"]
adapter["FastAPI 适配器<br/>litellm_lmstudio_adapter.py"]
lm_openai["LM Studio 兼容接口<br/>/v1/models /v1/embeddings /v1/completions"]
lm_native["LM Studio 原生接口<br/>/api/v1/chat /api/v1/* /api/v0/*"]
response["返回 OpenAI 兼容响应<br/>或原生透传响应"]
caller --> adapter
adapter --> lm_openai
adapter --> lm_native
lm_openai --> adapter
lm_native --> adapter
adapter --> response
classDef client fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a5f
classDef adapter_node fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#78350f
classDef backend fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#14532d
classDef out fill:#f3e8ff,stroke:#7e22ce,stroke-width:2px,color:#581c87
class caller client
class adapter adapter_node
class lm_openai,lm_native backend
class response out
LM Studio /api/v1/chatLM Studio /v1/*LM Studio 原生接口,就原样透传,不改 body| 路径 | 作用 |
|---|---|
| litellm_lmstudio_adapter.py | 主程序,包含路由、请求翻译、流式桥接、原生透传 |
| README.md | 简要使用说明 |
| tests/test_adapter.py | 单元测试,覆盖请求映射与流式事件翻译 |
GET /v1/modelsPOST /v1/chat/completionsPOST /v1/responsesPOST /v1/embeddingsPOST /v1/completionsANY /api/v1/*ANY /api/v0/*/v1/audio/*/v1/images/*/v1/moderations/v1/files这些接口目前统一返回 OpenAI 风格的 501 not_implemented。
常见启动方式:
python services\litellm-lmstudio-adapter\litellm_lmstudio_adapter.py --host 127.0.0.1 --port 8010 --upstream http://127.0.0.1:7860
参数含义:
| 参数 | 含义 |
|---|---|
--host |
适配器绑定地址 |
--port |
适配器监听端口 |
--upstream |
LM Studio 服务地址 |
--timeout |
适配器请求上游时使用的超时秒数 |
main() 启动时具体做了什么main() 入口主要做四件事:
app.stateLM Studio 地址uvicorn启动后,内存中最关键的两个状态是:
app.state.upstream_base
http://127.0.0.1:7860app.state.request_timeout
这个服务不依赖数据库,也没有持久化状态。
flowchart TD
accTitle: 路由分流决策图
accDescr: 适配器根据请求路径决定是做协议翻译、直接代理,还是返回占位错误。
start["收到 HTTP 请求"]
route_type["判断请求路径"]
chat["/v1/chat/completions"]
responses["/v1/responses"]
simple["/v1/models /v1/embeddings /v1/completions"]
native["/api/v1/* 或 /api/v0/*"]
stub["audio/images/files/moderations"]
translate["翻译为 LM Studio 原生请求"]
proxy["直接代理到上游"]
stub_resp["返回 501 not_implemented"]
openai_resp["返回 OpenAI 兼容结果"]
native_resp["返回原生透传结果"]
start --> route_type
route_type --> chat
route_type --> responses
route_type --> simple
route_type --> native
route_type --> stub
chat --> translate
responses --> translate
simple --> proxy
native --> proxy
stub --> stub_resp
translate --> openai_resp
proxy --> openai_resp
proxy --> native_resp
classDef main fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a5f
classDef transform fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#78350f
classDef backend fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#14532d
classDef error fill:#fee2e2,stroke:#dc2626,stroke-width:2px,color:#7f1d1d
class start,route_type,chat,responses,simple,native,stub main
class translate,proxy transform
class openai_resp,native_resp backend
class stub_resp error
chat/completions 请求流程适用场景:
POST /v1/chat/completionsstream = false{
"model": "qwen/qwen3.6-35b-a3b",
"messages": [
{
"role": "user",
"content": "Compute 317 * 29. Give the final answer only."
}
],
"reasoning": "off",
"max_tokens": 96,
"temperature": 0.2,
"stream": false
}
入口函数:
chat_completions(request: Request)这个函数依次做:
build_chat_native_payload(payload) 组装原生请求LM Studio /api/v1/chattranslate_chat_response(native_response) 转成 OpenAI 风格响应核心函数:
build_chat_native_payload(request_payload)字段映射如下:
| OpenAI 字段 | 处理方式 | LM Studio 原生字段 |
|---|---|---|
model |
直接复制 | model |
messages |
压平成 transcript | input |
system 消息 |
合并 | system_prompt |
reasoning |
规范化 | reasoning |
enable_thinking |
作为 reasoning 的后备来源 | reasoning |
max_tokens |
重命名 | max_output_tokens |
stream |
如为 true 则传递 |
stream |
核心函数:
resolve_reasoning_mode(request_payload)优先级:
reasoning 存在且合法,优先使用enable_thinking 存在:
true -> "on"false -> "off""on"核心函数:
messages_to_native_input(messages)规则:
system 消息不放进主输入,单独合并到 system_promptuser 消息变成 User: ...assistant 消息变成 Assistant: ...RoleName: ...例如:
[
{"role": "system", "content": "be precise"},
{"role": "user", "content": "hello"},
{"role": "assistant", "content": "hi"},
{"role": "user", "content": "continue"}
]
会变成:
system_prompt = "be precise"
input =
User: hello
Assistant: hi
User: continue
示意:
{
"model": "qwen/qwen3.6-35b-a3b",
"input": "User: Compute 317 * 29. Give the final answer only.",
"reasoning": "off",
"store": false,
"max_output_tokens": 96,
"temperature": 0.2
}
核心函数:
translate_chat_response(native_response)它会读取:
output[type=message]output[type=reasoning]stats然后组装成:
choices[0].message.contentchoices[0].message.reasoning_contentusagecontent = "9193"reasoning_content = nullreasoning_tokens = 0content = ""reasoning_content 有思维内容reasoning_tokens > 0chat/completions 请求流程适用场景:
POST /v1/chat/completionsstream = trueLM Studio 原生流式事件不是 OpenAI chunk 格式。
例如 LM Studio 原生会发:
event: message.delta
data: {"type":"message.delta","content":"9"}
而上游 OpenAI 调用方期望的是:
data: {"object":"chat.completion.chunk","choices":[{"delta":{"content":"9"}}]}
适配器要做的就是把前者实时翻译成后者。
sequenceDiagram
autonumber
participant C as 客户端 / LiteLLM
participant A as 适配器
participant L as LM Studio 原生 /api/v1/chat
C->>A: POST /v1/chat/completions {stream:true}
A->>A: build_chat_native_payload()
A->>L: POST /api/v1/chat {stream:true}
L-->>A: SSE 事件流
A->>A: parse_sse_event_blocks()
A->>A: translate_chat_stream_event()
A-->>C: OpenAI chat.completion.chunk SSE
A-->>C: stop chunk
A-->>C: data: [DONE]
stream_lmstudio_events(native_payload, translator, model, final_frame)作用:
LM Studio /api/v1/chat\n\n 把 SSE block 重组出来parse_sse_event_blocks(...) 解析[DONE]parse_sse_event_blocks(raw_text)作用:
event: ...data: ...输出类似:
{
"event": "message.delta",
"data": {"type":"message.delta","content":"9"},
"data_raw": "{\"type\":\"message.delta\",\"content\":\"9\"}"
}
translate_chat_stream_event(event, model, chunk_id)作用:
message.delta -> choices[0].delta.contentreasoning.delta -> choices[0].delta.reasoning_content以下类型会被忽略:
chat.startprompt_processing.startprompt_processing.progressprompt_processing.endmessage.startmessage.end[DONE]为了兼容 OpenAI 流式习惯,适配器在流结束时主动补两个东西:
finish_reason = "stop"data: [DONE]这就是本地实测看到的结果:
data: {"id":"chatcmpl-...","choices":[{"delta":{"content":"9"},"finish_reason":null}]}
data: {"id":"chatcmpl-...","choices":[{"delta":{"content":"1"},"finish_reason":null}]}
data: {"id":"chatcmpl-...","choices":[{"delta":{"content":"9"},"finish_reason":null}]}
data: {"id":"chatcmpl-...","choices":[{"delta":{"content":"3"},"finish_reason":null}]}
data: {"id":"chatcmpl-...","choices":[{"delta":{},"finish_reason":"stop"}]}
data: [DONE]
responses 接口处理流程适用场景:
POST /v1/responses入口函数:
responses_api(request: Request)它支持两种输入:
input 是字符串input 是 message 列表然后统一改造成内部 chat 风格载荷:
{
"model": "...",
"messages": [...],
"reasoning": "...",
"stream": true|false,
"max_tokens": ...
}
当 stream = false:
LM Studio /api/v1/chatbuild_responses_response(native_response) 转成 Responses API 风格 JSON当 stream = true:
适配器会主动输出:
response.createdresponse.output_text.deltaresponse.reasoning.deltaresponse.completed负责事件翻译的函数:
translate_responses_stream_event(...)本地实测到的输出形态:
data: {"type":"response.created", ...}
data: {"type":"response.output_text.delta","delta":"9"}
data: {"type":"response.output_text.delta","delta":"1"}
data: {"type":"response.output_text.delta","delta":"9"}
data: {"type":"response.output_text.delta","delta":"3"}
data: {"type":"response.completed", ...}
下面这些接口不需要复杂翻译:
GET /v1/modelsPOST /v1/embeddingsPOST /v1/completions它们统一通过:
proxy_request(...)来处理。
proxy_request(...) 做了什么httpx 向上游发起请求content-lengthtransfer-encodingconnectioncontent-encoding这样能避免代理时 header 冲突。
如果外部直接请求:
/api/v1/.../api/v0/...适配器不做翻译。
对应入口:
native_v1_passthrough(...)native_v0_passthrough(...)它们会:
LM Studio 对应原生路径所以这个适配器除了是 OpenAI 兼容层,也同时是一个 LM Studio 网关。
当前以下能力先做成占位:
核心函数:
build_stub_error(feature_name)返回格式:
{
"error": {
"message": "audio.transcriptions is not implemented by this adapter yet",
"type": "not_implemented",
"param": null,
"code": "not_implemented"
}
}
状态码:
501这样调用方至少能拿到结构化错误,而不是路由不存在。
例如:
messages 缺失这些错误会在请求到达 LM Studio 之前就被适配器拦住。
如果 LM Studio 返回 >= 400:
[DONE]未实现接口统一返回:
501 not_implemented推荐拓扑:
flowchart LR
accTitle: LiteLLM 集成拓扑
accDescr: LiteLLM 把适配器当成 OpenAI 兼容后端,而适配器再调用 LM Studio 原生能力。
app["你的应用"]
litellm["LiteLLM Proxy 或 SDK"]
adapter["适配器 :8010"]
lmstudio["LM Studio :7860"]
app --> litellm
litellm --> adapter
adapter --> lmstudio
classDef a fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a5f
classDef b fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#78350f
classDef c fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#14532d
class app,litellm a
class adapter b
class lmstudio c
对 LiteLLM 来说,它只需要把这个适配器当成普通 OpenAI-compatible backend:
api_base = http://127.0.0.1:8010model 仍然写你要调用的 LM Studio 模型名这样:
LM Studio 原生 reasoning 控制能力已对 qwen/qwen3.6-35b-a3b 做过实测。
reasoning: "off" -> 直接输出最终答案,reasoning_tokens = 0reasoning: "on" -> 返回 reasoning 内容,reasoning_tokens > 0[DONE]response.createdresponse.output_text.deltaresponse.completed这个适配器的工作机制可以归纳成五步:
LM Studio 原生请求LM Studio它真正解决的核心不是“重做一套 OpenAI API”,而是:
LM Studio 原生 reasoning 能力这就是 LiteLLM + LM Studio native reasoning 能一起工作的根本原因。