mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-10 13:10:21 +08:00
feat(usage): show reasoning and fast badges in usage records
- extract provider reasoning effort from request body metadata - extract priority service tier and expose it as service_tier - show reasoning level and fast badges after model names - include badges in active request updates and usage list payloads - add targeted backend and frontend coverage
This commit is contained in:
@@ -499,6 +499,12 @@ fn build_users_me_usage_record_payload(
|
||||
if item.target_model.is_some() {
|
||||
payload["target_model"] = json!(item.target_model.clone());
|
||||
}
|
||||
if let Some(reasoning_effort) = item.provider_reasoning_effort() {
|
||||
payload["reasoning_effort"] = json!(reasoning_effort);
|
||||
}
|
||||
if let Some(service_tier) = item.provider_service_tier() {
|
||||
payload["service_tier"] = json!(service_tier);
|
||||
}
|
||||
if include_actual_cost {
|
||||
payload["actual_cost"] = json!(round_to(item.actual_total_cost_usd, 6));
|
||||
payload["rate_multiplier"] = json!(rate_multiplier);
|
||||
@@ -558,6 +564,12 @@ fn build_users_me_usage_active_payload(item: &StoredRequestUsageAudit) -> serde_
|
||||
.expect("object")
|
||||
.remove("target_model");
|
||||
}
|
||||
if let Some(reasoning_effort) = item.provider_reasoning_effort() {
|
||||
payload["reasoning_effort"] = json!(reasoning_effort);
|
||||
}
|
||||
if let Some(service_tier) = item.provider_service_tier() {
|
||||
payload["service_tier"] = json!(service_tier);
|
||||
}
|
||||
payload
|
||||
}
|
||||
|
||||
|
||||
@@ -1222,6 +1222,12 @@ fn admin_usage_active_request_json(
|
||||
if let Some(target_model) = item.target_model.as_ref() {
|
||||
value["target_model"] = json!(target_model);
|
||||
}
|
||||
if let Some(reasoning_effort) = item.provider_reasoning_effort() {
|
||||
value["reasoning_effort"] = json!(reasoning_effort);
|
||||
}
|
||||
if let Some(service_tier) = item.provider_service_tier() {
|
||||
value["service_tier"] = json!(service_tier);
|
||||
}
|
||||
if let Some(image_progress) = image_progress {
|
||||
value["image_progress"] = image_progress.clone();
|
||||
}
|
||||
@@ -1335,6 +1341,12 @@ pub fn admin_usage_record_json(
|
||||
"request_path_and_query",
|
||||
admin_usage_metadata_string(item, "request_path_and_query"),
|
||||
);
|
||||
if let Some(reasoning_effort) = item.provider_reasoning_effort() {
|
||||
object.insert("reasoning_effort".to_string(), json!(reasoning_effort));
|
||||
}
|
||||
if let Some(service_tier) = item.provider_service_tier() {
|
||||
object.insert("service_tier".to_string(), json!(service_tier));
|
||||
}
|
||||
payload
|
||||
}
|
||||
|
||||
@@ -2654,6 +2666,32 @@ mod tests {
|
||||
assert_eq!(record["client_family"], "codex");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_usage_record_includes_provider_reasoning_effort() {
|
||||
let item = StoredRequestUsageAudit {
|
||||
provider_request_body: Some(json!({
|
||||
"reasoning": { "effort": "xhigh" },
|
||||
"service_tier": "priority"
|
||||
})),
|
||||
..sample_usage("completed", Some(200), None)
|
||||
};
|
||||
|
||||
let record = admin_usage_record_json(
|
||||
&item,
|
||||
&BTreeMap::new(),
|
||||
&BTreeMap::new(),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
);
|
||||
let active = admin_usage_active_request_json(&item, None, None, None);
|
||||
|
||||
assert_eq!(record["reasoning_effort"], "xhigh");
|
||||
assert_eq!(active["reasoning_effort"], "xhigh");
|
||||
assert_eq!(record["service_tier"], "priority");
|
||||
assert_eq!(active["service_tier"], "priority");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_requested_stream_uses_stream_generate_content_path_over_stale_metadata_flag() {
|
||||
let item = StoredRequestUsageAudit {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod types;
|
||||
|
||||
pub use types::{
|
||||
extract_provider_reasoning_effort_from_body, extract_provider_service_tier_from_body,
|
||||
parse_usage_body_ref, usage_body_ref, usage_request_metadata_client_family,
|
||||
ApiKeyLastUsedDelta, ManagementTokenCounterDelta, PendingUsageCleanupSummary,
|
||||
ProviderApiKeyWindowUsageRequest, ProxyNodeCounterDelta, StoredProviderApiKeyUsageSummary,
|
||||
@@ -27,5 +28,6 @@ pub use types::{
|
||||
UsageLeaderboardQuery, UsageMonitoringErrorCountQuery, UsageMonitoringErrorListQuery,
|
||||
UsagePerformancePercentilesQuery, UsageProviderPerformanceQuery, UsageReadRepository,
|
||||
UsageRepository, UsageSettledCostSummaryQuery, UsageTimeSeriesGranularity,
|
||||
UsageTimeSeriesQuery, UsageWriteRepository,
|
||||
UsageTimeSeriesQuery, UsageWriteRepository, PROVIDER_REASONING_EFFORT_METADATA_KEY,
|
||||
PROVIDER_SERVICE_TIER_METADATA_KEY,
|
||||
};
|
||||
|
||||
@@ -2,6 +2,48 @@ use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde_json::Value;
|
||||
|
||||
pub const PROVIDER_REASONING_EFFORT_METADATA_KEY: &str = "provider_reasoning_effort";
|
||||
pub const PROVIDER_SERVICE_TIER_METADATA_KEY: &str = "provider_service_tier";
|
||||
|
||||
pub fn extract_provider_reasoning_effort_from_body(value: Option<&Value>) -> Option<String> {
|
||||
let object = value.and_then(Value::as_object)?;
|
||||
object
|
||||
.get("reasoning_effort")
|
||||
.and_then(Value::as_str)
|
||||
.or_else(|| {
|
||||
object
|
||||
.get("reasoning")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|reasoning| reasoning.get("effort"))
|
||||
.and_then(Value::as_str)
|
||||
})
|
||||
.and_then(normalize_provider_reasoning_effort)
|
||||
}
|
||||
|
||||
fn normalize_provider_reasoning_effort(value: &str) -> Option<String> {
|
||||
let normalized = value.trim().to_ascii_lowercase();
|
||||
if normalized.is_empty() || normalized.len() > 64 {
|
||||
return None;
|
||||
}
|
||||
Some(normalized)
|
||||
}
|
||||
|
||||
pub fn extract_provider_service_tier_from_body(value: Option<&Value>) -> Option<String> {
|
||||
value
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|object| object.get("service_tier"))
|
||||
.and_then(Value::as_str)
|
||||
.and_then(normalize_provider_service_tier)
|
||||
}
|
||||
|
||||
fn normalize_provider_service_tier(value: &str) -> Option<String> {
|
||||
let normalized = value.trim().to_ascii_lowercase();
|
||||
if normalized.is_empty() || normalized.len() > 64 {
|
||||
return None;
|
||||
}
|
||||
Some(normalized)
|
||||
}
|
||||
|
||||
/// Joined usage read model assembled from the accounting row plus the newer audit/snapshot
|
||||
/// satellite tables.
|
||||
///
|
||||
@@ -375,6 +417,22 @@ impl StoredRequestUsageAudit {
|
||||
self.request_metadata_string("trace_id")
|
||||
}
|
||||
|
||||
pub fn provider_reasoning_effort(&self) -> Option<String> {
|
||||
self.request_metadata_string(PROVIDER_REASONING_EFFORT_METADATA_KEY)
|
||||
.and_then(normalize_provider_reasoning_effort)
|
||||
.or_else(|| {
|
||||
extract_provider_reasoning_effort_from_body(self.provider_request_body.as_ref())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn provider_service_tier(&self) -> Option<String> {
|
||||
self.request_metadata_string(PROVIDER_SERVICE_TIER_METADATA_KEY)
|
||||
.and_then(normalize_provider_service_tier)
|
||||
.or_else(|| {
|
||||
extract_provider_service_tier_from_body(self.provider_request_body.as_ref())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn body_ref(&self, field: UsageBodyField) -> Option<&str> {
|
||||
match field {
|
||||
UsageBodyField::RequestBody => self.request_body_ref.as_deref(),
|
||||
@@ -2339,6 +2397,35 @@ mod tests {
|
||||
assert_eq!(result.request_capture_source(), "stored_original");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_reasoning_effort_reads_metadata_and_provider_request_body() {
|
||||
let mut usage = sample_usage();
|
||||
usage.provider_request_body = Some(json!({
|
||||
"reasoning": { "effort": "XHigh" },
|
||||
"service_tier": "Priority"
|
||||
}));
|
||||
|
||||
assert_eq!(usage.provider_reasoning_effort().as_deref(), Some("xhigh"));
|
||||
assert_eq!(usage.provider_service_tier().as_deref(), Some("priority"));
|
||||
|
||||
usage.request_metadata = Some(json!({
|
||||
"provider_reasoning_effort": "max",
|
||||
"provider_service_tier": "standard"
|
||||
}));
|
||||
|
||||
assert_eq!(usage.provider_reasoning_effort().as_deref(), Some("max"));
|
||||
assert_eq!(usage.provider_service_tier().as_deref(), Some("standard"));
|
||||
|
||||
usage.request_metadata = None;
|
||||
usage.provider_request_body = Some(json!({
|
||||
"reasoning_effort": "High",
|
||||
"service_tier": "priority"
|
||||
}));
|
||||
|
||||
assert_eq!(usage.provider_reasoning_effort().as_deref(), Some("high"));
|
||||
assert_eq!(usage.provider_service_tier().as_deref(), Some("priority"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_body_capture_json_entry_includes_capture_source_and_body_ref() {
|
||||
let mut usage = sample_usage();
|
||||
|
||||
+20
@@ -114,6 +114,15 @@ SELECT
|
||||
OR NULLIF(BTRIM("usage".request_metadata->>'user_agent'), '') IS NOT NULL
|
||||
OR NULLIF(BTRIM("usage".request_metadata->>'request_path'), '') IS NOT NULL
|
||||
OR NULLIF(BTRIM("usage".request_metadata->>'request_path_and_query'), '') IS NOT NULL
|
||||
OR COALESCE(
|
||||
NULLIF(BTRIM("usage".request_metadata->>'provider_reasoning_effort'), ''),
|
||||
NULLIF(BTRIM("usage".provider_request_body->>'reasoning_effort'), ''),
|
||||
NULLIF(BTRIM("usage".provider_request_body->'reasoning'->>'effort'), '')
|
||||
) IS NOT NULL
|
||||
OR COALESCE(
|
||||
NULLIF(BTRIM("usage".request_metadata->>'provider_service_tier'), ''),
|
||||
NULLIF(BTRIM("usage".provider_request_body->>'service_tier'), '')
|
||||
) IS NOT NULL
|
||||
OR ("usage".request_metadata->>'client_requested_stream') IN ('true', 'false')
|
||||
OR ("usage".request_metadata->>'upstream_is_stream') IN ('true', 'false')
|
||||
THEN jsonb_strip_nulls(jsonb_build_object(
|
||||
@@ -125,6 +134,17 @@ SELECT
|
||||
NULLIF(BTRIM("usage".request_metadata->>'request_path'), ''),
|
||||
'request_path_and_query',
|
||||
NULLIF(BTRIM("usage".request_metadata->>'request_path_and_query'), ''),
|
||||
'provider_reasoning_effort',
|
||||
COALESCE(
|
||||
NULLIF(BTRIM("usage".request_metadata->>'provider_reasoning_effort'), ''),
|
||||
NULLIF(BTRIM("usage".provider_request_body->>'reasoning_effort'), ''),
|
||||
NULLIF(BTRIM("usage".provider_request_body->'reasoning'->>'effort'), '')
|
||||
),
|
||||
'provider_service_tier',
|
||||
COALESCE(
|
||||
NULLIF(BTRIM("usage".request_metadata->>'provider_service_tier'), ''),
|
||||
NULLIF(BTRIM("usage".provider_request_body->>'service_tier'), '')
|
||||
),
|
||||
'client_requested_stream',
|
||||
CASE
|
||||
WHEN ("usage".request_metadata->>'client_requested_stream') IN ('true', 'false')
|
||||
|
||||
@@ -114,6 +114,15 @@ SELECT
|
||||
OR NULLIF(BTRIM("usage".request_metadata->>'user_agent'), '') IS NOT NULL
|
||||
OR NULLIF(BTRIM("usage".request_metadata->>'request_path'), '') IS NOT NULL
|
||||
OR NULLIF(BTRIM("usage".request_metadata->>'request_path_and_query'), '') IS NOT NULL
|
||||
OR COALESCE(
|
||||
NULLIF(BTRIM("usage".request_metadata->>'provider_reasoning_effort'), ''),
|
||||
NULLIF(BTRIM("usage".provider_request_body->>'reasoning_effort'), ''),
|
||||
NULLIF(BTRIM("usage".provider_request_body->'reasoning'->>'effort'), '')
|
||||
) IS NOT NULL
|
||||
OR COALESCE(
|
||||
NULLIF(BTRIM("usage".request_metadata->>'provider_service_tier'), ''),
|
||||
NULLIF(BTRIM("usage".provider_request_body->>'service_tier'), '')
|
||||
) IS NOT NULL
|
||||
OR ("usage".request_metadata->>'client_requested_stream') IN ('true', 'false')
|
||||
OR ("usage".request_metadata->>'upstream_is_stream') IN ('true', 'false')
|
||||
THEN jsonb_strip_nulls(jsonb_build_object(
|
||||
@@ -125,6 +134,17 @@ SELECT
|
||||
NULLIF(BTRIM("usage".request_metadata->>'request_path'), ''),
|
||||
'request_path_and_query',
|
||||
NULLIF(BTRIM("usage".request_metadata->>'request_path_and_query'), ''),
|
||||
'provider_reasoning_effort',
|
||||
COALESCE(
|
||||
NULLIF(BTRIM("usage".request_metadata->>'provider_reasoning_effort'), ''),
|
||||
NULLIF(BTRIM("usage".provider_request_body->>'reasoning_effort'), ''),
|
||||
NULLIF(BTRIM("usage".provider_request_body->'reasoning'->>'effort'), '')
|
||||
),
|
||||
'provider_service_tier',
|
||||
COALESCE(
|
||||
NULLIF(BTRIM("usage".request_metadata->>'provider_service_tier'), ''),
|
||||
NULLIF(BTRIM("usage".provider_request_body->>'service_tier'), '')
|
||||
),
|
||||
'client_requested_stream',
|
||||
CASE
|
||||
WHEN ("usage".request_metadata->>'client_requested_stream') IN ('true', 'false')
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use aether_data_contracts::repository::usage::UpsertUsageRecord;
|
||||
use aether_data_contracts::DataLayerError;
|
||||
|
||||
use crate::request_metadata::sanitize_usage_request_metadata;
|
||||
use crate::request_metadata::{
|
||||
attach_provider_request_body_metadata, sanitize_usage_request_metadata,
|
||||
};
|
||||
use crate::{UsageEvent, UsageEventType};
|
||||
|
||||
fn metadata_string(metadata: Option<&serde_json::Value>, key: &str) -> Option<String> {
|
||||
@@ -29,7 +31,11 @@ pub fn build_upsert_usage_record_from_event(
|
||||
event: &UsageEvent,
|
||||
) -> Result<UpsertUsageRecord, DataLayerError> {
|
||||
let (status, billing_status) = lifecycle_status_and_billing(event.event_type);
|
||||
let data = event.data.clone();
|
||||
let mut data = event.data.clone();
|
||||
data.request_metadata = attach_provider_request_body_metadata(
|
||||
data.request_metadata,
|
||||
data.provider_request_body.as_ref(),
|
||||
);
|
||||
let now_unix_secs = event.timestamp_ms / 1_000;
|
||||
|
||||
Ok(UpsertUsageRecord {
|
||||
@@ -169,6 +175,10 @@ mod tests {
|
||||
output_tokens: Some(20),
|
||||
total_tokens: Some(30),
|
||||
status_code: Some(200),
|
||||
provider_request_body: Some(serde_json::json!({
|
||||
"reasoning": { "effort": "max" },
|
||||
"service_tier": "priority"
|
||||
})),
|
||||
..UsageEventData::default()
|
||||
},
|
||||
})
|
||||
@@ -178,6 +188,22 @@ mod tests {
|
||||
assert_eq!(record.status, "completed");
|
||||
assert_eq!(record.billing_status, "pending");
|
||||
assert_eq!(record.total_tokens, Some(30));
|
||||
assert_eq!(
|
||||
record
|
||||
.request_metadata
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("provider_reasoning_effort"))
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("max")
|
||||
);
|
||||
assert_eq!(
|
||||
record
|
||||
.request_metadata
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("provider_service_tier"))
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("priority")
|
||||
);
|
||||
assert_eq!(record.finalized_at_unix_secs, Some(1_700_000_000));
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@ use aether_ai_formats::api::{
|
||||
};
|
||||
use aether_ai_formats::UPSTREAM_IS_STREAM_KEY;
|
||||
use aether_contracts::ExecutionPlan;
|
||||
use aether_data_contracts::repository::usage::{
|
||||
extract_provider_reasoning_effort_from_body, extract_provider_service_tier_from_body,
|
||||
PROVIDER_REASONING_EFFORT_METADATA_KEY, PROVIDER_SERVICE_TIER_METADATA_KEY,
|
||||
};
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
const MAX_USAGE_REQUEST_METADATA_DEPTH: usize = 32;
|
||||
@@ -69,6 +73,34 @@ pub(crate) fn sanitize_usage_request_metadata_ref(value: Option<&Value>) -> Opti
|
||||
(!filtered.is_empty()).then_some(Value::Object(filtered))
|
||||
}
|
||||
|
||||
pub(crate) fn attach_provider_request_body_metadata(
|
||||
metadata: Option<Value>,
|
||||
provider_request_body: Option<&Value>,
|
||||
) -> Option<Value> {
|
||||
let reasoning_effort = extract_provider_reasoning_effort_from_body(provider_request_body);
|
||||
let service_tier = extract_provider_service_tier_from_body(provider_request_body);
|
||||
if reasoning_effort.is_none() && service_tier.is_none() {
|
||||
return metadata;
|
||||
}
|
||||
let mut object = match metadata {
|
||||
Some(Value::Object(object)) => object,
|
||||
_ => Map::new(),
|
||||
};
|
||||
if let Some(reasoning_effort) = reasoning_effort {
|
||||
object.insert(
|
||||
PROVIDER_REASONING_EFFORT_METADATA_KEY.to_string(),
|
||||
Value::String(reasoning_effort),
|
||||
);
|
||||
}
|
||||
if let Some(service_tier) = service_tier {
|
||||
object.insert(
|
||||
PROVIDER_SERVICE_TIER_METADATA_KEY.to_string(),
|
||||
Value::String(service_tier),
|
||||
);
|
||||
}
|
||||
Some(Value::Object(object))
|
||||
}
|
||||
|
||||
fn copy_allowed_metadata_fields(source: &Map<String, Value>, target: &mut Map<String, Value>) {
|
||||
copy_non_empty_string(source, target, "trace_id");
|
||||
copy_non_empty_string(source, target, "client_ip");
|
||||
@@ -81,6 +113,8 @@ fn copy_allowed_metadata_fields(source: &Map<String, Value>, target: &mut Map<St
|
||||
copy_non_empty_string(source, target, "request_path");
|
||||
copy_non_empty_string(source, target, "request_query_string");
|
||||
copy_non_empty_string(source, target, "request_path_and_query");
|
||||
copy_non_empty_string(source, target, PROVIDER_REASONING_EFFORT_METADATA_KEY);
|
||||
copy_non_empty_string(source, target, PROVIDER_SERVICE_TIER_METADATA_KEY);
|
||||
copy_number(source, target, "provider_request_body_base64_bytes");
|
||||
copy_number(source, target, "provider_response_body_base64_bytes");
|
||||
copy_number(source, target, "client_response_body_base64_bytes");
|
||||
@@ -121,6 +155,8 @@ fn move_allowed_metadata_fields(mut source: Map<String, Value>, target: &mut Map
|
||||
remove_non_empty_string(&mut source, target, "request_path");
|
||||
remove_non_empty_string(&mut source, target, "request_query_string");
|
||||
remove_non_empty_string(&mut source, target, "request_path_and_query");
|
||||
remove_non_empty_string(&mut source, target, PROVIDER_REASONING_EFFORT_METADATA_KEY);
|
||||
remove_non_empty_string(&mut source, target, PROVIDER_SERVICE_TIER_METADATA_KEY);
|
||||
remove_number(&mut source, target, "provider_request_body_base64_bytes");
|
||||
remove_number(&mut source, target, "provider_response_body_base64_bytes");
|
||||
remove_number(&mut source, target, "client_response_body_base64_bytes");
|
||||
|
||||
@@ -13,9 +13,9 @@ use crate::body_capture::{
|
||||
RuntimeBodyCaptureMetadataInput,
|
||||
};
|
||||
use crate::request_metadata::{
|
||||
build_usage_request_metadata_seed, merge_usage_request_metadata,
|
||||
merge_usage_request_metadata_owned, sanitize_usage_request_metadata,
|
||||
sanitize_usage_request_metadata_ref,
|
||||
attach_provider_request_body_metadata, build_usage_request_metadata_seed,
|
||||
merge_usage_request_metadata, merge_usage_request_metadata_owned,
|
||||
sanitize_usage_request_metadata, sanitize_usage_request_metadata_ref,
|
||||
};
|
||||
use crate::{
|
||||
map_usage_from_response, stream_capture_terminal_state, GatewayStreamReportRequest,
|
||||
@@ -559,6 +559,8 @@ fn build_terminal_usage_event_from_seed_impl(
|
||||
} else {
|
||||
merge_usage_request_metadata(request_metadata, audit_payload)
|
||||
};
|
||||
let request_metadata =
|
||||
attach_provider_request_body_metadata(request_metadata, provider_request.as_ref());
|
||||
|
||||
let mut data = UsageEventData {
|
||||
user_id,
|
||||
|
||||
@@ -54,6 +54,8 @@ export interface UsageRecordDetail {
|
||||
id: string
|
||||
provider?: string // 仅管理员可见
|
||||
model: string
|
||||
reasoning_effort?: string | null
|
||||
service_tier?: string | null
|
||||
input_tokens: number
|
||||
effective_input_tokens?: number
|
||||
output_tokens: number
|
||||
@@ -351,6 +353,8 @@ export const meApi = {
|
||||
has_format_conversion?: boolean | null
|
||||
has_fallback?: boolean | null
|
||||
target_model?: string | null
|
||||
reasoning_effort?: string | null
|
||||
service_tier?: string | null
|
||||
}>
|
||||
}> {
|
||||
const params = ids ? { ids } : {}
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface UsageRecord {
|
||||
provider_id?: string // UUID
|
||||
provider_name?: string
|
||||
model: string
|
||||
reasoning_effort?: string | null
|
||||
service_tier?: string | null
|
||||
input_tokens: number
|
||||
effective_input_tokens?: number
|
||||
output_tokens: number
|
||||
@@ -522,6 +524,8 @@ export const usageApi = {
|
||||
has_format_conversion?: boolean | null
|
||||
has_fallback?: boolean | null
|
||||
target_model?: string | null
|
||||
reasoning_effort?: string | null
|
||||
service_tier?: string | null
|
||||
image_progress?: ImageProgress | null
|
||||
}>
|
||||
}> {
|
||||
|
||||
@@ -187,7 +187,25 @@
|
||||
<!-- 第一行:模型 + 费用 -->
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<span class="text-sm font-medium truncate block">{{ record.model }}</span>
|
||||
<div class="flex min-w-0 items-center gap-1">
|
||||
<span class="text-sm font-medium truncate">{{ record.model }}</span>
|
||||
<Badge
|
||||
v-if="getReasoningEffort(record)"
|
||||
variant="outline"
|
||||
class="h-4 rounded-full border-primary/30 bg-primary/5 px-1.5 text-[10px] leading-4 text-primary flex-shrink-0"
|
||||
:title="getReasoningEffortTitle(record)"
|
||||
>
|
||||
{{ getReasoningEffort(record) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="getFastBadge(record)"
|
||||
variant="outline"
|
||||
class="h-4 rounded-full border-emerald-500/30 bg-emerald-500/10 px-1.5 text-[10px] leading-4 text-emerald-600 dark:text-emerald-400 flex-shrink-0"
|
||||
:title="getFastBadgeTitle(record)"
|
||||
>
|
||||
fast
|
||||
</Badge>
|
||||
</div>
|
||||
<span
|
||||
v-if="getActualModel(record)"
|
||||
class="text-[11px] text-muted-foreground truncate block"
|
||||
@@ -556,8 +574,24 @@
|
||||
v-if="getActualModel(record)"
|
||||
class="flex flex-col text-xs gap-0.5"
|
||||
>
|
||||
<div class="flex items-center gap-1 truncate">
|
||||
<div class="flex min-w-0 items-center gap-1">
|
||||
<span class="truncate">{{ record.model }}</span>
|
||||
<Badge
|
||||
v-if="getReasoningEffort(record)"
|
||||
variant="outline"
|
||||
class="h-4 rounded-full border-primary/30 bg-primary/5 px-1.5 text-[10px] leading-4 text-primary flex-shrink-0"
|
||||
:title="getReasoningEffortTitle(record)"
|
||||
>
|
||||
{{ getReasoningEffort(record) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="getFastBadge(record)"
|
||||
variant="outline"
|
||||
class="h-4 rounded-full border-emerald-500/30 bg-emerald-500/10 px-1.5 text-[10px] leading-4 text-emerald-600 dark:text-emerald-400 flex-shrink-0"
|
||||
:title="getFastBadgeTitle(record)"
|
||||
>
|
||||
fast
|
||||
</Badge>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
@@ -575,8 +609,26 @@
|
||||
</div>
|
||||
<span
|
||||
v-else
|
||||
class="truncate block"
|
||||
>{{ record.model }}</span>
|
||||
class="flex min-w-0 items-center gap-1"
|
||||
>
|
||||
<span class="truncate">{{ record.model }}</span>
|
||||
<Badge
|
||||
v-if="getReasoningEffort(record)"
|
||||
variant="outline"
|
||||
class="h-4 rounded-full border-primary/30 bg-primary/5 px-1.5 text-[10px] leading-4 text-primary flex-shrink-0"
|
||||
:title="getReasoningEffortTitle(record)"
|
||||
>
|
||||
{{ getReasoningEffort(record) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="getFastBadge(record)"
|
||||
variant="outline"
|
||||
class="h-4 rounded-full border-emerald-500/30 bg-emerald-500/10 px-1.5 text-[10px] leading-4 text-emerald-600 dark:text-emerald-400 flex-shrink-0"
|
||||
:title="getFastBadgeTitle(record)"
|
||||
>
|
||||
fast
|
||||
</Badge>
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
v-if="isAdmin && isColumnVisible('provider')"
|
||||
@@ -1302,12 +1354,39 @@ function getActualModel(record: UsageRecord): string | null {
|
||||
return null
|
||||
}
|
||||
|
||||
function getReasoningEffort(record: UsageRecord): string | null {
|
||||
const effort = record.reasoning_effort?.trim()
|
||||
return effort || null
|
||||
}
|
||||
|
||||
function getReasoningEffortTitle(record: UsageRecord): string {
|
||||
const effort = getReasoningEffort(record)
|
||||
return effort ? `Reasoning: ${effort}` : ''
|
||||
}
|
||||
|
||||
function getServiceTier(record: UsageRecord): string | null {
|
||||
const serviceTier = record.service_tier?.trim().toLowerCase()
|
||||
return serviceTier || null
|
||||
}
|
||||
|
||||
function getFastBadge(record: UsageRecord): boolean {
|
||||
return getServiceTier(record) === 'priority'
|
||||
}
|
||||
|
||||
function getFastBadgeTitle(record: UsageRecord): string {
|
||||
const serviceTier = getServiceTier(record)
|
||||
return serviceTier ? `Service tier: ${serviceTier}` : ''
|
||||
}
|
||||
|
||||
// 获取模型列的 tooltip
|
||||
function getModelTooltip(record: UsageRecord): string {
|
||||
const actualModel = getActualModel(record)
|
||||
const reasoningEffort = getReasoningEffort(record)
|
||||
const fastSuffix = getFastBadge(record) ? '\nService tier: priority' : ''
|
||||
const suffix = `${reasoningEffort ? `\nReasoning: ${reasoningEffort}` : ''}${fastSuffix}`
|
||||
if (actualModel) {
|
||||
return `${record.model} -> ${actualModel}`
|
||||
return `${record.model} -> ${actualModel}${suffix}`
|
||||
}
|
||||
return record.model
|
||||
return `${record.model}${suffix}`
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -278,6 +278,20 @@ describe('UsageRecordsTable', () => {
|
||||
expect(root.textContent).toContain('gpt-5')
|
||||
})
|
||||
|
||||
it('shows reasoning effort next to the model name', () => {
|
||||
const root = mountUsageRecordsTable([buildRecord({ reasoning_effort: 'xhigh' })])
|
||||
|
||||
expect(root.textContent).toContain('gpt-5')
|
||||
expect(root.textContent).toContain('xhigh')
|
||||
})
|
||||
|
||||
it('shows fast badge for priority service tier', () => {
|
||||
const root = mountUsageRecordsTable([buildRecord({ service_tier: 'priority' })])
|
||||
|
||||
expect(root.textContent).toContain('gpt-5')
|
||||
expect(root.textContent).toContain('fast')
|
||||
})
|
||||
|
||||
it('offers embedding API formats in the usage record filter', () => {
|
||||
const root = mountUsageRecordsTable([buildRecord({ api_format: 'openai:chat' })])
|
||||
|
||||
|
||||
@@ -523,7 +523,9 @@ export function useUsageData(options: UseUsageDataOptions) {
|
||||
api_key_name: existing.api_key_name || record.api_key_name,
|
||||
provider_key_name: existing.provider_key_name || record.provider_key_name,
|
||||
rate_multiplier: existing.rate_multiplier ?? record.rate_multiplier,
|
||||
target_model: existing.target_model || record.target_model
|
||||
target_model: existing.target_model || record.target_model,
|
||||
reasoning_effort: existing.reasoning_effort || record.reasoning_effort,
|
||||
service_tier: existing.service_tier || record.service_tier
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +96,8 @@ export interface UsageRecord {
|
||||
model: string
|
||||
target_model?: string | null // 映射后的目标模型名(若无映射则为空)
|
||||
model_version?: string | null // Provider 返回的实际模型版本(列表轻量字段)
|
||||
reasoning_effort?: string | null // 从发送给 Provider 的请求体提取的 reasoning 级别
|
||||
service_tier?: string | null // 从发送给 Provider 的请求体提取的服务层级
|
||||
api_format?: string
|
||||
endpoint_api_format?: string // 端点原生格式
|
||||
has_format_conversion?: boolean // 是否发生了格式转换
|
||||
|
||||
@@ -532,6 +532,16 @@ async function pollActiveRequests() {
|
||||
if ('target_model' in update && (typeof update.target_model === 'string' || update.target_model === null)) {
|
||||
record.target_model = update.target_model
|
||||
}
|
||||
if ('reasoning_effort' in update) {
|
||||
record.reasoning_effort = typeof update.reasoning_effort === 'string'
|
||||
? update.reasoning_effort
|
||||
: null
|
||||
}
|
||||
if ('service_tier' in update) {
|
||||
record.service_tier = typeof update.service_tier === 'string'
|
||||
? update.service_tier
|
||||
: null
|
||||
}
|
||||
// 管理员接口返回额外字段
|
||||
// 只有当返回的 provider 不是 pending/unknown/unknow 时才更新,避免覆盖已有的正确值
|
||||
if ('provider' in update && typeof update.provider === 'string') {
|
||||
|
||||
Reference in New Issue
Block a user