Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions open-sse/handlers/chatCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,7 @@ export async function handleChatCore({
stage: "registered",
correlationId,
sessionTag: conversationId || null,
callLogId: traceId,
}) || generateRequestId();

// Initialize rate limit settings from persisted DB (once, lazy)
Expand Down
6 changes: 3 additions & 3 deletions src/app/api/logs/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { NextResponse } from "next/server";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { getCallLogById } from "@/lib/usageDb";
import { getCompletedDetails, getPendingById } from "@/lib/usage/usageHistory";
import { getPendingById } from "@/lib/usage/usageHistory";
import { findCompletedDetailForCallLog } from "@/lib/usage/completedRequestDetails";

// Each logged chunk-array element is one raw network read, timestamp-prefixed
// for the debug display — NOT one complete SSE `data:` line. A single JSON
Expand Down Expand Up @@ -116,8 +117,7 @@ export async function GET(
Object.keys(persistedRequest.pipelinePayloads).length === 0
) {
try {
const completed = getCompletedDetails();
const inMem = completed.get(id);
const inMem = findCompletedDetailForCallLog(id, persistedRequest);
if (inMem) {
const pipelinePayloads: any = {
clientRequest: inMem.clientRequest ?? null,
Expand Down
24 changes: 22 additions & 2 deletions src/app/api/usage/call-logs/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const dynamic = "force-dynamic";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { getCallLogs } from "@/lib/usageDb";
import { getCompletedDetails, getPendingById } from "@/lib/usage/usageHistory";
import { callLogCorrelationKey } from "@/lib/usage/completedRequestDetails";
import { getProviderConnections } from "@/lib/db/providers";
import { getProviderNodes } from "@/models";
import { matchesSearch } from "@/shared/utils/turkishText";
Expand Down Expand Up @@ -44,7 +45,10 @@ export function rowMatchesFilter(row: any, filter: Record<string, any>): boolean
if (!(Number(row?.status) >= 400 || Boolean(row?.error))) return false;
} else if (filter.status === "ok") {
if (!(Number(row?.status) >= 200 && Number(row?.status) < 300)) return false;
} else if (typeof filter.status === "number" || (typeof filter.status === "string" && !isNaN(Number(filter.status)))) {
} else if (
typeof filter.status === "number" ||
(typeof filter.status === "string" && !isNaN(Number(filter.status)))
) {
if (Number(row?.status) !== Number(filter.status)) return false;
}

Expand All @@ -63,7 +67,10 @@ export function rowMatchesFilter(row: any, filter: Record<string, any>): boolean
if (filter.combo && !matchesSearch(row?.comboName || "", String(filter.combo))) {
return false;
}
if (filter.correlationId && !matchesSearch(row?.correlationId || "", String(filter.correlationId))) {
if (
filter.correlationId &&
!matchesSearch(row?.correlationId || "", String(filter.correlationId))
) {
return false;
}
if (filter.search) {
Expand Down Expand Up @@ -155,9 +162,22 @@ export function buildCallLogListRows({
}

const pendingIds = new Set(activeEntries.map((entry) => entry.id));
// Persisted rows are keyed on the chatCore trace id (#13481), not the in-memory
// detail id, so in-memory copies are matched via `callLogId`, falling back to the
// correlation key for details that carry no callLogId. Only in-memory copies are
// ever skipped — persisted rows sharing a correlationId (real retries) all stay.
const persistedCorrelationKeys = new Set(
logs.map((log: any) => callLogCorrelationKey(log)).filter(Boolean)
);
const completedEntries: any[] = [];
for (const detail of completedDetails) {
if (persistedIds.has(detail.id) || pendingIds.has(detail.id)) continue;
if (detail.callLogId) {
if (persistedIds.has(detail.callLogId)) continue;
} else {
const key = callLogCorrelationKey(detail);
if (key && persistedCorrelationKeys.has(key)) continue;
}
const completedAt = typeof detail.completedAt === "number" ? detail.completedAt : null;
const duration =
typeof detail.durationMs === "number" && Number.isFinite(detail.durationMs)
Expand Down
45 changes: 43 additions & 2 deletions src/lib/usage/completedRequestDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,48 @@ export function scheduleCompletedDetailCleanup(id: string) {
completedDetailTimers.set(id, timer);
}

/**
* Identity shared by a persisted call-log row and the in-memory copy of the same
* request. Deliberately excludes `connectionId`: the in-memory detail keeps the
* starting account while the persisted row stores the post-rotation one. Returns
* null without a correlationId so rows are never matched on a missing correlation.
*/
export function callLogCorrelationKey(row: {
correlationId?: string | null;
model?: string | null;
provider?: string | null;
}): string | null {
if (!row?.correlationId) return null;
return `${row.correlationId}\u0000${row.model || ""}\u0000${row.provider || ""}`;
}

/**
* Finds the in-memory completed detail backing a persisted call-log row. Matches on
* the detail id, then on `callLogId` (persisted rows are keyed on the chatCore trace
* id), then on the correlation key — the last only for details without a
* `callLogId`, since one that has a different callLogId belongs to another attempt.
*/
export function findCompletedDetailForCallLog(
id: string,
persisted?: {
correlationId?: string | null;
model?: string | null;
provider?: string | null;
} | null
): PendingRequestDetail | undefined {
const direct = completedDetails.get(id);
if (direct) return direct;
const key = persisted ? callLogCorrelationKey(persisted) : null;
let correlated: PendingRequestDetail | undefined;
for (const detail of completedDetails.values()) {
if (detail.callLogId === id) return detail;
if (key && !correlated && !detail.callLogId && callLogCorrelationKey(detail) === key) {
correlated = detail;
}
}
return correlated;
}

export function clearCompletedDetails() {
for (const timer of completedDetailTimers.values()) clearTimeout(timer);
completedDetailTimers.clear();
Expand Down Expand Up @@ -71,8 +113,7 @@ export function maybeEnrichCompletedDetail(updated: PendingRequestDetail, connec
const art = readCallArtifact(row.artifact_relpath);
if (art.state !== "ready" || !art.artifact) continue;
const pipeline = art.artifact.pipeline as
| { providerResponse?: unknown; clientResponse?: unknown }
| undefined;
{ providerResponse?: unknown; clientResponse?: unknown } | undefined;
if (missingProvider && pipeline?.providerResponse) {
updated.providerResponse = pipeline.providerResponse;
}
Expand Down
7 changes: 7 additions & 0 deletions src/lib/usage/usageHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export type PendingRequestMetadata = {
stageUpdatedAt?: number | null;
correlationId?: string | null;
sessionTag?: string | null;
callLogId?: string | null;
};
export type PendingRequestDetail = {
id: string;
Expand All @@ -77,6 +78,9 @@ export type PendingRequestDetail = {
stageUpdatedAt?: number | null;
correlationId?: string | null;
sessionTag?: string | null;
// Id of the persisted call_logs row for this request (chatCore traceId). Differs
// from `id`, so the call-log list/detail routes use it to pair in-memory copies.
callLogId?: string | null;
streamChunks?: {
provider?: string[];
openai?: string[];
Expand Down Expand Up @@ -139,6 +143,9 @@ function normalizePendingMetadata(metadata?: PendingRequestMetadata): PendingReq
if (metadata.sessionTag !== undefined) {
normalized.sessionTag = toStringOrNull(metadata.sessionTag) || null;
}
if (metadata.callLogId !== undefined) {
normalized.callLogId = toStringOrNull(metadata.callLogId) || null;
}

return normalized;
}
Expand Down
Loading
Loading