mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
Merge branch 'fawney19:main' into main
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user