mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
Normalize canonical API formats
This commit is contained in:
@@ -130,17 +130,17 @@ async fn admin_gemini_files_upload_single_key(
|
||||
&& endpoint
|
||||
.api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("gemini:chat")
|
||||
.eq_ignore_ascii_case("gemini:generate_content")
|
||||
})
|
||||
}) else {
|
||||
return Err("找不到有效的 gemini:chat 端点".to_string());
|
||||
return Err("找不到有效的 gemini:generate_content 端点".to_string());
|
||||
};
|
||||
let transport = state
|
||||
.read_provider_transport_snapshot(&key.provider_id, &endpoint.id, &key.id)
|
||||
.await
|
||||
.map_err(|err| format!("{err:?}"))?
|
||||
.ok_or_else(|| "无法读取 Key 传输配置".to_string())?;
|
||||
if !state.supports_local_gemini_transport_with_network(&transport, "gemini:chat") {
|
||||
if !state.supports_local_gemini_transport_with_network(&transport, "gemini:generate_content") {
|
||||
return Err("Key 传输配置不支持 Gemini Files 上传".to_string());
|
||||
}
|
||||
if transport.endpoint.body_rules.is_some() {
|
||||
|
||||
@@ -94,14 +94,10 @@ pub(super) async fn maybe_handle(
|
||||
}),
|
||||
"antigravity" => endpoints.into_iter().find(|endpoint| {
|
||||
endpoint.is_active
|
||||
&& (endpoint
|
||||
&& endpoint
|
||||
.api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("gemini:chat")
|
||||
|| endpoint
|
||||
.api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("gemini:cli"))
|
||||
.eq_ignore_ascii_case("gemini:generate_content")
|
||||
}),
|
||||
"kiro" => endpoints
|
||||
.iter()
|
||||
@@ -110,7 +106,7 @@ pub(super) async fn maybe_handle(
|
||||
&& endpoint
|
||||
.api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("claude:cli")
|
||||
.eq_ignore_ascii_case("claude:messages")
|
||||
})
|
||||
.cloned()
|
||||
.or_else(|| endpoints.into_iter().find(|endpoint| endpoint.is_active)),
|
||||
@@ -120,7 +116,7 @@ pub(super) async fn maybe_handle(
|
||||
let Some(endpoint) = endpoint else {
|
||||
let detail = match normalized_provider_type.as_str() {
|
||||
"codex" => "找不到有效的 openai:responses 端点",
|
||||
"antigravity" => "找不到有效的 gemini:chat/gemini:cli 端点",
|
||||
"antigravity" => "找不到有效的 gemini:generate_content 端点",
|
||||
"kiro" => "找不到有效的 Kiro 端点",
|
||||
_ => "找不到有效端点",
|
||||
};
|
||||
|
||||
@@ -68,7 +68,7 @@ async fn execute_antigravity_quota_plan(
|
||||
body_ref: None,
|
||||
},
|
||||
stream: false,
|
||||
client_api_format: "gemini:chat".to_string(),
|
||||
client_api_format: "gemini:generate_content".to_string(),
|
||||
provider_api_format: "antigravity:fetch_available_models".to_string(),
|
||||
model_name: Some("fetchAvailableModels".to_string()),
|
||||
proxy,
|
||||
|
||||
@@ -99,7 +99,7 @@ pub(super) async fn execute_kiro_quota_plan(
|
||||
body_ref: None,
|
||||
},
|
||||
stream: false,
|
||||
client_api_format: "claude:cli".to_string(),
|
||||
client_api_format: "claude:messages".to_string(),
|
||||
provider_api_format: "kiro:usage".to_string(),
|
||||
model_name: Some("kiro-usage-limits".to_string()),
|
||||
proxy,
|
||||
|
||||
@@ -26,14 +26,10 @@ pub(crate) fn provider_oauth_runtime_endpoint_for_provider(
|
||||
.iter()
|
||||
.find(|endpoint| {
|
||||
endpoint.is_active
|
||||
&& (endpoint
|
||||
&& endpoint
|
||||
.api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("gemini:chat")
|
||||
|| endpoint
|
||||
.api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("gemini:cli"))
|
||||
.eq_ignore_ascii_case("gemini:generate_content")
|
||||
})
|
||||
.cloned(),
|
||||
"kiro" => endpoints
|
||||
@@ -43,7 +39,7 @@ pub(crate) fn provider_oauth_runtime_endpoint_for_provider(
|
||||
&& endpoint
|
||||
.api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("claude:cli")
|
||||
.eq_ignore_ascii_case("claude:messages")
|
||||
})
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
|
||||
@@ -289,7 +289,7 @@ fn provider_query_key_supports_endpoint(
|
||||
}
|
||||
|
||||
fn provider_query_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 provider_query_transport_supports_standard_test_execution(
|
||||
@@ -297,7 +297,7 @@ fn provider_query_transport_supports_standard_test_execution(
|
||||
transport: &AdminGatewayProviderTransportSnapshot,
|
||||
api_format: &str,
|
||||
) -> bool {
|
||||
match crate::ai_pipeline::normalize_legacy_openai_format_alias(api_format).as_str() {
|
||||
match crate::ai_pipeline::normalize_api_format_alias(api_format).as_str() {
|
||||
"openai:chat" => {
|
||||
crate::provider_transport::policy::supports_local_openai_chat_transport(transport)
|
||||
}
|
||||
@@ -306,12 +306,12 @@ fn provider_query_transport_supports_standard_test_execution(
|
||||
transport, api_format,
|
||||
)
|
||||
}
|
||||
"claude:chat" | "claude:cli" => {
|
||||
"claude:messages" => {
|
||||
crate::provider_transport::policy::supports_local_standard_transport_with_network(
|
||||
transport, api_format,
|
||||
)
|
||||
}
|
||||
"gemini:chat" | "gemini:cli" => {
|
||||
"gemini:generate_content" => {
|
||||
if crate::provider_transport::is_vertex_api_key_transport_context(transport) {
|
||||
aether_provider_transport::vertex::supports_local_vertex_api_key_gemini_transport_with_network(transport)
|
||||
} else {
|
||||
@@ -930,7 +930,7 @@ async fn provider_query_execute_standard_test_candidate(
|
||||
|
||||
let provider_api_format = candidate.endpoint.api_format.as_str();
|
||||
let normalized_provider_api_format =
|
||||
crate::ai_pipeline::normalize_legacy_openai_format_alias(provider_api_format);
|
||||
crate::ai_pipeline::normalize_api_format_alias(provider_api_format);
|
||||
let provider_request_body = match normalized_provider_api_format.as_str() {
|
||||
"openai:chat" => {
|
||||
let Some(mut provider_request_body) =
|
||||
@@ -957,7 +957,7 @@ async fn provider_query_execute_standard_test_candidate(
|
||||
}
|
||||
provider_request_body
|
||||
}
|
||||
"claude:chat" | "claude:cli" | "gemini:chat" | "gemini:cli" => {
|
||||
"claude:messages" | "gemini:generate_content" => {
|
||||
let Some(mut provider_request_body) =
|
||||
crate::ai_pipeline::build_cross_format_openai_chat_request_body(
|
||||
&request_body,
|
||||
@@ -1035,26 +1035,22 @@ async fn provider_query_execute_standard_test_candidate(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let oauth_auth = match crate::ai_pipeline::normalize_legacy_openai_format_alias(
|
||||
provider_api_format,
|
||||
)
|
||||
.as_str()
|
||||
{
|
||||
"openai:chat" | "openai:responses" | "claude:chat" | "claude:cli" | "gemini:chat"
|
||||
| "gemini:cli" => state.resolve_local_oauth_header_auth(&transport).await?,
|
||||
_ => None,
|
||||
};
|
||||
let auth = match crate::ai_pipeline::normalize_legacy_openai_format_alias(provider_api_format)
|
||||
.as_str()
|
||||
{
|
||||
let oauth_auth =
|
||||
match crate::ai_pipeline::normalize_api_format_alias(provider_api_format).as_str() {
|
||||
"openai:chat" | "openai:responses" | "claude:messages" | "gemini:generate_content" => {
|
||||
state.resolve_local_oauth_header_auth(&transport).await?
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let auth = match crate::ai_pipeline::normalize_api_format_alias(provider_api_format).as_str() {
|
||||
"openai:chat" | "openai:responses" => {
|
||||
crate::provider_transport::auth::resolve_local_openai_bearer_auth(&transport)
|
||||
.or(oauth_auth)
|
||||
}
|
||||
"claude:chat" | "claude:cli" => {
|
||||
"claude:messages" => {
|
||||
crate::provider_transport::auth::resolve_local_standard_auth(&transport).or(oauth_auth)
|
||||
}
|
||||
"gemini:chat" | "gemini:cli" => {
|
||||
"gemini:generate_content" => {
|
||||
if uses_vertex_query_auth {
|
||||
oauth_auth
|
||||
} else {
|
||||
@@ -1099,15 +1095,13 @@ async fn provider_query_execute_standard_test_candidate(
|
||||
};
|
||||
|
||||
let mut request_headers = match provider_api_format {
|
||||
"claude:chat" | "claude:cli" => {
|
||||
crate::provider_transport::auth::build_claude_passthrough_headers(
|
||||
&parts.headers,
|
||||
auth_header.as_deref().unwrap_or_default(),
|
||||
auth_value.as_deref().unwrap_or_default(),
|
||||
&BTreeMap::new(),
|
||||
Some("application/json"),
|
||||
)
|
||||
}
|
||||
"claude:messages" => crate::provider_transport::auth::build_claude_passthrough_headers(
|
||||
&parts.headers,
|
||||
auth_header.as_deref().unwrap_or_default(),
|
||||
auth_value.as_deref().unwrap_or_default(),
|
||||
&BTreeMap::new(),
|
||||
Some("application/json"),
|
||||
),
|
||||
"openai:responses" => {
|
||||
crate::provider_transport::auth::build_complete_passthrough_headers_with_auth(
|
||||
&parts.headers,
|
||||
@@ -1262,13 +1256,16 @@ fn provider_query_test_attempt_payload(
|
||||
}
|
||||
|
||||
fn provider_query_prefers_chat_standard_test_api_format(api_format: &str) -> bool {
|
||||
matches!(api_format, "openai:chat" | "claude:chat" | "gemini:chat")
|
||||
matches!(
|
||||
api_format,
|
||||
"openai:chat" | "claude:messages" | "gemini:generate_content"
|
||||
)
|
||||
}
|
||||
|
||||
fn provider_query_supports_cli_standard_test_api_format(api_format: &str) -> bool {
|
||||
matches!(
|
||||
crate::ai_pipeline::normalize_legacy_openai_format_alias(api_format).as_str(),
|
||||
"openai:responses" | "claude:cli" | "gemini:cli"
|
||||
crate::ai_pipeline::normalize_api_format_alias(api_format).as_str(),
|
||||
"openai:responses" | "claude:messages" | "gemini:generate_content"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -12,9 +12,41 @@ use aether_data_contracts::repository::global_models::{
|
||||
AdminProviderModelListQuery, StoredAdminProviderModel, UpsertAdminProviderModelRecord,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use uuid::Uuid;
|
||||
|
||||
fn normalize_provider_model_mappings_api_formats(
|
||||
value: Option<serde_json::Value>,
|
||||
) -> Option<serde_json::Value> {
|
||||
let Some(mut value) = value else {
|
||||
return None;
|
||||
};
|
||||
let Some(items) = value.as_array_mut() else {
|
||||
return Some(value);
|
||||
};
|
||||
for item in items {
|
||||
let Some(object) = item.as_object_mut() else {
|
||||
continue;
|
||||
};
|
||||
let Some(api_formats) = object.get_mut("api_formats") else {
|
||||
continue;
|
||||
};
|
||||
let Some(array) = api_formats.as_array() else {
|
||||
continue;
|
||||
};
|
||||
let mut seen = BTreeSet::new();
|
||||
let normalized = array
|
||||
.iter()
|
||||
.filter_map(serde_json::Value::as_str)
|
||||
.map(crate::ai_pipeline::normalize_api_format_alias)
|
||||
.filter(|format| seen.insert(format.clone()))
|
||||
.map(serde_json::Value::String)
|
||||
.collect::<Vec<_>>();
|
||||
*api_formats = serde_json::Value::Array(normalized);
|
||||
}
|
||||
Some(value)
|
||||
}
|
||||
|
||||
impl<'a> AdminAppState<'a> {
|
||||
pub(crate) async fn admin_provider_model_name_exists(
|
||||
&self,
|
||||
@@ -105,8 +137,9 @@ impl<'a> AdminAppState<'a> {
|
||||
"price_per_request",
|
||||
)?;
|
||||
let tiered_pricing = normalize_json_object(payload.tiered_pricing, "tiered_pricing")?;
|
||||
let provider_model_mappings =
|
||||
normalize_json_array(payload.provider_model_mappings, "provider_model_mappings")?;
|
||||
let provider_model_mappings = normalize_provider_model_mappings_api_formats(
|
||||
normalize_json_array(payload.provider_model_mappings, "provider_model_mappings")?,
|
||||
);
|
||||
let config = normalize_json_object(payload.config, "config")?;
|
||||
admin_provider_models_write_pure::build_admin_provider_model_create_record(
|
||||
Uuid::new_v4().to_string(),
|
||||
@@ -190,7 +223,10 @@ impl<'a> AdminAppState<'a> {
|
||||
existing.tiered_pricing.clone()
|
||||
};
|
||||
let provider_model_mappings = if fields.contains("provider_model_mappings") {
|
||||
normalize_json_array(payload.provider_model_mappings, "provider_model_mappings")?
|
||||
normalize_provider_model_mappings_api_formats(normalize_json_array(
|
||||
payload.provider_model_mappings,
|
||||
"provider_model_mappings",
|
||||
)?)
|
||||
} else {
|
||||
existing.provider_model_mappings.clone()
|
||||
};
|
||||
|
||||
@@ -463,7 +463,7 @@ fn aggregate_sync_sse_response_for_client(
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty());
|
||||
match api_format.map(crate::ai_pipeline::normalize_legacy_openai_format_alias) {
|
||||
match api_format.map(crate::ai_pipeline::normalize_api_format_alias) {
|
||||
Some(value) if value.eq_ignore_ascii_case("openai:chat") => {
|
||||
aggregate_openai_chat_stream_sync_response(body)
|
||||
}
|
||||
@@ -473,16 +473,10 @@ fn aggregate_sync_sse_response_for_client(
|
||||
{
|
||||
aggregate_openai_responses_stream_sync_response(body)
|
||||
}
|
||||
Some(value)
|
||||
if value.eq_ignore_ascii_case("claude:chat")
|
||||
|| value.eq_ignore_ascii_case("claude:cli") =>
|
||||
{
|
||||
Some(value) if value.eq_ignore_ascii_case("claude:messages") => {
|
||||
aggregate_claude_stream_sync_response(body)
|
||||
}
|
||||
Some(value)
|
||||
if value.eq_ignore_ascii_case("gemini:chat")
|
||||
|| value.eq_ignore_ascii_case("gemini:cli") =>
|
||||
{
|
||||
Some(value) if value.eq_ignore_ascii_case("gemini:generate_content") => {
|
||||
aggregate_gemini_stream_sync_response(body)
|
||||
}
|
||||
_ if public_path == "/v1/chat/completions" => {
|
||||
@@ -534,25 +528,25 @@ fn resolve_affinity_forward_client_api_format(
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty());
|
||||
match api_format.map(crate::ai_pipeline::normalize_legacy_openai_format_alias) {
|
||||
match api_format.map(crate::ai_pipeline::normalize_api_format_alias) {
|
||||
Some(value) if value.eq_ignore_ascii_case("openai:chat") => Some("openai:chat"),
|
||||
Some(value) if value.eq_ignore_ascii_case("openai:responses") => Some("openai:responses"),
|
||||
Some(value) if value.eq_ignore_ascii_case("openai:responses:compact") => {
|
||||
Some("openai:responses:compact")
|
||||
}
|
||||
Some(value) if value.eq_ignore_ascii_case("claude:chat") => Some("claude:chat"),
|
||||
Some(value) if value.eq_ignore_ascii_case("claude:cli") => Some("claude:cli"),
|
||||
Some(value) if value.eq_ignore_ascii_case("gemini:chat") => Some("gemini:chat"),
|
||||
Some(value) if value.eq_ignore_ascii_case("gemini:cli") => Some("gemini:cli"),
|
||||
Some(value) if value.eq_ignore_ascii_case("claude:messages") => Some("claude:messages"),
|
||||
Some(value) if value.eq_ignore_ascii_case("gemini:generate_content") => {
|
||||
Some("gemini:generate_content")
|
||||
}
|
||||
_ if public_path == "/v1/chat/completions" => Some("openai:chat"),
|
||||
_ if public_path == "/v1/responses" => Some("openai:responses"),
|
||||
_ if public_path == "/v1/responses/compact" => Some("openai:responses:compact"),
|
||||
_ if public_path == "/v1/messages" => Some("claude:chat"),
|
||||
_ if public_path == "/v1/messages" => Some("claude:messages"),
|
||||
_ if decision.route_family.as_deref() == Some("gemini")
|
||||
&& (public_path.contains(":generateContent")
|
||||
|| public_path.contains(":streamGenerateContent")) =>
|
||||
{
|
||||
Some("gemini:chat")
|
||||
Some("gemini:generate_content")
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
@@ -1760,8 +1754,8 @@ mod tests {
|
||||
"/v1/messages",
|
||||
Some("ai_public".to_string()),
|
||||
Some("claude".to_string()),
|
||||
Some("chat".to_string()),
|
||||
Some("claude:chat".to_string()),
|
||||
Some("messages".to_string()),
|
||||
Some("claude:messages".to_string()),
|
||||
);
|
||||
let diagnostic = LocalExecutionRuntimeMissDiagnostic {
|
||||
reason: "missing_auth_context".to_string(),
|
||||
|
||||
@@ -594,10 +594,14 @@ pub(crate) fn api_format_display_name(api_format: &str) -> String {
|
||||
};
|
||||
let kind_label = match kind {
|
||||
"chat" => "Chat",
|
||||
"cli" => "CLI",
|
||||
"messages" => "Messages",
|
||||
"generate_content" => "Generate Content",
|
||||
"responses" => "Responses",
|
||||
"responses:compact" => "Responses Compact",
|
||||
"compact" => "Compact",
|
||||
"video" => "Video",
|
||||
"image" => "Image",
|
||||
"files" => "Files",
|
||||
other => other,
|
||||
};
|
||||
format!("{family_label} {kind_label}")
|
||||
|
||||
@@ -9,7 +9,7 @@ use serde_json::json;
|
||||
|
||||
pub(crate) fn build_models_auth_error_response(api_format: &str) -> Response<Body> {
|
||||
match api_format {
|
||||
"claude:chat" => (
|
||||
"claude:messages" => (
|
||||
http::StatusCode::UNAUTHORIZED,
|
||||
Json(json!({
|
||||
"type": "error",
|
||||
@@ -20,7 +20,7 @@ pub(crate) fn build_models_auth_error_response(api_format: &str) -> Response<Bod
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
"gemini:chat" => (
|
||||
"gemini:generate_content" => (
|
||||
http::StatusCode::UNAUTHORIZED,
|
||||
Json(json!({
|
||||
"error": {
|
||||
@@ -48,7 +48,7 @@ pub(crate) fn build_models_auth_error_response(api_format: &str) -> Response<Bod
|
||||
|
||||
pub(super) fn build_models_not_found_response(model_id: &str, api_format: &str) -> Response<Body> {
|
||||
match api_format {
|
||||
"claude:chat" => (
|
||||
"claude:messages" => (
|
||||
http::StatusCode::NOT_FOUND,
|
||||
Json(json!({
|
||||
"type": "error",
|
||||
@@ -59,7 +59,7 @@ pub(super) fn build_models_not_found_response(model_id: &str, api_format: &str)
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
"gemini:chat" => (
|
||||
"gemini:generate_content" => (
|
||||
http::StatusCode::NOT_FOUND,
|
||||
Json(json!({
|
||||
"error": {
|
||||
@@ -87,14 +87,14 @@ pub(super) fn build_models_not_found_response(model_id: &str, api_format: &str)
|
||||
|
||||
pub(super) fn build_empty_models_list_response(api_format: &str) -> Response<Body> {
|
||||
match api_format {
|
||||
"claude:chat" => Json(json!({
|
||||
"claude:messages" => Json(json!({
|
||||
"data": [],
|
||||
"has_more": false,
|
||||
"first_id": serde_json::Value::Null,
|
||||
"last_id": serde_json::Value::Null,
|
||||
}))
|
||||
.into_response(),
|
||||
"gemini:chat" => Json(json!({ "models": [] })).into_response(),
|
||||
"gemini:generate_content" => Json(json!({ "models": [] })).into_response(),
|
||||
_ => Json(json!({ "object": "list", "data": [] })).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ pub(super) async fn maybe_build_local_models_route_response(
|
||||
return Some(build_empty_models_list_response(api_format));
|
||||
}
|
||||
let response = match api_format {
|
||||
"claude:chat" => {
|
||||
"claude:messages" => {
|
||||
let before_id = query_param_value(
|
||||
request_context.request_query_string.as_deref(),
|
||||
"before_id",
|
||||
@@ -136,7 +136,7 @@ pub(super) async fn maybe_build_local_models_route_response(
|
||||
limit,
|
||||
)
|
||||
}
|
||||
"gemini:chat" => {
|
||||
"gemini:generate_content" => {
|
||||
let page_size = query_param_value(
|
||||
request_context.request_query_string.as_deref(),
|
||||
"pageSize",
|
||||
@@ -167,8 +167,8 @@ pub(super) async fn maybe_build_local_models_route_response(
|
||||
return Some(build_models_not_found_response(&model_id, api_format));
|
||||
};
|
||||
let response = match api_format {
|
||||
"claude:chat" => build_claude_model_detail_response(row),
|
||||
"gemini:chat" => build_gemini_model_detail_response(row),
|
||||
"claude:messages" => build_claude_model_detail_response(row),
|
||||
"gemini:generate_content" => build_gemini_model_detail_response(row),
|
||||
_ => build_openai_model_detail_response(row),
|
||||
};
|
||||
Some(response)
|
||||
|
||||
@@ -12,13 +12,13 @@ pub(crate) fn models_api_format(request_context: &GatewayPublicRequestContext) -
|
||||
.and_then(|decision| decision.auth_endpoint_signature.as_deref())
|
||||
.map(str::trim)
|
||||
.filter(|signature| !signature.is_empty())?;
|
||||
match crate::ai_pipeline::normalize_legacy_openai_format_alias(signature).as_str() {
|
||||
match crate::ai_pipeline::normalize_api_format_alias(signature).as_str() {
|
||||
"openai:chat" => Some("openai:chat"),
|
||||
"openai:responses" => Some("openai:responses"),
|
||||
"openai:responses:compact" => Some("openai:responses:compact"),
|
||||
"openai:image" => Some("openai:image"),
|
||||
"claude:chat" => Some("claude:chat"),
|
||||
"gemini:chat" => Some("gemini:chat"),
|
||||
"claude:messages" => Some("claude:messages"),
|
||||
"gemini:generate_content" => Some("gemini:generate_content"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -27,22 +27,18 @@ const MODELS_CROSS_FORMAT_QUERY_API_FORMATS: &[&str] = &[
|
||||
"openai:chat",
|
||||
"openai:responses",
|
||||
"openai:responses:compact",
|
||||
"openai:cli",
|
||||
"openai:compact",
|
||||
"openai:image",
|
||||
"claude:chat",
|
||||
"claude:cli",
|
||||
"gemini:chat",
|
||||
"gemini:cli",
|
||||
"claude:messages",
|
||||
"gemini:generate_content",
|
||||
];
|
||||
|
||||
pub(super) fn models_query_api_formats(api_format: &str) -> &'static [&'static str] {
|
||||
match crate::ai_pipeline::normalize_legacy_openai_format_alias(api_format).as_str() {
|
||||
match crate::ai_pipeline::normalize_api_format_alias(api_format).as_str() {
|
||||
"openai:chat"
|
||||
| "openai:responses"
|
||||
| "openai:responses:compact"
|
||||
| "claude:chat"
|
||||
| "gemini:chat" => MODELS_CROSS_FORMAT_QUERY_API_FORMATS,
|
||||
| "claude:messages"
|
||||
| "gemini:generate_content" => MODELS_CROSS_FORMAT_QUERY_API_FORMATS,
|
||||
"openai:image" => &["openai:image"],
|
||||
_ => &[],
|
||||
}
|
||||
|
||||
@@ -90,17 +90,17 @@ pub(super) async fn maybe_build_local_test_connection_route_response(
|
||||
} else {
|
||||
let endpoint = active_endpoints.into_iter().next()?;
|
||||
let format_value = if endpoint.api_format.trim().is_empty() {
|
||||
"claude:chat".to_string()
|
||||
"claude:messages".to_string()
|
||||
} else {
|
||||
endpoint.api_format.clone()
|
||||
crate::ai_pipeline::normalize_api_format_alias(&endpoint.api_format)
|
||||
};
|
||||
(endpoint, format_value)
|
||||
};
|
||||
|
||||
let format_value = format_value.trim().to_string();
|
||||
let format_value = crate::ai_pipeline::normalize_api_format_alias(&format_value);
|
||||
if !matches!(
|
||||
format_value.as_str(),
|
||||
"openai:chat" | "claude:chat" | "gemini:chat"
|
||||
"openai:chat" | "claude:messages" | "gemini:generate_content"
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
@@ -169,12 +169,12 @@ pub(super) async fn maybe_build_local_test_connection_route_response(
|
||||
}
|
||||
|
||||
let mut provider_request_body = match format_value.as_str() {
|
||||
"openai:chat" | "claude:chat" => json!({
|
||||
"openai:chat" | "claude:messages" => json!({
|
||||
"model": model,
|
||||
"messages": [{"role": "user", "content": "Health check"}],
|
||||
"max_tokens": 5,
|
||||
}),
|
||||
"gemini:chat" => json!({
|
||||
"gemini:generate_content" => json!({
|
||||
"contents": [{
|
||||
"role": "user",
|
||||
"parts": [{"text": "Health check"}],
|
||||
@@ -194,7 +194,7 @@ pub(super) async fn maybe_build_local_test_connection_route_response(
|
||||
}
|
||||
|
||||
let oauth_auth = match format_value.as_str() {
|
||||
"openai:chat" | "claude:chat" => {
|
||||
"openai:chat" | "claude:messages" => {
|
||||
match state.resolve_local_oauth_request_auth(&transport).await {
|
||||
Ok(Some(crate::provider_transport::LocalResolvedOAuthRequestAuth::Header {
|
||||
name,
|
||||
@@ -211,9 +211,13 @@ pub(super) async fn maybe_build_local_test_connection_route_response(
|
||||
crate::provider_transport::auth::resolve_local_openai_bearer_auth(&transport)
|
||||
.or(oauth_auth.clone())
|
||||
}
|
||||
"claude:chat" => crate::provider_transport::auth::resolve_local_standard_auth(&transport)
|
||||
.or(oauth_auth.clone()),
|
||||
"gemini:chat" => crate::provider_transport::auth::resolve_local_gemini_auth(&transport),
|
||||
"claude:messages" => {
|
||||
crate::provider_transport::auth::resolve_local_standard_auth(&transport)
|
||||
.or(oauth_auth.clone())
|
||||
}
|
||||
"gemini:generate_content" => {
|
||||
crate::provider_transport::auth::resolve_local_gemini_auth(&transport)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let Some((auth_header, auth_value)) = auth else {
|
||||
|
||||
@@ -213,13 +213,12 @@ fn users_me_usage_api_format_defaults_to_non_stream(item: &StoredRequestUsageAud
|
||||
return false;
|
||||
};
|
||||
matches!(
|
||||
crate::ai_pipeline::normalize_legacy_openai_format_alias(value).as_str(),
|
||||
crate::ai_pipeline::normalize_api_format_alias(value).as_str(),
|
||||
"openai:chat"
|
||||
| "openai:responses"
|
||||
| "openai:responses:compact"
|
||||
| "openai:image"
|
||||
| "claude:chat"
|
||||
| "claude:cli"
|
||||
| "claude:messages"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1347,9 +1346,9 @@ mod tests {
|
||||
let item = StoredRequestUsageAudit {
|
||||
provider_name: "Claude".to_string(),
|
||||
model: "claude-sonnet-4-5".to_string(),
|
||||
api_format: Some("claude:chat".to_string()),
|
||||
api_format: Some("claude:messages".to_string()),
|
||||
api_family: Some("claude".to_string()),
|
||||
endpoint_api_format: Some("claude:chat".to_string()),
|
||||
endpoint_api_format: Some("claude:messages".to_string()),
|
||||
provider_api_family: Some("claude".to_string()),
|
||||
input_tokens: 4941,
|
||||
output_tokens: 973,
|
||||
|
||||
@@ -33,7 +33,7 @@ pub(crate) fn provider_catalog_key_supports_format(
|
||||
}
|
||||
formats
|
||||
.iter()
|
||||
.any(|candidate| candidate.trim().eq_ignore_ascii_case(api_format))
|
||||
.any(|candidate| crate::ai_pipeline::api_format_alias_matches(candidate, api_format))
|
||||
}
|
||||
|
||||
pub(crate) fn decrypt_catalog_secret_with_fallbacks(
|
||||
|
||||
Reference in New Issue
Block a user