Skip to content

Instantly share code, notes, and snippets.

@trashhalo
Created June 15, 2026 14:36
Show Gist options
  • Select an option

  • Save trashhalo/ea9048155f3a43c2f77a610ed18d25a0 to your computer and use it in GitHub Desktop.

Select an option

Save trashhalo/ea9048155f3a43c2f77a610ed18d25a0 to your computer and use it in GitHub Desktop.
Bosun + node-llama-cpp: the supported completion+logits path (NOT --rerank/LlamaRankingContext). score = P(yes)/(P(yes)+P(no)) at the final token.
// Bosun + node-llama-cpp — the SUPPORTED path (completion + logits), NOT the rerank API.
//
// npm install node-llama-cpp
// node bosun.mjs
//
// Why NOT LlamaRankingContext / llama.cpp --rerank: that path uses rank-pooling + a baked rerank
// template whose instruction is HARDCODED — there is no slot for a per-request rule. Bosun's whole
// value is the programmable <Instruct> you supply each call, so the rerank path silently drops it
// (opposite rules score identically). Bosun's GGUF is also a plain causal LM (full vocab head, no
// rank head), so LlamaRankingContext would refuse it anyway.
//
// The contract (serving.json): prompt = prefix + "<Instruct>/<Query>/<Document>" + suffix, then read
// the next-token distribution at the final position and take
// score = P(yes) / (P(yes) + P(no)) == sigmoid(logit_yes - logit_no)
// (renormalized two-class softmax — exact, and avoids needing raw logits).
import {getLlama, resolveModelFile} from "node-llama-cpp";
const REPO = "Hanno-Labs/bosun-xs-GGUF";
const MODEL_URI = `hf:${REPO}/Bosun-XS-Q8_0.gguf`; // f16 / Q8_0 / Q4_K_M available
const cfg = await (await fetch(`https://huggingface.co/${REPO}/resolve/main/serving.json`)).json();
const llama = await getLlama();
// MODEL_PATH lets you point at an already-downloaded file; otherwise it pulls from HF.
const modelPath = process.env.MODEL_PATH ?? await resolveModelFile(MODEL_URI);
const model = await llama.loadModel({modelPath});
const context = await model.createContext({contextSize: cfg.max_len});
const seq = context.getSequence();
async function score(instruct, query, document) {
const body = `<Instruct>: ${instruct}\n<Query>: ${query}\n<Document>: ${document}`;
const prompt = cfg.prefix + body + cfg.suffix; // suffix already ends with the empty <think> block
const tokens = model.tokenize(prompt, true); // special tokens on (<|im_start|> ...), no BOS
await seq.eraseContextTokenRanges([{start: 0, end: seq.nextTokenIndex}]); // fresh state per pair
// full next-token distribution after the final prompt token; temperature:1 + no truncation = true softmax
const input = tokens.map((t, i) =>
i === tokens.length - 1
? [t, {generateNext: {probabilities: true, options: {temperature: 1, topK: 0, topP: 1, minP: 0}}}]
: t
);
const out = await seq.controlledEvaluate(input);
const probs = out[tokens.length - 1].next.probabilities; // Map<token, probability>
const pYes = probs.get(cfg.yes_id) ?? 0;
const pNo = probs.get(cfg.no_id) ?? 0;
return pYes / (pYes + pNo);
}
// the document is an ORDERED pair — FINDING A then FINDING B (direction can matter)
const Q = "These two findings share the specified relationship.";
const sec = "FINDING A:\nThe SEC proposed new disclosure rules for crypto exchanges.\n\n" +
"FINDING B:\nA startup launched an AI tool for drafting marketing emails.";
// same pair, opposite rules -> the score flips, because the instruction IS the rubric
console.log("same-topic rule :", (await score("Connected ONLY if both findings are about the same broad topic. Otherwise not.", Q, sec)).toFixed(3));
console.log("diff-topic rule :", (await score("Connected ONLY if the two findings are about DIFFERENT topics. Otherwise not.", Q, sec)).toFixed(3));
await model.dispose();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment