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 不在同一層。