mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-09 18:59:50 +08:00
Normalize canonical API formats
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
use crate::handlers::admin::provider::shared::payloads::AdminProviderKeyCreateRequest;
|
||||
use crate::handlers::admin::provider::write::normalize::{
|
||||
normalize_auth_type, validate_vertex_api_formats,
|
||||
normalize_api_format_json_object_keys, normalize_api_format_list, normalize_auth_type,
|
||||
validate_vertex_api_formats,
|
||||
};
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::admin::shared::{
|
||||
@@ -26,8 +27,10 @@ pub(crate) async fn build_admin_create_provider_key_record(
|
||||
return Err("name 为必填字段".to_string());
|
||||
}
|
||||
|
||||
let api_formats = normalize_string_list(payload.api_formats)
|
||||
.ok_or_else(|| "api_formats 为必填字段".to_string())?;
|
||||
let api_formats = normalize_api_format_list(
|
||||
normalize_string_list(payload.api_formats)
|
||||
.ok_or_else(|| "api_formats 为必填字段".to_string())?,
|
||||
);
|
||||
let auth_type = normalize_auth_type(payload.auth_type.as_deref())?;
|
||||
validate_vertex_api_formats(&provider.provider_type, &auth_type, &api_formats)?;
|
||||
|
||||
@@ -149,7 +152,7 @@ pub(crate) async fn build_admin_create_provider_key_record(
|
||||
},
|
||||
encrypted_api_key,
|
||||
encrypted_auth_config,
|
||||
normalize_json_object(payload.rate_multipliers, "rate_multipliers")?,
|
||||
normalize_api_format_json_object_keys(payload.rate_multipliers, "rate_multipliers")?,
|
||||
None,
|
||||
normalize_string_list(payload.allowed_models).map(|value| json!(value)),
|
||||
None,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::handlers::admin::provider::shared::payloads::AdminProviderKeyUpdatePatch;
|
||||
use crate::handlers::admin::provider::write::normalize::{
|
||||
normalize_auth_type, validate_vertex_api_formats,
|
||||
normalize_api_format_json_object_keys, normalize_api_format_list, normalize_auth_type,
|
||||
validate_vertex_api_formats,
|
||||
};
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::admin::shared::{
|
||||
@@ -190,8 +191,10 @@ pub(crate) async fn build_admin_update_provider_key_record(
|
||||
}
|
||||
|
||||
if fields.contains("api_formats") {
|
||||
let api_formats = normalize_string_list(payload.api_formats)
|
||||
.ok_or_else(|| "api_formats 为必填字段".to_string())?;
|
||||
let api_formats = normalize_api_format_list(
|
||||
normalize_string_list(payload.api_formats)
|
||||
.ok_or_else(|| "api_formats 为必填字段".to_string())?,
|
||||
);
|
||||
if managed_fixed_oauth_key {
|
||||
updated.api_formats = None;
|
||||
} else {
|
||||
@@ -202,7 +205,8 @@ pub(crate) async fn build_admin_update_provider_key_record(
|
||||
if managed_fixed_oauth_key {
|
||||
updated.api_formats = None;
|
||||
} else {
|
||||
let api_formats = json_string_list(existing.api_formats.as_ref());
|
||||
let api_formats =
|
||||
normalize_api_format_list(json_string_list(existing.api_formats.as_ref()));
|
||||
validate_vertex_api_formats(&provider.provider_type, &target_auth_type, &api_formats)?;
|
||||
}
|
||||
}
|
||||
@@ -218,13 +222,13 @@ pub(crate) async fn build_admin_update_provider_key_record(
|
||||
}
|
||||
if fields.contains("rate_multipliers") {
|
||||
updated.rate_multipliers =
|
||||
normalize_json_object(payload.rate_multipliers, "rate_multipliers")?;
|
||||
normalize_api_format_json_object_keys(payload.rate_multipliers, "rate_multipliers")?;
|
||||
}
|
||||
if let Some(internal_priority) = payload.internal_priority {
|
||||
updated.internal_priority = internal_priority;
|
||||
}
|
||||
if fields.contains("global_priority_by_format") {
|
||||
updated.global_priority_by_format = normalize_json_object(
|
||||
updated.global_priority_by_format = normalize_api_format_json_object_keys(
|
||||
payload.global_priority_by_format,
|
||||
"global_priority_by_format",
|
||||
)?;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
pub(crate) fn normalize_provider_type_input(value: &str) -> Result<String, String> {
|
||||
let normalized = value.trim().to_ascii_lowercase();
|
||||
match normalized.as_str() {
|
||||
@@ -10,6 +12,36 @@ pub(crate) fn normalize_provider_type_input(value: &str) -> Result<String, Strin
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_api_format_list(values: Vec<String>) -> Vec<String> {
|
||||
let mut seen = BTreeSet::new();
|
||||
let mut normalized = Vec::new();
|
||||
for value in values {
|
||||
let canonical = crate::ai_pipeline::normalize_api_format_alias(&value);
|
||||
if seen.insert(canonical.clone()) {
|
||||
normalized.push(canonical);
|
||||
}
|
||||
}
|
||||
normalized
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_api_format_json_object_keys(
|
||||
value: Option<serde_json::Value>,
|
||||
field_name: &str,
|
||||
) -> Result<Option<serde_json::Value>, String> {
|
||||
let Some(value) = normalize_json_like_object(value, field_name)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let serde_json::Value::Object(map) = value else {
|
||||
return Ok(Some(value));
|
||||
};
|
||||
let mut normalized = serde_json::Map::new();
|
||||
for (key, value) in map {
|
||||
let canonical = crate::ai_pipeline::normalize_api_format_alias(&key);
|
||||
normalized.insert(canonical, value);
|
||||
}
|
||||
Ok(Some(serde_json::Value::Object(normalized)))
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_auth_type(value: Option<&str>) -> Result<String, String> {
|
||||
let auth_type = value.unwrap_or("api_key").trim().to_ascii_lowercase();
|
||||
match auth_type.as_str() {
|
||||
@@ -42,8 +74,8 @@ pub(crate) fn validate_vertex_api_formats(
|
||||
}
|
||||
|
||||
let allowed = match auth_type {
|
||||
"api_key" => &["gemini:chat"][..],
|
||||
"service_account" | "vertex_ai" => &["claude:chat", "gemini:chat"][..],
|
||||
"api_key" => &["gemini:generate_content"][..],
|
||||
"service_account" | "vertex_ai" => &["claude:messages", "gemini:generate_content"][..],
|
||||
_ => return Ok(()),
|
||||
};
|
||||
let invalid = api_formats
|
||||
@@ -61,9 +93,26 @@ pub(crate) fn validate_vertex_api_formats(
|
||||
))
|
||||
}
|
||||
|
||||
fn normalize_json_like_object(
|
||||
value: Option<serde_json::Value>,
|
||||
field_name: &str,
|
||||
) -> Result<Option<serde_json::Value>, String> {
|
||||
let Some(value) = value else {
|
||||
return Ok(None);
|
||||
};
|
||||
match value {
|
||||
serde_json::Value::Null => Ok(None),
|
||||
serde_json::Value::Object(map) => Ok(Some(serde_json::Value::Object(map))),
|
||||
_ => Err(format!("{field_name} 必须是 JSON 对象")),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{normalize_auth_type, normalize_pool_advanced_config};
|
||||
use super::{
|
||||
normalize_api_format_json_object_keys, normalize_api_format_list, normalize_auth_type,
|
||||
normalize_pool_advanced_config, validate_vertex_api_formats,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
@@ -93,4 +142,60 @@ mod tests {
|
||||
"bearer"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_api_format_list_dedupes_canonical_formats() {
|
||||
assert_eq!(
|
||||
normalize_api_format_list(vec![
|
||||
"claude:messages".to_string(),
|
||||
"claude:messages".to_string(),
|
||||
"gemini:generate_content".to_string(),
|
||||
"openai:image".to_string(),
|
||||
]),
|
||||
vec![
|
||||
"claude:messages".to_string(),
|
||||
"gemini:generate_content".to_string(),
|
||||
"openai:image".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_api_format_json_object_keys_keeps_canonical_keys() {
|
||||
assert_eq!(
|
||||
normalize_api_format_json_object_keys(
|
||||
Some(json!({
|
||||
"claude:messages": 2,
|
||||
"gemini:generate_content": 3,
|
||||
"openai:video": 4
|
||||
})),
|
||||
"rate_multipliers",
|
||||
)
|
||||
.expect("object should normalize"),
|
||||
Some(json!({
|
||||
"claude:messages": 2,
|
||||
"gemini:generate_content": 3,
|
||||
"openai:video": 4
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_vertex_api_formats_uses_canonical_message_formats() {
|
||||
assert!(validate_vertex_api_formats(
|
||||
"vertex_ai",
|
||||
"service_account",
|
||||
&[
|
||||
"claude:messages".to_string(),
|
||||
"gemini:generate_content".to_string()
|
||||
],
|
||||
)
|
||||
.is_ok());
|
||||
assert!(validate_vertex_api_formats(
|
||||
"vertex_ai",
|
||||
"service_account",
|
||||
&["claude:chat".to_string()],
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,7 +350,7 @@ fn endpoint_matches_fixed_provider_template(
|
||||
}
|
||||
|
||||
fn normalize_api_format_alias(value: &str) -> String {
|
||||
crate::ai_pipeline::normalize_legacy_openai_format_alias(value)
|
||||
crate::ai_pipeline::normalize_api_format_alias(value)
|
||||
}
|
||||
|
||||
fn api_format_matches(left: &str, right: &str) -> bool {
|
||||
|
||||
Reference in New Issue
Block a user