Compare commits

...

3 Commits

Author SHA1 Message Date
celia-oai
30c7e0a7de changes 2026-04-14 16:11:19 -07:00
celia-oai
139aee5a64 changes 2026-04-14 14:26:05 -07:00
celia-oai
ba9b25b791 changes 2026-04-14 14:13:15 -07:00
11 changed files with 665 additions and 18 deletions

12
codex-rs/Cargo.lock generated
View File

@@ -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"

View File

@@ -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" }

View File

@@ -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 }

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -0,0 +1,6 @@
load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "model-provider",
crate_name = "codex_model_provider",
)

View 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 }

View 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()
)
);
}
}

View File

@@ -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,

View File

@@ -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;