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 five, 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, 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; 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.
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 |
| In a local/cloud Ollama daemon | wire (native /api/chat) | Ollama |
| In the browser, MLC-compiled | wire (structured) | WebLLM |
In the browser, Google .litertlm on WebGPU | text (parsed) | LiteRT-LM |
| In Node OR the browser, ONNX | text (parsed) | Transformers.js |
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.