2026-04-23 14:42:51 +08:00
|
|
|
use aether_data_contracts::repository::provider_catalog::{
|
|
|
|
|
StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
|
|
|
|
|
};
|
2026-05-02 13:23:54 +08:00
|
|
|
use aether_provider_transport::provider_types::{
|
|
|
|
|
fixed_provider_key_inherits_api_formats, provider_type_is_fixed,
|
|
|
|
|
};
|
2026-05-16 19:23:09 +08:00
|
|
|
use serde_json::{Map, Value};
|
2026-04-23 14:42:51 +08:00
|
|
|
use std::collections::BTreeSet;
|
2026-04-17 12:57:06 +08:00
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
|
|
|
pub(crate) enum ProviderKeyCredentialKind {
|
|
|
|
|
RawSecret,
|
|
|
|
|
OAuthSession,
|
|
|
|
|
ServiceAccount,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ProviderKeyCredentialKind {
|
|
|
|
|
pub(crate) const fn as_str(self) -> &'static str {
|
|
|
|
|
match self {
|
|
|
|
|
Self::RawSecret => "raw_secret",
|
|
|
|
|
Self::OAuthSession => "oauth_session",
|
|
|
|
|
Self::ServiceAccount => "service_account",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
|
|
|
pub(crate) enum ProviderKeyRuntimeAuthKind {
|
|
|
|
|
ApiKey,
|
|
|
|
|
Bearer,
|
|
|
|
|
ServiceAccount,
|
2026-04-29 15:46:50 +08:00
|
|
|
Mixed,
|
2026-04-17 12:57:06 +08:00
|
|
|
Unknown,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ProviderKeyRuntimeAuthKind {
|
|
|
|
|
pub(crate) const fn as_str(self) -> &'static str {
|
|
|
|
|
match self {
|
|
|
|
|
Self::ApiKey => "api_key",
|
|
|
|
|
Self::Bearer => "bearer",
|
|
|
|
|
Self::ServiceAccount => "service_account",
|
2026-04-29 15:46:50 +08:00
|
|
|
Self::Mixed => "mixed",
|
2026-04-17 12:57:06 +08:00
|
|
|
Self::Unknown => "unknown",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
|
|
|
pub(crate) struct ProviderKeyAuthSemantics {
|
|
|
|
|
credential_kind: ProviderKeyCredentialKind,
|
|
|
|
|
runtime_auth_kind: ProviderKeyRuntimeAuthKind,
|
|
|
|
|
oauth_managed: bool,
|
2026-05-18 19:12:36 +08:00
|
|
|
can_refresh_oauth: bool,
|
2026-04-17 12:57:06 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ProviderKeyAuthSemantics {
|
|
|
|
|
pub(crate) const fn credential_kind(self) -> ProviderKeyCredentialKind {
|
|
|
|
|
self.credential_kind
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) const fn runtime_auth_kind(self) -> ProviderKeyRuntimeAuthKind {
|
|
|
|
|
self.runtime_auth_kind
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) const fn oauth_managed(self) -> bool {
|
|
|
|
|
self.oauth_managed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) const fn can_refresh_oauth(self) -> bool {
|
2026-05-18 19:12:36 +08:00
|
|
|
self.can_refresh_oauth
|
2026-04-17 12:57:06 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) const fn can_export_oauth(self) -> bool {
|
|
|
|
|
self.oauth_managed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) const fn can_edit_oauth(self) -> bool {
|
|
|
|
|
self.oauth_managed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) const fn can_show_oauth_metadata(self) -> bool {
|
|
|
|
|
self.oauth_managed
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 19:23:09 +08:00
|
|
|
pub(crate) fn provider_key_can_refresh_oauth(
|
|
|
|
|
auth_semantics: ProviderKeyAuthSemantics,
|
|
|
|
|
auth_config: Option<&Map<String, Value>>,
|
|
|
|
|
) -> bool {
|
|
|
|
|
auth_semantics.can_refresh_oauth()
|
|
|
|
|
&& auth_config
|
|
|
|
|
.and_then(|config| config.get("refresh_token"))
|
|
|
|
|
.and_then(Value::as_str)
|
|
|
|
|
.map(str::trim)
|
|
|
|
|
.is_some_and(|value| !value.is_empty())
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-20 22:08:33 +08:00
|
|
|
pub(crate) fn provider_key_auth_config_uses_header_authorization(
|
|
|
|
|
auth_config: Option<&Map<String, Value>>,
|
|
|
|
|
) -> bool {
|
|
|
|
|
auth_config
|
|
|
|
|
.and_then(|config| config.get("headers"))
|
|
|
|
|
.and_then(Value::as_object)
|
|
|
|
|
.is_some_and(|headers| {
|
|
|
|
|
headers.iter().any(|(key, value)| {
|
|
|
|
|
key.trim().eq_ignore_ascii_case("authorization")
|
|
|
|
|
&& value
|
|
|
|
|
.as_str()
|
|
|
|
|
.map(str::trim)
|
|
|
|
|
.is_some_and(|value| !value.is_empty())
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 12:57:06 +08:00
|
|
|
fn normalized_auth_type(key: &StoredProviderCatalogKey) -> String {
|
|
|
|
|
key.auth_type.trim().to_ascii_lowercase()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn key_has_auth_config(key: &StoredProviderCatalogKey) -> bool {
|
|
|
|
|
key.encrypted_auth_config
|
|
|
|
|
.as_deref()
|
|
|
|
|
.map(str::trim)
|
|
|
|
|
.is_some_and(|value| !value.is_empty())
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 15:46:50 +08:00
|
|
|
fn key_has_auth_type_overrides(key: &StoredProviderCatalogKey) -> bool {
|
|
|
|
|
key.auth_type_by_format
|
|
|
|
|
.as_ref()
|
|
|
|
|
.and_then(serde_json::Value::as_object)
|
|
|
|
|
.is_some_and(|items| !items.is_empty())
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 12:57:06 +08:00
|
|
|
fn provider_uses_bearer_oauth_runtime(provider_type: &str) -> bool {
|
|
|
|
|
matches!(
|
|
|
|
|
provider_type.trim().to_ascii_lowercase().as_str(),
|
2026-05-18 15:23:08 +08:00
|
|
|
"claude_code"
|
|
|
|
|
| "codex"
|
|
|
|
|
| "chatgpt_web"
|
|
|
|
|
| "gemini_cli"
|
|
|
|
|
| "antigravity"
|
|
|
|
|
| "kiro"
|
|
|
|
|
| "windsurf"
|
2026-04-17 12:57:06 +08:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 19:23:09 +08:00
|
|
|
fn provider_uses_grok_session_runtime(provider_type: &str) -> bool {
|
|
|
|
|
provider_type.trim().eq_ignore_ascii_case("grok")
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 12:57:06 +08:00
|
|
|
fn provider_key_is_legacy_kiro_oauth_session(
|
|
|
|
|
key: &StoredProviderCatalogKey,
|
|
|
|
|
provider_type: &str,
|
|
|
|
|
auth_type: &str,
|
|
|
|
|
) -> bool {
|
|
|
|
|
provider_type.trim().eq_ignore_ascii_case("kiro")
|
|
|
|
|
&& auth_type.eq_ignore_ascii_case("bearer")
|
|
|
|
|
&& key_has_auth_config(key)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn provider_key_auth_semantics(
|
|
|
|
|
key: &StoredProviderCatalogKey,
|
|
|
|
|
provider_type: &str,
|
|
|
|
|
) -> ProviderKeyAuthSemantics {
|
|
|
|
|
let auth_type = normalized_auth_type(key);
|
|
|
|
|
let oauth_managed = auth_type == "oauth"
|
2026-05-16 19:23:09 +08:00
|
|
|
|| provider_key_is_legacy_kiro_oauth_session(key, provider_type, &auth_type)
|
|
|
|
|
|| (provider_uses_grok_session_runtime(provider_type) && key_has_auth_config(key));
|
2026-04-17 12:57:06 +08:00
|
|
|
let credential_kind = if oauth_managed {
|
|
|
|
|
ProviderKeyCredentialKind::OAuthSession
|
|
|
|
|
} else if matches!(auth_type.as_str(), "service_account" | "vertex_ai") {
|
|
|
|
|
ProviderKeyCredentialKind::ServiceAccount
|
|
|
|
|
} else {
|
|
|
|
|
ProviderKeyCredentialKind::RawSecret
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let runtime_auth_kind = match credential_kind {
|
|
|
|
|
ProviderKeyCredentialKind::OAuthSession => {
|
|
|
|
|
if provider_uses_bearer_oauth_runtime(provider_type) {
|
|
|
|
|
ProviderKeyRuntimeAuthKind::Bearer
|
2026-05-16 19:23:09 +08:00
|
|
|
} else if provider_uses_grok_session_runtime(provider_type) {
|
|
|
|
|
ProviderKeyRuntimeAuthKind::Unknown
|
2026-04-17 12:57:06 +08:00
|
|
|
} else {
|
|
|
|
|
ProviderKeyRuntimeAuthKind::Unknown
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ProviderKeyCredentialKind::ServiceAccount => ProviderKeyRuntimeAuthKind::ServiceAccount,
|
2026-04-29 15:46:50 +08:00
|
|
|
ProviderKeyCredentialKind::RawSecret => {
|
|
|
|
|
if key_has_auth_type_overrides(key) {
|
|
|
|
|
ProviderKeyRuntimeAuthKind::Mixed
|
|
|
|
|
} else {
|
|
|
|
|
match auth_type.as_str() {
|
|
|
|
|
"bearer" => ProviderKeyRuntimeAuthKind::Bearer,
|
|
|
|
|
"api_key" => ProviderKeyRuntimeAuthKind::ApiKey,
|
|
|
|
|
_ => ProviderKeyRuntimeAuthKind::Unknown,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-17 12:57:06 +08:00
|
|
|
};
|
|
|
|
|
|
2026-05-18 19:12:36 +08:00
|
|
|
let provider_type_normalized = provider_type.trim().to_ascii_lowercase();
|
2026-04-17 12:57:06 +08:00
|
|
|
ProviderKeyAuthSemantics {
|
|
|
|
|
credential_kind,
|
|
|
|
|
runtime_auth_kind,
|
|
|
|
|
oauth_managed,
|
2026-05-18 19:12:36 +08:00
|
|
|
can_refresh_oauth: oauth_managed && provider_type_normalized != "windsurf",
|
2026-04-17 12:57:06 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn provider_key_is_oauth_managed(
|
|
|
|
|
key: &StoredProviderCatalogKey,
|
|
|
|
|
provider_type: &str,
|
|
|
|
|
) -> bool {
|
|
|
|
|
provider_key_auth_semantics(key, provider_type).oauth_managed()
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 14:42:51 +08:00
|
|
|
pub(crate) fn provider_key_configured_api_formats(key: &StoredProviderCatalogKey) -> Vec<String> {
|
2026-04-29 09:25:19 +08:00
|
|
|
let mut seen = BTreeSet::new();
|
2026-04-23 14:42:51 +08:00
|
|
|
key.api_formats
|
|
|
|
|
.as_ref()
|
|
|
|
|
.and_then(serde_json::Value::as_array)
|
|
|
|
|
.map(|items| {
|
|
|
|
|
items
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(serde_json::Value::as_str)
|
|
|
|
|
.map(str::trim)
|
|
|
|
|
.filter(|value| !value.is_empty())
|
2026-05-02 13:23:54 +08:00
|
|
|
.map(crate::ai_serving::normalize_api_format_alias)
|
2026-04-29 09:25:19 +08:00
|
|
|
.filter(|value| seen.insert(value.clone()))
|
2026-04-23 14:42:51 +08:00
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn provider_active_api_formats(
|
|
|
|
|
endpoints: &[StoredProviderCatalogEndpoint],
|
|
|
|
|
) -> Vec<String> {
|
|
|
|
|
let mut formats = Vec::new();
|
|
|
|
|
let mut seen = BTreeSet::new();
|
|
|
|
|
for endpoint in endpoints.iter().filter(|endpoint| endpoint.is_active) {
|
2026-05-02 13:23:54 +08:00
|
|
|
let api_format = crate::ai_serving::normalize_api_format_alias(&endpoint.api_format);
|
2026-04-29 09:25:19 +08:00
|
|
|
if api_format.is_empty() || !seen.insert(api_format.clone()) {
|
2026-04-23 14:42:51 +08:00
|
|
|
continue;
|
|
|
|
|
}
|
2026-04-29 09:25:19 +08:00
|
|
|
formats.push(api_format);
|
2026-04-23 14:42:51 +08:00
|
|
|
}
|
|
|
|
|
formats
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn provider_key_inherits_provider_api_formats(
|
|
|
|
|
key: &StoredProviderCatalogKey,
|
|
|
|
|
provider_type: &str,
|
|
|
|
|
) -> bool {
|
2026-05-02 13:23:54 +08:00
|
|
|
fixed_provider_key_inherits_api_formats(
|
|
|
|
|
provider_type,
|
|
|
|
|
&key.auth_type,
|
|
|
|
|
key.encrypted_auth_config.as_deref(),
|
|
|
|
|
)
|
2026-04-23 14:42:51 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn provider_key_effective_api_formats(
|
|
|
|
|
key: &StoredProviderCatalogKey,
|
|
|
|
|
provider_type: &str,
|
|
|
|
|
endpoints: &[StoredProviderCatalogEndpoint],
|
|
|
|
|
) -> Vec<String> {
|
|
|
|
|
if provider_key_inherits_provider_api_formats(key, provider_type) {
|
|
|
|
|
provider_active_api_formats(endpoints)
|
|
|
|
|
} else {
|
|
|
|
|
provider_key_configured_api_formats(key)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 12:57:06 +08:00
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::{
|
2026-06-20 22:08:33 +08:00
|
|
|
provider_active_api_formats, provider_key_auth_config_uses_header_authorization,
|
|
|
|
|
provider_key_auth_semantics, provider_key_can_refresh_oauth,
|
2026-04-23 14:42:51 +08:00
|
|
|
provider_key_configured_api_formats, provider_key_effective_api_formats,
|
|
|
|
|
provider_key_inherits_provider_api_formats, ProviderKeyCredentialKind,
|
|
|
|
|
ProviderKeyRuntimeAuthKind,
|
2026-04-17 12:57:06 +08:00
|
|
|
};
|
2026-04-23 14:42:51 +08:00
|
|
|
use aether_data_contracts::repository::provider_catalog::{
|
|
|
|
|
StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
|
|
|
|
|
};
|
|
|
|
|
use serde_json::json;
|
2026-04-17 12:57:06 +08:00
|
|
|
|
|
|
|
|
fn sample_key(auth_type: &str) -> StoredProviderCatalogKey {
|
|
|
|
|
StoredProviderCatalogKey::new(
|
|
|
|
|
"key-1".to_string(),
|
|
|
|
|
"provider-1".to_string(),
|
|
|
|
|
"key-1".to_string(),
|
|
|
|
|
auth_type.to_string(),
|
|
|
|
|
None,
|
|
|
|
|
true,
|
|
|
|
|
)
|
|
|
|
|
.expect("key should build")
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 14:42:51 +08:00
|
|
|
fn sample_endpoint(api_format: &str, is_active: bool) -> StoredProviderCatalogEndpoint {
|
|
|
|
|
let mut endpoint = StoredProviderCatalogEndpoint::new(
|
|
|
|
|
format!("endpoint-{api_format}"),
|
|
|
|
|
"provider-1".to_string(),
|
|
|
|
|
api_format.to_string(),
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
true,
|
|
|
|
|
)
|
|
|
|
|
.expect("endpoint should build");
|
|
|
|
|
endpoint.is_active = is_active;
|
|
|
|
|
endpoint.base_url = "https://example.invalid".to_string();
|
|
|
|
|
endpoint
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 12:57:06 +08:00
|
|
|
#[test]
|
|
|
|
|
fn recognizes_oauth_managed_key() {
|
|
|
|
|
let semantics = provider_key_auth_semantics(&sample_key("oauth"), "codex");
|
|
|
|
|
|
|
|
|
|
assert!(semantics.oauth_managed());
|
|
|
|
|
assert_eq!(
|
|
|
|
|
semantics.credential_kind(),
|
|
|
|
|
ProviderKeyCredentialKind::OAuthSession
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
semantics.runtime_auth_kind(),
|
|
|
|
|
ProviderKeyRuntimeAuthKind::Bearer
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 02:29:17 +08:00
|
|
|
#[test]
|
|
|
|
|
fn recognizes_chatgpt_web_oauth_as_bearer_runtime() {
|
|
|
|
|
let semantics = provider_key_auth_semantics(&sample_key("oauth"), "chatgpt_web");
|
|
|
|
|
|
|
|
|
|
assert!(semantics.oauth_managed());
|
|
|
|
|
assert_eq!(
|
|
|
|
|
semantics.credential_kind(),
|
|
|
|
|
ProviderKeyCredentialKind::OAuthSession
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
semantics.runtime_auth_kind(),
|
|
|
|
|
ProviderKeyRuntimeAuthKind::Bearer
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 19:23:09 +08:00
|
|
|
#[test]
|
|
|
|
|
fn recognizes_grok_oauth_session_as_managed_without_bearer_runtime() {
|
|
|
|
|
let mut key = sample_key("oauth");
|
|
|
|
|
key.encrypted_auth_config = Some(r#"{"sso_token":"abc"}"#.to_string());
|
|
|
|
|
|
|
|
|
|
let semantics = provider_key_auth_semantics(&key, "grok");
|
|
|
|
|
assert!(semantics.oauth_managed());
|
|
|
|
|
assert_eq!(
|
|
|
|
|
semantics.credential_kind(),
|
|
|
|
|
ProviderKeyCredentialKind::OAuthSession
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
semantics.runtime_auth_kind(),
|
|
|
|
|
ProviderKeyRuntimeAuthKind::Unknown
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 15:23:08 +08:00
|
|
|
#[test]
|
|
|
|
|
fn recognizes_windsurf_oauth_as_bearer_runtime() {
|
|
|
|
|
let semantics = provider_key_auth_semantics(&sample_key("oauth"), "windsurf");
|
|
|
|
|
|
|
|
|
|
assert!(semantics.oauth_managed());
|
2026-05-18 19:12:36 +08:00
|
|
|
assert!(!semantics.can_refresh_oauth());
|
2026-05-18 15:23:08 +08:00
|
|
|
assert_eq!(
|
|
|
|
|
semantics.credential_kind(),
|
|
|
|
|
ProviderKeyCredentialKind::OAuthSession
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
semantics.runtime_auth_kind(),
|
|
|
|
|
ProviderKeyRuntimeAuthKind::Bearer
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 19:23:09 +08:00
|
|
|
#[test]
|
|
|
|
|
fn refresh_capability_requires_stored_refresh_token() {
|
|
|
|
|
let semantics = provider_key_auth_semantics(&sample_key("oauth"), "codex");
|
|
|
|
|
|
|
|
|
|
assert!(!provider_key_can_refresh_oauth(
|
|
|
|
|
semantics,
|
|
|
|
|
json!({
|
|
|
|
|
"access_token": "access-token",
|
|
|
|
|
"access_token_import_temporary": true
|
|
|
|
|
})
|
|
|
|
|
.as_object()
|
|
|
|
|
));
|
|
|
|
|
assert!(!provider_key_can_refresh_oauth(
|
|
|
|
|
semantics,
|
|
|
|
|
json!({ "refresh_token": " " }).as_object()
|
|
|
|
|
));
|
|
|
|
|
assert!(provider_key_can_refresh_oauth(
|
|
|
|
|
semantics,
|
|
|
|
|
json!({ "refresh_token": "refresh-token" }).as_object()
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-20 22:08:33 +08:00
|
|
|
#[test]
|
|
|
|
|
fn detects_oauth_header_authorization_in_auth_config() {
|
|
|
|
|
assert!(provider_key_auth_config_uses_header_authorization(
|
|
|
|
|
json!({
|
|
|
|
|
"headers": {
|
|
|
|
|
"Authorization": "Bearer imported-session"
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.as_object()
|
|
|
|
|
));
|
|
|
|
|
assert!(!provider_key_auth_config_uses_header_authorization(
|
|
|
|
|
json!({
|
|
|
|
|
"headers": {
|
|
|
|
|
"authorization": " "
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.as_object()
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 12:57:06 +08:00
|
|
|
#[test]
|
|
|
|
|
fn recognizes_legacy_kiro_bearer_key_with_auth_config_as_oauth_managed() {
|
|
|
|
|
let mut key = sample_key("bearer");
|
|
|
|
|
key.encrypted_auth_config = Some("ciphertext".to_string());
|
|
|
|
|
|
|
|
|
|
let semantics = provider_key_auth_semantics(&key, "kiro");
|
|
|
|
|
|
|
|
|
|
assert!(semantics.oauth_managed());
|
|
|
|
|
assert_eq!(
|
|
|
|
|
semantics.credential_kind(),
|
|
|
|
|
ProviderKeyCredentialKind::OAuthSession
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
semantics.runtime_auth_kind(),
|
|
|
|
|
ProviderKeyRuntimeAuthKind::Bearer
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn keeps_plain_bearer_key_as_raw_secret() {
|
|
|
|
|
let semantics = provider_key_auth_semantics(&sample_key("bearer"), "kiro");
|
|
|
|
|
|
|
|
|
|
assert!(!semantics.oauth_managed());
|
|
|
|
|
assert_eq!(
|
|
|
|
|
semantics.credential_kind(),
|
|
|
|
|
ProviderKeyCredentialKind::RawSecret
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
semantics.runtime_auth_kind(),
|
|
|
|
|
ProviderKeyRuntimeAuthKind::Bearer
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn recognizes_service_account_key() {
|
|
|
|
|
let semantics = provider_key_auth_semantics(&sample_key("service_account"), "vertex_ai");
|
|
|
|
|
|
|
|
|
|
assert!(!semantics.oauth_managed());
|
|
|
|
|
assert_eq!(
|
|
|
|
|
semantics.credential_kind(),
|
|
|
|
|
ProviderKeyCredentialKind::ServiceAccount
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
semantics.runtime_auth_kind(),
|
|
|
|
|
ProviderKeyRuntimeAuthKind::ServiceAccount
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-04-23 14:42:51 +08:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deduplicates_active_provider_api_formats() {
|
|
|
|
|
let endpoints = vec![
|
2026-04-26 20:32:55 +08:00
|
|
|
sample_endpoint("openai:responses", true),
|
2026-04-23 14:42:51 +08:00
|
|
|
sample_endpoint("openai:image", true),
|
2026-04-26 20:32:55 +08:00
|
|
|
sample_endpoint("openai:responses", true),
|
|
|
|
|
sample_endpoint("openai:responses:compact", false),
|
2026-04-23 14:42:51 +08:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
provider_active_api_formats(&endpoints),
|
2026-04-26 20:32:55 +08:00
|
|
|
vec!["openai:responses".to_string(), "openai:image".to_string()]
|
2026-04-23 14:42:51 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn fixed_oauth_key_with_null_formats_inherits_provider_formats() {
|
|
|
|
|
let key = sample_key("oauth");
|
|
|
|
|
let endpoints = vec![
|
2026-04-26 20:32:55 +08:00
|
|
|
sample_endpoint("openai:responses", true),
|
2026-04-23 14:42:51 +08:00
|
|
|
sample_endpoint("openai:image", true),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
assert!(provider_key_inherits_provider_api_formats(&key, "codex"));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
provider_key_effective_api_formats(&key, "codex", &endpoints),
|
2026-04-26 20:32:55 +08:00
|
|
|
vec!["openai:responses".to_string(), "openai:image".to_string()]
|
2026-04-23 14:42:51 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn fixed_oauth_key_with_legacy_explicit_formats_still_inherits_provider_formats() {
|
|
|
|
|
let mut key = sample_key("oauth");
|
2026-04-26 21:12:57 +08:00
|
|
|
key.api_formats = Some(json!(["openai:responses:compact"]));
|
2026-04-23 14:42:51 +08:00
|
|
|
let endpoints = vec![
|
2026-04-26 20:32:55 +08:00
|
|
|
sample_endpoint("openai:responses", true),
|
2026-04-23 14:42:51 +08:00
|
|
|
sample_endpoint("openai:image", true),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
assert!(provider_key_inherits_provider_api_formats(&key, "codex"));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
provider_key_effective_api_formats(&key, "codex", &endpoints),
|
2026-04-26 20:32:55 +08:00
|
|
|
vec!["openai:responses".to_string(), "openai:image".to_string()]
|
2026-04-23 14:42:51 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 13:23:54 +08:00
|
|
|
#[test]
|
|
|
|
|
fn configured_kiro_bearer_key_inherits_provider_formats() {
|
|
|
|
|
let mut key = sample_key("bearer");
|
|
|
|
|
key.encrypted_auth_config = Some("encrypted-auth-config".to_string());
|
|
|
|
|
key.api_formats = Some(json!(["openai:responses:compact"]));
|
|
|
|
|
let endpoints = vec![
|
|
|
|
|
sample_endpoint("claude:messages", true),
|
|
|
|
|
sample_endpoint("openai:chat", true),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
assert!(provider_key_inherits_provider_api_formats(&key, "kiro"));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
provider_key_effective_api_formats(&key, "kiro", &endpoints),
|
|
|
|
|
vec!["claude:messages".to_string(), "openai:chat".to_string()]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 14:42:51 +08:00
|
|
|
#[test]
|
|
|
|
|
fn explicit_formats_do_not_inherit_for_non_fixed_key() {
|
|
|
|
|
let mut key = sample_key("oauth");
|
2026-04-26 20:32:55 +08:00
|
|
|
key.api_formats = Some(json!(["openai:responses:compact"]));
|
2026-04-23 14:42:51 +08:00
|
|
|
let endpoints = vec![
|
2026-04-26 20:32:55 +08:00
|
|
|
sample_endpoint("openai:responses", true),
|
2026-04-23 14:42:51 +08:00
|
|
|
sample_endpoint("openai:image", true),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
assert!(!provider_key_inherits_provider_api_formats(&key, "openai"));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
provider_key_configured_api_formats(&key),
|
2026-04-26 20:32:55 +08:00
|
|
|
vec!["openai:responses:compact".to_string()]
|
2026-04-23 14:42:51 +08:00
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
provider_key_effective_api_formats(&key, "openai", &endpoints),
|
2026-04-26 20:32:55 +08:00
|
|
|
vec!["openai:responses:compact".to_string()]
|
2026-04-23 14:42:51 +08:00
|
|
|
);
|
|
|
|
|
}
|
2026-04-17 12:57:06 +08:00
|
|
|
}
|