-
Measure first — no optimization without a baseline. Log per request: input_tokens, output_tokens, model id, end-to-end latency, and time-to-first-token (TTFT). Get exact counts from the provider's usage object or a count-tokens endpoint — never estimate from len(text)/4 for billing decisions. Compute $/req from current per-token prices and aggregate p50/p95 (means hide the tail that users actually feel). You cannot claim a win without these numbers before and after.
u = resp.usage
cost = (u.input_tokens*IN + u.output_tokens*OUT
+ u.cache_creation_input_tokens*CACHE_WRITE
+ u.cache_read_input_tokens*CACHE_READ) / 1e6
-
Apply levers in ROI order — cheapest, highest-impact first. Do not jump to a fancy router before you've capped output and cached the prefix.
| Lever | Typical win | Effort | Use when |
|---|
Cap max_tokens + stop sequences | 10–40% output cost | trivial | output runs longer than needed |
| Context diet (trim history, drop dead few-shot) | 20–60% input cost | low | long/growing prompts |
| Prompt caching (cache stable prefix) | up to 90% on the cached portion, lower TTFT | low | long fixed system prompt / RAG docs reused across calls |
| Streaming | TTFT 5–10x better (perceived) | low | user-facing chat/UI |
| Model tiering/routing | 50–95% on easy traffic | medium | mixed easy/hard requests |
| Semantic cache | ~100% on a cache hit | medium | repeated/near-duplicate queries |
| Batch API | ~50% on $ | medium | offline, non-urgent jobs |
-
Context diet — shrink input before you optimize how you send it. Every input token is paid on every call. (a) Cap history: keep the last N turns; once over a token budget, summarize older turns into a compact running summary instead of carrying raw transcript. (b) Cut dead few-shot examples: drop each example and re-run the eval — keep only those that move accuracy. Most prompts carry 3–4 examples that earn nothing. (c) Strip boilerplate, redundant instructions, and verbose tool schemas. Re-measure tokens after each cut.
-
Prompt caching — usually the single biggest win. Put the stable, large content first (system prompt, tool definitions, RAG documents, long instructions) and the variable part (the user's actual turn) last, then mark the boundary with the provider's cache control so the prefix is reused across requests. Order matters: the cache keys on an exact prefix match, so one moving token near the top busts the whole cache.
system=[{"type":"text","text":BIG_STABLE_PROMPT,"cache_control":{"type":"ephemeral"}}]
Cache reads are ~10% of input price; writes ~25% more than input — so caching pays off once a prefix is reused even a handful of times within its TTL (~5 min ephemeral). Verify hits via cache_read_input_tokens > 0.
-
Model tiering + routing — send easy work to the cheap model. Default the bulk of traffic to a small/fast model; escalate only when needed. Pick a deterministic router over an LLM-classifier router when you can (no extra call, no extra latency): route on cheap signals — input length, required output schema, presence of code/math, retrieval confidence, or an explicit difficulty flag. Use a tiny classifier model only when rules can't separate easy from hard. Always escalate on low-confidence or a validation failure rather than returning a bad cheap answer.
def route(req):
if req.tokens < 800 and not req.needs_reasoning: return SMALL
if req.needs_long_reasoning or req.high_stakes: return LARGE
return MID
-
Semantic cache for repeated/near-duplicate queries. Embed the normalized query, look it up in a vector store; on cosine similarity ≥ ~0.95 return the cached answer (0 LLM tokens). Set a conservative threshold (too low → wrong cached answers), scope the key by anything that changes the answer (user/tenant/locale/tool-version), and TTL it so stale facts expire. Bypass the cache for personalized or time-sensitive responses. This is exact/near-exact answer reuse, layered on top of prompt caching (which reuses the prefix, not the answer).
-
Batch the offline work. Anything not blocking a user — nightly classification, backfills, evals, bulk summarization — goes through the provider Batch API (Anthropic Message Batches / OpenAI Batch) for ~50% off, accepting up to ~24h turnaround. Never batch interactive requests.
-
Stream to cut perceived latency. For any user-facing surface, stream tokens (stream=True / SSE) so first text shows in a few hundred ms instead of after the full generation. This doesn't reduce cost or total wall-clock, but it collapses TTFT — often the only latency users perceive. Combine with caching: a cached prefix also lowers real TTFT.
-
Re-measure and prove equal quality. Re-run the same metrics (step 1) after changes, and run your eval set to confirm output quality didn't regress (see Verify). A cost win that drops accuracy is not a win — report cost/latency and the quality delta together.
Done = a before/after table shows lower $/req and lower p50/p95 (with cache hit rate and cheap-model share stated), and the eval confirms output quality is unchanged at the new, cheaper, faster configuration.