From 85b1c13c06c3c1763907b0f835e2b55dc10cfe2b Mon Sep 17 00:00:00 2001
From: Yongzhao Chen
Date: Tue, 22 Sep 2026 21:27:47 +0200
Subject: [PATCH 1/5] feat(usage): show per-model usage for client keys
Add published GPT-6 Sol and Luna pricing for estimated costs.
Co-authored-by: Codex
---
README.md | 2 +-
.../gateway-admin/src/model/key_usage.rs | 4 +-
.../gateway-admin/src/use_case/key_usage.rs | 10 +++--
.../gateway-api/src/key_usage/presenter.rs | 27 ++++++++++++
backend/crates/gateway-api/tests/admin/mod.rs | 12 ++++--
.../gateway-api/tests/key_usage/fixtures.rs | 29 +++++++++++--
.../crates/gateway-api/tests/key_usage/mod.rs | 23 +++++++++++
.../postgres/observability/queries/usage.rs | 9 +++-
.../providers/openai/src/transport/usage.rs | 26 +++++++++++-
.../providers/openai/tests/transport/usage.rs | 33 +++++++++++++++
docs/api.md | 2 +-
frontend/src/api/modules/key-usage.ts | 9 ++++
.../key-usage/components/KeyUsageModels.vue | 41 +++++++++++++++++++
frontend/src/views/key-usage/index.vue | 2 +
14 files changed, 214 insertions(+), 15 deletions(-)
create mode 100644 frontend/src/views/key-usage/components/KeyUsageModels.vue
diff --git a/README.md b/README.md
index 4d8dffeab..3078bd401 100644
--- a/README.md
+++ b/README.md
@@ -75,7 +75,7 @@ INSTALL_DIR="$HOME/services/codex-proxy-rs" bash install.sh
### 登录管理端
部署完成后,打开 `http://127.0.0.1:8080`,使用 `admin@cpr.local` 和管理员密码登录。
-API Key 持有者可在同一登录页切换登录身份,进入 `/key-usage` 查看自己的用量、趋势、请求日志、额度与健康时间线;不能访问管理员页面。
+API Key 持有者可在同一登录页切换登录身份,进入 `/key-usage` 查看自己的用量、按模型统计的 Token 与估算费用、趋势、请求日志、额度与健康时间线;不能访问管理员页面。
页面右上角的「密钥配置」支持复制 Codex 配置文件和导入 CCSwitch,导入时同时启用当前 Key 的日/周额度查询,默认刷新间隔为 30 分钟
默认地址只能在服务器本机访问,从其他设备使用时,需要配置
diff --git a/backend/crates/gateway-admin/src/model/key_usage.rs b/backend/crates/gateway-admin/src/model/key_usage.rs
index 8c37735af..29a6780ce 100644
--- a/backend/crates/gateway-admin/src/model/key_usage.rs
+++ b/backend/crates/gateway-admin/src/model/key_usage.rs
@@ -4,7 +4,8 @@ use super::{
PageSize,
client_keys::ClientKeyRecord,
observability::{
- HealthTimeline, OpsErrorPage, RequestMetricPoint, TimeRange, UsageOverview, UsagePage,
+ DiagnosticObservation, HealthTimeline, OpsErrorPage, RequestMetricPoint, TimeRange,
+ UsageOverview, UsagePage,
},
};
@@ -32,6 +33,7 @@ pub struct KeyUsageOverview {
pub key: ClientKeyRecord,
pub overview: UsageOverview,
pub trend: Vec,
+ pub models: Vec,
pub health_timeline: HealthTimeline,
}
diff --git a/backend/crates/gateway-admin/src/use_case/key_usage.rs b/backend/crates/gateway-admin/src/use_case/key_usage.rs
index 0aa53668d..26eacd011 100644
--- a/backend/crates/gateway-admin/src/use_case/key_usage.rs
+++ b/backend/crates/gateway-admin/src/use_case/key_usage.rs
@@ -16,7 +16,8 @@ use crate::{
KeyUsageRecordsQuery,
},
observability::{
- OpsErrorFilter, OpsErrorQuery, TimeRange, UsageFilter, UsageQuery, china_day_start,
+ DiagnosticDimension, OpsErrorFilter, OpsErrorQuery, TimeRange, UsageFilter, UsageQuery,
+ china_day_start,
},
system::SystemVersion,
},
@@ -171,9 +172,11 @@ impl KeyUsageService for DefaultKeyUsageService {
start: china_day_start(now),
end: now,
};
- let (overview, trend, health_points) = futures::try_join!(
+ let (overview, trend, models, health_points) = futures::try_join!(
self.observations.usage_summary(query.range, filter.clone()),
- self.observations.usage_trend(query.range, filter),
+ self.observations.usage_trend(query.range, filter.clone()),
+ self.observations
+ .usage_diagnostics(query.range, filter, DiagnosticDimension::Model),
self.observations
.usage_trend(today, usage_filter(&id, None)),
)
@@ -182,6 +185,7 @@ impl KeyUsageService for DefaultKeyUsageService {
key,
overview,
trend,
+ models,
health_timeline: health_timeline_at(&health_points, now),
}))
}
diff --git a/backend/crates/gateway-api/src/key_usage/presenter.rs b/backend/crates/gateway-api/src/key_usage/presenter.rs
index 11d118080..fceabf6b3 100644
--- a/backend/crates/gateway-api/src/key_usage/presenter.rs
+++ b/backend/crates/gateway-api/src/key_usage/presenter.rs
@@ -54,6 +54,7 @@ pub(super) struct OverviewView {
end_time: DateTime,
key: KeyView,
summary: MetricsView,
+ models: Vec,
trend: Vec,
health_timeline: HealthTimelineView,
}
@@ -87,6 +88,16 @@ struct MetricsView {
cost_incomplete: bool,
}
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+struct ModelUsageView {
+ model: String,
+ requests: u64,
+ total_tokens: u64,
+ cost_usd: Option,
+ cost_incomplete: bool,
+}
+
fn metrics(value: &RequestMetrics, costs: &[CurrencyCost], coverage: &CostCoverage) -> MetricsView {
MetricsView {
requests: value.request_count,
@@ -137,6 +148,22 @@ pub(super) fn overview(value: KeyUsageOverview) -> OverviewView {
&value.overview.attempts.costs,
&value.overview.attempts.cost_coverage,
),
+ models: value
+ .models
+ .into_iter()
+ .map(|model| ModelUsageView {
+ model: model.name,
+ requests: model.request_count,
+ total_tokens: model.total_tokens,
+ cost_usd: model
+ .costs
+ .iter()
+ .find(|cost| cost.currency.eq_ignore_ascii_case("USD"))
+ .map(|cost| cost.amount.to_string()),
+ cost_incomplete: model.cost_coverage.partial_count > 0
+ || model.cost_coverage.unavailable_count > 0,
+ })
+ .collect(),
trend: value
.trend
.into_iter()
diff --git a/backend/crates/gateway-api/tests/admin/mod.rs b/backend/crates/gateway-api/tests/admin/mod.rs
index e7475c8cf..1b762c3d9 100644
--- a/backend/crates/gateway-api/tests/admin/mod.rs
+++ b/backend/crates/gateway-api/tests/admin/mod.rs
@@ -587,6 +587,7 @@ pub(super) struct MemoryObservations {
pub trend: Option>,
pub summaries: Vec<(TimeRange, UsageFilter)>,
pub trends: Vec<(TimeRange, UsageFilter)>,
+ pub diagnostic_queries: Vec<(TimeRange, UsageFilter, DiagnosticDimension)>,
pub records: Vec,
pub errors: Vec,
}
@@ -1227,10 +1228,15 @@ impl ObservabilityStore for UnusedStore {
async fn usage_diagnostics(
&self,
- _: TimeRange,
- _: UsageFilter,
- _: DiagnosticDimension,
+ range: TimeRange,
+ filter: UsageFilter,
+ dimension: DiagnosticDimension,
) -> AdminStoreResult> {
+ self.observations
+ .lock()
+ .expect("observations")
+ .diagnostic_queries
+ .push((range, filter, dimension));
Ok(self.diagnostics.lock().expect("diagnostics").clone())
}
diff --git a/backend/crates/gateway-api/tests/key_usage/fixtures.rs b/backend/crates/gateway-api/tests/key_usage/fixtures.rs
index 56200d723..6d7a89f52 100644
--- a/backend/crates/gateway-api/tests/key_usage/fixtures.rs
+++ b/backend/crates/gateway-api/tests/key_usage/fixtures.rs
@@ -3,9 +3,9 @@ use chrono::{Duration, Utc};
use gateway_admin::model::{
client_keys::ClientKeyRecord,
observability::{
- AttemptMetrics, CostCoverage, CurrencyCost, DecimalAmount, Granularity, OpsError,
- RequestMetricPoint, RequestMetrics, TimeRange, UsageBilling, UsageListRecord,
- UsageOverview,
+ AttemptMetrics, CostCoverage, CurrencyCost, DecimalAmount, DiagnosticObservation,
+ Granularity, OpsError, RequestMetricPoint, RequestMetrics, TimeRange, UsageBilling,
+ UsageListRecord, UsageOverview,
},
};
use gateway_core::{
@@ -85,6 +85,29 @@ pub(super) async fn fixture() -> AdminTestFixture {
}]);
}
fixture.usage_records.lock().unwrap().push(usage_record());
+ fixture.diagnostics.lock().unwrap().push(DiagnosticObservation {
+ key: "coding".to_owned(),
+ name: "coding".to_owned(),
+ request_count: 2,
+ success_count: 1,
+ failure_count: 1,
+ attempt_count: 2,
+ total_tokens: 1100,
+ average_latency_ms: None,
+ latency_p95_ms: None,
+ first_token_p95_ms: None,
+ non_completion_count: 0,
+ retry_count: 0,
+ cost_coverage: CostCoverage {
+ calculated_count: 1,
+ unavailable_count: 1,
+ ..Default::default()
+ },
+ costs: vec![CurrencyCost {
+ currency: "USD".to_owned(),
+ amount: "0.123456".parse().unwrap(),
+ }],
+ });
fixture.ops_errors.lock().unwrap().push(error_record());
fixture
}
diff --git a/backend/crates/gateway-api/tests/key_usage/mod.rs b/backend/crates/gateway-api/tests/key_usage/mod.rs
index 43a6051e9..249bc2d7c 100644
--- a/backend/crates/gateway-api/tests/key_usage/mod.rs
+++ b/backend/crates/gateway-api/tests/key_usage/mod.rs
@@ -176,6 +176,7 @@ async fn overview_scopes_every_query_and_projects_only_key_visible_fields() {
"endTime",
"key",
"summary",
+ "models",
"trend",
"healthTimeline",
],
@@ -202,6 +203,16 @@ async fn overview_scopes_every_query_and_projects_only_key_visible_fields() {
assert_eq!(data["trend"][0]["reasoningTokens"], 40);
assert_eq!(data["summary"]["costUsd"], "0.123456");
assert_eq!(data["summary"]["costIncomplete"], true);
+ assert_eq!(
+ data["models"],
+ json!([{
+ "model": "coding",
+ "requests": 2,
+ "totalTokens": 1100,
+ "costUsd": "0.123456",
+ "costIncomplete": true,
+ }])
+ );
assert_eq!(data["trend"][0]["bucketSeconds"], 900);
assert_eq!(
data["healthTimeline"]["points"].as_array().unwrap().len(),
@@ -221,6 +232,18 @@ async fn overview_scopes_every_query_and_projects_only_key_visible_fields() {
assert!(filter.provider_kind.is_none());
}
assert_eq!(observations.summaries[0].1.model.as_deref(), Some("coding"));
+ assert_eq!(observations.diagnostic_queries.len(), 1);
+ assert_eq!(
+ observations.diagnostic_queries[0]
+ .1
+ .client_api_key_ref
+ .as_deref(),
+ Some("key-42")
+ );
+ assert_eq!(
+ observations.diagnostic_queries[0].1.model.as_deref(),
+ Some("coding")
+ );
let health = observations
.trends
.iter()
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 d231a83bd..6fe66b43a 100644
--- a/backend/crates/gateway-store/src/postgres/observability/queries/usage.rs
+++ b/backend/crates/gateway-store/src/postgres/observability/queries/usage.rs
@@ -449,7 +449,14 @@ pub(crate) async fn usage_diagnostics(
where currency_grouping = 1
order by request_count desc, dimension_name limit ",
);
- statement.push_bind(DIAGNOSTIC_LIMIT);
+ // 单个 Key 的模型用量页需要列出该 Key 在区间内的全部模型。
+ statement.push_bind(if dimension == DiagnosticDimension::Model
+ && filter.client_api_key_ref.is_some()
+ {
+ i64::MAX
+ } else {
+ DIAGNOSTIC_LIMIT
+ });
statement.push(
")
select aggregated.*
diff --git a/backend/crates/providers/openai/src/transport/usage.rs b/backend/crates/providers/openai/src/transport/usage.rs
index 3dbaeac69..9dc0253e6 100644
--- a/backend/crates/providers/openai/src/transport/usage.rs
+++ b/backend/crates/providers/openai/src/transport/usage.rs
@@ -226,12 +226,34 @@ struct PricingRule {
pricing: ModelPricing,
}
-// 价格来源:https://developers.openai.com/api/docs/pricing,核验日期 2026-09-09。
-// 使用常规价,不采用 Sol 的临时 $4/$20 优惠;缓存、Flex、Fast 和长上下文
+// 价格来源:https://developers.openai.com/api/docs/pricing;原有条目核验于 2026-09-09,
+// GPT-6 Sol/Luna 核验于 2026-09-22。
+// 使用常规价,不采用 GPT-5.6 Sol 的临时 $4/$20 优惠;缓存、Flex、Fast 和长上下文
// 档位也统一按常规价计算。
// 已按 https://developers.openai.com/api/docs/deprecations 核验至 2026-09-13,
// 移除已关闭的型号;仅宣布弃用但尚未到关闭日期的型号继续保留。
const PRICING_RULES: &[PricingRule] = &[
+ // Sol / Luna:https://developers.openai.com/api/docs/pricing
+ PricingRule {
+ model: "gpt-6-sol",
+ pricing: ModelPricing::new(20_000, 100_000, 2_000)
+ .with_cache_write(125)
+ .with_flex(10_000, 50_000, 1_000)
+ .with_fast(40_000, 200_000, 4_000)
+ .with_long(40_000, 150_000, 4_000)
+ .with_long_flex(20_000, 75_000, 2_000)
+ .with_long_fast(80_000, 300_000, 8_000),
+ },
+ PricingRule {
+ model: "gpt-6-luna",
+ pricing: ModelPricing::new(1_000, 5_000, 100)
+ .with_cache_write(125)
+ .with_flex(500, 2_500, 50)
+ .with_fast(2_000, 10_000, 200)
+ .with_long(2_000, 7_500, 200)
+ .with_long_flex(1_000, 3_750, 100)
+ .with_long_fast(4_000, 15_000, 400),
+ },
// Astra:https://developers.openai.com/api/docs/models/gpt-6-astra
// 已于 2026-09-09 对照官方价目表核验。
PricingRule {
diff --git a/backend/crates/providers/openai/tests/transport/usage.rs b/backend/crates/providers/openai/tests/transport/usage.rs
index f19d597f4..69ceeab3b 100644
--- a/backend/crates/providers/openai/tests/transport/usage.rs
+++ b/backend/crates/providers/openai/tests/transport/usage.rs
@@ -76,6 +76,39 @@ fn astra_billing_should_preserve_components_across_tiers_and_context_boundary()
);
}
+#[test]
+fn gpt_6_sol_and_luna_should_use_published_tier_and_context_prices() {
+ // 每百万 token 价格依次为输入、缓存读、缓存写、输出。
+ for (model, tier, input, expected) in [
+ ("gpt-6-sol", None, 272_000, ["2", "0.2", "2.5", "10"]),
+ ("gpt-6-sol", None, 272_001, ["4", "0.4", "5", "15"]),
+ ("gpt-6-sol", Some("flex"), 272_000, ["1", "0.1", "1.25", "5"]),
+ ("gpt-6-sol", Some("fast"), 272_001, ["8", "0.8", "10", "30"]),
+ ("gpt-6-luna", None, 272_000, ["0.1", "0.01", "0.125", "0.5"]),
+ ("gpt-6-luna", None, 272_001, ["0.2", "0.02", "0.25", "0.75"]),
+ ("gpt-6-luna", Some("flex"), 272_001, ["0.1", "0.01", "0.125", "0.375"]),
+ ("gpt-6-luna", Some("fast"), 272_000, ["0.2", "0.02", "0.25", "1"]),
+ ] {
+ let breakdown = openai_billing_breakdown(model, billing_usage(input, 1, 1, 1), tier)
+ .expect("published GPT-6 pricing");
+ let actual = [
+ breakdown.input_price_per_million(),
+ breakdown.cache_read_price_per_million(),
+ breakdown.cache_write_price_per_million(),
+ breakdown.output_price_per_million(),
+ ]
+ .map(|price| {
+ price
+ .amount()
+ .to_string()
+ .trim_end_matches('0')
+ .trim_end_matches('.')
+ .to_owned()
+ });
+ assert_eq!(actual, expected, "{model} {tier:?} {input}");
+ }
+}
+
#[test]
fn billing_breakdown_should_preserve_input_output_and_cache_components() {
let breakdown = openai_billing_breakdown("gpt-5.6-sol", billing_usage(100, 5, 20, 10), None)
diff --git a/docs/api.md b/docs/api.md
index 50293a7c0..20ac12740 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -379,7 +379,7 @@ OpenAI 选号阶段确认本次可选账号全部额度耗尽时,HTTP 返回 `
| 方法 | 路由 | 查询 | 说明 |
| --- | --- | --- | --- |
-| `GET` | `/api/key-usage/overview` | `startTime`、`endTime`、`model?` | 用量汇总、趋势、当前额度和北京时间今日健康时间线 |
+| `GET` | `/api/key-usage/overview` | `startTime`、`endTime`、`model?` | 用量汇总、逐模型 Token 与估算费用、趋势、当前额度和北京时间今日健康时间线 |
| `GET` | `/api/key-usage/records` | 同上,另含 `kind?`、`currentPage?`、`pageSize?` | 当前 Key 的成功请求或错误记录 |
| `GET` | `/api/key-usage/config` | 无 | 当前 Key 的客户端配置凭据 |
| `GET` | `/api/key-usage/version` | 无 | “关于”弹窗使用的当前版本号和提交号 |
diff --git a/frontend/src/api/modules/key-usage.ts b/frontend/src/api/modules/key-usage.ts
index 7e742f4d1..89243ef02 100644
--- a/frontend/src/api/modules/key-usage.ts
+++ b/frontend/src/api/modules/key-usage.ts
@@ -49,10 +49,19 @@ export interface KeyUsageOverview {
endTime: string
key: KeyUsageBudget
summary: KeyUsageMetrics
+ models: KeyUsageModel[]
trend: KeyUsageTrendPoint[]
healthTimeline: DashboardHealthTimeline
}
+export interface KeyUsageModel {
+ model: string
+ requests: number
+ totalTokens: number
+ costUsd: string | null
+ costIncomplete: boolean
+}
+
export type KeyUsageRecordKind = 'success' | 'error'
export interface KeyUsageRecord {
diff --git a/frontend/src/views/key-usage/components/KeyUsageModels.vue b/frontend/src/views/key-usage/components/KeyUsageModels.vue
new file mode 100644
index 000000000..8e33498e7
--- /dev/null
+++ b/frontend/src/views/key-usage/components/KeyUsageModels.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+ {{ row.model }}
+
+
+ {{ formatInteger(row.requests) }}
+
+
+ {{ formatInteger(row.totalTokens) }}
+
+
+
+ {{ money(row.costUsd) }}
+ 部分费用缺失
+
+
+
+
+
+
diff --git a/frontend/src/views/key-usage/index.vue b/frontend/src/views/key-usage/index.vue
index c2e08eac4..57eefd72c 100644
--- a/frontend/src/views/key-usage/index.vue
+++ b/frontend/src/views/key-usage/index.vue
@@ -10,6 +10,7 @@ import BaseScrollbar from '@/components/base/BaseScrollbar.vue'
import RequestHealthTimelineCard from '@/views/dashboard/components/RequestHealthTimelineCard.vue'
import KeyUsageBudget from './components/KeyUsageBudget.vue'
import KeyUsageHeader from './components/KeyUsageHeader.vue'
+import KeyUsageModels from './components/KeyUsageModels.vue'
import KeyUsageRecords from './components/KeyUsageRecords.vue'
import KeyUsageSkeleton from './components/KeyUsageSkeleton.vue'
import KeyUsageSummary from './components/KeyUsageSummary.vue'
@@ -63,6 +64,7 @@ async function openAbout() {
+
From 1d91e7bc25a6cfb3b2b2d86d4cc7cd9a58d5ab66 Mon Sep 17 00:00:00 2001
From: Yongzhao Chen
Date: Tue, 22 Sep 2026 21:33:39 +0200
Subject: [PATCH 2/5] feat(usage): group admin diagnostics by client key and
model
Co-authored-by: Codex
---
.../gateway-admin/src/model/observability.rs | 2 ++
.../src/use_case/observability.rs | 2 ++
.../src/admin/observability/presenter.rs | 1 +
.../src/admin/observability/query.rs | 4 +++
.../src/admin/observability/wire.rs | 1 +
.../tests/admin/observability/query.rs | 4 +++
.../src/postgres/observability/mapping.rs | 1 +
.../src/postgres/observability/model.rs | 1 +
.../postgres/observability/queries/usage.rs | 36 +++++++++++++++++--
.../gateway-store/tests/postgres/snapshots.rs | 28 ++++++++++++++-
docs/api.md | 3 +-
frontend/src/api/modules/usage.ts | 1 +
.../usage/components/UsageDiagnosticCard.vue | 34 +++++++++++++-----
13 files changed, 105 insertions(+), 13 deletions(-)
diff --git a/backend/crates/gateway-admin/src/model/observability.rs b/backend/crates/gateway-admin/src/model/observability.rs
index 717341444..556c22e92 100644
--- a/backend/crates/gateway-admin/src/model/observability.rs
+++ b/backend/crates/gateway-admin/src/model/observability.rs
@@ -158,6 +158,7 @@ pub struct OpsErrorQuery {
pub enum DiagnosticDimension {
Provider,
Model,
+ KeyModel,
Account,
ApiKey,
Transport,
@@ -1156,6 +1157,7 @@ pub struct DiagnosticsItem {
pub retry_rate: f64,
pub impact_score: f64,
pub estimated_cost: Option,
+ pub cost_incomplete: bool,
pub attempt_count: u64,
pub total_tokens: u64,
}
diff --git a/backend/crates/gateway-admin/src/use_case/observability.rs b/backend/crates/gateway-admin/src/use_case/observability.rs
index 6535331dc..516ebfe36 100644
--- a/backend/crates/gateway-admin/src/use_case/observability.rs
+++ b/backend/crates/gateway-admin/src/use_case/observability.rs
@@ -385,6 +385,8 @@ impl ObservabilityService for DefaultObservabilityService {
retry_rate,
impact_score,
estimated_cost: usd_cost(&item.costs),
+ cost_incomplete: item.cost_coverage.partial_count > 0
+ || item.cost_coverage.unavailable_count > 0,
attempt_count: item.attempt_count,
total_tokens: item.total_tokens,
}
diff --git a/backend/crates/gateway-api/src/admin/observability/presenter.rs b/backend/crates/gateway-api/src/admin/observability/presenter.rs
index d88819220..8351398be 100644
--- a/backend/crates/gateway-api/src/admin/observability/presenter.rs
+++ b/backend/crates/gateway-api/src/admin/observability/presenter.rs
@@ -1296,6 +1296,7 @@ pub(crate) fn diagnostics_view(
retry_rate: item.retry_rate,
impact_score: item.impact_score,
estimated_cost: item.estimated_cost.as_ref().map(ToString::to_string),
+ cost_incomplete: item.cost_incomplete,
attempt_count: item.attempt_count,
total_tokens: item.total_tokens,
})
diff --git a/backend/crates/gateway-api/src/admin/observability/query.rs b/backend/crates/gateway-api/src/admin/observability/query.rs
index 3b0acc118..e42a82e46 100644
--- a/backend/crates/gateway-api/src/admin/observability/query.rs
+++ b/backend/crates/gateway-api/src/admin/observability/query.rs
@@ -160,6 +160,7 @@ impl TrendKind {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagnosticDimension {
Model,
+ KeyModel,
Account,
ApiKey,
Provider,
@@ -173,6 +174,7 @@ impl DiagnosticDimension {
pub fn parse(value: Option<&str>) -> Result {
match trimmed(value) {
None | Some("model") => Ok(Self::Model),
+ Some("keyModel") => Ok(Self::KeyModel),
Some("account") => Ok(Self::Account),
Some("apiKey" | "api_key") => Ok(Self::ApiKey),
Some("provider") => Ok(Self::Provider),
@@ -188,6 +190,7 @@ impl DiagnosticDimension {
pub const fn display_name(self) -> &'static str {
match self {
Self::Model => "model",
+ Self::KeyModel => "keyModel",
Self::Account => "account",
Self::ApiKey => "apiKey",
Self::Provider => "provider",
@@ -257,6 +260,7 @@ pub(crate) fn domain_diagnostic_dimension(
) -> domain::DiagnosticDimension {
match dimension {
DiagnosticDimension::Model => domain::DiagnosticDimension::Model,
+ DiagnosticDimension::KeyModel => domain::DiagnosticDimension::KeyModel,
DiagnosticDimension::Account => domain::DiagnosticDimension::Account,
DiagnosticDimension::ApiKey => domain::DiagnosticDimension::ApiKey,
DiagnosticDimension::Provider => domain::DiagnosticDimension::Provider,
diff --git a/backend/crates/gateway-api/src/admin/observability/wire.rs b/backend/crates/gateway-api/src/admin/observability/wire.rs
index 26e5a0f13..7285c1567 100644
--- a/backend/crates/gateway-api/src/admin/observability/wire.rs
+++ b/backend/crates/gateway-api/src/admin/observability/wire.rs
@@ -760,6 +760,7 @@ pub struct DiagnosticItemView {
pub retry_rate: f64,
pub impact_score: f64,
pub estimated_cost: Option,
+ pub cost_incomplete: bool,
pub attempt_count: u64,
pub total_tokens: u64,
}
diff --git a/backend/crates/gateway-api/tests/admin/observability/query.rs b/backend/crates/gateway-api/tests/admin/observability/query.rs
index a832080af..00ddd0774 100644
--- a/backend/crates/gateway-api/tests/admin/observability/query.rs
+++ b/backend/crates/gateway-api/tests/admin/observability/query.rs
@@ -61,6 +61,10 @@ fn diagnostics_query_should_keep_wire_dimension_name() {
serde_json::from_value(json!({"dimension": "failure_class"})).unwrap();
assert_eq!(query.dimension().unwrap(), DiagnosticDimension::Failure);
assert_eq!(DiagnosticDimension::Failure.display_name(), "failureClass");
+ let key_model: DiagnosticsQuery =
+ serde_json::from_value(json!({"dimension": "keyModel"})).unwrap();
+ assert_eq!(key_model.dimension().unwrap(), DiagnosticDimension::KeyModel);
+ assert_eq!(DiagnosticDimension::KeyModel.display_name(), "keyModel");
}
#[test]
diff --git a/backend/crates/gateway-store/src/postgres/observability/mapping.rs b/backend/crates/gateway-store/src/postgres/observability/mapping.rs
index 07f4e019a..af5818e26 100644
--- a/backend/crates/gateway-store/src/postgres/observability/mapping.rs
+++ b/backend/crates/gateway-store/src/postgres/observability/mapping.rs
@@ -79,6 +79,7 @@ pub(crate) const fn store_diagnostic_dimension(
match dimension {
admin_observability::DiagnosticDimension::Provider => DiagnosticDimension::Provider,
admin_observability::DiagnosticDimension::Model => DiagnosticDimension::Model,
+ admin_observability::DiagnosticDimension::KeyModel => DiagnosticDimension::KeyModel,
admin_observability::DiagnosticDimension::Account => DiagnosticDimension::Account,
admin_observability::DiagnosticDimension::ApiKey => DiagnosticDimension::ApiKey,
admin_observability::DiagnosticDimension::Transport => DiagnosticDimension::Transport,
diff --git a/backend/crates/gateway-store/src/postgres/observability/model.rs b/backend/crates/gateway-store/src/postgres/observability/model.rs
index 496f8336a..365e0ae72 100644
--- a/backend/crates/gateway-store/src/postgres/observability/model.rs
+++ b/backend/crates/gateway-store/src/postgres/observability/model.rs
@@ -185,6 +185,7 @@ pub struct OpsErrorQuery {
pub enum DiagnosticDimension {
Provider,
Model,
+ KeyModel,
Account,
ApiKey,
Transport,
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 6fe66b43a..aeb6d932d 100644
--- a/backend/crates/gateway-store/src/postgres/observability/queries/usage.rs
+++ b/backend/crates/gateway-store/src/postgres/observability/queries/usage.rs
@@ -5,6 +5,8 @@ use serde_json::Value;
const MODEL_DIAGNOSTIC_DIMENSION_SQL: &str =
"coalesce(mr.upstream_model_id, mr.requested_model_id)";
+const KEY_MODEL_DIAGNOSTIC_DIMENSION_SQL: &str =
+ "json_build_array(mr.client_api_key_ref, coalesce(mr.upstream_model_id, mr.requested_model_id))::text";
pub(crate) fn push_usage_filter(
query: &mut QueryBuilder,
@@ -449,9 +451,9 @@ pub(crate) async fn usage_diagnostics(
where currency_grouping = 1
order by request_count desc, dimension_name limit ",
);
- // 单个 Key 的模型用量页需要列出该 Key 在区间内的全部模型。
- statement.push_bind(if dimension == DiagnosticDimension::Model
- && filter.client_api_key_ref.is_some()
+ // Key 用量页及管理端 Key×模型视图都需要保留区间内的全部组合。
+ statement.push_bind(if dimension == DiagnosticDimension::KeyModel
+ || (dimension == DiagnosticDimension::Model && filter.client_api_key_ref.is_some())
{
i64::MAX
} else {
@@ -522,6 +524,28 @@ pub(crate) async fn usage_diagnostics(
)
.await?
}
+ DiagnosticDimension::KeyModel => {
+ let pairs = observations
+ .iter()
+ .map(|item| {
+ serde_json::from_str::<(String, String)>(&item.key)
+ .map_err(|_| postgres_unavailable("decode key/model diagnostic"))
+ })
+ .collect::>>()?;
+ let names = diagnostic_api_key_display_names(
+ pool,
+ &pairs.iter().map(|(key, _)| key.clone()).collect::>(),
+ )
+ .await?;
+ observations
+ .iter()
+ .zip(pairs)
+ .map(|(item, (key, model))| {
+ let name = names.get(&key).map_or(key.as_str(), String::as_str);
+ (item.key.clone(), format!("{name} → {model}"))
+ })
+ .collect()
+ }
_ => HashMap::new(),
};
for observation in &mut observations {
@@ -600,6 +624,7 @@ pub(crate) fn diagnostic_dimension_sql(dimension: DiagnosticDimension) -> &'stat
match dimension {
DiagnosticDimension::Provider => "coalesce(mr.provider_kind, 'unrouted')",
DiagnosticDimension::Model => MODEL_DIAGNOSTIC_DIMENSION_SQL,
+ DiagnosticDimension::KeyModel => KEY_MODEL_DIAGNOSTIC_DIMENSION_SQL,
DiagnosticDimension::Account => "coalesce(mr.provider_account_ref, 'unrouted')",
DiagnosticDimension::ApiKey => "mr.client_api_key_ref",
DiagnosticDimension::Transport => {
@@ -625,6 +650,11 @@ pub(crate) fn push_diagnostic_dimension_filter(
statement.push(MODEL_DIAGNOSTIC_DIMENSION_SQL);
statement.push(" is not null");
}
+ DiagnosticDimension::KeyModel => {
+ statement.push(" and mr.client_api_key_ref is not null and ");
+ statement.push(MODEL_DIAGNOSTIC_DIMENSION_SQL);
+ statement.push(" is not null");
+ }
DiagnosticDimension::Status => {
statement
.push(" and coalesce(mr.upstream_status_code, mr.client_status_code) is not null");
diff --git a/backend/crates/gateway-store/tests/postgres/snapshots.rs b/backend/crates/gateway-store/tests/postgres/snapshots.rs
index c7cba7789..e69b9c91b 100644
--- a/backend/crates/gateway-store/tests/postgres/snapshots.rs
+++ b/backend/crates/gateway-store/tests/postgres/snapshots.rs
@@ -847,6 +847,11 @@ async fn api_key_diagnostics_should_display_key_name_and_fallback_to_ref() {
seed_api_key(&database.pool, "key_diag", "My Key", started_at).await;
let store = PgExecutionStore::new(database.pool.clone());
for id in ["req_key_a", "req_key_b"] {
+ let upstream_model = if id == "req_key_a" {
+ "upstream-model"
+ } else {
+ "other-model"
+ };
let mut request = new_request(id, started_at);
request.client_api_key_id = Some("key_diag".to_owned());
request.client_api_key_ref = "key_diag".to_owned();
@@ -859,7 +864,7 @@ async fn api_key_diagnostics_should_display_key_name_and_fallback_to_ref() {
provider_kind: "openai".to_owned(),
provider_account_id: None,
provider_account_ref: Some("acct_key".to_owned()),
- upstream_model_id: Some("upstream-model".to_owned()),
+ upstream_model_id: Some(upstream_model.to_owned()),
upstream_transport: "http_sse".to_owned(),
http_version: None,
};
@@ -893,6 +898,27 @@ async fn api_key_diagnostics_should_display_key_name_and_fallback_to_ref() {
assert_eq!(diagnostics[0].request_count, 2);
assert_eq!(diagnostics[0].latency_p95_ms, Some(1950));
+ let key_models = repository
+ .usage_diagnostics(
+ range_around(started_at),
+ UsageRecordFilter::default(),
+ DiagnosticDimension::KeyModel,
+ )
+ .await
+ .expect("key/model diagnostics");
+ assert_eq!(key_models.len(), 2);
+ let names = key_models
+ .iter()
+ .map(|item| {
+ let (key, model): (String, String) = serde_json::from_str(&item.key).unwrap();
+ assert_eq!(key, "key_diag");
+ assert_eq!(item.request_count, 1);
+ (model, item.name.clone())
+ })
+ .collect::>();
+ assert_eq!(names["upstream-model"], "My Key → upstream-model");
+ assert_eq!(names["other-model"], "My Key → other-model");
+
sqlx::query("delete from client_api_keys where id = 'key_diag'")
.execute(&database.pool)
.await
diff --git a/docs/api.md b/docs/api.md
index 20ac12740..788828a7d 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -1337,7 +1337,8 @@ Dashboard 的 `capacityInfo.maxConcurrentPerAccount` 为默认账号并发上限
用量查询可组合页码/游标、时间范围、Provider、Client Key、账号、模型、route、transport、状态码、
request/response/upstream ID、outcome 与搜索文本。诊断 `dimension` 可取 `model`、`account`、
-`apiKey`、`provider`、`transport`、`failureClass`、`status`。
+`apiKey`、`keyModel`、`provider`、`transport`、`failureClass`、`status`。`keyModel` 按 Client Key 与上游发送模型交叉汇总,
+返回请求数、Token 和估算费用;费用不完整时 `costIncomplete` 为 `true`。
管理端请求列表及 Dashboard 最近请求中的 `accountNotes` 为账号当前备注,按内部账号 ID 关联。
备注不写入请求历史快照;无备注或账号已删除时返回 `null`,修改备注不改变历史请求的账号归属。
diff --git a/frontend/src/api/modules/usage.ts b/frontend/src/api/modules/usage.ts
index 2de801872..3a708abb0 100644
--- a/frontend/src/api/modules/usage.ts
+++ b/frontend/src/api/modules/usage.ts
@@ -450,6 +450,7 @@ export interface UsageDiagnosticItem {
retryRate: number
impactScore: number
estimatedCost: string | null
+ costIncomplete: boolean
attemptCount: number
totalTokens: number
}
diff --git a/frontend/src/views/usage/components/UsageDiagnosticCard.vue b/frontend/src/views/usage/components/UsageDiagnosticCard.vue
index b30fa8a5a..0a085d35d 100644
--- a/frontend/src/views/usage/components/UsageDiagnosticCard.vue
+++ b/frontend/src/views/usage/components/UsageDiagnosticCard.vue
@@ -28,6 +28,7 @@ const dimension = defineModel('dimension', { type: String, required: true })
const dimensionOptions = [
{ label: '模型', value: 'model' },
+ { label: '密钥×模型', value: 'keyModel' },
{ label: '账号', value: 'account' },
{ label: '密钥', value: 'apiKey' },
{ label: '上游', value: 'provider' },
@@ -69,18 +70,28 @@ const diagnosticColumns = defineTableColumns([
},
])
+const keyModelColumns = defineTableColumns([
+ { key: 'nameDisplay', label: '密钥 / 模型', kind: 'custom', size: 'xl' },
+ { key: 'requestCount', label: '请求', kind: 'numeric', size: 'sm' },
+ { key: 'totalTokens', label: 'TOKEN', kind: 'numeric', size: 'lg' },
+ { key: 'estimatedCost', label: '估算费用 USD', kind: 'numeric', size: 'lg' },
+])
+
const selectedDimensionLabel = computed(
() => dimensionOptions.find(option => option.value === dimension.value)?.label ?? '维度',
)
const resultDimension = computed(() => props.diagnostics.dimension || dimension.value)
+const visibleColumns = computed(() => resultDimension.value === 'keyModel' ? keyModelColumns : diagnosticColumns)
const resultDimensionLabel = computed(
() => dimensionOptions.find(option => option.value === resultDimension.value)?.label ?? '维度',
)
const sortedItems = computed(() =>
[...props.diagnostics.items].sort(
- (left, right) => right.impactScore - left.impactScore || right.requestCount - left.requestCount,
+ (left, right) => resultDimension.value === 'keyModel'
+ ? right.requestCount - left.requestCount
+ : right.impactScore - left.impactScore || right.requestCount - left.requestCount,
),
)
@@ -100,7 +111,7 @@ function diagnosticNameDisplay(name: string) {
const raw = name.trim() || '未知'
const full
= resultDimension.value === 'transport' ? ({ websocket: 'WS', http_sse: 'SSE' }[raw] ?? raw) : raw
- if (resultDimension.value !== 'model' && resultDimension.value !== 'account') {
+ if (resultDimension.value !== 'model' && resultDimension.value !== 'account' && resultDimension.value !== 'keyModel') {
return { primary: full, secondary: '', full }
}
@@ -116,8 +127,8 @@ function diagnosticNameDisplay(name: string) {
@@ -126,7 +137,7 @@ function diagnosticNameDisplay(name: string) {
label="诊断维度"
:options="dimensionOptions"
:disabled="loading"
- class="w-full min-w-0 lg:w-80"
+ class="w-full min-w-0 lg:w-96"
/>
@@ -135,7 +146,7 @@ function diagnosticNameDisplay(name: string) {
v-if="hasData"
:key="resultDimension"
class="min-h-0 w-full xl:contain-[size]"
- :columns="diagnosticColumns"
+ :columns="visibleColumns"
:rows="displayItems"
density="compact"
row-key="key"
@@ -229,8 +240,15 @@ function diagnosticNameDisplay(name: string) {
-
- {{ formatUsd(row.estimatedCost) }}
+
+ {{ formatUsd(row.estimatedCost) }}
+ 部分费用缺失
+
+
+
+
+
+ {{ formatCompactNumber(row.totalTokens) }}
From db35074ba67f4a64e49cffe82257894e5c5afcb3 Mon Sep 17 00:00:00 2001
From: Yongzhao Chen
Date: Tue, 22 Sep 2026 21:35:25 +0200
Subject: [PATCH 3/5] test(usage): distinguish keys sharing a model
Co-authored-by: Codex
---
.../gateway-store/tests/postgres/snapshots.rs | 37 +++++++++++++++++++
1 file changed, 37 insertions(+)
diff --git a/backend/crates/gateway-store/tests/postgres/snapshots.rs b/backend/crates/gateway-store/tests/postgres/snapshots.rs
index e69b9c91b..b7e763a30 100644
--- a/backend/crates/gateway-store/tests/postgres/snapshots.rs
+++ b/backend/crates/gateway-store/tests/postgres/snapshots.rs
@@ -935,6 +935,43 @@ async fn api_key_diagnostics_should_display_key_name_and_fallback_to_ref() {
assert_eq!(diagnostics[0].key, "key_diag");
assert_eq!(diagnostics[0].name, "key_diag");
+ seed_api_key(&database.pool, "key_other", "Other Key", started_at).await;
+ let mut request = new_request("req_key_other", started_at);
+ request.client_api_key_id = Some("key_other".to_owned());
+ request.client_api_key_ref = "key_other".to_owned();
+ store
+ .insert_model_request_with_first_attempt(
+ request,
+ ModelRequestAttemptStart {
+ account_selection_wait_ms: None,
+ capacity_used_slots: None,
+ capacity_total_slots: None,
+ model_request_id: "req_key_other".to_owned(),
+ attempt_count: 1,
+ provider_kind: "openai".to_owned(),
+ provider_account_id: None,
+ provider_account_ref: Some("acct_key".to_owned()),
+ upstream_model_id: Some("upstream-model".to_owned()),
+ upstream_transport: "http_sse".to_owned(),
+ http_version: None,
+ },
+ )
+ .await
+ .expect("insert other key request");
+ finalize_request(&database.pool, "req_key_other", started_at).await;
+ let key_models = repository
+ .usage_diagnostics(
+ range_around(started_at),
+ UsageRecordFilter::default(),
+ DiagnosticDimension::KeyModel,
+ )
+ .await
+ .expect("key/model diagnostics for both keys");
+ assert_eq!(key_models.len(), 3);
+ assert!(key_models.iter().any(|item| {
+ item.name == "Other Key → upstream-model" && item.request_count == 1
+ }));
+
database.close().await;
}
From 9a54793593d17f542ec81faa8e59ff3535b6a8c7 Mon Sep 17 00:00:00 2001
From: Yongzhao Chen
Date: Tue, 22 Sep 2026 21:35:49 +0200
Subject: [PATCH 4/5] docs(api): clarify key-model grouping fallback
Co-authored-by: Codex
---
docs/api.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/docs/api.md b/docs/api.md
index 788828a7d..47eb16268 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -1337,7 +1337,8 @@ Dashboard 的 `capacityInfo.maxConcurrentPerAccount` 为默认账号并发上限
用量查询可组合页码/游标、时间范围、Provider、Client Key、账号、模型、route、transport、状态码、
request/response/upstream ID、outcome 与搜索文本。诊断 `dimension` 可取 `model`、`account`、
-`apiKey`、`keyModel`、`provider`、`transport`、`failureClass`、`status`。`keyModel` 按 Client Key 与上游发送模型交叉汇总,
+`apiKey`、`keyModel`、`provider`、`transport`、`failureClass`、`status`。`keyModel` 按 Client Key 与模型交叉汇总,
+模型优先采用上游发送值,缺失时采用客户端请求值;
返回请求数、Token 和估算费用;费用不完整时 `costIncomplete` 为 `true`。
管理端请求列表及 Dashboard 最近请求中的 `accountNotes` 为账号当前备注,按内部账号 ID 关联。
From 6d4099c59fa7177dc4993dc38961ecb701ed0c1f Mon Sep 17 00:00:00 2001
From: Yongzhao Chen
Date: Tue, 22 Sep 2026 22:53:30 +0200
Subject: [PATCH 5/5] style: apply rustfmt to usage and pricing changes
Co-authored-by: Codex
---
.../tests/admin/observability/query.rs | 5 +-
.../gateway-api/tests/key_usage/fixtures.rs | 50 ++++++++++---------
.../postgres/observability/queries/usage.rs | 19 +++----
.../gateway-store/tests/postgres/snapshots.rs | 8 +--
.../providers/openai/tests/transport/usage.rs | 21 ++++++--
5 files changed, 64 insertions(+), 39 deletions(-)
diff --git a/backend/crates/gateway-api/tests/admin/observability/query.rs b/backend/crates/gateway-api/tests/admin/observability/query.rs
index 00ddd0774..fc77fbe97 100644
--- a/backend/crates/gateway-api/tests/admin/observability/query.rs
+++ b/backend/crates/gateway-api/tests/admin/observability/query.rs
@@ -63,7 +63,10 @@ fn diagnostics_query_should_keep_wire_dimension_name() {
assert_eq!(DiagnosticDimension::Failure.display_name(), "failureClass");
let key_model: DiagnosticsQuery =
serde_json::from_value(json!({"dimension": "keyModel"})).unwrap();
- assert_eq!(key_model.dimension().unwrap(), DiagnosticDimension::KeyModel);
+ assert_eq!(
+ key_model.dimension().unwrap(),
+ DiagnosticDimension::KeyModel
+ );
assert_eq!(DiagnosticDimension::KeyModel.display_name(), "keyModel");
}
diff --git a/backend/crates/gateway-api/tests/key_usage/fixtures.rs b/backend/crates/gateway-api/tests/key_usage/fixtures.rs
index 6d7a89f52..79c1fa620 100644
--- a/backend/crates/gateway-api/tests/key_usage/fixtures.rs
+++ b/backend/crates/gateway-api/tests/key_usage/fixtures.rs
@@ -85,29 +85,33 @@ pub(super) async fn fixture() -> AdminTestFixture {
}]);
}
fixture.usage_records.lock().unwrap().push(usage_record());
- fixture.diagnostics.lock().unwrap().push(DiagnosticObservation {
- key: "coding".to_owned(),
- name: "coding".to_owned(),
- request_count: 2,
- success_count: 1,
- failure_count: 1,
- attempt_count: 2,
- total_tokens: 1100,
- average_latency_ms: None,
- latency_p95_ms: None,
- first_token_p95_ms: None,
- non_completion_count: 0,
- retry_count: 0,
- cost_coverage: CostCoverage {
- calculated_count: 1,
- unavailable_count: 1,
- ..Default::default()
- },
- costs: vec![CurrencyCost {
- currency: "USD".to_owned(),
- amount: "0.123456".parse().unwrap(),
- }],
- });
+ fixture
+ .diagnostics
+ .lock()
+ .unwrap()
+ .push(DiagnosticObservation {
+ key: "coding".to_owned(),
+ name: "coding".to_owned(),
+ request_count: 2,
+ success_count: 1,
+ failure_count: 1,
+ attempt_count: 2,
+ total_tokens: 1100,
+ average_latency_ms: None,
+ latency_p95_ms: None,
+ first_token_p95_ms: None,
+ non_completion_count: 0,
+ retry_count: 0,
+ cost_coverage: CostCoverage {
+ calculated_count: 1,
+ unavailable_count: 1,
+ ..Default::default()
+ },
+ costs: vec![CurrencyCost {
+ currency: "USD".to_owned(),
+ amount: "0.123456".parse().unwrap(),
+ }],
+ });
fixture.ops_errors.lock().unwrap().push(error_record());
fixture
}
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 aeb6d932d..ba4103796 100644
--- a/backend/crates/gateway-store/src/postgres/observability/queries/usage.rs
+++ b/backend/crates/gateway-store/src/postgres/observability/queries/usage.rs
@@ -5,8 +5,7 @@ use serde_json::Value;
const MODEL_DIAGNOSTIC_DIMENSION_SQL: &str =
"coalesce(mr.upstream_model_id, mr.requested_model_id)";
-const KEY_MODEL_DIAGNOSTIC_DIMENSION_SQL: &str =
- "json_build_array(mr.client_api_key_ref, coalesce(mr.upstream_model_id, mr.requested_model_id))::text";
+const KEY_MODEL_DIAGNOSTIC_DIMENSION_SQL: &str = "json_build_array(mr.client_api_key_ref, coalesce(mr.upstream_model_id, mr.requested_model_id))::text";
pub(crate) fn push_usage_filter(
query: &mut QueryBuilder,
@@ -452,13 +451,15 @@ pub(crate) async fn usage_diagnostics(
order by request_count desc, dimension_name limit ",
);
// Key 用量页及管理端 Key×模型视图都需要保留区间内的全部组合。
- statement.push_bind(if dimension == DiagnosticDimension::KeyModel
- || (dimension == DiagnosticDimension::Model && filter.client_api_key_ref.is_some())
- {
- i64::MAX
- } else {
- DIAGNOSTIC_LIMIT
- });
+ statement.push_bind(
+ if dimension == DiagnosticDimension::KeyModel
+ || (dimension == DiagnosticDimension::Model && filter.client_api_key_ref.is_some())
+ {
+ i64::MAX
+ } else {
+ DIAGNOSTIC_LIMIT
+ },
+ );
statement.push(
")
select aggregated.*
diff --git a/backend/crates/gateway-store/tests/postgres/snapshots.rs b/backend/crates/gateway-store/tests/postgres/snapshots.rs
index b7e763a30..264a5c32c 100644
--- a/backend/crates/gateway-store/tests/postgres/snapshots.rs
+++ b/backend/crates/gateway-store/tests/postgres/snapshots.rs
@@ -968,9 +968,11 @@ async fn api_key_diagnostics_should_display_key_name_and_fallback_to_ref() {
.await
.expect("key/model diagnostics for both keys");
assert_eq!(key_models.len(), 3);
- assert!(key_models.iter().any(|item| {
- item.name == "Other Key → upstream-model" && item.request_count == 1
- }));
+ assert!(
+ key_models
+ .iter()
+ .any(|item| { item.name == "Other Key → upstream-model" && item.request_count == 1 })
+ );
database.close().await;
}
diff --git a/backend/crates/providers/openai/tests/transport/usage.rs b/backend/crates/providers/openai/tests/transport/usage.rs
index 69ceeab3b..3a59fa831 100644
--- a/backend/crates/providers/openai/tests/transport/usage.rs
+++ b/backend/crates/providers/openai/tests/transport/usage.rs
@@ -82,12 +82,27 @@ fn gpt_6_sol_and_luna_should_use_published_tier_and_context_prices() {
for (model, tier, input, expected) in [
("gpt-6-sol", None, 272_000, ["2", "0.2", "2.5", "10"]),
("gpt-6-sol", None, 272_001, ["4", "0.4", "5", "15"]),
- ("gpt-6-sol", Some("flex"), 272_000, ["1", "0.1", "1.25", "5"]),
+ (
+ "gpt-6-sol",
+ Some("flex"),
+ 272_000,
+ ["1", "0.1", "1.25", "5"],
+ ),
("gpt-6-sol", Some("fast"), 272_001, ["8", "0.8", "10", "30"]),
("gpt-6-luna", None, 272_000, ["0.1", "0.01", "0.125", "0.5"]),
("gpt-6-luna", None, 272_001, ["0.2", "0.02", "0.25", "0.75"]),
- ("gpt-6-luna", Some("flex"), 272_001, ["0.1", "0.01", "0.125", "0.375"]),
- ("gpt-6-luna", Some("fast"), 272_000, ["0.2", "0.02", "0.25", "1"]),
+ (
+ "gpt-6-luna",
+ Some("flex"),
+ 272_001,
+ ["0.1", "0.01", "0.125", "0.375"],
+ ),
+ (
+ "gpt-6-luna",
+ Some("fast"),
+ 272_000,
+ ["0.2", "0.02", "0.25", "1"],
+ ),
] {
let breakdown = openai_billing_breakdown(model, billing_usage(input, 1, 1, 1), tier)
.expect("published GPT-6 pricing");