JuliusBrussee/caveman · error · Error

cave_budget_denomination_unavailable

Error message

cave_budget_denomination_unavailable

What it means

The caller set options.maxBudgetUsd (a USD spend cap) but the SDK's init message reports an apiKeySource that maps to a non-metered credential regime (subscription or unknown) via claudeCredentialRegime(). A USD cap only has meaning when per-token dollar billing is provable; subscription or unknown credentials cannot authorize it, so the run fails closed before any model call.

Source

Thrown at packages/agent/src/claude-runtime.ts:255

    const toolCalls: string[] = [];
    try {
      for await (const message of query) {
        if (message.type === "system" && message.subtype === "init") {
          initVersion = message.claude_code_version;
          // The exact-pin is enforced at the FIRST message the SDK emits, before
          // it drives any model call, so a version mismatch costs nothing rather
          // than being caught only after the whole run has drained and spent
          // `finally` closes the query.
          if (initVersion !== CLAUDE_CODE_VERSION) {
            throw new Error("cave_harness_upstream_version_mismatch");
          }
          credentialRegime = claudeCredentialRegime(message.apiKeySource);
          // apiKeySource is emitted on init before the SDK drives a model call.
          // It is the credential the SDK actually selected, unlike ambient env
          // presence. A subscription or unknown regime cannot authorize a USD
          // cap because no per-token dollar charge is proven.
          if (options.maxBudgetUsd !== undefined && credentialRegime !== "metered") {
            throw new Error("cave_budget_denomination_unavailable");
          }
          assistantModel ??= message.model;
        }
        if (message.type === "assistant") {
          assistantModel = message.message.model;
          for (const block of message.message.content) {
            if (block.type === "tool_use") toolCalls.push(unprefixClaudeTool(block.name));
          }
        }
        if (message.type === "result") result = message;
      }
    } finally {
      query.close?.();
    }
    if (initVersion === undefined) {
      // The SDK never announced its version — the exact-pin cannot be proven,
      // so this fails closed the same as a mismatch.
      throw new Error("cave_harness_upstream_version_mismatch");

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Set ANTHROPIC_API_KEY to a metered API key in the environment the agent runs in, and remove the subscription credential the SDK is currently selecting.
  2. Drop options.maxBudgetUsd if you intentionally run on a subscription and want the run to proceed without a dollar cap.
  3. Run `claude doctor` / inspect the init message's apiKeySource to see which credential the CLI actually selected, then fix the auth setup accordingly.

Example fix

// before: subscription credential + USD cap → throws on init
await run({ prompt, maxBudgetUsd: 1.5 });

// after: either supply a metered key…
process.env.ANTHROPIC_API_KEY = meteredKey;
await run({ prompt, maxBudgetUsd: 1.5 });
// …or omit the cap on subscription auth
await run({ prompt });
Defensive patterns

Strategy: validation

Validate before calling

function meteredCredentialLikely(): boolean {
  // The definitive source is the init message's apiKeySource, but absence of a
  // subscription login plus a present ANTHROPIC_API_KEY is a strong precondition.
  return Boolean(process.env.ANTHROPIC_API_KEY) && !hasClaudeSubscriptionLogin();
}
function hasClaudeSubscriptionLogin(): boolean {
  // subscription token stored by `claude login`, location varies by OS
  return existsSync(join(homedir(), ".claude", "credentials.json"));
}

Try / catch

try {
  await run({ ...options, maxBudgetUsd });
} catch (error) {
  if (error instanceof Error && error.message === "cave_budget_denomination_unavailable") {
    // Subscription credential cannot back a USD cap — drop the cap or switch to a metered key.
    return run({ ...options }); // no maxBudgetUsd
  }
  throw error;
}

Prevention

When it happens

Trigger: Calling the Claude run API with maxBudgetUsd defined while the environment authenticates with a Claude subscription (OAuth/login) or an unrecognizable credential, i.e. apiKeySource on init is anything but a metered API key. Checked on the init message, so it throws before the first model turn.

Common situations: Developer machine logged into Claude Pro/Max via `claude login` while the config requests a dollar budget; CI missing ANTHROPIC_API_KEY so the SDK falls back to a stored subscription token; a new apiKeySource string the regime classifier doesn't recognize after a CLI update.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/5fb87a892777b040. Report an issue: GitHub.