From 56451601fb76e752a835b378df84f39d8f382e8a Mon Sep 17 00:00:00 2001
From: Zyy
Date: Mon, 31 Aug 2026 10:57:53 +0800
Subject: [PATCH] feat(observability): enrich error request traces
Expose request, client, model, transport, and account facts in operations errors and surface them in the error table and detail view.
Co-authored-by: Codex
---
.../gateway-admin/src/model/observability.rs | 12 ++
.../src/admin/observability/presenter.rs | 23 +++-
.../src/admin/observability/query.rs | 3 +-
.../src/admin/observability/wire.rs | 17 +++
.../tests/admin/observability/response.rs | 21 ++++
.../src/postgres/observability/mapping.rs | 23 ++++
.../src/postgres/observability/model.rs | 13 ++
.../src/postgres/observability/queries/ops.rs | 18 ++-
.../tests/postgres/observability.rs | 25 +++-
frontend/src/api/modules/usage.ts | 17 +++
.../usage/components/OpsErrorDetailModal.vue | 113 ++++++++++++------
.../views/usage/components/OpsErrorPanel.vue | 68 ++++++++++-
frontend/src/views/usage/constants.ts | 8 +-
13 files changed, 315 insertions(+), 46 deletions(-)
diff --git a/backend/crates/gateway-admin/src/model/observability.rs b/backend/crates/gateway-admin/src/model/observability.rs
index 74125a7e9..e4923ce20 100644
--- a/backend/crates/gateway-admin/src/model/observability.rs
+++ b/backend/crates/gateway-admin/src/model/observability.rs
@@ -135,6 +135,7 @@ pub struct OpsErrorFilter {
pub provider_kind: Option,
pub provider_account_ref: Option,
pub operation: Option,
+ pub endpoint: Option,
pub model: Option,
pub transport: Option,
pub attempt_index: Option,
@@ -762,6 +763,10 @@ pub struct OpsError {
pub client_api_key_ref: Option,
pub component: String,
pub operation: String,
+ pub protocol: Option,
+ pub client_transport: Option,
+ pub requested_model_id: Option,
+ pub service_tier: Option,
pub endpoint: Option,
pub provider_kind: Option,
pub provider_account_ref: Option,
@@ -779,6 +784,13 @@ pub struct OpsError {
pub latency_ms: Option,
pub message: String,
pub occurrence_count: u32,
+ pub client_ip: Option,
+ pub user_agent: Option,
+ pub reasoning_effort: Option,
+ pub reasoning_preset: Option,
+ pub request_kind: Option,
+ pub subagent_kind: Option,
+ pub compact: Option,
pub occurred_at: DateTime,
pub stable_sort_id: String,
}
diff --git a/backend/crates/gateway-api/src/admin/observability/presenter.rs b/backend/crates/gateway-api/src/admin/observability/presenter.rs
index f3c012e18..2c99805fc 100644
--- a/backend/crates/gateway-api/src/admin/observability/presenter.rs
+++ b/backend/crates/gateway-api/src/admin/observability/presenter.rs
@@ -531,24 +531,45 @@ pub(crate) fn ops_error_view(error: domain::OpsError) -> OpsErrorView {
.or(error.provider_account_name.as_ref())
.or(error.provider_account_ref.as_ref())
.cloned();
+ let model = error
+ .upstream_model_id
+ .clone()
+ .or_else(|| error.requested_model_id.clone());
OpsErrorView {
id: error.event_id,
request_id: error.request_id,
client_api_key_id: error.client_api_key_ref,
kind: error.component.clone(),
+ operation: error.operation,
+ protocol: error.protocol,
+ client_transport: error.client_transport,
provider: error.provider_kind,
authentication_kind: error.provider_account_authentication_kind,
account_id: error.provider_account_ref,
+ account_name: error.provider_account_name,
+ account_email: error.provider_account_email,
route: error.endpoint.unwrap_or_else(|| "—".to_owned()),
- model: error.upstream_model_id,
+ model,
+ requested_model: error.requested_model_id,
+ upstream_model: error.upstream_model_id,
+ service_tier: error.service_tier,
client_status_code: error.client_status_code.map(i64::from),
upstream_status_code: error.upstream_status_code.map(i64::from),
transport: error.upstream_transport,
attempt_index: error.attempt_index,
failure_class: error.failure_kind,
+ provider_error_code: error.provider_error_code,
+ occurrence_count: error.occurrence_count,
response_id: error.client_response_id,
upstream_request_id: error.upstream_request_id,
latency_ms: error.latency_ms,
+ client_ip: error.client_ip,
+ user_agent: error.user_agent,
+ reasoning_effort: error.reasoning_effort,
+ reasoning_preset: error.reasoning_preset,
+ request_kind: error.request_kind,
+ subagent_kind: error.subagent_kind,
+ compact: error.compact,
message: error.message,
metadata: OpsErrorMetadataView {
source: error.source,
diff --git a/backend/crates/gateway-api/src/admin/observability/query.rs b/backend/crates/gateway-api/src/admin/observability/query.rs
index 07bb18aaa..40fa1abba 100644
--- a/backend/crates/gateway-api/src/admin/observability/query.rs
+++ b/backend/crates/gateway-api/src/admin/observability/query.rs
@@ -351,7 +351,8 @@ pub(crate) fn ops_command(query: &OpsQuery) -> Result,
pub client_api_key_id: Option,
pub kind: String,
+ pub operation: String,
+ pub protocol: Option,
+ pub client_transport: Option,
pub provider: Option,
pub authentication_kind: Option,
pub account_id: Option,
+ pub account_name: Option,
+ pub account_email: Option,
pub route: String,
pub model: Option,
+ pub requested_model: Option,
+ pub upstream_model: Option,
+ pub service_tier: Option,
pub client_status_code: Option,
pub upstream_status_code: Option,
pub transport: Option,
pub attempt_index: Option,
pub failure_class: String,
+ pub provider_error_code: Option,
+ pub occurrence_count: u32,
pub response_id: Option,
pub upstream_request_id: Option,
pub latency_ms: Option,
+ pub client_ip: Option,
+ pub user_agent: Option,
+ pub reasoning_effort: Option,
+ pub reasoning_preset: Option,
+ pub request_kind: Option,
+ pub subagent_kind: Option,
+ pub compact: Option,
pub message: String,
pub metadata: OpsErrorMetadataView,
pub created_at: DateTime,
diff --git a/backend/crates/gateway-api/tests/admin/observability/response.rs b/backend/crates/gateway-api/tests/admin/observability/response.rs
index a3e020fd7..fc4fd84d7 100644
--- a/backend/crates/gateway-api/tests/admin/observability/response.rs
+++ b/backend/crates/gateway-api/tests/admin/observability/response.rs
@@ -539,6 +539,10 @@ 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(),
+ protocol: Some("openai".to_owned()),
+ client_transport: Some("http_sse".to_owned()),
+ requested_model_id: Some("gpt-5.4".to_owned()),
+ service_tier: Some("priority".to_owned()),
endpoint: Some("/v1/responses".to_owned()),
provider_kind: Some("openai".to_owned()),
provider_account_ref: Some("acct_err".to_owned()),
@@ -556,6 +560,13 @@ async fn ops_errors_should_keep_account_label_and_authentication_contract() {
latency_ms: Some(90),
message: "snapshot error".to_owned(),
occurrence_count: 1,
+ client_ip: Some("203.0.113.8".to_owned()),
+ user_agent: Some("codex-cli/0.144.0".to_owned()),
+ reasoning_effort: Some("medium".to_owned()),
+ reasoning_preset: None,
+ request_kind: Some("root".to_owned()),
+ subagent_kind: None,
+ compact: Some(false),
occurred_at: Utc::now(),
stable_sort_id: "model_request:req_err".to_owned(),
});
@@ -585,6 +596,11 @@ async fn ops_errors_should_keep_account_label_and_authentication_contract() {
"accountLabel": value["data"]["items"][0]["metadata"]["accountLabel"],
"clientStatusCode": value["data"]["items"][0]["clientStatusCode"],
"route": value["data"]["items"][0]["route"],
+ "requestedModel": value["data"]["items"][0]["requestedModel"],
+ "clientTransport": value["data"]["items"][0]["clientTransport"],
+ "clientIp": value["data"]["items"][0]["clientIp"],
+ "userAgent": value["data"]["items"][0]["userAgent"],
+ "providerErrorCode": value["data"]["items"][0]["providerErrorCode"],
}),
serde_json::json!({
"provider": "openai",
@@ -594,6 +610,11 @@ async fn ops_errors_should_keep_account_label_and_authentication_contract() {
"accountLabel": "err@example.invalid",
"clientStatusCode": 502,
"route": "/v1/responses",
+ "requestedModel": "gpt-5.4",
+ "clientTransport": "http_sse",
+ "clientIp": "203.0.113.8",
+ "userAgent": "codex-cli/0.144.0",
+ "providerErrorCode": "upstream",
})
);
}
diff --git a/backend/crates/gateway-store/src/postgres/observability/mapping.rs b/backend/crates/gateway-store/src/postgres/observability/mapping.rs
index eb1010274..4945a87d9 100644
--- a/backend/crates/gateway-store/src/postgres/observability/mapping.rs
+++ b/backend/crates/gateway-store/src/postgres/observability/mapping.rs
@@ -51,6 +51,7 @@ pub(crate) fn store_ops_error_filter(
provider_account_ref: filter.provider_account_ref,
provider_kind: filter.provider_kind,
operation: filter.operation,
+ endpoint: filter.endpoint,
model: filter.model,
transport: filter.transport,
attempt_index: filter.attempt_index,
@@ -700,6 +701,10 @@ pub(crate) fn admin_ops_error(error: OpsErrorRecord) -> admin_observability::Ops
client_api_key_ref: error.client_api_key_ref,
component: error.component,
operation: error.operation,
+ protocol: error.protocol,
+ client_transport: error.client_transport,
+ requested_model_id: error.requested_model_id,
+ service_tier: error.service_tier,
endpoint: error.endpoint,
provider_kind: error.provider_kind,
provider_account_ref: error.provider_account_ref,
@@ -717,6 +722,13 @@ pub(crate) fn admin_ops_error(error: OpsErrorRecord) -> admin_observability::Ops
latency_ms: error.latency_ms,
message: error.message,
occurrence_count: error.occurrence_count,
+ client_ip: error.client_ip,
+ user_agent: error.user_agent,
+ reasoning_effort: error.reasoning_effort,
+ reasoning_preset: error.reasoning_preset,
+ request_kind: error.request_kind,
+ subagent_kind: error.subagent_kind,
+ compact: error.compact,
occurred_at: error.occurred_at,
stable_sort_id: error.stable_sort_id,
}
@@ -866,6 +878,10 @@ pub(crate) fn ops_error_from_row(row: &sqlx::postgres::PgRow) -> StoreResult StoreResult,
pub provider_kind: Option,
pub operation: Option,
+ pub endpoint: Option,
pub model: Option,
pub transport: Option,
pub attempt_index: Option,
@@ -147,6 +148,7 @@ impl OpsErrorFilter {
),
(self.provider_kind.as_deref(), "provider filter"),
(self.operation.as_deref(), "operation filter"),
+ (self.endpoint.as_deref(), "endpoint filter"),
(self.model.as_deref(), "model filter"),
(self.transport.as_deref(), "transport filter"),
(
@@ -694,6 +696,10 @@ pub struct OpsErrorRecord {
pub client_api_key_ref: Option,
pub component: String,
pub operation: String,
+ pub protocol: Option,
+ pub client_transport: Option,
+ pub requested_model_id: Option,
+ pub service_tier: Option,
pub endpoint: Option,
pub provider_kind: Option,
pub provider_account_ref: Option,
@@ -711,6 +717,13 @@ pub struct OpsErrorRecord {
pub latency_ms: Option,
pub message: String,
pub occurrence_count: u32,
+ pub client_ip: Option,
+ pub user_agent: Option,
+ pub reasoning_effort: Option,
+ pub reasoning_preset: Option,
+ pub request_kind: Option,
+ pub subagent_kind: Option,
+ pub compact: Option,
pub occurred_at: DateTime,
pub stable_sort_id: String,
}
diff --git a/backend/crates/gateway-store/src/postgres/observability/queries/ops.rs b/backend/crates/gateway-store/src/postgres/observability/queries/ops.rs
index b23be3531..99a8e006b 100644
--- a/backend/crates/gateway-store/src/postgres/observability/queries/ops.rs
+++ b/backend/crates/gateway-store/src/postgres/observability/queries/ops.rs
@@ -6,6 +6,7 @@ const REQUEST_ERROR_SELECT: &str = "select 'model_request'::text as source,
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.protocol, mr.client_transport, mr.requested_model_id, mr.service_tier,
mr.endpoint, mr.provider_kind, mr.provider_account_ref,
mr.provider_account_name_snapshot as provider_account_name,
mr.provider_account_email_snapshot as provider_account_email,
@@ -17,7 +18,11 @@ const REQUEST_ERROR_SELECT: &str = "select 'model_request'::text as source,
mr.provider_error_code, mr.client_response_id, mr.upstream_request_id,
mr.latency_ms,
coalesce(mr.error_message, mr.error_kind, 'request failed') as message,
- 1::integer as occurrence_count, mr.completed_at as occurred_at,
+ 1::integer as occurrence_count,
+ mr.client_ip::text as client_ip, mr.user_agent,
+ mr.reasoning_effort, mr.reasoning_preset, mr.request_kind,
+ mr.subagent_kind, mr.compact,
+ mr.completed_at as occurred_at,
'model_request:' || mr.id as stable_sort_id
from model_requests mr
where mr.outcome = 'failed'";
@@ -25,7 +30,8 @@ where mr.outcome = 'failed'";
const OPS_EVENT_SELECT: &str = "select 'ops_event'::text as source,
oe.id as event_id, oe.model_request_id as 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,
+ mr.protocol, mr.client_transport, mr.requested_model_id, mr.service_tier,
+ mr.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,
oe.provider_account_authentication_kind_snapshot
@@ -33,7 +39,11 @@ const OPS_EVENT_SELECT: &str = "select 'ops_event'::text as source,
oe.upstream_model_id, null::text as upstream_transport, oe.failure_kind,
null::integer as client_status_code, oe.status_code as upstream_status_code,
oe.provider_error_code, mr.client_response_id, oe.upstream_request_id,
- oe.latency_ms, oe.message, oe.occurrence_count, oe.created_at as occurred_at,
+ oe.latency_ms, oe.message, oe.occurrence_count,
+ mr.client_ip::text as client_ip, mr.user_agent,
+ mr.reasoning_effort, mr.reasoning_preset, mr.request_kind,
+ mr.subagent_kind, mr.compact,
+ oe.created_at as occurred_at,
'ops_event:' || oe.id as stable_sort_id
from ops_events oe
left join model_requests mr on mr.id = oe.model_request_id
@@ -107,6 +117,7 @@ fn push_request_error_predicates(
("mr.provider_account_ref", &filter.provider_account_ref),
("mr.provider_kind", &filter.provider_kind),
("mr.operation", &filter.operation),
+ ("mr.endpoint", &filter.endpoint),
("mr.upstream_transport", &filter.transport),
("mr.upstream_request_id", &filter.upstream_request_id),
] {
@@ -153,6 +164,7 @@ fn push_ops_event_predicates(
("oe.provider_account_ref", &filter.provider_account_ref),
("oe.provider_kind", &filter.provider_kind),
("oe.operation", &filter.operation),
+ ("mr.endpoint", &filter.endpoint),
("oe.upstream_request_id", &filter.upstream_request_id),
("oe.failure_kind", &filter.failure_kind),
] {
diff --git a/backend/crates/gateway-store/tests/postgres/observability.rs b/backend/crates/gateway-store/tests/postgres/observability.rs
index b5c0d4735..51f630116 100644
--- a/backend/crates/gateway-store/tests/postgres/observability.rs
+++ b/backend/crates/gateway-store/tests/postgres/observability.rs
@@ -761,6 +761,8 @@ async fn admin_observability_adapter_preserves_utc_queries_metrics_costs_and_det
request_id: Some("req_observe_failed".to_owned()),
provider_kind: Some("openai".to_owned()),
provider_account_ref: Some("acct_observe".to_owned()),
+ operation: Some("responses".to_owned()),
+ endpoint: Some("/v1/responses".to_owned()),
model: Some("upstream-model".to_owned()),
failure_kind: Some("rate_limited".to_owned()),
status_code: Some(429),
@@ -1191,6 +1193,15 @@ async fn observability_queries_preserve_request_account_cost_and_diagnostic_fact
.expect("request error");
assert_eq!(request_error.client_status_code, Some(502));
assert_eq!(request_error.upstream_status_code, Some(429));
+ assert_eq!(request_error.client_ip.as_deref(), Some("203.0.113.9"));
+ assert_eq!(
+ request_error.user_agent.as_deref(),
+ Some("codex-cli/0.144.0")
+ );
+ assert_eq!(
+ request_error.requested_model_id.as_deref(),
+ Some("public-model")
+ );
let attempt_error = errors
.items
.iter()
@@ -1198,6 +1209,12 @@ async fn observability_queries_preserve_request_account_cost_and_diagnostic_fact
.expect("attempt error");
assert_eq!(attempt_error.client_status_code, None);
assert_eq!(attempt_error.upstream_status_code, Some(429));
+ assert_eq!(attempt_error.endpoint.as_deref(), Some("/v1/responses"));
+ assert_eq!(attempt_error.client_ip.as_deref(), Some("203.0.113.9"));
+ assert_eq!(
+ attempt_error.user_agent.as_deref(),
+ Some("codex-cli/0.144.0")
+ );
database.close().await;
}
@@ -1286,7 +1303,7 @@ async fn seed_observability_facts(
sqlx::query(
"insert into model_requests (
id, client_api_key_ref, config_revision, protocol, operation, endpoint,
- client_transport, requested_model_id,
+ client_transport, requested_model_id, service_tier,
provider_kind, provider_account_id,
provider_account_ref, upstream_model_id, upstream_transport, attempt_count,
provider_account_name_snapshot, provider_account_email_snapshot,
@@ -1295,16 +1312,20 @@ async fn seed_observability_facts(
error_kind, provider_error_code, error_message, retry_after_ms,
input_tokens, cached_tokens, image_generation_requested,
image_generation_succeeded, cost_source, latency_ms,
+ client_ip, user_agent, reasoning_effort, reasoning_preset,
+ request_kind, subagent_kind, compact,
started_at, deadline_at, completed_at,
routing_scope, routing_group_refs, routing_group_names_snapshot
) values (
'req_observe_failed', 'key_observe', 1, 'openai', 'responses', '/v1/responses',
- 'http_sse', 'public-model', 'openai', 'acct_observe',
+ 'http_sse', 'public-model', 'priority', 'openai', 'acct_observe',
'acct_observe', 'upstream-model',
'http_sse', 2,
'primary', 'account@example.invalid', 'oauth',
'sent', 'failed', 502, 429, 'rate_limited', 'rate_limit',
'upstream limited', 1000, 0, 0, true, false, 'unavailable', 700,
+ '203.0.113.9', 'codex-cli/0.144.0', 'medium', null,
+ 'root', null, false,
$1 - interval '10 minutes', $1 + interval '20 minutes', $1 - interval '9 minutes',
'all', '{}'::text[], '[]'::jsonb
)",
diff --git a/frontend/src/api/modules/usage.ts b/frontend/src/api/modules/usage.ts
index 84bc2766b..469ed22e1 100644
--- a/frontend/src/api/modules/usage.ts
+++ b/frontend/src/api/modules/usage.ts
@@ -211,19 +211,36 @@ export interface OpsError {
requestId: string | null
clientApiKeyId: string | null
kind: string
+ operation: string
+ protocol: string | null
+ clientTransport: string | null
provider: string | null
authenticationKind: string | null
accountId: string | null
+ accountName: string | null
+ accountEmail: string | null
route: string
model: string | null
+ requestedModel: string | null
+ upstreamModel: string | null
+ serviceTier: string | null
clientStatusCode: number | null
upstreamStatusCode: number | null
transport: string | null
attemptIndex: number | null
failureClass: string
+ providerErrorCode: string | null
+ occurrenceCount: number
responseId: string | null
upstreamRequestId: string | null
latencyMs: number | null
+ clientIp: string | null
+ userAgent: string | null
+ reasoningEffort: string | null
+ reasoningPreset: string | null
+ requestKind: string | null
+ subagentKind: string | null
+ compact: boolean | null
message: string
metadata: OpsErrorMetadata
createdAt: string
diff --git a/frontend/src/views/usage/components/OpsErrorDetailModal.vue b/frontend/src/views/usage/components/OpsErrorDetailModal.vue
index 3dab19be7..9e1b3ea9b 100644
--- a/frontend/src/views/usage/components/OpsErrorDetailModal.vue
+++ b/frontend/src/views/usage/components/OpsErrorDetailModal.vue
@@ -6,6 +6,7 @@ import BaseModal from '@/components/base/BaseModal/index.vue'
import { displayValue } from '../utils/detail'
import { presentOpsError } from '../utils/opsErrorPresentation'
import UsageDetailCodePanel from './UsageDetailCodePanel.vue'
+import UsageDetailFieldGrid from './UsageDetailFieldGrid.vue'
import UsageStatusCodeBadge from './UsageStatusCodeBadge.vue'
const props = defineProps<{
@@ -16,23 +17,52 @@ const open = defineModel({ default: false })
const presentation = computed(() => props.record ? presentOpsError(props.record) : null)
-const fields = computed(() => [
- { label: '账号', value: props.record?.metadata.accountLabel },
- { label: '时间', value: props.record?.createdAtDisplay },
+const panelClass = 'min-w-0 rounded-cp-card bg-cp-fill-quaternary px-4 py-3.5'
+const panelTitleClass = 'm-0 text-cp-sm leading-none font-heavy text-cp-text-secondary'
+
+const accountLabel = computed(() => props.record?.accountEmail
+ || props.record?.accountName
+ || props.record?.metadata.accountLabel
+ || props.record?.accountId)
+
+const requestFields = computed(() => [
+ { label: '客户端 IP', value: props.record?.clientIp, mono: true },
+ { label: 'User-Agent', value: props.record?.userAgent, mono: true, wrap: true },
+ { label: '协议', value: props.record?.protocol, mono: true },
+ { label: '客户端传输', value: props.record?.clientTransport, mono: true },
+ { label: '客户端 Key ID', value: props.record?.clientApiKeyId, mono: true },
+ { label: '请求 ID', value: props.record?.requestId, mono: true },
+ { label: '响应 ID', value: props.record?.responseId, mono: true },
+ { label: '上游请求 ID', value: props.record?.upstreamRequestId, mono: true },
+])
+
+const routeFields = computed(() => [
+ { label: '端点', value: props.record?.route, mono: true },
+ { label: '请求模型', value: props.record?.requestedModel, mono: true },
+ { label: '上游模型', value: props.record?.upstreamModel, mono: true },
+ { label: '记录模型', value: props.record?.model, mono: true },
+ { label: '推理强度', value: props.record?.reasoningEffort, mono: true },
+ { label: '推理预设', value: props.record?.reasoningPreset, mono: true },
+ { label: '请求类型', value: props.record?.requestKind, mono: true },
+ { label: '子代理类型', value: props.record?.subagentKind, mono: true },
+ { label: '压缩请求', value: booleanDisplay(props.record?.compact) },
+ { label: '服务档位', value: props.record?.serviceTier, mono: true },
+ { label: '上游传输', value: props.record?.transport, mono: true },
+ { label: '尝试序号', value: props.record?.attemptIndex },
+])
+
+const diagnosticFields = computed(() => [
+ { label: '账号', value: accountLabel.value, mono: true },
+ { label: '时间', value: props.record?.createdAtDisplay, mono: true },
{ label: '记录来源', value: presentation.value?.sourceLabel },
{ label: '组件', value: presentation.value?.componentLabel },
+ { label: '操作/触发', value: props.record?.operation, mono: true },
{ label: '失败分类', value: presentation.value?.failureClassLabel },
{ label: '平台/类型', value: providerKindLabel(props.record) },
- { label: '端点', value: props.record?.route },
- { label: '模型', value: props.record?.model },
- { label: '账号 ID', value: props.record?.accountId },
- { label: '客户端 Key ID', value: props.record?.clientApiKeyId },
- { label: '传输方式', value: props.record?.transport },
- { label: '尝试序号', value: props.record?.attemptIndex },
- { label: '耗时', value: latencyDisplay(props.record?.latencyMs) },
- { label: '请求 ID', value: props.record?.requestId, wide: true },
- { label: '响应 ID', value: props.record?.responseId, wide: true },
- { label: '上游请求 ID', value: props.record?.upstreamRequestId, wide: true },
+ { label: '账号 ID', value: props.record?.accountId, mono: true },
+ { label: 'Provider 错误码', value: props.record?.providerErrorCode, mono: true },
+ { label: '聚合次数', value: props.record?.occurrenceCount },
+ { label: '耗时', value: latencyDisplay(props.record?.latencyMs), mono: true },
])
const metadataText = computed(() => {
@@ -46,6 +76,14 @@ function latencyDisplay(value: unknown) {
return typeof value === 'number' ? `${value} ms` : '—'
}
+function booleanDisplay(value: boolean | null | undefined) {
+ if (value === true)
+ return '是'
+ if (value === false)
+ return '否'
+ return '—'
+}
+
function providerKindLabel(record: OpsError | null) {
if (!record?.provider)
return '—'
@@ -59,7 +97,7 @@ function providerKindLabel(record: OpsError | null) {
@@ -79,34 +117,37 @@ function providerKindLabel(record: OpsError | null) {
-
-
-
-
- {{ field.label }}
-
- -
- {{ displayValue(field.value) }}
-
-
-
+
+
-
+
+
+
+
diff --git a/frontend/src/views/usage/components/OpsErrorPanel.vue b/frontend/src/views/usage/components/OpsErrorPanel.vue
index c3b294b67..2d4f53e71 100644
--- a/frontend/src/views/usage/components/OpsErrorPanel.vue
+++ b/frontend/src/views/usage/components/OpsErrorPanel.vue
@@ -8,11 +8,13 @@ import BaseIconButton from '@/components/base/BaseIconButton.vue'
import BaseInput from '@/components/base/BaseInput.vue'
import BaseTablePagination from '@/components/base/BaseTable/BaseTablePagination.vue'
import BaseTable from '@/components/base/BaseTable/index.vue'
+import ProviderIconGroup from '@/components/ProviderIconGroup.vue'
import { useOpsErrorsTable } from '../composables/useOpsErrorsTable'
import { opsErrorColumns } from '../constants'
import { presentOpsError } from '../utils/opsErrorPresentation'
import OpsErrorDetailModal from './OpsErrorDetailModal.vue'
import UsageStatusCodeBadge from './UsageStatusCodeBadge.vue'
+import UsageTransportBadge from './UsageTransportBadge.vue'
const props = defineProps<{
timeRangeParams: UsageTimeRangeParams
@@ -46,6 +48,30 @@ function failureClassText(record: OpsError) {
function errorSummary(record: OpsError) {
return presentOpsError(record).summary
}
+
+function accountText(record: OpsError) {
+ return record.accountEmail
+ || record.accountName
+ || record.metadata.accountLabel
+ || record.accountId
+ || '—'
+}
+
+function modelText(record: OpsError) {
+ return record.requestedModel || record.model || record.upstreamModel || '—'
+}
+
+function modelTitle(record: OpsError) {
+ const requested = record.requestedModel
+ const upstream = record.upstreamModel
+ return requested && upstream && requested !== upstream
+ ? `${requested} → ${upstream}`
+ : modelText(record)
+}
+
+function reasoningText(record: OpsError) {
+ return record.reasoningPreset || record.reasoningEffort || '—'
+}
@@ -111,6 +137,12 @@ function errorSummary(record: OpsError) {
{{ failureClassText(row) }}
+
+
+
{{ errorSummary(row) }}
@@ -119,9 +151,41 @@ function errorSummary(record: OpsError) {
- {{ row.metadata.accountLabel || '—' }}
+ {{ accountText(row) }}
+
+
+
+
+ {{ modelText(row) }}
+
+
+
+
+ {{ reasoningText(row) }}
+
+
+
+
+
+
+
+
+
+
+ {{ row.clientIp || '—' }}
+
+
+
+
+ {{ row.userAgent || '—' }}
diff --git a/frontend/src/views/usage/constants.ts b/frontend/src/views/usage/constants.ts
index f2ecf6cc6..a0883b6c3 100644
--- a/frontend/src/views/usage/constants.ts
+++ b/frontend/src/views/usage/constants.ts
@@ -35,13 +35,19 @@ export const usageRecordColumns = defineTableColumns([
export const opsErrorColumns = defineTableColumns([
{ key: 'accountId', label: '账号', kind: 'mono', size: '3xl' },
+ { key: 'provider', label: '平台/类型', kind: 'custom', size: 'sm' },
{ key: 'createdAtDisplay', label: '时间', kind: 'datetime' },
{ key: 'upstreamStatusCode', label: '上游状态', kind: 'status', size: 'sm' },
{ key: 'clientStatusCode', label: '客户端状态', kind: 'status', size: 'md' },
{ key: 'failureClass', label: '失败分类', kind: 'mono' },
{ key: 'kind', label: '事件', kind: 'mono' },
{ key: 'route', label: '端点', kind: 'mono' },
- { key: 'model', label: '模型', kind: 'mono' },
+ { key: 'model', label: '模型', kind: 'custom', size: 'xl' },
+ { key: 'reasoningEffort', label: '推理强度', kind: 'custom' },
+ { key: 'transport', label: '上游', kind: 'custom', size: 'md' },
+ { key: 'clientTransport', label: '接入', kind: 'custom', size: 'md' },
+ { key: 'clientIp', label: 'IP', kind: 'custom', size: '3xl' },
+ { key: 'userAgent', label: 'User-Agent', kind: 'custom', size: '4xl' },
{ key: 'requestId', label: '请求 ID', kind: 'mono', size: '2xl' },
{ key: 'message', label: '错误摘要', kind: 'custom', size: '3xl' },
{ key: 'actions', label: '操作', kind: 'actions', size: 'sm' },