Skip to content

Model Provider 抽象

源码版本rust-v0.145.0

codex 把"跟谁对话"这件事抽成 ModelProvider trait:一次配置,后端可以是 OpenAI、Amazon Bedrock,也可以是本地 Ollama / LM Studio,甚至是用户自定义的 OpenAI 兼容端点。围绕 trait 的有 model-provider-info(配置序列化)、backend-client(HTTP 调用)、ollama / lmstudio(本地 OSS 适配)和 TUI 里的模型选择面板。这一层决定了 Agent 最终把请求发给谁、用什么凭证、走哪条 wire 协议。

职责

  1. 暴露运行时 provider 元信息:info() 返回 ModelProviderInfo,包含 base URL、wire API、env key、auth 配置等 (codex-rs/model-provider/src/provider.rs:101-162)。
  2. 解决鉴权:走 ChatGPT 登录、API key、命令吐出的 bearer token,或 AWS SigV4,统一收口到 api_auth / api_auth_for_scope (codex-rs/model-provider/src/provider.rs:171-194)。
  3. 报告能力上界:ProviderCapabilities 告诉上层能否做 image generation / web search / namespace tools,provider 拥有否决权 (codex-rs/model-provider/src/provider.rs:33-48)。
  4. 构造模型目录管理器:models_manager 决定是从 /v1/models 拉远程 catalog,还是用打包的静态列表 (Bedrock) (codex-rs/model-provider/src/provider.rs:197-215)。
  5. 适配本地 OSS:ollama / lmstudio crate 负责探测本地服务、拉取模型,把它们的 OpenAI 兼容端点映射回 ModelProviderInfo (codex-rs/ollama/src/lib.rs:23-50)。

设计动机

早期 codex 只跟 OpenAI 对话,配置就是 OPENAI_API_KEY + base_url。但用户开始接 Bedrock、本地推理引擎、自建网关时,这种"硬编码 OpenAI"的写法就到处漏出来。重新做 trait 抽象的目的是把"OpenAI 兼容的部分"留在默认实现里,把"各家不一样的地方"压成 trait method 的 override。

ConfiguredModelProvider 是默认实现——只要你的后端长得像 OpenAI /v1/responses,就能直接用 ModelProviderInfo 配置出来,不用写新 trait impl。只有 Bedrock 因为走 SigV4 签名、有独立的 amazon_bedrock::AmazonBedrockModelProvider 实现。这套设计的取舍点:把 trait method 控制在十几条,大部分默认实现走 info() + auth_manager(),既给了 override 钩子,又避免每接一个 provider 都要重写一大堆方法。

Ollama / LM Studio 没有自己实现 ModelProvider,而是把本地服务包装成"OpenAI 兼容 base URL"喂给 ConfiguredModelProvider,再用 ensure_oss_ready 之类的辅助函数做服务探测和模型拉取。这样 trait 数量保持小,本地 OSS 路径的复用度反而最高。

关键文件

codex-rs/model-provider/src/provider.rs:101-162ModelProvider trait 主体,info / auth / capabilities / models_manager 的入口。codex-rs/model-provider/src/provider.rs:232-241create_model_provider 工厂,Bedrock 单独分流,其余走 ConfiguredModelProvidercodex-rs/model-provider-info/src/lib.rs:89-141ModelProviderInfo 结构,序列化形态即 config.toml[model_providers.xxx] 段。codex-rs/model-provider-info/src/lib.rs:524-544create_oss_provider_with_base_url,本地 OSS provider 的标准构造。codex-rs/model-provider/src/auth.rs:49-71ResolvedProviderAuth,把鉴权结果连同 telemetry 一起打包供请求层使用。codex-rs/backend-client/src/client.rs:124-183backend_client::Client,ChatGPT 后端 WHAM / Codex API 路径双形态客户端。codex-rs/ollama/src/client.rs:25-78OllamaClient,从 ModelProviderInfo 推出 host root 并探活 /api/tags/v1/modelscodex-rs/tui/src/oss_selection.rs:316-370select_oss_provider,在 TUI 启动时探端口,单实例自动选,双实例弹选择框。

