Files
9router/open-sse/handlers/chatCore/requestDetail.js
luulam bef54d5f12 fix: merge logUsage + saveUsageStats to prevent duplicate usage stats for streaming requests
- Enhanced saveUsageStats to include cache tokens (cache_read, cache_creation), reasoning tokens, and estimated flag
- Replaced logUsage calls in stream.js with saveUsageStats (2 locations)
- Removed duplicate saveUsageStats call from streamingHandler.js onStreamComplete
- Removed logUsage function and unused imports from usageTracking.js
- Each streaming request now writes exactly 1 usage record instead of 2
2026-06-19 08:43:10 +07:00

118 lines
4.5 KiB
JavaScript

import { saveRequestUsage, appendRequestLog, saveRequestDetail } from "@/lib/usageDb.js";
import { COLORS } from "../../utils/stream.js";
const OPTIONAL_PARAMS = [
"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"
];
export function extractRequestConfig(body, stream) {
const config = { messages: body.messages || [], model: body.model, stream };
for (const param of OPTIONAL_PARAMS) {
if (body[param] !== undefined) config[param] = body[param];
}
return config;
}
export function extractUsageFromResponse(responseBody) {
if (!responseBody || typeof responseBody !== "object") return null;
// Claude format
if (responseBody.usage?.input_tokens !== undefined) {
return {
prompt_tokens: responseBody.usage.input_tokens || 0,
completion_tokens: responseBody.usage.output_tokens || 0,
cache_read_input_tokens: responseBody.usage.cache_read_input_tokens,
cache_creation_input_tokens: responseBody.usage.cache_creation_input_tokens
};
}
// OpenAI format
if (responseBody.usage?.prompt_tokens !== undefined) {
return {
prompt_tokens: responseBody.usage.prompt_tokens || 0,
completion_tokens: responseBody.usage.completion_tokens || 0,
cached_tokens: responseBody.usage.prompt_tokens_details?.cached_tokens,
reasoning_tokens: responseBody.usage.completion_tokens_details?.reasoning_tokens
};
}
// Gemini format
if (responseBody.usageMetadata) {
return {
prompt_tokens: responseBody.usageMetadata.promptTokenCount || 0,
completion_tokens: responseBody.usageMetadata.candidatesTokenCount || 0,
reasoning_tokens: responseBody.usageMetadata.thoughtsTokenCount
};
}
return null;
}
export function buildRequestDetail(base, overrides = {}) {
return {
provider: base.provider || "unknown",
model: base.model || "unknown",
connectionId: base.connectionId || undefined,
timestamp: new Date().toISOString(),
latency: base.latency || { ttft: 0, total: 0 },
tokens: base.tokens || { prompt_tokens: 0, completion_tokens: 0 },
request: base.request,
providerRequest: base.providerRequest || null,
providerResponse: base.providerResponse || null,
response: base.response || {},
status: base.status || "success",
...overrides
};
}
export function saveUsageStats({ provider, model, tokens, connectionId, apiKey, endpoint, label = "USAGE" }) {
if (!tokens || typeof tokens !== "object") return;
const inTokens = tokens.input_tokens ?? tokens.prompt_tokens ?? 0;
const outTokens = tokens.output_tokens ?? tokens.completion_tokens ?? 0;
if (inTokens === 0 && outTokens === 0) return;
// Extract cache/reasoning tokens (unified from different formats)
const cacheRead = tokens.cache_read_input_tokens || tokens.cached_tokens || tokens.prompt_tokens_details?.cached_tokens || 0;
const cacheCreation = tokens.cache_creation_input_tokens || 0;
const reasoning = tokens.reasoning_tokens || 0;
const time = new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
const accountSuffix = connectionId ? ` | account=${connectionId.slice(0, 8)}...` : "";
let msg = `${COLORS.green}[${time}] 📊 [${label}] ${provider?.toUpperCase() || "UNKNOWN"} | in=${inTokens} | out=${outTokens}${accountSuffix}`;
if (tokens.estimated) msg += ` ${COLORS.yellow}(estimated)${COLORS.reset}`;
if (cacheRead) msg += ` | cache_read=${cacheRead}`;
if (cacheCreation) msg += ` | cache_create=${cacheCreation}`;
if (reasoning) msg += ` | reasoning=${reasoning}`;
msg += `${COLORS.reset}`;
console.log(msg);
// Normalize to OpenAI token shape for storage (include all token types)
const normalized = {
prompt_tokens: tokens.prompt_tokens ?? tokens.input_tokens ?? 0,
completion_tokens: tokens.completion_tokens ?? tokens.output_tokens ?? 0,
cache_read_input_tokens: cacheRead,
cache_creation_input_tokens: cacheCreation,
reasoning_tokens: reasoning,
};
saveRequestUsage({
provider: provider || "unknown",
model: model || "unknown",
tokens: normalized,
timestamp: new Date().toISOString(),
connectionId: connectionId || undefined,
apiKey: apiKey || undefined,
endpoint: endpoint || null
}).catch(() => {});
}