API reference
Every declaration in Model Hub's API for other plugins, generated from the code. API version 0.4.0.
For tools and coding agentsmodel-hub-api.d.tsapi.json
HubApi
The Hub, as getHub returns it. Every call another plugin makes goes through this.
version
readonly version: string
Semantic version of this API. Check it with isCompatible (or use getHub).
registerRoles
registerRoles(pluginId: string, roles: RoleDefinition[]): void
Declares the roles your plugin uses. Each role names a tier (default balanced) and/or its own ordered candidates, with an optional per-call ceiling and monthly budget. The first plugin to use a namespace owns it. A custom tier is named by id or by name: store the id createTier returns; names keep working across renames via aliases (a renamed tier keeps its old names, and its id reads as the name it was made with). Harmless to repeat, for example on model-hub:ready (see onHubReady).
rolessince 0.4
roles(pluginId: string): ResolvedRole[]
Your plugin's roles, with the tier, candidates and limits each resolves to now. tier is the id of the tier a role runs on. A role may name a tier by id or by name (in registerRoles or setRole): store the id createTier returns; names keep working across renames via aliases. When the tier it names doesn't exist (not made yet, or deleted), it runs on balanced and comes back with tierMissing: true and missingTier holding the name as given. (Since 0.4; before, an unknown tier became balanced unflagged.)
setRole
setRole(pluginId: string, role: string, override: RoleOverride | null): void
Changes one of your roles (a tier, own candidates, limits); null puts it back to your default. Only the owning plugin may.
openRoleEditor
openRoleEditor(pluginId: string): void
Opens the Hub's role editor for your plugin's roles: one consistent screen, no UI to write.
tiers
tiers(): Tier[]
Every tier as the user has set them (candidates, ceiling, monthly budget): the seven built-in tiers first, then the user's own in the order they were made. Each has a stable id (a built-in tier's is its name; a custom tier's never changes, even when the user renames it), a display name, and custom.
createTier
createTier(pluginId: string, name: string, policy: ModelPolicy): Promise<Tier | null>
Asks the user to add a tier of their own: the Hub opens a dialog naming your plugin (the id is as you declare it; the dialog shows the name from that plugin's manifest, or "(unknown plugin)" when no installed plugin has that id) and showing the tier's models and limits, which the user may change before confirming. Resolves to the tier made (its name may be the user's), or null if the user declines. Rejects with a HubError for a name that is taken, empty, over 40 characters or a built-in tier's (invalid-tier-name), or a malformed policy (invalid-request).
deleteTier
deleteTier(pluginId: string, id: string): Promise<boolean>
Asks the user to delete a tier of their own (by id, or name): the Hub's dialog names your plugin as createTier's does (the id is self-declared) and lists the roles mapped to it, and each the user moved there goes back to its plugin's default. Resolves true once deleted, false if the user declines. Rejects for an unknown tier (unknown-tier) or a built-in one (invalid-request).
complete
complete(role: string, messages: ModelMessage[], opts?: CompleteOptions): Promise<CompleteResult>
One call through a role. Rejects with an error whose code is a HubErrorCode.
stream
stream(role: string, messages: ModelMessage[], opts?: StreamOptions): StreamResult
The same, streamed.
decide
decide(role: string, request: DecisionRequest, opts?: DecideOptions): Promise<DecideResult>
Typed decisions about a state: yes/no (noul), pick one (choice), rate on levels (score). Through a role on the decision tier; calibrated says whether the probabilities come from a decision model (Jev).
batch
batch<T, R extends {
billedUsd?: number;
listedUsd: number | null;
overcharged: boolean;
}>(items: T[], fn: (item: T, signal: AbortSignal) => Promise<R>, opts?: BatchOptions): Promise<BatchResult<R>>Runs many calls with limited concurrency, reporting progress and cost, and stops early when a call is overcharged, a spend limit is reached or the signal aborts. Pass the given signal to each Hub call.
embed
embed(role: string, values: string[], opts?: EmbedOptions): Promise<EmbedResult>
Embeddings for a batch of texts.
models
models(filter?: {
provider?: string;
needs?: Capability[];
}): ModelInfo[]Models the Hub knows, filtered by provider or by what they can do (for pickers).
skills
skills(): SkillSummary[]
Skills the Hub can apply (name them in skills on a call): the Hub's own, those plugins registered, and the vault's. Vault skills arrive at layout-ready: await skillsReady() before relying on them.
registerSkillssince 0.4
registerSkills(pluginId: string, skills: SkillRegistration[]): void
Registers your plugin's built-in skills, each as its folder's text files: { files: { 'SKILL.md': …, 'reader.yml': …, 'prompts/extract.md': … } }. Replaces what your plugin registered before ([] removes them). They stay through every reload of the vault skills, and are removed when your plugin unloads. A vault skill of the same name overrides one; a name belongs to the first plugin to register it. Throws a HubError: skill-taken (another plugin's, or the Hub's own), invalid-skill (no or bad SKILL.md, a path outside the folder, a name given twice, mode: always: a plugin's skill is used only when a call names it; the user can make a vault copy always-on), invalid-request (no plugin id). (Since 0.4.)
Registrations live in the Hub that is loaded: when the user disables and enables Model Hub, the new Hub starts without them and fires model-hub:ready on the workspace. Register again then: onHubReady (below getHub) does both, or this.registerEvent(this.app.workspace.on('model-hub:ready', (api) => …)).
skillsReadysince 0.4
skillsReady(): Promise<void>
Resolves once the vault's skills have loaded (at layout-ready); registered skills are there as soon as registered. (Since 0.4.)
onSkillsChangedsince 0.4
onSkillsChanged(cb: () => void): () => void
Calls cb whenever the skills change: a plugin registers or unregisters, or the skills folder is edited (files included) or reloaded. Returns the function that stops it; pass it to this.register(). (Since 0.4.)
usage
usage(filter?: LedgerFilter): Promise<LedgerRow[]>
Ledger rows: every call, with tokens and cost.
Functions
isBuiltinTier
declare function isBuiltinTier(s: unknown): s is BuiltinTierName;
True for the id of a built-in tier (quick, balanced…); false for a custom tier's id or any name.
isCompatible
declare function isCompatible(actual: string, wanted: string): boolean;
True when a consumer built against wanted can use an API at actual: same major and a minor at least as new. Before 1.0, minors break by default, so a 0.x Hub serves only the minors it promises (0.1 and up).
getHub
declare function getHub(app: unknown, wanted: string): GetHubResult;
Finds the Hub in this vault and checks its API version against the one you built for. The Hub may be disabled and enabled again while your plugin runs, and the new Hub has none of your registered skills: listen for model-hub:ready (or use onHubReady) and call getHub + registerSkills again there.
onHubReadysince 0.4
declare function onHubReady(app: unknown, plugin: {
registerEvent(ref: unknown): void;
}, wanted: string, cb: (hub: HubApi) => void): void;Runs cb with the Hub now if it is loaded (and compatible with wanted), and again each time the Hub loads: the Hub fires model-hub:ready on the workspace when it loads, and one disabled and enabled again starts with no registered skills. Do your registerSkills (and registerRoles, harmless to repeat) in cb, on the hub it is given. The listener is registered on your plugin, so it stops when your plugin unloads. (Since 0.4.)
onHubReady(this.app, this, '0.4.0', (hub) => hub.registerSkills(this.manifest.id, MY_SKILLS));
Without the helper: this.registerEvent(this.app.workspace.on('model-hub:ready', (api) => …)), checking api.version with isCompatible.
Constants
BUILTIN_TIERS
declare const BUILTIN_TIERS: readonly [
"quick",
"balanced",
"careful",
"frontier",
"research",
"embedding",
"decision"
];The seven tiers every Hub has. They can't be deleted, and no custom tier may shadow one.
DEFAULT_TIER
declare const DEFAULT_TIER: BuiltinTierName;
What a role resolves to when it names neither a tier nor models, or names a tier that doesn't exist.
HUB_PLUGIN_ID
declare const HUB_PLUGIN_ID = "model-hub";
Model Hub's plugin id, as app.plugins.getPlugin knows it.
HUB_READY_EVENT
declare const HUB_READY_EVENT = "model-hub:ready";
The workspace event the Hub fires each time it loads, with its API: at startup, and when the user disables and enables it again. A new Hub starts with no registered skills, so consumers register theirs again on it.
TIER_NAMES
declare const TIER_NAMES: readonly [
"quick",
"balanced",
"careful",
"frontier",
"research",
"embedding",
"decision"
];The name API 0.1–0.3 gave {@link BUILTIN_TIERS}; kept so their consumers still compile.
Types
Select a type to see its members.
Answer type
The answer to one question, in the shape of its type.
type Answer = {
type: "noul";
noul: number;
} | {
type: "choice";
choice: string;
probabilities: Record<string, number>;
confidence?: number;
} | {
type: "score";
score: number;
legend: Record<string, string>;
probabilities: Record<string, number>;
confidence?: number;
};Attempt interface
One candidate the call considered: skipped before sending, sent and failed, or the one that answered.
interface Attempt
provider: stringmodel: stringoutcome: "skipped" | "failed" | "ok"code?: HubErrorCodereason?: string
BatchOptions interface
Options for batch().
interface BatchOptions
concurrency?: numberCalls running at once (default 4).
stopOnOvercharge?: booleanStop the batch when a call is billed well above its listed price (default true).
maxSpendUsd?: numberStop the batch once this much has been spent, in US dollars.
signal?: AbortSignalStops the whole batch.
onProgress?: (p: BatchProgress) => voidCalled after each item finishes or fails.
BatchProgress interface
How far a batch has got.
interface BatchProgress
done: numberItems finished, failures included.
total: numberfailed: numberItems whose call rejected.
spentUsd: numberSpent so far: each call's billed cost, or its listed cost when the provider reports none.
BatchResult interface
What batch() resolves to. It doesn't reject when calls fail: each failure is in results.
interface BatchResult<R>
results: (R | HubError | undefined)[]One entry per item, in order: the result, the error, or undefined when the batch stopped before it.
progress: BatchProgressThe final count and spend.
stopped?: { reason: "overcharge" | "spend-limit" | "aborted"; index: number; message: string; }Why the batch stopped early, if it did.
BillingKind type
How a provider charges: per token, nothing (local), or a flat subscription.
type BillingKind = "metered" | "local" | "subscription";
BudgetStatus interface
One monthly budget that applies to a call, and how much of it is spent.
interface BudgetStatus
scope: "role" | "tier"Whether the budget is the role's own or its tier's.
spentUsd: numberSpent this calendar month, before this call.
limitUsd: numberwarn: booleanSpent at least 80% of the limit.
BuiltinTierName type
The id of one of the seven built-in tiers.
type BuiltinTierName = (typeof BUILTIN_TIERS)[number];
CallOptionsBase interface · not exported, part of another type
interface CallOptionsBase
tier?: TierNameUse this tier's models and limits for one run instead of the role's (roles that allow any model).
routing?: Record<string, unknown>Routing merged into every candidate's own routing for this call (OpenRouter: pin or exclude providers, a price cap…). Recorded in the ledger.
candidates?: Candidate[]This call's own ordered candidates (roles that allow any model). Limits still apply.
model?: { provider: string; model: string; }Use this model for one run instead of the role's candidates (no fallback). It must be one of the role's candidates, or the role must allow any model.
for?: stringWhat the call is about, for the ledger.
signal?: AbortSignalStops the call; it then rejects with
aborted.
CallReport interface
What every finished call reports: who served it, the usage, the cost, and how it got there.
interface CallReport
provider: stringbilling: LedgerRow["billing"]How the provider charges: per token, nothing (local) or a subscription.
askedModel: stringservedModel?: stringservedProvider?: stringusage: UsagelistedUsd: number | nullUS dollars at listed prices (for a subscription: the API-price equivalent); null when unknown.
billedUsd?: numberovercharged: booleandurationMs: numberledgerId: stringtier: TierNameattempts: Attempt[]Every candidate considered, in order.
fellBack: booleanTrue when the answer didn't come from the first candidate.
budgets: BudgetStatus[]Monthly budgets that apply, with this month's spend before this call.
Candidate interface
One model to try: a provider, a model, and how to call it.
interface Candidate
provider: stringmodel: stringrouting?: Record<string, unknown>Provider-specific routing (OpenRouter: order, ignore, max_price, data policy, fallbackModels…).
reasoning?: booleanmaxOutputTokens?: numbercontextTokens?: numberContext length to ask a local model for (Ollama's num_ctx).
CAPABILITIES const · not exported, part of another type
declare const CAPABILITIES: readonly [
"streaming",
"tools",
"structuredOutput",
"reasoning",
"webSearch",
"imageInput",
"pdfInput",
"embeddings",
"promptCaching",
"decisions"
];Capability type
One thing a model can do. A call that needs a capability its model lacks is refused (capability-missing).
type Capability = (typeof CAPABILITIES)[number];
CompleteOptions interface
Options for complete().
interface CompleteOptions extends CallOptionsBase
system?: stringThe system prompt. Skills' text goes before it.
maxOutputTokens?: numberThe longest reply allowed (default 4096). The per-call ceiling is checked against the cost of a reply this long.
tools?: ToolSetTools the model may call. They are returned, not run: v1 is single-turn.
output?: FlexibleSchema<unknown>A schema (zod or JSON Schema) the reply must match.
webSearch?: booleanLet the provider search the web; the model must have
webSearch. Sources come back insources.needs?: Capability[]Capabilities the caller needs beyond what the options imply.
skills?: string[]Skills to apply, by name. When given (even empty), the always-on skills join them; their text goes before
system, and what they require of the model is checked like any other capability.
CompleteResult interface
What complete() resolves to: the reply, and the call's report.
interface CompleteResult extends CallReport
text: stringThe reply's text.
object?: unknownThe reply as data, when the call gave an
outputschema.toolCalls: { toolName: string; input: unknown; }[]Tools the model asked to call. The Hub returns them; it doesn't run them.
sources: { url: string; title?: string; }[]Web pages the reply drew on, when the call searched the web.
finishReason: stringWhy the model stopped, as the provider reports it (
stop,length,tool-calls…).skills?: { included: string[]; dropped: string[]; }Skills that went into the request, and any left out for size.
DecideOptions type
Options for decide().
type DecideOptions = CallOptionsBase;
DecideResult interface
What decide() resolves to: the answers, whether they are calibrated, and the call's report.
interface DecideResult extends CallReport, Pick<DecisionAnswers, "answers" | "calibrated">
DecisionAnswers interface · not exported, part of another type
interface DecisionAnswers
answers: Record<string, Answer>One answer per question, under the question's id.
calibrated: booleanTrue when the probabilities come from a calibrated decision model (e.g. Jev), not a text model's single answer.
usage: { inputTokens: number; outputTokens: number; }model?: stringThe model that answered, when the provider says.
DecisionInput type
Text, or structured data the question can refer to by field name in backticks.
type DecisionInput = string | Record<string, unknown> | unknown[];
DecisionRequest interface
What decide() takes: the thing being judged, and the questions to answer about it.
interface DecisionRequest
state: DecisionInputWhat the questions are about: text, or structured data.
questions: Record<string, Question>The questions, each under an id of your choosing; the answers come back under the same ids.
EmbedOptions type
Options for embed().
type EmbedOptions = CallOptionsBase;
EmbedResult interface
What embed() resolves to.
interface EmbedResult extends CallReport
embeddings: number[][]One vector per input text, in order.
GetHubResult type
What getHub returns: the Hub, or why there isn't one, with a message to show the user.
type GetHubResult = {
hub: HubApi;
} | {
hub: null;
reason: "missing" | "incompatible";
message: string;
version?: string;
};HubError class · not exported, part of another type
class HubError extends Error
readonly code: HubErrorCodeWhy the call was refused or failed.
readonly details: Record<string, unknown>Facts behind the refusal, by code: the role, the tier, the estimate and ceiling, what the model lacks (
missing)…
HubErrorCode type
Why the Hub refused or failed a call. Every refusal names its cause; nothing falls back silently.
type HubErrorCode = "invalid-role-name" | "invalid-skill" | "skill-taken" | "unknown-role" | "role-taken" | "unknown-tier" | "invalid-tier-name" | "unknown-provider" | "unknown-model" | "model-not-allowed" | "capability-missing" | "unpriced-model" | "over-ceiling" | "over-budget" | "no-models" | "invalid-request" | "provider-unavailable" | "provider-error" | "aborted";
LedgerFilter interface
Which ledger rows usage() returns. Every field given must match; none returns every row.
interface LedgerFilter
plugin?: stringCalls made through this plugin's roles.
role?: stringCalls through this role.
since?: stringCalls at or after this time (ISO 8601).
until?: stringCalls before this time (ISO 8601).
LedgerRow interface
One call, as the ledger records it. Refusals and failures are recorded too, at cost 0.
interface LedgerRow
id: stringtime: stringplugin: stringrole: stringtier?: stringThe id of the tier the role resolved to.
tierName?: stringThat tier's name at the time of the call.
attempt?: numberWhich try this was, when the call fell back along its candidates (1 = the first candidate sent to).
routing?: Record<string, unknown>Routing the caller added for this call, if any.
for?: stringWhat the call was about, as the calling plugin describes it.
operation: "complete" | "stream" | "embed" | "decide"provider: stringbilling: BillingKindaskedModel: stringservedModel?: stringservedProvider?: stringusage: UsagelistedUsd: number | nullUS dollars at listed prices; null when the model has no listed price.
billedUsd?: numberUS dollars the provider says it billed, when it says.
overcharged: booleandurationMs: numberoutcome: "ok" | "refused" | "failed"errorCode?: HubErrorCodeerrorMessage?: string
ModelInfo interface
A model as the Hub knows it, from its provider's own listing. models() returns these for pickers.
interface ModelInfo
provider: stringThe provider's id in the Hub, such as
openrouterorollama.id: stringThe model's id at that provider, as a candidate names it.
label?: stringA name to show; fall back to
id.contextLength?: numberContext window in tokens, when the provider says.
pricing: Pricingcapabilities: Capability[]
ModelPolicy interface
The models to try and the spending limits: what a tier holds, and what a role may carry of its own.
interface ModelPolicy
candidates: Candidate[]Priority order: the first that can serve the call is used; a failed call moves to the next.
ceilingUsd?: numberMost a single call may cost, in US dollars.
monthlyBudgetUsd?: numberMost this may spend in a calendar month, in US dollars (metered calls only).
Pricing interface
US dollars per token. null means the price is unknown.
interface Pricing
input: number | nulloutput: number | nullcacheRead?: number | nullcacheWrite?: number | null
Question type
One question about the state: yes/no (noul), pick one (choice) or rate on levels (score).
type Question = {
type: "noul";
instructions: DecisionInput;
criteria?: {
true?: DecisionInput;
false?: DecisionInput;
};
} | {
type: "choice";
instructions: DecisionInput;
criteria: Record<string, DecisionInput | null>;
} | {
type: "score";
instructions: DecisionInput;
criteria: DecisionInput[];
};ResolvedRole interface
What a call through a role uses: the candidates to try, and every limit that applies.
interface ResolvedRole extends RoleSpec
effective: Candidate[]The candidates to try, in order (the role's own, else the tier's).
ceilings: number[]Every per-call ceiling that applies (role and tier); a call must fit all of them.
budgets: { scope: "role" | "tier"; limitUsd: number; }[]Every monthly budget that applies.
tierName: stringThe display name of the tier
tieris the id of.tierMissing?: trueSet when the role names a tier that doesn't exist (a deleted or not-yet-made custom tier): it runs on
balanced.missingTier?: TierNameThe tier the role names (as it names it) but that doesn't exist.
RoleDefinition interface
A role as a plugin registers it: a name, a description, and its model policy — a tier and/or its own ordered candidates, a per-call ceiling and a monthly budget. The same schema as a tier.
The one-model form (provider + model, API 0.1) still works and becomes a one-candidate list.
interface RoleDefinition extends Partial<ModelPolicy>
name: stringdescription?: stringtier?: TierNameThe tier this role uses, built in or the user's own, by id or by name;
balancedwhen it names none. A tier that doesn't exist (yet) is kept as named: the role runs onbalanced, flaggedtierMissing, until it is made. Store theidcreateTierreturns; a name keeps working across renames (old names stay as aliases).allowAnyModel?: booleanAny model in the catalog may be picked for one run (interactive roles such as the chat panel's). Limits still apply.
provider?: stringAPI 0.1: one provider and model.
model?: stringreasoning?: booleanmaxOutputTokens?: numbercontextTokens?: numberrouting?: Record<string, unknown>allowedModels?: { provider: string; model: string; }[]API 0.1: models a caller may pick for one run.
RoleOverride interface
The user's setting for a role: any of a tier, own candidates and limits. null fields clear back to the plugin's default.
interface RoleOverride
tier?: TierNamecandidates?: Candidate[]ceilingUsd?: number | nullmonthlyBudgetUsd?: number | null
RoleSpec interface
A role's settings after the plugin's default and the user's override are combined.
interface RoleSpec
name: stringowner: stringdescription?: stringtier: TierNamecandidates: Candidate[]The role's own candidates; empty means "the tier's".
ceilingUsd?: numbermonthlyBudgetUsd?: numberallowAnyModel: booleanallowedModels: { provider: string; model: string; }[]overridden: booleanTrue when the user has changed anything.
SkillRegistration interface
A skill a plugin registers: its folder's text files, by relative path, SKILL.md among them.
interface SkillRegistration
files: Record<string, string>
SkillSummary interface
A skill as skills() lists it.
interface SkillSummary
name: stringThe name a call puts in
skills.description: stringWhat the skill is for, from its
SKILL.md.mode: "always" | "chosen"alwaysskills join every call that asks for skills. Only the user's vault skills may bealways.requires: Capability[]What a model must be able to do for this skill to be used.
origin: "builtin" | "vault"builtin: shipped with the Hub or registered by a plugin (seeplugin);vault: in the user's skills folder.plugin?: stringsince 0.4The plugin that registered this skill with
registerSkills. (Since 0.4.)extends?: stringA skill this one layers on; its instructions are included before this one's.
extendsMissing?: booleansince 0.4extendsnames a skill that isn't loaded (for example, the plugin that registers it is off). (Since 0.4.)files(): Promise<Record<string, string>>since 0.4The skill's own text files, by path relative to its folder (
reader.yml,prompts/extract.md), recursive, withoutSKILL.md. A vault skill's are read from the vault at each call, so edits show at once. Only this skill's files: a parent's (extends) are the parent'sfiles(); merging them is up to you. (Since 0.4.)
StreamOptions type
Options for stream(): those of complete(), without structured output.
type StreamOptions = Omit<CompleteOptions, "output">;
StreamResult interface
What stream() returns at once: the text as it arrives, and a promise of the full result.
interface StreamResult
textStream: AsyncIterable<string>Text as it arrives. Ends with a HubError if the call fails or is stopped.
result: Promise<CompleteResult>Resolves when the stream ends, with the full result; rejects with a HubError.
Tier type
A tier as listed: its policy, its stable id (a built-in tier's is its name; a custom tier's never changes, even when it is renamed), its display name, and whether the user made it.
type Tier = ModelPolicy & {
id: string;
name: TierName;
custom: boolean;
};TierName type
A tier's id (a built-in tier's name, or a custom tier's id such as clusters-bulk); lookups also accept its name.
type TierName = string;
Usage interface
Tokens a call used, as the provider reported them.
interface Usage
inputTokens: numberAll input tokens, cached ones included.
outputTokens: numberAll output tokens, reasoning included.
cacheReadTokens: numberInput tokens read from the provider's prompt cache.
cacheWriteTokens: numberInput tokens written to the provider's prompt cache.
reasoningTokens: numberOutput tokens spent on reasoning.