Abstracción de Model Provider
codex abstrae «con quién hablar» en el trait ModelProvider: una sola configuración, y el backend puede ser OpenAI, Amazon Bedrock, o un Ollama / LM Studio local, o incluso un endpoint compatible con OpenAI definido por el usuario. Alrededor del trait hay model-provider-info (serialización de config), backend-client (llamadas HTTP), ollama / lmstudio (adaptadores de OSS local) y el panel de selección de modelo en el TUI. Esta capa decide a quién envía al final el Agent la petición, con qué credenciales y por cuál protocolo wire.
Responsabilidades
- Exponer la metadata del provider en runtime:
info()devuelveModelProviderInfo, con base URL, wire API, env key, configuración de auth, etc. (codex-rs/model-provider/src/provider.rs:101-162) - Resolver la autenticación: login de ChatGPT, API key, bearer token escupido por un comando, o AWS SigV4, todo unificado en
api_auth/api_auth_for_scope(codex-rs/model-provider/src/provider.rs:171-194) - Reportar el techo de capacidades:
ProviderCapabilitiesdice a la capa superior si se puede image generation / web search / namespace tools; el provider tiene poder de veto (codex-rs/model-provider/src/provider.rs:33-48) - Construir el gestor del catálogo de modelos:
models_managerdecide si se hace pull del catálogo remoto desde/v1/modelso si se usa la lista estática empaquetada (Bedrock) (codex-rs/model-provider/src/provider.rs:197-215) - Adaptar OSS local: los crates
ollama/lmstudiodetectan el servicio local, tiran de modelos, y mapean su endpoint compatible con OpenAI de vuelta aModelProviderInfo(codex-rs/ollama/src/lib.rs:23-50)
Motivación de diseño
Al principio codex solo hablaba con OpenAI, y la configuración era OPENAI_API_KEY + base_url. Pero al empezar los usuarios a conectar Bedrock, motores de inferencia local, y gateways propios, este «OpenAI hardcodeado» se colaba por todas partes. Rediseñar con un trait abstracto busca dejar «lo que es compatible con OpenAI» en la implementación por defecto, y comprimir «lo que cambia entre proveedores» en overrides de métodos del trait.
ConfiguredModelProvider es la implementación por defecto: si tu backend se parece a OpenAI /v1/responses, se puede configurar directamente con ModelProviderInfo, sin escribir un impl nuevo del trait. Solo Bedrock, por su firma SigV4, tiene una implementación amazon_bedrock::AmazonBedrockModelProvider aparte. La trade-off de este diseño: mantener el trait en una docena de métodos, con la mayoría de las implementaciones por defecto yendo por info() + auth_manager(), dando ganchos de override y evitando que cada provider nuevo implante un gran bloque de métodos.
Ollama / LM Studio no implementan ModelProvider directamente, sino que envuelven el servicio local como una «base URL compatible con OpenAI» que se le pasa a ConfiguredModelProvider, y usan funciones auxiliares como ensure_oss_ready para detectar el servicio y tirar de modelos. Así el trait se mantiene pequeño y la reutilización de la ruta OSS local es la mayor.
Archivos clave
codex-rs/model-provider/src/provider.rs:101-162 — cuerpo del trait ModelProvider, entrada de info / auth / capabilities / models_manager.codex-rs/model-provider/src/provider.rs:232-241 — factory create_model_provider, con Bedrock en una rama aparte y el resto a ConfiguredModelProvider.codex-rs/model-provider-info/src/lib.rs:89-141 — struct ModelProviderInfo, cuya forma serializada es exactamente la sección [model_providers.xxx] del config.toml.codex-rs/model-provider-info/src/lib.rs:524-544 — create_oss_provider_with_base_url, constructor estándar de provider OSS local.codex-rs/model-provider/src/auth.rs:49-71 — ResolvedProviderAuth, empaqueta el resultado de la autenticación junto con telemetry para la capa de peticiones.codex-rs/backend-client/src/client.rs:124-183 — backend_client::Client, cliente de doble forma para el backend de ChatGPT: WHAM / Codex API.codex-rs/ollama/src/client.rs:25-78 — OllamaClient, deriva el host root del ModelProviderInfo y hace probe a /api/tags o /v1/models.codex-rs/tui/src/oss_selection.rs:316-370 — select_oss_provider, al arrancar el TUI sondea puertos; elige automáticamente si hay una sola instancia, abre un selector si hay dos.La función factory saca a Bedrock aparte; el resto se apoya en ConfiguredModelProvider con la configuración por defecto de ModelProviderInfo:
// provider.rs:232-241 — la factory se reparte por tipo de 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 se corresponde directamente con la sección [model_providers.<id>] del config.toml; los campos son Option, y los huecos en blanco caen al valor por defecto de OpenAI:
// model-provider-info/src/lib.rs:89-141 — forma serializada de la configuración del 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,
// ...hay más campos como query_params / http_headers / stream_*
pub requires_openai_auth: bool,
pub supports_websockets: bool,
}La clave del adaptador de Ollama no es «implementar el trait», sino envolver el servicio local como un endpoint compatible con OpenAI y dejar el resto a ConfiguredModelProvider:
// ollama/src/client.rs:59-78 — construye el cliente desde la configuración del provider y hace probe
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)
}Flujo de datos
Bordes y fallos
- Campos de auth mutuamente excluyentes:
ModelProviderInfo::validateprohibe queawscoexista conenv_key/experimental_bearer_token/auth/requires_openai_auth, para evitar que la semántica de la configuración se contradiga (codex-rs/model-provider-info/src/lib.rs:154-212). - Detección de versión de Ollama:
ensure_responses_supportedexige Ollama ≥ 0.13.4 para usar Responses API; las versiones más antiguas dan error directo (codex-rs/ollama/src/lib.rs:63-77). - Bedrock no pasa por la ruta de auth de OpenAI: los tests
create_model_provider_builds_command_auth_managery similares dejan claro que el provider Bedrock, incluso recibiendo un auth manager de OpenAI, lo ignora, para evitar usar credenciales equivocadas (codex-rs/model-provider/src/provider.rs:552-565). - Auth first-party por ruta especial:
provider_uses_first_party_auth_pathexigerequires_openai_auth=truey ningún campo env_key/bearer/aws/auth; solo el login puro de ChatGPT va por la ruta de auth con scope (codex-rs/model-provider/src/provider.rs:223-229). - Probe de OSS no es fatal: si
ensure_oss_readyfalla al hacerfetch_models, solo sueltatracing::warnsin abortar, dejando que el error real salga al correr el modelo (codex-rs/ollama/src/lib.rs:34-47).
Resumen
El trait ModelProvider comprime «con quién hablar» en una docena de métodos; la implementación por defecto ConfiguredModelProvider cubre todos los backends compatibles con OpenAI, y Bedrock tiene impl aparte por SigV4. Ollama / LM Studio no implementan el trait directamente, sino que reutilizan la implementación por defecto con detección local + base URL compatible con OpenAI, manteniendo el trait pequeño. La forma concreta de la configuración se define en ModelProviderInfo, en sintonía con Sistema de configuración; la ejecución de tareas en la nube va por la ruta Cloud Tasks, que no está en la misma capa que el provider local.