feat: add opt-in provider runtime abstraction (#17713)

## Summary

- Add `codex-model-provider` as the runtime home for model-provider
behavior that does not belong in `codex-core`, `codex-login`, or
`codex-api`.
- The new crate wraps configured `ModelProviderInfo` in a
`ModelProvider` trait object that can resolve the API provider config,
provider-scoped auth manager, and request auth provider for each call.
- This centralizes provider auth behavior in one place today, and gives
us an extension point for future provider-specific auth, model listing,
request setup, and related runtime behavior.

## Tests
Ran tests manually to make sure that provider auth under different
configs still work as expected.

---------

Co-authored-by: pakrym-oai <pakrym@openai.com>
This commit is contained in:
Celia Chenandpakrym-oai authored and GitHub committed 2026-04-17 02:27:45 +00:00
1 parent 91e8eebd03
commit a803790a10
45 files changed
+577 -369

No files matched your search

+31 -21
View File
@@ -7,6 +7,7 @@ use codex_api::ModelsClient;
use codex_api::RequestTelemetry;
use codex_api::ReqwestTransport;
use codex_api::TransportError;
use codex_api::auth_header_telemetry;
use codex_api::map_api_error;
use codex_app_server_protocol::AuthMode;
use codex_feedback::FeedbackRequestTags;
@@ -14,10 +15,10 @@ use codex_feedback::emit_feedback_request_tags_with_auth_env;
use codex_login::AuthEnvTelemetry;
use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_login::auth_provider_from_auth;
use codex_login::collect_auth_env_telemetry;
use codex_login::default_client::build_reqwest_client;
use codex_login::required_auth_manager_for_provider;
use codex_model_provider::SharedModelProvider;
use codex_model_provider::create_model_provider;
use codex_model_provider_info::ModelProviderInfo;
use codex_otel::TelemetryAuthMode;
use codex_protocol::config_types::CollaborationModeMask;
@@ -178,10 +179,9 @@ pub struct ModelsManager {
remote_models: RwLock<Vec<ModelInfo>>,
catalog_mode: CatalogMode,
collaboration_modes_config: CollaborationModesConfig,
auth_manager: Arc<AuthManager>,
etag: RwLock<Option<String>>,
cache_manager: ModelsCacheManager,
provider: ModelProviderInfo,
provider: SharedModelProvider,
}
impl ModelsManager {
@@ -206,14 +206,17 @@ impl ModelsManager {
}
/// Construct a manager with an explicit provider used for remote model refreshes.
// TODO(celia-oai): Revisit this ownership direction: the model provider should likely
// own or return the models manager instead of requiring the manager to construct and use
// a provider from provider info.
pub fn new_with_provider(
codex_home: PathBuf,
auth_manager: Arc<AuthManager>,
model_catalog: Option<ModelsResponse>,
collaboration_modes_config: CollaborationModesConfig,
provider: ModelProviderInfo,
provider_info: ModelProviderInfo,
) -> Self {
let auth_manager = required_auth_manager_for_provider(auth_manager, &provider);
let model_provider = create_model_provider(provider_info, Some(auth_manager));
let cache_path = codex_home.join(MODEL_CACHE_FILE);
let cache_manager = ModelsCacheManager::new(cache_path, DEFAULT_MODEL_CACHE_TTL);
let catalog_mode = if model_catalog.is_some() {
@@ -228,10 +231,9 @@ impl ModelsManager {
remote_models: RwLock::new(remote_models),
catalog_mode,
collaboration_modes_config,
auth_manager,
etag: RwLock::new(None),
cache_manager,
provider,
provider: model_provider,
}
}
@@ -395,9 +397,11 @@ impl ModelsManager {
return Ok(());
}
if self.auth_manager.auth_mode() != Some(AuthMode::Chatgpt)
&& !self.provider.has_command_auth()
{
let auth_mode = self
.provider
.auth_manager()
.and_then(|auth_manager| auth_manager.auth_mode());
if auth_mode != Some(AuthMode::Chatgpt) && !self.provider.info().has_command_auth() {
if matches!(
refresh_strategy,
RefreshStrategy::Offline | RefreshStrategy::OnlineIfUncached
@@ -432,19 +436,21 @@ impl ModelsManager {
async fn fetch_and_update_models(&self) -> CoreResult<()> {
let _timer =
codex_otel::start_global_timer("codex.remote_models.fetch_update.duration_ms", &[]);
let auth = self.auth_manager.auth().await;
let auth_manager = self.provider.auth_manager();
let codex_api_key_env_enabled = auth_manager
.as_ref()
.is_some_and(|auth_manager| auth_manager.codex_api_key_env_enabled());
let auth = self.provider.auth().await;
let auth_mode = auth.as_ref().map(CodexAuth::auth_mode);
let api_provider = self.provider.to_api_provider(auth_mode)?;
let api_auth = auth_provider_from_auth(auth.clone(), &self.provider)?;
let auth_env = collect_auth_env_telemetry(
&self.provider,
self.auth_manager.codex_api_key_env_enabled(),
);
let api_provider = self.provider.api_provider().await?;
let api_auth = self.provider.api_auth().await?;
let auth_env = collect_auth_env_telemetry(self.provider.info(), codex_api_key_env_enabled);
let transport = ReqwestTransport::new(build_reqwest_client());
let auth_telemetry = auth_header_telemetry(api_auth.as_ref());
let request_telemetry: Arc<dyn RequestTelemetry> = Arc::new(ModelsRequestTelemetry {
auth_mode: auth_mode.map(|mode| TelemetryAuthMode::from(mode).to_string()),
auth_header_attached: api_auth.auth_header_attached(),
auth_header_name: api_auth.auth_header_name(),
auth_header_attached: auth_telemetry.attached,
auth_header_name: auth_telemetry.name,
auth_env,
});
let client = ModelsClient::new(transport, api_provider, api_auth)
@@ -520,7 +526,11 @@ impl ModelsManager {
remote_models.sort_by(|a, b| a.priority.cmp(&b.priority));
let mut presets: Vec<ModelPreset> = remote_models.into_iter().map(Into::into).collect();
let chatgpt_mode = matches!(self.auth_manager.auth_mode(), Some(AuthMode::Chatgpt));
let auth_mode = self
.provider
.auth_manager()
.and_then(|auth_manager| auth_manager.auth_mode());
let chatgpt_mode = matches!(auth_mode, Some(AuthMode::Chatgpt));
presets = ModelPreset::filter_by_auth(presets, chatgpt_mode);
ModelPreset::mark_default_by_picker_visibility(&mut presets);