升级到 v0.22.6 后所有模型调用都失败?先看 SSL_CERT_FILE 这条线


公司网络里跑着一个会拆开 HTTPS 的代理,你不是第一天遇到它了。你早就把公司的根证书导出成一个 pem 包,export SSL_CERT_FILE=/etc/corp/ca-bundle.pem,curl、pip、git 全都正常。然后你升级了 Hermes,第一句话发出去就卡住,日志里反复出现 certificate is not trusted,重试三次之后只剩一句“Provider temporarily unavailable”。

这不是你的网络坏了,也不是代理配置错了。从 v0.22.6 开始,Hermes 的模型客户端在解析证书信任时走了一条新路径,而 SSL_CERT_FILE 里的那个包没有跟上去。

先确认你遇到的是同一个问题

症状很有辨识度:

  • 所有 provider 都失败,不只是某一家;换模型也没用。
  • 报错链是 httpcore.ConnectError → httpx.ConnectError → openai.APIConnectionError: Connection error.,中间夹一句 "[host] certificate is not trusted"。
  • 用 curl 或 openssl s_client 访问同一个 host 是通的,因为它们在读 SSL_CERT_FILE。
  • 同一台机器上把 Hermes 降回 v0.22.5,一切恢复正常。

如果这四条都中,基本可以确定是证书信任的解析路径问题,而不是网络策略。

代码里发生了什么

v0.22.6 引入的 agent/ssl_verify.py 里,resolve_httpx_verify() 负责回答“这个 HTTP 客户端该用什么证书校验”。它的逻辑顺序是:显式关掉校验 → 显式的 ssl_ca_cert 包 → 平台信任库。问题出在第三步附近:

if os.environ.get("SSL_CERT_FILE") or os.environ.get("SSL_CERT_DIR"):
    return _shared_context(None)

_shared_context(None) 的含义是“只用系统平台信任库,不加额外 CA 包”。也就是说,只要你导出了 SSL_CERT_FILE,反而会走到一个忽略它的分支上。

而这个模块自己的文档字符串写的是相反的契约:导出的 SSL_CERT_FILE 包应当在平台信任库之上叠加,只有显式的按 provider 配置(ssl_ca_cert、ssl_verify: false)才会替换掉这份信任。同文件里的 platform_ssl_context() 也确实实现了叠加语义。换句话说,环境变量这条分支和模块其余部分对不上。

更有意思的是 Linux 为什么看起来“没事”:在 Linux 上,truststore 的 OpenSSL 后端会调用 set_default_verify_paths(),而 OpenSSL 会自己读 SSL_CERT_FILE,于是信任“歪打正着”地加上了。macOS 走 SecTrustSetAnchorCertificates、Windows 走 CryptoAPI,都不查 OpenSSL 的环境变量,所以就硬失败。这也是为什么同一份公司配置,同事的 Mac 全挂、Linux 服务器却正常。

三种绕法,按推荐顺序

一、把根证书装进操作系统信任库(官方文档推荐的做法)

security 文档现在的口径很明确:企业 TLS 代理的根证书请按组织的操作系统流程安装。装好之后 SSL_CERT_FILE 设不设都不影响 provider 调用。macOS 用钥匙串把证书标记为“始终信任”,Windows 用 certlm.msc 导入到“受信任的根证书颁发机构”,Debian/Ubuntu 系把 pem 放到 /usr/local/share/ca-certificates/ 再 update-ca-certificates。这是最干净、升级不受影响的方案,缺点是需要管理员权限。

二、给 provider 显式指定 CA 包

如果你的机器不让你动系统信任库,可以在 provider 条目上直接指包:

providers:
  deepseek:
    api: https://api.deepseek.com
    key_env: DEEPSEEK_API_KEY
    ssl_ca_cert: ${env:SSL_CERT_FILE}

ssl_ca_cert 的语义是替换平台信任,只用于这个 endpoint 的 chat、模型元数据与目录探测。也就是说,它只保证这个 provider 通,不会影响别的服务;证书包路径写错时会打一条警告并回退到平台信任,不会静默失败。

三、临时关掉校验(只用于本地开发)

ssl_verify: false 能让调用立刻通,但它让你对所有中间人攻击敞开,尤其在公司的拦截代理后面,等于把代理也一起信了。官方文档对这条的措辞是“不要把它当作缺失根证书的长期修复”,我同意:拿它排查问题可以,别留在配置里。

这个问题会怎么修

修复 PR(#148253)的做法就是把那条分支的返回值从 _shared_context(None) 换成 platform_ssl_context(),让环境变量里的包重新叠加在平台信任之上。它同时解释了 Linux 上的“偶然正确”,并把 macOS/Windows 的真实行为写进了说明。

截至 v0.22.7 发布时这个 PR 还没有合并,所以你现在装的稳定版仍然是旧行为。如果你在等它,判断标准很简单:等一个版本之后重新验证,或者直接看 agent/ssl_verify.py 里那条分支返回的是不是 platform_ssl_context()。

一句提醒给团队

证书问题最容易被误判成“网络不通”或“provider 挂了”,因为表面症状就是连接错误。下次遇到“所有 provider 同时连不上”,先做一次分层验证:curl --cacert "$SSL_CERT_FILE" https://<host> 能不能通?能通,问题就在应用侧的信任解析,不在网络。这一步能省掉半小时的抓包。

延伸阅读:本地模型与自签证书、密钥与凭据的本地加密、v0.22.7 这个窗口里还有什么、安装与升级。