diff --git a/open-sse/handlers/chatCore.js b/open-sse/handlers/chatCore.js
index 914a822..f502c4b 100644
--- a/open-sse/handlers/chatCore.js
+++ b/open-sse/handlers/chatCore.js
@@ -10,7 +10,7 @@ import { getModelTargetFormat, PROVIDER_ID_TO_ALIAS } from "../config/providerMo
import { createErrorResult, parseUpstreamError, formatProviderError } from "../utils/error.js";
import { HTTP_STATUS } from "../config/constants.js";
import { handleBypassRequest } from "../utils/bypassHandler.js";
-import { saveRequestUsage, trackPendingRequest, appendRequestLog, saveRequestDetail } from "@/lib/usageDb.js";
+import { saveRequestUsage, trackPendingRequest, appendRequestLog } from "@/lib/usageDb.js";
import { getExecutor } from "../executors/index.js";
/**
@@ -225,38 +225,6 @@ function extractUsageFromResponse(responseBody, provider) {
return null;
}
-/**
- * Extract full request configuration from body
- * Captures all relevant parameters for request details
- */
-function extractRequestConfig(body, stream) {
- const config = {
- messages: body.messages || [],
- model: body.model,
- stream: stream
- };
-
- // Add all optional configuration parameters
- const optionalParams = [
- 'temperature', 'top_p', 'top_k',
- 'max_tokens', 'max_completion_tokens',
- 'thinking', 'reasoning', 'enable_thinking',
- 'presence_penalty', 'frequency_penalty',
- 'seed', 'stop', 'tools', 'tool_choice',
- 'response_format', 'prediction', 'store', 'metadata',
- 'n', 'logprobs', 'top_logprobs', 'logit_bias',
- 'user', 'parallel_tool_calls'
- ];
-
- for (const param of optionalParams) {
- if (body[param] !== undefined) {
- config[param] = body[param];
- }
- }
-
- return config;
-}
-
/**
* Convert OpenAI-style SSE chunks into a single non-streaming JSON response.
* Used as a fallback when upstream returns text/event-stream for stream=false.
@@ -347,7 +315,6 @@ function parseSSEToOpenAIResponse(rawSSE, fallbackModel) {
*/
export async function handleChatCore({ body, modelInfo, credentials, log, onCredentialsRefreshed, onRequestSuccess, onDisconnect, clientRawRequest, connectionId, userAgent }) {
const { provider, model } = modelInfo;
- const requestStartTime = Date.now();
const sourceFormat = detectFormat(body);
@@ -440,26 +407,6 @@ 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 : HTTP_STATUS.BAD_GATEWAY}` }).catch(() => { });
-
- const errorDetail = {
- provider: provider || "unknown",
- model: model || "unknown",
- connectionId: connectionId || undefined,
- timestamp: new Date().toISOString(),
- latency: { ttft: 0, total: Date.now() - requestStartTime },
- tokens: { prompt_tokens: 0, completion_tokens: 0 },
- request: extractRequestConfig(body, stream),
- providerRequest: translatedBody || null,
- providerResponse: null,
- response: {
- error: error.message || String(error),
- status: error.name === "AbortError" ? 499 : 502,
- thinking: null
- },
- status: "error"
- };
- saveRequestDetail(errorDetail).catch(() => {});
-
if (error.name === "AbortError") {
streamController.handleError(error);
return createErrorResult(499, "Request aborted");
@@ -516,26 +463,6 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
trackPendingRequest(model, provider, connectionId, false);
const { statusCode, message, retryAfterMs } = await parseUpstreamError(providerResponse, provider);
appendRequestLog({ model, provider, connectionId, status: `FAILED ${statusCode}` }).catch(() => { });
-
- const errorDetail = {
- provider: provider || "unknown",
- model: model || "unknown",
- connectionId: connectionId || undefined,
- timestamp: new Date().toISOString(),
- latency: { ttft: 0, total: Date.now() - requestStartTime },
- tokens: { prompt_tokens: 0, completion_tokens: 0 },
- request: extractRequestConfig(body, stream),
- providerRequest: finalBody || translatedBody || null,
- providerResponse: null,
- response: {
- error: message,
- status: statusCode,
- thinking: null
- },
- status: "error"
- };
- saveRequestDetail(errorDetail).catch(() => {});
-
const errMsg = formatProviderError(new Error(message), provider, model, statusCode);
console.log(`${COLORS.red}[ERROR] ${errMsg}${COLORS.reset}`);
@@ -604,37 +531,6 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
translatedResponse.usage = filterUsageForFormat(buffered, sourceFormat);
}
- const totalLatency = Date.now() - requestStartTime;
- const requestDetail = {
- provider: provider || "unknown",
- model: model || "unknown",
- connectionId: connectionId || undefined,
- timestamp: new Date().toISOString(),
- latency: {
- ttft: totalLatency,
- total: totalLatency
- },
- tokens: usage || { prompt_tokens: 0, completion_tokens: 0 },
- request: extractRequestConfig(body, stream),
- providerRequest: finalBody || translatedBody || null,
- providerResponse: responseBody || null,
- response: {
- content: translatedResponse?.choices?.[0]?.message?.content ||
- translatedResponse?.content ||
- null,
- thinking: translatedResponse?.choices?.[0]?.message?.reasoning_content ||
- translatedResponse?.reasoning_content ||
- null,
- finish_reason: translatedResponse?.choices?.[0]?.finish_reason || "unknown"
- },
- status: "success"
- };
-
- // Async save (don't block response)
- saveRequestDetail(requestDetail).catch(err => {
- console.error("[RequestDetail] Failed to save:", err.message);
- });
-
return {
success: true,
response: new Response(JSON.stringify(translatedResponse), {
@@ -660,103 +556,31 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
"Access-Control-Allow-Origin": "*"
};
- let streamContent = "";
- let streamUsage = null;
- const streamDetailId = `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
-
- const onStreamComplete = (contentObj, usage, ttftAt) => {
- // contentObj is object { content, thinking }
- streamUsage = usage;
-
- const updatedDetail = {
- provider: provider || "unknown",
- model: model || "unknown",
- connectionId: connectionId || undefined,
- timestamp: new Date().toISOString(),
- latency: {
- ttft: ttftAt ? ttftAt - requestStartTime : Date.now() - requestStartTime,
- total: Date.now() - requestStartTime
- },
- tokens: usage || { prompt_tokens: 0, completion_tokens: 0 },
- request: extractRequestConfig(body, stream),
- providerRequest: finalBody || translatedBody || null,
- providerResponse: contentObj.content || "[Empty streaming response]",
- response: {
- content: contentObj.content || "[Empty streaming response]",
- thinking: contentObj.thinking || null,
- type: "streaming"
- },
- status: "success",
- id: streamDetailId
- };
-
- saveRequestDetail(updatedDetail).catch(err => {
- console.error("[RequestDetail] Failed to update streaming content:", err.message);
- });
-
- // Save usage stats for dashboard
- if (usage && typeof usage === 'object') {
- const msg = `[${new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" })}] 📊 [STREAM 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}`);
-
- saveRequestUsage({
- provider: provider || "unknown",
- model: model || "unknown",
- tokens: usage,
- timestamp: new Date().toISOString(),
- connectionId: connectionId || undefined
- }).catch(err => {
- console.error("Failed to save streaming usage stats:", err.message);
- });
- }
- };
-
+ // Create transform stream with logger for streaming response
let transformStream;
+ // For Codex provider, translate response from openai-responses to openai (Chat Completions) format
+ // UNLESS client is Droid CLI which expects openai-responses format back
const isDroidCLI = userAgent?.toLowerCase().includes('droid') || userAgent?.toLowerCase().includes('codex-cli');
const needsCodexTranslation = provider === 'codex'
&& targetFormat === 'openai-responses'
&& !isDroidCLI;
if (needsCodexTranslation) {
+ // Codex returns openai-responses, translate to openai (Chat Completions) that clients expect
log?.debug?.("STREAM", `Codex translation mode: openai-responses → openai`);
- transformStream = createSSETransformStreamWithLogger('openai-responses', 'openai', provider, reqLogger, toolNameMap, model, connectionId, body, onStreamComplete);
+ transformStream = createSSETransformStreamWithLogger('openai-responses', 'openai', provider, reqLogger, toolNameMap, model, connectionId, body);
} else if (needsTranslation(targetFormat, sourceFormat)) {
+ // Standard translation for other providers
log?.debug?.("STREAM", `Translation mode: ${targetFormat} → ${sourceFormat}`);
- transformStream = createSSETransformStreamWithLogger(targetFormat, sourceFormat, provider, reqLogger, toolNameMap, model, connectionId, body, onStreamComplete);
+ transformStream = createSSETransformStreamWithLogger(targetFormat, sourceFormat, provider, reqLogger, toolNameMap, model, connectionId, body);
} else {
log?.debug?.("STREAM", `Standard passthrough mode`);
- transformStream = createPassthroughStreamWithLogger(provider, reqLogger, model, connectionId, body, onStreamComplete);
+ transformStream = createPassthroughStreamWithLogger(provider, reqLogger, model, connectionId, body);
}
+ // Pipe response through transform with disconnect detection
const transformedBody = pipeWithDisconnect(providerResponse, transformStream, streamController);
- const totalLatency = Date.now() - requestStartTime;
- const streamingDetail = {
- provider: provider || "unknown",
- model: model || "unknown",
- connectionId: connectionId || undefined,
- timestamp: new Date().toISOString(),
- latency: {
- ttft: 0,
- total: Date.now() - requestStartTime
- },
- tokens: { prompt_tokens: 0, completion_tokens: 0 },
- request: extractRequestConfig(body, stream),
- providerRequest: finalBody || translatedBody || null,
- providerResponse: "[Streaming - raw response not captured]",
- response: {
- content: "[Streaming in progress...]",
- thinking: null,
- type: "streaming"
- },
- status: "success",
- id: streamDetailId
- };
-
- saveRequestDetail(streamingDetail).catch(err => {
- console.error("[RequestDetail] Failed to save streaming request:", err.message);
- });
-
return {
success: true,
response: new Response(transformedBody, {
diff --git a/open-sse/translator/response/claude-to-openai.js b/open-sse/translator/response/claude-to-openai.js
index 0501393..aa8ccd6 100644
--- a/open-sse/translator/response/claude-to-openai.js
+++ b/open-sse/translator/response/claude-to-openai.js
@@ -64,7 +64,7 @@ export function claudeToOpenAIResponse(chunk, state) {
if (delta?.type === "text_delta" && delta.text) {
results.push(createChunk(state, { content: delta.text }));
} else if (delta?.type === "thinking_delta" && delta.thinking) {
- results.push(createChunk(state, { reasoning_content: delta.thinking }));
+ results.push(createChunk(state, { content: delta.thinking }));
} else if (delta?.type === "input_json_delta" && delta.partial_json) {
const toolCall = state.toolCalls.get(chunk.index);
if (toolCall) {
@@ -83,7 +83,7 @@ export function claudeToOpenAIResponse(chunk, state) {
case "content_block_stop": {
if (state.inThinkingBlock && chunk.index === state.currentBlockIndex) {
- results.push(createChunk(state, { reasoning_content: "" }));
+ results.push(createChunk(state, { content: "" }));
state.inThinkingBlock = false;
}
state.textBlockStarted = false;
diff --git a/open-sse/utils/stream.js b/open-sse/utils/stream.js
index 1e7938a..76f4e7a 100644
--- a/open-sse/utils/stream.js
+++ b/open-sse/utils/stream.js
@@ -28,7 +28,6 @@ const STREAM_MODE = {
* @param {string} options.model - Model name
* @param {string} options.connectionId - Connection ID for usage tracking
* @param {object} options.body - Request body (for input token estimation)
- * @param {function} options.onStreamComplete - Callback when stream completes (content, usage)
*/
export function createSSEStream(options = {}) {
const {
@@ -40,25 +39,20 @@ export function createSSEStream(options = {}) {
toolNameMap = null,
model = null,
connectionId = null,
- body = null,
- onStreamComplete = null
+ body = null
} = options;
let buffer = "";
let usage = null;
+ // State for translate mode
const state = mode === STREAM_MODE.TRANSLATE ? { ...initState(sourceFormat), provider, toolNameMap } : null;
+ // Track content length for usage estimation (both modes)
let totalContentLength = 0;
- let accumulatedContent = "";
- let accumulatedThinking = "";
- let ttftAt = null;
return new TransformStream({
transform(chunk, controller) {
- if (!ttftAt) {
- ttftAt = Date.now();
- }
const text = sharedDecoder.decode(chunk, { stream: true });
buffer += text;
reqLogger?.appendProviderChunk?.(text);
@@ -85,15 +79,9 @@ export function createSSEStream(options = {}) {
}
const delta = parsed.choices?.[0]?.delta;
- const content = delta?.content;
- const reasoning = delta?.reasoning_content;
+ const content = delta?.content || delta?.reasoning_content;
if (content && typeof content === "string") {
totalContentLength += content.length;
- accumulatedContent += content;
- }
- if (reasoning && typeof reasoning === "string") {
- totalContentLength += reasoning.length;
- accumulatedThinking += reasoning;
}
const extracted = extractUsage(parsed);
@@ -146,39 +134,30 @@ export function createSSEStream(options = {}) {
continue;
}
- // Claude format - content
+ // Track content length for estimation (from various formats)
+ // Include both regular content and reasoning/thinking content
+
+ // Claude format
if (parsed.delta?.text) {
totalContentLength += parsed.delta.text.length;
- accumulatedContent += parsed.delta.text;
}
- // Claude format - thinking
if (parsed.delta?.thinking) {
totalContentLength += parsed.delta.thinking.length;
- accumulatedThinking += parsed.delta.thinking;
}
- // OpenAI format - content
+ // OpenAI format
if (parsed.choices?.[0]?.delta?.content) {
totalContentLength += parsed.choices[0].delta.content.length;
- accumulatedContent += parsed.choices[0].delta.content;
}
- // OpenAI format - reasoning
if (parsed.choices?.[0]?.delta?.reasoning_content) {
totalContentLength += parsed.choices[0].delta.reasoning_content.length;
- accumulatedThinking += parsed.choices[0].delta.reasoning_content;
}
- // Gemini format
+ // Gemini format - may have multiple parts
if (parsed.candidates?.[0]?.content?.parts) {
for (const part of parsed.candidates[0].content.parts) {
if (part.text && typeof part.text === "string") {
totalContentLength += part.text.length;
- // Check if this is thinking content
- if (part.thought === true) {
- accumulatedThinking += part.text;
- } else {
- accumulatedContent += part.text;
- }
}
}
}
@@ -241,6 +220,7 @@ export function createSSEStream(options = {}) {
controller.enqueue(sharedEncoder.encode(output));
}
+ // Estimate usage if provider didn't return valid usage (PASSTHROUGH is always OpenAI format)
if (!hasValidUsage(usage) && totalContentLength > 0) {
usage = estimateUsage(body, totalContentLength, FORMATS.OPENAI);
}
@@ -250,21 +230,16 @@ export function createSSEStream(options = {}) {
} else {
appendRequestLog({ model, provider, connectionId, tokens: null, status: "200 OK" }).catch(() => { });
}
-
- if (onStreamComplete) {
- onStreamComplete({
- content: accumulatedContent,
- thinking: accumulatedThinking
- }, usage, ttftAt);
- }
return;
}
+ // Translate mode: process remaining buffer
if (buffer.trim()) {
const parsed = parseSSELine(buffer.trim());
if (parsed && !parsed.done) {
const translated = translateResponse(targetFormat, sourceFormat, parsed, state);
+ // Log OpenAI intermediate chunks
if (translated?._openaiIntermediate) {
for (const item of translated._openaiIntermediate) {
const openaiOutput = formatSSE(item, FORMATS.OPENAI);
@@ -282,8 +257,10 @@ export function createSSEStream(options = {}) {
}
}
+ // Flush remaining events (only once at stream end)
const flushed = translateResponse(targetFormat, sourceFormat, null, state);
+ // Log OpenAI intermediate chunks for flushed events
if (flushed?._openaiIntermediate) {
for (const item of flushed._openaiIntermediate) {
const openaiOutput = formatSSE(item, FORMATS.OPENAI);
@@ -299,10 +276,12 @@ export function createSSEStream(options = {}) {
}
}
+ // Send [DONE] and log usage
const doneOutput = "data: [DONE]\n\n";
reqLogger?.appendConvertedChunk?.(doneOutput);
controller.enqueue(sharedEncoder.encode(doneOutput));
+ // Estimate usage if provider didn't return valid usage (for translate mode)
if (!hasValidUsage(state?.usage) && totalContentLength > 0) {
state.usage = estimateUsage(body, totalContentLength, sourceFormat);
}
@@ -312,13 +291,6 @@ export function createSSEStream(options = {}) {
} else {
appendRequestLog({ model, provider, connectionId, tokens: null, status: "200 OK" }).catch(() => { });
}
-
- if (onStreamComplete) {
- onStreamComplete({
- content: accumulatedContent,
- thinking: accumulatedThinking
- }, state?.usage, ttftAt);
- }
} catch (error) {
console.log("Error in flush:", error);
}
@@ -326,7 +298,8 @@ export function createSSEStream(options = {}) {
});
}
-export function createSSETransformStreamWithLogger(targetFormat, sourceFormat, provider = null, reqLogger = null, toolNameMap = null, model = null, connectionId = null, body = null, onStreamComplete = null) {
+// Convenience functions for backward compatibility
+export function createSSETransformStreamWithLogger(targetFormat, sourceFormat, provider = null, reqLogger = null, toolNameMap = null, model = null, connectionId = null, body = null) {
return createSSEStream({
mode: STREAM_MODE.TRANSLATE,
targetFormat,
@@ -336,19 +309,17 @@ export function createSSETransformStreamWithLogger(targetFormat, sourceFormat, p
toolNameMap,
model,
connectionId,
- body,
- onStreamComplete
+ body
});
}
-export function createPassthroughStreamWithLogger(provider = null, reqLogger = null, model = null, connectionId = null, body = null, onStreamComplete = null) {
+export function createPassthroughStreamWithLogger(provider = null, reqLogger = null, model = null, connectionId = null, body = null) {
return createSSEStream({
mode: STREAM_MODE.PASSTHROUGH,
provider,
reqLogger,
model,
connectionId,
- body,
- onStreamComplete
+ body
});
}
diff --git a/open-sse/utils/usageTracking.js b/open-sse/utils/usageTracking.js
index 36054b3..b6646e2 100644
--- a/open-sse/utils/usageTracking.js
+++ b/open-sse/utils/usageTracking.js
@@ -312,11 +312,11 @@ export function logUsage(provider, usage, model = null, connectionId = null) {
// Save to usage DB
const tokens = {
- prompt_tokens: inTokens,
- completion_tokens: outTokens,
- cache_read_input_tokens: cacheRead || 0,
- cache_creation_input_tokens: cacheCreation || 0,
- reasoning_tokens: reasoning || 0
+ input: inTokens,
+ output: outTokens,
+ cacheRead: cacheRead || 0,
+ cacheCreation: cacheCreation || 0,
+ reasoning: reasoning || 0
};
saveRequestUsage({ model, provider, connectionId, tokens }).catch(() => { });
appendRequestLog({ model, provider, connectionId, tokens, status: "200 OK" }).catch(() => { });
diff --git a/src/app/(dashboard)/dashboard/profile/page.js b/src/app/(dashboard)/dashboard/profile/page.js
index 32bae26..fa6387c 100644
--- a/src/app/(dashboard)/dashboard/profile/page.js
+++ b/src/app/(dashboard)/dashboard/profile/page.js
@@ -110,24 +110,6 @@ export default function ProfilePage() {
}
};
- const updateObservabilitySetting = async (key, value) => {
- const numValue = parseInt(value);
- if (isNaN(numValue) || numValue < 1) return;
-
- try {
- const res = await fetch("/api/settings", {
- method: "PATCH",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ [key]: numValue }),
- });
- if (res.ok) {
- setSettings(prev => ({ ...prev, [key]: numValue }));
- }
- } catch (err) {
- console.error(`Failed to update ${key}:`, err);
- }
- };
-
return (
@@ -311,7 +293,6 @@ export default function ProfilePage() {
{["light", "dark", "system"].map((option) => (
setTheme(option)}
className={cn(
"flex items-center gap-2 px-4 py-2 rounded-md font-medium transition-all",
@@ -349,97 +330,6 @@ export default function ProfilePage() {
- {/* Observability Settings */}
-
-
-
- monitoring
-
-
Observability
-
-
-
-
-
Max Records
-
- Maximum request detail records to keep (older records are auto-deleted)
-
-
-
updateObservabilitySetting("observabilityMaxRecords", parseInt(e.target.value))}
- disabled={loading}
- className="w-28 text-center"
- />
-
-
-
-
-
Batch Size
-
- Number of items to accumulate before writing to database (higher = better performance)
-
-
-
updateObservabilitySetting("observabilityBatchSize", parseInt(e.target.value))}
- disabled={loading}
- className="w-28 text-center"
- />
-
-
-
-
-
Flush Interval (ms)
-
- Maximum time to wait before flushing buffer (prevents data loss during low traffic)
-
-
-
updateObservabilitySetting("observabilityFlushIntervalMs", parseInt(e.target.value))}
- disabled={loading}
- className="w-28 text-center"
- />
-
-
-
-
-
Max JSON Size (KB)
-
- Maximum size for each JSON field (request/response) before truncation
-
-
-
updateObservabilitySetting("observabilityMaxJsonSize", parseInt(e.target.value))}
- disabled={loading}
- className="w-28 text-center"
- />
-
-
-
- Current: Keeps {settings.observabilityMaxRecords || 1000} records, batches every {settings.observabilityBatchSize || 20} requests, max {settings.observabilityMaxJsonSize || 1024}KB per field
-
-
-
-
{/* App Info */}
{APP_CONFIG.name} v{APP_CONFIG.version}
diff --git a/src/app/(dashboard)/dashboard/usage/components/RequestDetailsTab.js b/src/app/(dashboard)/dashboard/usage/components/RequestDetailsTab.js
deleted file mode 100644
index 4f52736..0000000
--- a/src/app/(dashboard)/dashboard/usage/components/RequestDetailsTab.js
+++ /dev/null
@@ -1,425 +0,0 @@
-"use client";
-
-import { useState, useEffect, useCallback } from "react";
-import Card from "@/shared/components/Card";
-import Button from "@/shared/components/Button";
-import Drawer from "@/shared/components/Drawer";
-import Pagination from "@/shared/components/Pagination";
-import { cn } from "@/shared/utils/cn";
-import { AI_PROVIDERS, getProviderByAlias } from "@/shared/constants/providers";
-
-let providerNameCache = null;
-let providerNodesCache = null;
-
-async function fetchProviderNames() {
- if (providerNameCache && providerNodesCache) {
- return { providerNameCache, providerNodesCache };
- }
-
- const nodesRes = await fetch("/api/provider-nodes");
- const nodesData = await nodesRes.json();
- const nodes = nodesData.nodes || [];
- providerNodesCache = {};
-
- for (const node of nodes) {
- providerNodesCache[node.id] = node.name;
- }
-
- providerNameCache = {
- ...AI_PROVIDERS,
- ...providerNodesCache
- };
-
- return { providerNameCache, providerNodesCache };
-}
-
-function getProviderName(providerId, cache) {
- if (!providerId) return providerId;
- if (!cache) return providerId;
-
- const cached = cache[providerId];
-
- if (typeof cached === 'string') {
- return cached;
- }
-
- if (cached?.name) {
- return cached.name;
- }
-
- const providerConfig = getProviderByAlias(providerId) || AI_PROVIDERS[providerId];
- return providerConfig?.name || providerId;
-}
-
-function CollapsibleSection({ title, children, defaultOpen = false, icon = null }) {
- const [isOpen, setIsOpen] = useState(defaultOpen);
-
- return (
-
-
setIsOpen(!isOpen)}
- className="w-full flex items-center justify-between p-3 bg-black/[0.02] dark:bg-white/[0.02] hover:bg-black/[0.04] dark:hover:bg-white/[0.04] transition-colors"
- >
-
- {icon && {icon} }
- {title}
-
-
- chevron_right
-
-
-
- {isOpen && (
-
- {children}
-
- )}
-
- );
-}
-
-export default function RequestDetailsTab() {
- const [details, setDetails] = useState([]);
- const [pagination, setPagination] = useState({
- page: 1,
- pageSize: 20,
- totalItems: 0,
- totalPages: 0
- });
- const [loading, setLoading] = useState(false);
- const [selectedDetail, setSelectedDetail] = useState(null);
- const [isDrawerOpen, setIsDrawerOpen] = useState(false);
- const [providers, setProviders] = useState([]);
- const [providerNameCache, setProviderNameCache] = useState(null);
- const [filters, setFilters] = useState({
- provider: "",
- startDate: "",
- endDate: ""
- });
-
- const fetchProviders = useCallback(async () => {
- try {
- const res = await fetch("/api/usage/providers");
- const data = await res.json();
- setProviders(data.providers || []);
-
- const cache = await fetchProviderNames();
- setProviderNameCache(cache.providerNameCache);
- } catch (error) {
- console.error("Failed to fetch providers:", error);
- }
- }, []);
-
- const fetchDetails = useCallback(async () => {
- setLoading(true);
- try {
- const params = new URLSearchParams({
- page: pagination.page.toString(),
- pageSize: pagination.pageSize.toString()
- });
- if (filters.provider) params.append("provider", filters.provider);
- if (filters.startDate) params.append("startDate", filters.startDate);
- if (filters.endDate) params.append("endDate", filters.endDate);
-
- const res = await fetch(`/api/usage/request-details?${params}`);
- const data = await res.json();
-
- setDetails(data.details || []);
- setPagination(prev => ({ ...prev, ...data.pagination }));
- } catch (error) {
- console.error("Failed to fetch request details:", error);
- } finally {
- setLoading(false);
- }
- }, [pagination.page, pagination.pageSize, filters]);
-
- useEffect(() => {
- fetchProviders();
- }, [fetchProviders]);
-
- useEffect(() => {
- fetchDetails();
- }, [fetchDetails]);
-
- const handleViewDetail = (detail) => {
- setSelectedDetail(detail);
- setIsDrawerOpen(true);
- };
-
- const handlePageChange = (newPage) => {
- setPagination(prev => ({ ...prev, page: newPage }));
- };
-
- const handlePageSizeChange = (newPageSize) => {
- setPagination(prev => ({ ...prev, pageSize: newPageSize, page: 1 }));
- };
-
- const handleClearFilters = () => {
- setFilters({ provider: "", startDate: "", endDate: "" });
- };
-
- return (
-
-
-
-
- Provider
- setFilters({ ...filters, provider: e.target.value })}
- className={cn(
- "h-9 px-3 rounded-lg border border-black/10 dark:border-white/10 bg-surface",
- "text-sm text-text-main focus:outline-none focus:ring-2 focus:ring-primary/20",
- "cursor-pointer min-w-[150px]"
- )}
- >
- All Providers
- {providers.map((provider) => (
-
- {provider.name}
-
- ))}
-
-
-
-
- Start Date
- setFilters({ ...filters, startDate: e.target.value })}
- className={cn(
- "h-9 px-3 rounded-lg border border-black/10 dark:border-white/10 bg-surface",
- "text-sm text-text-main focus:outline-none focus:ring-2 focus:ring-primary/20"
- )}
- />
-
-
-
- End Date
- setFilters({ ...filters, endDate: e.target.value })}
- className={cn(
- "h-9 px-3 rounded-lg border border-black/10 dark:border-white/10 bg-surface",
- "text-sm text-text-main focus:outline-none focus:ring-2 focus:ring-primary/20"
- )}
- />
-
-
-
- Clear
-
- Clear Filters
-
-
-
-
-
-
-
-
-
-
- Timestamp
- Model
- Provider
- Input Tokens
- Output Tokens
- Latency
- Action
-
-
-
- {loading ? (
-
-
-
- progress_activity
- Loading...
-
-
-
- ) : details.length === 0 ? (
-
-
- No request details found
-
-
- ) : (
- details.map((detail, index) => (
-
-
- {new Date(detail.timestamp).toLocaleString()}
-
-
- {detail.model}
-
-
-
- {getProviderName(detail.provider, providerNameCache)}
-
-
-
- {detail.tokens?.prompt_tokens?.toLocaleString() || 0}
-
-
- {detail.tokens?.completion_tokens?.toLocaleString() || 0}
-
-
-
-
TTFT: {detail.latency?.ttft || 0}ms
-
Total: {detail.latency?.total || 0}ms
-
-
-
- handleViewDetail(detail)}
- >
- Detail
-
-
-
- ))
- )}
-
-
-
-
- {!loading && details.length > 0 && (
-
- )}
-
-
-
setIsDrawerOpen(false)}
- title="Request Details"
- width="lg"
- >
- {selectedDetail && (
-
-
-
- ID: {" "}
- {selectedDetail.id}
-
-
- Timestamp: {" "}
- {new Date(selectedDetail.timestamp).toLocaleString()}
-
-
- Provider: {" "}
- {getProviderName(selectedDetail.provider, providerNameCache)}
-
-
- Model: {" "}
- {selectedDetail.model}
-
-
- Status: {" "}
-
- {selectedDetail.status}
-
-
-
- Latency: {" "}
-
- TTFT {selectedDetail.latency?.ttft || 0}ms / Total {selectedDetail.latency?.total || 0}ms
-
-
-
- Input Tokens: {" "}
-
- {selectedDetail.tokens?.prompt_tokens?.toLocaleString() || 0}
-
-
-
- Output Tokens: {" "}
-
- {selectedDetail.tokens?.completion_tokens?.toLocaleString() || 0}
-
-
-
-
-
-
-
- {JSON.stringify(selectedDetail.request, null, 2)}
-
-
-
- {selectedDetail.providerRequest && (
-
-
- {JSON.stringify(selectedDetail.providerRequest, null, 2)}
-
-
- )}
-
- {selectedDetail.providerResponse && (
-
-
- {typeof selectedDetail.providerResponse === 'object'
- ? JSON.stringify(selectedDetail.providerResponse, null, 2)
- : selectedDetail.providerResponse
- }
-
-
- )}
-
-
- {selectedDetail.response?.thinking && (
-
-
- psychology
- Thinking Process
-
-
- {selectedDetail.response.thinking}
-
-
- )}
-
-
- Content
-
-
- {selectedDetail.response?.content || "[No content]"}
-
-
-
-
- )}
-
-
- );
-}
diff --git a/src/app/(dashboard)/dashboard/usage/page.js b/src/app/(dashboard)/dashboard/usage/page.js
index b674e97..90e372b 100644
--- a/src/app/(dashboard)/dashboard/usage/page.js
+++ b/src/app/(dashboard)/dashboard/usage/page.js
@@ -3,7 +3,6 @@
import { useState, Suspense } from "react";
import { UsageStats, RequestLogger, CardSkeleton, SegmentedControl } from "@/shared/components";
import ProviderLimits from "./components/ProviderLimits";
-import RequestDetailsTab from "./components/RequestDetailsTab";
export default function UsagePage() {
const [activeTab, setActiveTab] = useState("overview");
@@ -15,7 +14,6 @@ export default function UsagePage() {
{ value: "overview", label: "Overview" },
{ value: "logs", label: "Logger" },
{ value: "limits", label: "Limits" },
- { value: "details", label: "Details" },
]}
value={activeTab}
onChange={setActiveTab}
@@ -33,7 +31,6 @@ export default function UsagePage() {
)}
- {activeTab === "details" &&
}
);
}
diff --git a/src/app/api/usage/providers/route.js b/src/app/api/usage/providers/route.js
deleted file mode 100644
index baa1cff..0000000
--- a/src/app/api/usage/providers/route.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import { NextResponse } from "next/server";
-import { getRequestDetailsDb } from "@/lib/requestDetailsDb";
-import { getProviderNodes } from "@/lib/localDb";
-import { AI_PROVIDERS, getProviderByAlias } from "@/shared/constants/providers";
-
-/**
- * GET /api/usage/providers
- * Returns list of unique providers from request details
- */
-export async function GET() {
- try {
- const db = await getRequestDetailsDb();
-
- const stmt = db.prepare(`
- SELECT DISTINCT provider
- FROM request_details
- WHERE provider IS NOT NULL AND provider != ''
- ORDER BY provider ASC
- `);
-
- const rows = stmt.all();
-
- // Fetch all provider nodes to get names for custom providers
- const providerNodes = await getProviderNodes();
- const nodeMap = {};
- for (const node of providerNodes) {
- nodeMap[node.id] = node.name;
- }
-
- const providers = rows.map(row => {
- const providerId = row.provider;
-
- // Try to find name from various sources
- let name = providerId;
-
- // 1. Check if it's a custom provider node
- if (nodeMap[providerId]) {
- name = nodeMap[providerId];
- }
- // 2. Check predefined providers
- else {
- const providerConfig = getProviderByAlias(providerId) || AI_PROVIDERS[providerId];
- if (providerConfig?.name) {
- name = providerConfig.name;
- }
- }
-
- return {
- id: providerId,
- name
- };
- });
-
- return NextResponse.json({ providers });
- } catch (error) {
- console.error("[API] Failed to get providers:", error);
- return NextResponse.json(
- { error: "Failed to fetch providers" },
- { status: 500 }
- );
- }
-}
diff --git a/src/app/api/usage/request-details/route.js b/src/app/api/usage/request-details/route.js
deleted file mode 100644
index 73a3ceb..0000000
--- a/src/app/api/usage/request-details/route.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import { NextResponse } from "next/server";
-import { getRequestDetails } from "@/lib/usageDb";
-
-/**
- * GET /api/usage/request-details
- * Query parameters: page, pageSize (1-100), provider, model, connectionId, status, startDate, endDate
- */
-export async function GET(request) {
- try {
- const { searchParams } = new URL(request.url);
-
- const page = parseInt(searchParams.get("page")) || 1;
- const pageSize = parseInt(searchParams.get("pageSize")) || 20;
- const provider = searchParams.get("provider");
- const model = searchParams.get("model");
- const connectionId = searchParams.get("connectionId");
- const status = searchParams.get("status");
- const startDate = searchParams.get("startDate");
- const endDate = searchParams.get("endDate");
-
- if (page < 1) {
- return NextResponse.json(
- { error: "Page must be >= 1" },
- { status: 400 }
- );
- }
-
- if (pageSize < 1 || pageSize > 100) {
- return NextResponse.json(
- { error: "PageSize must be between 1 and 100" },
- { status: 400 }
- );
- }
-
- const filter = {
- page,
- pageSize
- };
-
- if (provider) filter.provider = provider;
- if (model) filter.model = model;
- if (connectionId) filter.connectionId = connectionId;
- if (status) filter.status = status;
- if (startDate) filter.startDate = startDate;
- if (endDate) filter.endDate = endDate;
-
- const result = await getRequestDetails(filter);
-
- return NextResponse.json(result);
- } catch (error) {
- console.error("[API] Failed to get request details:", error);
- return NextResponse.json(
- { error: "Failed to fetch request details" },
- { status: 500 }
- );
- }
-}
diff --git a/src/lib/localDb.js b/src/lib/localDb.js
index f15df97..615d608 100644
--- a/src/lib/localDb.js
+++ b/src/lib/localDb.js
@@ -50,11 +50,7 @@ const defaultData = {
settings: {
cloudEnabled: false,
stickyRoundRobinLimit: 3,
- requireLogin: true,
- observabilityMaxRecords: 1000,
- observabilityBatchSize: 20,
- observabilityFlushIntervalMs: 5000,
- observabilityMaxJsonSize: 1024
+ requireLogin: true
},
pricing: {} // NEW: pricing configuration
};
@@ -71,10 +67,6 @@ function cloneDefaultData() {
cloudEnabled: false,
stickyRoundRobinLimit: 3,
requireLogin: true,
- observabilityMaxRecords: 1000,
- observabilityBatchSize: 20,
- observabilityFlushIntervalMs: 5000,
- observabilityMaxJsonSize: 1024
},
pricing: {},
};
diff --git a/src/lib/requestDetailsDb.js b/src/lib/requestDetailsDb.js
deleted file mode 100644
index 85b86c5..0000000
--- a/src/lib/requestDetailsDb.js
+++ /dev/null
@@ -1,499 +0,0 @@
-import Database from "better-sqlite3";
-import path from "path";
-import os from "os";
-import fs from "fs";
-
-const isCloud = typeof caches !== 'undefined' || typeof caches === 'object';
-
-// ============================================================================
-// CONFIGURATION: Batch Processing Settings
-// ============================================================================
-
-/**
- * Get observability configuration from settings.
- * Falls back to environment variables, then defaults.
- */
-async function getObservabilityConfig() {
- try {
- const { getSettings } = await import("@/lib/localDb");
- const settings = await getSettings();
-
- return {
- maxRecords: settings.observabilityMaxRecords || parseInt(process.env.OBSERVABILITY_MAX_RECORDS || '1000', 10),
- batchSize: settings.observabilityBatchSize || parseInt(process.env.OBSERVABILITY_BATCH_SIZE || '20', 10),
- flushIntervalMs: settings.observabilityFlushIntervalMs || parseInt(process.env.OBSERVABILITY_FLUSH_INTERVAL_MS || '5000', 10),
- maxJsonSize: (settings.observabilityMaxJsonSize || parseInt(process.env.OBSERVABILITY_MAX_JSON_SIZE || '1024', 10)) * 1024
- };
- } catch (error) {
- console.error("[requestDetailsDb] Failed to load observability config:", error);
- return {
- maxRecords: 1000,
- batchSize: 20,
- flushIntervalMs: 5000,
- maxJsonSize: 1024 * 1024
- };
- }
-}
-
-// Cache config to avoid repeated database reads
-let cachedConfig = null;
-
-let dbInstance = null;
-
-// Get app name
-function getAppName() {
- return "9router";
-}
-
-// Get user data directory based on platform
-function getUserDataDir() {
- if (isCloud) return "/tmp";
-
- try {
- const platform = process.platform;
- const homeDir = os.homedir();
- const appName = getAppName();
-
- if (platform === "win32") {
- return path.join(process.env.APPDATA || path.join(homeDir, "AppData", "Roaming"), appName);
- } else {
- return path.join(homeDir, `.${appName}`);
- }
- } catch (error) {
- console.error("[requestDetailsDb] Failed to get user data directory:", error.message);
- return path.join(process.cwd(), ".9router");
- }
-}
-
-// Database file path
-const DATA_DIR = getUserDataDir();
-const DB_FILE = isCloud ? null : path.join(DATA_DIR, "request-details.sqlite");
-
-// Ensure data directory exists
-if (!isCloud && fs && typeof fs.existsSync === "function") {
- try {
- if (!fs.existsSync(DATA_DIR)) {
- fs.mkdirSync(DATA_DIR, { recursive: true });
- }
- } catch (error) {
- console.error("[requestDetailsDb] Failed to create data directory:", error.message);
- }
-}
-
-// ============================================================================
-// BATCH WRITE QUEUE
-// ============================================================================
-
-/**
- * In-memory buffer for batch writes.
- * Accumulates request details before flushing to database in a transaction.
- * @type {Array
}
- */
-let writeBuffer = [];
-
-/**
- * Timer reference for auto-flush mechanism.
- * Ensures data is written even during low traffic periods.
- * @type {NodeJS.Timeout|null}
- */
-let flushTimer = null;
-
-/**
- * Flag indicating if a flush operation is currently in progress.
- * Prevents concurrent flushes.
- * @type {boolean}
- */
-let isFlushing = false;
-
-/**
- * Get SQLite database instance (singleton)
- */
-export async function getRequestDetailsDb() {
- if (isCloud) {
- // In-memory mock for Workers
- if (!dbInstance) {
- dbInstance = {
- prepare: () => ({
- run: () => {},
- get: () => null,
- all: () => []
- }),
- exec: () => {},
- pragma: () => {}
- };
- }
- return dbInstance;
- }
-
- if (!dbInstance) {
- const db = new Database(DB_FILE);
-
- // Configure for better concurrency
- db.pragma('journal_mode = WAL'); // Write-Ahead Logging for concurrent access
- db.pragma('synchronous = NORMAL'); // Faster than FULL, still safe
- db.pragma('cache_size = -64000'); // 64MB cache
- db.pragma('temp_store = MEMORY'); // Use memory for temp tables
-
- // Create table with indexes
- db.exec(`
- CREATE TABLE IF NOT EXISTS request_details (
- id TEXT PRIMARY KEY,
- provider TEXT,
- model TEXT,
- connection_id TEXT,
- timestamp INTEGER NOT NULL,
- status TEXT,
- latency TEXT,
- tokens TEXT,
- request TEXT,
- provider_request TEXT,
- provider_response TEXT,
- response TEXT
- );
-
- -- Indexes for common queries
- CREATE INDEX IF NOT EXISTS idx_timestamp
- ON request_details(timestamp DESC);
- CREATE INDEX IF NOT EXISTS idx_provider
- ON request_details(provider);
- CREATE INDEX IF NOT EXISTS idx_model
- ON request_details(model);
- CREATE INDEX IF NOT EXISTS idx_connection
- ON request_details(connection_id);
- CREATE INDEX IF NOT EXISTS idx_status
- ON request_details(status);
- `);
-
- dbInstance = db;
-
- // Register shutdown handler on first database initialization
- ensureShutdownHandler();
- }
-
- return dbInstance;
-}
-
-/**
- * Generate unique ID for request detail
- */
-function generateDetailId(model) {
- const timestamp = new Date().toISOString();
- const random = Math.random().toString(36).substring(2, 8);
- const modelPart = model ? model.replace(/[^a-zA-Z0-9-]/g, '-') : 'unknown';
- return `${timestamp}-${random}-${modelPart}`;
-}
-
-/**
- * Flush all buffered items to database in a single transaction.
- * This function is called automatically when:
- * 1. Buffer size reaches OBSERVABILITY_BATCH_SIZE
- * 2. OBSERVABILITY_FLUSH_INTERVAL_MS elapses
- * 3. Process is shutting down (graceful shutdown)
- *
- * @private
- */
-async function flushToDatabase() {
- if (isCloud || isFlushing || writeBuffer.length === 0) {
- return;
- }
-
- isFlushing = true;
-
- try {
- // Take a snapshot of the buffer and clear it immediately
- const itemsToSave = [...writeBuffer];
- writeBuffer = [];
-
- const db = await getRequestDetailsDb();
- const config = await getObservabilityConfig();
-
- // Prepare statements outside transaction for better performance
- const insertStmt = db.prepare(`
- INSERT OR REPLACE INTO request_details
- (id, provider, model, connection_id, timestamp, status, latency, tokens,
- request, provider_request, provider_response, response)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- `);
-
- const deleteStmt = db.prepare(`
- DELETE FROM request_details
- WHERE id NOT IN (
- SELECT id FROM request_details
- ORDER BY timestamp DESC
- LIMIT ?
- )
- `);
-
- // Execute all writes in a single transaction for atomicity
- const transaction = db.transaction((items) => {
- const maxJsonSize = config.maxJsonSize;
-
- for (const item of items) {
- if (!item.id) {
- item.id = generateDetailId(item.model);
- }
-
- if (!item.timestamp) {
- item.timestamp = new Date().toISOString();
- }
-
- // Sanitize headers if present
- if (item.request && item.request.headers) {
- item.request.headers = sanitizeHeaders(item.request.headers);
- }
-
- insertStmt.run(
- item.id,
- item.provider || null,
- item.model || null,
- item.connectionId || null,
- new Date(item.timestamp).getTime(),
- item.status || null,
- JSON.stringify(item.latency || {}),
- JSON.stringify(item.tokens || {}),
- safeJsonStringify(item.request || {}, maxJsonSize),
- safeJsonStringify(item.providerRequest || {}, maxJsonSize),
- safeJsonStringify(item.providerResponse || {}, maxJsonSize),
- safeJsonStringify(item.response || {}, maxJsonSize)
- );
- }
-
- // Cleanup old records once per batch (not per item)
- deleteStmt.run(config.maxRecords);
- });
-
- transaction(itemsToSave);
- } catch (error) {
- console.error("[requestDetailsDb] Batch write failed:", error);
- } finally {
- isFlushing = false;
- }
-}
-
-/**
- * Safely stringify an object with a size limit.
- * Truncates the result if it exceeds the limit.
- * @param {object} obj - Object to stringify
- * @param {number} maxSize - Maximum string size in bytes
- * @returns {string}
- */
-function safeJsonStringify(obj, maxSize) {
- try {
- const str = JSON.stringify(obj);
- if (str.length > maxSize) {
- return str.substring(0, maxSize) + "... (truncated due to size limit)";
- }
- return str;
- } catch (error) {
- return JSON.stringify({ error: "Failed to stringify object", message: error.message });
- }
-}
-
-/**
- * Sanitize sensitive headers from request
- */
-function sanitizeHeaders(headers) {
- if (!headers || typeof headers !== 'object') return {};
-
- const sensitiveKeys = ['authorization', 'x-api-key', 'cookie', 'token', 'api-key'];
- const sanitized = { ...headers };
-
- for (const key of Object.keys(sanitized)) {
- if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) {
- delete sanitized[key];
- }
- }
-
- return sanitized;
-}
-
-/**
- * Save request detail to SQLite (batched for performance).
- * Details are accumulated in memory and flushed to database in batches.
- *
- * @param {object} detail - Request detail object
- * @see {@link flushToDatabase} for batch write implementation
- */
-export async function saveRequestDetail(detail) {
- if (isCloud) return;
-
- if (!cachedConfig) {
- cachedConfig = await getObservabilityConfig();
- }
-
- writeBuffer.push(detail);
-
- if (writeBuffer.length >= cachedConfig.batchSize) {
- await flushToDatabase();
-
- if (flushTimer) {
- clearTimeout(flushTimer);
- flushTimer = null;
- }
- } else if (!flushTimer) {
- flushTimer = setTimeout(() => {
- flushToDatabase().catch(() => {});
- flushTimer = null;
- }, cachedConfig.flushIntervalMs);
- }
-}
-
-// ============================================================================
-// GRACEFUL SHUTDOWN HANDLER
-// ============================================================================
-
-let shutdownHandlerRegistered = false;
-
-/**
- * Register process shutdown handlers to flush remaining data before exit.
- * Should be called once when the module initializes.
- */
-function ensureShutdownHandler() {
- if (shutdownHandlerRegistered || isCloud) {
- return;
- }
-
- const handler = async () => {
- // Clear timer to prevent any pending flush
- if (flushTimer) {
- clearTimeout(flushTimer);
- flushTimer = null;
- }
-
- // Flush any remaining data in buffer
- if (writeBuffer.length > 0) {
- console.log(`[requestDetailsDb] Flushing ${writeBuffer.length} items before shutdown...`);
- await flushToDatabase();
- }
- };
-
- // Register handlers for various termination signals
- process.on('beforeExit', handler);
- process.on('SIGINT', handler);
- process.on('SIGTERM', handler);
- process.on('exit', handler);
-
- shutdownHandlerRegistered = true;
-}
-
-/**
- * Get request details with filtering and pagination
- * @param {object} filter - Filter options
- * @returns {Promise} Details with pagination info
- */
-export async function getRequestDetails(filter = {}) {
- const db = await getRequestDetailsDb();
-
- if (isCloud) {
- return { details: [], pagination: { page: 1, pageSize: filter.pageSize || 50, totalItems: 0, totalPages: 0, hasNext: false, hasPrev: false } };
- }
-
- let query = 'SELECT * FROM request_details WHERE 1=1';
- const params = [];
-
- if (filter.provider) {
- query += ' AND provider = ?';
- params.push(filter.provider);
- }
-
- if (filter.model) {
- query += ' AND model = ?';
- params.push(filter.model);
- }
-
- if (filter.connectionId) {
- query += ' AND connection_id = ?';
- params.push(filter.connectionId);
- }
-
- if (filter.status) {
- query += ' AND status = ?';
- params.push(filter.status);
- }
-
- if (filter.startDate) {
- query += ' AND timestamp >= ?';
- params.push(new Date(filter.startDate).getTime());
- }
-
- if (filter.endDate) {
- query += ' AND timestamp <= ?';
- params.push(new Date(filter.endDate).getTime());
- }
-
- // Get total count first
- const countQuery = query.replace('SELECT *', 'SELECT COUNT(*)');
- const countStmt = db.prepare(countQuery);
- const totalResult = countStmt.get(...params);
- const total = totalResult['COUNT(*)'];
-
- // Add pagination
- query += ' ORDER BY timestamp DESC';
- const page = filter.page || 1;
- const pageSize = filter.pageSize || 50;
- query += ' LIMIT ? OFFSET ?';
- params.push(pageSize, (page - 1) * pageSize);
-
- // Execute query
- const stmt = db.prepare(query);
- const rows = stmt.all(...params);
-
- // Convert back to original format
- const details = rows.map(row => ({
- id: row.id,
- provider: row.provider,
- model: row.model,
- connectionId: row.connection_id,
- timestamp: new Date(row.timestamp).toISOString(),
- status: row.status,
- latency: JSON.parse(row.latency || '{}'),
- tokens: JSON.parse(row.tokens || '{}'),
- request: JSON.parse(row.request || '{}'),
- providerRequest: JSON.parse(row.provider_request || '{}'),
- providerResponse: JSON.parse(row.provider_response || '{}'),
- response: JSON.parse(row.response || '{}')
- }));
-
- return {
- details,
- pagination: {
- page,
- pageSize,
- totalItems: total,
- totalPages: Math.ceil(total / pageSize),
- hasNext: page < Math.ceil(total / pageSize),
- hasPrev: page > 1
- }
- };
-}
-
-/**
- * Get single request detail by ID
- * @param {string} id - Request detail ID
- * @returns {Promise} Request detail or null
- */
-export async function getRequestDetailById(id) {
- const db = await getRequestDetailsDb();
-
- if (isCloud) return null;
-
- const stmt = db.prepare('SELECT * FROM request_details WHERE id = ?');
- const row = stmt.get(id);
-
- if (!row) return null;
-
- return {
- id: row.id,
- provider: row.provider,
- model: row.model,
- connectionId: row.connection_id,
- timestamp: new Date(row.timestamp).toISOString(),
- status: row.status,
- latency: JSON.parse(row.latency || '{}'),
- tokens: JSON.parse(row.tokens || '{}'),
- request: JSON.parse(row.request || '{}'),
- providerRequest: JSON.parse(row.provider_request || '{}'),
- providerResponse: JSON.parse(row.provider_response || '{}'),
- response: JSON.parse(row.response || '{}')
- };
-}
diff --git a/src/lib/usageDb.js b/src/lib/usageDb.js
index 99546ce..b658001 100644
--- a/src/lib/usageDb.js
+++ b/src/lib/usageDb.js
@@ -511,6 +511,3 @@ export async function getUsageStats() {
return stats;
}
-
-// Re-export request details functions from new SQLite-based module
-export { saveRequestDetail, getRequestDetails, getRequestDetailById } from "./requestDetailsDb.js";
diff --git a/src/shared/components/Drawer.js b/src/shared/components/Drawer.js
deleted file mode 100644
index b1f095d..0000000
--- a/src/shared/components/Drawer.js
+++ /dev/null
@@ -1,89 +0,0 @@
-"use client";
-
-import { useEffect } from "react";
-import { cn } from "@/shared/utils/cn";
-
-export default function Drawer({
- isOpen,
- onClose,
- title,
- children,
- width = "md",
- className
-}) {
- const widths = {
- sm: "w-[400px]",
- md: "w-[500px]",
- lg: "w-[600px]",
- xl: "w-[800px]",
- full: "w-full",
- };
-
- // Lock body scroll when drawer is open
- useEffect(() => {
- if (isOpen) {
- document.body.style.overflow = "hidden";
- } else {
- document.body.style.overflow = "";
- }
- return () => {
- document.body.style.overflow = "";
- };
- }, [isOpen]);
-
- // Handle escape key
- useEffect(() => {
- const handleEscape = (e) => {
- if (e.key === "Escape" && isOpen) {
- onClose();
- }
- };
- document.addEventListener("keydown", handleEscape);
- return () => document.removeEventListener("keydown", handleEscape);
- }, [isOpen, onClose]);
-
- if (!isOpen) return null;
-
- return (
-
- {/* Overlay */}
-
-
- {/* Drawer panel */}
-
- {/* Header */}
-
-
- {title && (
-
- {title}
-
- )}
-
-
- close
-
-
-
- {/* Body */}
-
- {children}
-
-
-
- );
-}
diff --git a/src/shared/components/Pagination.js b/src/shared/components/Pagination.js
deleted file mode 100644
index 5c4f908..0000000
--- a/src/shared/components/Pagination.js
+++ /dev/null
@@ -1,146 +0,0 @@
-"use client";
-
-import { cn } from "@/shared/utils/cn";
-import Button from "./Button";
-
-export default function Pagination({
- currentPage,
- pageSize,
- totalItems,
- onPageChange,
- onPageSizeChange,
- className,
-}) {
- const totalPages = Math.ceil(totalItems / pageSize);
- const startItem = totalItems > 0 ? (currentPage - 1) * pageSize + 1 : 0;
- const endItem = Math.min(currentPage * pageSize, totalItems);
-
- const getPageNumbers = () => {
- const pages = [];
- const showMax = 5;
-
- let start = Math.max(1, currentPage - 2);
- let end = Math.min(totalPages, start + showMax - 1);
-
- if (end - start + 1 < showMax) {
- start = Math.max(1, end - showMax + 1);
- }
-
- for (let i = start; i <= end; i++) {
- pages.push(i);
- }
- return pages;
- };
-
- const pageNumbers = getPageNumbers();
-
- return (
-
- {/* Info text */}
- {totalItems > 0 && (
-
- Showing {startItem} to{" "}
- {endItem} of{" "}
- {totalItems} results
-
- )}
-
-
- {/* Page size selector */}
- {onPageSizeChange && (
-
- Rows:
- onPageSizeChange(Number(e.target.value))}
- className={cn(
- "h-9 rounded-lg border border-black/10 dark:border-white/10 bg-surface",
- "text-sm text-text-main focus:outline-none focus:ring-2 focus:ring-primary/20",
- "cursor-pointer"
- )}
- >
- {[10, 20, 50].map((size) => (
-
- {size}
-
- ))}
-
-
- )}
-
- {totalPages > 1 && (
-
- onPageChange(currentPage - 1)}
- disabled={currentPage === 1}
- className="w-9 px-0"
- >
- chevron_left
-
-
- {pageNumbers[0] > 1 && (
- <>
- onPageChange(1)}
- className="w-9 px-0"
- >
- 1
-
- {pageNumbers[0] > 2 && (
- ...
- )}
- >
- )}
-
- {pageNumbers.map((page) => (
- onPageChange(page)}
- className="w-9 px-0"
- >
- {page}
-
- ))}
-
- {pageNumbers[pageNumbers.length - 1] < totalPages && (
- <>
- {pageNumbers[pageNumbers.length - 1] < totalPages - 1 && (
- ...
- )}
- onPageChange(totalPages)}
- className="w-9 px-0"
- >
- {totalPages}
-
- >
- )}
-
- onPageChange(currentPage + 1)}
- disabled={currentPage === totalPages}
- className="w-9 px-0"
- >
- chevron_right
-
-
- )}
-
-
- );
-}