mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
402570ce31
* model pricing routing: extend prefix-match families (gpt-/o1-o5/
gemini-/deepseek-/qwen-/glm-/kimi-/minimax-) with per-family dash
thresholds so short base IDs like gpt-5 no longer mis-match
gpt-5-mini; strip ISO and 8-digit date suffixes via UTF-8-safe
byte matching so claude-haiku-4-5-20251001 falls back to
claude-haiku-4-5 pricing
* SSE collector: SseUsageFinishGuard (RAII) guarantees finish() on
early return or panic; AtomicBool fast path lets push() skip the
Mutex once first-event time is recorded
* validation: shared validate_cost_multiplier / validate_pricing_source
helpers across DAO and service layers; PRICING_SOURCE_RESPONSE /
PRICING_SOURCE_REQUEST constants replace string literals; price
fields in update_model_pricing now reject empty / non-decimal /
negative input before INSERT
* backfill: add backfill_missing_usage_costs_for_model so a single
price edit only scans matching rows instead of the full log table;
startup backfill remains full-scan
* session_usage{,_codex,_gemini}: share find_model_pricing helper from
usage_stats; metadata_modified_nanos centralizes mtime precision
* frontend: NON_NEGATIVE_DECIMAL_REGEX + isNonNegativeDecimalString
replace three copies of the same multiplier regex; isUnpricedUsage
surfaces zero-cost rows that have usage tokens (cached per row to
avoid double evaluation); invalidate usageKeys.all on pricing mutate
so backfilled rows refresh
247 lines
6.0 KiB
TypeScript
247 lines
6.0 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;
|
||
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<AppType> = [
|
||
"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<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;
|
||
}
|