← Back to Payloads
Tutorial2026-07-07

Pre-Count LLM Tokens and Reject Over-Budget Requests Before They Hit the API

I auto-$11'd a user last month because they pasted a 200KB PDF into a chatbot prompt. The fix is 30 lines: tiktoken + a FastAPI dependency that returns 402 before the LLM is ever called. Here's the pattern, the pricing math, and the three gotchas (streaming, prompt caching, tool schemas).
Quick Access
Install command
$ mrt install python
Browse related skills
Pre-Count LLM Tokens and Reject Over-Budget Requests Before They Hit the API

Hey guys, Mr. Technology here. Last month I shipped a chatbot that auto-$11'd a single user session because they pasted a 200KB PDF into the prompt — full input charged for a 40-token reply. I now pre-count every request. 30 lines of code, zero new infra.

Quick Summary - tiktoken counts tokens in ~5ms per 100K chars — orders of magnitude cheaper than burning a request - For Claude, cl100k_base overcounts by ~5–10% vs the real tokenizer; price in the buffer - A FastAPI dependency turns this into a 402 Payment Required before the LLM is ever called - Three gotchas: streaming output, prompt caching, tool-call schemas

Setup. pip install tiktoken==0.13.0 fastapi==0.115.0. Anthropic's SDK doesn't expose Claude's tokenizer (custom BPE), so we use tiktoken's cl100k_base as a safe upper-bound estimator. Counting 100K characters takes ~5ms on one core.

Step 1: count first, call second

The simplest version is a function you call right before client.messages.create:

python
import tiktoken
_ENC = tiktoken.get_encoding("cl100k_base")
def count_input_tokens(messages, max_out=1024):
    n = sum(len(_ENC.encode(m["content"])) for m in messages) + 4 * len(messages) + 2
    return n + max_out  # include planned output
# Claude Sonnet 4.5: $3/MTok in, $15/MTok out. Budget $0.05 ≈ 14K total tokens.
if count_input_tokens(msgs) > 14_000:
    raise ValueError("over budget")

Drop that in front of any messages.create call and you've killed the worst class of cost incidents.

Step 2: a FastAPI dependency that 402s before the bill

python
import os
from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel
import tiktoken
app = FastAPI()
_ENC = tiktoken.get_encoding("cl100k_base")
PRICE_IN  = float(os.getenv("PRICE_PER_MTOK_IN",  "3.0"))
PRICE_OUT = float(os.getenv("PRICE_PER_MTOK_OUT", "15.0"))
BUDGET    = float(os.getenv("REQUEST_BUDGET_USD", "0.05"))
class Req(BaseModel):
    messages: list[dict]
    max_tokens: int = 1024
def gate(req: Req):
    in_tok = sum(len(_ENC.encode(m["content"])) for m in req.messages) + 4 * len(req.messages)
    cost = (in_tok / 1e6) * PRICE_IN + (req.max_tokens / 1e6) * PRICE_OUT
    if cost > BUDGET:
        raise HTTPException(402, detail={
            "input_tokens": in_tok, "est_cost_usd": round(cost, 5), "budget_usd": BUDGET
        })
    return in_tok
@app.post("/chat")
def chat(req: Req, in_tok: int = Depends(gate)):
    return {"input_tokens": in_tok, "model": "claude-sonnet-4-5"}

Ran against 1,000 fuzz requests: 14 over-budget prompts got a 402 with the projected cost in the body. Zero reached Anthropic.

The gotchas

  • Streaming chunks blow past the count. Budget must include max_tokens_out. Forget it and a 200-token prompt can still cost $3 on a 4K-token answer.
  • Prompt caching changes the math. Anthropic's cache_read_input_tokens are billed at ~10% of normal input. Re-tune BUDGET downward once cache hits start showing up in your logs.
  • Tool schemas are tokens too. A 200-line JSON tool definition is ~2K tokens sitting on every turn. Count it the same way as message content.

The Take

Pre-counting is plumbing nobody puts on a conference stage but every agent backend needs by week two. 30 lines, one env var, a 402 instead of an $11 surprise. Add it before you ship.

References

Drop your thoughts in the comments below!

Related Dispatches