{"record":{"id":"5fb87a892777b040","repo":"JuliusBrussee/caveman","slug":"cave-budget-denomination-unavailable","errorCode":null,"errorMessage":"cave_budget_denomination_unavailable","messagePattern":"cave_budget_denomination_unavailable","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/agent/src/claude-runtime.ts","lineNumber":255,"sourceCode":"    const toolCalls: string[] = [];\n    try {\n      for await (const message of query) {\n        if (message.type === \"system\" && message.subtype === \"init\") {\n          initVersion = message.claude_code_version;\n          // The exact-pin is enforced at the FIRST message the SDK emits, before\n          // it drives any model call, so a version mismatch costs nothing rather\n          // than being caught only after the whole run has drained and spent\n          // `finally` closes the query.\n          if (initVersion !== CLAUDE_CODE_VERSION) {\n            throw new Error(\"cave_harness_upstream_version_mismatch\");\n          }\n          credentialRegime = claudeCredentialRegime(message.apiKeySource);\n          // apiKeySource is emitted on init before the SDK drives a model call.\n          // It is the credential the SDK actually selected, unlike ambient env\n          // presence. A subscription or unknown regime cannot authorize a USD\n          // cap because no per-token dollar charge is proven.\n          if (options.maxBudgetUsd !== undefined && credentialRegime !== \"metered\") {\n            throw new Error(\"cave_budget_denomination_unavailable\");\n          }\n          assistantModel ??= message.model;\n        }\n        if (message.type === \"assistant\") {\n          assistantModel = message.message.model;\n          for (const block of message.message.content) {\n            if (block.type === \"tool_use\") toolCalls.push(unprefixClaudeTool(block.name));\n          }\n        }\n        if (message.type === \"result\") result = message;\n      }\n    } finally {\n      query.close?.();\n    }\n    if (initVersion === undefined) {\n      // The SDK never announced its version — the exact-pin cannot be proven,\n      // so this fails closed the same as a mismatch.\n      throw new Error(\"cave_harness_upstream_version_mismatch\");","sourceCodeStart":237,"sourceCodeEnd":273,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/claude-runtime.ts#L237-L273","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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.","Drop options.maxBudgetUsd if you intentionally run on a subscription and want the run to proceed without a dollar cap.","Run `claude doctor` / inspect the init message's apiKeySource to see which credential the CLI actually selected, then fix the auth setup accordingly."],"exampleFix":"// before: subscription credential + USD cap → throws on init\nawait run({ prompt, maxBudgetUsd: 1.5 });\n\n// after: either supply a metered key…\nprocess.env.ANTHROPIC_API_KEY = meteredKey;\nawait run({ prompt, maxBudgetUsd: 1.5 });\n// …or omit the cap on subscription auth\nawait run({ prompt });","handlingStrategy":"validation","validationCode":"function meteredCredentialLikely(): boolean {\n  // The definitive source is the init message's apiKeySource, but absence of a\n  // subscription login plus a present ANTHROPIC_API_KEY is a strong precondition.\n  return Boolean(process.env.ANTHROPIC_API_KEY) && !hasClaudeSubscriptionLogin();\n}\nfunction hasClaudeSubscriptionLogin(): boolean {\n  // subscription token stored by `claude login`, location varies by OS\n  return existsSync(join(homedir(), \".claude\", \"credentials.json\"));\n}","typeGuard":null,"tryCatchPattern":"try {\n  await run({ ...options, maxBudgetUsd });\n} catch (error) {\n  if (error instanceof Error && error.message === \"cave_budget_denomination_unavailable\") {\n    // Subscription credential cannot back a USD cap — drop the cap or switch to a metered key.\n    return run({ ...options }); // no maxBudgetUsd\n  }\n  throw error;\n}","preventionTips":["Only set maxBudgetUsd in environments that authenticate with a metered ANTHROPIC_API_KEY.","Run `claude logout` (or clear the stored subscription credential) on machines that must run budgeted agents.","Treat this error as fail-closed by design: do not retry with the same options; change the credential or the cap."],"tags":["budget","credentials","claude-sdk","fail-closed"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}