diff --git a/codex-rs/api-client/src/model_provider.rs b/codex-rs/api-client/src/model_provider.rs index c2c2e82093..88fea05c0f 100644 --- a/codex-rs/api-client/src/model_provider.rs +++ b/codex-rs/api-client/src/model_provider.rs @@ -1,3 +1,10 @@ +//! Registry of model providers supported by Codex. +//! +//! Providers can be defined in two places: +//! 1. Built-in defaults compiled into the binary so Codex works out-of-the-box. +//! 2. User-defined entries inside `~/.codex/config.toml` under the `model_providers` +//! key. These override or extend the defaults at runtime. + use std::collections::HashMap; use std::env::VarError; use std::time::Duration; @@ -13,38 +20,81 @@ use crate::error::Result; const DEFAULT_STREAM_IDLE_TIMEOUT_MS: u64 = 300_000; const DEFAULT_STREAM_MAX_RETRIES: u64 = 5; const DEFAULT_REQUEST_MAX_RETRIES: u64 = 4; +/// Hard cap for user-configured `stream_max_retries`. const MAX_STREAM_MAX_RETRIES: u64 = 100; +/// Hard cap for user-configured `request_max_retries`. const MAX_REQUEST_MAX_RETRIES: u64 = 100; const DEFAULT_OLLAMA_PORT: u32 = 11434; +/// Wire protocol that the provider speaks. Most third-party services only +/// implement the classic OpenAI Chat Completions JSON schema, whereas OpenAI +/// itself (and a handful of others) additionally expose the more modern +/// Responses API. The two protocols use different request/response shapes +/// and cannot be auto-detected at runtime, therefore each provider entry +/// must declare which one it expects. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum WireApi { + /// The Responses API exposed by OpenAI at `/v1/responses`. Responses, + /// Regular Chat Completions compatible with `/v1/chat/completions`. #[default] Chat, } +/// Serializable representation of a provider definition. #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct ModelProviderInfo { + /// Friendly display name. pub name: String, + /// Base URL for the provider's OpenAI-compatible API. pub base_url: Option, + /// Environment variable that stores the user's API key for this provider. pub env_key: Option, + /// Optional instructions to help the user get a valid value for the + /// variable and set it. pub env_key_instructions: Option, + /// Value to use with `Authorization: Bearer ` header. Use of this + /// config is discouraged in favor of `env_key` for security reasons, but + /// this may be necessary when using this programmatically. pub experimental_bearer_token: Option, + /// Which wire protocol this provider expects. #[serde(default)] pub wire_api: WireApi, + /// Optional query parameters to append to the base URL. pub query_params: Option>, + /// Additional HTTP headers to include in requests to this provider where + /// the (key, value) pairs are the header name and value. pub http_headers: Option>, + /// Optional HTTP headers to include in requests to this provider where the + /// (key, value) pairs are the header name and environment variable whose + /// value should be used. If the environment variable is not set, or the + /// value is empty, the header will not be included in the request. pub env_http_headers: Option>, + /// Maximum number of times to retry a failed HTTP request to this provider. pub request_max_retries: Option, + /// Number of times to retry reconnecting a dropped streaming response before failing. pub stream_max_retries: Option, + /// Idle timeout (in milliseconds) to wait for activity on a streaming response before treating + /// the connection as lost. pub stream_idle_timeout_ms: Option, + /// Does this provider require an OpenAI API Key or ChatGPT login token? If true, + /// the user is presented with a login screen on first run, and login preference and token/key + /// are stored in auth.json. If false (which is the default), the login screen is skipped, + /// and the API key (if needed) comes from the `env_key` environment variable. #[serde(default)] pub requires_openai_auth: bool, } impl ModelProviderInfo { + /// Construct a `POST` request builder for the given URL using the provided + /// [`reqwest::Client`] applying: + /// - provider-specific headers (static and environment based) + /// - Bearer auth header when an API key is available + /// - Auth token for OAuth + /// + /// If the provider declares an `env_key` but the variable is missing or empty, this returns an + /// error identical to the one produced by [`ModelProviderInfo::api_key`]. pub async fn create_request_builder( &self, client: &reqwest::Client, @@ -131,6 +181,8 @@ impl ModelProviderInfo { .unwrap_or(false) } + /// Apply provider-specific HTTP headers (both static and environment-based) onto an existing + /// [`reqwest::RequestBuilder`] and return the updated builder. fn apply_http_headers(&self, mut builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder { if let Some(extra) = &self.http_headers { for (k, v) in extra { @@ -150,6 +202,8 @@ impl ModelProviderInfo { builder } + /// If `env_key` is `Some`, returns the API key for this provider if present (and non-empty) in + /// the environment. If `env_key` is required but cannot be found, returns an error. pub fn api_key(&self) -> Result> { match &self.env_key { Some(env_key) => { @@ -171,18 +225,21 @@ impl ModelProviderInfo { } } + /// Effective maximum number of request retries for this provider. pub fn request_max_retries(&self) -> u64 { self.request_max_retries .unwrap_or(DEFAULT_REQUEST_MAX_RETRIES) .min(MAX_REQUEST_MAX_RETRIES) } + /// Effective maximum number of stream reconnection attempts for this provider. pub fn stream_max_retries(&self) -> u64 { self.stream_max_retries .unwrap_or(DEFAULT_STREAM_MAX_RETRIES) .min(MAX_STREAM_MAX_RETRIES) } + /// Effective idle timeout for streaming responses. pub fn stream_idle_timeout(&self) -> Duration { self.stream_idle_timeout_ms .map(Duration::from_millis) @@ -192,14 +249,21 @@ impl ModelProviderInfo { pub const BUILT_IN_OSS_MODEL_PROVIDER_ID: &str = "oss"; +/// Built-in default provider list. pub fn built_in_model_providers() -> HashMap { use ModelProviderInfo as P; + // We do not want to be in the business of adjudicating which third-party providers are bundled + // with Codex CLI, so we only include the OpenAI and open source ("oss") providers by default. + // Users are encouraged to add to `model_providers` in config.toml to add their own providers. [ ( "openai", P { name: "OpenAI".into(), + // Allow users to override the default OpenAI endpoint by exporting `OPENAI_BASE_URL`. + // This is useful when pointing Codex at a proxy, mock server, or Azure-style + // deployment without requiring a full TOML override for the built-in OpenAI provider. base_url: std::env::var("OPENAI_BASE_URL") .ok() .filter(|v| !v.trim().is_empty()), @@ -224,6 +288,7 @@ pub fn built_in_model_providers() -> HashMap { .into_iter() .collect(), ), + // Use global defaults for retry/timeout unless overridden in config.toml. request_max_retries: None, stream_max_retries: None, stream_idle_timeout_ms: None, @@ -237,7 +302,10 @@ pub fn built_in_model_providers() -> HashMap { .collect() } +/// Convenience helper for the built-in OSS provider. pub fn create_oss_provider() -> ModelProviderInfo { + // These CODEX_OSS_ environment variables are experimental: we may switch to reading values from + // config.toml instead. let codex_oss_base_url = match std::env::var("CODEX_OSS_BASE_URL") .ok() .filter(|v| !v.trim().is_empty()) diff --git a/codex-rs/api-client/src/responses.rs b/codex-rs/api-client/src/responses.rs index eacdcd56f9..c3a4ca8c89 100644 --- a/codex-rs/api-client/src/responses.rs +++ b/codex-rs/api-client/src/responses.rs @@ -1,6 +1,7 @@ use std::io::BufRead; use std::path::Path; -use std::sync::{Arc, OnceLock}; +use std::sync::Arc; +use std::sync::OnceLock; use std::time::Duration; use async_trait::async_trait;