多 Provider 适配与声明式注册
多 Provider 适配与声明式注册
学习目标
读完本章后,你将能够:
- 理解
Providertrait 的统一抽象接口及其在 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_identity | Inventory 身份解析器 |
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_url | API 端点,支持 ${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>;工作原理:
- ACP Provider 通过 stdio 启动外部 CLI 子进程(如
claude、codex) - 通过 ACP 协议(JSON-RPC over stdio)与该 CLI 通信
- 将外部 CLI 的能力封装为
Providertrait 接口 - 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 适配层)