零依赖 · Node ≥ 18 · Apache-2.0

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 兼容接口。

核心能力

每一项都可通过配置文件、环境变量或命令行参数调整。

session

自动生成会话 ID

三级策略:客户端显式标识 → 内容指纹(system + 首条用户消息)→ 一次性随机。同一对话稳定复用,提示词缓存才有效。

inject

任意参数注入

请求头与请求体字段均可注入,值支持 {{session.id}}、{{uuid}}、{{env.HOME}} 等模板变量。

model

模型别名重写

前缀剥离(proxy-glm → glm-5.3)与精确映射双管齐下,两种写法可以叠加。OpenCode Go 的别名表已内置,客户端必须带的 proxy- 前缀真能解析出结果;别名剥完却对不上任何条目时,会按名字报出来,而不是静默转发。

router

按规则分桶路由

四个桶——default / background / think / longContext,各自可以换模型、挂请求体变换。规则按路径前缀、模型前缀、请求体字段、请求体大小匹配,自上而下、首个命中即止,同一条规则内条件是与。默认关闭,升级不会改变现有行为。

transform

可组合的请求体变换

五个内置命名变换(drop-fields、drop-empty-fields、rename-fields、clamp-max-tokens、noop),可全局挂也可按桶挂,按固定顺序执行。注册表是写死的清单:代理绝不从某个路径加载代码。

protocol

协议互转

OpenAI Chat ↔ Anthropic Messages ↔ OpenAI Responses,请求体、JSON 响应与 SSE 逐事件转码全覆盖。只会说一种协议的客户端,也能调到说另一种协议的模型。按模型前缀路由,默认关闭。

path

请求路径重写

客户端只会填 /v1 时,一条正则把它转到上游真正要的路径。

stream

SSE 零缓冲透传

逐块转发而不是攒完再发,流式输出体验不受影响;上游 4xx 错误体原样回传。

ops

日志默认落盘

~/.lsp/logs/llm-session-proxy.log,支持按大小 / 按日期轮转与 30 天归档——进程消失也留得下线索。

check

启动前自检

--dry-run 在不走网络的前提下打印生效的路由、渲染后的注入表与模型解析链路;--doctor 再加 DNS/TCP/TLS 连通性与监听端口检查,有问题时退出码非零。

deps

零依赖

只用 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.3proxy-glmglm-5.3
GLM-5.3-Flashproxy-glm-flashglm-5.3-flash
GLM-5.2 / 5.1proxy-glm-5.2glm-5.2
Kimi K3proxy-kimikimi-k3
Kimi K2.7 Codeproxy-kimi-codekimi-k2.7-code
Kimi K2.6proxy-kimi-k2.6kimi-k2.6
DeepSeek V4.1 Flashproxy-deepseekdeepseek-flash
DeepSeek V4 Proproxy-deepseek-prodeepseek-v4-pro
DeepSeek V4 Flashproxy-deepseek-v4-flashdeepseek-v4-flash
DeepSeek V4 Flash Visionproxy-deepseek-visiondeepseek-v4-flash-vision-exp
LongCat-2.0proxy-longcatlongcat-2.0
MiMo-V2.5 / Proproxy-mimomimo-v2.5
Hy3 / Hy4 previewproxy-hy3hy3

② /zen/go/v1/responses · OpenAI Responses API,客户端需支持该协议

模型客户端模型 ID上游真实 ID
Grok 4.6proxy-grokgrok-4.6
GPT 5.6 Lunaproxy-gpt-lunagpt-5.6-luna
Muse Spark 1.3 Contributorproxy-muse-1.3muse-spark-1.3-contributor

③ /zen/go/v1/messages · Anthropic 协议,客户端需能发 Anthropic 格式

模型客户端模型 ID上游真实 ID
MiniMax M3 / M2.7 / M2.5proxy-minimaxminimax-m3
Qwen3.8 Maxproxy-qwen-maxqwen3.8-max
Qwen3.8 Flashproxy-qwen-flashqwen3.8-flash
Qwen3.7 / 3.6 Plusproxy-qwen-plusqwen3.6-plus
本代理不做协议转换。 客户端发 OpenAI 格式的请求体时,②③ 两类模型用不了——它只负责补头、改名、换路径, 不会把 Chat Completions 的 body 翻译成 Messages 的 body。 直接写上游真实 ID(不加别名、不加前缀)也能用,代理会原样放行。 模型清单随时可能变,以上游文档为准。

