Expand description
cratefield-adapter-workers-ai: the Classifier port over the
Cloudflare Workers AI binding (issue #456). The model is reached through
env.AI.run(...) — a platform binding, no API key at all — so this
adapter exists only on the Workers runtime, and it bills through the
venture’s Cloudflare account instead of to a vendor key.
It is a convenience for ventures already on Workers, explicitly NOT the
primary way in: bring-your-own-key via cratefield-adapter-typesafe is
the default Classifier. But it is the same purpose-trained model, so
profile() reports Calibration::Classifier too — the same family of
numbers, and thresholds tuned on either hold on both (unlike the
adapter-classifier-llm family; see CLASSIFIER.md).
Two shapes are load-bearing here. First, the JS binding: worker::Ai is
a wasm-bindgen handle held inside a Send + Sync trait object, which
is sound without a line of unsafe here because wasm-bindgen itself
declares JsValue Send + Sync off the atomics target (the same shape
runtime-cloudflare’s D1 wrapper uses); only the returned future needs
worker’s into_send. Second, the binding cannot be constructed off
wasm, while ask must still be testable natively — so the call goes
through the narrow AiRunner seam and tests inject a fake runner.
There is no key in this adapter, so there is nothing to leak. A Workers
AI binding error can still echo platform identifiers (account ids,
binding names), so error text only ever travels inside
ClassifierError — whose Display scrubs — and this crate’s log lines
carry outcome labels, never raw binding text. The same binding also
carries the Speech port (issue #861) over its own SpeechRunner seam
— see the speech module.
§cratefield-adapter-workers-ai
Classifier port over the Cloudflare Workers AI binding for the Cratefield
harness (issue #456). The model is reached through env.AI.run(...) — a
platform binding, no API key at all — so this adapter only exists on the
Workers runtime, and it bills through the venture’s Cloudflare account rather
than to a vendor key.
This is a convenience for ventures already on Workers, explicitly not the
primary way in: bring-your-own-key via cratefield-adapter-typesafe is the
default Classifier, because it runs on every runtime through the HttpClient
port. This adapter’s profile() still reports Calibration::Classifier — it
is the same purpose-trained model (typesafe/jev) behind a different
transport, so its numbers are the same family as the direct adapter’s, and a
threshold tuned on either holds on both (unlike the
adapter-classifier-llm family; see CLASSIFIER.md).
§Usage
use cratefield_adapter_workers_ai::WorkersAi;
// `AI` is the conventional binding name; a missing or wrong-typed binding
// yields `None`, meaning the runtime simply does not provide the port.
// Never a panic.
if let Some(classifier) = WorkersAi::from_env(&env, "AI") {
runtime = runtime.classifier(classifier);
}The model id defaults to typesafe/jev; override it (and the state ceiling)
with WorkersAi::from_env_with_model or WorkersAi::with_runner.
§The context window
Workers AI documents a 32,000-token context window for this model. At the
port’s conservative ~4 chars/token that is about 128,000 chars, but the
questions share the window too, so the default state ceiling is core’s
DEFAULT_MAX_STATE_CHARS (96,000 chars, roughly 24,000 tokens), leaving
roughly a quarter of the window for the question set. The ceiling is
overridable per constructor through a ClassifierProfile.
§The wire format
The request and response documents are this crate’s assumption, not
something this repository’s vendors document: the same model as the direct
adapter-typesafe adapter, so the same documents travel, one level down in
the binding instead of in an HTTP body.
- Input:
{"state": "...", "questions": [{"id", "kind", "instructions", "criteria" (choice), "levels" (score)}]}— all questions of the call in onerun(). - Output:
{"answers": [{"id", "value", "probabilities"}]}wherevalueis a criterion label, a number on the scale, ortrue/false.
§What is refused, what is retryable
The adapter never invents an answer: a missing question id, a label the
question never offered, or a distribution that is not normalisable is
ClassifierError::Rejected. Probabilities that do not sum to one are
normalised (the model’s softmax is not obliged to land exactly on one);
non-finite or non-positive totals are refused.
There is no key here, so there is nothing to leak — but a Workers AI binding
error can echo platform identifiers (account ids, binding names), so error
text only ever travels inside ClassifierError (whose Display scrubs) and
this crate’s log lines carry outcome labels, never raw binding text.
Structs§
- AiBinding
- The real runner: the
env.AIbinding, obtained fromworker::Env::ai(..). - Speech
Binding - The real runner: the
env.AIbinding. Nounsafeis needed to hold the JS handle in aSend + Syncstruct —wasm-bindgendeclaresJsValueSend + Sync; only the call futures are!Send, whichworker’ssendmodule fixes at the call site. Off wasm there is noEnv— hence the seam. - Workers
Ai Classifierover the Cloudflare Workers AI binding.- Workers
AiSpeech Speechover the Cloudflare Workers AI binding. The runner is a type parameter so tests can substitute the binding; venture code writesWorkersAiSpeech::from_env(..)and never names the parameter.
Enums§
- Speech
Model - The text-to-speech model a
WorkersAiSpeechvoices with: Deepgram’s Aura, English-only on both generations. - Transcription
Model - The speech-to-text model a
WorkersAiSpeechtranscribes with.Nova3is Deepgram’s multilingual Nova-3 (words with confidence, no clip length); the Whisper models areOpenAI’s (Whisperwithout confidence,WhisperLargeV3Turbowith language and duration).
Constants§
- DEFAULT_
MODEL_ ID - The model this adapter runs by default: the same purpose-trained
classifier the direct
adapter-typesafeadapter calls over HTTP, one transport further away. Override it per constructor. - WORKERS_
AI_ CONTEXT_ TOKENS - The context window Workers AI documents for the model, in tokens. At the
port’s conservative ~4 chars/token the default state ceiling
(
DEFAULT_MAX_STATE_CHARS, 96,000 chars) spends roughly 24,000 tokens and leaves roughly a quarter of the window for the question set. - WORKERS_
AI_ MAX_ AUDIO_ BYTES - The most audio one transcription may carry when no
WorkersAiSpeech::with_limitsoverride says otherwise: 1 MiB, well under the port’s 25 MiB default. The binding takes the clip as a JSON number array — roughly 32serde_json::Valuebytes per audio byte beforeserde_wasm_bindgencopies it into a JavaScript array again — so a 25 MiB clip would ask a 128 MB Workers isolate for ~800 MB twice over and be killed mid-request instead of refused with a typedSpeechError::LimitExceeded. The other limits stay at the port defaults.
Traits§
- AiRunner
- The runner seam: one call to the binding, all questions at once.
- Speech
Runner - The speech seam, the sibling of
AiRunner: one call to the binding, a JSON document in and a JSON document or an audio byte stream out. The error type isSpeechErroritself, so a fake can exercise every mapping.