From c8777b45ffeb310002cacb10d0fd4443180154ab Mon Sep 17 00:00:00 2001 From: William Stuckey Date: Fri, 27 Mar 2026 10:59:28 -0500 Subject: [PATCH] updated api return --- .vscode/settings.json | 9 +++ proxy.mjs | 157 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 137 insertions(+), 29 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b3132b2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "workbench.colorCustomizations": { + "titleBar.activeBackground": "#083846", + "titleBar.activeForeground": "#ffffff", + "titleBar.inactiveBackground": "#0c323d", + "titleBar.inactiveForeground": "#ffffffcc", + "titleBar.border": "#0c1e23" + } +} \ No newline at end of file diff --git a/proxy.mjs b/proxy.mjs index 968583a..b52316e 100644 --- a/proxy.mjs +++ b/proxy.mjs @@ -77,6 +77,13 @@ const MODEL_ROUTES = { // Default route for unknown models 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 ----------------------------------------------------------- function getCurrentPeriod() { @@ -84,6 +91,10 @@ function getCurrentPeriod() { return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`; } +function getCurrentDate() { + return new Date().toISOString().slice(0, 10); // "2026-03-27" +} + function loadUsageData() { if (!existsSync(USAGE_DATA_PATH)) { return { @@ -117,6 +128,7 @@ function saveUsageData(data) { function recordUsage(requestedModel, provider, actualModel, inputTokens, outputTokens) { const usageData = loadUsageData(); const currentPeriod = getCurrentPeriod(); + const todayStr = getCurrentDate(); if (!usageData.models[requestedModel]) { usageData.models[requestedModel] = { @@ -124,6 +136,7 @@ function recordUsage(requestedModel, provider, actualModel, inputTokens, outputT actualModel, total: { 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 }; } + // 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 modelData.total.inputTokens += inputTokens || 0; modelData.total.outputTokens += outputTokens || 0; @@ -144,6 +162,11 @@ function recordUsage(requestedModel, provider, actualModel, inputTokens, outputT modelData.monthly.outputTokens += outputTokens || 0; 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(); saveUsageData(usageData); } @@ -228,10 +251,20 @@ function calculateCost(provider, inputTokens, outputTokens) { // --- 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() { const usageData = loadUsageData(); - const balances = fetchProviderBalances(); // Start fetching but don't await const currentPeriod = getCurrentPeriod(); + const todayStr = getCurrentDate(); const providers = { anthropic: { balance: null, models: {} }, @@ -241,19 +274,33 @@ function buildUsageResponse() { let totalRequests = 0; let monthlyRequests = 0; + let todayRequests = 0; let totalCost = 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 || {})) { const providerName = modelData.provider; 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] = { routesTo: actualModel || requestedModel, + pricing: buildModelPricing(providerName), total: { inputTokens: total.inputTokens, outputTokens: total.outputTokens, @@ -267,23 +314,58 @@ function buildUsageResponse() { requests: monthly.requests, 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; - 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 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; - else flatRateModels++; + else flatRateProviders.add(providerName); 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)) { - const pricing = config.pricing?.[providerName] || DEFAULT_PRICING[providerName]; + const pricing = getProviderPricing(providerName); if (pricing?.type === "flat") { providerData.pricing = "flat_rate"; providerData.monthlyCostUsd = pricing.monthlyCostUsd; @@ -293,29 +375,38 @@ function buildUsageResponse() { 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 = []; - if (flatRateModels > 0) { - notes.push(`Cost excludes ${flatRateModels} flat-rate model(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"); + if (flatRateProviders.size > 0) { + notes.push("Cost excludes flat-rate provider(s). Z.ai costs $10/mo regardless of usage."); } return { - usageData, providers, - totalRequests, - monthlyRequests, - totalCost, - monthlyCost, currentPeriod, 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) { recordUsage(requestedModel, route.provider, actualModel, inputTokens, outputTokens); } + providerStatus[route.provider] = { status: "ok", lastSeen: new Date().toISOString(), lastError: null }; } catch (streamErr) { console.error(`[proxy] Stream error from ${route.provider}:`, streamErr.message); + providerStatus[route.provider] = { status: "error", lastSeen: new Date().toISOString(), lastError: streamErr.message }; } finally { res.end(); } @@ -499,11 +592,19 @@ async function handleMessages(req, res) { } 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.end(responseBody); } } catch (fetchErr) { 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.end( JSON.stringify({ @@ -550,8 +651,7 @@ const server = createServer(async (req, res) => { // Usage endpoint (no auth required - no sensitive data) if (req.method === "GET" && urlPath === "/usage") { - const { usageData, providers, totalRequests, monthlyRequests, totalCost, monthlyCost, currentPeriod, notes } = - buildUsageResponse(); + const { providers, currentPeriod, notes, totals } = buildUsageResponse(); // Fetch live balances in parallel const balances = await fetchProviderBalances(); @@ -572,6 +672,8 @@ const server = createServer(async (req, res) => { providers.zai.balance = null; 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.end( @@ -581,10 +683,7 @@ const server = createServer(async (req, res) => { currentPeriod, providers, totals: { - totalRequests, - monthlyRequests, - totalCostUsd: Number(totalCost.toFixed(4)), - monthlyCostUsd: Number(monthlyCost.toFixed(4)), + ...totals, note: notes.join(" ") || null, }, })