-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconfig.example.jsonc
More file actions
446 lines (389 loc) · 20.8 KB
/
Copy pathconfig.example.jsonc
File metadata and controls
446 lines (389 loc) · 20.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
// config.json 示例(带注释版)
// 用法:
// 1) 复制本文件为同目录下的 config.json
// 2) 按需修改 providers / keys / routes
// 3) 启动:python3 sse2json.py
//
// 环境变量覆盖(可选):
// - PROXY_CONFIG_PATH=... 指定 config.json 路径
// - PROXY_PROVIDERS_JSON='{"p1":{...}}' 覆盖整个 providers(JSON 字符串)
// - PROXY_PROVIDER_KEYS__opencode='["k1"]' 覆盖单个 provider 的 keys(JSON 数组字符串)
// - PROXY_MODEL_ROUTES_JSON='{...}' 覆盖 models.routes(JSON 字符串)
// - PROXY_TRUSTED_PROXY_CIDRS='["172.16.0.0/12"]' 可信反代来源网段
// - PROXY_TRUSTED_PROXY_HEADERS='["x-forwarded-for","x-real-ip"]' 可信客户端 IP 头
//
// 兼容旧环境变量覆盖(短期保留):
// - 仍可设置 UPSTREAM_URL / UPSTREAM_API_KEY / MODEL_MAP / DISABLE_MODEL_MAP
// - 当这些旧变量存在时,会自动覆盖到默认 provider 上
{
// 代理自身的监听与运行参数
"server": {
// 监听地址。本机/反代场景建议 127.0.0.1;容器内或需被外部直接访问时用 0.0.0.0
"host": "127.0.0.1",
// 本地监听端口;建议正式使用 4894,便于直接访问 http://127.0.0.1:4894
"port": 4894,
// 最大并发 worker 线程数(默认 20)
// 注意:流式连接会长期占用线程;如你并发 stream 很多可适当调大
"max_workers": 20,
// 日志目录(相对路径会以项目目录为基准)
"log_dir": "proxy_logs",
// 是否开启“请求级落盘日志”(非常多文件,建议仅调试时开启)
// 等同于旧环境变量 PROXY_DEBUG=true
"debug_disk_log": false,
// 仅信任这些反向代理网段提供的真实客户端 IP 请求头。
// Docker bridge 通常属于 172.16.0.0/12;请按实际网络收窄,直连公网时不要盲目信任私网段。
"trusted_proxy_cidrs": ["127.0.0.0/8", "172.16.0.0/12"],
"trusted_proxy_headers": ["cf-connecting-ip", "forwarded", "x-forwarded-for", "x-real-ip"],
// Admin API 密钥。为空时 /-/admin/* 不可访问。
// 可通过 X-Admin-Key、Authorization: Bearer ... 或 ?admin_key=... 传入。
"admin_key": "sk-admin-change-me"
},
// 路由/尝试/超时策略
"routing": {
// 默认供应商池:当 models.routes 里没对某个 canonical model 配路由时,就用这里
"default_provider_pool": ["opencode", "rawchat", "deepseek"],
// 供应商选择策略:
// - priority_failover 默认推荐。优先访问 priority 高的 provider;同 provider 多 key 按配置顺序作为备用。
// 第一把可用 key 成功时会持续优先使用;不可用、冷却、失败后,再切到后续 key/provider。
// 适合保护上游缓存命中、固定主供应商、成本/质量优先级明确的生产场景。
// - round_robin provider 之间公平轮询,不使用 weight;适合主动均衡多供应商流量。
// - weighted_rr provider 按 route weight 展开后轮询;适合按比例分摊流量。
// - random 同一次请求内顺序稳定的随机;适合临时打散,不推荐作为生产默认。
"provider_select": "priority_failover",
// 格式与供应商优先级的关系:
// - priority_first:先比较 provider priority;同优先级才优先客户端原生格式
// - native_first:所有客户端原生格式候选都排在跨格式候选之前
"format_preference": "priority_first",
// 单次请求允许的“总尝试次数”上限(跨 provider + key 的总和)
// 只会在“向客户端写响应前”重试;一旦开始写 SSE,就不会透明重试
"max_attempts": 6,
// 连接/读取超时;stream 首个可输出事件前按首事件预算换路由:
// - first_event_timeout_s / agent_first_event_timeout_s:单次尝试预算
// (后者用于带 tools/vision/reasoning 特征的请求,即推理模型场景)
// - first_event_total_timeout_s / agent_first_event_total_timeout_s:跨尝试总预算
// 运行期会按该 provider+model 的近期 p95 首事件延迟自适应上调(有上限);
// 总预算将耗尽时,后续尝试仍保底获得单次预算的 60%,避免被切到几秒就超时。
"connect_timeout_s": 15,
"read_timeout_s": 120,
"first_token_timeout_s": 30,
"first_event_timeout_s": 25,
"agent_first_event_timeout_s": 45,
"first_event_total_timeout_s": 90,
"agent_first_event_total_timeout_s": 150,
// 跨格式参数语义策略:
// - safe:映射可安全转换的字段,丢弃 cache_control 等非语义提示,并在请求详情记录决定
// - strict:任何无法无损保留的字段都会阻止该格式候选
"semantic_conversion": "safe",
// 原生同格式非流式响应:
// - safe: 旧行为,解析上游 JSON 后重新序列化返回
// - validated: 解析一次做 usage/空输出校验,但向客户端返回上游原始 JSON bytes,减少重序列化开销
"native_nonstream_mode": "validated",
// 原生同格式流式响应:
// - safe: 旧行为,先等首个 SSE data 事件,失败时还能在写响应前换路由
// - guarded: 默认。上游 HTTP 状态 OK 后立即给客户端发 SSE headers,不等首事件,降低首 token 代理等待;
// 代价是 headers 发出后首事件超时不能再透明 failover
"native_stream_mode": "safe",
// 非流式“空可见输出”兜底(默认 true):
// 推理模型在 max_tokens 耗尽时可能只返回思考、没有正文(finish_reason=length)。
// 所有候选都如此时,与其返回 502,不如把最后一次上游 200 响应原样返回给客户端
// (带 X-Route-Note: empty-visible-output-fallback 头),由客户端自行判断截断。
"empty_visible_output_fallback": true
},
// 错误分类、可重试状态码与冷却时间
"retry": {
// 这些 HTTP 状态码会触发“换 key / 换 provider”继续尝试
"retryable_status": [408, 409, 425, 429, 500, 502, 503, 504],
// 这些状态码视为 key 失效(会对该 key 施加长冷却/禁用)
"key_fatal_status": [401, 403],
// 冷却时间(秒)
"cooldown_s": {
// 429 限流
"rate_limit": 30,
// 5xx 上游故障
"server_error": 10,
// 网络错误(超时/断开/连接失败等)
"network_error": 10,
// 401/403 之类 key 无效(建议较长)
"key_invalid": 3600,
// 402 余额/额度不足,建议长冷却,避免每次请求都先撞失败供应商
"quota_or_balance": 3600
},
// key 瞬时故障、凭据/余额故障,以及模型/格式兼容性组合的升级阶梯。
"key_failure_ladder_s": [10, 60, 3600],
"credential_failure_ladder_s": [3600, 21600, 86400],
"compatibility_failure_ladder_s": [10, 60, 3600],
// 可配置失败策略。未配置的字段会使用代码默认值。
// cooldown_scope:
// - none 只记录失败,不冷却 key/provider
// - key 冷却当前 key
// - provider 冷却当前 provider
// - key_provider 同时冷却 key 和 provider
// provider_cooldown_s 会被限制到合理范围,避免误配置导致 provider 长时间不可用。
"failure_policies": {
"key_invalid": { "cooldown_scope": "key", "cooldown_s": 3600, "disables_key": true },
"rate_limited": { "cooldown_scope": "key", "cooldown_s": 30, "disables_key": false },
"quota_or_balance": { "cooldown_scope": "key", "cooldown_s": 3600, "disables_key": false },
"server_error": { "cooldown_scope": "key", "cooldown_s": 10, "disables_key": false },
"network_error": {
"cooldown_scope": "key_provider",
"cooldown_s": 10,
"provider_cooldown_s": 10,
"disables_key": false
},
"provider_compat": { "cooldown_scope": "none", "cooldown_s": 0, "disables_key": false },
"empty_visible_output": { "cooldown_scope": "none", "cooldown_s": 0, "disables_key": false },
"client_error": { "cooldown_scope": "none", "cooldown_s": 0, "disables_key": false },
"unknown": { "cooldown_scope": "key", "cooldown_s": 10, "disables_key": false }
},
// 429 时是否优先使用 Retry-After 头(推荐 true)
"respect_retry_after": true,
// 预留:指数退避参数(当前实现主要用 cooldown_s + Retry-After)
"backoff": { "mode": "exp", "base_s": 2, "max_s": 120 }
},
// 模型映射与路由
"models": {
// 是否禁用“客户端模型名 -> canonical model”的映射(等同旧 DISABLE_MODEL_MAP=true)
// true:客户端发什么 model 就按原样作为 canonical model
"disable_client_model_map": true,
// 客户端模型名 → canonical model。默认不做别名映射,需要时再显式填写。
// 注意:canonical model 用于路由;最终发往上游时,还会经过 provider_model_map 二次映射
"client_model_map": {},
// 针对某个 canonical model 的“专用路由”
// - providers: 列表,支持 weight 和 priority
// weight 只影响 weighted_rr;priority 影响 priority_failover。
// route provider 的 priority 会覆盖 provider 自身 priority。
// - provider_select: 可覆盖 routing.provider_select
"routes": {
"deepseek-v4-flash": {
"providers": [
{ "name": "opencode", "weight": 1, "priority": 100 },
{ "name": "deepseek", "weight": 1, "priority": 90 }
],
"provider_select": "priority_failover"
},
"deepseek-v4-pro": {
"providers": [
{ "name": "opencode", "weight": 1, "priority": 100 },
{ "name": "deepseek", "weight": 1, "priority": 90 }
],
"provider_select": "priority_failover"
},
"gpt-5.5": {
"providers": [
{ "name": "rawchat", "weight": 1, "priority": 80 }
],
"provider_select": "priority_failover"
},
// reasoning_effort: 模型级思考强度覆盖(可选)
// - 不设置 / 设为空:跟随客户端请求中的思考强度(默认行为)
// - minimal | low | medium | high:强制替换客户端请求的思考强度
// - off:关闭思考(移除 thinking / reasoning 参数)
// 例:客户端请求 low,但该模型配置了 high,则上游收到 high
"claude-opus-4.6": {
"providers": [
{ "name": "anthropic", "weight": 1, "priority": 100 }
],
"provider_select": "priority_failover",
"reasoning_effort": "high"
}
},
// provider-specific 的手工模型名映射:
// canonical model → 某 provider 实际可用的 model id
// 自动模型发现会先做安全归一:大小写、vendor 前缀、空格/下划线这类差异会自动合并。
// 例:deepseek-ai/DeepSeek_V4 Flash 会自动归一为 deepseek-v4-flash,并保留真实上游 id。
// 手工映射只用于自动规则无法安全判断的供应商别名;手工映射优先级最高。
"provider_model_map": {
"opencode": {
"deepseek-v4-flash": "deepseek-v4-flash",
"deepseek-v4-pro": "deepseek-v4-pro"
},
"rawchat": {
"gpt-5.5": "gpt-5.5"
},
"deepseek": {
"deepseek-v4-flash": "deepseek-v4-flash",
"deepseek-v4-pro": "deepseek-v4-pro"
}
},
// 同一个 canonical model 可对应同一 provider 的多个真实模型。
// priority 越大越先尝试;候选身份包含 raw model,因此失败/兼容熔断不会串到其它 variant。
"provider_model_variants": {
"rawchat": {
"gpt-5.5": [
{ "model": "gpt-5.5-high", "priority": 100 },
{ "model": "gpt-5.5-low", "priority": 10 }
]
}
},
// Provider 模型发现快照:后台发现、添加/修改 provider、手动刷新时写入。
// GET /v1/models 只读取本地快照,不会临时请求上游,避免请求时等待。
"provider_model_capabilities": {},
// /v1/models 的本地聚合快照:由 provider_model_capabilities 和显式配置重建。
// 不建议手工维护;会随 runtime state 一起保存,重启后可立即返回。
"models_union_snapshot": {},
// 自动发现失败/尚未发现时,是否仍假设 provider 可能支持未知模型。
// true 更容错;false 更严格,会减少误发请求但依赖模型发现成功。
// 一旦已有 provider_model_capabilities 快照,路由会保守过滤已知不支持该模型的 provider;
// 未发现能力的 provider 只有显式配置 provider.assume_supports_unknown_models=true 时才继续参与。
"assume_supports_unknown_models": true,
// GET /v1/models 的本地快照策略:
// - first_healthy_provider:返回首个健康 provider 的已发现模型快照(默认)
// - union:返回所有启用 provider 的已发现模型并集;失败/刷新中会保留 last-known-good
"models_source": "first_healthy_provider"
},
// ★ 后台网络任务让路(QoS):即时请求永远优先,后台任务闲时才跑。
// 有真实请求在途、或最后一个请求结束后 quiet_window_s 秒内,
// 模型发现 / AA 价格抓取 / 启动 AA 预取等后台网络任务自动推迟,
// 按短间隔反复重试直到网络空闲。手动触发的操作(刷新模型、测试按钮)
// 走紧急通道,不受此窗口限制。
// max_defer_s 是防饿死上限:某项后台任务被连续推迟超过该时长后,
// 即使仍有流量也会执行一次,保证 7x24 高负载下模型列表/价格不会永久过期。
"background": {
"quiet_window_s": 120,
"max_defer_s": 1800
},
// ★ 全局代理(最低优先级 fallback;key/provider 都没设置 proxy 时才使用)
// 支持两种写法:
// 写法1(简单):"proxy": "http://127.0.0.1:10808"
// 写法2(完整):"proxy": {"http": "http://127.0.0.1:10808", "https": "http://127.0.0.1:10808"}
// 不填 = 全直连;优先级:key.proxy > provider.proxy > 全局 proxy > 直连
"proxy": {},
// 上游供应商列表(多供应商切换的核心)
"providers": {
"opencode": {
// provider 默认优先级。priority_failover 下数值越大越优先。
// 如果某个 models.routes.providers 项单独写了 priority,则 route priority 优先生效。
"priority": 100,
// 供应商 base_url(不含具体 endpoint path)
"base_url": "https://opencode.ai/zen/go",
// 该供应商支持哪些上游格式。router 会优先选择客户端同格式的上游。
// 旧字段 chat_completions_path / responses_path / anthropic_messages_path 仍兼容,
// 但新配置建议直接写 formats。
"formats": {
"chat_completions": { "enabled": true, "path": "/v1/chat/completions" },
"responses": { "enabled": false, "path": "/v1/responses" },
"anthropic_messages": { "enabled": false, "path": "/v1/messages" }
},
// Models 路径(可选;用于 GET /v1/models 透传/拉取)
"models_path": "/v1/models",
// 多 key:按配置顺序优先使用,失败或冷却时才切到后续 key
// key 可写成字符串,也可写成对象来单独设置 key 级代理。
// key.proxy 优先级最高,只对这一把 key 生效。
"keys": [
"sk-your-key-1",
{ "key": "sk-your-key-2", "proxy": "http://127.0.0.1:9000" }
],
// DeepSeek-compatible thinking-mode Chat 上游可能要求历史 assistant 消息带 reasoning_content。
// 仅对这类 provider 开启,不要把它当作所有 OpenAI-compatible 供应商的默认规则。
"force_reasoning_content": true,
// 额外 headers(会自动补齐 Content-Type/Authorization/User-Agent)
"headers": { "User-Agent": "Mozilla/5.0" },
// 可选:用于控制台估算 cost_usd。单位是 USD / 1M tokens;不填或为 0 则只统计 token。
// 也可以用 models 按 provider_model 覆盖价格。
"pricing": {
"input_per_million": 0,
"output_per_million": 0,
"models": {
"deepseek-v4-flash": { "input_per_million": 0, "output_per_million": 0 }
}
},
// ★ 供应商级别代理(优先级:key.proxy > provider.proxy > 全局 proxy > 直连)
// 支持两种写法:
// 写法1(简单):"proxy": "http://127.0.0.1:10808"
// 写法2(完整):"proxy": {"http": "http://127.0.0.1:10808", "https": "http://127.0.0.1:10808"}
// 不填 = 回退到全局 proxy;全局也没填则直连
// 是否启用该 provider
"enabled": true
},
"rawchat": {
"priority": 80,
// 示例:原生 OpenAI Responses 上游。base_url 不需要包含完整 endpoint;
// 具体 endpoint 由 formats.responses.path 决定。
"base_url": "https://rawchat.example.com",
"formats": {
"chat_completions": { "enabled": false, "path": "/v1/chat/completions" },
"responses": { "enabled": true, "path": "/v1/responses" },
"anthropic_messages": { "enabled": false, "path": "/v1/messages" }
},
"models_path": "/v1/models",
// 每把 key 可声明自己的 canonical → raw model 映射。路由只会把 raw model
// 发给明确支持它的 key;后台发现也会逐 key 保存独立 catalog。
"keys": [
{ "key": "sk-your-rawchat-key-high", "models": { "gpt-5.5": "gpt-5.5-high" } },
{ "key": "sk-your-rawchat-key-low", "models": { "gpt-5.5": "gpt-5.5-low" } }
],
"headers": { "User-Agent": "Mozilla/5.0" },
"enabled": true
},
"deepseek": {
"priority": 90,
// 示例:原生 Anthropic Messages 上游。
"base_url": "https://api.deepseek.com",
"formats": {
"chat_completions": { "enabled": false, "path": "/v1/chat/completions" },
"responses": { "enabled": false, "path": "/v1/responses" },
"anthropic_messages": { "enabled": true, "path": "/v1/messages" }
},
"models_path": "/v1/models",
"keys": ["sk-your-key"],
// DeepSeek Anthropic thinking mode 可能要求历史 assistant 消息带 content[].thinking。
"force_anthropic_thinking": true,
// 假设 deepseek provider 默认需要走代理;单个 key 仍可通过 key.proxy 覆盖:
"proxy": "http://127.0.0.1:10808",
"headers": { "User-Agent": "Mozilla/5.0" },
"enabled": true
}
},
// 日志与可观测性
"observability": {
// 日志级别(目前主要通过 print 输出,后续可扩展为 JSON lines)
"log_level": "info",
// key 在日志里怎么截断显示:前 prefix 位 + ** + 后 suffix 位
"log_key_mask": { "prefix": 6, "suffix": 2 },
// 每次请求是否打印 provider/key/attempt 选择信息。关闭可减少热路径同步 stdout 开销。
"log_provider_on_each_request": false,
// 原生同格式 SSE pass-through 是否扫描 usage:
// - full: 旧行为,逐行扫描 usage 并计入统计
// - off: 只转发不解析 SSE 行,减少 CPU;usage 统计会为空
"native_stream_usage": "full",
// 缺少本地价格时使用有界后台队列查询 AA;请求线程不会等待网络。
"pricing": {
"resolve_missing_prices": true,
"queue_size": 64,
"max_retries": 2,
"retry_backoff_s": 1,
"connect_timeout_s": 3,
"total_timeout_s": 8
},
// 安全诊断日志(JSONL,stdlib,无额外依赖)
// 用于区分失败发生在上游 HTTP、格式兼容重试、请求转换、网络传输还是代理内部异常。
// 只保存 request_id、provider/model/format、attempt、错误分类、上游错误摘要和脱敏 key;
// 不保存请求正文,也不保存完整 API key。
"diagnostics": {
"enabled": true,
"path": "tmp/proxy_diagnostics.jsonl"
},
// 控制台历史数据持久化(SQLite,stdlib,无额外依赖)
// 只保存请求元数据、attempt 链路、耗时、状态、provider/model/format、错误分类和脱敏 key;
// 不保存请求正文,也不保存完整 API key。
"history": {
"enabled": true,
"path": "tmp/proxy_history.sqlite3",
"retention_days": 30
},
// 永久保存低体积的每日/累计用量;小时趋势默认保留 90 天。
// reporting_timezone 留空时在首次初始化统计库时固定为系统时区。
"usage_statistics": {
"enabled": true,
"hourly_retention_days": 90,
"reporting_timezone": ""
},
// Admin 配置/运行时操作审计(JSONL,stdlib,无额外依赖)
// 只保存动作、目标、来源、时间和脱敏摘要;不保存完整 provider key。
"audit": {
"enabled": true,
"path": "tmp/admin_audit.jsonl",
"max_records": 1000
}
}
}