diff --git a/backend/crates/gateway-admin/src/model/observability.rs b/backend/crates/gateway-admin/src/model/observability.rs index fa426d325..60a4e8f0b 100644 --- a/backend/crates/gateway-admin/src/model/observability.rs +++ b/backend/crates/gateway-admin/src/model/observability.rs @@ -720,6 +720,7 @@ pub struct DiagnosticObservation { pub attempt_count: u64, pub total_tokens: u64, pub average_latency_ms: Option, + pub latency_p95_ms: Option, pub cost_coverage: CostCoverage, pub costs: Vec, } @@ -734,6 +735,7 @@ pub struct OpsError { pub client_api_key_ref: Option, pub component: String, pub operation: String, + pub endpoint: Option, pub provider_kind: Option, pub provider_account_ref: Option, pub provider_account_name: Option, @@ -1040,6 +1042,7 @@ pub struct DiagnosticsItem { pub error_rate: f64, pub request_share: f64, pub average_latency_ms: Option, + pub latency_p95_ms: Option, pub estimated_cost: Option, pub attempt_count: u64, pub total_tokens: u64, diff --git a/backend/crates/gateway-admin/src/use_case/observability.rs b/backend/crates/gateway-admin/src/use_case/observability.rs index 59df98ccc..669475abf 100644 --- a/backend/crates/gateway-admin/src/use_case/observability.rs +++ b/backend/crates/gateway-admin/src/use_case/observability.rs @@ -255,6 +255,7 @@ impl ObservabilityService for DefaultObservabilityService { error_rate: rate_or_zero(item.failure_count, item.request_count), request_share: rate_or_zero(item.request_count, total_requests), average_latency_ms: item.average_latency_ms, + latency_p95_ms: item.latency_p95_ms, estimated_cost: usd_cost(&item.costs), attempt_count: item.attempt_count, total_tokens: item.total_tokens, diff --git a/backend/crates/gateway-admin/tests/use_case/observability.rs b/backend/crates/gateway-admin/tests/use_case/observability.rs index c2c0d7684..36fcaff19 100644 --- a/backend/crates/gateway-admin/tests/use_case/observability.rs +++ b/backend/crates/gateway-admin/tests/use_case/observability.rs @@ -703,6 +703,7 @@ fn diagnostic(name: &str, request_count: u64) -> DiagnosticObservation { attempt_count: request_count, total_tokens: request_count.saturating_mul(100), average_latency_ms: Some(100), + latency_p95_ms: Some(200), cost_coverage: CostCoverage::default(), costs: Vec::new(), } diff --git a/backend/crates/gateway-api/src/admin/observability.rs b/backend/crates/gateway-api/src/admin/observability.rs index 014d5b6b6..786cd1b05 100644 --- a/backend/crates/gateway-api/src/admin/observability.rs +++ b/backend/crates/gateway-api/src/admin/observability.rs @@ -771,8 +771,6 @@ pub struct TrendPointView { pub errors_value: u64, pub latency: String, pub latency_value: Option, - pub first_token_latency: String, - pub first_token_latency_value: Option, pub max_latency: String, pub max_latency_value: Option, pub min_latency: String, @@ -844,21 +842,6 @@ pub struct DashboardCacheCardView { pub average_first_token_latency_ms: String, } -/// Dashboard 凭据用量摘要。 -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct DashboardCredentialUsageView { - pub id: String, - pub display_name: String, - pub plan_type: Option, - pub tokens: String, - pub tokens_value: Option, - pub last_used: String, - pub provider: String, - pub availability: String, - pub request_count: u64, -} - /// 旧 Dashboard 账号概览卡片所需的 Provider 安全投影。 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -1016,18 +999,6 @@ pub struct DashboardDesktopReleaseView { #[serde(skip_serializing_if = "Option::is_none")] pub latest_build: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub published_at: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub minimum_system_version: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub hardware_requirements: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub download_url: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub download_size: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub signature_present: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, } @@ -1061,14 +1032,10 @@ pub struct DashboardDataView { pub health_timeline: HealthTimelineView, pub wire_profiles: Vec, pub account_usage: Vec, - pub credential_usage: Vec, pub usage_records: Vec, pub pool_summary: DashboardPoolSummaryView, pub capacity_info: DashboardCapacityInfoView, pub rotation_strategy: String, - pub logical_requests: RequestMetricsView, - pub attempts: AttemptMetricsView, - pub costs: Vec, } /// 用量汇总响应数据。 @@ -1215,6 +1182,7 @@ pub struct DiagnosticItemView { pub error_rate: f64, pub request_share: f64, pub average_latency_ms: Option, + pub latency_p95_ms: Option, pub estimated_cost: Option, pub attempt_count: u64, pub total_tokens: u64, @@ -1973,11 +1941,11 @@ fn ops_error_view(error: domain::OpsError) -> OpsErrorView { id: error.event_id, request_id: error.request_id, client_api_key_id: error.client_api_key_ref, - kind: error.operation.clone(), + kind: error.component.clone(), provider: error.provider_kind, authentication_kind: error.provider_account_authentication_kind, account_id: error.provider_account_ref, - route: error.operation, + route: error.endpoint.unwrap_or_else(|| "—".to_owned()), model: error.upstream_model_id, client_status_code: error.client_status_code.map(i64::from), upstream_status_code: error.upstream_status_code.map(i64::from), @@ -2037,8 +2005,6 @@ fn trend_point_view(point: domain::TrendPoint) -> TrendPointView { errors_value: point.service_failure_count, latency: display_duration(point.average_latency_ms), latency_value: point.average_latency_ms, - first_token_latency: display_duration(point.average_first_token_latency_ms), - first_token_latency_value: point.average_first_token_latency_ms, max_latency: display_duration(point.metrics.max_latency_ms), max_latency_value: point.metrics.max_latency_ms, min_latency: display_duration(point.metrics.min_latency_ms), @@ -2189,12 +2155,6 @@ fn wire_profile_view(profile: domain::DashboardWireProfile) -> DashboardWireProf checked_at: release.checked_at, latest_version: release.latest_version, latest_build: release.latest_build, - published_at: release.published_at, - minimum_system_version: release.minimum_system_version, - hardware_requirements: release.hardware_requirements, - download_url: release.download_url, - download_size: release.download_size, - signature_present: release.signature_present, error: release.error, }), } @@ -2250,14 +2210,12 @@ fn dashboard_view(result: domain::DashboardResult, kind: TrendKind) -> Dashboard let domain::DashboardObservation { range, requests, - attempts, provider_accounts, - trend: _, account_usage, recent_requests, + .. } = observation; let mut account_usage_views = Vec::with_capacity(account_usage.len()); - let mut credential_usage_views = Vec::with_capacity(account_usage.len()); for credential in account_usage { account_usage_views.push(DashboardAccountUsageView { id: credential.account_id.clone(), @@ -2283,21 +2241,6 @@ fn dashboard_view(result: domain::DashboardResult, kind: TrendKind) -> Dashboard quota_used_percent: credential.quota_used_percent, last_used: relative_time(credential.last_used_at, range.end), }); - credential_usage_views.push(DashboardCredentialUsageView { - id: credential.account_id, - display_name: credential.email.unwrap_or(credential.name), - plan_type: credential.plan_type, - tokens: credential - .total_tokens - .map_or_else(|| "-".to_owned(), format_compact_number), - tokens_value: credential.total_tokens, - last_used: credential - .last_used_at - .map_or_else(|| "-".to_owned(), |value| china_datetime(&value)), - provider: credential.provider_kind, - availability: credential.availability, - request_count: credential.request_count, - }); } DashboardDataView { cards: DashboardCardsView { @@ -2337,7 +2280,6 @@ fn dashboard_view(result: domain::DashboardResult, kind: TrendKind) -> Dashboard health_timeline: health_timeline_view(health_timeline), wire_profiles: wire_profiles.into_iter().map(wire_profile_view).collect(), account_usage: account_usage_views, - credential_usage: credential_usage_views, usage_records: recent_requests.into_iter().map(usage_record_view).collect(), pool_summary: DashboardPoolSummaryView { total: provider_accounts.total, @@ -2355,9 +2297,6 @@ fn dashboard_view(result: domain::DashboardResult, kind: TrendKind) -> Dashboard available_slots: capacity.available_slots, }, rotation_strategy: rotation_strategy_name(rotation_strategy).to_owned(), - logical_requests: request_metrics_view(&requests), - attempts: attempt_metrics_view(&attempts), - costs: cost_views(&attempts.costs), } } @@ -2554,6 +2493,7 @@ fn diagnostics_view( error_rate: item.error_rate, request_share: item.request_share, average_latency_ms: item.average_latency_ms, + latency_p95_ms: item.latency_p95_ms, estimated_cost: item.estimated_cost.as_ref().map(ToString::to_string), attempt_count: item.attempt_count, total_tokens: item.total_tokens, diff --git a/backend/crates/gateway-api/tests/admin/observability.rs b/backend/crates/gateway-api/tests/admin/observability.rs index 72f32bb3c..2e35984ff 100644 --- a/backend/crates/gateway-api/tests/admin/observability.rs +++ b/backend/crates/gateway-api/tests/admin/observability.rs @@ -167,8 +167,6 @@ mod response { errors_value: 0, latency: "1 ms".to_owned(), latency_value: Some(1), - first_token_latency: "1 ms".to_owned(), - first_token_latency_value: Some(1), max_latency: "1 ms".to_owned(), max_latency_value: Some(1), min_latency: "1 ms".to_owned(), @@ -468,6 +466,7 @@ async fn ops_errors_should_keep_account_label_and_authentication_contract() { client_api_key_ref: Some("key_err".to_owned()), component: "model_request".to_owned(), operation: "responses".to_owned(), + endpoint: Some("/v1/responses".to_owned()), provider_kind: Some("openai".to_owned()), provider_account_ref: Some("acct_err".to_owned()), provider_account_name: None, @@ -508,14 +507,20 @@ async fn ops_errors_should_keep_account_label_and_authentication_contract() { serde_json::json!({ "provider": value["data"]["items"][0]["provider"], "authenticationKind": value["data"]["items"][0]["authenticationKind"], + "kind": value["data"]["items"][0]["kind"], "accountId": value["data"]["items"][0]["accountId"], "accountLabel": value["data"]["items"][0]["metadata"]["accountLabel"], + "clientStatusCode": value["data"]["items"][0]["clientStatusCode"], + "route": value["data"]["items"][0]["route"], }), serde_json::json!({ "provider": "openai", "authenticationKind": "api_key", + "kind": "model_request", "accountId": "acct_err", "accountLabel": "err@example.invalid", + "clientStatusCode": 502, + "route": "/v1/responses", }) ); } @@ -547,6 +552,7 @@ async fn diagnostics_should_keep_stable_key_and_display_name_contract() { attempt_count: 2, total_tokens: 200, average_latency_ms: Some(100), + latency_p95_ms: Some(3800), cost_coverage: CostCoverage::default(), costs: Vec::new(), }); @@ -571,11 +577,13 @@ async fn diagnostics_should_keep_stable_key_and_display_name_contract() { ( &value["data"]["items"][0]["key"], &value["data"]["items"][0]["name"], + &value["data"]["items"][0]["latencyP95Ms"], &value["data"]["dimension"], ), ( &serde_json::json!("acct_diag"), &serde_json::json!("diag@example.invalid"), + &serde_json::json!(3800), &serde_json::json!("account"), ) ); diff --git a/backend/crates/gateway-store/src/postgres/observability.rs b/backend/crates/gateway-store/src/postgres/observability.rs index 87112a6ae..9e560a64e 100644 --- a/backend/crates/gateway-store/src/postgres/observability.rs +++ b/backend/crates/gateway-store/src/postgres/observability.rs @@ -647,6 +647,7 @@ pub struct DiagnosticObservation { pub attempt_count: u64, pub total_tokens: u64, pub average_latency_ms: Option, + pub latency_p95_ms: Option, pub cost_coverage: CostCoverage, pub costs: Vec, } @@ -660,6 +661,7 @@ pub struct OpsErrorRecord { pub client_api_key_ref: Option, pub component: String, pub operation: String, + pub endpoint: Option, pub provider_kind: Option, pub provider_account_ref: Option, pub provider_account_name: Option, @@ -1614,6 +1616,7 @@ fn admin_diagnostic_observation( attempt_count: observation.attempt_count, total_tokens: observation.total_tokens, average_latency_ms: observation.average_latency_ms, + latency_p95_ms: observation.latency_p95_ms, cost_coverage: admin_cost_coverage(observation.cost_coverage), costs: admin_currency_costs(observation.costs)?, }) @@ -1636,6 +1639,7 @@ fn admin_ops_error(error: OpsErrorRecord) -> admin_observability::OpsError { client_api_key_ref: error.client_api_key_ref, component: error.component, operation: error.operation, + endpoint: error.endpoint, provider_kind: error.provider_kind, provider_account_ref: error.provider_account_ref, provider_account_name: error.provider_account_name, @@ -2887,6 +2891,8 @@ async fn usage_diagnostics( coalesce(sum(mr.attempt_count), 0)::bigint as attempt_count, coalesce(sum(mr.total_tokens), 0)::bigint as total_tokens, round(avg(mr.latency_ms))::bigint as average_latency_ms, + round(percentile_cont(0.95) within group (order by mr.latency_ms))::bigint + as latency_p95_ms, count(*) filter (where mr.cost_source = 'provider_reported')::bigint as provider_reported_count, count(*) filter (where mr.cost_source = 'calculated')::bigint @@ -2919,6 +2925,7 @@ async fn usage_diagnostics( attempt_count: unsigned(row, "attempt_count")?, total_tokens: unsigned(row, "total_tokens")?, average_latency_ms: optional_unsigned(row, "average_latency_ms")?, + latency_p95_ms: optional_unsigned(row, "latency_p95_ms")?, cost_coverage: coverage_from_row(row)?, costs: Vec::new(), }) @@ -3090,6 +3097,7 @@ const OPS_ERRORS_CTE: &str = "with errors as ( mr.id as event_id, mr.id as request_id, nullif(mr.attempt_count, 0) as attempt_index, mr.client_api_key_ref, 'model_request'::text as component, mr.operation, + mr.endpoint, mr.provider_kind, mr.provider_account_ref, mr.provider_account_name_snapshot as provider_account_name, @@ -3108,6 +3116,7 @@ const OPS_ERRORS_CTE: &str = "with errors as ( union all select 'ops_event'::text, oe.id, oe.model_request_id, oe.attempt_index, mr.client_api_key_ref, oe.component, oe.operation, + null::text as endpoint, oe.provider_kind, oe.provider_account_ref, oe.provider_account_name_snapshot as provider_account_name, oe.provider_account_email_snapshot as provider_account_email, @@ -3127,7 +3136,7 @@ async fn list_ops_errors(pool: &PgPool, query: OpsErrorQuery) -> StoreResult::new(OPS_ERRORS_CTE); statement.push( " select source, event_id, request_id, attempt_index, client_api_key_ref, - component, operation, provider_kind, provider_account_ref, + component, operation, endpoint, provider_kind, provider_account_ref, provider_account_name, provider_account_email, provider_account_authentication_kind, upstream_model_id, upstream_transport, @@ -3331,6 +3340,7 @@ fn ops_error_from_row(row: &sqlx::postgres::PgRow) -> StoreResult [ { label: '时间', value: props.record?.createdAtDisplay }, { label: '事件', value: props.record?.kind }, { label: '失败分类', value: props.record?.failureClass }, - { label: 'Provider', value: props.record?.provider }, + { label: '平台/类型', value: providerKindLabel(props.record) }, { label: '端点', value: props.record?.route }, { label: '模型', value: props.record?.model }, { label: '账号 ID', value: props.record?.accountId }, @@ -44,6 +44,14 @@ function display(value: unknown) { function latencyDisplay(value: unknown) { return typeof value === 'number' ? `${value} ms` : '—' } + +function providerKindLabel(record: Awaited>['items'][number] | null) { + if (!record?.provider) + return '—' + return record.authenticationKind + ? `${record.provider} · ${record.authenticationKind}` + : record.provider +}