mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-31 03:14:27 +08:00
* add glm-5.2 pricing * feat(usage): add live end time option for custom date range Add a "End time follows current time" checkbox in the custom date range picker. When enabled, the end time becomes read-only and automatically tracks the current moment, so usage data always reflects up-to-the-second consumption from the chosen start time. This is especially useful under the Coding Plan 5-hour quota window — users can set the start time to when their 5h window began and keep the end time live to monitor real-time token consumption. * style(usage): fix prettier formatting for UsageDashboard * fix(usage): include liveEndTime in React Query cache keys Without liveEndTime in the query key, a live custom range and a fixed custom range with the same stored endpoints share the same cache entry. After the live range refreshes (endDate = now), switching to the fixed range shows stale data fetched through "now" instead of the original end time.
274 lines
7.2 KiB
TypeScript
274 lines
7.2 KiB
TypeScript
// 使用统计相关类型定义
|
||
|
||
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;
|
||
/** 写入时实际用于计价的模型名;路由接管 + request 计价模式下可能与 model 不同 */
|
||
pricingModel?: 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;
|
||
}
|
||
|
||
/**
|
||
* Dashboard 顶栏的全局筛选维度,作用于 Hero / 趋势图 / 三个统计 Tab。
|
||
*
|
||
* - `providerName` 按展示名精确匹配(与 Provider 统计列表同口径,含
|
||
* "Claude (Session)" 等会话占位名);
|
||
* - `model` 按「有效计价模型」匹配(pricing_model 优先、回落 model,
|
||
* 与模型统计的分组口径一致)。
|
||
*/
|
||
export interface UsageScopeFilters {
|
||
appType?: string;
|
||
providerName?: string;
|
||
model?: string;
|
||
}
|
||
|
||
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;
|
||
/** When true (custom mode only), endDate resolves to "now" instead of the
|
||
* fixed customEndDate snapshot, and the end-time field becomes read-only. */
|
||
liveEndTime?: boolean;
|
||
}
|
||
|
||
/**
|
||
* App types surfaced as dashboard filter buttons.
|
||
*
|
||
* `claude-desktop` is intentionally NOT listed: the Desktop gateway's proxy
|
||
* traffic is still recorded under its own `app_type` (preserving route-takeover
|
||
* billing audit — the request detail panel shows the real value), but the
|
||
* dashboard folds it into `claude` for display. It is the embedded Claude Code
|
||
* runtime running inside the Desktop shell, and Desktop *chat* usage never
|
||
* passes through this app at all, so a separate "Claude Desktop" bucket would
|
||
* only ever show a partial number and mislead users into reading it as the
|
||
* Desktop's full usage. The backend collapses `claude-desktop → claude` in
|
||
* every dashboard query (see `folded_app_type_sql`).
|
||
* `opencode` / `openclaw` / `hermes` have no proxy handler at all — they
|
||
* appear only as managed apps elsewhere.
|
||
*/
|
||
export type AppType = "claude" | "codex" | "gemini" | "opencode";
|
||
|
||
export type AppTypeFilter = "all" | AppType;
|
||
|
||
export const KNOWN_APP_TYPES: ReadonlyArray<AppType> = [
|
||
"claude",
|
||
"codex",
|
||
"gemini",
|
||
"opencode",
|
||
];
|
||
|
||
/**
|
||
* 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<string> = 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<Pick<RequestLog, "costMultiplier">>;
|
||
|
||
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;
|
||
}
|