Request a callbackBook a call
← All posts

LLM Routing and Caching: How to Actually Price a Request

TL;DR
  • Stamp a price_snapshot_id on every request. Cost computed at query time is fiction the moment a vendor reprices, and vendors repriced twice in the month before this was written.
  • Routing by request class is the largest single lever: on a modelled one-million-request month it takes $42,085 to $16,818, a 60% reduction, before any caching at all.
  • Semantic caching is a correctness decision wearing a cost badge. At a 40% hit rate and a 3% false-hit rate you save about $4,780 a month and ship about 12,000 wrong answers, buying them at 40 cents each.
The five things that price a request, in the order they execute
1 · Classify

Assign a request class before anything else. Class is the join key for routing, caching policy, budgets, alerting and every cost report you will ever run. Fails as: one undifferentiated bucket you cannot optimise.

~0 cost · enables everything
2 · Route

A policy table from class to model tier, with a fallback on 429 and 5xx. Modelled saving: 60% versus routing everything to the frontier tier. Fails as: silent quality regression with no eval to catch it.

-60% modelled
3 · Assemble for cacheability

Stable content first, volatile content last, deterministic serialisation throughout. Cache reads bill at 10% of input on Anthropic and OpenAI. Fails as: a timestamp at the top zeroes your hit rate and triples the bill.

-29% on routed spend
4 · Decide on semantic cache

Not a cost setting. A correctness setting with a cost side effect. A 3% false-hit rate at a 40% hit rate is roughly 12,000 wrong answers a month per million requests. Fails as: a confident answer to a different question.

$0.40 per wrong answer
5 · Record with a price snapshot

Tokens, cache status, model version, tenant, feature, and the id of the rate card the cost was computed against. Fails as: a vendor price cut silently rewrites last quarter's unit economics.

one afternoon · permanent
Only layer five is genuinely novel, and it is the one nobody builds. The other four are well-covered ground executed badly. Note the ordering: classification has to happen first because every other layer keys off the class, and the price snapshot has to be written at call time because it cannot be reconstructed afterwards. The rate card in force on 3 June is not recoverable from a system that only stores today's.

How much does one LLM request actually cost?

Uncached input tokens at the input price, plus cached input at roughly a tenth of it, plus output at five to six times input, plus cache writes at 1.25x or 2x, plus retrieval, tools, judges and storage. Write the equation once, put it in middleware, and stop arguing. The interesting part is not the equation; it is that most teams cannot evaluate it for any individual request.

The reason they cannot is structural. Provider dashboards give you a total for an account. Your application knows which customer and which feature caused a call. Nothing joins those two facts unless you build the join yourself, at call time, on the way through. That is the whole of per-request cost attribution: a middleware that records the token counts the provider returned, the cache status, the model and model version, the class, the tenant, the feature, the trace identifier, and the identifier of the rate card you priced it against.

Store cost as an integer number of micro-dollars, never a float. A run costing $1.0530 is 1053000 micros. Floats accumulate representation error across millions of rows, and the first time finance reconciles your number against a provider invoice and finds a fourteen-cent discrepancy across a quarter, you will spend a day proving it was IEEE 754 and not fraud. This is a boring rule and it has saved me that day more than once.

The equation below is worked across five request shapes at August 2026 list prices. Every cost figure in this post is a modelled worked example computed from printed token counts, not a measurement of any customer system. The one number that is not modelled is my own track record on cloud spend: I cut a $200,000-a-year cloud bill by more than 70% and secured $300,000 in cloud credits. I keep those separate from the router arithmetic below, because conflating a published result with a model is how cost posts become untrustworthy.

Request classInput (static/volatile)OutputTierUncachedPrefix-cachedSaving
Classify / extract500 / 40060Cheap $0.20 / $1.20$0.000252$0.00016236%
Summarise1,200 / 2,300400Cheap $0.20 / $1.20$0.001180$0.00096418%
RAG answer6,000 / 3,240400Mid $2 / $10$0.022480$0.01168048%
Agent step (mid-run)9,000 / 16,000250Mid $2 / $10$0.052500$0.03630031%
Hard reasoning3,000 / 9,0001,200Frontier $5 / $25$0.090000$0.07650015%
Model inputsStatic = system prompt, tool schemas, few-shot. Volatile = retrieved chunks, history, user inputList prices checked 24 Aug 2026No cacheCache read at 0.10x input, write at 1.25x, TTL not expiredDepends entirely on the static share

