LIVE
The Download: the Kids issue arrives, and Bill Gates reveals his AI fears26/08/26|Z.ai confirms Ox Alpha is a new GLM-series model and will release its weights26/08/26|How loveholidays is making everyone a builder with Codex26/08/26 · OpenAI|Training and Finetuning Multi-Vector Embedding Models with Sentence Transformers26/08/26 · Hugging Face|Recursive Experiential-Working Memory Evolution for Long-Horizon Agent Harnesses25/08/26 · OpenAI|Granite 4.2 LLMs: How They're Built25/08/26 · Hugging Face|OpenAI Jalapeño: Better than Nvidia Blackwell25/08/26 · OpenAI|Anthropic tells staff to work from home due to possible security team strike25/08/26 · Anthropic|OpenAI restores 5-hour Codex and Work limits for ChatGPT Plus users25/08/26 · OpenAI|Quantization-Aware Healing: a compressed, 4-bit model that outperforms its full-precision original25/08/26 · Hugging Face|The full stack behind abundant intelligence25/08/26 · OpenAI|Jalapeño’s first results show industry-leading speed and efficiency in AI inference25/08/26 · OpenAI|The Download: the Kids issue arrives, and Bill Gates reveals his AI fears26/08/26|Z.ai confirms Ox Alpha is a new GLM-series model and will release its weights26/08/26|How loveholidays is making everyone a builder with Codex26/08/26 · OpenAI|Training and Finetuning Multi-Vector Embedding Models with Sentence Transformers26/08/26 · Hugging Face|Recursive Experiential-Working Memory Evolution for Long-Horizon Agent Harnesses25/08/26 · OpenAI|Granite 4.2 LLMs: How They're Built25/08/26 · Hugging Face|OpenAI Jalapeño: Better than Nvidia Blackwell25/08/26 · OpenAI|Anthropic tells staff to work from home due to possible security team strike25/08/26 · Anthropic|OpenAI restores 5-hour Codex and Work limits for ChatGPT Plus users25/08/26 · OpenAI|Quantization-Aware Healing: a compressed, 4-bit model that outperforms its full-precision original25/08/26 · Hugging Face|The full stack behind abundant intelligence25/08/26 · OpenAI|Jalapeño’s first results show industry-leading speed and efficiency in AI inference25/08/26 · OpenAI|
IntermediateNew💸

LLM cost optimization: the 10 levers, from mental model to code

Your AI bill is exploding because you pay for tokens nobody reads. This guide gives you the mental model, the 10 levers grouped by family, a 30-60-90 day battle plan for the CIO, and the Python code for each lever.

32 min readPublished August 26, 2026 · today

Your first AI pilot cost €40 a month. The second, €400. The third — the one with agents — hit €4,000 in three weeks, and nobody can say which workflow burned what. This isn't a provider problem or a model problem: it's a problem of tokens sent that nobody reads, calls you could have skipped, and a bill nobody attributes.

This guide covers the whole territory. The first half is for the CIO: the mental model, the levers grouped by family, the impact/effort matrix, and the order in which to activate them. The second half is for your technical team: each lever with its Python code, the pitfalls, and a worked example on a real pipeline. You can read either half on its own.

Prerequisites. At least one LLM use case in production or pilot, with access to the provider invoice. For the second half: Python 3.11+, a provider SDK (examples use the Anthropic SDK; the principles are identical with OpenAI, Mistral or Google). No GPU knowledge needed except for the self-hosting section.

1. The mental model: where the bill comes from

Before touching any lever, you need to understand what you're paying for. An LLM call bills three things, and most teams only see one.

Anatomy of an LLM bill
Call cost = Input × price_in + Output × price_out then × number of turns for an agent System + tools (stable, repeated on every call) History Question INPUT · price_in · 40 to 60% often ignored by the model Output OUTPUT · price_out = 4 to 5 × price_in · verbosity is money × turns (looping agent, invalid-JSON retries, evaluator-optimizer) MULTIPLIER · this is what turns €40 into €4,000
The price on the provider's page is the price of a single call. In production, the real bill is multiplied by the number of turns (agents, retries, loops) and dominated by the context repeated on every call.

Three observations follow, and they structure the rest of this guide.

Input is dominated by what doesn't change. Your system prompt, your tool definitions, your few-shot examples, your injected documentation: all of it is resent verbatim on every call. On an agent doing 30 turns, you pay 30 times for the same prefix. That's the first reservoir, and it's the one prompt caching drains.

