|
|
4514270e21
|
fix(opencodezen): UA 升到 1.19.4 并补回 x-opencode-session-id
- 上游按客户端指纹判定免费层资格:CLI 1.18.29(4 个 x-opencode-* 头)
稳定 429;CLI 1.19.4(5 个头,含 session-id)200 且任务真实完成。
- 适配器的 UA 与头集合必须**同步**升级:只改 UA 声称的版本、不发对应头,
指纹反而更不一致,实测重新落回 429。
- 头形状按官方 CLI 抓包校正;`x-opencode-session-id` 与 `x-opencode-session`
同值,此前被误删。
- live 测试新增 `gatewayBinary()`:优先 `MODELROUTER_BIN`,其次
`/usr/local/bin/modelrouter`,回退改名前的 `/usr/local/bin/llmsproxy`。
注:本提交是改名/插件整改之外的独立修复,先于 v3.0.0 收口。
|
2026-10-27 00:06:41 +08:00 |
|
|
|
9e5fec08b8
|
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。凡"某内容触发"的规律
必须交叉验证。
|
2026-10-15 20:30:33 +08:00 |
|
|
|
68446fb6fe
|
fix(vnc): noVNC 的 path 必须用相对名,否则 WebSocket 路径重复拼接
真实缺陷(白屏真因之一):view_url 里传了**站点根路径**作为 noVNC 的
path 参数:
.../proxy/<id>/vnc.html?path=api%2Fplugins%2Fhttp%2Fchannels%2Fbrowser%2Fproxy%2F<id>%2Fwebsockify
但 noVNC 用 `new URL(path, location.href)` 解析它(app/ui.js 的 connect()),
即**相对 vnc.html 所在目录**解析,默认值就是相对名 "websockify"。传根路径
会被当相对路径 ⇒ 实际请求变成路径重复拼接:
.../proxy/<id>/api/plugins/http/channels/browser/proxy/<id>/websockify
于是 WebSocket 永远连不上,noVNC 显示连接失败,页面看似白屏。
修法:只传相对名 path=websockify,与 noVNC 默认值一致。
★ 判据:必须解析**产品自己给出的 URL**(而不是测试里手拼)。我上一轮验证时
手拼 URL 并省掉了 path 参数(默认值恰好正确)⇒ 掩盖了这个真实缺陷。
新判据断言:path 存在、不以 / 开头、且 ResolveReference 后等于同级 websockify。
|
2026-10-15 20:30:33 +08:00 |
|