SPB Git forge

spb/llm-api

Public
0commits 0branches 0releases
0 Bsize
maindefault branch
—last push
3.0 KB · 90 lines typescript
Raw Blame History
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