工厂函数把 Bedrock 单拎出来,其余都靠 ConfiguredModelProviderModelProviderInfo 默认配置:

rust
// provider.rs:232-241 — 工厂按 provider 类型分流
pub fn create_model_provider(
    provider_info: ModelProviderInfo,
    auth_manager: Option<Arc<AuthManager>>,
) -> SharedModelProvider {
    if provider_info.is_amazon_bedrock() {
        Arc::new(AmazonBedrockModelProvider::new(provider_info, auth_manager))
    } else {
        Arc::new(ConfiguredModelProvider::new(provider_info, auth_manager))
    }
}

ModelProviderInfo 直接对应 config.toml[model_providers.<id>] 段,字段都是 Option,留白就走 OpenAI 默认值:

rust
// model-provider-info/src/lib.rs:89-141 — provider 配置的序列化形态
pub struct ModelProviderInfo {
    pub name: String,
    pub base_url: Option<String>,
    pub env_key: Option<String>,
    pub env_key_instructions: Option<String>,
    pub experimental_bearer_token: Option<String>,
    pub auth: Option<ModelProviderAuthInfo>,
    pub aws: Option<ModelProviderAwsAuthInfo>,
    pub wire_api: WireApi,
    // ...还有 query_params / http_headers / stream_* 等字段
    pub requires_openai_auth: bool,
    pub supports_websockets: bool,
}

Ollama 适配的关键不是"实现 trait",而是把本地服务包装成 OpenAI 兼容端点,剩下交给 ConfiguredModelProvider:

rust
// ollama/src/client.rs:59-78 — 从 provider 配置构造客户端并探活
pub(crate) async fn try_from_provider(provider: &ModelProviderInfo) -> io::Result<Self> {
    let base_url = provider.base_url.as_ref().expect("oss provider must have a base_url");
    let uses_openai_compat = is_openai_compatible_base_url(base_url);
    let host_root = base_url_to_host_root(base_url);
    let client = reqwest::Client::builder()
        .connect_timeout(std::time::Duration::from_secs(5))
        .build()
        .unwrap_or_else(|_| reqwest::Client::new());
    let client = Self { client, host_root, uses_openai_compat };
    client.probe_server().await?;
    Ok(client)
}

数据流

边界与失败

  • 多 auth 字段互斥:ModelProviderInfo::validate 禁止 awsenv_key / experimental_bearer_token / auth / requires_openai_auth 同时出现,防止配置语义打架 (codex-rs/model-provider-info/src/lib.rs:154-212)。
  • Ollama 版本检测:ensure_responses_supported 要求 Ollama ≥ 0.13.4 才能用 Responses API,旧版本直接报错(codex-rs/ollama/src/lib.rs:63-77)。
  • Bedrock 不走 OpenAI auth 路径:create_model_provider_builds_command_auth_manager 等测试明确 Bedrock provider 即使传入 OpenAI auth manager 也会被忽略,避免凭证错用 (codex-rs/model-provider/src/provider.rs:552-565)。
  • first-party auth 走特殊分支:provider_uses_first_party_auth_path 要求 requires_openai_auth=true 且没有任何 env_key/bearer/aws/auth 字段,只有纯 ChatGPT 登录才走带 scope 的鉴权路径 (codex-rs/model-provider/src/provider.rs:223-229)。
  • OSS 探活不致命:ensure_oss_readyfetch_models 失败时只 tracing::warn,不直接 fail,让上层跑模型时再撞出真实错误 (codex-rs/ollama/src/lib.rs:34-47)。

小结

ModelProvider trait 把"跟谁对话"压成十几条方法,默认实现 ConfiguredModelProvider 覆盖所有 OpenAI 兼容后端,Bedrock 因为 SigV4 单独实现。Ollama / LM Studio 不直接 impl trait,而是用本地探测 + OpenAI 兼容 base URL 的方式复用默认实现,trait 数量保持小。具体的配置形态在 ModelProviderInfo 里定义,跟 配置系统 一脉相承,云端任务执行走的是 Cloud Tasks 路径,跟本地 provider 不在同一层。