mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
feat(opencode): Zen 免费层可用 —— 门禁四要素 + id 公式逆出
问题:localzen 源(opencode Zen)整个免费池恒 403 FreeTierError
("free tier can only be used from within OpenCode"),space-bunny-free
等免费模型全部失效。
排查(装官方 CLI 抓包 + 逆其本地 opencode.db,见下):门禁是**四条请求形状
条件**,与 TLS 指纹 / UA / 凭据来源 / system 内容 / 消息条数全部无关:
1. 端点必须是 /inference/v1/chat/completions(原来打的 /zen/v1 恒 403)
2. 必须同时发 x-opencode-session 与 x-opencode-session-id(值相同)
3. tools 里必须有一个名为 read 的工具,且其 parameters.properties **非空**
(实测:抽掉官方 11 个工具中任意一个仍 200,**只有抽掉 read 变 403**;
properties 里放任意属性名即可,描述/$schema/required 都不需要;
改成 properties:{} 或只有 type:object 则 403)
4. stream 必须为 true(同 body 改 false 恒 403)
★ 两个 id 的 12 位 hex 段由**确定公式**给出(逆自 opencode.db 的 session 表,
用 11/12 条真实 id 逐条比对完全一致):
MASK = 2^48-1 ; K = 0x1afffffffffffe (注意不是 27<<48,两者差 2)
session 段 = (K - now_ms*4096) & MASK ← 倒计时式,Δseg/Δms ≡ -2^12
request 段 = (now_ms*16) & MASK
时间容差实测 ≥1 小时(-3600s 仍 200),故 os.time() 的秒精度足够;
now_ms*4096 ≈ 7.3e15 < 2^53,在 double 安全整数范围内。
实现:
- id 生成换成上述公式(原用 sha256 派生,完全不符)
- 补 x-opencode-session-id;两个头同值
- 会话号按**整分钟桶**取:既满足门禁的倒计时公式,又保留上游前缀缓存命中
(实测 cached_tokens: 256 生效)
- tools 兜底:客户端没带 tools 时注入带非空 properties 的 read 桩;
用户自带 read 则原样保留,不覆盖其定义
- 强制 stream=true
- ★ transform_response 支持 SSE 聚合:免费层只能流式,而客户端可能发非流式,
此时内核按客户端意图走 Chat/transform_response(期望 JSON)⇒ 必须自行把
SSE 聚合成 chat.completion,否则报 "invalid character 'd'"
配置(已在生产落地):
- localzen base_url: /zen/v1 → /inference/v1(billing 规则同步)
- 免费模型 1 个 → 8 个(含 deepseek-v4.1-flash-free)
- 适配器移入 internal/lua/adapters/,随 deploy.sh 自动投放
判据(4 条):
- TestOpencodeZenAdapterE2E 头公式/两头部同值/tools 兜底/不覆盖
- TestOpencodeZenAggregatesSSEForNonStreamClient SSE → chat.completion 聚合
- TestOpencodeZenLiveE2E ★真上游:纯对话/自定义system/多轮/算术
- TestOpencodeZenLiveStreamThroughAdapter ★真上游:完整流解析链(25 chunk)
(后两条需 LLMSPROXY_LIVE_ZEN=1 + 真实凭据,默认 skip)
生产 e2e 实测:非流式返回 GW-NONSTREAM-OK(usage 含 cached_tokens 256);
流式原生 SSE 透传;多轮+工具返回正确结果(12*12=144 → 144+1=145)。
回归:go test -tags luajit ./... 10 包全绿。
★ 方法论教训(本轮绕了 4 轮、自我推翻 3 次):
- 一次只改一个变量。我多次同时改 system+条数+tools,拟合出"消息条数上限 3"
这种假规律,后续每次反转都源于此 —— 真相是那些测试**都缺 tools**。
- 重放成功 ≠ 解释成功。第一次逐字节重放就 200,我却当"重放了官方刚发的请求",
继续追 TLS 指纹/端点/形状。正确做法:一旦重放成功立刻做**单变量消去**。
- "毒药短语"陷阱:我二分到 963/964 字符精度锁定 Never use tools 是触发点,
但它单独出现全 200 —— 真相是缺 read 的 properties。凡"某内容触发"的规律
必须交叉验证。
This commit is contained in:
@ -19,7 +19,7 @@ func TestOpencodeZenLiveE2E(t *testing.T) {
|
||||
if os.Getenv("LLMSPROXY_LIVE_ZEN") != "1" {
|
||||
t.Skip("设 LLMSPROXY_LIVE_ZEN=1 开启真上游 e2e")
|
||||
}
|
||||
src, err := os.ReadFile("../../adapters/_local/opencodezen.lua")
|
||||
src, err := bundledAdapters.ReadFile("adapters/opencodezen.lua")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -134,7 +134,7 @@ func TestOpencodeZenLiveStreamThroughAdapter(t *testing.T) {
|
||||
if os.Getenv("LLMSPROXY_LIVE_ZEN") != "1" {
|
||||
t.Skip("设 LLMSPROXY_LIVE_ZEN=1 开启")
|
||||
}
|
||||
src, err := os.ReadFile("../../adapters/_local/opencodezen.lua")
|
||||
src, err := bundledAdapters.ReadFile("adapters/opencodezen.lua")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user