refactor: 抽离 AI pipeline 与调度共享能力逻辑

This commit is contained in:
fawney19
2026-04-10 01:46:14 +08:00
parent b901a6ffc7
commit 5014e2f5fd
255 changed files with 15057 additions and 3115 deletions

View File

@@ -1,5 +1,7 @@
use crate::api::ai::public_api_format_local_path;
use crate::handlers::shared::{query_param_optional_bool, query_param_value, unix_secs_to_rfc3339};
use crate::handlers::shared::{
query_param_optional_bool, query_param_value, unix_ms_to_rfc3339, unix_secs_to_rfc3339,
};
use crate::AppState;
use aether_data_contracts::repository::candidates::{
PublicHealthTimelineBucket, RequestCandidateStatus, StoredRequestCandidate,
@@ -25,11 +27,11 @@ pub(crate) fn request_candidate_status_label(status: RequestCandidateStatus) ->
}
}
pub(crate) fn request_candidate_event_unix_secs(candidate: &StoredRequestCandidate) -> u64 {
pub(crate) fn request_candidate_event_unix_ms(candidate: &StoredRequestCandidate) -> u64 {
candidate
.finished_at_unix_secs
.or(candidate.started_at_unix_secs)
.unwrap_or(candidate.created_at_unix_secs)
.finished_at_unix_ms
.or(candidate.started_at_unix_ms)
.unwrap_or(candidate.created_at_unix_ms)
}
pub(crate) fn normalize_admin_base_url(base_url: &str) -> Result<String, String> {
@@ -410,28 +412,24 @@ pub(crate) async fn build_api_format_health_monitor_payload(
total_count: 0,
success_count: 0,
failed_count: 0,
min_created_at_unix_secs: None,
max_created_at_unix_secs: None,
min_created_at_unix_ms: None,
max_created_at_unix_ms: None,
});
bucket.total_count += row.total_count;
bucket.success_count += row.success_count;
bucket.failed_count += row.failed_count;
bucket.min_created_at_unix_secs = match (
bucket.min_created_at_unix_secs,
row.min_created_at_unix_secs,
) {
(Some(left), Some(right)) => Some(left.min(right)),
(None, Some(right)) => Some(right),
(left, None) => left,
};
bucket.max_created_at_unix_secs = match (
bucket.max_created_at_unix_secs,
row.max_created_at_unix_secs,
) {
(Some(left), Some(right)) => Some(left.max(right)),
(None, Some(right)) => Some(right),
(left, None) => left,
};
bucket.min_created_at_unix_ms =
match (bucket.min_created_at_unix_ms, row.min_created_at_unix_ms) {
(Some(left), Some(right)) => Some(left.min(right)),
(None, Some(right)) => Some(right),
(left, None) => left,
};
bucket.max_created_at_unix_ms =
match (bucket.max_created_at_unix_ms, row.max_created_at_unix_ms) {
(Some(left), Some(right)) => Some(left.max(right)),
(None, Some(right)) => Some(right),
(left, None) => left,
};
}
let mut formats = Vec::new();
@@ -456,19 +454,19 @@ pub(crate) async fn build_api_format_health_monitor_payload(
};
let last_event_at = attempts.first().and_then(|candidate| {
candidate
.finished_at_unix_secs
.or(candidate.started_at_unix_secs)
.or(Some(candidate.created_at_unix_secs))
.finished_at_unix_ms
.or(candidate.started_at_unix_ms)
.or(Some(candidate.created_at_unix_ms))
});
let events = attempts
.into_iter()
.filter_map(|candidate| {
let timestamp = candidate
.finished_at_unix_secs
.or(candidate.started_at_unix_secs)
.unwrap_or(candidate.created_at_unix_secs);
.finished_at_unix_ms
.or(candidate.started_at_unix_ms)
.unwrap_or(candidate.created_at_unix_ms);
Some(json!({
"timestamp": unix_secs_to_rfc3339(timestamp)?,
"timestamp": unix_ms_to_rfc3339(timestamp)?,
"status": request_candidate_status_label(candidate.status),
"status_code": candidate.status_code,
"latency_ms": candidate.latency_ms,
@@ -490,11 +488,11 @@ pub(crate) async fn build_api_format_health_monitor_payload(
"failed_count": failed_count,
"skipped_count": skipped_count,
"success_rate": success_rate,
"last_event_at": last_event_at.and_then(unix_secs_to_rfc3339),
"last_event_at": last_event_at.and_then(unix_ms_to_rfc3339),
"events": events,
"timeline": timeline,
"time_range_start": time_range_start.and_then(unix_secs_to_rfc3339),
"time_range_end": time_range_end.or(Some(now_unix_secs)).and_then(unix_secs_to_rfc3339),
"time_range_start": time_range_start.and_then(unix_ms_to_rfc3339),
"time_range_end": time_range_end.map(|ms| unix_ms_to_rfc3339(ms)).unwrap_or_else(|| unix_secs_to_rfc3339(now_unix_secs)),
});
if options.include_api_path {
format_payload["api_path"] = json!(public_api_format_local_path(&api_format));
@@ -536,12 +534,12 @@ pub(crate) fn build_public_health_timeline(
continue;
}
earliest_time = match (earliest_time, bucket.min_created_at_unix_secs) {
earliest_time = match (earliest_time, bucket.min_created_at_unix_ms) {
(Some(left), Some(right)) => Some(left.min(right)),
(None, Some(right)) => Some(right),
(left, None) => left,
};
latest_time = match (latest_time, bucket.max_created_at_unix_secs) {
latest_time = match (latest_time, bucket.max_created_at_unix_ms) {
(Some(left), Some(right)) => Some(left.max(right)),
(None, Some(right)) => Some(right),
(left, None) => left,
@@ -592,3 +590,50 @@ pub(crate) fn api_format_display_name(api_format: &str) -> String {
};
format!("{family_label} {kind_label}")
}
#[cfg(test)]
mod tests {
use super::request_candidate_event_unix_ms;
use crate::handlers::shared::unix_ms_to_rfc3339;
use aether_data_contracts::repository::candidates::{
RequestCandidateStatus, StoredRequestCandidate,
};
#[test]
fn request_candidate_event_timestamp_uses_millisecond_precision() {
let candidate = StoredRequestCandidate::new(
"cand-1".to_string(),
"req-1".to_string(),
None,
None,
None,
None,
0,
0,
Some("provider-1".to_string()),
Some("endpoint-1".to_string()),
Some("key-1".to_string()),
RequestCandidateStatus::Success,
None,
false,
Some(200),
None,
None,
Some(42),
Some(1),
None,
None,
1_700_000_000_000,
Some(1_700_000_000_111),
Some(1_700_000_000_123),
)
.expect("candidate should build");
let event_unix_ms = request_candidate_event_unix_ms(&candidate);
assert_eq!(event_unix_ms, 1_700_000_000_123);
assert_eq!(
unix_ms_to_rfc3339(event_unix_ms).as_deref(),
Some("2023-11-14T22:13:20.123Z")
);
}
}

View File

@@ -10,7 +10,7 @@ pub(crate) use self::catalog_helpers::{
admin_requested_force_stream, api_format_display_name, build_api_format_health_monitor_payload,
build_public_catalog_models_payload, build_public_catalog_search_models_payload,
build_public_health_timeline, build_public_providers_payload, normalize_admin_base_url,
provider_key_api_formats, request_candidate_event_unix_secs, request_candidate_status_label,
provider_key_api_formats, request_candidate_event_unix_ms, request_candidate_status_label,
ApiFormatHealthMonitorOptions,
};
pub(crate) use self::system_modules_helpers::{

View File

@@ -84,7 +84,7 @@ pub(super) fn build_public_announcement_payload(
},
"start_time": format_optional_unix_datetime(announcement.start_time_unix_secs),
"end_time": format_optional_unix_datetime(announcement.end_time_unix_secs),
"created_at": format_required_unix_datetime(announcement.created_at_unix_secs),
"created_at": format_required_unix_datetime(announcement.created_at_unix_ms),
"updated_at": format_required_unix_datetime(announcement.updated_at_unix_secs),
})
}

View File

@@ -361,7 +361,7 @@ fn dashboard_usage_local_date(
item: &StoredRequestUsageAudit,
tz_offset_minutes: i32,
) -> Option<chrono::NaiveDate> {
let timestamp = i64::try_from(item.created_at_unix_secs).ok()?;
let timestamp = i64::try_from(item.created_at_unix_ms).ok()?;
let datetime = chrono::DateTime::<chrono::Utc>::from_timestamp(timestamp, 0)?;
Some((datetime + chrono::Duration::minutes(i64::from(tz_offset_minutes))).date_naive())
}
@@ -1008,7 +1008,7 @@ pub(super) async fn handle_dashboard_recent_requests_get(
"user": username,
"model": dashboard_non_empty_value(&item.model, "N/A"),
"tokens": item.total_tokens,
"time": dashboard_format_time_hhmm(item.created_at_unix_secs),
"time": dashboard_format_time_hhmm(item.created_at_unix_ms),
"is_stream": item.is_stream,
})
})

View File

@@ -95,7 +95,7 @@ fn users_me_usage_total_input_context(item: &StoredRequestUsageAudit) -> u64 {
fn users_me_usage_effective_unix_secs(item: &StoredRequestUsageAudit) -> u64 {
item.finalized_at_unix_secs
.unwrap_or(item.created_at_unix_secs)
.unwrap_or(item.created_at_unix_ms)
}
fn users_me_usage_cache_hit_rate(total_input_context: u64, cache_read_tokens: u64) -> f64 {
@@ -159,7 +159,7 @@ fn build_users_me_usage_record_payload(
"first_byte_time_ms": item.first_byte_time_ms,
"is_stream": item.is_stream,
"status": item.status,
"created_at": unix_secs_to_rfc3339(item.created_at_unix_secs),
"created_at": unix_secs_to_rfc3339(item.created_at_unix_ms),
"cache_creation_input_tokens": item.cache_creation_input_tokens,
"cache_read_input_tokens": item.cache_read_input_tokens,
"status_code": item.status_code,
@@ -649,8 +649,8 @@ pub(super) async fn handle_users_me_usage_get(
.collect::<Vec<_>>();
records.sort_by(|left, right| {
right
.created_at_unix_secs
.cmp(&left.created_at_unix_secs)
.created_at_unix_ms
.cmp(&left.created_at_unix_ms)
.then_with(|| left.id.cmp(&right.id))
});
let total_record_count = records.len();
@@ -735,8 +735,8 @@ pub(super) async fn handle_users_me_usage_active_get(
.collect::<Vec<_>>();
items.sort_by(|left, right| {
right
.created_at_unix_secs
.cmp(&left.created_at_unix_secs)
.created_at_unix_ms
.cmp(&left.created_at_unix_ms)
.then_with(|| left.id.cmp(&right.id))
});
if ids.is_none() && items.len() > 50 {
@@ -798,20 +798,19 @@ pub(super) async fn handle_users_me_usage_interval_timeline_get(
};
items.retain(|item| item.status == "completed");
items.sort_by(|left, right| {
left.created_at_unix_secs
.cmp(&right.created_at_unix_secs)
left.created_at_unix_ms
.cmp(&right.created_at_unix_ms)
.then_with(|| left.id.cmp(&right.id))
});
let mut points = Vec::new();
let mut previous_created_at_unix_secs = None;
let mut previous_created_at_unix_ms = None;
for item in items {
if let Some(previous) = previous_created_at_unix_secs {
let interval_minutes =
(item.created_at_unix_secs.saturating_sub(previous) as f64) / 60.0;
if let Some(previous) = previous_created_at_unix_ms {
let interval_minutes = (item.created_at_unix_ms.saturating_sub(previous) as f64) / 60.0;
if interval_minutes <= 120.0 {
points.push(json!({
"x": unix_secs_to_rfc3339(item.created_at_unix_secs),
"x": unix_secs_to_rfc3339(item.created_at_unix_ms),
"y": round_to(interval_minutes, 2),
"model": item.model,
}));
@@ -820,7 +819,7 @@ pub(super) async fn handle_users_me_usage_interval_timeline_get(
}
}
}
previous_created_at_unix_secs = Some(item.created_at_unix_secs);
previous_created_at_unix_ms = Some(item.created_at_unix_ms);
}
Json(json!({

View File

@@ -153,7 +153,7 @@ pub(super) fn wallet_transaction_payload_from_record(
"link_id": record.link_id.clone(),
"operator_id": record.operator_id.clone(),
"description": record.description.clone(),
"created_at": record.created_at_unix_secs.and_then(unix_secs_to_rfc3339),
"created_at": record.created_at_unix_ms.and_then(unix_secs_to_rfc3339),
})
}

View File

@@ -207,7 +207,7 @@ pub(crate) fn wallet_payment_order_payload_from_row(
row: &sqlx::postgres::PgRow,
) -> Result<serde_json::Value, GatewayError> {
let created_at = row
.try_get::<Option<i64>, _>("created_at_unix_secs")
.try_get::<Option<i64>, _>("created_at_unix_ms")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.and_then(|value| u64::try_from(value).ok())
.and_then(unix_secs_to_rfc3339);
@@ -280,7 +280,7 @@ fn wallet_payment_order_payload_from_record(
record.gateway_order_id.clone(),
record.gateway_response.clone(),
record.status.clone(),
Some(unix_secs_to_rfc3339(record.created_at_unix_secs)).flatten(),
Some(unix_secs_to_rfc3339(record.created_at_unix_ms)).flatten(),
record.paid_at_unix_secs.and_then(unix_secs_to_rfc3339),
record.credited_at_unix_secs.and_then(unix_secs_to_rfc3339),
record.expires_at_unix_secs.and_then(unix_secs_to_rfc3339),

View File

@@ -98,7 +98,7 @@ fn wallet_refund_payload_from_row(
row: &sqlx::postgres::PgRow,
) -> Result<serde_json::Value, GatewayError> {
let created_at = row
.try_get::<Option<i64>, _>("created_at_unix_secs")
.try_get::<Option<i64>, _>("created_at_unix_ms")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.and_then(|value| u64::try_from(value).ok())
.and_then(unix_secs_to_rfc3339);
@@ -157,7 +157,7 @@ fn wallet_refund_payload_from_record(
"payout_method": record.payout_method,
"payout_reference": record.payout_reference,
"payout_proof": record.payout_proof,
"created_at": unix_secs_to_rfc3339(record.created_at_unix_secs),
"created_at": unix_secs_to_rfc3339(record.created_at_unix_ms),
"updated_at": unix_secs_to_rfc3339(record.updated_at_unix_secs),
"processed_at": record.processed_at_unix_secs.and_then(unix_secs_to_rfc3339),
"completed_at": record.completed_at_unix_secs.and_then(unix_secs_to_rfc3339),
@@ -247,7 +247,7 @@ pub(super) async fn handle_wallet_refunds_list(
"payout_method": record.payout_method,
"payout_reference": record.payout_reference,
"payout_proof": record.payout_proof,
"created_at": unix_secs_to_rfc3339(record.created_at_unix_secs),
"created_at": unix_secs_to_rfc3339(record.created_at_unix_ms),
"updated_at": unix_secs_to_rfc3339(record.updated_at_unix_secs),
"processed_at": record.processed_at_unix_secs.and_then(unix_secs_to_rfc3339),
"completed_at": record.completed_at_unix_secs.and_then(unix_secs_to_rfc3339),

View File

@@ -156,7 +156,7 @@ pub(crate) async fn build_admin_keys_grouped_by_format_payload(
.and_then(serde_json::Value::as_bool)
.unwrap_or(false),
"last_used_at": key.last_used_at_unix_secs.and_then(unix_secs_to_rfc3339),
"created_at": unix_secs_to_rfc3339(key.created_at_unix_secs.unwrap_or(now_unix_secs)),
"created_at": unix_secs_to_rfc3339(key.created_at_unix_ms.unwrap_or(now_unix_secs)),
"updated_at": unix_secs_to_rfc3339(key.updated_at_unix_secs.unwrap_or(now_unix_secs)),
}));
}