Skip to content

Model Provider Abstraction

源码版本rust-v0.145.0

codex abstracts "who to talk to" as the ModelProvider trait: one config, and the backend can be OpenAI, Amazon Bedrock, a local Ollama / LM Studio, or even a user-defined OpenAI-compatible endpoint. Around the trait are model-provider-info (config serialization), backend-client (HTTP calls), ollama / lmstudio (local OSS adapters), and the TUI's model picker panel. This layer decides who the Agent ultimately sends requests to, with what credentials, over which wire protocol.

Responsibilities

  1. Expose runtime provider metadata: info() returns ModelProviderInfo, including base URL, wire API, env key, auth config (codex-rs/model-provider/src/provider.rs:101-162).
  2. Resolve auth: ChatGPT login, API key, a bearer token from a command, or AWS SigV4, all unified into api_auth / api_auth_for_scope (codex-rs/model-provider/src/provider.rs:171-194).
  3. Report capability ceilings: ProviderCapabilities tells the upper layer whether image generation / web search / namespace tools are possible; the provider has veto power (codex-rs/model-provider/src/provider.rs:33-48).
  4. Build the model catalog manager: models_manager decides whether to pull a remote catalog from /v1/models or use a packaged static list (Bedrock) (codex-rs/model-provider/src/provider.rs:197-215).
  5. Adapt local OSS: the ollama / lmstudio crate probes local services, pulls models, and maps their OpenAI-compatible endpoints back into ModelProviderInfo (codex-rs/ollama/src/lib.rs:23-50).

Design motivation

Early codex only talked to OpenAI, and config was just OPENAI_API_KEY + base_url. But as users started hooking up Bedrock, local inference engines, and self-built gateways, this "hardcoded OpenAI" approach leaked everywhere. Re-doing the trait abstraction aims to keep "the OpenAI-compatible parts" in the default impl and compress "the parts that differ between vendors" into trait-method overrides.

ConfiguredModelProvider is the default impl — as long as your backend looks like OpenAI /v1/responses, you can configure it via ModelProviderInfo without writing a new trait impl. Only Bedrock, because it uses SigV4 signing, has a separate amazon_bedrock::AmazonBedrockModelProvider impl. The trade-off of this design: keep trait methods around a dozen, most default impls go through info() + auth_manager(), providing override hooks while avoiding rewriting a pile of methods per new provider.

Ollama / LM Studio don't implement ModelProvider themselves; instead they wrap the local service as an "OpenAI-compatible base URL" fed to ConfiguredModelProvider, and use helpers like ensure_oss_ready for service probing and model pulling. This keeps the trait count small and maximizes reuse on the local OSS path.

Key files

codex-rs/model-provider/src/provider.rs:101-162 — the ModelProvider trait body; the entry for info / auth / capabilities / models_manager.codex-rs/model-provider/src/provider.rs:232-241 — the create_model_provider factory; Bedrock is branched out, the rest go through ConfiguredModelProvider.codex-rs/model-provider-info/src/lib.rs:89-141 — the ModelProviderInfo struct; its serialized form is the [model_providers.xxx] section in config.toml.codex-rs/model-provider-info/src/lib.rs:524-544create_oss_provider_with_base_url, the standard constructor for local OSS providers.codex-rs/model-provider/src/auth.rs:49-71ResolvedProviderAuth, bundling the auth result with telemetry for the request layer.codex-rs/backend-client/src/client.rs:124-183backend_client::Client, the ChatGPT backend WHAM / Codex API dual-form client.codex-rs/ollama/src/client.rs:25-78OllamaClient, deriving the host root from ModelProviderInfo and probing /api/tags or /v1/models.codex-rs/tui/src/oss_selection.rs:316-370select_oss_provider, probing ports at TUI startup; auto-selects on single instance, pops a picker on two.

The factory function pulls Bedrock out separately; the rest rely on ConfiguredModelProvider consuming ModelProviderInfo default config:

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 directly corresponds to the [model_providers.<id>] section of config.toml; fields are all Option, and blanks fall back to OpenAI defaults:

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,
}

The key of the Ollama adapter is not "implementing the trait" but wrapping the local service as an OpenAI-compatible endpoint and handing the rest to 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)
}

Data flow

Boundaries and failures

  • Multiple auth fields are mutually exclusive: ModelProviderInfo::validate forbids aws from appearing alongside env_key / experimental_bearer_token / auth / requires_openai_auth, to prevent config-semantic clashes (codex-rs/model-provider-info/src/lib.rs:154-212).
  • Ollama version detection: ensure_responses_supported requires Ollama ≥ 0.13.4 to use the Responses API; older versions error out directly (codex-rs/ollama/src/lib.rs:63-77).
  • Bedrock doesn't go through the OpenAI auth path: tests like create_model_provider_builds_command_auth_manager explicitly note that Bedrock providers ignore OpenAI auth managers even if passed in, to prevent credential misuse (codex-rs/model-provider/src/provider.rs:552-565).
  • First-party auth takes a special branch: provider_uses_first_party_auth_path requires requires_openai_auth=true and no env_key / bearer / aws / auth fields — only pure ChatGPT login takes the scoped auth path (codex-rs/model-provider/src/provider.rs:223-229).
  • OSS probing is non-fatal: ensure_oss_ready only tracing::warns on fetch_models failure, not failing directly, letting the upper layer hit the real error at model-run time (codex-rs/ollama/src/lib.rs:34-47).

Summary

The ModelProvider trait compresses "who to talk to" into a dozen methods; the default impl ConfiguredModelProvider covers all OpenAI-compatible backends, and Bedrock has a separate impl because of SigV4. Ollama / LM Studio don't directly impl the trait; they reuse the default impl via local probing + OpenAI-compatible base URLs, keeping the trait count small. The concrete config form is defined in ModelProviderInfo, aligning with config system; cloud task execution takes the Cloud Tasks path, on a different layer from local providers.