Skip to the content

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

local.ts
// 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

models.ts
// 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

plan.ts
// 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

resolve.sh
#!/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"
echo

team

team.ts
// 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();