fix: add four-stage request log payloads

This commit is contained in:
R.D.
2026-03-28 07:32:40 -04:00
committed by diegosouzapw
parent 6ec8745d2e
commit 04de492019
18 changed files with 773 additions and 264 deletions

View File

@@ -1,10 +1,9 @@
/**
* GET /api/logs/detail — List detailed request logs
* GET /api/logs/detail/:id — Get specific detailed log
* POST /api/logs/detail/toggle — Enable/disable detailed logging
* GET /api/logs/detail — List detailed request logs + current enabled flag
* POST /api/logs/detail — Enable/disable detailed logging
*/
import { NextRequest, NextResponse } from "next/server";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import {
getRequestDetailLogs,
getRequestDetailLogCount,
@@ -15,9 +14,8 @@ import { updateSettings } from "@/lib/db/settings";
export const dynamic = "force-dynamic";
export async function GET(req: NextRequest) {
if (!isAuthenticated(req)) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const authError = await requireManagementAuth(req);
if (authError) return authError;
const url = new URL(req.url);
const limit = Math.min(Number(url.searchParams.get("limit") ?? 50), 200);
@@ -31,9 +29,8 @@ export async function GET(req: NextRequest) {
}
export async function POST(req: NextRequest) {
if (!isAuthenticated(req)) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const authError = await requireManagementAuth(req);
if (authError) return authError;
const body = await req.json();
const enabled = body.enabled === true || body.enabled === "1";

View File

@@ -1,8 +1,12 @@
import { NextResponse } from "next/server";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { getCallLogById } from "@/lib/usageDb";
export async function GET(request, { params }) {
try {
const authError = await requireManagementAuth(request);
if (authError) return authError;
const { id } = await params;
const log = await getCallLogById(id);

View File

@@ -1,8 +1,12 @@
import { NextResponse } from "next/server";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { getCallLogs } from "@/lib/usageDb";
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> = {};

View File

@@ -1,5 +1,5 @@
import { CORS_ORIGIN, CORS_HEADERS } from "@/shared/utils/cors";
import { handleChat } from "@/sse/handlers/chat";
import { buildClientRawRequest, handleChat } from "@/sse/handlers/chat";
import { initTranslators } from "@omniroute/open-sse/translator/index.ts";
import { createInjectionGuard } from "@/middleware/promptInjectionGuard";
@@ -75,7 +75,7 @@ export async function POST(request: Request) {
headers: request.headers,
body: JSON.stringify(normalized),
});
return await handleChat(newRequest);
return await handleChat(newRequest, buildClientRawRequest(request, body));
}
}
} catch (error) {

View File

@@ -1,5 +1,5 @@
import { CORS_ORIGIN } from "@/shared/utils/cors";
import { handleChat } from "@/sse/handlers/chat";
import { buildClientRawRequest, handleChat } from "@/sse/handlers/chat";
import { initTranslators } from "@omniroute/open-sse/translator/index.ts";
import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
@@ -91,5 +91,5 @@ export async function POST(request, { params }) {
body: JSON.stringify(body),
});
return await handleChat(newRequest);
return await handleChat(newRequest, buildClientRawRequest(request, rawBody));
}

View File

