mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-01 12:22:09 +08:00
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
This commit is contained in:
@@ -21,6 +21,7 @@ import { useRequestLogs } from "@/lib/query/usage";
|
||||
import {
|
||||
KNOWN_APP_TYPES,
|
||||
getFreshInputTokens,
|
||||
isUnpricedUsage,
|
||||
type LogFilters,
|
||||
type UsageRangeSelection,
|
||||
} from "@/types/usage";
|
||||
@@ -293,115 +294,127 @@ export function RequestLogTable({
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
logs.map((log) => (
|
||||
<TableRow key={log.requestId}>
|
||||
<TableCell className="text-center whitespace-nowrap text-xs px-1.5">
|
||||
{new Date(log.createdAt * 1000).toLocaleString(locale, {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{log.providerName || t("usage.unknownProvider")}
|
||||
</TableCell>
|
||||
<TableCell className="text-center font-mono text-xs max-w-[200px]">
|
||||
<div
|
||||
className="truncate"
|
||||
title={
|
||||
log.requestModel && log.requestModel !== log.model
|
||||
? `${log.requestModel} → ${log.model}`
|
||||
: log.model
|
||||
}
|
||||
>
|
||||
{log.requestModel &&
|
||||
log.requestModel !== log.model ? (
|
||||
<span>
|
||||
{log.requestModel}
|
||||
<span className="text-muted-foreground">
|
||||
{" → "}
|
||||
{log.model}
|
||||
logs.map((log) => {
|
||||
const unpriced = isUnpricedUsage(log);
|
||||
return (
|
||||
<TableRow key={log.requestId}>
|
||||
<TableCell className="text-center whitespace-nowrap text-xs px-1.5">
|
||||
{new Date(log.createdAt * 1000).toLocaleString(
|
||||
locale,
|
||||
{
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
},
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{log.providerName || t("usage.unknownProvider")}
|
||||
</TableCell>
|
||||
<TableCell className="text-center font-mono text-xs max-w-[200px]">
|
||||
<div
|
||||
className="truncate"
|
||||
title={
|
||||
log.requestModel && log.requestModel !== log.model
|
||||
? `${log.requestModel} → ${log.model}`
|
||||
: log.model
|
||||
}
|
||||
>
|
||||
{log.requestModel &&
|
||||
log.requestModel !== log.model ? (
|
||||
<span>
|
||||
{log.requestModel}
|
||||
<span className="text-muted-foreground">
|
||||
{" → "}
|
||||
{log.model}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
log.model
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-center px-1.5">
|
||||
{(() => {
|
||||
const freshInput = getFreshInputTokens(log);
|
||||
const isCacheInclusive =
|
||||
log.inputTokens !== freshInput;
|
||||
return (
|
||||
<div
|
||||
className="tabular-nums"
|
||||
title={
|
||||
isCacheInclusive
|
||||
? `Raw: ${log.inputTokens.toLocaleString()}`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{fmtInt(freshInput, locale)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{(log.cacheReadTokens > 0 ||
|
||||
log.cacheCreationTokens > 0) && (
|
||||
<div className="text-[10px] text-muted-foreground whitespace-nowrap">
|
||||
{[
|
||||
log.cacheReadTokens > 0 &&
|
||||
`R${fmtInt(log.cacheReadTokens, locale)}`,
|
||||
log.cacheCreationTokens > 0 &&
|
||||
`W${fmtInt(log.cacheCreationTokens, locale)}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("·")}
|
||||
) : (
|
||||
log.model
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{fmtInt(log.outputTokens, locale)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center px-1.5">
|
||||
<div className="font-medium tabular-nums">
|
||||
{fmtUsd(log.totalCostUsd, 4)}
|
||||
</div>
|
||||
{parseFiniteNumber(log.costMultiplier) != null &&
|
||||
parseFiniteNumber(log.costMultiplier) !== 1 && (
|
||||
<div className="text-[11px] text-muted-foreground">
|
||||
×
|
||||
{parseFiniteNumber(log.costMultiplier)?.toFixed(
|
||||
2,
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center px-1.5">
|
||||
{(() => {
|
||||
const freshInput = getFreshInputTokens(log);
|
||||
const isCacheInclusive =
|
||||
log.inputTokens !== freshInput;
|
||||
return (
|
||||
<div
|
||||
className="tabular-nums"
|
||||
title={
|
||||
isCacheInclusive
|
||||
? `Raw: ${log.inputTokens.toLocaleString()}`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{fmtInt(freshInput, locale)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{(log.cacheReadTokens > 0 ||
|
||||
log.cacheCreationTokens > 0) && (
|
||||
<div className="text-[10px] text-muted-foreground whitespace-nowrap">
|
||||
{[
|
||||
log.cacheReadTokens > 0 &&
|
||||
`R${fmtInt(log.cacheReadTokens, locale)}`,
|
||||
log.cacheCreationTokens > 0 &&
|
||||
`W${fmtInt(log.cacheCreationTokens, locale)}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("·")}
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center whitespace-nowrap text-xs tabular-nums">
|
||||
{(log.latencyMs / 1000).toFixed(1)}s
|
||||
{log.firstTokenMs != null && (
|
||||
<span className="text-muted-foreground">
|
||||
/{(log.firstTokenMs / 1000).toFixed(1)}s
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{fmtInt(log.outputTokens, locale)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center px-1.5">
|
||||
<div
|
||||
className={`font-medium tabular-nums ${
|
||||
unpriced ? "text-muted-foreground" : ""
|
||||
}`}
|
||||
>
|
||||
{unpriced
|
||||
? t("usage.unpriced", "未定价")
|
||||
: fmtUsd(log.totalCostUsd, 4)}
|
||||
</div>
|
||||
{parseFiniteNumber(log.costMultiplier) != null &&
|
||||
parseFiniteNumber(log.costMultiplier) !== 1 && (
|
||||
<div className="text-[11px] text-muted-foreground">
|
||||
×
|
||||
{parseFiniteNumber(log.costMultiplier)?.toFixed(
|
||||
2,
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center whitespace-nowrap text-xs tabular-nums">
|
||||
{(log.latencyMs / 1000).toFixed(1)}s
|
||||
{log.firstTokenMs != null && (
|
||||
<span className="text-muted-foreground">
|
||||
/{(log.firstTokenMs / 1000).toFixed(1)}s
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<span
|
||||
className={
|
||||
log.statusCode >= 200 && log.statusCode < 300
|
||||
? "text-green-600"
|
||||
: "text-red-600"
|
||||
}
|
||||
>
|
||||
{log.statusCode}
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<span
|
||||
className={
|
||||
log.statusCode >= 200 && log.statusCode < 300
|
||||
? "text-green-600"
|
||||
: "text-red-600"
|
||||
}
|
||||
>
|
||||
{log.statusCode}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-xs text-muted-foreground">
|
||||
{log.dataSource || "proxy"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-xs text-muted-foreground">
|
||||
{log.dataSource || "proxy"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
Reference in New Issue
Block a user