mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
Merge origin/main into codex/gemini-embedding-batch
# Conflicts: # apps/aether-gateway/src/ai_serving/api.rs # apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/request.rs # apps/aether-gateway/src/ai_serving/planner/standard/family/request.rs # apps/aether-gateway/src/ai_serving/transport.rs # apps/aether-gateway/src/handlers/admin/provider/query/models/model_test/summary.rs # apps/aether-gateway/src/handlers/admin/provider/query/models/model_test/tests.rs # crates/aether-data/src/repository/candidate_selection/postgres.rs # crates/aether-model-fetch/src/strategy.rs
This commit is contained in:
@@ -13,6 +13,7 @@ aether-contracts.workspace = true
|
||||
aether-data.workspace = true
|
||||
aether-data-contracts.workspace = true
|
||||
aether-provider-pool.workspace = true
|
||||
aether-provider-transport.workspace = true
|
||||
axum.workspace = true
|
||||
base64.workspace = true
|
||||
chrono.workspace = true
|
||||
|
||||
@@ -727,6 +727,7 @@ pub fn build_admin_monitoring_system_status_payload_response(
|
||||
active_streams: usize,
|
||||
path_prefixes: &[&str],
|
||||
recent_errors: usize,
|
||||
usage_counter: Value,
|
||||
) -> Response<Body> {
|
||||
Json(json!({
|
||||
"timestamp": timestamp.to_rfc3339(),
|
||||
@@ -757,6 +758,7 @@ pub fn build_admin_monitoring_system_status_payload_response(
|
||||
"path_prefixes": path_prefixes,
|
||||
},
|
||||
"recent_errors": recent_errors,
|
||||
"usage_counter": usage_counter,
|
||||
}))
|
||||
.into_response()
|
||||
}
|
||||
|
||||
@@ -667,7 +667,9 @@ pub fn admin_stats_performance_percentiles_empty_response() -> Response<Body> {
|
||||
Json(json!([])).into_response()
|
||||
}
|
||||
|
||||
pub fn admin_stats_provider_performance_empty_response() -> Response<Body> {
|
||||
pub fn admin_stats_provider_performance_empty_response(
|
||||
usage_counter: serde_json::Value,
|
||||
) -> Response<Body> {
|
||||
Json(json!({
|
||||
"summary": {
|
||||
"request_count": 0,
|
||||
@@ -678,6 +680,7 @@ pub fn admin_stats_provider_performance_empty_response() -> Response<Body> {
|
||||
},
|
||||
"providers": [],
|
||||
"timeline": [],
|
||||
"usage_counter": usage_counter,
|
||||
}))
|
||||
.into_response()
|
||||
}
|
||||
@@ -1093,6 +1096,7 @@ pub fn build_admin_stats_performance_percentiles_response_from_summaries(
|
||||
|
||||
pub fn build_admin_stats_provider_performance_response(
|
||||
performance: &StoredUsageProviderPerformance,
|
||||
usage_counter: serde_json::Value,
|
||||
) -> Response<Body> {
|
||||
let summary = &performance.summary;
|
||||
let providers = performance
|
||||
@@ -1158,6 +1162,7 @@ pub fn build_admin_stats_provider_performance_response(
|
||||
},
|
||||
"providers": providers,
|
||||
"timeline": timeline,
|
||||
"usage_counter": usage_counter,
|
||||
}))
|
||||
.into_response()
|
||||
}
|
||||
|
||||
@@ -469,6 +469,67 @@ fn admin_usage_simplify_all_candidates_skipped_client_error_message(
|
||||
Some(format!("没有可用提供商支持本次{request_mode}请求"))
|
||||
}
|
||||
|
||||
fn admin_usage_local_runtime_miss_reason_label(reason: &str) -> &'static str {
|
||||
match reason {
|
||||
"all_candidates_skipped" => "所有候选均被跳过",
|
||||
"candidate_list_empty" => "没有可调度候选",
|
||||
"local_runtime_unavailable" => "本地执行运行时不可用",
|
||||
"provider_transport_unavailable" => "提供商传输不可用",
|
||||
_ => "本地调度未命中",
|
||||
}
|
||||
}
|
||||
|
||||
fn admin_usage_extract_local_runtime_miss_reason_summary(message: &str) -> Option<String> {
|
||||
let without_reason_code = message
|
||||
.split_once("(原因代码:")
|
||||
.map(|(prefix, _)| prefix)
|
||||
.unwrap_or(message)
|
||||
.trim();
|
||||
let summary = without_reason_code
|
||||
.rsplit_once(':')
|
||||
.or_else(|| without_reason_code.rsplit_once(':'))
|
||||
.map(|(_, suffix)| suffix.trim())?;
|
||||
(!summary.is_empty()).then(|| summary.to_string())
|
||||
}
|
||||
|
||||
fn admin_usage_scheduling_failure_json(
|
||||
item: &StoredRequestUsageAudit,
|
||||
client_error: &Value,
|
||||
) -> Value {
|
||||
if item.routing_execution_path() != Some("local_execution_runtime_miss") {
|
||||
return Value::Null;
|
||||
}
|
||||
|
||||
let reason = item
|
||||
.routing_local_execution_runtime_miss_reason()
|
||||
.unwrap_or("local_execution_runtime_miss");
|
||||
let message = admin_usage_error_domain_message(client_error)
|
||||
.or_else(|| admin_usage_client_error_fallback_message(item))
|
||||
.or_else(|| item.error_message.as_deref().map(str::to_string));
|
||||
let raw_message = item
|
||||
.error_message
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty());
|
||||
let reason_summary =
|
||||
raw_message.and_then(admin_usage_extract_local_runtime_miss_reason_summary);
|
||||
|
||||
json!({
|
||||
"source": "local_execution_runtime_miss",
|
||||
"reason": reason,
|
||||
"reason_label": admin_usage_local_runtime_miss_reason_label(reason),
|
||||
"title": format!("本地调度失败:{}", admin_usage_local_runtime_miss_reason_label(reason)),
|
||||
"message": message,
|
||||
"reason_summary": reason_summary,
|
||||
"status_code": item.status_code,
|
||||
"no_upstream_attempt": item.candidate_id.is_none()
|
||||
&& item.provider_api_key_id.is_none()
|
||||
&& item.provider_request_headers.is_none()
|
||||
&& item.provider_request_body.is_none()
|
||||
&& item.provider_request_body_ref.is_none(),
|
||||
})
|
||||
}
|
||||
|
||||
fn admin_usage_extract_local_execution_request_mode(message: &str) -> Option<&str> {
|
||||
let rest = message.get(message.find("本次")? + "本次".len()..)?;
|
||||
let mode = rest.get(..rest.find("请求")?)?.trim();
|
||||
@@ -998,6 +1059,71 @@ fn admin_usage_upstream_is_stream(item: &StoredRequestUsageAudit) -> bool {
|
||||
.unwrap_or(item.is_stream)
|
||||
}
|
||||
|
||||
fn admin_usage_metadata_string<'a>(
|
||||
item: &'a StoredRequestUsageAudit,
|
||||
key: &str,
|
||||
) -> Option<&'a str> {
|
||||
item.request_metadata
|
||||
.as_ref()
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|metadata| metadata.get(key))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
fn infer_client_family_from_user_agent(user_agent: &str) -> Option<&'static str> {
|
||||
let normalized = user_agent.trim().to_ascii_lowercase();
|
||||
if normalized.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if normalized.starts_with("codex_vscode") {
|
||||
return Some("codex_vscode");
|
||||
}
|
||||
if normalized.starts_with("codex") {
|
||||
return Some("codex");
|
||||
}
|
||||
if normalized.contains("claude-code") || normalized.contains("claude_code") {
|
||||
return Some("claude_code");
|
||||
}
|
||||
if normalized.contains("opencode") {
|
||||
return Some("opencode");
|
||||
}
|
||||
if normalized.contains("geminicli") || normalized.contains("gemini-cli") {
|
||||
return Some("gemini_cli");
|
||||
}
|
||||
if normalized.starts_with("openai/js") {
|
||||
return Some("openai_js_sdk");
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn admin_usage_client_family(item: &StoredRequestUsageAudit) -> Option<&str> {
|
||||
item.client_family
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.or_else(|| {
|
||||
item.request_metadata
|
||||
.as_ref()
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|metadata| {
|
||||
metadata
|
||||
.get("client_session_affinity")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|affinity| affinity.get("client_family"))
|
||||
.and_then(Value::as_str)
|
||||
.or_else(|| metadata.get("client_family").and_then(Value::as_str))
|
||||
})
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
})
|
||||
.or_else(|| {
|
||||
admin_usage_metadata_string(item, "user_agent")
|
||||
.and_then(infer_client_family_from_user_agent)
|
||||
})
|
||||
}
|
||||
|
||||
fn admin_usage_active_request_json(
|
||||
item: &StoredRequestUsageAudit,
|
||||
api_key_name: Option<String>,
|
||||
@@ -1030,6 +1156,11 @@ fn admin_usage_active_request_json(
|
||||
"upstream_is_stream": upstream_is_stream,
|
||||
"client_requested_stream": client_is_stream,
|
||||
"client_is_stream": client_is_stream,
|
||||
"client_family": admin_usage_client_family(item),
|
||||
"client_ip": admin_usage_metadata_string(item, "client_ip"),
|
||||
"user_agent": admin_usage_metadata_string(item, "user_agent"),
|
||||
"request_path": admin_usage_metadata_string(item, "request_path"),
|
||||
"request_path_and_query": admin_usage_metadata_string(item, "request_path_and_query"),
|
||||
"has_fallback": admin_usage_has_fallback(item),
|
||||
});
|
||||
if let Some(api_format) = item.api_format.as_ref() {
|
||||
@@ -1131,6 +1262,27 @@ pub fn admin_usage_record_json(
|
||||
json!(client_is_stream),
|
||||
);
|
||||
object.insert("client_is_stream".to_string(), json!(client_is_stream));
|
||||
maybe_insert_string_field(object, "client_family", admin_usage_client_family(item));
|
||||
maybe_insert_string_field(
|
||||
object,
|
||||
"client_ip",
|
||||
admin_usage_metadata_string(item, "client_ip"),
|
||||
);
|
||||
maybe_insert_string_field(
|
||||
object,
|
||||
"user_agent",
|
||||
admin_usage_metadata_string(item, "user_agent"),
|
||||
);
|
||||
maybe_insert_string_field(
|
||||
object,
|
||||
"request_path",
|
||||
admin_usage_metadata_string(item, "request_path"),
|
||||
);
|
||||
maybe_insert_string_field(
|
||||
object,
|
||||
"request_path_and_query",
|
||||
admin_usage_metadata_string(item, "request_path_and_query"),
|
||||
);
|
||||
payload
|
||||
}
|
||||
|
||||
@@ -2199,6 +2351,8 @@ pub fn build_admin_usage_detail_payload(
|
||||
payload["upstream_error"] = error_domains["upstream_error"].clone();
|
||||
payload["client_error"] = error_domains["client_error"].clone();
|
||||
payload["failure_summary"] = error_domains["failure_summary"].clone();
|
||||
payload["scheduling_failure"] =
|
||||
admin_usage_scheduling_failure_json(item, &error_domains["client_error"]);
|
||||
payload["error_flow"] = error_flow;
|
||||
payload["has_request_body"] = json!(admin_usage_has_body_value(
|
||||
item,
|
||||
@@ -2380,6 +2534,74 @@ mod tests {
|
||||
assert_eq!(record["client_is_stream"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_usage_record_infers_client_family_from_user_agent() {
|
||||
let item = StoredRequestUsageAudit {
|
||||
request_metadata: Some(json!({
|
||||
"client_ip": "192.168.0.28",
|
||||
"user_agent": "codex_vscode/0.131.0-alpha.9 (Windows 10.0.26200; x86_64)"
|
||||
})),
|
||||
..sample_usage("completed", Some(200), None)
|
||||
};
|
||||
|
||||
let record = admin_usage_record_json(
|
||||
&item,
|
||||
&BTreeMap::new(),
|
||||
&BTreeMap::new(),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
);
|
||||
let active = admin_usage_active_request_json(&item, None, None, None);
|
||||
|
||||
assert_eq!(record["client_family"], "codex_vscode");
|
||||
assert_eq!(record["client_ip"], "192.168.0.28");
|
||||
assert_eq!(active["client_family"], "codex_vscode");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_usage_record_labels_openai_js_user_agent_as_sdk() {
|
||||
let item = StoredRequestUsageAudit {
|
||||
request_metadata: Some(json!({
|
||||
"user_agent": "OpenAI/JS 6.34.0"
|
||||
})),
|
||||
..sample_usage("completed", Some(200), None)
|
||||
};
|
||||
|
||||
let record = admin_usage_record_json(
|
||||
&item,
|
||||
&BTreeMap::new(),
|
||||
&BTreeMap::new(),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(record["client_family"], "openai_js_sdk");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_usage_record_prefers_typed_client_family() {
|
||||
let item = StoredRequestUsageAudit {
|
||||
client_family: Some("codex".to_string()),
|
||||
request_metadata: Some(json!({
|
||||
"user_agent": "OpenAI/JS 6.34.0"
|
||||
})),
|
||||
..sample_usage("completed", Some(200), None)
|
||||
};
|
||||
|
||||
let record = admin_usage_record_json(
|
||||
&item,
|
||||
&BTreeMap::new(),
|
||||
&BTreeMap::new(),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(record["client_family"], "codex");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_requested_stream_uses_stream_generate_content_path_over_stale_metadata_flag() {
|
||||
let item = StoredRequestUsageAudit {
|
||||
@@ -2886,6 +3108,11 @@ mod tests {
|
||||
fn detail_payload_simplifies_local_client_error_when_client_body_is_unloaded() {
|
||||
let message = "没有可用提供商支持模型 gpt-5.4 的流式请求。请检查模型映射、端点启用状态和 API Key 权限(原因代码: candidate_list_empty)";
|
||||
let item = StoredRequestUsageAudit {
|
||||
provider_api_key_id: None,
|
||||
provider_request_headers: None,
|
||||
provider_request_body: None,
|
||||
provider_request_body_ref: None,
|
||||
candidate_id: None,
|
||||
execution_path: Some("local_execution_runtime_miss".to_string()),
|
||||
local_execution_runtime_miss_reason: Some("candidate_list_empty".to_string()),
|
||||
error_category: Some("http_error".to_string()),
|
||||
@@ -2918,12 +3145,31 @@ mod tests {
|
||||
payload["failure_summary"]["message"],
|
||||
"没有可用提供商支持模型 gpt-5.4 的流式请求"
|
||||
);
|
||||
assert_eq!(
|
||||
payload["scheduling_failure"]["title"],
|
||||
"本地调度失败:没有可调度候选"
|
||||
);
|
||||
assert_eq!(
|
||||
payload["scheduling_failure"]["reason"],
|
||||
"candidate_list_empty"
|
||||
);
|
||||
assert!(payload["scheduling_failure"]["reason_summary"].is_null());
|
||||
assert_eq!(
|
||||
payload["scheduling_failure"]["message"],
|
||||
"没有可用提供商支持模型 gpt-5.4 的流式请求"
|
||||
);
|
||||
assert_eq!(payload["scheduling_failure"]["no_upstream_attempt"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detail_payload_simplifies_all_candidates_skipped_when_client_body_is_unloaded() {
|
||||
let message = "找到 1 个支持模型 gpt-5.4 的候选提供商,但本次流式请求全部不可用:provider_quota_blocked 2 次(原因代码: all_candidates_skipped)";
|
||||
let item = StoredRequestUsageAudit {
|
||||
provider_api_key_id: None,
|
||||
provider_request_headers: None,
|
||||
provider_request_body: None,
|
||||
provider_request_body_ref: None,
|
||||
candidate_id: None,
|
||||
execution_path: Some("local_execution_runtime_miss".to_string()),
|
||||
local_execution_runtime_miss_reason: Some("all_candidates_skipped".to_string()),
|
||||
error_category: Some("http_error".to_string()),
|
||||
@@ -2956,6 +3202,23 @@ mod tests {
|
||||
payload["failure_summary"]["message"],
|
||||
"没有可用提供商支持模型 gpt-5.4 的流式请求"
|
||||
);
|
||||
assert_eq!(
|
||||
payload["scheduling_failure"]["title"],
|
||||
"本地调度失败:所有候选均被跳过"
|
||||
);
|
||||
assert_eq!(
|
||||
payload["scheduling_failure"]["reason"],
|
||||
"all_candidates_skipped"
|
||||
);
|
||||
assert_eq!(
|
||||
payload["scheduling_failure"]["reason_summary"],
|
||||
"provider_quota_blocked 2 次"
|
||||
);
|
||||
assert_eq!(
|
||||
payload["scheduling_failure"]["message"],
|
||||
"没有可用提供商支持模型 gpt-5.4 的流式请求"
|
||||
);
|
||||
assert_eq!(payload["scheduling_failure"]["no_upstream_attempt"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
use aether_data_contracts::repository::provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
|
||||
};
|
||||
use aether_provider_transport::provider_types::fixed_provider_key_inherits_api_formats;
|
||||
use chrono::{TimeZone, Utc};
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub fn normalize_endpoint_api_format(api_format: &str) -> String {
|
||||
aether_ai_formats::normalize_api_format_alias(api_format)
|
||||
}
|
||||
|
||||
fn unix_secs_to_rfc3339(unix_secs: u64) -> Option<String> {
|
||||
Utc.timestamp_opt(unix_secs as i64, 0)
|
||||
.single()
|
||||
@@ -41,23 +46,63 @@ pub fn key_api_formats_without_entry(
|
||||
)
|
||||
}
|
||||
|
||||
fn active_endpoint_api_formats(endpoints: &[StoredProviderCatalogEndpoint]) -> Vec<String> {
|
||||
let mut formats = Vec::new();
|
||||
for endpoint in endpoints.iter().filter(|endpoint| endpoint.is_active) {
|
||||
let api_format = normalize_endpoint_api_format(&endpoint.api_format);
|
||||
if !formats.iter().any(|existing| existing == &api_format) {
|
||||
formats.push(api_format);
|
||||
}
|
||||
}
|
||||
formats
|
||||
}
|
||||
|
||||
fn configured_key_api_formats(key: &StoredProviderCatalogKey) -> Vec<String> {
|
||||
let Some(formats) = key
|
||||
.api_formats
|
||||
.as_ref()
|
||||
.and_then(serde_json::Value::as_array)
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut normalized = Vec::new();
|
||||
for api_format in formats.iter().filter_map(serde_json::Value::as_str) {
|
||||
let api_format = normalize_endpoint_api_format(api_format);
|
||||
if !normalized.iter().any(|existing| existing == &api_format) {
|
||||
normalized.push(api_format);
|
||||
}
|
||||
}
|
||||
normalized
|
||||
}
|
||||
|
||||
pub fn endpoint_key_counts_by_format(
|
||||
provider_type: &str,
|
||||
endpoints: &[StoredProviderCatalogEndpoint],
|
||||
keys: &[StoredProviderCatalogKey],
|
||||
) -> (BTreeMap<String, usize>, BTreeMap<String, usize>) {
|
||||
let mut total = BTreeMap::new();
|
||||
let mut active = BTreeMap::new();
|
||||
let inherited_api_formats = active_endpoint_api_formats(endpoints);
|
||||
|
||||
for key in keys {
|
||||
let Some(formats) = key
|
||||
.api_formats
|
||||
.as_ref()
|
||||
.and_then(serde_json::Value::as_array)
|
||||
else {
|
||||
if fixed_provider_key_inherits_api_formats(
|
||||
provider_type,
|
||||
&key.auth_type,
|
||||
key.encrypted_auth_config.as_deref(),
|
||||
) {
|
||||
for api_format in &inherited_api_formats {
|
||||
*total.entry(api_format.clone()).or_insert(0) += 1;
|
||||
if key.is_active {
|
||||
*active.entry(api_format.clone()).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
};
|
||||
for api_format in formats.iter().filter_map(serde_json::Value::as_str) {
|
||||
*total.entry(api_format.to_string()).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
for api_format in configured_key_api_formats(key) {
|
||||
*total.entry(api_format.clone()).or_insert(0) += 1;
|
||||
if key.is_active {
|
||||
*active.entry(api_format.to_string()).or_insert(0) += 1;
|
||||
*active.entry(api_format).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -861,6 +861,7 @@ pub fn build_admin_system_stats_payload(
|
||||
active_providers: u64,
|
||||
total_api_keys: u64,
|
||||
total_requests: u64,
|
||||
usage_counter: serde_json::Value,
|
||||
) -> serde_json::Value {
|
||||
json!({
|
||||
"users": {
|
||||
@@ -873,6 +874,7 @@ pub fn build_admin_system_stats_payload(
|
||||
},
|
||||
"api_keys": total_api_keys,
|
||||
"requests": total_requests,
|
||||
"usage_counter": usage_counter,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user