/** * Browser-side LLM client. Speaks to a backend proxy that brokers to the * configured provider (Anthropic, OpenAI, etc.) using the same request * shape as pi-ai's normalized completions. When the proxy is unreachable * or unconfigured the client returns { configured: false } and the caller * is expected to fall back to a deterministic path. */ import { api } from "./api"; export interface LlmMessage { role: "system" | "user" | "assistant"; content: string; } export interface LlmRequest { messages: LlmMessage[]; max_tokens?: number; } export interface LlmReply { ok: true; content: string; provider?: string; } export interface LlmUnconfigured { ok: false; configured: false; reason: string; } export interface LlmError { ok: false; configured: true; error: string; } export type LlmResponse = LlmReply | LlmUnconfigured | LlmError; export const llmClient = { async chat(req: LlmRequest, signal?: AbortSignal): Promise { try { const res = await fetch(`${api.config.baseUrl}/internal/canvas-llm/chat`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${sessionStorage.getItem("fm.mc.token.v1")}`, }, body: JSON.stringify({ messages: req.messages, max_tokens: req.max_tokens ?? 512 }), signal, }); if (res.status === 404) return { ok: false, configured: false, reason: "LLM proxy not wired" }; if (res.status === 503) return { ok: false, configured: false, reason: "LLM provider not configured" }; if (!res.ok) return { ok: false, configured: true, error: `Provider error ${res.status}` }; const body = await res.json(); if (typeof body?.content !== "string") return { ok: false, configured: true, error: "Provider returned unparseable body" }; return { ok: true, content: body.content, provider: body.provider }; } catch (e) { return { ok: false, configured: false, reason: `LLM proxy unreachable: ${(e as Error).message}` }; } }, };