Why does my historical cost data change when a vendor changes prices?

Because you are computing cost at query time against today's rate card instead of storing it at call time against the rate card that was actually in force. The fix is a price_snapshot_id stamped on every request. It is one small table, one foreign key and about an afternoon of work, and it is the single most under-built thing in AI cost engineering.

The failure it prevents is quiet and expensive. A provider cuts input pricing by 30% on 1 June. Your dashboard multiplies stored token counts by current prices, so every historical month is instantly re-priced at the new rate. March now looks 30% cheaper than March was. On the modelled traffic in this post, $16,818 a month of routed spend, a quarter of history silently understates by roughly $15,100, and every unit-economics conversation downstream inherits that error. Somebody prices an enterprise contract off a gross margin that never existed. There is no alert for this, because nothing failed.

The second failure is the reverse and it is worse in a board meeting. A price increase makes last year look more expensive than it was, so your cost-per-request trend line shows an improvement you did not earn, or a regression you did not cause. Either way the graph is not a measurement of your system; it is a measurement of the vendor's pricing page crossed with your system, and you cannot separate the two after the fact.

A price snapshot is a row that captures a complete rate card for one provider, model and model version, with an effective_from timestamp, the source URL and a captured_at. Every call writes the id of the snapshot it was priced against, together with the cost it computed. Cost becomes immutable history rather than a derived quantity. Three capabilities fall out of this immediately and none of them are otherwise available: your historical costs stop moving, you can answer what last month would have cost on the new rate card as a genuine counterfactual, and you can attribute a step change in your cost graph to a specific pricing event rather than guessing. That third one is the reason the on-call engineer stops being asked why cost went up on the ninth.

002_price_snapshot.sql
create table price_snapshot (
  id                        uuid primary key,
  provider                  text not null,
  model                     text not null,
  model_version             text not null,     -- pin it. -latest is not a version.
  input_micros_per_mtok         bigint not null,
  cached_input_micros_per_mtok  bigint not null,
  cache_write_5m_micros_per_mtok bigint,
  cache_write_1h_micros_per_mtok bigint,
  output_micros_per_mtok        bigint not null,
  effective_from            timestamptz not null,
  effective_to              timestamptz,        -- null = current
  source_url                text not null,      -- the vendor pricing page
  captured_at               timestamptz not null default now(),
  captured_by               text not null,      -- 'scraper' | 'neeraj' | ...
  unique (provider, model, model_version, effective_from)
);

create unique index one_current_card
  on price_snapshot (provider, model, model_version)
  where effective_to is null;

create table llm_call (
  id                 uuid primary key,
  trace_id           text not null,
  run_id             uuid,
  tenant_id          uuid not null,
  feature            text not null,      -- what the user was doing
  request_class      text not null,      -- the routing + caching join key
  provider           text not null,
  model              text not null,
  model_version      text not null,
  input_tokens       int  not null,
  cached_input_tokens int not null default 0,
  cache_write_tokens int  not null default 0,
  output_tokens      int  not null,
  price_snapshot_id  uuid not null references price_snapshot(id),
  cost_micros        bigint not null,    -- computed ONCE, at call time
  latency_ms         int  not null,
  created_at         timestamptz not null default now()
);

create index on llm_call (tenant_id, created_at desc);
create index on llm_call (request_class, created_at desc);
create index on llm_call (price_snapshot_id);

-- The counterfactual that is impossible without snapshots:
-- what would last month have cost on the CURRENT rate card?
select
  c.request_class,
  sum(c.cost_micros) / 1e6                          as actual_usd,
  sum( c.input_tokens        * n.input_micros_per_mtok
     + c.cached_input_tokens * n.cached_input_micros_per_mtok
     + c.output_tokens       * n.output_micros_per_mtok ) / 1e12 as at_new_prices_usd
from llm_call c
join price_snapshot n
  on  n.provider = c.provider
  and n.model    = c.model
  and n.model_version = c.model_version
  and n.effective_to is null
where c.created_at >= date_trunc('month', now()) - interval '1 month'
  and c.created_at <  date_trunc('month', now())
