LLM Routing and Caching: How to Actually Price a Request
- 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.
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 everythingA 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% modelledStable 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 spendNot 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 answerTokens, 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 · permanentHow 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 class | Input (static/volatile) | Output | Tier | Uncached | Prefix-cached | Saving |
|---|---|---|---|---|---|---|
| Classify / extract | 500 / 400 | 60 | Cheap $0.20 / $1.20 | $0.000252 | $0.000162 | 36% |
| Summarise | 1,200 / 2,300 | 400 | Cheap $0.20 / $1.20 | $0.001180 | $0.000964 | 18% |
| RAG answer | 6,000 / 3,240 | 400 | Mid $2 / $10 | $0.022480 | $0.011680 | 48% |
| Agent step (mid-run) | 9,000 / 16,000 | 250 | Mid $2 / $10 | $0.052500 | $0.036300 | 31% |
| Hard reasoning | 3,000 / 9,000 | 1,200 | Frontier $5 / $25 | $0.090000 | $0.076500 | 15% |
| Model inputs | Static = system prompt, tool schemas, few-shot. Volatile = retrieved chunks, history, user input | List prices checked 24 Aug 2026 | No cache | Cache read at 0.10x input, write at 1.25x, TTL not expired | Depends 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.
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;- 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
- 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
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 class | Share of 1M | Routed tier | Per request | Routed monthly | All-frontier monthly | Failure if downgraded too far |
|---|---|---|---|---|---|---|
| Classify / extract | 35% | Cheap | $0.000252 | $88 | $2,100 | Silent misclassification; downstream logic acts on a wrong label |
| Summarise | 20% | Cheap | $0.001180 | $236 | $5,500 | Omitted detail; user cannot tell without reading the source |
| RAG answer | 30% | Mid | $0.022480 | $6,744 | $16,860 | Groundedness drops; citations stop matching their spans |
| Agent step | 10% | Mid | $0.052500 | $5,250 | $13,125 | Worse tool selection, more turns — cost rises while quality falls |
| Hard reasoning | 5% | Frontier | $0.090000 | $4,500 | $4,500 | This class is why the frontier tier exists. Do not route it down |
| Total | 1,000,000 | Mixed | $0.0168 avg | $16,818 | $42,085 | Routing saves $25,267 a month, a 60% reduction, before any caching |
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.
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.
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.
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.
An unclassified request is unroutable, uncacheable and unattributable. Fails as: one undifferentiated cost bucket and no way to optimise any of it.
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.
| Cached read price | Write premium | Activation | Design consequence | What breaks it | |
|---|---|---|---|---|---|
| Anthropic (Claude) | 0.1x base input | 1.25x for a 5-minute TTL, 2x for one hour | Explicit cache breakpoints | Each hit saves 0.9x, so the 5-minute cache repays after one hit and the 1-hour cache after two | Per-tenant prefixes: modelled $97,200/month of write cost at 500 tenants |
| OpenAI (GPT-5 family) | 90% off cached input | None | Automatic on consistent prefixes, no code changes | You do not design the cache, you just avoid breaking the prefix | Any volatile token near the top; a non-deterministic serialiser |
| Google (Gemini) | 75% off explicit, with implicit caching also available | Hourly storage fee on explicit caches | Explicit context cache, plus automatic implicit caching | Explicit caching is rent; it only pays above a traffic threshold you must compute | Low-traffic prefixes: storage outruns the saving |
| Semantic cache (any vendor) | ~100% of the model call | Embedding + vector lookup, roughly $0.00002 | Similarity threshold you choose | This is not prompt caching. It is a correctness decision; see the next section | A threshold set for hit rate rather than for false-hit rate |
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,
});
}
}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.
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.
$ $ 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 | calls4c1e...9a (Northwind, $499/mo plan) | 1,284.40 | 61,203 <-- unprofitable77bd...02 (Contoso, $2,400/mo plan) | 611.02 | 39,8800af3...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 deltarag_answer $0.01168 $0.01191 +2.0% okagent_step $0.03630 $0.05942 +63.7% ALERTclassify $0.00016 $0.00016 +0.0% ok$ $ costctl explain agent_step --since 2026-08-17cached_input_tokens p50: 9,014 -> 0 <-- prefix cache dieddeploy 4f2a1c 'add build sha to system prompt' at 2026-08-18T09:12Zmodelled monthly impact: +$2,312 on this class alone$ $ costctl retries --class agent_step --month 2026-08attempts>1: 5.1% of 100,412 steps, 10,240 extra calls, $525.32(invisible in any dashboard built from success-path logs only)
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.
| Option | Reach for it when | What it costs you | Failure mode | Modelled monthly cost at 1M requests |
|---|---|---|---|---|
| Policy table + switch statement | Always, first. It is ~100 lines and it teaches you your traffic mix | An afternoon, and you own the fallback logic | A manual routing edit shipped without an eval gate | $0 of infrastructure |
| LiteLLM (self-hosted) | Many providers, many SDKs, you want one OpenAI-compatible surface | A proxy to run, scale and page someone about | The proxy is now in the hot path for every call | Compute only, roughly $80-$300 |
| OpenRouter | You want a wide model catalogue behind one key without provider contracts | A margin on tokens and a third party in the data path | A provider you did not choose is serving your traffic | A per-token margin on $16,818 |
| Portkey | Several teams need shared observability, guardrails and spend limits | A gateway hop plus a vendor relationship | Guardrail latency lands in the user-visible path | Tiered; measure the p95 hop first |
| RouteLLM / Martian / Not Diamond | You have measured that a static table leaves real money on the table | A learned component you must evaluate like a model | The router regresses quality and nothing alerts, because it succeeded | Varies; needs an eval budget of its own |
| Semantic cache layer | Only after the correctness arithmetic in the section above | A wrong-answer rate you have chosen deliberately | A confident answer to a different question | Saves ~$4,780; costs ~12,000 wrong answers |
- 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.
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.