OpenCode Go 套餐模型漂移(Model Substitution)记录
OpenCode Go 订阅(opencode.ai/zen/go/v1)在请求时可能出现模型被服务器端透明替换的现象——发送的模型与实际执行的模型不一致。
现象描述
Hermes 配置为通过 opencode-go provider 请求 deepseek-v4-flash,但在 OpenCode 用量面板中显示实际消耗了 glm-5 或 kimi-k2.6 等不同模型的配额。
典型实例:
| 时间 | 发送模型(Hermes) | 用量面板显示 | 消耗 |
|---|---|---|---|
| 2026-05-25 08:49 | deepseek-v4-flash |
glm-5 |
$0.0363 (32,020 in / 1,360 out) |
| 多次历史记录 | deepseek-v4-flash |
kimi-k2.6 |
— |
根因
这是 OpenCode Go 池化计算(pooled-compute)订阅行为,并非 Hermes 配置问题。原因如下:
- Hermes 的
custom_providers.opencode-go.models仅配置了deepseek-v4-flash和qwen3.5-plus,不会发送model=glm-5 - OpenCode Go 是池化计算服务:后端在负载或资源调度时,可能将请求路由到其他可用模型来保证响应
- Go 用量面板显示的是实际执行模型,而非请求模型
- 此行为在 OpenCode 官方文档中已隐含说明——Go 提供的是"稳定地访问开源编程模型"的订阅服务
fallback 机制详解
Hermes 内部有 两条 fallback 路径,触发条件不同:
路径 A:API 响应级故障(网络错误、malformed response)
位置:run_agent.py:13136
# 立即 fallback,不重试
if self._try_activate_fallback():
retry_count = 0
continue
适用于:API 彻底不可用(HTTP 5xx、连接超时等)。
路径 B:内容级空响应(模型返回空 content)
位置:run_agent.py:15533-15574
# 第1步:同模型重试 3 次
if _truly_empty and self._empty_content_retries < 3:
self._empty_content_retries += 1
continue
# 第2步:3 次重试都空 → 尝试 fallback
if _truly_empty and self._fallback_chain:
if self._try_activate_fallback():
self._empty_content_retries = 0
continue # 切到 V100 重新请求
适用于:模型响应了请求但内容为空。先重试 3 次,再走 fallback。
关键结论:"no response" 确实会触发 fallback,但需要先耗尽 3 次同模型重试。你看到的 "⚠️ The model returned no response" 意味着 3 次重试 + V100 fallback 全部失败。
两个事件的时序线:
08:28 deepseek-v4-flash → 空内容
→ retry #1 → 空
→ retry #2 → 空
→ retry #3 → 空
→ _try_activate_fallback() → V100/Qwen3.5-27B
→ V100 也返回空(或失败)
→ fallback 链耗尽 → "(empty)" 终端消息
→ gateway 转译为用户看到的警告
V100 fallback 可能失败的原因
- 上下文过长: 出问题时会话约 277K tokens,Qwen3.5-27B 上限 262K(恰好卡在边界)
- 首次请求超时: llama.cpp 在第一轮请求时可能做 prompt processing,超出 180s 默认超时
- Fallback 条目缺少显式 base_url: 当前配置只有
provider和model,依赖resolve_provider_client从 custom_providers 查找 V100 的 base_url/api_key。查表链路正常,但瞬时故障不可排除。
08:49 glm-5 消耗与 08:28 不是同一事件
08:49 的 glm-5 费用来自后续的正常 deepseek-v4-flash 请求被 Go 服务端替换,和 08:28 的 empty response 是两个独立的机制。08:31-08:49 之间有多次 API call(含一次 82s/11 calls 的大轮),这些请求被 Go 后端以 glm-5 执行。
触发前兆
模型漂移前通常出现以下信号:
- "The model returned no response" — deepseek-v4-flash 返回空响应
- 用户重试后请求成功,但实际由其他模型(如 GLM-5)处理
- 用量面板出现异常模型记录
影响
- 成本差异: GLM-5 比 deepseek-v4-flash 贵约 27 倍(Go 订阅分组定价)
- 行为差异: 不同模型的 context window、tokenizer 可能不同,影响压缩/摘要一致性
- 无法在前端锁定: 发送的 model 参数仅作为"请求偏好",Go 后端有权选择实际执行模型
应对方案
方案 A:接受(当前选择)
不做额外配置,接受 Go 订阅的池化行为。偶尔的模型漂移是订阅套餐的一部份。Auxiliary 保持 auto 模式。
方案 B:加 fallback 到本地 V100(当前配置)
当 opencode-go 故障时回退到本地 V100 GPU,不经过 Go 套餐。
fallback_providers:
- provider: V100
model: Qwen3.5-27B
⚠️ 注意事项: 当前配置未填写 base_url 和 api_key,依赖运行时从 custom_providers 列表查表解析。建议补全明确字段,减少解析链路引入的故障点:
fallback_providers:
- provider: V100
model: Qwen3.5-27B
base_url: ${V100_BASE_URL}
api_key: ${V100_API_KEY}
方案 C:配置 auxiliary 固定模型
限制辅助任务(压缩、摘要等)使用指定模型,避免辅助任务触发 fallback 链:
auxiliary:
compression:
provider: opencode-go
model: qwen3.5-plus
session_search:
provider: opencode-go
model: qwen3.5-plus
相关配置
# config.yaml(2026-05-25 状态)
model:
default: deepseek-v4-flash
provider: opencode-go
base_url: https://opencode.ai/zen/go/v1
api_key: ${OPENCODE_GO_API_KEY}
fallback_providers:
- provider: V100
model: Qwen3.5-27B
custom_providers:
- name: opencode-go
base_url: https://opencode.ai/zen/go/v1
api_key: ${OPENCODE_GO_API_KEY}
model: deepseek-v4-flash
models:
deepseek-v4-flash:
type: chat
qwen3.5-plus:
type: chat
- name: V100
base_url: ${V100_BASE_URL}
api_key: ${V100_API_KEY}
models:
Qwen3.5-27B:
type: chat
context_length: 262144
变更记录
- 2026-05-22 — 首次发现并记录:deepseek-v4-flash 请求被替换为 kimi-k2.6/GLM-5
- 2026-05-25 08:49 — deepseek-v4-flash 返回空响应,重试后用量面板显示 glm-5 消费 $0.0363
- 2026-05-25 — 本文档创建,系统记录模型漂移现象及应对方案
- 2026-05-25 — 更新 fallback 触发机制详解:no response 确实触发 fallback(路径 B,3 次重试后),但 V100 可能也失败了;补充 fallback 条目建议加显式 base_url
- 2026-05-25 — V100 base_url 脱敏处理,改为引用环境变量 ${V100_BASE_URL}