diff --git a/open-sse/handlers/chatCore.js b/open-sse/handlers/chatCore.js index fd95a37..6b07575 100644 --- a/open-sse/handlers/chatCore.js +++ b/open-sse/handlers/chatCore.js @@ -8,7 +8,7 @@ import { createRequestLogger } from "../utils/requestLogger.js"; import { getModelTargetFormat, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.js"; import { createErrorResult, parseUpstreamError, formatProviderError } from "../utils/error.js"; import { handleBypassRequest } from "../utils/bypassHandler.js"; -import { saveRequestUsage, trackPendingRequest } from "@/lib/usageDb.js"; +import { saveRequestUsage, trackPendingRequest, appendRequestLog } from "@/lib/usageDb.js"; /** * Extract usage from non-streaming response body @@ -125,6 +125,9 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred // Track pending request trackPendingRequest(model, provider, connectionId, true); + // Log start + appendRequestLog({ model, provider, connectionId, status: "PENDING" }).catch(() => {}); + // 2. Log converted request to provider reqLogger.logConvertedRequest(providerUrl, providerHeaders, translatedBody); @@ -159,6 +162,7 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred }); } catch (error) { trackPendingRequest(model, provider, connectionId, false); + appendRequestLog({ model, provider, connectionId, status: `FAILED ${error.name === "AbortError" ? 499 : 502}` }).catch(() => {}); if (error.name === "AbortError") { streamController.handleError(error); return createErrorResult(499, "Request aborted"); @@ -254,6 +258,7 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred if (!providerResponse.ok) { trackPendingRequest(model, provider, connectionId, false); const { statusCode, message } = await parseUpstreamError(providerResponse); + appendRequestLog({ model, provider, connectionId, status: `FAILED ${statusCode}` }).catch(() => {}); const errMsg = formatProviderError(new Error(message), provider, model, statusCode); console.log(`${COLORS.red}[ERROR] ${errMsg}${COLORS.reset}`); @@ -275,6 +280,7 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred // Log usage for non-streaming responses const usage = extractUsageFromResponse(responseBody, provider); + appendRequestLog({ model, provider, connectionId, tokens: usage, status: "200 OK" }).catch(() => {}); if (usage) { const msg = `[${new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" })}] 📊 [USAGE] ${provider.toUpperCase()} | in=${usage.prompt_tokens || 0} | out=${usage.completion_tokens || 0}${connectionId ? ` | account=${connectionId.slice(0, 8)}...` : ""}`; console.log(`${COLORS.green}${msg}${COLORS.reset}`); diff --git a/open-sse/utils/stream.js b/open-sse/utils/stream.js index 678a5e4..1975824 100644 --- a/open-sse/utils/stream.js +++ b/open-sse/utils/stream.js @@ -1,6 +1,6 @@ import { translateResponse, initState } from "../translator/index.js"; import { FORMATS } from "../translator/formats.js"; -import { saveRequestUsage, trackPendingRequest } from "@/lib/usageDb.js"; +import { saveRequestUsage, trackPendingRequest, appendRequestLog } from "@/lib/usageDb.js"; // Get HH:MM:SS timestamp function getTimeString() { @@ -66,6 +66,9 @@ function logUsage(provider, usage, model = null, connectionId = null) { console.log(`${COLORS.green}${msg}${COLORS.reset}`); + // Log to log.txt + appendRequestLog({ model, provider, connectionId, tokens: usage, status: "200 OK" }).catch(() => {}); + // Save to DB saveRequestUsage({ provider: provider || "unknown", diff --git a/src/app/(dashboard)/dashboard/providers/page.js b/src/app/(dashboard)/dashboard/providers/page.js index a99063c..32a23bb 100644 --- a/src/app/(dashboard)/dashboard/providers/page.js +++ b/src/app/(dashboard)/dashboard/providers/page.js @@ -1,13 +1,14 @@ "use client"; import { useState, useEffect } from "react"; -import { Card, CardSkeleton, Badge, UsageStats } from "@/shared/components"; +import { Card, CardSkeleton, Badge, UsageStats, RequestLogger } from "@/shared/components"; import { OAUTH_PROVIDERS, APIKEY_PROVIDERS } from "@/shared/constants/config"; import Link from "next/link"; import { getErrorCode, getRelativeTime } from "@/shared/utils"; export default function ProvidersPage() { const [activeTab, setActiveTab] = useState("connections"); + const [usageSubTab, setUsageSubTab] = useState("overview"); const [connections, setConnections] = useState([]); const [loading, setLoading] = useState(true); @@ -97,7 +98,27 @@ export default function ProvidersPage() { {activeTab === "usage" ? ( - +
+
+ + +
+ {usageSubTab === "overview" ? : } +
) : ( <> {/* OAuth Providers */} diff --git a/src/app/api/usage/logs/route.js b/src/app/api/usage/logs/route.js new file mode 100644 index 0000000..b5b875e --- /dev/null +++ b/src/app/api/usage/logs/route.js @@ -0,0 +1,12 @@ +import { NextResponse } from "next/server"; +import { getRecentLogs } from "@/lib/usageDb"; + +export async function GET() { + try { + const logs = await getRecentLogs(200); + return NextResponse.json(logs); + } catch (error) { + console.error("Error fetching logs:", error); + return NextResponse.json({ error: "Failed to fetch logs" }, { status: 500 }); + } +} diff --git a/src/lib/usageDb.js b/src/lib/usageDb.js index b3289b4..21afcb1 100644 --- a/src/lib/usageDb.js +++ b/src/lib/usageDb.js @@ -35,6 +35,7 @@ function getUserDataDir() { // Data file path - stored in user home directory const DATA_DIR = getUserDataDir(); const DB_FILE = path.join(DATA_DIR, "usage.json"); +const LOG_FILE = path.join(DATA_DIR, "log.txt"); // Ensure data directory exists if (!fs.existsSync(DATA_DIR)) { @@ -167,6 +168,67 @@ export async function getUsageHistory(filter = {}) { return history; } +/** + * Format date as dd-mm-yyyy h:m:s + */ +function formatLogDate(date = new Date()) { + const pad = (n) => String(n).padStart(2, "0"); + const d = pad(date.getDate()); + const m = pad(date.getMonth() + 1); + const y = date.getFullYear(); + const h = pad(date.getHours()); + const min = pad(date.getMinutes()); + const s = pad(date.getSeconds()); + return `${d}-${m}-${y} ${h}:${min}:${s}`; +} + +/** + * Append to log.txt + * Format: datetime(dd-mm-yyyy h:m:s) | model | provider | account | tokens sent | tokens received | status + */ +export async function appendRequestLog({ model, provider, connectionId, tokens, status }) { + try { + const timestamp = formatLogDate(); + const p = provider?.toUpperCase() || "-"; + const m = model || "-"; + + // Resolve account name + let account = connectionId ? connectionId.slice(0, 8) : "-"; + try { + const { getProviderConnections } = await import("@/lib/localDb.js"); + const connections = await getProviderConnections(); + const conn = connections.find(c => c.id === connectionId); + if (conn) { + account = conn.name || conn.email || account; + } + } catch {} + + const sent = tokens?.prompt_tokens !== undefined ? tokens.prompt_tokens : "-"; + const received = tokens?.completion_tokens !== undefined ? tokens.completion_tokens : "-"; + + const line = `${timestamp} | ${m} | ${p} | ${account} | ${sent} | ${received} | ${status}\n`; + + fs.appendFileSync(LOG_FILE, line); + } catch (error) { + console.error("Failed to append to log.txt:", error.message); + } +} + +/** + * Get last N lines of log.txt + */ +export async function getRecentLogs(limit = 200) { + if (!fs.existsSync(LOG_FILE)) return []; + try { + const content = fs.readFileSync(LOG_FILE, "utf-8"); + const lines = content.trim().split("\n"); + return lines.slice(-limit).reverse(); + } catch (error) { + console.error("Failed to read log.txt:", error.message); + return []; + } +} + /** * Get aggregated usage stats */ diff --git a/src/shared/components/RequestLogger.js b/src/shared/components/RequestLogger.js new file mode 100644 index 0000000..d0f493a --- /dev/null +++ b/src/shared/components/RequestLogger.js @@ -0,0 +1,124 @@ +"use client"; + +import { useState, useEffect } from "react"; +import Card from "./Card"; + +export default function RequestLogger() { + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(true); + const [autoRefresh, setAutoRefresh] = useState(true); + + useEffect(() => { + fetchLogs(); + }, []); + + useEffect(() => { + let interval; + if (autoRefresh) { + interval = setInterval(() => { + fetchLogs(false); + }, 500); + } + return () => clearInterval(interval); + }, [autoRefresh]); + + const fetchLogs = async (showLoading = true) => { + if (showLoading) setLoading(true); + try { + const res = await fetch("/api/usage/logs"); + if (res.ok) { + const data = await res.json(); + setLogs(data); + } + } catch (error) { + console.error("Failed to fetch logs:", error); + } finally { + if (showLoading) setLoading(false); + } + }; + + return ( +
+
+

Request Logs

+
+ +
+
+ + +
+ {loading && logs.length === 0 ? ( +
Loading logs...
+ ) : logs.length === 0 ? ( +
No logs recorded yet.
+ ) : ( + + + + + + + + + + + + + + {logs.map((log, i) => { + const parts = log.split(" | "); + if (parts.length < 7) return null; + + const status = parts[6]; + const isPending = status.includes("PENDING"); + const isFailed = status.includes("FAILED"); + const isSuccess = status.includes("OK"); + + return ( + + + + + + + + + + ); + })} + +
DateTimeModelProviderAccountInOutStatus
{parts[0]}{parts[1]} + + {parts[2]} + + {parts[3]}{parts[4]}{parts[5]} + {status} +
+ )} +
+
+
+ Logs are saved to log.txt in the application data directory. +
+
+ ); +} diff --git a/src/shared/components/index.js b/src/shared/components/index.js index 13c47d0..3e30a83 100644 --- a/src/shared/components/index.js +++ b/src/shared/components/index.js @@ -16,6 +16,7 @@ export { default as Footer } from "./Footer"; export { default as OAuthModal } from "./OAuthModal"; export { default as ModelSelectModal } from "./ModelSelectModal"; export { default as UsageStats } from "./UsageStats"; +export { default as RequestLogger } from "./RequestLogger"; // Layouts export * from "./layouts";