Output costs 4 to 5 times more than input. A model that answers "Of course! Here is the information you requested, presented in a clear and structured way:" before giving you the JSON you expected just billed you 25 tokens of politeness at the most expensive rate. Over a million calls, that's a salary.

The multiplier is invisible on the price sheet. The provider shows a price per million tokens. It doesn't show that your agent will redo the call 12 times because the JSON was invalid on the first turn, or that your evaluator-optimizer pattern mechanically doubles every request. This multiplier is where pilots go off the rails.

🧮
The multiplication rule
Cost levers are multiplicative, not additive. Prompt caching that halves input, routing that divides the average price by 3, and compaction that removes 30% of context give 0.5 × 0.33 × 0.7 ≈ 0.12 — that's -88%. But it also means the most profitable lever is always the one that applies to the largest share of the bill, and you don't know which that is until you've measured.

2. The 10 levers, grouped by family

The infographic circulating on LinkedIn lists twelve techniques flat. That's an inventory, not a strategy. Here's the same material organized by what it does to the formula above.

Four families, ten levers
Pay less per token acts on price_in / price_out 1 · Prompt caching -45 to -80% on repeated input 2 · Batch API -50% when latency doesn't matter 3 · Complexity-aware routing -60 to -90% on simple tasks 4 · Quantization / distillation self-hosted: memory ÷ 2 to 4 5 · Speculative decoding self-hosted: throughput × 1.5 to 3 Send fewer tokens acts on Input / Output 6 · Context compaction -40 to -60% useless tokens 7 · Structured output less output + fewer retries 8 · Sharper retrieval chunks, top-k, reranking Don't call at all acts on number of calls 9 · Semantic cache realistic hit rate 20 to 40%, 60%+ on repetitive support Attribute cross-cutting · prerequisite 10 · Cost observability per chain, per agent, per user segment Without it, everything else is a bet
Each lever acts on a specific term of the formula. The 'Attribute' family saves nothing directly, but without it you're optimizing blind.

You'll notice two levers from the original infographic have been merged (quantization and distillation, which answer the same question: "do I need a model this big?") and that speculative decoding is pushed to the end of the list. Not because it's ineffective — because it only concerns organizations hosting their own GPUs, a minority of this guide's readers. We come back to it in section 5.

What each family costs in effort

The question a CIO must ask isn't "which of these levers works?" (they all do) but "which one can I activate this week with the people I have?".

Impact / effort matrix
Implementation effort → Impact on the bill → DO THIS WEEK PLAN THIS QUARTER SECONDARY QUICK WINS ONLY IF SELF-HOSTED Prompt caching Structured output Observability (prerequisite) Batch API Complexity-aware routing Context compaction Semantic cache Sharper retrieval Quantization / distillation Speculative decoding
The levers in the top-left are the ones to activate first: big gain, minor code change. Those in the bottom-right require infrastructure or a pipeline redesign.
The semantic cache trap. It's sold as an easy lever because "it's just a cache". That's wrong: a classic cache compares exact keys; a semantic cache compares embeddings against a threshold, and that threshold decides between serving a stale answer to a slightly different question or never serving anything. On a support chatbot with 200 recurring questions, hit rate can reach 60%. On an internal business assistant where every question is contextual, it drops to 5% and you're paying for embeddings for nothing. Measure first.

3. The battle plan: 30, 60, 90 days

Here's the sequence I recommend for a CIO discovering an LLM bill in drift. It assumes you have no dedicated team and that each lever must prove its gain before the next one.

30-60-90 day roadmap
Day 30 · See -30 to -50% expected • Observability per chain • Prompt caching enabled • Structured output everywhere • Hard spend cap per agent Effort: 3 to 5 dev days Day 60 · Sort another -20 to -40% • Routing via gateway • Batch API on non-urgent • Token budget per stage • History trimming Effort: 1 to 2 dev weeks Day 90 · Structure -10 to -30% depending on case • Semantic cache (if FAQ-like) • Retrieval reranking • Small model on narrow tasks • Self-hosting study Effort: a project in itself Typical cumulative gain: -70 to -85% at equal quality
Each stage funds the next: the gains from the first 30 days justify the engineering time of the following 60.

