updated api return

This commit is contained in:
William Stuckey
2026-03-27 10:59:28 -05:00
parent e7fe5732b7
commit c8777b45ff
2 changed files with 137 additions and 29 deletions
+9
View File
@@ -0,0 +1,9 @@
{
"workbench.colorCustomizations": {
"titleBar.activeBackground": "#083846",
"titleBar.activeForeground": "#ffffff",
"titleBar.inactiveBackground": "#0c323d",
"titleBar.inactiveForeground": "#ffffffcc",
"titleBar.border": "#0c1e23"
}
}
+128 -29
View File
@@ -77,6 +77,13 @@ const MODEL_ROUTES = {
// Default route for unknown models // Default route for unknown models
const DEFAULT_ROUTE = { provider: "zai", rewriteModel: "glm-4.7" }; const DEFAULT_ROUTE = { provider: "zai", rewriteModel: "glm-4.7" };
// --- Provider Status (in-memory, resets on restart) --------------------------
const providerStatus = {};
for (const name of Object.keys(PROVIDERS)) {
providerStatus[name] = { status: "idle", lastSeen: null, lastError: null };
}
// --- Usage Tracking ----------------------------------------------------------- // --- Usage Tracking -----------------------------------------------------------
function getCurrentPeriod() { function getCurrentPeriod() {
@@ -84,6 +91,10 @@ function getCurrentPeriod() {
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`; return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
} }
function getCurrentDate() {
return new Date().toISOString().slice(0, 10); // "2026-03-27"
}
function loadUsageData() { function loadUsageData() {
if (!existsSync(USAGE_DATA_PATH)) { if (!existsSync(USAGE_DATA_PATH)) {
return { return {
@@ -117,6 +128,7 @@ function saveUsageData(data) {
function recordUsage(requestedModel, provider, actualModel, inputTokens, outputTokens) { function recordUsage(requestedModel, provider, actualModel, inputTokens, outputTokens) {
const usageData = loadUsageData(); const usageData = loadUsageData();
const currentPeriod = getCurrentPeriod(); const currentPeriod = getCurrentPeriod();
const todayStr = getCurrentDate();
if (!usageData.models[requestedModel]) { if (!usageData.models[requestedModel]) {
usageData.models[requestedModel] = { usageData.models[requestedModel] = {
@@ -124,6 +136,7 @@ function recordUsage(requestedModel, provider, actualModel, inputTokens, outputT
actualModel, actualModel,
total: { inputTokens: 0, outputTokens: 0, requests: 0 }, total: { inputTokens: 0, outputTokens: 0, requests: 0 },
monthly: { period: currentPeriod, inputTokens: 0, outputTokens: 0, requests: 0 }, monthly: { period: currentPeriod, inputTokens: 0, outputTokens: 0, requests: 0 },
daily: { date: todayStr, inputTokens: 0, outputTokens: 0, requests: 0 },
}; };
} }
@@ -134,6 +147,11 @@ function recordUsage(requestedModel, provider, actualModel, inputTokens, outputT
modelData.monthly = { period: currentPeriod, inputTokens: 0, outputTokens: 0, requests: 0 }; modelData.monthly = { period: currentPeriod, inputTokens: 0, outputTokens: 0, requests: 0 };
} }
// Check if daily date has rolled over (or missing for backward compat)
if (!modelData.daily || modelData.daily.date !== todayStr) {
modelData.daily = { date: todayStr, inputTokens: 0, outputTokens: 0, requests: 0 };
}
// Update totals // Update totals
modelData.total.inputTokens += inputTokens || 0; modelData.total.inputTokens += inputTokens || 0;
modelData.total.outputTokens += outputTokens || 0; modelData.total.outputTokens += outputTokens || 0;
@@ -144,6 +162,11 @@ function recordUsage(requestedModel, provider, actualModel, inputTokens, outputT
modelData.monthly.outputTokens += outputTokens || 0; modelData.monthly.outputTokens += outputTokens || 0;
modelData.monthly.requests += 1; modelData.monthly.requests += 1;
// Update daily
modelData.daily.inputTokens += inputTokens || 0;
modelData.daily.outputTokens += outputTokens || 0;
modelData.daily.requests += 1;
usageData.lastUpdated = new Date().toISOString(); usageData.lastUpdated = new Date().toISOString();
saveUsageData(usageData); saveUsageData(usageData);
} }
@@ -228,10 +251,20 @@ function calculateCost(provider, inputTokens, outputTokens) {
// --- Usage Data Aggregation ------------------------------------------------- // --- Usage Data Aggregation -------------------------------------------------
function getProviderPricing(providerName) {
return config.pricing?.[providerName] || DEFAULT_PRICING[providerName];
}
function buildModelPricing(providerName) {
const pricing = getProviderPricing(providerName);
if (pricing?.type === "flat") return "flat_rate";
return { inputPerMTok: pricing?.inputPerMTok, outputPerMTok: pricing?.outputPerMTok };
}
function buildUsageResponse() { function buildUsageResponse() {
const usageData = loadUsageData(); const usageData = loadUsageData();
const balances = fetchProviderBalances(); // Start fetching but don't await
const currentPeriod = getCurrentPeriod(); const currentPeriod = getCurrentPeriod();
const todayStr = getCurrentDate();
const providers = { const providers = {
anthropic: { balance: null, models: {} }, anthropic: { balance: null, models: {} },
@@ -241,19 +274,33 @@ function buildUsageResponse() {
let totalRequests = 0; let totalRequests = 0;
let monthlyRequests = 0; let monthlyRequests = 0;
let todayRequests = 0;
let totalCost = 0; let totalCost = 0;
let monthlyCost = 0; let monthlyCost = 0;
let flatRateModels = 0; let todayCost = 0;
let totalInputTokens = 0;
let totalOutputTokens = 0;
let monthlyInputTokens = 0;
let monthlyOutputTokens = 0;
let todayInputTokens = 0;
let todayOutputTokens = 0;
let flatRateProviders = new Set();
// Process model data // Process model data from usage records
for (const [requestedModel, modelData] of Object.entries(usageData.models || {})) { for (const [requestedModel, modelData] of Object.entries(usageData.models || {})) {
const providerName = modelData.provider; const providerName = modelData.provider;
if (!providers[providerName]) continue; if (!providers[providerName]) continue;
const { total, monthly, actualModel } = modelData; const { total, monthly, daily, actualModel } = modelData;
// Daily data: only include if it's from today, otherwise zeros
const todayData = (daily && daily.date === todayStr) ? daily : { inputTokens: 0, outputTokens: 0, requests: 0 };
const todayCostVal = calculateCost(providerName, todayData.inputTokens, todayData.outputTokens);
providers[providerName].models[requestedModel] = { providers[providerName].models[requestedModel] = {
routesTo: actualModel || requestedModel, routesTo: actualModel || requestedModel,
pricing: buildModelPricing(providerName),
total: { total: {
inputTokens: total.inputTokens, inputTokens: total.inputTokens,
outputTokens: total.outputTokens, outputTokens: total.outputTokens,
@@ -267,23 +314,58 @@ function buildUsageResponse() {
requests: monthly.requests, requests: monthly.requests,
costUsd: calculateCost(providerName, monthly.inputTokens, monthly.outputTokens), costUsd: calculateCost(providerName, monthly.inputTokens, monthly.outputTokens),
}, },
today: {
date: todayStr,
inputTokens: todayData.inputTokens,
outputTokens: todayData.outputTokens,
requests: todayData.requests,
costUsd: todayCostVal,
},
}; };
// Aggregate totals
totalRequests += total.requests; totalRequests += total.requests;
monthlyRequests += monthly.requests; totalInputTokens += total.inputTokens;
totalOutputTokens += total.outputTokens;
monthlyRequests += monthly.period === currentPeriod ? monthly.requests : 0;
monthlyInputTokens += monthly.period === currentPeriod ? monthly.inputTokens : 0;
monthlyOutputTokens += monthly.period === currentPeriod ? monthly.outputTokens : 0;
todayRequests += todayData.requests;
todayInputTokens += todayData.inputTokens;
todayOutputTokens += todayData.outputTokens;
const totalCostVal = calculateCost(providerName, total.inputTokens, total.outputTokens); const totalCostVal = calculateCost(providerName, total.inputTokens, total.outputTokens);
const monthlyCostVal = calculateCost(providerName, monthly.inputTokens, monthly.outputTokens); const monthlyCostVal = calculateCost(providerName, monthly.period === currentPeriod ? monthly.inputTokens : 0, monthly.period === currentPeriod ? monthly.outputTokens : 0);
if (totalCostVal !== null) totalCost += totalCostVal; if (totalCostVal !== null) totalCost += totalCostVal;
else flatRateModels++; else flatRateProviders.add(providerName);
if (monthlyCostVal !== null) monthlyCost += monthlyCostVal; if (monthlyCostVal !== null) monthlyCost += monthlyCostVal;
if (todayCostVal !== null) todayCost += todayCostVal;
} }
// Calculate pricing per provider // Seed all configured models from MODEL_ROUTES (ensures zero-usage models appear)
for (const [requestedModel, route] of Object.entries(MODEL_ROUTES)) {
const providerName = route.provider;
if (!providers[providerName]) continue;
// Skip if already populated from usage data
if (providers[providerName].models[requestedModel]) continue;
providers[providerName].models[requestedModel] = {
routesTo: route.rewriteModel || requestedModel,
pricing: buildModelPricing(providerName),
total: { inputTokens: 0, outputTokens: 0, requests: 0, costUsd: calculateCost(providerName, 0, 0) },
monthly: { period: currentPeriod, inputTokens: 0, outputTokens: 0, requests: 0, costUsd: calculateCost(providerName, 0, 0) },
today: { date: todayStr, inputTokens: 0, outputTokens: 0, requests: 0, costUsd: calculateCost(providerName, 0, 0) },
};
}
// Set provider-level metadata: pricing, status
for (const [providerName, providerData] of Object.entries(providers)) { for (const [providerName, providerData] of Object.entries(providers)) {
const pricing = config.pricing?.[providerName] || DEFAULT_PRICING[providerName]; const pricing = getProviderPricing(providerName);
if (pricing?.type === "flat") { if (pricing?.type === "flat") {
providerData.pricing = "flat_rate"; providerData.pricing = "flat_rate";
providerData.monthlyCostUsd = pricing.monthlyCostUsd; providerData.monthlyCostUsd = pricing.monthlyCostUsd;
@@ -293,29 +375,38 @@ function buildUsageResponse() {
outputPerMTok: pricing?.outputPerMTok, outputPerMTok: pricing?.outputPerMTok,
}; };
} }
// Provider status from in-memory tracking
const status = providerStatus[providerName] || { status: "idle", lastSeen: null, lastError: null };
providerData.status = status.status;
providerData.lastSeen = status.lastSeen;
providerData.lastError = status.lastError;
} }
// Build note // Build notes (balance notes are added in the /usage handler after balances resolve)
const notes = []; const notes = [];
if (flatRateModels > 0) { if (flatRateProviders.size > 0) {
notes.push(`Cost excludes ${flatRateModels} flat-rate model(s). Z.ai costs $10/mo regardless of usage.`); notes.push("Cost excludes flat-rate provider(s). Z.ai costs $10/mo regardless of usage.");
}
if (!balances.anthropic) {
notes.push("Anthropic has no public balance API — check console.anthropic.com");
}
if (!balances.zai) {
notes.push("Z.ai is flat-rate; no balance API available");
} }
return { return {
usageData,
providers, providers,
totalRequests,
monthlyRequests,
totalCost,
monthlyCost,
currentPeriod, currentPeriod,
notes, notes,
totals: {
totalRequests,
monthlyRequests,
todayRequests,
totalInputTokens,
totalOutputTokens,
monthlyInputTokens,
monthlyOutputTokens,
todayInputTokens,
todayOutputTokens,
totalCostUsd: Number(totalCost.toFixed(4)),
monthlyCostUsd: Number(monthlyCost.toFixed(4)),
todayCostUsd: Number(todayCost.toFixed(4)),
},
}; };
} }
@@ -473,8 +564,10 @@ async function handleMessages(req, res) {
if (inputTokens > 0 || outputTokens > 0) { if (inputTokens > 0 || outputTokens > 0) {
recordUsage(requestedModel, route.provider, actualModel, inputTokens, outputTokens); recordUsage(requestedModel, route.provider, actualModel, inputTokens, outputTokens);
} }
providerStatus[route.provider] = { status: "ok", lastSeen: new Date().toISOString(), lastError: null };
} catch (streamErr) { } catch (streamErr) {
console.error(`[proxy] Stream error from ${route.provider}:`, streamErr.message); console.error(`[proxy] Stream error from ${route.provider}:`, streamErr.message);
providerStatus[route.provider] = { status: "error", lastSeen: new Date().toISOString(), lastError: streamErr.message };
} finally { } finally {
res.end(); res.end();
} }
@@ -499,11 +592,19 @@ async function handleMessages(req, res) {
} catch {} } catch {}
} }
// Update provider status based on HTTP status
if (statusCode >= 200 && statusCode < 300) {
providerStatus[route.provider] = { status: "ok", lastSeen: new Date().toISOString(), lastError: null };
} else {
providerStatus[route.provider] = { status: "degraded", lastSeen: new Date().toISOString(), lastError: `HTTP ${statusCode}` };
}
res.writeHead(statusCode, responseHeaders); res.writeHead(statusCode, responseHeaders);
res.end(responseBody); res.end(responseBody);
} }
} catch (fetchErr) { } catch (fetchErr) {
console.error(`[proxy] Fetch error to ${route.provider} (${upstreamUrl}):`, fetchErr.message); console.error(`[proxy] Fetch error to ${route.provider} (${upstreamUrl}):`, fetchErr.message);
providerStatus[route.provider] = { status: "error", lastSeen: new Date().toISOString(), lastError: fetchErr.message };
res.writeHead(502, { "content-type": "application/json" }); res.writeHead(502, { "content-type": "application/json" });
res.end( res.end(
JSON.stringify({ JSON.stringify({
@@ -550,8 +651,7 @@ const server = createServer(async (req, res) => {
// Usage endpoint (no auth required - no sensitive data) // Usage endpoint (no auth required - no sensitive data)
if (req.method === "GET" && urlPath === "/usage") { if (req.method === "GET" && urlPath === "/usage") {
const { usageData, providers, totalRequests, monthlyRequests, totalCost, monthlyCost, currentPeriod, notes } = const { providers, currentPeriod, notes, totals } = buildUsageResponse();
buildUsageResponse();
// Fetch live balances in parallel // Fetch live balances in parallel
const balances = await fetchProviderBalances(); const balances = await fetchProviderBalances();
@@ -572,6 +672,8 @@ const server = createServer(async (req, res) => {
providers.zai.balance = null; providers.zai.balance = null;
providers.zai.pricingNote = "Flat-rate subscription, no balance API available"; providers.zai.pricingNote = "Flat-rate subscription, no balance API available";
} }
notes.push("Anthropic has no public balance API — check console.anthropic.com");
notes.push("Z.ai is flat-rate; no balance API available");
res.writeHead(200, { "content-type": "application/json" }); res.writeHead(200, { "content-type": "application/json" });
res.end( res.end(
@@ -581,10 +683,7 @@ const server = createServer(async (req, res) => {
currentPeriod, currentPeriod,
providers, providers,
totals: { totals: {
totalRequests, ...totals,
monthlyRequests,
totalCostUsd: Number(totalCost.toFixed(4)),
monthlyCostUsd: Number(monthlyCost.toFixed(4)),
note: notes.join(" ") || null, note: notes.join(" ") || null,
}, },
}) })