LLM Batteries
An LLM battery is a complete DispatchExecutorFn — the function the TurnRunner calls to run one model turn. You construct one, hand it to the runner, and it owns the whole job: history rendering, the model call, pulling tool calls and reasoning back out of the response, persisting the assistant message. Construct, hand over, done. There is no "and then you also wire up X." X is in the box — and a battery for the common case being in the box is the point of this whole section, not an apology for it.
So the only real question is which box, and the answer is decided entirely by where your model runs and how it talks back. There are ten, and they are not interchangeable flavors of the same thing — they are genuinely different runtimes with genuinely different failure modes, which is the whole reason this section exists.
The split that actually matters: wire-format vs. native API
Forget "cloud vs. local" for a second — that's the obvious axis and it's not the load-bearing one. The axis that decides how much work you inherit is whether the runtime hands back structured output or text.
Wire-format batteries — OpenAI, Anthropic Messages, Gemini, Bedrock Converse, Ollama, WebLLM — talk a protocol that carries tool calls as a structured field. The model (or the server in front of it) did the work of separating "I want to call get_weather" from prose, and you get structured tool calls back rather than a string to dissect. (The exact shape varies — OpenAI/WebLLM hand back Chat-Completions tool_calls; Anthropic Messages returns tool_use blocks; Gemini returns functionCall parts correlated by tool NAME rather than by id; Converse returns {toolUse} content blocks; Ollama's native /api/chat returns native tool-call objects with its own history labels — but in every case something upstream already paid the structuring tax.) These are the easy ones, and that's the whole reason to use them.
Native-API on-device batteries — LiteRT-LM and transformers.js — hand back text. That's it. A local ONNX or .litertlm model emits a string, and somewhere in that string is <tool_call>{"name":...}</tool_call> or call:get_weather{city:Paris} or functools[...] or a <think> block — in a format that depends on the model family, that the chat template only half-describes, and that you have to parse back out yourself. There is no structured-output contract. There is a decoder and a hope.
The second axis: whose wire are you actually speaking?
There is a quieter distinction among the structured-output batteries, and it decides what a result means rather than how much work you inherit.
Most vendors are reachable two ways: through their own API, or through an OpenAI-compatible façade in front of it. The façade is genuinely convenient — one adapter, one shape, every model. But something has to translate your OpenAI-shaped request into what the vendor actually accepts, and that translation is invisible in the response. A gateway merging two consecutive user turns to satisfy a strict-alternation check produces a 200 byte-identical to the one you would get from a vendor that never minded. You cannot tell the repair from the tolerance.
That is fine right up until the moment you need to know which one it was: auditing a wire shape, filing a bug against a vendor, or deciding whether an ordering rule is describing a real constraint. Gemini generateContent and AWS Bedrock Converse exist for those moments. They speak the vendor's own grammar — contents[] and parts[] for Google, typed content blocks for Bedrock — so what you send is what the vendor receives.
Worth knowing before you reach for Converse to settle an argument: Converse is itself a translator. AWS accepts one content-block schema and reshapes it per model family, so a Converse verdict is a verdict about AWS's translation layer, not about the model underneath. That is exactly why its alternationPolicy is selectable — 'reject' sends history untouched so Converse's own error can surface. The full doctrine is in Which API surface a rule applies to, and it is worth reading before you trust any conclusion drawn through a gateway.
That hope is not left to chance. The two text-out batteries share a deliberate chat_common contract — a parser layer, a portable generation vocabulary, and a normalized lifecycle hook surface — so the runtime- agnostic parts of a turn are written once and behave identically across both. That's its own page: The shared contract.
"Text-out" is about output, not input
Do not read "text-out" as "text-only." transformers.js accepts rich multimodal input — image and audio flow through the model's processor and the model genuinely perceives them (Gemma-4 transcribes speech and names image colours; it'll do both in one turn). LiteRT-LM exposes the same image/audio modality flags and the adapter maps the content items, but the browser matrix currently treats that path as a probe — whether a given .litertlm honors it is runtime/model behavior, not a guarantee. And media output is wired too, opt-in: an extractMediaOutputs hook lets a wrapped media-emitting model surface generated audio/image as assistant Message.attachments. Default off → text output, byte-for-byte unchanged. The constraint is the default output shape of the tested chat checkpoints, not a ceiling on the battery. See the Transformers.js page for the input + output mechanics.
Picking one
| If your model runs… | …and talks | Use |
|---|---|---|
| Behind any OpenAI-compatible HTTP endpoint | wire (structured) | OpenAI Chat Completions |
| Behind OpenAI's Responses API | wire (flat input items) | OpenAI Responses |
| Behind Anthropic's Messages API or an Anthropic-shaped gateway | wire (first-party Messages) | Anthropic Messages |
At Google's own generateContent | wire (contents[] / parts[]) | Gemini generateContent |
| At Bedrock's own Converse | wire (typed content blocks) | AWS Bedrock Converse |
| In a local/cloud Ollama daemon | wire (native /api/chat) | Ollama |
| In the browser, MLC-compiled | wire (structured) | WebLLM |
Behind the claude binary you already have installed | subprocess (stream-json) | Claude Code CLI |
In the browser, Google .litertlm on WebGPU | text (parsed) | LiteRT-LM |
| In Node OR the browser, ONNX | text (parsed) | Transformers.js |
Two of those rows overlap on purpose. Gemini and Bedrock models are both reachable through an OpenAI-compatible gateway with the first row's battery, and that is the easier path — reach for the native rows when you need what the vendor received to be what you sent. See Which API surface a rule applies to.
Every one satisfies the same DispatchExecutorFn contract and shares the ADK-control surface (history buckets, context-window accounting, thought surfacing, the STASH_KEY per-iteration override). They differ in the runtime they drive and the trade-offs they hand you — which is what the rest of this section is about. The exact options, exceptions, and STASH_KEY for each are in Assembly → LLM batteries; these pages tell you what each one is, which models we actually ran against it, and where the bodies are buried.