mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 00:17:45 +08:00
536 lines
15 KiB
TypeScript
536 lines
15 KiB
TypeScript
import apiClient from './client'
|
||
import { cachedRequest, buildCacheKey } from '@/utils/cache'
|
||
|
||
const REQUEST_DETAIL_PREFETCH_TTL_MS = 5_000
|
||
|
||
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
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
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'
|
||
}
|
||
}
|
||
|
||
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
|
||
has_format_conversion?: boolean | null
|
||
model: string
|
||
target_model?: string | null // 映射后的目标模型名
|
||
response_model?: string | null // 上游响应体实际返回的模型名
|
||
requested_reasoning_effort?: string | null
|
||
reasoning_effort?: string | null
|
||
service_tier?: string | null
|
||
actual_service_tier?: string | null
|
||
tokens: {
|
||
input: number
|
||
output: number
|
||
total: number
|
||
}
|
||
cost: {
|
||
input: number
|
||
output: number
|
||
total: number
|
||
}
|
||
// Additional token fields
|
||
input_tokens?: number
|
||
effective_input_tokens?: number
|
||
output_tokens?: number
|
||
total_tokens?: number
|
||
cache_creation_input_tokens?: number
|
||
cache_creation_input_tokens_5m?: number
|
||
cache_creation_input_tokens_1h?: number
|
||
cache_creation_ephemeral_5m_input_tokens?: number
|
||
cache_creation_ephemeral_1h_input_tokens?: number
|
||
cache_read_input_tokens?: number
|
||
// Additional cost fields
|
||
input_cost?: number
|
||
output_cost?: number
|
||
total_cost?: number
|
||
actual_cost?: number
|
||
cache_creation_cost?: number
|
||
cache_read_cost?: number
|
||
image_output_cost?: number
|
||
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
|
||
status_code: number
|
||
status?: string // pending, streaming, completed, failed, cancelled
|
||
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
|
||
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
|
||
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
|
||
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
|
||
// 阶梯计费信息
|
||
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[] // 完整阶梯配置列表
|
||
} | null
|
||
// 视频/图像/音频计费信息
|
||
video_billing?: VideoBilling | null
|
||
}
|
||
|
||
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
|
||
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
|
||
}
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
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 // 仅管理员返回
|
||
model_breakdown: ModelBreakdown[]
|
||
}
|
||
|
||
export interface DailyStatsResponse {
|
||
daily_stats: DailyStat[]
|
||
model_summary: ModelSummary[]
|
||
provider_summary?: ProviderSummary[] // 仅管理员返回
|
||
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
|
||
}
|
||
|
||
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
|
||
)
|
||
},
|
||
|
||
// 获取最近的请求记录
|
||
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
|
||
)
|
||
},
|
||
|
||
// 获取请求详情
|
||
// 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
|
||
})
|
||
},
|
||
|
||
// 获取每日统计数据
|
||
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
|
||
}
|
||
}
|