283 lines
13 KiB
HTML
283 lines
13 KiB
HTML
|
|
<div data-litellm-usage-card style="font-family: var(--card-font, 'Iosevka', 'SF Mono', ui-monospace, Menlo, Consolas, monospace); background:#ffffff; color:#111827; padding:14px 16px;">
|
||
|
|
<div style="display:flex; align-items:flex-start; justify-content:space-between; gap:12px; margin-bottom:10px;">
|
||
|
|
<div data-usage-subtitle style="font-size:0.86rem; line-height:1.35; color:#4b5563; font-weight:600;">Loading…</div>
|
||
|
|
<span data-usage-status style="font-size:0.8rem; line-height:1.2; font-weight:700; color:#6b7280; white-space:nowrap;">Loading…</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div data-usage-grid style="display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:12px;">
|
||
|
|
<section style="min-width:0;">
|
||
|
|
<div style="font-size:0.74rem; line-height:1.2; text-transform:uppercase; letter-spacing:0.04em; color:#6b7280;">Last 24h</div>
|
||
|
|
<div style="display:flex; align-items:flex-end; gap:6px; margin-top:4px;">
|
||
|
|
<span data-usage-tokens-24h style="font-size:2rem; line-height:0.95; font-weight:800; letter-spacing:-0.04em;">--</span>
|
||
|
|
<span style="font-size:0.9rem; line-height:1.2; font-weight:700; color:#4b5563; padding-bottom:0.22rem;">tokens</span>
|
||
|
|
</div>
|
||
|
|
<div data-usage-power-24h style="margin-top:4px; font-size:0.9rem; line-height:1.3; color:#1f2937; font-weight:700;">--</div>
|
||
|
|
<div data-usage-window-24h style="margin-top:2px; font-size:0.76rem; line-height:1.3; color:#6b7280;">--</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section data-usage-month-section style="min-width:0;">
|
||
|
|
<div style="font-size:0.74rem; line-height:1.2; text-transform:uppercase; letter-spacing:0.04em; color:#6b7280;">This Month</div>
|
||
|
|
<div style="display:flex; align-items:flex-end; gap:6px; margin-top:4px;">
|
||
|
|
<span data-usage-tokens-month style="font-size:2rem; line-height:0.95; font-weight:800; letter-spacing:-0.04em;">--</span>
|
||
|
|
<span style="font-size:0.9rem; line-height:1.2; font-weight:700; color:#4b5563; padding-bottom:0.22rem;">tokens</span>
|
||
|
|
</div>
|
||
|
|
<div data-usage-power-month style="margin-top:4px; font-size:0.9rem; line-height:1.3; color:#1f2937; font-weight:700;">--</div>
|
||
|
|
<div data-usage-window-month style="margin-top:2px; font-size:0.76rem; line-height:1.3; color:#6b7280;">--</div>
|
||
|
|
</section>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div style="margin-top:10px; font-size:0.82rem; line-height:1.35; color:#6b7280;">Updated <span data-usage-updated>--</span></div>
|
||
|
|
</div>
|
||
|
|
<script>
|
||
|
|
(() => {
|
||
|
|
const script = document.currentScript;
|
||
|
|
const root = script?.closest('[data-nanobot-card-root]');
|
||
|
|
const state = window.__nanobotGetCardState?.(script) || {};
|
||
|
|
if (!(root instanceof HTMLElement)) return;
|
||
|
|
|
||
|
|
const subtitleEl = root.querySelector('[data-usage-subtitle]');
|
||
|
|
const statusEl = root.querySelector('[data-usage-status]');
|
||
|
|
const updatedEl = root.querySelector('[data-usage-updated]');
|
||
|
|
const gridEl = root.querySelector('[data-usage-grid]');
|
||
|
|
const monthSectionEl = root.querySelector('[data-usage-month-section]');
|
||
|
|
const tokens24hEl = root.querySelector('[data-usage-tokens-24h]');
|
||
|
|
const power24hEl = root.querySelector('[data-usage-power-24h]');
|
||
|
|
const window24hEl = root.querySelector('[data-usage-window-24h]');
|
||
|
|
const tokensMonthEl = root.querySelector('[data-usage-tokens-month]');
|
||
|
|
const powerMonthEl = root.querySelector('[data-usage-power-month]');
|
||
|
|
const windowMonthEl = root.querySelector('[data-usage-window-month]');
|
||
|
|
if (!(subtitleEl instanceof HTMLElement) || !(statusEl instanceof HTMLElement) || !(updatedEl instanceof HTMLElement) || !(gridEl instanceof HTMLElement) || !(monthSectionEl instanceof HTMLElement) || !(tokens24hEl instanceof HTMLElement) || !(power24hEl instanceof HTMLElement) || !(window24hEl instanceof HTMLElement) || !(tokensMonthEl instanceof HTMLElement) || !(powerMonthEl instanceof HTMLElement) || !(windowMonthEl instanceof HTMLElement)) return;
|
||
|
|
|
||
|
|
const subtitle = typeof state.subtitle === 'string' ? state.subtitle : '';
|
||
|
|
const configuredToolName24h = typeof state.tool_name_24h === 'string'
|
||
|
|
? state.tool_name_24h.trim()
|
||
|
|
: typeof state.tool_name === 'string'
|
||
|
|
? state.tool_name.trim()
|
||
|
|
: '';
|
||
|
|
const configuredToolNameMonth = typeof state.tool_name_month === 'string'
|
||
|
|
? state.tool_name_month.trim()
|
||
|
|
: '';
|
||
|
|
const rawToolArguments24h = state && typeof state.tool_arguments_24h === 'object' && state.tool_arguments_24h && !Array.isArray(state.tool_arguments_24h)
|
||
|
|
? state.tool_arguments_24h
|
||
|
|
: state && typeof state.tool_arguments === 'object' && state.tool_arguments && !Array.isArray(state.tool_arguments)
|
||
|
|
? state.tool_arguments
|
||
|
|
: {};
|
||
|
|
const rawToolArgumentsMonth = state && typeof state.tool_arguments_month === 'object' && state.tool_arguments_month && !Array.isArray(state.tool_arguments_month)
|
||
|
|
? state.tool_arguments_month
|
||
|
|
: {};
|
||
|
|
const source24h = typeof state.source_url_24h === 'string' ? state.source_url_24h.trim() : '';
|
||
|
|
const sourceMonth = typeof state.source_url_month === 'string' ? state.source_url_month.trim() : '';
|
||
|
|
const refreshMsRaw = Number(state.refresh_ms);
|
||
|
|
const refreshMs = Number.isFinite(refreshMsRaw) && refreshMsRaw >= 60000 ? refreshMsRaw : 15 * 60 * 1000;
|
||
|
|
|
||
|
|
const tokenFormatter = new Intl.NumberFormat([], { notation: 'compact', maximumFractionDigits: 1 });
|
||
|
|
const kwhFormatter = new Intl.NumberFormat([], { minimumFractionDigits: 1, maximumFractionDigits: 1 });
|
||
|
|
const moneyFormatter = new Intl.NumberFormat([], { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||
|
|
|
||
|
|
subtitleEl.textContent = subtitle || 'LiteLLM activity vs local UPS energy';
|
||
|
|
const hasMonthSource = Boolean(
|
||
|
|
sourceMonth ||
|
||
|
|
configuredToolNameMonth ||
|
||
|
|
Object.keys(rawToolArgumentsMonth).length,
|
||
|
|
);
|
||
|
|
if (!hasMonthSource) {
|
||
|
|
monthSectionEl.style.display = 'none';
|
||
|
|
gridEl.style.gridTemplateColumns = 'minmax(0, 1fr)';
|
||
|
|
}
|
||
|
|
const updateLiveContent = (snapshot) => {
|
||
|
|
window.__nanobotSetCardLiveContent?.(script, snapshot);
|
||
|
|
};
|
||
|
|
|
||
|
|
const setStatus = (label, color) => {
|
||
|
|
statusEl.textContent = label;
|
||
|
|
statusEl.style.color = color;
|
||
|
|
};
|
||
|
|
|
||
|
|
const parseLocalTimestamp = (raw) => {
|
||
|
|
if (typeof raw !== 'string' || !raw.trim()) return null;
|
||
|
|
const match = raw.trim().match(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) ([+-]\d{2})(\d{2})$/);
|
||
|
|
if (!match) return null;
|
||
|
|
const value = `${match[1]}T${match[2]}${match[3]}:${match[4]}`;
|
||
|
|
const parsed = new Date(value);
|
||
|
|
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
||
|
|
};
|
||
|
|
|
||
|
|
const formatRangeLabel = (payload, fallbackLabel) => {
|
||
|
|
const startRaw = typeof payload?.range_start_local === 'string' ? payload.range_start_local : '';
|
||
|
|
const endRaw = typeof payload?.range_end_local === 'string' ? payload.range_end_local : '';
|
||
|
|
const start = parseLocalTimestamp(startRaw);
|
||
|
|
const end = parseLocalTimestamp(endRaw);
|
||
|
|
if (!(start instanceof Date) || Number.isNaN(start.getTime()) || !(end instanceof Date) || Number.isNaN(end.getTime())) {
|
||
|
|
return fallbackLabel;
|
||
|
|
}
|
||
|
|
return `${start.toLocaleDateString([], { month: 'short', day: 'numeric' })} to ${end.toLocaleDateString([], { month: 'short', day: 'numeric' })}`;
|
||
|
|
};
|
||
|
|
|
||
|
|
const renderSection = (elements, payload, fallbackLabel) => {
|
||
|
|
const tokens = Number(payload?.total_tokens_processed);
|
||
|
|
const kwh = Number(payload?.total_ups_kwh_in_range);
|
||
|
|
const localCost = Number(payload?.local_cost_usd_in_range);
|
||
|
|
|
||
|
|
elements.tokens.textContent = Number.isFinite(tokens) ? tokenFormatter.format(tokens) : '--';
|
||
|
|
elements.power.textContent =
|
||
|
|
Number.isFinite(kwh) && Number.isFinite(localCost)
|
||
|
|
? `${kwhFormatter.format(kwh)} kWh · ${moneyFormatter.format(localCost)}`
|
||
|
|
: Number.isFinite(kwh)
|
||
|
|
? `${kwhFormatter.format(kwh)} kWh`
|
||
|
|
: '--';
|
||
|
|
elements.window.textContent = formatRangeLabel(payload, fallbackLabel);
|
||
|
|
};
|
||
|
|
|
||
|
|
const blankSection = (elements, fallbackLabel) => {
|
||
|
|
elements.tokens.textContent = '--';
|
||
|
|
elements.power.textContent = '--';
|
||
|
|
elements.window.textContent = fallbackLabel;
|
||
|
|
};
|
||
|
|
|
||
|
|
const shellEscape = (value) => `'${String(value ?? '').replace(/'/g, `'\"'\"'`)}'`;
|
||
|
|
|
||
|
|
const buildLegacyExecCommand = (rawUrl) => {
|
||
|
|
if (typeof rawUrl !== 'string' || !rawUrl.startsWith('/script/proxy/')) return '';
|
||
|
|
const [pathPart, queryPart = ''] = rawUrl.split('?', 2);
|
||
|
|
const relativeScript = pathPart.slice('/script/proxy/'.length).replace(/^\/+/, '');
|
||
|
|
if (!relativeScript) return '';
|
||
|
|
const params = new URLSearchParams(queryPart);
|
||
|
|
const args = params.getAll('arg').map((value) => value.trim()).filter(Boolean);
|
||
|
|
const scriptPath = `$HOME/.nanobot/workspace/${relativeScript}`;
|
||
|
|
return `python3 ${scriptPath}${args.length ? ` ${args.map(shellEscape).join(' ')}` : ''}`;
|
||
|
|
};
|
||
|
|
|
||
|
|
const resolveToolCall = (toolName, toolArguments, legacySourceUrl) => {
|
||
|
|
if (toolName) {
|
||
|
|
return {
|
||
|
|
toolName,
|
||
|
|
toolArguments,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
const legacyCommand = buildLegacyExecCommand(legacySourceUrl);
|
||
|
|
if (!legacyCommand) return null;
|
||
|
|
return {
|
||
|
|
toolName: 'exec',
|
||
|
|
toolArguments: { command: legacyCommand },
|
||
|
|
};
|
||
|
|
};
|
||
|
|
|
||
|
|
const loadPayload = async (toolCall) => {
|
||
|
|
if (!toolCall) throw new Error('Missing tool_name/tool_arguments');
|
||
|
|
if (!window.__nanobotCallToolAsync) throw new Error('Async tool helper unavailable');
|
||
|
|
const toolResult = await window.__nanobotCallToolAsync(
|
||
|
|
toolCall.toolName,
|
||
|
|
toolCall.toolArguments,
|
||
|
|
{ timeoutMs: 180000 },
|
||
|
|
);
|
||
|
|
if (toolResult && toolResult.parsed && typeof toolResult.parsed === 'object' && !Array.isArray(toolResult.parsed)) {
|
||
|
|
return toolResult.parsed;
|
||
|
|
}
|
||
|
|
if (typeof toolResult?.content === 'string' && toolResult.content.trim()) {
|
||
|
|
const parsed = JSON.parse(toolResult.content);
|
||
|
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||
|
|
return parsed;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
throw new Error('Tool returned invalid JSON');
|
||
|
|
};
|
||
|
|
|
||
|
|
const refresh = async () => {
|
||
|
|
setStatus('Refreshing', '#6b7280');
|
||
|
|
try {
|
||
|
|
const toolCall24h = resolveToolCall(configuredToolName24h, rawToolArguments24h, source24h);
|
||
|
|
const toolCallMonth = hasMonthSource
|
||
|
|
? resolveToolCall(configuredToolNameMonth, rawToolArgumentsMonth, sourceMonth)
|
||
|
|
: null;
|
||
|
|
const jobs = [loadPayload(toolCall24h), hasMonthSource ? loadPayload(toolCallMonth) : Promise.resolve(null)];
|
||
|
|
const results = await Promise.allSettled(jobs);
|
||
|
|
const twentyFourHour = results[0].status === 'fulfilled' ? results[0].value : null;
|
||
|
|
const month = hasMonthSource && results[1].status === 'fulfilled' ? results[1].value : null;
|
||
|
|
|
||
|
|
if (twentyFourHour) {
|
||
|
|
renderSection(
|
||
|
|
{ tokens: tokens24hEl, power: power24hEl, window: window24hEl },
|
||
|
|
twentyFourHour,
|
||
|
|
'Last 24 hours',
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
blankSection(
|
||
|
|
{ tokens: tokens24hEl, power: power24hEl, window: window24hEl },
|
||
|
|
'Last 24 hours',
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (hasMonthSource && month) {
|
||
|
|
renderSection(
|
||
|
|
{ tokens: tokensMonthEl, power: powerMonthEl, window: windowMonthEl },
|
||
|
|
month,
|
||
|
|
'This month',
|
||
|
|
);
|
||
|
|
} else if (hasMonthSource) {
|
||
|
|
blankSection(
|
||
|
|
{ tokens: tokensMonthEl, power: powerMonthEl, window: windowMonthEl },
|
||
|
|
'This month',
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const successCount = [twentyFourHour, month].filter(Boolean).length;
|
||
|
|
const expectedCount = hasMonthSource ? 2 : 1;
|
||
|
|
if (successCount === expectedCount) {
|
||
|
|
setStatus('Live', '#047857');
|
||
|
|
} else if (successCount === 1) {
|
||
|
|
setStatus('Partial', '#b45309');
|
||
|
|
} else {
|
||
|
|
setStatus('Unavailable', '#b91c1c');
|
||
|
|
}
|
||
|
|
|
||
|
|
const updatedText = new Date().toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||
|
|
updatedEl.textContent = updatedText;
|
||
|
|
updateLiveContent({
|
||
|
|
kind: 'litellm_ups_usage',
|
||
|
|
subtitle: subtitleEl.textContent || null,
|
||
|
|
status: statusEl.textContent || null,
|
||
|
|
updated_at: updatedText,
|
||
|
|
last_24h: twentyFourHour
|
||
|
|
? {
|
||
|
|
total_tokens_processed: Number(twentyFourHour.total_tokens_processed) || 0,
|
||
|
|
total_ups_kwh_in_range: Number(twentyFourHour.total_ups_kwh_in_range) || 0,
|
||
|
|
local_cost_usd_in_range: Number(twentyFourHour.local_cost_usd_in_range) || 0,
|
||
|
|
range_start_local: twentyFourHour.range_start_local || null,
|
||
|
|
range_end_local: twentyFourHour.range_end_local || null,
|
||
|
|
}
|
||
|
|
: null,
|
||
|
|
this_month: month
|
||
|
|
? {
|
||
|
|
total_tokens_processed: Number(month.total_tokens_processed) || 0,
|
||
|
|
total_ups_kwh_in_range: Number(month.total_ups_kwh_in_range) || 0,
|
||
|
|
local_cost_usd_in_range: Number(month.local_cost_usd_in_range) || 0,
|
||
|
|
range_start_local: month.range_start_local || null,
|
||
|
|
range_end_local: month.range_end_local || null,
|
||
|
|
}
|
||
|
|
: null,
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
const errorText = String(error);
|
||
|
|
blankSection({ tokens: tokens24hEl, power: power24hEl, window: window24hEl }, 'Last 24 hours');
|
||
|
|
if (hasMonthSource) {
|
||
|
|
blankSection({ tokens: tokensMonthEl, power: powerMonthEl, window: windowMonthEl }, 'This month');
|
||
|
|
}
|
||
|
|
updatedEl.textContent = errorText;
|
||
|
|
setStatus('Unavailable', '#b91c1c');
|
||
|
|
updateLiveContent({
|
||
|
|
kind: 'litellm_ups_usage',
|
||
|
|
subtitle: subtitleEl.textContent || null,
|
||
|
|
status: 'Unavailable',
|
||
|
|
updated_at: errorText,
|
||
|
|
last_24h: null,
|
||
|
|
this_month: null,
|
||
|
|
error: errorText,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
void refresh();
|
||
|
|
window.setInterval(() => { void refresh(); }, refreshMs);
|
||
|
|
})();
|
||
|
|
</script>
|