group by 1
order by actual_usd desc;
The whole idea, in two tables. price_snapshot is append-only: you never update a rate card, you insert a new one and close the previous with effective_to. llm_call stores both the snapshot id and the computed cost, deliberate redundancy: the id lets you audit and reprice, the stored micros mean a report never has to recompute and never drifts. source_url and captured_by exist because in eighteen months somebody will ask where a number came from, and pointing at a row beats remembering.
The same quarter, reported two ways
Cost computed at query time
Q1 reported spend
$35,354 — re-priced at June's rates
Q1 actual spend
$50,454
Understatement
$15,100, invisible
Cost-per-request trend
Shows a 30% improvement nobody earned
Attribution of a step change
Guesswork
Two dashboards built a week apart
Disagree, and neither is wrong
Detection signal
None. Nothing failed
Cost stamped with a price_snapshot_id
Q1 reported spend
$50,454, permanently
Q1 actual spend
$50,454
Understatement
Zero, by construction
Cost-per-request trend
Measures your system, not the vendor's pricing page
Attribution of a step change
Join to price_snapshot; the row names the date
Two dashboards built a week apart
Agree, because neither recomputes
Detection signal
A call whose snapshot is not the current card
One afternoon of work. Prevents a $15,100 reporting error per quarter on modelled traffic, and one enterprise contract priced off a margin that never existed.
The modelled figures assume $16,818 a month of routed spend and a 30% vendor input-price cut mid-quarter. Scale them to your own bill. The row that matters most is the last one in each column: without snapshots there is no detection signal at all, because a silently re-priced dashboard is indistinguishable from a correct one.

How should you route a request to a model?

By request class, from a policy table, with a fallback tier on 429 and 5xx. Not by prompt length, not by a learned router in v1, and not by whatever the last benchmark said. Classification first, because the class is also the join key for your caching policy, your budgets, your alerts and every cost report you will run.

The modelled saving is the largest single lever in this post. On a one-million-request month with the traffic mix below, routing everything to the frontier tier costs $42,085. The same month routed by class costs $16,818, a 60% reduction with no caching at all. The mix matters more than the tiers: 35% of that traffic is classification and extraction, competent on a cheap model and 14% of the bill there against 5% of the bill on a frontier model that charges 25 times as much for it.

The failure mode of routing is silent quality regression, and it has a precise cost. If a downgrade takes a class from 94% to 89% task success and that class is 300,000 requests a month, you have shipped 15,000 additional failures to buy $13,356 of savings, 89 cents of saving per failure. Whether that is a good trade depends on what the class does, which is why a routing change without an eval gate is not an optimisation, it is a wager. Build the deterministic eval suite first; the sequencing is in the reference architecture's eval ladder.

The second failure mode is the fallback that nobody tested. A 429 from your primary is routine, not exceptional, and the fallback route should be exercised in CI against the same eval set as the primary. A fallback that has never been evaluated is a quality regression waiting for a capacity incident to trigger it, and the cost of finding out in production is a percentage of a day's traffic answered by a model you have never measured.

Request classShare of 1MRouted tierPer requestRouted monthlyAll-frontier monthlyFailure if downgraded too far
Classify / extract35%Cheap$0.000252$88$2,100Silent misclassification; downstream logic acts on a wrong label
Summarise20%Cheap$0.001180$236$5,500Omitted detail; user cannot tell without reading the source
RAG answer30%Mid$0.022480$6,744$16,860Groundedness drops; citations stop matching their spans
Agent step10%Mid$0.052500$5,250$13,125Worse tool selection, more turns — cost rises while quality falls
Hard reasoning5%Frontier$0.090000$4,500$4,500This class is why the frontier tier exists. Do not route it down
Total1,000,000Mixed$0.0168 avg$16,818$42,085Routing saves $25,267 a month, a 60% reduction, before any caching
Three configurations of the same month
$ per month, one million requests, modelled traffic mixlower is better
Everything on the frontier tierthe default when nobody builds a router$42,085
Routed by request class-60%, no caching involved$16,818
Routed + prefix caching at 80% hit rate-69% vs baseline$12,930
Routed + caching + a 15,000-failure quality costwhat an ungated downgrade actually buys$12,930 + 15,000 failures
Modelled floor: cheap tier for everythingand a product nobody would ship$4,900
The gap between the second and fifth bars is the entire routing argument: you could go lower, and you should not. The fourth bar exists because a cost chart without a quality column is how teams justify a downgrade that costs them a customer. At 89 cents of saving per additional failure, the trade is only defensible for classes where a failure is cheap and recoverable.
Which tier, and what breaks if you get it wrong
How do you assign a request class to a model tier?
Output is a label, a boolean, or a short structured extraction with a schema
Cheap tier, with a schema gate

