{"record":{"id":"cc879d60c7ffdd2b","repo":"JuliusBrussee/caveman","slug":"cave-budget-denomination-unavailable-cc879d","errorCode":"cave_budget_denomination_unavailable","errorMessage":"cave_budget_denomination_unavailable","messagePattern":"cave_budget_denomination_unavailable","errorType":"error_code","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/agent/src/runtime.ts","lineNumber":1242,"sourceCode":"      conversationOriginals,\n    );\n\n    const gatewayURL = resolveGatewayURL(options.gatewayURL);\n    const caveRoute = await resolveCaveRoute(gatewayURL, {\n      ...options,\n      billingProofRequired: budgetMeter?.denomination === \"usd\" && options.streamFn === undefined,\n    }, efficiencyPlan !== undefined);\n    const nestedOptions: InternalRunOptions = { ...options, caveRoute };\n\n    const models = options.models ?? builtinModels();\n    const model = options.model ?? resolveModel(definition, models, options.rootDir ?? process.cwd());\n    // Runtime-gated denomination, first ground: the catalog must\n    // price the model, or a USD cap meters an honest zero and never binds. The\n    // second ground — the credential regime — is checked after routing below,\n    // because which credential pays depends on where the request goes.\n    if (budgetMeter?.denomination === \"usd\" &&\n        !modelIsPriced(model.provider, model.id)) {\n      throw new Error(\"cave_budget_denomination_unavailable\");\n    }\n    // Actual routing is the source of truth, not the route decision: the\n    // gateway only speaks the three provider dialects it proxies, so a model\n    // outside them keeps its own base URL even on a reachable gateway. Every\n    // downstream honesty question — which headers may be sent, what mode this\n    // run may claim — reads gatewayActive, never caveRoute.useGateway alone.\n    const routing = caveRoute.useGateway\n      ? routeModelThroughCave(model, gatewayURL)\n      : { model, routed: false };\n    const routedModel = routing.model;\n    const gatewayActive = routing.routed;\n    // Runtime-gated denomination, second ground: the run must actually be\n    // BILLED in dollars. A Claude Pro/Max subscription reached through Pi's\n    // credential store is not billed per token, so every dollar this ledger\n    // reported for it would be fiction.\n    //\n    // Which credential pays is what decides this. A caller-supplied streamFn\n    // owns its transport, so local login says nothing about billing. A routed","sourceCodeStart":1224,"sourceCodeEnd":1260,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/agent/src/runtime.ts#L1224-L1260","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Switch to a model the public catalog prices (verify with modelIsPriced(provider, modelId) before the run).","Regenerate/upgrade the package so catalog.ts includes the model's USD row (catalog is generated; never hand-edit prices).","Budget in tokens instead: budget: { maxTokens: n } avoids the USD pricing requirement.","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."],"exampleFix":"// before\nrunAgent(def, input, { budget: { maxUsd: 5 }, model: \"openai/gpt-5-preview-niche\" });\n// after\nif (modelIsPriced(\"openai\", \"gpt-5-preview-niche\")) {\n  runAgent(def, input, { budget: { maxUsd: 5 } });\n} else {\n  runAgent(def, input, { budget: { maxTokens: 200_000 } });\n}","handlingStrategy":"validation","validationCode":"import { modelIsPriced } from \"@caveman-ai/agent\";\nif (opts.budget?.maxUsd !== undefined && !modelIsPriced(provider, modelId)) {\n  // fall back to token denomination or pick a priced model\n}","typeGuard":"function usdBudgetIsAdmissible(budget, provider, modelId) {\n  return budget?.maxUsd === undefined || modelIsPriced(provider, modelId);\n}","tryCatchPattern":"try {\n  await runAgent(def, input, opts);\n} catch (err) {\n  if (err.message === \"cave_budget_denomination_unavailable\") {\n    await runAgent(def, input, {\n      ...opts,\n      budget: { maxTokens: 200_000 }, // token denomination needs no pricing\n    });\n  } else throw err;\n}","preventionTips":["Call modelIsPriced(provider, modelId) before attaching a maxUsd budget.","Keep the package updated so the generated catalog includes new models.","Verify the exact provider/model id strings against the catalog — typos look like unpriced models.","Prefer token budgets for custom, regional-only, or subscription-credential models."],"tags":["agent-runtime","budget","pricing","catalog"],"backgroundTag":"unsupported-operation","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}