Headroom

Know how much room your AI request has left, before you send it.

Headroom counts the whole request a Laravel AI SDK agent is about to send (instructions, history, tools, attachments and prompt), checks it against the model's context window and your cost limits, and can stop it before it goes.

composer require phoenix1331/laravel-ai-headroom
A support agent on a 200,000-token model 0 tokens, 0% of the window
  • Instructions
  • Tools
  • History
  • Prompt
  • Headroom
70

An illustration: roughly 1,450 tokens per turn of history, 1,800 of instructions and 3,600 of tool schemas. Headroom measures your real requests.

The problem it solves

A chat feature works in testing. Six weeks later a loyal customer's conversation has 90 turns of history, the retrieval step adds twelve document chunks, and the request quietly goes past the context window. The provider rejects it, or the SDK trims the oldest history and the agent forgets what was agreed.

The same growth shows up on the bill. In a multi-tenant app, one tenant's long threads can cost more per request than everyone else's put together, and you only find out at the end of the month.

Counting the prompt string is not enough: tool schemas, the system prompt and the history are all sent too, and often make up most of the request. Headroom counts what the SDK will actually send.

See what is in a request

Point headroom:inspect at an agent and a prompt to see where the tokens go. It exits with a failure when the request does not fit, so it can run in CI.

$ php artisan headroom:inspect "App\Ai\Agents\SupportAgent" "Where is my refund?"

  Agent ................................ App\Ai\Agents\SupportAgent
  Model ................................ claude-sonnet-5-5 (anthropic)
  Method ............................... Native (expected error 0%)

+------------------+---------+--------+
| Part             | Tokens  | Share  |
+------------------+---------+--------+
| Instructions     | 1,812   | 1.06%  |
| History          | 162,240 | 94.71% |
| Tools            | 3,604   | 2.1%   |
| Attachments      | 0       | 0%     |
| Prompt           | 14      | 0.01%  |
| Message overhead | 3,627   | 2.12%  |
| Total            | 171,297 | 100%   |
+------------------+---------+--------+
  Context .............................. 171,297 of 1,000,000 tokens (17.13%), 828,703 remaining
  Estimated cost ....................... $0.3426 input + up to $1.2800 output = up to $1.6226 USD

Quick start

  1. Install

    $ composer require phoenix1331/laravel-ai-headroom
    $ php artisan vendor:publish --tag=ai-headroom-config
  2. Count an agent request

    use Phoenix1331\AiHeadroom\Facades\Headroom;
    
    $count = Headroom::for(new SupportAgent($user), 'Where is my refund?');
    
    $count->total();         // 171297
    $count->breakdown();     // ['instructions' => 1812, 'history' => 162240, ...]
    $count->percentUsed();   // 17.13
    $count->fits();          // true
    $count->estimatedCost(); // Cost: input plus worst-case output
  3. Guard the agent

    use Phoenix1331\AiHeadroom\Middleware\EnsureHeadroom;
    
    public function middleware(): array
    {
        return [new EnsureHeadroom(maxPercentOfContext: 80, maxCost: 0.20)];
    }

    Every step the agent takes is counted before it is sent. Over a limit, Headroom throws TokenLimitExceeded carrying the full count, so you can trim history, summarise or refuse. Prefer a warning? Use (new EnsureHeadroom(...))->report() and listen for HeadroomLow.

Events, not storage

Headroom stores nothing about your requests. It fires events you can log, chart or ship to a dashboard, and they never contain prompt or response text.

EventWhen
TokensCountedBefore a request is sent, with the breakdown, window and estimated cost
TokensUsedAfter the response, including streams, with actual tokens and cost
HeadroomLowA limit was passed in report mode
RequestBlockedJust before TokenLimitExceeded is thrown
RequestFailedThe provider call failed (class and HTTP status only)

A request ID links the count to the usage. Tag requests with your own context, such as tenant or user, using Headroom::resolveContextUsing(). Every property is documented in the events reference.

Exact where it can be, honest where it cannot

Anthropic, OpenAI and Gemini all offer an endpoint that counts a request exactly. Calling it costs a network round trip, so in the default auto mode Headroom estimates locally first and only asks the provider when the estimate is close to a limit. Clear passes and clear failures never wait on the network.

ProviderNative countingLocal estimate
OpenAI/v1/responses/input_tokensSame tokeniser (o200k), small message overhead drift
Anthropic/v1/messages/count_tokensApproximation plus a safety margin, then self-calibrated
Geminimodels/{model}:countTokensApproximation plus a safety margin, then self-calibrated
Other laravel/ai providersRegister your own counterApproximation plus a safety margin, then self-calibrated

Measured accuracy is coming. Headroom ships an accuracy suite that compares its estimates with each provider's own counts across prose, code, JSON, French, Japanese and a tool-heavy agent. The first results will be published in docs/accuracy.md before the first release, rather than quoting numbers that have not been measured.

Prefer nothing to leave your app? Set HEADROOM_METHOD=estimate and Headroom never sends prompt content to a counting endpoint.

Estimates that improve with use

Every response reports the input tokens it actually used. Headroom compares that with what it estimated and learns a correction for each model, from your own traffic.

  1. EstimateCount locally, with a safety margin
  2. SendThe provider reports the real input tokens
  3. LearnUpdate the model's correction factor and measured error
  4. CorrectAfter 20 samples, estimates use the factor and a margin that shrinks as error falls

Each observation is clamped, so one unusual request cannot skew the factor, and the weighting adapts if a provider changes its tokeniser. Corrected counts are reported as Calibrated with their measured error, so you always know what you are looking at. Check progress with php artisan headroom:calibration.

Context windows and prices

Headroom ships the context windows, output limits and prices of current Anthropic, OpenAI and Gemini models, including long-prompt price tiers and cache pricing, and updates them with each release. Costs can be shown in US dollars or pounds sterling.

// config/ai-headroom.php
'models' => [
    'claude-sonnet-5-5' => ['input_price' => 1.80],   // your negotiated rate
    'my-fine-tune' => [
        'provider' => 'openai',
        'context_window' => 128_000,
        'max_output_tokens' => 16_000,
        'input_price' => 3.00,
        'output_price' => 12.00,
    ],
],

php artisan headroom:models lists every model, marks which prices are yours, and warns when the shipped prices were last checked over 60 days ago. Spotted an outdated price? Report a model update; no code needed.

Test your guards

Headroom::fake([SupportAgent::class => 900_000]);

expect(fn () => (new SupportAgent($user))->prompt('Hello'))
    ->toThrow(TokenLimitExceeded::class);

Headroom::assertCounted(SupportAgent::class);

The fake replaces only the counting, so your middleware, limits and event listeners run for real, and nothing reaches a provider.