fix: track usage for all providers, not just when tokens extracted
- Rewrote extractUsageFromSSE() to parse data: lines directly instead of buggy indexOf-based event/data pairing that silently failed for Z.ai and DeepSeek SSE formats - Always call recordUsage() on successful responses even when token extraction yields 0 — ensures request counts are tracked - Added console.warn when token extraction fails for debugging
This commit is contained in:
@@ -455,33 +455,45 @@ function extractUsageFromSSE(text, provider) {
|
|||||||
let inputTokens = 0;
|
let inputTokens = 0;
|
||||||
let outputTokens = 0;
|
let outputTokens = 0;
|
||||||
|
|
||||||
// Split by "data:" lines (SSE format)
|
// Parse every "data:" line for usage info (provider-agnostic approach)
|
||||||
const lines = text.split("\n");
|
const lines = text.split("\n");
|
||||||
for (const line of lines) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const trimmed = line.trim();
|
const trimmed = lines[i].trim();
|
||||||
if (trimmed.startsWith("event: message_start")) {
|
if (!trimmed.startsWith("data:")) continue;
|
||||||
// Find the next data line
|
|
||||||
const dataIndex = lines.indexOf(line) + 1;
|
const dataStr = trimmed.slice(5).trim();
|
||||||
if (dataIndex < lines.length && lines[dataIndex].trim().startsWith("data:")) {
|
if (!dataStr || dataStr === "[DONE]") continue;
|
||||||
try {
|
|
||||||
const dataStr = lines[dataIndex].trim().slice(5).trim();
|
try {
|
||||||
const data = JSON.parse(dataStr);
|
const data = JSON.parse(dataStr);
|
||||||
if (data.message?.usage?.input_tokens) {
|
|
||||||
inputTokens = data.message.usage.input_tokens;
|
// Anthropic message_start: { type: "message_start", message: { usage: { input_tokens } } }
|
||||||
}
|
if (data.type === "message_start" && data.message?.usage?.input_tokens) {
|
||||||
} catch {}
|
inputTokens = data.message.usage.input_tokens;
|
||||||
}
|
}
|
||||||
} else if (trimmed.startsWith("event: message_delta")) {
|
|
||||||
const dataIndex = lines.indexOf(line) + 1;
|
// Anthropic message_delta: { type: "message_delta", usage: { output_tokens } }
|
||||||
if (dataIndex < lines.length && lines[dataIndex].trim().startsWith("data:")) {
|
if (data.type === "message_delta" && data.usage?.output_tokens) {
|
||||||
try {
|
outputTokens = data.usage.output_tokens;
|
||||||
const dataStr = lines[dataIndex].trim().slice(5).trim();
|
|
||||||
const data = JSON.parse(dataStr);
|
|
||||||
if (data.usage?.output_tokens) {
|
|
||||||
outputTokens = data.usage.output_tokens;
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Some providers put usage at the top level of non-typed events
|
||||||
|
if (data.usage) {
|
||||||
|
if (data.usage.input_tokens && data.type !== "message_start") {
|
||||||
|
inputTokens = inputTokens || data.usage.input_tokens;
|
||||||
|
}
|
||||||
|
if (data.usage.output_tokens && data.type !== "message_delta") {
|
||||||
|
outputTokens = outputTokens || data.usage.output_tokens;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some providers put usage in a final message object
|
||||||
|
if (data.message?.usage && data.type !== "message_start") {
|
||||||
|
inputTokens = inputTokens || data.message.usage.input_tokens || 0;
|
||||||
|
outputTokens = outputTokens || data.message.usage.output_tokens || 0;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not valid JSON — skip (e.g. SSE comment lines)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -561,8 +573,10 @@ async function handleMessages(req, res) {
|
|||||||
|
|
||||||
// Extract usage from the accumulated SSE text
|
// Extract usage from the accumulated SSE text
|
||||||
const { inputTokens, outputTokens } = extractUsageFromSSE(fullText, route.provider);
|
const { inputTokens, outputTokens } = extractUsageFromSSE(fullText, route.provider);
|
||||||
if (inputTokens > 0 || outputTokens > 0) {
|
// Always record usage — even if token extraction fails, we count the request
|
||||||
recordUsage(requestedModel, route.provider, actualModel, inputTokens, outputTokens);
|
recordUsage(requestedModel, route.provider, actualModel, inputTokens, outputTokens);
|
||||||
|
if (inputTokens === 0 && outputTokens === 0) {
|
||||||
|
console.warn(`[usage] No tokens extracted from ${route.provider} SSE stream for ${requestedModel} (stream length: ${fullText.length} chars)`);
|
||||||
}
|
}
|
||||||
providerStatus[route.provider] = { status: "ok", lastSeen: new Date().toISOString(), lastError: null };
|
providerStatus[route.provider] = { status: "ok", lastSeen: new Date().toISOString(), lastError: null };
|
||||||
} catch (streamErr) {
|
} catch (streamErr) {
|
||||||
@@ -576,20 +590,22 @@ async function handleMessages(req, res) {
|
|||||||
const responseBody = await upstreamRes.text();
|
const responseBody = await upstreamRes.text();
|
||||||
const responseHeaders = { "content-type": upstreamRes.headers.get("content-type") || "application/json" };
|
const responseHeaders = { "content-type": upstreamRes.headers.get("content-type") || "application/json" };
|
||||||
|
|
||||||
// Try to extract usage from non-streaming response
|
// Record usage from non-streaming response
|
||||||
if (statusCode === 200 && responseHeaders["content-type"]?.includes("application/json")) {
|
if (statusCode === 200) {
|
||||||
try {
|
let extractedInput = 0;
|
||||||
const json = JSON.parse(responseBody);
|
let extractedOutput = 0;
|
||||||
if (json.usage?.input_tokens || json.usage?.output_tokens) {
|
if (responseHeaders["content-type"]?.includes("application/json")) {
|
||||||
recordUsage(
|
try {
|
||||||
requestedModel,
|
const json = JSON.parse(responseBody);
|
||||||
route.provider,
|
extractedInput = json.usage?.input_tokens || 0;
|
||||||
actualModel,
|
extractedOutput = json.usage?.output_tokens || 0;
|
||||||
json.usage.input_tokens,
|
} catch {}
|
||||||
json.usage.output_tokens
|
}
|
||||||
);
|
// Always record — even if token extraction fails, we count the request
|
||||||
}
|
recordUsage(requestedModel, route.provider, actualModel, extractedInput, extractedOutput);
|
||||||
} catch {}
|
if (extractedInput === 0 && extractedOutput === 0) {
|
||||||
|
console.warn(`[usage] No tokens extracted from ${route.provider} non-streaming response for ${requestedModel}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update provider status based on HTTP status
|
// Update provider status based on HTTP status
|
||||||
|
|||||||
Reference in New Issue
Block a user