Skip to main content

第3章:模型与凭证 —— 多 Provider 不是多写几个 if

一、模型差异不止 URL​

Hermes 同时面对 OpenAI Chat Completions、OpenAI Responses、Anthropic、Bedrock、Gemini、Codex/Copilot、OpenRouter、本地端点、Nous Portal 和用户自定义端点。差异至少有四层:

  1. 认证:API key、OAuth、环境变量、key pool、managed identity;
  2. 请求协议:messages、responses、Anthropic content blocks 或云 SDK;
  3. 流式事件:文本 delta、tool call delta、reasoning、usage 和终止事件;
  4. 能力与计费:上下文长度、vision、reasoning、缓存、信用额度和错误语义。

如果每个 provider 都把这些逻辑直接塞进 conversation_loop.py,Loop 会变成协议分支的垃圾场。Hermes 的做法是把 provider identity、transport、credential、model metadata 和错误分类拆开。

二、三层模型调用边界​

1. Loop 层:只关心统一事件​

agent/conversation_loop.py 通过统一的 assistant message、tool calls、usage 和错误结果推进状态。它不应知道某个 SDK 的 header 名称。

2. Transport 层:翻译协议​

agent/transports/base.py 定义 ProviderTransport;chat_completions.py、anthropic.py、bedrock.py、codex.py 分别承载请求和流式事件的方言。agent/auxiliary_client.py 还提供压缩、标题、记忆等辅助请求的异步/同步适配。

3. Identity 层:决定“用谁的身份调用”​

agent/backend_identity.py、credential_pool.py、credential_sources.py 和 hermes_cli/auth.py 把 provider、profile、key、OAuth token 和 endpoint 的关系放在调用前解析。这样“换模型”不等于“重写 Loop”。

三、ProviderProfile 的价值​

providers/base.py:39 的 ProviderProfile 是配置和能力的描述边界。它允许上层用一个 profile 表达:

  • provider 名称与 base URL;
  • 默认模型和可选模型;
  • API 模式;
  • 认证来源;
  • 能力标签和兼容性限制。

当 hermes model 或配置文件切换 provider 时,变化主要发生在 profile/catalog/credential 选择;消息历史仍保持 Agent 层的格式。

四、凭证池:为什么单 key 不够​

长期运行的 Gateway 会遇到并发和配额。agent/credential_pool.py 的设计目标不是“保存更多 key”,而是把 key 变成可路由资源:

请求
→ provider/model 约束
→ 可用 credential 集合
→ 当前 profile / account 选择
→ 失败后标记冷却或切换
→ 记录 identity,避免跨账户污染

测试里出现的 test_credential_pool_routing.py、...rotation_bound.py、...provider_boundary.py 说明这些边界是行为契约,而不是实现细节。

五、Failover 的关键:身份和失败原因必须绑定​

“请求失败就换下一个”很危险。401 可能意味着 key 失效,429 可能意味着某个账户限流,400 可能意味着模型不支持某种内容,billing block 则可能应该提示用户而不是轮换一圈。

所以 Loop 里的恢复逻辑要保持:

request → classify_api_error → FailoverReason
→ credential/provider/model 决策
→ retry / refresh / fallback / user guidance

agent/error_classifier.py 和 conversation_loop.py 的 billing、entitlement、stale credential 分支共同完成这个决策。

六、Reasoning、Vision、Usage:统一的不只是文本​

模型适配的难点还在于“非文本内容”:

  • reasoning 可能是独立字段、content block 或隐藏在 provider 扩展里;
  • vision 需要控制图片尺寸和字节,否则历史中的大图会让未来请求持续 400;
  • usage 需要归一化,才能做预算、计费、压缩触发和 session insights。

agent/usage_pricing.py、model_metadata.py、message_content.py 和各 transport adapter 共同把这些差异压到统一对象。第 5 章会看到,usage 不只是展示数字,还会改变上下文压缩时机。

七、一个新增 Provider 的最小路径​

以“接入 OpenAI-compatible endpoint”为例,最小工作不是加一个 URL:

  1. 在 provider catalog/profile 中声明身份、默认模型和能力;
  2. 选择 Chat Completions 或 Responses transport;
  3. 定义 key/base URL/headers 的来源和安全校验;
  4. 确认 tool call、reasoning、image、usage 和错误是否能映射;
  5. 加入 provider routing、credential、empty stream、timeout 和 failover 测试;
  6. 在 CLI 的 model/config surfaces 中暴露选择;
  7. 在日志中只保留经过 redact 的身份信息。

八、设计精华​

  • 协议适配器不拥有业务循环:transport 只翻译,不决定任务是否完成。
  • 凭证是运行资源,不是全局变量:可轮换、可冷却、可按 provider 隔离。
  • 错误先结构化再展示:让恢复策略与用户文案分离。
  • 能力元数据进入上下文策略:模型上下文长度、图片能力和 reasoning 参数直接影响压缩与预算。