TypeSafe decision services (experimental)
TypeSafe is the provider; Jev is its default decision model. The integration adapts the
org.springaicommunity:typesafe-java-sdk primitives to Embabel’s provider-neutral
DecisionService and ClassificationService contracts.
Use this integration when an application needs closed-category classification or proposition assessment without making the decision model a chat model. The result is evidence. Routing, authorization, thresholds and proposition revision remain application policy.
Architecture
DecisionService extends ClassificationService, so every TypeSafe decision service supports
both operations and reports ModelType.DECISION. A TypeSafe service does not implement
LlmService or Spring AI’s ChatModel.
classDiagram
ModelMetadata <|-- ClassificationService
ClassificationService <|-- DecisionService
DecisionService <|.. TypeSafeDecisionService
TypeSafeModelFactory --> DecisionService : builds by model name
TypeSafeDecisionService --> TypeSafeClient : internal SDK boundary
The public factory returns framework services rather than native clients. This keeps SDK request, answer and exception types behind the provider adapter and gives other implementations the same service boundary.
flowchart LR
App[Embabel workflow] --> Service[DecisionService]
Service --> Observe[ObservedDecisionService]
Observe --> Adapter[TypeSafeDecisionService]
Adapter --> Client[TypeSafe SDK client]
Client --> Guard[Guarded TypeSafe API]
Guard --> HTTP[Spring RestClient]
HTTP --> Provider[TypeSafe]
Dependencies and Spring configuration
Add com.embabel.agent:embabel-agent-starter-typesafe to a Spring Boot application. The starter
brings in embabel-agent-typesafe-autoconfigure and embabel-agent-typesafe.
Supply TYPESAFE_API_KEY or configure:
embabel:
agent:
platform:
models:
typesafe:
api-key: $\{TYPESAFE_API_KEY}
model: jev-latest
A nonblank environment value takes precedence over the property and is resolved for every request. Credential rotation therefore takes effect without rebuilding the service. Startup fails when neither source supplies a credential.
The complete property set under embabel.agent.platform.models.typesafe is:
| Property | Default | Purpose |
|---|---|---|
api-key | none | Fallback credential when TYPESAFE_API_KEY is absent or blank |
base-url | https://api.typesafe.ai | Provider or gateway endpoint |
model | jev-latest | Model identifier or alias used by the default service |
max-response-bytes | 1048576 | Maximum response size decoded by the guarded provider boundary |
Auto-configuration contributes the following beans:
TypeSafeModelsConfigis itself aTypeSafeModelFactory, following the same configuration inheritance pattern as the Anthropic and OpenAI providers. Inject it by theTypeSafeModelFactorytype.typeSafeDecisionServiceis built for the configured model. Because it implementsDecisionService, it is also injectable asClassificationService.typeSafeDefaultCandidateofferstypeSafeDecisionServiceas the default of the decision and classification families. See .- One
DecisionServicebean for each entry underservices, described below.
An application bean named typeSafeDecisionService replaces the default service, and the
auto-configuration then contributes no default candidate. Entries under services still register
their named services, and they need the credential. With the application bean and no entries under
services, the factory configuration does not run and no credential is required. Other decision
providers and additional named TypeSafe services can coexist in the same context.
Named services
Each entry under embabel.agent.platform.models.typesafe.services registers a DecisionService
bean for its model. The entry’s key is the bean name and the registration name.
embabel:
agent:
platform:
models:
typesafe:
services:
jev:
model: jev-latest
jev-fast:
model: jev-fast
model is the only key of an entry. Named services share the credential, base URL and response
limit of the top-level properties, and the typeSafeDecisionService bean stays registered. A bean
the application defines under an entry’s key replaces that entry, and one INFO line names it.
Startup fails when an entry has a blank model or an unknown key, and the error names the property.
Unknown keys are detected in configuration files and other property sources. Environment variables
and system properties are not checked for them.
An application that supplies its own TypeSafeModelFactory bean also keeps it: auto-configuration
then creates no factory, no default service, and needs no API key. Named services come from the
auto-configured factory, so a configured services entry is not registered when the application
builds its own factory instead.
Classification and proposition assessment
The starter supports ordinary constructor injection:
public record SupportTriage(DecisionService decisions) {
public ClassificationResult classify(String message) {
return decisions.classify(
message,
ClassificationSpec.builder()
.asking("Which team should handle this request?")
.category("billing", "Payments, invoices and refunds")
.category("support", "Product use and technical support")
.build());
}
public PropositionResult isUrgent(String message) {
return decisions.assess(
new PropositionRequest(message, "This request needs urgent attention"));
}
}
Classification sends a Jev choice whose labels and descriptions come only from the request’s
closed category set. The adapter validates that the response distribution has exactly the same
support and sums to one within numerical tolerance before returning a selection. It preserves the
provider’s reported confidence without treating it as a calibrated probability of correctness.
A flat distribution with zero confidence becomes Inconclusive; an unknown category, malformed
distribution or unexpected answer type becomes Failure(INVALID_RESPONSE).
The adapter never invents NoMatch. If none-of-the-above is meaningful, declare it as an explicit
category with a stable ID and map it in application code. This keeps a successful provider judgment
distinct from insufficient evidence and operational failure.
Proposition assessment sends a Jev noul. Jev defines 0.5 as undecided, so that exact value maps
to PropositionResult.Inconclusive. Values above 0.5 produce Answered(true, ...); values below
it produce Answered(false, ...). The original probability is retained as pTrue. The adapter does
not add an application acceptance threshold.
Successful results carry the model reported by TypeSafe, the TypeSafe provider name and the
provider request identifier when present. If a successful response omits its resolved model name,
provenance falls back to the configured model alias.
Plain Java and multiple models
TypeSafeModelFactory works without a Spring context. Supply credentials through a
Supplier<String> so the latest key is used for every request:
/**
* Creates a Jev decision service with credentials resolved for each request.
*
* @param apiKey supplier of a nonblank API key
* @return an observed provider-neutral decision service
*/
public DecisionService decisionService(Supplier<String> apiKey) {
return new TypeSafeModelFactory(apiKey).build();
}
One factory can build any number of independently identified decision services:
/**
* Builds an independent service for an explicit model and assesses one proposition.
*
* @param factory shared TypeSafe provider configuration
* @param model model identifier or alias
* @param message state to assess
* @return provider-neutral evidence with TypeSafe provenance
*/
public PropositionResult assessWithModel(
TypeSafeModelFactory factory, String model, String message) {
return factory.build(model)
.assess(
new PropositionRequest(
message, "This message requires urgent attention"));
}
build() selects jev-latest; build(model) selects another TypeSafe model identifier or alias.
Services share the factory’s guarded transport and observation registry. Create another factory for
a different endpoint or credential source. Factory construction and build do not call the provider
or validate credentials.
The factory also implements ByokFactory<DecisionService>. buildValidated() checks the current
credential with TypeSafe’s model-listing endpoint and returns the configured default decision
service. Provider validation failures become InvalidApiKeyException with the sanitized transport
exception retained as the cause. Raw response content stays inside the guarded boundary:
/**
* Validates a caller-supplied credential and returns a ready Jev decision service.
*
* @param apiKey caller-supplied credential
* @return the default decision service after provider validation
*/
public DecisionService validatedDecisionService(String apiKey) {
return new TypeSafeModelFactory(() -> apiKey).buildValidated();
}
In Spring, configure named services under services, or declare additional DecisionService beans
from the configured TypeSafeModelFactory, and inject them with @Qualifier.
Decision specs
A TypeSafe service runs a whole decision spec in one Jev systemOne call. It supports proposition,
choice and rating questions. Questions go
to Jev under the keys q1 to qN in spec order. A question assess and rate each make one call
for one question. A choice question asked on its own goes through classify, one call under the key
classification with the question’s instructions and categories.
A proposition becomes a Jev noul, a choice a choice and a rating a score. The score value is
an expected level index, and the answer carries a distribution over the levels and no selected level.
gives the full evidence mapping, the answer rules and the
diagnostics.
A provider failure fails the whole request with UNAVAILABLE, and a response that cannot be read
fails it with INVALID_RESPONSE. Each failed call logs one WARN under com.embabel.agent.typesafe
with the cause category and HTTP status class. SDK retries stay off, so each question set is one call.
Transport and failure boundaries
TypeSafeModelFactory accepts an optional RestClient.Builder and ObservationRegistry. Spring
prefers the qualified aiModelRestClientBuilder, then a unique application builder, and otherwise
uses a guarded fallback transport. Supplied builders are cloned, preserving proxy, TLS,
interceptors and HTTP instrumentation without mutating shared application state.
The application owns timeouts and redirect policy for a supplied builder. With Embabel’s shared
Netty builder, configure embabel.agent.platform.http-client.connect-timeout and
embabel.agent.platform.http-client.read-timeout. The fallback uses finite ten-second connect and
read timeouts and does not follow redirects. Calls are synchronous; callers can run them on Java 21
virtual threads when concurrency requires it. The API exposes no coroutine or suspend contract.
The guarded boundary disables SDK retries, bounds decoded responses, rejects duplicate JSON fields, trailing content, scalar coercion and invalid numeric evidence, and sanitizes transport and decoding failures before they can reach framework logs. It uses a private strict mapper because the TypeSafe wire contract must not change with application Jackson modules.
Provider connection failures, timeouts and HTTP failures map to Failure(UNAVAILABLE). Invalid or
unreadable successful responses map to Failure(INVALID_RESPONSE). These results contain no raw
response body, provider message, throwable or credential. Cancellation propagates rather than being
converted to model evidence, and the adapter never changes thread interruption state.
Logging and observability
Factory initialization and successful credential validation are logged at INFO. Initialization reports whether transport comes from the application or the fallback, and whether observations are active. DEBUG records service creation using the default or an explicit model selection, and credential-validation start and cancellation. Model names, endpoints and credentials are omitted.
Enable com.embabel.agent.typesafe at DEBUG for factory and provider diagnostics, and
com.embabel.common.ai.model.observation at DEBUG for classification and proposition outcomes.
Provider completion logs include the operation, outcome and HTTP status family. Failed credential
validation and failed decisions are logged at WARN. Provider validation failures retain and log
the sanitized transport exception; credential-supplier failures log only the exception type because
supplier messages and causes may contain secrets.
Each call has three observable layers when the application supplies Micrometer handlers:
| Observation | Owner | Bounded tags |
|---|---|---|
embabel.ai.classification or embabel.ai.decision | Framework service contract | Operation and result outcome |
embabel.typesafe.request | Provider boundary | Provider operation, success/failure and HTTP status family |
http.client.requests | Spring HTTP client | Application-owned HTTP instrumentation |
The framework observation remains the parent of provider and HTTP observations. Debug completion
logs and warning logs use fixed operation and failure categories. Inputs, category descriptions,
propositions, responses, credentials and raw exceptions are never logged or attached as tags.
The framework observation decorators isolate observation-handler and completion-logging failures
from the provider result and restore the caller’s observation scope. Registering an ObservationRegistry alone does not export metrics or traces; the application
must install the relevant handlers and exporters.
Experimental status
The API is experimental so real integrations can shape it, but the implementation is maintained as production code. Promotion requires stable Java and Kotlin consumption, provider and framework observability, credential rotation, multiple-model wiring, strict response validation, sanitized failure behavior, assembled starter tests and a clean repository build.
The upstream SDK source is Spring AI Community TypeSafe 0.2.0.




