DocsStart
Examples
Copy-paste examples in curl, TypeScript, Python and Rust.
Every example reads your key from RHUMB_KEY and runs in CI against the API, so it cannot drift from it. RHUMB_URL overrides the base URL.
local
// Resolve a local model that fits the device: the runtimes it has installed
// as providers, and its memory as max_memory_gb. Local offerings are free;
// the answer says what the model needs (memory at the context you asked for,
// GPU memory, disk) and its quantization. Cache it for 24 hours.
//
// RHUMB_KEY=nnm_live_... npx tsx local.ts
//
// RHUMB_URL overrides the base URL (default https://rhumbdata.com).
// With no RHUMB_KEY, or on any error, it returns the fallback.
import { totalmem } from "node:os";
import { pathToFileURL } from "node:url";
export type LocalModel = {
provider: string;
provider_model_id: string;
quantization?: string;
requirements?: { memory_gb: number; disk_gb?: number };
};
/**
* Memory the model may use, in GB (10^9 bytes): three quarters of the
* machine's, leaving room for the OS and your app.
*/
export function deviceMemoryGb(share = 0.75): number {
return Math.floor((totalmem() / 1e9) * share);
}
export async function resolveLocalModel(
query: Record<string, string>,
fallback: LocalModel,
): Promise<LocalModel> {
const key = process.env.RHUMB_KEY;
if (!key) return fallback;
const base = process.env.RHUMB_URL ?? "https://rhumbdata.com";
const qs = new URLSearchParams(query).toString();
try {
const res = await fetch(`${base}/v1/resolve?${qs}`, {
headers: { Authorization: `Bearer ${key}` },
signal: AbortSignal.timeout(2_000),
});
// 422 no_candidate: nothing on these runtimes fits the device.
if (!res.ok) throw new Error(`resolver answered ${res.status}`);
const { model } = (await res.json()) as { model: LocalModel };
return model;
} catch (error) {
console.error("resolver unavailable, using the fallback:", error);
return fallback;
}
}
async function main() {
// A 16 GB laptop with Ollama and LM Studio. On the device itself, send
// String(deviceMemoryGb()) and the runtimes you find installed.
const model = await resolveLocalModel(
{
task: "text.chat",
providers: "ollama,lmstudio",
optimize: "quality",
max_memory_gb: "16",
min_context: "8192",
},
{ provider: "ollama", provider_model_id: "llama3.2:3b" },
);
console.log(
JSON.stringify({
provider: model.provider,
provider_model_id: model.provider_model_id,
quantization: model.quantization,
memory_gb: model.requirements?.memory_gb,
}),
);
}
// Run directly (not imported): print the answer for an example device.
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
await main();models
// List the models offered on your providers: for example to propose additions
// to an app's allowlist for a person to accept.
//
// RHUMB_KEY=nnm_live_... npx tsx models.ts
//
// RHUMB_URL overrides the base URL (default https://rhumbdata.com).
import { pathToFileURL } from "node:url";
type ModelsPage = {
catalog_version: string;
total: number;
models: {
id: string;
display_name: string;
offerings: { provider: string; provider_model_id: string }[];
}[];
};
export async function listModels(
query: Record<string, string>,
): Promise<ModelsPage> {
const key = process.env.RHUMB_KEY;
if (!key) throw new Error("Set RHUMB_KEY to your nnm_live_ key");
const base = process.env.RHUMB_URL ?? "https://rhumbdata.com";
const qs = new URLSearchParams(query).toString();
const res = await fetch(`${base}/v1/models?${qs}`, {
headers: { Authorization: `Bearer ${key}` },
signal: AbortSignal.timeout(10_000),
});
if (!res.ok)
throw new Error(`resolver answered ${res.status}: ${await res.text()}`);
return (await res.json()) as ModelsPage;
}
async function main() {
const page = await listModels({
task: "text.chat",
providers: "gemini,openai,openrouter",
});
for (const m of page.models)
for (const o of m.offerings)
console.log(`${m.id}\t${o.provider}\t${o.provider_model_id}`);
console.log(
`models: ${page.models.length} of ${page.total} (catalog ${page.catalog_version})`,
);
}
// Run directly (not imported): print the answer for an example query.
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
await main();plan
// requires /v1/plan (phase 5)
//
// Plan a request from the prompt itself, within the request's time budget.
// On a timeout or any error, use the app's own default model.
//
// RHUMB_KEY=nnm_live_... npx tsx plan.ts
//
// RHUMB_URL overrides the base URL (default https://rhumbdata.com).
// The prompt goes only to the decision model you choose (OpenAI's by default;
// `decision: { allow_remote: false }` keeps it on our server) and is never
// stored or logged. Send tags or a summary instead if you prefer.
import { pathToFileURL } from "node:url";
type Plan = {
model: { id: string; provider: string; provider_model_id: string };
need: { task: string; purpose?: string };
/** Where each part of the need came from: fact, tag, decision, request, default. */
sources: Record<string, string>;
decision: { model: string; fallback_used: boolean; cached: boolean };
};
export async function planFor(prompt: string): Promise<Plan | null> {
const key = process.env.RHUMB_KEY;
if (!key) return null;
const base = process.env.RHUMB_URL ?? "https://rhumbdata.com";
const res = await fetch(`${base}/v1/plan`, {
method: "POST",
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
prompt,
tags: ["usa"],
providers: ["gemini", "openai", "openrouter"],
}),
signal: AbortSignal.timeout(600),
}).catch(() => null);
return res?.ok ? ((await res.json()) as Plan) : null; // null → use the app's own default
}
async function main() {
const plan = await planFor("Summarize this contract in five bullet points.");
console.log(
JSON.stringify(
plan
? {
provider: plan.model.provider,
provider_model_id: plan.model.provider_model_id,
purpose: plan.need.purpose ?? null,
purpose_from: plan.sources.purpose ?? null,
decision: plan.decision,
}
: { default: "gemini-3.5-flash" },
),
);
}
// Run directly (not imported): print the answer for an example prompt.
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
await main();resolve
#!/usr/bin/env bash
# Resolve a model with curl: which model on Gemini or OpenAI reads receipts best
# for the price? Prints the JSON answer; `model.provider_model_id` is the name to
# pass to the provider's API.
#
# RHUMB_KEY=nnm_live_... bash resolve.sh
#
# RHUMB_URL overrides the base URL (default https://rhumbdata.com).
set -euo pipefail
: "${RHUMB_KEY:?Set RHUMB_KEY to your nnm_live_ key}"
RHUMB_URL="${RHUMB_URL:-https://rhumbdata.com}"
curl --silent --show-error --fail-with-body --max-time 10 \
"$RHUMB_URL/v1/resolve?purpose=read-receipts&providers=gemini,openai" \
-H "Authorization: Bearer $RHUMB_KEY"
echoteam
// Resolve a team: a debate whose three roles come from three different makers,
// under one budget for the whole team. One counted call.
//
// RHUMB_KEY=nnm_live_... npx tsx team.ts
//
// RHUMB_URL overrides the base URL (default https://rhumbdata.com).
// `roles` also takes "name:count" pairs (roles=lead:1,workers:4) or a JSON
// object of role fields; shape=escalation&length=3 answers an ordered list.
import { pathToFileURL } from "node:url";
type Member = { id: string; provider: string; provider_model_id: string };
type Team = {
shape: "team";
team: {
strategy?: string;
roles: Record<string, { task: string; members: Member[] }>;
typical_call?: string;
};
};
/** Role → its members, or null on any error (use the app's own team then). */
export async function resolveTeam(
query: Record<string, string>,
): Promise<Team["team"] | null> {
const key = process.env.RHUMB_KEY;
if (!key) return null;
const base = process.env.RHUMB_URL ?? "https://rhumbdata.com";
const qs = new URLSearchParams({ ...query, shape: "team" }).toString();
const res = await fetch(`${base}/v1/resolve?${qs}`, {
headers: { Authorization: `Bearer ${key}` },
signal: AbortSignal.timeout(2_000),
}).catch(() => null);
if (!res?.ok) {
// A 422 names the role nothing was left for in error.role.
if (res) console.error("no team:", JSON.stringify(await res.json()));
return null;
}
return ((await res.json()) as Team).team;
}
async function main() {
const team = await resolveTeam({
task: "text.reasoning",
strategy: "debate",
providers: "gemini,openai,anthropic",
});
console.log(
JSON.stringify(
team
? {
strategy: team.strategy,
typical_call: team.typical_call,
roles: Object.fromEntries(
Object.entries(team.roles).map(([role, { members }]) => [
role,
members.map((m) => `${m.provider}:${m.provider_model_id}`),
]),
),
}
: { default: "the app's own team" },
),
);
}
// Run directly (not imported): print the team for an example query.
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
await main();