updated api return
This commit is contained in:
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"workbench.colorCustomizations": {
|
||||||
|
"titleBar.activeBackground": "#083846",
|
||||||
|
"titleBar.activeForeground": "#ffffff",
|
||||||
|
"titleBar.inactiveBackground": "#0c323d",
|
||||||
|
"titleBar.inactiveForeground": "#ffffffcc",
|
||||||
|
"titleBar.border": "#0c1e23"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user