# LiteLLM LM Studio 适配器启动与请求处理流程
## 1. 文档目的
本文档说明独立适配器 [litellm_lmstudio_adapter.py](E:\opencode\Fix-HauHasuCS-ThinkingMode-Toggle\services\litellm-lmstudio-adapter\litellm_lmstudio_adapter.py) 从**启动**到**接收外部请求并处理返回**的完整工作流程。
这个适配器解决的是一个很具体的问题:
- 上游调用方希望使用 **OpenAI 兼容接口**
- `LM Studio` 同时提供了 **OpenAI 兼容接口** 和 **原生接口**
- `LM Studio` 原生 `/api/v1/chat` 支持真实可用的 `reasoning: "on" | "off"`
- 但 `LM Studio` 的 OpenAI 兼容 `/v1/chat/completions` 对某些模型不一定会真正按请求关闭或开启 thinking
所以这个适配器的思路是:
- **对外保持 OpenAI 兼容**
- **对内优先调用 LM Studio 原生能力**
---
## 2. 整体架构
```mermaid
flowchart LR
accTitle: 适配器整体请求路径
accDescr: 从 LiteLLM 或其他 OpenAI 兼容客户端发起请求,经适配器路由到 LM Studio 原生接口或兼容接口,再返回结果。
caller["客户端 / LiteLLM
发起 OpenAI 兼容请求"]
adapter["FastAPI 适配器
litellm_lmstudio_adapter.py"]
lm_openai["LM Studio 兼容接口
/v1/models /v1/embeddings /v1/completions"]
lm_native["LM Studio 原生接口
/api/v1/chat /api/v1/* /api/v0/*"]
response["返回 OpenAI 兼容响应
或原生透传响应"]
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
```
### 核心路由原则
- 只要请求涉及 **reasoning/thinking 控制**,优先走 `LM Studio /api/v1/chat`
- 可以直接复用的接口,直接代理到 `LM Studio /v1/*`
- 如果调用方明确请求 `LM Studio` 原生接口,就原样透传,不改 body
---
## 3. 独立目录中的文件结构
| 路径 | 作用 |
| --- | --- |
| [litellm_lmstudio_adapter.py](E:\opencode\Fix-HauHasuCS-ThinkingMode-Toggle\services\litellm-lmstudio-adapter\litellm_lmstudio_adapter.py) | 主程序,包含路由、请求翻译、流式桥接、原生透传 |
| [README.md](E:\opencode\Fix-HauHasuCS-ThinkingMode-Toggle\services\litellm-lmstudio-adapter\README.md) | 简要使用说明 |
| [tests/test_adapter.py](E:\opencode\Fix-HauHasuCS-ThinkingMode-Toggle\services\litellm-lmstudio-adapter\tests\test_adapter.py) | 单元测试,覆盖请求映射与流式事件翻译 |
---
## 4. 对外暴露的接口
### OpenAI 兼容接口
- `GET /v1/models`
- `POST /v1/chat/completions`
- `POST /v1/responses`
- `POST /v1/embeddings`
- `POST /v1/completions`
### LM Studio 原生直通接口
- `ANY /api/v1/*`
- `ANY /api/v0/*`
### 当前占位接口
- `/v1/audio/*`
- `/v1/images/*`
- `/v1/moderations`
- `/v1/files`
这些接口目前统一返回 OpenAI 风格的 `501 not_implemented`。
---
## 5. 启动流程
### 5.1 启动命令
常见启动方式:
```bash
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` | 适配器请求上游时使用的超时秒数 |
### 5.2 `main()` 启动时具体做了什么
`main()` 入口主要做四件事:
1. 解析命令行参数
2. 把运行时配置写入 `app.state`
3. 记录上游 `LM Studio` 地址
4. 启动 `uvicorn`
### 5.3 启动后的运行态
启动后,内存中最关键的两个状态是:
- `app.state.upstream_base`
- 例如 `http://127.0.0.1:7860`
- `app.state.request_timeout`
- 上游请求超时时间
这个服务不依赖数据库,也没有持久化状态。
---
## 6. 路由分流逻辑
```mermaid
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
```
---
## 7. 非流式 `chat/completions` 请求流程
适用场景:
- `POST /v1/chat/completions`
- `stream = false`
### 7.1 外部请求示例
```json
{
"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
}
```
### 7.2 请求进入哪个函数
入口函数:
- `chat_completions(request: Request)`
这个函数依次做:
1. 读取请求 JSON
2. 调用 `build_chat_native_payload(payload)` 组装原生请求
3. 请求 `LM Studio /api/v1/chat`
4. 调用 `translate_chat_response(native_response)` 转成 OpenAI 风格响应
### 7.3 OpenAI 请求如何翻译成 LM Studio 原生请求
核心函数:
- `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` |
### 7.4 reasoning 是怎么决定的
核心函数:
- `resolve_reasoning_mode(request_payload)`
优先级:
1. 如果 `reasoning` 存在且合法,优先使用
2. 否则如果 `enable_thinking` 存在:
- `true -> "on"`
- `false -> "off"`
3. 都没有时默认 `"on"`
### 7.5 messages 是怎么压平的
核心函数:
- `messages_to_native_input(messages)`
规则:
- `system` 消息不放进主输入,单独合并到 `system_prompt`
- `user` 消息变成 `User: ...`
- `assistant` 消息变成 `Assistant: ...`
- 其他 role 变成 `RoleName: ...`
例如:
```json
[
{"role": "system", "content": "be precise"},
{"role": "user", "content": "hello"},
{"role": "assistant", "content": "hi"},
{"role": "user", "content": "continue"}
]
```
会变成:
```text
system_prompt = "be precise"
input =
User: hello
Assistant: hi
User: continue
```
### 7.6 适配器发给 LM Studio 的原生请求长什么样
示意:
```json
{
"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
}
```
### 7.7 LM Studio 原生返回后怎么再变回 OpenAI 风格
核心函数:
- `translate_chat_response(native_response)`
它会读取:
- `output[type=message]`
- `output[type=reasoning]`
- `stats`
然后组装成:
- `choices[0].message.content`
- `choices[0].message.reasoning_content`
- OpenAI 风格 `usage`
### 7.8 本地实测结果
#### reasoning 关闭时
- `content = "9193"`
- `reasoning_content = null`
- `reasoning_tokens = 0`
#### reasoning 开启时
- `content = ""`
- `reasoning_content` 有思维内容
- `reasoning_tokens > 0`
---
## 8. 流式 `chat/completions` 请求流程
适用场景:
- `POST /v1/chat/completions`
- `stream = true`
### 8.1 为什么需要流式桥接
`LM Studio` 原生流式事件不是 OpenAI chunk 格式。
例如 `LM Studio` 原生会发:
```text
event: message.delta
data: {"type":"message.delta","content":"9"}
```
而上游 OpenAI 调用方期望的是:
```text
data: {"object":"chat.completion.chunk","choices":[{"delta":{"content":"9"}}]}
```
适配器要做的就是把前者实时翻译成后者。
### 8.2 整体流式路径
```mermaid
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]
```
### 8.3 参与流式处理的关键函数
#### `stream_lmstudio_events(native_payload, translator, model, final_frame)`
作用:
1. 以 SSE 方式请求 `LM Studio /api/v1/chat`
2. 持续读取返回的文本块
3. 用 `\n\n` 把 SSE block 重组出来
4. 调用 `parse_sse_event_blocks(...)` 解析
5. 把每个事件交给 translator 翻译
6. 把翻译后的 SSE frame 回推给上游
7. 最后补 stop chunk 和 `[DONE]`
#### `parse_sse_event_blocks(raw_text)`
作用:
- 从原始 SSE 文本中提取:
- `event: ...`
- `data: ...`
输出类似:
```json
{
"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.content`
- `reasoning.delta` -> `choices[0].delta.reasoning_content`
以下类型会被忽略:
- `chat.start`
- `prompt_processing.start`
- `prompt_processing.progress`
- `prompt_processing.end`
- `message.start`
- `message.end`
### 8.4 为什么最后会有 stop chunk 和 `[DONE]`
为了兼容 OpenAI 流式习惯,适配器在流结束时主动补两个东西:
1. 一个 final chunk,`finish_reason = "stop"`
2. 一行 `data: [DONE]`
这就是本地实测看到的结果:
```text
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]
```
---
## 9. `responses` 接口处理流程
适用场景:
- `POST /v1/responses`
### 9.1 输入归一化
入口函数:
- `responses_api(request: Request)`
它支持两种输入:
- `input` 是字符串
- `input` 是 message 列表
然后统一改造成内部 chat 风格载荷:
```json
{
"model": "...",
"messages": [...],
"reasoning": "...",
"stream": true|false,
"max_tokens": ...
}
```
### 9.2 非流式 responses
当 `stream = false`:
1. 适配器请求 `LM Studio /api/v1/chat`
2. 拿到原生聊天结果
3. 用 `build_responses_response(native_response)` 转成 Responses API 风格 JSON
### 9.3 流式 responses
当 `stream = true`:
适配器会主动输出:
1. `response.created`
2. 多个 `response.output_text.delta`
3. 如有 reasoning,再输出 `response.reasoning.delta`
4. `response.completed`
负责事件翻译的函数:
- `translate_responses_stream_event(...)`
本地实测到的输出形态:
```text
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", ...}
```
---
## 10. 简单代理接口的工作方式
下面这些接口不需要复杂翻译:
- `GET /v1/models`
- `POST /v1/embeddings`
- `POST /v1/completions`
它们统一通过:
- `proxy_request(...)`
来处理。
### `proxy_request(...)` 做了什么
1. 用 `httpx` 向上游发起请求
2. 保持原请求方法和 body
3. 原样保留响应内容
4. 去掉一些纯传输层 header,例如:
- `content-length`
- `transfer-encoding`
- `connection`
- `content-encoding`
这样能避免代理时 header 冲突。
---
## 11. 原生接口透传流程
如果外部直接请求:
- `/api/v1/...`
- `/api/v0/...`
适配器**不做翻译**。
对应入口:
- `native_v1_passthrough(...)`
- `native_v0_passthrough(...)`
它们会:
1. 读取原始 body 字节
2. 保留原请求方法
3. 保留 query string
4. 转发到 `LM Studio` 对应原生路径
5. 原样返回上游结果
所以这个适配器除了是 OpenAI 兼容层,也同时是一个 LM Studio 网关。
---
## 12. 占位接口流程
当前以下能力先做成占位:
- audio
- images
- files
- moderations
核心函数:
- `build_stub_error(feature_name)`
返回格式:
```json
{
"error": {
"message": "audio.transcriptions is not implemented by this adapter yet",
"type": "not_implemented",
"param": null,
"code": "not_implemented"
}
}
```
状态码:
- `501`
这样调用方至少能拿到结构化错误,而不是路由不存在。
---
## 13. 错误处理策略
### 13.1 本地参数校验错误
例如:
- `messages` 缺失
- 全部消息都没有可用文本
这些错误会在请求到达 `LM Studio` 之前就被适配器拦住。
### 13.2 上游 HTTP 错误
如果 `LM Studio` 返回 `>= 400`:
- 非流式路径直接返回上游状态和结果
- 流式路径会输出一个 SSE 错误 frame,随后输出 `[DONE]`
### 13.3 功能未实现
未实现接口统一返回:
- `501 not_implemented`
---
## 14. LiteLLM 是怎么接这个适配器的
推荐拓扑:
```mermaid
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:8010`
- `model` 仍然写你要调用的 `LM Studio` 模型名
这样:
- LiteLLM 负责统一调用层
- 适配器负责保留 `LM Studio` 原生 reasoning 控制能力
---
## 15. 本地已验证行为
已对 `qwen/qwen3.6-35b-a3b` 做过实测。
### 非流式 chat
- `reasoning: "off"` -> 直接输出最终答案,`reasoning_tokens = 0`
- `reasoning: "on"` -> 返回 reasoning 内容,`reasoning_tokens > 0`
### 流式 chat
- 能正确输出 SSE 内容 chunk
- 能输出 final stop chunk
- 能输出 `[DONE]`
### 流式 responses
- `response.created`
- `response.output_text.delta`
- `response.completed`
---
## 16. 总结
这个适配器的工作机制可以归纳成五步:
1. 接收 OpenAI 风格请求
2. 归一化并翻译成 `LM Studio` 原生请求
3. 调用上游 `LM Studio`
4. 把原生返回或原生 SSE 事件翻译回 OpenAI 风格
5. 返回给 LiteLLM 或其他调用方
它真正解决的核心不是“重做一套 OpenAI API”,而是:
- 对外维持统一协议
- 对内保住 `LM Studio` 原生 reasoning 能力
- 同时兼容原生接口直通与流式事件桥接
这就是 `LiteLLM + LM Studio native reasoning` 能一起工作的根本原因。