2026-04-09 00:10:38 +08:00
|
|
|
|
use crate::handlers::admin::provider::shared::payloads::AdminProviderKeyCreateRequest;
|
|
|
|
|
|
use crate::handlers::admin::provider::write::normalize::{
|
2026-05-03 00:49:22 +08:00
|
|
|
|
normalize_allow_auth_channel_mismatch_formats, normalize_api_format_json_object_keys,
|
|
|
|
|
|
normalize_api_format_list, normalize_auth_type, normalize_auth_type_by_format,
|
2026-07-11 03:42:30 +08:00
|
|
|
|
normalize_max_probe_interval_minutes, normalize_rate_multipliers, validate_vertex_api_formats,
|
2026-04-09 00:10:38 +08:00
|
|
|
|
};
|
|
|
|
|
|
use crate::handlers::admin::request::AdminAppState;
|
|
|
|
|
|
use crate::handlers::admin::shared::{
|
|
|
|
|
|
decrypt_catalog_secret_with_fallbacks, encrypt_catalog_secret_with_fallbacks,
|
|
|
|
|
|
normalize_json_object, normalize_string_list, parse_catalog_auth_config_json,
|
|
|
|
|
|
};
|
2026-05-03 01:55:23 +08:00
|
|
|
|
use crate::handlers::shared::normalize_optional_api_key_concurrent_limit;
|
2026-04-09 00:10:38 +08:00
|
|
|
|
use aether_data_contracts::repository::provider_catalog::{
|
|
|
|
|
|
StoredProviderCatalogKey, StoredProviderCatalogProvider,
|
|
|
|
|
|
};
|
2026-04-23 14:42:51 +08:00
|
|
|
|
use aether_provider_transport::provider_types::provider_type_is_fixed;
|
2026-04-09 00:10:38 +08:00
|
|
|
|
use serde_json::json;
|
|
|
|
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
|
|
|
|
|
|
pub(crate) async fn build_admin_create_provider_key_record(
|
|
|
|
|
|
state: &AdminAppState<'_>,
|
|
|
|
|
|
provider: &StoredProviderCatalogProvider,
|
|
|
|
|
|
payload: AdminProviderKeyCreateRequest,
|
|
|
|
|
|
) -> Result<StoredProviderCatalogKey, String> {
|
|
|
|
|
|
let state = state.as_ref();
|
|
|
|
|
|
let name = payload.name.trim();
|
|
|
|
|
|
if name.is_empty() {
|
|
|
|
|
|
return Err("name 为必填字段".to_string());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 09:25:19 +08:00
|
|
|
|
let api_formats = normalize_api_format_list(
|
|
|
|
|
|
normalize_string_list(payload.api_formats)
|
|
|
|
|
|
.ok_or_else(|| "api_formats 为必填字段".to_string())?,
|
|
|
|
|
|
);
|
2026-04-09 00:10:38 +08:00
|
|
|
|
let auth_type = normalize_auth_type(payload.auth_type.as_deref())?;
|
|
|
|
|
|
validate_vertex_api_formats(&provider.provider_type, &auth_type, &api_formats)?;
|
2026-04-29 15:46:50 +08:00
|
|
|
|
let auth_type_by_format = if matches!(auth_type.as_str(), "api_key" | "bearer") {
|
|
|
|
|
|
normalize_auth_type_by_format(
|
|
|
|
|
|
payload.auth_type_by_format,
|
|
|
|
|
|
"auth_type_by_format",
|
|
|
|
|
|
&api_formats,
|
|
|
|
|
|
)?
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
2026-04-09 00:10:38 +08:00
|
|
|
|
|
|
|
|
|
|
let api_key = payload.api_key.unwrap_or_default().trim().to_string();
|
|
|
|
|
|
let auth_config = normalize_json_object(payload.auth_config, "auth_config")?;
|
|
|
|
|
|
let auth_config_object = auth_config
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.and_then(serde_json::Value::as_object)
|
|
|
|
|
|
.cloned();
|
|
|
|
|
|
|
2026-07-21 20:57:48 +08:00
|
|
|
|
if auth_type == "oauth"
|
|
|
|
|
|
&& provider.provider_type.trim().eq_ignore_ascii_case("codex")
|
|
|
|
|
|
&& auth_config
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.is_some_and(aether_provider_transport::is_codex_agent_identity_auth_config_value)
|
|
|
|
|
|
{
|
|
|
|
|
|
aether_provider_transport::validate_codex_agent_identity_auth_config(
|
|
|
|
|
|
auth_config
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.expect("Agent Identity auth_config was checked"),
|
|
|
|
|
|
)?;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-09 00:10:38 +08:00
|
|
|
|
match auth_type.as_str() {
|
2026-04-17 01:43:45 +08:00
|
|
|
|
"service_account" if auth_config_object.is_none() => {
|
|
|
|
|
|
return Err("Service Account 认证模式下 auth_config 为必填字段".to_string());
|
2026-04-09 00:10:38 +08:00
|
|
|
|
}
|
2026-04-17 01:43:45 +08:00
|
|
|
|
"oauth" if !api_key.is_empty() => {
|
|
|
|
|
|
return Err("OAuth 认证模式下不允许直接填写 api_key".to_string());
|
2026-04-09 00:10:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let existing_keys = state
|
|
|
|
|
|
.list_provider_catalog_keys_by_provider_ids(std::slice::from_ref(&provider.id))
|
|
|
|
|
|
.await
|
|
|
|
|
|
.map_err(|err| format!("{err:?}"))?;
|
|
|
|
|
|
|
2026-04-29 15:46:50 +08:00
|
|
|
|
if matches!(auth_type.as_str(), "api_key" | "bearer") && !api_key.is_empty() {
|
2026-04-09 00:10:38 +08:00
|
|
|
|
for existing in existing_keys
|
|
|
|
|
|
.iter()
|
2026-04-29 15:46:50 +08:00
|
|
|
|
.filter(|existing| raw_secret_auth_type(&existing.auth_type))
|
2026-04-09 00:10:38 +08:00
|
|
|
|
{
|
2026-04-29 15:46:50 +08:00
|
|
|
|
let Some(decrypted) = existing
|
|
|
|
|
|
.encrypted_api_key
|
|
|
|
|
|
.as_deref()
|
|
|
|
|
|
.and_then(|ciphertext| {
|
|
|
|
|
|
decrypt_catalog_secret_with_fallbacks(state.encryption_key(), ciphertext)
|
|
|
|
|
|
})
|
|
|
|
|
|
else {
|
2026-04-09 00:10:38 +08:00
|
|
|
|
continue;
|
|
|
|
|
|
};
|
|
|
|
|
|
if decrypted != "__placeholder__" && decrypted == api_key {
|
|
|
|
|
|
return Err(format!(
|
|
|
|
|
|
"该 API Key 已存在于当前 Provider 中(名称: {})",
|
|
|
|
|
|
existing.name
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if auth_type == "service_account" {
|
|
|
|
|
|
let new_client_email = auth_config_object
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.and_then(|config| config.get("client_email"))
|
|
|
|
|
|
.and_then(serde_json::Value::as_str)
|
|
|
|
|
|
.map(str::trim)
|
|
|
|
|
|
.filter(|value| !value.is_empty())
|
|
|
|
|
|
.map(ToOwned::to_owned);
|
|
|
|
|
|
if let Some(new_client_email) = new_client_email {
|
|
|
|
|
|
for existing in existing_keys.iter().filter(|existing| {
|
|
|
|
|
|
matches!(
|
|
|
|
|
|
existing.auth_type.trim().to_ascii_lowercase().as_str(),
|
|
|
|
|
|
"service_account" | "vertex_ai"
|
|
|
|
|
|
)
|
|
|
|
|
|
}) {
|
|
|
|
|
|
let Some(existing_config) = parse_catalog_auth_config_json(state, existing) else {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
};
|
|
|
|
|
|
let Some(existing_email) = existing_config
|
|
|
|
|
|
.get("client_email")
|
|
|
|
|
|
.and_then(serde_json::Value::as_str)
|
|
|
|
|
|
.map(str::trim)
|
|
|
|
|
|
.filter(|value| !value.is_empty())
|
|
|
|
|
|
else {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
};
|
|
|
|
|
|
if existing_email == new_client_email {
|
|
|
|
|
|
return Err(format!(
|
|
|
|
|
|
"该 Service Account ({new_client_email}) 已存在于当前 Provider 中(名称: {})",
|
|
|
|
|
|
existing.name
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let encrypted_api_key = match auth_type.as_str() {
|
2026-04-29 15:46:50 +08:00
|
|
|
|
"api_key" | "bearer" if !api_key.is_empty() => Some(
|
|
|
|
|
|
encrypt_catalog_secret_with_fallbacks(state, &api_key)
|
|
|
|
|
|
.ok_or_else(|| "gateway 未配置 provider key 加密密钥".to_string())?,
|
|
|
|
|
|
),
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
};
|
2026-04-09 00:10:38 +08:00
|
|
|
|
|
|
|
|
|
|
let encrypted_auth_config = auth_config
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.map(serde_json::to_string)
|
|
|
|
|
|
.transpose()
|
|
|
|
|
|
.map_err(|err| err.to_string())?
|
|
|
|
|
|
.and_then(|plaintext| encrypt_catalog_secret_with_fallbacks(state, &plaintext));
|
|
|
|
|
|
|
|
|
|
|
|
let now_unix_secs = SystemTime::now()
|
|
|
|
|
|
.duration_since(UNIX_EPOCH)
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.map(|duration| duration.as_secs())
|
|
|
|
|
|
.unwrap_or(0);
|
2026-04-23 14:42:51 +08:00
|
|
|
|
let inherits_provider_api_formats =
|
|
|
|
|
|
auth_type == "oauth" && provider_type_is_fixed(&provider.provider_type);
|
2026-04-09 00:10:38 +08:00
|
|
|
|
let mut key = StoredProviderCatalogKey::new(
|
|
|
|
|
|
Uuid::new_v4().to_string(),
|
|
|
|
|
|
provider.id.clone(),
|
|
|
|
|
|
name.to_string(),
|
|
|
|
|
|
auth_type,
|
|
|
|
|
|
normalize_json_object(payload.capabilities, "capabilities")?,
|
|
|
|
|
|
true,
|
|
|
|
|
|
)
|
|
|
|
|
|
.map_err(|err| err.to_string())?
|
|
|
|
|
|
.with_transport_fields(
|
2026-04-23 14:42:51 +08:00
|
|
|
|
if inherits_provider_api_formats {
|
|
|
|
|
|
None
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Some(json!(api_formats))
|
|
|
|
|
|
},
|
2026-04-09 00:10:38 +08:00
|
|
|
|
encrypted_api_key,
|
|
|
|
|
|
encrypted_auth_config,
|
2026-07-11 03:42:30 +08:00
|
|
|
|
normalize_rate_multipliers(payload.rate_multipliers)?,
|
2026-04-09 00:10:38 +08:00
|
|
|
|
None,
|
|
|
|
|
|
normalize_string_list(payload.allowed_models).map(|value| json!(value)),
|
|
|
|
|
|
None,
|
|
|
|
|
|
None,
|
2026-05-05 22:21:23 +08:00
|
|
|
|
normalize_json_object(payload.fingerprint, "fingerprint")?,
|
2026-04-09 00:10:38 +08:00
|
|
|
|
)
|
|
|
|
|
|
.map_err(|err| err.to_string())?;
|
|
|
|
|
|
key.note = payload
|
|
|
|
|
|
.note
|
|
|
|
|
|
.map(|value| value.trim().to_string())
|
|
|
|
|
|
.filter(|value| !value.is_empty());
|
|
|
|
|
|
key.internal_priority = payload.internal_priority.unwrap_or(50);
|
|
|
|
|
|
key.rpm_limit = payload.rpm_limit;
|
2026-05-03 01:55:23 +08:00
|
|
|
|
key.concurrent_limit = normalize_optional_api_key_concurrent_limit(payload.concurrent_limit)?;
|
2026-04-09 00:10:38 +08:00
|
|
|
|
key.cache_ttl_minutes = payload.cache_ttl_minutes.unwrap_or(5);
|
2026-05-19 13:57:36 +08:00
|
|
|
|
key.max_probe_interval_minutes =
|
|
|
|
|
|
normalize_max_probe_interval_minutes(payload.max_probe_interval_minutes.unwrap_or(32))?;
|
2026-04-09 00:10:38 +08:00
|
|
|
|
key.request_count = Some(0);
|
|
|
|
|
|
key.success_count = Some(0);
|
|
|
|
|
|
key.error_count = Some(0);
|
|
|
|
|
|
key.total_response_time_ms = Some(0);
|
|
|
|
|
|
key.auto_fetch_models = payload.auto_fetch_models.unwrap_or(false);
|
|
|
|
|
|
key.locked_models = normalize_string_list(payload.locked_models).map(|value| json!(value));
|
|
|
|
|
|
key.model_include_patterns =
|
|
|
|
|
|
normalize_string_list(payload.model_include_patterns).map(|value| json!(value));
|
|
|
|
|
|
key.model_exclude_patterns =
|
|
|
|
|
|
normalize_string_list(payload.model_exclude_patterns).map(|value| json!(value));
|
|
|
|
|
|
key.health_by_format = Some(json!({}));
|
|
|
|
|
|
key.circuit_breaker_by_format = Some(json!({}));
|
2026-04-29 15:46:50 +08:00
|
|
|
|
key.auth_type_by_format = auth_type_by_format;
|
2026-05-03 00:49:22 +08:00
|
|
|
|
let allow_auth_channel_mismatch_formats = payload
|
|
|
|
|
|
.allow_auth_channel_mismatch_formats
|
|
|
|
|
|
.unwrap_or_else(|| Some(api_formats.clone()));
|
|
|
|
|
|
key.allow_auth_channel_mismatch_formats = normalize_allow_auth_channel_mismatch_formats(
|
|
|
|
|
|
allow_auth_channel_mismatch_formats,
|
|
|
|
|
|
"allow_auth_channel_mismatch_formats",
|
|
|
|
|
|
&api_formats,
|
|
|
|
|
|
)?;
|
2026-04-10 01:46:14 +08:00
|
|
|
|
key.created_at_unix_ms = Some(now_unix_secs);
|
2026-04-09 00:10:38 +08:00
|
|
|
|
key.updated_at_unix_secs = Some(now_unix_secs);
|
|
|
|
|
|
Ok(key)
|
|
|
|
|
|
}
|
2026-04-29 15:46:50 +08:00
|
|
|
|
|
|
|
|
|
|
fn raw_secret_auth_type(value: &str) -> bool {
|
|
|
|
|
|
matches!(
|
|
|
|
|
|
value.trim().to_ascii_lowercase().as_str(),
|
|
|
|
|
|
"api_key" | "bearer"
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|