This page is the map. The full reference is generated from the code:
- For reading: the API reference, with every declaration, its signature and its comment.
- For tools and coding agents:
model-hub-api.d.ts, every public declaration in one TypeScript file, andapi.json, the same as data.
Both are also in the repo under docs/api/.
Finding the Hub#
| Call | What it does |
|---|---|
getHub(app, wanted) | Returns { hub }, or { hub: null, reason, message } when the Hub is missing or its API is too old. |
onHubReady(app, plugin, wanted, cb) | Runs cb(hub) now if the Hub is loaded, and again each time it loads. |
isCompatible(actual, wanted) | True when an API at actual serves a plugin built for wanted. |
hub.version | The API's version. |
Roles and tiers#
| Call | What it does |
|---|---|
registerRoles(pluginId, roles) | Declares your roles. Harmless to repeat. |
roles(pluginId) | Your roles as they resolve now: the tier, the models to try, and every limit that applies. tierMissing is set when a role names a tier that doesn't exist. |
setRole(pluginId, role, override) | Changes one of your roles; null puts it back to your default. Only the owning plugin may. |
onRolesChanged(cb) | Calls cb when what your roles resolve to may have changed: the user moved a role or gave it models, a tier changed, or providers were switched on or off. Read roles() again in it. Several changes at once call it once. Returns the function that stops it. (0.12) |
openRoleEditor(pluginId) | Opens the Hub's editor for your roles. |
tiers() | Every tier as the user has set it: the seven built-in ones, then the user's own. Each has a stable id, a display name and custom. |
createTier(pluginId, name, policy) | Asks the user to add a tier. The Hub shows a dialog naming your plugin. Resolves to the tier, or null if the user declines. |
deleteTier(pluginId, id) | Asks the user to delete one of their own tiers. Resolves true or false. |
A role definition:
{
name: 'mine.summarize', // namespace.name, lowercase
description: 'Summaries of a note',
tier: 'quick', // a tier's id or name; 'balanced' if left out
reasoning: 'medium', // optional: off, low, medium or high; left out, thinking is off
ceilingUsd: 0.02, // optional: this role's own limit per call
monthlyBudgetUsd: 2, // optional: this role's own limit per month
allowAnyModel: false, // true for interactive roles where the user picks a model per run
hosts: 'balanced', // optional: how OpenRouter picks a host (cheapest, balanced, fastest); the user can change it (0.12)
pinnedModels: false, // embedding roles: true lets an index keep a model the role used before (0.12)
}
A role may also carry its own ordered candidates instead of a tier. Store the id that createTier returns; names keep working across renames.
Calls#
Every call takes a role name first. All of them reject with an error whose code is one of the refusal codes.
| Call | What it does |
|---|---|
complete(role, messages, opts?) | One call. Returns the text, plus tool calls, sources and a structured object when asked for. |
stream(role, messages, opts?) | The same, with textStream to read as it arrives. |
embed(role, values, opts?) | Embeddings for a batch of texts. |
estimate(role, req) | What a call or an agent run would cost, without making it. See below. |
decide(role, request, opts?) | Typed decisions: yes/no, pick one, or a score. calibrated says whether the probabilities come from a decision model. |
batch(items, fn, opts?) | Runs many calls with limited concurrency, reporting progress and cost. Stops early on an overcharged call, a spend limit or an abort. |
Options on complete and stream:
| Option | What it does |
|---|---|
system | The system prompt. |
maxOutputTokens | The longest reply allowed, not counting thinking: the Hub adds the model's thinking budget when it sends the call. The per-call limit is checked against both. |
output | A schema for the reply, which comes back in object: a Standard Schema such as zod's, which the reply is checked against, or a JSON Schema document, which the model is asked to follow but which isn't checked. complete only. |
webSearch | Ask the provider to search the web. |
skills | Skills to apply, by name. |
needs | Capabilities you need beyond what the options imply. |
for | What the call is about, for the ledger. |
signal | An AbortSignal to stop the call. |
Options on embed:
| Option | What it does |
|---|---|
kind | query or passage. The Hub adds the model family's prefix: an Instruct: … Query: line for Qwen3 embeddings, query: / passage: for e5, search_query: / search_document: for nomic, the retrieval line for English bge and mxbai. Without kind, texts go as they are. |
dimensions | Cut each vector to this length and rescale it to unit length. For Matryoshka models such as Qwen3 and OpenAI's text-embedding-3. |
instruction | The task for instruction-tuned models, in place of "retrieve relevant passages". |
embed returns dimensions and a fingerprint: a hash of the Hub provider, the model, the dimensions and the input form. Which upstream host OpenRouter used is left out, so the fingerprint stays the same from call to call. Vectors with the same fingerprint can be compared. Name your vector files by it, so a model or settings change can't mix rows.
An index that keeps its model#
When the user changes your embedding role, vectors you already stored stop matching new ones. To keep an index on the model it was built with, store an EmbeddingSpec with it and pass it on every call for that index:
const spec: EmbeddingSpec = { provider: 'openrouter', model: 'qwen/qwen3-embedding-8b', dimensions: 1024, form: 'family', instruction: null };
const r = await hub.embed('mine.embed', texts, { spec, kind: 'passage', expectFingerprint: index.fingerprint });
| Field | What it means |
|---|---|
provider, model | The model exactly, as the Hub's provider and that provider name it. |
dimensions | Cut vectors to this length; null keeps the model's own. |
form | family: the Hub adds the family's prefix for each call's kind (so kind is required). bare: texts go as given, whatever the kind. |
instruction | The task for instruction models in the family form; null is the Hub's default. |
- A spec replaces
model,candidates,tier,dimensionsandinstruction; giving one of them too isinvalid-request. There is no fallback: the spec's model runs, or the call is refused. - The spec's model may have left the role. It still runs when the role sets
allowAnyModel, or setspinnedModels: trueand has embedded with that model before (the ledger says so). Such calls are markedpinnedin the ledger. Otherwise it'smodel-not-allowed, and the message says to add the model to the role. - With a spec,
fingerprintisv2:and 16 hex characters. It covers the provider, model, dimensions, form and the prefixes themselves, so bare and prefixed vectors never share one.fingerprintV1is the 0.9–0.11 value for the same vectors, for matching stores made before 0.12. expectFingerprinttakes either. Av2:value is checked before anything is sent. A v1 value hashed what served the call, so it is checked after the call. A mismatch rejects withfingerprint-mismatch(details.expected,details.actual,details.formVersion).matchedsays which one matched, so you can store the v2 value and drop v1.describeEmbedding(spec)gives the fingerprint, the prefixes, the price per million tokens and whether the spec can run now, without a call. When it can't,available.codeisunknown-provider,provider-unavailable,unknown-model(a retired model looks like this) orcapability-missing, with a sentence to show. Use it at load, and to price a re-embed.
Price a call before making it#
estimate(role, { input | messages, system?, skills?, tools?, maxOutputTokens?, steps? }) prices a call through one of your roles without making it: no request goes out and no ledger row is written. It resolves the role as a call would and takes the first model that could serve it, with the same checks for capabilities, prices and the per-call limit.
{ ok: true, low, high, usd, model, billing, inputTokens, outputTokens, approximate: true }.lowis one step with a short reply.highis every step with a full reply and its thinking room, each step's input grown by the step before.usdis between the two.{ ok: false, reason, message }with the code the call itself would get:no-models,unpriced-model,over-ceiling,capability-missing,tool-unknownand so on.- For an agent run, pass its
maxStepsasstepsand the tool names it offers astools; their descriptions and schemas count as prompt. - For many calls of one shape (one per chapter), pass
times: the price is multiplied, and inputs don't grow as they do oversteps. - For text you haven't read yet, pass its size as
inputTokens; it is added to the prompt. Withtimes, also pass the longest call's aslongestInputTokens: the per-call limit is checked against it, since that call is the one refused at run time. - To price a
decide()call, pass the request asdecision: its own prompt and a 512-token reply. - With
webSearch: truethe model must be one that searches. The provider's search fee isn't known, so the result says so innotCounted. highleaves out tool results unless you passtoolResultTokens: the most tokens of tool results fed back after each step (a tool that returns a 15k-token note, called twice a step: 30 000). It also leaves out a fallback to another model after a failure, and retries.- A plan that a budget is checked against should use
high, and the run should still carry itsbudgetUsdas a hard cap.
For search over your own index, bundle the shared retrieval library rather than writing BM25 and fusion yourself: npm install @insurgently/retrieval (https://www.npmjs.com/package/@insurgently/retrieval).
| tier, model, candidates | Another tier, one model or a list for this run. Only for roles with allowAnyModel. |
| routing | Routing merged into each model's own for this call, such as an OpenRouter price cap. |
| hosts | How OpenRouter picks a host for this call: cheapest, balanced (the fastest at or under the listed price) or fastest. Over the role's and the tier's setting. Any role. |
| reasoning | How much the model may think on this call (off, low, medium, high), over whatever the role or its models say. Any role. |
| run | The id of a run from startRun (it must be your plugin's): the call's cost counts against the run's budget and is recorded with it. |
| attributes | Your own labels for the call, such as { collection: 'garden', pass: 2 }, put on its trace span as hub.attr.<key> when the user has traces on, so your logs can be matched to traces. Up to 16; keys in lowercase letters, digits, _ and .; text values up to 200 characters. Malformed ones are refused with invalid-request. They go to the user's collector, so put no note text in them. (0.12) |
What every finished call reports:
| Field | What it holds |
|---|---|
servedModel, servedProvider | Who answered. |
usage | Tokens in and out, cached and reasoning tokens. |
listedUsd | The cost at listed prices; null when unknown. |
billedUsd | What the provider says it charged, when it says. |
overcharged | Billed well above listed. |
attempts, fellBack | Every model considered, in order, and whether the answer came from a fallback. |
budgets | The monthly budgets that apply, with this month's spend; warn at 80%. |
ledgerId | The call's row in the ledger. |
Models, skills and usage#
| Call | What it does |
|---|---|
models(filter?) | The models the Hub knows, filtered by provider or by capability, for pickers. |
skills() | The skills the Hub can apply: its own, those plugins registered, and the vault's. Each has files() for its own text files. |
registerSkills(pluginId, skills) | Registers your built-in skills, each as a folder of text files with a SKILL.md. Replaces what you registered before. |
skillsReady() | Resolves once the vault's skills have loaded. |
onSkillsChanged(cb) | Calls cb when skills change. Returns the function that stops it. |
usage(filter?) | Ledger rows: every call, with tokens and cost. |
spend() | What has been spent and what is left: each tier's metered spend this month (UTC) against its monthly budget, and a report per provider (a subscription's session and week percent with reset times, OpenRouter's key limit, local tokens, others' month through the Hub). A figure that couldn't be had is in unavailable with why. Ledger rows of Claude subscription calls carry limits. |
A skill your plugin registers is used only when a call names it; it can't be always on. A vault skill with the same name overrides yours.
Offer a skill to MCP clients#
Since API 0.7. Add mcp-prompt: true to a skill's SKILL.md frontmatter and the Hub's MCP server offers it as a prompt of the same name, with your description and one optional argument, focus. The prompt is one user message: the skills it extends (root first), its body, each of its files under ## <path>, then ## Task and the caller's focus. It is cut at 100 000 characters, with the cut stated. A skill whose parent isn't loaded is not offered, and the Tools page says why. The user can turn all prompts off on the Tools page, and a vault copy of your skill is offered only if the user marked it. skills() shows the mark as mcpPrompt.
Write such a skill for an agent that has your tools: name the tools it should call, and say what focus may hold.
Tools and runs#
Since API 0.6. The guide is Register tools from your plugin.
| Call | What it does |
|---|---|
registerTools(pluginId, tools, { undo }) | Registers your tools, each { name, description, input, effect, handler } with an optional undoHandle. Replaces what you registered before. undo takes a handle a write returned and puts the change back. |
unregisterTools(pluginId) | Removes your tools. Your namespace stays yours. |
tools() | Every registered tool, with its owner and the policy now in force. |
callTool(pluginId, name, input, { run, signal }) | Runs a tool through the user's settings. Without run it is a run of its own. pluginId names the caller in the approval dialog and the log; it grants nothing. |
startRun(pluginId, { budgetUsd, tools }) | Opens a run, owned by your plugin: a spending limit for model calls and, if tools is given, the only tools it may call. |
run(pluginId, runId) | The run as it is now: what it has spent and the undo handles of its writes. undefined if it has ended or isn't yours. |
endRun(pluginId, runId) | Ends a run and stops its calls in flight. A run belongs to the plugin that started it; another caller's run is refused. |
undo(owner, handle) | Puts back a write. Resolves to what it put back. |
toolCalls(filter?) | Rows of the tool-call log. Arguments are a hash only. |
onToolsChanged(cb) | Calls cb when tools are registered or removed. Returns the function that stops it. |
Agent runs#
Since API 0.10. The guide is Run a tool loop from your plugin.
| Call | What it does |
|---|---|
agent(role, { system, messages, tools, finishTools, budgetUsd, run, maxSteps, tier, model, skills, signal }) | Runs a tool loop on one of your roles. Returns at once with { id, events, result, stop() }; result resolves on every end. Refusals (another caller's run, an unknown tool, a finish tool not in tools, no tool registry) throw before anything starts. |
Edits#
Since API 0.9. Change a user's notes safely: preview first, apply all or nothing, undo one op.
const p = await hub.previewEdits(this.manifest.id, [
{ path: 'Books/Dune.md', ops: [{ oldText: 'status: reading', newText: 'status: read' }] },
]);
// p.files[i].ops[j]: where each op matched, how ('exact', 'normalized', 'fuzzy'), and its risk flags
const r = await hub.applyEdits(this.manifest.id, p.id); // or { ops: [{ path, op }] } for some
await hub.undoEdit(this.manifest.id, r.undo[0].handle);
- Ops, not rewrites.
{ oldText, newText, replaceAll? }. The Hub findsoldTextexactly, then with quotes, dashes and spaces normalized, then (for ops of 24 characters or more) by a strict fuzzy match. Each placed op says which. The note's own characters, line ends and BOM are kept. - All or nothing. A preview with an op that can't be placed is refused. An apply checks every range in every file first; if one changed since the preview, nothing is written. If a write fails part-way, the files already written are put back.
- One op, one step. In an open note each op is one editor transaction, so the user's Cmd+Z undoes one. Each op also gets a snapshot (kept 30 days, 20 MB in all) and an undo handle;
undoEditrefuses if the text it wrote has changed since. - Risk flags. Each op is checked for changed numbers, dates, links and negations. Pass
riskChecksto add your own, such as character names. - Excluded files read as missing.
- For pure work without the vault (validating revision cards, building anchors, cut checks), bundle the shared text library:
npm install @insurgently/text(https://www.npmjs.com/package/@insurgently/text).