Day 30 — See. You instrument every call (which workflow, which agent, which internal customer, how many tokens, how many euros), enable prompt caching on everything with a stable prefix, force structured output, and set a hard spend cap per agent. That last item isn't a cost lever: it's insurance. An agent looping without a cap can spend in one night what you hoped to save in a quarter.

Day 60 — Sort. With 30 days of data, you know which workflows are expensive. You put in a gateway (LiteLLM, Portkey, OpenRouter, or a simple in-house Python module) that routes each request to the cheapest model meeting the quality bar. You move everything that isn't real-time (enrichment, overnight classification, reports) to the Batch API. You set a token budget per pipeline stage and trim history.

Day 90 — Structure. This is where you decide whether a semantic cache makes sense (only if your traffic is repetitive), whether your RAG deserves a reranker, whether a narrow high-volume task justifies a dedicated small model, and whether your volume is such that self-hosting becomes profitable. That last question has a numerical answer: below a few tens of millions of tokens per day, almost never.

🎯
The only KPI that matters
Not "total cost" but cost per unit of value: per resolved ticket, per processed document, per qualified lead. A pipeline that goes from €400 to €800 a month while processing 4 times more documents is a success. That ratio is what your observability must produce, and it's what you present to leadership.

4. The levers in code

What follows is for the team that will implement. Each lever uses the same format: what it does, the minimal code, the pitfall that makes implementations fail. Examples use the Anthropic SDK; the concepts transfer without surprises.

4.1 Observability: instrument before you optimize

The principle: every LLM call carries metadata (workflow, agent, user or segment) and its computed cost. You send these to Langfuse, Helicone, or plain OpenTelemetry into your existing stack. The simplest way to start is a homemade decorator.

import time, functools
from anthropic import Anthropic

client = Anthropic()

# Price per million tokens — move to config, verify regularly
PRICES = {
    "claude-haiku-4-5": {"in": 1.00, "out": 5.00, "cache_read": 0.10, "cache_write": 1.25},
    "claude-sonnet-4-5": {"in": 3.00, "out": 15.00, "cache_read": 0.30, "cache_write": 3.75},
}

def cost_eur(model: str, usage) -> float:
    p = PRICES[model]
    cached = getattr(usage, "cache_read_input_tokens", 0) or 0
    written = getattr(usage, "cache_creation_input_tokens", 0) or 0
    fresh = usage.input_tokens - cached - written
    usd = (fresh * p["in"] + cached * p["cache_read"] + written * p["cache_write"]
           + usage.output_tokens * p["out"]) / 1_000_000
    return usd * 0.92  # exchange rate belongs in config

def traced(workflow: str, agent: str):
    def deco(fn):
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            t0 = time.perf_counter()
            resp = fn(*args, **kwargs)
            record = {
                "workflow": workflow, "agent": agent,
                "model": resp.model, "latency_ms": round((time.perf_counter() - t0) * 1000),
                "in": resp.usage.input_tokens, "out": resp.usage.output_tokens,
                "cache_read": getattr(resp.usage, "cache_read_input_tokens", 0),
                "cost_eur": cost_eur(resp.model, resp.usage),
                "segment": kwargs.get("segment", "unknown"),
            }
            emit(record)  # → Langfuse / OTel / your PostgreSQL table
            return resp
        return wrapper
    return deco

@traced(workflow="support-triage", agent="classifier")
def classify(ticket: str, segment: str = "b2b"):
    return client.messages.create(
        model="claude-haiku-4-5", max_tokens=50,
        messages=[{"role": "user", "content": ticket}],
    )

The pitfall: instrumenting cost per call and stopping there. What drives decisions is aggregation per workflow and per agent turn. A request costing €0.002 is invisible; a workflow making 800 of them a day with 15 turns each is not.

The hard cap. Beyond tracing, set a counter per agent session and cut off past a threshold (say €0.50 or 25 turns). It's three lines of code and it's what prevents the €4,000 night. Anthropic's Agent SDK exposes it natively via max_budget_usd; in a custom loop, you write it yourself.

4.2 Prompt caching: pay for the prefix once

The principle: the provider keeps the stable prefix of your prompt in memory (tools, system, examples, documents) and bills re-reading it at a fraction of the price — with Anthropic, 10% of the input price for a read, 125% for the initial write. The cache pays off from the second read within the window (5 minutes by default, extendable to one hour).

