use crate::config::{AuthStore, Credential, ProviderConfig};
use crate::ProviderDeclaration as ProviderManifest;
use crate::ValueSource;
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub enum CredentialSource {
Explicit,
StoredApiKey,
StoredOauth,
ModelsJson,
Environment(String),
Headers,
Anonymous,
Missing,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub enum AuthStatus {
Missing,
Ready,
Expired,
Anonymous,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub struct AuthState {
pub status: AuthStatus,
pub source: CredentialSource,
}
impl AuthState {
pub fn is_ready(&self) -> bool {
matches!(self.status, AuthStatus::Ready | AuthStatus::Anonymous)
}
}
pub(crate) fn state(
provider: &str,
config: Option<&ProviderConfig>,
store: &AuthStore,
manifest: Option<&ProviderManifest>,
values: &dyn ValueSource,
has_headers: impl FnOnce() -> bool,
) -> AuthState {
let credential = resolve(provider, config, store, manifest, values);
let source = if credential.source.is_present() {
credential.source
} else if has_headers() {
CredentialSource::Headers
} else {
CredentialSource::Missing
};
let status = match source {
CredentialSource::Anonymous => AuthStatus::Anonymous,
CredentialSource::Missing => match store.get(provider) {
Some(Credential::Oauth { expires, .. })
if *expires != 0 && *expires <= values.now_unix_seconds() =>
{
AuthStatus::Expired
}
_ => AuthStatus::Missing,
},
_ => AuthStatus::Ready,
};
AuthState { status, source }
}
impl CredentialSource {
pub fn is_present(&self) -> bool {
!matches!(self, Self::Missing)
}
pub fn as_json_str(&self) -> &str {
match self {
Self::Explicit => "override",
Self::StoredApiKey => "auth.json",
Self::StoredOauth => "oauth",
Self::ModelsJson => "models.json",
Self::Environment(name) => name,
Self::Headers => "headers",
Self::Anonymous => "anonymous",
Self::Missing => "none",
}
}
}
pub struct ResolvedCredential {
pub key: Option<String>,
pub source: CredentialSource,
pub bearer: bool,
}
pub fn builtin_api_key_env(provider: &str) -> Option<&'static str> {
match provider {
"anthropic" => Some("ANTHROPIC_API_KEY"),
"openai" => Some("OPENAI_API_KEY"),
_ => None,
}
}
fn nonempty(value: Option<String>) -> Option<String> {
value.filter(|key| !key.trim().is_empty())
}
pub fn resolve(
provider: &str,
config: Option<&ProviderConfig>,
store: &AuthStore,
manifest: Option<&ProviderManifest>,
values: &dyn ValueSource,
) -> ResolvedCredential {
let bearer = config
.and_then(|config| config.auth_header)
.unwrap_or(false);
let credential = |key, source, bearer| ResolvedCredential {
key: Some(key),
source,
bearer,
};
match store.get(provider) {
Some(Credential::ApiKey {
key: Some(key),
env,
}) => {
if let Some(key) = nonempty(crate::values::resolve_config_value(
key,
env.as_ref(),
values,
true,
)) {
return credential(key, CredentialSource::StoredApiKey, bearer);
}
}
Some(Credential::Oauth {
access, expires, ..
}) if !access.trim().is_empty() => {
let now = values.now_unix_seconds();
if *expires == 0 || *expires > now {
return credential(access.clone(), CredentialSource::StoredOauth, true);
}
}
_ => {}
}
if let Some(key) = config
.and_then(|config| config.api_key.as_deref())
.and_then(|raw| nonempty(crate::values::resolve_config_value(raw, None, values, true)))
{
return credential(key, CredentialSource::ModelsJson, bearer);
}
if manifest.is_none() && provider == "anthropic" {
if let Some(key) = nonempty(values.env("ANTHROPIC_AUTH_TOKEN")) {
return credential(
key,
CredentialSource::Environment("ANTHROPIC_AUTH_TOKEN".into()),
true,
);
}
}
let env = match manifest {
Some(manifest) => manifest.api_key_env.as_deref(),
None => builtin_api_key_env(provider),
};
if let Some(name) = env {
if let Some(key) = nonempty(values.env(name)) {
return credential(key, CredentialSource::Environment(name.into()), bearer);
}
}
ResolvedCredential {
key: None,
source: if manifest.is_some_and(|manifest| !manifest.requires_auth) {
CredentialSource::Anonymous
} else {
CredentialSource::Missing
},
bearer,
}
}
pub fn has_header_auth(headers: Option<&std::collections::BTreeMap<String, String>>) -> bool {
headers.is_some_and(|headers| {
headers.iter().any(|(name, value)| {
["authorization", "x-api-key", "cf-aig-authorization"]
.contains(&name.to_ascii_lowercase().as_str())
&& !value.trim().is_empty()
})
})
}