Files
GopherGate/static/js/api.js
hobokenchicken db5824f0fb
Some checks failed
CI / Check (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Release Build (push) Has been cancelled
feat: add cache token tracking and cache-aware cost calculation
Track cache_read_tokens and cache_write_tokens end-to-end: parse from
provider responses (OpenAI, DeepSeek, Grok, Gemini), persist to SQLite,
apply cache-aware pricing from the model registry, and surface in API
responses and the dashboard.

- Add cache fields to ProviderResponse, StreamUsage, RequestLog structs
- Parse cached_tokens (OpenAI/Grok), prompt_cache_hit/miss (DeepSeek),
  cachedContentTokenCount (Gemini) from provider responses
- Send stream_options.include_usage for streaming; capture real usage
  from final SSE chunk in AggregatingStream
- ALTER TABLE migration for cache_read_tokens/cache_write_tokens columns
- Cache-aware cost formula using registry cache_read/cache_write rates
- Update Provider trait calculate_cost signature across all providers
- Add cache_read_tokens/cache_write_tokens to Usage API response
- Dashboard: cache hit rate card, cache columns in pricing and usage
  tables, cache token aggregation in SQL queries
- Remove API debug panel and verbose console logging from api.js
- Bump static asset cache-bust to v5
2026-03-02 14:45:21 -05:00

105 lines
2.7 KiB
JavaScript

// Unified API client for the dashboard
class ApiClient {
constructor() {
this.baseUrl = '/api';
}
async request(path, options = {}) {
const url = `${this.baseUrl}${path}`;
const headers = {
'Content-Type': 'application/json',
...options.headers
};
// Add auth token if available
if (window.authManager && window.authManager.token) {
headers['Authorization'] = `Bearer ${window.authManager.token}`;
}
const response = await fetch(url, {
...options,
headers
});
const text = await response.text();
let result;
try {
result = JSON.parse(text);
} catch (parseErr) {
throw new Error(`JSON parse failed for ${url}: ${parseErr.message}`);
}
if (!response.ok || !result.success) {
throw new Error(result.error || `HTTP error! status: ${response.status}`);
}
return result.data;
}
async get(path) {
return this.request(path, { method: 'GET' });
}
async post(path, body) {
return this.request(path, {
method: 'POST',
body: JSON.stringify(body)
});
}
async put(path, body) {
return this.request(path, {
method: 'PUT',
body: JSON.stringify(body)
});
}
async delete(path) {
return this.request(path, { method: 'DELETE' });
}
// Helper for formatting large numbers
formatNumber(num) {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
}
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString();
}
// Helper for formatting currency
formatCurrency(amount) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 4
}).format(amount);
}
// Helper for relative time
formatTimeAgo(dateStr) {
if (!dateStr) return 'Never';
const date = luxon.DateTime.fromISO(dateStr);
return date.toRelative();
}
}
window.api = new ApiClient();
// Helper: waits until chartManager is available (defensive against load-order edge cases)
window.waitForChartManager = async (timeoutMs = 3000) => {
if (window.chartManager) return window.chartManager;
const start = Date.now();
while (Date.now() - start < timeoutMs) {
await new Promise(r => setTimeout(r, 50));
if (window.chartManager) return window.chartManager;
}
console.warn('chartManager not available after', timeoutMs, 'ms');
return null;
};