配置参考

优先级:默认值 < 配置文件 < 环境变量 < 命令行参数。

upstream —— 转发目标

字段默认值说明
protocolhttpshttps 或 http
hostopencode.ai上游主机名,也可以直接写完整 URL
portnullnull 表示按协议取默认端口
basePath""转发时统一加的前缀,如 /zen/go/v1
rewriteHosttrue是否把 Host 头改写成上游主机

session —— 会话 ID

字段默认值说明
enabledtrue是否启用会话 ID 注入
headerNamesx-opencode-session 等按顺序从这些请求头读取客户端自带会话 ID
bodyFieldssession_id 等按顺序从这些请求体字段读取(支持点路径)
contentHash.fields["system", ...]参与内容指纹计算的字段
includeFirstUserMessagetrue首条 user 消息是否参与指纹
idPrefixses_生成 ID 的前缀
idFormathex26hex26 / hex / uuid / base36 / short
requestIdFormatmsg_{{session.count}}请求号模板
maxSessions512会话表上限,超出淘汰最旧的
ttlSeconds0会话过期秒数,0 表示不过期

inject —— 参数注入

字段默认值说明
headers4 个 x-opencode-*要注入的请求头,值为模板字符串;设为 null 可跳过
body{}合并进请求体的字段,支持点路径与模板
removeBodyFields[]要从请求体删掉的字段路径
overwritetruefalse 表示不覆盖客户端已有的同名头 / 字段

model —— 模型名重写

字段默认值说明
stripPrefixes["proxy-"]需要剥离的前缀,按顺序匹配,命中一个即停
map27 条内置别名别名 → 上游真实模型 ID。逐键合并到内置表之上:同名的键覆盖内置条目,其余保留。内置条目无法删除,要改就覆盖它
defaultnull兜底模型名。仅在「什么都没命中且没有剥掉任何前缀」时生效;它刻意不救那些剥完前缀却对不上映射的 proxy-xxx
warnUnmappedtrue别名剥前缀后既无映射也无 default 时告警,同一别名只报一次,并指明该补哪条配置

transformers —— 请求体变换(始终生效)

只能写名字:注册表是编译进代理里的固定清单,配置文件无法让它执行任意代码。

字段默认值说明
enabled[]变换名列表,从左到右依次执行
options{}每个变换各自的参数,按变换名索引
名称参数作用
noop—什么都不做。适合用来确认注册表确实接上了
drop-fieldsfields: ["a.b"]删除列出的点路径
drop-empty-fieldsfields?: [...]删除值为 null、""、[]、{} 的字段。0 和 false 会保留。不给 fields 时逐个检查顶层键
rename-fieldsmap: {"from": "to"}重命名点路径,父对象不存在会自动创建。映射到自己身上的会被忽略
clamp-max-tokensmax: 4096把 max_tokens / max_completion_tokens 压到不超过 max;只降不升

顺序会影响结果,因为每个变换都是原地改请求体:rename-fields 放在 drop-fields 前面,和反过来跑,结果不一样。名字不在上表里是启动即报错, 不会静默忽略。

router —— 分桶与规则

一个桶决定两件事:用哪个模型、挂哪些变换。enabled 默认 false,所以现有配置在你主动打开之前行为完全不变。

字段默认值说明
enabledfalse总开关。--no-router 强制关闭
forcednull所有请求都走这个桶、忽略全部规则(--router <桶名>)。它的优先级同样高于 enabled: false——用户都点名了,再被配置挡掉只会让人困惑
defaultBucketdefault没命中任何规则的请求落到这个桶
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。

字段默认值说明
enabledfalse总开关。--no-protocol 强制关闭
forcednull把所有请求强制转成该协议,忽略全部路由(--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- 前缀,被客户端截走了。

工作原理

每个请求按固定顺序处理,会话识别的三级策略是关键。

01 读体读入请求体并解析 JSON
→
02 会话显式标识 > 内容指纹 > 随机
→
03 重写模型名:先剥前缀,再查映射
→
04 分桶桶的模型覆盖 + 请求体变换
→
05 注入请求头与请求体参数
→
06 互转翻译成上游的协议
→
07 转发SSE 逐块回传

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。它们都会和「零依赖、本地、单进程」这个形状相抵触。