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 outputTokens = 0;
|
||||
|
||||
// Split by "data:" lines (SSE format)
|
||||
// Parse every "data:" line for usage info (provider-agnostic approach)
|
||||
const lines = text.split("\n");
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith("event: message_start")) {
|
||||
// Find the next data line
|
||||
const dataIndex = lines.indexOf(line) + 1;
|
||||
if (dataIndex < lines.length && lines[dataIndex].trim().startsWith("data:")) {
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const trimmed = lines[i].trim();
|
||||
if (!trimmed.startsWith("data:")) continue;
|
||||
|
||||
const dataStr = trimmed.slice(5).trim();
|
||||
if (!dataStr || dataStr === "[DONE]") continue;
|
||||
|
||||
try {
|
||||
const dataStr = lines[dataIndex].trim().slice(5).trim();
|
||||
const data = JSON.parse(dataStr);
|
||||
if (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) {
|
||||
inputTokens = data.message.usage.input_tokens;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
} else if (trimmed.startsWith("event: message_delta")) {
|
||||
const dataIndex = lines.indexOf(line) + 1;
|
||||
if (dataIndex < lines.length && lines[dataIndex].trim().startsWith("data:")) {
|
||||
try {
|
||||
const dataStr = lines[dataIndex].trim().slice(5).trim();
|
||||
const data = JSON.parse(dataStr);
|
||||
if (data.usage?.output_tokens) {
|
||||
|
||||
// Anthropic message_delta: { type: "message_delta", usage: { output_tokens } }
|
||||
if (data.type === "message_delta" && 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
|
||||
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);
|
||||
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 };
|
||||
} catch (streamErr) {
|
||||
@@ -576,21 +590,23 @@ async function handleMessages(req, res) {
|
||||
const responseBody = await upstreamRes.text();
|
||||
const responseHeaders = { "content-type": upstreamRes.headers.get("content-type") || "application/json" };
|
||||
|
||||
// Try to extract usage from non-streaming response
|
||||
if (statusCode === 200 && responseHeaders["content-type"]?.includes("application/json")) {
|
||||
// Record usage from non-streaming response
|
||||
if (statusCode === 200) {
|
||||
let extractedInput = 0;
|
||||
let extractedOutput = 0;
|
||||
if (responseHeaders["content-type"]?.includes("application/json")) {
|
||||
try {
|
||||
const json = JSON.parse(responseBody);
|
||||
if (json.usage?.input_tokens || json.usage?.output_tokens) {
|
||||
recordUsage(
|
||||
requestedModel,
|
||||
route.provider,
|
||||
actualModel,
|
||||
json.usage.input_tokens,
|
||||
json.usage.output_tokens
|
||||
);
|
||||
}
|
||||
extractedInput = json.usage?.input_tokens || 0;
|
||||
extractedOutput = json.usage?.output_tokens || 0;
|
||||
} catch {}
|
||||
}
|
||||
// Always record — even if token extraction fails, we count the request
|
||||
recordUsage(requestedModel, route.provider, actualModel, extractedInput, extractedOutput);
|
||||
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
|
||||
if (statusCode >= 200 && statusCode < 300) {
|
||||
|
||||
Reference in New Issue
Block a user