src/lib/llmClient.ts: browser POSTs to /internal/canvas-llm/chat with
{messages, max_tokens}. Mirrors the normalized request shape used by
earendil-works/pi's @earendil-works/pi-ai. Returns:
- ok:true with content+provider when configured and successful
- ok:false, configured:false when proxy 404/503/unreachable
- ok:false, configured:true when provider returned an error
routeAgentInput now falls back to the LLM with a system prompt that
includes the user identity, tool catalogue, and the top-3 vault recall
hits when no deterministic tool matches. When the proxy is not
configured the deterministic vault-hint reply is used (no regression
in the demo tier).
64 lines
2.0 KiB
TypeScript
64 lines
2.0 KiB
TypeScript
/**
|
|
* 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<LlmResponse> {
|
|
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}` };
|
|
}
|
|
},
|
|
};
|