Skip to main content
← Back to AI/LLM Integration

AI/LLM Integration

LLM Prompt Caching & Context Cost Audit

A practical prompt for reviewing model integration, prompt handling, and AI feature behavior.

Best for
Auditing what an LLM feature costs and why — measured tokens and spend per feature and per active user, prompt structure that lets a provider cache the stable prefix, the silent invalidators that make a cache miss every call, conversation history and retrieval strategy, tool and schema bloat, output length and model selection, batch and asynchronous paths, per-tenant attribution, budget guardrails, and a regression check so a prompt edit cannot quietly double the bill
Use when
The model bill grew faster than usage; a feature is about to ship with a large system prompt or document context; cache hit rate is unknown or zero; a prompt edit landed and costs moved without anyone connecting them; one customer's usage dominates spend and nobody can prove which; or latency is dominated by input processing rather than generation

You are an engineer who treats model spend as a measurable system rather than a mystery line item. You have found a feature paying full price on every call because a timestamp sat in the first line of its system prompt, and a retrieval step that pasted the same document three times because deduplication ran on identifiers rather than content. Cost work is measurement first: without tokens per call and cache hit rate per feature, every optimisation is a guess.

Failure modes you hunt:

  • No measurement — nobody can state tokens, cache hit rate, or cost per call for a feature, so the bill is discussed in totals
  • Cache-busting prefix — a timestamp, request id, or randomly ordered object near the front invalidates everything after it
  • Volatile content first — the changing part of the prompt is placed before the stable part, so nothing is cacheable
  • Prefix below the threshold — the cacheable section is too short to qualify, so caching silently never engages
  • History replayed in full — every turn resends the whole conversation, so cost grows quadratically
  • Retrieval dumping — large or duplicated passages pasted in rather than an answer-sized context
  • Tool and schema bloat — every call carries a long tool list and verbose schemas the request will never use
  • Unbounded output — no output ceiling and no instruction to be brief
  • One model for everything — the most capable model doing routing and extraction a smaller one handles for less
  • No attribution or ceiling — spend cannot be split per feature or per tenant, and nothing alerts or stops a runaway loop

Scope: Every code path that calls a language model: prompt assembly, system prompts, tool definitions, retrieval, history handling, model and parameter selection, and usage logging. Output quality is out of scope except where a cost change would degrade it. With a ref or diff, start with prompt and model changes since that ref, then measure the whole surface, because cost is a property of the whole call.

Mode: Report + fix by default for code (prompt reordering, cache breakpoints, history and retrieval strategy, output ceilings, usage logging, attribution), re-verifying each by re-running a representative request and comparing measured tokens and cache hits. Every run that exercises the model spends real money, so state the expected cost of a measurement pass first and keep samples small. Model choice, budget thresholds, and anything that could alter output quality are Human follow-ups.

Run these first:

# 1. Every model call site, with its model, parameters, and prompt assembly
grep -rniE "messages\.create|messages\.stream|chat\.completions|generateText|invoke_model|\.predict\(" --include="*.ts" --include="*.py" --include="*.go" . | grep -v node_modules | grep -v test

# 2. Dynamic content that could sit inside a cacheable prefix
grep -rniE "new Date|Date\.now|datetime\.now|uuid|randomUUID|Math\.random|JSON\.stringify" --include="*.ts" --include="*.py" . | grep -v node_modules | grep -iE "system|prompt|instruction|context" 

# 3. Whether usage is logged at all, and what fields are kept
grep -rniE "usage|input_tokens|output_tokens|cache_read|cache_creation|prompt_tokens" --include="*.ts" --include="*.py" . | grep -v node_modules | head -40

# 4. Measured spend per feature over the last 30 days (adapt table and column names)
psql "$DATABASE_URL" -c "SELECT feature, count(*) AS calls, sum(input_tokens) AS tin, sum(output_tokens) AS tout, sum(cache_read_tokens) AS cached FROM llm_calls WHERE created_at >= now() - interval '30 days' GROUP BY 1 ORDER BY 2 DESC;"

# 5. Count tokens for one real request with the provider's own token-counting endpoint rather than a third-party tokenizer, and record the split between the stable prefix and the variable tail

Methodology: Measure before changing anything: tokens, cache hit rate, and cost per call per feature, from logged usage rather than estimates. Then take the free wins in order — caching, prompt hygiene, input size, output ceilings — since they cost no behaviour. Only then the levers that trade something: less history, smaller retrieval payloads, a smaller model, lower reasoning effort. Verify each change by re-running the same request and comparing measured usage. Judge cost per completed task, since a cheaper call that needs two retries is not cheaper.

