# 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` 能一起工作的根本原因。