diff --git a/backend/crates/gateway-api/src/admin/observability/presenter.rs b/backend/crates/gateway-api/src/admin/observability/presenter.rs index b1738940a..2e87850c9 100644 --- a/backend/crates/gateway-api/src/admin/observability/presenter.rs +++ b/backend/crates/gateway-api/src/admin/observability/presenter.rs @@ -250,10 +250,6 @@ pub(crate) fn usage_record_view(record: domain::UsageRecord) -> UsageRecordView .upstream_model_id .clone() .or_else(|| record.requested_model_id.clone()); - let transport = record - .upstream_transport - .clone() - .or_else(|| Some(record.client_transport.clone())); let metadata = provider_metadata_fields(record.provider_metadata_json.as_deref()); UsageRecordView { id: record.id.clone(), @@ -274,7 +270,8 @@ pub(crate) fn usage_record_view(record: domain::UsageRecord) -> UsageRecordView upstream_model: record.upstream_model_id, service_tier: record.service_tier, status_code, - transport, + client_transport: record.client_transport, + upstream_transport: record.upstream_transport, protocol: record.protocol.clone(), http_version: record.http_version.clone(), client_status_code: record.client_status_code.map(i64::from), @@ -349,6 +346,8 @@ pub(crate) fn provider_metadata_fields(value: Option<&str>) -> BTreeMap, pub service_tier: Option, pub status_code: Option, - pub transport: Option, + pub client_transport: String, + pub upstream_transport: Option, pub protocol: String, #[serde(skip_serializing_if = "Option::is_none")] pub http_version: Option, diff --git a/backend/crates/gateway-api/tests/admin/observability/response.rs b/backend/crates/gateway-api/tests/admin/observability/response.rs index 038f00d3b..63b3a2c17 100644 --- a/backend/crates/gateway-api/tests/admin/observability/response.rs +++ b/backend/crates/gateway-api/tests/admin/observability/response.rs @@ -835,7 +835,7 @@ async fn usage_route_should_expose_image_and_websocket_facts() { protocol: "openai".to_owned(), operation: "generate".to_owned(), endpoint: "/v1/responses".to_owned(), - client_transport: "http_sse".to_owned(), + client_transport: "websocket".to_owned(), requested_model_id: Some("grok-4.5".to_owned()), provider_kind: Some("xai".to_owned()), provider_account_ref: Some("acct_snapshot".to_owned()), @@ -981,7 +981,8 @@ async fn usage_route_should_expose_image_and_websocket_facts() { "requestKind": value["data"]["items"][0]["requestKind"], "subagentKind": value["data"]["items"][0]["subagentKind"], "compact": value["data"]["items"][0]["compact"], - "transport": value["data"]["items"][0]["transport"], + "clientTransport": value["data"]["items"][0]["clientTransport"], + "upstreamTransport": value["data"]["items"][0]["upstreamTransport"], "transportDecisionWaitMs": value["data"]["items"][0]["latencyDetails"]["transportDecisionWaitMs"], "wsConnectMs": value["data"]["items"][0]["latencyDetails"]["wsConnectMs"], "firstReasoningMs": value["data"]["items"][0]["latencyDetails"]["firstReasoningMs"], @@ -1014,7 +1015,8 @@ async fn usage_route_should_expose_image_and_websocket_facts() { "requestKind": "review", "subagentKind": "worker", "compact": true, - "transport": "http_sse", + "clientTransport": "websocket", + "upstreamTransport": "http_sse", "transportDecisionWaitMs": 7, "wsConnectMs": 11, "firstReasoningMs": 19, diff --git a/backend/crates/gateway-store/src/postgres/observability/queries/usage.rs b/backend/crates/gateway-store/src/postgres/observability/queries/usage.rs index bd695e5ed..206812025 100644 --- a/backend/crates/gateway-store/src/postgres/observability/queries/usage.rs +++ b/backend/crates/gateway-store/src/postgres/observability/queries/usage.rs @@ -92,9 +92,11 @@ pub(crate) fn push_usage_filter( /// `model_requests` 同时承担执行审计:包括上游已发送、但尚未收到首个事件就断开的 /// WebSocket 请求。这些失败仍必须留给 Ops Errors 和调度健康度分析,不能混入用量、 /// 成本、账号使用次数或请求明细。 +/// 客户端 WebSocket 的每个 `response.create` 没有独立 HTTP 状态码;成功终态与下游 +/// 提交边界已足以证明交付,连接握手的 101 不能冒充单次请求状态。 pub(crate) fn completed_usage_fact_predicate(alias: &str) -> String { format!( - "{alias}.outcome = 'succeeded' and {alias}.downstream_committed_at is not null and {alias}.client_status_code between 200 and 399" + "{alias}.outcome = 'succeeded' and {alias}.downstream_committed_at is not null and (({alias}.client_transport = 'websocket' and {alias}.client_status_code is null) or {alias}.client_status_code between 200 and 399)" ) } diff --git a/backend/crates/gateway-store/tests/postgres/snapshots.rs b/backend/crates/gateway-store/tests/postgres/snapshots.rs index 346441d71..352415e01 100644 --- a/backend/crates/gateway-store/tests/postgres/snapshots.rs +++ b/backend/crates/gateway-store/tests/postgres/snapshots.rs @@ -9,6 +9,62 @@ use sqlx::PgPool; use super::TestDatabase; +#[tokio::test] +async fn usage_facts_should_accept_statusless_websocket_but_reject_statusless_http() { + let Some(database) = TestDatabase::create("snapshot_statusless_websocket_usage").await else { + return; + }; + let started_at = Utc::now(); + seed_account( + &database.pool, + "acct_statusless_transport", + "Statusless transport", + None, + "oauth", + started_at, + ) + .await; + let store = PgExecutionStore::new(database.pool.clone()); + + let mut websocket_request = new_request("req_statusless_websocket", started_at); + websocket_request.client_transport = "websocket".to_owned(); + store + .insert_model_request_with_first_attempt( + websocket_request, + attempt("req_statusless_websocket", 1, "acct_statusless_transport"), + ) + .await + .expect("insert statusless WebSocket request"); + store + .insert_model_request_with_first_attempt( + new_request("req_statusless_http", started_at), + attempt("req_statusless_http", 1, "acct_statusless_transport"), + ) + .await + .expect("insert statusless HTTP request"); + finalize_request_without_client_status(&database.pool, "req_statusless_websocket", started_at) + .await; + finalize_request_without_client_status(&database.pool, "req_statusless_http", started_at).await; + + let repository = PgObservabilityRepository::new(database.pool.clone(), None); + let page = repository + .list_usage_records(usage_query(started_at, UsageRecordFilter::default())) + .await + .expect("list statusless transport usage records"); + let request_ids = page + .items + .iter() + .map(|record| record.id.as_str()) + .collect::>(); + + assert_eq!( + (page.total, request_ids), + (1, vec!["req_statusless_websocket"]) + ); + + database.close().await; +} + #[tokio::test] async fn request_snapshots_should_survive_account_deletion() { let Some(database) = TestDatabase::create("snapshot_request_delete").await else { @@ -767,6 +823,25 @@ async fn finalize_request(pool: &PgPool, id: &str, started_at: DateTime) { .expect("finalize request"); } +async fn finalize_request_without_client_status( + pool: &PgPool, + id: &str, + started_at: DateTime, +) { + sqlx::query( + "update model_requests + set outcome = 'succeeded', upstream_send_state = 'sent', + client_status_code = null, upstream_status_code = 200, + downstream_committed_at = $2, completed_at = $2 + where id = $1", + ) + .bind(id) + .bind(started_at + chrono::Duration::seconds(5)) + .execute(pool) + .await + .expect("finalize request without client status"); +} + async fn seed_api_key(pool: &PgPool, id: &str, name: &str, now: DateTime) { sqlx::query( "insert into client_api_keys (id, name, key, enabled, created_at, updated_at) diff --git a/frontend/src/api/modules/usage.ts b/frontend/src/api/modules/usage.ts index 9f7ede515..cc3775fc7 100644 --- a/frontend/src/api/modules/usage.ts +++ b/frontend/src/api/modules/usage.ts @@ -89,7 +89,8 @@ export interface UsageRecord { upstreamModel: string | null serviceTier: string | null statusCode: number | null - transport: string | null + clientTransport: string + upstreamTransport: string | null protocol: string httpVersion: string | null clientStatusCode: number | null diff --git a/frontend/src/components/base/BaseSwitch.vue b/frontend/src/components/base/BaseSwitch.vue index a19f99d04..090f2bb35 100644 --- a/frontend/src/components/base/BaseSwitch.vue +++ b/frontend/src/components/base/BaseSwitch.vue @@ -6,22 +6,55 @@ const props = withDefaults( label: string disabled?: boolean showLabel?: boolean + activeText?: string + inactiveText?: string + inlinePrompt?: boolean + width?: string | number }>(), { disabled: false, showLabel: false, + activeText: '', + inactiveText: '', + inlinePrompt: false, + width: '', }, ) const model = defineModel({ default: false }) +const hasStateText = computed(() => Boolean(props.activeText || props.inactiveText)) +const inlineText = computed(() => model.value ? props.activeText : props.inactiveText) +const trackStyle = computed(() => props.width === '' + ? undefined + : { + width: typeof props.width === 'number' || /^\d+(?:\.\d+)?$/.test(props.width) + ? `${props.width}px` + : props.width, + }) const trackClasses = computed(() => [ - 'relative inline-flex h-6 w-11 shrink-0 items-center rounded-full p-0.5 transition-[background-color,box-shadow] duration-180 motion-reduce:transition-none', + 'relative inline-flex h-6 min-w-11 shrink-0 items-center rounded-full p-0.5 transition-[background-color,box-shadow] duration-180 motion-reduce:transition-none', props.disabled ? 'bg-cp-bg-container-disabled shadow-none' : model.value - ? 'bg-cp-primary shadow-cp-tertiary group-hover:bg-cp-primary-hover' - : 'bg-cp-fill-tertiary shadow-cp-tertiary group-hover:bg-cp-bg-text-active', + ? 'bg-cp-primary shadow-cp-tertiary' + : 'bg-cp-fill-tertiary shadow-cp-tertiary', +]) +const thumbClasses = computed(() => [ + 'absolute top-0.5 z-10 size-5 rounded-full bg-cp-white shadow-cp-tertiary transition-[left] duration-180 ease-out motion-reduce:transition-none', + model.value ? 'left-[calc(100%-1.375rem)]' : 'left-0.5', +]) +const inlineTextClasses = computed(() => [ + 'pointer-events-none absolute inset-0 flex min-w-0 select-none items-center justify-center overflow-hidden text-[10px] leading-none font-heavy text-cp-white transition-[padding] duration-180 motion-reduce:transition-none', + model.value ? 'pr-6 pl-1' : 'pr-1 pl-6', +]) +const inactiveTextClasses = computed(() => [ + 'text-cp leading-none font-emphasis transition-colors duration-150 motion-reduce:transition-none', + model.value ? 'text-cp-text-secondary' : 'text-cp-primary-text', +]) +const activeTextClasses = computed(() => [ + 'text-cp leading-none font-emphasis transition-colors duration-150 motion-reduce:transition-none', + model.value ? 'text-cp-primary-text' : 'text-cp-text-secondary', ]) @@ -29,9 +62,12 @@ const trackClasses = computed(() => [ diff --git a/frontend/src/views/api-keys/components/ApiKeyUseModal.vue b/frontend/src/views/api-keys/components/ApiKeyUseModal.vue index 557c4be0f..e01dda302 100644 --- a/frontend/src/views/api-keys/components/ApiKeyUseModal.vue +++ b/frontend/src/views/api-keys/components/ApiKeyUseModal.vue @@ -7,6 +7,7 @@ import BaseIconButton from '@/components/base/BaseIconButton.vue' import BaseModal from '@/components/base/BaseModal/index.vue' import BaseScrollbar from '@/components/base/BaseScrollbar.vue' import BaseSegmented from '@/components/base/BaseSegmented.vue' +import BaseSwitch from '@/components/base/BaseSwitch.vue' const props = defineProps<{ apiKey: { @@ -23,6 +24,7 @@ const emit = defineEmits<{ const open = defineModel({ default: false }) const activePlatform = shallowRef('unix') +const websocketEnabled = shallowRef(true) const platformOptions = [ { label: 'macOS / Linux', value: 'unix', icon: Apple }, @@ -49,21 +51,20 @@ model_reasoning_effort = "max" service_tier = "default" disable_response_storage = true network_access = "enabled" -windows_wsl_setup_acknowledged = true [model_providers.OpenAI] name = "OpenAI" base_url = "${props.apiBaseUrl}" -wire_api = "responses" +wire_api = "responses"${websocketEnabled.value ? '\nsupports_websockets = true' : ''} requires_openai_auth = true -[features] +[features]${websocketEnabled.value ? '\nresponses_websockets_v2 = true' : ''} goals = true`, ) const visibleFiles = computed(() => [ - { path: configPath.value, content: codexConfigToml.value }, - { path: authPath.value, content: codexAuthJson.value }, + { path: configPath.value, content: codexConfigToml.value, scrollbarHeight: '360px' }, + { path: authPath.value, content: codexAuthJson.value, scrollbarHeight: undefined }, ]) @@ -75,8 +76,16 @@ const visibleFiles = computed(() => [ size="lg" >
-
+
+
@@ -101,6 +110,7 @@ const visibleFiles = computed(() => [
diff --git a/frontend/src/views/theme/fixtures/dashboard.ts b/frontend/src/views/theme/fixtures/dashboard.ts index 04d189629..195b3e15f 100644 --- a/frontend/src/views/theme/fixtures/dashboard.ts +++ b/frontend/src/views/theme/fixtures/dashboard.ts @@ -11,7 +11,8 @@ interface PreviewUsageRecordOptions { provider: 'openai' | 'xai' authenticationKind: 'oauth' | 'api_key' model: string - transport: 'http_sse' | 'websocket' | 'http' + clientTransport: 'http_sse' | 'websocket' | 'http_json' + upstreamTransport: 'http_sse' | 'websocket' reasoningEffort: string inputTokens: number outputTokens: number @@ -106,7 +107,7 @@ function previewHealthStatus(index: number): DashboardHealthStatus { function previewUsageRecord(options: PreviewUsageRecordOptions): UsageRecord { const totalTokens = options.inputTokens + options.outputTokens const reasoningTokens = Math.round(options.outputTokens * 0.32) - const stream = options.transport !== 'http' + const stream = options.clientTransport !== 'http_json' return { id: options.id, @@ -124,12 +125,13 @@ function previewUsageRecord(options: PreviewUsageRecordOptions): UsageRecord { upstreamModel: options.model, serviceTier: options.provider === 'openai' ? 'priority' : 'standard', statusCode: 200, - transport: options.transport, + clientTransport: options.clientTransport, + upstreamTransport: options.upstreamTransport, protocol: stream ? 'responses' : 'chat_completions', - httpVersion: options.transport === 'websocket' ? null : 'HTTP/2', - clientStatusCode: 200, - upstreamStatusCode: 200, - websocketPool: options.transport === 'websocket' ? { kind: 'warm' } : null, + httpVersion: options.upstreamTransport === 'websocket' ? null : 'HTTP/2', + clientStatusCode: options.clientTransport === 'websocket' ? null : 200, + upstreamStatusCode: options.upstreamTransport === 'websocket' ? null : 200, + websocketPool: options.upstreamTransport === 'websocket' ? { kind: 'warm' } : null, imageGenerationRequested: false, imageGenerationSucceeded: null, latencyDetails: { @@ -364,16 +366,16 @@ export const themeDashboardSummary: DashboardSummaryResponse = { }, ], usageRecords: [ - previewUsageRecord({ id: 'preview-01', accountEmail: 'relay@example.com', provider: 'openai', authenticationKind: 'oauth', model: 'gpt-5.6-codex', transport: 'http_sse', reasoningEffort: 'high', inputTokens: 18_420, outputTokens: 3_860, cachedTokens: 8_192, firstTokenMs: 486, latencyMs: 3_280, estimatedCost: '$0.0618', createdAt: '2026-08-23T14:42:16+08:00', createdAtDisplay: '14:42:16', clientIp: '10.24.8.16', userAgent: 'codex-cli/0.34.0' }), - previewUsageRecord({ id: 'preview-02', accountEmail: 'gateway@example.com', provider: 'xai', authenticationKind: 'oauth', model: 'grok-code-fast-1', transport: 'websocket', reasoningEffort: 'medium', inputTokens: 12_760, outputTokens: 2_140, cachedTokens: 4_096, firstTokenMs: 318, latencyMs: 2_460, estimatedCost: '$0.0286', createdAt: '2026-08-23T14:39:52+08:00', createdAtDisplay: '14:39:52', clientIp: '10.24.8.21', userAgent: 'grok-cli/0.7.3' }), - previewUsageRecord({ id: 'preview-03', accountEmail: 'control@example.com', provider: 'openai', authenticationKind: 'oauth', model: 'gpt-5.5', transport: 'http_sse', reasoningEffort: 'xhigh', inputTokens: 32_840, outputTokens: 6_720, cachedTokens: 16_384, firstTokenMs: 572, latencyMs: 5_840, estimatedCost: '$0.1042', createdAt: '2026-08-23T14:35:08+08:00', createdAtDisplay: '14:35:08', clientIp: '10.24.9.11', userAgent: 'codex-desktop/26.818.41705', compact: true }), - previewUsageRecord({ id: 'preview-04', accountEmail: 'batch@example.com', provider: 'openai', authenticationKind: 'api_key', model: 'gpt-5.2', transport: 'http', reasoningEffort: 'low', inputTokens: 4_860, outputTokens: 920, cachedTokens: 0, firstTokenMs: 264, latencyMs: 1_180, estimatedCost: '$0.0124', createdAt: '2026-08-23T14:31:44+08:00', createdAtDisplay: '14:31:44', clientIp: '10.24.12.9', userAgent: 'openai-node/6.4.0' }), - previewUsageRecord({ id: 'preview-05', accountEmail: 'analysis@example.com', provider: 'xai', authenticationKind: 'api_key', model: 'grok-4.20', transport: 'http_sse', reasoningEffort: 'high', inputTokens: 21_640, outputTokens: 4_380, cachedTokens: 7_168, firstTokenMs: 426, latencyMs: 4_120, estimatedCost: '$0.0731', createdAt: '2026-08-23T14:28:19+08:00', createdAtDisplay: '14:28:19', clientIp: '10.24.15.32', userAgent: 'curl/8.14.1' }), - previewUsageRecord({ id: 'preview-06', accountEmail: 'scheduler@example.com', provider: 'openai', authenticationKind: 'oauth', model: 'gpt-5.6-codex', transport: 'websocket', reasoningEffort: 'medium', inputTokens: 9_760, outputTokens: 1_840, cachedTokens: 3_072, firstTokenMs: 296, latencyMs: 2_020, estimatedCost: '$0.0248', createdAt: '2026-08-23T14:24:03+08:00', createdAtDisplay: '14:24:03', clientIp: '10.24.4.18', userAgent: 'codex-cli/0.34.0' }), - previewUsageRecord({ id: 'preview-07', accountEmail: 'review@example.com', provider: 'openai', authenticationKind: 'oauth', model: 'gpt-5.5', transport: 'http_sse', reasoningEffort: 'high', inputTokens: 15_280, outputTokens: 3_120, cachedTokens: 6_144, firstTokenMs: 448, latencyMs: 3_740, estimatedCost: '$0.0527', createdAt: '2026-08-23T14:18:37+08:00', createdAtDisplay: '14:18:37', clientIp: '10.24.18.7', userAgent: 'codex-desktop/26.818.41705' }), - previewUsageRecord({ id: 'preview-08', accountEmail: 'fallback@example.com', provider: 'xai', authenticationKind: 'oauth', model: 'grok-code-fast-1', transport: 'websocket', reasoningEffort: 'medium', inputTokens: 7_940, outputTokens: 1_460, cachedTokens: 2_048, firstTokenMs: 342, latencyMs: 1_920, estimatedCost: '$0.0194', createdAt: '2026-08-23T14:13:25+08:00', createdAtDisplay: '14:13:25', clientIp: '10.24.6.23', userAgent: 'grok-cli/0.7.3' }), - previewUsageRecord({ id: 'preview-09', accountEmail: 'desktop@example.com', provider: 'openai', authenticationKind: 'oauth', model: 'gpt-5.6-codex', transport: 'http_sse', reasoningEffort: 'xhigh', inputTokens: 28_420, outputTokens: 5_940, cachedTokens: 12_288, firstTokenMs: 618, latencyMs: 5_260, estimatedCost: '$0.0925', createdAt: '2026-08-23T14:07:11+08:00', createdAtDisplay: '14:07:11', clientIp: '10.24.7.40', userAgent: 'codex-desktop/26.818.41705', compact: true }), - previewUsageRecord({ id: 'preview-10', accountEmail: 'api@example.com', provider: 'openai', authenticationKind: 'api_key', model: 'gpt-5.2', transport: 'http', reasoningEffort: 'low', inputTokens: 3_620, outputTokens: 680, cachedTokens: 0, firstTokenMs: 238, latencyMs: 980, estimatedCost: '$0.0091', createdAt: '2026-08-23T13:58:46+08:00', createdAtDisplay: '13:58:46', clientIp: '10.24.3.14', userAgent: 'openai-python/1.99.0' }), + previewUsageRecord({ id: 'preview-01', accountEmail: 'relay@example.com', provider: 'openai', authenticationKind: 'oauth', model: 'gpt-5.6-codex', clientTransport: 'http_sse', upstreamTransport: 'websocket', reasoningEffort: 'high', inputTokens: 18_420, outputTokens: 3_860, cachedTokens: 8_192, firstTokenMs: 486, latencyMs: 3_280, estimatedCost: '$0.0618', createdAt: '2026-08-23T14:42:16+08:00', createdAtDisplay: '14:42:16', clientIp: '10.24.8.16', userAgent: 'codex-cli/0.34.0' }), + previewUsageRecord({ id: 'preview-02', accountEmail: 'gateway@example.com', provider: 'xai', authenticationKind: 'oauth', model: 'grok-code-fast-1', clientTransport: 'websocket', upstreamTransport: 'http_sse', reasoningEffort: 'medium', inputTokens: 12_760, outputTokens: 2_140, cachedTokens: 4_096, firstTokenMs: 318, latencyMs: 2_460, estimatedCost: '$0.0286', createdAt: '2026-08-23T14:39:52+08:00', createdAtDisplay: '14:39:52', clientIp: '10.24.8.21', userAgent: 'grok-cli/0.7.3' }), + previewUsageRecord({ id: 'preview-03', accountEmail: 'control@example.com', provider: 'openai', authenticationKind: 'oauth', model: 'gpt-5.5', clientTransport: 'http_sse', upstreamTransport: 'websocket', reasoningEffort: 'xhigh', inputTokens: 32_840, outputTokens: 6_720, cachedTokens: 16_384, firstTokenMs: 572, latencyMs: 5_840, estimatedCost: '$0.1042', createdAt: '2026-08-23T14:35:08+08:00', createdAtDisplay: '14:35:08', clientIp: '10.24.9.11', userAgent: 'codex-desktop/26.818.41705', compact: true }), + previewUsageRecord({ id: 'preview-04', accountEmail: 'batch@example.com', provider: 'openai', authenticationKind: 'api_key', model: 'gpt-5.2', clientTransport: 'http_json', upstreamTransport: 'http_sse', reasoningEffort: 'low', inputTokens: 4_860, outputTokens: 920, cachedTokens: 0, firstTokenMs: 264, latencyMs: 1_180, estimatedCost: '$0.0124', createdAt: '2026-08-23T14:31:44+08:00', createdAtDisplay: '14:31:44', clientIp: '10.24.12.9', userAgent: 'openai-node/6.4.0' }), + previewUsageRecord({ id: 'preview-05', accountEmail: 'analysis@example.com', provider: 'xai', authenticationKind: 'api_key', model: 'grok-4.20', clientTransport: 'http_sse', upstreamTransport: 'http_sse', reasoningEffort: 'high', inputTokens: 21_640, outputTokens: 4_380, cachedTokens: 7_168, firstTokenMs: 426, latencyMs: 4_120, estimatedCost: '$0.0731', createdAt: '2026-08-23T14:28:19+08:00', createdAtDisplay: '14:28:19', clientIp: '10.24.15.32', userAgent: 'curl/8.14.1' }), + previewUsageRecord({ id: 'preview-06', accountEmail: 'scheduler@example.com', provider: 'openai', authenticationKind: 'oauth', model: 'gpt-5.6-codex', clientTransport: 'websocket', upstreamTransport: 'websocket', reasoningEffort: 'medium', inputTokens: 9_760, outputTokens: 1_840, cachedTokens: 3_072, firstTokenMs: 296, latencyMs: 2_020, estimatedCost: '$0.0248', createdAt: '2026-08-23T14:24:03+08:00', createdAtDisplay: '14:24:03', clientIp: '10.24.4.18', userAgent: 'codex-cli/0.34.0' }), + previewUsageRecord({ id: 'preview-07', accountEmail: 'review@example.com', provider: 'openai', authenticationKind: 'oauth', model: 'gpt-5.5', clientTransport: 'http_sse', upstreamTransport: 'websocket', reasoningEffort: 'high', inputTokens: 15_280, outputTokens: 3_120, cachedTokens: 6_144, firstTokenMs: 448, latencyMs: 3_740, estimatedCost: '$0.0527', createdAt: '2026-08-23T14:18:37+08:00', createdAtDisplay: '14:18:37', clientIp: '10.24.18.7', userAgent: 'codex-desktop/26.818.41705' }), + previewUsageRecord({ id: 'preview-08', accountEmail: 'fallback@example.com', provider: 'xai', authenticationKind: 'oauth', model: 'grok-code-fast-1', clientTransport: 'websocket', upstreamTransport: 'http_sse', reasoningEffort: 'medium', inputTokens: 7_940, outputTokens: 1_460, cachedTokens: 2_048, firstTokenMs: 342, latencyMs: 1_920, estimatedCost: '$0.0194', createdAt: '2026-08-23T14:13:25+08:00', createdAtDisplay: '14:13:25', clientIp: '10.24.6.23', userAgent: 'grok-cli/0.7.3' }), + previewUsageRecord({ id: 'preview-09', accountEmail: 'desktop@example.com', provider: 'openai', authenticationKind: 'oauth', model: 'gpt-5.6-codex', clientTransport: 'http_sse', upstreamTransport: 'websocket', reasoningEffort: 'xhigh', inputTokens: 28_420, outputTokens: 5_940, cachedTokens: 12_288, firstTokenMs: 618, latencyMs: 5_260, estimatedCost: '$0.0925', createdAt: '2026-08-23T14:07:11+08:00', createdAtDisplay: '14:07:11', clientIp: '10.24.7.40', userAgent: 'codex-desktop/26.818.41705', compact: true }), + previewUsageRecord({ id: 'preview-10', accountEmail: 'api@example.com', provider: 'openai', authenticationKind: 'api_key', model: 'gpt-5.2', clientTransport: 'http_json', upstreamTransport: 'http_sse', reasoningEffort: 'low', inputTokens: 3_620, outputTokens: 680, cachedTokens: 0, firstTokenMs: 238, latencyMs: 980, estimatedCost: '$0.0091', createdAt: '2026-08-23T13:58:46+08:00', createdAtDisplay: '13:58:46', clientIp: '10.24.3.14', userAgent: 'openai-python/1.99.0' }), ], poolSummary: { total: 52, diff --git a/frontend/src/views/usage/components/UsageRecordDetailModal.vue b/frontend/src/views/usage/components/UsageRecordDetailModal.vue index fe470f69f..3c618e433 100644 --- a/frontend/src/views/usage/components/UsageRecordDetailModal.vue +++ b/frontend/src/views/usage/components/UsageRecordDetailModal.vue @@ -20,8 +20,8 @@ import { usageLatencyDetails, usageModelDisplay, usageReasoningEffort, - usageRecordType, usageTokenDetails, + usageTransportType, usageUserAgent, visibleRequestText, visibleResponseText, @@ -58,7 +58,8 @@ const finalAttemptIndex = computed(() => { }) const overviewItems = computed(() => [ { label: '时间', value: props.record?.createdAtDisplay, mono: true }, - { label: '类型', value: props.record ? usageRecordType(props.record) : '—' }, + { label: '客户端传输', value: usageTransportType(props.record?.clientTransport) }, + { label: '上游传输', value: usageTransportType(props.record?.upstreamTransport) }, { label: '耗时', value: props.record?.latencyMsDisplay, mono: true }, { label: latencyDetails.value?.firstOutputLabel ?? '首字', @@ -99,7 +100,6 @@ const detailGroups = computed(() => [ mono: true, wrap: true, }, - { label: '传输方式', value: props.record?.transport, mono: true }, { label: '事件类型', value: props.record?.kind, mono: true }, { label: '尝试序号', value: finalAttemptIndex.value }, { label: '服务档位', value: props.record?.serviceTier, mono: true }, @@ -128,7 +128,7 @@ const attemptColumns = defineTableColumns([ { key: 'outcome', label: '结果', kind: 'status', size: 'sm' }, { key: 'provider', label: '平台', kind: 'meta', size: 'sm' }, { key: 'model', label: '模型', kind: 'mono', size: 'lg' }, - { key: 'transport', label: '传输', kind: 'status', size: 'sm' }, + { key: 'transport', label: '上游传输', kind: 'status', size: 'md' }, { key: 'statusCode', label: '状态', kind: 'status', size: 'sm' }, { key: 'accountLabel', label: '账号', kind: 'mono', size: '3xl' }, { key: 'latencyMs', label: '耗时', kind: 'numeric', size: 'sm' }, diff --git a/frontend/src/views/usage/components/UsageRecordsTable.vue b/frontend/src/views/usage/components/UsageRecordsTable.vue index b0470d012..b0af95650 100644 --- a/frontend/src/views/usage/components/UsageRecordsTable.vue +++ b/frontend/src/views/usage/components/UsageRecordsTable.vue @@ -8,8 +8,6 @@ import { usageAccountText, usageAuthenticationKind, usageIsCompact, - usageRecordType, - usageRecordTypeClass, usageUserAgent, } from '../utils/records' import UsageBillingCell from './UsageBillingCell.vue' @@ -18,6 +16,7 @@ import UsageLatencyCell from './UsageLatencyCell.vue' import UsageModelCell from './UsageModelCell.vue' import UsageReasoningEffortCell from './UsageReasoningEffortCell.vue' import UsageTokenCell from './UsageTokenCell.vue' +import UsageTransportBadge from './UsageTransportBadge.vue' // 使用记录表只负责该领域的单元格呈现;筛选与分页由页面组合。 withDefaults( @@ -89,13 +88,12 @@ withDefaults(
-