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.
+ ) : (
+
+
+
+ | DateTime |
+ Model |
+ Provider |
+ Account |
+ In |
+ Out |
+ Status |
+
+
+
+ {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 (
+
+ | {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";