第 7 章:OpenAI 兼容 API 与远程 Provider —— 统一入口的另一面¶
7.1 为什么 Jan 自己还要做一层 proxy¶
Jan 既要让 Web UI 调云端模型,也要向其他客户端暴露本地 API。Rust core/server 提供一个 Hyper HTTP server,把本地 llama.cpp、macOS MLX 和远程 Provider 统一到 OpenAI 风格的入口。
flowchart LR
Client[OpenAI SDK / curl / Jan Web UI]
Proxy[Hyper proxy\ntrusted hosts + api key]
Local[llama.cpp router]
MLX[MLX session]
Remote[Anthropic / Google / OpenAI / custom]
Conv[converters.rs\nwire format translation]
Client --> Proxy
Proxy --> Local
Proxy --> MLX
Proxy --> Conv --> Remote
7.2 server command 的生命周期¶
start_server 接收 host、port、prefix、api_key、trusted_hosts、timeout 和是否允许 server-side tool execution,拿到 AppState 中的 provider configs、model defaults、MCP server map 后调用 proxy::start_server。
服务器 handle 存在 AppState.server_handle 中;stop_server 与 get_server_status 通过同一个共享 handle 管理。这样的生命周期集中在 Rust,避免 WebView 刷新后丢失 server 状态。
7.3 ProviderConfig 是远程模型的运行时注册表¶
ProviderConfig 保存 provider、API key chain、base URL、custom headers、models 和 api_type。Web 的 DataProvider 启动时从 keyring 重新 seed keys,再调用 register_provider_config 写入 Rust 的 AppState.provider_configs。
关键设计是 key chain:当上游返回 401/403/429 时可以尝试后备 key,而不是把一个临时配额错误暴露成“模型不可用”。密钥值本身走 OS keyring;配置和模型列表可以持久化,secret 不应该跟着 localStorage 导出。
7.4 converters:把“OpenAI 形状”翻译成上游协议¶
converters.rs 定义 UpstreamConverter、converter_for、SseAccumulator 等。默认 Provider 可以 verbatim passthrough;anthropic、google、openai-responses 等 api_type 选择对应转换器。
Jan request: OpenAI ChatCompletionRequest
├─ messages / tool calls / images
├─ sampling params
└─ stream=true
↓ converter
upstream native request
↓ SSE accumulator
OpenAI-shaped streamed response
流式翻译尤其难:Anthropic 的 content blocks、Google 的 candidates、OpenAI Responses 的 event taxonomy 都不一样;转换器必须在“上游事件尚未结束”时维护累积状态,再输出下游能理解的 delta。
7.5 安全检查不应被兼容性掩盖¶
proxy 在转发前使用 is_valid_host、CORS/origin 检查、prefix 移除和 API key 验证。trusted_hosts 控制允许的来源。开放本地端口不是天然安全:同机其他进程、浏览器网页和局域网设备都可能尝试访问,因此 host、origin、token、CORS 和 tool execution 必须一起看。
7.6 server-side tool execution 是一个危险开关¶
server 配置中的 enable_server_tool_execution 决定 proxy 是否可以在 Rust 侧把 MCP 工具加入远程请求链。默认关闭是合理的:如果一个外部客户端仅仅调用 /v1/chat/completions,不应该无意间获得本机文件、网络或 MCP 子进程能力。
开启后,工具权限从“Jan UI 的审批弹窗”扩展到“任何拿到 API key 的客户端”,这应当被视为更高风险模式。
7.7 参数清洗与错误翻译¶
前端 model-factory 会先过滤 provider 不支持的参数;Rust proxy/converter 还要承担第二道边界,因为外部客户端可以发送任意 JSON。错误翻译要做到:
- 保留 HTTP status 与上游可操作原因。
- 不回显 API key、完整 Authorization header 或内部文件路径。
- 对 llama.cpp Jinja trace 做清洗,把最终
Error: ...作为用户可读原因。 - 把 DNS/TLS/timeout 等 transport error 转成行动建议。
这条链路是“统一 API”带来的代价:对调用方隐藏差异,就必须由 Jan 负责解释差异。