Docs/Skills and code/src/
src/llm.js
Mentioned on: page 01, page 12
import { TextDecoder } from "util";
import {
GeminiError, callGeminiWithRetry, explainGeminiError, getGeminiConfig, secondsUntilPacificMidnight
} from "./gemini.js";
// ============================================================
// LLM ADAPTER
//
// callLLM() sends to Gemma 4 on the Google Gemini API by default and falls back to the
// Ollama model when Gemini cannot answer (daily limit, a rate limit that does not clear,
// overload, bad key, timeout). Ollama is used directly when there is no Gemini key.
//
// LLM_PROVIDER gemini | ollama default: gemini when GEMINI_API_KEY is set, else ollama
// LLM_FALLBACK off disable the Ollama fallback (default: on)
// GEMINI_MODEL (gemma-4-26b-a4b-it; gemma-4-31b-it is the larger but slower, flakier option),
// GEMINI_THINKING (minimal default; high, or "default" for the model's own setting),
// GEMINI_MAX_OUTPUT_TOKENS (16000), GEMINI_TEMPERATURE (0.1), GEMINI_TIMEOUT_S (300),
// GEMINI_MAX_RETRIES (2), GEMINI_MAX_WAIT_S (90), GEMINI_JSON_MODE=off (JSON mode is on by default)
// OLLAMA_HOST, OLLAMA_MODEL
// Variables come from the environment or the project .env file (see .env.example).
// ============================================================
function camelToSnake(key) {
return key.replace(/([A-Z])/g, "_$1").toLowerCase();
}
export function normalizeKeys(value) {
if (Array.isArray(value)) {
return value.map(normalizeKeys);
}
if (value && typeof value === "object") {
const normalized = {};
for (const key of Object.keys(value)) {
const snakeKey = camelToSnake(key);
normalized[snakeKey] = normalizeKeys(value[key]);
}
return normalized;
}
return value;
}
function extractJsonCandidate(text) {
if (!text || typeof text !== "string") {
return null;
}
const trimmed = text.trim();
const fenceMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
if (fenceMatch) {
// Models sometimes leave stray characters after the object inside the fence
// (e.g. a lone quote before the closing ```); keep only the balanced object.
const inner = fenceMatch[1].trim();
return scanBalancedJson(inner) ?? inner;
}
return scanBalancedJson(trimmed) ?? trimmed;
}
function scanBalancedJson(trimmed) {
let started = false;
let depth = 0;
let start = -1;
let openChar = "";
let closeChar = "";
let inString = false;
let escaped = false;
for (let i = 0; i < trimmed.length; i++) {
const ch = trimmed[i];
if (started) {
if (inString) {
if (escaped) {
escaped = false;
} else if (ch === "\\") {
escaped = true;
} else if (ch === '"') {
inString = false;
}
continue;
}
if (ch === '"') {
inString = true;
continue;
}
if (ch === openChar) {
depth += 1;
} else if (ch === closeChar) {
depth -= 1;
if (depth === 0) {
return trimmed.slice(start, i + 1).trim();
}
}
continue;
}
if (ch === "{" || ch === "[") {
started = true;
depth = 1;
start = i;
openChar = ch;
closeChar = ch === "{" ? "}" : "]";
}
}
return null;
}
function cleanJsonString(rawText) {
if (typeof rawText !== "string") {
return rawText;
}
let candidate = extractJsonCandidate(rawText);
if (!candidate) {
return rawText;
}
candidate = candidate.trim();
candidate = candidate.replace(/```(?:json)?/gi, "").replace(/```/g, "").trim();
candidate = candidate.replace(/,\s*([}\]])/g, "$1");
return candidate;
}
// Gemma (Gemini API, minimal thinking) occasionally mangles the colon after a key:
// `"confidence:0.95` (full-width colon) or `"confidence:0.95` (closing quote dropped).
// Only used after a normal parse has failed, and the result still has to parse.
function repairKeySeparators(text) {
return text
.replace(/"([A-Za-z_][A-Za-z0-9_]*)[::]\s*(?=[-0-9"\[{]|true\b|false\b|null\b)/g, '"$1":')
.replace(/([,{]\s*)([A-Za-z_][A-Za-z0-9_]*)":/g, '$1"$2":');
}
export function parseJson(rawText) {
const cleaned = cleanJsonString(rawText);
const attempts = [
cleaned,
cleaned.replace(/[\r\n]+/g, " "),
repairKeySeparators(cleaned).replace(/[\r\n]+/g, " ")
];
for (const candidate of attempts) {
try {
return normalizeKeys(JSON.parse(candidate));
} catch {
// Try the next, more forgiving form.
}
}
throw new Error(
`Unable to parse JSON from LLM output. Raw output:\n${rawText}`
);
}
export function getOllamaConfig() {
return {
host: process.env.OLLAMA_HOST || "http://localhost:11434",
model: process.env.OLLAMA_MODEL || "gemma4:31b-cloud"
};
}
export async function callOllama(prompt, { stream = true } = {}) {
const { host, model } = getOllamaConfig();
const res = await fetch(`${host.replace(/\/$/, "")}/api/generate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model,
prompt,
stream
})
});
if (!res.ok) {
const text = await res.text();
throw new Error(
`Ollama request failed: ${res.status} ${res.statusText} - ${text}`
);
}
if (!stream) {
const data = await res.json();
return data.response;
}
if (!res.body) {
throw new Error("Ollama response has no body to stream");
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let result = "";
process.stdout.write("[LLM] ");
const flushLine = (line) => {
const trimmed = line.trim();
if (!trimmed || trimmed === "[DONE]") {
return;
}
let payloadText = trimmed;
if (trimmed.startsWith("data:")) {
payloadText = trimmed.slice(5).trim();
}
try {
const payload = JSON.parse(payloadText);
const text =
payload.response ||
payload.output ||
payload.text ||
payload.data ||
"";
if (text) {
process.stdout.write(text);
result += text;
}
} catch {
process.stdout.write(payloadText);
result += payloadText;
}
};
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split(/\r?\n/);
buffer = lines.pop() || "";
for (const line of lines) {
flushLine(line);
}
}
buffer += decoder.decode();
if (buffer) {
flushLine(buffer);
}
process.stdout.write("\n");
return result.trim();
}
export function getLLMProvider() {
const requested = (process.env.LLM_PROVIDER || "").trim().toLowerCase();
if (requested === "ollama" || requested === "gemini") {
return requested;
}
return getGeminiConfig().apiKey ? "gemini" : "ollama";
}
const fallbackEnabled = () => !/^(off|false|0|no)$/i.test((process.env.LLM_FALLBACK || "").trim());
const numberFromEnv = (name, fallback) => {
const value = Number(process.env[name]);
return process.env[name] && Number.isFinite(value) ? value : fallback;
};
// Which backend callLLM() tries first, and what it falls back to.
export function getLLMConfig() {
const provider = getLLMProvider();
const ollama = getOllamaConfig();
return {
provider,
model: provider === "gemini" ? getGeminiConfig().model : ollama.model,
host: ollama.host,
fallback: provider === "gemini" && fallbackEnabled() ? { provider: "ollama", model: ollama.model } : null
};
}
// After a failure that will keep failing, skip Gemini for a while instead of paying
// its retry waits on every batch. Per process only.
let geminiPausedUntil = 0;
let geminiPauseReason = "";
export function resetLLMState() {
geminiPausedUntil = 0;
geminiPauseReason = "";
}
function pauseGemini(error) {
const seconds = {
"daily-limit": secondsUntilPacificMidnight(),
auth: Infinity,
"no-key": Infinity,
"requests-per-minute-limit": 300,
"tokens-per-minute-limit": 300,
"rate-limit-unknown": 300,
overloaded: 60,
"server-error": 60
}[error?.kind];
if (seconds) {
geminiPausedUntil = seconds === Infinity ? Infinity : Date.now() + seconds * 1000;
geminiPauseReason = error.kind;
}
}
// Thinking tokens made up over half of the generated tokens and doubled the latency, with no
// quality loss seen in the 16-channel comparison, so thinking is minimal unless overridden.
function thinkingLevelFromEnv() {
const value = (process.env.GEMINI_THINKING || "minimal").trim().toLowerCase();
return ["default", "off", "none", ""].includes(value) ? "" : value;
}
async function callGeminiBackend(prompt, { stream }) {
const { model } = getGeminiConfig();
const result = await callGeminiWithRetry({
// One combined prompt string, exactly like the Ollama path.
user: prompt,
model,
stream,
maxOutputTokens: numberFromEnv("GEMINI_MAX_OUTPUT_TOKENS", 16000),
temperature: numberFromEnv("GEMINI_TEMPERATURE", 0.1),
thinkingLevel: thinkingLevelFromEnv(),
jsonMode: !/^(off|false|0|no)$/i.test((process.env.GEMINI_JSON_MODE || "").trim()),
timeoutMs: numberFromEnv("GEMINI_TIMEOUT_S", 300) * 1000
}, {
maxRetries: numberFromEnv("GEMINI_MAX_RETRIES", 2),
maxWaitSeconds: numberFromEnv("GEMINI_MAX_WAIT_S", 90),
onWait: ({ error, waitSeconds }) =>
console.warn(`[LLM] Gemini ${error.kind}; waiting ${waitSeconds}s before retrying`)
});
return { content: result.content, model, provider: "gemini", usage: result.usage, fellBack: false };
}
export async function callLLM({
system = "",
user = "",
stream = true
} = {}) {
const prompt = [
system?.trim(),
user?.trim()
].filter(Boolean).join("\n\n");
let fellBackBecause = null;
if (getLLMProvider() === "gemini") {
if (Date.now() < geminiPausedUntil) {
fellBackBecause = `paused after ${geminiPauseReason}`;
} else {
try {
return await callGeminiBackend(prompt, { stream });
} catch (error) {
if (!fallbackEnabled()) {
throw error;
}
pauseGemini(error);
fellBackBecause = explainGeminiError(error).split("\n")[0].slice(0, 300);
}
}
if (!fallbackEnabled()) {
throw new GeminiError(`Gemini is ${fellBackBecause} and LLM_FALLBACK is off`, { kind: "paused" });
}
console.warn(`[LLM] Gemini unavailable (${fellBackBecause}); using Ollama ${getOllamaConfig().model}`);
}
const { model } = getOllamaConfig();
const content = await callOllama(prompt, { stream });
return {
content,
model,
provider: "ollama",
fellBack: fellBackBecause !== null
};
}
export const parseJsonObject = parseJson;
Shown as it is in the repository at build time (2026-11-03); the home directory appears as <home>. If this page and the file differ, the file is right. Back to the catalogue.