Write an AI policy for the AI Gateway¶
An AI policy inspects, controls, and modifies the traffic going to and from a large language model (LLM) provider such as OpenAI or Anthropic.
AI policies use the same Policy interface as standard gateway policies. For full API details, see the policy SDK reference.
The key difference is how you handle LLM request and response bodies, especially:
- JSON responses
- Streaming responses (SSE)
How it works¶
Every request and response that flows through the gateway passes through a policy chain. Each policy declares which phases it participates in, and the kernel calls the appropriate hook for each phase:
Incoming Request
│
▼
Request Headers ──► OnRequestHeaders()
│
▼
Request Body ──► OnRequestBody() (or OnRequestBodyChunk() for streaming)
│
▼
Upstream LLM
│
▼
Response Headers ──► OnResponseHeaders()
│
▼
Response Body ──► OnResponseBody() (or OnResponseBodyChunk() for streaming)
│
▼
Downstream Client
Note
A policy chain is an ordered sequence of policies that the gateway runs on every request and response for a given LLM provider or App LLM proxy. Policies run in the order the runtime configuration lists them, and each policy sees the modifications the ones before it made.
Key idea¶
LLM responses come in two formats:
| Mode | Format |
|---|---|
| Non-streaming | Single JSON object |
| Streaming | SSE events (data: {...}) |
Your policy must handle both formats.
Which interfaces to implement¶
Choose based on what your policy needs to do:
| Goal | Interface | Mode setting |
|---|---|---|
| Inspect prompt / model | RequestPolicy |
RequestBodyMode: BodyModeBuffer |
| Inspect headers (auth, routing) | RequestHeaderPolicy |
RequestHeaderMode: HeaderModeProcess |
| Inspect or modify buffered (in-memory) response | ResponsePolicy |
ResponseBodyMode: BodyModeBuffer |
| Inspect or modify streaming response | StreamingResponsePolicy (embeds ResponsePolicy) |
ResponseBodyMode: BodyModeStream |
How to write an AI policy¶
Step 1: Create the policy¶
Each policy lives in its own Go module. Create a "policies" directory inside your gateway:
Step 2: Implement the mode¶
Mode() declares which phases this policy participates in and how bodies are handled. The kernel reads this once at startup — there is no per-request overhead.
package myaipolicy
import (
"context"
policy "github.com/wso2/api-platform/sdk/core/policy/v1alpha2"
)
type MyAIPolicy struct{}
func (p *MyAIPolicy) Mode() policy.ProcessingMode {
return policy.ProcessingMode{
RequestBodyMode: policy.BodyModeBuffer,
ResponseBodyMode: policy.BodyModeStream,
}
}
Tip
If your policy doesn't need to inspect a phase, set that phase to HeaderModeSkip or BodyModeSkip.
Step 3: Implement request inspection¶
OnRequestBody is called once the request body is fully buffered. Use it to inspect the model name, messages, or parameters before the request reaches the LLM provider.
func (p *MyAIPolicy) OnRequestBody(
ctx context.Context,
reqCtx *policy.RequestContext,
params map[string]interface{},
) policy.RequestAction {
// Inspect model + messages
return nil
}
Step 4: Implement response handling¶
For most AI policies, implement both:
ResponsePolicy: handles buffered responses, where the entire response is available at once. That's either a non-streaming JSON response or concatenated SSE events.StreamingResponsePolicy: handles streaming responses, either JSON or SSE events.
Tip
The gateway automatically chooses which handler to call. The gateway calls OnResponseBodyChunk only when the entire policy chain is streaming-compatible. If any policy in the chain doesn't support streaming, the gateway falls back to OnResponseBody, so implement both even when streaming is your primary target.
// Streaming Response Handling
func (p *MyAIPolicy) OnResponseBodyChunk(
ctx context.Context,
respCtx *policy.ResponseStreamContext,
chunk *policy.StreamBody,
params map[string]interface{},
) policy.StreamingResponseAction {
// Accumulate + process
return policy.ForwardResponseChunk{}
}
// Gating response chunks before processing response
func (p *MyAIPolicy) NeedsMoreResponseData(_ []byte) bool {
return false
}
// Buffered Fallback
func (p *MyAIPolicy) OnResponseBody(
ctx context.Context,
respCtx *policy.ResponseContext,
params map[string]interface{},
) policy.ResponseAction {
// Same logic as streaming, applied to the full body
return nil
}
Gate-then-stream pattern
A common pattern for AI guardrails is to accumulate chunks until you have a complete SSE event to inspect, then switch to pass-through:
// Buffer until we can parse a complete SSE event, then stream freely
func (p *MyAIPolicy) NeedsMoreResponseData(accumulated []byte) bool {
return !bytes.Contains(accumulated, []byte("\n\n"))
}
Step 5: Factory function¶
Initialize your policy and validate parameters:
func GetPolicy(
metadata policy.PolicyMetadata,
params map[string]interface{},
) (policy.Policy, error) {
threshold, ok := params["blockThreshold"].(float64)
if !ok {
return nil, fmt.Errorf("invalid blockThreshold")
}
return &MyAIPolicy{blockThreshold: threshold}, nil
}
Step 6: Define parameters¶
Create a policy-definition.yaml in your policy directory:
name: my-ai-policy
displayName: my ai policy
version: v1.0.0
parameters:
type: object
properties:
blockThreshold:
type: number
default: 0.8
Step 7: Share data between phases¶
Use the Metadata map to pass data between request and response phases — for example, the model name read from the request, used later to apply model-specific logic in the response phase:
// In request phase
reqCtx.Metadata["model"] = model
// In response phase
model := respCtx.Metadata["model"]
Step 8: Register and build¶
Add your policy to the gateway folder's build.yaml under policies: using filePath for local development:
For published policies (production), use the module reference instead:
Best practices¶
- Always handle both streaming and non-streaming. The gateway falls back to buffered mode when any policy in the chain doesn't support streaming.
- Use the
Metadatamap to share state. Pass data between the request and response phases through it. - Implement streaming and the buffered fallback. Your policy then works whether the chain runs in streaming or buffered mode.
- Parse SSE incrementally. When gating on streaming responses, buffer only until you have a complete SSE event, terminated by
\n\n, rather than the entire response. This keeps latency low.
Next steps¶
- Build the gateway with AI policies: build a gateway image that includes your custom AI policy
- Apply AI policies to proxies: sync your custom AI policy to the organization and apply it to LLM providers, App LLM proxies, and MCP proxies
- Write a custom policy for the self-hosted gateway: the general-purpose policy SDK that the AI Gateway's policy engine builds on