Validation catches most failures deterministically, which is what makes the downgrade safe. Fails as: confident wrong labels that pass schema validation. Modelled saving: $2,012 a month on 350K requests.

Answer is grounded in retrieved text and must cite it
Mid tier, with a groundedness check on the cheap tier

The check costs about $0.0008 per answer and catches the failure the downgrade would introduce. Fails as: fluent answers whose citations do not match their spans. Modelled: $6,744 versus $16,860 on frontier.

Multi-step tool use where a wrong tool choice costs a turn
Mid tier, and measure turns not tokens

A cheaper model that needs three more turns is more expensive, because history is re-sent every turn. Fails as: cost rises while quality falls, and the router looks like it worked.

Novel reasoning, long-horizon planning, or the output is customer-visible and irreversible
Frontier tier. Do not route down

5% of traffic, 27% of the routed bill, and the reason the router is defensible at all. Fails as: a downgrade here is the one that loses a customer.

You cannot state which class this is
Classify before you route

An unclassified request is unroutable, uncacheable and unattributable. Fails as: one undifferentiated cost bucket and no way to optimise any of it.

Every branch names both the saving and the failure, because a routing table without failure modes attached is a list of guesses. Note the third branch specifically: measuring a router by tokens rather than by turns is the most common way a cost optimisation quietly becomes a cost increase.

How does prompt caching work, and why does prefix ordering matter so much?

The provider caches a prefix of your prompt and bills a later identical prefix at a fraction of the input price. Identical means byte-identical from token zero. That is the whole mechanism, and it is why ordering is not a micro-optimisation: a single volatile token near the top moves the cache boundary to the first token and sets your hit rate to zero.

The three major providers differ in ways that change your design. Anthropic bills cache reads at 0.1x base input, with five-minute cache writes at 1.25x and one-hour writes at 2x, and requires explicit cache breakpoints. Each hit saves 0.9x, so the five-minute cache repays its write after one hit and the one-hour cache after two. OpenAI's GPT-5 family gives 90% off cached input automatically on consistent prefixes, no code changes and no write premium. Google offers explicit context caching at a 75% discount plus an hourly storage fee, and implicit caching that fires automatically. In practice: on Anthropic you design the prefix and place breakpoints, on OpenAI you just avoid breaking the prefix, and on Google you choose between paying rent and taking what you are given.

The ordering rule is one sentence: most stable first, most volatile last, deterministic serialisation throughout. System prompt, then tool schemas in a stable sorted order, then few-shot examples, then long-lived retrieved context, then conversation history, then the current user turn. Four things break it and all four are common. A timestamp or a request id in the system prompt. A tool catalogue serialised from a hash map without sorting, so key order varies between processes. A user's display name injected above the few-shot block. And a framework that silently reorders messages. Each of them is a one-line bug and each of them multiplies the input portion of your bill by ten.

The failure mode with the largest number attached is per-tenant prefixes. If you personalise the system prompt per customer, you have as many distinct cache entries as you have customers, and each one is written and re-written at 1.25x. Modelled: 500 tenants with continuous traffic, a 9,000-token prefix, a five-minute TTL rewritten 288 times a day, at $2.50 per million write tokens is roughly $97,200 a month of pure cache-write cost. The fix is to hoist everything shared above the personalised block so the shared part caches once globally, and it takes an hour.

Prompt caching across the three major providers, August 2026
 Cached read priceWrite premiumActivationDesign consequenceWhat breaks it
Anthropic (Claude)0.1x base input1.25x for a 5-minute TTL, 2x for one hourExplicit cache breakpointsEach hit saves 0.9x, so the 5-minute cache repays after one hit and the 1-hour cache after twoPer-tenant prefixes: modelled $97,200/month of write cost at 500 tenants
OpenAI (GPT-5 family)90% off cached inputNoneAutomatic on consistent prefixes, no code changesYou do not design the cache, you just avoid breaking the prefixAny volatile token near the top; a non-deterministic serialiser
Google (Gemini)75% off explicit, with implicit caching also availableHourly storage fee on explicit cachesExplicit context cache, plus automatic implicit cachingExplicit caching is rent; it only pays above a traffic threshold you must computeLow-traffic prefixes: storage outruns the saving
Semantic cache (any vendor)~100% of the model callEmbedding + vector lookup, roughly $0.00002Similarity threshold you chooseThis is not prompt caching. It is a correctness decision; see the next sectionA threshold set for hit rate rather than for false-hit rate
The last row is a different category, included because it is so often presented alongside the other three as if it were the same kind of thing. Prefix caching returns the same model output for the same input; semantic caching returns a different question's answer and hopes it fits. Verified against vendor documentation on 24 August 2026; re-check before you build on it.
assemble-for-cache.ts
type Block = { role: "system" | "user" | "assistant"; text: string };

