Insurgently

Obsidian plugin· Active · beta · not yet released

Model Hub

Named roles mapped to provider and model, keys kept in secret storage, and a cost ledger with the billed cost of every call. Other plugins call it for their AI.

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: string
  • model: string
  • outcome: "skipped" | "failed" | "ok"
  • code?: HubErrorCode
  • reason?: string
BatchOptions interface

Options for batch().

interface BatchOptions
  • concurrency?: number

    Calls running at once (default 4).

  • stopOnOvercharge?: boolean

    Stop the batch when a call is billed well above its listed price (default true).

  • maxSpendUsd?: number

    Stop the batch once this much has been spent, in US dollars.

  • signal?: AbortSignal

    Stops the whole batch.

  • onProgress?: (p: BatchProgress) => void

    Called after each item finishes or fails.

BatchProgress interface

How far a batch has got.

interface BatchProgress
  • done: number

    Items finished, failures included.

  • total: number
  • failed: number

    Items whose call rejected.

  • spentUsd: number

    Spent 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: BatchProgress

    The 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: number

    Spent this calendar month, before this call.

  • limitUsd: number
  • warn: boolean

    Spent 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?: TierName

    Use 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?: string

    What the call is about, for the ledger.

  • signal?: AbortSignal

    Stops 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: string
  • billing: LedgerRow["billing"]

    How the provider charges: per token, nothing (local) or a subscription.

  • askedModel: string
  • servedModel?: string
  • servedProvider?: string
  • usage: Usage
  • listedUsd: number | null

    US dollars at listed prices (for a subscription: the API-price equivalent); null when unknown.

  • billedUsd?: number
  • overcharged: boolean
  • durationMs: number
  • ledgerId: string
  • tier: TierName
  • attempts: Attempt[]

    Every candidate considered, in order.

  • fellBack: boolean

    True 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: string
  • model: string
  • routing?: Record<string, unknown>

    Provider-specific routing (OpenRouter: order, ignore, max_price, data policy, fallbackModels…).

  • reasoning?: boolean
  • maxOutputTokens?: number
  • contextTokens?: number

    Context 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?: string

    The system prompt. Skills' text goes before it.

  • maxOutputTokens?: number

    The longest reply allowed (default 4096). The per-call ceiling is checked against the cost of a reply this long.

  • tools?: ToolSet

    Tools 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?: boolean

    Let the provider search the web; the model must have webSearch. Sources come back in sources.

  • 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: string

    The reply's text.

  • object?: unknown

    The reply as data, when the call gave an output schema.

  • 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: string

    Why 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: boolean

      True 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?: string

      The 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: DecisionInput

      What 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: HubErrorCode

      Why 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?: string

      Calls made through this plugin's roles.

    • role?: string

      Calls through this role.

    • since?: string

      Calls at or after this time (ISO 8601).

    • until?: string

      Calls 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: string
    • time: string
    • plugin: string
    • role: string
    • tier?: string

      The id of the tier the role resolved to.

    • tierName?: string

      That tier's name at the time of the call.

    • attempt?: number

      Which 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?: string

      What the call was about, as the calling plugin describes it.

    • operation: "complete" | "stream" | "embed" | "decide"
    • provider: string
    • billing: BillingKind
    • askedModel: string
    • servedModel?: string
    • servedProvider?: string
    • usage: Usage
    • listedUsd: number | null

      US dollars at listed prices; null when the model has no listed price.

    • billedUsd?: number

      US dollars the provider says it billed, when it says.

    • overcharged: boolean
    • durationMs: number
    • outcome: "ok" | "refused" | "failed"
    • errorCode?: HubErrorCode
    • errorMessage?: string
    ModelInfo interface

    A model as the Hub knows it, from its provider's own listing. models() returns these for pickers.

    interface ModelInfo
    • provider: string

      The provider's id in the Hub, such as openrouter or ollama.

    • id: string

      The model's id at that provider, as a candidate names it.

    • label?: string

      A name to show; fall back to id.

    • contextLength?: number

      Context window in tokens, when the provider says.

    • pricing: Pricing
    • capabilities: 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?: number

      Most a single call may cost, in US dollars.

    • monthlyBudgetUsd?: number

      Most 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 | null
    • output: number | null
    • cacheRead?: number | null
    • cacheWrite?: 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: string

      The display name of the tier tier is the id of.

    • tierMissing?: true

      Set when the role names a tier that doesn't exist (a deleted or not-yet-made custom tier): it runs on balanced.

    • missingTier?: TierName

      The 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: string
    • description?: string
    • tier?: TierName

      The tier this role uses, built in or the user's own, by id or by name; balanced when it names none. A tier that doesn't exist (yet) is kept as named: the role runs on balanced, flagged tierMissing, until it is made. Store the id createTier returns; a name keeps working across renames (old names stay as aliases).

    • allowAnyModel?: boolean

      Any model in the catalog may be picked for one run (interactive roles such as the chat panel's). Limits still apply.

    • provider?: string

      API 0.1: one provider and model.

    • model?: string
    • reasoning?: boolean
    • maxOutputTokens?: number
    • contextTokens?: number
    • routing?: 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?: TierName
    • candidates?: Candidate[]
    • ceilingUsd?: number | null
    • monthlyBudgetUsd?: number | null
    RoleSpec interface

    A role's settings after the plugin's default and the user's override are combined.

    interface RoleSpec
    • name: string
    • owner: string
    • description?: string
    • tier: TierName
    • candidates: Candidate[]

      The role's own candidates; empty means "the tier's".

    • ceilingUsd?: number
    • monthlyBudgetUsd?: number
    • allowAnyModel: boolean
    • allowedModels: { provider: string; model: string; }[]
    • overridden: boolean

      True 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: string

      The name a call puts in skills.

    • description: string

      What the skill is for, from its SKILL.md.

    • mode: "always" | "chosen"

      always skills join every call that asks for skills. Only the user's vault skills may be always.

    • 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 (see plugin); vault: in the user's skills folder.

    • plugin?: string since 0.4

      The plugin that registered this skill with registerSkills. (Since 0.4.)

    • extends?: string

      A skill this one layers on; its instructions are included before this one's.

    • extendsMissing?: boolean since 0.4

      extends names 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.4

      The skill's own text files, by path relative to its folder (reader.yml, prompts/extract.md), recursive, without SKILL.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's files(); 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: number

      All input tokens, cached ones included.

    • outputTokens: number

      All output tokens, reasoning included.

    • cacheReadTokens: number

      Input tokens read from the provider's prompt cache.

    • cacheWriteTokens: number

      Input tokens written to the provider's prompt cache.

    • reasoningTokens: number

      Output tokens spent on reasoning.