Streaming, Semantic Caching, and Agent-to-Agent Traffic
Three AI Gateway capabilities that change what the gateway can enforce, what it costs, and what it can see
Support Status
All three capabilities are available on the Prisma AIRS AI Gateway today. They are grouped into one guide because each one changes an assumption the core deployment guide makes about a request: streaming changes when a guardrail can act, caching changes whether a provider is called at all, and the Agent Gateway changes what sits at the far end of the connection.
| Capability | Status | What it covers |
|---|---|---|
| HTTP streaming | Available | Server-sent events on /chat/completions, /completions, /embeddings, and /messages, with guardrail results delivered inside the stream. |
| Semantic caching | Available | Cosine-similarity matching on /chat/completions and /completions, on top of the exact-match simple cache. |
| Agent Gateway (A2A) | Available | Agent Registry and virtual agent servers for agents speaking the A2A protocol. Shipped as a preview in backend v2.6.1 and now carries full product documentation. |
| Semantic search | Not a gateway feature | The gateway proxies embedding calls and can front retrieval-oriented MCP servers, but it does not provide a search index of its own. |
Semantic caching reuses a previous answer when a new prompt means roughly the same thing. Semantic search retrieves documents by meaning. The gateway does the first and not the second. If what you need is retrieval, the gateway's role is to proxy and govern the embedding calls your retrieval stack makes, and to front an MCP server such as Vectara or Snowflake Cortex that does the searching. Treating the cache as a retrieval layer produces stale answers rather than relevant ones.
Roadmap
One published roadmap statement bears on these three capabilities, and it is on the Agent Registry. Access control today works per agent and per skill. Three finer grains are listed as coming soon and are not available yet:
- Access control for specific users
- Access control for skills inside specific workspaces
- Access control for skills for specific users
Nothing comparable is published for streaming or for caching, so treat both as feature-complete in their current shape rather than as works in progress.
The statements in this guide come from the product documentation and the enterprise changelog as mirrored on 21 September 2026, with the changelog topping out at v2.22.0 dated 11 September 2026. That is reliable for what exists today and thin for anything further out. Re-check the Agent Registry page before quoting the coming-soon list to a customer, because it is the one list here that is designed to change.
Prerequisites
This guide assumes a working gateway. If you do not have one, complete the Core Deployment Guide first, through at least Phase 3, so that one LLM integration is live and you can make a successful call.
Across the three phases you will need:
- A gateway API key that can reach at least one provisioned model
- The ability to create and attach a config, since caching and guardrails are both config-driven
- A terminal with
curl, or a Python or Node environment, for the verification steps
Two phases add requirements of their own:
| Phase | Extra requirement | Applies to |
|---|---|---|
| 2. Semantic caching | An embedding provider credential, and a Milvus or Pinecone instance the gateway can reach | Self-hosted and hybrid data planes, where you set the environment variables yourself |
| 3. Agent Gateway | An A2A agent server published on a public http or https endpoint | Every deployment model |
Phase 1 depends on how a guardrail is configured, not just on whether it exists. If the terms async and deny are unfamiliar, read Phase 5 of the core deployment guide before starting, because the streaming behaviour below only makes sense once those two settings do.
Phase 1 — HTTP Streaming
Streaming returns the response as server-sent events, one chunk at a time, instead of one complete body at the end. Users see the first token in a fraction of the time, which is why most chat-style applications turn it on. The cost is that the gateway gives up the ability to withhold the answer, because by the time it has the whole response the client has it too.
The diagram below is the one thing to take away from this phase. The gateway can stop a streaming request, but only before the first chunk.
Add stream: true to the request body. Nothing else about the call changes, and the gateway key, the model slug, and any config you already use all stay the same.
curl https://aigw.portkey.ai/v1/chat/completions \
-H "Authorization: Bearer $PORTKEY_API_KEY" \
-H "Content-Type: application/json" \
-N \
-d '{
"model": "@openai-prod/gpt-4o",
"stream": true,
"messages": [{"role": "user", "content": "Name three UK cities."}]
}'
The -N flag disables curl's output buffering. Without it the response still streams, but curl holds it and prints everything at once, which makes a working stream look like a non-streaming call.
Verify: you see a run of data: lines appear progressively rather than all together, each carrying a small delta, and the sequence ends with data: [DONE]. If the whole body lands at once, either -N is missing or stream: true did not reach the provider.
Streaming with guardrail results is supported on /chat/completions, /completions, /embeddings, and /messages. The Anthropic-style /messages route behaves the same way, so an application that moved from Anthropic direct to the gateway keeps its streaming code.
On a streaming route, an input guardrail is the only guardrail that can stop anything. It runs before the stream opens, while the gateway still owns the entire response. Two settings decide whether it blocks:
- async = false — the check runs before the request goes to the provider rather than alongside it. Left at the default of true, the check only logs.
- deny = true — a failed check kills the request with
446. Left at the default of false, a failed check returns246and the request proceeds anyway.
Attach the guardrail through the config's input_guardrails list, by ID:
{
"input_guardrails": ["pii-and-prompt-injection-guardrail"],
"output_guardrails": ["response-policy-guardrail"]
}
Send a prompt that the guardrail should reject, with stream: true set.
Verify: the call returns HTTP 446 and no data: chunks at all. A 246 means the guardrail ran and failed but deny is off. A 200 with a full stream means the guardrail either is not attached to this config or is still asynchronous.
Output guardrails on streaming requests are evaluated on the assembled response after the stream has finished, and no action is taken on the result. Fallback does not fire and retry does not fire. The verdict is recorded and returned, which is useful for alerting and for tuning the policy, but the user has already read the text. Any policy whose job is to prevent a user seeing something has to be enforced on the input, or the route has to stay non-streaming.
By default the gateway holds itself to strict OpenAI response compliance, and a hook_results chunk is not part of the OpenAI schema. The verdicts are therefore suppressed. Set the header to false to receive them.
curl https://aigw.portkey.ai/v1/chat/completions \
-H "Authorization: Bearer $PORTKEY_API_KEY" \
-H "Content-Type: application/json" \
-H "x-portkey-strict-open-ai-compliance: false" \
-N \
-d '{
"model": "@openai-prod/gpt-4o",
"stream": true,
"messages": [{"role": "user", "content": "Summarise the onboarding policy."}]
}'
Input guardrail verdicts arrive under before_request_hooks before the content starts. Output guardrail verdicts arrive under after_request_hooks in an extra chunk after [DONE].
In the OpenAI Python client the results sit on the chunk's extra fields, because the SDK model does not know about them:
async for chunk in response:
extra = chunk.model_extra or {}
hooks = extra.get("hook_results")
if hooks:
if hooks.get("before_request_hooks"):
print("input guardrails:", hooks["before_request_hooks"])
if hooks.get("after_request_hooks"):
print("output guardrails (informational):", hooks["after_request_hooks"])
print(chunk.choices, end="", flush=True)
Verify: at least one chunk carries a hook_results object. If every chunk is a plain content delta, the header did not reach the gateway, since the default of true silently drops these chunks rather than erroring.
Setting this header to false means the gateway emits a chunk shape the OpenAI SDKs do not model. The Python and Node clients tolerate it and park the field in an extras bag, which is what the code above reads. A client that rejects unknown fields outright will need the header left at its default, and in that case the guardrail verdicts have to be read from the logs instead of from the stream.
Providers omit usage data from streaming responses unless asked for it. No token counts means no cost calculation, so every streamed request records as zero spend. Budgets built on that figure never trip, and chargeback reports under-count whichever teams stream the most.
Ask for usage explicitly:
{
"model": "@openai-prod/gpt-4o",
"stream": true,
"stream_options": { "include_usage": true },
"messages": [{"role": "user", "content": "Name three UK cities."}]
}
This applies to every OpenAI-compatible provider, Azure OpenAI included. Non-streaming requests return usage automatically and need no change.
Verify: open the log entry for the call and confirm the token counts and cost are non-zero. Then compare against a streamed call made without stream_options, which should show zero, to confirm you are looking at the right lever.
This is a client-side setting, so turning it on centrally is not possible. Any application already streaming without stream_options has been recording zero cost for as long as it has been running, and its historical figures cannot be recovered. Before presenting AI spend to a customer, check which of their applications stream and whether each one sends this option.
A request timeout on a streaming route does not mean what it means elsewhere. The timeout will not be triggered if the gateway receives at least one chunk within the configured duration. A provider that returns the first token quickly and then stalls for several minutes will not produce a 408, and the client will sit waiting.
Two things follow for an application that streams:
- Client-side idle timeout — the application needs its own timer measuring the gap between chunks, because the gateway's timeout only guards the gap before the first one.
- Timeout value — the configured duration is effectively a time-to-first-chunk budget on these routes, so set it against that figure rather than against a full-response target.
Verify: this one is best confirmed by reading the config rather than by inducing a provider stall. Check that any route carrying stream: true has a client-side idle timeout, and that the gateway timeout on that route is sized for first-chunk latency.
Phase 2 — Semantic Caching
A cache hit returns a stored answer without calling the provider, which removes both the latency and the cost of that call. Simple caching requires the new request to be byte-identical to a stored one. Semantic caching relaxes that to requests that mean the same thing, which is what makes it useful for anything a human types.
The two modes are not alternatives. Semantic cache is a superset: the gateway always tries the exact match first and only embeds the prompt when that misses. Every gate on the path below is a reason a hit you expected did not happen.
Semantic mode adds an embedding call and a vector search to every request that misses the exact-match check, and on a self-hosted gateway it adds two components to run. Work out whether the traffic will repay that before building it.
| Traffic shape | Mode | Why |
|---|---|---|
| Machine-generated prompts from a template | Simple | Identical inputs recur naturally, so the exact match already catches them and the embedding call would be pure overhead. |
| Human-typed questions on a narrow topic | Semantic | The same question arrives worded ten ways. This is the case semantic mode exists for. |
| Long documents or multi-turn conversations | Neither | Requests over 8,191 tokens or carrying more than four messages are not eligible for semantic matching, and long bodies rarely repeat exactly. |
| Anything where a stale answer is a problem | Neither | A cache returns a previous answer. If freshness matters more than cost, keep the provider in the path. |
Verify: before enabling anything, turn on simple mode and read the hit rate on the Analytics cache tab after a day of real traffic. If simple caching is already hitting well, semantic mode has little left to add. If it hits almost never and the prompts are human-written, that is the signal to continue.
Skip this step and the next on a SaaS data plane, where the components are managed for you. On a self-hosted or hybrid gateway, set these in the gateway's environment:
SEMANTIC_CACHE_EMBEDDING_PROVIDER=openai
SEMANTIC_CACHE_EMBEDDINGS_URL=https://api.openai.com/v1/embeddings
SEMANTIC_CACHE_EMBEDDING_MODEL=text-embedding-3-small
SEMANTIC_CACHE_EMBEDDING_API_KEY=<openai-api-key>
SEMANTIC_CACHE_SIMILARITY_THRESHOLD=0.95
SEMANTIC_CACHE_EMBEDDING_DIMENSIONS=1536
The provider accepts openai, azure-openai, google for Gemini embeddings, or vertex-ai. Set the URL, model, and dimensions to match whichever you pick. For Azure OpenAI:
SEMANTIC_CACHE_EMBEDDING_PROVIDER=azure-openai
SEMANTIC_CACHE_EMBEDDINGS_URL=https://YOUR-RESOURCE.openai.azure.com/openai/v1/embeddings
SEMANTIC_CACHE_EMBEDDING_MODEL=text-embedding-3-small
SEMANTIC_CACHE_EMBEDDING_API_KEY=<azure-openai-api-key>
SEMANTIC_CACHE_SIMILARITY_THRESHOLD=0.95
SEMANTIC_CACHE_EMBEDDING_DIMENSIONS=1536
Verify: restart the gateway and confirm it starts without an embedding configuration error in the logs. The variables are not exercised until a semantic lookup runs, so a clean start is only a partial check. The real confirmation comes in Step 2.5.
Managed identity and Entra ID token authentication are not supported for the semantic cache embedding call, even where the same gateway uses them for inference against the same resource. SEMANTIC_CACHE_EMBEDDING_MODEL must also match the Azure deployment name rather than the underlying model name. Both of these differ from how the rest of the gateway talks to Azure OpenAI, and see LLM API Key Management for where that key should live.
The gateway supports Milvus and Pinecone. Four variables point it at whichever you run:
VECTOR_STORE=milvus
VECTOR_STORE_ADDRESS=<your-vector-store-address>
VECTOR_STORE_COLLECTION_NAME=<your-collection-name>
VECTOR_STORE_API_KEY=<your-vector-db-api-key>
Each store has its own setup conditions, and they are not interchangeable.
| Store | What it needs |
|---|---|
| Milvus | A collection whose name matches SEMANTIC_CACHE_EMBEDDING_MODEL exactly, with fields id (Varchar), values (FloatVector at the configured dimension), and metadata (JSON). |
| Pinecone | Omit VECTOR_STORE_COLLECTION_NAME entirely. Set VECTOR_STORE_ADDRESS to the index name rather than a host string, and create the index with cosine as its similarity metric. |
Verify: confirm the vector dimension configured on the store matches SEMANTIC_CACHE_EMBEDDING_DIMENSIONS. A mismatch here produces no lookups and no obvious error, just a cache that never hits.
The collection is named after the embedding model and sized to its dimension. Switching models means creating a new collection and updating both the model name and the dimension variable together. Changing one without the other leaves the gateway writing vectors the store will not accept.
Caching is a config property, not a header. Add a cache block:
{ "cache": { "mode": "semantic", "max_age": 3600 } }
In a config with routing targets, the block can sit at the top level so it applies to all of them, or on an individual target. A target-level block takes precedence over a top-level one, which lets an expensive model cache aggressively while a cheap one does not:
{
"strategy": { "mode": "fallback" },
"targets": [
{
"override_params": { "model": "@openai-prod/gpt-4o" },
"cache": { "mode": "semantic", "max_age": 3600 }
},
{
"override_params": { "model": "@anthropic-prod/claude-3-5-sonnet-20241022" },
"cache": { "mode": "simple", "max_age": 200 }
}
]
}
Verify: send any request through the config and check the log line. The status column should read Cache Miss rather than Cache Disabled. A first request can never hit, so a miss here is the correct result and confirms the cache is active.
Caching does not work on any request carrying x-portkey-debug: "false". If a test client sets that header and the status column reads Cache Disabled while the config clearly has a cache block, this is the reason.
Send one request, then send a reworded version of the same question. Everything other than the user text has to stay identical, including the model and every body parameter.
# First call — populates the cache
curl https://aigw.portkey.ai/v1/chat/completions \
-H "Authorization: Bearer $PORTKEY_API_KEY" \
-H "x-portkey-config: pc-cache-xxx" \
-H "Content-Type: application/json" \
-d '{"model": "@openai-prod/gpt-4o",
"messages": [{"role": "user", "content": "Who is the US president?"}]}'
# Second call — different wording, same meaning
curl https://aigw.portkey.ai/v1/chat/completions \
-H "Authorization: Bearer $PORTKEY_API_KEY" \
-H "x-portkey-config: pc-cache-xxx" \
-H "Content-Type: application/json" \
-d '{"model": "@openai-prod/gpt-4o",
"messages": [{"role": "user", "content": "Tell me who the president of the US is."}]}'
Verify: the second call returns noticeably faster, returns the same text as the first, and its log line reads Cache Semantic Hit. A status of Cache Hit on the second call means the two requests were byte-identical and you tested the simple cache by accident, so change more of the wording and try again.
If the second call reads Cache Miss, work down the gates in the order the diagram shows them:
- Mode — is the config's cache block set to
semanticrather thansimple? - Route — semantic matching only runs on
/chat/completionsand/completions. - Size — under 8,191 tokens and four messages or fewer.
- User message — at least one is required. A request carrying only system messages is dropped rather than matched.
- Exact-match fields — the model and every other body parameter, and the metadata header if you use one, still have to match exactly. Only the user text is matched by meaning.
- Threshold — two phrasings may simply not be similar enough for 0.95. Lower
SEMANTIC_CACHE_SIMILARITY_THRESHOLDdeliberately and in small steps, because a loose threshold returns confidently wrong answers.
Semantic matching looks at the user text and disregards the system prompt entirely. Changing the system prompt neither causes nor prevents a hit. Where several applications share a config and distinguish themselves only by their system prompt, they will serve each other's cached answers. Separate them with a cache namespace, covered in the next step.
Two controls decide how long an entry survives and who can be served it.
Lifetime comes from max_age in seconds on the cache block. The minimum is 60 seconds, the maximum is 90 days, and leaving it out gives 7 days. An organisation-level default set under Admin Settings, Organisation Properties, Cache Settings changes the arithmetic:
| Request | Result |
|---|---|
No max_age sent | The organisation default applies. |
max_age longer than the organisation default | The organisation default wins. |
max_age shorter than the organisation default | The request value is honoured. |
A per-request value can therefore only ever shorten the lifetime, which is what makes the organisation default a usable data-retention control rather than a suggestion.
Partitioning defaults to every request header, so two callers differing in any header never share an entry. That is safe and it is usually why a hit rate looks lower than expected. Narrow it to one string of your choosing:
curl https://aigw.portkey.ai/v1/chat/completions \
-H "Authorization: Bearer $PORTKEY_API_KEY" \
-H "x-portkey-config: pc-cache-xxx" \
-H "x-portkey-cache-namespace: tenant-42" \
-H "Content-Type: application/json" \
-d '{"model": "@openai-prod/gpt-4o",
"messages": [{"role": "user", "content": "Hello!"}]}'
Verify: send the same prompt under two different namespace values and confirm the second one misses. Then repeat it within one namespace and confirm it hits. That pair of results proves the partition is real rather than assumed.
Everything sharing a namespace can be served everything else's cached answers. Where a gateway serves several customers, or several classification levels, the namespace has to carry the tenant identifier, and the value has to come from the authenticated identity rather than from a field the caller can set freely. A namespace chosen to raise the hit rate, without that constraint, is how one customer's answer reaches another.
A cached answer that has gone stale needs replacing before its TTL expires. Send the force-refresh header, which is a per-request header rather than a config property:
curl https://aigw.portkey.ai/v1/chat/completions \
-H "Authorization: Bearer $PORTKEY_API_KEY" \
-H "x-portkey-config: pc-cache-xxx" \
-H "x-portkey-cache-force-refresh: true" \
-H "Content-Type: application/json" \
-d '{"model": "@openai-prod/gpt-4o",
"messages": [{"role": "user", "content": "Who is the US president?"}]}'
The request still needs its cache config attached. Without one the header does nothing, because there is no cache to refresh.
Verify: the log line reads Cache Refreshed, and a subsequent ordinary request returns the new text rather than the old.
Where the request matches semantically, the refresh replaces all matching entries rather than the single closest one. That is the behaviour you want when a stored answer is wrong, since the wrong answer was reachable from many phrasings. It also means a force refresh is a heavier operation than it looks, so it belongs in a corrective workflow rather than on a retry path.
Phase 3 — Agent Gateway for A2A Traffic
The first two phases govern calls to a model. This one governs calls to an agent. An agent speaking the A2A protocol is reached over HTTP like any other service, publishes a card describing its skills, and until now has authenticated its callers itself. The Agent Gateway puts the gateway in the middle of that conversation.
Two components do the work. The Agent Registry holds the organisation's agents and decides who may reach which agent and which of its skills. Agent Servers are virtual servers: to your client each one looks like the agent, and to the real agent it looks like the client.
Agent access is off by default on every key. Two separate things have to be set, and missing either one produces an authorization failure rather than a useful message.
- Open the API Keys page in Strata Cloud Manager and create or edit a key.
- Turn on the Agent Gateway toggle in the key's permissions.
- Enable the
agents.invokescope on any key that will actually call an agent, as opposed to one used only for administration.
Verify: reopen the key and confirm both the toggle and the scope are set. Record which key you used, because the registry's access decisions in Step 3.3 are made against it.
In the Agent Registry, add the upstream agent server URL. This is the address of the real agent, and it is also where the gateway will fetch the agent card and authenticate.
If you are testing before a production agent exists, two public A2A agents are published for the purpose:
| Test agent | URL | Authentication |
|---|---|---|
| Hello world | https://hello.a2aregistry.org | None |
| Tools with header auth | https://a2a-header-auth.ayush-0a3.workers.dev | Authorization: Bearer secret-key-1 |
Press Fetch Agent Card. The gateway reads /.well-known/agent.json from the URL you gave it and validates the transport and authentication schemes the agent declares.
Verify: the card comes back and lists the skills you expect. This is the step that proves the gateway can reach and authenticate to the agent, so do not move on from a failure here. Everything downstream assumes this call works.
Backend v1.16.1 and later apply SSRF protections to the registered URL. The gateway rejects private IP addresses, cloud metadata hosts, and any URL carrying a # fragment. An agent running inside your own network cannot be registered by its internal address: it has to be published on a reachable public http or https endpoint first. Raise this during design, because it is a networking change rather than a gateway setting, and it has blocked more than one proof of concept at this exact step.
Choose which workspaces the agent is available in, then narrow what those workspaces can call. Access control operates at two grains today:
- The agent — whether a caller may reach it at all.
- The skill — which of the capabilities the agent publishes a caller may invoke.
Scope to the skills a client was built for rather than to the agent's whole surface. An agent that can both read a record and delete one should not expose deletion to a client that only ever reads, and the skill grain is what makes that distinction enforceable at the gateway instead of inside the agent.
Verify: from a client holding a key scoped to one skill, attempt a skill you did not grant. The call should be refused. A refusal here is the control working, and it is worth capturing as evidence rather than treating as a failed test.
Per-user access control, skill control inside specific workspaces, and skill control for specific users are all published as coming soon and are not available. Until they arrive the workspace is the smallest unit an agent can be scoped to. Where two groups of users need different agent access, that difference has to be expressed as two workspaces today.
Open the Agents tab and copy the virtual server URL for the agent. It follows the agent's slug:
https://aigw.portkey.ai/agent/{agent-slug}
Replace the agent server URL in your client with that one, and send the gateway API key as the bearer token. No other client change is needed, because the virtual server speaks A2A to your client exactly as the real agent did.
Confirm the swap worked by fetching the card through the gateway:
curl --location --request POST \
'https://aigw.portkey.ai/agent/{agent-slug}/.well-known/agent.json' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer {PORTKEY_API_KEY}'
In Python, the A2A SDK's card resolver needs nothing beyond the new base URL and the gateway key:
import asyncio, json
import httpx
from a2a.client import A2ACardResolver
BASE_URL = "https://aigw.portkey.ai/agent/{agent-slug}"
API_KEY = "{PORTKEY_API_KEY}"
async def main():
async with httpx.AsyncClient(
base_url=BASE_URL,
headers={"Authorization": f"Bearer {API_KEY}"},
) as client:
resolver = A2ACardResolver(httpx_client=client, base_url=BASE_URL)
card = await resolver.get_agent_card()
print(json.dumps(card.model_dump(mode="json", exclude_none=True), indent=2))
asyncio.run(main())
Verify: the card returned through the gateway matches the one the registry fetched in Step 3.2, and the skills listed are the ones this key is scoped to. Then run a real task against the agent and confirm it completes as it did before the swap.
On a self-hosted or hybrid data plane, replace aigw.portkey.ai/agent with your own gateway address. The slug and the path after it are unchanged.
Logging is the reason most organisations put a gateway in front of their agents in the first place. Every request to a virtual server is recorded, and those records are what the observability and governance features are built on.
Verify: open the Logs view and find the calls you made in Step 3.4. Confirm the agent and the invoking key are both identifiable on the log line. Then make a call that the registry should refuse, from Step 3.3, and confirm it appears too. A refused call that leaves no record is worse than no control at all, so check both.
The gateway only sees traffic sent to the virtual server. Any client still holding the real agent's address and credential continues to reach it directly, unlogged and unscoped. Completing this phase means also withdrawing that direct access at the agent, otherwise the registry describes a policy the network does not enforce.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The whole streamed response arrives at once | Client-side buffering, not a gateway problem | Add -N to curl, or disable buffering in the client. |
No hook_results in any chunk | x-portkey-strict-open-ai-compliance left at its default of true | Set it to false on the request. |
A guardrail that should block returns 246 | Deny is off, so the verdict is advisory | Set deny to true on the guardrail. |
A guardrail that should block returns 200 | The guardrail is asynchronous, or is not attached to this config | Set async to false, and confirm the ID is in input_guardrails. |
| Streaming requests record zero cost | The provider omits usage data unless asked | Send stream_options: {"include_usage": true}. |
| A stalled stream never times out | The timeout does not fire once one chunk has arrived | Add a client-side idle timeout between chunks. |
Log status reads Cache Disabled | No cache block on the config, or x-portkey-debug: "false" on the request | Add the cache block, and remove the debug header. |
| Semantic cache never hits | Ineligible request, a non-matching body parameter, or a vector dimension mismatch | Work down the gates in Step 2.5 in order. |
| Two applications serve each other's answers | The system prompt is ignored by semantic matching | Separate them with x-portkey-cache-namespace. |
| Fetch Agent Card fails on a valid agent | SSRF protections reject private addresses, metadata hosts, and # fragments | Publish the agent on a public endpoint and re-register it. |
| Agent call returns an authorization error | The Agent Gateway toggle or the agents.invoke scope is missing from the key | Set both on the key, as in Step 3.1. |
Reference
| Header | Default | Effect |
|---|---|---|
x-portkey-strict-open-ai-compliance | true | At false, guardrail hook_results chunks are emitted inside the stream. |
x-portkey-cache-namespace | Unset | Partitions the cache by this string instead of by every request header. |
x-portkey-cache-force-refresh | false | Bypasses the cache and replaces the stored entry. Requires a cache config on the request. |
x-portkey-debug | true | At false, caching is switched off for the request. |
x-portkey-metadata | Unset | Must match exactly for a cache hit, in both simple and semantic mode. |
| Variable | Accepted values |
|---|---|
SEMANTIC_CACHE_EMBEDDING_PROVIDER | openai, azure-openai, google, vertex-ai |
SEMANTIC_CACHE_EMBEDDINGS_URL | The provider's embeddings endpoint |
SEMANTIC_CACHE_EMBEDDING_MODEL | Model name, or the deployment name on Azure OpenAI |
SEMANTIC_CACHE_EMBEDDING_API_KEY | A plain API key. Token-based auth is not supported here. |
SEMANTIC_CACHE_EMBEDDING_DIMENSIONS | Must match the vector store's configured dimension |
SEMANTIC_CACHE_SIMILARITY_THRESHOLD | Cosine similarity floor for a hit. Defaults to 0.95. |
VECTOR_STORE | milvus or pinecone |
VECTOR_STORE_ADDRESS | Store address, or the index name on Pinecone |
VECTOR_STORE_COLLECTION_NAME | Milvus only. Must match the embedding model name. |
VECTOR_STORE_API_KEY | Vector store credential |
| Limit | Value |
|---|---|
| Semantic cache request size | Under 8,191 tokens |
| Semantic cache message count | Four or fewer, with at least one user message |
| Semantic cache routes | /chat/completions and /completions |
| Cache TTL | 60 seconds minimum, 90 days maximum, 7 days by default |
| Organisation cache TTL ceiling | 25,923,000 seconds |
| Streaming routes with guardrail results | /chat/completions, /completions, /embeddings, /messages |
| Status value | Meaning |
|---|---|
Cache Hit | Exact match served from the cache. |
Cache Semantic Hit | A differently worded prompt matched by meaning. |
Cache Miss | No match. The provider was called and the response stored. |
Cache Refreshed | The force-refresh header replaced the stored entry. |
Cache Disabled | No cache block on the config, or caching suppressed by a header. |
246 | A guardrail failed, and the request was processed anyway. |
446 | A guardrail failed with deny set, and the request was stopped. |
Sources
Everything in this guide is drawn from the Prisma AIRS AI Gateway product documentation as mirrored on 21 September 2026. Nothing here is inferred from behaviour observed in a tenant, so each claim can be checked against the page it came from.
| Phase | Source page | Last modified |
|---|---|---|
| 1. Streaming | Guardrails, under Streaming Responses | Carries the input and output asymmetry, the compliance header, and the four supported routes. |
| 1. Streaming | Request timeouts, under Caveats and Considerations | Carries the first-chunk timeout behaviour. |
| 1. Streaming | Cost management, troubleshooting section | Carries the zero-cost behaviour and stream_options. |
| 2. Caching | Cache (Simple and Semantic) | 18 September 2026. Carries the mode table, hit and miss rules, environment variables, TTL, namespace, and force refresh. |
| 3. Agent Gateway | Agent Gateway, Registry, Servers, and Quickstart | 15 September 2026. Carries the two components, the scopes, the SSRF constraints, the virtual URL, and the coming-soon list. |
The coming-soon list on the Agent Registry is designed to change, so confirm it is still accurate before quoting it. The similarity threshold default of 0.95 is a documented default rather than a measured one, so treat any hit-rate projection built on it as an estimate until it is tested against the customer's own traffic.