TramAI

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

SPIWhat it interceptsBest for
OperationInterceptorRequest messages and provider responsesPII masking, redaction, auditing
DlpInterceptorModel outputs and tool resultsData loss prevention, compliance scanning
EngineEventObserverEngine lifecycle eventsMonitoring, 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 nameWhen it firesAttributes
dlp.rejectionDLP interceptor rejected outputoperationInterface, operationMethod, reason
dlp.redactionDLP interceptor redacted contentredactionCount, ruleIds
circuit_breaker.openedCircuit breaker trippedproviderId, failureCount
circuit_breaker.closedCircuit breaker resetproviderId
token_budget.warningSoft token limit crossedcurrent, limit
token_budget.exceededHard token limit crossedcurrent, limit
retry.scheduledRetry attempt scheduledattempt, delayMs
fallback.activatedFallback provider selectedoriginal, 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

ImplementationModulePurpose
NoOpOperationInterceptortramai-coreDefault no-op (pass through)
CompositeOperationInterceptortramai-coreChains multiple interceptors in order
NoOpDlpInterceptortramai-coreDefault no-op DLP
RuleBasedDlpInterceptortramai-securityConfigurable regex-based DLP with rules
NoOpEngineEventObservertramai-engineDefault no-op observer

Interceptor Execution Order

In a single operation, interceptors fire in this order:

  1. OperationInterceptor.interceptRequest — modify messages before provider call
  2. Provider call
  3. DlpInterceptor.inspect — scan model output / tool result
  4. OperationInterceptor.interceptResponse — modify provider response
  5. EngineEventObserver.onEngineEvent — record lifecycle events

Limitations

  • DLP failure (DlpInspectionException) does not count toward circuit breakers or retry
  • Only one DlpInterceptor and one EngineEventObserver in Spring Boot
  • Multiple OperationInterceptor beans are composed in order
  • DLP interceptors fire per-provider-response, not per-stream-chunk

Next Steps