From 3f6607209b0a5ce4b4a98e51e32115b4ef6e998d Mon Sep 17 00:00:00 2001 From: Zyy Date: Sun, 25 Oct 2026 04:03:27 +0800 Subject: [PATCH] refactor(usage): remove reported model tracking and polish UI Remove upstream response model tracking across providers, storage, APIs, and plugin facts without migrating historical records. Refine sidebar hints, locale loading indicators, model columns, and usage page labels and filters. Co-authored-by: Codex --- .../gateway-admin/src/model/observability.rs | 2 - .../tests/use_case/observability/mod.rs | 1 - .../src/admin/observability/export/archive.rs | 3 +- .../src/admin/observability/presenter.rs | 2 - .../src/admin/observability/wire.rs | 2 - .../tests/admin/observability/response.rs | 4 -- .../gateway-api/tests/key_usage/fixtures.rs | 1 - .../gateway-core/src/engine/coordinator.rs | 12 ----- backend/crates/gateway-core/src/engine/mod.rs | 1 - .../gateway-core/src/engine/observation.rs | 9 ---- backend/crates/gateway-core/src/event.rs | 17 ------ .../gateway-core/tests/engine/coordinator.rs | 9 ---- .../tests/engine/execution/mod.rs | 2 - .../runtime/src/adapter/observer.rs | 1 - .../adapter/upstream_adapter/observation.rs | 6 --- .../runtime/src/callback/facts.rs | 1 - .../tests/generation/policy/facts/mod.rs | 1 - .../tests/generation/upstream_adapter.rs | 5 +- .../sdk/src/call/model/facts.rs | 2 - .../sdk/src/call/observation.rs | 3 -- .../sdk/tests/call/observation.rs | 10 +--- .../gateway-protocol/src/openai/events.rs | 52 +------------------ .../gateway-protocol/tests/openai/events.rs | 44 ---------------- .../src/postgres/execution/mod.rs | 8 --- .../postgres/execution/request_observation.rs | 12 ----- .../src/postgres/execution_buffer.rs | 1 - .../src/postgres/observability/mapping.rs | 2 - .../postgres/observability/queries/usage.rs | 4 +- .../tests/postgres/execution/mod.rs | 11 ---- .../openai/src/provider/execution.rs | 36 ++----------- .../openai/src/provider/observation.rs | 27 ---------- .../openai/src/transport/canonical.rs | 35 +------------ .../openai/src/transport/response_meta.rs | 28 ---------- .../src/transport/websocket/exchange/mod.rs | 5 +- .../transport/websocket/exchange/reducer.rs | 3 -- .../transport/websocket/exchange/stream.rs | 4 -- .../openai/tests/provider/contract/mod.rs | 24 ++------- .../openai/tests/transport/canonical.rs | 51 ------------------ .../openai/tests/transport/http_client.rs | 4 -- .../providers/openai/tests/transport/mod.rs | 4 -- .../tests/transport/websocket/metadata.rs | 1 - .../openai/tests/transport/websocket/mod.rs | 9 ---- .../providers/xai/src/provider/stream.rs | 16 ------ .../providers/xai/src/transport/canonical.rs | 15 +----- .../xai/tests/transport/canonical.rs | 13 ----- docs/api.md | 7 +-- frontend/src/api/modules/usage.ts | 2 - frontend/src/components/AppLanguageMenu.vue | 10 ++-- .../src/components/usage/UsageModelCell.vue | 10 ++-- .../src/components/usage/shared/columns.ts | 2 +- .../src/components/usage/shared/presenter.ts | 24 +-------- frontend/src/i18n/locales/en/usage.json | 9 ++-- frontend/src/i18n/locales/zh-Hans/usage.json | 9 ++-- frontend/src/i18n/locales/zh-Hant/usage.json | 9 ++-- frontend/src/layout/components/AppSidebar.vue | 4 +- .../key-usage/components/KeyUsageHeader.vue | 2 +- frontend/src/views/key-usage/index.vue | 13 ++--- .../src/views/theme/fixtures/dashboard.ts | 1 - .../components/UsageRecordDetailModal.vue | 1 - 59 files changed, 53 insertions(+), 553 deletions(-) diff --git a/backend/crates/gateway-admin/src/model/observability.rs b/backend/crates/gateway-admin/src/model/observability.rs index 5ec07c46c..719f0dc77 100644 --- a/backend/crates/gateway-admin/src/model/observability.rs +++ b/backend/crates/gateway-admin/src/model/observability.rs @@ -740,7 +740,6 @@ pub struct UsageListRecord { pub provider_account_authentication_kind: Option, pub upstream_model_id: Option, pub upstream_transport: Option, - pub upstream_response_model: Option, pub service_tier: Option, pub input_tokens: Option, pub output_tokens: Option, @@ -802,7 +801,6 @@ pub struct UsageRecord { pub upstream_transport: Option, pub http_version: Option, pub websocket_pool: Option, - pub upstream_response_model: Option, pub service_tier: Option, /// Provider 已筛选的专有观测 JSON;管理领域保持不透明 pub provider_metadata_json: Option, diff --git a/backend/crates/gateway-admin/tests/use_case/observability/mod.rs b/backend/crates/gateway-admin/tests/use_case/observability/mod.rs index f55ad8dc9..4c4b02a46 100644 --- a/backend/crates/gateway-admin/tests/use_case/observability/mod.rs +++ b/backend/crates/gateway-admin/tests/use_case/observability/mod.rs @@ -1361,7 +1361,6 @@ fn total_record( provider_account_authentication_kind: None, upstream_model_id: Some("gpt-5.5".to_owned()), upstream_transport: None, - upstream_response_model: None, service_tier: None, input_tokens: Some(800), output_tokens: Some(200), diff --git a/backend/crates/gateway-api/src/admin/observability/export/archive.rs b/backend/crates/gateway-api/src/admin/observability/export/archive.rs index 20ce224a9..26b2582e0 100644 --- a/backend/crates/gateway-api/src/admin/observability/export/archive.rs +++ b/backend/crates/gateway-api/src/admin/observability/export/archive.rs @@ -28,13 +28,12 @@ pub(super) fn build(document: &Value) -> io::Result> { let detail = &evidence["detail"]["request"]; writeln!( archive, - "\n结果:{};状态码:{};尝试数:{}\n模型:请求={},发送={},上游报告={}\n传输:{} → {}\n时延:{}", + "\n结果:{};状态码:{};尝试数:{}\n模型:请求={},发送={}\n传输:{} → {}\n时延:{}", detail["logicalOutcome"], detail["statusCode"], detail["attemptCount"], detail["requestedModel"], detail["upstreamModel"], - detail["upstreamResponseModel"], detail["clientTransport"], detail["upstreamTransport"], detail["latencyDetails"] diff --git a/backend/crates/gateway-api/src/admin/observability/presenter.rs b/backend/crates/gateway-api/src/admin/observability/presenter.rs index fcb949803..e5ddedcd2 100644 --- a/backend/crates/gateway-api/src/admin/observability/presenter.rs +++ b/backend/crates/gateway-api/src/admin/observability/presenter.rs @@ -279,7 +279,6 @@ pub(crate) fn usage_list_record_view( model, requested_model: record.requested_model_id, upstream_model: record.upstream_model_id, - upstream_response_model: record.upstream_response_model, service_tier: record.service_tier, client_transport: record.client_transport, upstream_transport: record.upstream_transport, @@ -377,7 +376,6 @@ pub(crate) fn usage_record_view( model, requested_model: record.requested_model_id, upstream_model: record.upstream_model_id, - upstream_response_model: record.upstream_response_model, service_tier: record.service_tier, status_code, client_transport: record.client_transport, diff --git a/backend/crates/gateway-api/src/admin/observability/wire.rs b/backend/crates/gateway-api/src/admin/observability/wire.rs index 4e4579374..8cabcc265 100644 --- a/backend/crates/gateway-api/src/admin/observability/wire.rs +++ b/backend/crates/gateway-api/src/admin/observability/wire.rs @@ -103,7 +103,6 @@ pub struct UsageListRecordView { pub model: Option, pub requested_model: Option, pub upstream_model: Option, - pub upstream_response_model: Option, pub service_tier: Option, pub client_transport: String, pub upstream_transport: Option, @@ -142,7 +141,6 @@ pub struct UsageRecordView { pub model: Option, pub requested_model: Option, pub upstream_model: Option, - pub upstream_response_model: Option, pub service_tier: Option, pub status_code: Option, pub client_transport: String, diff --git a/backend/crates/gateway-api/tests/admin/observability/response.rs b/backend/crates/gateway-api/tests/admin/observability/response.rs index 64a8a6d6f..738326453 100644 --- a/backend/crates/gateway-api/tests/admin/observability/response.rs +++ b/backend/crates/gateway-api/tests/admin/observability/response.rs @@ -764,7 +764,6 @@ pub(super) fn usage_record_with_account( upstream_transport: Some("http_sse".to_owned()), http_version: Some("h2".to_owned()), websocket_pool: None, - upstream_response_model: None, service_tier: None, provider_metadata_json: None, attempt_count: 1, @@ -909,7 +908,6 @@ async fn usage_route_should_expose_table_facts_without_detail_payload() { provider_account_authentication_kind: Some("oauth".to_owned()), upstream_model_id: Some("grok-4.5".to_owned()), upstream_transport: Some("http_sse".to_owned()), - upstream_response_model: Some("grok-4.6-build".to_owned()), service_tier: Some("default".to_owned()), input_tokens: Some(1), output_tokens: Some(1), @@ -1059,7 +1057,6 @@ async fn usage_route_should_expose_table_facts_without_detail_payload() { "imageOutputTokens": value["data"]["items"][0]["tokenDetails"]["imageOutputTokens"], "requestedModel": value["data"]["items"][0]["requestedModel"], "upstreamModel": value["data"]["items"][0]["upstreamModel"], - "upstreamResponseModel": value["data"]["items"][0]["upstreamResponseModel"], "reasoningEffort": value["data"]["items"][0]["reasoningEffort"], "reasoningPreset": value["data"]["items"][0]["reasoningPreset"], "subagentKind": value["data"]["items"][0]["subagentKind"], @@ -1083,7 +1080,6 @@ async fn usage_route_should_expose_table_facts_without_detail_payload() { "imageOutputTokens": 9, "requestedModel": "grok-4.5", "upstreamModel": "grok-4.5", - "upstreamResponseModel": "grok-4.6-build", "reasoningEffort": "max", "reasoningPreset": "ultra", "subagentKind": "worker", diff --git a/backend/crates/gateway-api/tests/key_usage/fixtures.rs b/backend/crates/gateway-api/tests/key_usage/fixtures.rs index 674bb6259..318c66f99 100644 --- a/backend/crates/gateway-api/tests/key_usage/fixtures.rs +++ b/backend/crates/gateway-api/tests/key_usage/fixtures.rs @@ -110,7 +110,6 @@ fn usage_record() -> UsageListRecord { provider_account_authentication_kind: Some("private-sentinel".to_owned()), upstream_model_id: Some("private-sentinel".to_owned()), upstream_transport: Some("websocket".to_owned()), - upstream_response_model: Some("private-sentinel".to_owned()), service_tier: Some("private-sentinel".to_owned()), input_tokens: Some(1000), output_tokens: Some(100), diff --git a/backend/crates/gateway-core/src/engine/coordinator.rs b/backend/crates/gateway-core/src/engine/coordinator.rs index 554f6edff..7ea88cfd1 100644 --- a/backend/crates/gateway-core/src/engine/coordinator.rs +++ b/backend/crates/gateway-core/src/engine/coordinator.rs @@ -2302,12 +2302,6 @@ where http_version, websocket_pool, service_tier, - upstream_response_model: self - .current - .as_ref() - .and_then(|current| current.response_observation.as_ref()) - .and_then(ProviderResponseObservation::upstream_response_model) - .map(str::to_owned), provider_metadata_json, diagnostic_trace_json: self.trace.snapshot().map(|value| value.to_string()), error: None, @@ -2490,12 +2484,6 @@ where http_version, websocket_pool, service_tier, - upstream_response_model: self - .current - .as_ref() - .and_then(|current| current.response_observation.as_ref()) - .and_then(ProviderResponseObservation::upstream_response_model) - .map(str::to_owned), provider_metadata_json, diagnostic_trace_json: self.trace.snapshot().map(|value| value.to_string()), error: Some(finalization.error), diff --git a/backend/crates/gateway-core/src/engine/mod.rs b/backend/crates/gateway-core/src/engine/mod.rs index cabee6dc1..fc3386e36 100644 --- a/backend/crates/gateway-core/src/engine/mod.rs +++ b/backend/crates/gateway-core/src/engine/mod.rs @@ -1070,7 +1070,6 @@ pub struct ModelRequestFinalization { pub http_version: Option, pub websocket_pool: Option, pub service_tier: Option, - pub upstream_response_model: Option, /// Provider 已筛选的专有观测 JSON;Core 不解释字段 pub provider_metadata_json: Option, /// 请求全程的有界诊断快照,跨 Provider 与重试保留 diff --git a/backend/crates/gateway-core/src/engine/observation.rs b/backend/crates/gateway-core/src/engine/observation.rs index d1a1d3b81..91c4c35f8 100644 --- a/backend/crates/gateway-core/src/engine/observation.rs +++ b/backend/crates/gateway-core/src/engine/observation.rs @@ -249,7 +249,6 @@ pub struct RequestObservation { provider: Option, account_id: Option, upstream_model: Option, - response_model: Option, service_tier: Option, outcome: RequestObservationOutcome, send_state: UpstreamSendState, @@ -287,7 +286,6 @@ impl RequestObservation { provider: None, account_id: None, upstream_model: None, - response_model: None, service_tier: None, outcome, send_state, @@ -341,11 +339,6 @@ impl RequestObservation { self.upstream_model.as_ref() } - #[must_use] - pub fn response_model(&self) -> Option<&str> { - self.response_model.as_deref() - } - #[must_use] pub fn service_tier(&self) -> Option<&str> { self.service_tier.as_deref() @@ -626,7 +619,6 @@ impl RequestObservationDispatch { outcome: RequestObservationOutcome::Rejected, account_id: None, upstream_model: None, - response_model: None, service_tier: None, send_state: UpstreamSendState::NotSent, attempt_count: 0, @@ -699,7 +691,6 @@ impl RequestObservationDispatch { provider, account_id: attempt.map(|attempt| attempt.provider_account_id().clone()), upstream_model: attempt.and_then(|attempt| attempt.upstream_model().cloned()), - response_model: finalization.upstream_response_model.clone(), service_tier: finalization.service_tier.clone(), outcome, send_state: finalization.send_state, diff --git a/backend/crates/gateway-core/src/event.rs b/backend/crates/gateway-core/src/event.rs index fcf0fa920..d1b1ee268 100644 --- a/backend/crates/gateway-core/src/event.rs +++ b/backend/crates/gateway-core/src/event.rs @@ -624,7 +624,6 @@ pub struct ProviderResponseObservation { status_code: Option, request_id: Option, service_tier: Option, - upstream_response_model: Option, timings: ProviderResponseTimings, // 流式事件分别持有快照,共享不可变头集合,避免逐事件复制头名和集合 client_headers: Arc<[ProviderResponseHeader]>, @@ -641,7 +640,6 @@ impl ProviderResponseObservation { status_code: None, request_id: None, service_tier: None, - upstream_response_model: None, timings: ProviderResponseTimings::default(), client_headers: Arc::default(), provider_metadata: None, @@ -697,21 +695,6 @@ impl ProviderResponseObservation { self } - /// 记录 Provider 从上游响应明确取得的模型,不改写发送模型或计费合同 - #[must_use] - pub fn with_upstream_response_model_if_valid(mut self, model: &str) -> Self { - let model = model.trim(); - if validate_text(model, 256, false, None).is_ok() { - self.upstream_response_model = Some(model.to_owned()); - } - self - } - - #[must_use] - pub fn upstream_response_model(&self) -> Option<&str> { - self.upstream_response_model.as_deref() - } - #[must_use] pub const fn with_timings(mut self, timings: ProviderResponseTimings) -> Self { self.timings = timings; diff --git a/backend/crates/gateway-core/tests/engine/coordinator.rs b/backend/crates/gateway-core/tests/engine/coordinator.rs index c6ae3a7cb..a547d6ab4 100644 --- a/backend/crates/gateway-core/tests/engine/coordinator.rs +++ b/backend/crates/gateway-core/tests/engine/coordinator.rs @@ -491,7 +491,6 @@ struct FinalState { http_version: Option, websocket_pool: Option, service_tier: Option, - upstream_response_model: Option, upstream_request_id: Option, upstream_status_code: Option, transport_decision_wait_ms: Option, @@ -682,7 +681,6 @@ impl ExecutionStore for FakeStore { http_version: finalization.http_version, websocket_pool: finalization.websocket_pool, service_tier: finalization.service_tier, - upstream_response_model: finalization.upstream_response_model, upstream_request_id: finalization.upstream_request_id, upstream_status_code: finalization.upstream_status_code, transport_decision_wait_ms: finalization.timings.transport_decision_wait_ms, @@ -1764,7 +1762,6 @@ fn response_observation_is_persisted_but_never_delivered() { .with_websocket_pool(WebSocketPoolKind::New) .with_status_code(200) .with_request_id(OpaqueUpstreamValue::new("upstream-observed")) - .with_upstream_response_model_if_valid("gpt-returned") .try_with_service_tier("priority") .expect("service tier") .with_timings(ProviderResponseTimings { @@ -1804,10 +1801,6 @@ fn response_observation_is_persisted_but_never_delivered() { assert_eq!(finalization.http_version.as_deref(), Some("HTTP/2")); assert_eq!(finalization.websocket_pool.as_deref(), Some("new")); assert_eq!(finalization.service_tier.as_deref(), Some("priority")); - assert_eq!( - finalization.upstream_response_model.as_deref(), - Some("gpt-returned") - ); assert_eq!( finalization.upstream_request_id.as_deref(), Some("upstream-observed") @@ -2083,7 +2076,6 @@ fn discarded_attempt_observation_does_not_leak_into_retry_result() { .with_http_version(UpstreamHttpVersion::Http11) .with_status_code(503) .with_request_id(OpaqueUpstreamValue::new("discarded-request")) - .with_upstream_response_model_if_valid("discarded-model") .with_timings(ProviderResponseTimings { transport_decision_wait_ms: Some(7), connect_ms: Some(11), @@ -2146,7 +2138,6 @@ fn discarded_attempt_observation_does_not_leak_into_retry_result() { let state = store.state.lock().expect("store lock"); let finalization = &state.finalizations[0]; - assert_eq!(finalization.upstream_response_model, None); let trace: Value = serde_json::from_str(finalization.diagnostic_trace_json.as_deref().unwrap()).unwrap(); let events = trace["events"].as_array().unwrap(); diff --git a/backend/crates/gateway-core/tests/engine/execution/mod.rs b/backend/crates/gateway-core/tests/engine/execution/mod.rs index e860704cf..3e57bbe8c 100644 --- a/backend/crates/gateway-core/tests/engine/execution/mod.rs +++ b/backend/crates/gateway-core/tests/engine/execution/mod.rs @@ -2282,7 +2282,6 @@ impl Provider for ObservationProvider { gateway_core::event::ProviderResponseObservation::new( UpstreamTransport::new("http_sse").unwrap(), ) - .with_upstream_response_model_if_valid("gpt-reported") .with_service_tier_if_valid("priority"), ); Ok(ProviderStream::new( @@ -2615,7 +2614,6 @@ fn final_observation_is_emitted_once_for_success_rejection_and_detached_cancella .map(UpstreamModelId::as_str), Some("gpt-start") ); - assert_eq!(observations[0].response_model(), Some("gpt-reported")); assert_eq!(observations[0].service_tier(), Some("priority")); assert_eq!( observations[0].outcome(), diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/observer.rs b/backend/crates/gateway-plugin/runtime/src/adapter/observer.rs index 6aa31818f..8de3437fa 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/observer.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/observer.rs @@ -455,7 +455,6 @@ fn wire_observation(observation: &RequestObservation) -> RequestCompleted { upstream_model: observation .upstream_model() .map(|model| model.as_str().to_owned()), - response_model: observation.response_model().map(str::to_owned), service_tier: observation.service_tier().map(str::to_owned), requested_model: observation .requested_model() diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/observation.rs b/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/observation.rs index 9549a1096..1676b1859 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/observation.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/upstream_adapter/observation.rs @@ -47,12 +47,6 @@ pub(super) fn merge( .try_with_service_tier(value) .map_err(|source| invalid(sent).with_source(source))?; } - if let Some(value) = update.upstream_response_model { - current = current.with_upstream_response_model_if_valid(&value); - if current.upstream_response_model() != Some(value.trim()) { - return Err(invalid(sent)); - } - } if !update.client_headers.is_empty() { current = current.with_client_headers( update diff --git a/backend/crates/gateway-plugin/runtime/src/callback/facts.rs b/backend/crates/gateway-plugin/runtime/src/callback/facts.rs index d10929a9a..856a88b64 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/facts.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/facts.rs @@ -119,7 +119,6 @@ fn observation(value: &ProviderResponseObservation) -> facts::ResponseObservatio status_code: value.status_code(), request_id: value.request_id().map(|value| value.as_str().to_owned()), service_tier: value.service_tier().map(str::to_owned), - upstream_response_model: value.upstream_response_model().map(str::to_owned), timings: facts::ResponseTimings { transport_decision_wait_ms: timings.transport_decision_wait_ms, connect_ms: timings.connect_ms, diff --git a/backend/crates/gateway-plugin/runtime/tests/generation/policy/facts/mod.rs b/backend/crates/gateway-plugin/runtime/tests/generation/policy/facts/mod.rs index 5c9366196..e09db0542 100644 --- a/backend/crates/gateway-plugin/runtime/tests/generation/policy/facts/mod.rs +++ b/backend/crates/gateway-plugin/runtime/tests/generation/policy/facts/mod.rs @@ -189,7 +189,6 @@ async fn facts_are_readable_without_replacing_or_duplicating_the_original_execut .with_status_code(200) .with_request_id(OpaqueUpstreamValue::new("response-request-id")) .with_service_tier_if_valid("priority") - .with_upstream_response_model_if_valid("returned-model") .with_client_headers(vec![ProviderResponseHeader::new( "set-cookie", Bytes::from_static(b"fixture=secret"), diff --git a/backend/crates/gateway-plugin/runtime/tests/generation/upstream_adapter.rs b/backend/crates/gateway-plugin/runtime/tests/generation/upstream_adapter.rs index e4ac2e4b5..36553cbb7 100644 --- a/backend/crates/gateway-plugin/runtime/tests/generation/upstream_adapter.rs +++ b/backend/crates/gateway-plugin/runtime/tests/generation/upstream_adapter.rs @@ -667,7 +667,7 @@ async fn adapter_preserves_observation_updates_and_measures_only_real_output() { {"event":{"facts":[{"type":"started","id":"response-one","model":"native-model"}, {"type":"content_added","index":0,"kind":"reasoning"}]}, "observation":{"transport":"http_sse","http_version":"HTTP/2","status_code":200, - "request_id":"upstream-494","upstream_response_model":"real-model", + "request_id":"upstream-494", "client_headers":[{"name":"x-bytes","value":[255]},{"name":"x-bytes","value":[254]}], "provider_metadata":"{ \"future\": 1e3 }","timings":{"connect_ms":5,"headers_ms":8}}}, {"event":{"facts":[{"type":"reasoning_delta","index":0,"text":""}]}}, @@ -711,7 +711,6 @@ async fn adapter_preserves_observation_updates_and_measures_only_real_output() { assert_eq!(last.status_code(), Some(200)); assert_eq!(last.http_version().unwrap().as_str(), "HTTP/2"); assert_eq!(last.request_id().unwrap().as_str(), "upstream-494"); - assert_eq!(last.upstream_response_model(), Some("real-model")); assert_eq!(last.service_tier(), Some("priority")); assert_eq!( last.provider_metadata().unwrap().as_json(), @@ -1359,7 +1358,7 @@ async fn cancelling_adapter_body_closes_pending_websocket_and_releases_selected_ } #[tokio::test] -async fn usage_without_response_model_still_uses_native_account_pricing() { +async fn upstream_adapter_usage_uses_native_account_pricing() { let server = MockServer::start().await; Mock::given(method("POST")) .respond_with(ResponseTemplate::new(200).set_body_string("{}")) diff --git a/backend/crates/gateway-plugin/sdk/src/call/model/facts.rs b/backend/crates/gateway-plugin/sdk/src/call/model/facts.rs index 63e327672..8d238775d 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/model/facts.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/model/facts.rs @@ -72,7 +72,6 @@ pub struct ResponseObservation { pub status_code: Option, pub request_id: Option, pub service_tier: Option, - pub upstream_response_model: Option, #[serde(default)] pub timings: ResponseTimings, #[serde(default)] @@ -119,7 +118,6 @@ impl ResponseObservation { status_code: None, request_id: None, service_tier: None, - upstream_response_model: None, timings: ResponseTimings::default(), client_headers: Vec::new(), provider_metadata: None, diff --git a/backend/crates/gateway-plugin/sdk/src/call/observation.rs b/backend/crates/gateway-plugin/sdk/src/call/observation.rs index 658da10de..194d43b9a 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/observation.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/observation.rs @@ -187,9 +187,6 @@ pub struct RequestCompleted { /// 本次实际选中账号使用的上游模型,未进入上游时可能不存在 #[serde(default, skip_serializing_if = "Option::is_none")] pub upstream_model: Option, - /// 上游响应报告的模型,与请求中的模型名称分开保存 - #[serde(default, skip_serializing_if = "Option::is_none")] - pub response_model: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub service_tier: Option, #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/backend/crates/gateway-plugin/sdk/tests/call/observation.rs b/backend/crates/gateway-plugin/sdk/tests/call/observation.rs index ce16cdfd4..0e4fdc430 100644 --- a/backend/crates/gateway-plugin/sdk/tests/call/observation.rs +++ b/backend/crates/gateway-plugin/sdk/tests/call/observation.rs @@ -64,7 +64,6 @@ fn observation() -> RequestCompleted { client_key_id: Some("key_fixture".into()), account_id: Some("acct_fixture".into()), upstream_model: Some("upstream-model".into()), - response_model: Some("reported-model".into()), service_tier: Some("default".into()), provider: None, completed_at_ms: 1234, @@ -114,19 +113,12 @@ fn observation_exposes_selected_and_reported_facts_without_inventing_values() { assert_eq!(wire["client_key_id"], "key_fixture"); assert_eq!(wire["account_id"], "acct_fixture"); assert_eq!(wire["upstream_model"], "upstream-model"); - assert_eq!(wire["response_model"], "reported-model"); assert_eq!(wire["service_tier"], "default"); request.account_id = None; request.upstream_model = None; - request.response_model = None; request.service_tier = None; let wire = serde_json::to_value(request).unwrap(); - for field in [ - "account_id", - "upstream_model", - "response_model", - "service_tier", - ] { + for field in ["account_id", "upstream_model", "service_tier"] { assert!(wire.get(field).is_none(), "unknown {field} must be omitted"); } } diff --git a/backend/crates/gateway-protocol/src/openai/events.rs b/backend/crates/gateway-protocol/src/openai/events.rs index 18057f62d..845dc3363 100644 --- a/backend/crates/gateway-protocol/src/openai/events.rs +++ b/backend/crates/gateway-protocol/src/openai/events.rs @@ -1,4 +1,4 @@ -//! 从 OpenAI 事件与响应头提取模型、用量、额度和重试等待事实 +//! 从 OpenAI 事件与响应头提取用量、额度和重试等待事实 use std::{collections::BTreeMap, time::Duration}; @@ -7,56 +7,6 @@ use serde_json::Value; use super::sse::{SseError, parse_sse_events}; -/// 单次 Responses 流明确声明的模型;终态优先,缺失时不使用请求模型补齐 -#[derive(Debug, Default)] -pub struct ResponseModelObservation { - model: Option, - terminal: bool, -} - -impl ResponseModelObservation { - /// 从原始事件读取响应模型,避免工具转换或 canonical 兜底改写观测事实 - pub fn observe(&mut self, event_type: Option<&str>, value: &Value) { - let terminal = matches!( - event_type, - Some( - "response.completed" - | "response.done" - | "response.incomplete" - | "response.failed" - | "response.cancelled" - | "response.canceled" - ) - ); - if self.terminal || (self.model.is_some() && !terminal) { - return; - } - let Some(model) = value - .pointer("/response/model") - .and_then(Value::as_str) - .and_then(observed_model_name) - else { - return; - }; - self.model = Some(model.to_owned()); - self.terminal = terminal; - } - - /// 返回原始响应声明,未声明时保持未知 - #[must_use] - pub fn model(&self) -> Option<&str> { - self.model.as_deref() - } -} - -/// 规范化可展示的上游模型名,非法值不影响响应交付 -#[must_use] -pub fn observed_model_name(value: &str) -> Option<&str> { - let value = value.trim(); - (!value.is_empty() && value.len() <= 256 && !value.chars().any(char::is_control)) - .then_some(value) -} - /// 从 Codex/OpenAI usage 结构中提取出的标准化 token 用量 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/backend/crates/gateway-protocol/tests/openai/events.rs b/backend/crates/gateway-protocol/tests/openai/events.rs index 31b94a422..ec40e6493 100644 --- a/backend/crates/gateway-protocol/tests/openai/events.rs +++ b/backend/crates/gateway-protocol/tests/openai/events.rs @@ -7,50 +7,6 @@ use gateway_protocol::openai::events::{ }; use serde_json::json; -#[test] -fn response_model_observation_keeps_first_declaration_until_terminal() { - use gateway_protocol::openai::events::ResponseModelObservation; - - let mut observed = ResponseModelObservation::default(); - observed.observe( - Some("response.created"), - &json!({"response": {"model": " first "}}), - ); - observed.observe( - Some("response.in_progress"), - &json!({"response": {"model": "intermediate"}}), - ); - assert_eq!(observed.model(), Some("first")); - observed.observe( - Some("response.completed"), - &json!({"response": {"model": "final"}}), - ); - observed.observe( - Some("response.in_progress"), - &json!({"response": {"model": "late"}}), - ); - assert_eq!(observed.model(), Some("final")); - assert_eq!(ResponseModelObservation::default().model(), None); -} - -#[test] -fn response_model_observation_ignores_missing_and_invalid_values() { - use gateway_protocol::openai::events::ResponseModelObservation; - - for response in [ - json!({}), - json!({"model": null}), - json!({"model": 42}), - json!({"model": " "}), - json!({"model": "a\nb"}), - json!({"model": "a".repeat(257)}), - ] { - let mut observed = ResponseModelObservation::default(); - observed.observe(Some("response.completed"), &json!({"response": response})); - assert_eq!(observed.model(), None); - } -} - #[test] fn billable_usage_should_validate_cache_writes_totals_and_overflow() { for (raw, expected) in [ diff --git a/backend/crates/gateway-store/src/postgres/execution/mod.rs b/backend/crates/gateway-store/src/postgres/execution/mod.rs index 4f13734ad..e44b4f7e0 100644 --- a/backend/crates/gateway-store/src/postgres/execution/mod.rs +++ b/backend/crates/gateway-store/src/postgres/execution/mod.rs @@ -248,7 +248,6 @@ pub struct ModelRequestFinalization { pub http_version: Option, pub websocket_pool: Option, pub service_tier: Option, - pub upstream_response_model: Option, pub provider_metadata_json: Option, pub diagnostic_trace_json: Option, pub error_kind: Option, @@ -314,12 +313,6 @@ impl ModelRequestFinalization { return Err(invalid("service tier is invalid")); } } - if let Some(model) = self.upstream_response_model.as_deref() { - require_nonempty(ENTITY, "upstream_response_model", model)?; - if model.len() > 256 || model.chars().any(char::is_control) { - return Err(invalid("upstream response model is invalid")); - } - } if self .provider_metadata_json .as_ref() @@ -1180,7 +1173,6 @@ impl ExecutionStore for PgExecutionStore { http_version: finalization.http_version, websocket_pool: finalization.websocket_pool, service_tier: finalization.service_tier, - upstream_response_model: finalization.upstream_response_model, provider_metadata_json, diagnostic_trace_json: finalization .diagnostic_trace_json diff --git a/backend/crates/gateway-store/src/postgres/execution/request_observation.rs b/backend/crates/gateway-store/src/postgres/execution/request_observation.rs index 676d42d7d..2eaa6ac16 100644 --- a/backend/crates/gateway-store/src/postgres/execution/request_observation.rs +++ b/backend/crates/gateway-store/src/postgres/execution/request_observation.rs @@ -21,8 +21,6 @@ pub(super) struct RequestObservation { #[serde(skip_serializing_if = "Option::is_none")] transport: Option, #[serde(skip_serializing_if = "Option::is_none")] - response: Option, - #[serde(skip_serializing_if = "Option::is_none")] continuation: Option, } @@ -146,13 +144,6 @@ struct ConnectionObservation { idle_ms: Option, } -#[derive(Default, Serialize)] -#[serde(rename_all = "camelCase")] -struct ResponseObservation { - #[serde(skip_serializing_if = "Option::is_none")] - response_model: Option, -} - #[derive(Default, Serialize)] #[serde(rename_all = "camelCase")] struct ContinuationObservation { @@ -263,9 +254,6 @@ impl RequestObservation { idle_ms: value.upstream_connection_idle_ms, }), }), - response: Some(ResponseObservation { - response_model: value.upstream_response_model.clone(), - }), error: Some(ErrorObservation { provider_error_code: value.provider_error_code.clone(), message: value.error_message.clone(), diff --git a/backend/crates/gateway-store/src/postgres/execution_buffer.rs b/backend/crates/gateway-store/src/postgres/execution_buffer.rs index 3d2ff596e..0242c4713 100644 --- a/backend/crates/gateway-store/src/postgres/execution_buffer.rs +++ b/backend/crates/gateway-store/src/postgres/execution_buffer.rs @@ -849,7 +849,6 @@ impl ExecutionObservationWrite { finalization.http_version.as_deref(), finalization.websocket_pool.as_deref(), finalization.service_tier.as_deref(), - finalization.upstream_response_model.as_deref(), finalization.provider_metadata_json.as_deref(), finalization.diagnostic_trace_json.as_deref(), finalization.provider_error_code.as_deref(), diff --git a/backend/crates/gateway-store/src/postgres/observability/mapping.rs b/backend/crates/gateway-store/src/postgres/observability/mapping.rs index adc96d1f2..8a4c3492e 100644 --- a/backend/crates/gateway-store/src/postgres/observability/mapping.rs +++ b/backend/crates/gateway-store/src/postgres/observability/mapping.rs @@ -329,7 +329,6 @@ pub(crate) fn usage_list_record_from_row( provider_account_authentication_kind: get(row, "provider_account_authentication_kind")?, upstream_model_id: get(row, "upstream_model_id")?, upstream_transport: get(row, "upstream_transport")?, - upstream_response_model: get(row, "upstream_response_model")?, service_tier: get(row, "service_tier")?, input_tokens: optional_unsigned(row, "input_tokens")?, output_tokens: optional_unsigned(row, "output_tokens")?, @@ -404,7 +403,6 @@ pub(crate) fn usage_record_from_row(row: &sqlx::postgres::PgRow) -> StoreResult< upstream_transport: get(row, "upstream_transport")?, http_version: get(row, "http_version")?, websocket_pool: get(row, "websocket_pool")?, - upstream_response_model: get(row, "upstream_response_model")?, service_tier: get(row, "service_tier")?, provider_metadata_json: get::>(row, "provider_observation_json")? .map(|value| serde_json::to_string(&value)) 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 4ef32e36e..47c96ea5c 100644 --- a/backend/crates/gateway-store/src/postgres/observability/queries/usage.rs +++ b/backend/crates/gateway-store/src/postgres/observability/queries/usage.rs @@ -146,7 +146,7 @@ pub(crate) const USAGE_LIST_RECORD_SELECT: &str = mr.provider_account_plan_type_snapshot as provider_account_plan_type, mr.provider_account_authentication_kind_snapshot as provider_account_authentication_kind, - mr.upstream_model_id, mr.upstream_transport, mr.upstream_response_model, mr.service_tier, + mr.upstream_model_id, mr.upstream_transport, mr.service_tier, mr.input_tokens, mr.output_tokens, mr.cached_input_tokens, mr.cache_write_input_tokens, mr.reasoning_output_tokens, mr.image_input_tokens, mr.image_output_tokens, mr.total_tokens, mr.billing_snapshot_json, mr.cost_source, mr.cost_amount::text, mr.cost_currency, @@ -171,7 +171,7 @@ pub(crate) const USAGE_RECORD_DETAIL_SELECT: &str = mr.provider_account_authentication_kind_snapshot as provider_account_authentication_kind, mr.upstream_model_id, mr.upstream_transport, mr.http_version, mr.websocket_pool, - mr.service_tier, mr.upstream_response_model, mr.provider_observation_json, mr.diagnostic_trace_json, + mr.service_tier, mr.provider_observation_json, mr.diagnostic_trace_json, mr.attempt_count, mr.upstream_send_state, mr.downstream_committed_at, mr.outcome, mr.client_status_code, mr.upstream_status_code, mr.client_response_id, mr.upstream_request_id, mr.upstream_response_id, diff --git a/backend/crates/gateway-store/tests/postgres/execution/mod.rs b/backend/crates/gateway-store/tests/postgres/execution/mod.rs index 2f67c66a2..548ed311e 100644 --- a/backend/crates/gateway-store/tests/postgres/execution/mod.rs +++ b/backend/crates/gateway-store/tests/postgres/execution/mod.rs @@ -407,7 +407,6 @@ async fn core_adapter_should_persist_calculated_cost_exactly() { finalization.downstream_committed_at = Some(std::time::SystemTime::now()); finalization.websocket_pool = Some("reuse".to_owned()); finalization.service_tier = Some("priority".to_owned()); - finalization.upstream_response_model = Some("grok-4.6-build".to_owned()); finalization.cost = CalculatedCost::from_usd_ticks(12_345) .expect("calculated cost") .into_estimate(); @@ -452,10 +451,6 @@ async fn core_adapter_should_persist_calculated_cost_exactly() { detail.request.upstream_model_id.as_deref(), Some("grok-4.5") ); - assert_eq!( - detail.request.upstream_response_model.as_deref(), - Some("grok-4.6-build") - ); let page = observation .list_usage_records(UsageRecordQuery { status: Default::default(), @@ -471,10 +466,6 @@ async fn core_adapter_should_persist_calculated_cost_exactly() { .await .expect("request list"); assert_eq!(page.items.len(), 1); - assert_eq!( - page.items[0].upstream_response_model.as_deref(), - Some("grok-4.6-build") - ); database.close().await; } @@ -668,7 +659,6 @@ fn successful_core_finalization(id: &str) -> CoreModelRequestFinalization { upstream_transport: Some("websocket".to_owned()), http_version: Some("HTTP/2".to_owned()), websocket_pool: None, - upstream_response_model: None, service_tier: None, provider_metadata_json: None, error: None, @@ -1589,7 +1579,6 @@ pub(super) fn early_failure(request: &CoreNewModelRequest) -> CoreModelRequestFi upstream_transport: None, http_version: None, websocket_pool: None, - upstream_response_model: None, service_tier: None, provider_metadata_json: None, diagnostic_trace_json: trace.snapshot().map(|value| value.to_string()), diff --git a/backend/crates/providers/openai/src/provider/execution.rs b/backend/crates/providers/openai/src/provider/execution.rs index 07e21dc3b..055374672 100644 --- a/backend/crates/providers/openai/src/provider/execution.rs +++ b/backend/crates/providers/openai/src/provider/execution.rs @@ -966,7 +966,6 @@ pub(super) fn cold_response_stream(response: ColdResponse) -> EventStream { let mut decoder = CodexCanonicalDecoder::new(upstream_model.as_str()) .with_trace(trace.clone()) .with_pricing(context.pricing().get("openai").and_then(|models| models.get(upstream_model.as_str())).cloned()) - .with_reported_model(response.response_metadata.effective_model.as_deref()) .with_requested_service_tier(request.service_tier()) .with_request_tool_pricing(upstream_model.as_str(), request.tools()) .with_raw_sse_passthrough(); @@ -1025,7 +1024,6 @@ pub(super) fn cold_response_stream(response: ColdResponse) -> EventStream { response_metadata_updates.as_ref(), &mut session_capture, &mut observation_state, - &mut decoder, ) .await; let observation_event = if rate_limits_changed || metadata_merge.is_some() @@ -1103,12 +1101,9 @@ pub(super) fn cold_response_stream(response: ColdResponse) -> EventStream { response_metadata_updates.as_ref(), &mut session_capture, &mut observation_state, - &mut decoder, ) .await; let metadata_changed = metadata_merge.unwrap_or(false); - let response_model_changed = observation_state - .observe_upstream_response_model(decoder.response_model()); let service_tier_changed = observation_state .observe_upstream_service_tier(decoder.response_service_tier()); let terminal_failure = canonical_failure.map(|(error, semantic_output_seen)| { @@ -1196,7 +1191,6 @@ pub(super) fn cold_response_stream(response: ColdResponse) -> EventStream { })); } if (rate_limits_changed - || response_model_changed || service_tier_changed || timing_changed || metadata_changed @@ -1266,8 +1260,6 @@ pub(super) fn cold_response_stream(response: ColdResponse) -> EventStream { ) }); let timing_signals = decoder.take_timing_signals(); - let response_model_changed = observation_state - .observe_upstream_response_model(decoder.response_model()); let service_tier_changed = observation_state .observe_upstream_service_tier(decoder.response_service_tier()); let output_timing_changed = observation_state @@ -1302,7 +1294,6 @@ pub(super) fn cold_response_stream(response: ColdResponse) -> EventStream { response_metadata_updates.as_ref(), &mut session_capture, &mut observation_state, - &mut decoder, ) .await .unwrap_or(false); @@ -1328,8 +1319,7 @@ pub(super) fn cold_response_stream(response: ColdResponse) -> EventStream { .observe_cyber_policy_success(cyber_policy_scope.as_ref()) .await; } - if (response_model_changed - || service_tier_changed + if (service_tier_changed || timing_changed || rate_limits_changed || metadata_changed @@ -1371,26 +1361,10 @@ async fn merge_response_metadata_updates( updates: Option<&CodexResponseMetadataUpdates>, session_capture: &mut Option, observation_state: &mut OpenAiResponseObservationState, - decoder: &mut CodexCanonicalDecoder, ) -> Option { - let updates = updates?; - let mut pending = updates.lock().await; - let turn_state = pending.turn_state.take(); - let reported_model = pending.reported_model.clone(); - drop(pending); - if turn_state.is_none() && reported_model.is_none() { - return None; + let turn_state = updates?.lock().await.turn_state.take()?; + if let Some(capture) = session_capture.as_mut() { + capture.turn_state = Some(turn_state.clone()); } - let mut changed = false; - if let Some(turn_state) = turn_state { - if let Some(capture) = session_capture.as_mut() { - capture.turn_state = Some(turn_state.clone()); - } - changed |= observation_state.merge_client_header("x-codex-turn-state", &turn_state); - } - if let Some(model) = reported_model { - decoder.observe_reported_model(&model); - changed |= observation_state.observe_upstream_response_model(decoder.response_model()); - } - Some(changed) + Some(observation_state.merge_client_header("x-codex-turn-state", &turn_state)) } diff --git a/backend/crates/providers/openai/src/provider/observation.rs b/backend/crates/providers/openai/src/provider/observation.rs index b00e785d9..8b2aeee79 100644 --- a/backend/crates/providers/openai/src/provider/observation.rs +++ b/backend/crates/providers/openai/src/provider/observation.rs @@ -33,7 +33,6 @@ pub(super) struct OpenAiResponseObservationState { stream: bool, requested_service_tier: Option, upstream_service_tier: Option, - upstream_response_model: Option, rate_limit_headers: Vec<(String, String)>, timings: ProviderResponseTimings, terminal: Option, @@ -80,7 +79,6 @@ impl OpenAiResponseObservationState { stream: request.stream(), requested_service_tier: normalize_service_tier(request.service_tier()), upstream_service_tier: None, - upstream_response_model: response.response_metadata.effective_model.clone(), rate_limit_headers: selected_observation_headers(&response.rate_limit_headers), timings: openai_response_timings( &response.transport_metrics, @@ -123,23 +121,9 @@ impl OpenAiResponseObservationState { if let Some(service_tier) = &self.requested_service_tier { observation = observation.with_service_tier_if_valid(service_tier.clone()); } - if let Some(model) = &self.upstream_response_model { - observation = observation.with_upstream_response_model_if_valid(model); - } Some(observation) } - pub(super) fn observe_upstream_response_model(&mut self, model: Option<&str>) -> bool { - let Some(model) = model else { - return false; - }; - if self.upstream_response_model.as_deref() == Some(model) { - return false; - } - self.upstream_response_model = Some(model.to_owned()); - true - } - pub(super) fn observe_stream_chunk(&mut self, chunk: &[u8], started_at: Instant) -> bool { if chunk.is_empty() { return false; @@ -263,17 +247,6 @@ impl OpenAiResponseObservationState { { metadata.insert("websocketOpeningRequestId".to_owned(), json!(request_id)); } - if let Some(model) = self - .response_metadata - .effective_model - .as_deref() - .filter(|model| !model.is_empty()) - { - metadata.insert( - "upstreamReportedModel".to_owned(), - Value::String(model.to_owned()), - ); - } metadata.insert( "modelsEtag".to_owned(), self.response_metadata diff --git a/backend/crates/providers/openai/src/transport/canonical.rs b/backend/crates/providers/openai/src/transport/canonical.rs index 533ba58ce..f1b209a42 100644 --- a/backend/crates/providers/openai/src/transport/canonical.rs +++ b/backend/crates/providers/openai/src/transport/canonical.rs @@ -14,10 +14,7 @@ use gateway_core::event::{ }; use gateway_core::metering::Usage; use gateway_core::upstream::UpstreamSendState; -use gateway_protocol::openai::events::{ - ResponseModelObservation, TokenUsage, billable_usage_is_complete, extract_usage, - observed_model_name, -}; +use gateway_protocol::openai::events::{TokenUsage, billable_usage_is_complete, extract_usage}; use gateway_protocol::openai::sse::{SseEvent, SseEventDecoder, SseFrame, sse_frame_is_done}; use serde_json::Value; use thiserror::Error; @@ -59,8 +56,6 @@ pub struct CodexCanonicalDecoder { response_service_tier: Option, upstream_response_ms: Option, upstream_timing_metrics: ResponseTimingMetrics, - response_model: ResponseModelObservation, - reported_model: Option, web_search_pricing: Option, timing_signals: ResponseEventSignals, raw_sse_passthrough: bool, @@ -171,8 +166,6 @@ impl CodexCanonicalDecoder { response_service_tier: None, upstream_response_ms: None, upstream_timing_metrics: ResponseTimingMetrics::default(), - response_model: ResponseModelObservation::default(), - reported_model: None, web_search_pricing: None, pricing: None, timing_signals: ResponseEventSignals::default(), @@ -288,28 +281,6 @@ impl CodexCanonicalDecoder { self.upstream_timing_metrics } - /// 真实 HTTP 响应头提供初始报告;流内请求级报告可覆盖它 - #[must_use] - pub fn with_reported_model(mut self, model: Option<&str>) -> Self { - self.reported_model = model.and_then(observed_model_name).map(str::to_owned); - self - } - - /// 接收 transport 已解析的内部 metadata 报告,内部帧无需交付客户端 - pub(crate) fn observe_reported_model(&mut self, model: &str) { - if let Some(model) = observed_model_name(model) { - self.reported_model = Some(model.to_owned()); - } - } - - /// 官方服务端报告优先;缺少报告时仅使用正文明确声明,不使用请求兜底值 - #[must_use] - pub fn response_model(&self) -> Option<&str> { - self.reported_model - .as_deref() - .or_else(|| self.response_model.model()) - } - fn decode(&mut self, events: Vec) -> CodexCanonicalOutcome { let mut output = Vec::new(); for event in events { @@ -419,10 +390,6 @@ impl CodexCanonicalDecoder { } } self.observe_response_service_tier(&value); - self.response_model.observe(event_type, &value); - if let Some(model) = super::response_meta::reported_model_from_event(&value) { - self.reported_model = Some(model.to_owned()); - } let signals = response_event_signals(event_type, &value); self.merge_timing_signals(signals); if matches!(event_type, Some("response.failed" | "error")) { diff --git a/backend/crates/providers/openai/src/transport/response_meta.rs b/backend/crates/providers/openai/src/transport/response_meta.rs index 4f64a01da..b260deb45 100644 --- a/backend/crates/providers/openai/src/transport/response_meta.rs +++ b/backend/crates/providers/openai/src/transport/response_meta.rs @@ -11,8 +11,6 @@ use super::diagnostics::CodexUpstreamDiagnostics; /// Codex Responses 上游响应元数据 #[derive(Clone, Default, PartialEq, Eq)] pub struct CodexResponseMetadata { - /// 上游实际选用的模型 - pub effective_model: Option, /// 模型目录版本 pub models_etag: Option, /// 上游是否声明响应包含 reasoning @@ -25,7 +23,6 @@ impl fmt::Debug for CodexResponseMetadata { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("CodexResponseMetadata") - .field("effective_model", &self.effective_model) .field("models_etag", &self.models_etag) .field("reasoning_included", &self.reasoning_included) .field("client_header_count", &self.client_headers.len()) @@ -119,37 +116,12 @@ fn observe_typed_response_header(metadata: &mut CodexResponseMetadata, name: &st return; } match name.trim().to_ascii_lowercase().as_str() { - "openai-model" | "x-openai-model" => { - metadata.effective_model = - gateway_protocol::openai::events::observed_model_name(value).map(str::to_owned) - } "x-models-etag" => metadata.models_etag = Some(value.to_string()), "x-reasoning-included" => metadata.reasoning_included = true, _ => {} } } -/// 与官方 Codex 一致,优先读取 response.headers,再读取 WS metadata 的顶层 headers -pub(super) fn reported_model_from_event(value: &serde_json::Value) -> Option<&str> { - [value.pointer("/response/headers"), value.get("headers")] - .into_iter() - .flatten() - .filter_map(serde_json::Value::as_object) - .find_map(|headers| { - headers.iter().find_map(|(name, value)| { - if !name.eq_ignore_ascii_case("openai-model") - && !name.eq_ignore_ascii_case("x-openai-model") - { - return None; - } - value - .as_str() - .or_else(|| value.as_array()?.first()?.as_str()) - .and_then(gateway_protocol::openai::events::observed_model_name) - }) - }) -} - fn filter_client_headers( headers: impl IntoIterator, ) -> Vec<(String, Bytes)> { diff --git a/backend/crates/providers/openai/src/transport/websocket/exchange/mod.rs b/backend/crates/providers/openai/src/transport/websocket/exchange/mod.rs index 278fd1f01..50ce7b8b7 100644 --- a/backend/crates/providers/openai/src/transport/websocket/exchange/mod.rs +++ b/backend/crates/providers/openai/src/transport/websocket/exchange/mod.rs @@ -59,8 +59,6 @@ pub type CodexWebSocketRateLimitUpdates = Arc>>; pub struct CodexWebSocketResponseMetadataUpdate { /// 当前响应首次声明的会话续接状态 pub turn_state: Option, - /// 当前响应最新的服务端模型报告 - pub reported_model: Option, } pub type CodexWebSocketResponseMetadataUpdates = Arc>; @@ -72,8 +70,7 @@ pub(super) fn reusable_websocket_metadata( metadata.set_cookie_headers.clear(); metadata.rate_limit_headers.clear(); metadata.turn_state = None; - // 模型报告属于上一轮请求,池中连接不能把它带到下一轮缺失报告的响应 - metadata.response_metadata.effective_model = None; + // 请求级响应头不能随池中连接带到下一轮响应 metadata .response_metadata .client_headers diff --git a/backend/crates/providers/openai/src/transport/websocket/exchange/reducer.rs b/backend/crates/providers/openai/src/transport/websocket/exchange/reducer.rs index 7735185d0..fca8d3b3f 100644 --- a/backend/crates/providers/openai/src/transport/websocket/exchange/reducer.rs +++ b/backend/crates/providers/openai/src/transport/websocket/exchange/reducer.rs @@ -62,9 +62,6 @@ pub(super) fn reduce_websocket_event( &mut metadata.response_metadata, websocket_metadata_headers(&value), ); - if let Some(model) = response_meta::reported_model_from_event(&value) { - metadata.response_metadata.effective_model = Some(model.to_owned()); - } let turn_state_update = websocket_metadata_turn_state(&value).and_then(|turn_state| { if metadata.turn_state.is_some() { return None; diff --git a/backend/crates/providers/openai/src/transport/websocket/exchange/stream.rs b/backend/crates/providers/openai/src/transport/websocket/exchange/stream.rs index 6da84fd67..6cf896ac9 100644 --- a/backend/crates/providers/openai/src/transport/websocket/exchange/stream.rs +++ b/backend/crates/providers/openai/src/transport/websocket/exchange/stream.rs @@ -75,7 +75,6 @@ pub(in crate::transport::websocket) fn stream_websocket_response( let rate_limit_updates_for_task = Arc::clone(&rate_limit_updates); let response_metadata_updates = Arc::new(Mutex::new(CodexWebSocketResponseMetadataUpdate { turn_state: metadata.turn_state.clone(), - reported_model: None, })); let response_metadata_updates_for_task = Arc::clone(&response_metadata_updates); let (tx, rx) = mpsc::channel(WEBSOCKET_STREAM_BUFFER); @@ -306,9 +305,6 @@ async fn forward_websocket_response_stream(state: WebSocketStreamForwardState) { pending.turn_state = Some(turn_state); } } - if let Some(model) = metadata.response_metadata.effective_model.as_ref() { - response_metadata_updates.lock().await.reported_model = Some(model.clone()); - } let terminal = match reduced.action { ExchangeAction::RateLimits(rate_limits) => { rate_limit_updates.lock().await.push(rate_limits); diff --git a/backend/crates/providers/openai/tests/provider/contract/mod.rs b/backend/crates/providers/openai/tests/provider/contract/mod.rs index 9882433fb..69c174fb7 100644 --- a/backend/crates/providers/openai/tests/provider/contract/mod.rs +++ b/backend/crates/providers/openai/tests/provider/contract/mod.rs @@ -618,7 +618,7 @@ async fn attempt_middleware_can_change_reasoning_before_native_encoding() { } #[tokio::test] -async fn responses_bill_sent_model_and_observe_unpriced_response_without_rewriting_it() { +async fn responses_bill_sent_model_and_preserve_returned_model() { let store = Arc::new(MemoryAccountStore::default()); create_account(&store, "acct_provider_contract").await; let server = MockServer::start().await; @@ -655,7 +655,6 @@ async fn responses_bill_sent_model_and_observe_unpriced_response_without_rewriti ) .await .unwrap(); - let mut observed = None; let mut returned = None; let mut costs = Vec::new(); while let Some(event) = stream.next().await { @@ -665,9 +664,6 @@ async fn responses_bill_sent_model_and_observe_unpriced_response_without_rewriti costs.push(cost.total().amount().scaled()); } } - if let Some(observation) = event.response_observation() { - observed = observation.upstream_response_model().map(str::to_owned); - } if let Some(wire) = event.wire_event() && wire.data().get("type").and_then(Value::as_str) == Some("response.completed") { @@ -681,12 +677,11 @@ async fn responses_bill_sent_model_and_observe_unpriced_response_without_rewriti let requests = server.received_requests().await.expect("upstream requests"); assert_eq!(captured_request_body(&requests[0])["model"], "gpt-5.4"); assert_eq!(costs, vec![3_437_500]); - assert_eq!(observed.as_deref(), Some("gpt-6-sol")); - assert_eq!(returned, observed); + assert_eq!(returned.as_deref(), Some("gpt-6-sol")); } #[tokio::test] -async fn websocket_bills_sent_model_and_observes_internal_model_report() { +async fn websocket_bills_sent_model_and_preserves_response_metadata() { for metadata_type in ["response.metadata", "codex.response.metadata"] { let store = Arc::new(MemoryAccountStore::default()); create_account(&store, "acct_provider_contract").await; @@ -729,7 +724,6 @@ async fn websocket_bills_sent_model_and_observes_internal_model_report() { ) .await .expect("provider stream"); - let mut observed = None; let mut returned = None; let mut costs = Vec::new(); while let Some(event) = stream.next().await { @@ -739,9 +733,6 @@ async fn websocket_bills_sent_model_and_observes_internal_model_report() { costs.push(cost.total().amount().scaled()); } } - if let Some(observation) = event.response_observation() { - observed = observation.upstream_response_model().map(str::to_owned); - } if let Some(wire) = event.wire_event() && wire.data().get("type").and_then(Value::as_str) == Some("response.completed") { @@ -754,11 +745,6 @@ async fn websocket_bills_sent_model_and_observes_internal_model_report() { } server.await.expect("server task"); assert_eq!(costs, vec![3_437_500]); - assert_eq!( - observed.as_deref(), - Some("gpt-server-report"), - "{metadata_type}" - ); assert_eq!(returned.as_deref(), Some("gpt-6-astra")); } } @@ -4116,10 +4102,6 @@ async fn websocket_fast_path_miss_uses_http_and_keeps_background_preconnect() { ); } assert_eq!(metadata["upstreamStatus"], 200); - assert_eq!( - metadata["upstreamReportedModel"], - "gpt-reported-by-upstream" - ); assert!(metadata["requestSummary"].is_object()); assert!(metadata["upstreamTraceHeaders"].is_array()); first_decision = metadata diff --git a/backend/crates/providers/openai/tests/transport/canonical.rs b/backend/crates/providers/openai/tests/transport/canonical.rs index 169faf726..3df42dbbc 100644 --- a/backend/crates/providers/openai/tests/transport/canonical.rs +++ b/backend/crates/providers/openai/tests/transport/canonical.rs @@ -83,56 +83,6 @@ fn diagnostic_parse_failure_keeps_raw_passthrough_without_persisting_plaintext() assert!(!snapshot.to_string().contains("private-invalid-json")); } -#[test] -fn response_model_observation_prefers_reported_headers_without_changing_wire() { - for websocket in [false, true] { - let mut decoder = CodexCanonicalDecoder::new("requested") - .with_reported_model(Some("opening-model")) - .with_raw_sse_passthrough(); - for (value, expected) in [ - ( - json!({"type":"response.created","response":{"id":"resp_model","model":"body-model"}}), - "opening-model", - ), - ( - json!({"type":"codex.response.metadata","headers":{"X-OpenAI-Model":["metadata-model"]}}), - "metadata-model", - ), - ( - json!({"type":"response.completed","headers":{"openai-model":"top-level"},"response":{"id":"resp_model","model":"final-body-model","headers":{"OpenAI-Model":"terminal-model"}}}), - "terminal-model", - ), - ] { - let raw = value.to_string(); - let frame = if websocket { - websocket_event_to_sse_frame(&raw).expect("WS event") - } else { - format!("data: {raw}\n\n") - }; - let events = decoder - .push(frame.as_bytes()) - .expect("decode model observation"); - assert_eq!(decoder.response_model(), Some(expected)); - assert!( - events - .iter() - .any(|event| event.wire_event().is_some_and(|wire| wire.data() == &value)) - ); - } - } -} - -#[test] -fn response_model_observation_uses_explicit_body_and_never_request_fallback() { - let mut decoder = CodexCanonicalDecoder::new("requested"); - decoder - .push(b"data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_model\"}}\n\n") - .expect("response model event"); - assert_eq!(decoder.response_model(), None); - decoder.push(b"data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_model\",\"model\":\"returned\"}}\n\n").expect("response model event"); - assert_eq!(decoder.response_model(), Some("returned")); -} - const METADATA_PREFIX_FIXTURE: &str = include_str!("fixtures/metadata_only_prefix.sse"); #[test] @@ -475,7 +425,6 @@ fn decoder_should_bill_the_sent_model_independently_of_the_response_model() { _ => None, }); assert_eq!(cost, expected_cost, "{sent} -> {returned:?}"); - assert_eq!(decoder.response_model(), returned); assert!(canonical_facts(&events).into_iter().any(|event| matches!( event, GatewayEvent::Completed(meta) if meta.model() == Some(returned.unwrap_or(sent)) diff --git a/backend/crates/providers/openai/tests/transport/http_client.rs b/backend/crates/providers/openai/tests/transport/http_client.rs index f1b64917d..fa8cc4f0b 100644 --- a/backend/crates/providers/openai/tests/transport/http_client.rs +++ b/backend/crates/providers/openai/tests/transport/http_client.rs @@ -431,10 +431,6 @@ async fn codex_backend_client_should_capture_forwardable_response_metadata() { .await .expect("response should succeed"); - assert_eq!( - response.response_metadata.effective_model.as_deref(), - Some("gpt-5.5-2026-07-01") - ); assert_eq!( response.response_metadata.models_etag.as_deref(), Some("models-v2") diff --git a/backend/crates/providers/openai/tests/transport/mod.rs b/backend/crates/providers/openai/tests/transport/mod.rs index 4f7209b1b..5594fe1c9 100644 --- a/backend/crates/providers/openai/tests/transport/mod.rs +++ b/backend/crates/providers/openai/tests/transport/mod.rs @@ -180,7 +180,6 @@ struct CollectedBackendResponse { rate_limit_headers: Vec<(String, String)>, websocket_pool_decision: Option, response_metadata: CodexResponseMetadata, - reported_model: Option, transport_metrics: CodexTransportMetrics, connection_local_continuation: bool, } @@ -286,11 +285,9 @@ async fn collect_backend_response( rate_limit_headers.extend(rate_limits_to_header_pairs(update)); } } - let mut reported_model = response_metadata.effective_model.clone(); if let Some(update) = response_metadata_updates { let update = update.lock().await; turn_state = update.turn_state.clone().or(turn_state); - reported_model = update.reported_model.clone().or(reported_model); } let body = String::from_utf8_lossy(&body_bytes).into_owned(); let usage = extract_sse_usage(&body).map_err(CodexClientError::InvalidSse)?; @@ -303,7 +300,6 @@ async fn collect_backend_response( rate_limit_headers, websocket_pool_decision, response_metadata, - reported_model, transport_metrics, connection_local_continuation, }) diff --git a/backend/crates/providers/openai/tests/transport/websocket/metadata.rs b/backend/crates/providers/openai/tests/transport/websocket/metadata.rs index 0e0b5341c..320887d05 100644 --- a/backend/crates/providers/openai/tests/transport/websocket/metadata.rs +++ b/backend/crates/providers/openai/tests/transport/websocket/metadata.rs @@ -80,7 +80,6 @@ async fn reused_websocket_should_keep_response_metadata_scoped_to_each_exchange( .expect("response within timeout") .expect("response should complete on the same socket"); assert_eq!(response.response_metadata, expected, "exchange {round}"); - assert_eq!(response.reported_model, Some(format!("model-{round}"))); assert!(response.body.contains(&format!("etag-{round}"))); assert_eq!(response.turn_state, Some(format!("turn-{round}"))); if round > 0 { diff --git a/backend/crates/providers/openai/tests/transport/websocket/mod.rs b/backend/crates/providers/openai/tests/transport/websocket/mod.rs index c77af8cb8..ae5b89fcb 100644 --- a/backend/crates/providers/openai/tests/transport/websocket/mod.rs +++ b/backend/crates/providers/openai/tests/transport/websocket/mod.rs @@ -993,10 +993,6 @@ async fn websocket_execute_response_create_request_should_capture_internal_metad server.await.unwrap(); assert_eq!(response.turn_state.as_deref(), Some("turn-from-metadata")); - assert_eq!( - response.reported_model.as_deref(), - Some("gpt-internal-report") - ); assert!(!response.body.contains("codex.rate_limits")); assert!(response.body.contains("response.metadata")); assert!(response.body.contains("event: response.completed")); @@ -1132,11 +1128,6 @@ async fn reused_websocket_should_not_leak_turn_state_into_the_next_response() { .websocket_pool_decision .is_some_and(WebSocketPoolDecision::is_reuse) ); - assert_eq!( - first.reported_model.as_deref(), - Some("first-response-model") - ); - assert_eq!(second.reported_model, None); assert_eq!(second.turn_state, None); assert!( second diff --git a/backend/crates/providers/xai/src/provider/stream.rs b/backend/crates/providers/xai/src/provider/stream.rs index 53664d298..5073ad7e3 100644 --- a/backend/crates/providers/xai/src/provider/stream.rs +++ b/backend/crates/providers/xai/src/provider/stream.rs @@ -322,10 +322,6 @@ pub(super) fn cold_compaction_http_sse_stream( } } - if let Some(model) = canonical.response_model() { - observation = observation.with_upstream_response_model_if_valid(model); - yield ProviderEvent::observation(observation.clone()); - } if let Some(tier) = canonical.response_service_tier() { observation = observation.with_service_tier_if_valid(tier.to_owned()); yield ProviderEvent::observation(observation); @@ -606,12 +602,6 @@ pub(super) fn cold_http_sse_stream( if observe_output_timings(&mut observation, &events, context.timing_started_at()) { yield ProviderEvent::observation(observation.clone()); } - if let Some(model) = decoder.response_model() - && observation.upstream_response_model() != Some(model) - { - observation = observation.with_upstream_response_model_if_valid(model); - yield ProviderEvent::observation(observation.clone()); - } if let Some(tier) = decoder.response_service_tier() && observation.service_tier() != Some(tier) { @@ -667,12 +657,6 @@ pub(super) fn cold_http_sse_stream( .iter() .flat_map(ProviderEvent::canonical_facts) .any(|event| matches!(event, GatewayEvent::Completed(_))); - if let Some(model) = decoder.response_model() - && observation.upstream_response_model() != Some(model) - { - observation = observation.with_upstream_response_model_if_valid(model); - yield ProviderEvent::observation(observation.clone()); - } if let Some(tier) = decoder.response_service_tier() && observation.service_tier() != Some(tier) { diff --git a/backend/crates/providers/xai/src/transport/canonical.rs b/backend/crates/providers/xai/src/transport/canonical.rs index 9891e5ff0..9b9cfdd82 100644 --- a/backend/crates/providers/xai/src/transport/canonical.rs +++ b/backend/crates/providers/xai/src/transport/canonical.rs @@ -15,9 +15,7 @@ use gateway_core::metering::{ CurrencyCode, Decimal, Money, ProviderReportedCost, Usage, }; use gateway_core::upstream::UpstreamSendState; -use gateway_protocol::openai::events::{ - ResponseModelObservation, TokenUsage, billable_usage_is_complete, extract_usage, -}; +use gateway_protocol::openai::events::{TokenUsage, billable_usage_is_complete, extract_usage}; use gateway_protocol::openai::sse::{SseEvent, SseEventDecoder}; use serde_json::Value; @@ -295,7 +293,6 @@ pub struct GrokCanonicalDecoder { tool_arguments_seen: BTreeSet, usage_emitted: bool, response_service_tier: Option, - response_model: ResponseModelObservation, requires_provider_cost: bool, } @@ -372,7 +369,6 @@ impl GrokCanonicalDecoder { tool_arguments_seen: BTreeSet::new(), usage_emitted: false, response_service_tier: None, - response_model: ResponseModelObservation::default(), requires_provider_cost: false, } } @@ -416,12 +412,6 @@ impl GrokCanonicalDecoder { self.response_service_tier.as_deref() } - /// 返回原始上游响应声明的模型,缺失时不使用请求模型补齐 - #[must_use] - pub fn response_model(&self) -> Option<&str> { - self.response_model.model() - } - pub fn push(&mut self, chunk: &[u8]) -> Result, ProviderError> { let events = self.decoder.push(chunk).map_err(protocol_error)?; self.decode(events).map(|batch| batch.projected_events) @@ -522,9 +512,6 @@ impl GrokCanonicalDecoder { event.retry, ) .map_err(|_| protocol_error_marker())?; - if !event_type.is_empty() { - self.response_model.observe(Some(&event_type), &value); - } // 工具转换可能隐藏注入的调用,计费事实必须从转换前的上游事件读取 if let Some(response) = value.get("response") { if let Some(tier) = response.get("service_tier").and_then(Value::as_str) { diff --git a/backend/crates/providers/xai/tests/transport/canonical.rs b/backend/crates/providers/xai/tests/transport/canonical.rs index d710d20f8..bc966c5fd 100644 --- a/backend/crates/providers/xai/tests/transport/canonical.rs +++ b/backend/crates/providers/xai/tests/transport/canonical.rs @@ -50,7 +50,6 @@ fn decoder_should_bill_the_sent_model_independently_of_the_response_model() { provider_cost_ticks(&facts), reported_cost.filter(|ticks| *ticks > 0) ); - assert_eq!(decoder.response_model(), returned); assert!(facts.iter().any(|event| matches!( event, GatewayEvent::Completed(meta) if meta.model() == Some(returned.unwrap_or(sent)) @@ -58,18 +57,6 @@ fn decoder_should_bill_the_sent_model_independently_of_the_response_model() { } } -#[test] -fn response_model_observation_preserves_raw_model_and_missing_values() { - for model in [None, Some("grok-4.6-build")] { - let mut decoder = GrokCanonicalDecoder::new("grok-4.6"); - let body = serde_json::json!({"type":"response.created","response":{"id":"resp_model","model":model}}); - decoder - .push(format!("data: {body}\n\n").as_bytes()) - .expect("decode created"); - assert_eq!(decoder.response_model(), model); - } -} - fn terminal_cost_events( model: &str, input_tokens: u64, diff --git a/docs/api.md b/docs/api.md index 624b892b2..a25c80408 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2099,12 +2099,7 @@ Dashboard 返回原始 `asOf` 及 `asOfDisplay`,作为当前数据的查询锚 详情不出现在 Key 用量接口和普通错误信封中;[诊断导出](#诊断与恢复关联)在服务端重新脱敏后包含已存详情。 记录仍受异步观测写入与保留周期约束 -管理端请求列表与详情分别保留 `requestedModel`(客户端请求)、`upstreamModel`(网关发送)与 -`upstreamResponseModel`(上游返回)。返回模型缺失时为 `null`,不使用请求或映射模型补齐。 -OpenAI 优先采用服务端 `openai-model` / `x-openai-model` 报告(流内报告可覆盖初始响应头), -没有报告时采用正文明确声明的 `response.model`;xAI 采用原始正文声明。正文模型以终态优先, -缺少终态声明时保留首次声明。这些值仅表示上游报告,不作为模型真实性证明,也不参与路由、 -聚合或本地计价模型选择。没有保存上游模型报告的记录保持未知 +管理端请求列表与详情保留 `requestedModel`(客户端请求)与 `upstreamModel`(网关发送) 请求明细的 `clientApiKeyName`、`accountName` 按字面量包含匹配请求保存的历史名称快照,区分大小写, 不解析为 ID,也不依赖当前配置是否存在。`model` 按字面量包含匹配请求模型或上游模型,`%`、`_` 和 `\` 按普通字符处理 diff --git a/frontend/src/api/modules/usage.ts b/frontend/src/api/modules/usage.ts index 7bd585d81..a48b00b23 100644 --- a/frontend/src/api/modules/usage.ts +++ b/frontend/src/api/modules/usage.ts @@ -98,7 +98,6 @@ export interface UsageListRecord { model: string | null requestedModel: string | null upstreamModel: string | null - upstreamResponseModel: string | null serviceTier: string | null clientTransport: string upstreamTransport: string | null @@ -131,7 +130,6 @@ export interface UsageRecord { model: string | null requestedModel: string | null upstreamModel: string | null - upstreamResponseModel: string | null serviceTier: string | null statusCode: number | null clientTransport: string diff --git a/frontend/src/components/AppLanguageMenu.vue b/frontend/src/components/AppLanguageMenu.vue index 1b1b1e30e..c4d667757 100644 --- a/frontend/src/components/AppLanguageMenu.vue +++ b/frontend/src/components/AppLanguageMenu.vue @@ -2,7 +2,7 @@ import type { IconButtonProps } from '@codex-proxy/ui' import type { Locale } from '@/i18n/locale' import { ZIconButton, ZMenuItem, ZPopover } from '@codex-proxy/ui' -import { Check, Languages } from '@lucide/vue' +import { Check, Languages, LoaderCircle } from '@lucide/vue' import chineseFlag from 'circle-flags/flags/cn.svg?url&no-inline' import englishFlag from 'circle-flags/flags/us.svg?url&no-inline' import { computed, shallowRef, useTemplateRef } from 'vue' @@ -61,7 +61,8 @@ async function selectLocale(value: Locale, close: () => void) { v-for="option in localeOptions" :key="option.value" :aria-pressed="option.value === locale" - :loading="pendingLocale === option.value" + :disabled="pendingLocale === option.value" + :aria-busy="pendingLocale === option.value || undefined" class="aria-pressed:bg-cp-control-item-bg-active aria-pressed:text-cp-primary-text aria-pressed:hover:bg-cp-control-item-bg-active" @click="selectLocale(option.value, close)" > @@ -70,7 +71,10 @@ async function selectLocale(value: Locale, close: () => void) { {{ option.label }} - diff --git a/frontend/src/components/usage/UsageModelCell.vue b/frontend/src/components/usage/UsageModelCell.vue index b79e14405..ae1bdfd72 100644 --- a/frontend/src/components/usage/UsageModelCell.vue +++ b/frontend/src/components/usage/UsageModelCell.vue @@ -24,19 +24,17 @@ const { t } = useI18n({ useScope: 'global' }) {{ modelDisplay.primary }}
diff --git a/frontend/src/components/usage/shared/columns.ts b/frontend/src/components/usage/shared/columns.ts index 88d5609ba..85e1a64c4 100644 --- a/frontend/src/components/usage/shared/columns.ts +++ b/frontend/src/components/usage/shared/columns.ts @@ -6,7 +6,7 @@ import { t } from '@/i18n' import { formatProviderLabel } from '@/utils/providers' export const usageDataColumns = computed(() => ([ - { key: 'model', label: t('usage.fields.model'), kind: 'custom', size: 'xl' }, + { key: 'model', label: t('usage.fields.model'), kind: 'custom', size: 'lg' }, { key: 'reasoningEffort', label: t('usage.fields.reasoningEffort'), kind: 'status', size: 'md', align: 'left' }, { key: 'route', label: t('usage.fields.endpoint'), kind: 'mono' }, { key: 'upstreamTransport', label: t('usage.fields.upstream'), kind: 'status', size: 'md' }, diff --git a/frontend/src/components/usage/shared/presenter.ts b/frontend/src/components/usage/shared/presenter.ts index c928dea7c..fdfb2b852 100644 --- a/frontend/src/components/usage/shared/presenter.ts +++ b/frontend/src/components/usage/shared/presenter.ts @@ -10,7 +10,7 @@ export type UsagePerformanceRecord = Pick | null } -export type UsageModelRecord = Pick & Partial> +export type UsageModelRecord = Pick & Partial> export type UsageReasoningRecord = Pick & Partial> interface UsageTimingItem { @@ -93,27 +93,7 @@ export function usageModelDisplay(record: UsageModelRecord) { ? storedModel : '' - const responseModel = record.upstreamResponseModel || '' - const returned = responseModel && responseModel !== primary ? responseModel : '' - const routes = [] - if (secondary && secondary !== returned) { - routes.push({ - model: secondary, - kind: 'mapped' as const, - description: t('usage.status.mappedModel', { secondary }), - }) - } - if (returned) { - routes.push({ - model: returned, - kind: 'returned' as const, - description: returned === secondary - ? t('usage.status.returnedModel', { returned }) - : t('usage.status.returnedModelHelp', { returned }), - }) - } - - return { primary, secondary, routes } + return { primary, secondary } } export function usagePerformanceDetails(record: UsagePerformanceRecord) { diff --git a/frontend/src/i18n/locales/en/usage.json b/frontend/src/i18n/locales/en/usage.json index 9c7df9a0a..f0d4a3b89 100644 --- a/frontend/src/i18n/locales/en/usage.json +++ b/frontend/src/i18n/locales/en/usage.json @@ -82,6 +82,7 @@ "reviewSubagentRequest": "Review subagent request" }, "filters": { + "modelPlaceholder": "Filter by model", "error": "Error", "requestDetailType": "Request detail type", "label": "Usage record filters and actions", @@ -127,8 +128,6 @@ "future": "Future", "status": { "mappedModel": "Model sent upstream after gateway mapping: {secondary}", - "returnedModel": "Upstream response model: {returned} (matches the mapped request model)", - "returnedModelHelp": "Model returned by upstream: {returned}", "firstOutputWaiting": "First output", "reasoningToText": "Reasoning → Text", "firstOneIsOutputToText": "First output → Text", @@ -168,6 +167,7 @@ "requestsPerMinute": "RPM" }, "header": { + "title": "Analytics", "currentKey": "Current key", "keyPrefix": "Key prefix", "keyConfiguration": "Key configuration", @@ -201,8 +201,8 @@ "clickRefreshAtTopToTryAgain": ", please click refresh at the top to try again", "description": "View usage, performance trends, and request errors", "dateRangeLabel": "Statistics date range, limited to the last 31 days", - "requestDetails": "Request details", - "recordsDescription": "Successful and failed request details", + "requestDetails": "Request log", + "recordsDescription": "Successful and failed request records", "failedToLoad": "Failed to load: {error}", "noRequestsMatchTheseFilters": "No requests match these filters", "viewErrorDetails": "View error details", @@ -310,7 +310,6 @@ "message": "Message", "upstreamTimeConsumption": "Upstream time consumption", "storageModel": "Storage model", - "upstreamReturnModel": "Upstream return model", "clientIp": "Client IP", "eventType": "Event type", "trySerialNumber": "Try serial number", diff --git a/frontend/src/i18n/locales/zh-Hans/usage.json b/frontend/src/i18n/locales/zh-Hans/usage.json index d0a626c28..e8327a7bf 100644 --- a/frontend/src/i18n/locales/zh-Hans/usage.json +++ b/frontend/src/i18n/locales/zh-Hans/usage.json @@ -82,6 +82,7 @@ "reviewSubagentRequest": "Review 子代理请求" }, "filters": { + "modelPlaceholder": "按模型筛选", "error": "错误", "requestDetailType": "请求明细类型", "label": "使用记录筛选与操作", @@ -127,8 +128,6 @@ "future": "未来", "status": { "mappedModel": "网关映射后发送给上游的模型:{secondary}", - "returnedModel": "上游返回模型:{returned}(与网关映射后发送的模型一致)", - "returnedModelHelp": "上游返回模型:{returned}", "firstOutputWaiting": "首个输出等待", "reasoningToText": "推理到正文", "firstOneIsOutputToText": "首个输出到正文", @@ -168,6 +167,7 @@ "requestsPerMinute": "RPM" }, "header": { + "title": "使用统计", "currentKey": "当前密钥", "keyPrefix": "密钥前缀", "keyConfiguration": "密钥配置", @@ -201,8 +201,8 @@ "clickRefreshAtTopToTryAgain": ",请点击顶部刷新重试", "description": "查看请求用量、性能趋势与调用错误记录", "dateRangeLabel": "统计日期范围,仅可选择最近 31 天", - "requestDetails": "请求明细", - "recordsDescription": "成功请求与失败请求明细", + "requestDetails": "请求记录", + "recordsDescription": "成功请求与失败请求记录", "failedToLoad": "加载失败:{error}", "noRequestsMatchTheseFilters": "所选条件下暂无请求记录", "viewErrorDetails": "查看错误详情", @@ -310,7 +310,6 @@ "message": "消息", "upstreamTimeConsumption": "上游耗时", "storageModel": "存储模型", - "upstreamReturnModel": "上游返回模型", "clientIp": "客户端 IP", "eventType": "事件类型", "trySerialNumber": "尝试序号", diff --git a/frontend/src/i18n/locales/zh-Hant/usage.json b/frontend/src/i18n/locales/zh-Hant/usage.json index 663072b89..30b9d4217 100644 --- a/frontend/src/i18n/locales/zh-Hant/usage.json +++ b/frontend/src/i18n/locales/zh-Hant/usage.json @@ -82,6 +82,7 @@ "reviewSubagentRequest": "Review 子代理請求" }, "filters": { + "modelPlaceholder": "按模型篩選", "error": "錯誤", "requestDetailType": "請求明細類型", "label": "使用記錄篩選與操作", @@ -127,8 +128,6 @@ "future": "未來", "status": { "mappedModel": "網關映射後發送給上游的模型:{secondary}", - "returnedModel": "上游返回模型:{returned}(與網關映射後發送的模型一致)", - "returnedModelHelp": "上游返回模型:{returned}", "firstOutputWaiting": "首個輸出等待", "reasoningToText": "推理到正文", "firstOneIsOutputToText": "首個輸出到正文", @@ -168,6 +167,7 @@ "requestsPerMinute": "RPM" }, "header": { + "title": "使用統計", "currentKey": "當前密鑰", "keyPrefix": "密鑰前綴", "keyConfiguration": "密鑰配置", @@ -201,8 +201,8 @@ "clickRefreshAtTopToTryAgain": ",請點擊頂部刷新重試", "description": "查看請求用量、性能趨勢與調用錯誤記錄", "dateRangeLabel": "統計日期範圍,僅可選擇最近 31 天", - "requestDetails": "請求明細", - "recordsDescription": "成功請求與失敗請求明細", + "requestDetails": "請求記錄", + "recordsDescription": "成功請求與失敗請求記錄", "failedToLoad": "加載失敗:{error}", "noRequestsMatchTheseFilters": "所選條件下暫無請求記錄", "viewErrorDetails": "查看錯誤詳情", @@ -310,7 +310,6 @@ "message": "消息", "upstreamTimeConsumption": "上游耗時", "storageModel": "存儲模型", - "upstreamReturnModel": "上游返回模型", "clientIp": "客戶端 IP", "eventType": "事件類型", "trySerialNumber": "嘗試序號", diff --git a/frontend/src/layout/components/AppSidebar.vue b/frontend/src/layout/components/AppSidebar.vue index a9bb59143..72e4e17f6 100644 --- a/frontend/src/layout/components/AppSidebar.vue +++ b/frontend/src/layout/components/AppSidebar.vue @@ -440,7 +440,7 @@ onBeforeUnmount(() => {