Prompt caching: where to place the breakpoint
Tools JSON definitions System role, rules, examples Documents knowledge base, contract cache_control History varies Question varies Read from cache · 10% of price Full price Minimum cacheable: 1,024 tokens (2,048 on small models) · up to 4 breakpoints
Everything before the cache_control marker is cached. The content must be strictly identical, byte for byte, from one call to the next — a timestamp in the system prompt breaks everything.
SYSTEM = open("prompts/support_system.md").read()        # 3,000 tokens, stable
KNOWLEDGE = open("kb/returns_policy.md").read()          # 8,000 tokens, stable

def answer(history: list[dict], question: str):
    return client.messages.create(
        model="claude-sonnet-4-5", max_tokens=600,
        system=[
            {"type": "text", "text": SYSTEM},
            {"type": "text", "text": KNOWLEDGE,
             "cache_control": {"type": "ephemeral"}},   # ← everything before this is cached
        ],
        messages=history + [{"role": "user", "content": question}],
    )

resp = answer([], "Can I return an item bought on sale?")
u = resp.usage
print(u.input_tokens, u.cache_creation_input_tokens, u.cache_read_input_tokens)
# 1st call:  11,060  |  11,000 written  |  0 read
# 2nd call:  11,075  |  0               |  11,000 read  → input costs ~12% of list price

The pitfalls, by frequency:

  • A variable element in the prefix. Today's date, a session id, the user's name in the system prompt: each one invalidates the cache. Anything that varies goes into messages, after the breakpoint.
  • Block order. The cache applies to a prefix. If you change tool order between two calls, the prefix differs and nothing is reused.
  • A prefix that's too short. Below the minimum threshold, the marker is silently ignored. You think you're caching; you're caching nothing. Check cache_read_input_tokens in usage, not your intuition.
  • An expiring window. On a workflow that runs once an hour, the 5-minute cache is useless. Either you switch to the one-hour TTL (write at 200%), or you group the calls.

With OpenAI, caching is automatic on identical prefixes above a threshold, with a 50 to 90% discount depending on the model — no marker to place, but the same discipline on prefix stability. With Mistral and Google, the mechanism is explicit and close to Anthropic's.

4.3 Structured output: stop paying for politeness

The principle: force the model to answer in a schema (typed JSON) rather than prose. Double effect: fewer output tokens (the most expensive line item), and zero retries for "the JSON was invalid". The most reliable way to get it across all providers is to define a tool with a strict schema and force its use.

EXTRACT_TOOL = {
    "name": "extract_ticket",
    "description": "Extracts structured fields from a support ticket.",
    "input_schema": {
        "type": "object",
        "properties": {
            "category": {"type": "string", "enum": ["billing", "bug", "feature", "other"]},
            "urgency": {"type": "integer", "minimum": 1, "maximum": 5},
            "product": {"type": "string"},
            "summary": {"type": "string", "maxLength": 200},
        },
        "required": ["category", "urgency", "summary"],
        "additionalProperties": False,
    },
}

def extract(ticket: str) -> dict:
    resp = client.messages.create(
        model="claude-haiku-4-5", max_tokens=200,
        tools=[EXTRACT_TOOL],
        tool_choice={"type": "tool", "name": "extract_ticket"},   # ← no prose possible
        messages=[{"role": "user", "content": ticket}],
    )
    return next(b.input for b in resp.content if b.type == "tool_use")

On an average ticket, the prose version produced 180 output tokens ("Here is the analysis of this ticket…"); the tool version produces 45. At €5 per million output tokens and 20,000 tickets a month, that's €13 instead of €54. Not huge on its own — but multiply it by every stage of every pipeline, and above all it's the disappearance of retries that counts: one fewer turn on an agent is the entire context saved.

max_tokens is a lever, not a formality. A value that's too high costs nothing as long as the model stops early, but it doesn't protect you from a model that wanders off. A value calibrated to your expected output (+30% margin) cuts drift and flags abnormal cases via stop_reason == "max_tokens".

4.4 Batch API: the 50% discount nobody uses

The principle: anything that doesn't need an answer within the second (data enrichment, overnight classification, report generation, quality evaluation) goes into a batch processed within 24 hours, at half price. With Anthropic, OpenAI and Google, the discount is 50% on both input and output, stackable with prompt caching.

from anthropic.types.message_create_params import MessageCreateParamsNonStreaming
from anthropic.types.messages.batch_create_params import Request

