SPB Git forge
7commits 1branches 0releases
229.0 KBsize
maindefault branch
12 days agolast push
TypeScript 91.8% HTML 3.2% JavaScript 3% SQL 1.4% CSS 0.7%
3.8 KB · 79 lines typescript
Raw Blame History
1import Anthropic from "@anthropic-ai/sdk";2import { createLogger, type AppConfig } from "@src/shared";34const log = createLogger("llm");56/** Tier 3/4 model access (§49–§50). One tiny interface so the planner never depends on a vendor SDK. */7export interface LlmClient {8  readonly name: string;9  complete(opts: { system: string; user: string; maxTokens?: number }): Promise<{ text: string; input_tokens: number; output_tokens: number }>;10}1112class AnthropicClient implements LlmClient {13  readonly name: string;14  private client = new Anthropic();15  constructor(private readonly model: string) {16    this.name = `anthropic:${model}`;17  }18  async complete(opts: { system: string; user: string; maxTokens?: number }) {19    const res = await this.client.messages.create({20      model: this.model,21      max_tokens: opts.maxTokens ?? 2000,22      thinking: { type: "adaptive" },23      output_config: { effort: "low" }, // planning over a compact action list is routine work24      system: [{ type: "text", text: opts.system, cache_control: { type: "ephemeral" } }],25      messages: [{ role: "user", content: opts.user }],26    });27    if (res.stop_reason === "refusal") {28      log.warn("planner request refused", { category: res.stop_details?.category });29      return { text: "", input_tokens: res.usage.input_tokens, output_tokens: res.usage.output_tokens };30    }31    const text = res.content.filter((b) => b.type === "text").map((b) => (b as { text: string }).text).join("\n");32    return { text, input_tokens: res.usage.input_tokens, output_tokens: res.usage.output_tokens };33  }34}3536/** OpenAI-compatible local endpoint (llm-api.io on the MacLustr cluster, llama.cpp, MLX…). */37class LocalOpenAiCompatibleClient implements LlmClient {38  readonly name: string;39  constructor(private readonly baseUrl: string, private readonly model: string, private readonly apiKey?: string) {40    this.name = `local:${model}`;41  }42  async complete(opts: { system: string; user: string; maxTokens?: number }) {43    const res = await fetch(`${this.baseUrl.replace(/\/$/, "")}/chat/completions`, {44      method: "POST",45      headers: { "content-type": "application/json", ...(this.apiKey ? { authorization: `Bearer ${this.apiKey}` } : {}) },46      body: JSON.stringify({ model: this.model, max_tokens: opts.maxTokens ?? 1200, temperature: 0.2, messages: [{ role: "system", content: opts.system }, { role: "user", content: opts.user }] }),47    });48    if (!res.ok) throw new Error(`local llm ${res.status}: ${(await res.text()).slice(0, 200)}`);49    const data = (await res.json()) as { choices?: { message?: { content?: string } }[]; usage?: { prompt_tokens?: number; completion_tokens?: number } };50    return { text: data.choices?.[0]?.message?.content ?? "", input_tokens: data.usage?.prompt_tokens ?? 0, output_tokens: data.usage?.completion_tokens ?? 0 };51  }52}5354export function createLlmClient(cfg: AppConfig): LlmClient | undefined {55  if (cfg.llmProvider === "anthropic") return new AnthropicClient(cfg.llmModel);56  if (cfg.llmProvider === "local") {57    if (!cfg.localLlmUrl || !cfg.localLlmModel) {58      log.warn("SRC_LLM_PROVIDER=local requires SRC_LOCAL_LLM_URL and SRC_LOCAL_LLM_MODEL — falling back to heuristic planner");59      return undefined;60    }61    return new LocalOpenAiCompatibleClient(cfg.localLlmUrl, cfg.localLlmModel, cfg.localLlmKey);62  }63  return undefined;64}6566/** Extract the first JSON object from a model reply (tolerates fences and prose). */67export function extractJson(text: string): Record<string, unknown> | undefined {68  const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/);69  const body = fence?.[1] ?? text;70  const start = body.indexOf("{");71  const end = body.lastIndexOf("}");72  if (start < 0 || end <= start) return undefined;73  try {74    return JSON.parse(body.slice(start, end + 1)) as Record<string, unknown>;75  } catch {76    return undefined;77  }78}79