Step 4: 模型适配层
04 - 模型适配层深度分析
概述
Hermes Agent 的模型适配层位于 agent/ 目录,负责将 Hermes 统一的 OpenAI 风格内部消息格式翻译为各个 LLM Provider 的原生 API 协议,并将响应归一化回 OpenAI ChatCompletion 形状以供 run_agent.py 消费。同时,该层还管理凭证池、辅助客户端路由、智能模型选择、限流守卫和模型元数据索引。
架构总览
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
┌──────────────────────────────────────────────────────────────────┐
│ run_agent.py (Agent Loop) │
│ 统一消费 OpenAI ChatCompletion 形状 │
└──────────────┬───────────────────────────────────┬───────────────┘
│ │
┌──────▼──────┐ ┌────────▼────────┐
│ 主模型调用 │ │ 辅助任务调用 │
│ (Anthropic/ │ │ (压缩/摘要/视觉 │
│ Bedrock/ │ │ /搜索/技能) │
│ Gemini/ │ └────────┬────────┘
│ OpenAI SDK) │ │
└──────┬──────┘ ┌──────────▼──────────┐
│ │ auxiliary_client.py │
┌───────────▼───────────────┐ │ provider 路由 + 链式 │
│ │ │ fallback + call_llm() │
│ ┌─────────────────────┐ │ └──────────┬──────────┘
│ │ anthropic_adapter.py│ │ │
│ │ bedrock_adapter.py │ │ ┌──────────▼──────────┐
│ │ gemini_cloudcode_ │ │ │ credential_pool.py │
│ │ adapter.py │ │ │ 凭证轮转 + OAuth 刷新 │
│ └─────────────────────┘ │ └──────────┬──────────┘
│ │ │
└───────────────────────────┘ ┌──────────▼──────────┐
│ smart_model_routing.py│
│ 简单消息→便宜模型 │
└──────────┬──────────┘
│
┌─────────────────────▼───────────────────┐
│ │
┌─────────▼────────┐ ┌─────────▼────────┐
│rate_limit_tracker│ │ nous_rate_guard │
│ 通用限流头解析 │ │ Nous 专属文件锁 │
└─────────────────┘ └──────────────────┘
┌─────────────────┐ ┌──────────────────┐
│ model_metadata │ │ models_dev.py │
│ context_length │◄──────────────────│ models.dev 注册表 │
│ 探测 + 缓存 │ │ 集成 (离线优先) │
└─────────────────┘ └──────────────────┘
各模块职责详解
1. anthropic_adapter.py (1,520 行) — Anthropic 适配器
职责:将 OpenAI 风格消息/工具/响应翻译为 Anthropic Messages API 格式,并管理 Anthropic 全套认证链路。
核心功能层:
| 层 | 功能 | 行数估算 |
|---|---|---|
| 认证 (1-700) | OAuth PKCE 流、Claude Code 凭证读写/刷新、API Key / OAuth / 第三方 Bearer 三路检测 | ~700 |
| 消息转换 (825-1263) | convert_messages_to_anthropic() — 系统/用户/助手/工具消息转换、思考块签名管理、角色交替合并、孤立块清理 |
~440 |
| 请求构建 (1266-1451) | build_anthropic_kwargs() — thinking/adaptive 配置、采样参数禁用、OAuth mcp_ 前缀、fast mode |
~185 |
| 响应归一化 (1454-1520) | normalize_anthropic_response() — text/thinking/tool_use 块提取、stop_reason 映射 |
~66 |
认证优先级(resolve_anthropic_token()):
ANTHROPIC_TOKENenv(Hermes 管理的 OAuth/setup token)CLAUDE_CODE_OAUTH_TOKENenv~/.claude/.credentials.jsonClaude Code 凭证文件(支持 refresh)ANTHROPIC_API_KEYenv(普通 API Key)
关键设计:
- 思考块签名管理:第三方端点(MiniMax 等)无法验证 Anthropic 专属签名 → 全量剥离;直连 Anthropic 仅保留最后一轮助手消息的签名块
- 自适应思考 (Adaptive Thinking):4.6+ 模型使用
type: "adaptive"+output_config.effort;4.7+ 禁止temperature/top_p/top_k;xhigheffort 仅 4.7+ 支持 - OAuth Claude Code 兼容:自动加
mcp_前缀 +You are Claude Code系统 prompt + 产品名替换
风险点:
resolve_anthropic_token()有 4 层 fallback + Claude Code 凭证刷新,任何一层失败都静默降级,排查困难_write_claude_code_credentials()直接操作~/.claude/.credentials.json(非 Hermes 管理的文件),多人 profile 可能冲突convert_messages_to_anthropic()过于复杂(~440 行),含 3 遍清理循环(孤立 tool_use → 孤立 tool_result → 角色交替合并),任何新增消息类型都需在此添加处理逻辑
2. bedrock_adapter.py (1,098 行) — AWS Bedrock 适配器
职责:通过 AWS Bedrock Converse API 与所有 Bedrock 可用模型交互,绕过 OpenAI 兼容端点,获得完整 Claude 特性(prompt caching、thinking budgets、adaptive thinking)。
核心功能层:
| 层 | 功能 |
|---|---|
| 认证 (42-193) | boto3 凭证链检测、区域解析 |
| 消息转换 (251-451) | convert_messages_to_converse() / convert_tools_to_converse() |
| 响应归一化 (457-698) | normalize_converse_response() / stream_converse_with_callbacks() |
| 模型发现 (827-988) | discover_bedrock_models() — foundation models + inference profiles,1h TTL 缓存 |
| 错误分类 (993-1045) | classify_bedrock_error() — context_overflow / rate_limit / overloaded |
| Context 长度表 (1054-1098) | BEDROCK_CONTEXT_LENGTHS 静态映射 + 子串匹配查找 |
关键设计:
- 双通道路由:
is_anthropic_bedrock_model()判断模型 ID → Claude 模型走 AnthropicBedrock SDK(全特性),非 Claude 走 Converse API - 流式回调:
stream_converse_with_callbacks()支持on_text_delta/on_tool_start/on_reasoning_delta/on_interrupt_check四个回调 - 工具拒绝列表:DeepSeek R1、Stability、embedding 模型不支持 tool_use → 自动剥离工具定义
风险点:
discover_bedrock_models()使用模块级 dict 缓存,在 AWS credential 轮换场景下可能返回过期数据normalize_converse_response()用SimpleNamespace模拟 OpenAI SDK 对象,缺少类型安全保护
3. gemini_cloudcode_adapter.py (764 行) — Gemini CloudCode 适配器
职责:将 Hermes 的 OpenAI 风格请求翻译为 Google Cloud Code Assist API 的 {project, model, user_prompt_id, request} 封装格式,通过 OAuth PKCE 认证访问 cloudcode-pa.googleapis.com。
核心组件:
| 组件 | 功能 |
|---|---|
GeminiCloudCodeClient |
OpenAI SDK 兼容外观类(.chat.completions.create() 接口) |
build_gemini_request() |
OpenAI messages → Gemini contents + systemInstruction + toolConfig |
_translate_gemini_response() |
Gemini candidates → OpenAI choices |
_iter_sse_events() |
SSE 流式解析 |
_translate_stream_event() |
实时流 delta 转换 |
关键设计:
thoughtSignature: "skip_thought_signature_validator"哨兵值,避免 Code Assist 拒绝非自身发起的 functionCall- 所有请求包裹
wrap_code_assist_request()— project + model + user_prompt_id 信封 - OAuth 令牌通过
agent.google_oauth模块获取,project context 懒初始化并缓存 - 流式支持通过 httpx SSE 实现,返回
_GeminiStreamChunk(SimpleNamespace子类)模拟 OpenAIChatCompletionChunk
风险点:
- Code Assist API 是逆向工程的 undocumented 接口,随时可能变化
GeminiCloudCodeClient硬编码 900s 超时,对于长时间推理可能不够- 缺少重试/错误恢复机制——HTTP 错误直接抛出
CodeAssistError
4. auxiliary_client.py (2,716 行) — 辅助 LLM 客户端路由
为何达 2,716 行? 因为它是整个辅助 LLM 调用的全能路由中心,包含:
- 10+ 条 Provider 解析链(
_try_openrouter,_try_nous,_try_codex,_try_custom_endpoint,_resolve_api_key_provider,_try_anthropic等),每条都有凭证查找 + base_url 解析 + 特殊处理 - 3 个 API 格式适配器(
CodexAuxiliaryClient、AnthropicAuxiliaryClient、普通 OpenAI),含同步 + 异步版本(6 个类) - 支付故障自动降级(
_try_payment_fallback()) - 视觉任务专用路由(
resolve_vision_provider_client()) - 集中式 LLM 调用 API(
call_llm()/async_call_llm()) - 客户端缓存系统(
_client_cache+ loop 一致性检测 + FD 泄漏防护)
| 功能区 | 核心函数/类 | 行数估算 |
|---|---|---|
| Codex Responses→Chat 适配器 | CodexAuxiliaryClient / _CodexCompletionsAdapter |
~210 |
| Anthropic 适配器 | AnthropicAuxiliaryClient / _AnthropicCompletionsAdapter |
~115 |
| Provider 解析链(10+ 条) | _try_* / _resolve_auto() |
~250 |
| Central Provider Router | resolve_provider_client() |
~390 |
| Public API | get_text_auxiliary_client() / call_llm() |
~400 |
| 客户端缓存 | _get_cached_client() / shutdown_cached_clients() |
~200 |
| 视觉路由 | resolve_vision_provider_client() |
~100 |
| 支付降级 | _try_payment_fallback() / _is_payment_error() |
~100 |
自动检测优先级(文本任务):
- 用户主 Provider(非聚合器如 OpenRouter/Nous)→ 直接使用
- OpenRouter → Nous Portal → 自定义端点 → Codex OAuth → API-key 提供商 → Anthropic
- 支付/连接错误 → 自动跳过失败 Provider 尝试下一个
风险点:
resolve_provider_client()超过 390 行,包含 8 种 provider 分支 + 嵌套条件,圈复杂度极高_resolve_auto()全局修改auxiliary_is_nous状态变量,非线程安全call_llm()在 ~270 行的 try/except 中处理所有 Provider 的所有错误类型,错误信息可能被吞掉- 文件承担了太多职责——凭证解析、API 格式适配、客户端生命周期管理、路由决策、缓存——应考虑拆分
拆分建议:
auxiliary_providers.py— Provider 解析链 +resolve_provider_client()auxiliary_adapters.py—CodexAuxiliaryClient+AnthropicAuxiliaryClient+ 适配器类auxiliary_cache.py— 客户端缓存 + 生命周期管理auxiliary_client.py— 保留公共 API (call_llm(),get_text_auxiliary_client()等) + 配置解析
5. credential_pool.py (1,418 行) — 凭证池管理
职责:为同一 Provider 管理多凭证的轮转选择、OAuth 令牌刷新、耗尽标记与冷恢复。
核心数据模型:
1
2
3
4
5
6
7
8
9
10
11
12
@dataclass
class PooledCredential:
provider: str
id: str
label: str
auth_type: str # "oauth" | "api_key"
priority: int # 排序优先级
source: str # "manual" | "device_code" | "claude_code" | "hermes_pkce" | "env:*"
access_token: str
refresh_token: Optional[str]
# ... 耗尽状态跟踪字段
# ... OAuth 元数据字段(agent_key, client_id, scope 等)
选择策略:
| 策略 | 行为 |
|---|---|
fill_first |
按优先级排序,选第一个可用 |
round_robin |
轮转,每次选中后降低其优先级 |
random |
随机选择 |
least_used |
选 request_count 最小的 |
关键机制:
- 耗尽标记 + 冷却:429 → 1h 冷却;402/信用耗尽也标记;含
reset_at时间戳的误差优先使用 - OAuth 刷新:Anthropic 使用
refresh_anthropic_oauth_pure();Codex 使用refresh_codex_oauth_pure();Nous 使用refresh_nous_oauth_from_state() - 跨进程同步:Anthropic 的 Claude Code 凭证和 Codex 的
~/.codex/auth.json在刷新后会写回外部文件,多 profile/多进程场景下通过文件变化检测避免refresh_token_reused错误 - 自动播种:
_seed_from_singletons()在load_pool()时自动从 auth.json / env vars / Claude Code 凭证文件发现并插入凭证 - 软租约:
acquire_lease()/release_lease()用于 gateway 并发请求分配
风险点:
_refresh_entry()超过 190 行,含 3 种 Provider 的刷新逻辑 + 双重重试 + 文件同步,极难测试完整路径_seed_from_singletons()对is_provider_explicitly_configured("anthropic")的依赖意味着如果用户只用辅助通道而未显式配置 anthropic,凭证池不会自动播种 Claude Code 令牌(设计如此,但可能令人困惑)CredentialPool不是线程安全的——_lock保护select()/mark_exhausted_and_rotate()但不保护_refresh_entry()中的文件 I/O
6. smart_model_routing.py (195 行) — 智能模型路由
职责:对于”简单”用户消息,自动路由到更便宜的模型以节省成本。
决策逻辑(choose_cheap_model_route()):
输入消息必须同时满足以下全部条件才路由到便宜模型:
routing_config.enabled == true- 长度 ≤
max_simple_chars(默认 160) - 词数 ≤
max_simple_words(默认 28) - 行数 ≤ 2(无多行内容)
- 无代码块(
\或 ` ``` `) - 无 URL
- 不含复杂关键词(debug, implement, refactor, traceback, test, cron, docker 等 32 个)
风险点:
- 关键词黑名单是硬编码英文,不覆盖其他语言
- 没有学习/自适应机制——不会根据实际结果调整路由决策
- 路由决策与
resolve_runtime_provider耦合——如果运行时解析失败,静默回退到主模型,但不会记录原因
7. model_metadata.py (1,116 行) — 模型元数据
职责:解析和缓存 LLM 模型的上下文窗口大小、最大输出 token 数等信息。
Context Length 解析优先级(get_model_context_length()):
| 优先级 | 来源 | 缓存 |
|---|---|---|
| 0 | config.yaml 显式配置 |
— |
| 1 | 持久化磁盘缓存 ~/.hermes/context_length_cache.yaml |
YAML |
| 2 | 自定义端点 /models API |
内存 5min |
| 3 | 本地服务器查询(Ollama / LM Studio) | 磁盘 |
| 4 | Anthropic /v1/models API |
无 |
| 5 | Bedrock 静态表 | — |
| 6 | Nous → OpenRouter 后缀匹配 | 内存 1h |
| 7 | models.dev Registry | 内存 1h |
| 8 | 硬编码 DEFAULT_CONTEXT_LENGTHS |
— |
| 9 | 默认 128K | — |
辅助功能:
detect_local_server_type()— 探测本地服务器类型(Ollama / LM Studio / vLLM / llama.cpp)query_ollama_num_ctx()— 查询 Ollama 模型的运行时上下文长度parse_context_limit_from_error()— 从 API 错误信息中提取上下文限制estimate_tokens_rough()— 基于 4 字符/token 的粗略估计
硬编码表(DEFAULT_CONTEXT_LENGTHS):约 55 个条目,覆盖 Claude / GPT / Gemini / DeepSeek / Llama / Qwen / xAI Grok / MiniMax 等主要模型家族。使用子串匹配(最长键优先),因此需要按长度降序排列。
风险点:
- 硬编码表需要持续维护——新模型发布后如果不更新,用户会得到 128K 默认值
fetch_model_metadata()使用同步requests.get(),在异步上下文中可能阻塞query_ollama_num_ctx()的 3s 超时在高负载 Ollama 上可能不够_query_local_context_length()嵌套了多层 try/except 静默吞错,调试困难
8. models_dev.py (586 行) — models.dev 注册表集成
职责:从 https://models.dev/api.json 获取全量模型注册表(4,000+ 模型 / 109+ 提供商),提供模型能力查询。
数据流:
1
2
3
4
network fetch ──► 内存缓存 (1h TTL) ──► 磁盘缓存 (~/.hermes/models_dev_cache.json)
│
▼
ProviderInfo / ModelInfo dataclasses
提供的查询接口:
| 函数 | 用途 |
|---|---|
lookup_models_dev_context() |
查某个 Provider+模型的上下文长度 |
get_model_capabilities() |
查模型的完整能力元数据(reasoning, tools, vision 等) |
get_provider_info() |
查 Provider 信息(名称、API URL、env vars) |
list_agentic_models() |
列出某 Provider 支持工具调用的模型 |
Provider ID 映射(PROVIDER_TO_MODELS_DEV):将 Hermes 的 Provider 名(如 "kimi-coding")映射到 models.dev 的 Provider ID(如 "kimi-for-coding")。
风险点:
- 磁盘缓存使用
atomic_json_write()但内存缓存没有过期清理机制,长期运行进程可能使用过期数据 fetch_models_dev()也是同步的,在 gateway 异步上下文中可能阻塞_NOISE_PATTERNS用正则过滤模型列表,但规则相对主观
9. nous_rate_guard.py (182 行) — Nous 限流守卫
职责:跨会话的 Nous Portal 限流状态共享,防止 429 重试放大。
机制:
record_nous_rate_limit()— 解析x-ratelimit-reset-requests-1h/retry-after头写入~/.hermes/rate_limits/nous.jsonnous_rate_limit_remaining()— 读取文件,返回剩余秒数;过期时自动删除clear_nous_rate_limit()— 成功请求后清除状态
风险点:
- 文件 I/O 无锁——并发会话可能竞争写入(但
os.replace()原子操作缓解了此问题) - 默认冷却时间 300s 可能过长或过短,取决于 Nous 的实际限流窗口
10. rate_limit_tracker.py (246 行) — 通用限流追踪器
职责:从 Provider 响应头中提取 12 个 x-ratelimit-* 标准化头部,提供格式化显示。
数据模型:
1
2
3
4
5
6
7
8
9
10
11
RateLimitState
├── requests_min: RateLimitBucket (RPM)
├── requests_hour: RateLimitBucket (RPH)
├── tokens_min: RateLimitBucket (TPM)
└── tokens_hour: RateLimitBucket (TPH)
RateLimitBucket
├── limit: int
├── remaining: int
├── reset_seconds: float
└── captured_at: float
输出格式:
format_rate_limit_display()— 多行终端显示(进度条 + 百分比)format_rate_limit_compact()— 单行紧凑格式用于 gateway 消息
适配器接口模式
所有适配器遵循相同的翻译模式:
1
OpenAI 格式 (内部) ──► [适配器] ──► Provider 原生格式 ──► API 调用 ──► 原生响应 ──► [适配器] ──► OpenAI 格式 (内部)
具体翻译对照:
| 概念 | OpenAI 内部格式 | Anthropic | Bedrock Converse | Gemini CloudCode |
|---|---|---|---|---|
| 系统消息 | {"role": "system"} |
独立 system 参数 |
独立 system 列表 |
systemInstruction 字段 |
| 工具定义 | {"function": {"name", "parameters"}} |
{"name", "input_schema"} |
{"toolSpec": {"name", "inputSchema"}} |
{"functionDeclarations": [...]} |
| 工具调用 | tool_calls[].function |
tool_use content block |
toolUse content block |
functionCall part |
| 工具结果 | {"role": "tool"} |
tool_result content block (user role) |
toolResult content block (user role) |
functionResponse part (user role) |
| 思考/推理 | reasoning / reasoning_content |
thinking / redacted_thinking blocks |
reasoningContent delta |
thought: true part |
| 停止原因 | stop / tool_calls / length |
end_turn / tool_use / max_tokens |
end_turn / tool_use / max_tokens |
STOP / MAX_TOKENS / SAFETY |
统一接口特征:
- 所有适配器的输出都是
SimpleNamespace对象,模拟 OpenAI SDK 的.choices[0].message结构 - 异步适配器通过
asyncio.to_thread()包装同步调用 - 流式响应通过 SSE 或 SDK 流式接口消费后归一化为完整响应
代码质量评估
问题
-
anthropic_adapter.py— 职责过多:认证(OAuth PKCE、Claude Code 凭证管理)、消息转换、请求构建、响应归一化全部在一个文件。认证逻辑可独立为anthropic_auth.py。 -
auxiliary_client.py— 上帝文件:2700+ 行承担凭证解析、API 格式适配、客户端缓存、路由决策、支付降级、视觉路由等 6+ 职责。圈复杂度极高——仅resolve_provider_client()就有 30+ 分支。 -
SimpleNamespace 类型不安全:所有适配器的返回值都用
SimpleNamespace模拟 OpenAI SDK 类型。缺少 pydantic/dataclass 验证,任何字段拼写错误都只在运行时暴露。 -
credential_pool.py_refresh_entry()过长:超过 190 行,含 3 种 Provider 的刷新逻辑 + 文件同步 + 双重重试。应拆分为 Provider-specific 刷新策略类。 -
全局可变状态:
auxiliary_is_nous全局变量在_resolve_auto()中被设置_model_metadata_cache/_models_dev_cache等模块级 dict 无过期清理_bedrock_runtime_client_cache/_bedrock_control_client_cache无大小限制
风险点
-
OAuth 令牌跨进程冲突:Anthropic 和 Codex 的 refresh token 是单次使用的。多个 Hermes profile 或 Claude Code CLI 同时刷新时,先刷新者使后者的 token 失效。当前通过文件同步检测缓解,但不是原子操作。
-
辅助客户端静默降级链:
_resolve_auto()顺次尝试 5+ Provider,任何一个的异常都被except Exception: pass吞掉。用户可能不知道辅助任务实际跑在了哪个 Provider 上。 -
convert_messages_to_anthropic()的单向性:只处理 OpenAI → Anthropic 方向,且包含多遍清理。如果消息格式有新类型(如input_audio),需在此处手动添加处理。 -
Bedrock 客户端缓存无失效:
_bedrock_runtime_client_cache按区域缓存 boto3 客户端,但 AWS credential 轮换后旧客户端的 session token 可能失效。 -
模型元数据硬编码陈旧:
DEFAULT_CONTEXT_LENGTHS和_ANTHROPIC_OUTPUT_LIMITS需要随新模型发布手动更新。Claude Opus 4.7 的 1M context 已添加,但未来版本易遗漏。
改进建议
短期(低风险)
-
拆分
auxiliary_client.py:将 Provider 解析链(_try_*函数 +resolve_provider_client())提取为auxiliary_providers.py;Codex/Anthropic 适配器类提取为auxiliary_adapters.py;客户端缓存提取为auxiliary_cache.py。 -
拆分
anthropic_adapter.py认证逻辑:将resolve_anthropic_token()、run_hermes_oauth_login_pure()、_write_claude_code_credentials()等 OAuth/PKCE 函数提取为anthropic_auth.py,让适配器文件聚焦于消息格式翻译。 -
为
credential_pool.py的刷新逻辑引入策略模式:1 2 3 4 5 6 7
class RefreshStrategy(ABC): @abstractmethod def refresh(self, entry: PooledCredential) -> Optional[PooledCredential]: ... class AnthropicRefreshStrategy(RefreshStrategy): ... class CodexRefreshStrategy(RefreshStrategy): ... class NousRefreshStrategy(RefreshStrategy): ...
中期(中等风险)
-
引入响应类型 dataclass:为所有适配器定义统一的响应类型
LLMResponse,替代SimpleNamespace:1 2 3 4 5 6
@dataclass class LLMResponse: choices: List[LLMChoice] usage: LLMUsage model: str finish_reason: str
-
统一模型元数据更新机制:在 CI 中定期从 models.dev 和 Anthropic API 拉取最新模型列表,自动更新
DEFAULT_CONTEXT_LENGTHS和_ANTHROPIC_OUTPUT_LIMITS。 -
辅助客户端添加结构化日志:在
_resolve_auto()的每次 fallback 时记录 provider 名 + 失败原因,而非仅logger.debug。
长期(架构级)
-
适配器注册表模式:将
anthropic_adapter.py、bedrock_adapter.py、gemini_cloudcode_adapter.py注册到统一适配器注册表,每个适配器实现标准接口:1 2 3 4 5 6 7 8 9
class LLMAdapter(ABC): @abstractmethod def translate_request(self, messages, tools, **kwargs) -> dict: ... @abstractmethod def translate_response(self, response) -> LLMResponse: ... @abstractmethod def detect_context_overflow(self, error) -> bool: ...
-
消息格式版本化:当前所有消息都假设 OpenAI 格式为内部标准。随着 Gemini CloudCode 等非 OpenAI Provider 增多,考虑引入
InternalMessage协议类型,使格式转换显式化且可测试。 -
credential_pool.py线程安全加固:将_lock从threading.Lock升级为threading.RLock,或将文件 I/O 操作移出锁保护区,或使用asyncio.Lock在 gateway 异步上下文中使用。