def enrich_batch(articles: list[dict]):
    requests = [
        Request(
            custom_id=a["id"],
            params=MessageCreateParamsNonStreaming(
                model="claude-haiku-4-5", max_tokens=300,
                system=[{"type": "text", "text": ENRICH_SYSTEM,
                         "cache_control": {"type": "ephemeral"}}],   # cache + batch stack
                tools=[ENRICH_TOOL],
                tool_choice={"type": "tool", "name": "enrich_article"},
                messages=[{"role": "user", "content": a["text"]}],
            ),
        )
        for a in articles
    ]
    batch = client.messages.batches.create(requests=requests)
    return batch.id

# Later (polling or cron):
def collect(batch_id: str):
    b = client.messages.batches.retrieve(batch_id)
    if b.processing_status != "ended":
        return None
    return {r.custom_id: r.result for r in client.messages.batches.results(batch_id)}

The pitfall is organizational, not technical: batching forces you to decouple ingestion from inference. Your pipeline must accept that an article ingested at 8am gets enriched at 2pm. On a monitoring pipeline like nAIvigate's (11 sources, 4 passes a day), that's perfectly acceptable for translation and relevance scoring; it isn't for answering a user who's waiting.

4.5 Complexity-aware routing: the right model for each request

The principle: the majority of requests in a production system are simple tasks (classification, extraction, rephrasing) that a Haiku-class model handles as well as the frontier model, for 10 to 20 times less. Routing means classifying each request, then sending it to the cheapest model that meets the quality bar. A gateway turns this choice into configuration rather than code.

Complexity-aware routing via gateway
Request Classifier rules + small model simple / medium / complex Gateway LiteLLM · Portkey fallback · quotas · logs Haiku / local 8B 70% of traffic · ×1 Sonnet 25% of traffic · ×3 Opus / frontier 5% of traffic · ×15
The classifier is itself a small model (or simple rules). The gateway centralizes keys, quotas, fallback and observability — switching models becomes a config change.
import re

TIERS = {
    "simple":  "claude-haiku-4-5",
    "medium":  "claude-sonnet-4-5",
    "complex": "claude-opus-4-1",   # reserve for multi-step reasoning
}

def route(task: str, text: str) -> str:
    """Rules first (free), small model only when in doubt."""
    if task in {"classify", "extract", "translate", "summarize_short"}:
        return TIERS["simple"]
    if task in {"plan", "audit", "multi_step"} or len(text) > 12_000:
        return TIERS["complex"]
    if re.search(r"`{3}|def |SELECT |import ", text):        # code present
        return TIERS["medium"]
    # Ambiguous case: let Haiku decide (negligible cost)
    verdict = client.messages.create(
        model=TIERS["simple"], max_tokens=5,
        system="Answer with exactly one word: simple, medium or complex.",
        messages=[{"role": "user", "content": text[:2000]}],
    ).content[0].text.strip().lower()
    return TIERS.get(verdict, TIERS["medium"])

The pitfall: routing without evaluating. Routing assumes you can measure quality per tier. Before shifting 70% of traffic to a small model, you take 200 representative requests, run them through both tiers, and compare (LLM judge or ground truth). If the small model is at 97% of the big one's quality on your task, you switch. If it's at 80%, you don't — or you refine the task until it gets there.

Where should routing logic live?

 In-house Python moduleGateway (LiteLLM, Portkey…)
Setup1 day2-3 days (deploy + config)
Changing a modelCommit + redeployConfig change
Multi-providerCode itNative
Fallback / retry / quotasCode itNative
ObservabilityYour decoratorBuilt-in (Langfuse, OTel)
Attack surfaceMinimalOne more service to secure
Relevant when1 team, 1 providerSeveral teams or providers

4.6 Context compaction: 40 to 60% of tokens the model ignores

The principle: on every call, the context contains stale history, boilerplate, verbose tool results where only one line matters. The model reads them (you pay), then ignores them. Compaction sets a token budget per pipeline stage and applies it before every call: keep the system, summarize old history, keep only the last N turns verbatim, truncate tool outputs.

