TramAI

Module: tramai-anthropic

One-liner: Provider for Anthropic's Messages API (Claude models). Module type: providerSource files: 1 — AnthropicProvider.kt (192 LOC) Test files: 1 Group: dev.tramai, Version: 0.3.1


L1: Quick Start (30-second read)

What

tramai-anthropic is a Tramai provider module that implements ModelProvider and StreamCapable over Anthropic's Messages API. It translates normalized Tramai ModelRequest objects into Anthropic API calls and maps the responses back into ModelResponse and StreamChunk values.

The module exposes a single public class:

TypeRole
AnthropicProviderModelProvider + StreamCapable for Claude models

Why

Anthropic's API has a different shape from OpenAI's. Messages are split into a separate system parameter rather than a system role message, the content block model uses typed arrays (text, tool_use, etc.), and streaming follows a server-sent event (SSE) protocol with named events (content_block_delta, message_delta, message_start). This module encapsulates all of that so that the rest of Tramai — and your application code — never sees Anthropic-specific wire details.

It also populates the provider failure model with correct retryable flags, parses Retry-After headers, and maps Anthropic stop reasons to the normalized FinishReason enum.

When

Use tramai-anthropic when:

  • Your application uses Claude models (sonnet, haiku, opus) and you want typed, structured interactions through Tramai's @AiService / @Operation path.
  • You want streaming token-by-token output from Claude without manually consuming Anthropic's SSE endpoint.
  • You already have Anthropic API access and want a consistent provider abstraction alongside other models (OpenAI, Ollama).

