// Shared "talk to the configured agentic model" helper. Resolves whichever brain is // active (Nemotron / llama.cpp+OpenBMB / HF) and sends an OpenAI-compatible chat // request, so the per-agent fan-out reasons with the SAME agentic model as the synthesiser. import { stripReasoning } from "./model-prompt.mjs"; const JSON_GRAMMAR = `root ::= "{" ws members? "}" ws members ::= pair ("," ws pair)* pair ::= string ws ":" ws value value ::= object | array | string | number | "true" | "false" | "null" object ::= "{" ws members? "}" ws array ::= "[" ws (value ("," ws value)*)? "]" ws string ::= "\\"" ( [^"\\\\] | "\\\\" ["\\\\/bfnrt] )* "\\"" ws number ::= "-"? ([0-9] | [1-9] [0-9]*) ("." [0-9]+)? ws ws ::= [ \\t\\n]*`; export function resolveActiveProvider(env = process.env) { const raw = (env.AGENT_MODEL_PROVIDER || env.AGENT_PROVIDER || "auto").toLowerCase(); const has = { nvidia: Boolean(env.NVIDIA_API_KEY || env.NGC_API_KEY), llamacpp: env.LLAMACPP_ENABLED === "1", hf: Boolean(env.HF_TOKEN || env.HUGGINGFACEHUB_API_TOKEN || env.HUGGING_FACE_HUB_TOKEN), }; if (raw !== "auto" && raw !== "") return has[raw] ? raw : firstAvailable(has); return firstAvailable(has); } function firstAvailable(has) { if (has.nvidia) return "nvidia"; if (has.llamacpp) return "llamacpp"; if (has.hf) return "hf"; return null; } function availability(env) { return { nvidia: Boolean(env.NVIDIA_API_KEY || env.NGC_API_KEY), llamacpp: env.LLAMACPP_ENABLED === "1", hf: Boolean(env.HF_TOKEN || env.HUGGINGFACEHUB_API_TOKEN || env.HUGGING_FACE_HUB_TOKEN), }; } // The MAIN agentic brain that makes decisions. Nemotron (nvidia) is the default; when it is // not keyed, prefer another capable model (HF) over the tiny local llama.cpp model — that 0.5B // is the summariser, not a reasoner, so it is only the main brain as a last resort. export function resolveMainProvider(env = process.env) { const has = availability(env); const raw = (env.AGENT_MODEL_PROVIDER || env.AGENT_PROVIDER || "auto").toLowerCase(); if (raw !== "auto" && raw !== "" && has[raw]) return raw; if (has.nvidia) return "nvidia"; if (has.hf) return "hf"; if (has.llamacpp) return "llamacpp"; return null; } // The lightweight "assistant" model used for summarisation. Defaults to the small local // OpenBMB/llama.cpp model so it offloads summarising from the main Nemotron brain; falls // back to the main agentic model if no dedicated summariser is available. export function resolveSummarizerProvider(env = process.env) { const has = availability(env); const raw = (env.AGENT_SUMMARIZER_PROVIDER || "llamacpp").toLowerCase(); if (raw && raw !== "auto" && has[raw]) return raw; if (has.llamacpp) return "llamacpp"; return resolveMainProvider(env); } function endpointFor(provider, env) { if (provider === "nvidia") { const base = env.NVIDIA_BASE_URL || "https://integrate.api.nvidia.com/v1"; return { url: env.NVIDIA_CHAT_COMPLETIONS_URL || `${base.replace(/\/$/, "")}/chat/completions`, model: env.NVIDIA_MODEL || "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning", auth: env.NVIDIA_API_KEY || env.NGC_API_KEY, }; } if (provider === "llamacpp") { const base = env.LLAMACPP_BASE_URL || "http://127.0.0.1:8080/v1"; return { url: env.LLAMACPP_CHAT_COMPLETIONS_URL || `${base.replace(/\/$/, "")}/chat/completions`, model: env.LLAMACPP_MODEL || "local-gguf", auth: env.LLAMACPP_API_KEY || "", grammar: true, }; } return { url: env.HF_CHAT_COMPLETIONS_URL || "https://router.huggingface.co/v1/chat/completions", model: env.HF_MODEL || "Qwen/Qwen3-Coder-30B-A3B-Instruct", auth: env.HF_TOKEN || env.HUGGINGFACEHUB_API_TOKEN || env.HUGGING_FACE_HUB_TOKEN, }; } // Returns { provider, model, content } or null. `json` constrains output to valid JSON // (grammar for llama.cpp, response_format elsewhere). export async function agenticChat(messages, env = process.env, opts = {}) { const provider = opts.provider || resolveActiveProvider(env); if (!provider) return null; const ep = endpointFor(provider, env); if (!ep.url) return null; const payload = { model: ep.model, messages, temperature: Number(opts.temperature ?? (provider === "nvidia" ? 0.6 : 0.2)), max_tokens: Number(opts.maxTokens ?? 220), stream: false, }; if (opts.json) { if (ep.grammar) payload.grammar = JSON_GRAMMAR; else payload.response_format = { type: "json_object" }; } // Nemotron reasoning recipe. Thinking is only enabled where the caller asks (the // recommendation/decision), so the many lightweight summary calls stay fast. if (provider === "nvidia") { payload.top_p = Number(env.NVIDIA_TOP_P ?? 0.95); const thinking = Boolean(opts.thinking); payload.chat_template_kwargs = { enable_thinking: thinking }; if (thinking) { payload.reasoning_budget = Number(env.NVIDIA_REASONING_BUDGET ?? 4096); payload.max_tokens = Number(opts.maxTokens ?? 4096) + payload.reasoning_budget; } } try { const response = await fetch(ep.url, { method: "POST", headers: { ...(ep.auth ? { Authorization: `Bearer ${ep.auth}` } : {}), "Content-Type": "application/json", }, body: JSON.stringify(payload), signal: AbortSignal.timeout(Number(opts.timeoutMs ?? env.AGENT_CHAT_TIMEOUT_MS ?? 60000)), }); if (!response.ok) return null; const data = await response.json(); const content = stripReasoning(data?.choices?.[0]?.message?.content); return content ? { provider, model: ep.model, content } : null; } catch { return null; } }