Model Provider Abstraction
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
- Expose runtime provider metadata:
info()returnsModelProviderInfo, including base URL, wire API, env key, auth config (codex-rs/model-provider/src/provider.rs:101-162). - 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). - Report capability ceilings:
ProviderCapabilitiestells 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). - Build the model catalog manager:
models_managerdecides whether to pull a remote catalog from/v1/modelsor use a packaged static list (Bedrock) (codex-rs/model-provider/src/provider.rs:197-215). - Adapt local OSS: the
ollama/lmstudiocrate probes local services, pulls models, and maps their OpenAI-compatible endpoints back intoModelProviderInfo(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:
// 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:
// 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:
// 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::validateforbidsawsfrom appearing alongsideenv_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_supportedrequires 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_managerexplicitly 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_pathrequiresrequires_openai_auth=trueand 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_readyonlytracing::warns onfetch_modelsfailure, 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.