def compact(history: list[dict], budget_tokens: int, keep_last: int = 6) -> list[dict]:
    """Apply a budget to context: summarize the old, keep recent turns verbatim."""
    count = lambda msgs: client.messages.count_tokens(
        model="claude-haiku-4-5", messages=msgs).input_tokens

    if count(history) <= budget_tokens:
        return history

    old, recent = history[:-keep_last], history[-keep_last:]
    summary = client.messages.create(
        model="claude-haiku-4-5", max_tokens=400,
        system="Summarize this exchange as facts and decisions, no commentary. Max 300 words.",
        messages=old + [{"role": "user", "content": "Summarize."}],
    ).content[0].text

    compacted = [{"role": "user", "content": f"[Summary of earlier exchanges]\n{summary}"},
                 {"role": "assistant", "content": "Understood, resuming from there."}] + recent
    return compacted

def truncate_tool_result(text: str, max_chars: int = 2000) -> str:
    """A 40 KB tool result never contains 40 KB of useful information."""
    return text if len(text) <= max_chars else text[:max_chars] + f"\n…[truncated, {len(text)} chars total]"

The pitfall: compacting blindly and losing the information that mattered. The summary must be oriented to "facts and decisions", not "vibe". And on an agent, the best compaction isn't a summary but external memory: the agent writes its plan and conclusions to a file or a database, and the context only holds the pointer. That's the pattern described in our agentic systems course.

4.7 Sharper retrieval: RAG is a token generator

The principle: in a RAG system, cost is directly driven by chunk size × number of chunks returned. Returning 10 chunks of 1,000 tokens on every question is 10,000 input tokens of which 8,000 are noise. Three settings: smaller, semantically coherent chunks (300 to 800 tokens), a generous top-k at search time followed by a reranker that keeps only the 3 to 5 truly relevant ones, and hybrid search (BM25 + vectors) that reduces noise at the source.

def retrieve(query: str, top_k_search: int = 20, top_k_final: int = 4) -> list[str]:
    # 1. Hybrid: union of lexical and vector candidates
    lexical = bm25_index.search(query, k=top_k_search)
    vector = vector_index.search(embed(query), k=top_k_search)
    candidates = dedupe(lexical + vector)

    # 2. Reranking: a cross-encoder (bge-reranker, Cohere Rerank, etc.)
    scored = reranker.score(query, [c.text for c in candidates])
    best = sorted(zip(scored, candidates), reverse=True)[:top_k_final]

    # 3. Threshold: if nothing is relevant, send nothing (and tell the model)
    return [c.text for s, c in best if s > 0.35]

With this pipeline, a RAG context typically drops from 8-10,000 tokens to 2-3,000, with equal or better answer quality — the model no longer has to sort the noise itself. The classic pitfall: a poorly calibrated reranker that cuts too much and leaves the model hallucinating for lack of context. The threshold is tuned on a test question set, not by feel.

4.8 Semantic cache: don't call at all

The principle: before calling the LLM, you compute the question's embedding, search a vector index for a past question that's close enough (cosine similarity above a threshold), and if you find one, return the stored answer. Cost: one embedding (a hundredth of a cent) instead of a generation.

Semantic cache: the threshold decides everything
Question Embedding ~€0.0001 Vector index Redis · pgvector similarity ≥ threshold? yes no Cached answer cost ≈ 0 · latency ≈ 0 LLM call then store Q + A Scope per tenant · TTL mandatory · never on personalized answers
Too low, you serve wrong answers to neighboring questions. Too high, you never serve anything and pay for embeddings for nothing. 0.92 to 0.95 is a starting point; the right value depends on the embedding model and the domain.
import numpy as np

class SemanticCache:
    def __init__(self, store, threshold: float = 0.93, ttl_s: int = 86_400):
        self.store, self.threshold, self.ttl = store, threshold, ttl_s

    def get(self, tenant: str, question: str):
        q = embed(question)
        hit = self.store.nearest(tenant, q, k=1)          # (score, answer, created_at)
        if hit and hit.score >= self.threshold and not expired(hit.created_at, self.ttl):
            return hit.answer
        return None

    def put(self, tenant: str, question: str, answer: str):
        self.store.add(tenant, embed(question), answer)

cache = SemanticCache(store=RedisVectorStore("faq"))

def ask(tenant: str, question: str) -> str:
    if (cached := cache.get(tenant, question)):
        return cached
    answer = llm_answer(question)
    cache.put(tenant, question, answer)
    return answer

Three non-negotiable rules: scope per tenant (one customer's cache must never serve another — that's a data leak, not an optimization), TTL (a returns policy changes, so does the cached answer), and exclusion of personalized answers ("where's my order?" never gets cached, whatever the similarity). Measure hit rate for two weeks before concluding: on repetitive support it climbs to 40-60%, on a business assistant it can stay under 10%, and in that case remove it.

