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:
AAEE86
2026-05-27 00:36:52 +08:00
parent e21cd72181
commit e27ca671fd
16 changed files with 371 additions and 13 deletions
@@ -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();
@@ -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')
+28 -2
View File
@@ -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");
+5 -3
View File
@@ -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,
+4
View File
@@ -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 } : {}
+4
View File
@@ -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
}
}
+2
View File
@@ -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 // 是否发生了格式转换
+10
View File
@@ -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') {