Spring Boot Integration
What it is: tramai-spring provides Spring Boot auto-configuration that creates a Tramai engine instance, registers @AiService interfaces as Spring beans, and binds provider configuration from application.yml.
When to use it: You have a Spring Boot application and want TramAI services injected as standard Spring beans with YAML-driven configuration and zero manual wiring.
Minimum Setup
Step 1: Add the dependency
dependencies {
implementation("dev.tramai:tramai-spring:0.3.1")
implementation("dev.tramai:tramai-openai:0.3.1") // or your provider
}
Step 2: Add @EnableTramai
Put @EnableTramai on any configuration class or your main application class:
import dev.tramai.spring.EnableTramai
@SpringBootApplication
@EnableTramai
class InvoiceApplication
fun main(args: Array<String>) {
runApplication<InvoiceApplication>(*args)
}
Step 3: Configure in YAML
tramai:
default-provider: openai
models:
gpt-4o: openai
providers:
openai:
api-key: ${OPENAI_API_KEY}
Step 4: Inject and Use
@Service
class BillingService(
private val invoiceAnalyzer: InvoiceAnalyzer, // injected Spring bean
) {
fun process(invoiceText: String) {
val result = invoiceAnalyzer.analyze(invoiceText)
// ...
}
}
That's 5 lines of YAML + 1 annotation. TramAI creates the engine, registers @AiService interfaces as Spring beans, and routes calls to gpt-4o on OpenAI.
What Auto-Configuration Provides
- Engine creation: Binds
tramai.*properties fromapplication.ymland creates aTramaiengine instance - Service bean registration: Scans for
@AiServiceinterfaces and registers proxy beans (no manualTramai.create()needed) - Tool discovery: Scans for
@AiToolmethods on@Componentbeans and registers them as tools - Secret resolution: Resolves
env:NAMEandfile:/path/secretreferences natively; supports Vault and AWS Secrets Manager - Resilience: Configures circuit breaker, retry policy, and token budgets from YAML
- Cache: Optional in-memory response cache
InvoiceAnalyzer Example
import dev.tramai.core.annotations.AiService
import dev.tramai.core.annotations.Operation
// 1. Define your typed result
data class InvoiceVerdict(
val status: String, // "approved" | "needs_review" | "rejected"
val reason: String,
val missingFields: List<String>,
)
// 2. Define your AI service interface
@AiService
interface InvoiceAnalyzer {
@Operation(
prompt = "Analyze the invoice. Return status, reason, and any missing required fields.",
model = "gpt-4o",
)
suspend fun analyze(invoiceText: String): InvoiceVerdict
}
// 3. Inject and use it anywhere
@Service
class BillingService(
private val analyzer: InvoiceAnalyzer, // Auto-wired by Spring!
) {
suspend fun process(invoiceText: String): InvoiceVerdict =
analyzer.analyze(invoiceText)
}
// 4. Use it in a controller
@RestController
class InvoiceController(
private val billingService: BillingService,
) {
@PostMapping("/invoices/analyze")
suspend fun analyze(@RequestBody text: String): InvoiceVerdict =
billingService.process(text)
}
TramAI handles the LLM call, JSON extraction, parsing, validation, and retries automatically. Your code never touches HTTP clients or JSON parsers.
Multiple Providers
tramai:
default-provider: openai
models:
gpt-4o: openai
claude-sonnet-4-20250514: anthropic
llama3.2: ollama
providers:
openai:
api-key: ${OPENAI_API_KEY}
anthropic:
api-key: ${ANTHROPIC_API_KEY}
ollama:
base-url: http://localhost:11434
Operations reference models by name in @Operation(model = "claude-sonnet-4-20250514").
YAML Reference
| Property | Description | Required |
|---|---|---|
tramai.default-provider | Fallback provider name | No (but recommended) |
tramai.models.<model> | Model-to-provider mapping | Yes (at least one) |
tramai.providers.openai.api-key | OpenAI API key | Conditional |
tramai.providers.anthropic.api-key | Anthropic API key | Conditional |
tramai.providers.ollama.base-url | Ollama base URL | Conditional |
tramai.providers.openai-compatible.base-url | Custom gateway URL | Conditional |
tramai.providers.openai-compatible.provider-name | Custom provider name | No (default: openai-compatible) |
tramai.fallbacks.<model> | Fallback route config | No |
tramai.resilience.circuit-breaker.enabled | Enable circuit breaker | No (default: false) |
tramai.resilience.circuit-breaker.failure-threshold | Failures before open | No (default: 3) |
tramai.resilience.circuit-breaker.open-duration-millis | Open duration | No (default: 30000) |
tramai.resilience.retry.max-retry-after-millis | Max retry delay | No |
tramai.resilience.retry.jitter-ratio | Retry jitter | No (default: 0.1) |
tramai.cost.token-budget.hard-max-tokens-per-attempt | Per-attempt limit | No |
tramai.cost.token-budget.hard-max-tokens-per-operation | Per-operation limit | No |
Fallback Routing in Spring
tramai:
models:
gpt-4o: openai
gpt-4o-mini: openai
fallbacks:
gpt-4o:
- provider: openai
model: gpt-4o-mini
providers:
openai:
api-key: ${OPENAI_API_KEY}
If a call to gpt-4o fails, TramAI retries with gpt-4o-mini automatically. Fallback order is explicit — the engine does not guess.
Resilience Controls
tramai:
resilience:
circuit-breaker:
enabled: true
failure-threshold: 3
open-duration-millis: 30000
retry:
max-retry-after-millis: 20000
jitter-ratio: 0.1
cost:
token-budget:
hard-max-tokens-per-attempt: 4000
hard-max-tokens-per-operation: 12000
Circuit breaker opens after 3 failures and stays open for 30 seconds. Retries are jittered. Token budgets are enforced per-attempt and per-operation.
Secret References
TramAI resolves these reference schemes natively in YAML configuration:
providers:
openai:
api-key-secret-ref: env:OPENAI_API_KEY
Supported schemes:
env:NAME— reads from environment variablefile:/absolute/path/to/secret.txt— reads from file
You can also provide a custom SecretValueResolver bean for Vault, AWS Secrets Manager, or other backends.
Kotlin + Java Examples
Kotlin (recommended)
@SpringBootApplication
@EnableTramai
class SupportApp
data class TicketResponse(
val resolution: String,
val priority: String,
val needsEscalation: Boolean,
)
@AiService
interface SupportAgent {
@Operation(prompt = "Resolve the support ticket", model = "gpt-4o")
suspend fun resolve(ticket: String): TicketResponse
}
@Service
class SupportService(
private val agent: SupportAgent,
) {
suspend fun handleTicket(ticket: String): TicketResponse = agent.resolve(ticket)
}
Java
import dev.tramai.spring.EnableTramai;
@SpringBootApplication
@EnableTramai
public class SupportApplication {
public static void main(String[] args) {
SpringApplication.run(SupportApplication.class, args);
}
}
public record TicketResponse(
String resolution,
String priority,
boolean needsEscalation
) {}
@AiService
public interface SupportAgent {
@Operation(prompt = "Resolve the support ticket", model = "gpt-4o")
TicketResponse resolve(String ticket);
}
@Service
public class SupportService {
private final SupportAgent agent;
public SupportService(SupportAgent agent) {
this.agent = agent;
}
public TicketResponse handleTicket(String ticket) {
return agent.resolve(ticket);
}
}
Limitations
- No advanced bean scopes: AI service beans are standard singleton proxies.
- No per-bean provider overrides outside annotations: Provider selection is through
@Operation(provider = ...)or the model mapping registry. - No automatic test slices: Spring Boot test slices (e.g.,
@WebMvcTest) don't automatically isolate TramAI. Usetramai-testingwith mock providers in tests. - Thin adapter: The Spring module focuses on wiring — it does not add Spring-specific abstractions on top of the engine.
Next Steps
- Structured Output — Validation annotations and retry details
- Providers & Routing — Multi-provider configuration guide
- Testing — Deterministic testing with mock providers
- Observability — OpenTelemetry metrics for Spring applications
