6ixPulse / server /model-chat.mjs
OwnPath's picture
Kokoro-82M TTS + README/diagram update + dead-code/Ollama cleanup
0cb5613 verified
Raw
History Blame Contribute Delete
5.68 kB
// 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;
}
}