Module: tramai-spring
One-liner: Spring Boot auto-configuration that wires Tramai's
Tramairuntime, scans@AiServiceinterfaces, discovers@AiToolbeans, and bindstramai.*application properties — all with zero manual bean declarations. Module type:framework-adapterSource files: compatibility facade — since 0.6.0 the Spring integration itself lives intramai-spring-core(21 sources);tramai-springdeclares no production sources of its own and re-exports that artifact Test files: 2 integration tests (tramai-spring-coreowns the contract tests) Build:dev.tramai:tramai-spring:0.6.0(compatibility facade) — new applications depend ondev.tramai:tramai-spring-boot-starterDepends on:api(tramai-spring-core). Provider adapters and secret backends are separate modules (tramai-spring-provider-openai|-anthropic|-ollama,tramai-spring-secrets-file|-vault|-aws), and the sovereign profile comes fromtramai-spring-sovereign
L1: Quick Start (30-second read)
What
For 0.6.0 the canonical entry point is tramai-spring-boot-starter — an aggregation artifact that brings in tramai-spring-core (the profile-neutral Spring integration) and tramai-spring-sovereign (the sovereign runtime profile). It takes the same Tramai runtime you'd build manually with tramai-standalone and makes it available through Spring's DI container: add the starter plus a provider adapter, define an @AiService interface, and it becomes an injectable bean — no @Bean factory methods, no manual Tramai.builder() chains, and no required annotation on your application class.
tramai-spring itself is kept as a compatibility facade over tramai-spring-core: depending on it is equivalent to depending on the integration core.
Why
Spring Boot developers expect framework-managed configuration, auto-discovery, and constructor injection. tramai-spring delivers on those expectations without reintroducing framework coupling into Tramai's core:
- Configuration-driven — all provider API keys, model mappings, fallback routes, resilience settings, and caching live in
application.ymlunder a singletramai:namespace, bound via@ConfigurationProperties - Zero boilerplate —
@AiServiceinterfaces are discovered by classpath scanning and registered as singletonFactoryBeanproxies;@AiToolmethods on any Spring bean are auto-discovered and registered as callable tools - Secrets management — dedicated resolver modules (
tramai-spring-secrets-file,tramai-spring-secrets-vault,tramai-spring-secrets-aws) plug into the property-binding pipeline - Same engine underneath — the auto-configured
Tramaibean is the samedev.tramai.standalone.Tramaithat standalone users build manually; behavior, retry policy, structured output, and provider routing are identical
When to use
Use this module when:
- You already have a Spring Boot application and want AI capabilities
- You want @AiService interfaces injected as Spring beans via @Autowired / constructor injection
- You prefer application.yml over builder chains for configuration
- You want Spring's auto-scanning to discover @AiTool beans in your existing service layer
- You need Vault or AWS Secrets Manager for credential resolution at startup
Don't use this module when:
- You don't use Spring Boot (use tramai-standalone instead)
- You need full control over the Tramai builder chain without Spring property binding
- You want to avoid Spring Boot's startup overhead in a CLI tool or script
How to add
// build.gradle.kts
dependencies {
implementation(platform("dev.tramai:tramai-bom:0.6.0"))
implementation("dev.tramai:tramai-spring-boot-starter") // standard + sovereign
implementation("dev.tramai:tramai-spring-provider-openai") // or -anthropic, -ollama
}
<!-- pom.xml -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>dev.tramai</groupId>
<artifactId>tramai-bom</artifactId>
<version>0.6.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>dev.tramai</groupId>
<artifactId>tramai-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>dev.tramai</groupId>
<artifactId>tramai-spring-provider-openai</artifactId>
</dependency>
</dependencies>
Where to go next
- tramai-spring-boot-starter — The starter to depend on
- tramai-spring-core — The integration implementation
- Spring Boot Integration Guide — Full walkthrough with code
- Getting Started — Pick your setup path
- Configuration Reference — All
tramai.*properties - Production Hardening — Vault/AWS Secrets Manager, circuit breakers, caching
- tramai-standalone — The same runtime without Spring
- tramai-core — Annotations and SPI contracts
L2: Usage Guide (5-minute read)
Minimal setup
Three steps: add the starter plus a provider adapter, define an @AiService interface, configure application.yml.
@SpringBootApplication
class InvoiceApplication
fun main() = runApplication<InvoiceApplication>()
No annotation is required on the application class — Spring Boot discovers the auto-configurations from the artifact's META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. @EnableTramai remains available as an explicit opt-in for non-Boot Spring contexts and for tests that build a context by hand.
@AiService
interface InvoiceAnalyzer {
@Operation(
prompt = "Analyze this invoice and return a one-line status.",
model = "gpt-4o",
)
suspend fun analyze(invoiceText: String): String
}
# application.yml
tramai:
default-provider: openai
models:
gpt-4o: openai
providers:
openai:
api-key: ${OPENAI_API_KEY}
The InvoiceAnalyzer interface can now be injected anywhere:
@Service
class BillingService(
private val invoiceAnalyzer: InvoiceAnalyzer,
) {
suspend fun process(invoiceText: String): String =
invoiceAnalyzer.analyze(invoiceText)
}
application.yml reference
Providers
tramai:
providers:
anthropic:
api-key: ${ANTHROPIC_API_KEY}
api-key-secret-ref: vault:providers/anthropic/api-key # mutual exclusive with api-key
base-url: https://api.anthropic.com
openai:
api-key: ${OPENAI_API_KEY}
api-key-secret-ref: vault:providers/openai/api-key
bearer-token: ${OPENAI_BEARER_TOKEN}
bearer-token-secret-ref: vault:providers/openai/bearer-token
base-url: https://api.openai.com/v1
organization: org-xxx
project: proj_xxx
codex-auth:
enabled: false
auth-file: /home/user/.codex/auth.json
openai-compatible:
provider-name: my-local
api-key: ${COMPATIBLE_API_KEY}
base-url: https://my-endpoint.example.com/v1
ollama:
base-url: http://localhost:11434
Model routing and fallbacks
tramai:
default-provider: openai
models:
gpt-4o: openai
gpt-4o-mini: openai
llama3.2: ollama
fallbacks:
gpt-4o:
- provider: openai
model: gpt-4o-mini
- provider: ollama
model: llama3.2
Resilience, cost control, and caching
tramai:
resilience:
circuit-breaker:
enabled: true
failure-threshold: 5
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
soft-max-tokens-per-operation: 8000
cache:
in-memory:
enabled: true
max-entries: 1000
Secret resolvers (Vault / AWS Secrets Manager)
tramai:
secrets:
vault:
enabled: true
base-url: https://vault.example.com
token-secret-ref: env:VAULT_TOKEN
mount-path: secret
kv-version: 2
namespace: my-namespace
default-field: value
aws-secrets-manager:
enabled: true
region: eu-west-1
access-key-id-secret-ref: env:AWS_ACCESS_KEY_ID
secret-access-key-secret-ref: env:AWS_SECRET_ACCESS_KEY
endpoint: https://secretsmanager.eu-west-1.amazonaws.com
default-field: value
@AiTool bean scanning
Any Spring bean with methods annotated @AiTool is automatically discovered and registered as a callable tool on the Tramai runtime:
@Component
class WeatherTools {
data class GetTemperatureInput(val city: String, val unit: String = "celsius")
@AiTool(
name = "get_temperature",
description = "Get the current temperature for a city",
idempotent = true,
sideEffectLevel = SideEffectLevel.NONE,
)
suspend fun getTemperature(input: GetTemperatureInput): String {
// Call a weather API...
return "The temperature in ${input.city} is 22° ${input.unit}"
}
}
Tool methods must satisfy:
- Exactly one parameter, which must be a data class
- The method may be
suspendor blocking; the runtime resolves invocation strategy via reflection
The scanner (AiToolScanner) iterates all Spring beans, inspects each for @AiTool-annotated methods via kotlin.reflect.full, validates the signature, and wraps them into TramaiTool instances that are registered with the Tramai builder before build() is called.
L3: Architecture & Mechanics (15-minute read)
Auto-configuration entry point
Activation is automatic: Spring Boot discovers the auto-configurations listed in tramai-spring-core's META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:
dev.tramai.spring.TramaiSecretResolutionAutoConfiguration
dev.tramai.spring.StandardTramaiProfileAutoConfiguration
dev.tramai.spring.AiServiceProxyAutoConfiguration
dev.tramai.spring.SecurityClassificationAutoConfiguration
TramaiAutoConfiguration is not in that list — it is @Imported by StandardTramaiProfileAutoConfiguration, which activates when tramai.profile is standard or absent (matchIfMissing = true). @EnableTramai is a meta-annotation that @Imports TramaiEnableImportSelector and declares @AutoConfigurationPackage; it is optional, and it does not select a runtime profile (tramai.profile remains the authority).
Bean registration flow
TramaiAutoConfiguration (auto-discovered, or imported by @EnableTramai)
│
├─ @Bean "tramai" (Tramai instance)
│ 1. Binds TramaiProperties from application.yml
│ 2. Resolves secret values (env, file, Vault, AWS)
│ 3. Calls AiToolScanner.fromApplicationContext() → discovers @AiTool beans
│ 4. Registers property-defined providers (Anthropic, OpenAI, Ollama, OpenAI-compatible)
│ 5. Collects user-defined ModelProvider beans via ObjectProvider
│ 6. Configures model routing, fallbacks, cache, interceptors
│ 7. Builds and returns Tramai instance
│
└─ @Bean "aiServiceBeanDefinitionRegistrar" (AiServiceBeanDefinitionRegistrar)
implements BeanDefinitionRegistryPostProcessor
1. Scans AutoConfigurationPackages for @AiService interfaces
2. For each interface, registers a RootBeanDefinition(AiServiceFactoryBean)
3. AiServiceFactoryBean.getObject() calls tramai.create(serviceType.kotlin)
4. Returns a JDK dynamic proxy that delegates to TramaiEngine
TramaiProperties — property binding
The runtime profile is selected with tramai.profile (standard or sovereign); the standard profile uses the framework-level settings below, the sovereign profile adds the tramai.sovereign.* allowlists (see tramai-spring-sovereign).
TramaiProperties is a Spring @ConfigurationProperties("tramai") data class that binds the framework-level settings the Spring core itself consumes. Key design choices:
- Mutable
varproperties — Spring Boot binds via setter reflection; Kotlin data classes withvarfields are the idiomatic Spring Boot pattern - Nested data classes —
Resilience,Cost,Cache, andSecurityeach map to their own subsection (plusFallbackRoutefor routing entries), keeping the top-level class readable - Provider-owned and secret-owned properties are not bound here —
tramai.providers.*is bound by the provider adapter modules (OpenAiProperties→tramai.providers.openai,OpenAiCompatibleProperties→tramai.providers.openai-compatible,AnthropicProperties,OllamaProperties) andtramai.secrets.*by the secret-backend modules (VaultSecretProperties,AwsSecretsManagerProperties);tramai-spring-core's own properties coverdefaultProvider,models,fallbacks,resilience,cost,cache, andsecurity - Mutual-exclusion enforcement —
SpringSecretResolution.resolve(directValue, secretRef, fieldName, resolver)intramai-spring-corefails viacheck(...)(IllegalStateException) at startup if both a direct value and its*-secret-refare provided for the same field, preventing silent misconfiguration - Graceful absence — if a provider block is missing, no provider is registered (e.g., omitting
tramai.providers.ollamasimply skips Ollama registration)
AiServiceBeanDefinitionRegistrar — @AiService scanning
This BeanDefinitionRegistryPostProcessor runs during Spring's bean-definition phase, before any beans are instantiated:
- Checks that
AutoConfigurationPackagesis available (the Spring Boot application has a base package) - Creates a
ClassPathScanningCandidateComponentProviderfiltered to interfaces annotated with@AiService - Iterates all auto-configuration base packages, finds candidate
@AiServiceinterfaces - For each, registers a
RootBeanDefinition(AiServiceFactoryBean::class.java)with the interface class as a constructor argument - The
AiServiceFactoryBeanlazily resolves theTramaibean from theBeanFactoryand callstramai.create(serviceType)when Spring asks for the proxy
This approach keeps the scanning decoupled from the Tramai bean lifecycle — the registrar works at definition time, the factory bean works at dependency-injection time.
AiToolScanner — method-level tool discovery
The scanner (in tramai-spring-core) uses kotlin.reflect.full to introspect each Spring bean at Tramai construction time:
- Iterates all bean names (skipping the
"tramai"bean to avoid circular resolution) - For each bean, checks whether any method carries
@AiTool - Validates the tool method signature (exactly one parameter, must be a data class)
- Wraps each
@AiToolmethod into aMethodBackedTramaiToolthat delegates to the bean viaKFunction.call()orcallSuspend() - Returns the list to
TramaiAutoConfiguration, which passes it tobuilder.tools(...)
Because scanning happens at the Tramai bean construction phase (not at bean-definition time), all Spring beans — including those created by @Bean factory methods — are available for tool discovery.
Secret resolution chain
TramaiSecretResolutionAutoConfiguration (tramai-spring-core) composes the chain from SecretValueResolver beans, with optional modules contributing marker-typed resolvers (SpringBuiltInSecretValueResolver). The resulting order is:
1. User-provided SecretValueResolver beans (highest priority)
2. Built-in resolvers contributed by optional modules, in Spring @Order:
VaultSecretValueResolver (tramai-spring-secrets-vault, if tramai.secrets.vault.enabled=true)
AwsSecretsManagerSecretValueResolver (tramai-spring-secrets-aws, if tramai.secrets.aws-secrets-manager.enabled=true)
file resolver (tramai-spring-secrets-file)
3. EnvironmentSecretValueResolver (env:* references)
4. Bootstrap resolvers (SpringBootstrapSecretValueResolver markers)
The chain is split into a bootstrap chain (user + environment + bootstrap markers) and a full chain (user + built-ins + environment + bootstrap markers), exposed as SpringBootstrapSecretChain / SpringSecretChain holders. The split lets Vault/AWS credentials bootstrap through the lower-level resolvers without recursing through the yet-uninitialised Vault/AWS resolvers. The holders are deliberately not SecretValueResolver beans themselves, so they cannot break unqualified application-level injection.
Vault reference format: vault:path/to/secret#fieldAWS Secrets Manager reference format: aws-secretsmanager:secret-id#field
Dependency graph
tramai-spring (compatibility facade, no production sources)
Depends on:
- tramai-spring-core (api) — re-exported; the Spring integration itself
tramai-spring-core (the implementation, 21 sources)
Depends on:
- tramai-standalone (api) — the Tramai builder + runtime
- tramai-security (compileOnly) — classification/policy types
- spring-context (implementation)
- spring-boot-autoconfigure (implementation)
- spring-boot-configuration-processor (annotationProcessor)
Depended on by:
- tramai-spring (facade)
- tramai-spring-boot-starter (aggregation starter)
- tramai-spring-provider-* / tramai-spring-secrets-* adapter modules
- Application code (end-user Spring Boot apps)
Provider adapters (tramai-spring-provider-openai|-anthropic|-ollama) and secret backends (tramai-spring-secrets-file|-vault|-aws) are separate artifacts: tramai-spring-core never binds or reads provider-specific or secret-backend-specific configuration. The tramai-spring facade's own test source set is the integration-test home, so it pulls those adapters as testImplementation only.
Error model
| Situation | Exception | When |
|---|---|---|
apiKey + apiKeySecretRef both set | IllegalStateException | SpringSecretResolution.resolve() (tramai-spring-core) at startup, from the owning provider/secret adapter's auto-configuration |
Vault enabled but baseUrl missing | IllegalStateException | VaultSecretValueResolverAutoConfiguration (tramai-spring-secrets-vault) at startup |
| Vault token unresolvable | IllegalStateException | VaultSecretValueResolverAutoConfiguration at startup |
| AWS enabled but a required property missing | IllegalStateException | AwsSecretsManagerSecretValueResolverAutoConfiguration (tramai-spring-secrets-aws) at startup |
No SecretValueResolver could resolve a *-secret-ref | IllegalStateException | SpringSecretResolution.resolve() at startup |
| Unknown provider in fallback route | IllegalStateException | TramaiAutoConfiguration at startup |
| Duplicate primary route / provider for a model | ConfigurationException | ProviderRoutingPlan.Builder.validate() when the Tramai bean builder runs |
| @AiTool method with != 1 parameter | IllegalStateException | AiToolScanner at Tramai construction |
| @AiTool parameter not a data class | IllegalStateException | AiToolScanner at Tramai construction |
| @AiService interface not found | No bean registered (silent) | Bean-definition phase |
Testing strategy
TramaiAutoConfigurationis verified through Spring Boot integration tests that assert correct bean registration, property binding, and tool scanningVaultSecretValueResolverandAwsSecretsManagerSecretValueResolverare tested against embedded HTTP servers and mock clients- Core runtime correctness (proxy dispatch, retry, structured output) is verified in
tramai-engineandtramai-structured— the Spring adapter is a thin wiring layer and does not re-test engine behavior
