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
- Instructions
- Tools
- History
- Prompt
- Headroom
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
-
Install
$ composer require phoenix1331/laravel-ai-headroom $ php artisan vendor:publish --tag=ai-headroom-config
-
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
-
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
TokenLimitExceededcarrying the full count, so you can trim history, summarise or refuse. Prefer a warning? Use(new EnsureHeadroom(...))->report()and listen forHeadroomLow.
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.
| Event | When |
|---|---|
TokensCounted | Before a request is sent, with the breakdown, window and estimated cost |
TokensUsed | After the response, including streams, with actual tokens and cost |
HeadroomLow | A limit was passed in report mode |
RequestBlocked | Just before TokenLimitExceeded is thrown |
RequestFailed | The 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.
| Provider | Native counting | Local estimate |
|---|---|---|
| OpenAI | /v1/responses/input_tokens | Same tokeniser (o200k), small message overhead drift |
| Anthropic | /v1/messages/count_tokens | Approximation plus a safety margin, then self-calibrated |
| Gemini | models/{model}:countTokens | Approximation plus a safety margin, then self-calibrated |
| Other laravel/ai providers | Register your own counter | Approximation 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.
- EstimateCount locally, with a safety margin
- SendThe provider reports the real input tokens
- LearnUpdate the model's correction factor and measured error
- 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.
