Budget Controls & Cost Optimization — Harness Tutorials

# Budget Controls & Cost Optimization

Set hard token and dollar limits that automatically switch models or stop execution before you get a surprise bill.

Intermediate 15 min read

## 1The Cost Problem

AI coding agents can be expensive. A Cursor Pro subscription costs $240/year, but power users report spending $7,000+ annually with usage-based pricing. One poorly scoped prompt asking an agent to "refactor the entire codebase" can burn through $50 in tokens in minutes.

⚠ Cost Risk
Without budget controls, a single `harness --permission bypass 'Refactor entire codebase'` could cost hundreds of dollars. The model has no way to stop itself once it starts.

Harness solves this with a three-layer approach: per-session token limits, per-session cost limits, and automatic model fallback when limits are reached.

⚡ Harness Advantage
Token and cost budgets with automatic fallback are unique to Harness. Claude Code, Cursor, and Copilot have no programmatic budget controls — you only find out what you spent after the invoice arrives.

## 2Model Cost Catalog

Different models have drastically different price points. Use the table below to understand what a typical 100K-token task costs across providers. The 100K task cost assumes roughly 70K input tokens and 30K output tokens.

Provider | Model | Input $/1M | Output $/1M | 100K Task Cost
--- | --- | --- | --- | ---
Anthropic | claude-opus-4 | $15.00 | $75.00 | ~$9.00
Anthropic | claude-sonnet-4 | $3.00 | $15.00 | ~$1.80
OpenAI | gpt-4o | $2.50 | $10.00 | ~$1.25
OpenAI | o3-mini | $1.10 | $4.40 | ~$0.55
Google | gemini-2.5-pro | $1.25 | $10.00 | ~$1.13
Ollama | any local model | Free | Free | $0.00

ℹ Pricing Note
Prices shown are approximate list prices as of early 2025. Check each provider's pricing page for current rates. Harness cost tracking uses the actual token counts reported by each API.

## 3TOML Budget Configuration

The simplest way to set budgets is in your `.harness/config.toml`. These limits are enforced automatically on every `harness` invocation without any code changes.

TOML .harness/config.toml

    [router]
    strategy = "cost_optimized"
    fallback_chain = ["anthropic", "openai", "google"]
    max_cost_per_session = 1.00     # Hard limit: $1 per session
    max_tokens_per_session = 500000  # Hard limit: 500K tokens
    simple_task_model = "claude-haiku-4-5-20251001"  # Use cheap model for simple tasks

When `max_cost_per_session` or `max_tokens_per_session` is reached, Harness raises a `BudgetExhaustedError` and stops the agent loop cleanly before issuing another API call.

The TOML `[router]` section maps directly to `RouterConfigData`:

Python harness/config.py

    @dataclass
    class RouterConfigData:
        strategy: str = "manual"
        fallback_chain: tuple[str, ...] = ()
        max_cost_per_session: float = 0.0   # 0.0 means unlimited
        max_tokens_per_session: int = 0     # 0 means unlimited
        simple_task_model: str | None = None

A value of `0` (default) means unlimited — Harness will not enforce a budget unless you explicitly set a positive value.

## 4TokenBudgetTracker in the SDK

For programmatic control, use `TokenBudgetTracker` directly. It tracks cumulative usage across multiple API calls and raises an error the moment a limit is exceeded.

### Class Reference

Python harness/providers/budget.py

    from harness.providers.budget import (
        BudgetSnapshot,
        BudgetExhaustedError,
        TokenBudgetTracker,
    )

    # BudgetSnapshot — immutable point-in-time view
    @dataclass
    class BudgetSnapshot:
        input_tokens_used: int = 0
        output_tokens_used: int = 0
        total_tokens_used: int = 0
        cost_used: float = 0.0
        tokens_remaining: int = 0   # 0 when no limit is configured
        cost_remaining: float = 0.0  # 0.0 when no limit is configured

    # TokenBudgetTracker — stateful accumulator
    class TokenBudgetTracker:
        def __init__(self, *, max_tokens: int = 0, max_cost: float = 0.0): ...
        @property
        def total_tokens(self) -> int: ...       # Tokens used so far
        @property
        def total_cost(self) -> float: ...       # Cost accrued so far
        def record_usage(
            self, input_tokens=0, output_tokens=0, cost=0.0
        ) -> BudgetSnapshot: ...                 # Returns snapshot after recording
        def snapshot(self) -> BudgetSnapshot: ... # Current state without modifying
        def is_exhausted(self) -> bool: ...      # True if any limit exceeded
        def check_budget(self) -> None: ...      # Raises BudgetExhaustedError if exhausted
        def reset(self) -> None: ...             # Reset counters (new session)

