mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
fix(usage): show CPR badges for linked accounts
Use current peer summaries for request records and account diagnostics without changing historical snapshots or database schema. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -739,6 +739,8 @@ pub struct UsageListRecord {
|
||||
/// 请求选号时冻结的账号套餐
|
||||
pub provider_account_plan_type: Option<String>,
|
||||
pub provider_account_plan_type_display: Option<String>,
|
||||
/// 查询时的互联识别结果,缺少当前摘要时为未知
|
||||
pub provider_account_upstream_peer: Option<bool>,
|
||||
pub provider_account_authentication_kind: Option<String>,
|
||||
pub upstream_model_id: Option<String>,
|
||||
pub upstream_transport: Option<String>,
|
||||
@@ -1439,6 +1441,8 @@ pub struct DiagnosticsItem {
|
||||
pub name: String,
|
||||
pub account_authentication_kind: Option<String>,
|
||||
pub account_plan_type: Option<String>,
|
||||
/// 仅账号维度补充当前互联识别结果
|
||||
pub account_upstream_peer: Option<bool>,
|
||||
pub account_plan_type_display: Option<String>,
|
||||
pub request_count: u64,
|
||||
pub success_count: u64,
|
||||
|
||||
@@ -8,6 +8,7 @@ use std::{
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use futures::StreamExt as _;
|
||||
use gateway_core::{account::ProviderAccountId, routing::ProviderKind};
|
||||
|
||||
use crate::{
|
||||
@@ -133,7 +134,8 @@ impl DefaultObservabilityService {
|
||||
)
|
||||
.map_err(|error| map_store_error(error, "dashboard"))?;
|
||||
let runtime_slots = observation.runtime_slots;
|
||||
self.enrich_list_records(&mut observation.recent_requests);
|
||||
self.enrich_list_records(&mut observation.recent_requests)
|
||||
.await;
|
||||
self.enrich_dashboard_quotas(&mut observation.account_usage)
|
||||
.await;
|
||||
for account in &mut observation.account_usage {
|
||||
@@ -259,7 +261,7 @@ impl ObservabilityService for DefaultObservabilityService {
|
||||
.list_usage_records(query)
|
||||
.await
|
||||
.map_err(|error| map_store_error(error, "usage records"))?;
|
||||
self.enrich_list_records(&mut page.items);
|
||||
self.enrich_list_records(&mut page.items).await;
|
||||
Ok(page)
|
||||
}
|
||||
|
||||
@@ -336,6 +338,12 @@ impl ObservabilityService for DefaultObservabilityService {
|
||||
.usage_diagnostics(range, filter, dimension, 100)
|
||||
.await
|
||||
.map_err(|error| map_store_error(error, "usage diagnostics"))?;
|
||||
let peers = if dimension == DiagnosticDimension::Account {
|
||||
self.current_upstream_peers(observation.items.iter().map(|item| item.key.as_str()))
|
||||
.await
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
let items = observation
|
||||
.items
|
||||
.into_iter()
|
||||
@@ -345,6 +353,7 @@ impl ObservabilityService for DefaultObservabilityService {
|
||||
rate_or_zero(item.non_completion_count, item.request_count);
|
||||
let retry_rate = rate_or_zero(item.retried_request_count, item.request_count);
|
||||
DiagnosticsItem {
|
||||
account_upstream_peer: peers.get(&item.key).copied(),
|
||||
key: item.key,
|
||||
name: item.name,
|
||||
account_authentication_kind: item.account_authentication_kind,
|
||||
@@ -912,6 +921,29 @@ fn usd_cost(costs: &[CurrencyCost]) -> Option<DecimalAmount> {
|
||||
}
|
||||
|
||||
impl DefaultObservabilityService {
|
||||
async fn current_upstream_peers<'a>(
|
||||
&self,
|
||||
ids: impl Iterator<Item = &'a str>,
|
||||
) -> HashMap<String, bool> {
|
||||
let Some(peer) = self.upstream_info.as_deref() else {
|
||||
return HashMap::new();
|
||||
};
|
||||
let ids = ids
|
||||
.map(str::to_owned)
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
// 同一页重复账号只读取一次摘要,单个账号不存在或读取失败不阻断历史记录
|
||||
futures::stream::iter(ids)
|
||||
.map(|id: String| async move {
|
||||
let account_id = ProviderAccountId::new(id.clone()).ok()?;
|
||||
let upstream = peer.read_summary(&account_id).await.ok()??.upstream?;
|
||||
Some((id, upstream.discovered && upstream.observed_at.is_some()))
|
||||
})
|
||||
.buffer_unordered(8)
|
||||
.filter_map(|value| async move { value })
|
||||
.collect()
|
||||
.await
|
||||
}
|
||||
|
||||
async fn enrich_dashboard_quotas(&self, accounts: &mut [DashboardAccountUsage]) {
|
||||
if accounts.is_empty() {
|
||||
return;
|
||||
@@ -976,8 +1008,22 @@ impl DefaultObservabilityService {
|
||||
|
||||
/// 补充当前套餐展示名称,逐条尽力把可校验的总额升级为完整分解;单条脏数据(非法 Provider kind、
|
||||
/// 不支持的来源或费用规则失败)只保留该条已存的总额,不影响整页返回
|
||||
fn enrich_list_records(&self, records: &mut [crate::model::observability::UsageListRecord]) {
|
||||
async fn enrich_list_records(
|
||||
&self,
|
||||
records: &mut [crate::model::observability::UsageListRecord],
|
||||
) {
|
||||
let peers = self
|
||||
.current_upstream_peers(
|
||||
records
|
||||
.iter()
|
||||
.filter_map(|record| record.provider_account_ref.as_deref()),
|
||||
)
|
||||
.await;
|
||||
for record in records {
|
||||
record.provider_account_upstream_peer = record
|
||||
.provider_account_ref
|
||||
.as_ref()
|
||||
.and_then(|id| peers.get(id).copied());
|
||||
record.provider_account_plan_type_display =
|
||||
record.provider_kind.as_deref().and_then(|provider| {
|
||||
self.providers
|
||||
|
||||
@@ -1479,6 +1479,7 @@ fn total_record(
|
||||
now: DateTime<Utc>,
|
||||
) -> UsageListRecord {
|
||||
UsageListRecord {
|
||||
provider_account_upstream_peer: None,
|
||||
error_code: None,
|
||||
error_message: None,
|
||||
client_status_code: None,
|
||||
|
||||
@@ -271,6 +271,7 @@ pub(crate) fn usage_list_record_view(
|
||||
account_notes: record.provider_account_notes,
|
||||
account_plan_type: record.provider_account_plan_type,
|
||||
account_plan_type_display: record.provider_account_plan_type_display,
|
||||
account_upstream_peer: record.provider_account_upstream_peer,
|
||||
route: record.endpoint,
|
||||
model,
|
||||
requested_model: record.requested_model_id,
|
||||
@@ -1351,6 +1352,7 @@ pub(crate) fn diagnostics_view(
|
||||
request_count: item.request_count,
|
||||
account_plan_type: item.account_plan_type,
|
||||
account_plan_type_display: item.account_plan_type_display,
|
||||
account_upstream_peer: item.account_upstream_peer,
|
||||
success_count: item.success_count,
|
||||
error_count: item.error_count,
|
||||
error_rate: item.error_rate,
|
||||
|
||||
@@ -99,6 +99,7 @@ pub struct UsageListRecordView {
|
||||
pub account_notes: Option<String>,
|
||||
pub account_plan_type: Option<String>,
|
||||
pub account_plan_type_display: Option<String>,
|
||||
pub account_upstream_peer: Option<bool>,
|
||||
pub route: String,
|
||||
pub model: Option<String>,
|
||||
pub requested_model: Option<String>,
|
||||
@@ -849,6 +850,7 @@ pub struct DiagnosticItemView {
|
||||
pub authentication_kind: Option<String>,
|
||||
pub account_plan_type: Option<String>,
|
||||
pub account_plan_type_display: Option<String>,
|
||||
pub account_upstream_peer: Option<bool>,
|
||||
pub request_count: u64,
|
||||
pub success_count: u64,
|
||||
pub error_count: u64,
|
||||
|
||||
@@ -656,6 +656,23 @@ async fn ops_errors_should_keep_account_label_and_authentication_contract() {
|
||||
);
|
||||
}
|
||||
|
||||
fn peer_summary(
|
||||
discovered: bool,
|
||||
observed: bool,
|
||||
) -> gateway_admin::model::provider_credentials::ProviderQuota {
|
||||
gateway_admin::model::provider_credentials::ProviderQuota {
|
||||
upstream: Some(gateway_admin::model::upstream_info::ProviderUpstreamInfo {
|
||||
discovered,
|
||||
observed_at: observed.then(chrono::Utc::now),
|
||||
stale: true,
|
||||
connection_revision: "test-connection".to_owned(),
|
||||
last_error: None,
|
||||
pool: None,
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn diagnostics_should_keep_stable_key_and_display_name_contract() {
|
||||
use axum::{
|
||||
@@ -670,13 +687,17 @@ async fn diagnostics_should_keep_stable_key_and_display_name_contract() {
|
||||
|
||||
use crate::admin::{AdminTestFixture, AdminTestState};
|
||||
|
||||
for (authentication_kind, plan_type, plan_display) in [
|
||||
(Some("oauth"), Some("pro"), Some("Pro")),
|
||||
(Some("api_key"), None, None),
|
||||
(None, None, None),
|
||||
for (authentication_kind, plan_type, plan_display, peer) in [
|
||||
(Some("oauth"), Some("pro"), Some("Pro"), None),
|
||||
(Some("api_key"), None, None, None),
|
||||
(Some("api_key"), None, None, Some((true, true))),
|
||||
(Some("api_key"), None, None, Some((true, false))),
|
||||
(Some("api_key"), None, None, Some((false, true))),
|
||||
(None, None, None, None),
|
||||
] {
|
||||
let fixture = AdminTestFixture::new().await;
|
||||
fixture.auth.insert_session("valid-session");
|
||||
*fixture.provider_quota.lock().unwrap() = peer.map(|(d, o)| peer_summary(d, o));
|
||||
*fixture.diagnostics.lock().expect("diagnostics") = DiagnosticsObservation {
|
||||
analytics: Default::default(),
|
||||
total_request_count: 4,
|
||||
@@ -718,6 +739,10 @@ async fn diagnostics_should_keep_stable_key_and_display_name_contract() {
|
||||
.await
|
||||
.expect("diagnostics body");
|
||||
let value: serde_json::Value = serde_json::from_slice(&body).expect("diagnostics JSON");
|
||||
assert_eq!(
|
||||
value["data"]["items"][0]["accountUpstreamPeer"],
|
||||
json!(peer.map(|(d, o)| d && o))
|
||||
);
|
||||
assert_eq!(value["data"]["items"][0]["requestShare"], 0.5);
|
||||
assert_eq!(value["data"]["items"][0]["retryCount"], 3);
|
||||
assert_eq!(value["data"]["items"][0]["retryRate"], 0.5);
|
||||
@@ -906,6 +931,7 @@ async fn usage_route_projects_request_facts_and_billing() {
|
||||
.lock()
|
||||
.expect("usage records")
|
||||
.push(UsageListRecord {
|
||||
provider_account_upstream_peer: None,
|
||||
error_code: None,
|
||||
error_message: None,
|
||||
client_status_code: None,
|
||||
@@ -1131,6 +1157,39 @@ async fn usage_route_projects_request_facts_and_billing() {
|
||||
value["data"]["items"][2]["billing"]["totalAmountDisplay"],
|
||||
"$0.007"
|
||||
);
|
||||
|
||||
// 互联标识随当前摘要变化,重复请求共享同一账号,既有请求快照保持原值
|
||||
for peer in [
|
||||
Some((true, true)),
|
||||
Some((true, false)),
|
||||
Some((false, true)),
|
||||
None,
|
||||
] {
|
||||
*fixture.provider_quota.lock().unwrap() = peer.map(|(d, o)| peer_summary(d, o));
|
||||
let response = observability::router::<AdminTestState>()
|
||||
.with_state(fixture.state())
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/admin/usage/records?status=all")
|
||||
.header(header::COOKIE, "cpr_session=valid-session")
|
||||
.header("x-request-id", "req_peer_badge")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = to_bytes(response.into_body(), 1024 * 1024).await.unwrap();
|
||||
let value: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
for item in value["data"]["items"].as_array().unwrap() {
|
||||
assert_eq!(
|
||||
item["accountUpstreamPeer"],
|
||||
json!(peer.map(|(d, o)| d && o))
|
||||
);
|
||||
assert_eq!(item["accountName"], "Snapshot Alpha");
|
||||
assert_eq!(item["accountPlanType"], "supergrok_plus");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -92,6 +92,7 @@ pub(super) async fn fixture() -> AdminTestFixture {
|
||||
|
||||
fn usage_record() -> UsageListRecord {
|
||||
UsageListRecord {
|
||||
provider_account_upstream_peer: None,
|
||||
error_code: None,
|
||||
error_message: None,
|
||||
client_status_code: None,
|
||||
|
||||
@@ -327,6 +327,7 @@ pub(crate) fn usage_list_record_from_row(
|
||||
provider_account_name: get(row, "provider_account_name")?,
|
||||
provider_account_email: get(row, "provider_account_email")?,
|
||||
provider_account_notes: get(row, "provider_account_notes")?,
|
||||
provider_account_upstream_peer: None,
|
||||
provider_account_plan_type: get(row, "provider_account_plan_type")?,
|
||||
provider_account_plan_type_display: None,
|
||||
provider_account_authentication_kind: get(row, "provider_account_authentication_kind")?,
|
||||
|
||||
+5
-1
@@ -2511,10 +2511,14 @@ Dashboard 返回原始 `asOf` 及 `asOfDisplay`,作为当前数据的查询锚
|
||||
|
||||
管理端请求列表及 Dashboard 最近请求中的 `accountNotes` 来自请求保存的账号备注快照,未保存时为 `null`
|
||||
|
||||
请求列表、Dashboard 最近请求及账号维度诊断的 `accountUpstreamPeer` 来自账号当前的互联摘要,
|
||||
已发现且有观测时间时为 `true`,尚未识别成功时为 `false`;摘要缺失、账号已删除或读取失败时为 `null`。
|
||||
该字段仅供 CPR 标识展示,不写入历史快照,非账号维度诊断为 `null`
|
||||
|
||||
`/api/admin/usage/records` 与 `/api/admin/operations/errors` 的 `clientApiKeyName` 来自请求保存的 Key 名称快照,
|
||||
未保存时为 `null`,不影响记录返回,不包含密钥原文
|
||||
|
||||
查询不关联当前配置,改名、修改订阅或删除配置不改变已有快照。补写的历史 Key 名称与订阅取自补写时仍存在的配置,
|
||||
上述快照字段不关联当前配置,改名、修改订阅或删除配置不改变已有快照。补写的历史 Key 名称与订阅取自补写时仍存在的配置,
|
||||
不保证等于请求发生时的值;无法补齐的字段保持未知
|
||||
|
||||
运维错误记录的 `errorDetails` 为受控诊断文本,无详情时为 `null`。
|
||||
|
||||
@@ -94,6 +94,7 @@ export interface UsageListRecord {
|
||||
accountNotes: string | null
|
||||
accountPlanType: string | null
|
||||
accountPlanTypeDisplay: string | null
|
||||
accountUpstreamPeer: boolean | null
|
||||
route: string
|
||||
model: string | null
|
||||
requestedModel: string | null
|
||||
@@ -364,6 +365,7 @@ export interface UsageDiagnosticItem {
|
||||
authenticationKind: string | null
|
||||
accountPlanType: string | null
|
||||
accountPlanTypeDisplay: string | null
|
||||
accountUpstreamPeer: boolean | null
|
||||
requestCount: number
|
||||
successCount: number
|
||||
errorCount: number
|
||||
|
||||
@@ -11,6 +11,7 @@ const props = withDefaults(
|
||||
planType?: string | null
|
||||
authenticationKind?: string
|
||||
upstream?: AccountUpstream | null
|
||||
upstreamPeer?: boolean | null
|
||||
planTypeDisplay: string
|
||||
size?: 'xs' | 'sm' | 'md'
|
||||
}>(),
|
||||
@@ -61,7 +62,7 @@ const paletteClass = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccountUpstreamBadge v-if="upstream?.discovered && upstream.observedAt" :size="size" />
|
||||
<AccountUpstreamBadge v-if="upstreamPeer || (upstream?.discovered && upstream.observedAt)" :size="size" />
|
||||
<span
|
||||
v-else-if="!accountUpstreamPending(upstream)"
|
||||
class="inline-flex min-w-0 max-w-full items-center justify-center whitespace-nowrap leading-none shadow-z-tertiary"
|
||||
|
||||
@@ -79,8 +79,9 @@ const { t } = useI18n({ useScope: 'global' })
|
||||
|
||||
<template #accountPlanType="{ row }">
|
||||
<AccountPlanBadge
|
||||
v-if="row.authenticationKind === 'api_key' || row.accountPlanType"
|
||||
v-if="row.accountUpstreamPeer || row.authenticationKind === 'api_key' || row.accountPlanType"
|
||||
:authentication-kind="row.authenticationKind ?? undefined"
|
||||
:upstream-peer="row.accountUpstreamPeer"
|
||||
:plan-type="row.accountPlanType"
|
||||
:plan-type-display="row.accountPlanTypeDisplay || row.accountPlanType || ''"
|
||||
size="sm"
|
||||
|
||||
@@ -119,6 +119,7 @@ function previewUsageRecord(options: PreviewUsageRecordOptions): UsageListRecord
|
||||
accountNotes: null,
|
||||
accountPlanType: null,
|
||||
accountPlanTypeDisplay: null,
|
||||
accountUpstreamPeer: null,
|
||||
route: stream ? '/v1/responses' : '/v1/chat/completions',
|
||||
model: options.model,
|
||||
requestedModel: options.model,
|
||||
|
||||
@@ -200,8 +200,9 @@ function diagnosticNameDisplay(name: string) {
|
||||
|
||||
<template #accountPlanType="{ row }">
|
||||
<AccountPlanBadge
|
||||
v-if="row.authenticationKind === 'api_key' || row.accountPlanType"
|
||||
v-if="row.accountUpstreamPeer || row.authenticationKind === 'api_key' || row.accountPlanType"
|
||||
:authentication-kind="row.authenticationKind ?? undefined"
|
||||
:upstream-peer="row.accountUpstreamPeer"
|
||||
:plan-type="row.accountPlanType"
|
||||
:plan-type-display="row.accountPlanTypeDisplay || row.accountPlanType || ''"
|
||||
size="sm"
|
||||
|
||||
Reference in New Issue
Block a user