mirror of
https://github.com/openai/codex.git
synced 2026-04-15 03:51:43 +03:00
Compare commits
3 Commits
pr17088
...
dev/cc/pro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30c7e0a7de | ||
|
|
139aee5a64 | ||
|
|
ba9b25b791 |
12
codex-rs/Cargo.lock
generated
12
codex-rs/Cargo.lock
generated
@@ -2313,10 +2313,12 @@ dependencies = [
|
||||
"codex-client",
|
||||
"codex-config",
|
||||
"codex-keyring-store",
|
||||
"codex-model-provider",
|
||||
"codex-model-provider-info",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"codex-terminal-detection",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-template",
|
||||
"core_test_support",
|
||||
"keyring",
|
||||
@@ -2403,6 +2405,16 @@ dependencies = [
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-model-provider"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-model-provider-info",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
"pretty_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-model-provider-info"
|
||||
version = "0.0.0"
|
||||
|
||||
@@ -42,6 +42,7 @@ members = [
|
||||
"login",
|
||||
"codex-mcp",
|
||||
"mcp-server",
|
||||
"model-provider",
|
||||
"model-provider-info",
|
||||
"models-manager",
|
||||
"network-proxy",
|
||||
@@ -143,6 +144,7 @@ codex-lmstudio = { path = "lmstudio" }
|
||||
codex-login = { path = "login" }
|
||||
codex-mcp = { path = "codex-mcp" }
|
||||
codex-mcp-server = { path = "mcp-server" }
|
||||
codex-model-provider = { path = "model-provider" }
|
||||
codex-model-provider-info = { path = "model-provider-info" }
|
||||
codex-models-manager = { path = "models-manager" }
|
||||
codex-network-proxy = { path = "network-proxy" }
|
||||
|
||||
@@ -16,6 +16,7 @@ codex-api = { workspace = true }
|
||||
codex-client = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
codex-keyring-store = { workspace = true }
|
||||
codex-model-provider = { workspace = true }
|
||||
codex-model-provider-info = { workspace = true }
|
||||
codex-otel = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
@@ -45,6 +46,7 @@ webbrowser = { workspace = true }
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
core_test_support = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
keyring = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
regex-lite = { workspace = true }
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
use codex_api::CoreAuthProvider;
|
||||
use codex_model_provider::ProviderAuthStrategy;
|
||||
use codex_model_provider::ResolvedModelProvider;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::EnvVarError;
|
||||
|
||||
use crate::CodexAuth;
|
||||
|
||||
@@ -7,20 +11,41 @@ pub fn auth_provider_from_auth(
|
||||
auth: Option<CodexAuth>,
|
||||
provider: &ModelProviderInfo,
|
||||
) -> codex_protocol::error::Result<CoreAuthProvider> {
|
||||
if let Some(api_key) = provider.api_key()? {
|
||||
return Ok(CoreAuthProvider {
|
||||
token: Some(api_key),
|
||||
account_id: None,
|
||||
});
|
||||
}
|
||||
let resolved_provider = ResolvedModelProvider::resolve(provider.name.clone(), provider.clone())
|
||||
.map_err(|err| CodexErr::Fatal(err.to_string()))?;
|
||||
|
||||
if let Some(token) = provider.experimental_bearer_token.clone() {
|
||||
return Ok(CoreAuthProvider {
|
||||
token: Some(token),
|
||||
match resolved_provider.auth_strategy() {
|
||||
ProviderAuthStrategy::EnvBearer {
|
||||
env_key,
|
||||
env_key_instructions,
|
||||
} => {
|
||||
let token = std::env::var(env_key)
|
||||
.ok()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.ok_or_else(|| {
|
||||
CodexErr::EnvVar(EnvVarError {
|
||||
var: env_key.clone(),
|
||||
instructions: env_key_instructions.clone(),
|
||||
})
|
||||
})?;
|
||||
Ok(CoreAuthProvider {
|
||||
token: Some(token),
|
||||
account_id: None,
|
||||
})
|
||||
}
|
||||
ProviderAuthStrategy::ExperimentalBearer { token } => Ok(CoreAuthProvider {
|
||||
token: Some(token.clone()),
|
||||
account_id: None,
|
||||
});
|
||||
}),
|
||||
ProviderAuthStrategy::OpenAi
|
||||
| ProviderAuthStrategy::ExternalBearer { .. }
|
||||
| ProviderAuthStrategy::NoProviderAuth => auth_provider_from_codex_auth(auth),
|
||||
}
|
||||
}
|
||||
|
||||
fn auth_provider_from_codex_auth(
|
||||
auth: Option<CodexAuth>,
|
||||
) -> codex_protocol::error::Result<CoreAuthProvider> {
|
||||
if let Some(auth) = auth {
|
||||
let token = auth.get_token()?;
|
||||
Ok(CoreAuthProvider {
|
||||
@@ -34,3 +59,109 @@ pub fn auth_provider_from_auth(
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_model_provider_info::WireApi;
|
||||
use codex_protocol::config_types::ModelProviderAuthInfo;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::num::NonZeroU64;
|
||||
|
||||
const MISSING_ENV_KEY: &str =
|
||||
"CODEX_TEST_AUTH_PROVIDER_FROM_AUTH_MISSING_PROVIDER_KEY_9F54D778";
|
||||
|
||||
fn custom_provider() -> ModelProviderInfo {
|
||||
ModelProviderInfo {
|
||||
name: "Test Provider".to_string(),
|
||||
base_url: Some("https://example.com/v1".to_string()),
|
||||
env_key: None,
|
||||
env_key_instructions: None,
|
||||
experimental_bearer_token: None,
|
||||
auth: None,
|
||||
wire_api: WireApi::Responses,
|
||||
query_params: None,
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
request_max_retries: None,
|
||||
stream_max_retries: None,
|
||||
stream_idle_timeout_ms: None,
|
||||
websocket_connect_timeout_ms: None,
|
||||
requires_openai_auth: false,
|
||||
supports_websockets: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_auth_uses_supplied_codex_auth() {
|
||||
let provider = ModelProviderInfo::create_openai_provider(/*base_url*/ None);
|
||||
|
||||
let auth = auth_provider_from_auth(Some(CodexAuth::from_api_key("openai-key")), &provider)
|
||||
.expect("auth provider");
|
||||
|
||||
assert_eq!(auth.token.as_deref(), Some("openai-key"));
|
||||
assert_eq!(auth.account_id, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_no_auth_preserves_supplied_codex_auth_behavior() {
|
||||
let provider = custom_provider();
|
||||
|
||||
let auth = auth_provider_from_auth(Some(CodexAuth::from_api_key("custom-key")), &provider)
|
||||
.expect("auth provider");
|
||||
|
||||
assert_eq!(auth.token.as_deref(), Some("custom-key"));
|
||||
assert_eq!(auth.account_id, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn experimental_bearer_overrides_supplied_codex_auth() {
|
||||
let mut provider = custom_provider();
|
||||
provider.experimental_bearer_token = Some("provider-token".to_string());
|
||||
|
||||
let auth = auth_provider_from_auth(Some(CodexAuth::from_api_key("ignored")), &provider)
|
||||
.expect("auth provider");
|
||||
|
||||
assert_eq!(auth.token.as_deref(), Some("provider-token"));
|
||||
assert_eq!(auth.account_id, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_bearer_reports_missing_env_key() {
|
||||
let mut provider = custom_provider();
|
||||
provider.env_key = Some(MISSING_ENV_KEY.to_string());
|
||||
provider.env_key_instructions = Some("Set the test key.".to_string());
|
||||
|
||||
let err = match auth_provider_from_auth(/*auth*/ None, &provider) {
|
||||
Ok(_) => panic!("expected missing env var"),
|
||||
Err(err) => err,
|
||||
};
|
||||
|
||||
let CodexErr::EnvVar(err) = err else {
|
||||
panic!("expected env var error");
|
||||
};
|
||||
assert_eq!(err.var, MISSING_ENV_KEY);
|
||||
assert_eq!(err.instructions.as_deref(), Some("Set the test key."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_bearer_uses_supplied_codex_auth() {
|
||||
let mut provider = custom_provider();
|
||||
provider.auth = Some(ModelProviderAuthInfo {
|
||||
command: "credential-helper".to_string(),
|
||||
args: vec!["token".to_string()],
|
||||
timeout_ms: NonZeroU64::new(10_000).unwrap(),
|
||||
refresh_interval_ms: 300_000,
|
||||
cwd: AbsolutePathBuf::from_absolute_path("/tmp").unwrap(),
|
||||
});
|
||||
|
||||
let auth =
|
||||
auth_provider_from_auth(Some(CodexAuth::from_api_key("command-token")), &provider)
|
||||
.expect("auth provider");
|
||||
|
||||
assert_eq!(auth.token.as_deref(), Some("command-token"));
|
||||
assert_eq!(auth.account_id, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,5 +46,6 @@ pub use auth::save_auth;
|
||||
pub use auth_env_telemetry::AuthEnvTelemetry;
|
||||
pub use auth_env_telemetry::collect_auth_env_telemetry;
|
||||
pub use provider_auth::auth_manager_for_provider;
|
||||
pub use provider_auth::provider_uses_external_bearer_auth;
|
||||
pub use provider_auth::required_auth_manager_for_provider;
|
||||
pub use token_data::TokenData;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_model_provider::ProviderAuthStrategy;
|
||||
use codex_model_provider::ResolvedModelProvider;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
|
||||
use crate::AuthManager;
|
||||
@@ -11,10 +13,15 @@ pub fn auth_manager_for_provider(
|
||||
auth_manager: Option<Arc<AuthManager>>,
|
||||
provider: &ModelProviderInfo,
|
||||
) -> Option<Arc<AuthManager>> {
|
||||
match provider.auth.clone() {
|
||||
Some(config) => Some(AuthManager::external_bearer_only(config)),
|
||||
None => auth_manager,
|
||||
}
|
||||
external_bearer_auth_manager(provider).or(auth_manager)
|
||||
}
|
||||
|
||||
/// Whether this provider uses command-backed bearer-token auth.
|
||||
pub fn provider_uses_external_bearer_auth(provider: &ModelProviderInfo) -> bool {
|
||||
matches!(
|
||||
resolved_provider_auth(provider),
|
||||
Some(ProviderAuthStrategy::ExternalBearer { .. })
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns an auth manager for request paths that always require authentication.
|
||||
@@ -25,8 +32,121 @@ pub fn required_auth_manager_for_provider(
|
||||
auth_manager: Arc<AuthManager>,
|
||||
provider: &ModelProviderInfo,
|
||||
) -> Arc<AuthManager> {
|
||||
match provider.auth.clone() {
|
||||
Some(config) => AuthManager::external_bearer_only(config),
|
||||
None => auth_manager,
|
||||
external_bearer_auth_manager(provider).unwrap_or(auth_manager)
|
||||
}
|
||||
|
||||
fn external_bearer_auth_manager(provider: &ModelProviderInfo) -> Option<Arc<AuthManager>> {
|
||||
match resolved_provider_auth(provider)? {
|
||||
ProviderAuthStrategy::ExternalBearer { config } => {
|
||||
Some(AuthManager::external_bearer_only(config))
|
||||
}
|
||||
ProviderAuthStrategy::OpenAi
|
||||
| ProviderAuthStrategy::EnvBearer { .. }
|
||||
| ProviderAuthStrategy::ExperimentalBearer { .. }
|
||||
| ProviderAuthStrategy::NoProviderAuth => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolved_provider_auth(provider: &ModelProviderInfo) -> Option<ProviderAuthStrategy> {
|
||||
ResolvedModelProvider::resolve(provider.name.clone(), provider.clone())
|
||||
.ok()
|
||||
.map(|resolved_provider| resolved_provider.auth_strategy().clone())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_model_provider_info::WireApi;
|
||||
use codex_protocol::config_types::ModelProviderAuthInfo;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::num::NonZeroU64;
|
||||
|
||||
use crate::CodexAuth;
|
||||
|
||||
fn provider() -> ModelProviderInfo {
|
||||
ModelProviderInfo {
|
||||
name: "test".to_string(),
|
||||
base_url: Some("https://example.com/v1".to_string()),
|
||||
env_key: None,
|
||||
env_key_instructions: None,
|
||||
experimental_bearer_token: None,
|
||||
auth: None,
|
||||
wire_api: WireApi::Responses,
|
||||
query_params: None,
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
request_max_retries: None,
|
||||
stream_max_retries: None,
|
||||
stream_idle_timeout_ms: None,
|
||||
websocket_connect_timeout_ms: None,
|
||||
requires_openai_auth: false,
|
||||
supports_websockets: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn external_auth_config() -> ModelProviderAuthInfo {
|
||||
let cwd = std::env::current_dir().expect("current dir");
|
||||
ModelProviderAuthInfo {
|
||||
command: "echo".to_string(),
|
||||
args: vec!["provider-token".to_string()],
|
||||
timeout_ms: NonZeroU64::new(1_000).unwrap(),
|
||||
refresh_interval_ms: 60_000,
|
||||
cwd: AbsolutePathBuf::try_from(cwd).expect("cwd should be absolute"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_manager_for_provider_uses_external_bearer_auth() {
|
||||
let provider = ModelProviderInfo {
|
||||
auth: Some(external_auth_config()),
|
||||
..provider()
|
||||
};
|
||||
|
||||
assert!(provider_uses_external_bearer_auth(&provider));
|
||||
|
||||
let auth_manager = auth_manager_for_provider(/*auth_manager*/ None, &provider)
|
||||
.expect("external bearer auth manager");
|
||||
|
||||
assert_eq!(auth_manager.auth_mode(), Some(AuthMode::ApiKey));
|
||||
assert_eq!(auth_manager.auth_cached(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_manager_for_provider_ignores_non_external_provider_auth() {
|
||||
let provider = ModelProviderInfo {
|
||||
env_key: Some("TEST_PROVIDER_API_KEY".to_string()),
|
||||
..provider()
|
||||
};
|
||||
|
||||
let auth_manager = auth_manager_for_provider(/*auth_manager*/ None, &provider);
|
||||
|
||||
assert!(!provider_uses_external_bearer_auth(&provider));
|
||||
assert!(auth_manager.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn required_auth_manager_for_provider_reuses_base_manager_without_external_auth() {
|
||||
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("base"));
|
||||
|
||||
let scoped_auth_manager =
|
||||
required_auth_manager_for_provider(auth_manager.clone(), &provider());
|
||||
|
||||
assert!(Arc::ptr_eq(&scoped_auth_manager, &auth_manager));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn required_auth_manager_for_provider_uses_external_bearer_auth() {
|
||||
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("base"));
|
||||
let provider = ModelProviderInfo {
|
||||
auth: Some(external_auth_config()),
|
||||
..provider()
|
||||
};
|
||||
|
||||
let scoped_auth_manager = required_auth_manager_for_provider(auth_manager, &provider);
|
||||
|
||||
assert_eq!(scoped_auth_manager.auth_mode(), Some(AuthMode::ApiKey));
|
||||
assert_eq!(scoped_auth_manager.auth_cached(), None);
|
||||
}
|
||||
}
|
||||
|
||||
6
codex-rs/model-provider/BUILD.bazel
Normal file
6
codex-rs/model-provider/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "model-provider",
|
||||
crate_name = "codex_model_provider",
|
||||
)
|
||||
21
codex-rs/model-provider/Cargo.toml
Normal file
21
codex-rs/model-provider/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
name = "codex-model-provider"
|
||||
version.workspace = true
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
name = "codex_model_provider"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
codex-model-provider-info = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
334
codex-rs/model-provider/src/lib.rs
Normal file
334
codex-rs/model-provider/src/lib.rs
Normal file
@@ -0,0 +1,334 @@
|
||||
//! Runtime model provider resolution.
|
||||
//!
|
||||
//! `codex_model_provider_info` owns the config-facing provider metadata. This
|
||||
//! crate turns that metadata into the narrow runtime facade that model-facing
|
||||
//! callsites should depend on. The first slice is intentionally auth-only; the
|
||||
//! facade can grow transport, catalog, and capability accessors as those
|
||||
//! callsites move behind provider ownership.
|
||||
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_protocol::config_types::ModelProviderAuthInfo;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
|
||||
/// Stable identifier for a configured model provider.
|
||||
pub type ModelProviderId = String;
|
||||
|
||||
/// Runtime facade for provider-owned model behavior.
|
||||
///
|
||||
/// This trait starts with only auth-facing accessors. Add model listing,
|
||||
/// Responses client construction, and optional specialized clients here as
|
||||
/// those callsites move behind provider ownership.
|
||||
pub trait ModelProvider {
|
||||
fn id(&self) -> &str;
|
||||
fn info(&self) -> &ModelProviderInfo;
|
||||
fn auth_strategy(&self) -> &ProviderAuthStrategy;
|
||||
}
|
||||
|
||||
/// Auth strategy selected for a resolved model provider.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ProviderAuthStrategy {
|
||||
/// OpenAI-managed auth through API key, ChatGPT, or ChatGPT auth tokens.
|
||||
OpenAi,
|
||||
/// Bearer token read from an environment variable.
|
||||
EnvBearer {
|
||||
env_key: String,
|
||||
env_key_instructions: Option<String>,
|
||||
},
|
||||
/// Bearer token embedded directly in provider config.
|
||||
ExperimentalBearer { token: String },
|
||||
/// Bearer token produced by an external command.
|
||||
ExternalBearer { config: ModelProviderAuthInfo },
|
||||
/// No provider-specific auth is configured; callers may use session auth fallback.
|
||||
NoProviderAuth,
|
||||
}
|
||||
|
||||
impl ProviderAuthStrategy {
|
||||
/// Whether this auth strategy uses OpenAI account/API-key auth flows.
|
||||
pub fn requires_openai_auth(&self) -> bool {
|
||||
matches!(self, Self::OpenAi)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolved runtime provider facade.
|
||||
///
|
||||
/// This type starts as an auth-only facade. Future provider-owned behavior
|
||||
/// should be added as methods/fields on this type rather than by teaching
|
||||
/// callsites to branch on provider IDs.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ResolvedModelProvider {
|
||||
id: ModelProviderId,
|
||||
info: ModelProviderInfo,
|
||||
auth: ProviderAuthStrategy,
|
||||
}
|
||||
|
||||
impl ResolvedModelProvider {
|
||||
/// Resolve config-facing provider metadata into the runtime provider facade.
|
||||
pub fn resolve(
|
||||
id: impl Into<ModelProviderId>,
|
||||
info: ModelProviderInfo,
|
||||
) -> Result<Self, ResolveProviderError> {
|
||||
info.validate()
|
||||
.map_err(ResolveProviderError::InvalidConfig)?;
|
||||
let auth = resolve_auth(&info)?;
|
||||
Ok(Self {
|
||||
id: id.into(),
|
||||
info,
|
||||
auth,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &str {
|
||||
self.id.as_str()
|
||||
}
|
||||
|
||||
pub fn info(&self) -> &ModelProviderInfo {
|
||||
&self.info
|
||||
}
|
||||
|
||||
/// Return the provider-owned auth strategy.
|
||||
pub fn auth_strategy(&self) -> &ProviderAuthStrategy {
|
||||
&self.auth
|
||||
}
|
||||
}
|
||||
|
||||
impl ModelProvider for ResolvedModelProvider {
|
||||
fn id(&self) -> &str {
|
||||
self.id.as_str()
|
||||
}
|
||||
|
||||
fn info(&self) -> &ModelProviderInfo {
|
||||
&self.info
|
||||
}
|
||||
|
||||
fn auth_strategy(&self) -> &ProviderAuthStrategy {
|
||||
&self.auth
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_auth(info: &ModelProviderInfo) -> Result<ProviderAuthStrategy, ResolveProviderError> {
|
||||
if let Some(config) = info.auth.as_ref() {
|
||||
return Ok(ProviderAuthStrategy::ExternalBearer {
|
||||
config: config.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(env_key) = info.env_key.as_ref() {
|
||||
return Ok(ProviderAuthStrategy::EnvBearer {
|
||||
env_key: env_key.clone(),
|
||||
env_key_instructions: info.env_key_instructions.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(token) = info.experimental_bearer_token.as_ref() {
|
||||
return Ok(ProviderAuthStrategy::ExperimentalBearer {
|
||||
token: token.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
if info.requires_openai_auth {
|
||||
Ok(ProviderAuthStrategy::OpenAi)
|
||||
} else {
|
||||
Ok(ProviderAuthStrategy::NoProviderAuth)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ResolveProviderError {
|
||||
InvalidConfig(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for ResolveProviderError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::InvalidConfig(message) => write!(f, "invalid provider config: {message}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ResolveProviderError {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_model_provider_info::WireApi;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::num::NonZeroU64;
|
||||
|
||||
fn provider() -> ModelProviderInfo {
|
||||
ModelProviderInfo {
|
||||
name: "Test Provider".to_string(),
|
||||
base_url: Some("https://example.com/v1".to_string()),
|
||||
env_key: None,
|
||||
env_key_instructions: None,
|
||||
experimental_bearer_token: None,
|
||||
auth: None,
|
||||
wire_api: WireApi::Responses,
|
||||
query_params: None,
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
request_max_retries: None,
|
||||
stream_max_retries: None,
|
||||
stream_idle_timeout_ms: None,
|
||||
websocket_connect_timeout_ms: None,
|
||||
requires_openai_auth: false,
|
||||
supports_websockets: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_openai_auth() {
|
||||
let info = ModelProviderInfo::create_openai_provider(/*base_url*/ None);
|
||||
|
||||
let provider = ResolvedModelProvider::resolve("openai", info.clone()).unwrap();
|
||||
|
||||
assert_eq!(provider.id(), "openai");
|
||||
assert_eq!(provider.info(), &info);
|
||||
assert_eq!(provider.auth_strategy(), &ProviderAuthStrategy::OpenAi);
|
||||
assert!(provider.auth_strategy().requires_openai_auth());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolved_provider_implements_model_provider_facade() {
|
||||
fn auth_from_provider(provider: &impl ModelProvider) -> &ProviderAuthStrategy {
|
||||
provider.auth_strategy()
|
||||
}
|
||||
|
||||
let provider = ResolvedModelProvider::resolve(
|
||||
"openai",
|
||||
ModelProviderInfo::create_openai_provider(/*base_url*/ None),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(auth_from_provider(&provider), &ProviderAuthStrategy::OpenAi);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_env_bearer_auth() {
|
||||
let mut info = provider();
|
||||
info.env_key = Some("TEST_API_KEY".to_string());
|
||||
info.env_key_instructions = Some("Set TEST_API_KEY.".to_string());
|
||||
|
||||
let provider = ResolvedModelProvider::resolve("custom", info).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
provider.auth_strategy(),
|
||||
&ProviderAuthStrategy::EnvBearer {
|
||||
env_key: "TEST_API_KEY".to_string(),
|
||||
env_key_instructions: Some("Set TEST_API_KEY.".to_string()),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_legacy_auth_priority_for_non_command_auth_fields() {
|
||||
let mut env_over_experimental = provider();
|
||||
env_over_experimental.env_key = Some("TEST_API_KEY".to_string());
|
||||
env_over_experimental.experimental_bearer_token = Some("token".to_string());
|
||||
assert_eq!(
|
||||
ResolvedModelProvider::resolve("custom", env_over_experimental)
|
||||
.unwrap()
|
||||
.auth_strategy(),
|
||||
&ProviderAuthStrategy::EnvBearer {
|
||||
env_key: "TEST_API_KEY".to_string(),
|
||||
env_key_instructions: None,
|
||||
}
|
||||
);
|
||||
|
||||
let mut env_over_openai = provider();
|
||||
env_over_openai.env_key = Some("TEST_API_KEY".to_string());
|
||||
env_over_openai.requires_openai_auth = true;
|
||||
assert_eq!(
|
||||
ResolvedModelProvider::resolve("custom", env_over_openai)
|
||||
.unwrap()
|
||||
.auth_strategy(),
|
||||
&ProviderAuthStrategy::EnvBearer {
|
||||
env_key: "TEST_API_KEY".to_string(),
|
||||
env_key_instructions: None,
|
||||
}
|
||||
);
|
||||
|
||||
let mut experimental_over_openai = provider();
|
||||
experimental_over_openai.experimental_bearer_token = Some("token".to_string());
|
||||
experimental_over_openai.requires_openai_auth = true;
|
||||
assert_eq!(
|
||||
ResolvedModelProvider::resolve("custom", experimental_over_openai)
|
||||
.unwrap()
|
||||
.auth_strategy(),
|
||||
&ProviderAuthStrategy::ExperimentalBearer {
|
||||
token: "token".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_experimental_bearer_auth() {
|
||||
let mut info = provider();
|
||||
info.experimental_bearer_token = Some("token".to_string());
|
||||
|
||||
let provider = ResolvedModelProvider::resolve("custom", info).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
provider.auth_strategy(),
|
||||
&ProviderAuthStrategy::ExperimentalBearer {
|
||||
token: "token".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_external_bearer_auth() {
|
||||
let mut info = provider();
|
||||
let auth_config = ModelProviderAuthInfo {
|
||||
command: "credential-helper".to_string(),
|
||||
args: vec!["token".to_string()],
|
||||
timeout_ms: NonZeroU64::new(10_000).unwrap(),
|
||||
refresh_interval_ms: 300_000,
|
||||
cwd: AbsolutePathBuf::from_absolute_path("/tmp").unwrap(),
|
||||
};
|
||||
info.auth = Some(auth_config.clone());
|
||||
|
||||
let provider = ResolvedModelProvider::resolve("custom", info).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
provider.auth_strategy(),
|
||||
&ProviderAuthStrategy::ExternalBearer {
|
||||
config: auth_config,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_no_auth_for_custom_provider() {
|
||||
let provider = ResolvedModelProvider::resolve("custom", provider()).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
provider.auth_strategy(),
|
||||
&ProviderAuthStrategy::NoProviderAuth
|
||||
);
|
||||
assert!(!provider.auth_strategy().requires_openai_auth());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_command_auth() {
|
||||
let mut info = provider();
|
||||
info.auth = Some(ModelProviderAuthInfo {
|
||||
command: " ".to_string(),
|
||||
args: Vec::new(),
|
||||
timeout_ms: NonZeroU64::new(10_000).unwrap(),
|
||||
refresh_interval_ms: 300_000,
|
||||
cwd: AbsolutePathBuf::from_absolute_path("/tmp").unwrap(),
|
||||
});
|
||||
|
||||
let err = ResolvedModelProvider::resolve("custom", info).unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
err,
|
||||
ResolveProviderError::InvalidConfig(
|
||||
"provider auth.command must not be empty".to_string()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ 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::provider_uses_external_bearer_auth;
|
||||
use codex_login::required_auth_manager_for_provider;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_otel::TelemetryAuthMode;
|
||||
@@ -44,6 +45,7 @@ const MODEL_CACHE_FILE: &str = "models_cache.json";
|
||||
const DEFAULT_MODEL_CACHE_TTL: Duration = Duration::from_secs(300);
|
||||
const MODELS_REFRESH_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const MODELS_ENDPOINT: &str = "/models";
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ModelsRequestTelemetry {
|
||||
auth_mode: Option<String>,
|
||||
@@ -396,7 +398,7 @@ impl ModelsManager {
|
||||
}
|
||||
|
||||
if self.auth_manager.auth_mode() != Some(AuthMode::Chatgpt)
|
||||
&& !self.provider.has_command_auth()
|
||||
&& !provider_uses_external_bearer_auth(&self.provider)
|
||||
{
|
||||
if matches!(
|
||||
refresh_strategy,
|
||||
|
||||
@@ -447,6 +447,22 @@ async fn refresh_available_models_uses_provider_auth_token() {
|
||||
assert_models_contain(&manager.get_remote_models().await, &remote_models);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_external_bearer_check_uses_resolved_provider_auth() {
|
||||
let auth_script = ProviderAuthScript::new(&["provider-token"]).unwrap();
|
||||
let external_provider = ModelProviderInfo {
|
||||
auth: Some(auth_script.auth_config()),
|
||||
..provider_for("http://example.test".to_string())
|
||||
};
|
||||
let env_provider = ModelProviderInfo {
|
||||
env_key: Some("TEST_PROVIDER_API_KEY".to_string()),
|
||||
..provider_for("http://example.test".to_string())
|
||||
};
|
||||
|
||||
assert!(provider_uses_external_bearer_auth(&external_provider));
|
||||
assert!(!provider_uses_external_bearer_auth(&env_provider));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn refresh_available_models_uses_cache_when_fresh() {
|
||||
let server = MockServer::start().await;
|
||||
|
||||
Reference in New Issue
Block a user