### Full Example

Python cost_runner.py

    import asyncio
    import harness
    from harness.providers.budget import TokenBudgetTracker, BudgetExhaustedError

    async def main():
        # Create a budget: max $0.50 or 100K tokens (whichever hits first)
        budget = TokenBudgetTracker(max_tokens=100_000, max_cost=0.50)

        try:
            async for msg in harness.run(
                "Analyze and document this codebase",
                provider="anthropic",
                model="claude-sonnet-4-20250514",
            ):
                if isinstance(msg, harness.Result):
                    # Record usage from each Result message
                    budget.record_usage(
                        # Note: total_tokens doesn't split input/output.
                        # This 50/50 approximation is for budget tracking only.
                        input_tokens=msg.total_tokens // 2,
                        output_tokens=msg.total_tokens // 2,
                        cost=msg.total_cost,
                    )

                    snap = budget.snapshot()
                    print(f"Used: ${snap.cost_used:.4f} / ${snap.cost_remaining:.4f} remaining")
                    print(f"Tokens: {snap.total_tokens_used:,} / {snap.tokens_remaining:,} remaining")

                    # Manually check before issuing the next turn
                    budget.check_budget()

        except BudgetExhaustedError as e:
            print(f"Budget exceeded! {e}")
            print(f"Final: ${e.snapshot.cost_used:.4f} spent, "
                  f"{e.snapshot.total_tokens_used:,} tokens used")

    asyncio.run(main())

ℹ Result Message
`harness.Result` is yielded at the end of each agent turn. It contains `total_tokens` and `total_cost` attributes that reflect the entire turn's usage.

## 5Handling BudgetExhaustedError

`BudgetExhaustedError` carries a `snapshot` attribute so you can inspect why the budget was exhausted and take different recovery actions.

Python

    from harness.providers.budget import BudgetExhaustedError

    try:
        budget.check_budget()
    except BudgetExhaustedError as e:
        snap = e.snapshot

        if snap.cost_remaining <= 0:
            print(f"Cost limit hit! Spent ${snap.cost_used:.4f}")
            print("Switching to free local model via Ollama...")
            # Re-run with Ollama provider (free)
            async for msg in harness.run(task, provider="ollama", model="llama3.2"):
                ...

        elif snap.tokens_remaining <= 0:
            print(f"Token limit hit! Used {snap.total_tokens_used:,} tokens")
            print("Compacting context and retrying with summary...")
            # Summarize results so far and continue with compact context
            summary = summarize_progress(results_so_far)
            budget.reset()
            async for msg in harness.run(
                f"Continue from: {summary}",
                provider="anthropic",
                model="claude-haiku-4-5-20251001",  # Cheaper model for remainder
            ):
                ...

✓ Best Practice
Always inspect `snap.cost_remaining` vs `snap.tokens_remaining` to choose the right recovery strategy. A cost limit often means switching to a free model; a token limit often means compacting context.

## 6Model Router Strategies

`ModelRouter` wraps a primary provider and automatically selects the right model based on your chosen strategy. This lets you save money on simple tasks without sacrificing quality for complex ones.

You explicitly choose the model for every run. Useful when you need deterministic behavior.

Python

    from harness.providers.router import ModelRouter, RoutingStrategy

    router = ModelRouter(
        primary=anthropic_provider,
        strategy=RoutingStrategy.MANUAL,
        # No automatic switching — you control the model
    )

Harness analyzes each task and routes simple requests (short prompts, single-file edits) to the cheaper `simple_task_provider`.

Python

    from harness.providers.router import ModelRouter, RoutingStrategy
    from harness.providers.budget import TokenBudgetTracker

    budget = TokenBudgetTracker(max_cost=5.00)

    router = ModelRouter(
        primary=anthropic_provider,           # Sonnet 4 for complex tasks
        strategy=RoutingStrategy.COST_OPTIMIZED,
        simple_task_provider=haiku_provider,  # Haiku for simple tasks
        budget=budget,
    )

