What you'll learn: how to check a model's reply before it leaves your service, and how to cache replies without leaking one tenant's answer to another.
At a glance
Figure 1. The reply path with a cache and a guardrail
How to read this: nothing reaches the cache or the client until the guardrail has checked it, and the cache key decides who may be served a stored reply.
- An output guardrail checks the reply before the client sees it: schema, policy, redaction.
- It is a filter with known gaps, not a guarantee. Say which gaps.
- A cache key holds everything the answer depends on: tenant, role, prompt version, question.
Scenario
Composite scenario, not a real company. Lumen Industrial Supply, a fictional distributor, runs its product copilot for two tenants, acme and globex. Before a pilot the team worries that a reply might carry an email address or name a partner-only product to a public caller, and the same question arrives many times a day. The FDE adds an output guardrail and a response cache. All numbers are illustrative.
What you will be able to do
- Write a guardrail and list what it cannot catch.
- Build a tenant-safe cache key and prove it with a test.
- Choose between three kinds of cache.
Prerequisites
- Structured output and validation and Permissions the application enforces.
- Theory: Security and threat modeling, Cost and economics and Caching basics.
Core ideas
The reply path
Figure 2. One request: a miss, then a hit
How to read this: the cache holds only checked replies, so a hit never skips the guardrail.
An output guardrail: schema, policy, redaction
OWASP names Improper Output Handling as insufficient validation of model output before it passes downstream, and advises treating the model like any other user. Three checks:
| Check | Action | Lumen example | Cannot catch |
|---|---|---|---|
| Schema | block | reply is not JSON or lacks citations |
a well-formed reply that is wrong |
| Policy | block | a SKU the caller may not see; a claim with no citation; a blocked term | a wrong claim that cites a real chunk |
| Redaction | rewrite | an email address becomes [email removed] |
a phone number, a name, "jo at example dot com" |
Verified: capstone/fde-product-discovery-copilot/lessons/M4-L9/m4_l9_guardrails_cache.py
def check_reply(raw: str, allowed_skus: set[str], blocked_terms: tuple[str, ...] = ()) -> GuardResult:
"""Schema, then policy (block), then redaction (rewrite). The caller never sees `raw` on a block."""
try:
draft = parse_json_model(Draft, raw)
except (ValueError, ValidationError):
return GuardResult("block", REFUSAL_MESSAGE, ["schema"])
reasons: list[str] = []
for item in draft.recommendations:
if item.sku not in allowed_skus:
reasons.append(f"sku not allowed: {item.sku}")
if not item.citations:
reasons.append(f"no citation: {item.sku}")
if any(term in item.why.lower() for term in blocked_terms):
reasons.append(f"blocked term: {item.sku}")
if reasons:
return GuardResult("block", REFUSAL_MESSAGE, reasons)
cleaned = EMAIL.sub(REDACTED, raw)
return GuardResult("rewrite" if cleaned != raw else "allow", cleaned, ["email"] if cleaned != raw else [])
allowed_skus must come from the verified identity, never from the request.
The guardrail checks what a reply contains, not whether it is true. That is the job of citations and evaluation.
Three kinds of cache
The semantic cache is design reasoning, not a provider feature.
| Provider prompt caching | Your response cache | Semantic cache | |
|---|---|---|---|
| What is stored | the start of the prompt | the final reply | a reply found by similarity |
| Who controls it | you opt in, the provider holds it | you | you, plus a threshold |
| Hit needs | an identical prompt start | the same key | a close enough question |
| Main risk | a short prefix gives no saving | a leaking key, a stale answer | two different questions match |
| Model called on a hit | yes | no | no |
As of 2026-11-03 the Anthropic page says:
- the cache covers
tools,systemandmessages, in that order; - a hit needs 100 percent identical prompt segments;
- the default lifetime is 5 minutes;
- a prompt below a model-specific minimum length is processed without caching, and no error is returned;
- caching has no effect on output generation;
- different organizations never share caches, and on the Claude API caches are also isolated by workspace.
Tenants served from your one workspace share its cache, so the last line does not separate them.
A response cache that cannot leak
HTTP caches face the same problem. RFC 9111 keys a cache on at least the method and target URI, uses Vary to name the request headers that changed the response, and bars a shared cache from reusing an Authorization response unless allowed. Our key names what changed the answer.
| Key part | Without it |
|---|---|
| tenant | acme is served globex's answer |
| role | a public caller gets a partner-only answer |
| prompt version | old-prompt answers after a release |
| normalised question | spacing and case differences miss the cache |
Verified: capstone/fde-product-discovery-copilot/lessons/M4-L9/m4_l9_guardrails_cache.py