JuliusBrussee/caveman · error
cave_budget_denomination_ambiguous
cave_budget_denomination_ambiguous
Error message
cave_budget_denomination_ambiguous
What it means
normalizeRunBudget requires a budget to specify exactly one denomination: exactly one of maxUsd or maxTokens must be defined. If both are defined, or neither is, the budget is self-contradictory or vacuous, so it throws cave_budget_denomination_ambiguous before any provider call rather than silently running with no cap.
Solutions
- Set exactly one of maxUsd or maxTokens; delete the other field.
- If you must express both, convert one to the other (e.g. estimate tokens from USD) before building the budget.
- Deep-check merged config objects for leftover keys before normalizing.
Example fix
// before
budget({ maxUsd: 5, maxTokens: 10000 })
// after
budget({ maxUsd: 5 }) Defensive patterns
Strategy: validation
Validate before calling
function validBudgetDenomination(b) {
const usd = b.maxUsd !== undefined, tokens = b.maxTokens !== undefined;
return usd !== tokens; // exactly one must be set
}
if (!validBudgetDenomination(budget)) throw new Error('set exactly one of maxUsd or maxTokens'); Try / catch
try { normalizeRunBudget(b); } catch (e) {
if (e.message === 'cave_budget_denomination_ambiguous') { b = pickSingleDenomination(b); }
else throw e;
} Prevention
- Express the run cap in exactly one denomination; strip the other key when merging configs.
- Centralize budget construction in one helper instead of ad-hoc object merges.
- When migrating between USD and token budgets, delete stale keys.
When it happens
Trigger: Passing { maxUsd: 5, maxTokens: 10000 } (both set) or {} / { initialUsd: 5 } (neither max set) to normalizeRunBudget / budgetMeter.
Common situations: Merging two config objects where one sets maxUsd and the other maxTokens; a config file carrying stale maxTokens after maxUsd was added; constructing the budget programmatically with optional fields that all end up set or unset.
Related errors
- cave_breaker_retry_requires_budget
- cave_budget_initial_invalid
- cave_budget_max_invalid
- caveman build: maxSearchCostUsd must be positive
- ccr max storage bytes must be positive
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/b15226b1c10db315.
Report an issue: GitHub.
Appendix: source
Thrown at packages/agent/src/budget.ts:93
export interface NormalizedBudget {
readonly denomination: BudgetDenomination;
readonly max: number;
readonly initial: number;
readonly outputFloorTokens: number;
readonly onExhausted: "compact" | "stop";
readonly compaction: NormalizedCompaction;
}
/**
* Validate a caller-supplied budget. Fails closed: an ambiguous, unbounded, or
* self-contradicting budget is rejected before the first provider call rather
* than silently degrading into no cap at all.
*/
export function normalizeRunBudget(budget: RunBudget): NormalizedBudget {
const usd = budget.maxUsd !== undefined;
const tokens = budget.maxTokens !== undefined;
if (usd === tokens) throw new Error("cave_budget_denomination_ambiguous");
const denomination: BudgetDenomination = usd ? "usd" : "tokens";
const max = usd ? budget.maxUsd! : budget.maxTokens!;
if (!Number.isFinite(max) || max <= 0) throw new Error("cave_budget_max_invalid");
if (denomination === "tokens" && !Number.isSafeInteger(max)) {
throw new Error("cave_budget_max_invalid");
}
const wrongInitial = denomination === "usd" ? budget.initialTokens : budget.initialUsd;
if (wrongInitial !== undefined) throw new Error("cave_budget_denomination_ambiguous");
const declaredInitial = denomination === "usd" ? budget.initialUsd : budget.initialTokens;
const initial = declaredInitial ?? max;
if (!Number.isFinite(initial) || initial <= 0 || initial > max) {
throw new Error("cave_budget_initial_invalid");
}
if (denomination === "tokens" && !Number.isSafeInteger(initial)) {
throw new Error("cave_budget_initial_invalid");
}
const outputFloorTokens = budget.outputFloorTokens ?? OUTPUT_CLAMP_FLOOR_TOKENS;
if (!Number.isSafeInteger(outputFloorTokens) || outputFloorTokens <= 0) {View on GitHub (pinned to 3ee70a1026)