Hermes 升级后,调用本地 litellm 代理时全部模型 400,一个回归改动引发的血案!


问题现象

Hermes 升级后,调用本地模型时所有请求都返回 HTTP 400:

1
2
3
4
5
HTTP 400: litellm.UnsupportedParamsError: openai does not support parameters: ['reasoning_effort'],
for model=GLM-5.2. To drop these, set `litellm.drop_params=True` or for proxy:

`litellm_settings:
drop_params: true`

主模型和 fallback 模型全部报同样的错,会话完全无法使用。


背景

自建的 litellm 代理转发到内部模型服务。升级前使用不同厂商的独立 endpoint(Kimi、Qwen、GLM 各自有独立地址),升级后切换到了统一的 litellm 代理 endpoint。

Hermes 有一个 reasoning_effort 参数(推理力度控制),默认值是 medium。这个参数不是所有模型都支持的,经过 litellm 代理时会直接拦截并返回 400。litellm 提示要么在代理端设置 drop_params: true,要么从请求端去掉该参数。


排查过程

第一步:对比升级前后配置

升级前(state-snapshots/20260722-021530-pre-update/config.yaml):

1
2
3
4
model:
provider: custom:local-kimi
default: Kimi-K2.6
base_url: ${LOCAL_KIMI_URL}

使用的是 custom:local-kimi 这种命名自定义 provider,通过 custom_providers 列表定义,provider 名称带前缀。

升级后(当前 config.yaml):

1
2
3
4
model:
provider: custom
default: GLM-5.2
base_url: http://192.168.205.185:9876/v1

切换成了裸 provider: custom + 直接写 base_url

第二步:查阅源码找到真正根因

从 Hermes git history 里找到关键 commit(67df958db,2026-07-04):

1
fix(custom-provider): emit reasoning_effort at the live profile path

这个改动的说明很关键:

PR #57601’s original branch added a top-level reasoning_effort emit to the LEGACY build_kwargs path (agent/transports/chat_completions.py), but provider=custom resolves to CustomProfile (plugins/model-providers/custom/), so chat_completion_helpers takes the profile path and returns early — the added branch was unreachable dead code for every custom endpoint.

Move the fix to its real site, CustomProfile.build_api_kwargs_extras()

也就是说,在这个 commit 之前,provider=customreasoning_effort 参数根本不会发给 API(因为代码在 profile path 里是无效的 dead code)!而 2026-07-04 这个 commit 把 reasoning_effort 的发射逻辑移到了 CustomProfile 里,终于让它对 custom provider 生效了。

这个 commit 对应的 Hermes 版本是 v0.18.x 到 v0.19.0 之间。升级后代码生效了,所以之前不存在的 behavior 突然冒出来了。

第三步:验证 reasoning_overrides 无效

查到 Hermes 有 agent.reasoning_overrides 可以按模型覆盖该参数:

1
2
hermes config set agent.reasoning_overrides '{"GLM-5.2": null, "Kimi-K2.6": null}'
hermes gateway restart

但按这个方法执行后,问题依然存在。后续通过源码分析发现 null 被解析为 “未设置”,直接回退到全局 reasoning_effort: medium

第四步:对照实验找到正确解法

做了三组对照实验:

  • 场景 1:reasoning_effort: medium + reasoning_overrides: null400 报错
  • 场景 2:reasoning_effort: "" + reasoning_overrides: null正常
  • 场景 3:reasoning_effort: "" + reasoning_overrides: {}仍然正常

从 Hermes 源码里找到关键路径(hermes_constants.py):

1
2
3
4
5
6
def resolve_reasoning_config(cfg, model=""):
overrides = agent_cfg.get("reasoning_overrides") or {}
per_model = resolve_per_model_reasoning_effort(model, overrides)
if per_model is not None:
return per_model # 有覆盖才生效
# 没命中 → 回退到全局 reasoning_effort

reasoning_overrides 里写 null 时:

  • parse_reasoning_effort(None)None
  • 被判定为”未设置”,直接跳过
  • 最终回退到全局 reasoning_effort: medium,仍然发射该参数 → 报错

所以 null 在这个设计里代表”未设置/回退”,而不是”禁用”。另一台电脑上之所以能工作,也是因为那台电脑的 agent.reasoning_effort 本身已经是空的。


根本原因

直接原因:Hermes v0.19.0 中 CustomProfilebuild_api_kwargs_extras() 方法会把 reasoning_effort 参数发送给 provider=custom 的端点(model_metadata 路径),而之前版本这个逻辑是死代码,不会触发。

本质原因agent.reasoning_effort 的默认值为 medium。当代理端(litellm)不支持该参数时,就会返回 400。

对于 provider: custom 的端点,即使 reasoning_overrides 写了 null"none",CustomProfile 源码里仍然会发射 reasoning_effort 参数给 API(值变成 "none"),litellm 依然会拒绝。

唯一能让 Hermes 彻底不发该参数的方法,是清空全局默认值。


解决方案

直接清空全局默认值:

1
hermes config set agent.reasoning_effort ""

验证:

1
2
3
4
$ hermes config get agent.reasoning_effort

$ hermes config get agent.reasoning_overrides
{}

输出为空即表示生效。新开会话后,请求不再带 reasoning_effort 参数,litellm 代理正常转发。


经验总结

  1. 升级后的行为变化要关注 ProviderProfile 改动:Hermes 的推理参数发射从 legacy path 迁移到 profile path 后,provider=custom 的行为发生了巨大变化。升级前不生效的代码突然生效,导致之前没暴露的问题浮出水面。

  2. reasoning_overrides 的真实用途:按模型单独调高/调低 reasoning 力度(如 Claude 用 high、GPT 用 medium),而不是用来”禁用”参数。null 会被解析为”未设置”,回退全局。

  3. YAML 语义陷阱none 在 YAML 1.1 里是 null 的保留别名,会被解析成 Python None;字符串 "none" 则会被 CustomProfile 发射成 reasoning_effort: "none" 给 API。两者都不能实现”彻底不发”。

  4. 全局清空 vs 按模型覆盖:如果代理端连 "none" 都拒绝(比如 litellm 直接抛 UnsupportedParamsError),只能从客户端全局清空 reasoning_effort,不能用 reasoning_overrides 做 workaround。

  5. 对照实验很重要:当某个配置组合”恰好能工作”时,不一定是新加的那个配置项起了作用,可能是旧配置本身就已经在起作用。通过逐项删除来验证真正原因。

  6. litellm drop_params 方案:如果不想动 Hermes 配置,也可以在 litellm 代理端设置 drop_params: true 全局丢弃不支持的参数。但客户端控制更精准,只影响特定会话。


参考资料


创建时间:2026-07-22 · 本文档由 Hermes 用户排查并记录