JuliusBrussee/caveman · error · Error

cave_budget_denomination_unavailable

cave_budget_denomination_unavailable

Error message

cave_budget_denomination_unavailable

What it means

A USD-denominated budget (budget: { maxUsd }) requires that the runtime can honestly price the run's model. The runtime-gated denomination check has two grounds: the public catalog must price the provider/model (checked here), and the credential regime must actually bill in dollars (checked after routing). If the selected model has no catalog price, a USD cap would meter an honest zero and never bind, so the run fails closed.

Solutions

  1. Switch to a model the public catalog prices (verify with modelIsPriced(provider, modelId) before the run).
  2. Regenerate/upgrade the package so catalog.ts includes the model's USD row (catalog is generated; never hand-edit prices).
  3. Budget in tokens instead: budget: { maxTokens: n } avoids the USD pricing requirement.
  4. If the run is gateway-routed or uses a caller-supplied streamFn, ensure the credential gate applies as documented; otherwise drop the USD cap and rely on receipt reporting.

Example fix

// before
runAgent(def, input, { budget: { maxUsd: 5 }, model: "openai/gpt-5-preview-niche" });
// after
if (modelIsPriced("openai", "gpt-5-preview-niche")) {
  runAgent(def, input, { budget: { maxUsd: 5 } });
} else {
  runAgent(def, input, { budget: { maxTokens: 200_000 } });
}
Defensive patterns

Strategy: validation

Validate before calling

import { modelIsPriced } from "@caveman-ai/agent";
if (opts.budget?.maxUsd !== undefined && !modelIsPriced(provider, modelId)) {
  // fall back to token denomination or pick a priced model
}

Type guard

function usdBudgetIsAdmissible(budget, provider, modelId) {
  return budget?.maxUsd === undefined || modelIsPriced(provider, modelId);
}

Try / catch

try {
  await runAgent(def, input, opts);
} catch (err) {
  if (err.message === "cave_budget_denomination_unavailable") {
    await runAgent(def, input, {
      ...opts,
      budget: { maxTokens: 200_000 }, // token denomination needs no pricing
    });
  } else throw err;
}

Prevention

When it happens

Trigger: Setting options.budget with maxUsd (or a USD denomination) while the resolved model is not priced in the generated public catalog (modelIsPriced(provider, id) is false) — e.g. a niche/preview model id, a custom provider string, or a typo'd model id.

Common situations: New or regional-only catalog rows absent from the generated catalog; custom/self-hosted provider ids; subscription (Claude Pro/Max) credential regimes failing the second ground; using an old package version whose catalog predates the model.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/cc879d60c7ffdd2b. Report an issue: GitHub.

Appendix: source

Thrown at packages/agent/src/runtime.ts:1242

      conversationOriginals,
    );

    const gatewayURL = resolveGatewayURL(options.gatewayURL);
    const caveRoute = await resolveCaveRoute(gatewayURL, {
      ...options,
      billingProofRequired: budgetMeter?.denomination === "usd" && options.streamFn === undefined,
    }, efficiencyPlan !== undefined);
    const nestedOptions: InternalRunOptions = { ...options, caveRoute };

    const models = options.models ?? builtinModels();
    const model = options.model ?? resolveModel(definition, models, options.rootDir ?? process.cwd());
    // Runtime-gated denomination, first ground: the catalog must
    // price the model, or a USD cap meters an honest zero and never binds. The
    // second ground — the credential regime — is checked after routing below,
    // because which credential pays depends on where the request goes.
    if (budgetMeter?.denomination === "usd" &&
        !modelIsPriced(model.provider, model.id)) {
      throw new Error("cave_budget_denomination_unavailable");
    }
    // Actual routing is the source of truth, not the route decision: the
    // gateway only speaks the three provider dialects it proxies, so a model
    // outside them keeps its own base URL even on a reachable gateway. Every
    // downstream honesty question — which headers may be sent, what mode this
    // run may claim — reads gatewayActive, never caveRoute.useGateway alone.
    const routing = caveRoute.useGateway
      ? routeModelThroughCave(model, gatewayURL)
      : { model, routed: false };
    const routedModel = routing.model;
    const gatewayActive = routing.routed;
    // Runtime-gated denomination, second ground: the run must actually be
    // BILLED in dollars. A Claude Pro/Max subscription reached through Pi's
    // credential store is not billed per token, so every dollar this ledger
    // reported for it would be fiction.
    //
    // Which credential pays is what decides this. A caller-supplied streamFn
    // owns its transport, so local login says nothing about billing. A routed

View on GitHub (pinned to 3ee70a1026)