Merge branch 'fawney19:main' into main

This commit is contained in:
ZheFox
2026-05-19 00:50:52 +08:00
committed by GitHub
29 changed files with 795 additions and 79 deletions

View File

@@ -82,6 +82,18 @@ pub(crate) fn build_local_execution_report_context(
.client_session_affinity
.and_then(client_session_affinity_report_context_value)
{
if let Some(client_family) = value
.as_object()
.and_then(|object| object.get("client_family"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|client_family| !client_family.is_empty())
{
extra_fields.insert(
"client_family".to_string(),
Value::String(client_family.to_ascii_lowercase()),
);
}
extra_fields.insert(
CLIENT_SESSION_AFFINITY_REPORT_CONTEXT_FIELD.to_string(),
value,

View File

@@ -5,12 +5,13 @@ use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
use crate::handlers::admin::shared::query_param_value;
use crate::GatewayError;
use aether_admin::observability::usage::{
admin_usage_bad_request_response, admin_usage_data_unavailable_response,
admin_usage_has_fallback, admin_usage_is_failed, admin_usage_matches_search,
admin_usage_matches_username, admin_usage_parse_ids, admin_usage_parse_limit,
admin_usage_parse_offset, admin_usage_provider_key_name, admin_usage_record_json,
build_admin_usage_active_requests_response, build_admin_usage_records_response,
build_admin_usage_summary_stats_response_from_summary, ADMIN_USAGE_DATA_UNAVAILABLE_DETAIL,
admin_usage_bad_request_response, admin_usage_client_family,
admin_usage_data_unavailable_response, admin_usage_has_fallback, admin_usage_is_failed,
admin_usage_matches_search, admin_usage_matches_username, admin_usage_parse_ids,
admin_usage_parse_limit, admin_usage_parse_offset, admin_usage_provider_key_name,
admin_usage_record_json, build_admin_usage_active_requests_response,
build_admin_usage_records_response, build_admin_usage_summary_stats_response_from_summary,
ADMIN_USAGE_DATA_UNAVAILABLE_DETAIL,
};
use aether_data::repository::users::StoredUserSummary;
use aether_data_contracts::repository::{
@@ -263,6 +264,19 @@ fn admin_usage_matches_attempt_status(
}
}
fn admin_usage_matches_client_family(
item: &StoredRequestUsageAudit,
client_family: Option<&str>,
) -> bool {
let Some(client_family) = client_family
.map(str::trim)
.filter(|value| !value.is_empty())
else {
return true;
};
admin_usage_client_family(item).is_some_and(|value| value.eq_ignore_ascii_case(client_family))
}
#[allow(clippy::too_many_arguments)]
fn build_admin_usage_records_response_with_attempt_flags(
items: &[StoredRequestUsageAudit],
@@ -600,6 +614,7 @@ pub(super) async fn maybe_build_local_admin_usage_summary_response(
admin_usage_attempt_status_filter(query_param_value(query, "status").as_deref());
let search = query_param_value(query, "search");
let username_filter = query_param_value(query, "username");
let client_family_filter = query_param_value(query, "client_family");
let limit = match admin_usage_parse_limit(query) {
Ok(value) => value,
Err(detail) => return Ok(Some(admin_usage_bad_request_response(detail))),
@@ -634,7 +649,12 @@ pub(super) async fn maybe_build_local_admin_usage_summary_response(
let active_username_filter = username_filter
.as_deref()
.filter(|value| !value.trim().is_empty());
let (usage, total) = if let Some(attempt_status) = attempt_status_filter {
let active_client_family_filter = client_family_filter
.as_deref()
.filter(|value| !value.trim().is_empty());
let (usage, total) = if attempt_status_filter.is_some()
|| active_client_family_filter.is_some()
{
let mut usage = state.list_usage_audits(&base_query).await?;
let user_ids: Vec<String> = usage
.iter()
@@ -664,12 +684,14 @@ pub(super) async fn maybe_build_local_admin_usage_summary_response(
active_username_filter,
&users_by_id,
state.has_auth_user_data_reader(),
) && admin_usage_matches_attempt_status(
item,
attempt_status,
&attempt_flags_by_usage_id,
request_candidate_reader_available,
)
) && attempt_status_filter.is_none_or(|attempt_status| {
admin_usage_matches_attempt_status(
item,
attempt_status,
&attempt_flags_by_usage_id,
request_candidate_reader_available,
)
}) && admin_usage_matches_client_family(item, active_client_family_filter)
});
sort_usage_newest_first(&mut usage);
let total = usage.len();

View File

@@ -23,6 +23,10 @@ pub(super) fn endpoint_key_counts_by_format(
admin_provider_endpoints_pure::endpoint_key_counts_by_format(provider_type, endpoints, keys)
}
pub(super) fn normalize_endpoint_api_format(api_format: &str) -> String {
admin_provider_endpoints_pure::normalize_endpoint_api_format(api_format)
}
pub(super) fn build_admin_provider_endpoint_response(
endpoint: &StoredProviderCatalogEndpoint,
provider_name: &str,

View File

@@ -4,7 +4,10 @@ use aether_data_contracts::repository::provider_catalog::{
};
use std::time::{SystemTime, UNIX_EPOCH};
use super::payloads::{build_admin_provider_endpoint_response, endpoint_key_counts_by_format};
use super::payloads::{
build_admin_provider_endpoint_response, endpoint_key_counts_by_format,
normalize_endpoint_api_format,
};
pub(crate) async fn build_admin_provider_endpoints_payload(
state: &AdminAppState<'_>,
@@ -52,8 +55,7 @@ pub(crate) async fn build_admin_provider_endpoints_payload(
.skip(skip)
.take(limit)
.map(|endpoint| {
let endpoint_api_format =
aether_ai_formats::normalize_api_format_alias(&endpoint.api_format);
let endpoint_api_format = normalize_endpoint_api_format(&endpoint.api_format);
build_admin_provider_endpoint_response(
&endpoint,
&provider.name,
@@ -105,7 +107,7 @@ pub(crate) async fn build_admin_endpoint_payload(
.ok()
.map(|duration| duration.as_secs())
.unwrap_or(0);
let endpoint_api_format = aether_ai_formats::normalize_api_format_alias(&endpoint.api_format);
let endpoint_api_format = normalize_endpoint_api_format(&endpoint.api_format);
Some(build_admin_provider_endpoint_response(
&endpoint,

View File

@@ -1,7 +1,7 @@
use super::extractors::admin_endpoint_id;
use super::payloads::{
build_admin_provider_endpoint_response, endpoint_key_counts_by_format,
AdminProviderEndpointUpdatePatch,
normalize_endpoint_api_format, AdminProviderEndpointUpdatePatch,
};
use super::support::build_admin_endpoints_data_unavailable_response;
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
@@ -152,7 +152,7 @@ pub(super) async fn maybe_handle(
std::slice::from_ref(&updated),
&keys,
);
let updated_api_format = aether_ai_formats::normalize_api_format_alias(&updated.api_format);
let updated_api_format = normalize_endpoint_api_format(&updated.api_format);
Ok(Some(
Json(build_admin_provider_endpoint_response(

View File

@@ -321,6 +321,75 @@ fn users_me_usage_upstream_is_stream(item: &StoredRequestUsageAudit) -> bool {
.unwrap_or(item.is_stream)
}
fn users_me_usage_metadata_string<'a>(
item: &'a StoredRequestUsageAudit,
key: &str,
) -> Option<&'a str> {
item.request_metadata
.as_ref()
.and_then(serde_json::Value::as_object)
.and_then(|metadata| metadata.get(key))
.and_then(serde_json::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
}
fn users_me_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(serde_json::Value::as_object)
.and_then(|metadata| {
metadata
.get("client_session_affinity")
.and_then(serde_json::Value::as_object)
.and_then(|affinity| affinity.get("client_family"))
.and_then(serde_json::Value::as_str)
.or_else(|| {
metadata
.get("client_family")
.and_then(serde_json::Value::as_str)
})
})
.map(str::trim)
.filter(|value| !value.is_empty())
})
.or_else(|| {
users_me_usage_metadata_string(item, "user_agent")
.and_then(infer_client_family_from_user_agent)
})
}
fn build_users_me_usage_record_payload(
item: &StoredRequestUsageAudit,
include_actual_cost: bool,
@@ -353,6 +422,11 @@ fn build_users_me_usage_record_payload(
"upstream_is_stream": upstream_is_stream,
"client_requested_stream": client_is_stream,
"client_is_stream": client_is_stream,
"client_family": users_me_usage_client_family(item),
"client_ip": users_me_usage_metadata_string(item, "client_ip"),
"user_agent": users_me_usage_metadata_string(item, "user_agent"),
"request_path": users_me_usage_metadata_string(item, "request_path"),
"request_path_and_query": users_me_usage_metadata_string(item, "request_path_and_query"),
"status": item.status,
"has_fallback": item.has_fallback(),
"created_at": unix_secs_to_rfc3339(item.created_at_unix_ms),
@@ -411,6 +485,9 @@ fn build_users_me_usage_active_payload(item: &StoredRequestUsageAudit) -> serde_
"client_requested_stream": client_is_stream,
"client_is_stream": client_is_stream,
"has_format_conversion": item.has_format_conversion,
"client_family": users_me_usage_client_family(item),
"client_ip": users_me_usage_metadata_string(item, "client_ip"),
"user_agent": users_me_usage_metadata_string(item, "user_agent"),
"target_model": item.target_model,
"has_fallback": item.has_fallback(),
});
@@ -1368,6 +1445,41 @@ mod tests {
assert_eq!(active_payload["client_is_stream"], false);
}
#[test]
fn user_usage_payload_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")
};
let record_payload =
build_users_me_usage_record_payload(&item, false, &BTreeMap::new(), false);
let active_payload = build_users_me_usage_active_payload(&item);
assert_eq!(record_payload["client_family"], "codex_vscode");
assert_eq!(record_payload["client_ip"], "192.168.0.28");
assert_eq!(active_payload["client_family"], "codex_vscode");
assert_eq!(active_payload["client_ip"], "192.168.0.28");
}
#[test]
fn user_usage_payload_labels_openai_js_user_agent_as_sdk() {
let item = StoredRequestUsageAudit {
request_metadata: Some(json!({
"user_agent": "OpenAI/JS 6.34.0"
})),
..sample_usage("completed")
};
let record_payload =
build_users_me_usage_record_payload(&item, false, &BTreeMap::new(), false);
assert_eq!(record_payload["client_family"], "openai_js_sdk");
}
#[test]
fn user_usage_stream_inference_falls_back_to_request_body_stream_flag() {
let item = StoredRequestUsageAudit {