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:
Jason
2026-05-14 11:53:51 +08:00
parent 206125b4e3
commit 402570ce31
19 changed files with 590 additions and 375 deletions
+10 -3
View File
@@ -6,7 +6,7 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { useRequestDetail } from "@/lib/query/usage";
import { getFreshInputTokens } from "@/types/usage";
import { getFreshInputTokens, isUnpricedUsage } from "@/types/usage";
interface RequestDetailPanelProps {
requestId: string;
@@ -53,6 +53,7 @@ export function RequestDetailPanel({
const freshInput = getFreshInputTokens(request);
const isCacheInclusive = request.inputTokens !== freshInput;
const unpriced = isUnpricedUsage(request);
return (
<Dialog open onOpenChange={onClose}>
@@ -255,8 +256,14 @@ export function RequestDetailPanel({
</span>
)}
</dt>
<dd className="text-lg font-semibold text-primary">
${parseFloat(request.totalCostUsd).toFixed(6)}
<dd
className={`text-lg font-semibold ${
unpriced ? "text-muted-foreground" : "text-primary"
}`}
>
{unpriced
? t("usage.unpriced", "未定价")
: `$${parseFloat(request.totalCostUsd).toFixed(6)}`}
</dd>
</div>
</dl>