Always uses the highest-quality available model, regardless of cost, until the budget is exhausted — then falls back.

Python

    from harness.providers.router import ModelRouter, RoutingStrategy

    router = ModelRouter(
        primary=opus_provider,   # Always try Opus first
        strategy=RoutingStrategy.QUALITY_FIRST,
        budget=budget,           # Falls back when budget is hit
    )

Selects the fastest model (typically a smaller, cheaper model) for each task to minimize response time in latency-sensitive workflows.

Python

    from harness.providers.router import ModelRouter, RoutingStrategy

    router = ModelRouter(
        primary=haiku_provider,  # Fast model as primary
        strategy=RoutingStrategy.LATENCY_FIRST,
        # Optimizes for time-to-first-token
    )

### RoutingStrategy Enum

Python harness/providers/router.py

    from enum import Enum

    class RoutingStrategy(Enum):
        MANUAL         = "manual"          # You choose the model explicitly
        COST_OPTIMIZED = "cost_optimized"  # Routes simple tasks to cheaper models
        QUALITY_FIRST  = "quality_first"   # Always uses best model, budget permitting
        LATENCY_FIRST  = "latency_first"   # Uses fastest model for each task

    class ModelRouter:
        def __init__(
            self,
            primary,                         # Primary ProviderAdapter
            *,
            strategy=RoutingStrategy.MANUAL,
            simple_task_provider=None,       # Cheap provider for simple tasks
            budget=None,                     # TokenBudgetTracker instance
        ): ...

## 7Automatic Fallback

`FallbackProvider` wraps a list of providers and tries each in order if the previous one fails (network error, rate limit, service outage). Your agent keeps running even if a provider goes down.

Python fallback_example.py

    from harness.providers.fallback import FallbackProvider

    # Try Anthropic first, then OpenAI, then Google
    fallback = FallbackProvider([
        anthropic_provider,
        openai_provider,
        google_provider,
    ])

    # active_provider reflects whichever is currently serving requests
    print(f"Active: {fallback.active_provider}")

    # If Anthropic is down, automatically tries OpenAI
    # If OpenAI is down, automatically tries Google
    async for msg in harness.run(
        "Review this PR",
        # Advanced: pass a pre-configured provider instance
        # (internal API — prefer TOML fallback_chain config instead)
        _provider=fallback,
    ):
        print(msg)

### TOML Equivalent

The same fallback chain can be configured in TOML — no code required:

TOML .harness/config.toml

    [router]
    fallback_chain = ["anthropic", "openai", "google"]
    # Harness will try providers in this order on failure

⚡ No Other Agent Has This
No other coding agent has automatic provider failover. When Anthropic has an outage, Claude Code stops working entirely — there is no fallback. Cursor is locked to a single provider. Harness keeps running by switching to the next healthy provider in your chain automatically.

`FallbackProvider` catches `ConnectionError`, `OSError`, and `TimeoutError` — if the current provider fails with any of these, it automatically tries the next provider in the chain. It does not catch `BudgetExhaustedError` — budget limits always propagate to the caller.

Python harness/providers/fallback.py

    class FallbackProvider:
        def __init__(self, providers: list[ProviderAdapter]): ...

        @property
        def active_provider(self) -> ProviderAdapter:
            """Returns the currently active (healthy) provider."""
            ...

## 8Next Steps

You now have full control over what your agents spend. Here is a recommended workflow:

1. Set `max_cost_per_session = 1.00` in `.harness/config.toml` as a safety net
2. Use `strategy = "cost_optimized"` with a cheap `simple_task_model` to reduce spend by 60-80%
3. Add a `fallback_chain` so your workflows survive provider outages
4. In scripts, wrap `harness.run()` with a `TokenBudgetTracker` and handle `BudgetExhaustedError`

▶ Try It Now
Add this to your `.harness/config.toml` and run any harness command — you will see a cost summary printed at the end of each session:
TOML

    [router]
    max_cost_per_session = 0.50
    max_tokens_per_session = 100000

Next, learn how to govern what your agent can do (not just how much it costs) with Policy-as-Code.