Files
cc-switch/src/types/usage.ts
T
Arc 9171ad752c feat(usage): add live end time option for custom date range (#4438)
* 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.
2026-06-23 23:04:00 +08:00

274 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 使用统计相关类型定义
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;
}