5. Infrastructure levers (self-hosted only)

If you consume APIs, this section doesn't concern you: the provider already does all of this on their side, and it's included in the price. If you host your own models (sovereignty, volume, sensitive data), here are the three settings that change the GPU bill.

Quantization. A model in INT8 or INT4 weights takes 2 to 4 times less memory than its 16-bit version, with negligible quality loss on most tasks (measurable on long reasoning). Concretely, a 70B that needed two 80 GB GPUs fits on one. With Ollama, it's the tag suffix (llama3.3:70b-instruct-q4_K_M); with vLLM, a launch flag. KV cache quantization (the working memory during generation) is a second, more recent lever that lets you serve more concurrent requests per GPU.

# vLLM: AWQ-quantized weights + FP8 KV cache
vllm serve Qwen/Qwen3-32B-AWQ \
  --quantization awq \
  --kv-cache-dtype fp8 \
  --max-model-len 32768 \
  --gpu-memory-utilization 0.92

# Ollama: the tag picks the quantization
ollama pull qwen3:32b-q4_K_M

Distillation / dedicated small model. For a narrow, high-volume task (classifying 50,000 tickets a day into 12 categories), a 3-8 billion parameter model fine-tuned on the frontier model's outputs does just as well for a fraction of the cost, and runs on a modest GPU or even CPU. It's a project (dataset, training, evaluation, maintenance), not a setting — reserve it for tasks whose volume justifies it. Our local model comparator gives you candidates by hardware tier.

Speculative decoding and disaggregation. Speculative decoding has a small "draft" model propose several tokens that the large model verifies in a single pass: 1.5 to 3 times more tokens per second, without changing the output. Prefill/decode disaggregation separates prompt reading (compute-hungry) from generation (memory-hungry) onto different GPUs, for better hardware utilization. Both are inference-server settings (vLLM, SGLang, TensorRT-LLM), relevant from several production GPUs upward. Below that, the effort isn't justified.

When does self-hosting become profitable? A rented H100 runs around €2-3 an hour, i.e. €1,500-2,200 a month continuously, engineering excluded. It serves a quantized 70B at a few hundred tokens per second. At equivalent API prices, that corresponds to several tens of millions of tokens a day. Below that volume — or if the constraint is sovereignty rather than price — the calculation is different and must be done honestly, engineering and on-call included.

6. Worked example: a monitoring pipeline

Take a real-world structure: a pipeline that ingests 11 news sources 4 times a day, filters, then enriches each retained article (title translation, relevance score, 3 key points extracted). Around 200 enriched articles a day. Before optimization, each enrichment is a direct call to a Sonnet-class model, with a 1,200-token system prompt, the article (500 tokens on average), and a 350-token prose answer.

Cumulative effect of levers on the pipeline
Baseline (Sonnet, prose) 100 + Structured output 73 + Prompt caching 52 + Routing → Haiku 17 + Batch API 9 Article trim (500 → 300 tok.) 7 Base 100 = daily cost before optimization
Each bar represents relative daily cost after activating the lever. The first four levers are code changes of a few hours; the gain is -90% at equal quality on this type of task.

The calculation in detail, with relative prices (input = 1, output = 5 for Sonnet; Haiku at one third):

  • Baseline: 200 × (1,700 input tokens × 1 + 350 output × 5) = 200 × 3,450 = 690,000 units. Index 100.
  • Structured output: the answer drops from 350 to 90 tokens (typed JSON with 3 fields). 200 × (1,700 + 450) = 430,000. Index 62 — but we keep 73 in the chart to account for a few residual retries on malformed articles, which only disappear at the next step. Be conservative in your own projections.
  • Prompt caching: the 1,200 system tokens are read at 10% of price (the 4 daily passes are grouped within the window). Input drops to 120 + 500 = 620. Index 52.
  • Routing to Haiku: the task (translate a title, score, extract three points) evaluates at 96% of Sonnet's quality on 200 test articles. Average price divided by 3. Index 17.
  • Batch API: enrichment doesn't need to be instant. -50%. Index 9.
  • Trim: only the lede and first two paragraphs are sent (300 tokens instead of 500) — the rest doesn't improve the score. Index 7.

None of these levers took more than half a day of work. The gain is 93%, on a task that — this matters — lends itself particularly well: repetitive, non-urgent, structured output. A real-time conversational assistant with long answers won't see 93%. It'll see 40 to 60%, which is still considerable.

