Files
Aether/frontend/src/api/dashboard.ts
T

536 lines
15 KiB
TypeScript
Raw Normal View History

2025-12-10 20:52:44 +08:00
import apiClient from './client'
import { cachedRequest, buildCacheKey } from '@/utils/cache'
2025-12-10 20:52:44 +08:00
const REQUEST_DETAIL_PREFETCH_TTL_MS = 5_000
2025-12-10 20:52:44 +08:00
export interface DashboardStat {
name: string
value: string
subValue?: string
change?: string
changeType?: 'increase' | 'decrease' | 'neutral'
extraBadge?: string
icon: string
}
export interface RecentRequest {
id: string // UUID
user: string
model: string
tokens: number
time: string
}
export interface ProviderStatus {
name: string
status: 'active' | 'inactive'
requests: number
}
// 系统健康指标(管理员专用)
export interface SystemHealth {
avg_response_time: number
error_rate: number
error_requests: number
fallback_count: number
total_requests: number
}
// 成本统计(管理员专用)
export interface CostStats {
total_cost: number
total_actual_cost: number
cost_savings: number
}
// 缓存统计
export interface CacheStats {
cache_creation_tokens: number
cache_read_tokens: number
cache_creation_cost?: number
cache_read_cost?: number
cache_hit_rate?: number
total_cache_tokens: number
}
// 用户统计(管理员专用)
export interface UserStats {
total: number
active: number
online?: number
2025-12-10 20:52:44 +08:00
}
// Token 详细分类
export interface TokenBreakdown {
input: number
output: number
cache_creation: number
cache_read: number
}
export interface DashboardStatsResponse {
stats: DashboardStat[]
today?: {
requests: number
tokens: number
cost: number
actual_cost?: number
cache_creation_tokens?: number
cache_read_tokens?: number
}
api_keys?: {
total: number
active: number
}
tokens?: {
month: number
}
// 管理员专用字段
system_health?: SystemHealth
cost_stats?: CostStats
cache_stats?: CacheStats
users?: UserStats
token_breakdown?: TokenBreakdown
// 普通用户专用字段
monthly_cost?: number
2025-12-10 20:52:44 +08:00
}
export interface RecentRequestsResponse {
requests: RecentRequest[]
}
export interface ProviderStatusResponse {
providers: ProviderStatus[]
}
// 视频/图像/音频计费信息
export interface VideoBilling {
task_type: 'video' | 'image' | 'audio'
duration_seconds?: number // 视频时长(秒)
resolution?: string // 分辨率
video_price_per_second?: number // 每秒单价
video_cost?: number // 视频费用
cost?: number // 总费用
rule_name?: string // 计费规则名称
expression?: string // 计费公式
status?: string // 计费状态
}
export interface RequestErrorDomain {
source?: string | null
status_code?: number | null
type?: string | null
message?: string | null
code?: string | number | null
content_type?: string | null
body?: unknown
category?: string | null
}
export interface RequestErrorDomains {
request_error?: RequestErrorDomain | null
upstream_error?: RequestErrorDomain | null
client_error?: RequestErrorDomain | null
failure_summary?: RequestErrorDomain | null
}
export interface RequestErrorFlow {
source?: string | null
status_code?: number | null
propagation?: string | null
client_response_source?: string | null
safe_to_expose_upstream?: boolean | null
summary_source?: string | null
}
export interface RequestSchedulingFailure {
source?: string | null
reason?: string | null
reason_label?: string | null
title?: string | null
message?: string | null
reason_summary?: string | null
status_code?: number | null
no_upstream_attempt?: boolean | null
}
export interface RequestPricingTier {
up_to?: number | null
input_price_per_1m?: number | null
output_price_per_1m?: number | null
cache_creation_price_per_1m?: number | null
cache_read_price_per_1m?: number | null
cache_ttl_pricing?: Array<{
ttl_minutes?: number | null
cache_creation_price_per_1m?: number | null
cache_read_price_per_1m?: number | null
}> | null
[key: string]: unknown
}
export interface RequestSettlementTieredPricing {
tiers?: RequestPricingTier[] | null
[key: string]: unknown
}
export interface RequestSettlementPricingSnapshot {
requested_processing_tier?: string | null
actual_processing_tier?: string | null
billing_processing_tier?: string | null
processing_tier_price_multiplier?: number | null
pricing_source?: string | null
tiered_pricing_source?: string | null
price_per_request_source?: string | null
tiered_pricing?: RequestSettlementTieredPricing | null
[key: string]: unknown
}
export interface RequestSettlementSnapshot {
pricing_snapshot?: RequestSettlementPricingSnapshot | null
[key: string]: unknown
}
export type RequestBodyField = 'request_body' | 'provider_request_body' | 'response_body' | 'client_response_body'
export type RequestBodyLoadErrorCode = 'too_large' | 'decode_failed' | 'missing' | 'storage_unavailable'
export class RequestBodyProtocolError extends Error {
constructor() {
super('Invalid body response')
this.name = 'RequestBodyProtocolError'
}
}
2025-12-10 20:52:44 +08:00
export interface RequestDetail {
id: string // UUID
request_id: string
user: {
id: string // UUID
username: string
email: string
}
api_key: {
id: string // UUID
name: string
display: string
}
provider: string
api_format?: string
endpoint_api_format?: string
2026-06-20 22:08:33 +08:00
has_format_conversion?: boolean | null
2025-12-10 20:52:44 +08:00
model: string
target_model?: string | null // 映射后的目标模型名
response_model?: string | null // 上游响应体实际返回的模型名
requested_reasoning_effort?: string | null
2026-06-20 22:08:33 +08:00
reasoning_effort?: string | null
service_tier?: string | null
actual_service_tier?: string | null
2025-12-10 20:52:44 +08:00
tokens: {
input: number
output: number
total: number
}
cost: {
input: number
output: number
total: number
}
// Additional token fields
input_tokens?: number
effective_input_tokens?: number
2025-12-10 20:52:44 +08:00
output_tokens?: number
total_tokens?: number
cache_creation_input_tokens?: number
cache_creation_input_tokens_5m?: number
cache_creation_input_tokens_1h?: number
2026-06-20 22:08:33 +08:00
cache_creation_ephemeral_5m_input_tokens?: number
cache_creation_ephemeral_1h_input_tokens?: number
2025-12-10 20:52:44 +08:00
cache_read_input_tokens?: number
// Additional cost fields
input_cost?: number
output_cost?: number
total_cost?: number
2026-06-20 22:08:33 +08:00
actual_cost?: number
2025-12-10 20:52:44 +08:00
cache_creation_cost?: number
cache_read_cost?: number
image_output_cost?: number
2025-12-10 20:52:44 +08:00
request_cost?: number // 按次计费费用
// Historical pricing fields (per 1M tokens)
input_price_per_1m?: number
output_price_per_1m?: number
cache_creation_price_per_1m?: number
cache_read_price_per_1m?: number
price_per_request?: number // 按次计费价格
request_type: string
is_stream: boolean
is_websocket?: boolean
websocket_transport?: string | null
usage_available?: boolean
usage_pricing_available?: boolean
input_audio_tokens?: number | null
output_audio_tokens?: number | null
live_session?: Record<string, unknown> | null
realtime_session?: Record<string, unknown> | null
upstream_is_stream?: boolean
client_requested_stream?: boolean
client_is_stream?: boolean
2025-12-10 20:52:44 +08:00
status_code: number
status?: string // pending, streaming, completed, failed, cancelled
2025-12-10 20:52:44 +08:00
error_message?: string
request_error?: RequestErrorDomain | null
upstream_error?: RequestErrorDomain | null
client_error?: RequestErrorDomain | null
failure_summary?: RequestErrorDomain | null
errors?: RequestErrorDomains | null
error_flow?: RequestErrorFlow | null
scheduling_failure?: RequestSchedulingFailure | null
2025-12-10 20:52:44 +08:00
response_time_ms: number
first_byte_time_ms?: number | null
end_to_end_time_ms?: number | null
end_to_end_first_byte_time_ms?: number | null
2025-12-10 20:52:44 +08:00
created_at: string
updated_at?: string | null
request_headers?: Record<string, unknown> | null
request_body?: Record<string, unknown> | null
provider_request_headers?: Record<string, unknown> | null
provider_request_body?: Record<string, unknown> | null
response_headers?: Record<string, unknown> | null
client_response_headers?: Record<string, unknown> | null
response_body?: Record<string, unknown> | null
client_response_body?: Record<string, unknown> | null
has_request_body?: boolean
has_provider_request_body?: boolean
has_response_body?: boolean
has_client_response_body?: boolean
2026-06-10 09:16:15 +08:00
body_load_errors?: {
request_body?: boolean
provider_request_body?: boolean
response_body?: boolean
client_response_body?: boolean
} | null
body_load_error_codes?: Partial<Record<RequestBodyField, RequestBodyLoadErrorCode>> | null
metadata?: Record<string, unknown>
routing?: Record<string, unknown>
body_capture?: Record<string, unknown>
trace?: Record<string, unknown>
settlement?: {
billing_snapshot?: Record<string, unknown>
billing_snapshot_schema_version?: string
billing_snapshot_status?: string
rate_multiplier?: number
is_free_tier?: boolean
input_price_per_1m?: number
output_price_per_1m?: number
cache_creation_price_per_1m?: number
cache_read_price_per_1m?: number
price_per_request?: number
settlement_snapshot?: RequestSettlementSnapshot | null
} | null
2025-12-10 20:52:44 +08:00
// 阶梯计费信息
tiered_pricing?: {
total_input_context: number // 总输入上下文 (input + cache_read)
tier_index: number // 命中的阶梯索引 (0-based)
tier_count: number // 阶梯总数
source?: 'provider' | 'global' // 定价来源: 提供商或全局
current_tier: RequestPricingTier // 当前命中的阶梯配置
tiers: RequestPricingTier[] // 完整阶梯配置列表
2025-12-10 20:52:44 +08:00
} | null
// 视频/图像/音频计费信息
video_billing?: VideoBilling | null
2025-12-10 20:52:44 +08:00
}
export interface CurlData {
url: string
method: string
headers: Record<string, string>
body: Record<string, unknown>
curl: string
}
export interface ReplayRequest {
provider_id?: string
endpoint_id?: string
api_key_id?: string
body_override?: Record<string, unknown>
}
export interface ReplayResponse {
url: string
provider: string
status_code: number
response_headers: Record<string, string>
response_body: Record<string, unknown>
response_time_ms: number
2026-03-17 17:56:06 +08:00
mapping?: {
source_model: string
original_target_model?: string | null
resolved_model: string
target_provider_id: string
target_provider: string
target_endpoint_id: string
target_api_format: string
replay_mode: string
mapping_applied: boolean
mapping_source: string
}
}
2025-12-10 20:52:44 +08:00
export interface ModelBreakdown {
model: string
requests: number
tokens: number
cost: number
}
export interface ModelSummary {
model: string
requests: number
tokens: number
cost: number
avg_response_time: number
cost_per_request: number
tokens_per_request: number
}
export interface ProviderSummary {
provider: string
requests: number
tokens: number
cost: number
}
2025-12-10 20:52:44 +08:00
export interface DailyStat {
date: string // ISO date string
requests: number
tokens: number
cost: number
avg_response_time: number // in seconds
unique_models: number
unique_providers?: number // 仅管理员返回
2025-12-10 20:52:44 +08:00
model_breakdown: ModelBreakdown[]
}
export interface DailyStatsResponse {
daily_stats: DailyStat[]
model_summary: ModelSummary[]
provider_summary?: ProviderSummary[] // 仅管理员返回
2025-12-10 20:52:44 +08:00
period: {
start_date: string
end_date: string
days: number
}
}
export interface TimeRangeParams {
start_date?: string
end_date?: string
preset?: string
granularity?: 'hour' | 'day' | 'week' | 'month'
timezone?: string
tz_offset_minutes?: number
}
2025-12-10 20:52:44 +08:00
export const dashboardApi = {
// 获取仪表盘统计数据
async getStats(params?: TimeRangeParams): Promise<DashboardStatsResponse> {
const cacheKey = buildCacheKey('dashboard:stats', params as Record<string, unknown> | undefined)
return cachedRequest(
cacheKey,
async () => {
const response = await apiClient.get<DashboardStatsResponse>('/api/dashboard/stats', { params })
return response.data
},
30 * 1000
)
2025-12-10 20:52:44 +08:00
},
// 获取最近的请求记录
async getRecentRequests(limit: number = 10): Promise<RecentRequest[]> {
const response = await apiClient.get<RecentRequestsResponse>('/api/dashboard/recent-requests', {
params: { limit }
})
return response.data.requests
},
// 获取提供商状态
async getProviderStatus(): Promise<ProviderStatus[]> {
return cachedRequest(
'dashboard:provider-status',
async () => {
const response = await apiClient.get<ProviderStatusResponse>('/api/dashboard/provider-status')
return response.data.providers
},
20 * 1000
)
2025-12-10 20:52:44 +08:00
},
// 获取请求详情
// NOTE: This method now calls the new RESTful API at /api/admin/usage/{id}
async getRequestDetail(
requestId: string,
options: { includeBodies?: boolean, cacheTtlMs?: number, signal?: AbortSignal } = {}
): Promise<RequestDetail> {
const includeBodies = options.includeBodies ?? false
const cacheTtlMs = options.cacheTtlMs ?? 0
const cacheKey = buildCacheKey('dashboard:request-detail', { requestId, includeBodies })
const fetchDetail = async () => {
const response = await apiClient.get<RequestDetail>(`/api/admin/usage/${requestId}`, {
params: { include_bodies: includeBodies },
...(options.signal ? { signal: options.signal } : {}),
})
return response.data
}
return options.signal ? fetchDetail() : cachedRequest(cacheKey, fetchDetail, cacheTtlMs)
},
async getRequestBody(requestId: string, field: RequestBodyField, signal?: AbortSignal, onProgress?: (loaded: number) => void) {
const response = await apiClient.get<ArrayBuffer>(`/api/admin/usage/${requestId}`, {
params: { include_bodies: true, body_field: field, body_format: 'raw' },
responseType: 'arraybuffer',
signal,
...(onProgress ? { onDownloadProgress: (event: { loaded: number }) => onProgress(event.loaded) } : {}),
})
const encoding = response.headers['x-aether-body-encoding']
if ((encoding !== 'gzip' && encoding !== 'json') || response.headers['x-aether-usage-id'] !== requestId || response.headers['x-aether-body-field'] !== field) {
throw new RequestBodyProtocolError()
}
return { bytes: response.data, encoding: encoding as 'gzip' | 'json' }
},
async prefetchRequestDetail(requestId: string): Promise<void> {
await dashboardApi.getRequestDetail(requestId, {
includeBodies: false,
cacheTtlMs: REQUEST_DETAIL_PREFETCH_TTL_MS
})
2025-12-10 20:52:44 +08:00
},
// 获取每日统计数据
async getDailyStats(params?: TimeRangeParams & { days?: number }): Promise<DailyStatsResponse> {
const cacheKey = buildCacheKey('dashboard:daily-stats', params as Record<string, unknown> | undefined)
return cachedRequest(
cacheKey,
async () => {
const response = await apiClient.get<DailyStatsResponse>('/api/dashboard/daily-stats', {
params
})
return response.data
},
60 * 1000
)
},
// 获取 cURL 命令数据(含明文 API Key)
async getCurlData(requestId: string): Promise<CurlData> {
const response = await apiClient.get<CurlData>(`/api/admin/usage/${requestId}/curl`)
return response.data
},
// 回放请求到提供商
async replayRequest(requestId: string, params?: ReplayRequest): Promise<ReplayResponse> {
const response = await apiClient.post<ReplayResponse>(
`/api/admin/usage/${requestId}/replay`,
params || {}
)
return response.data
2025-12-10 20:52:44 +08:00
}
}