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
+44
View File
@@ -195,6 +195,50 @@ export function getFreshInputTokens(log: CacheNormalizableLog): number {
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;