export function assemble(req: Request, tools: ToolDef[], hist: Turn[]): Block[] {
  const blocks: Block[] = [];

  // ---- STABLE ZONE: byte-identical across every request of this class ----
  // KILLER 1: never interpolate a timestamp, request id, or tenant name here.
  blocks.push({ role: "system", text: SYSTEM_PROMPT_V7 });

  // KILLER 2: sort tools by name and serialise deterministically.
  // A Map iterated in insertion order gives different bytes per process.
  const catalogue = [...tools]
    .sort((a, b) => a.name.localeCompare(b.name))
    .map(stableStringify)
    .join("\n");
  blocks.push({ role: "system", text: catalogue });

  blocks.push({ role: "system", text: FEW_SHOT_V3 });
  // <-- Anthropic cache breakpoint goes here. Everything above is the prefix.

  // ---- SEMI-STABLE ZONE ----
  // KILLER 3: personalisation above this line multiplies cache entries by
  // your tenant count. 500 tenants x 288 writes/day x 9,000 tokens at
  // 1.25x mid-tier input is roughly $97,200/month of pure write cost.
  if (req.tenantPreamble) blocks.push({ role: "system", text: req.tenantPreamble });

  // ---- VOLATILE ZONE: different every request, never cacheable ----
  for (const t of hist) blocks.push({ role: t.role, text: t.text });
  blocks.push({ role: "user", text: req.userText });

  return blocks;
}

/** The only reliable detector. Without it, a zeroed hit rate is found
 *  on next month's invoice rather than in this afternoon's deploy. */
export function assertCacheHealth(m: CallMetrics, expectPrefix: number) {
  const ratio = m.cachedInputTokens / Math.max(expectPrefix, 1);
  if (ratio < 0.5) {
    metrics.increment("llm.cache.prefix_miss", { class: m.requestClass });
    log.warn("prefix cache miss", {
      class: m.requestClass, cached: m.cachedInputTokens,
      expected: expectPrefix, model_version: m.modelVersion,
    });
  }
}
Context assembly ordered for cacheability, with the four hit-rate killers commented where they normally appear. stableStringify is the boring one that matters most: a tool catalogue serialised from a Map or object without sorted keys can produce different bytes on different processes for identical content, so half your fleet misses the cache and nobody can reproduce it locally. The assertion at the end is the only reliable detector: a cached_input_tokens value near zero on a request whose prefix should be warm is the signal, and without it the detection time is next month's invoice.

Is semantic caching safe to turn on?

Only if you treat it as a correctness decision. Semantic caching returns a previous answer to a different question because a vector said the two were close. That is not a cache the way prefix caching is a cache. It is an approximation of your product's answer, and the similarity threshold is the dial that decides how often it is wrong.

The trade-off is entirely in the threshold, and the published guidance is wide. GPTCache ships a default around 0.75, some vendor examples sit at 0.95, and practitioner guidance for English FAQ workloads clusters around 0.92 to 0.97. The common recommendation for a first deployment is 0.97: conservative, a hit rate of only 5 to 10%, but a false-positive rate under about half a percent. Every step down that dial buys hit rate with wrong answers.

Price it that way and the decision becomes obvious rather than tempting. On the modelled million-request month, a 40% hit rate against an average routed-and-cached cost of about $0.012 saves roughly $4,780 after the embedding and vector-lookup cost of about $20. If the threshold that produces that hit rate carries a 3% false-hit rate, you have served roughly 12,000 wrong answers. That is 40 cents of saving per wrong answer. Nobody who states it in those terms ships it for a medical, legal, financial or account-specific product, and plenty of people ship it without ever computing the ratio.

If you turn it on, three controls make it defensible and none are optional. Hard metadata boundaries first: never match across tenant, locale, model version, permission set or safety flag, whatever the similarity score says. Soft matching inside hard boundaries is the only safe shape. Second, exclude classes where the answer depends on private or time-sensitive state; anything containing an account number, a balance, a date or the word my is a different question by construction. Third, verify before serving on anything customer-visible: a cheap-tier call asking whether this cached answer answers this question costs about $0.0003 and, on the modelled traffic, converts most of the false-hit rate into a cache miss for about 12% of the gross saving.

