llm-session-proxy
可自定义参数的 LLM 本地反向代理。自动补上客户端不会发的会话头,注入任意自定义参数, 重写模型名与请求路径,还能在 OpenAI、Anthropic 与 Responses 三种协议之间互转—— 然后把请求(含 SSE 流式响应)转发出去。
npx llm-session-proxy
解决什么问题
OpenCode Go 要求每个请求带会话标识,而多数客户端没法自定义请求头。
上游要求这样一行头:
x-opencode-session : 会话 ID,同一对话内保持稳定(用于提示词缓存与路由优化)
客户端不发,上游就直接拒绝:
400 Request is missing x-opencode-session and cannot be routed efficiently
本工具把这件事挪到本机完成:客户端只管把 Base URL 指向本地代理,剩下的事代理来办。
它不绑定任何一家上游——换掉 upstream 和 inject.headers
就能服务任意 OpenAI / Anthropic 兼容接口。
核心能力
每一项都可通过配置文件、环境变量或命令行参数调整。
自动生成会话 ID
三级策略:客户端显式标识 → 内容指纹(system + 首条用户消息)→ 一次性随机。同一对话稳定复用,提示词缓存才有效。
任意参数注入
请求头与请求体字段均可注入,值支持 {{session.id}}、{{uuid}}、{{env.HOME}} 等模板变量。
模型别名重写
前缀剥离(proxy-glm → glm-5.3)与精确映射双管齐下,两种写法可以叠加。OpenCode Go 的别名表已内置,客户端必须带的 proxy- 前缀真能解析出结果;别名剥完却对不上任何条目时,会按名字报出来,而不是静默转发。
按规则分桶路由
四个桶——default / background / think / longContext,各自可以换模型、挂请求体变换。规则按路径前缀、模型前缀、请求体字段、请求体大小匹配,自上而下、首个命中即止,同一条规则内条件是与。默认关闭,升级不会改变现有行为。
可组合的请求体变换
五个内置命名变换(drop-fields、drop-empty-fields、rename-fields、clamp-max-tokens、noop),可全局挂也可按桶挂,按固定顺序执行。注册表是写死的清单:代理绝不从某个路径加载代码。
协议互转
OpenAI Chat ↔ Anthropic Messages ↔ OpenAI Responses,请求体、JSON 响应与 SSE 逐事件转码全覆盖。只会说一种协议的客户端,也能调到说另一种协议的模型。按模型前缀路由,默认关闭。
请求路径重写
客户端只会填 /v1 时,一条正则把它转到上游真正要的路径。
SSE 零缓冲透传
逐块转发而不是攒完再发,流式输出体验不受影响;上游 4xx 错误体原样回传。
日志默认落盘
~/.lsp/logs/llm-session-proxy.log,支持按大小 / 按日期轮转与 30 天归档——进程消失也留得下线索。
启动前自检
--dry-run 在不走网络的前提下打印生效的路由、渲染后的注入表与模型解析链路;--doctor 再加 DNS/TCP/TLS 连通性与监听端口检查,有问题时退出码非零。
零依赖
只用 Node 内置模块,不引入任何第三方包,npx 启动无安装负担。
快速开始
要求 Node.js ≥ 18。
# 默认配置就是 OpenCode Go,直接启动
npx llm-session-proxy
# 客户端 Base URL 填:http://127.0.0.1:9355/zen/go/v1
# 模型 ID 填:proxy- + 真实模型名,例如 proxy-glm-5.3-flash
# API Key 填:你的 OpenCode API Key
proxy- 前缀不是可选项。
Trae 这类客户端按模型 ID 决定走不走自定义通道。填成内置预设名(如 glm-5.3-flash),
流量会被它自己的云通道接走,完全绕过本代理,然后照样报 400。
加前缀强制走代理后,代理会自动把前缀剥掉再发给上游。
npx llm-session-proxy \
--upstream https://api.deepseek.com \
--port 8788 \
--inject "x-session-id={{session.id}}" \
--inject "x-trace-id={{uuid}}" \
--model-map fast=deepseek-chat \
--model-map smart=deepseek-reasoner \
--model-prefix ""
# 客户端 Base URL 填 http://127.0.0.1:8788/v1
# 模型名可以用 fast / smart 这类短别名
# 生成带注释的示例配置
npx llm-session-proxy --init
# 按需修改后启动
npx llm-session-proxy -c llm-session-proxy.config.json
{
// 支持 // 与 /* */ 注释、尾随逗号
"listen": { "host": "127.0.0.1", "port": 9355 },
"upstream": { "host": "opencode.ai" },
"inject": {
"headers": {
"x-opencode-session": "{{session.id}}",
"x-opencode-request": "{{session.requestId}}"
}
},
"model": { "stripPrefixes": ["proxy-"] }
}
客户端 Base URL 怎么填
| 客户端只能填 | 启动参数 |
|---|---|
http://127.0.0.1:9355/zen/go/v1 | 不需要额外参数(原样透传) |
http://127.0.0.1:9355/v1 | --path-rewrite "^/v1/=>/zen/go/v1/" |
http://127.0.0.1:9355(不带路径) | --base-path /zen/go/v1 |
OpenCode Go 常见模型速查表
上游把模型分在三类端点上——能不能用取决于客户端发什么协议,这比模型名更关键。
下表每一行的客户端模型 ID 都能开箱解析:别名已内置在代理里,
想确认某个别名最终会变成什么,跑一次
npx llm-session-proxy --dry-run --model proxy-glm 即可。
① /zen/go/v1/chat/completions · OpenAI 兼容,绝大多数客户端走这条
| 模型 | 客户端模型 ID | 上游真实 ID |
|---|---|---|
| GLM-5.3 | proxy-glm | glm-5.3 |
| GLM-5.3-Flash | proxy-glm-flash | glm-5.3-flash |
| GLM-5.2 / 5.1 | proxy-glm-5.2 | glm-5.2 |
| Kimi K3 | proxy-kimi | kimi-k3 |
| Kimi K2.7 Code | proxy-kimi-code | kimi-k2.7-code |
| Kimi K2.6 | proxy-kimi-k2.6 | kimi-k2.6 |
| DeepSeek V4.1 Flash | proxy-deepseek | deepseek-flash |
| DeepSeek V4 Pro | proxy-deepseek-pro | deepseek-v4-pro |
| DeepSeek V4 Flash | proxy-deepseek-v4-flash | deepseek-v4-flash |
| DeepSeek V4 Flash Vision | proxy-deepseek-vision | deepseek-v4-flash-vision-exp |
| LongCat-2.0 | proxy-longcat | longcat-2.0 |
| MiMo-V2.5 / Pro | proxy-mimo | mimo-v2.5 |
| Hy3 / Hy4 preview | proxy-hy3 | hy3 |
② /zen/go/v1/responses · OpenAI Responses API,客户端需支持该协议
| 模型 | 客户端模型 ID | 上游真实 ID |
|---|---|---|
| Grok 4.6 | proxy-grok | grok-4.6 |
| GPT 5.6 Luna | proxy-gpt-luna | gpt-5.6-luna |
| Muse Spark 1.3 Contributor | proxy-muse-1.3 | muse-spark-1.3-contributor |
③ /zen/go/v1/messages · Anthropic 协议,客户端需能发 Anthropic 格式
| 模型 | 客户端模型 ID | 上游真实 ID |
|---|---|---|
| MiniMax M3 / M2.7 / M2.5 | proxy-minimax | minimax-m3 |
| Qwen3.8 Max | proxy-qwen-max | qwen3.8-max |
| Qwen3.8 Flash | proxy-qwen-flash | qwen3.8-flash |
| Qwen3.7 / 3.6 Plus | proxy-qwen-plus | qwen3.6-plus |
配置参考
优先级:默认值 < 配置文件 < 环境变量 < 命令行参数。
upstream —— 转发目标
| 字段 | 默认值 | 说明 |
|---|---|---|
protocol | https | https 或 http |
host | opencode.ai | 上游主机名,也可以直接写完整 URL |
port | null | null 表示按协议取默认端口 |
basePath | "" | 转发时统一加的前缀,如 /zen/go/v1 |
rewriteHost | true | 是否把 Host 头改写成上游主机 |
session —— 会话 ID
| 字段 | 默认值 | 说明 |
|---|---|---|
enabled | true | 是否启用会话 ID 注入 |
headerNames | x-opencode-session 等 | 按顺序从这些请求头读取客户端自带会话 ID |
bodyFields | session_id 等 | 按顺序从这些请求体字段读取(支持点路径) |
contentHash.fields | ["system", ...] | 参与内容指纹计算的字段 |
includeFirstUserMessage | true | 首条 user 消息是否参与指纹 |
idPrefix | ses_ | 生成 ID 的前缀 |
idFormat | hex26 | hex26 / hex / uuid / base36 / short |
requestIdFormat | msg_{{session.count}} | 请求号模板 |
maxSessions | 512 | 会话表上限,超出淘汰最旧的 |
ttlSeconds | 0 | 会话过期秒数,0 表示不过期 |
inject —— 参数注入
| 字段 | 默认值 | 说明 |
|---|---|---|
headers | 4 个 x-opencode-* | 要注入的请求头,值为模板字符串;设为 null 可跳过 |
body | {} | 合并进请求体的字段,支持点路径与模板 |
removeBodyFields | [] | 要从请求体删掉的字段路径 |
overwrite | true | false 表示不覆盖客户端已有的同名头 / 字段 |
model —— 模型名重写
| 字段 | 默认值 | 说明 |
|---|---|---|
stripPrefixes | ["proxy-"] | 需要剥离的前缀,按顺序匹配,命中一个即停 |
map | 27 条内置别名 | 别名 → 上游真实模型 ID。逐键合并到内置表之上:同名的键覆盖内置条目,其余保留。内置条目无法删除,要改就覆盖它 |
default | null | 兜底模型名。仅在「什么都没命中且没有剥掉任何前缀」时生效;它刻意不救那些剥完前缀却对不上映射的 proxy-xxx |
warnUnmapped | true | 别名剥前缀后既无映射也无 default 时告警,同一别名只报一次,并指明该补哪条配置 |
transformers —— 请求体变换(始终生效)
只能写名字:注册表是编译进代理里的固定清单,配置文件无法让它执行任意代码。
| 字段 | 默认值 | 说明 |
|---|---|---|
enabled | [] | 变换名列表,从左到右依次执行 |
options | {} | 每个变换各自的参数,按变换名索引 |
| 名称 | 参数 | 作用 |
|---|---|---|
noop | — | 什么都不做。适合用来确认注册表确实接上了 |
drop-fields | fields: ["a.b"] | 删除列出的点路径 |
drop-empty-fields | fields?: [...] | 删除值为 null、""、[]、{} 的字段。0 和 false 会保留。不给 fields 时逐个检查顶层键 |
rename-fields | map: {"from": "to"} | 重命名点路径,父对象不存在会自动创建。映射到自己身上的会被忽略 |
clamp-max-tokens | max: 4096 | 把 max_tokens / max_completion_tokens 压到不超过 max;只降不升 |
顺序会影响结果,因为每个变换都是原地改请求体:rename-fields 放在
drop-fields 前面,和反过来跑,结果不一样。名字不在上表里是启动即报错,
不会静默忽略。
router —— 分桶与规则
一个桶决定两件事:用哪个模型、挂哪些变换。enabled 默认 false,所以现有配置在你主动打开之前行为完全不变。
| 字段 | 默认值 | 说明 |
|---|---|---|
enabled | false | 总开关。--no-router 强制关闭 |
forced | null | 所有请求都走这个桶、忽略全部规则(--router <桶名>)。它的优先级同样高于 enabled: false——用户都点名了,再被配置挡掉只会让人困惑 |
defaultBucket | default | 没命中任何规则的请求落到这个桶 |
buckets | 内置四个,全为空 | { "model": <模型 ID 或 null>, "transformers": [<名字>] }。可以自己加桶名;没声明过的桶名退化成空桶 |
rules | [] | 自上而下求值,首个命中即止。一条规则必须有 bucket 外加至少一个匹配条件——一个条件都不给是配置错误:无法判定的规则和「命中一切」不是一回事 |
| 匹配条件 | 命中条件 |
|---|---|
path | 请求路径以该字符串开头。比对的是客户端发来的路径,早于 request.pathRewrite |
modelPrefix | 客户端原始模型名或重写后的模型名任一个以它开头,所以 proxy-think 和 glm-5.3 都能拿来写规则 |
bodyField | 该点路径存在且非空。再加 bodyFieldValue 则要求等于某个具体值 |
minBytes / maxBytes | 请求体字节数,两端都是闭区间。按字节、不按 token——估 token 就得带一个分词器,而字节数是你能实际调得动的数字 |
"router": {
"enabled": true,
"defaultBucket": "default",
"buckets": {
"think": { "model": "glm-5.3-think" },
"longContext": { "model": "glm-5.3-long" },
"background": { "model": "glm-5.3-flash", "transformers": ["clamp-max-tokens"] }
},
"rules": [
{ "bucket": "think", "path": "/zen/go/v1/messages" },
{ "bucket": "longContext", "minBytes": 60000 },
{ "bucket": "background", "modelPrefix": "proxy-haiku", "maxBytes": 4096 }
]
}
桶的 transformers 是追加到全局 transformers.enabled
后面的:桶只能加、不能取消全局的某个变换。桶的 model 在别名重写之后生效并
直接替换结果,所以这里应当写真实的上游模型 ID、而不是 proxy- 别名。引用
{{model}} 的请求头模板看到的是桶覆盖后的值,因为注入是最后一步。
protocol —— 协议互转
代理从请求路径推断客户端的协议(/chat/completions → chat、/messages → messages、/responses → responses),再由路由决定上游说哪套。两者不同时:请求体转发前整体转换,响应回来前转换回去——JSON 整包改写,SSE 按事件逐条转码。enabled 默认 false。
| 字段 | 默认值 | 说明 |
|---|---|---|
enabled | false | 总开关。--no-protocol 强制关闭 |
forced | null | 把所有请求强制转成该协议,忽略全部路由(--protocol <target>)。同样压过 enabled: false |
paths | 三条 /zen/go/v1/… 默认路径 | 每种目标协议对应的上游路径。paths.<target> 会整体替换转发路径,注意与 request.pathRewrite「修补客户端路径」的语义不同 |
routes | [] | { "model": <前缀,可选>, "target": <协议>, "path": <上游路径,可选> }。自上而下、首个命中即止;不带 model 的路由匹配所有请求 |
"protocol": {
"enabled": true,
"paths": {
"chat": "/zen/go/v1/chat/completions",
"messages": "/zen/go/v1/messages",
"responses": "/zen/go/v1/responses"
},
"routes": [
{ "model": "minimax", "target": "messages" },
{ "model": "gpt", "target": "responses" }
]
}
字段归一化是内建的:max_tokens ↔ max_completion_tokens ↔
max_output_tokens;reasoning_effort ↔ thinking.budget_tokens
(固定表 1024 / 8192 / 16384,反向按阈值反推);工具声明与工具调用在三种形状间重塑;
stop ↔ stop_sequences;chat 的 system 消息 ↔ Anthropic 的顶层
system ↔ Responses 的 instructions。没有等价物的字段逐个记名
丢进日志里的 dropped 列表。Anthropic 必填 max_tokens,chat 请求两个名字
都没带时补一个记入日志的 4096,而不是等上游 400。上游 4xx/5xx 错误体永不转换——
错误是上游协议的一部分,原样回传。
模板变量
| 变量 | 含义 |
|---|---|
{{session.id}} | 本次请求使用的会话 ID |
{{session.count}} | 该会话内的第几个请求(从 1 开始) |
{{session.requestId}} | 按 requestIdFormat 渲染出的请求号,如 msg_3 |
{{model}} / {{path}} / {{method}} | 重写后的模型名 / 原始路径 / 方法 |
{{header.x-foo}} / {{query.foo}} | 客户端请求头(小写)/ 查询参数 |
{{env.HOME}} | 环境变量 |
{{uuid}} / {{random}} / {{randomHex:16}} | 每次渲染都不同的随机值 |
{{timestamp}} / {{timestampMs}} | 秒 / 毫秒时间戳 |
命令行参考
所有选项都能与配置文件混用,命令行优先级最高。
| 参数 | 说明 |
|---|---|
-c, --config <file> | 读取配置文件(支持注释与尾随逗号) |
-p, --port <n> / --host <addr> | 监听端口 / 地址 |
-u, --upstream <url> | 上游地址,如 https://opencode.ai 或 host:port |
--base-path <path> | 转发路径前缀 |
--path-rewrite <a=>b> | 路径重写(正则),可重复 |
--inject <name=value> | 注入请求头,可重复 |
--body-inject <k=v> | 注入请求体字段,支持点路径,可重复 |
--model-prefix <prefix> | 要剥离的模型名前缀,可重复 |
--model-map <a=b> | 模型名精确映射,可重复;合并到内置别名表之上 |
--transformer <name> | 挂一个命名请求体变换,可重复。是整体替换 transformers.enabled 而不是追加(与 --model-prefix 替换 stripPrefixes 同理) |
--router <bucket> | 强制所有请求走指定桶,忽略全部规则 |
--no-router | 关闭路由分桶,即使配置文件里开着 |
--protocol <target> | 转发前把所有请求转成指定协议(chat / messages / responses),压过 protocol.routes |
--no-protocol | 关闭协议互转,即使配置文件里开着 |
--session-header <name> / --session-field <path> | 追加会话来源位置,可重复 |
--session-id-format <f> / --request-id-format <t> | 会话 ID 格式 / 请求号模板 |
--no-session / --no-stream | 关闭会话注入 / 关闭流式透传 |
--timeout <ms> / --max-body <bytes> | 上游超时 / 请求体上限 |
--log-level <l> | 日志级别:silent | error | warn | info | debug |
-l, --lang <en|zh> | 控制台与日志文案语言(也可用 PROXY_LANG 或配置项 lang) |
--log-file <f> / --no-log-file | 日志文件路径 / 关闭文件输出(只出控制台) |
--log-dir <dir> | 默认日志文件所在目录 |
--log-rotate <mode> | size | daily | off |
--log-keep-days <n> | 自动删除超过 N 天的历史日志(0 为永久保留) |
--init [file] | 生成示例配置 |
--print-config | 打印合并后的最终配置并退出 |
--dry-run | 校验配置并打印路由、分桶、注入与模型解析结果,不走任何网络 |
--doctor | 在 --dry-run 基础上追加 DNS/TCP/TLS 连通性与监听端口检查;有问题时退出码非零 |
--model <id> | --dry-run / --doctor 用于演示的样例模型名 |
日志
默认就会写日志文件,因为最难查的故障——未捕获异常、进程悄无声息地消失——恰恰是终端窗口一关就什么线索都不剩的那类。
默认位置是 $LSP_HOME/logs/llm-session-proxy.log;未设置 LSP_HOME 时是
~/.lsp/logs/llm-session-proxy.log。用 --log-file <path> /
--log-dir <dir> 覆盖,用 --no-log-file 关掉。
log.rotate | 行为 |
|---|---|
size (默认) | app.log 涨到 log.maxBytes 后变成 app.log.1、.2 …,最多保留 log.backups 份 |
daily | 每天一个文件:app-YYYY-MM-DD.log;log.maxBytes 依然限制单日文件大小 |
off | 不轮转也不截断,交给 logrotate 之类的外部工具 |
归档。启动时、以及运行期间最多每 6 小时一次,符合命名规则
(app.log、app.log.N、app-YYYY-MM-DD.log)且 mtime 早于
log.keepDays(默认 30 天)的文件会被删除。正在写的文件永不删除;不符合命名规则的文件——
包括同目录里其他任何文件——一律不动。
# 日志实际会落在哪儿?
llm-session-proxy --print-config | grep resolvedFile
启动前自检:--dry-run 与 --doctor
两个参数都会校验配置、把代理接下来实际会做的事打印出来,然后退出——不起服务、不写日志文件、 不向上游发任何请求。注入那张表打印的不是模板原文,而是渲染后的结果, 所以模板写错了会在这里暴露,而不是等上游回一个 400。
$ llm-session-proxy --dry-run
llm-session-proxy v0.2.2 —— 试运行
模型
样例 proxy-glm
剥离 前缀 "proxy-" -> glm
映射 glm -> glm-5.3
结果 glm-5.3 (命中映射)
映射表 内置 27 条,覆盖 0 条
路由分桶
启用 否
默认桶 default
桶 default (无)
桶 background (无)
桶 think (无)
桶 longContext (无)
规则 (无规则)
样例路由 default(路由已关闭)
变换
全局 (无)
生效 (无)
可用 noop, drop-fields, drop-empty-fields, rename-fields, clamp-max-tokens
协议互转
启用 否(原样透传)
路径 chat=/zen/go/v1/chat/completions,messages=/zen/go/v1/messages,responses=/zen/go/v1/responses
规则 (无规则)
样例转换 样例请求不发生转换
结果
通过 —— 配置有效。
结果 | 含义 |
|---|---|
(命中映射) | 别名命中了 model.map。这就是你要的结果 |
(来自 model.default) | 什么都没命中、也没剥掉前缀,于是套用了 default |
(剥了前缀却没有映射,原样发上游) | 前缀剥掉了,剩下的名字原样发给了上游——「模型不存在」报错的常见成因 |
(未匹配任何前缀,原样转发) | 客户端填的是真实模型 ID,正常情况 |
路由分桶与变换两段回答的是「这条请求会落进哪个桶、会被怎么改」。以上文 router 的配置为例:
路由分桶
启用 是
默认桶 default
桶 default (无)
桶 background model=glm-5.3-flash transformers=clamp-max-tokens
桶 think model=glm-5.3-think
桶 longContext model=glm-5.3-long
#0 path^=/zen/go/v1/messages -> think
#1 bytes>=60000 -> longContext
#2 model~=proxy-haiku* AND bytes<=4096 -> background
样例路由 default(默认桶,/v1/chat/completions 未命中任何规则)
变换
全局 drop-empty-fields
生效 drop-empty-fields
可用 noop, drop-fields, drop-empty-fields, rename-fields, clamp-max-tokens
#0/#1/#2 就是按顺序排列的规则,path^=
表示「路径以此开头」,model~= 表示「模型前缀」。样例路由 那行是真的拿一条
POST /v1/chat/completions 跑了一遍匹配,所以它落在兜底路径上:一条规则都没命中,
请求进默认桶。--router think 会覆盖这一切:样例路由 think(由 --router 强制)。
生效 才是真正会执行的那串——全局列表在前,命中的桶往里追加。
协议互转那段回答「这条请求会不会被转换、转成什么」。加了 --protocol messages
之后,强制目标与样例转换行会变成(样例转换同时给出转换后请求要走的上游路径):
chat -> messages /zen/go/v1/messages。英文输出里这一段叫 Protocol。
--doctor 在同一份报告之上追加 DNS 解析、TCP 连接与握手耗时、协议为 https
时的 TLS 握手(证书不受信任会如实报出,但不算硬失败)、监听端口是否空闲,以及一句「代理从不注入凭据」的提醒。
它不发 HTTP 请求、不带任何凭据——连通性只问到传输层为止——所以跑体检不会消耗你的速率配额。
一切正常退出码为 0,有问题为 1,可以直接当启动闸门用:
llm-session-proxy --doctor && llm-session-proxy
本地状态端点
# 运行概览:会话数、命中率、注入的头、上游地址
curl http://127.0.0.1:9355/__llm_session_proxy__/status
# 会话明细:每个会话的 ID、请求数、最后使用时间
curl http://127.0.0.1:9355/__llm_session_proxy__/sessions
排错时很有用:如果 sessions.active 一直是 0,说明请求根本没到代理——
多半是模型 ID 没加 proxy- 前缀,被客户端截走了。
工作原理
每个请求按固定顺序处理,会话识别的三级策略是关键。
03~06 的顺序是刻意排的:规则能同时看到客户端原始模型名和解析后的模型名;桶的模型覆盖落在
别名解析之后,所以它只可能是真实 ID;注入放在倒数第二步,请求头里的 {{model}}
反映的是最终决定;协议互转放在所有步骤之后——变换与注入针对的是客户端协议下的字段名,
等请求体定稿了才整体翻译。
会话识别的三级策略
显式标识——客户端自己发了 x-opencode-session,或请求体里有 session_id。直接复用,最准。
内容指纹——对 system + 首条 user 消息做 SHA-256。同一对话的后续轮次只是往后追加消息,
首条不变,所以指纹稳定,能落回同一个 ID。
一次性随机——两者都没有时(比如请求体不是 JSON),发一个随机 ID,只保证上游不报 400; 这类请求不进会话表,避免把表撑爆。
稳定性
单个畸形请求不会让进程退出。非法 Host 头、畸形请求行、上游返回含非法字符的状态行或响应头,都会被转成对应的 4xx / 5xx,进程继续服务。
未捕获异常会写进日志文件。兜底处理器把 uncaughtException 与 unhandledRejection 的完整栈写进日志文件。Node 默认只打 stderr 就终止进程,日志文件里一片空白——现场看起来就是「日志正常、进程凭空消失」。正因如此,文件日志默认就是开着的,详见日志一节。
限制与注意
只监听本机。代理会带上你的 API Key 转发请求,不要把监听地址改成 0.0.0.0。
response.stream: false 会破坏 SSE。除非确实需要整体缓冲,否则保持默认的 true。
bufferBody: false 时无法改写请求体。模型重写和参数注入会失效,只能注入请求头。
内置映射表是某个时间点的快照,不是实时目录。上游的模型 ID 会不作通知地变更,
而本代理刻意不通过网络去发现它们。某个别名哪天解析不出来了,那是配置里改一行的事,不是 bug——
发真实请求之前先用 --dry-run 看一眼每个别名最终变成了什么。
协议互转在边缘处刻意有损。Anthropic 的 thinking / signature
流增量在 chat 里没有标准位置,会丢弃(日志里记名);messages ↔ responses 经 chat
中转,不做直达转换器。保下来的是文本、工具调用、结束原因与 usage——客户端真正会用的就这些。
变换注册表依然是写死的清单:代理绝不会从某个路径加载变换。
路线图
零依赖、本地、单进程——每一项都必须符合这个形状。完整的取舍、与同类网关的对照,以及明确的非目标,见 ROADMAP.zh-CN.md。
| 里程碑 | 主题 | 要点 |
|---|---|---|
v0.2.0 ✓ | 正确性 | 内置精选模型映射表(27 条别名)、以 proxy- 开头的别名解析不出结果时的「同别名只报一次」告警,以及 --dry-run / --doctor 启动前自检。修掉的正是「别名被静默原样转发给上游」这个坑。 |
v0.2.1 ✓ | 路由 | 五个内置命名变换(noop、drop-fields、drop-empty-fields、rename-fields、clamp-max-tokens),以及路由分桶(default / background / think / longContext)——规则自上而下、首个命中即止,可匹配路径、模型前缀、请求体字段与请求体大小。默认关闭;互转器要挂靠的宿主。原本设想的「从路径加载插件」被有意砍掉了。 |
v0.2.2 ✓ | 协议覆盖 | Anthropic ↔ OpenAI ↔ Responses 互转与字段归一化,含流式——请求体、JSON 响应与 SSE 逐事件转码。从这个版本起,客户端用哪套协议不再决定它能用到哪些模型。 |
v0.3 | 可观测与可控 | Prometheus /metrics、JSON 日志模式、OTLP/JSON 导出、会话与提示词缓存检查器、token 与成本统计、免构建的本地看板。 |
v0.4 | 真实上游下的可靠性 | 多上游、带抖动重试的兜底、熔断、超时拆分与流空闲看门狗、退出时优雅排空。 |
v0.5 | 安全与卫生 | 日志脱敏、正则护栏、精确到行的配置 schema 报错、CI 用的 --check-config。 |
v1.0 | 分发与承诺 | 单文件可执行产物、按客户端的兼容性矩阵、基于 golden SSE fixture 的协议契约测试、配置迁移工具,以及从此开始的语义化版本。 |
v0.2 的性能目标由随仓库一起提供的工具测量:对本地 mock 上游的附加延迟 ≤ 1 ms(p50)与 ≤ 5 ms(p99)、100 并发连接下 ≥ 1,200 RPS、畸形输入语料下进程零退出、SSE 逐字节透传。 基准数字必须与测量口径一起发布——没有口径的基准说明不了任何问题。
刻意不放进路线图的:虚拟密钥与计费、本地跑模型、托管版本、插件市场、Kubernetes operator。它们都会和「零依赖、本地、单进程」这个形状相抵触。