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() 按顺序导入:

  1. 内置插件 → 先加载,提供基础覆盖
  2. 用户插件 → 后加载,同名覆盖内置(last-writer-wins)
  3. 旧版单文件providers/<name>.py,向后兼容

这意味着:

  • 用户可以覆盖任何内置 Profile(比如修改 base_url 指向私有代理)
  • 第三方可通过插件添加新 Provider,无需修改 Hermes 源码
  • 旧版单文件 Profile 仍然可用(平滑迁移)

三种传输协议

Hermes 支持三种 API 传输协议,Provider 通过 api_mode 声明:

api_mode协议典型 Provider
chat_completionsOpenAI Chat Completions APIOpenRouter、DeepSeek、Kimi、Ollama、自定义
anthropic_messagesAnthropic Messages APIAnthropic 原生、MiniMax、Kimi Code(/coding 路由)
codex_responsesOpenAI Codex Responses APIOpenAI Codex、xAI、OpenAI API(GPT-5.x)

自动检测

runtime_provider.py_detect_api_mode_for_url() 可以根据 base_url 自动推断协议:

  • api.openai.comcodex_responses(GPT-5.x 需要 Responses API)
  • URL 路径以 /anthropic 结尾 → anthropic_messages
  • api.kimi.com/codinganthropic_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 KeyOpenRouter、DeepSeek、Gemini
oauth_device_codeOAuth 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.comDEEPSEEK_API_KEY),同时防御了域名仿冒攻击api.deepseek.com.attacker.test 会推导出 ATTACKER_API_KEY,而不是 DEEPSEEK_API_KEY)。

OAuth Device Code 流程

以 Nous Portal 为例:

  1. Hermes 向 Portal 请求 device code
  2. 用户浏览器打开 Portal 页面,输入 user code
  3. Hermes 轮询 Portal 获取 access_token
  4. Token 持久化到 ~/.hermes/auth.json,带文件锁保护
  5. 过期前 120 秒自动刷新

Credential Pool

agent/credential_pool.py 实现了凭证池——同一个 Provider 可以有多个 API Key,按轮询或失败率自动切换。


运行时解析

hermes_cli/runtime_provider.py 是 CLI、Gateway、Cron、ACP 共享的解析入口。

解析优先级

  1. 显式 CLI/运行时请求(如 hermes chat --provider anthropic
  2. config.yaml 的 model/provider 配置(用户通过 hermes model 保存的选择)
  3. 环境变量(如 OPENROUTER_API_KEY
  4. Provider 默认值或自动解析

config.yaml 优先于环境变量——这防止了一个过期的 shell export 静默覆盖用户在 hermes model 里选择的端点。

解析结果

运行时解析返回一个包含以下字段的数据结构:

  • provider:Provider ID
  • api_mode:传输协议
  • base_url:推理端点
  • api_key:凭证
  • source:凭证来源(env / config / auth_store)
  • Provider 特定的元数据(如 token 过期时间)

Fallback Provider 链

Hermes 支持配置一个 Fallback Provider 列表——当主 Provider 出错时,按顺序尝试下一个。

触发点

_try_activate_fallback() 在三个场景被调用:

  1. API 响应无效(None choices、missing content)且重试耗尽
  2. 不可重试的客户端错误(HTTP 401、403、404)
  3. 瞬态错误(HTTP 429、500、502、503)且重试耗尽

激活流程

  1. 检查是否已激活(_fallback_activated 标志,防止重复触发)
  2. 调用 resolve_provider_client() 构建新客户端
  3. 确定 api_mode(anthropic → anthropic_messages,codex → codex_responses,其他 → chat_completions
  4. 原地替换self.modelself.providerself.base_urlself.api_modeself.client
  5. 重新评估 prompt caching(Claude 模型在 OpenRouter 上启用)
  6. 重置重试计数为 0,继续循环

限制

  • 子 Agent 不继承 fallback 配置——delegate_tool 只继承 Provider,不继承 fallback 链
  • 辅助任务不走 fallback——它们有独立的 Provider 自动检测链
  • Cron Job 支持 fallback——run_job() 读取 config.yamlfallback_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 工具如何在隔离环境中安全运行代码。