What you are actually buying at each similarity threshold
5,8804,4102,9401,47000.990.970.950.920.880.85Modelled monthly value, USD (savings) and wrong answers / 100Similarity threshold
Monthly saving, USDWrong answers per month, hundredsSaving after a cheap verification pass, USD
Modelled on one million requests a month, an average routed-and-cached cost of $0.012, and false-hit rates interpolated from published practitioner guidance: roughly 0.5% at a 0.97 threshold rising steeply below 0.95. Substitute your own measured false-hit rate; measuring it takes a few hundred labelled pairs and an afternoon, and it is the only number in this chart that matters. The cyan line is what a $0.0003 verification call does: it costs about 12% of the gross saving and converts most false hits into misses.

How do you attribute cost to a customer, a feature and a request?

With five columns written at call time (tenant, feature, request class, trace id and price_snapshot_id) and nothing written afterwards. Attribution cannot be reconstructed later, because the only system that knows why a call happened is the code that made it, and that context is gone by the time the invoice arrives.

The immediate return is that arguments become queries. Which tenant is unprofitable, which feature moved last week's cost, which class regressed after Tuesday's deploy, what the p95 cost of a run is rather than the mean: all four are single SELECTs against the llm_call table and otherwise unanswerable. The mean is worth calling out. Agent cost distributions are bimodal, because a cached two-turn run and a cold twenty-turn run are not the same population, and quoting a mean cost per request on that distribution is how a pricing model ends up underwater on exactly the customers who use the product most.

The alert that actually catches regressions is cost per request per class, week over week, at plus 25%. Total spend is not an alert, because total spend rises when you grow, and a team that gets paged for growth stops reading the page within two weeks. Cost per request rising is always a signal: it means a prompt got longer, a cache broke, a router changed, an agent needed more turns, or a retry loop appeared. The failure mode of not having it is a bill that grows 40% with no attributable cause and a week of engineering time spent bisecting deploys.

The last column that earns its place is retries. Failed calls are missing from most teams' cost data entirely, because the failed call never reached the success-path logger, and a retry storm is therefore invisible until it is on the invoice. Record every attempt with its own row and an attempt number. On a modelled agent class at $0.0525 a step, a retry loop that adds two extra attempts to 5% of 100,000 monthly steps is $525 a month that does not appear in any dashboard you would otherwise build.

Four questions you cannot answer without per-request attribution
$ $ psql -c "select tenant_id, sum(cost_micros)/1e6 usd, count(*) calls
$ from llm_call where created_at > now() - interval '30 days'
$ group by 1 order by usd desc limit 3"
tenant_id | usd | calls
4c1e...9a (Northwind, $499/mo plan) | 1,284.40 | 61,203 <-- unprofitable
77bd...02 (Contoso, $2,400/mo plan) | 611.02 | 39,880
0af3...d1 (Fabrikam, $999/mo plan) | 402.77 | 24,101
$ $ costctl class-trend --weeks 2 --alert-at 25%
request_class last_wk/req this_wk/req delta
rag_answer $0.01168 $0.01191 +2.0% ok
agent_step $0.03630 $0.05942 +63.7% ALERT
classify $0.00016 $0.00016 +0.0% ok
$ $ costctl explain agent_step --since 2026-08-17
cached_input_tokens p50: 9,014 -> 0 <-- prefix cache died
deploy 4f2a1c 'add build sha to system prompt' at 2026-08-18T09:12Z
modelled monthly impact: +$2,312 on this class alone
$ $ costctl retries --class agent_step --month 2026-08
attempts>1: 5.1% of 100,412 steps, 10,240 extra calls, $525.32
(invisible in any dashboard built from success-path logs only)
Four queries, each impossible without five columns written at call time. The third one is the whole argument for this section: a build SHA added to a system prompt on 18 August moved the p50 cached-input tokens from 9,014 to zero, and the modelled impact is $2,312 a month on one class. With attribution that is a three-minute diagnosis; without it, it is a bill that grew for no reason anyone can name.
Modelled outcome of the full stack on one million requests a month
$42,085
baseline: every request on the frontier tier, no caching, cost computed at query time
$16,818
after routing by request class
-60%
$12,930
after prefix caching at a modelled 80% hit rate on the static prefix
-69% vs baseline
1 afternoon
to add price_snapshot_id, after which none of the numbers above ever silently change again
Modelled, not measured, from the traffic mix and token counts printed earlier on this page. The fourth figure is the one I would build first even though it saves nothing directly: every other number here is only trustworthy because it exists, and a cost programme built on recomputed history will eventually mis-price a contract.

