Cloudflare Workers AI decision services (experimental)
Cloudflare Workers AI runs System One decision models behind its REST API. It hosts the CLEF models itself and runs TypeSafe’s Jev as a third-party model. The Cloudflare integration is the System One client () with a binding for that API.
| Catalog name | Model id | Hosted by |
|---|---|---|
clef | @cf/cloudflare/clef | Cloudflare |
clef-flash | @cf/cloudflare/clef-flash | Cloudflare |
clef-omni | @cf/cloudflare/clef-omni | Cloudflare |
cloudflare-jev | typesafe/jev | TypeSafe, as a third-party model |
clef is a 27B model, clef-flash a faster 9B model, and clef-omni a mixture-of-experts model that also reads audio.
This integration sends text input only.
Cloudflare bills third-party models such as Jev through AI Gateway, from gateway credits or a provider key stored in the gateway.
Without either, Cloudflare refuses Jev calls and the request fails.
The CLEF models work on the same token.
The CLEF weights are open, and Ollama serves them through the plain System One client. That is the quickest way to try CLEF with no Cloudflare account. walks through it.
Cloudflare’s CLEF schema truncates long text state to fit the model’s context window, which is 65,536 tokens for clef.
A request within the schema limits can still lose text to truncation, so check retention and quality on your own inputs before relying on the results.
Dependencies and Spring configuration
Add com.embabel.agent:embabel-agent-starter-cloudflare to a Spring Boot application.
The starter brings in embabel-agent-cloudflare-autoconfigure, embabel-agent-cloudflare and embabel-agent-systemone.
Supply CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID, or configure:
embabel:
agent:
platform:
models:
cloudflare:
api-key: $\{CLOUDFLARE_API_TOKEN:}
account-id: $\{CLOUDFLARE_ACCOUNT_ID:}
The api-key property holds a Cloudflare API token, and CLOUDFLARE_API_TOKEN sets the same value.
The token needs Workers AI access on the account. A nonblank environment value takes precedence over its property. Both token sources are read through the Spring environment on every call, so a rotated token takes effect without rebuilding the service. The account id is read once, at startup. Startup fails without a token or an account id, and when the account id is not 32 hexadecimal characters.
Property under embabel.agent.platform.models.cloudflare | Default | Purpose |
|---|---|---|
api-key | none | The API key to use when CLOUDFLARE_API_TOKEN is absent or blank |
account-id | none | The account to use when CLOUDFLARE_ACCOUNT_ID is absent or blank |
max-response-bytes | 1048576 | The largest response body that is read |
Auto-configuration contributes these beans:
CloudflareModelsConfig, which is itself aCloudflareModelFactory. Inject it by theCloudflareModelFactorytype.- One
DecisionServicebean per model inmodels/cloudflare-models.yml, namedclef,clef-flash,clef-omniandcloudflare-jev. Each is also injectable asClassificationService. An application bean or alias with one of those names replaces that model. The alias also works in default and role properties for both service families. cloudflareModelsRegistrar, which defines the model beans, andcloudflareModelsInitializer, theProviderInitializationthat reports them.cloudflareDefaultCandidate, which offersclefas the default of the decision and classification families when the starter definesclef.
A default candidate applies only when it is the only one.
With the TypeSafe starter on the classpath too, neither jev-latest nor clef becomes a family default, and defaultService() fails until you set one.
Set embabel.models.decision.default and embabel.models.classification.default to the models you want.
See .
An application that supplies its own CloudflareModelFactory bean keeps it.
Auto-configuration then registers no models and needs no token.
The starter uses the qualified aiModelRestClientBuilder.
Without it, it uses a unique application RestClient.Builder, and otherwise the factory’s own client.
Using the models
Inject a model by its catalog name:
@Bean
ParcelReview parcelReview(@Qualifier("clef-flash") DecisionService clefFlash) {
return new ParcelReview(clefFlash);
}
The service answers a whole spec in one call:
public record ParcelReview(DecisionService clefFlash) {
static final ChoiceQuestionSpec STATUS =
Questions.named("status")
.choice("What happened to the parcel?")
.option("delivered", "It reached the customer")
.option("returned", "It came back to the sender")
.build();
static final RatingQuestionSpec RISK =
Questions.named("risk")
.rating("How likely is a refund claim?")
.level("low", "Low")
.level("high", "High")
.build();
public DecisionResponse review(String message) {
return clefFlash.ask(message, DecisionSpec.of(STATUS, RISK));
}
}
CloudflareModelFactory works without Spring.
Build the factory and service once and reuse them:
private final DecisionService clef;
/**
* Creates a reusable CLEF service for an account.
*
* @param accountId the Cloudflare account that runs the model
* @param apiToken supplies a token with Workers AI access for each call
*/
public CloudflareUsageExamples(String accountId, Supplier<String> apiToken) {
var factory = new CloudflareModelFactory(accountId, apiToken::get);
this.clef = factory.build();
}
/**
* Asks a spec using the shared service.
*
* @param spec the questions to ask
* @return one answer per question
*/
public DecisionResponse ask(DecisionSpec spec) {
return clef.ask("I was charged twice for one invoice.", spec);
}
build() returns clef, and build(model) returns another Workers AI model by its full id:
/**
* Builds a service for the smaller CLEF model.
*
* @param factory the account's factory
* @return the CLEF Flash service
*/
public DecisionService flash(CloudflareModelFactory factory) {
return factory.build(CloudflareModelFactory.CLEF_FLASH);
}
buildValidated() checks catalog access by searching the account’s models for clef, then returns clef.
This catalog check does not exercise inference or establish inference readiness, and it does not establish Jev availability, AI Gateway credits or provider credentials.
An already interrupted caller is cancelled before the token supplier is read, including when the token is missing or blank.
modelNames() returns names from the same clef-filtered search.
It excludes TypeSafe’s typesafe/jev and is not a complete list of models the factory can build.
A missing or refused token throws InvalidApiKeyException:
/**
* Checks a token supplied at run time and returns CLEF.
*
* @param accountId the Cloudflare account that runs the model
* @param apiToken the caller's token
* @return CLEF, once Cloudflare accepts the token
*/
public DecisionService validated(String accountId, String apiToken) {
return new CloudflareModelFactory(accountId, () -> apiToken).buildValidated();
}
How the binding maps the API
- Every call sends the token as a bearer token to
https://api.cloudflare.com/client/v4/accounts/<account id>. - A Cloudflare-hosted model runs at
/ai/run/<model id>, and the body’smodelis its short name, such asclef.build(model)throwsIllegalArgumentExceptionfor a hosted model id that doesn’t look like@cf/<author>/<model>, so a name can’t change the path. Model id validation checks the format, not the model’s capabilities: choose a System One-compatible model. The hosted route uses the CLEF request format and limits. - A third-party model runs at
/ai/run. The body is{"model": "typesafe/jev", "input": {...}}, with the System One request asinput.build(model)throwsIllegalArgumentExceptionfor a third-party model id that doesn’t look like<author>/<model>. A catalog model with such an id fails when its service bean is created, normally at startup. - A successful response has
successset to true, no errors, and the System One response inresult. Anything else fails the request withINVALID_RESPONSE. Cloudflare’s error text is never kept, since it can echo the input. - Provenance names the model Cloudflare reports, the
Cloudflareprovider and thecf-rayresponse header. - The CLEF schema takes 1 to 64 questions, 2 to 255 options per choice and 2 to 10 levels per rating.
A request to a CLEF model outside those limits throws
UnsupportedDecisionExceptionbefore the token is read or anything is sent. - A missing token fails each call with
UNAVAILABLEand causecredential, before anything is sent.
Failures, logging and observations are the System One client’s. See .
Experimental status
The Workers AI mapping is tested against Cloudflare’s published schemas with a mock server.
CloudflareClefSmokeTest in embabel-agent-cloudflare runs when CLOUDFLARE_SMOKE_TEST=true, CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID are set.
The smoke test covers the CLEF models.
It covers Jev only with CLOUDFLARE_SMOKE_JEV=true.
It asks each model a triage spec with a proposition, a choice and a rating in one call, lists the account’s CLEF models and validates the token.
Promotion needs stable Java and Kotlin use and a consumer application.