Do not use tramai-anthropic if:

  • You need native Anthropic-specific features not exposed by the provider (e.g., custom /v1/complete endpoints, pre-2023 API versions, or Anthropic-specific tools/metadata that the normalized ModelRequest/ModelResponse contract doesn't carry).

L2: Usage Guide (5-minute read)

Minimum: standalone completion

import dev.tramai.anthropic.AnthropicProvider
import dev.tramai.core.model.Message
import dev.tramai.core.model.MessageRole
import dev.tramai.core.model.ModelRequest

val provider = AnthropicProvider(
    apiKey = System.getenv("ANTHROPIC_API_KEY"),
)

val response = provider.complete(
    ModelRequest(
        model = "claude-sonnet-4-20250514",
        messages = listOf(
            Message(MessageRole.SYSTEM, "You are a concise assistant."),
            Message(MessageRole.USER, "What is the capital of France?"),
        ),
        maxTokens = 512,
    ),
)

println(response.content)        // "Paris."
println(response.modelUsed)      // "claude-sonnet-4-20250514"
println(response.finishReason)   // STOP

The provider accepts system instructions through the standard SYSTEM-role Message — it automatically extracts it from the conversation list and sends it in the dedicated system parameter.

Advanced: custom transport and version pinning

import dev.tramai.anthropic.AnthropicProvider
import dev.tramai.core.model.*
import java.net.http.HttpClient

val provider = AnthropicProvider(
    apiKey = System.getenv("ANTHROPIC_API_KEY"),
    baseUrl = "https://api.anthropic.com",          // default; change for proxies
    anthropicVersion = "2023-06-01",                // pin the API version header
    httpClient = HttpClient.newBuilder()             // custom HTTP config
        .connectTimeout(java.time.Duration.ofSeconds(10))
        .build(),
)

val result = provider.complete(
    ModelRequest(
        model = "claude-sonnet-4-20250514",
        messages = listOf(Message(MessageRole.USER, "Hello")),
        temperature = 0.7,
        maxTokens = 2048,
        timeoutMillis = 30_000,
    ),
)

Registration with Tramai engine

import dev.tramai.standalone.Tramai

val tramai = Tramai.create {
    provider(AnthropicProvider(System.getenv("ANTHROPIC_API_KEY")), name = "anthropic")
    model("claude-sonnet-4-20250514", "anthropic")
}

All the usual Tramai configuration paths work — YAML config, programmatic registry, Spring auto-configuration.


L3: Architecture & Mechanics (15-minute read)

Messages API integration

AnthropicProvider maps the Tramai ModelRequest to the Anthropic Messages API (POST /v1/messages) with the following mapping:

Tramai fieldAnthropic API location
request.modelmodel in JSON body
request.maxTokensmax_tokens (default: 1024)
request.temperaturetemperature
request.timeoutMillisjava.net.http.HttpRequest timeout via applyTramaiTimeout()
messages (non-system)messages[] array with role and content
messages (system role)system top-level parameter (extracted, not sent in messages[])

HTTP headers:

  • x-api-key — the API key passed at construction
  • anthropic-version — defaults to 2023-06-01, configurable via the anthropicVersion constructor parameter
  • Content-Type: application/json

The response is parsed from the JSON body:

{
  "content": [{ "type": "text", "text": "..." }],
  "usage": { "input_tokens": 19, "output_tokens": 9 },
  "stop_reason": "end_turn",
  "model": "claude-sonnet-4-20250514"
}

If the response contains no type: "text" content block (e.g., only tool_use blocks), a ProviderException is thrown with a clear message: "Anthropic response did not contain a text content block".

Stop reason mapping:

Anthropic stop_reasonTramai FinishReason
end_turnSTOP
max_tokensLENGTH
any other valueOTHER

Streaming support

AnthropicProvider implements StreamCapable, so it can be used both for blocking completions and for streaming. Streaming is enabled automatically when the engine's operation level or calling code requests streaming semantics.

Wire protocol:

  1. A "stream": true flag is injected into the request payload.
  2. The SSE endpoint returns text/event-stream lines.
  3. Each line is consumed with HttpResponse.BodyHandlers.ofLines().

Event processing:

SSE eventStreamChunk emittedAction
event: content_block_deltaStreamChunk.Token(text)Appends the delta text, emits a token chunk
event: message_start(none)Captures initial input_tokens / output_tokens usage from the nested message.usage
event: message_delta(none)Updates output_tokens from the event's usage block
Stream endStreamChunk.Complete(fullText, usage)Emitted after the SSE stream is exhausted
Transport errorStreamChunk.Error(cause)Emitted if the line iterator throws

The provider accumulates all delta text fragments into a StringBuilder so the terminal Complete chunk carries the full response alongside final usage metrics.

Cancellation propagates: because the stream is a kotlinx.coroutines.flow.Flow, upstream cancellation (e.g., timeout or consumer cancellation) closes the underlying SSE connection.

Error model

AnthropicProvider uses the shared Tramai failure helpers from tramai-core:

HTTP error responses

Non-2xx status codes are handled by providerHttpFailure():

throw providerHttpFailure(
    providerName = "Anthropic",
    statusCode = response.statusCode(),
    body = response.body(),
    retryAfterHeader = response.headers().firstValue("Retry-After").orElse(null),
)

This produces a ProviderException with:

PropertyValue
statusCodeThe HTTP status from the API (e.g., 429, 500, 503)
retryabletrue for 408, 425, 429, 500, 502, 503, 504; false otherwise
retryAfterMillisParsed from the Retry-After header (supports both seconds and RFC 1123 dates)
message"Anthropic returned HTTP {status}: {body}"

Transport errors

IOExceptions at the transport layer are caught and wrapped by providerTransportFailure():

Root causeretryable
HttpTimeoutExceptiontrue
ConnectExceptiontrue
IOExceptiontrue
Other Throwablefalse

Response parsing errors

If the API returns a successful HTTP status but the content array contains no type: "text" block (for example, a tool_use-only response), the provider throws a ProviderException with message "Anthropic response did not contain a text content block" and retryable = false.

Streaming error propagation

Streaming errors are not thrown — they are emitted as StreamChunk.Error(cause) terminal events on the flow. This lets consumers decide how to handle partial responses. The cause is always a TramaiException (usually a ProviderException).

Exception hierarchy (from tramai-core)

TramaiException (RuntimeException)
 ├── ProviderException       ← raised by AnthropicProvider
 ├── StructuredOutputException
 ├── ConfigurationException
 ├── TimeoutException
 ├── ProviderCapabilityException
 ├── CircuitBreakerOpenException
 └── TokenBudgetExceededException

All ProviderException instances raised by this module carry retryable semantics that the Tramai engine respects when computing retry policy.