TramAI - governed AI workflows for Java and Kotlin

Module: tramai-spring

One-liner: Spring Boot auto-configuration that wires Tramai's Tramai runtime, scans @AiService interfaces, discovers @AiTool beans, and binds tramai.* application properties — all with zero manual bean declarations. Module type: framework-adapterSource files: compatibility facade — since 0.6.0 the Spring integration itself lives in tramai-spring-core (21 sources); tramai-spring declares no production sources of its own and re-exports that artifact Test files: 2 integration tests (tramai-spring-core owns the contract tests) Build: dev.tramai:tramai-spring:0.6.0 (compatibility facade) — new applications depend on dev.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 from tramai-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.yml under a single tramai: namespace, bound via @ConfigurationProperties
  • Zero boilerplate — @AiService interfaces are discovered by classpath scanning and registered as singleton FactoryBean proxies; @AiTool methods 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 Tramai bean is the same dev.tramai.standalone.Tramai that 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


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 suspend or 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 var properties — Spring Boot binds via setter reflection; Kotlin data classes with var fields are the idiomatic Spring Boot pattern
  • Nested data classes — Resilience, Cost, Cache, and Security each map to their own subsection (plus FallbackRoute for 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) and tramai.secrets.* by the secret-backend modules (VaultSecretProperties, AwsSecretsManagerProperties); tramai-spring-core's own properties cover defaultProvider, models, fallbacks, resilience, cost, cache, and security
  • Mutual-exclusion enforcement — SpringSecretResolution.resolve(directValue, secretRef, fieldName, resolver) in tramai-spring-core fails via check(...) (IllegalStateException) at startup if both a direct value and its *-secret-ref are 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.ollama simply skips Ollama registration)

AiServiceBeanDefinitionRegistrar — @AiService scanning

This BeanDefinitionRegistryPostProcessor runs during Spring's bean-definition phase, before any beans are instantiated:

  1. Checks that AutoConfigurationPackages is available (the Spring Boot application has a base package)
  2. Creates a ClassPathScanningCandidateComponentProvider filtered to interfaces annotated with @AiService
  3. Iterates all auto-configuration base packages, finds candidate @AiService interfaces
  4. For each, registers a RootBeanDefinition(AiServiceFactoryBean::class.java) with the interface class as a constructor argument
  5. The AiServiceFactoryBean lazily resolves the Tramai bean from the BeanFactory and calls tramai.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:

  1. Iterates all bean names (skipping the "tramai" bean to avoid circular resolution)
  2. For each bean, checks whether any method carries @AiTool
  3. Validates the tool method signature (exactly one parameter, must be a data class)
  4. Wraps each @AiTool method into a MethodBackedTramaiTool that delegates to the bean via KFunction.call() or callSuspend()
  5. Returns the list to TramaiAutoConfiguration, which passes it to builder.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

SituationExceptionWhen
apiKey + apiKeySecretRef both setIllegalStateExceptionSpringSecretResolution.resolve() (tramai-spring-core) at startup, from the owning provider/secret adapter's auto-configuration
Vault enabled but baseUrl missingIllegalStateExceptionVaultSecretValueResolverAutoConfiguration (tramai-spring-secrets-vault) at startup
Vault token unresolvableIllegalStateExceptionVaultSecretValueResolverAutoConfiguration at startup
AWS enabled but a required property missingIllegalStateExceptionAwsSecretsManagerSecretValueResolverAutoConfiguration (tramai-spring-secrets-aws) at startup
No SecretValueResolver could resolve a *-secret-refIllegalStateExceptionSpringSecretResolution.resolve() at startup
Unknown provider in fallback routeIllegalStateExceptionTramaiAutoConfiguration at startup
Duplicate primary route / provider for a modelConfigurationExceptionProviderRoutingPlan.Builder.validate() when the Tramai bean builder runs
@AiTool method with != 1 parameterIllegalStateExceptionAiToolScanner at Tramai construction
@AiTool parameter not a data classIllegalStateExceptionAiToolScanner at Tramai construction
@AiService interface not foundNo bean registered (silent)Bean-definition phase

Testing strategy

  • TramaiAutoConfiguration is verified through Spring Boot integration tests that assert correct bean registration, property binding, and tool scanning
  • VaultSecretValueResolver and AwsSecretsManagerSecretValueResolver are tested against embedded HTTP servers and mock clients
  • Core runtime correctness (proxy dispatch, retry, structured output) is verified in tramai-engine and tramai-structured — the Spring adapter is a thin wiring layer and does not re-test engine behavior