@@ -1,5 +1,5 @@
import { CORS_ORIGIN } from "@/shared/utils/cors";
import { handleChat } from "@/sse/handlers/chat";
import { buildClientRawRequest, handleChat } from "@/sse/handlers/chat";
import { initTranslators } from "@omniroute/open-sse/translator/index.ts";
import { v1betaGeminiGenerateSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
@@ -87,7 +87,7 @@ export async function POST(request, { params }) {
body: JSON.stringify(convertedBody),
});
return await handleChat(newRequest);
return await handleChat(newRequest, buildClientRawRequest(request, rawBody));
} catch (error) {
console.log("Error handling Gemini request:", error);
return Response.json({ error: { message: error.message, code: 500 } }, { status: 500 });

View File

@@ -8,20 +8,28 @@
import { v4 as uuidv4 } from "uuid";
import { getDbInstance } from "./core";
import { getSettings } from "./settings";
import { isNoLog } from "../compliance";
import {
protectPayloadForLog,
serializePayloadForStorage,
parseStoredPayload,
} from "../logPayloads";
export interface RequestDetailLog {
id?: string;
call_log_id?: string | null;
timestamp?: string;
client_request?: string | null;
translated_request?: string | null;
provider_response?: string | null;
client_response?: string | null;
client_request?: unknown | null;
translated_request?: unknown | null;
provider_response?: unknown | null;
client_response?: unknown | null;
provider?: string | null;
model?: string | null;
source_format?: string | null;
target_format?: string | null;
duration_ms?: number;
api_key_id?: string | null;
no_log?: boolean;
}
/** Returns true if detailed logging is enabled in settings */
@@ -37,16 +45,14 @@ export async function isDetailedLoggingEnabled(): Promise<boolean> {
/** Save a detailed log entry — caller must verify isDetailedLoggingEnabled() first */
export function saveRequestDetailLog(entry: RequestDetailLog): void {
const noLogEnabled =
Boolean(entry.no_log) || (entry.api_key_id ? isNoLog(entry.api_key_id) : false);
if (noLogEnabled) return;
const db = getDbInstance();
const id = entry.id ?? uuidv4();
const timestamp = entry.timestamp ?? new Date().toISOString();
// Trim large bodies to avoid excessive disk usage (max 64KB each)
const trim = (s: string | null | undefined, max = 65536): string | null => {
if (!s) return null;
return s.length > max ? s.slice(0, max) + "…[truncated]" : s;
};
db.prepare(
`
INSERT INTO request_detail_logs
@@ -58,10 +64,10 @@ export function saveRequestDetailLog(entry: RequestDetailLog): void {
id,
entry.call_log_id ?? null,
timestamp,
trim(entry.client_request),
trim(entry.translated_request),
trim(entry.provider_response),
trim(entry.client_response),
serializePayloadForStorage(protectPayloadForLog(entry.client_request)),
serializePayloadForStorage(protectPayloadForLog(entry.translated_request)),
serializePayloadForStorage(protectPayloadForLog(entry.provider_response)),
serializePayloadForStorage(protectPayloadForLog(entry.client_response)),
entry.provider ?? null,
entry.model ?? null,
entry.source_format ?? null,
@@ -73,7 +79,7 @@ export function saveRequestDetailLog(entry: RequestDetailLog): void {
/** Fetch detailed logs (latest first) */
export function getRequestDetailLogs(limit = 50, offset = 0): RequestDetailLog[] {
const db = getDbInstance();
return db
const rows = db
.prepare(
`
SELECT * FROM request_detail_logs
@@ -81,14 +87,34 @@ export function getRequestDetailLogs(limit = 50, offset = 0): RequestDetailLog[]
LIMIT ? OFFSET ?
`
)
.all(limit, offset) as RequestDetailLog[];
.all(limit, offset) as Array<Record<string, unknown>>;
return rows.map(mapDetailedLogRow);
}
/** Get a single detailed log by ID */
export function getRequestDetailLogById(id: string): RequestDetailLog | null {
const db = getDbInstance();
return (db.prepare("SELECT * FROM request_detail_logs WHERE id = ?").get(id) ??
null) as RequestDetailLog | null;
const row = db.prepare("SELECT * FROM request_detail_logs WHERE id = ?").get(id) as
| Record<string, unknown>
| undefined;
return row ? mapDetailedLogRow(row) : null;
}
/** Get the most recent detailed log for a call log ID */
export function getRequestDetailLogByCallLogId(callLogId: string): RequestDetailLog | null {
const db = getDbInstance();
const row = db
.prepare(
`
SELECT * FROM request_detail_logs
WHERE call_log_id = ?
ORDER BY timestamp DESC
LIMIT 1
`
)
.get(callLogId) as Record<string, unknown> | undefined;
return row ? mapDetailedLogRow(row) : null;
}
/** Get total count of detailed logs */
@@ -99,3 +125,20 @@ export function getRequestDetailLogCount(): number {
};
return row?.cnt ?? 0;
}
function mapDetailedLogRow(row: Record<string, unknown>): RequestDetailLog {
return {
id: typeof row.id === "string" ? row.id : undefined,
call_log_id: typeof row.call_log_id === "string" ? row.call_log_id : null,
timestamp: typeof row.timestamp === "string" ? row.timestamp : undefined,
client_request: parseStoredPayload(row.client_request),
translated_request: parseStoredPayload(row.translated_request),
provider_response: parseStoredPayload(row.provider_response),
client_response: parseStoredPayload(row.client_response),
provider: typeof row.provider === "string" ? row.provider : null,
model: typeof row.model === "string" ? row.model : null,
source_format: typeof row.source_format === "string" ? row.source_format : null,
target_format: typeof row.target_format === "string" ? row.target_format : null,
duration_ms: typeof row.duration_ms === "number" ? row.duration_ms : 0,
};
}

109
src/lib/logPayloads.ts Normal file
View File

@@ -0,0 +1,109 @@
import { sanitizePII } from "./piiSanitizer";
const SENSITIVE_KEYS = new Set([
"api_key",
"apiKey",
"api-key",
"authorization",
"Authorization",
"x-api-key",
"X-Api-Key",
"access_token",
"accessToken",
"refresh_token",
"refreshToken",
"password",
"secret",
"token",
]);
type JsonRecord = Record<string, unknown>;
export function cloneLogPayload<T>(value: T): T {
if (value === null || value === undefined) return value;
if (typeof globalThis.structuredClone === "function") {
return globalThis.structuredClone(value);
}
return JSON.parse(JSON.stringify(value)) as T;
}
export function normalizePayloadForLog(payload: unknown): unknown {
if (typeof payload !== "string") return payload;
const trimmed = payload.trim();
if (!trimmed) return "";
try {
return JSON.parse(trimmed);
} catch {
return { _rawText: payload };
}
}
export function redactPayload(payload: unknown): unknown {
if (!payload || typeof payload !== "object") return payload;
if (Array.isArray(payload)) return payload.map(redactPayload);
const redacted: JsonRecord = {};
for (const [key, value] of Object.entries(payload)) {
if (SENSITIVE_KEYS.has(key)) {
redacted[key] = "[REDACTED]";
} else if (typeof value === "string" && value.startsWith("Bearer ")) {
redacted[key] = "Bearer [REDACTED]";
} else if (typeof value === "object" && value !== null) {
redacted[key] = redactPayload(value);
} else {
redacted[key] = value;
}
}
return redacted;
}
export function sanitizePayloadPII(payload: unknown): unknown {
if (typeof payload === "string") {
return sanitizePII(payload).text;
}
if (Array.isArray(payload)) {
return payload.map(sanitizePayloadPII);
}
if (!payload || typeof payload !== "object") {
return payload;
}
const sanitized: JsonRecord = {};
for (const [key, value] of Object.entries(payload)) {
sanitized[key] = sanitizePayloadPII(value);
}
return sanitized;
}
export function protectPayloadForLog(payload: unknown): unknown {
if (payload === null || payload === undefined) return null;
const normalized = normalizePayloadForLog(payload);
const piiSanitized = sanitizePayloadPII(normalized);
return redactPayload(piiSanitized);
}
export function serializePayloadForStorage(payload: unknown, maxLength = 65536): string | null {
if (payload === null || payload === undefined) return null;
const exact = JSON.stringify(payload);
if (exact.length <= maxLength) {
return exact;
}
return JSON.stringify({
_truncated: true,
_originalSize: exact.length,
_preview: exact.slice(0, maxLength),
});
}
export function parseStoredPayload(value: unknown): unknown | null {
if (typeof value !== "string" || value.trim().length === 0) return null;
try {
return JSON.parse(value);
} catch {
return { _rawText: value };
}
}

View File

@@ -11,10 +11,12 @@ import path from "path";
import fs from "fs";
import { getDbInstance } from "../db/core";
import { getSettings } from "../db/settings";
import { getRequestDetailLogByCallLogId } from "../db/detailedLogs";
import { shouldPersistToDisk, CALL_LOGS_DIR } from "./migrations";
import { getLoggedInputTokens, getLoggedOutputTokens } from "./tokenAccounting";
import { isNoLog } from "../compliance";
import { sanitizePII } from "../piiSanitizer";
import { protectPayloadForLog, parseStoredPayload } from "../logPayloads";
type JsonRecord = Record<string, unknown>;
@@ -35,15 +37,6 @@ function toStringOrNull(value: unknown): string | null {
return typeof value === "string" ? value : null;
}
function parseJsonString(value: unknown): unknown | null {
if (typeof value !== "string" || value.trim().length === 0) return null;
try {
return JSON.parse(value);
} catch {
return null;
}
}
function hasTruncatedFlag(value: unknown): boolean {
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
return (value as Record<string, unknown>)._truncated === true;
@@ -108,80 +101,6 @@ export function invalidateCallLogsMaxCache(): void {
expiresAt: 0,
};
}
/** Fields that should always be redacted from logged payloads */
const SENSITIVE_KEYS = new Set([
"api_key",
"apiKey",
"api-key",
"authorization",
"Authorization",
"x-api-key",
"X-Api-Key",
"access_token",
"accessToken",
"refresh_token",
"refreshToken",
"password",
"secret",
"token",
]);
/**
* Redact sensitive fields from a payload before persistence.
*/
function redactPayload(obj: any): any {
if (!obj || typeof obj !== "object") return obj;
if (Array.isArray(obj)) return obj.map(redactPayload);
const redacted: Record<string, any> = {};
for (const [key, value] of Object.entries(obj)) {
if (SENSITIVE_KEYS.has(key)) {
redacted[key] = "[REDACTED]";
} else if (typeof value === "string" && value.startsWith("Bearer ")) {
redacted[key] = "Bearer [REDACTED]";
} else if (typeof value === "object" && value !== null) {
redacted[key] = redactPayload(value);
} else {
redacted[key] = value;
}
}
return redacted;
}
/**
* Recursively sanitize PII from string fields in a payload.
* Uses lib/piiSanitizer config flags to determine if redaction is enabled.
*/
function sanitizePayloadPII(obj: any): any {
if (typeof obj === "string") {
return sanitizePII(obj).text;
}
if (Array.isArray(obj)) {
return obj.map(sanitizePayloadPII);
}
if (!obj || typeof obj !== "object") {
return obj;
}
const sanitized: Record<string, any> = {};
for (const [key, value] of Object.entries(obj)) {
sanitized[key] = sanitizePayloadPII(value);
}
return sanitized;
}
/**
* Apply payload protection chain before persistence.
* 1) Optional PII sanitization
* 2) Mandatory key/token redaction
*/
function protectPayloadForLog(payload: any): any {
if (!payload || !shouldLogPayloadInDb) return null;
const piiSanitized = sanitizePayloadPII(payload);
return redactPayload(piiSanitized);
}
let logIdCounter = 0;
function generateLogId() {
logIdCounter++;
@@ -198,8 +117,10 @@ export async function saveCallLog(entry: any) {
const apiKeyId = entry.apiKeyId || null;
const noLogEnabled = Boolean(entry.noLog) || (apiKeyId ? isNoLog(apiKeyId) : false);
const protectedRequestBody = noLogEnabled ? null : protectPayloadForLog(entry.requestBody);
const protectedResponseBody = noLogEnabled ? null : protectPayloadForLog(entry.responseBody);
const protectedRequestBody =
noLogEnabled || !shouldLogPayloadInDb ? null : protectPayloadForLog(entry.requestBody);
const protectedResponseBody =
noLogEnabled || !shouldLogPayloadInDb ? null : protectPayloadForLog(entry.responseBody);
// Resolve account name
let account = entry.connectionId ? entry.connectionId.slice(0, 8) : "-";
@@ -227,7 +148,7 @@ export async function saveCallLog(entry: any) {
};
const logEntry = {
id: generateLogId(),
id: typeof entry.id === "string" && entry.id.length > 0 ? entry.id : generateLogId(),
timestamp: new Date().toISOString(),
method: entry.method || "POST",
path: entry.path || "/v1/chat/completions",
@@ -470,8 +391,8 @@ export async function getCallLogById(id: string) {
apiKeyId: toStringOrNull(entryRow.api_key_id),
apiKeyName: toStringOrNull(entryRow.api_key_name),
comboName: toStringOrNull(entryRow.combo_name),
requestBody: parseJsonString(entryRow.request_body),
responseBody: parseJsonString(entryRow.response_body),
requestBody: parseStoredPayload(entryRow.request_body),
responseBody: parseStoredPayload(entryRow.response_body),
error: toStringOrNull(entryRow.error),
};
@@ -492,7 +413,20 @@ export async function getCallLogById(id: string) {
}
}
return entry;
const detailed = getRequestDetailLogByCallLogId(id);
if (!detailed) {
return entry;
}
return {
...entry,
pipelinePayloads: {
clientRequest: detailed.client_request ?? null,
providerRequest: detailed.translated_request ?? null,
providerResponse: detailed.provider_response ?? null,
clientResponse: detailed.client_response ?? null,
},
};
}
/**

View File

@@ -80,8 +80,42 @@ export default function RequestLoggerDetail({ log, detail, loading, onClose, onC
}
};
const requestJson = detail?.requestBody ? JSON.stringify(detail.requestBody, null, 2) : null;
const responseJson = detail?.responseBody ? JSON.stringify(detail.responseBody, null, 2) : null;
const toPrettyJson = (payload) => {
if (payload === null || payload === undefined) return null;
try {
return JSON.stringify(payload, null, 2);
} catch {
return String(payload);
}
};
const pipelinePayloads = detail?.pipelinePayloads || null;
const payloadSections = pipelinePayloads
? [
{
key: "client-request",
title: "Client Request",
json: toPrettyJson(pipelinePayloads.clientRequest),
},
{
key: "provider-request",
title: "Provider Request",
json: toPrettyJson(pipelinePayloads.providerRequest),
},
{
key: "provider-response",
title: "Provider Response",
json: toPrettyJson(pipelinePayloads.providerResponse),
},
{
key: "client-response",
title: "Client Response",
json: toPrettyJson(pipelinePayloads.clientResponse),
},
].filter((section) => section.json)
: [];
const requestJson = detail?.requestBody ? toPrettyJson(detail.requestBody) : null;
const responseJson = detail?.responseBody ? toPrettyJson(detail.responseBody) : null;
return (
<div
@@ -240,33 +274,41 @@ export default function RequestLoggerDetail({ log, detail, loading, onClose, onC
</div>
) : (
<>
{/* Response Payload (返回) — show first */}
{responseJson && (
{payloadSections.length > 0 &&
payloadSections.map((section) => (
<PayloadSection
key={section.key}
title={section.title}
json={section.json}
onCopy={() => onCopy(section.json)}
/>
))}
{payloadSections.length === 0 && responseJson && (
<PayloadSection
title="Response Payload (返回)"
title="Response Payload (Legacy)"
json={responseJson}
onCopy={() => onCopy(responseJson)}
/>
)}
{/* Request Payload (请求) */}
{requestJson && (
{payloadSections.length === 0 && requestJson && (
<PayloadSection
title="Request Payload (请求)"
title="Request Payload (Legacy)"
json={requestJson}
onCopy={() => onCopy(requestJson)}
/>
)}
{!requestJson && !responseJson && !loading && (
{payloadSections.length === 0 && !requestJson && !responseJson && !loading && (
<div className="p-6 text-center text-text-muted">
<span className="material-symbols-outlined text-[32px] mb-2 block opacity-40">
info
</span>
<p className="text-sm">No payload data available for this log entry.</p>
<p className="text-xs mt-1">
Request/response bodies are only captured for non-streaming calls or when
streaming completes normally.
Enable detailed logging first if you want the four-stage client/provider payload
view for new requests.
</p>
</div>
)}

View File

@@ -93,6 +93,9 @@ export default function RequestLoggerV2() {
const [selectedLog, setSelectedLog] = useState(null);
const [detailLoading, setDetailLoading] = useState(false);
const [detailData, setDetailData] = useState(null);
const [detailLoggingEnabled, setDetailLoggingEnabled] = useState(false);
const [detailLoggingLoading, setDetailLoggingLoading] = useState(false);
const [detailLoggingReady, setDetailLoggingReady] = useState(false);
const intervalRef = useRef(null);
const hasLoadedRef = useRef(false);
const [providerNodes, setProviderNodes] = useState([]);
@@ -161,6 +164,20 @@ export default function RequestLoggerV2() {
.catch(() => {});
}, []);
useEffect(() => {
fetch("/api/logs/detail?limit=1")
.then(async (res) => {
if (!res.ok) return null;
return await res.json();
})
.then((data) => {
if (!data) return;
setDetailLoggingEnabled(data.enabled === true);
setDetailLoggingReady(true);
})
.catch(() => {});
}, []);
// Auto-refresh
useEffect(() => {
if (intervalRef.current) clearInterval(intervalRef.current);
@@ -232,6 +249,25 @@ export default function RequestLoggerV2() {
setDetailData(null);
};
const toggleDetailLogging = async () => {
setDetailLoggingLoading(true);
try {
const nextEnabled = !detailLoggingEnabled;
const res = await fetch("/api/logs/detail", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ enabled: nextEnabled }),
});
if (!res.ok) throw new Error("Failed to update detailed logging");
setDetailLoggingEnabled(nextEnabled);
setDetailLoggingReady(true);
} catch (error) {
console.error("Failed to toggle detailed logging:", error);
} finally {
setDetailLoggingLoading(false);
}
};
// Unique accounts and providers for dropdowns
const uniqueAccounts = [...new Set(logs.map((l) => l.account).filter((a) => a && a !== "-"))];
@@ -271,6 +307,33 @@ export default function RequestLoggerV2() {
{recording ? "Recording" : "Paused"}
</button>
<button
onClick={toggleDetailLogging}
disabled={detailLoggingLoading}
className={`flex items-center gap-2 px-3 py-1.5 rounded-full text-sm font-medium border transition-colors disabled:opacity-60 ${
detailLoggingEnabled
? "bg-amber-500/10 border-amber-500/30 text-amber-700 dark:text-amber-300"
: "bg-bg-subtle border-border text-text-muted"
}`}
title="Capture four-stage pipeline payloads for new requests"
>
<span
className={`w-2 h-2 rounded-full ${detailLoggingEnabled ? "bg-amber-500" : "bg-text-muted"}`}
/>
{detailLoggingLoading
? "Updating detailed logs..."
: detailLoggingEnabled
? "Detailed Logs On"
: "Detailed Logs Off"}
</button>
{detailLoggingReady && (
<span className="text-[11px] text-text-muted">
New requests will {detailLoggingEnabled ? "" : "not "}capture client/provider pipeline
payloads.
</span>
)}
{/* Search */}
<div className="flex-1 min-w-[200px] relative">
<span className="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-text-muted text-[18px]">

View File

@@ -44,6 +44,7 @@ import { RequestTelemetry, recordTelemetry } from "../../shared/utils/requestTel
import { generateRequestId } from "../../shared/utils/requestId";
import { logAuditEvent } from "../../lib/compliance/index";
import { enforceApiKeyPolicy } from "../../shared/utils/apiKeyPolicy";
import { cloneLogPayload } from "@/lib/logPayloads";
import {
applyTaskAwareRouting,
getTaskRoutingConfig,
@@ -81,6 +82,13 @@ export async function handleChat(request: any, clientRawRequest: any = null) {
return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid JSON body");
}
const rawClientBody = cloneLogPayload(body);
// Build clientRawRequest for logging (if not provided)
if (!clientRawRequest) {
clientRawRequest = buildClientRawRequest(request, rawClientBody);
}
// FASE-01: Input sanitization — prompt injection detection & PII redaction
telemetry.startPhase("validate");
const sanitizeResult = sanitizeRequest(body, log as any);
@@ -113,16 +121,6 @@ export async function handleChat(request: any, clientRawRequest: any = null) {
);
}
// Build clientRawRequest for logging (if not provided)
if (!clientRawRequest) {
const url = new URL(request.url);
clientRawRequest = {
endpoint: url.pathname,
body,
headers: Object.fromEntries(request.headers.entries()),
};
}
// Log request endpoint and model
const url = new URL(request.url);
const modelStr = body.model;
@@ -344,6 +342,15 @@ export async function handleChat(request: any, clientRawRequest: any = null) {
return withSessionHeader(response, sessionId);
}
export function buildClientRawRequest(request: Request, body: unknown) {
const url = new URL(request.url);
return {
endpoint: url.pathname,
body: cloneLogPayload(body),
headers: Object.fromEntries(request.headers.entries()),
};
}
/**
* Handle single model chat request
*