OpenCode Go 模型漂移记录

OpenCode Go 套餐模型漂移(Model Substitution)记录

OpenCode Go 订阅(opencode.ai/zen/go/v1)在请求时可能出现模型被服务器端透明替换的现象——发送的模型与实际执行的模型不一致。


现象描述

Hermes 配置为通过 opencode-go provider 请求 deepseek-v4-flash,但在 OpenCode 用量面板中显示实际消耗了 glm-5kimi-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-flashqwen3.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: 当前配置只有 providermodel,依赖 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_urlapi_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}