Do you need a router product, or a switch statement?

A switch statement and a policy table, until you can name the thing a product would do that your table cannot. That is not a dismissal of the category. LiteLLM, OpenRouter, Portkey, RouteLLM, Martian and Not Diamond all do real work. It is a sequencing claim: the policy table teaches you your own traffic mix, and you cannot evaluate a router product before you have that.

Start with about a hundred lines. A class-to-tier map, a fallback chain, a token-bucket rate limiter per provider, and the attribution middleware from the previous section. It has no operational surface, no new dependency in the hot path, and it produces the classification data every other decision needs. Its failure mode is a manual edit shipped without an eval gate, which is the same failure mode every router product has, so you are not buying that away.

Reach for a gateway when you have a specific reason. Many providers with different SDKs and you want one OpenAI-compatible surface: LiteLLM, self-hosted, open source. Access to a wide model catalogue behind one key without provider contracts: OpenRouter. Centralised observability, guardrails and spend controls across several teams: Portkey, whose core gateway is open source alongside a cloud tier. Automatic best-or-cheapest model selection learned from data rather than declared in a table: Martian or Not Diamond, with RouteLLM as the research-grade open-source approach that trains classifiers on preference data. Each of those is a real reason. None of them is the reason people usually give, which is that routing is complicated.

The honest cost of a gateway is a hop in the hot path and a dependency that can fail. Budget a few milliseconds at p50 and more at p95 under a cold cache, measure it rather than trusting a benchmark, and make sure the failure mode is degradation to a direct provider call rather than an outage. My own voice work is where this discipline came from: running production voice agents at about 2.5 cents a minute on a custom LiveKit stack only works if every hop in the path is measured, because at that price point a gateway adding 15 milliseconds and one more thing that can page you is a real architectural cost, not a rounding error. If you want that arithmetic run against your own stack, that is what AI product development services are for.

OptionReach for it whenWhat it costs youFailure modeModelled monthly cost at 1M requests
Policy table + switch statementAlways, first. It is ~100 lines and it teaches you your traffic mixAn afternoon, and you own the fallback logicA manual routing edit shipped without an eval gate$0 of infrastructure
LiteLLM (self-hosted)Many providers, many SDKs, you want one OpenAI-compatible surfaceA proxy to run, scale and page someone aboutThe proxy is now in the hot path for every callCompute only, roughly $80-$300
OpenRouterYou want a wide model catalogue behind one key without provider contractsA margin on tokens and a third party in the data pathA provider you did not choose is serving your trafficA per-token margin on $16,818
PortkeySeveral teams need shared observability, guardrails and spend limitsA gateway hop plus a vendor relationshipGuardrail latency lands in the user-visible pathTiered; measure the p95 hop first
RouteLLM / Martian / Not DiamondYou have measured that a static table leaves real money on the tableA learned component you must evaluate like a modelThe router regresses quality and nothing alerts, because it succeededVaries; needs an eval budget of its own
Semantic cache layerOnly after the correctness arithmetic in the section aboveA wrong-answer rate you have chosen deliberatelyA confident answer to a different questionSaves ~$4,780; costs ~12,000 wrong answers
Checklist
The cost-engineering build order, cheapest and highest-leverage first
  • Attribution middleware: tenant, feature, request class, trace id, tokens, cache status, model versionOne afternoon. Every later decision is an argument until this exists. Saves nothing directly, enables everything.
  • price_snapshot table and a foreign key on every call rowHalf a day. Prevents a modelled $15,100 quarterly reporting error and makes counterfactual repricing a single query.
  • Request classification before routingThe join key for routing, caching, budgets and alerting. An unclassified request is unroutable and unattributable.
  • Policy table from class to tier, with a fallback chain exercised in CIModelled -60%. The fallback must be evaluated against the same suite as the primary, or it is an untested quality regression.
  • Cache-ordered context assembly with a deterministic serialiserModelled -29% on routed spend. Four one-line bugs can zero it, and the assertion on cached_input_tokens is the only detector.
  • Cost-per-request-per-class alert, week over week, at +25%Never alert on total spend. Total spend rises when you grow, and a team paged for growth stops reading pages.
  • Record every attempt, including failures and retries, with an attempt numberModelled $525 a month of retry cost invisible to success-path logging alone.
  • Semantic cachingOnly after you have written the wrong-answers-per-month number in the design doc and someone has signed it.
  • A learned router or a gateway productOnly after the policy table has told you what your traffic actually looks like and where the residual money is.
