Skip to content

CircuitBreaker.wrapProvider drops .model → Loop budget cost accounting silently disabled #3

@hamr0

Description

@hamr0

Summary

Composing two bare-agent primitives — CircuitBreaker.wrapProvider + Loop — silently disables LLM cost accounting, so a bareguard budget.maxCostUsd cap accrues zero token cost and never halts.

Root cause

  • CircuitBreaker.wrapProvider(provider, key) returns only { generate } (src/circuit-breaker.js:105–107) — it does not carry over provider.model.
  • Loop derives cost from const model = this.provider.model || null (src/loop.js:181) and estimateCost(model, usage) returns null when !model (src/loop.js:29).
  • Net: when the Loop's provider is a wrapped provider (the documented resilience pattern), model is null, roundCost is null, onLlmResult reports costUsd: null, and budget.maxCostUsd never accrues. The cost cap is a no-op on the wrapped path.

Repro

const breaker = new CircuitBreaker({ threshold: 5 });
const wrapped = breaker.wrapProvider(anthropic, 'anthropic'); // anthropic.model set
const loop = new Loop({ provider: wrapped, onLlmResult });
// onLlmResult receives costUsd: null for every round → budget never accrues

Impact

Any consumer that wraps its provider in CircuitBreaker (recommended for resilience) loses budget.maxCostUsd enforcement on the agent loop. Found while validating multis (first baresuite customer) — the per-run cost cap was silently unenforced.

Suggested fix (either)

  1. wrapProvider preserves passthrough props: return { ...provider, generate } (or at least copy model).
  2. Loop falls back to the generate result's model: const model = result.model || this.provider.model || null.

Option 2 is more robust (model can vary per response, e.g. Fallback provider).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions