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-544 — create_oss_provider_with_base_url, the standard constructor for local OSS providers.codex-rs/model-provider/src/auth.rs:49-71 — ResolvedProviderAuth, bundling the auth result with telemetry for the request layer.codex-rs/backend-client/src/client.rs:124-183 — backend_client::Client, the ChatGPT backend WHAM / Codex API dual-form client.codex-rs/ollama/src/client.rs:25-78 — OllamaClient, deriving the host root from ModelProviderInfo and probing /api/tags or /v1/models.codex-rs/tui/src/oss_selection.rs:316-370 — select_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.