// 使用统计相关类型定义 export interface TokenUsage { inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheCreationTokens: number; } export interface RequestLog { requestId: string; providerId: string; providerName?: string; appType: string; model: string; requestModel?: string; costMultiplier: string; inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheCreationTokens: number; inputCostUsd: string; outputCostUsd: string; cacheReadCostUsd: string; cacheCreationCostUsd: string; totalCostUsd: string; isStreaming: boolean; latencyMs: number; firstTokenMs?: number; durationMs?: number; statusCode: number; errorMessage?: string; createdAt: number; dataSource?: string; } export interface SessionSyncResult { imported: number; skipped: number; filesScanned: number; errors: string[]; } export interface DataSourceSummary { dataSource: string; requestCount: number; totalCostUsd: string; } export interface PaginatedLogs { data: RequestLog[]; total: number; page: number; pageSize: number; } export interface ModelPricing { modelId: string; displayName: string; inputCostPerMillion: string; outputCostPerMillion: string; cacheReadCostPerMillion: string; cacheCreationCostPerMillion: string; } export interface UsageSummary { totalRequests: number; totalCost: string; totalInputTokens: number; totalOutputTokens: number; totalCacheCreationTokens: number; totalCacheReadTokens: number; successRate: number; /** input + output + cache_creation + cache_read, all cache-normalized */ realTotalTokens: number; /** cache_read / (input + cache_creation + cache_read), range 0–1 */ cacheHitRate: number; } export interface UsageSummaryByApp { appType: string; summary: UsageSummary; } export interface DailyStats { date: string; requestCount: number; totalCost: string; totalTokens: number; totalInputTokens: number; totalOutputTokens: number; totalCacheCreationTokens: number; totalCacheReadTokens: number; } export interface ProviderStats { providerId: string; providerName: string; requestCount: number; totalTokens: number; totalCost: string; successRate: number; avgLatencyMs: number; } export interface ModelStats { model: string; requestCount: number; totalTokens: number; totalCost: string; avgCostPerRequest: string; } export interface LogFilters { appType?: string; providerName?: string; model?: string; statusCode?: number; startDate?: number; endDate?: number; } export interface ProviderLimitStatus { providerId: string; dailyUsage: string; dailyLimit?: string; dailyExceeded: boolean; monthlyUsage: string; monthlyLimit?: string; monthlyExceeded: boolean; } export type UsageRangePreset = "today" | "1d" | "7d" | "14d" | "30d" | "custom"; export interface UsageRangeSelection { preset: UsageRangePreset; customStartDate?: number; customEndDate?: number; } /** * App types whose token usage is reliably collected by the proxy. * * The proxy has handlers for `claude-desktop` too, but in practice those * requests overwhelmingly fail (500/503) and contribute near-zero tokens, so * it is hidden from the Dashboard. `opencode` / `openclaw` / `hermes` have * no proxy handler at all — they appear only as managed apps elsewhere. */ export type AppType = "claude" | "codex" | "gemini"; export type AppTypeFilter = "all" | AppType; export const KNOWN_APP_TYPES: ReadonlyArray = [ "claude", "codex", "gemini", ]; /** * App types whose proxy uses an OpenAI-style protocol. Two consequences: * * 1. `inputTokens` already includes the cached portion (must subtract * `cacheReadTokens` to get fresh-input semantics — see * [getFreshInputTokens]). * 2. The protocol does not report cache _creation_ separately, only cache * _reads_. So `cacheCreationTokens` is always 0 for these app types and * the UI should label it as N/A rather than 0. * * Mirror of the Rust `CACHE_INCLUSIVE_APP_TYPES` whitelist. */ export const CACHE_INCLUSIVE_APP_TYPES: ReadonlySet = new Set([ "codex", "gemini", ]); /** Subset of request-log fields needed to derive cache-normalized input. */ export interface CacheNormalizableLog { appType: string; inputTokens: number; cacheReadTokens: number; } /** * For a single request log, return the input token count with cache reads * removed. Anthropic-style providers already report `inputTokens` without * cache, so they pass through unchanged. */ export function getFreshInputTokens(log: CacheNormalizableLog): number { if ( CACHE_INCLUSIVE_APP_TYPES.has(log.appType) && log.inputTokens >= log.cacheReadTokens ) { return log.inputTokens - log.cacheReadTokens; } return log.inputTokens; } export const NON_NEGATIVE_DECIMAL_REGEX = /^\d+(?:\.\d+)?$/; export function isNonNegativeDecimalString(value: string): boolean { const trimmed = value.trim(); if (!NON_NEGATIVE_DECIMAL_REGEX.test(trimmed)) return false; return Number.isFinite(Number(trimmed)); } type UsageCostLog = Pick< RequestLog, | "inputTokens" | "outputTokens" | "cacheReadTokens" | "cacheCreationTokens" | "totalCostUsd" | "statusCode" > & Partial>; export function hasUsageTokens(log: UsageCostLog): boolean { return ( log.inputTokens > 0 || log.outputTokens > 0 || log.cacheReadTokens > 0 || log.cacheCreationTokens > 0 ); } export function isUnpricedUsage(log: UsageCostLog): boolean { const totalCost = Number.parseFloat(log.totalCostUsd); const multiplier = log.costMultiplier == null ? undefined : Number.parseFloat(log.costMultiplier); return ( log.statusCode >= 200 && log.statusCode < 300 && hasUsageTokens(log) && Number.isFinite(totalCost) && (!Number.isFinite(multiplier) || multiplier !== 0) && totalCost === 0 ); } export interface StatsFilters { timeRange: UsageRangePreset; providerId?: string; appType?: string; }