feat: 候选排序引入 API 格式偏好, 追踪页面展示完整格式转换信息

- 候选排序优先同 kind (chat/cli) 再同 family, 替代原有固定顺序
- 不再隐藏 format_conversion_disabled 候选, 保留完整追踪链路
- DecisionTrace 新增 provider/endpoint/key 格式转换相关字段
- 前端追踪面板新增 Key 支持端点和转换策略展示
This commit is contained in:
fawney19
2026-04-16 13:00:44 +08:00
parent d805a28c9a
commit 9a41b2c0dc
14 changed files with 433 additions and 92 deletions

View File

@@ -291,13 +291,18 @@ pub fn build_admin_monitoring_trace_request_candidate_payload(
"provider_website": item.provider_website,
"provider_priority": item.provider_priority,
"provider_keep_priority_on_conversion": item.provider_keep_priority_on_conversion,
"provider_enable_format_conversion": item.provider_enable_format_conversion,
"endpoint_id": candidate.endpoint_id,
"endpoint_name": item.endpoint_api_format,
"endpoint_api_family": item.endpoint_api_family,
"endpoint_kind": item.endpoint_kind,
"endpoint_format_acceptance_config": item.endpoint_format_acceptance_config,
"key_id": candidate.key_id,
"key_name": item.provider_key_name,
"key_account_label": serde_json::Value::Null,
"key_preview": serde_json::Value::Null,
"key_auth_type": item.provider_key_auth_type,
"key_api_formats": item.provider_key_api_formats,
"key_internal_priority": item.provider_key_internal_priority,
"key_global_priority_by_format": item.provider_key_global_priority_by_format,
"key_oauth_plan_type": serde_json::Value::Null,

View File

@@ -69,12 +69,13 @@ pub use crate::conversion::response::{
};
pub use crate::conversion::{
build_core_error_body_for_client_format, is_core_error_finalize_kind,
request_candidate_api_formats, request_conversion_direct_auth,
request_conversion_enabled_for_transport, request_conversion_kind,
request_conversion_requires_enable_flag, request_conversion_transport_supported,
request_conversion_transport_unsupported_reason, request_pair_allowed_for_transport,
sync_chat_response_conversion_kind, sync_cli_response_conversion_kind, LocalCoreSyncErrorKind,
RequestConversionKind, SyncChatResponseConversionKind, SyncCliResponseConversionKind,
request_candidate_api_format_preference, request_candidate_api_formats,
request_conversion_direct_auth, request_conversion_enabled_for_transport,
request_conversion_kind, request_conversion_requires_enable_flag,
request_conversion_transport_supported, request_conversion_transport_unsupported_reason,
request_pair_allowed_for_transport, sync_chat_response_conversion_kind,
sync_cli_response_conversion_kind, LocalCoreSyncErrorKind, RequestConversionKind,
SyncChatResponseConversionKind, SyncCliResponseConversionKind,
};
pub use crate::finalize::common::{
build_generated_tool_call_id, build_local_success_background_report,

View File

@@ -9,10 +9,11 @@ pub use error::{
is_core_error_finalize_kind, LocalCoreSyncErrorKind,
};
pub use registry::{
request_candidate_api_formats, request_conversion_direct_auth,
request_conversion_enabled_for_transport, request_conversion_kind,
request_conversion_requires_enable_flag, request_conversion_transport_supported,
request_conversion_transport_unsupported_reason, request_pair_allowed_for_transport,
sync_chat_response_conversion_kind, sync_cli_response_conversion_kind, RequestConversionKind,
SyncChatResponseConversionKind, SyncCliResponseConversionKind,
request_candidate_api_format_preference, request_candidate_api_formats,
request_conversion_direct_auth, request_conversion_enabled_for_transport,
request_conversion_kind, request_conversion_requires_enable_flag,
request_conversion_transport_supported, request_conversion_transport_unsupported_reason,
request_pair_allowed_for_transport, sync_chat_response_conversion_kind,
sync_cli_response_conversion_kind, RequestConversionKind, SyncChatResponseConversionKind,
SyncCliResponseConversionKind,
};

View File

@@ -40,18 +40,57 @@ const NON_COMPACT_STANDARD_CANDIDATE_API_FORMATS: &[&str] = &[
"gemini:chat",
"gemini:cli",
];
const STANDARD_API_FAMILY_ORDER: &[&str] = &["openai", "claude", "gemini"];
pub fn request_candidate_api_format_preference(
client_api_format: &str,
provider_api_format: &str,
) -> Option<(u8, u8)> {
let client_api_format = client_api_format.trim().to_ascii_lowercase();
let provider_api_format = provider_api_format.trim().to_ascii_lowercase();
if client_api_format == "openai:compact" {
return (provider_api_format == "openai:compact").then_some((0, 0));
}
let (client_family, client_kind) =
parse_non_compact_standard_api_format(client_api_format.as_str())?;
let (provider_family, provider_kind) =
parse_non_compact_standard_api_format(provider_api_format.as_str())?;
let preference_bucket = if client_family == provider_family && client_kind == provider_kind {
0
} else if client_kind == provider_kind {
1
} else if client_family == provider_family {
2
} else {
3
};
Some((
preference_bucket,
standard_api_family_priority(provider_family),
))
}
pub fn request_candidate_api_formats(
client_api_format: &str,
_require_streaming: bool,
) -> Vec<&'static str> {
let client_api_format = client_api_format.trim().to_ascii_lowercase();
match client_api_format.as_str() {
"openai:chat" | "openai:cli" | "claude:chat" | "claude:cli" | "gemini:chat"
| "gemini:cli" => NON_COMPACT_STANDARD_CANDIDATE_API_FORMATS.to_vec(),
"openai:compact" => vec!["openai:compact"],
_ => Vec::new(),
if client_api_format == "openai:compact" {
return vec!["openai:compact"];
}
if parse_non_compact_standard_api_format(client_api_format.as_str()).is_none() {
return Vec::new();
}
let mut candidate_api_formats = NON_COMPACT_STANDARD_CANDIDATE_API_FORMATS.to_vec();
candidate_api_formats.sort_by_key(|provider_api_format| {
request_candidate_api_format_preference(client_api_format.as_str(), provider_api_format)
.unwrap_or((u8::MAX, u8::MAX))
});
candidate_api_formats
}
pub fn request_conversion_kind(
@@ -259,6 +298,21 @@ fn is_standard_api_format(api_format: &str) -> bool {
)
}
fn parse_non_compact_standard_api_format(api_format: &str) -> Option<(&str, &str)> {
let (family, kind) = api_format.split_once(':')?;
if !STANDARD_API_FAMILY_ORDER.contains(&family) || !matches!(kind, "chat" | "cli") {
return None;
}
Some((family, kind))
}
fn standard_api_family_priority(family: &str) -> u8 {
STANDARD_API_FAMILY_ORDER
.iter()
.position(|candidate| *candidate == family)
.unwrap_or(STANDARD_API_FAMILY_ORDER.len()) as u8
}
fn api_data_format_id(api_format: &str) -> Option<&'static str> {
match api_format {
"claude:chat" | "claude:cli" => Some("claude"),
@@ -315,12 +369,12 @@ fn json_format_list_contains(value: &serde_json::Value, api_format: &str) -> boo
#[cfg(test)]
mod tests {
use super::{
request_candidate_api_formats, request_conversion_direct_auth,
request_conversion_enabled_for_transport, request_conversion_kind,
request_conversion_requires_enable_flag, request_conversion_transport_supported,
request_pair_allowed_for_transport, sync_chat_response_conversion_kind,
sync_cli_response_conversion_kind, RequestConversionKind, SyncChatResponseConversionKind,
SyncCliResponseConversionKind,
request_candidate_api_format_preference, request_candidate_api_formats,
request_conversion_direct_auth, request_conversion_enabled_for_transport,
request_conversion_kind, request_conversion_requires_enable_flag,
request_conversion_transport_supported, request_pair_allowed_for_transport,
sync_chat_response_conversion_kind, sync_cli_response_conversion_kind,
RequestConversionKind, SyncChatResponseConversionKind, SyncCliResponseConversionKind,
};
use aether_provider_transport::snapshot::{
GatewayProviderTransportEndpoint, GatewayProviderTransportKey,
@@ -406,33 +460,33 @@ mod tests {
request_candidate_api_formats("openai:chat", false),
vec![
"openai:chat",
"openai:cli",
"claude:chat",
"claude:cli",
"gemini:chat",
"openai:cli",
"claude:cli",
"gemini:cli",
]
);
assert_eq!(
request_candidate_api_formats("openai:cli", false),
vec![
"openai:chat",
"openai:cli",
"claude:chat",
"claude:cli",
"gemini:chat",
"gemini:cli",
"openai:chat",
"claude:chat",
"gemini:chat",
]
);
assert_eq!(
request_candidate_api_formats("claude:cli", false),
vec![
"openai:chat",
"openai:cli",
"claude:chat",
"claude:cli",
"gemini:chat",
"openai:cli",
"gemini:cli",
"claude:chat",
"openai:chat",
"gemini:chat",
]
);
assert_eq!(
@@ -441,6 +495,22 @@ mod tests {
);
}
#[test]
fn request_candidate_registry_prefers_same_kind_before_same_family_fallbacks() {
assert_eq!(
request_candidate_api_format_preference("claude:cli", "openai:cli"),
Some((1, 0))
);
assert_eq!(
request_candidate_api_format_preference("claude:cli", "claude:chat"),
Some((2, 1))
);
assert_eq!(
request_candidate_api_format_preference("claude:cli", "openai:chat"),
Some((3, 0))
);
}
#[test]
fn request_conversion_enable_flag_only_applies_to_real_data_format_conversions() {
assert!(!request_conversion_requires_enable_flag(

View File

@@ -305,11 +305,14 @@ pub struct DecisionTraceCandidate {
pub provider_type: Option<String>,
pub provider_priority: Option<i32>,
pub provider_keep_priority_on_conversion: Option<bool>,
pub provider_enable_format_conversion: Option<bool>,
pub endpoint_api_format: Option<String>,
pub endpoint_api_family: Option<String>,
pub endpoint_kind: Option<String>,
pub endpoint_format_acceptance_config: Option<serde_json::Value>,
pub provider_key_name: Option<String>,
pub provider_key_auth_type: Option<String>,
pub provider_key_api_formats: Option<serde_json::Value>,
pub provider_key_internal_priority: Option<i32>,
pub provider_key_global_priority_by_format: Option<serde_json::Value>,
pub provider_key_capabilities: Option<serde_json::Value>,
@@ -384,13 +387,17 @@ fn enrich_decision_trace_candidate(
provider_type: provider.map(|item| item.provider_type.clone()),
provider_priority: provider.map(|item| item.provider_priority),
provider_keep_priority_on_conversion: provider.map(|item| item.keep_priority_on_conversion),
provider_enable_format_conversion: provider.map(|item| item.enable_format_conversion),
endpoint_api_format: endpoint.map(|item| item.api_format.clone()),
endpoint_api_family: endpoint.and_then(|item| item.api_family.clone()),
endpoint_kind: endpoint.and_then(|item| item.endpoint_kind.clone()),
endpoint_format_acceptance_config: endpoint
.and_then(|item| item.format_acceptance_config.clone()),
provider_key_name: provider_key
.map(|item| item.name.clone())
.or_else(|| candidate.api_key_name.clone()),
provider_key_auth_type: provider_key.map(|item| item.auth_type.clone()),
provider_key_api_formats: provider_key.and_then(|item| item.api_formats.clone()),
provider_key_internal_priority: provider_key.map(|item| item.internal_priority),
provider_key_global_priority_by_format: provider_key
.and_then(|item| item.global_priority_by_format.clone()),

View File

@@ -259,11 +259,14 @@ mod tests {
provider_type: Some("custom".to_string()),
provider_priority: Some(0),
provider_keep_priority_on_conversion: Some(false),
provider_enable_format_conversion: Some(false),
endpoint_api_format: Some("openai:chat".to_string()),
endpoint_api_family: Some("openai".to_string()),
endpoint_kind: Some("chat".to_string()),
endpoint_format_acceptance_config: None,
provider_key_name: Some("prod".to_string()),
provider_key_auth_type: Some("api_key".to_string()),
provider_key_api_formats: None,
provider_key_internal_priority: Some(10),
provider_key_global_priority_by_format: None,
provider_key_capabilities: None,