From bdda9be18702d25fbe6488ec4258e970ee7c85de Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Mon, 23 Feb 2026 21:10:18 +0800 Subject: [PATCH] refactor(logs): replace score-based usage trace matching with path-and-model filtering --- src/i18n/locales/en.json | 1 + src/i18n/locales/ru.json | 1 + src/i18n/locales/zh-CN.json | 1 + src/pages/LogsPage.module.scss | 20 +---- src/pages/LogsPage.tsx | 17 ++-- src/pages/hooks/useTraceResolver.ts | 126 +++++++++++----------------- 6 files changed, 57 insertions(+), 109 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index a73b389..03d96da 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1023,6 +1023,7 @@ "trace_confidence_low": "Low", "trace_score": "Score {{score}}", "trace_delta_seconds": "Δt {{seconds}}s", + "trace_model_matched": "Model Matched", "trace_request_id": "Request ID", "trace_method": "Method", "trace_path": "Path", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 6831100..1bebdb0 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -1026,6 +1026,7 @@ "trace_confidence_low": "Низкая", "trace_score": "Оценка {{score}}", "trace_delta_seconds": "Δt {{seconds}}с", + "trace_model_matched": "Модель совпала", "trace_request_id": "Request ID", "trace_method": "Метод", "trace_path": "Путь", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 6a572ca..ea2ad97 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -1023,6 +1023,7 @@ "trace_confidence_low": "低", "trace_score": "分数 {{score}}", "trace_delta_seconds": "时间差 {{seconds}} 秒", + "trace_model_matched": "模型匹配", "trace_request_id": "请求 ID", "trace_method": "请求方法", "trace_path": "路径", diff --git a/src/pages/LogsPage.module.scss b/src/pages/LogsPage.module.scss index a88bccd..6985135 100644 --- a/src/pages/LogsPage.module.scss +++ b/src/pages/LogsPage.module.scss @@ -693,34 +693,18 @@ flex-wrap: wrap; } -.traceConfidenceBadge { +.traceModelBadge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: $radius-full; - border: 1px solid var(--border-color); + border: 1px solid var(--success-badge-border, #6ee7b7); font-size: 11px; font-weight: 700; -} - -.traceConfidenceHigh { color: var(--success-badge-text, #065f46); background: var(--success-badge-bg, #d1fae5); - border-color: var(--success-badge-border, #6ee7b7); } -.traceConfidenceMedium { - color: var(--warning-text); - background: var(--warning-bg); - border-color: var(--warning-border); -} - -.traceConfidenceLow { - color: var(--text-secondary); - background: var(--bg-primary); -} - -.traceScore, .traceDelta { font-size: 11px; color: var(--text-secondary); diff --git a/src/pages/LogsPage.tsx b/src/pages/LogsPage.tsx index 86dfa12..b38cc20 100644 --- a/src/pages/LogsPage.tsx +++ b/src/pages/LogsPage.tsx @@ -970,12 +970,6 @@ export function LogsPage() { ) : (
{trace.traceCandidates.map((candidate) => { - const confidenceClass = - candidate.confidence === 'high' - ? styles.traceConfidenceHigh - : candidate.confidence === 'medium' - ? styles.traceConfidenceMedium - : styles.traceConfidenceLow; const sourceInfo = trace.resolveTraceSourceInfo( String(candidate.detail.source ?? ''), candidate.detail.auth_index @@ -986,12 +980,11 @@ export function LogsPage() { className={styles.traceCandidate} >
- - {t(`logs.trace_confidence_${candidate.confidence}`)} - - - {t('logs.trace_score', { score: candidate.score })} - + {candidate.modelMatched && ( + + {t('logs.trace_model_matched')} + + )} {candidate.timeDeltaMs !== null && ( {t('logs.trace_delta_seconds', { diff --git a/src/pages/hooks/useTraceResolver.ts b/src/pages/hooks/useTraceResolver.ts index 6b2f90b..daab3f9 100644 --- a/src/pages/hooks/useTraceResolver.ts +++ b/src/pages/hooks/useTraceResolver.ts @@ -12,19 +12,14 @@ import { } from '@/utils/usage'; import type { ParsedLogLine } from './logTypes'; -type TraceConfidence = 'high' | 'medium' | 'low'; - export type TraceCandidate = { detail: UsageDetailWithEndpoint; - score: number; - confidence: TraceConfidence; + modelMatched: boolean; timeDeltaMs: number | null; }; const TRACE_AUTH_CACHE_MS = 60 * 1000; -const TRACE_MATCH_STRONG_WINDOW_MS = 3 * 1000; -const TRACE_MATCH_WINDOW_MS = 10 * 1000; -const TRACE_MATCH_MAX_WINDOW_MS = 30 * 1000; +const TRACE_MAX_CANDIDATES = 5; const TRACEABLE_EXACT_PATHS = new Set(['/v1/chat/completions', '/v1/messages', '/v1/responses']); const TRACEABLE_PREFIX_PATHS = ['/v1beta/models']; @@ -48,70 +43,17 @@ export const isTraceableRequestPath = (value?: string): boolean => { return TRACEABLE_PREFIX_PATHS.some((prefix) => normalizedPath.startsWith(prefix)); }; -const scoreTraceCandidate = ( - line: ParsedLogLine, - detail: UsageDetailWithEndpoint -): TraceCandidate | null => { - let score = 0; - let timeDeltaMs: number | null = null; +const MODEL_EXTRACT_REGEX = /\bmodel[=:]\s*"?([a-zA-Z0-9._\-/]+)"?/i; - const logTimestampMs = line.timestamp ? Date.parse(line.timestamp) : Number.NaN; - const detailTimestampMs = detail.__timestampMs; - if (!Number.isNaN(logTimestampMs) && detailTimestampMs > 0) { - timeDeltaMs = Math.abs(logTimestampMs - detailTimestampMs); - if (timeDeltaMs <= TRACE_MATCH_STRONG_WINDOW_MS) { - score += 42; - } else if (timeDeltaMs <= TRACE_MATCH_WINDOW_MS) { - score += 30; - } else if (timeDeltaMs <= TRACE_MATCH_MAX_WINDOW_MS) { - score += 12; - } else { - score -= 12; - } - } +const extractModelFromMessage = (message?: string): string | undefined => { + if (!message) return undefined; + const match = message.match(MODEL_EXTRACT_REGEX); + return match?.[1] || undefined; +}; - let methodMatched = false; - if (line.method && detail.__endpointMethod) { - if (line.method.toUpperCase() === detail.__endpointMethod.toUpperCase()) { - score += 18; - methodMatched = true; - } else { - score -= 8; - } - } - - const logPath = normalizeTracePath(line.path); - const detailPath = normalizeTracePath(detail.__endpointPath); - let pathMatched = false; - if (logPath && detailPath) { - if (logPath === detailPath) { - score += 24; - pathMatched = true; - } else if (logPath.startsWith(detailPath) || detailPath.startsWith(logPath)) { - score += 12; - pathMatched = true; - } else { - score -= 8; - } - } - - if (typeof line.statusCode === 'number') { - const logFailed = line.statusCode >= 400; - score += logFailed === detail.failed ? 10 : -6; - } - - if ( - timeDeltaMs !== null && - timeDeltaMs > TRACE_MATCH_MAX_WINDOW_MS && - !methodMatched && - !pathMatched - ) { - return null; - } - - if (score <= 0) return null; - const confidence: TraceConfidence = score >= 70 ? 'high' : score >= 45 ? 'medium' : 'low'; - return { detail, score, confidence, timeDeltaMs }; +const isPathMatch = (logPath: string, detailPath: string): boolean => { + if (!logPath || !detailPath) return false; + return logPath === detailPath || logPath.startsWith(detailPath) || detailPath.startsWith(logPath); }; const getErrorMessage = (err: unknown): string => { @@ -236,16 +178,42 @@ export function useTraceResolver(options: UseTraceResolverOptions): UseTraceReso const traceCandidates = useMemo(() => { if (!traceLogLine) return []; - const scored = traceUsageDetails - .map((detail) => scoreTraceCandidate(traceLogLine, detail)) - .filter((item): item is TraceCandidate => item !== null) - .sort((a, b) => { - if (b.score !== a.score) return b.score - a.score; - const aDelta = a.timeDeltaMs ?? Number.MAX_SAFE_INTEGER; - const bDelta = b.timeDeltaMs ?? Number.MAX_SAFE_INTEGER; - return aDelta - bDelta; - }); - return scored.slice(0, 8); + + const logPath = normalizeTracePath(traceLogLine.path); + if (!logPath) return []; + + const logTimestampMs = traceLogLine.timestamp + ? Date.parse(traceLogLine.timestamp) + : Number.NaN; + + // Step 1: filter by path match + const pathMatched = traceUsageDetails.filter((detail) => + isPathMatch(logPath, normalizeTracePath(detail.__endpointPath)) + ); + if (pathMatched.length === 0) return []; + + // Step 2: try to extract model from log message, then filter by model + const logModel = extractModelFromMessage(traceLogLine.message); + const modelMatched = logModel + ? pathMatched.filter( + (d) => d.__modelName?.toLowerCase() === logModel.toLowerCase() + ) + : []; + + // Step 3: prefer model-matched set; fall back to path-matched + const useModelSet = modelMatched.length > 0; + const source = useModelSet ? modelMatched : pathMatched; + + return source + .map((detail) => { + const timeDeltaMs = + !Number.isNaN(logTimestampMs) && detail.__timestampMs > 0 + ? Math.abs(logTimestampMs - detail.__timestampMs) + : null; + return { detail, modelMatched: useModelSet, timeDeltaMs } satisfies TraceCandidate; + }) + .sort((a, b) => (b.detail.__timestampMs || 0) - (a.detail.__timestampMs || 0)) + .slice(0, TRACE_MAX_CANDIDATES); }, [traceLogLine, traceUsageDetails]); const resolveTraceSourceInfo = useCallback(