llm-session-proxy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,471 @@
1
+ # llm-session-proxy
2
+
3
+ [![npm version](https://img.shields.io/npm/v/llm-session-proxy.svg)](https://www.npmjs.com/package/llm-session-proxy)
4
+ [![Publish to npm](https://github.com/alaahong/llm-session-proxy/actions/workflows/publish.yml/badge.svg)](https://github.com/alaahong/llm-session-proxy/actions/workflows/publish.yml)
5
+ [![Docs](https://img.shields.io/badge/docs-GitHub%20Pages-1f6feb)](https://www.ianzhang.cn/llm-session-proxy/)
6
+
7
+ *中文 · [English](README.en.md) · [在线文档](https://www.ianzhang.cn/llm-session-proxy/)*
8
+
9
+ **可自定义参数的 LLM 本地反向代理。** 零依赖,`npx` 即可运行。
10
+
11
+ 它坐在你的客户端和上游 API 之间,自动补上客户端不会发的会话头、注入任意自定义参数、
12
+ 重写模型名和请求路径,然后把请求(含 SSE 流式响应)原样转发出去。
13
+
14
+ 最初是为 [OpenCode Go](https://opencode.ai/docs/go/) 写的,但设计上不绑定任何一家上游——
15
+ 换掉 `upstream` 和 `inject.headers` 就能服务任意 OpenAI / Anthropic 兼容接口。
16
+
17
+ ---
18
+
19
+ ## 解决什么问题
20
+
21
+ OpenCode Go / Zen API 要求每个请求带上会话标识:
22
+
23
+ ```
24
+ x-opencode-session : 会话 ID,同一对话内保持稳定(用于提示词缓存与路由优化)
25
+ ```
26
+
27
+ 客户端不带这个头时,上游直接返回:
28
+
29
+ ```
30
+ 400 Request is missing x-opencode-session and cannot be routed efficiently
31
+ ```
32
+
33
+ 而大多数客户端(Trae、部分版本的 Codex、各类自建 Agent)没法自定义请求头。
34
+ 本工具把这件事挪到本机完成:客户端只管把 Base URL 指向本地代理,剩下的事代理来办。
35
+
36
+ ### 相关客户端的原生支持情况
37
+
38
+ 如果你用的客户端**已经**原生发送会话头,就不需要本工具(但用它也无害):
39
+
40
+ | 客户端 | 现状 | 是否需要本代理 |
41
+ | --- | --- | --- |
42
+ | OpenCode | 原生支持 | 不需要 |
43
+ | Claude Code | 上游识别其原生会话头 | 不需要 |
44
+ | Codex | 识别原生会话头,但部分版本/代理链路会丢 | 视情况,丢了就用 |
45
+ | ZCode / Pi / jcode / Kilo Code CLI | 新版本已补上 | 升级即可,旧版本可用本代理 |
46
+ | Trae / 自建 Agent / 各类 GUI 客户端 | 无法自定义请求头 | **需要** |
47
+
48
+ ---
49
+
50
+ ## 特性
51
+
52
+ - **自动生成会话 ID**——三级策略:客户端显式会话标识 → 内容指纹(system + 首条用户消息)→ 一次性随机。同一对话稳定复用,提示词缓存才有效。
53
+ - **任意请求头注入**——值支持模板(`{{session.id}}`、`{{uuid}}`、`{{env.HOME}}`…),想注什么注什么。
54
+ - **模型别名重写**——前缀剥离(`proxy-glm-5.3-flash` → `glm-5.3-flash`)与精确映射(`fast` → `deepseek-chat`)双管齐下。
55
+ - **请求路径重写**——客户端只会填 `/v1` 时,用一条正则把它转到上游真正要的路径。
56
+ - **请求体参数注入 / 删除**——统一给所有请求补 `temperature`、`metadata`,或删掉上游不认的字段。
57
+ - **SSE 流式零缓冲透传**——逐块转发,不攒完再发,流式体验不受影响。
58
+ - **零依赖**——只用 Node 内置模块,不引入任何第三方包,`npx` 启动无安装负担。
59
+ - **本地状态端点**——随时查看会话数、命中率、注入配置和会话明细。
60
+
61
+ ---
62
+
63
+ ## 安装与运行
64
+
65
+ ```bash
66
+ # 免安装直接跑(推荐先这样试)
67
+ npx llm-session-proxy
68
+
69
+ # 或全局安装
70
+ npm install -g llm-session-proxy
71
+ llm-session-proxy
72
+ ```
73
+
74
+ 要求 Node.js >= 18。
75
+
76
+ ---
77
+
78
+ ## 快速开始
79
+
80
+ ### 场景一:让 Trae 用上 OpenCode Go
81
+
82
+ 先确认你已订阅 OpenCode Go 并拿到 API Key([opencode.ai/auth](https://opencode.ai/auth))。
83
+
84
+ ```bash
85
+ npx llm-session-proxy
86
+ ```
87
+
88
+ 然后在这个客户端里新建自定义模型:
89
+
90
+ | 配置项 | 填写内容 |
91
+ | --- | --- |
92
+ | 请求地址 / Base URL | `http://127.0.0.1:9355/zen/go/v1` |
93
+ | 模型 ID | `proxy-` + 真实模型名,如 `proxy-glm-5.3-flash` |
94
+ | API Key | 你的 OpenCode API Key |
95
+
96
+ > **`proxy-` 前缀不是可选项。** Trae 按模型 ID 决定走不走自定义通道,如果填成内置预设名
97
+ > (如 `glm-5.3-flash`),流量会被 Trae 自己的云通道接走,**完全绕过本代理**,然后照样报 400。
98
+ > 加前缀强制走代理后,代理会自动把前缀剥掉再发给上游。
99
+
100
+ 可用模型名见[上游文档](https://opencode.ai/docs/go/)的 Endpoints 表,例如 `glm-5.3-flash`、
101
+ `kimi-k3`、`deepseek-flash`、`qwen3.7-max`、`minimax-m3`。
102
+
103
+ 如果你的客户端只允许把 Base URL 填成 `http://127.0.0.1:9355/v1`,加一条路径重写:
104
+
105
+ ```bash
106
+ npx llm-session-proxy --path-rewrite "^/v1/=>/zen/go/v1/"
107
+ ```
108
+
109
+ ### OpenCode Go 常见模型速查表
110
+
111
+ 完整可直接用的配置见 [`examples/opencode-go-models.json`](examples/opencode-go-models.json)——
112
+ 它预置了下面所有别名的 `model.map`,启动即可用。
113
+
114
+ 上游把模型分在三类端点上,**能不能用取决于你的客户端发什么协议**,这点比模型名更关键:
115
+
116
+ **① `/zen/go/v1/chat/completions`——OpenAI 兼容,绝大多数客户端走这条**
117
+
118
+ | 模型 | 客户端模型 ID | 上游真实 ID |
119
+ | --- | --- | --- |
120
+ | GLM-5.3 | `proxy-glm` / `proxy-glm-5.3` | `glm-5.3` |
121
+ | GLM-5.3-Flash | `proxy-glm-flash` | `glm-5.3-flash` |
122
+ | GLM-5.2 | `proxy-glm-5.2` | `glm-5.2` |
123
+ | GLM-5.1 | `proxy-glm-5.1` | `glm-5.1` |
124
+ | Kimi K3 | `proxy-kimi` | `kimi-k3` |
125
+ | Kimi K2.7 Code | `proxy-kimi-code` | `kimi-k2.7-code` |
126
+ | Kimi K2.6 | `proxy-kimi-k2.6` | `kimi-k2.6` |
127
+ | DeepSeek V4.1 Flash | `proxy-deepseek` | `deepseek-flash` |
128
+ | DeepSeek V4 Pro | `proxy-deepseek-pro` | `deepseek-v4-pro` |
129
+ | DeepSeek V4 Flash | `proxy-deepseek-v4-flash` | `deepseek-v4-flash` |
130
+ | DeepSeek V4 Flash Vision Exp | `proxy-deepseek-vision` | `deepseek-v4-flash-vision-exp` |
131
+ | LongCat-2.0 | `proxy-longcat` | `longcat-2.0` |
132
+ | MiMo-V2.5 | `proxy-mimo` | `mimo-v2.5` |
133
+ | MiMo-V2.5-Pro | `proxy-mimo-pro` | `mimo-v2.5-pro` |
134
+ | Hy3 | `proxy-hy3` | `hy3` |
135
+ | Hy4 preview | `proxy-hy4` | `hy4-preview` |
136
+
137
+ **② `/zen/go/v1/responses`——OpenAI Responses API,客户端必须支持该协议**
138
+
139
+ | 模型 | 客户端模型 ID | 上游真实 ID |
140
+ | --- | --- | --- |
141
+ | Grok 4.6 | `proxy-grok` | `grok-4.6` |
142
+ | GPT 5.6 Luna | `proxy-gpt-luna` | `gpt-5.6-luna` |
143
+ | Muse Spark 1.3 Contributor | `proxy-muse-1.3` | `muse-spark-1.3-contributor` |
144
+ | Muse Spark 1.2 Contributor | `proxy-muse-1.2` | `muse-spark-1.2-contributor` |
145
+
146
+ **③ `/zen/go/v1/messages`——Anthropic Messages API,客户端必须能发 Anthropic 格式**
147
+
148
+ | 模型 | 客户端模型 ID | 上游真实 ID |
149
+ | --- | --- | --- |
150
+ | MiniMax M3 | `proxy-minimax` | `minimax-m3` |
151
+ | MiniMax M2.7 | `proxy-minimax-2.7` | `minimax-m2.7` |
152
+ | MiniMax M2.5 | `proxy-minimax-2.5` | `minimax-m2.5` |
153
+ | Qwen3.8 Max | `proxy-qwen-max` | `qwen3.8-max` |
154
+ | Qwen3.8 Flash | `proxy-qwen-flash` | `qwen3.8-flash` |
155
+ | Qwen3.7 Max | `proxy-qwen3.7-max` | `qwen3.7-max` |
156
+ | Qwen3.7 Plus | `proxy-qwen3.7-plus` | `qwen3.7-plus` |
157
+ | Qwen3.6 Plus | `proxy-qwen-plus` | `qwen3.6-plus` |
158
+
159
+ > 本代理**不做协议转换**。客户端发 OpenAI 格式的请求体时,②③ 两类模型用不了——
160
+ > 它只负责补头、改名、换路径,不会把 Chat Completions 的 body 翻译成 Messages 的 body。
161
+ >
162
+ > 直接写上游真实 ID(不加别名、不加前缀)也能用,代理会原样放行。
163
+ > 模型清单随时可能变,以[上游文档](https://opencode.ai/docs/go/)和 `https://opencode.ai/zen/go/v1/models` 为准。
164
+
165
+ ### 场景二:接任意 OpenAI 兼容上游
166
+
167
+ ```bash
168
+ npx llm-session-proxy \
169
+ --upstream https://api.deepseek.com \
170
+ --port 8788 \
171
+ --inject "x-session-id={{session.id}}" \
172
+ --inject "x-trace-id={{uuid}}" \
173
+ --inject "x-client-version=2026-01-01" \
174
+ --model-map fast=deepseek-chat \
175
+ --model-map smart=deepseek-reasoner \
176
+ --model-prefix ""
177
+ ```
178
+
179
+ 客户端 Base URL 填 `http://127.0.0.1:8788/v1`,模型名可以用 `fast` / `smart` 这种短别名,
180
+ 代理会翻译成真实模型名。完整版本见 [`examples/generic-openai.json`](examples/generic-openai.json)。
181
+
182
+ ### 场景三:用配置文件(推荐长期使用)
183
+
184
+ ```bash
185
+ npx llm-session-proxy --init # 生成带注释的示例配置
186
+ npx llm-session-proxy -c llm-session-proxy.config.json
187
+ ```
188
+
189
+ 配置文件是有注释的 JSON(支持 `//`、`/* */` 和尾随逗号),可以按需增删字段:
190
+
191
+ ```jsonc
192
+ {
193
+ "listen": { "host": "127.0.0.1", "port": 9355 },
194
+ "upstream": { "host": "opencode.ai" },
195
+ "inject": {
196
+ "headers": {
197
+ "x-opencode-session": "{{session.id}}",
198
+ "x-opencode-request": "{{session.requestId}}"
199
+ }
200
+ },
201
+ "model": { "stripPrefixes": ["proxy-"] }
202
+ }
203
+ ```
204
+
205
+ 现成配置:[`examples/opencode-go.json`](examples/opencode-go.json)、
206
+ [`examples/generic-openai.json`](examples/generic-openai.json)。
207
+
208
+ ---
209
+
210
+ ## 配置详解
211
+
212
+ 配置优先级:**默认值 < 配置文件 < 环境变量 < 命令行参数**。
213
+
214
+ ### `listen`
215
+
216
+ | 字段 | 默认值 | 说明 |
217
+ | --- | --- | --- |
218
+ | `host` | `127.0.0.1` | 监听地址。只监听本机,不要改成 `0.0.0.0`,否则同网段都能用你的 Key 和额度 |
219
+ | `port` | `9355` | 监听端口 |
220
+
221
+ ### `upstream`
222
+
223
+ | 字段 | 默认值 | 说明 |
224
+ | --- | --- | --- |
225
+ | `protocol` | `https` | `https` 或 `http` |
226
+ | `host` | `opencode.ai` | 上游主机名。也可以直接写完整 URL,代理会自动拆解 |
227
+ | `port` | `null` | 端口,`null` 表示按协议取默认值(443 / 80) |
228
+ | `basePath` | `""` | 转发时统一加在路径前面的前缀,如 `/zen/go/v1` |
229
+ | `rewriteHost` | `true` | 是否把 `Host` 头改写成上游主机 |
230
+
231
+ ### `request`
232
+
233
+ | 字段 | 默认值 | 说明 |
234
+ | --- | --- | --- |
235
+ | `bufferBody` | `true` | 需要改写请求体(模型名、参数注入)时必须为 `true` |
236
+ | `maxBodyBytes` | `67108864` | 请求体上限(64MB),超限返回 413 |
237
+ | `timeoutMs` | `600000` | 上游请求超时 |
238
+ | `pathRewrite` | `[]` | 路径重写规则,每项 `{ "pattern": "正则", "replacement": "替换" }` |
239
+ | `dropHeaders` | `[]` | 这些请求头不转发给上游(小写) |
240
+ | `forwardClientSessionHeaders` | `true` | 是否把客户端原有的会话头一起转发 |
241
+
242
+ ### `session`
243
+
244
+ | 字段 | 默认值 | 说明 |
245
+ | --- | --- | --- |
246
+ | `enabled` | `true` | 是否启用会话 ID 注入 |
247
+ | `headerNames` | `x-opencode-session` 等 5 个 | 按顺序尝试从这些**请求头**读取客户端自带会话 ID |
248
+ | `bodyFields` | `session_id` 等 | 按顺序尝试从这些**请求体字段**读取(支持点路径) |
249
+ | `contentHash.enabled` | `true` | 客户端没带会话标识时,是否用内容指纹兜底 |
250
+ | `contentHash.fields` | `["system","system_instruction","instructions"]` | 参与指纹计算的字段 |
251
+ | `contentHash.includeFirstUserMessage` | `true` | 首条 user 消息是否参与指纹 |
252
+ | `idPrefix` | `ses_` | 生成 ID 的前缀(置空则无前缀) |
253
+ | `idFormat` | `hex26` | `hex26` / `hex` / `uuid` / `base36` / `short` |
254
+ | `requestIdFormat` | `msg_{{session.count}}` | 请求号模板,`session.count` 是同一会话内的递增序号 |
255
+ | `maxSessions` | `512` | 会话表上限,超出淘汰最旧的 |
256
+ | `ttlSeconds` | `0` | 会话过期秒数,`0` 表示不过期 |
257
+
258
+ ### `inject`
259
+
260
+ | 字段 | 默认值 | 说明 |
261
+ | --- | --- | --- |
262
+ | `headers` | 4 个 `x-opencode-*` | 要注入的请求头,值是模板字符串。设为 `null` 可跳过某项 |
263
+ | `body` | `{}` | 要合并进请求体的字段(支持嵌套对象与模板) |
264
+ | `removeBodyFields` | `[]` | 要从请求体里删掉的字段路径 |
265
+ | `overwrite` | `true` | `false` 表示客户端已有的同名头/字段不覆盖 |
266
+
267
+ ### `model`
268
+
269
+ | 字段 | 默认值 | 说明 |
270
+ | --- | --- | --- |
271
+ | `enabled` | `true` | 是否启用模型名重写 |
272
+ | `field` | `model` | 模型名所在的请求体字段 |
273
+ | `stripPrefixes` | `["proxy-"]` | 需要剥离的前缀列表,按顺序匹配,命中一个即停 |
274
+ | `map` | `{}` | 精确映射。先按客户端原始名查(优先),未命中则剥掉前缀后再查一次,所以 `proxy-glm` 与 `glm` 都能命中同一条 |
275
+ | `default` | `null` | 兜底模型名 |
276
+
277
+ ### 其他
278
+
279
+ | 字段 | 默认值 | 说明 |
280
+ | --- | --- | --- |
281
+ | `userAgent` | `opencode/1.18.29 cli` | 注入的 UA |
282
+ | `userAgentMode` | `replace-generic` | `keep` 保留客户端 UA;`replace` 总是替换;`replace-generic` 仅在客户端 UA 缺失或像个通用 HTTP 库时替换 |
283
+ | `response.stream` | `true` | 是否流式透传。`false` 会整体缓冲后返回(**会破坏 SSE,除非有特殊需要否则别关**) |
284
+ | `log.level` | `info` | `silent` / `error` / `warn` / `info` / `debug` |
285
+ | `log.file` | `null` | 额外的日志文件路径,按大小自动轮转 |
286
+
287
+ ---
288
+
289
+ ## 模板变量
290
+
291
+ `inject.headers` 和 `inject.body` 的值都是模板,可用变量如下:
292
+
293
+ | 变量 | 含义 |
294
+ | --- | --- |
295
+ | `{{session.id}}` | 本次请求使用的会话 ID |
296
+ | `{{session.count}}` | 该会话内的第几个请求(从 1 开始) |
297
+ | `{{session.requestId}}` | 按 `requestIdFormat` 渲染出的请求号,如 `msg_3` |
298
+ | `{{session.source}}` | 会话来源:`header:*` / `body:*` / `content-hash` / `random` |
299
+ | `{{model}}` | 重写后的模型名 |
300
+ | `{{path}}` / `{{method}}` | 原始请求路径 / 方法 |
301
+ | `{{header.x-foo}}` | 客户端请求头(小写) |
302
+ | `{{query.foo}}` | 查询参数 |
303
+ | `{{env.HOME}}` | 环境变量 |
304
+ | `{{uuid}}` | 每次渲染都不同的 UUID |
305
+ | `{{random}}` / `{{randomHex:16}}` | 随机十六进制串,默认 26 / 16 位 |
306
+ | `{{timestamp}}` / `{{timestampMs}}` | 秒 / 毫秒时间戳 |
307
+
308
+ ---
309
+
310
+ ## 命令行参数
311
+
312
+ | 参数 | 说明 |
313
+ | --- | --- |
314
+ | `-c, --config <file>` | 读取配置文件 |
315
+ | `-p, --port <n>` / `--host <addr>` | 监听端口 / 地址 |
316
+ | `-u, --upstream <url>` | 上游地址,如 `https://opencode.ai` 或 `host:port` |
317
+ | `--base-path <path>` | 转发路径前缀 |
318
+ | `--path-rewrite <a=>b>` | 路径重写,可重复 |
319
+ | `--inject <name=value>` | 注入请求头,可重复 |
320
+ | `--body-inject <k=v>` | 注入请求体字段,可重复 |
321
+ | `--model-prefix <prefix>` | 要剥离的模型名前缀,可重复 |
322
+ | `--model-map <a=b>` | 模型名精确映射,可重复 |
323
+ | `--session-header <name>` | 追加会话来源请求头,可重复 |
324
+ | `--session-field <path>` | 追加会话来源请求体字段,可重复 |
325
+ | `--session-id-format <f>` | 会话 ID 格式 |
326
+ | `--request-id-format <t>` | 请求号模板 |
327
+ | `--no-session` | 关闭会话注入 |
328
+ | `--no-stream` | 关闭流式透传 |
329
+ | `--timeout <ms>` / `--max-body <bytes>` | 超时 / 请求体上限 |
330
+ | `--log-level <l>` / `--log-file <f>` | 日志级别 / 日志文件 |
331
+ | `--init [file]` | 生成示例配置 |
332
+ | `--print-config` | 打印合并后的最终配置并退出 |
333
+
334
+ 环境变量与配置文件同名字段一一对应(大写形式):`PROXY_PORT`、`UPSTREAM_HOST`、
335
+ `UPSTREAM_PROTO`、`OPENCODE_UA`、`LOG_LEVEL`、`LOG_FILE`、`MODEL_ALIAS_PREFIX`、
336
+ `INJECT_HEADERS`(JSON)等。
337
+
338
+ ---
339
+
340
+ ## 本地状态端点
341
+
342
+ 只监听本机,不会转发到上游:
343
+
344
+ ```bash
345
+ # 运行概览:会话数、命中率、注入的头、上游地址
346
+ curl http://127.0.0.1:9355/__llm_session_proxy__/status
347
+
348
+ # 会话明细:每个会话的 ID、请求数、最后使用时间
349
+ curl http://127.0.0.1:9355/__llm_session_proxy__/sessions
350
+ ```
351
+
352
+ 排错时很有用——如果 `sessions.active` 一直是 0,说明请求根本没到代理(多半是模型 ID 没加 `proxy-` 前缀)。
353
+
354
+ ---
355
+
356
+ ## 工作原理
357
+
358
+ ```
359
+ 客户端 ──▶ llm-session-proxy ──▶ 上游 API
360
+
361
+ ├─ 1. 读请求体,解析 JSON
362
+ ├─ 2. 解析会话:显式标识 > 内容指纹 > 随机
363
+ ├─ 3. 重写路径、模型名
364
+ ├─ 4. 注入请求头与请求体参数
365
+ └─ 5. 转发,SSE 逐块回传
366
+ ```
367
+
368
+ 会话识别的三级策略是关键,它决定了会话 ID 能不能在同一对话内保持稳定:
369
+
370
+ 1. **显式标识**——客户端自己发了 `x-opencode-session` 之类的头,或请求体里有 `session_id`。直接复用,最准。
371
+ 2. **内容指纹**——对 `system` + 首条 user 消息做 SHA-256。同一对话的后续轮次只是往后追加消息,首条不变,所以指纹稳定,能落回同一个 ID。
372
+ 3. **一次性随机**——两者都没有时(比如请求体不是 JSON),发一个随机 ID,只保证上游不报 400;这类请求不进会话表,避免把表撑爆。
373
+
374
+ ---
375
+
376
+ ## 作为库使用
377
+
378
+ ```js
379
+ import { startProxy, buildConfig, createProxyServer } from 'llm-session-proxy';
380
+
381
+ const proxy = await startProxy({
382
+ flags: {
383
+ listen: { port: 9355 },
384
+ upstream: { host: 'opencode.ai' },
385
+ inject: { headers: { 'x-opencode-session': '{{session.id}}' } },
386
+ },
387
+ });
388
+
389
+ console.log(`代理已启动: ${proxy.url}`);
390
+ console.log(`运行中会话: ${proxy.store.size}`);
391
+
392
+ // 退出时
393
+ await proxy.stop();
394
+ ```
395
+
396
+ 也可以只取零件:`createProxyServer`(自己控制生命周期)、`SessionStore`(会话表)、
397
+ `renderTemplate`(模板引擎)、`buildConfig`(配置合并)。
398
+
399
+ ---
400
+
401
+ ## 常见问题
402
+
403
+ **客户端报连接失败 / 代理日志里没有请求记录**
404
+
405
+ 检查 Base URL 是否指向了本代理,以及模型 ID 是否加了 `proxy-` 前缀。后者是最常见的原因——
406
+ Trae 之类客户端会按模型 ID 把流量分流到自己的云通道。
407
+
408
+ **还是 400,说缺 session 头**
409
+
410
+ 看代理日志里那次请求的 `session=` 字段。如果有值,说明代理注入了但上游没收到,
411
+ 多半是 `inject.headers` 被配置文件覆盖掉了;如果没有值,说明 `session.enabled` 被关了。
412
+
413
+ **对话越长越慢,提示词缓存没生效**
414
+
415
+ 说明会话 ID 不稳定。检查客户端是否每轮都在改变 `system` 或首条 user 消息的内容
416
+ (某些客户端会把时间戳、当前文件路径塞进 system)。可以改用显式会话头,
417
+ 或把 `contentHash.fields` 收窄到最稳定的那个字段。
418
+
419
+ **想代理到别的上游,注入头会冲突吗**
420
+
421
+ 不会。默认注入的头对不认它们的上游来说是无害的多余头。要干净的话,
422
+ 把 `inject.headers` 改成空对象 `{}`,或只留你需要的那几个。
423
+
424
+ **端口被占用**
425
+
426
+ `EADDRINUSE` 时换端口:`--port 9356`,同时记得改客户端里的 Base URL。
427
+
428
+ ---
429
+
430
+ ## 限制与注意
431
+
432
+ - **只监听本机(127.0.0.1)**——代理会带上你的 API Key 转发请求,不要把监听地址改成 `0.0.0.0`。
433
+ - **`response.stream: false` 会破坏 SSE**——除非确实需要整体缓冲,否则保持 `true`。
434
+ - **`bufferBody: false` 时无法改写请求体**——模型重写和参数注入会失效,只能注入请求头、也只能靠请求头识别会话。
435
+ - 本工具只做转发与参数修补,不缓存、不计费、不修改响应内容。
436
+
437
+ ---
438
+
439
+ ## English
440
+
441
+ **llm-session-proxy** is a dependency-free local reverse proxy for LLM APIs. It injects the
442
+ session headers your client won't send (`x-opencode-session` and friends), rewrites model
443
+ aliases and request paths, injects arbitrary custom parameters, and streams SSE responses
444
+ through untouched.
445
+
446
+ ```bash
447
+ npx llm-session-proxy # default: OpenCode Go on 127.0.0.1:9355
448
+ npx llm-session-proxy -u https://api.example.com --inject "x-session-id={{session.id}}"
449
+ ```
450
+
451
+ Point your client's base URL at `http://127.0.0.1:9355/zen/go/v1` and use `proxy-<model-id>`
452
+ as the model name. See the Chinese sections above for the full configuration reference.
453
+
454
+ ## 发布新版本
455
+
456
+ 发布由 GitHub Actions 完成([`.github/workflows/publish.yml`](.github/workflows/publish.yml)),不需要在本机登录 npm:
457
+
458
+ 1. 改 `package.json` 里的 `version`
459
+ 2. 提交并推送
460
+ 3. 打一个同名 tag 并推送:
461
+
462
+ ```bash
463
+ git tag v0.1.1 && git push origin v0.1.1
464
+ ```
465
+
466
+ workflow 会先跑完全部单元测试、校验 tag 与 `package.json` 版本一致,再用仓库 secrets 里的
467
+ `NPM_TOKEN` 发布到 registry.npmjs.org。也可以在 Actions 页面手动触发,勾选 dry run 只做干跑。
468
+
469
+ ## License
470
+
471
+ [Apache License 2.0](LICENSE) © 2026 alaahong
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from '../src/cli.js';
3
+
4
+ runCli().catch((error) => {
5
+ process.stderr.write(`未捕获异常: ${error?.stack || error}\n`);
6
+ process.exit(1);
7
+ });
@@ -0,0 +1,76 @@
1
+ {
2
+ // 通用示例:把任意 OpenAI 兼容上游接进来,注入自定义请求头,
3
+ // 并把客户端用的短别名映射成真实模型名。
4
+ //
5
+ // 客户端 Base URL 填:http://127.0.0.1:8788/v1
6
+ // (上游本身就是 /v1/... 路径,所以不需要 pathRewrite)
7
+
8
+ "listen": { "host": "127.0.0.1", "port": 8788 },
9
+
10
+ "upstream": {
11
+ // 也可以直接写 "https://api.deepseek.com",下面三项会被自动解析
12
+ "protocol": "https",
13
+ "host": "api.deepseek.com",
14
+ "port": null,
15
+ "basePath": ""
16
+ },
17
+
18
+ "request": {
19
+ "bufferBody": true,
20
+ "timeoutMs": 600000,
21
+ "pathRewrite": [],
22
+ // 这些头没必要带给上游
23
+ "dropHeaders": ["x-stainless-arch", "x-stainless-os", "x-stainless-runtime"],
24
+ "forwardClientSessionHeaders": true
25
+ },
26
+
27
+ "session": {
28
+ "enabled": true,
29
+ // 这个上游没有会话要求,我们自己造一个稳定的会话 ID 给它,便于排障和计费归因
30
+ "headerNames": ["x-session-id", "x-conversation-id", "x-thread-id"],
31
+ "bodyFields": ["session_id", "conversation_id"],
32
+ "contentHash": {
33
+ "enabled": true,
34
+ "fields": ["system"],
35
+ "includeFirstUserMessage": true
36
+ },
37
+ "idPrefix": "",
38
+ "idFormat": "uuid",
39
+ "requestIdFormat": "{{session.id}}:{{session.count}}",
40
+ "maxSessions": 1024,
41
+ "ttlSeconds": 3600
42
+ },
43
+
44
+ "inject": {
45
+ "headers": {
46
+ "x-session-id": "{{session.id}}",
47
+ "x-request-id": "{{session.requestId}}",
48
+ "x-trace-id": "{{uuid}}",
49
+ "x-client-version": "2026-01-01",
50
+ "x-origin-app": "llm-session-proxy"
51
+ },
52
+ // 往请求体里补参数,客户端不支持的字段也能在这里统一加上
53
+ "body": {},
54
+ // 在上游不接受某些字段时可以删掉,例如关闭客户端自带的流式统计
55
+ "removeBodyFields": [],
56
+ "overwrite": true
57
+ },
58
+
59
+ "model": {
60
+ "enabled": true,
61
+ "field": "model",
62
+ "stripPrefixes": [],
63
+ // 让客户端可以填 "fast" / "smart",由代理翻译成真实模型名
64
+ "map": {
65
+ "fast": "deepseek-chat",
66
+ "smart": "deepseek-reasoner"
67
+ }
68
+ },
69
+
70
+ // 保留客户端自己的 UA,不做伪装
71
+ "userAgentMode": "keep",
72
+
73
+ "response": { "stream": true },
74
+
75
+ "log": { "level": "info", "file": "./logs/proxy.log" }
76
+ }
@@ -0,0 +1,116 @@
1
+ {
2
+ // ════════════════════════════════════════════════════════════════════
3
+ // OpenCode Go 常见模型配置样例
4
+ //
5
+ // 客户端 Base URL 填:http://127.0.0.1:9355/zen/go/v1
6
+ // 模型名按下面 model.map 的键来写,加不加 proxy- 前缀都可以:
7
+ // "glm" → glm-5.3
8
+ // "proxy-glm" → glm-5.3 (先剥前缀、再查映射)
9
+ // "glm-5.3" → glm-5.3 (直接写真实名也放行)
10
+ //
11
+ // 用 Trae 时建议一律加 proxy- 前缀:它按模型 ID 决定走不走自定义通道,
12
+ // 填内置预设名(如 glm-5.3)会被它自己的云通道截走,绕过本代理。
13
+ //
14
+ // 启动:llm-session-proxy -c examples/opencode-go-models.json
15
+ // ════════════════════════════════════════════════════════════════════
16
+
17
+ "listen": { "host": "127.0.0.1", "port": 9355 },
18
+
19
+ "upstream": {
20
+ "protocol": "https",
21
+ "host": "opencode.ai",
22
+ "port": null,
23
+ "basePath": ""
24
+ },
25
+
26
+ "request": {
27
+ "bufferBody": true,
28
+ "maxBodyBytes": 67108864,
29
+ "timeoutMs": 600000,
30
+ "pathRewrite": [],
31
+ "dropHeaders": [],
32
+ "forwardClientSessionHeaders": true
33
+ },
34
+
35
+ "session": {
36
+ "enabled": true,
37
+ "headerNames": ["x-opencode-session", "x-session-id", "x-conversation-id", "x-thread-id"],
38
+ "bodyFields": ["session_id", "sessionId", "conversation_id", "conversationId"],
39
+ "contentHash": {
40
+ "enabled": true,
41
+ "fields": ["system", "system_instruction", "instructions"],
42
+ "includeFirstUserMessage": true
43
+ },
44
+ "idPrefix": "ses_",
45
+ "idFormat": "hex26",
46
+ "requestIdFormat": "msg_{{session.count}}",
47
+ "maxSessions": 512,
48
+ "ttlSeconds": 0
49
+ },
50
+
51
+ "inject": {
52
+ "headers": {
53
+ "x-opencode-session": "{{session.id}}",
54
+ "x-opencode-request": "{{session.requestId}}",
55
+ "x-opencode-client": "cli",
56
+ "x-opencode-project": "global"
57
+ },
58
+ "body": {},
59
+ "removeBodyFields": [],
60
+ "overwrite": true
61
+ },
62
+
63
+ "model": {
64
+ "enabled": true,
65
+ "field": "model",
66
+ "stripPrefixes": ["proxy-"],
67
+
68
+ "map": {
69
+ // ── 端点 /zen/go/v1/chat/completions(OpenAI 兼容,绝大多数客户端走这条)──
70
+ "glm": "glm-5.3",
71
+ "glm-flash": "glm-5.3-flash",
72
+ "glm-5.2": "glm-5.2",
73
+ "glm-5.1": "glm-5.1",
74
+ "kimi": "kimi-k3",
75
+ "kimi-code": "kimi-k2.7-code",
76
+ "kimi-k2.6": "kimi-k2.6",
77
+ "deepseek": "deepseek-flash",
78
+ "deepseek-pro": "deepseek-v4-pro",
79
+ "deepseek-v4-flash": "deepseek-v4-flash",
80
+ "deepseek-vision": "deepseek-v4-flash-vision-exp",
81
+ "longcat": "longcat-2.0",
82
+ "mimo": "mimo-v2.5",
83
+ "mimo-pro": "mimo-v2.5-pro",
84
+ "hy3": "hy3",
85
+ "hy4": "hy4-preview"
86
+
87
+ // ── 端点 /zen/go/v1/responses(OpenAI Responses API)──
88
+ // 客户端必须支持 Responses 协议,用 OpenAI Chat Completions 格式调用会被上游拒绝。
89
+ // "grok": "grok-4.6",
90
+ // "gpt-luna": "gpt-5.6-luna",
91
+ // "muse-1.3": "muse-spark-1.3-contributor",
92
+ // "muse-1.2": "muse-spark-1.2-contributor"
93
+
94
+ // ── 端点 /zen/go/v1/messages(Anthropic Messages API)──
95
+ // 客户端必须能发 Anthropic 格式的请求体。若客户端只支持 OpenAI 格式,
96
+ // 本代理不做协议转换,这些模型用不了。
97
+ // "minimax": "minimax-m3",
98
+ // "minimax-2.7": "minimax-m2.7",
99
+ // "minimax-2.5": "minimax-m2.5",
100
+ // "qwen-max": "qwen3.8-max",
101
+ // "qwen-flash": "qwen3.8-flash",
102
+ // "qwen3.7-max": "qwen3.7-max",
103
+ // "qwen-plus": "qwen3.6-plus"
104
+ },
105
+
106
+ // 客户端填了映射表里没有、也剥不掉前缀的名字时,是否兜底到某个模型
107
+ "default": null
108
+ },
109
+
110
+ "userAgent": "opencode/1.18.29 cli",
111
+ "userAgentMode": "replace-generic",
112
+
113
+ "response": { "stream": true },
114
+
115
+ "log": { "level": "info", "file": null }
116
+ }