【AgentDaily】Hermes 升级后 reasoning_effort 导致 litellm 400 报错排查记
Hermes 升级后,调用本地 litellm 代理时全部模型 400,一个回归改动引发的血案!
问题现象
Hermes 升级后,调用本地模型时所有请求都返回 HTTP 400:
1 | HTTP 400: litellm.UnsupportedParamsError: openai does not support parameters: ['reasoning_effort'], |
主模型和 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 | model: |
使用的是 custom:local-kimi 这种命名自定义 provider,通过 custom_providers 列表定义,provider 名称带前缀。
升级后(当前 config.yaml):
1 | model: |
切换成了裸 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=custom 时 reasoning_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 | hermes config set agent.reasoning_overrides '{"GLM-5.2": null, "Kimi-K2.6": null}' |
但按这个方法执行后,问题依然存在。后续通过源码分析发现 null 被解析为 “未设置”,直接回退到全局 reasoning_effort: medium。
第四步:对照实验找到正确解法
做了三组对照实验:
- 场景 1:
reasoning_effort: medium+reasoning_overrides: null→ 400 报错 - 场景 2:
reasoning_effort: ""+reasoning_overrides: null→ 正常 - 场景 3:
reasoning_effort: ""+reasoning_overrides: {}→ 仍然正常
从 Hermes 源码里找到关键路径(hermes_constants.py):
1 | def resolve_reasoning_config(cfg, model=""): |
当 reasoning_overrides 里写 null 时:
parse_reasoning_effort(None)→None- 被判定为”未设置”,直接跳过
- 最终回退到全局
reasoning_effort: medium,仍然发射该参数 → 报错
所以 null 在这个设计里代表”未设置/回退”,而不是”禁用”。另一台电脑上之所以能工作,也是因为那台电脑的 agent.reasoning_effort 本身已经是空的。
根本原因
直接原因:Hermes v0.19.0 中 CustomProfile 的 build_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 | $ hermes config get agent.reasoning_effort |
输出为空即表示生效。新开会话后,请求不再带 reasoning_effort 参数,litellm 代理正常转发。
经验总结
升级后的行为变化要关注 ProviderProfile 改动:Hermes 的推理参数发射从 legacy path 迁移到 profile path 后,
provider=custom的行为发生了巨大变化。升级前不生效的代码突然生效,导致之前没暴露的问题浮出水面。reasoning_overrides的真实用途:按模型单独调高/调低 reasoning 力度(如 Claude 用high、GPT 用medium),而不是用来”禁用”参数。null会被解析为”未设置”,回退全局。YAML 语义陷阱:
none在 YAML 1.1 里是null的保留别名,会被解析成 PythonNone;字符串"none"则会被 CustomProfile 发射成reasoning_effort: "none"给 API。两者都不能实现”彻底不发”。全局清空 vs 按模型覆盖:如果代理端连
"none"都拒绝(比如 litellm 直接抛UnsupportedParamsError),只能从客户端全局清空reasoning_effort,不能用reasoning_overrides做 workaround。对照实验很重要:当某个配置组合”恰好能工作”时,不一定是新加的那个配置项起了作用,可能是旧配置本身就已经在起作用。通过逐项删除来验证真正原因。
litellm drop_params 方案:如果不想动 Hermes 配置,也可以在 litellm 代理端设置
drop_params: true全局丢弃不支持的参数。但客户端控制更精准,只影响特定会话。
参考资料
- Hermes commit
67df958db: fix(custom-provider): emit reasoning_effort at the live profile path - Hermes Agent Docs - Configuring Models: https://hermes-agent.nousresearch.com/docs/user-guide/configuring-models
- litellm drop_params docs: https://docs.litellm.ai/docs/proxy/configs#drop_params
创建时间:2026-07-22 · 本文档由 Hermes 用户排查并记录

