Files
OmniRoute/src/app/api/usage/call-logs/route.ts
Rouzbeh† 7ddbaf69a4 feat(cli): add native Bun backend support and Dockerfile.bun (#11039)
4 — Suporte de backend nativo Bun + Dockerfile.bun multi-stage + fallback dinâmico de driver SQLite (better-sqlite3 prioritário sob Bun, bun:sqlite fallback; Node preservado) + correção de estabilidade do DAST CI smoke.
Validado a fundo (worktree board sobre tip): bun-support 4/4, typecheck:core limpo, dashboard-typecheck OK (220 dentro do baseline), open-sse-typecheck OK (5 pré-existentes), gate de runtime OK sob Node, changelog-integrity OK, file-size/complexity/cognitive/dead-code OK. Verificado que o driver preserva a cadeia Node/falback conforme AGENTS.md; teste bun-support presente. Baselines de typecheck removidos são ratchet honesto (erros não existem mais).
OBS: destravei 2 base-reds do tip neste turno (push direto 7ffa3ef): movi o changelog fragment da #11050 da seção inválida breaking/ para fixes/, e rebaselinei AddApiKeyModal 1067->1073 (crescimento da #11056). Sem isso a #11039 e o resto da fila ficariam vermelhos.
2026-08-21 21:28:43 -03:00

191 lines
7.2 KiB
TypeScript

import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { getCallLogs } from "@/lib/usageDb";
import { getCompletedDetails, getPendingById } from "@/lib/usage/usageHistory";
import { getProviderConnections } from "@/lib/localDb";
import { getProviderNodes } from "@/models";
import { matchesSearch } from "@/shared/utils/turkishText";
type CallLogListRowsInput = {
logs: any[];
connections: any[];
providerDisplayNames?: Map<string, string>;
pendingDetails: Iterable<any>;
completedDetails: Iterable<any>;
now?: number;
};
function rowTimestampMs(row: any): number {
const value = Date.parse(String(row?.timestamp || ""));
return Number.isFinite(value) ? value : 0;
}
function rowPriority(row: any): number {
if (row?.active) return 0;
if (row?.completed) return 1;
return 2;
}
export function buildCallLogListRows({
logs,
connections,
providerDisplayNames = new Map<string, string>(),
pendingDetails,
completedDetails,
now = Date.now(),
}: CallLogListRowsInput): any[] {
const connectionNames = new Map(
connections.map((connection: any) => [
connection.id,
connection.displayName || connection.name || connection.email || connection.id,
])
);
const getProviderDisplay = (providerId: unknown): string | null => {
if (typeof providerId !== "string" || providerId.length === 0) return null;
return providerDisplayNames.get(providerId) || null;
};
// Include active (in-flight) requests from the pending-by-id map
// so they appear in the logs grid alongside persisted entries.
const activeEntries: any[] = [];
const persistedIds = new Set(logs.map((log: any) => log.id).filter(Boolean));
for (const detail of pendingDetails) {
activeEntries.push({
id: detail.id,
timestamp: new Date(detail.startedAt).toISOString(),
method: "",
path: detail.clientEndpoint || "",
status: 0,
model: detail.model,
requestedModel: null,
provider: detail.provider,
providerDisplay: getProviderDisplay(detail.provider),
account: connectionNames.get(detail.connectionId || "") || detail.connectionId || "unknown",
connectionId: detail.connectionId,
duration: Math.max(0, now - detail.startedAt),
tokens: { in: 0, out: 0 },
cacheSource: null,
sourceFormat: null,
targetFormat: null,
apiKeyId: null,
apiKeyName: null,
comboName: null,
error: null,
correlationId: detail.correlationId || null,
active: true,
});
}
const pendingIds = new Set(activeEntries.map((entry) => entry.id));
const completedEntries: any[] = [];
for (const detail of completedDetails) {
if (persistedIds.has(detail.id) || pendingIds.has(detail.id)) continue;
const completedAt = typeof detail.completedAt === "number" ? detail.completedAt : null;
const duration =
typeof detail.durationMs === "number" && Number.isFinite(detail.durationMs)
? detail.durationMs
: Math.max(0, (completedAt ?? now) - detail.startedAt);
completedEntries.push({
id: detail.id,
timestamp: new Date(detail.startedAt).toISOString(),
method: "",
path: detail.clientEndpoint || "",
status: typeof detail.status === "number" ? detail.status : detail.error ? 502 : 200,
model: detail.model,
requestedModel: null,
provider: detail.provider,
providerDisplay: getProviderDisplay(detail.provider),
account: connectionNames.get(detail.connectionId || "") || detail.connectionId || "unknown",
connectionId: detail.connectionId,
duration,
tokens: { in: 0, out: 0 },
cacheSource: null,
sourceFormat: null,
targetFormat: null,
apiKeyId: null,
apiKeyName: null,
comboName: null,
error: detail.error || null,
correlationId: detail.correlationId || null,
active: false,
completed: true,
completedAt: completedAt ? new Date(completedAt).toISOString() : null,
detailState: "in-memory",
});
}
return [...activeEntries, ...completedEntries, ...logs].sort((a, 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);
});
}
export async function GET(request: Request) {
try {
const authError = await requireManagementAuth(request);
if (authError) return authError;
const { searchParams } = new URL(request.url);
const filter: Record<string, any> = {};
if (searchParams.get("status")) filter.status = searchParams.get("status");
if (searchParams.get("model")) filter.model = searchParams.get("model");
if (searchParams.get("provider")) filter.provider = searchParams.get("provider");
if (searchParams.get("account")) filter.account = searchParams.get("account");
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"));
// Home Recent Requests feed sets excludeTests=1 so connection-test probe rows
// are dropped at the SQL layer (before LIMIT), not client-side after slicing.
if (searchParams.get("excludeTests") === "1") filter.excludeTests = true;
const [logs, connections, providerNodes] = await Promise.all([
getCallLogs(filter),
getProviderConnections(),
getProviderNodes(),
]);
const providerDisplayNames = new Map<string, string>(
(Array.isArray(providerNodes) ? providerNodes : []).flatMap((node: any) => {
if (typeof node?.id !== "string" || node.id.length === 0) return [];
const label =
(typeof node?.name === "string" && node.name.trim().length > 0
? node.name.trim()
: typeof node?.prefix === "string" && node.prefix.trim().length > 0
? node.prefix.trim()
: "") || null;
return label ? [[node.id, label] as const] : [];
})
);
const rows = buildCallLogListRows({
logs,
connections,
providerDisplayNames,
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) => matchesSearch(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 });
}
}