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.

CapabilityStatusWhat it covers
HTTP streamingAvailableServer-sent events on /chat/completions, /completions, /embeddings, and /messages, with guardrail results delivered inside the stream.
Semantic cachingAvailableCosine-similarity matching on /chat/completions and /completions, on top of the exact-match simple cache.
Agent Gateway (A2A)AvailableAgent 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 searchNot a gateway featureThe gateway proxies embedding calls and can front retrieval-oriented MCP servers, but it does not provide a search index of its own.
Semantic caching and semantic search are different things

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.

How current this is

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:

PhaseExtra requirementApplies to
2. Semantic cachingAn embedding provider credential, and a Milvus or Pinecone instance the gateway can reachSelf-hosted and hybrid data planes, where you set the environment variables yourself
3. Agent GatewayAn A2A agent server published on a public http or https endpointEvery deployment model
Guardrail behaviour carries over from the core guide

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.

A six-step left-to-right timeline of one streaming request: the client opens the stream, input guardrails run and can block with a 446, the gateway forwards to the provider, chunks relay through to the client past a marked point of no return, the stream ends with a DONE chunk, and output guardrails run on the assembled response and only report. Two lanes underneath mark where a request can be blocked and where a verdict can only be recorded, with panels listing the headers to set and the pitfalls to expect.
▶ Step 1.1 — Make a streaming call and confirm chunks arrive

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.

Which routes accept it

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.

▶ Step 1.2 — Put the enforcing guardrail on the input side

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 returns 246 and 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.

An output guardrail on a streaming route is a detection, not a control

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.

▶ Step 1.3 — Expose the guardrail verdicts inside the stream

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.

Clients that validate responses strictly

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.

▶ Step 1.4 — Restore cost accounting on streamed calls

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.

Audit existing applications before trusting a spend report

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.

▶ Step 1.5 — Re-check timeout behaviour

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.

A top-to-bottom decision flow for a cached lookup: is a cache block set in the config, then an exact-match simple cache lookup, then whether the mode is semantic, then an eligibility check on route, token count, message count and the presence of a user message, then an embedding and vector search against a similarity threshold. Outcomes branch to the right as Cache Disabled, Cache Hit, Cache Semantic Hit and Cache Miss. A side panel lists the embedding provider, vector store and Redis a self-hosted gateway needs, with the environment variables for each, and a strip at the bottom covers TTL, partitioning and log status values.
▶ Step 2.1 — Decide whether semantic mode is worth it

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 shapeModeWhy
Machine-generated prompts from a templateSimpleIdentical inputs recur naturally, so the exact match already catches them and the embedding call would be pure overhead.
Human-typed questions on a narrow topicSemanticThe same question arrives worded ten ways. This is the case semantic mode exists for.
Long documents or multi-turn conversationsNeitherRequests 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 problemNeitherA 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.

▶ Step 2.2 — Configure the embedding provider (self-hosted)

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.

Azure OpenAI needs a plain API key for this call

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.

▶ Step 2.3 — Connect the vector store (self-hosted)

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.

StoreWhat it needs
MilvusA collection whose name matches SEMANTIC_CACHE_EMBEDDING_MODEL exactly, with fields id (Varchar), values (FloatVector at the configured dimension), and metadata (JSON).
PineconeOmit 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.

Changing the embedding model is a schema change

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.

▶ Step 2.4 — Turn caching on in the config

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.

A debug header switches caching off

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.

▶ Step 2.5 — Prove a semantic hit

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 semantic rather than simple?
  • Route — semantic matching only runs on /chat/completions and /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_THRESHOLD deliberately and in small steps, because a loose threshold returns confidently wrong answers.
The system prompt is ignored

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.

▶ Step 2.6 — Set lifetime and partitioning

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:

RequestResult
No max_age sentThe organisation default applies.
max_age longer than the organisation defaultThe organisation default wins.
max_age shorter than the organisation defaultThe 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.

A namespace is a tenancy boundary

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.

▶ Step 2.7 — Force a refresh when the answer is wrong

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.

One refresh clears every semantic match

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.

Two stacked lanes comparing the same A2A call. The top lane shows an agent client calling an upstream A2A agent server directly, with a note that each client authenticates to the agent itself, reaches every skill, and leaves no auditable record. The bottom lane routes the client through the Prisma AIRS AI Gateway, where a virtual agent server, the Agent Registry access decision, and a log entry sit between the client and the upstream agent. Four numbered boxes below give the configuration order, and two panels cover the public-endpoint requirement and the access-control grains listed as coming soon.
▶ Step 3.1 — Enable the Agent Gateway on an API key

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.

  1. Open the API Keys page in Strata Cloud Manager and create or edit a key.
  2. Turn on the Agent Gateway toggle in the key's permissions.
  3. Enable the agents.invoke scope 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.

▶ Step 3.2 — Register the agent and fetch its card

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 agentURLAuthentication
Hello worldhttps://hello.a2aregistry.orgNone
Tools with header authhttps://a2a-header-auth.ayush-0a3.workers.devAuthorization: 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.

