import type { AuthSessionResponse, PricingEntry, PricingResponse, StatusResponse, UsageAnalysisResponse, UsageEventFilterOptionsResponse, UsedModelsResponse, UsageIdentitiesResponse, UsageEventsResponse, UsageOverviewResponse } from './types' export class ApiError extends Error { status: number constructor(message: string, status: number) { super(message) this.name = 'ApiError' this.status = status } } const APP_BASE_PATH_PLACEHOLDER = '__APP_BASE_PATH__' declare global { interface Window { __APP_BASE_PATH__?: string } } function normalizeBasePath(basePath: string | undefined): string { if (!basePath || basePath === '/' || basePath === APP_BASE_PATH_PLACEHOLDER) { return '' } return basePath.endsWith('/') ? basePath.slice(0, -1) : basePath } export function apiPath(path: string): string { const normalizedPath = path.startsWith('/') ? path : `/${path}` return `${normalizeBasePath(window.__APP_BASE_PATH__)}/api/v1${normalizedPath}` } async function parseApiError(response: Response, fallback: string): Promise { let message = fallback try { const payload = await response.json() as { error?: string } if (payload.error) { message = payload.error } } catch { // ignore invalid error payloads } throw new ApiError(message, response.status) } async function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise { return fetch(input, { credentials: 'include', ...init, }) } export async function getSession(signal?: AbortSignal): Promise { const response = await apiFetch(apiPath('/auth/session'), { signal }) if (!response.ok) { await parseApiError(response, `Failed to load auth session: ${response.status}`) } return response.json() } export async function login(password: string): Promise { const response = await apiFetch(apiPath('/auth/login'), { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ password }), }) if (!response.ok) { await parseApiError(response, `Failed to login: ${response.status}`) } } export async function fetchUsageOverview(range: string, start?: string, end?: string, signal?: AbortSignal): Promise { const params = new URLSearchParams() params.set('range', range) if (start) { params.set('start', start) } if (end) { params.set('end', end) } const query = params.toString() const response = await apiFetch(`${apiPath('/usage/overview')}${query ? `?${query}` : ''}`, { signal }) if (!response.ok) { await parseApiError(response, `Failed to load usage overview: ${response.status}`) } return response.json() } export interface FetchUsageEventsOptions { page?: number pageSize?: number model?: string source?: string result?: string } export async function fetchUsageEventFilterOptions(signal?: AbortSignal): Promise { const response = await apiFetch(apiPath('/usage/events/filters'), { signal }) if (!response.ok) { await parseApiError(response, `Failed to load usage event filters: ${response.status}`) } return response.json() } export async function fetchUsageEvents(range: string, start?: string, end?: string, signal?: AbortSignal, options?: FetchUsageEventsOptions): Promise { const params = new URLSearchParams() params.set('range', range) if (start) { params.set('start', start) } if (end) { params.set('end', end) } if (typeof options?.page === 'number' && Number.isFinite(options.page) && options.page > 0) { params.set('page', String(Math.floor(options.page))) } if (typeof options?.pageSize === 'number' && Number.isFinite(options.pageSize) && options.pageSize > 0) { params.set('page_size', String(Math.floor(options.pageSize))) } const model = options?.model?.trim() if (model) { params.set('model', model) } const source = options?.source?.trim() if (source) { params.set('source', source) } const result = options?.result?.trim() if (result) { params.set('result', result) } const query = params.toString() const response = await apiFetch(`${apiPath('/usage/events')}${query ? `?${query}` : ''}`, { signal }) if (!response.ok) { await parseApiError(response, `Failed to load usage events: ${response.status}`) } return response.json() } export async function fetchUsageIdentities(signal?: AbortSignal): Promise { const response = await apiFetch(apiPath('/usage/identities'), { signal }) if (!response.ok) { await parseApiError(response, `Failed to load usage identities: ${response.status}`) } return response.json() } export async function fetchUsageAnalysis(range: string, start?: string, end?: string, signal?: AbortSignal): Promise { const params = new URLSearchParams() params.set('range', range) if (start) { params.set('start', start) } if (end) { params.set('end', end) } const query = params.toString() const response = await apiFetch(`${apiPath('/usage/analysis')}${query ? `?${query}` : ''}`, { signal }) if (!response.ok) { await parseApiError(response, `Failed to load usage analysis: ${response.status}`) } return response.json() } export async function fetchUsedModels(signal?: AbortSignal): Promise { const response = await apiFetch(apiPath('/models/used'), { signal }) if (!response.ok) { await parseApiError(response, `Failed to load used models: ${response.status}`) } return response.json() } export async function fetchStatus(signal?: AbortSignal): Promise { const response = await apiFetch(apiPath('/status'), { signal }) if (!response.ok) { await parseApiError(response, `Failed to load status: ${response.status}`) } return response.json() } export async function triggerSync(signal?: AbortSignal): Promise { const response = await apiFetch(apiPath('/sync'), { method: 'POST', signal }) if (!response.ok) { await parseApiError(response, `Failed to start sync: ${response.status}`) } return response.json() } export async function fetchPricing(signal?: AbortSignal): Promise { const response = await apiFetch(apiPath('/pricing'), { signal }) if (!response.ok) { await parseApiError(response, `Failed to load pricing: ${response.status}`) } return response.json() } export async function updatePricing(model: string, pricing: Omit): Promise { const response = await apiFetch(apiPath('/pricing'), { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ model, ...pricing }), }) if (!response.ok) { await parseApiError(response, `Failed to update pricing: ${response.status}`) } return response.json() } export async function deletePricing(model: string): Promise { const params = new URLSearchParams({ model }) const response = await apiFetch(`${apiPath('/pricing')}?${params.toString()}`, { method: 'DELETE', }) if (!response.ok) { await parseApiError(response, `Failed to delete pricing: ${response.status}`) } }