diff --git a/open-sse/utils/progressTracker.ts b/open-sse/utils/progressTracker.ts index aaaede2223..fda582d12d 100644 --- a/open-sse/utils/progressTracker.ts +++ b/open-sse/utils/progressTracker.ts @@ -90,6 +90,10 @@ export function createProgressTransform({ } } }, + + cancel() { + clearInterval(intervalId); + }, }, { highWaterMark: 16384 }, { highWaterMark: 16384 } diff --git a/open-sse/utils/requestLogger.ts b/open-sse/utils/requestLogger.ts index bdb78be2fb..560c65a1a6 100644 --- a/open-sse/utils/requestLogger.ts +++ b/open-sse/utils/requestLogger.ts @@ -270,7 +270,8 @@ function makeStreamChunkMethods(options: RequestLoggerOptions, captureChunks: bo const append = (arr: string[], bytes: { value: number; truncated: boolean }, chunk: string) => { if (!captureChunks) return; push(); - appendBoundedChunk(arr, bytes, chunk, maxBytes, maxItems); + const ts = new Date().toISOString().slice(11, 23); + appendBoundedChunk(arr, bytes, `[${ts}] ${chunk}`, maxBytes, maxItems); }; return { diff --git a/open-sse/utils/sseHeartbeat.ts b/open-sse/utils/sseHeartbeat.ts index f0ef75a26f..9a12214145 100644 --- a/open-sse/utils/sseHeartbeat.ts +++ b/open-sse/utils/sseHeartbeat.ts @@ -111,5 +111,9 @@ export function createSseHeartbeatTransform({ flush() { stop(); }, + + cancel() { + stop(); + }, }); } diff --git a/open-sse/utils/streamPayloadCollector.ts b/open-sse/utils/streamPayloadCollector.ts index 26246601b1..26e9f4418e 100644 --- a/open-sse/utils/streamPayloadCollector.ts +++ b/open-sse/utils/streamPayloadCollector.ts @@ -3,6 +3,7 @@ import { FORMATS } from "../translator/formats.ts"; type StructuredSSEEvent = { index: number; + timestamp?: string; event?: string; data: unknown; }; @@ -660,6 +661,7 @@ export function createStructuredSSECollector(options: CollectorOptions = {}) { const event: StructuredSSEEvent = { index: events.length + droppedEvents, + timestamp: new Date().toISOString(), data: cloneLogPayload(payload), }; diff --git a/src/app/api/usage/call-logs/route.ts b/src/app/api/usage/call-logs/route.ts index 5a469cb59e..e59ab37e0b 100644 --- a/src/app/api/usage/call-logs/route.ts +++ b/src/app/api/usage/call-logs/route.ts @@ -63,6 +63,7 @@ export function buildCallLogListRows({ apiKeyName: null, comboName: null, error: null, + correlationId: detail.correlationId || null, active: true, }); } @@ -96,6 +97,7 @@ export function buildCallLogListRows({ apiKeyName: null, comboName: null, error: detail.error || null, + correlationId: detail.correlationId || null, active: false, completed: true, completedAt: completedAt ? new Date(completedAt).toISOString() : null, @@ -104,9 +106,12 @@ export function buildCallLogListRows({ } return [...activeEntries, ...completedEntries, ...logs].sort((a, b) => { - const timestampDelta = rowTimestampMs(b) - rowTimestampMs(a); - if (timestampDelta !== 0) return timestampDelta; - return rowPriority(a) - rowPriority(b); + // Active requests always on top + const pa = rowPriority(a); + const pb = rowPriority(b); + if (pa !== pb) return pa - pb; + // Within same priority, newest first + return rowTimestampMs(b) - rowTimestampMs(a); }); } @@ -125,19 +130,28 @@ export async function GET(request: Request) { if (searchParams.get("apiKey")) filter.apiKey = searchParams.get("apiKey"); if (searchParams.get("combo")) filter.combo = searchParams.get("combo"); if (searchParams.get("search")) filter.search = searchParams.get("search"); + if (searchParams.get("correlationId")) filter.correlationId = searchParams.get("correlationId"); if (searchParams.get("limit")) filter.limit = parseInt(searchParams.get("limit")); if (searchParams.get("offset")) filter.offset = parseInt(searchParams.get("offset")); const [logs, connections] = await Promise.all([getCallLogs(filter), getProviderConnections()]); - return NextResponse.json( - buildCallLogListRows({ - logs, - connections, - pendingDetails: getPendingById().values(), - completedDetails: getCompletedDetails().values(), - }) - ); + const rows = buildCallLogListRows({ + logs, + connections, + pendingDetails: getPendingById().values(), + completedDetails: getCompletedDetails().values(), + }); + + // When correlationId filter is set, also filter in-memory entries + // (active + completed) that don't match — getCallLogs already filters + // the DB rows but activeEntries/completedEntries bypass it. + if (filter.correlationId) { + const cid = filter.correlationId; + return NextResponse.json(rows.filter((r: any) => r.correlationId === cid)); + } + + return NextResponse.json(rows); } catch (error) { console.error("[API ERROR] /api/usage/call-logs failed:", error); return NextResponse.json({ error: "Failed to fetch call logs" }, { status: 500 }); diff --git a/src/lib/tokenHealthCheck.ts b/src/lib/tokenHealthCheck.ts index 5760e41a75..63ff0c5e05 100644 --- a/src/lib/tokenHealthCheck.ts +++ b/src/lib/tokenHealthCheck.ts @@ -353,7 +353,8 @@ export async function checkConnection(conn) { // - transient cooldown state (unavailable) owned by the request path const refreshCapableNeedsReauth = supportsTokenRefresh(conn.provider) && - (!conn.testStatus || conn.testStatus === "active"); + (!conn.testStatus || conn.testStatus === "active") && + !(conn.apiKey && conn.apiKey.length > 0); // API-key-only connections don't need refresh tokens if (refreshCapableNeedsReauth) { const now = new Date().toISOString(); await updateProviderConnection(conn.id, { diff --git a/src/shared/components/RequestLoggerDetail.tsx b/src/shared/components/RequestLoggerDetail.tsx index ecb28345a9..d4d42b0f31 100644 --- a/src/shared/components/RequestLoggerDetail.tsx +++ b/src/shared/components/RequestLoggerDetail.tsx @@ -176,6 +176,8 @@ export default function RequestLoggerDetail({ onCopy, onPrevious, onNext, + relatedLogs = [], + onSelectRelated, }) { // Close on Escape key useEffect(() => { @@ -202,11 +204,12 @@ export default function RequestLoggerDetail({ if (iso == null) return "\u2014"; try { const d = new Date(iso); + if (!Number.isFinite(d.getTime())) return "\u2014"; return ( d.toLocaleDateString("pt-BR") + ", " + d.toLocaleTimeString("en-US", { hour12: false }) ); } catch { - return iso; + return "\u2014"; } }; @@ -239,32 +242,17 @@ export default function RequestLoggerDetail({ : []; const requestJson = detail?.requestBody ? toPrettyJson(detail.requestBody) : null; const responseJson = detail?.responseBody ? toPrettyJson(detail.responseBody) : null; - const streamChunksText = (() => { + const streamChunks = (() => { if (!debugEnabled || !detail?.pipelinePayloads?.streamChunks) return null; let chunks: StreamChunks = detail.pipelinePayloads.streamChunks; - if (typeof chunks === "string") { try { - const parsed = JSON.parse(chunks); - chunks = parsed; + chunks = JSON.parse(chunks); } catch { - return chunks; + return null; } } - - if (chunks && typeof chunks === "object") { - try { - return Object.entries(chunks) - .map(([stage, arr]) => { - const joined = Array.isArray(arr) ? arr.join("") : String(arr); - return `--- ${stage} ---\n${joined}`; - }) - .join("\n\n"); - } catch { - return toPrettyJson(chunks); - } - } - + if (chunks && typeof chunks === "object") return chunks; return null; })(); const detailIssue = @@ -346,6 +334,14 @@ export default function RequestLoggerDetail({ {log.id} )} + {log.correlationId && ( + + cid: {log.correlationId} + + )}
)} + {/* Related Requests (same correlation ID) */} + {relatedLogs.length > 1 && ( +
+
+ Related Requests ({relatedLogs.length}) +
+
+ {[...relatedLogs] + .sort((a, b) => { + const aStart = new Date(a.timestamp).getTime() - (a.duration || 0); + const bStart = new Date(b.timestamp).getTime() - (b.duration || 0); + return aStart - bStart; + }) + .map((r) => { + const rStatusStyle = r.active ? null : getStatusStyle(r.status); + const isCurrent = r.id === log.id; + const startTime = new Date(new Date(r.timestamp).getTime() - (r.duration || 0)); + return ( + + ); + })} +
+
+ )} + {detailIssue && (
@@ -612,14 +680,63 @@ export default function RequestLoggerDetail({
) : ( <> - {streamChunksText && ( + {streamChunks && streamChunks.provider && ( onCopy(streamChunksText)} + title="Provider Event Stream" + json={ + Array.isArray(streamChunks.provider) + ? streamChunks.provider.join("") + : String(streamChunks.provider) + } + onCopy={() => + onCopy( + Array.isArray(streamChunks.provider) + ? streamChunks.provider.join("") + : String(streamChunks.provider) + ) + } /> )} + {streamChunks && streamChunks.client && ( + + onCopy( + Array.isArray(streamChunks.client) + ? streamChunks.client.join("") + : String(streamChunks.client) + ) + } + /> + )} + + {streamChunks && + streamChunks.openai && + !streamChunks.provider && + !streamChunks.client && ( + + onCopy( + Array.isArray(streamChunks.openai) + ? streamChunks.openai.join("") + : String(streamChunks.openai) + ) + } + /> + )} + {payloadSections.length > 0 && payloadSections.map((section) => ( { + const mapping = columnSortMap[column as keyof typeof columnSortMap]; + if (!mapping) return; + setSortBy((prev) => { + if (prev === mapping.desc) return mapping.asc; + return mapping.desc; + }); + }, []); + const getSortIndicator = useCallback( + (column: string) => { + const mapping = columnSortMap[column as keyof typeof columnSortMap]; + if (!mapping) return ""; + if (sortBy === mapping.desc) return " ↓"; + if (sortBy === mapping.asc) return " ↑"; + return ""; + }, + [sortBy] + ); const [detailData, setDetailData] = useState(null); const [detailLoggingEnabled, setDetailLoggingEnabled] = useState(false); const [detailLoggingLoading, setDetailLoggingLoading] = useState(false); @@ -222,6 +251,7 @@ const RequestLoggerV2 = forwardRef { + const cidGroups = new Map(); + for (const log of sortedLogs) { + const cid = log.correlationId; + if (cid) { + if (!cidGroups.has(cid)) cidGroups.set(cid, []); + cidGroups.get(cid).push(log); + } + } + return sortedLogs.map((log) => { + const cid = log.correlationId; + if (!cid) return { ...log, isRetry: false, groupSize: 1, groupStatus: null }; + const group = cidGroups.get(cid); + if (!group || group.length <= 1) + return { ...log, isRetry: false, groupSize: 1, groupStatus: null }; + const isFirst = group[0].id === log.id; + const hasFailure = group.some((g) => g.status >= 400 || g.active); + const hasSuccess = group.some((g) => g.status >= 200 && g.status < 300); + const groupStatus = hasFailure && hasSuccess ? "healed" : hasFailure ? "failed" : null; + return { ...log, isRetry: !isFirst, groupSize: group.length, groupStatus }; + }); + }, [sortedLogs]); + // Fetch log detail from the persisted call-log endpoint. If a deep-linked // request is still being finalized, keep the modal open and poll this same // endpoint until the row appears. @@ -598,6 +657,50 @@ const RequestLoggerV2 = forwardRef { + const cid = selectedLog?.correlationId; + if (!selectedLog?.id || !cid) return; + let cancelled = false; + const interval = setInterval(async () => { + if (document.visibilityState !== "visible") return; + try { + const res = await fetch(`/api/usage/call-logs?correlationId=${encodeURIComponent(cid)}`, { + cache: "no-store", + }); + if (cancelled || !res.ok) return; + const cidLogs = await res.json(); + if (!Array.isArray(cidLogs) || cidLogs.length === 0) return; + setLogs((prev) => { + const ids = new Set(cidLogs.map((l: any) => l.id)); + let changed = false; + const merged = prev.map((l: any) => { + const updated = cidLogs.find((c: any) => c.id === l.id); + if (updated) { + changed = true; + return { ...l, ...updated }; + } + return l; + }); + for (const cl of cidLogs) { + if (!merged.some((m: any) => m.id === cl.id)) { + merged.push(cl); + changed = true; + } + } + return changed ? merged : prev; + }); + } catch { + /* poll failed — non-critical */ + } + }, 3000); + return () => { + cancelled = true; + clearInterval(interval); + }; + }, [selectedLog?.id, selectedLog?.correlationId]); + const currentLogIndex = useMemo(() => { if (!selectedLog) return -1; return sortedLogsForNav.findIndex((l) => l.id === selectedLog.id); @@ -685,13 +788,14 @@ const RequestLoggerV2 = forwardRef ({ totalCount: filteredLogs.length, okCount: filteredLogs.filter((l) => l.status >= 200 && l.status < 300).length, errorCount: filteredLogs.filter((l) => l.status >= 400).length, comboCount: logs.filter((l) => l.comboName).length, apiKeyCount: uniqueApiKeys.length, + runningCount: filteredLogs.filter((l) => l.active === true).length, }), [filteredLogs, logs, uniqueApiKeys] ); @@ -749,6 +853,20 @@ const RequestLoggerV2 = forwardRef
+ {/* Correlation ID Filter */} +
+ + tag + + setCorrelationIdFilter(e.target.value)} + className="w-full pl-9 pr-3 py-2 rounded-lg bg-bg-subtle border border-border text-sm text-text-primary font-mono placeholder:text-text-muted focus:outline-none focus:border-primary" + /> +
+ {/* Provider Dropdown */}