Hermes Codex API Timeout 代理与 DNS 排障报告¶
生成时间: 2026-04-17T13:34:24+0800
1. Problem Description¶
本地部署的 Hermes Agent 使用 openai-codex provider 调用 Codex backend 时反复失败,典型错误为:
API call failed (attempt 1/3): APITimeoutError
Provider: openai-codex Model: gpt-5.4
Endpoint: https://chatgpt.com/backend-api/codex
Error: Request timed out.
用户环境中 Clash Verge Rev 已开启系统代理,混合代理端口为 127.0.0.1:7897。最初未开启 TUN,后来开启 TUN 后仍出现 API timeout。最终问题在不改 Hermes 源码的前提下,通过系统网络配置解决。
2. Environment and Scope¶
- 工作目录:
/Users/chesszyh987/.hermes - Hermes 源码目录:
/Users/chesszyh987/.hermes/hermes-agent - 当前源码分支:
main - 当前 HEAD:
816e3e37 test(feishu): cover new SDK event handler registrations - Hermes provider:
openai-codex - Codex endpoint:
https://chatgpt.com/backend-api/codex - Clash Verge Rev:
- mixed-port:
7897 - system proxy:已开启
- TUN:最终已开启
- Clash fake-ip DNS:
127.0.0.1:53可返回198.18.0.x - Tailscale:
- 之前启用了 Tailscale DNS
- 最终设置为
accept-dns=false - Hermes gateway:
- launchd plist:
/Users/chesszyh987/Library/LaunchAgents/ai.hermes.gateway.plist - 当前 PID:
9982
本报告只记录本地网络与配置排障。敏感信息如 token、cookie、OAuth 凭据、API key、代理节点完整凭据均不记录。
3. Symptoms and Reproduction¶
3.1 Hermes API 调用超时¶
最小 Codex 请求失败时的命令:
cd /Users/chesszyh987/.hermes/hermes-agent
./venv/bin/hermes chat -Q -t '' --max-turns 1 -q '只回复 OK 两个字母。'
失败结果:
API call failed after 3 retries: Request timed out.
3.2 显式走 Clash HTTP 代理时可到达 HTTP 层¶
curl -sS -I -L --max-time 15 \
-x http://127.0.0.1:7897 \
https://chatgpt.com/backend-api/codex
预期未授权/挑战响应,例如 403 或 401,说明代理和目标 HTTP 路径可达。
实际观察到:
proxy_http_code=403 total=0.162281 remote=127.0.0.1
3.3 不显式走 HTTP proxy 时出现 TLS EOF 或 timeout¶
在 TUN 初开但 DNS 仍异常时,模拟 Hermes 当前 custom transport 路径:
env -u http_proxy -u https_proxy -u HTTP_PROXY -u HTTPS_PROXY -u ALL_PROXY -u all_proxy \
curl -sS -I -L --max-time 15 https://chatgpt.com/backend-api/codex
曾观察到:
curl: (35) LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to chatgpt.com:443
direct_http_code=000
同时 Python/httpx custom transport 路径曾出现:
ConnectError: [SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol
4. Investigation Timeline¶
4.1 检查 Hermes 是否继承代理环境变量¶
发现当前 shell 有代理变量:
http_proxy=http://127.0.0.1:7897
https_proxy=http://127.0.0.1:7897
但 launchd 启动的 Hermes gateway 不可靠继承 shell/system proxy 环境。因此曾在 /Users/chesszyh987/.hermes/.env 中加入:
http_proxy=http://127.0.0.1:7897
https_proxy=http://127.0.0.1:7897
HTTP_PROXY=http://127.0.0.1:7897
HTTPS_PROXY=http://127.0.0.1:7897
ALL_PROXY=http://127.0.0.1:7897
all_proxy=http://127.0.0.1:7897
NO_PROXY=127.0.0.1,localhost
no_proxy=127.0.0.1,localhost
该配置能让普通 httpx/OpenAI SDK default transport 走代理,但对 Hermes 当前实际请求路径仍不够。
4.2 发现 Hermes 当前源码会注入 custom HTTPTransport¶
当前 Hermes upstream main 的 run_agent.py 会在创建 OpenAI client 时注入自定义 httpx.HTTPTransport,用于 TCP keepalive。这个 transport 会绕过 httpx 默认 trust_env proxy 处理路径。
验证对照:
default_httpx: status=403 elapsed=0.16s
custom_transport_like_hermes: ConnectTimeout: timed out elapsed=40.03s
因此,单纯在 Hermes .env 里设置 HTTP_PROXY/HTTPS_PROXY,无法保证当前 Hermes Codex 请求走 Clash HTTP proxy。
4.3 曾尝试源码补丁,但最终撤回¶
曾在本地做过小范围源码补丁:当存在 proxy env vars 时不注入 custom transport,并加了回归测试。补丁验证后可以让 Hermes 请求成功。
但用户希望不改 Hermes 代码,并且后来关闭了相关 PR、删除 fork、删除本地分支。因此最终方案回到纯配置层。
当前仓库状态确认:
## main...origin/main
M scripts/whatsapp-bridge/package-lock.json
当前仅剩无关 lockfile 改动,Hermes 源码没有保留该代理补丁。
4.4 开启 Clash TUN 后仍失败¶
用户开启 Clash Verge Rev TUN 后,配置显示:
enable_tun_mode: true
tun:
enable: true
stack: gvisor
auto-route: true
Clash 日志也显示 TUN 已启动:
[TUN] Tun adapter listening at: utun6([198.18.0.1/30],[fdfe:dcba:9876::1/126]), mtu: 9000, auto route: true, auto redir: false, ip stack: gVisor
但 Hermes 真实请求仍然超时。这说明问题不只是“是否开启 TUN”。
4.5 发现系统 DNS 被 Tailscale 接管并返回错误 IP¶
系统/Python 实际解析 chatgpt.com 时,曾返回明显异常的地址:
name: chatgpt.com
ip_address: 157.240.16.50
Python 也拿到同样结果:
('157.240.16.50', 443)
Clash service 日志对应显示 TUN 捕获的是错误目标 IP:
[TCP] 198.18.0.1:57616 --> 104.244.46.85:443 match Match using Proxy[tuic-udp-8443]
这类 IP 不是正常的 chatgpt.com 解析结果。TUN 捕获并转发了连接,但连接目标 IP 错误,TLS SNI 与目标 IP 不匹配,导致 TLS EOF 或超时。
4.6 Clash DNS 本身是正常的¶
直接查询 Clash DNS:
dig @127.0.0.1 -p 53 +short chatgpt.com
返回:
198.18.0.4
这说明 Clash fake-ip DNS 正常,问题在系统解析路径没有使用它。
4.7 确认 Tailscale DNS 接管¶
Tailscale DNS 状态显示:
Tailscale DNS: enabled.
Tailscale is configured to handle DNS queries on this device.
Run 'tailscale set --accept-dns=false' to revert to your system default DNS resolver.
同时系统 DNS 已配置成 127.0.0.1,但 Tailscale DNS 仍接管实际解析。
5. Root Cause¶
根因是两个配置/实现层叠加:
-
Hermes 当前 upstream 代码为 OpenAI/Codex client 注入 custom
httpx.HTTPTransport。该路径不会自动使用HTTP_PROXY/HTTPS_PROXY环境变量,因此在未开启 TUN 时,Hermes Codex 请求会绕过 Clash HTTP proxy,尝试直连chatgpt.com。 -
开启 Clash TUN 后,TUN 可以捕获直连流量,但 Tailscale DNS 仍接管了系统 DNS,导致
chatgpt.com被解析到错误 IP。TUN 捕获到的是错误 IP 的 TLS 连接,最终表现为 TLS EOF 或 Hermes API timeout。
症状是 APITimeoutError,但最终原因不是 Codex token、模型名或 Hermes provider 配置错误,而是“custom transport 绕过 env proxy + Tailscale DNS 污染/接管导致 TUN 目标 IP 错误”。
6. Changes Made¶
6.1 Hermes .env 代理变量¶
文件:
/Users/chesszyh987/.hermes/.env
加入代理变量:
http_proxy=http://127.0.0.1:7897
https_proxy=http://127.0.0.1:7897
HTTP_PROXY=http://127.0.0.1:7897
HTTPS_PROXY=http://127.0.0.1:7897
ALL_PROXY=http://127.0.0.1:7897
all_proxy=http://127.0.0.1:7897
NO_PROXY=127.0.0.1,localhost
no_proxy=127.0.0.1,localhost
注意:这对默认 httpx/OpenAI SDK 路径有用,但对当前 Hermes custom transport 路径不是充分条件。
6.2 Clash Verge Rev TUN¶
用户在 Clash Verge Rev 中开启 TUN。
确认文件:
/Users/chesszyh987/Library/Application Support/io.github.clash-verge-rev.clash-verge-rev/verge.yaml
/Users/chesszyh987/Library/Application Support/io.github.clash-verge-rev.clash-verge-rev/clash-verge.yaml
关键状态:
enable_tun_mode: true
tun:
enable: true
6.3 Wi-Fi DNS 改为 Clash DNS¶
执行:
networksetup -setdnsservers Wi-Fi 127.0.0.1
确认:
networksetup -getdnsservers Wi-Fi
输出:
127.0.0.1
6.4 关闭 Tailscale DNS 接管¶
执行:
/Applications/Tailscale.app/Contents/MacOS/Tailscale set --accept-dns=false
确认:
Tailscale DNS: disabled.
影响:这会影响 Tailscale MagicDNS,例如 *.taile68dad.ts.net 这类尾网域名解析。需要时可以恢复。
6.5 重启 Hermes gateway¶
执行:
cd /Users/chesszyh987/.hermes/hermes-agent
./venv/bin/hermes gateway restart
确认:
✓ Gateway service is loaded
PID = 9982
7. Verification¶
7.1 系统 DNS 当前返回 Clash fake-ip¶
dscacheutil -q host -a name chatgpt.com
输出:
name: chatgpt.com
ip_address: 198.18.0.4
Python 解析:
./venv/bin/python - <<'PY'
import socket
print(socket.getaddrinfo('chatgpt.com', 443, type=socket.SOCK_STREAM))
PY
输出:
[... ('198.18.0.4', 443) ...]
7.2 无显式代理的直连请求已由 TUN 接管¶
env -u http_proxy -u https_proxy -u HTTP_PROXY -u HTTPS_PROXY -u ALL_PROXY -u all_proxy \
curl -sS -I -L --max-time 15 \
https://chatgpt.com/backend-api/codex
输出:
direct_http_code=403 total=0.174835 remote=198.18.0.4
403 是未带真实认证的 HTTP 层响应,证明网络已到达目标服务/Cloudflare 层,不再是连接超时。
7.3 模拟 Hermes custom transport 路径成功¶
使用 httpx.Client(transport=httpx.HTTPTransport(...)) 模拟当前 Hermes 客户端创建方式,不依赖 env proxy:
custom_transport_no_env status=403 elapsed=0.21s
这证明即使 Hermes 绕过 env proxy,只要 TUN + DNS 正确,连接也能成功进入 Clash。
7.4 Hermes Codex 真实最小请求成功¶
cd /Users/chesszyh987/.hermes/hermes-agent
./venv/bin/hermes chat -Q -t '' --max-turns 1 -q '只回复 OK 两个字母。'
输出:
session_id: 20260417_132857_7cfa93
OK
8. Problems Encountered During Debugging¶
8.1 “系统代理已开启”不等于后台进程一定使用代理¶
macOS 系统代理主要面向遵循系统网络设置的应用。launchd 后台服务、Python/httpx、OpenAI SDK 是否使用代理取决于环境变量、库默认行为、transport 实现等。
8.2 Hermes .env 代理变量不是充分条件¶
Hermes 会加载 /Users/chesszyh987/.hermes/.env,但当前 Codex/OpenAI client 注入了 custom HTTPTransport,该路径不走 httpx 默认环境代理处理。
8.3 TUN 开启后仍可能失败¶
TUN 解决的是“流量捕获”,不是“DNS 正确性”。如果系统解析已经把域名解析到错误 IP,TUN 只会把错误 IP 的连接转发出去,TLS 仍会失败。
8.4 dig @127.0.0.1 正常不代表系统实际解析正常¶
Clash DNS 单独查询正常:
198.18.0.4
但系统 resolver/Python 实际解析曾仍返回错误 IP。必须用 dscacheutil 和 Python socket.getaddrinfo() 验证真实应用路径。
8.5 Tailscale DNS 是隐藏干扰源¶
networksetup -getdnsservers Wi-Fi 显示 127.0.0.1 后,系统仍可能被 Tailscale DNS extension 接管。需要用:
/Applications/Tailscale.app/Contents/MacOS/Tailscale dns status
确认 Tailscale DNS 是否启用。
8.6 管理员权限限制¶
曾考虑用 /etc/resolver/chatgpt.com 和 /etc/resolver/openai.com 做 per-domain resolver,但当前会话无权限创建 /etc/resolver:
mkdir: /etc/resolver: Permission denied
因此最终选择关闭 Tailscale DNS 接管,而不是写系统 resolver 文件。
9. Reuse Notes and Lessons¶
- 如果 Hermes/Codex 出现
APITimeoutError,先区分三层: - HTTP proxy 显式请求是否可达;
- Hermes 当前 transport 是否绕过 env proxy;
- TUN 捕获后的 DNS/IP 是否正确。
- 不要只看
HTTP_PROXY/HTTPS_PROXY是否存在。customhttpx.HTTPTransport可能绕过它。 - TUN 模式需要配套 DNS。Clash fake-ip 模式下,应用实际解析应得到
198.18.0.x,而不是污染公网 IP。 - 在 macOS 上验证实际 DNS 路径,优先用:
dscacheutil -q host -a name <domain>- Python
socket.getaddrinfo() scutil --dns- 如果同时使用 Tailscale 和 Clash,Tailscale DNS 可能抢占系统 resolver。必要时可临时关闭:
/Applications/Tailscale.app/Contents/MacOS/Tailscale set --accept-dns=false
恢复命令:
/Applications/Tailscale.app/Contents/MacOS/Tailscale set --accept-dns=true
- 当前最终方案没有改 Hermes 源码。升级 Hermes 后,只要 Clash TUN、系统 DNS 和 Tailscale DNS 状态保持一致,理论上不应再次触发同一类 timeout。
10. Appendix: Reusable Commands¶
Hermes 最小 Codex 验证¶
cd /Users/chesszyh987/.hermes/hermes-agent
./venv/bin/hermes chat -Q -t '' --max-turns 1 -q '只回复 OK 两个字母。'
检查 Hermes gateway¶
cd /Users/chesszyh987/.hermes/hermes-agent
./venv/bin/hermes gateway status
./venv/bin/hermes gateway restart
检查 Clash HTTP proxy 是否可达¶
curl -sS -I -L --max-time 15 \
-x http://127.0.0.1:7897 \
https://chatgpt.com/backend-api/codex
检查不显式代理时是否被 TUN 接管¶
env -u http_proxy -u https_proxy -u HTTP_PROXY -u HTTPS_PROXY -u ALL_PROXY -u all_proxy \
curl -sS -I -L --max-time 15 \
https://chatgpt.com/backend-api/codex
检查 Clash DNS¶
dig @127.0.0.1 -p 53 +short chatgpt.com
dig @127.0.0.1 -p 53 +short openai.com
检查系统实际 DNS¶
dscacheutil -q host -a name chatgpt.com
scutil --dns | sed -n '1,120p'
检查 Python 应用实际解析¶
cd /Users/chesszyh987/.hermes/hermes-agent
./venv/bin/python - <<'PY'
import socket
print(socket.getaddrinfo('chatgpt.com', 443, type=socket.SOCK_STREAM))
PY
设置 Wi-Fi DNS 为 Clash DNS¶
networksetup -setdnsservers Wi-Fi 127.0.0.1
networksetup -getdnsservers Wi-Fi
刷新 macOS DNS 缓存¶
dscacheutil -flushcache
killall -HUP mDNSResponder
检查 Tailscale DNS¶
/Applications/Tailscale.app/Contents/MacOS/Tailscale dns status
/Applications/Tailscale.app/Contents/MacOS/Tailscale debug prefs
关闭或恢复 Tailscale DNS 接管¶
/Applications/Tailscale.app/Contents/MacOS/Tailscale set --accept-dns=false
/Applications/Tailscale.app/Contents/MacOS/Tailscale set --accept-dns=true
查看 Clash Verge Rev TUN 状态¶
sed -n '16,75p' "$HOME/Library/Application Support/io.github.clash-verge-rev.clash-verge-rev/verge.yaml"
sed -n '60,76p' "$HOME/Library/Application Support/io.github.clash-verge-rev.clash-verge-rev/clash-verge.yaml"
tail -n 120 "$HOME/Library/Application Support/io.github.clash-verge-rev.clash-verge-rev/logs/service/service_latest.log"