Files
CC-Switch/src/lib/query/usage.ts
T
Jason 402570ce31 fix(usage): pricing routing, SSE lifecycle, and validation hardening
* 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
2026-05-14 11:53:51 +08:00

320 lines
8.5 KiB
TypeScript

import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { usageApi } from "@/lib/api/usage";
import { resolveUsageRange } from "@/lib/usageRange";
import type { LogFilters, UsageRangeSelection } from "@/types/usage";
const DEFAULT_REFETCH_INTERVAL_MS = 30000;
type UsageQueryOptions = {
refetchInterval?: number | false;
refetchIntervalInBackground?: boolean;
};
type RequestLogsQueryArgs = {
filters: LogFilters;
range: UsageRangeSelection;
page?: number;
pageSize?: number;
options?: UsageQueryOptions;
};
type RequestLogsKey = {
preset: UsageRangeSelection["preset"];
customStartDate?: number;
customEndDate?: number;
appType?: string;
providerName?: string;
model?: string;
statusCode?: number;
};
// Query keys
export const usageKeys = {
all: ["usage"] as const,
summary: (
preset: UsageRangeSelection["preset"],
customStartDate: number | undefined,
customEndDate: number | undefined,
appType?: string,
) =>
[
...usageKeys.all,
"summary",
preset,
customStartDate ?? 0,
customEndDate ?? 0,
appType ?? "all",
] as const,
summaryByApp: (
preset: UsageRangeSelection["preset"],
customStartDate: number | undefined,
customEndDate: number | undefined,
) =>
[
...usageKeys.all,
"summary-by-app",
preset,
customStartDate ?? 0,
customEndDate ?? 0,
] as const,
trends: (
preset: UsageRangeSelection["preset"],
customStartDate: number | undefined,
customEndDate: number | undefined,
appType?: string,
) =>
[
...usageKeys.all,
"trends",
preset,
customStartDate ?? 0,
customEndDate ?? 0,
appType ?? "all",
] as const,
providerStats: (
preset: UsageRangeSelection["preset"],
customStartDate: number | undefined,
customEndDate: number | undefined,
appType?: string,
) =>
[
...usageKeys.all,
"provider-stats",
preset,
customStartDate ?? 0,
customEndDate ?? 0,
appType ?? "all",
] as const,
modelStats: (
preset: UsageRangeSelection["preset"],
customStartDate: number | undefined,
customEndDate: number | undefined,
appType?: string,
) =>
[
...usageKeys.all,
"model-stats",
preset,
customStartDate ?? 0,
customEndDate ?? 0,
appType ?? "all",
] as const,
logs: (key: RequestLogsKey, page: number, pageSize: number) =>
[
...usageKeys.all,
"logs",
key.preset,
key.customStartDate ?? 0,
key.customEndDate ?? 0,
key.appType ?? "",
key.providerName ?? "",
key.model ?? "",
key.statusCode ?? -1,
page,
pageSize,
] as const,
detail: (requestId: string) =>
[...usageKeys.all, "detail", requestId] as const,
pricing: () => [...usageKeys.all, "pricing"] as const,
limits: (providerId: string, appType: string) =>
[...usageKeys.all, "limits", providerId, appType] as const,
script: (providerId: string, appType: string) =>
[...usageKeys.all, providerId, appType] as const,
};
// Hooks
export function useUsageSummary(
range: UsageRangeSelection,
appType?: string,
options?: UsageQueryOptions,
) {
const effectiveAppType = appType === "all" ? undefined : appType;
return useQuery({
queryKey: usageKeys.summary(
range.preset,
range.customStartDate,
range.customEndDate,
appType,
),
queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range);
return usageApi.getUsageSummary(startDate, endDate, effectiveAppType);
},
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS,
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
});
}
export function useUsageSummaryByApp(
range: UsageRangeSelection,
options?: UsageQueryOptions,
) {
return useQuery({
queryKey: usageKeys.summaryByApp(
range.preset,
range.customStartDate,
range.customEndDate,
),
queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range);
return usageApi.getUsageSummaryByApp(startDate, endDate);
},
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS,
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
});
}
export function useUsageTrends(
range: UsageRangeSelection,
appType?: string,
options?: UsageQueryOptions,
) {
const effectiveAppType = appType === "all" ? undefined : appType;
return useQuery({
queryKey: usageKeys.trends(
range.preset,
range.customStartDate,
range.customEndDate,
appType,
),
queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range);
return usageApi.getUsageTrends(startDate, endDate, effectiveAppType);
},
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS,
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
});
}
export function useProviderStats(
range: UsageRangeSelection,
appType?: string,
options?: UsageQueryOptions,
) {
const effectiveAppType = appType === "all" ? undefined : appType;
return useQuery({
queryKey: usageKeys.providerStats(
range.preset,
range.customStartDate,
range.customEndDate,
appType,
),
queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range);
return usageApi.getProviderStats(startDate, endDate, effectiveAppType);
},
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS,
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
});
}
export function useModelStats(
range: UsageRangeSelection,
appType?: string,
options?: UsageQueryOptions,
) {
const effectiveAppType = appType === "all" ? undefined : appType;
return useQuery({
queryKey: usageKeys.modelStats(
range.preset,
range.customStartDate,
range.customEndDate,
appType,
),
queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range);
return usageApi.getModelStats(startDate, endDate, effectiveAppType);
},
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS,
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
});
}
export function useRequestLogs({
filters,
range,
page = 0,
pageSize = 20,
options,
}: RequestLogsQueryArgs) {
const key: RequestLogsKey = {
preset: range.preset,
customStartDate: range.customStartDate,
customEndDate: range.customEndDate,
appType: filters.appType,
providerName: filters.providerName,
model: filters.model,
statusCode: filters.statusCode,
};
return useQuery({
queryKey: usageKeys.logs(key, page, pageSize),
queryFn: () => {
const effectiveFilters = { ...filters, ...resolveUsageRange(range) };
return usageApi.getRequestLogs(effectiveFilters, page, pageSize);
},
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS, // 每30秒自动刷新
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
});
}
export function useRequestDetail(requestId: string) {
return useQuery({
queryKey: usageKeys.detail(requestId),
queryFn: () => usageApi.getRequestDetail(requestId),
enabled: !!requestId,
});
}
export function useModelPricing() {
return useQuery({
queryKey: usageKeys.pricing(),
queryFn: usageApi.getModelPricing,
});
}
export function useProviderLimits(providerId: string, appType: string) {
return useQuery({
queryKey: usageKeys.limits(providerId, appType),
queryFn: () => usageApi.checkProviderLimits(providerId, appType),
enabled: !!providerId && !!appType,
});
}
export function useUpdateModelPricing() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (params: {
modelId: string;
displayName: string;
inputCost: string;
outputCost: string;
cacheReadCost: string;
cacheCreationCost: string;
}) =>
usageApi.updateModelPricing(
params.modelId,
params.displayName,
params.inputCost,
params.outputCost,
params.cacheReadCost,
params.cacheCreationCost,
),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: usageKeys.all });
},
});
}
export function useDeleteModelPricing() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (modelId: string) => usageApi.deleteModelPricing(modelId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: usageKeys.all });
},
});
}