跳转到内容

多 Provider 适配与声明式注册

多 Provider 适配与声明式注册

学习目标

读完本章后,你将能够:

  • 理解 Provider trait 的统一抽象接口及其在 Goose 架构中的角色
  • 掌握全局 ProviderRegistry 的懒初始化与泛型注册机制
  • 实现基于 JSON 配置文件的声明式 Provider 注册,支持自定义端点
  • 理解 ACP 包装器模式如何将外部 AI CLI 封装为 Provider
  • 分析声明式注册与代码注册的权衡

前置知识

  • [[model-provider/01-overview]] — 多模型适配架构的统一抽象、认证策略与传输层设计
  • [[model-provider/04-declarative-provider-registry]] — 声明式 Provider 注册表模式

项目实践

Provider Trait 统一抽象

Goose 将所有 LLM 提供商统一抽象为 Provider trait,所有内置和自定义提供商都实现这一接口:

trait Provider {
// 提供商元数据
fn metadata() -> ProviderMetadata;
// 流式响应
async fn stream_response(&self, messages, tools, settings)
-> Result<MessageStream>;
// Token 用量统计
async fn usage(&self) -> Result<ProviderUsage>;
// 权限路由(审批模式判断)
fn permission_routing() -> PermissionRouting;
}

关键设计

  • metadata() 是关联函数而非方法,在编译时返回静态元信息(名称、支持的模型列表、能力声明)
  • stream_response() 返回 MessageStream(即 BoxStream),与 Agent 的流式架构一致
  • usage() 提供 Token 用量统计,支持成本追踪
  • permission_routing() 决定不同审批模式下应该使用哪个模型

所有 25+ 内置提供商(Anthropic、OpenAI、Google、Ollama、OpenRouter、Azure、Bedrock 等)都实现这一 trait,Agent 核心完全不感知具体提供商类型。

全局 ProviderRegistry 架构

注册表是一个 OnceCell<RwLock<ProviderRegistry>> 全局单例,懒初始化。所有提供商通过统一入口注册:

注册表中每个条目是一个 ProviderEntry,包含:

字段说明
metadata提供商元数据(名称、模型列表、上下文限制)
constructor构造函数闭包,延迟到首次使用时实例化
inventory_identityInventory 身份解析器
inventory_configured检查 Inventory 是否已配置
cleanup可选的清理函数(用于 ACP 子进程)
provider_type类型标识(Builtin / Declarative / ACP)
supports_inventory_refresh是否支持 Inventory 刷新

声明式 JSON 加载

Goose 支持三种来源的声明式 Provider 配置:

