Interceptors SDK integration reference
The Interceptors SDK (aidial-interceptors-sdk) is a Python library for writing interceptors — the middleware DIAL Core runs over requests and responses travelling between a caller and a model or application, for PII redaction, guardrails, content filtering, and audit logging.
It turns an interceptor into an ordinary HTTP service that DIAL Core can call like any other deployment. You subclass a base class, override the callbacks for the parts of the request or response you want to change, and the SDK handles the rest: parsing the payload, calling onward to the next link in the chain, reassembling the response stream, and exposing the configuration endpoint.
Two things distinguish it from the DIAL SDK, which builds applications and adapters:
- You modify traffic rather than produce it. An application implements a chat completion and owns the response. An interceptor sits in the middle, changes what passes through, and defers to whatever comes next.
- You do not name what comes next. The SDK calls the downstream deployment through a reserved name that DIAL Core resolves, so an interceptor works the same whether it is first, last, or in the middle of a chain.
It depends on the DIAL SDK internally — it serves through DIALApp and reuses its request and response objects.
| Package | aidial-interceptors-sdk |
| Source | github.com/epam/ai-dial-interceptors-sdk |
Installation
pip install aidial-interceptors-sdk
How DIAL Core reaches your interceptor
You register the service in DIAL Core configuration and assign it to a model or application — see Configure and assign interceptors. DIAL Core then calls it on every request to that deployment, passing two pieces of context:
| What you read | Where it comes from | Notes |
|---|---|---|
upstream_deployment_id | X-DIAL-DEPLOYMENT-ID header | The deployment this interceptor is assigned to. Available since DIAL Core 0.43.0. |
configuration | Interceptor configuration in DIAL Core config | Parsed into your Pydantic model, if you declare a schema. None when no schema is defined. |
Your interceptor also receives request and response objects from the DIAL SDK, and a dial_client for calling onward.
How your interceptor calls the next deployment
DialClient (dial_client.py) is how an interceptor reaches whatever comes next in the chain:
| Attribute | Purpose |
|---|---|
dial_url | DIAL Core server URL |
client | OpenAI-compatible client for calling onward through DIAL Core |
storage | Read and write files on DIAL file storage |
The important detail is the routing contract: client is configured with the reserved deployment name interceptor. DIAL Core resolves that name to the next interceptor in the chain, or to the target deployment if yours is the last one. You never name the downstream deployment yourself — that is what makes interceptors composable and order-independent from your code's point of view.
Overriding behaviour
Both base classes are Pydantic models you subclass. Every callback has a pass-through default, so you override only what you change.
Chat completion interceptors
Subclass ChatCompletionInterceptor (base.py). The callbacks most interceptors use:
| Method | When it runs | Typical use |
|---|---|---|
on_request_message | Per message in the incoming request | Redact or rewrite user content |
on_request | Once, on the whole request dict | Inspect or reshape the full payload |
on_response_message | Per message in the outgoing response | Restore redacted content, filter output |
on_stream_start / on_stream_end | Around the upstream response stream | Inject stages, flush buffers |
on_stream_error | Upstream returned an error | Convert or suppress errors. Default re-raises. |
call_upstreams | Controls how the upstream is called | Override for fan-out patterns |
Request callbacks run bottom-up: message-level, then list-level, then the full request. Methods returning list[dict] | dict may add or remove elements — return a list to replace one element with several, or an empty list to drop it.
Beyond these, the SDK exposes finer-grained callbacks for attachments, stages, state, custom content, choices, finish reasons and usage, on both the request and response side, plus utilities such as reserve_stage_index, send_chunk, get_configuration and configuration_schema. Their signatures are defined in request_handler.py, request_message_handler.py and response_handler.py.
ChatCompletionNoOpInterceptor passes everything through unchanged — useful for testing the chain without side effects.
Embeddings interceptors
Subclass EmbeddingsInterceptor (base.py). A smaller surface:
| Method | Default behaviour |
|---|---|
modify_input | Returns the input string unchanged |
modify_embedding | Returns the embedding unchanged |
modify_request | Calls modify_input on each input in the request |
modify_response | Calls modify_embedding on each embedding in the response |
For simple transformations override modify_input and modify_embedding. For full control override modify_request and modify_response. EmbeddingsNoOpInterceptor is the pass-through equivalent.
Wiring a service
Interceptor classes become DIAL deployments through two adapter functions — interceptor_to_chat_completion and interceptor_to_embeddings — which wrap your class into something a DIALApp can serve. Both take your class, the DIAL Core URL, and an async factory returning an httpx.AsyncClient.
from aidial_sdk import DIALApp
from aidial_sdk.telemetry.types import TelemetryConfig
from aidial_interceptors_sdk.chat_completion import (
interceptor_to_chat_completion,
)
from aidial_interceptors_sdk.embeddings.adapter import (
interceptor_to_embeddings,
)
from aidial_interceptors_sdk.utils._http_client import get_http_client
DIAL_URL = "http://dial-core:8080"
async def client_factory():
return get_http_client()
app = DIALApp(
dial_url=DIAL_URL,
telemetry_config=TelemetryConfig(),
add_healthcheck=True,
propagate_auth_headers=True,
allow_extra_request_fields=True,
)
app.add_chat_completion(
"my-interceptor",
interceptor_to_chat_completion(MyChatInterceptor, DIAL_URL, client_factory),
)
app.add_embeddings(
"my-emb-interceptor",
interceptor_to_embeddings(MyEmbInterceptor, DIAL_URL, client_factory),
)
Run it with uvicorn:
uvicorn app:app --host 0.0.0.0 --port 5000
This exposes the endpoints DIAL Core will call:
http://localhost:5000/openai/deployments/my-interceptor/chat/completionshttp://localhost:5000/openai/deployments/my-emb-interceptor/embeddings
Environment variables
| Variable | Default | Description |
|---|---|---|
DIAL_URL | (required) | DIAL Core server URL |
LOG_LEVEL | INFO | Application log level |
DIAL_SDK_LOG | WARNING | DIAL SDK internal log level |
WEB_CONCURRENCY | 1 | Number of uvicorn workers |
Further reading
- Tutorial: PII-redacting interceptor — build a working interceptor end to end
- Configure and assign interceptors — register it with DIAL Core and choose a scope
- Interceptor examples — run the SDK's example interceptors
- Request flow — how a request travels from DIAL Core to models, apps, and tools