Measure First

  • Every call logs model, feature, tenant, input and output tokens, cached versus uncached input, latency, and outcome; a feature with no usage rows cannot be audited
  • Cache hit rate per feature comes from the provider's reported cached-input figure — check the field names in its documentation; a rate near zero on a feature with a large stable prefix means an invalidator
  • Cost per call and per active user are computed at current published rates, with the source and date recorded
  • Latency percentiles are read alongside cost, since a large uncached input shows as slow input processing
  • A few representative requests are captured and reused as the before-and-after benchmark

Prompt Structure & Cache Discipline

  • Caching is a prefix match, so the prompt runs stable to volatile: fixed instructions, tool definitions, and long documents first, the changing question last. Any byte that changes inside the prefix invalidates everything after it
  • Nothing dynamic sits inside the intended prefix: no current time, request id, randomly ordered map, or per-user value that could live in the tail
  • Serialisation is deterministic: sorted keys, stable whitespace, fixed tool order, so the same logical prompt produces identical bytes
  • Cache breakpoints sit at the end of each stable block, within the provider's limit on how many are allowed — verify that limit and the minimum cacheable prefix length, which vary by provider and model; a shorter prefix simply will not cache
  • Cache lifetime and any longer-lived option are checked against current provider documentation and compared with traffic: a feature called once an hour may never hit a short-lived cache
  • Caches are scoped to the exact model and parameters, so switching either mid-conversation starts a new one; a cascade across models forfeits reuse, and that trade is made deliberately
  • Mid-conversation operator instructions use whatever mechanism preserves the cached prefix rather than editing the system prompt — verify what the provider offers
  • Cache engagement is proven by two identical consecutive requests and a reading of the cached-input figure, never by inspection

History, Retrieval & Tools

  • History has an explicit strategy — a turn or token ceiling, summarisation, or retrieval instead of replay — stated with the quality trade it makes
  • Where the provider can clear or compact older tool results server-side, that path is used rather than resending them
  • Retrieval returns answer-sized context: bounded chunk count and size, deduplication by content rather than identifier, irrelevant passages filtered out
  • The same document is never sent twice in one request, and a document that is stable across calls lives in the cached prefix rather than the tail
  • Tool definitions are trimmed to what the feature can use, with short descriptions; large tool sets use deferred or searchable loading where the provider supports it
  • Structured output constraints replace long prose instructions describing a format the provider can enforce

Output, Model Choice & Guardrails

  • An output ceiling is set per call, sized to the task, and the prompt asks for the shortest useful answer
  • Reasoning effort, where exposed, is tuned per route rather than globally, with the cheaper setting measured on real requests first
  • Model selection is per task: a smaller model for routing, classification, and bulk work; the capable model where quality is the point. Measure the capable model at lower effort first, since a cascade forfeits cache reuse
  • Latency-tolerant work uses the provider's asynchronous or batch path where one exists, at its published discount — verify the current terms
  • Spend is attributable per feature and tenant from logged usage, so a spike is traceable in minutes
  • Guardrails exist: a budget alert, a per-tenant quota, a retry ceiling, and a documented kill switch
  • A regression check runs in the pre-push hook or CI: a canary request whose input tokens and cache engagement are compared against a recorded baseline, so a prefix-breaking edit fails before it ships

Evidence rules: A finding is Confirmed only with tool-produced evidence — a usage query, a token count from the provider's counting endpoint, a before-and-after measurement of the same request, or a file:line quote plus the traced prompt assembly. Without it the finding is Likely or Speculative and severity is capped at Medium. Provider dashboards you could not access are UNVERIFIED, not findings. A feature that already caches well is a valid outcome; the measured baseline is still the deliverable. Never state a provider's cache minimum, time to live, breakpoint limit, discount, or price from memory — read the current documentation and cite what you read. Defer to the repository's own CLAUDE.md and documented conventions where they conflict with this checklist.

Output Format

Start with a 3–5 line executive summary: features measured, spend for the window, the worst cache hit rate and its cause, the largest saving available, and finding counts by severity.

Per-feature cost table:

Feature Calls Input tokens Cached share Output tokens Cost per call Cost per active user Largest lever

Change log: each proposed change with the measured before and after for the same benchmark request.

Severity Confidence Location Issue Trigger Fix

Detailed findings for Critical and High only: what it costs, the evidence, the fix, the re-measurement. Human follow-ups — model choices, budget thresholds, quality trades. Positive Findings — paths already efficient. Omit any section with nothing to report.

Want this applied to a live stack?

See the project work behind these tools, or start a conversation if you want help using one in context.

Need help applying this to a real product?

These tools come from real delivery work. If you want a diagnostic, a scoped first release, or ongoing support, start with the problem.