Spaces:
Running
Running
| 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<never> { | |
| 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<Response> { | |
| return fetch(input, { | |
| credentials: 'include', | |
| ...init, | |
| }) | |
| } | |
| export async function getSession(signal?: AbortSignal): Promise<AuthSessionResponse> { | |
| 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<void> { | |
| 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<UsageOverviewResponse> { | |
| 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<UsageEventFilterOptionsResponse> { | |
| 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<UsageEventsResponse> { | |
| 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<UsageIdentitiesResponse> { | |
| 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<UsageAnalysisResponse> { | |
| 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<UsedModelsResponse> { | |
| 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<StatusResponse> { | |
| 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<StatusResponse> { | |
| 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<PricingResponse> { | |
| 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<PricingEntry, 'model'>): Promise<PricingEntry> { | |
| 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<void> { | |
| 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}`) | |
| } | |
| } | |