Provider 抽象层:让 Hermes 同时驾驭 30+ 个 LLM 提供商
声明式 Profile、插件化注册、三种传输协议、多认证路径、Fallback 链
系列:通过 Hermes 探秘 Agent 工程 | 第 9 篇 上一篇:Gateway 网关:连接 20+ 平台的统一消息路由
问题:一个 Agent,30+ 个大脑
Hermes 可以接入的 LLM 提供商超过 30 个:
- 商业 API:OpenAI、Anthropic、Google Gemini、xAI Grok、DeepSeek、Kimi
- 聚合器:OpenRouter、HuggingFace、Kilo Code、Novita
- 本地推理:Ollama、LM Studio、vLLM、llama.cpp
- 企业/云:Azure Foundry、AWS Bedrock、NVIDIA GMI Cloud
- 自定义:任何 OpenAI 兼容端点
每个提供商的差异很大:
- 传输协议:OpenAI Chat Completions、Anthropic Messages API、OpenAI Codex Responses API
- 认证方式:API Key、OAuth Device Code、OAuth External、外部进程
- 请求格式:extra_body 字段位置、reasoning 配置方式、温度参数处理
- 模型列表:有的有 REST 端点、有的只能静态配置
Hermes 的 Provider 抽象层用一个声明式 + 插件化的架构统一了这些差异。
ProviderProfile:声明式描述一切
providers/base.py 里的 ProviderProfile 是一个 dataclass,用声明式的方式描述一个 Provider 的所有行为特征:
@dataclass
class ProviderProfile:
name: str
api_mode: str = "chat_completions" # 传输协议
aliases: tuple = () # 别名(如 "kimi" → "kimi-coding")
# 认证 & 端点
env_vars: tuple = () # 环境变量名(按优先级)
base_url: str = "" # 推理端点
auth_type: str = "api_key" # api_key | oauth_device_code | oauth_external | external_process
# 视觉支持
supports_vision: bool = False
supports_vision_tool_messages: bool = True
# 模型目录
fallback_models: tuple = () # 静态模型列表(live fetch 失败时用)
# 客户端怪癖
default_headers: dict = field(default_factory=dict)
fixed_temperature: Any = None # None=调用方默认, OMIT_TEMPERATURE=不发送
default_max_tokens: int | None = None
default_aux_model: str = "" # 辅助任务用的便宜模型
# 钩子
def prepare_messages(self, messages): ...
def build_extra_body(self, *, session_id, **context): ...
def build_api_kwargs_extras(self, *, reasoning_config, **context): ...
def get_max_tokens(self, model): ...
def fetch_models(self, *, api_key, base_url, timeout): ...
关键设计:Profile 是声明式的——它描述 Provider 的行为,但不拥有客户端构造、凭证轮换或流式处理。那些职责在 AIAgent 层。
插件化注册发现
Provider Profile 通过插件目录注册,有两层位置:
1. 内置插件(bundled)
plugins/model-providers/<name>/ 目录,随 Hermes 一起发布:
plugins/model-providers/
├── anthropic/
│ ├── __init__.py # 调用 register_provider(profile)
│ └── plugin.yaml # 清单文件
├── openai-codex/
├── deepseek/
├── gemini/
├── ollama/
├── openrouter/
├── nous/
└── ...(30+ 个)
2. 用户插件(user)
$HERMES_HOME/plugins/model-providers/<name>/,用户自定义或第三方覆盖。
发现顺序
_discover_providers() 按顺序导入:
- 内置插件 → 先加载,提供基础覆盖
- 用户插件 → 后加载,同名覆盖内置(last-writer-wins)
- 旧版单文件 →
providers/<name>.py,向后兼容
这意味着:
- 用户可以覆盖任何内置 Profile(比如修改
base_url指向私有代理) - 第三方可通过插件添加新 Provider,无需修改 Hermes 源码
- 旧版单文件 Profile 仍然可用(平滑迁移)
三种传输协议
Hermes 支持三种 API 传输协议,Provider 通过 api_mode 声明:
| api_mode | 协议 | 典型 Provider |
|---|---|---|
chat_completions | OpenAI Chat Completions API | OpenRouter、DeepSeek、Kimi、Ollama、自定义 |
anthropic_messages | Anthropic Messages API | Anthropic 原生、MiniMax、Kimi Code(/coding 路由) |
codex_responses | OpenAI Codex Responses API | OpenAI Codex、xAI、OpenAI API(GPT-5.x) |
自动检测
runtime_provider.py 的 _detect_api_mode_for_url() 可以根据 base_url 自动推断协议:
api.openai.com→codex_responses(GPT-5.x 需要 Responses API)- URL 路径以
/anthropic结尾 →anthropic_messages api.kimi.com/coding→anthropic_messages
请求级怪癖
不同 Provider 的请求格式差异通过两个钩子处理:
build_extra_body():Provider 特定的 extra_body 字段(如 OpenRouter 的 reasoning 配置)build_api_kwargs_extras():返回(extra_body_additions, top_level_kwargs)元组,因为有些 Provider 把 reasoning 配置放在 extra_body,有些放在顶层 api_kwargs
四种认证路径
auth_type 字段决定了如何获取凭证:
| auth_type | 机制 | 典型 Provider |
|---|---|---|
api_key | 从环境变量读取 API Key | OpenRouter、DeepSeek、Gemini |
oauth_device_code | OAuth 2.0 Device Code 流程 | Nous Portal |
oauth_external | 外部浏览器 OAuth 流程 | OpenAI Codex、xAI、Qwen |
external_process | 外部进程获取凭证 | GitHub Copilot ACP |
API Key 凭证隔离
Hermes 有一个重要的安全设计:每个 Provider 的 API Key 只发往自己的 base URL。
# 不会把 OPENROUTER_API_KEY 发到 api.openai.com
# 不会把 OPENAI_API_KEY 发到 api.openai.com.attacker.test
runtime_provider.py 的 _host_derived_api_key() 会根据 base URL 的 hostname 自动推导对应的环境变量名(如 api.deepseek.com → DEEPSEEK_API_KEY),同时防御了域名仿冒攻击(api.deepseek.com.attacker.test 会推导出 ATTACKER_API_KEY,而不是 DEEPSEEK_API_KEY)。
OAuth Device Code 流程
以 Nous Portal 为例:
- Hermes 向 Portal 请求 device code
- 用户浏览器打开 Portal 页面,输入 user code
- Hermes 轮询 Portal 获取 access_token
- Token 持久化到
~/.hermes/auth.json,带文件锁保护 - 过期前 120 秒自动刷新
Credential Pool
agent/credential_pool.py 实现了凭证池——同一个 Provider 可以有多个 API Key,按轮询或失败率自动切换。
运行时解析
hermes_cli/runtime_provider.py 是 CLI、Gateway、Cron、ACP 共享的解析入口。
解析优先级
- 显式 CLI/运行时请求(如
hermes chat --provider anthropic) - config.yaml 的 model/provider 配置(用户通过
hermes model保存的选择) - 环境变量(如
OPENROUTER_API_KEY) - Provider 默认值或自动解析
config.yaml 优先于环境变量——这防止了一个过期的 shell export 静默覆盖用户在 hermes model 里选择的端点。
解析结果
运行时解析返回一个包含以下字段的数据结构:
provider:Provider IDapi_mode:传输协议base_url:推理端点api_key:凭证source:凭证来源(env / config / auth_store)- Provider 特定的元数据(如 token 过期时间)
Fallback Provider 链
Hermes 支持配置一个 Fallback Provider 列表——当主 Provider 出错时,按顺序尝试下一个。
触发点
_try_activate_fallback() 在三个场景被调用:
- API 响应无效(None choices、missing content)且重试耗尽
- 不可重试的客户端错误(HTTP 401、403、404)
- 瞬态错误(HTTP 429、500、502、503)且重试耗尽
激活流程
- 检查是否已激活(
_fallback_activated标志,防止重复触发) - 调用
resolve_provider_client()构建新客户端 - 确定
api_mode(anthropic →anthropic_messages,codex →codex_responses,其他 →chat_completions) - 原地替换:
self.model、self.provider、self.base_url、self.api_mode、self.client - 重新评估 prompt caching(Claude 模型在 OpenRouter 上启用)
- 重置重试计数为 0,继续循环
限制
- 子 Agent 不继承 fallback 配置——
delegate_tool只继承 Provider,不继承 fallback 链 - 辅助任务不走 fallback——它们有独立的 Provider 自动检测链
- Cron Job 支持 fallback——
run_job()读取config.yaml的fallback_providers并传给 AIAgent
辅助模型路由
辅助任务(视觉、上下文压缩、技能操作、MCP 辅助、记忆刷新)可以使用与主对话不同的 Provider/Model。
配置方式:
auxiliary:
provider: openrouter
model: google/gemini-2.0-flash-001
当 auxiliary.provider 设为 main 时,辅助任务走与主对话相同的运行时解析路径。这意味着:
- 环境变量驱动的自定义端点仍然生效
hermes model保存的自定义端点也生效- 辅助路由能区分"真正的自定义端点"和"OpenRouter fallback"
HermesOverlay:Hermes 特有的元数据
hermes_cli/providers.py 定义了 HermesOverlay,补充了 models.dev 目录不覆盖的 Hermes 特有信息:
@dataclass(frozen=True)
class HermesOverlay:
transport: str = "openai_chat" # 传输协议
is_aggregator: bool = False # 是否为聚合器(如 OpenRouter)
auth_type: str = "api_key" # 认证类型
extra_env_vars: tuple = () # models.dev 不追踪的额外环境变量
base_url_override: str = "" # 覆盖 models.dev 的 base_url
base_url_env_var: str = "" # 自定义 base URL 的环境变量名
这个 Overlay 让 Hermes 可以在不修改上游 models.dev 目录的前提下,为每个 Provider 添加 Hermes 特有的配置。
总结
Hermes 的 Provider 抽象层是一个声明式 + 插件化的架构:
| 层 | 职责 |
|---|---|
| ProviderProfile | 声明式描述 Provider 行为(协议、认证、怪癖) |
| 插件注册 | 内置 → 用户 → 旧版,last-writer-wins |
| 运行时解析 | 共享解析器,config.yaml 优先于 env |
| 传输适配 | 三种协议(chat_completions / anthropic_messages / codex_responses) |
| 认证系统 | 四种路径(api_key / oauth_device / oauth_external / external_process) |
| Fallback 链 | 主 Provider 失败时自动切换 |
| 辅助路由 | 辅助任务可独立配置 Provider |
这套设计让 Hermes 能用一个统一接口对接 30+ 个 LLM 提供商,同时保持高度的可扩展性——添加新 Provider 只需要放一个插件目录,修改内置 Profile 只需要放一个同名用户插件。
下一篇,也是本系列的最后一篇,我们将深入 Hermes 的沙箱与代码执行系统——execute_code 工具如何在隔离环境中安全运行代码。