700 lines
27 KiB
TypeScript
700 lines
27 KiB
TypeScript
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";
|
|
|
|
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-reqlogger-ep-"));
|
|
process.env.DATA_DIR = TEST_DATA_DIR;
|
|
|
|
const core = await import("../../src/lib/db/core.ts");
|
|
const usageHistory = await import("../../src/lib/usage/usageHistory.ts");
|
|
const callLogs = await import("../../src/lib/usage/callLogs.ts");
|
|
|
|
test.after(() => {
|
|
core.resetDbInstance();
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
|
});
|
|
|
|
// Captured stream chunks are prefixed with a per-chunk arrival timestamp
|
|
// ("[HH:MM:SS.mmm] ") by the request logger for streaming-latency observability
|
|
// (added in #5834 / correlation-id observability). Strip it before comparing the
|
|
// raw payload so these assertions verify the chunk content + ordering, not the clock.
|
|
const stripChunkTs = (chunk: string): string => chunk.replace(/^\[\d{2}:\d{2}:\d{2}\.\d{3}\] /, "");
|
|
|
|
// ─── Active log endpoint removal ─────────────────────────────────────────
|
|
|
|
test("/api/logs/active route is removed", () => {
|
|
assert.equal(
|
|
fs.existsSync("src/app/api/logs/active/route.ts"),
|
|
false,
|
|
"active logs must not expose an in-memory timing-sensitive endpoint"
|
|
);
|
|
});
|
|
|
|
test("RequestLoggerV2 detail fetch uses /api/logs/[id] and not /api/logs/active", () => {
|
|
const content = fs.readFileSync("src/shared/components/RequestLoggerV2.tsx", "utf8");
|
|
assert.match(content, /fetch\(`\/api\/logs\/\$\{[^}]+\.id\}`,\s*\{\s*cache:\s*"no-store"/);
|
|
assert.doesNotMatch(content, /fetch\(["'`]\/api\/logs\/active/);
|
|
assert.match(content, /detailState:\s*"pending"/);
|
|
});
|
|
|
|
// ─── usageHistory module behaviour ───────────────────────────────────────
|
|
|
|
test("trackPendingRequest creates a detail entry", () => {
|
|
usageHistory.clearPendingRequests();
|
|
usageHistory.trackPendingRequest("gpt-4", "openai", "conn-1", true, {
|
|
clientRequest: { messages: [{ role: "user", content: "hi" }] },
|
|
providerRequest: { model: "gpt-4" },
|
|
providerUrl: "https://api.openai.com/v1/chat/completions",
|
|
});
|
|
|
|
const pending = usageHistory.getPendingRequests();
|
|
const detail = pending.details["conn-1"]?.["gpt-4 (openai)"]?.[0];
|
|
assert.ok(detail, "should create detail entry");
|
|
assert.equal(detail.model, "gpt-4");
|
|
assert.equal(detail.provider, "openai");
|
|
assert.equal(detail.connectionId, "conn-1");
|
|
assert.ok(detail.id, "should have an id");
|
|
assert.ok(detail.startedAt > 0, "should have startedAt timestamp");
|
|
assert.ok(detail.clientRequest, "should preserve clientRequest");
|
|
assert.equal(detail.clientRequest.messages[0].content, "hi");
|
|
});
|
|
|
|
test("trackPendingRequest decrements and removes detail on finish", () => {
|
|
usageHistory.clearPendingRequests();
|
|
usageHistory.trackPendingRequest("gpt-4", "openai", "conn-1", true);
|
|
assert.equal(usageHistory.getPendingRequests().details["conn-1"]?.["gpt-4 (openai)"]?.length, 1);
|
|
|
|
usageHistory.trackPendingRequest("gpt-4", "openai", "conn-1", false);
|
|
const after = usageHistory.getPendingRequests();
|
|
assert.equal(after.details["conn-1"]?.["gpt-4 (openai)"]?.length ?? 0, 0);
|
|
});
|
|
|
|
test("trackPendingRequest does not go negative", () => {
|
|
usageHistory.clearPendingRequests();
|
|
usageHistory.trackPendingRequest("gpt-4", "openai", "conn-1", false);
|
|
usageHistory.trackPendingRequest("gpt-4", "openai", "conn-1", false);
|
|
const pending = usageHistory.getPendingRequests();
|
|
assert.equal(pending.byModel["gpt-4 (openai)"], 0);
|
|
});
|
|
|
|
test("updatePendingRequestStreamChunks stores stream chunks in the detail", () => {
|
|
usageHistory.clearPendingRequests();
|
|
usageHistory.trackPendingRequest("gpt-4", "openai", "conn-1", true);
|
|
|
|
const chunks = { provider: ['data: {"a":1}'], openai: [], client: [] };
|
|
usageHistory.updatePendingRequestStreamChunks("gpt-4", "openai", "conn-1", chunks);
|
|
|
|
const pending = usageHistory.getPendingRequests();
|
|
const detail = pending.details["conn-1"]?.["gpt-4 (openai)"]?.[0];
|
|
assert.ok(detail.streamChunks, "streamChunks should be set");
|
|
assert.equal(detail.streamChunks.provider.length, 1);
|
|
assert.equal(detail.streamChunks.provider[0], 'data: {"a":1}');
|
|
});
|
|
|
|
test("updatePendingRequest keeps pending detail API view in sync", () => {
|
|
usageHistory.clearPendingRequests();
|
|
const requestId = usageHistory.trackPendingRequest(
|
|
"claude-sonnet-4-6",
|
|
"cc-test",
|
|
"conn-1",
|
|
true,
|
|
{
|
|
clientRequest: { model: "cc-test/claude-sonnet-4-6", reasoning_effort: "xhigh" },
|
|
providerRequest: { model: "claude-sonnet-4-6", reasoning_effort: "xhigh" },
|
|
}
|
|
);
|
|
assert.ok(requestId, "trackPendingRequest should return an id");
|
|
|
|
usageHistory.updatePendingRequest("claude-sonnet-4-6", "cc-test", "conn-1", {
|
|
providerRequest: { model: "claude-sonnet-4-6", reasoning_effort: "high" },
|
|
stage: "provider_response_started",
|
|
});
|
|
|
|
const modelKey = "claude-sonnet-4-6 (cc-test)";
|
|
const detailFromQueue = usageHistory.getPendingRequests().details["conn-1"]?.[modelKey]?.[0];
|
|
const detailFromId = usageHistory.getPendingById().get(requestId);
|
|
|
|
assert.equal(detailFromId, detailFromQueue);
|
|
assert.deepEqual(detailFromId?.providerRequest, {
|
|
model: "claude-sonnet-4-6",
|
|
reasoning_effort: "high",
|
|
});
|
|
assert.deepEqual(detailFromId?.clientRequest, {
|
|
model: "cc-test/claude-sonnet-4-6",
|
|
reasoning_effort: "xhigh",
|
|
});
|
|
});
|
|
|
|
test("updatePendingRequestById updates and finalizes the exact overlapping request", () => {
|
|
usageHistory.clearPendingRequests();
|
|
const firstId = usageHistory.trackPendingRequest("claude-sonnet-4-6", "cc-test", "conn-1", true, {
|
|
providerRequest: { request: "first", reasoning_effort: "xhigh" },
|
|
});
|
|
const secondId = usageHistory.trackPendingRequest(
|
|
"claude-sonnet-4-6",
|
|
"cc-test",
|
|
"conn-1",
|
|
true,
|
|
{
|
|
providerRequest: { request: "second", reasoning_effort: "xhigh" },
|
|
}
|
|
);
|
|
assert.ok(firstId);
|
|
assert.ok(secondId);
|
|
|
|
const updated = usageHistory.updatePendingRequestById(firstId, {
|
|
providerRequest: { request: "first", reasoning_effort: "high" },
|
|
stage: "sending_to_provider",
|
|
});
|
|
assert.equal(updated, true);
|
|
|
|
const modelKey = "claude-sonnet-4-6 (cc-test)";
|
|
const details = usageHistory.getPendingRequests().details["conn-1"]?.[modelKey];
|
|
assert.equal(details?.length, 2);
|
|
assert.deepEqual(details?.[0]?.providerRequest, {
|
|
request: "first",
|
|
reasoning_effort: "high",
|
|
});
|
|
assert.deepEqual(details?.[1]?.providerRequest, {
|
|
request: "second",
|
|
reasoning_effort: "xhigh",
|
|
});
|
|
|
|
const completed = usageHistory.finalizePendingRequestById(firstId, {
|
|
clientResponse: { ok: true },
|
|
});
|
|
assert.equal(completed, true);
|
|
assert.deepEqual(usageHistory.getCompletedDetails().get(firstId)?.providerRequest, {
|
|
request: "first",
|
|
reasoning_effort: "high",
|
|
});
|
|
assert.equal(usageHistory.getPendingById().has(secondId), true);
|
|
});
|
|
|
|
test("updatePendingRequestStreamChunks stores empty streamChunks object (not null)", () => {
|
|
usageHistory.clearPendingRequests();
|
|
usageHistory.trackPendingRequest("gpt-4", "openai", "conn-1", true);
|
|
|
|
// Call with empty arrays (as pushStreamChunks does before data flows)
|
|
const empty = { provider: [], openai: [], client: [] };
|
|
usageHistory.updatePendingRequestStreamChunks("gpt-4", "openai", "conn-1", empty);
|
|
|
|
const pending = usageHistory.getPendingRequests();
|
|
const detail = pending.details["conn-1"]?.["gpt-4 (openai)"]?.[0];
|
|
assert.ok(detail.streamChunks, "streamChunks should be set even when empty");
|
|
assert.deepEqual(detail.streamChunks, { provider: [], openai: [], client: [] });
|
|
|
|
// Verify the reference is live: mutations to the original object are visible
|
|
empty.provider.push("data: hello");
|
|
assert.equal(detail.streamChunks.provider.length, 1);
|
|
assert.equal(detail.streamChunks.provider[0], "data: hello");
|
|
});
|
|
|
|
test("clearPendingRequests resets all counts and details", () => {
|
|
usageHistory.clearPendingRequests();
|
|
usageHistory.trackPendingRequest("m1", "p1", "c1", true);
|
|
usageHistory.trackPendingRequest("m2", "p2", "c2", true);
|
|
assert.equal(Object.keys(usageHistory.getPendingRequests().byModel).length, 2);
|
|
|
|
usageHistory.clearPendingRequests();
|
|
const pending = usageHistory.getPendingRequests();
|
|
assert.equal(Object.keys(pending.byModel).length, 0);
|
|
assert.equal(Object.keys(pending.byAccount).length, 0);
|
|
assert.equal(Object.keys(pending.details).length, 0);
|
|
});
|
|
|
|
// ─── Pending request data remains available for internal usage stats ─────
|
|
|
|
test("pending request detail shape remains available internally", () => {
|
|
usageHistory.clearPendingRequests();
|
|
usageHistory.trackPendingRequest("claude-3-opus", "anthropic", "conn-2", true, {
|
|
clientRequest: { messages: [{ role: "user", content: "hello" }] },
|
|
providerRequest: { model: "claude-3-opus" },
|
|
providerUrl: "https://api.anthropic.com/v1/messages",
|
|
});
|
|
|
|
const pending = usageHistory.getPendingRequests();
|
|
const entries = Object.entries(pending.details).flatMap(([connectionId, models]) =>
|
|
Object.entries(models).flatMap(([modelKey, details]) =>
|
|
details.map((detail) => ({
|
|
id: detail.id,
|
|
model: detail.model,
|
|
provider: detail.provider,
|
|
connectionId,
|
|
startedAt: detail.startedAt,
|
|
clientRequest: detail.clientRequest ?? null,
|
|
providerRequest: detail.providerRequest ?? null,
|
|
providerUrl: detail.providerUrl ?? null,
|
|
streamChunks: detail.streamChunks ?? null,
|
|
}))
|
|
)
|
|
);
|
|
|
|
assert.equal(entries.length, 1);
|
|
const row = entries[0];
|
|
assert.ok(row.id);
|
|
assert.equal(row.model, "claude-3-opus");
|
|
assert.equal(row.provider, "anthropic");
|
|
assert.equal(row.connectionId, "conn-2");
|
|
assert.ok(row.startedAt > 0);
|
|
assert.ok(row.clientRequest, "clientRequest should be present");
|
|
assert.ok(row.providerRequest, "providerRequest should be present");
|
|
assert.ok(row.providerUrl, "providerUrl should be present");
|
|
assert.equal(row.streamChunks, null, "streamChunks should be null initially");
|
|
});
|
|
|
|
// ─── /api/usage/call-logs/[id] route structure ────────────────────────────
|
|
|
|
test("GET /api/usage/call-logs/[id] route exists and uses auth", () => {
|
|
const content = fs.readFileSync("src/app/api/usage/call-logs/[id]/route.ts", "utf8");
|
|
assert.ok(content.includes("export async function GET"), "should export GET handler");
|
|
assert.ok(content.includes("requireManagementAuth"), "should check auth");
|
|
assert.ok(content.includes("getCallLogById"), "should import getCallLogById");
|
|
});
|
|
|
|
test("GET /api/usage/call-logs includes completed in-memory fallback rows", async () => {
|
|
const { buildCallLogListRows } = await import("../../src/app/api/usage/call-logs/route.ts");
|
|
usageHistory.clearPendingRequests();
|
|
|
|
const requestId = usageHistory.trackPendingRequest("gpt-4", "openai", "completed-conn-1", true, {
|
|
clientEndpoint: "/v1/chat/completions",
|
|
});
|
|
assert.ok(requestId);
|
|
|
|
const completed = usageHistory.finalizePendingRequestById(requestId, {
|
|
clientResponse: { choices: [{ message: { content: "done" } }] },
|
|
});
|
|
assert.equal(completed, true);
|
|
|
|
const completedDetail = usageHistory.getCompletedDetails().get(requestId);
|
|
assert.ok(completedDetail);
|
|
const rows = buildCallLogListRows({
|
|
logs: [],
|
|
connections: [{ id: "completed-conn-1", displayName: "Completed Account" }],
|
|
pendingDetails: usageHistory.getPendingById().values(),
|
|
completedDetails: usageHistory.getCompletedDetails().values(),
|
|
now: completedDetail!.startedAt + 60_000,
|
|
});
|
|
|
|
assert.equal(rows.length, 1);
|
|
assert.equal(rows[0].id, requestId);
|
|
assert.equal(rows[0].detailState, "in-memory");
|
|
assert.equal(rows[0].completed, true);
|
|
assert.equal(rows[0].account, "Completed Account");
|
|
assert.equal(rows[0].duration, completedDetail!.durationMs);
|
|
});
|
|
|
|
test("GET /api/usage/call-logs rows are ordered by priority then newest-first", async () => {
|
|
const { buildCallLogListRows } = await import("../../src/app/api/usage/call-logs/route.ts");
|
|
const rows = buildCallLogListRows({
|
|
logs: [
|
|
{
|
|
id: "persisted-newest",
|
|
timestamp: new Date(3000).toISOString(),
|
|
active: false,
|
|
},
|
|
{
|
|
id: "persisted-oldest",
|
|
timestamp: new Date(1000).toISOString(),
|
|
active: false,
|
|
},
|
|
],
|
|
connections: [],
|
|
pendingDetails: [],
|
|
completedDetails: [
|
|
{
|
|
id: "completed-middle",
|
|
model: "gpt-4",
|
|
provider: "openai",
|
|
connectionId: "conn",
|
|
startedAt: 2000,
|
|
completedAt: 2500,
|
|
durationMs: 500,
|
|
},
|
|
],
|
|
now: 4000,
|
|
});
|
|
|
|
// Rows sort by priority first (active=0 > completed in-memory=1 > persisted=2),
|
|
// then newest-first within each priority (#5834 correlation-id observability).
|
|
// The completed in-memory entry therefore floats above the persisted DB rows,
|
|
// which are ordered newest-first among themselves.
|
|
assert.deepEqual(
|
|
rows.map((row) => row.id),
|
|
["completed-middle", "persisted-newest", "persisted-oldest"]
|
|
);
|
|
});
|
|
|
|
test("getCallLogById returns null for unknown id", async () => {
|
|
const log = await callLogs.getCallLogById("nonexistent-id-12345");
|
|
assert.equal(log, null);
|
|
});
|
|
|
|
// ─── Merged view data shape ────────────────────────────────────────────
|
|
|
|
test("normalized active row has expected fields for the grid view", () => {
|
|
const rawRow = {
|
|
id: "test-id-1",
|
|
model: "gpt-4",
|
|
provider: "openai",
|
|
account: "conn-1",
|
|
startedAt: Date.now() - 5000,
|
|
runningTimeMs: 5000,
|
|
stage: "streaming",
|
|
stageUpdatedAt: Date.now(),
|
|
clientRequest: { messages: [] },
|
|
providerRequest: { model: "gpt-4" },
|
|
providerUrl: "https://api.openai.com/v1",
|
|
streamChunks: null,
|
|
};
|
|
|
|
const normalized = {
|
|
active: true,
|
|
id: rawRow.id,
|
|
model: rawRow.model,
|
|
provider: rawRow.provider,
|
|
account: rawRow.account,
|
|
timestamp: new Date(rawRow.startedAt).toISOString(),
|
|
duration: Math.max(0, Date.now() - rawRow.startedAt),
|
|
status: 0,
|
|
sourceFormat: null,
|
|
tokens: null,
|
|
comboName: null,
|
|
apiKeyName: null,
|
|
apiKeyId: null,
|
|
cacheSource: null,
|
|
requestedModel: null,
|
|
stage: rawRow.stage,
|
|
stageUpdatedAt: rawRow.stageUpdatedAt,
|
|
_activeRow: rawRow,
|
|
};
|
|
|
|
assert.equal(normalized.active, true);
|
|
assert.equal(normalized.id, "test-id-1");
|
|
assert.equal(normalized.model, "gpt-4");
|
|
assert.equal(normalized.provider, "openai");
|
|
assert.equal(normalized.status, 0);
|
|
assert.ok(normalized.duration >= 5000);
|
|
assert.ok(normalized.duration < 60000);
|
|
assert.equal(normalized.tokens, null);
|
|
assert.equal(normalized.cacheSource, null);
|
|
assert.equal(normalized.stage, "streaming");
|
|
});
|
|
|
|
// ─── requestLoggerSignature module ────────────────────────────────────────
|
|
|
|
const sigMod = await import("../../src/shared/components/requestLoggerSignature.ts");
|
|
|
|
test("resolveInitialVisibility returns true when document is undefined (SSR)", () => {
|
|
assert.equal(sigMod.resolveInitialVisibility(), true);
|
|
});
|
|
|
|
test("shouldAutoRefresh returns true when recording and on first page", () => {
|
|
assert.equal(sigMod.shouldAutoRefresh(true, 50, 50), true);
|
|
assert.equal(sigMod.shouldAutoRefresh(true, 25, 50), true);
|
|
});
|
|
|
|
test("shouldAutoRefresh returns false when not recording or past first page", () => {
|
|
assert.equal(sigMod.shouldAutoRefresh(false, 50, 50), false);
|
|
assert.equal(sigMod.shouldAutoRefresh(true, 100, 50), false);
|
|
assert.equal(sigMod.shouldAutoRefresh(false, 100, 50), false);
|
|
});
|
|
|
|
test("computeLogsSignature produces different signatures for different data", () => {
|
|
const a = sigMod.computeLogsSignature([
|
|
{ id: "1", status: 200, duration: 100, tokens: { out: 50 } },
|
|
]);
|
|
const b = sigMod.computeLogsSignature([
|
|
{ id: "1", status: 200, duration: 150, tokens: { out: 50 } },
|
|
]);
|
|
assert.notEqual(a, b, "different duration should change signature");
|
|
});
|
|
|
|
test("computeLogsSignature returns empty string for non-array input", () => {
|
|
assert.equal(sigMod.computeLogsSignature(null), "");
|
|
assert.equal(sigMod.computeLogsSignature(undefined), "");
|
|
assert.equal(sigMod.computeLogsSignature({}), "");
|
|
});
|
|
|
|
// ─── Duration live computation ───────────────────────────────────────────
|
|
|
|
test("duration is computed from startedAt and grows over time", () => {
|
|
const startedAt = Date.now() - 3000;
|
|
const duration = Math.max(0, Date.now() - startedAt);
|
|
assert.ok(duration >= 3000, "duration should be at least 3s");
|
|
assert.ok(duration < 60000, "duration should be within reason");
|
|
});
|
|
|
|
// ─── Navigation logic ────────────────────────────────────────────────────
|
|
|
|
test("handlePrev at first item closes modal (no wrap-around)", () => {
|
|
const items = [
|
|
{ id: "a", active: false },
|
|
{ id: "b", active: false },
|
|
{ id: "c", active: false },
|
|
];
|
|
const selectedId = "a";
|
|
const idx = items.findIndex((l) => l.id === selectedId);
|
|
assert.equal(idx, 0);
|
|
|
|
// handlePrev should NOT wrap to last item
|
|
if (idx > 0) {
|
|
assert.equal(items[idx - 1].id, "should not reach");
|
|
} else {
|
|
// closes modal
|
|
assert.ok(true, "prev at first item closes modal");
|
|
}
|
|
});
|
|
|
|
test("handlePrev navigates backward when not at first item", () => {
|
|
const items = [
|
|
{ id: "a", active: false },
|
|
{ id: "b", active: false },
|
|
{ id: "c", active: false },
|
|
];
|
|
const selectedId = "b";
|
|
const idx = items.findIndex((l) => l.id === selectedId);
|
|
assert.equal(idx, 1);
|
|
|
|
if (idx > 0) {
|
|
assert.equal(items[idx - 1].id, "a", "should navigate to previous item");
|
|
}
|
|
});
|
|
|
|
test("handleNext at last item closes modal (no wrap-around)", () => {
|
|
const items = [
|
|
{ id: "a", active: false },
|
|
{ id: "b", active: false },
|
|
];
|
|
const selectedId = "b";
|
|
const idx = items.findIndex((l) => l.id === selectedId);
|
|
assert.equal(idx, items.length - 1);
|
|
|
|
// handleNext at last item should close
|
|
if (idx < items.length - 1) {
|
|
assert.equal(items[idx + 1].id, "should not reach");
|
|
} else {
|
|
assert.ok(true, "next at last item closes modal");
|
|
}
|
|
});
|
|
|
|
test("handleNext navigates forward when not at last item", () => {
|
|
const items = [
|
|
{ id: "a", active: false },
|
|
{ id: "b", active: false },
|
|
];
|
|
const selectedId = "a";
|
|
const idx = items.findIndex((l) => l.id === selectedId);
|
|
|
|
if (idx < items.length - 1) {
|
|
assert.equal(items[idx + 1].id, "b", "should navigate to next item");
|
|
}
|
|
});
|
|
|
|
// ─── Detail endpoint polling ─────────────────────────────────────────────
|
|
|
|
test("deep-linked missing request id is kept pending for /api/logs/[id] polling", () => {
|
|
const content = fs.readFileSync("src/shared/components/RequestLoggerV2.tsx", "utf8");
|
|
assert.match(content, /pendingLookup:\s*true/);
|
|
// A deep-linked id that 404s while still active is kept pending...
|
|
assert.match(content, /setDetailData\(\{\s*detailState:\s*"pending"\s*\}\)/);
|
|
// ...and the detail-polling effect re-runs on that pending state.
|
|
assert.match(content, /detailData\?\.detailState/);
|
|
assert.doesNotMatch(content, /activeRequests|completedRows|completedRow/);
|
|
});
|
|
|
|
// ─── End-to-end streamChunks integration ──────────────────────────────────
|
|
|
|
test("createRequestLogger with connectionId/model/provider populates streamChunks on pending request", async () => {
|
|
const { createRequestLogger } = await import("../../open-sse/utils/requestLogger.ts");
|
|
usageHistory.clearPendingRequests();
|
|
|
|
// track request so the pending detail entry exists
|
|
usageHistory.trackPendingRequest("gpt-4", "openai", "test-conn-1", true, {
|
|
clientRequest: { messages: [{ role: "user", content: "hi" }] },
|
|
});
|
|
|
|
const logger = await createRequestLogger("openai", "openai", "gpt-4", {
|
|
enabled: true,
|
|
captureStreamChunks: true,
|
|
connectionId: "test-conn-1",
|
|
model: "gpt-4",
|
|
provider: "openai",
|
|
});
|
|
|
|
// append a provider chunk — this should call pushStreamChunks() → updatePendingRequestStreamChunks()
|
|
logger.appendProviderChunk('data: {"content":"hello"}');
|
|
logger.appendProviderChunk('data: {"content":" world"}');
|
|
|
|
const pending = usageHistory.getPendingRequests();
|
|
const detail = pending.details["test-conn-1"]?.["gpt-4 (openai)"]?.[0];
|
|
|
|
assert.ok(detail, "pending request detail should exist");
|
|
assert.ok(detail.streamChunks, "streamChunks should be non-null after appendProviderChunk");
|
|
assert.ok(Array.isArray(detail.streamChunks.provider), "provider array should exist");
|
|
assert.equal(detail.streamChunks.provider.length, 2, "should have 2 provider chunks");
|
|
assert.equal(stripChunkTs(detail.streamChunks.provider[0]), 'data: {"content":"hello"}');
|
|
assert.equal(stripChunkTs(detail.streamChunks.provider[1]), 'data: {"content":" world"}');
|
|
assert.deepEqual(detail.streamChunks.openai, []);
|
|
assert.deepEqual(detail.streamChunks.client, []);
|
|
});
|
|
|
|
test("createRequestLogger requestId binds streamChunks to the exact pending request", async () => {
|
|
const { createRequestLogger } = await import("../../open-sse/utils/requestLogger.ts");
|
|
usageHistory.clearPendingRequests();
|
|
|
|
const firstId = usageHistory.trackPendingRequest("gpt-4", "openai", "test-conn-a", true);
|
|
const secondId = usageHistory.trackPendingRequest("gpt-4", "openai", "test-conn-b", true);
|
|
|
|
const logger = await createRequestLogger("openai", "openai", "gpt-4", {
|
|
enabled: true,
|
|
captureStreamChunks: true,
|
|
requestId: secondId,
|
|
});
|
|
|
|
logger.appendProviderChunk('data: {"content":"second"}');
|
|
|
|
const first = usageHistory.getPendingById().get(firstId);
|
|
const second = usageHistory.getPendingById().get(secondId);
|
|
|
|
assert.equal(first?.streamChunks, undefined);
|
|
assert.equal(stripChunkTs(second?.streamChunks?.provider[0]), 'data: {"content":"second"}');
|
|
});
|
|
|
|
test("createRequestLogger fallback match does not cross connectionId boundaries", async () => {
|
|
const { createRequestLogger } = await import("../../open-sse/utils/requestLogger.ts");
|
|
usageHistory.clearPendingRequests();
|
|
|
|
usageHistory.trackPendingRequest("gpt-4", "openai", "test-conn-a", true);
|
|
const secondId = usageHistory.trackPendingRequest("gpt-4", "openai", "test-conn-b", true);
|
|
|
|
const logger = await createRequestLogger("openai", "openai", "gpt-4", {
|
|
enabled: true,
|
|
captureStreamChunks: true,
|
|
connectionId: "test-conn-b",
|
|
model: "gpt-4",
|
|
provider: "openai",
|
|
});
|
|
|
|
logger.appendProviderChunk('data: {"content":"conn-b"}');
|
|
|
|
const second = usageHistory.getPendingById().get(secondId);
|
|
assert.equal(stripChunkTs(second?.streamChunks?.provider[0]), 'data: {"content":"conn-b"}');
|
|
});
|
|
|
|
test("createRequestLogger without connectionId does not populate streamChunks", async () => {
|
|
const { createRequestLogger } = await import("../../open-sse/utils/requestLogger.ts");
|
|
usageHistory.clearPendingRequests();
|
|
|
|
usageHistory.trackPendingRequest("gpt-4", "openai", "test-conn-2", true, {
|
|
clientRequest: { messages: [{ role: "user", content: "hi" }] },
|
|
});
|
|
|
|
// create logger WITHOUT connectionId/model/provider — pushStreamChunks will bail
|
|
const logger = await createRequestLogger("openai", "openai", "gpt-4", {
|
|
enabled: true,
|
|
captureStreamChunks: true,
|
|
// intentionally omit connectionId, model, provider
|
|
});
|
|
|
|
logger.appendProviderChunk('data: {"content":"hello"}');
|
|
|
|
const pending = usageHistory.getPendingRequests();
|
|
const detail = pending.details["test-conn-2"]?.["gpt-4 (openai)"]?.[0];
|
|
|
|
assert.ok(detail, "pending request detail should exist");
|
|
assert.equal(
|
|
detail.streamChunks,
|
|
undefined,
|
|
"streamChunks should be undefined when connectionId not provided to createRequestLogger"
|
|
);
|
|
});
|
|
|
|
test("createRequestLogger appendOpenAIChunk and appendConvertedChunk also populate streamChunks", async () => {
|
|
const { createRequestLogger } = await import("../../open-sse/utils/requestLogger.ts");
|
|
usageHistory.clearPendingRequests();
|
|
|
|
usageHistory.trackPendingRequest("claude-3", "anthropic", "test-conn-3", true);
|
|
|
|
const logger = await createRequestLogger("anthropic", "openai", "claude-3", {
|
|
enabled: true,
|
|
captureStreamChunks: true,
|
|
connectionId: "test-conn-3",
|
|
model: "claude-3",
|
|
provider: "anthropic",
|
|
});
|
|
|
|
logger.appendOpenAIChunk('data: {"role":"assistant","content":"hi"}');
|
|
logger.appendConvertedChunk('data: {"content":"there"}');
|
|
|
|
const pending = usageHistory.getPendingRequests();
|
|
const detail = pending.details["test-conn-3"]?.["claude-3 (anthropic)"]?.[0];
|
|
|
|
assert.ok(detail?.streamChunks, "streamChunks should be set");
|
|
assert.equal(detail.streamChunks.openai.length, 1);
|
|
assert.equal(
|
|
stripChunkTs(detail.streamChunks.openai[0]),
|
|
'data: {"role":"assistant","content":"hi"}'
|
|
);
|
|
assert.equal(detail.streamChunks.client.length, 1);
|
|
assert.equal(stripChunkTs(detail.streamChunks.client[0]), 'data: {"content":"there"}');
|
|
});
|
|
|
|
test("createRequestLogger captures stream chunks even when enabled: false", async () => {
|
|
const { createRequestLogger } = await import("../../open-sse/utils/requestLogger.ts");
|
|
usageHistory.clearPendingRequests();
|
|
|
|
usageHistory.trackPendingRequest("gpt-4", "openai", "test-conn-4", true);
|
|
|
|
// Logger is disabled (enabled: false) — previously this would return a
|
|
// no-op logger that didn't capture any chunks. Now stream chunks are
|
|
// always captured regardless of the enabled flag.
|
|
const logger = await createRequestLogger("openai", "openai", "gpt-4", {
|
|
enabled: false,
|
|
captureStreamChunks: true,
|
|
connectionId: "test-conn-4",
|
|
model: "gpt-4",
|
|
provider: "openai",
|
|
});
|
|
|
|
logger.appendProviderChunk('data: {"content":"hello"}');
|
|
|
|
const pending = usageHistory.getPendingRequests();
|
|
const detail = pending.details["test-conn-4"]?.["gpt-4 (openai)"]?.[0];
|
|
|
|
assert.ok(detail?.streamChunks, "streamChunks should be set even when logger is disabled");
|
|
assert.equal(detail.streamChunks.provider.length, 1);
|
|
assert.equal(stripChunkTs(detail.streamChunks.provider[0]), 'data: {"content":"hello"}');
|
|
|
|
// But getPipelinePayloads should return null when disabled
|
|
assert.equal(
|
|
logger.getPipelinePayloads(),
|
|
null,
|
|
"pipeline payloads should be null when disabled"
|
|
);
|
|
});
|
|
|
|
test("createRequestLogger disabled logger other methods are no-ops", async () => {
|
|
const { createRequestLogger } = await import("../../open-sse/utils/requestLogger.ts");
|
|
|
|
const logger = await createRequestLogger("openai", "openai", "gpt-4", {
|
|
enabled: false,
|
|
captureStreamChunks: false,
|
|
});
|
|
|
|
// These should not throw
|
|
logger.logClientRawRequest("/endpoint", { foo: "bar" });
|
|
logger.logOpenAIRequest({ model: "gpt-4" });
|
|
logger.logTargetRequest("https://api.openai.com", {}, { model: "gpt-4" });
|
|
logger.logProviderResponse(200, "OK", {}, {});
|
|
logger.logConvertedResponse({ choices: [] });
|
|
logger.logError(new Error("test"));
|
|
logger.appendProviderChunk("test");
|
|
logger.appendOpenAIChunk("test");
|
|
logger.appendConvertedChunk("test");
|
|
|
|
assert.equal(logger.getPipelinePayloads(), null);
|
|
});
|