mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-12 22:20:19 +08:00
refactor(workspace): enforce layered crate boundaries
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { getMock, cachedRequestMock, buildCacheKeyMock } = vi.hoisted(() => ({
|
||||
getMock: vi.fn(),
|
||||
cachedRequestMock: vi.fn(async (_key: string, fetcher: () => Promise<unknown>) => fetcher()),
|
||||
buildCacheKeyMock: vi.fn((prefix: string) => prefix),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/client', () => ({
|
||||
default: {
|
||||
get: getMock,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/cache', () => ({
|
||||
cache: {
|
||||
clear: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
cachedRequest: cachedRequestMock,
|
||||
buildCacheKey: buildCacheKeyMock,
|
||||
}))
|
||||
|
||||
import { adminApi } from '@/api/admin'
|
||||
|
||||
describe('adminApi analytics cache options', () => {
|
||||
const params = {
|
||||
start_date: '2026-07-01',
|
||||
end_date: '2026-07-15',
|
||||
preset: 'custom',
|
||||
timezone: 'Asia/Shanghai',
|
||||
tz_offset_minutes: 480,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
getMock.mockReset()
|
||||
getMock.mockResolvedValue({ data: {} })
|
||||
cachedRequestMock.mockClear()
|
||||
buildCacheKeyMock.mockClear()
|
||||
})
|
||||
|
||||
it('keeps the existing 20-second cache TTL by default', async () => {
|
||||
await adminApi.getTimeSeries(params)
|
||||
await adminApi.getPercentiles(params)
|
||||
await adminApi.getProviderPerformance(params)
|
||||
await adminApi.getErrorDistribution(params)
|
||||
|
||||
for (let call = 1; call <= 4; call += 1) {
|
||||
expect(cachedRequestMock).toHaveBeenNthCalledWith(
|
||||
call,
|
||||
expect.any(String),
|
||||
expect.any(Function),
|
||||
20 * 1000
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('uses a zero TTL when an analytics request skips the cache', async () => {
|
||||
const options = { skipCache: true }
|
||||
const providerParams = { ...params, include_timeline: false }
|
||||
|
||||
await adminApi.getTimeSeries(params, options)
|
||||
await adminApi.getPercentiles(params, options)
|
||||
await adminApi.getProviderPerformance(providerParams, options)
|
||||
await adminApi.getErrorDistribution(params, options)
|
||||
|
||||
for (let call = 1; call <= 4; call += 1) {
|
||||
expect(cachedRequestMock).toHaveBeenNthCalledWith(
|
||||
call,
|
||||
expect.any(String),
|
||||
expect.any(Function),
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
expect(getMock).toHaveBeenNthCalledWith(1, '/api/admin/stats/time-series', { params })
|
||||
expect(getMock).toHaveBeenNthCalledWith(2, '/api/admin/stats/performance/percentiles', {
|
||||
params,
|
||||
})
|
||||
expect(getMock).toHaveBeenNthCalledWith(3, '/api/admin/stats/performance/providers', {
|
||||
params: providerParams,
|
||||
})
|
||||
expect(getMock).toHaveBeenNthCalledWith(4, '/api/admin/stats/errors/distribution', { params })
|
||||
})
|
||||
})
|
||||
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { AxiosAdapter, AxiosInstance, InternalAxiosRequestConfig } from 'axios'
|
||||
|
||||
import apiClient, { AUTH_STATE_CHANGE_EVENT } from '@/api/client'
|
||||
import { cache, cachedRequest } from '@/utils/cache'
|
||||
|
||||
type TestableApiClient = typeof apiClient & {
|
||||
client: AxiosInstance
|
||||
@@ -34,6 +35,42 @@ describe('apiClient auth state change event', () => {
|
||||
window.removeEventListener(AUTH_STATE_CHANGE_EVENT, handler as EventListener)
|
||||
})
|
||||
|
||||
it('clears cached API data whenever the authentication identity changes', () => {
|
||||
apiClient.setToken('first-token')
|
||||
cache.set('dashboard', { owner: 'first-user' }, 30_000)
|
||||
|
||||
apiClient.setToken('second-token')
|
||||
|
||||
expect(cache.get('dashboard')).toBeNull()
|
||||
})
|
||||
|
||||
it('does not share or restore an in-flight cached response across token changes', async () => {
|
||||
let resolveFirst!: (value: string) => void
|
||||
let resolveSecond!: (value: string) => void
|
||||
const firstResponse = new Promise<string>((resolve) => {
|
||||
resolveFirst = resolve
|
||||
})
|
||||
const secondResponse = new Promise<string>((resolve) => {
|
||||
resolveSecond = resolve
|
||||
})
|
||||
const secondFetcher = vi.fn(() => secondResponse)
|
||||
|
||||
apiClient.setToken('first-token')
|
||||
const firstRequest = cachedRequest('dashboard', () => firstResponse, 30_000)
|
||||
|
||||
apiClient.setToken('second-token')
|
||||
const secondRequest = cachedRequest('dashboard', secondFetcher, 30_000)
|
||||
expect(secondFetcher).toHaveBeenCalledTimes(1)
|
||||
|
||||
resolveFirst('first-user-data')
|
||||
await expect(firstRequest).resolves.toBe('first-user-data')
|
||||
expect(cache.get('dashboard')).toBeNull()
|
||||
|
||||
resolveSecond('second-user-data')
|
||||
await expect(secondRequest).resolves.toBe('second-user-data')
|
||||
expect(cache.get('dashboard')).toBe('second-user-data')
|
||||
})
|
||||
|
||||
it('sends auth refresh without a request body', async () => {
|
||||
const rawClient = apiClient as TestableApiClient
|
||||
const previousAdapter = rawClient.client.defaults.adapter
|
||||
|
||||
+88
-49
@@ -1,11 +1,19 @@
|
||||
import apiClient from './client'
|
||||
import type { ModelTestCapabilities } from './endpoints/types'
|
||||
import axios, { type AxiosRequestConfig } from 'axios'
|
||||
import { cachedRequest, buildCacheKey } from '@/utils/cache'
|
||||
import { cache, cachedRequest, buildCacheKey } from '@/utils/cache'
|
||||
import type { BillingSummary } from './auth'
|
||||
import type { ApiKeyInstallSession, InstallSessionTargetSystem, InstallTargetCli } from './me'
|
||||
|
||||
const SYSTEM_DATA_IMPORT_TIMEOUT_MS = 10 * 60 * 1000
|
||||
const ALL_SYSTEM_CONFIGS_CACHE_KEY = 'admin:system:configs'
|
||||
|
||||
export interface AdminSystemConfigItem {
|
||||
key: string
|
||||
value: unknown
|
||||
description?: string
|
||||
is_set?: boolean
|
||||
}
|
||||
|
||||
export interface SystemDataImportOptions {
|
||||
onUploadProgress?: AxiosRequestConfig['onUploadProgress']
|
||||
@@ -736,6 +744,10 @@ export interface QuotaUsageResponse {
|
||||
providers: QuotaUsageProvider[]
|
||||
}
|
||||
|
||||
export interface AdminAnalyticsRequestOptions {
|
||||
skipCache?: boolean
|
||||
}
|
||||
|
||||
export interface PercentileItem {
|
||||
date: string
|
||||
p50_response_time_ms?: number | null
|
||||
@@ -947,9 +959,17 @@ export const adminApi = {
|
||||
|
||||
// 系统配置相关
|
||||
// 获取所有系统配置
|
||||
async getAllSystemConfigs(): Promise<Array<{ key: string; value: unknown; description?: string }>> {
|
||||
const response = await apiClient.get<Array<{ key: string; value: unknown; description?: string }>>('/api/admin/system/configs')
|
||||
return response.data
|
||||
async getAllSystemConfigs(
|
||||
options: { cacheTtlMs?: number } = {},
|
||||
): Promise<AdminSystemConfigItem[]> {
|
||||
return cachedRequest(
|
||||
ALL_SYSTEM_CONFIGS_CACHE_KEY,
|
||||
async () => {
|
||||
const response = await apiClient.get<AdminSystemConfigItem[]>('/api/admin/system/configs')
|
||||
return response.data
|
||||
},
|
||||
options.cacheTtlMs ?? 0,
|
||||
)
|
||||
},
|
||||
|
||||
// 获取特定系统配置
|
||||
@@ -983,6 +1003,8 @@ export const adminApi = {
|
||||
{ value, description },
|
||||
requestConfig,
|
||||
)
|
||||
cache.delete(ALL_SYSTEM_CONFIGS_CACHE_KEY)
|
||||
cache.delete(buildCacheKey('admin:system:config', { key }))
|
||||
return response.data
|
||||
},
|
||||
|
||||
@@ -991,6 +1013,8 @@ export const adminApi = {
|
||||
const response = await apiClient.delete<{ message: string }>(
|
||||
`/api/admin/system/configs/${key}`
|
||||
)
|
||||
cache.delete(ALL_SYSTEM_CONFIGS_CACHE_KEY)
|
||||
cache.delete(buildCacheKey('admin:system:config', { key }))
|
||||
return response.data
|
||||
},
|
||||
|
||||
@@ -1021,6 +1045,7 @@ export const adminApi = {
|
||||
data,
|
||||
{ timeout: SYSTEM_DATA_IMPORT_TIMEOUT_MS, ...options }
|
||||
)
|
||||
cache.clear()
|
||||
return response.data
|
||||
},
|
||||
|
||||
@@ -1053,6 +1078,7 @@ export const adminApi = {
|
||||
data,
|
||||
{ timeout: SYSTEM_DATA_IMPORT_TIMEOUT_MS, ...options }
|
||||
)
|
||||
cache.clear()
|
||||
return response.data
|
||||
},
|
||||
|
||||
@@ -1380,13 +1406,16 @@ export const adminApi = {
|
||||
)
|
||||
},
|
||||
|
||||
async getPercentiles(params?: {
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
preset?: string
|
||||
timezone?: string
|
||||
tz_offset_minutes?: number
|
||||
}): Promise<PercentileItem[]> {
|
||||
async getPercentiles(
|
||||
params?: {
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
preset?: string
|
||||
timezone?: string
|
||||
tz_offset_minutes?: number
|
||||
},
|
||||
options?: AdminAnalyticsRequestOptions
|
||||
): Promise<PercentileItem[]> {
|
||||
const cacheKey = buildCacheKey('admin:stats:performance:percentiles', params)
|
||||
return cachedRequest(
|
||||
cacheKey,
|
||||
@@ -1396,26 +1425,30 @@ export const adminApi = {
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
20 * 1000
|
||||
options?.skipCache ? 0 : 20 * 1000
|
||||
)
|
||||
},
|
||||
|
||||
async getProviderPerformance(params?: {
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
preset?: string
|
||||
timezone?: string
|
||||
tz_offset_minutes?: number
|
||||
granularity?: 'day' | 'hour'
|
||||
limit?: number
|
||||
provider_id?: string
|
||||
model?: string
|
||||
api_format?: string
|
||||
endpoint_kind?: string
|
||||
is_stream?: boolean
|
||||
has_format_conversion?: boolean
|
||||
slow_threshold_ms?: number
|
||||
}): Promise<ProviderPerformanceResponse> {
|
||||
async getProviderPerformance(
|
||||
params?: {
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
preset?: string
|
||||
timezone?: string
|
||||
tz_offset_minutes?: number
|
||||
granularity?: 'day' | 'hour'
|
||||
include_timeline?: boolean
|
||||
limit?: number
|
||||
provider_id?: string
|
||||
model?: string
|
||||
api_format?: string
|
||||
endpoint_kind?: string
|
||||
is_stream?: boolean
|
||||
has_format_conversion?: boolean
|
||||
slow_threshold_ms?: number
|
||||
},
|
||||
options?: AdminAnalyticsRequestOptions
|
||||
): Promise<ProviderPerformanceResponse> {
|
||||
const cacheKey = buildCacheKey('admin:stats:performance:providers', params)
|
||||
return cachedRequest(
|
||||
cacheKey,
|
||||
@@ -1425,17 +1458,20 @@ export const adminApi = {
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
20 * 1000
|
||||
options?.skipCache ? 0 : 20 * 1000
|
||||
)
|
||||
},
|
||||
|
||||
async getErrorDistribution(params?: {
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
preset?: string
|
||||
timezone?: string
|
||||
tz_offset_minutes?: number
|
||||
}): Promise<ErrorDistributionResponse> {
|
||||
async getErrorDistribution(
|
||||
params?: {
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
preset?: string
|
||||
timezone?: string
|
||||
tz_offset_minutes?: number
|
||||
},
|
||||
options?: AdminAnalyticsRequestOptions
|
||||
): Promise<ErrorDistributionResponse> {
|
||||
const cacheKey = buildCacheKey('admin:stats:errors:distribution', params)
|
||||
return cachedRequest(
|
||||
cacheKey,
|
||||
@@ -1445,7 +1481,7 @@ export const adminApi = {
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
20 * 1000
|
||||
options?.skipCache ? 0 : 20 * 1000
|
||||
)
|
||||
},
|
||||
|
||||
@@ -1535,17 +1571,20 @@ export const adminApi = {
|
||||
return response.data
|
||||
},
|
||||
|
||||
async getTimeSeries(params?: {
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
preset?: string
|
||||
granularity?: 'hour' | 'day' | 'week' | 'month'
|
||||
timezone?: string
|
||||
tz_offset_minutes?: number
|
||||
user_id?: string
|
||||
model?: string
|
||||
provider_name?: string
|
||||
}): Promise<Array<Record<string, unknown>>> {
|
||||
async getTimeSeries(
|
||||
params?: {
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
preset?: string
|
||||
granularity?: 'hour' | 'day' | 'week' | 'month'
|
||||
timezone?: string
|
||||
tz_offset_minutes?: number
|
||||
user_id?: string
|
||||
model?: string
|
||||
provider_name?: string
|
||||
},
|
||||
options?: AdminAnalyticsRequestOptions
|
||||
): Promise<Array<Record<string, unknown>>> {
|
||||
const cacheKey = buildCacheKey('admin:stats:time-series', params)
|
||||
return cachedRequest(
|
||||
cacheKey,
|
||||
@@ -1553,7 +1592,7 @@ export const adminApi = {
|
||||
const response = await apiClient.get<Array<Record<string, unknown>>>('/api/admin/stats/time-series', { params })
|
||||
return response.data
|
||||
},
|
||||
20 * 1000
|
||||
options?.skipCache ? 0 : 20 * 1000
|
||||
)
|
||||
},
|
||||
|
||||
|
||||
+30
-10
@@ -2,15 +2,30 @@ import axios, { getAdapter } from 'axios'
|
||||
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig, AxiosAdapter } from 'axios'
|
||||
import { NETWORK_CONFIG, AUTH_CONFIG } from '@/config/constants'
|
||||
import { isDemoMode } from '@/config/demo'
|
||||
import { handleMockRequest, setMockUserToken } from '@/mocks'
|
||||
import { getClientDeviceId } from '@/utils/deviceId'
|
||||
import { CrossTabRefreshCoordinator } from '@/utils/crossTabRefresh'
|
||||
import { log } from '@/utils/logger'
|
||||
import { cache } from '@/utils/cache'
|
||||
|
||||
// 在开发环境下使用代理,生产环境使用环境变量
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL || ''
|
||||
export const AUTH_STATE_CHANGE_EVENT = 'aether-auth-state-change'
|
||||
|
||||
type MockRuntime = typeof import('@/mocks')
|
||||
|
||||
let mockRuntimePromise: Promise<MockRuntime> | null = null
|
||||
let currentMockUserToken: string | null = null
|
||||
|
||||
function loadMockRuntime(): Promise<MockRuntime> {
|
||||
if (!mockRuntimePromise) {
|
||||
mockRuntimePromise = import('@/mocks').catch((error) => {
|
||||
mockRuntimePromise = null
|
||||
throw error
|
||||
})
|
||||
}
|
||||
return mockRuntimePromise
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断请求是否为公共端点
|
||||
*/
|
||||
@@ -53,7 +68,9 @@ function createDemoAdapter(defaultAdapter: AxiosAdapter) {
|
||||
return async (config: InternalAxiosRequestConfig): Promise<AxiosResponse> => {
|
||||
if (isDemoMode()) {
|
||||
try {
|
||||
const mockResponse = await handleMockRequest({
|
||||
const mockRuntime = await loadMockRuntime()
|
||||
mockRuntime.setMockUserToken(currentMockUserToken)
|
||||
const mockResponse = await mockRuntime.handleMockRequest({
|
||||
method: config.method?.toUpperCase(),
|
||||
url: config.url,
|
||||
data: config.data,
|
||||
@@ -290,30 +307,33 @@ class ApiClient {
|
||||
}
|
||||
|
||||
private syncTokenState(token: string | null): void {
|
||||
this.token = token
|
||||
if (isDemoMode()) {
|
||||
setMockUserToken(token)
|
||||
if (this.token !== token) {
|
||||
cache.clear()
|
||||
}
|
||||
this.token = token
|
||||
currentMockUserToken = token
|
||||
}
|
||||
|
||||
setToken(token: string): void {
|
||||
if (this.token === token) {
|
||||
cache.clear()
|
||||
}
|
||||
this.syncTokenState(token)
|
||||
localStorage.setItem('access_token', token)
|
||||
}
|
||||
|
||||
getToken(): string | null {
|
||||
if (!this.token) {
|
||||
this.token = localStorage.getItem('access_token')
|
||||
// 页面刷新时,从 localStorage 恢复 token 到 mock handler
|
||||
if (this.token && isDemoMode()) {
|
||||
setMockUserToken(this.token)
|
||||
}
|
||||
this.syncTokenState(localStorage.getItem('access_token'))
|
||||
}
|
||||
return this.token
|
||||
}
|
||||
|
||||
clearAuth(): void {
|
||||
const hadAuth = this.token !== null || localStorage.getItem('access_token') !== null
|
||||
if (hadAuth && this.token === null) {
|
||||
cache.clear()
|
||||
}
|
||||
this.syncTokenState(null)
|
||||
localStorage.removeItem('access_token')
|
||||
// 同标签页内清理认证状态时不会触发 storage 事件,这里主动广播一次。
|
||||
|
||||
@@ -413,7 +413,7 @@ export const dashboardApi = {
|
||||
const response = await apiClient.get<DashboardStatsResponse>('/api/dashboard/stats', { params })
|
||||
return response.data
|
||||
},
|
||||
10 * 1000
|
||||
30 * 1000
|
||||
)
|
||||
},
|
||||
|
||||
@@ -476,7 +476,7 @@ export const dashboardApi = {
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
20 * 1000
|
||||
60 * 1000
|
||||
)
|
||||
},
|
||||
|
||||
|
||||
+28
-15
@@ -1,6 +1,8 @@
|
||||
import apiClient from './client'
|
||||
import { buildCacheKey, cache, cachedRequest } from '@/utils/cache'
|
||||
|
||||
const MODULE_MANAGEMENT_ORDER_CONFIG_KEY = 'module_management.extension_order'
|
||||
const ALL_SYSTEM_CONFIGS_CACHE_KEY = 'admin:system:configs'
|
||||
|
||||
export interface ModuleStatus {
|
||||
name: string
|
||||
@@ -160,19 +162,30 @@ function normalizePlaceholderPrefix(value: unknown): string {
|
||||
: CHAT_PII_REDACTION_DEFAULT_CONFIG.placeholder_prefix
|
||||
}
|
||||
|
||||
async function getSystemConfigValue(key: string): Promise<unknown> {
|
||||
const response = await apiClient.get<{ key: string; value: unknown }>(`/api/admin/system/configs/${key}`)
|
||||
return response.data.value
|
||||
}
|
||||
|
||||
async function updateSystemConfigValue(key: string, value: unknown, description: string) {
|
||||
const response = await apiClient.put<{ key: string; value: unknown; description?: string }>(
|
||||
`/api/admin/system/configs/${key}`,
|
||||
{ value, description },
|
||||
)
|
||||
cache.delete(ALL_SYSTEM_CONFIGS_CACHE_KEY)
|
||||
cache.delete(buildCacheKey('admin:system:config', { key }))
|
||||
return response.data.value
|
||||
}
|
||||
|
||||
async function getAllSystemConfigValues(): Promise<Map<string, unknown>> {
|
||||
const configs = await cachedRequest(
|
||||
ALL_SYSTEM_CONFIGS_CACHE_KEY,
|
||||
async () => {
|
||||
const response = await apiClient.get<Array<{ key: string; value: unknown }>>(
|
||||
'/api/admin/system/configs'
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
30_000,
|
||||
)
|
||||
return new Map(configs.map(config => [config.key, config.value]))
|
||||
}
|
||||
|
||||
export const modulesApi = {
|
||||
/**
|
||||
* 获取所有模块状态(管理员)
|
||||
@@ -202,6 +215,7 @@ export const modulesApi = {
|
||||
`/api/admin/modules/status/${moduleName}/enabled`,
|
||||
{ enabled }
|
||||
)
|
||||
cache.delete(ALL_SYSTEM_CONFIGS_CACHE_KEY)
|
||||
return response.data
|
||||
},
|
||||
|
||||
@@ -227,22 +241,21 @@ export const modulesApi = {
|
||||
description: '模块管理扩展模块展示顺序',
|
||||
},
|
||||
)
|
||||
cache.delete(ALL_SYSTEM_CONFIGS_CACHE_KEY)
|
||||
cache.delete(buildCacheKey('admin:system:config', {
|
||||
key: MODULE_MANAGEMENT_ORDER_CONFIG_KEY,
|
||||
}))
|
||||
return normalizeModuleManagementOrder(response.data.value)
|
||||
},
|
||||
|
||||
async getChatPiiRedactionConfig(): Promise<ChatPiiRedactionConfig> {
|
||||
const [enabled, rules, cacheTtlSeconds, placeholderPrefix] = await Promise.all([
|
||||
getSystemConfigValue(CHAT_PII_REDACTION_CONFIG_KEYS.enabled),
|
||||
getSystemConfigValue(CHAT_PII_REDACTION_CONFIG_KEYS.rules),
|
||||
getSystemConfigValue(CHAT_PII_REDACTION_CONFIG_KEYS.cache_ttl_seconds),
|
||||
getSystemConfigValue(CHAT_PII_REDACTION_CONFIG_KEYS.placeholder_prefix),
|
||||
])
|
||||
const configsByKey = await getAllSystemConfigValues()
|
||||
|
||||
return normalizeChatPiiRedactionConfig({
|
||||
enabled,
|
||||
rules,
|
||||
cache_ttl_seconds: cacheTtlSeconds,
|
||||
placeholder_prefix: placeholderPrefix,
|
||||
enabled: configsByKey.get(CHAT_PII_REDACTION_CONFIG_KEYS.enabled),
|
||||
rules: configsByKey.get(CHAT_PII_REDACTION_CONFIG_KEYS.rules),
|
||||
cache_ttl_seconds: configsByKey.get(CHAT_PII_REDACTION_CONFIG_KEYS.cache_ttl_seconds),
|
||||
placeholder_prefix: configsByKey.get(CHAT_PII_REDACTION_CONFIG_KEYS.placeholder_prefix),
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -44,6 +44,8 @@ export interface UsageStats {
|
||||
total_cost: number
|
||||
total_actual_cost?: number
|
||||
avg_response_time: number
|
||||
error_count?: number
|
||||
error_rate?: number
|
||||
today?: {
|
||||
requests: number
|
||||
tokens: number
|
||||
|
||||
Reference in New Issue
Block a user