Custom Interceptors
TramAI has three interceptor SPIs that let you inspect and modify data at different points in the execution pipeline: OperationInterceptor, DlpInterceptor, and EngineEventObserver.
What This Covers
- OperationInterceptor — request/response hooks for PII masking and auditing
- DlpInterceptor — model output and tool result scanning
- EngineEventObserver — engine-level event hooks
- Interceptor ordering and composition
- Spring Boot integration
When to Use Each Interceptor
| SPI | What it intercepts | Best for |
|---|---|---|
OperationInterceptor | Request messages and provider responses | PII masking, redaction, auditing |
DlpInterceptor | Model outputs and tool results | Data loss prevention, compliance scanning |
EngineEventObserver | Engine lifecycle events | Monitoring, alerting, custom metrics |
OperationInterceptor
Interface location: dev.tramai.core.observation.OperationInterceptor
Minimum Setup
val interceptor = object : OperationInterceptor {
override fun interceptRequest(
context: OperationCallContext,
messages: List<Message>,
): List<Message> = messages.map { msg ->
msg.copy(content = msg.content.replace(Regex("\\b\\d{16}\\b"), "[CARD]"))
}
override fun interceptResponse(
context: OperationCallContext,
response: ModelResponse,
): ModelResponse = response.copy(
content = response.content.replace(Regex("\\b\\d{16}\\b"), "[CARD]"),
)
}
Standalone Registration
val tramai = Tramai {
provider(OpenAiProvider(...), name = "openai", default = true)
model("gpt-4o", "openai")
interceptor(interceptor)
}
Spring Boot Registration
@Bean
fun cardMaskingInterceptor(): OperationInterceptor = object : OperationInterceptor {
override fun interceptRequest(context: OperationCallContext, messages: List<Message>): List<Message> =
messages.map { msg -> msg.copy(content = maskCards(msg.content)) }
}
Composability
Multiple OperationInterceptor beans in Spring are composed in order via CompositeOperationInterceptor. Each interceptor's interceptRequest output feeds into the next interceptor's input.
DlpInterceptor
Interface location: dev.tramai.core.security.DlpInterceptor
The DLP SPI is a fun interface with a single method:
fun interface DlpInterceptor {
fun inspect(context: DlpContext, text: String): DlpResult
}
DlpContext Metadata
The context provides full operation metadata for policy decisions:
data class DlpContext(
val contentType: DlpContentType, // MODEL_OUTPUT or TOOL_RESULT
val contentLocation: DlpContentLocation,
val operationInterface: String, // fully qualified interface name
val operationMethod: String, // method name
val providerId: String?,
val modelName: String?,
val toolName: String?,
val correlationId: String,
val workflowRunId: String?,
val dataClassification: DataClassification?,
val classificationSource: ClassificationSource?,
)
DlpResult
data class DlpResult(
val sanitizedText: String,
val redactions: List<DlpRedaction> = emptyList(),
)
DlpRedaction contains ruleId and replacementCount — raw matched values are intentionally excluded to prevent accidental data leakage through audit or exception paths.
Example: Regex-Based Redaction
val ssnDlp = DlpInterceptor { context, text ->
val sanitized = text
.replace(Regex("\\b\\d{3}-\\d{2}-\\d{4}\\b"), "[SSN-REDACTED]")
.replace(Regex("\\b(?:\\d[ -]*?){13,16}\\b"), "[CARD-REDACTED]")
DlpResult(sanitized)
}
Example: Policy-Based Block
val policyDlp = DlpInterceptor { context, text ->
// Block model output if it contains classified data
if (context.contentType == DlpContentType.MODEL_OUTPUT && containsClassified(text)) {
throw DlpInspectionException("Model output contains classified data")
}
DlpResult(text) // pass through
}
Standalone Registration
val tramai = Tramai {
dlp(ssnDlp)
}
Spring Boot
Define exactly one DlpInterceptor bean:
@Bean
fun ssnDlpInterceptor(): DlpInterceptor = DlpInterceptor { context, text ->
DlpResult(text.replace(Regex("\\b\\d{3}-\\d{2}-\\d{4}\\b"), "[REDACTED]"))
}
If zero are defined, no DLP is applied. If more than one is found, auto-configuration throws at startup.
EngineEventObserver
Interface location: dev.tramai.engine.EngineEventObserver
A fun interface for observing engine-level lifecycle events:
fun interface EngineEventObserver {
fun onEngineEvent(name: String, attributes: Map<String, Any?>)
}
Observable Events
| Event name | When it fires | Attributes |
|---|---|---|
dlp.rejection | DLP interceptor rejected output | operationInterface, operationMethod, reason |
dlp.redaction | DLP interceptor redacted content | redactionCount, ruleIds |
circuit_breaker.opened | Circuit breaker tripped | providerId, failureCount |
circuit_breaker.closed | Circuit breaker reset | providerId |
token_budget.warning | Soft token limit crossed | current, limit |
token_budget.exceeded | Hard token limit crossed | current, limit |
retry.scheduled | Retry attempt scheduled | attempt, delayMs |
fallback.activated | Fallback provider selected | original, fallback |
Example: Alerting on DLP Rejection
val alertObserver = EngineEventObserver { name, attributes ->
if (name == "dlp.rejection") {
alertSecurityTeam(
"DLP rejection in ${attributes["operationInterface"]}." +
"${attributes["operationMethod"]}: ${attributes["reason"]}",
)
}
}
Example: Token Budget Tracking
val costObserver = EngineEventObserver { name, attributes ->
if (name.startsWith("token_budget.")) {
metricsRecorder.record(
name,
attributes["current"] as? Long ?: 0L,
mapOf("limit" to (attributes["limit"] as? Long ?: 0L)),
)
}
}
Standalone Registration
val tramai = Tramai {
engineEventObserver(alertObserver)
}
Spring Boot
@Bean
fun alertEngineEventObserver(): EngineEventObserver = EngineEventObserver { name, attributes ->
if (name == "dlp.rejection") alertSecurityTeam(attributes)
}
At most one EngineEventObserver bean is supported. If more than one is found, auto-configuration throws at startup.
Built-in Implementations
| Implementation | Module | Purpose |
|---|---|---|
NoOpOperationInterceptor | tramai-core | Default no-op (pass through) |
CompositeOperationInterceptor | tramai-core | Chains multiple interceptors in order |
NoOpDlpInterceptor | tramai-core | Default no-op DLP |
RuleBasedDlpInterceptor | tramai-security | Configurable regex-based DLP with rules |
NoOpEngineEventObserver | tramai-engine | Default no-op observer |
Interceptor Execution Order
In a single operation, interceptors fire in this order:
OperationInterceptor.interceptRequest— modify messages before provider call- Provider call
DlpInterceptor.inspect— scan model output / tool resultOperationInterceptor.interceptResponse— modify provider responseEngineEventObserver.onEngineEvent— record lifecycle events
Limitations
- DLP failure (
DlpInspectionException) does not count toward circuit breakers or retry - Only one
DlpInterceptorand oneEngineEventObserverin Spring Boot - Multiple
OperationInterceptorbeans are composed in order - DLP interceptors fire per-provider-response, not per-stream-chunk
Next Steps
- Custom Observers — engine event observers and custom metrics
- Custom Providers — implement the provider SPI
- Production Hardening — use interceptors in production