来源路径说明
编译内嵌 JSON代码中 include_str! 嵌入NVIDIA、Groq、Tanzu 等官方合作伙伴
磁盘 JSON应用数据目录下的 providers/ 目录发行版自带的额外提供商
用户自定义 JSON~/.config/goose/custom_providers/*.json用户自行添加的私有端点

JSON 配置 Schema 示例:

{
"name": "nvidia",
"engine": "openai",
"display_name": "NVIDIA",
"base_url": "https://integrate.api.nvidia.com/v1",
"api_key_env": "NVIDIA_API_KEY",
"models": [
{
"name": "meta/llama-3.1-70b-instruct",
"display_name": "Llama 3.1 70B",
"context_limit": 131072
}
],
"requires_auth": true,
"preserves_thinking": false
}

关键字段

字段说明
name唯一标识,如 "nvidia""custom_myapi"
engine复用哪个内置 Provider 的适配器:openai / anthropic / ollama
base_urlAPI 端点,支持 ${VAR_NAME} 占位符
api_key_env读取 API Key 的环境变量名
models该端点提供的模型列表
requires_auth是否需要 API Key 认证
preserves_thinking该端点是否保留 <think> 推理块

Engine 路由机制

JSON 中的 engine 字段决定路由到哪个内置 Provider 类型:

engine: "openai" → OpenAiProvider::from_custom_config()
engine: "anthropic" → AnthropicProvider::from_custom_config()
engine: "ollama" → OllamaProvider::from_custom_config()

为什么不是每个新端点写一个 Provider? 大多数 API 兼容端点遵循 OpenAI 或 Anthropic 的协议规范,复用已有的 Provider 适配器比从零编写成本低得多。一个 JSON 文件即可接入新端点。

环境变量占位符展开

JSON 中的 ${VAR_NAME} 占位符在 Provider 实例化时(而非注册时)展开:

base_url: "https://${CUSTOM_HOST}/v1"
→ 读取 Config::get_param("CUSTOM_HOST")
→ 如果未配置,使用 JSON 中的默认值

为什么延迟展开? 如果注册时就展开,用户通过 UI 修改配置后不会生效。延迟展开确保每次 Provider 实例化都读取最新配置。

ACP 包装器模式

ACP Provider 将外部 AI CLI 工具(Claude Code、Codex、Gemini CLI 等)封装为注册表中的 Provider:

// 伪代码:ACP Provider 工厂
type AcpProviderFactory = Arc<
dyn Fn(
String, // provider name
ModelConfig, // model configuration
Vec<ExtensionConfig>, // extensions
Option<PathBuf>, // working directory
) -> BoxFuture<'static, Result<Arc<dyn Provider>>> + Send + Sync
>;

工作原理

  1. ACP Provider 通过 stdio 启动外部 CLI 子进程(如 claudecodex
  2. 通过 ACP 协议(JSON-RPC over stdio)与该 CLI 通信
  3. 将外部 CLI 的能力封装为 Provider trait 接口
  4. Agent 可以像调用内置 Provider 一样调用 ACP Provider

设计价值:用户可以通过 ACP Provider 利用 Claude Code、Codex 等外部 AI 工具的独特功能,而无需离开 Goose 环境。

快速模型回退(Fast Model Fallback)

当主模型不可用(限流、故障、认证过期)时,Goose 支持自动回退到备用模型:

主 LLM 请求 → 捕获 ModelRateLimitError / ModelProviderError
检查是否有 fallback 配置 → 有
切换:provider = fallback_provider
使用 fallback 继续执行

回退策略遵循两阶段故障转移:

ToolShim 集成

Goose 的 toolshim 模块为不同提供商的工具调用提供统一的适配层。当模型提供商的工具调用格式存在差异时(如 OpenAI 的 tool_calls vs Anthropic 的 tool_use),ToolShim 负责归一化:

toolshim.normalize(tool_call, provider_type):
→ 统一为 Goose 内部的工具调用格式
→ 确保 Agent 核心不需要关心具体提供商

ThinkFilter 集成

不同提供商对推理块(<think> 标签)的处理方式不同:

提供商处理方式
Anthropic原生支持 extended thinking
OpenAI o 系列原生支持 reasoning
部分兼容端点不支持推理块,需要过滤

preserves_thinking 字段声明端点是否保留推理块。对于不支持的端点,ThinkFilter 在响应流中过滤掉 <think> 标签,确保前端不会收到无法渲染的内容。


问题与规避

同名冲突

问题:Declarative JSON 中的 name 与 Builtin Provider 同名,导致注册冲突。

规避策略

  • 注册时检查名称唯一性
  • Declarative/Custom 类型不允许覆盖 Builtin 名称
  • 用户自定义 Provider 建议使用 custom_ 前缀(如 custom_myapi

配置热更新

问题:用户通过 UI 修改了某个 Declarative Provider 的 API Key,已创建的 Provider 实例仍使用旧配置。

规避策略

  • 配置解析延迟到实例化时,而非注册时
  • 每次创建新的 Agent Session 时重新实例化 Provider
  • 旧 Session 中的 Provider 不受影响(遵循 Turn 上下文隔离原则)

JSON 配置校验

问题:用户编写的 JSON 配置格式错误或必填字段缺失。

规避策略

  • 使用 Serde 反序列化,字段缺失时自动报错
  • 启动时校验所有已存在的 Custom Provider JSON
  • engine 字段必须匹配已知的内置 Provider 类型

ACP 子进程生命周期管理

问题:ACP Provider 启动的外部 CLI 子进程可能在 Agent 退出后残留。

规避策略

  • ProviderEntry 中的 cleanup 函数负责清理子进程
  • Agent Session 结束时调用所有已注册 Provider 的 cleanup 函数
  • 使用 CancellationToken 确保子进程随 Session 终止而退出

设计取舍

声明式 JSON vs 代码 Provider

维度声明式 JSON代码 Provider
新增成本写一个 JSON 文件实现 Provider trait + 编译注册
灵活度受限于 engine 抽象完全自定义
维护成本低(无需编译)高(需改代码 + 编译)
适用场景API 兼容端点协议差异大的模型

Goose 的选择:API 兼容的模型(遵循 OpenAI 或 Anthropic 协议)用声明式 JSON;协议差异大的(如需要特殊鉴权、自定义请求格式)用代码 Provider。目前 25+ 内置提供商中,大部分是代码 Provider(核心提供商),而长尾兼容端点通过声明式 JSON 加载。

延迟实例化 vs 预加载

维度延迟实例化预加载
启动速度快(不创建实例)
首次调用延迟稍高(需要创建)
配置热更新支持不支持
错误发现时机首次使用时启动时

Goose 的选择:延迟实例化。注册阶段只存储配置和构造函数闭包,实际实例化延迟到首次使用。这使得:

  • 25+ 提供商中只有实际使用的才会被实例化,节省启动时间和内存
  • 环境变量和 UI 配置可以在注册后被设置
  • 每次新 Session 创建时都读取最新配置

代价:首次调用某 Provider 时有额外延迟;配置错误在使用时才发现而非启动时。


参考来源

  • [[model-provider/01-overview]] — 多模型适配架构
  • [[model-provider/04-declarative-provider-registry]] — 声明式 Provider 注册表模式
  • Goose 源码:crates/goose/src/providers/mod.rs(模块入口)
  • Goose 源码:crates/goose/src/providers/base.rs(Provider trait 定义)
  • Goose 源码:crates/goose/src/providers/provider_registry.rs(注册表实现)
  • Goose 源码:crates/goose/src/acp/provider.rs(ACP 包装器)
  • Goose 源码:crates/goose/src/providers/toolshim/(ToolShim 适配层)