The Hub never falls back silently. Every refusal or failure is an error with a code, a message written for the user, and details. Show the message; branch on the code.
try {
await hub.complete('mine.summarize', messages);
} catch (e) {
const err = e as Error & { code?: string };
if (err.code === 'aborted') return;
new Notice(err.message);
}
Refused and failed calls are written to the ledger at a cost of zero, except reply-cut-off: that call did use tokens, and its row keeps the cost.
| Code | When | What to do |
|---|
over-ceiling | The call could cost more than a per-call limit. Checked before sending. | Show the message. Send less, or lower maxOutputTokens. |
over-budget | A monthly budget on the role or its tier is used up. | Show the message. The user raises the limit or waits. |
unpriced-model | The model has no listed price, so a limit can't be checked. | Show the message. |
| Code | When | What to do |
|---|
no-models | The role's tier has no models set. | Show the message; it points the user to the Hub's settings. |
unknown-provider | A model's provider isn't set up in the Hub. | Show the message. |
unknown-model | The model isn't one the Hub knows. | Show the message. |
provider-unavailable | The provider has no key, or its server isn't answering. | Show the message. |
provider-error | The provider returned an error. details.retryable says whether trying again may help. | Retry if retryable; otherwise show the message. |
capability-missing | The model can't do what the call needs: tools, structured output, web search, images, embeddings. details.missing lists them. | Show the message. Declare needs so this is caught on every model. |
reply-cut-off | The model stopped at the cap before writing any text, usually because it spent every token reasoning. details carry the usage, the cap, the reply cap and the thinking budget. If the role had another candidate, it was tried first. | Show the message. Set the role's reasoning to off or a lower level, or raise maxOutputTokens. |
model-not-allowed | The call named a tier, model or list for one run, and the role doesn't allow it; or an embed spec named a model the role hasn't used. | Set allowAnyModel on roles where the user picks a model, or pinnedModels on an embedding role whose indexes keep their model. |
fingerprint-mismatch | expectFingerprint didn't match what the call would make (v2:, before sending) or made (v1, after). details has expected, actual and formVersion. | The index needs its own spec, or a re-embed. Price one with describeEmbedding. |
| Code | When | What to do |
|---|
invalid-role-name | A role isn't named namespace.name in lowercase. | Fix the name. |
role-taken | The namespace belongs to another plugin, or you tried to change a role you don't own. | Use your own namespace. |
unknown-role | No plugin has registered the role. | Register before calling. |
unknown-tier | deleteTier named a tier that doesn't exist. | Check tiers() first. |
invalid-tier-name | A tier's name is taken, empty, over 40 characters or a built-in tier's. | Choose another name. |
invalid-skill | A registered skill has no or a bad SKILL.md, a path outside its folder, a repeated name, or mode: always. | Fix the skill. |
skill-taken | The skill's name belongs to another plugin or to the Hub. | Choose another name. |
invalid-request | The request is malformed. | Fix the call. |
aborted | Your signal stopped the call. | Nothing; the user stopped it. |
Tool refusals and failures are written to the tool-call log, not the ledger.
| Code | When | What to do |
|---|
tool-unknown | callTool named a tool no plugin has registered. | Check tools() first. |
tool-name-taken | registerTools used a namespace that belongs to another plugin. | Use your own namespace. |
tool-invalid-input | The arguments don't match the tool's schema, or can't be written as JSON. details.problem says what is wrong. | Fix the call. |
tool-denied | The user's setting is Deny, the run's tool list leaves the tool out, the user declined, or nobody answered in 2 minutes. details.reason is policy, user or no-answer. | Show the message. Don't retry at once. |
tool-failed | The handler threw, its plugin was unloaded during the call, the plugin offers no undo, or an undo failed. | Show the message. |
tool-timeout | The handler ran over 60 seconds. | Show the message. |
run-budget | A call would take the run past its budgetUsd. | Show the message, or start a run with more room. |
aborted also applies to tools: your signal stopped the call, or its run ended.
| Code | When | What to do |
|---|
edit-no-match | previewEdits: an op couldn't be placed (not found, found in several places without replaceAll, overlapping another op, or empty), or a note is missing. The message names each op; details.failures lists them. Nothing was previewed. | Add context to the op's oldText, or use replaceAll. |
edit-stale | applyEdits: a range changed since the preview, so nothing was written; or the preview expired. undoEdit: the text the edit wrote has changed since, so the note was kept. | Preview again; for undo, tell the user the note was kept as it is. |