extracted CorrelationId observability changes from #5275 (#5834)

Integrated into release/v3.8.43
This commit is contained in:
Markus Hartung
2026-07-02 03:02:08 +02:00
committed by GitHub
parent a98b29e8be
commit 9bd0211da7
16 changed files with 2513 additions and 824 deletions

View File

@@ -90,6 +90,10 @@ export function createProgressTransform({
}
}
},
cancel() {
clearInterval(intervalId);
},
},
{ highWaterMark: 16384 },
{ highWaterMark: 16384 }

View File

@@ -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 {

View File

@@ -111,5 +111,9 @@ export function createSseHeartbeatTransform({
flush() {
stop();
},
cancel() {
stop();
},
});
}

View File

@@ -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),
};

View File

@@ -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 });

View File

@@ -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, {

View File

@@ -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}
</span>
)}
{log.correlationId && (
<span
className="text-[10px] text-text-muted/50 font-mono self-center ml-2 px-1.5 py-0.5 rounded bg-bg-subtle border border-border/40 select-all"
title="Correlation ID"
>
cid: {log.correlationId}
</span>
)}
</div>
<div className="flex items-center gap-1">
<button
@@ -421,7 +417,23 @@ export default function RequestLoggerDetail({
>
<div>
<div className="text-[10px] text-text-muted uppercase tracking-wider mb-1">
Completed Time
Started At
</div>
<div className="text-sm font-medium">
{(() => {
try {
const ts = new Date(log.timestamp).getTime();
if (!Number.isFinite(ts)) return "\u2014";
return formatDate(new Date(ts - (log.duration || 0)).toISOString());
} catch {
return "\u2014";
}
})()}
</div>
</div>
<div>
<div className="text-[10px] text-text-muted uppercase tracking-wider mb-1">
Ended At
</div>
<div className="text-sm font-medium">{formatDate(log.timestamp)}</div>
</div>
@@ -597,6 +609,62 @@ export default function RequestLoggerDetail({
</div>
)}
{/* Related Requests (same correlation ID) */}
{relatedLogs.length > 1 && (
<div className="p-4 rounded-xl bg-bg-subtle border border-border">
<div className="text-[10px] text-text-muted uppercase tracking-wider mb-2 font-bold">
Related Requests ({relatedLogs.length})
</div>
<div className="flex flex-col gap-1">
{[...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 (
<button
key={r.id}
onClick={() => !isCurrent && onSelectRelated?.(r)}
disabled={isCurrent}
className={`flex items-center gap-2 px-2 py-1.5 rounded text-left text-xs transition-colors ${
isCurrent
? "bg-primary/10 border border-primary/30 cursor-default"
: "hover:bg-bg-hover cursor-pointer"
}`}
>
<span
className="inline-block px-1.5 py-0.5 rounded text-[9px] font-bold min-w-[28px] text-center"
style={
rStatusStyle
? { backgroundColor: rStatusStyle.bg, color: rStatusStyle.text }
: { backgroundColor: "#374151", color: "#fff" }
}
>
{r.status || "..."}
</span>
<span className="font-mono text-text-muted">{r.id}</span>
<span className="text-text-muted">{r.model}</span>
<span className="text-text-muted text-[10px]">
{startTime.toLocaleTimeString("en-US", { hour12: false })}
</span>
<span className="text-text-muted ml-auto">
{formatDuration(r.duration)}
</span>
{isCurrent && (
<span className="text-[9px] text-primary font-bold ml-1">current</span>
)}
</button>
);
})}
</div>
</div>
)}
{detailIssue && (
<div className="p-4 rounded-xl bg-amber-500/10 border border-amber-500/30">
<div className="text-[10px] text-amber-700 dark:text-amber-400 uppercase tracking-wider mb-1 font-bold">
@@ -612,14 +680,63 @@ export default function RequestLoggerDetail({
</div>
) : (
<>
{streamChunksText && (
{streamChunks && streamChunks.provider && (
<StreamSection
title="Event Stream (Debug)"
json={streamChunksText}
onCopy={() => 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 && (
<StreamSection
title="Client Event Stream"
json={
Array.isArray(streamChunks.client)
? streamChunks.client.join("")
: String(streamChunks.client)
}
onCopy={() =>
onCopy(
Array.isArray(streamChunks.client)
? streamChunks.client.join("")
: String(streamChunks.client)
)
}
/>
)}
{streamChunks &&
streamChunks.openai &&
!streamChunks.provider &&
!streamChunks.client && (
<StreamSection
title="Event Stream"
json={
Array.isArray(streamChunks.openai)
? streamChunks.openai.join("")
: String(streamChunks.openai)
}
onCopy={() =>
onCopy(
Array.isArray(streamChunks.openai)
? streamChunks.openai.join("")
: String(streamChunks.openai)
)
}
/>
)}
{payloadSections.length > 0 &&
payloadSections.map((section) => (
<PayloadSection

View File

@@ -139,7 +139,36 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
const [selectedApiKey, setSelectedApiKey] = useState("");
const [sortBy, setSortBy] = useState("newest");
const [selectedLog, setSelectedLog] = useState(null);
const [correlationIdFilter, setCorrelationIdFilter] = useState("");
const [detailLoading, setDetailLoading] = useState(false);
// Column sort toggle: clicking a column header toggles asc/desc
const columnSortMap = {
status: { desc: "status_desc", asc: "status_asc" },
model: { desc: "model_desc", asc: "model_asc" },
tokens: { desc: "tokens_desc", asc: "tokens_asc" },
tps: { desc: "tps_desc", asc: "tps_asc" },
duration: { desc: "duration_desc", asc: "duration_asc" },
time: { desc: "newest", asc: "oldest" },
};
const toggleSort = useCallback((column: string) => {
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<RequestLoggerV2Handle, { initialSelectedId?:
if (selectedProvider) params.set("provider", selectedProvider);
if (selectedAccount) params.set("account", selectedAccount);
if (selectedApiKey) params.set("apiKey", selectedApiKey);
if (correlationIdFilter) params.set("correlationId", correlationIdFilter);
params.set("limit", String(limit));
const res = await fetch(`/api/usage/call-logs?${params}`);
@@ -251,6 +281,7 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
selectedAccount,
selectedProvider,
selectedApiKey,
correlationIdFilter,
limit,
]
);
@@ -437,6 +468,34 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
return arr;
}, [filteredLogs, sortBy]);
// Group by correlationId: mark retries as children so they render indented
// under the first request in each group. Compute group health:
// "healed" — at least one failure followed by a success
// "failed" — all attempts failed
// null — single request or all succeeded
const groupedLogs = useMemo(() => {
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<RequestLoggerV2Handle, { initialSelectedId?:
};
}, [selectedLog?.id, detailData?.detailState, selectedLog?.active, fetchLogs]);
// Poll for related logs (same correlationId) while the detail modal is open.
// This ensures newly completed retries appear without closing the modal.
useEffect(() => {
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<RequestLoggerV2Handle, { initialSelectedId?:
);
// Stats (memoized to avoid re-computation on every render)
const { totalCount, okCount, errorCount, comboCount, apiKeyCount } = useMemo(
const { totalCount, okCount, errorCount, comboCount, apiKeyCount, runningCount } = useMemo(
() => ({
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<RequestLoggerV2Handle, { initialSelectedId?:
/>
</div>
{/* Correlation ID Filter */}
<div className="min-w-[180px] relative">
<span className="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-text-muted text-[16px]">
tag
</span>
<input
type="text"
placeholder="Correlation ID"
value={correlationIdFilter}
onChange={(e) => 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"
/>
</div>
{/* Provider Dropdown */}
<select
value={selectedProvider}
@@ -818,6 +936,11 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
<span className="px-2 py-1 rounded bg-bg-subtle border border-border font-mono">
{totalCount} {t("total")}
</span>
{runningCount > 0 && (
<span className="px-2 py-1 rounded bg-amber-500/10 text-amber-700 dark:text-amber-400 font-mono">
{runningCount} running
</span>
)}
<span className="px-2 py-1 rounded bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 font-mono">
{okCount} {t("ok")}
</span>
@@ -1000,7 +1123,13 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
<thead className={LOG_TABLE_HEAD_CLASS} style={LOG_TABLE_HEADER_BG_STYLE}>
<tr className={LOG_TABLE_ROW_CLASS} style={LOG_TABLE_HEADER_BG_STYLE}>
{visibleColumns.status && (
<th className={LOG_TABLE_HEADER_CELL_CLASS}>{t("columns.status")}</th>
<th
className={`${LOG_TABLE_HEADER_CELL_CLASS} cursor-pointer select-none`}
onClick={() => toggleSort("status")}
>
{t("columns.status")}
{getSortIndicator("status")}
</th>
)}
{visibleColumns.cacheSource && (
<th className={LOG_TABLE_HEADER_CELL_CLASS}>{t("columns.cacheSource")}</th>
@@ -1027,21 +1156,45 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
<th className={LOG_TABLE_HEADER_CELL_CLASS}>{t("columns.combo")}</th>
)}
{visibleColumns.tokens && (
<th className={LOG_TABLE_HEADER_CELL_RIGHT_CLASS}>{t("columns.tokens")}</th>
<th
className={`${LOG_TABLE_HEADER_CELL_RIGHT_CLASS} cursor-pointer select-none`}
onClick={() => toggleSort("tokens")}
>
{t("columns.tokens")}
{getSortIndicator("tokens")}
</th>
)}
{visibleColumns.tps && (
<th className={LOG_TABLE_HEADER_CELL_RIGHT_CLASS}>{t("columns.tps")}</th>
<th
className={`${LOG_TABLE_HEADER_CELL_RIGHT_CLASS} cursor-pointer select-none`}
onClick={() => toggleSort("tps")}
>
{t("columns.tps")}
{getSortIndicator("tps")}
</th>
)}
{visibleColumns.duration && (
<th className={LOG_TABLE_HEADER_CELL_RIGHT_CLASS}>{t("columns.duration")}</th>
<th
className={`${LOG_TABLE_HEADER_CELL_RIGHT_CLASS} cursor-pointer select-none`}
onClick={() => toggleSort("duration")}
>
{t("columns.duration")}
{getSortIndicator("duration")}
</th>
)}
{visibleColumns.time && (
<th className={LOG_TABLE_HEADER_CELL_RIGHT_CLASS}>{t("columns.time")}</th>
<th
className={`${LOG_TABLE_HEADER_CELL_RIGHT_CLASS} cursor-pointer select-none`}
onClick={() => toggleSort("time")}
>
{t("columns.time")}
{getSortIndicator("time")}
</th>
)}
</tr>
</thead>
<tbody className="divide-y divide-border/30">
{sortedLogs.map((log) => {
{groupedLogs.map((log) => {
const isActive = log.active === true;
const statusStyle = isActive ? null : getStatusStyle(log.status);
const protocolKey = isActive ? null : log.sourceFormat || log.provider;
@@ -1064,7 +1217,10 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
<tr
key={log.id}
onClick={() => openDetail(log)}
className={`cursor-pointer hover:bg-sky-500/10 dark:hover:bg-sky-400/10 transition-colors ${isError ? "bg-red-500/5" : ""}`}
className={`cursor-pointer hover:bg-sky-500/10 dark:hover:bg-sky-400/10 transition-colors ${isError ? "bg-red-500/5" : ""} ${log.isRetry ? "border-l-2 border-l-amber-500/50" : ""}`}
style={
log.isRetry ? { backgroundColor: "rgba(245,158,11,0.03)" } : undefined
}
>
{visibleColumns.status && (
<td className="px-3 py-2">
@@ -1076,11 +1232,41 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
<span className="inline-block h-3 w-3 rounded-full border-2 border-amber-500 border-t-transparent animate-spin" />
</span>
) : (
<span
className="inline-block px-2 py-0.5 rounded text-[10px] font-bold min-w-[36px] text-center"
style={{ backgroundColor: statusStyle.bg, color: statusStyle.text }}
>
{log.status || "..."}
<span className="inline-flex items-center gap-1">
<span
className="inline-block px-2 py-0.5 rounded text-[10px] font-bold min-w-[36px] text-center"
style={{
backgroundColor: statusStyle.bg,
color: statusStyle.text,
}}
>
{log.status || "..."}
</span>
{log.groupStatus === "healed" &&
!log.isRetry &&
log.status >= 400 && (
<span
className="text-emerald-500 text-[11px]"
title="Recovered by retry"
>
</span>
)}
{log.isRetry && (
<button
className="inline-flex items-center text-amber-500 hover:text-amber-400 text-[11px] ml-0.5"
title="Go to parent request"
onClick={(e) => {
e.stopPropagation();
const parent = groupedLogs.find(
(g) => g.correlationId === log.correlationId && !g.isRetry
);
if (parent) openDetail(parent);
}}
>
</button>
)}
</span>
)}
</td>
@@ -1103,7 +1289,49 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
)}
{visibleColumns.model && (
<td className="px-3 py-2 font-medium text-primary font-mono text-[11px]">
{log.model}
<div className="flex items-center gap-1.5">
<span>{log.model}</span>
{log.groupStatus === "healed" && !log.isRetry && (
<span
className="inline-flex items-center gap-0.5 px-1 py-0 rounded text-[8px] font-bold bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border border-emerald-500/25"
title={`Failed, recovered after ${log.groupSize - 1} retry`}
>
healed
</span>
)}
{log.groupStatus === "failed" && !log.isRetry && (
<span
className="inline-flex items-center gap-0.5 px-1 py-0 rounded text-[8px] font-bold bg-red-500/15 text-red-600 dark:text-red-400 border border-red-500/25"
title={`All ${log.groupSize} attempts failed`}
>
failed
</span>
)}
</div>
{log.correlationId && !log.isRetry && log.groupSize > 1 && (
<div
className="text-[9px] text-text-muted font-normal truncate max-w-[120px]"
title={log.correlationId}
>
{log.correlationId.slice(0, 12)} · {log.groupSize} attempts
</div>
)}
{log.correlationId && !log.isRetry && log.groupSize <= 1 && (
<div
className="text-[9px] text-text-muted font-normal truncate max-w-[120px]"
title={log.correlationId}
>
{log.correlationId.slice(0, 12)}
</div>
)}
{log.correlationId && log.isRetry && (
<div
className="text-[9px] text-amber-500/70 font-normal truncate max-w-[120px]"
title={log.correlationId}
>
{log.correlationId.slice(0, 12)}
</div>
)}
</td>
)}
{visibleColumns.requestedModel && (
@@ -1303,6 +1531,15 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
onCopy={copyToClipboard}
onPrevious={handlePrev}
onNext={handleNext}
relatedLogs={
selectedLog.correlationId
? groupedLogs.filter((l) => l.correlationId === selectedLog.correlationId)
: []
}
onSelectRelated={(r) => {
closeDetail();
openDetail(r);
}}
/>
)}
</div>

View File

@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
const API_KEY = process.env.OMNIROUTE_API_KEY;
const BASE_URL = process.env.OMNIROUTE_URL || "http://localhost:20128";
const MODEL = process.env.TEST_GEMINI_MODEL || "default";
const MODEL = "default";
const skip = !API_KEY ? "OMNIROUTE_API_KEY not set — skipping live test" : undefined;
@@ -54,7 +54,14 @@ async function readSSEStream(response: Response, onChunk?: (chunk: string) => vo
}
test("live request returns streamChunks", { skip }, async () => {
console.log("[TEST] BASE_URL=", BASE_URL, "OMNIROUTE_URL=", process.env.OMNIROUTE_URL, "API_KEY set=", !!API_KEY);
console.log(
"[TEST] BASE_URL=",
BASE_URL,
"OMNIROUTE_URL=",
process.env.OMNIROUTE_URL,
"API_KEY set=",
!!API_KEY
);
const messages = [
{ role: "system", content: "Execute the user prompt and provide a detailed explanation." },
@@ -106,9 +113,10 @@ test("live request returns streamChunks", { skip }, async () => {
);
if (activeRequestResponse.ok) {
let activeRequest = await activeRequestResponse.json();
if (activeRequest.active &&
Array.isArray(activeRequest.pipelinePayloads.streamChunks.provider) &&
activeRequest.pipelinePayloads.streamChunks.provider.length > 0
if (
activeRequest.active &&
Array.isArray(activeRequest.pipelinePayloads.streamChunks.provider) &&
activeRequest.pipelinePayloads.streamChunks.provider.length > 0
) {
console.log("Stream chunks:", activeRequest.pipelinePayloads.streamChunks.provider);
sawLogChunksWhileStreaming = true;
@@ -140,9 +148,13 @@ test("live request returns streamChunks", { skip }, async () => {
let finishedRequest = await logDetailResponse.json();
assert.equal(finishedRequest.id, requestId, "log detail id should match request id");
assert.equal(finishedRequest.active, false, "request should be marked as inactive after completion");
assert.equal(
finishedRequest.active,
false,
"request should be marked as inactive after completion"
);
assert.ok(Array.isArray(finishedRequest.pipelinePayloads.streamChunks.provider) );
assert.ok(Array.isArray(finishedRequest.pipelinePayloads.streamChunks.provider));
assert.ok(Array.isArray(finishedRequest.pipelinePayloads.streamChunks.client));
assert.ok(finishedRequest.pipelinePayloads.streamChunks.provider.length > 0);

View File

@@ -8,7 +8,6 @@
* Uses same env vars as tests/manual/gemini.http:
* OMNIROUTE_URL — base URL (default http://localhost:20128)
* OMNIROUTE_API_KEY — API key for auth
* TEST_GEMINI_MODEL — model override (default gemini/gemma-4-31b-it)
* TEST_THINKING_GEMINI_MODEL — thinking model override, skipped if unset
*/
@@ -17,7 +16,7 @@ import assert from "node:assert/strict";
const API_KEY = process.env.OMNIROUTE_API_KEY;
const BASE_URL = process.env.OMNIROUTE_URL || "http://localhost:20128";
const MODEL = process.env.TEST_GEMINI_MODEL || "gemini/gemma-4-31b-it";
const MODEL = "default";
const THINKING_MODEL = process.env.TEST_THINKING_GEMINI_MODEL || "gemini/gemini-2.5-flash";
const NUM_HISTORICAL_ROUNDS = 15;

View File

@@ -0,0 +1,246 @@
/**
* tests/integration/live-gemini-nonstream.test.ts
*
* Non-streaming variant of live-gemini-workload.test.ts.
* Reuses the same CASE_BUILDERS payload generators but sends stream: false.
* Validates that non-streaming responses return content and complete without errors.
*/
import test from "node:test";
import assert from "node:assert/strict";
import {
skip,
randomInt,
sendAndValidate,
CASE_BUILDERS,
ensureTestEnvironment,
DELAY_BETWEEN_REQUESTS_MS,
type Message,
} from "./liveGeminiShared.ts";
test.before(async () => {
await ensureTestEnvironment();
});
// ── Non-streaming concurrent load — 5 parallel threads × 5 iterations ──
test("[00] non-streaming: concurrent load — 5 threads × 2 iterations", { skip }, async () => {
const THREAD_COUNT = 5;
const SET_COUNT = 2;
const TOTAL_REQUESTS = THREAD_COUNT * SET_COUNT;
console.log(
`\n Non-streaming concurrent: ${THREAD_COUNT} threads × ${SET_COUNT} iterations = ${TOTAL_REQUESTS} requests`
);
const start = performance.now();
const requestWindows: { cid: string; start: number; end: number }[] = [];
let parallelViolation: string | null = null;
const threadResults = await Promise.allSettled(
Array.from({ length: THREAD_COUNT }, (_, threadIdx) =>
(async () => {
const results: {
status: number;
duration: number;
tokens: number;
contentLength: number;
correlationId: string;
}[] = [];
for (let set = 1; set <= SET_COUNT; set++) {
if (parallelViolation) break;
const idx = randomInt(0, CASE_BUILDERS.length - 1);
const tc = CASE_BUILDERS[idx];
const label = `ns-t${threadIdx + 1}-i${set}: ${tc.name}`;
const requestStart = Date.now();
const r = await sendAndValidate(label, tc.build, false);
const requestEnd = Date.now();
const cid = r.correlationId;
const myWindow = { cid, start: requestStart, end: requestEnd };
const siblings = requestWindows.filter((w) => w.cid === cid);
for (const s of siblings) {
if (myWindow.start < s.end && s.start < myWindow.end) {
parallelViolation =
`PARALLEL REQUEST DETECTED: cid=${cid.slice(0, 12)}` +
`window [${new Date(myWindow.start).toISOString().slice(11, 23)}${new Date(myWindow.end).toISOString().slice(11, 23)}] ` +
`overlaps with [${new Date(s.start).toISOString().slice(11, 23)}${new Date(s.end).toISOString().slice(11, 23)}]`;
break;
}
}
requestWindows.push(myWindow);
results.push({ ...r } as any);
}
return results;
})()
)
);
const totalDuration = performance.now() - start;
const fulfilled = threadResults.filter((r) => r.status === "fulfilled") as PromiseFulfilledResult<
{
status: number;
duration: number;
tokens: number;
contentLength: number;
correlationId: string;
}[]
>[];
const rejected = threadResults.filter((r) => r.status === "rejected") as PromiseRejectedResult[];
const allResults = fulfilled.flatMap((r) => r.value);
const totalTokens = allResults.reduce((sum, r) => sum + r.tokens, 0);
const avgDuration =
allResults.length > 0
? Math.round(allResults.reduce((s, r) => s + r.duration, 0) / allResults.length)
: 0;
console.log(
`\n Non-streaming concurrent summary: ${fulfilled.length}/${THREAD_COUNT} threads completed | ` +
`${allResults.length}/${TOTAL_REQUESTS} requests succeeded | ` +
`${Math.round(totalDuration)}ms wall clock | ` +
`${avgDuration}ms avg per request | ` +
`${totalTokens} total tokens`
);
if (rejected.length > 0) {
for (const r of rejected) {
const msg = r.reason instanceof Error ? r.message : String(r.reason);
console.log(` THREAD FAILED: ${msg}`);
}
}
if (parallelViolation) {
console.log(`\n !! ${parallelViolation}`);
}
assert.ok(
fulfilled.length === THREAD_COUNT,
`expected all ${THREAD_COUNT} threads to complete, ${rejected.length} failed`
);
assert.ok(
allResults.length === TOTAL_REQUESTS,
`expected ${TOTAL_REQUESTS} total requests, got ${allResults.length}`
);
assert.ok(!parallelViolation, parallelViolation);
// Verify all correlation IDs are unique
const cids = allResults.map((r) => r.correlationId);
const uniqueCids = new Set(cids);
assert.equal(
uniqueCids.size,
cids.length,
`expected ${cids.length} unique CIDs, got ${uniqueCids.size}`
);
});
// ── Non-streaming sequential test ───────────────────────────────────────
test("[01] non-streaming: sequential — 1 thread × 5 iterations", { skip }, async () => {
const SET_COUNT = 5;
console.log(`\n Non-streaming sequential: 1 thread × ${SET_COUNT} iterations`);
const start = performance.now();
const results: {
status: number;
duration: number;
tokens: number;
contentLength: number;
correlationId: string;
}[] = [];
for (let i = 1; i <= SET_COUNT; i++) {
const idx = randomInt(0, CASE_BUILDERS.length - 1);
const tc = CASE_BUILDERS[idx];
const label = `ns-i${i}: ${tc.name}`;
try {
if (i > 1) await new Promise((r) => setTimeout(r, DELAY_BETWEEN_REQUESTS_MS));
const r = await sendAndValidate(label, tc.build, false);
results.push(r);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.log(` ${label.padEnd(45)} FAILED: ${msg}`);
assert.fail(`iteration ${i} failed: ${msg}`);
}
}
const totalDuration = performance.now() - start;
const totalTokens = results.reduce((sum, r) => sum + r.tokens, 0);
const avgDuration =
results.length > 0
? Math.round(results.reduce((s, r) => s + r.duration, 0) / results.length)
: 0;
console.log(
`\n Non-streaming summary: ${results.length}/${SET_COUNT} succeeded | ` +
`${Math.round(totalDuration)}ms wall clock | ` +
`${avgDuration}ms avg per request | ` +
`${totalTokens} total tokens`
);
const cids = results.map((r) => r.correlationId);
const uniqueCids = new Set(cids);
assert.equal(
uniqueCids.size,
cids.length,
`expected ${cids.length} unique CIDs, got ${uniqueCids.size}`
);
assert.equal(results.length, SET_COUNT, `expected ${SET_COUNT} results, got ${results.length}`);
});
// ── Non-streaming: all payloads return content ──────────────────────────
test("[02] non-streaming: all payloads return content", { skip }, async () => {
const failures: string[] = [];
for (let i = 0; i < CASE_BUILDERS.length; i++) {
const tc = CASE_BUILDERS[i];
const label = `ns-${String(i + 1).padStart(2, "0")}: ${tc.name}`;
try {
const r = await sendAndValidate(label, tc.build, false);
if (r.contentLength === 0) {
failures.push(`${tc.name}: 0 bytes content`);
}
if (r.status !== 200) {
failures.push(`${tc.name}: HTTP ${r.status}`);
}
} catch (err) {
failures.push(`${tc.name}: ${err instanceof Error ? err.message : String(err)}`);
}
}
if (failures.length > 0) {
console.log(`\n Non-streaming failures (${failures.length}):`);
for (const f of failures) console.log(` ${f}`);
}
assert.equal(
failures.length,
0,
`${failures.length}/${CASE_BUILDERS.length} non-streaming payloads failed`
);
});
// ── Non-streaming: correlation IDs are unique ───────────────────────────
test("[03] non-streaming: correlation IDs are unique per request", { skip }, async () => {
const cids: string[] = [];
const count = 5;
for (let i = 0; i < count; i++) {
const tc = CASE_BUILDERS[i % CASE_BUILDERS.length];
const r = await sendAndValidate(`ns-cid-${i + 1}: ${tc.name}`, tc.build, false);
cids.push(r.correlationId);
}
const unique = new Set(cids);
assert.equal(
unique.size,
count,
`expected ${count} unique CIDs, got ${unique.size}: ${cids.join(", ")}`
);
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,188 @@
import test from "node:test";
import assert from "node:assert/strict";
import { ensureTestEnvironment, MODEL, BASE_URL, API_KEY } from "./liveGeminiShared.ts";
const DIRECT_MODEL = process.env.TEST_GEMINI_DIRECT_MODEL || "gemini/gemini-2.0-flash";
const skip = !API_KEY ? "OMNIROUTE_API_KEY not set — skipping live test" : undefined;
test.before(async () => {
await ensureTestEnvironment();
});
async function readSSEStream(response: Response): Promise<{
fullContent: string;
finishReason: string;
model: string;
totalTokens: number;
}> {
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let buffer = "";
let fullContent = "";
let finishReason = "unknown";
let model = "";
let totalTokens = 0;
let chunkCount = 0;
let sampleLines: string[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
chunkCount++;
const data = line.slice(6).trim();
if (data === "[DONE]") continue;
try {
const parsed = JSON.parse(data) as Record<string, unknown>;
const choice = ((parsed.choices ?? []) as Array<Record<string, unknown>>)[0];
if (choice) {
const delta = choice.delta as Record<string, unknown> | undefined;
if (delta?.content) {
fullContent += delta.content as string;
} else if (delta?.reasoning_content) {
fullContent += delta.reasoning_content as string;
}
if (choice.finish_reason) finishReason = choice.finish_reason as string;
}
if (!model) {
if (parsed.model) {
model = parsed.model as string;
} else if (choice?.model) {
model = choice.model as string;
}
}
const usage = parsed.usage as Record<string, number> | undefined;
if (usage) {
totalTokens =
usage.total_tokens ?? (usage.prompt_tokens ?? 0) + (usage.completion_tokens ?? 0);
}
} catch {
// skip malformed chunks
}
if (sampleLines.length < 3) {
sampleLines.push(data.slice(0, 200));
}
}
}
console.log(
`[SSE] total raw chunks: ${chunkCount}, content length: ${fullContent.length}, finish: ${finishReason}, model: ${model}, tokens: ${totalTokens}`
);
if (sampleLines.length > 0) {
console.log(`[SSE] sample lines: ${JSON.stringify(sampleLines)}`);
}
return { fullContent, finishReason, model, totalTokens };
}
test("live Gemini — single hello-world request via combo 'default'", { skip }, async (t) => {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 120_000);
try {
console.log(`[TEST] Sending request with model=${MODEL} to ${BASE_URL}/v1/chat/completions`);
const response = await fetch(`${BASE_URL}/v1/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({
model: MODEL,
messages: [{ role: "user", content: "Say 'Hello world' and nothing else." }],
stream: true,
max_tokens: 50,
temperature: 0,
}),
signal: controller.signal,
});
clearTimeout(timeout);
console.log(`[TEST] Response status: ${response.status}`);
if (!response.ok) {
const body = await response.text().catch(() => "unknown");
console.log(`[TEST] Response body: ${body}`);
}
assert.equal(response.status, 200, "Expected HTTP 200 from combo request");
const { fullContent, finishReason, model, totalTokens } = await readSSEStream(response);
assert.ok(fullContent.length > 0, "response should have content");
assert.ok(
finishReason === "stop" || finishReason === "length",
`expected stop/length finish, got ${finishReason}`
);
assert.ok(totalTokens > 0, `should have non-zero token count, got ${totalTokens}`);
assert.ok(
model.toLowerCase().includes("gemini") || model.toLowerCase().includes("gemma"),
`response model "${model}" should be a Gemini/Gemma model`
);
console.log(
`[TEST] OK: model=${model}, finish=${finishReason}, tokens=${totalTokens}, content=${fullContent.length} chars`
);
} catch (err) {
clearTimeout(timeout);
throw err;
}
});
test("live Gemini — direct model request (skip combo)", { skip }, async (t) => {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 120_000);
try {
console.log(`[TEST] Direct model request with model=${DIRECT_MODEL}`);
const response = await fetch(`${BASE_URL}/v1/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({
model: DIRECT_MODEL,
messages: [{ role: "user", content: "Say 'Hello world' and nothing else." }],
stream: true,
max_tokens: 50,
temperature: 0,
}),
signal: controller.signal,
});
clearTimeout(timeout);
console.log(`[TEST] Direct response status: ${response.status}`);
if (!response.ok) {
const body = await response.text().catch(() => "unknown");
console.log(`[TEST] Direct response body: ${body}`);
}
assert.equal(response.status, 200, "Expected HTTP 200 for direct model");
const { fullContent, finishReason, model, totalTokens } = await readSSEStream(response);
assert.ok(fullContent.length > 0, "response should have content");
assert.ok(
finishReason === "stop" || finishReason === "length",
`expected stop/length finish, got ${finishReason}`
);
assert.ok(totalTokens > 0, `should have non-zero token count, got ${totalTokens}`);
assert.ok(
model.toLowerCase().includes("gemini") || model.toLowerCase().includes("gemma"),
`response model "${model}" should be a Gemini/Gemma model`
);
console.log(`[TEST] Direct OK: model=${model}, finish=${finishReason}, tokens=${totalTokens}`);
} catch (err) {
clearTimeout(timeout);
throw err;
}
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,173 @@
/**
* Regression test: API-key-only connections must not be falsely expired.
*
* Bug: tokenHealthCheck.checkConnection() marked API-key-only connections
* (e.g. gemini with just an API key, no OAuth refresh token) as
* testStatus="expired" because it expected OAuth refresh tokens for any
* provider in the supportsTokenRefresh set.
*
* Fix: connections that have an apiKey configured are skipped during OAuth
* token validation, since they don't require refresh tokens.
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
process.env.NODE_ENV = "test";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-apikey-health-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const { checkConnection } = await import("../../src/lib/tokenHealthCheck.ts");
async function resetStorage() {
core.resetDbInstance();
for (let attempt = 0; attempt < 10; attempt++) {
try {
if (fs.existsSync(TEST_DATA_DIR)) {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
}
break;
} catch (error: any) {
if ((error?.code === "EBUSY" || error?.code === "EPERM") && attempt < 9) {
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
} else {
throw error;
}
}
}
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.after(async () => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("API-key-only gemini connection is NOT marked expired by health check", async () => {
await resetStorage();
// Create a gemini connection with an API key but no refresh token
const conn = await providersDb.createProviderConnection({
provider: "gemini",
name: "gemini-apikey-test",
apiKey: "AIzaSyTest1234567890abcdefghijklmnop",
isActive: true,
testStatus: "active",
healthCheckInterval: 60,
// No refreshToken — this is an API-key-only connection
});
assert.equal(conn.testStatus, "active", "precondition: connection starts as active");
// Run the health check
await checkConnection(conn);
// Re-read from DB
const updated = await providersDb.getProviderConnectionById(conn.id);
assert.equal(
updated?.testStatus,
"active",
"API-key-only connection should remain active — not be marked expired"
);
assert.notEqual(
updated?.errorCode,
"no_refresh_token",
"API-key-only connection should not get no_refresh_token error"
);
});
test("gemini connection WITHOUT apiKey AND WITHOUT refreshToken IS marked expired", async () => {
await resetStorage();
// Create a gemini OAuth connection that lost its refresh token
const conn = await providersDb.createProviderConnection({
provider: "gemini",
name: "gemini-oauth-no-refresh",
accessToken: "ya29.expired-token",
isActive: true,
testStatus: "active",
healthCheckInterval: 60,
// No apiKey, no refreshToken — this is a broken OAuth connection
});
assert.equal(conn.testStatus, "active", "precondition: connection starts as active");
// Run the health check
await checkConnection(conn);
// Re-read from DB
const updated = await providersDb.getProviderConnectionById(conn.id);
assert.equal(
updated?.testStatus,
"expired",
"OAuth connection without refresh token should be marked expired"
);
assert.equal(
updated?.errorCode,
"no_refresh_token",
"OAuth connection should get no_refresh_token error code"
);
});
test("API-key-only antigravity connection is NOT marked expired by health check", async () => {
await resetStorage();
// antigravity also supports token refresh — verify the fix applies to all providers
const conn = await providersDb.createProviderConnection({
provider: "antigravity",
name: "agy-apikey-test",
apiKey: "sk-ant-test1234567890",
isActive: true,
testStatus: "active",
healthCheckInterval: 60,
});
await checkConnection(conn);
const updated = await providersDb.getProviderConnectionById(conn.id);
assert.equal(
updated?.testStatus,
"active",
"antigravity API-key-only connection should remain active"
);
});
test("connection with both apiKey and refreshToken: refresh path is tried", async () => {
await resetStorage();
// Edge case: connection has both an API key and a refresh token
// The health check tries the refresh token path first.
// With a stale/invalid refresh token, the connection gets marked expired
// even though an API key exists — the refresh path takes precedence.
const conn = await providersDb.createProviderConnection({
provider: "gemini",
name: "gemini-dual-auth",
apiKey: "AIzaSyTest1234567890abcdefghijklmnop",
refreshToken: "1//old-refresh-token",
accessToken: "ya29.expired-token",
isActive: true,
testStatus: "active",
healthCheckInterval: 60,
});
await checkConnection(conn);
const updated = await providersDb.getProviderConnectionById(conn.id);
// The refresh token path is tried first. Since the refresh token is invalid,
// the connection gets marked expired. This is expected — the operator should
// either remove the stale refresh token or re-authenticate.
assert.equal(
updated?.testStatus,
"expired",
"dual-auth connection with stale refresh token should be expired (refresh path takes precedence)"
);
});

View File

@@ -0,0 +1,130 @@
import test from "node:test";
import assert from "node:assert/strict";
// Pure-function coverage for buildCallLogListRows (src/app/api/usage/call-logs/route.ts):
// - merges active/completed in-memory entries with persisted DB rows
// - sorts by priority (active > completed > persisted) then newest-first
// - carries correlationId through so the GET handler can filter on it
import { buildCallLogListRows } from "../../src/app/api/usage/call-logs/route.ts";
test("buildCallLogListRows: active requests sort before completed and persisted rows", () => {
const now = 1_000_000;
const rows = buildCallLogListRows({
logs: [
{
id: "persisted-1",
timestamp: new Date(now - 5_000).toISOString(),
correlationId: "corr-a",
},
],
connections: [],
pendingDetails: [
{
id: "active-1",
startedAt: now - 1_000,
provider: "openai",
model: "gpt-4o",
connectionId: "conn-1",
correlationId: "corr-a",
},
],
completedDetails: [
{
id: "completed-1",
startedAt: now - 3_000,
completedAt: now - 2_000,
provider: "anthropic",
model: "claude",
connectionId: "conn-2",
correlationId: "corr-b",
},
],
now,
});
assert.equal(rows.length, 3);
// active (priority 0) first, then completed (priority 1), then persisted (priority 2)
assert.equal(rows[0].id, "active-1");
assert.equal(rows[0].active, true);
assert.equal(rows[1].id, "completed-1");
assert.equal(rows[1].completed, true);
assert.equal(rows[2].id, "persisted-1");
});
test("buildCallLogListRows: within the same priority, newest timestamp sorts first", () => {
const now = 2_000_000;
const rows = buildCallLogListRows({
logs: [
{ id: "old", timestamp: new Date(now - 10_000).toISOString() },
{ id: "new", timestamp: new Date(now - 1_000).toISOString() },
],
connections: [],
pendingDetails: [],
completedDetails: [],
now,
});
assert.deepEqual(
rows.map((r: any) => r.id),
["new", "old"]
);
});
test("buildCallLogListRows: in-memory entries carry correlationId for downstream filtering", () => {
const now = 3_000_000;
const rows = buildCallLogListRows({
logs: [],
connections: [],
pendingDetails: [
{
id: "active-cid",
startedAt: now - 500,
provider: "openai",
model: "gpt-4o",
connectionId: "conn-1",
correlationId: "corr-xyz",
},
],
completedDetails: [
{
id: "completed-no-cid",
startedAt: now - 4_000,
completedAt: now - 3_000,
provider: "anthropic",
model: "claude",
connectionId: "conn-2",
},
],
now,
});
const active = rows.find((r: any) => r.id === "active-cid");
const completed = rows.find((r: any) => r.id === "completed-no-cid");
assert.equal(active?.correlationId, "corr-xyz");
assert.equal(completed?.correlationId, null);
});
test("buildCallLogListRows: dedupes completed in-memory entries already persisted to the DB", () => {
const now = 4_000_000;
const rows = buildCallLogListRows({
logs: [{ id: "dup-1", timestamp: new Date(now - 1_000).toISOString() }],
connections: [],
pendingDetails: [],
completedDetails: [
{
id: "dup-1",
startedAt: now - 3_000,
completedAt: now - 2_000,
provider: "openai",
model: "gpt-4o",
connectionId: "conn-1",
},
],
now,
});
assert.equal(rows.length, 1);
assert.equal(rows[0].id, "dup-1");
// persisted row wins (no `completed` flag)
assert.equal(rows[0].completed, undefined);
});