The upstream URL has to be publicly reachable

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.

▶ Step 3.3 — Provision workspaces and scope the skills

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.

Plan workspace boundaries around agent access

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.

▶ Step 3.4 — Point the client at the virtual server

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.

Self-hosted deployments substitute their own host

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.

▶ Step 3.5 — Confirm agent traffic is being logged

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.

Direct calls to the agent bypass all of this

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

▶ Symptoms and causes
SymptomLikely causeFix
The whole streamed response arrives at onceClient-side buffering, not a gateway problemAdd -N to curl, or disable buffering in the client.
No hook_results in any chunkx-portkey-strict-open-ai-compliance left at its default of trueSet it to false on the request.
A guardrail that should block returns 246Deny is off, so the verdict is advisorySet deny to true on the guardrail.
A guardrail that should block returns 200The guardrail is asynchronous, or is not attached to this configSet async to false, and confirm the ID is in input_guardrails.
Streaming requests record zero costThe provider omits usage data unless askedSend stream_options: {"include_usage": true}.
A stalled stream never times outThe timeout does not fire once one chunk has arrivedAdd a client-side idle timeout between chunks.
Log status reads Cache DisabledNo cache block on the config, or x-portkey-debug: "false" on the requestAdd the cache block, and remove the debug header.
Semantic cache never hitsIneligible request, a non-matching body parameter, or a vector dimension mismatchWork down the gates in Step 2.5 in order.
Two applications serve each other's answersThe system prompt is ignored by semantic matchingSeparate them with x-portkey-cache-namespace.
Fetch Agent Card fails on a valid agentSSRF protections reject private addresses, metadata hosts, and # fragmentsPublish the agent on a public endpoint and re-register it.
Agent call returns an authorization errorThe Agent Gateway toggle or the agents.invoke scope is missing from the keySet both on the key, as in Step 3.1.

Reference

▶ Request headers
HeaderDefaultEffect
x-portkey-strict-open-ai-compliancetrueAt false, guardrail hook_results chunks are emitted inside the stream.
x-portkey-cache-namespaceUnsetPartitions the cache by this string instead of by every request header.
x-portkey-cache-force-refreshfalseBypasses the cache and replaces the stored entry. Requires a cache config on the request.
x-portkey-debugtrueAt false, caching is switched off for the request.
x-portkey-metadataUnsetMust match exactly for a cache hit, in both simple and semantic mode.
▶ Semantic cache environment variables
VariableAccepted values
SEMANTIC_CACHE_EMBEDDING_PROVIDERopenai, azure-openai, google, vertex-ai
SEMANTIC_CACHE_EMBEDDINGS_URLThe provider's embeddings endpoint
SEMANTIC_CACHE_EMBEDDING_MODELModel name, or the deployment name on Azure OpenAI
SEMANTIC_CACHE_EMBEDDING_API_KEYA plain API key. Token-based auth is not supported here.
SEMANTIC_CACHE_EMBEDDING_DIMENSIONSMust match the vector store's configured dimension
SEMANTIC_CACHE_SIMILARITY_THRESHOLDCosine similarity floor for a hit. Defaults to 0.95.
VECTOR_STOREmilvus or pinecone
VECTOR_STORE_ADDRESSStore address, or the index name on Pinecone
VECTOR_STORE_COLLECTION_NAMEMilvus only. Must match the embedding model name.
VECTOR_STORE_API_KEYVector store credential
▶ Limits and status values
LimitValue
Semantic cache request sizeUnder 8,191 tokens
Semantic cache message countFour or fewer, with at least one user message
Semantic cache routes/chat/completions and /completions
Cache TTL60 seconds minimum, 90 days maximum, 7 days by default
Organisation cache TTL ceiling25,923,000 seconds
Streaming routes with guardrail results/chat/completions, /completions, /embeddings, /messages
Status valueMeaning
Cache HitExact match served from the cache.
Cache Semantic HitA differently worded prompt matched by meaning.
Cache MissNo match. The provider was called and the response stored.
Cache RefreshedThe force-refresh header replaced the stored entry.
Cache DisabledNo cache block on the config, or caching suppressed by a header.
246A guardrail failed, and the request was processed anyway.
446A guardrail failed with deny set, and the request was stopped.

Sources

▶ Where each claim comes from

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.

PhaseSource pageLast modified
1. StreamingGuardrails, under Streaming ResponsesCarries the input and output asymmetry, the compliance header, and the four supported routes.
1. StreamingRequest timeouts, under Caveats and ConsiderationsCarries the first-chunk timeout behaviour.
1. StreamingCost management, troubleshooting sectionCarries the zero-cost behaviour and stream_options.
2. CachingCache (Simple and Semantic)18 September 2026. Carries the mode table, hit and miss rules, environment variables, TTL, namespace, and force refresh.
3. Agent GatewayAgent Gateway, Registry, Servers, and Quickstart15 September 2026. Carries the two components, the scopes, the SSRF constraints, the virtual URL, and the coming-soon list.
Two things to re-check before a customer conversation

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.