1"use client";23export class ApiError extends Error {4 status: number;5 code: string;6 detail: unknown;7 constructor(status: number, message: string, code = "ERROR", detail?: unknown) {8 super(message);9 this.status = status;10 this.code = code;11 this.detail = detail;12 }13}1415async function request<T>(method: string, path: string, body?: unknown, init?: RequestInit): Promise<T> {16 const headers: Record<string, string> = { Accept: "application/json" };17 if (method !== "GET" && method !== "HEAD") headers["X-LLM-CSRF"] = "1";18 if (body !== undefined) headers["Content-Type"] = "application/json";19 const res = await fetch(path, {20 method,21 headers,22 credentials: "same-origin",23 body: body !== undefined ? JSON.stringify(body) : undefined,24 ...init,25 });26 const text = await res.text();27 let data: unknown = null;28 try {29 data = text ? JSON.parse(text) : null;30 } catch {31 data = text;32 }33 if (!res.ok) {34 const err = (data as { error?: { message?: string; code?: string } })?.error;35 if (res.status === 401 && typeof window !== "undefined" && !location.pathname.startsWith("/login")) {36 location.href = `/login?next=${encodeURIComponent(location.pathname)}`;37 }38 throw new ApiError(res.status, err?.message || `HTTP ${res.status}`, err?.code || "ERROR", data);39 }40 return data as T;41}4243export const api = {44 get: <T,>(path: string) => request<T>("GET", path),45 post: <T,>(path: string, body?: unknown) => request<T>("POST", path, body),46 put: <T,>(path: string, body?: unknown) => request<T>("PUT", path, body),47 patch: <T,>(path: string, body?: unknown) => request<T>("PATCH", path, body),48 del: <T,>(path: string, body?: unknown) => request<T>("DELETE", path, body),49};5051/** Stream an OpenAI chat completion from the local API using the admin session. */52export async function* streamChat(body: Record<string, unknown>, signal?: AbortSignal): AsyncGenerator<Record<string, unknown>> {53 const res = await fetch("/v1/chat/completions", {54 method: "POST",55 headers: { "Content-Type": "application/json", "X-LLM-CSRF": "1" },56 credentials: "same-origin",57 body: JSON.stringify({ ...body, stream: true }),58 signal,59 });60 if (!res.ok || !res.body) {61 const t = await res.text();62 let msg = t;63 try {64 msg = JSON.parse(t).error?.message || t;65 } catch {}66 throw new ApiError(res.status, msg);67 }68 const reader = res.body.getReader();69 const dec = new TextDecoder();70 let buf = "";71 while (true) {72 const { value, done } = await reader.read();73 if (done) break;74 buf += dec.decode(value, { stream: true });75 let idx: number;76 while ((idx = buf.indexOf("\n\n")) >= 0) {77 const chunk = buf.slice(0, idx);78 buf = buf.slice(idx + 2);79 for (const line of chunk.split("\n")) {80 if (!line.startsWith("data: ")) continue;81 const payload = line.slice(6).trim();82 if (payload === "[DONE]") return;83 try {84 yield JSON.parse(payload);85 } catch {}86 }87 }88 }89}90