import { writable } from "svelte/store"; import type { Model, ActivityLogEntry, VersionInfo, LogData, APIEventEnvelope, ReqRespCapture, InFlightStats, PerformanceResponse, } from "../lib/types"; import { connectionState } from "./theme"; const LOG_LENGTH_LIMIT = 1024 * 100; /* 100KB of log data */ // Stores export const models = writable([]); export const proxyLogs = writable(""); export const upstreamLogs = writable(""); export const metrics = writable([]); export const inFlightRequests = writable(0); export const versionInfo = writable({ build_date: "unknown", commit: "unknown", version: "unknown", }); let apiEventSource: EventSource | null = null; function appendLog(newData: string, store: typeof proxyLogs | typeof upstreamLogs): void { store.update((prev) => { const updatedLog = prev + newData; return updatedLog.length > LOG_LENGTH_LIMIT ? updatedLog.slice(-LOG_LENGTH_LIMIT) : updatedLog; }); } export function enableAPIEvents(enabled: boolean): void { if (!enabled) { apiEventSource?.close(); apiEventSource = null; metrics.set([]); inFlightRequests.set(0); return; } let retryCount = 0; const initialDelay = 1000; // 1 second const connect = () => { apiEventSource?.close(); apiEventSource = new EventSource("/api/events"); connectionState.set("connecting"); apiEventSource.onopen = () => { // Clear everything on connect to keep things in sync proxyLogs.set(""); upstreamLogs.set(""); metrics.set([]); inFlightRequests.set(0); models.set([]); retryCount = 0; connectionState.set("connected"); }; apiEventSource.onmessage = (e: MessageEvent) => { try { const message = JSON.parse(e.data) as APIEventEnvelope; switch (message.type) { case "modelStatus": { const newModels = JSON.parse(message.data) as Model[]; // Sort models by name and id newModels.sort((a, b) => { return (a.name + a.id).localeCompare(b.name + b.id, undefined, { numeric: true }); }); models.set(newModels); break; } case "logData": { const logData = JSON.parse(message.data) as LogData; switch (logData.source) { case "proxy": appendLog(logData.data, proxyLogs); break; case "upstream": appendLog(logData.data, upstreamLogs); break; } break; } case "metrics": { const newMetrics = JSON.parse(message.data) as ActivityLogEntry[]; metrics.update((prevMetrics) => [...newMetrics, ...prevMetrics]); break; } case "inflight": { const stats = JSON.parse(message.data) as InFlightStats; inFlightRequests.set(stats.total ?? 0); break; } } } catch (err) { console.error(e.data, err); } }; apiEventSource.onerror = () => { apiEventSource?.close(); retryCount++; const delay = Math.min(initialDelay * Math.pow(2, retryCount - 1), 5000); connectionState.set("disconnected"); setTimeout(connect, delay); }; }; connect(); } // Fetch version info when connected connectionState.subscribe(async (status) => { if (status === "connected") { try { const response = await fetch("/api/version"); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data: VersionInfo = await response.json(); versionInfo.set(data); } catch (error) { console.error(error); } } }); export async function listModels(): Promise { try { const response = await fetch("/api/models/"); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); return data || []; } catch (error) { console.error("Failed to fetch models:", error); return []; } } export async function unloadAllModels(): Promise { try { const response = await fetch(`/api/models/unload`, { method: "POST", }); if (!response.ok) { throw new Error(`Failed to unload models: ${response.status}`); } } catch (error) { console.error("Failed to unload models:", error); throw error; } } export async function unloadSingleModel(model: string): Promise { try { const response = await fetch(`/api/models/unload/${model}`, { method: "POST", }); if (!response.ok) { throw new Error(`Failed to unload model: ${response.status}`); } } catch (error) { console.error("Failed to unload model", model, error); throw error; } } export async function loadModel(model: string, signal?: AbortSignal): Promise { try { const response = await fetch(`/upstream/${model}/?_=${Date.now()}`, { method: "GET", signal, }); if (!response.ok) { throw new Error(`Failed to load model: ${response.status}`); } } catch (error) { if (error instanceof DOMException && error.name === "AbortError") { return; } console.error("Failed to load model:", error); throw error; } } export async function getCapture(id: number): Promise { try { const response = await fetch(`/api/captures/${id}`); if (response.status === 404) { return null; } if (!response.ok) { throw new Error(`Failed to fetch capture: ${response.status}`); } return await response.json(); } catch (error) { console.error("Failed to fetch capture:", error); return null; } } export async function fetchPerformance(after?: string): Promise { try { const url = after ? `/api/performance?after=${encodeURIComponent(after)}` : "/api/performance"; const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { console.error("Failed to fetch performance data:", error); return null; } }