The first seven are roughly three engineer-days in total and account for essentially all of the modelled 69% reduction. The last two are where most AI cost budgets go first, before anyone has the data to evaluate them.

LLM routing and caching: common questions

What is a price_snapshot_id and why do I need one?

It is the identifier of the exact rate card a request was priced against, stored on the request row alongside the cost you computed at call time. Without it, every dashboard recomputes historical cost against today's prices, so a vendor price change silently rewrites your history: a modelled $15,100 understatement across a quarter after a 30% input price cut. With it, historical cost is immutable, you can reprice any past month against a new rate card as a genuine counterfactual, and you can attribute a step change in your cost graph to a specific pricing event instead of guessing.

How do you reduce LLM API costs without hurting quality?

In this order: classify requests, route by class from a policy table with an eval gate on every change, order your context most-stable-first so prefix caching works, and record cost per request so you can tell which changes helped. On a modelled one-million-request month that takes $42,085 to $12,930, a 69% reduction. Skip semantic caching until you have written down how many wrong answers a month it will serve, because it is the only item on that list that trades correctness for cost.

How much does prompt caching save?

It depends entirely on what share of your prompt is static. Anthropic bills cache reads at 0.1x base input with writes at 1.25x for a five-minute TTL and 2x for one hour, so each hit saves 0.9x and a five-minute cache repays after one hit. OpenAI's GPT-5 family gives 90% off cached input automatically. On the modelled RAG class here, 6,000 static tokens out of 9,240 takes a request from $0.0225 to $0.0117, a 48% saving. On a class that is mostly volatile input, the same mechanism saves 15%.

Why is my prompt cache hit rate zero?

Almost always one of four things, all one-line bugs. A timestamp, build SHA or request id interpolated into the system prompt. A tool catalogue serialised from a hash map without sorted keys, so the bytes differ between processes. A personalised preamble placed above the shared content. Or a framework that silently reorders messages. Cache matching requires a byte-identical prefix from token zero, so any of these moves the cache boundary to the first token. The detector is an assertion that cached_input_tokens is at least half the expected prefix length; without it, detection time is next month's invoice.

Is semantic caching safe for production?

Only when you have priced the wrong answers. Semantic caching returns a previous answer to a different question because a vector said they were close, so the similarity threshold is a correctness dial: published guidance ranges from a GPTCache default around 0.75 to practitioner recommendations of 0.97 for a first deployment, where the hit rate is 5 to 10% but the false-positive rate is under about half a percent. At a 40% hit rate with a 3% false-hit rate on a million requests, you save about $4,780 a month and serve about 12,000 wrong answers. Use hard tenant, locale, model-version and permission boundaries, exclude anything account-specific or time-sensitive, and add a cheap verification pass before serving.

Should I use an LLM gateway like LiteLLM, OpenRouter or Portkey?

Not first. Start with a policy table and a switch statement, roughly a hundred lines, because it teaches you your own traffic mix, which you need before you can evaluate any product. Then reach for a specific one for a specific reason: LiteLLM self-hosted for one OpenAI-compatible surface over many providers, OpenRouter for a wide catalogue behind one key, Portkey for shared observability and guardrails across teams, and Martian, Not Diamond or RouteLLM when you have measured that a static table is leaving real money on the table. Budget a few milliseconds at p50 and more at p95, and make sure the gateway failing degrades to a direct provider call rather than an outage.

How do you attribute LLM cost to a specific customer?

Write tenant id, feature, request class, trace id and price_snapshot_id on every call row at call time. It cannot be reconstructed later, because the only system that knows why a call happened is the code that made it. Once those five columns exist, unprofitable tenants, cost regressions by class and the effect of a specific deploy all become single SQL queries. Quote p95 rather than mean cost per request: agent cost distributions are bimodal, and a mean computed across cached two-turn runs and cold twenty-turn runs will underprice exactly the customers who use the product most.

Ready to talk numbers?

Twenty minutes, straight to the engineer. No sales rep, no deck.