2026-06-09-启动与请求处理流程.md 17 KB

LiteLLM LM Studio 适配器启动与请求处理流程

1. 文档目的

本文档说明独立适配器 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. 整体架构

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

核心路由原则

  • 只要请求涉及 reasoning/thinking 控制,优先走 LM Studio /api/v1/chat
  • 可以直接复用的接口,直接代理到 LM Studio /v1/*
  • 如果调用方明确请求 LM Studio 原生接口,就原样透传,不改 body

3. 独立目录中的文件结构

路径 作用
litellm_lmstudio_adapter.py 主程序,包含路由、请求翻译、流式桥接、原生透传
README.md 简要使用说明
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 启动命令

常见启动方式:

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. 路由分流逻辑

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 外部请求示例

{
  "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: ...

例如:

[
  {"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

7.6 适配器发给 LM Studio 的原生请求长什么样

示意:

{
  "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 原生会发:

event: message.delta
data: {"type":"message.delta","content":"9"}

而上游 OpenAI 调用方期望的是:

data: {"object":"chat.completion.chunk","choices":[{"delta":{"content":"9"}}]}

适配器要做的就是把前者实时翻译成后者。

8.2 整体流式路径

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: ...

输出类似:

{
  "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]

这就是本地实测看到的结果:

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 风格载荷:

{
  "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(...)

本地实测到的输出形态:

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)

返回格式:

{
  "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 是怎么接这个适配器的

推荐拓扑:

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