📐
What this case teaches
Most of the gain comes from code levers (structure, cache, routing, batch), not infrastructure. Quantization and speculative decoding don't even appear. For the vast majority of organizations consuming APIs, the answer to "how do I cut my LLM bill" fits in one week of work by a developer who has read this guide — provided you've first measured where the money goes.

7. Test your understanding

🧠 Quiz
Question 1 of 6

Your agent costs €4,000 a month while each unit call costs €0.01. Which term of the formula explains the gap?

📚Technical glossary (expand)

Token — An LLM's billing unit; about 0.75 English words, slightly less in French. Input and output are billed at different rates.

Prompt caching (prefix caching) — Provider mechanism that memorizes a stable prompt prefix and bills re-reading it at a fraction of the price (10% with Anthropic).

cache_control — Marker in the Anthropic API delimiting the end of the prefix to cache. Up to 4 breakpoints.

TTL (time to live) — Validity duration of a cache entry. 5 minutes by default for Anthropic prompt caching, extendable to 1 hour.

Batch API — Asynchronous endpoint that processes a batch of requests within 24 hours for 50% of the price. Stackable with prompt caching.

Complexity-aware routing — Classifying each request to send it to the cheapest model meeting the required quality.

LLM gateway — Proxy centralizing keys, quotas, fallback, routing and observability (LiteLLM, Portkey, OpenRouter…). Makes model changes configurable.

Structured output — Answer constrained to a schema (typed JSON), reliably obtained via a strict-schema tool and a forced tool_choice.

Context compaction — Reducing context before each call: summarizing old history, keeping recent turns, truncating tool outputs.

Token budget — Token ceiling allocated to a pipeline stage, applied before the call.

Reranker — Model (cross-encoder) that rescores search candidates to keep only the most relevant. Reduces the number of chunks sent to the LLM.

Hybrid search — Combination of lexical (BM25) and vector search to reduce noise at the source.

Semantic cache — Cache that compares question embeddings against a similarity threshold, to return an already-generated answer without calling the LLM.

Cosine similarity — Proximity measure between two embeddings, from -1 to 1. The semantic cache threshold is typically set between 0.92 and 0.95.

Quantization — Reducing weight precision (16-bit → 8 or 4-bit) to divide memory by 2 to 4, with low quality loss.

KV cache — The model's working memory during generation. Its quantization (FP8, INT4) allows serving more concurrent requests per GPU.

Distillation — Training a small model to reproduce a large one's outputs on a narrow task.

Speculative decoding — A small draft model proposes several tokens, the large one verifies them in one pass: 1.5-3× throughput without changing the output.

Prefill/decode disaggregation — Separating prompt reading and generation onto distinct GPUs, for better hardware utilization.

Cost per unit of value — The KPI to track: cost per resolved ticket, processed document, qualified lead — not total cost.

Frequently asked questions

Do prompt caching and the Batch API stack? Yes, with Anthropic both discounts apply: a prefix read from cache inside a batch costs 10% × 50% = 5% of standard input price.

Does routing degrade quality? Only if you route without evaluating. The protocol: 200 representative requests, processed by each tier, compared. You switch when the small model is above 95% of the big one's quality on your task.

Where do I start if I only have one day? Minimal observability (the decorator from section 4.1) and prompt caching on your largest prefix. You'll see the gain the next day in usage stats.

Do I need a gateway from the start? No. A 30-line Python module is enough for one team and one provider. The gateway becomes relevant when several teams or providers come into play.

Is self-hosting a cost lever? Rarely below several tens of millions of tokens a day. It's a sovereignty or confidentiality lever, and it must be priced honestly, engineering and on-call included.

Going further

Context compaction and external memory are covered in depth in our course on agentic systems architecture, and the orchestrator/workers pattern — with its spend cap — in the hands-on multi-agent guide. To pick a local model by hardware tier and compare API prices across providers, the nAIvigate comparator includes a monthly cost calculator by token volume.

If your LLM bill is already drifting and you want a quantified diagnosis of your workflows — where the money goes, which levers to activate, in what order — that's precisely what a Radar IA covers.

Tags
coutfinopsllmoptimisationprompt-cachingroutingbatchautomationagentspythonapi
⚡ FICHE #004The 50 AI terms to master for 2026 decisions12 MIN

Read next