Relay 1: a tool-calling model that reads instead of writes
An 11.5 MB model that turns a request and a list of tools into exact function calls, on the device. How it works, how we tested it against 12 other models, and where it loses.
19 min read views
Try Relay in your browserRelay 1 does one job. Given what a person said and the tools an app offers, it decides which tools to call and with which arguments. It is 22 million parameters in 11.5 MB of model files (about 14 MB with its tokenizer, retrieval prefilter and runtime), and it runs on Gust, our runtime: natively on x86-64 and ARM64, and in a browser tab through WebAssembly. There is no GPU, no server and no Python in the path.
Most tool-calling models are language models, so they write the call one token at a time. Relay writes nothing. It reads the request and the tool schemas together, then points at the answer. That single change is why it is small, why it cannot invent a phone number, and why it is unusually good at filling slots. It is also where its limits come from, and this post spends as long on those as on the wins.
- of model files; about 14 MB with the tokenizer, prefilter and runtime
- 11.5MB
- parameters, of which 12.5M are the encoder
- 22M
- mean over 12 evaluations. The best other on-device model scores 48.7
- 67.2
- median request on 8 threads, in WebAssembly
- 39ms
What Relay does
An app hands Relay three things: the user’s message, the tools it offers (each a name, a description and its parameters), and optional context such as today’s date. Relay returns the calls to make, or no call at all when nothing fits.
User Set an alarm for 6:45 tomorrow, then text Sam I'm running late
Tools set_alarm(time, label?), send_message(recipient, body), get_weather(city), ...
Relay set_alarm(time="2026-10-18T06:45:00")
send_message(recipient="Sam", body="I'm running late")The same machinery fills forms. In a dialogue about booking a restaurant, “a table for four at Bella Napoli at 8” becomes {restaurant: "Bella Napoli", party_size: 4, time: "20:00"}. Researchers call this dialogue state tracking, and it is the same problem as tool calling with a different name.
Everything happens locally. The user’s request is never sent anywhere, because there is nowhere for it to go.
Try it
This is Relay 1 itself, the shipped 11.5 MB model and its WebAssembly runtime, running in your browser, on as many threads as you pick under the examples (one, where the browser cannot share memory between threads). Every thread count gives the same calls; only the time changes. The demo’s tool lists are long and it reads them all, so its times are higher than Figure 11’s. Pick a flat, a phone or a car, ask it something, and watch the calls happen on the device. The words Relay read its values from are underlined, in your request and in the calls. Ask for something none of the tools covers and it makes no call, so nothing changes. Every example is one the model gets right, and “Play them all” runs them in turn.
9:30
Ask me to do something around the flat.
It is the shipped model with no tricks, so it gets things wrong too, mostly in the ways the limits describe.
Use it
Relay is a download, not a service. Each way below fetches the same files from Hugging Face, checks them against SHA-256 sums and runs the same Gust code, so the calls are the same bits whichever you pick. All of them use one small interface, the Gust model API: open a model by id, give it typed values, get typed values back. Relay takes JSON and returns JSON, and the next model will need no new code in any of them.
A request is the message, the tools and the date. A tool is a name, a description and its parameters; a parameter has a name, a type (string, integer, number, boolean, array or object), a description, whether it is required, and optionally an enum.
const tools = [
{ name: 'show_map', description: 'Shows a location on the map.',
params: [{ name: 'query', type: 'string', description: 'The place to show.', required: true }] },
{ name: 'turn_on_flashlight', description: 'Turns the flashlight on.', params: [] },
];
sealpup install relay-1
sealpup run relay-1 "Show me the Eiffel Tower on the map" --tools tools.json
# pip install sealresearch
import sealresearch as sr
relay = sr.Model.load("relay-1")
relay.run({"text": "Show me the Eiffel Tower on the map", "tools": tools})
// npm install @sealresearch/gust (@sealresearch/relay is the same with a smaller surface)
import { Model } from '@sealresearch/gust';
const relay = await Model.load('relay-1');
relay.run({ text: 'Show me the Eiffel Tower on the map', tools });
Each returns [{tool: "show_map", args: {query: "Eiffel Tower"}}] for that tool list, and [] for “Tell me a joke”. Pass the date as context: {now: "2026-10-23T09:00:00"} in the request when the message says “tomorrow”. Tools written as JSON Schema, the way OpenAI, Anthropic and MCP describe them, are not this format and need converting first (sealpup run --tools does it for you). The request reference has every field and a short converter. The demo above is the JavaScript one, running in a worker and loading the same files from this site instead of Hugging Face.
Results
We compared Relay 1 with 12 other models on 12 evaluations, and we report all of them, including the ones it loses.
The on-device models here are the reason Relay exists. Cactus’s Needle in particular showed that a tool-calling model can be small enough to ship inside an app and still be useful, and that gave us the confidence to try. Relay takes a different route to the same goal, reading instead of writing, and the comparisons below are there to show what that route buys and what it costs.
MethodHow we testedThe same inputs for every model, each in its own native tool-call format, one scorer per kind of task.
Every model saw the same inputs: the request, the tools as JSON schema, and the same system and date context. Each used its own chat template and its own native tool-call format, so no model was handicapped by a format it was not trained on.
- Decoding. The language models ran greedy (temperature 0), with reasoning turned off for the hosted DeepSeek models.
- A hint for chat models. On the extraction sets (DSTC8, SNIPS, ATIS), chat models also got a one-line instruction to fill in the most suitable function and copy each value as written. Without it they tend to answer in prose. Relay gets no hint.
- Needle 3. Run with Cactus’s own package (
cactus-needle3.2.0) and its default confidence gate. - Hardware. Open models ran through vLLM on a RunPod H100. DeepSeek V4 and V4.1 Flash ran through OpenRouter.
- Scoring. One scorer per kind of task, the same for everyone. Tool calls are an exact match of the full call list. BFCL uses its own accepted-answer lists. DSTC8, SNIPS and ATIS use slot micro-F1. The sealed set is the mean of four skills.
The 12 evaluations
Eleven of the twelve are public. The twelfth is ours.
| Group | Set | Rows | What it measures |
|---|---|---|---|
| Benchmarks | Mobile Actions (Google) | 961 | Phone actions: calendar, email, maps, contacts, flashlight |
| DroidCall | 200 | Android intents (alarms, timers, documents), single and multi-call | |
| BFCL v4, single-turn AST | 3,641 | General function calling, including parallel calls and irrelevance | |
| DSTC8 / SGD | 1,813 turns | Dialogue state: values stated across a conversation | |
| SNIPS gold / 7-way | 700 / 700 | Slot filling. The 7-way set also requires picking the right schema | |
| Battle | ToolACE, NexusRaven, Glaive, BitAgent | 799 / 318 / 640 / 800 | Public tool-calling data |
| ATIS | 681 | Flight-booking slot filling | |
| Private | Sealed set | 1,519 | Our own held-out set. Nobody, including us, has trained on it |
The sealed set is the one comparison where we know nobody had an advantage. We generated it with a different model family (microsoft/phi-4) and kept only the items that Qwen3.6 independently agreed with. We then verified it manually too. It was never published and never placed in any training folder, so it is clean for every model on the table, not only for ours.
Size against score
Start with the shape of the result. This is every model’s mean score over the 12 evaluations against the size of its weights file, on a log axis.
Among models small enough to ship inside an app, Relay is alone at the top. The nearest model below it is Qwen3.5-4B, a tenth of a point behind at 9.3 GB, about 800 times the size. The smallest model above it is Qwen3.5-9B, at 19.3 GB.
Evaluation by evaluation
An average hides where a model is strong. Here is Relay against the best of the other 12 models on each set, ordered from where it leads most to where it trails most.
Relay 1Best of the other 12 models
- SNIPS+26.5
- SNIPS 7-way+17.0
- Mobile Actionslevel
- BitAgent−0.4
- Sealed set−7.4
- DSTC8−8.8
- ATIS−9.4
- Glaive−10.2
- DroidCall−12.5
- BFCL−17.7
- ToolACE−24.8
- NexusRaven−33.0
Point at a row for what it measures and who set the mark.
Two patterns stand out. Relay leads, or ties, on Mobile Actions and the two SNIPS sets, where the task is to find the right values in a short request. And it trails wherever the task asks a model to reason about a long, unfamiliar API description or to compute a value. We come back to that below, because it is the honest center of this release.
Full results
The whole table at once, every cell lit by its score. Relay’s row is outlined. Read it as a map: bright columns are easy sets, and the places where Relay’s row stays bright while the large models’ rows dim are its territory.
| Model | |||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Relay 111.5 MB | 92.2 | 44.5 | 63.9 | 59.6 | 93.3 | 83.2 | 44.3 | 37.4 | 85.0 | 38.6 | 80.8 | 83.6 | 67.2 |
| Needle 335 MB | 87.5 | 45.5 | 51.8 | 18.5 | 14.9 | 10.9 | 37.5 | 17.3 | 82.3 | 32.6 | 2.2 | 67.1 | 39.0 |
| FunctionGemma536 MB | 59.3 | 20.5 | 49.0 | 17.5 | 18.1 | 9.0 | 46.7 | 23.0 | 79.4 | 26.2 | 6.5 | 57.5 | 34.4 |
| LFM2.5-230M459 MB | 70.6 | 29.0 | 62.7 | 15.7 | 15.0 | 10.4 | 47.4 | 36.5 | 93.4 | 26.0 | 19.6 | 69.6 | 41.3 |
| LFM2.5-350M709 MB | 77.0 | 38.0 | 63.9 | 14.6 | 20.9 | 16.8 | 53.8 | 33.6 | 93.6 | 32.8 | 19.9 | 74.0 | 44.9 |
| LFM2.5-1.2B2.3 GB | 81.6 | 36.0 | 68.4 | 25.1 | 27.2 | 26.9 | 53.7 | 32.1 | 88.0 | 38.6 | 25.4 | 81.0 | 48.7 |
| Qwen3.5-0.8B1.7 GB | 73.7 | 26.5 | 64.5 | 31.9 | 30.6 | 25.7 | 53.9 | 45.3 | 88.8 | 38.4 | 30.6 | 71.1 | 48.4 |
| Qwen3.5-2B4.5 GB | 86.2 | 51.5 | 71.1 | 33.5 | 37.2 | 33.5 | 65.0 | 59.7 | 80.9 | 39.0 | 66.3 | 81.0 | 58.7 |
| Qwen3.5-4B9.3 GB | 92.2 | 56.0 | 80.0 | 42.2 | 51.0 | 46.7 | 69.1 | 65.1 | 95.2 | 33.1 | 84.6 | 89.8 | 67.1 |
| Qwen3.5-9B19.3 GB | 91.4 | 57.0 | 81.6 | 51.1 | 53.7 | 52.3 | 68.7 | 70.4 | 94.5 | 38.6 | 87.6 | 91.0 | 69.8 |
| Qwen3.6-35B-A3B72 GB | 89.8 | 54.5 | 80.4 | 68.4 | 56.4 | 54.7 | 67.7 | 68.9 | 89.5 | 38.5 | 90.2 | 89.1 | 70.7 |
| DeepSeek V4 Flash160 GB | 87.0 | 57.0 | 73.1 | 55.9 | 66.6 | 62.7 | 64.8 | 54.1 | 92.2 | 36.1 | 88.0 | 79.8 | 68.1 |
| DeepSeek V4.1 Flash510 GB | 81.5 | 55.0 | 79.1 | 68.2 | 66.8 | 66.2 | 69.0 | 61.6 | 94.5 | 38.8 | 75.7 | 88.9 | 70.4 |
Point at a cell for its rank. Click a column head to sort by it.
DataThe full table, as numbersAll 13 models on all 12 evaluations, with parameters and weights sizes.
| Model | Parameters · weights | MA | DroidCall | BFCL | DSTC8 | SNIPS | SNIPS-7 | ToolACE | Nexus | Glaive | BitAgent | ATIS | Sealed | Mean |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Relay 1 | 22M · 11.5 MB | 92.2 | 44.5 | 63.9 | 59.6 | 93.3 | 83.2 | 44.3 | 37.4 | 85.0 | 38.6 | 80.8 | 83.6 | 67.2 |
| Needle 3 | 121M · 35 MB | 87.5 | 45.5 | 51.8 | 18.5 | 14.9 | 10.9 | 37.5 | 17.3 | 82.3 | 32.6 | 2.2 | 67.1 | 39.0 |
| FunctionGemma | 268M · 536 MB | 59.3 | 20.5 | 49.0 | 17.5 | 18.1 | 9.0 | 46.7 | 23.0 | 79.4 | 26.2 | 6.5 | 57.5 | 34.4 |
| LFM2.5-230M | 230M · 459 MB | 70.6 | 29.0 | 62.7 | 15.7 | 15.0 | 10.4 | 47.4 | 36.5 | 93.4 | 26.0 | 19.6 | 69.6 | 41.3 |
| LFM2.5-350M | 354M · 709 MB | 77.0 | 38.0 | 63.9 | 14.6 | 20.9 | 16.8 | 53.8 | 33.6 | 93.6 | 32.8 | 19.9 | 74.0 | 44.9 |
| LFM2.5-1.2B | 1.17B · 2.3 GB | 81.6 | 36.0 | 68.4 | 25.1 | 27.2 | 26.9 | 53.7 | 32.1 | 88.0 | 38.6 | 25.4 | 81.0 | 48.7 |
| Qwen3.5-0.8B | 873M · 1.7 GB | 73.7 | 26.5 | 64.5 | 31.9 | 30.6 | 25.7 | 53.9 | 45.3 | 88.8 | 38.4 | 30.6 | 71.1 | 48.4 |
| Qwen3.5-2B | 2.3B · 4.5 GB | 86.2 | 51.5 | 71.1 | 33.5 | 37.2 | 33.5 | 65.0 | 59.7 | 80.9 | 39.0 | 66.3 | 81.0 | 58.7 |
| Qwen3.5-4B | 4.7B · 9.3 GB | 92.2 | 56.0 | 80.0 | 42.2 | 51.0 | 46.7 | 69.1 | 65.1 | 95.2 | 33.1 | 84.6 | 89.8 | 67.1 |
| Qwen3.5-9B | 9.7B · 19.3 GB | 91.4 | 57.0 | 81.6 | 51.1 | 53.7 | 52.3 | 68.7 | 70.4 | 94.5 | 38.6 | 87.6 | 91.0 | 69.8 |
| Qwen3.6-35B-A3B | 35B (3B active) · 72 GB | 89.8 | 54.5 | 80.4 | 68.4 | 56.4 | 54.7 | 67.7 | 68.9 | 89.5 | 38.5 | 90.2 | 89.1 | 70.7 |
| DeepSeek V4 Flash | 284B · 160 GB | 87.0 | 57.0 | 73.1 | 55.9 | 66.6 | 62.7 | 64.8 | 54.1 | 92.2 | 36.1 | 88.0 | 79.8 | 68.1 |
| DeepSeek V4.1 Flash | 748B · 510 GB | 81.5 | 55.0 | 79.1 | 68.2 | 66.8 | 66.2 | 69.0 | 61.6 | 94.5 | 38.8 | 75.7 | 88.9 | 70.4 |
DeepSeek V4.1 Flash is 748B parameters as DeepSeek counts them: a 552B backbone plus 196B of Engram conditional memory. Its safetensors index lists about 763B stored values once auxiliary modules are included. DeepSeek V4 Flash is 284B by DeepSeek’s count and 291B stored. We scored Qwen3.6-35B-A3B from its FP8 release (37.5 GB); the table lists the bf16 weights, like the other open models.
What the table says
Among on-device models, Relay 1 is clearly ahead. Its mean is 67.2. The best other on-device mean is 48.7, from LFM2.5-1.2B, a model about 200 times larger on disk.
| Model | Relay 1 ahead | Relay 1 behind |
|---|---|---|
| Needle 3 | 11 of 12 sets | DroidCall, by 2 of 200 rows |
| FunctionGemma | 11 of 12 sets | ToolACE |
| LFM2.5-230M | 10 of 12 sets | ToolACE, Glaive |
| LFM2.5-1.2B | 8 sets, 1 tie | BFCL, ToolACE, Glaive |
Against much larger models, it has a territory of its own. It is first of all 13 on Mobile Actions (tied with Qwen3.5-4B) and on both SNIPS sets. It beats DeepSeek V4 Flash on DSTC8 (59.6 against 55.9) and on the sealed set (83.6 against 79.8), and it beats DeepSeek V4.1 Flash on ATIS (80.8 against 75.7). Its overall mean matches Qwen3.5-4B, 67.2 against 67.1, a model with more than 200 times the parameters.
Where it loses is general tool calling over arbitrary schemas. On BFCL, ToolACE and NexusRaven, every model from 2B parameters up is ahead of Relay, by 16 to 33 points at 4B to 9B, and so is Qwen3.5-0.8B. These sets reward reasoning over long API descriptions you have never seen, and computing values that are not written in the request. A 22M-parameter encoder that copies values cannot do that yet.
What each model trained on
A benchmark score means little without knowing what the model has already seen.
| Relay 1 | The others | |
|---|---|---|
| Trained on any of the 12 test sets | No. We filter our generated data against them and re-checked the whole mix: the only near-duplicates are in the two same-source sets below | Unknown |
| Same-source training data | Two of twelve: Google’s Mobile Actions train split (CC BY) and the SNIPS CC0 release, with exact test duplicates removed (a few that differ only in punctuation remain). Treat those two as in-distribution for Relay | Not disclosed |
| Disclosed tool-use data | Fully listed below | Needle 3: “Cactus proprietary datasets”. FunctionGemma: “public tool definitions”, none named. LFM2.5: its tool-use data are marked proprietary in the LFM2 technical report, which reports n-gram decontamination. Qwen and DeepSeek: not disclosed |
None of the other models documents training on Glaive, ToolACE, xLAM, SGD/DSTC8, SNIPS, ATIS, NexusRaven, BitAgent or BFCL. That does not mean they did not, only that it is not disclosed. Two individual sets deserve a caveat. Glaive is a public training set, so the two smaller LFM2.5 models’ scores above 93 there may reflect that. And BitAgent’s 800 rows contain only 37 distinct requests, so its scores move in big steps; treat it as weak evidence either way.
How Relay works
Reading, not writing
A language model produces a call by generating text, and every generated token is a chance to invent something: a city that is not in the request, a digit dropped from a phone number. Relay is an encoder. It reads the request and every tool schema together in one pass, and then three small heads point at the answer.
a network plain code: rules, search or signal processing
- The router decides, for each tool, whether to call it and how many times. A learned “none” score handles requests that no tool fits, which is how Relay abstains.
- The anchors say, for each call, which part of the request it comes from. This is what separates “alarm at 7” from “timer for 10 minutes” in the same sentence.
- The argument heads choose, for each parameter, a value program and its inputs.
Value programs are the core idea. Relay never generates a value. It picks a small, checkable recipe that builds the value from the request, and the recipe runs deterministically.
| Program | Example request | Value |
|---|---|---|
copy span |
“text Sam” | "Sam" |
number span with scale |
“two and a half km” | 2.5 |
datetime |
“tomorrow at 6:45” plus date context | "2026-10-18T06:45:00" |
timepart |
“10:15” | hour 10, minute 15 |
enum / option |
“make it loud” | the schema’s "high" |
bool |
“don’t vibrate” | false |
lookup table |
“in California” | "CA" |
select words |
“the Eiffel … Tower” | "Eiffel Tower" |
affix, digits, list |
“call +1 555 0100”, “milk, eggs and bread” | formatted values, lists |
ref |
“find a hotel, then book it” | the output of call #1 |
absent / default |
(not stated) | left out, or the schema’s default |
Because the programs are deterministic, Relay cannot hallucinate a phone number or a city that is not in the request. They are also why it is so strong at slot filling: 93.3 on SNIPS against 66.8 for the next best model.
Decoding rules
After the heads have scored everything, a few plain rules turn scores into calls.
- Required arguments. If a call would leave a required argument empty, Relay drops that call. In an earlier version, adding this rule lifted BFCL by 1.7 points and ToolACE by 2.5, at no cost elsewhere.
- Exclusive spans. Within a call, a stretch of text fills at most one argument, and the most confident one wins.
- Call order. A call that consumes an earlier call’s output goes after it. Otherwise calls follow the order the user stated them in.
Getting a good model into 11.5 MB
The shipped model has 21.8M parameters in 11.47 MB, and its exported graphs reproduce the PyTorch model exactly on our golden records. Most of them are the encoder:
21.8M parameters · 11.47 MB with 4-bit weights
- Encoderettin-encoder-32M, distilled12.5M
- Embeddings16k tokens, 4-bit6.1M
- Headsrouter, anchors, arguments3.1M
Under the hoodThe five-step squeeze, and what did not make the cutTeacher, student, vocabulary pruning, 4-bit training, Gust export.
- Teacher. An ettin-encoder-150M (MIT licence), trained on all our data.
- Student. An ettin-encoder-32M, distilled from the teacher on its outputs and its value-program distributions, over three training stages.
- Vocabulary pruning. From 50k tokens to 16k, keeping the most frequent ones; rarer words are split into smaller pieces, which leaves 97.8% of training tokens unchanged. This removed most of the embedding table. One caveat: the frequency counts that chose the 16k also included some third-party corpora (among them SGD, Hermes, xLAM and DroidCall). No text or label from them was trained on, but they shaped which existing tokens were kept. Relay 2 rebuilds the vocabulary from our own data.
- Quantisation-aware training. 4-bit per-channel weights in every linear layer, and 4-bit embeddings.
- Gust export. Integer activations (int16, per row) for the encoder’s matrix multiplies.
Some things did not make the cut. Int8 activations cost little after training (six-benchmark mean 72.7 against 72.8, NexusRaven 0.6 lower) but missed our strict per-set check; training through them cost 2.5 points on NexusRaven and 6 on BitAgent. 3-bit weights (9.5 MB) cost up to 3 points on most sets and were slower in practice. A 17M variant was 5.3 MB and faster but less accurate; it predates the clean-data rebuild, so we do not offer it.
The data
Everything Relay trained on is data we can license cleanly. That was not where we started. Early internal versions used public training splits such as DroidCall, SGD and Hermes. When we reviewed them, some turned out to be GPT-4 outputs with unclear terms of use (DroidCall), some CC BY-SA (SGD), and one SNIPS split had no licence at all. So we rebuilt Relay from scratch on data we can stand behind.
| Source | Size | Licence |
|---|---|---|
| Our generated data (six generations) | ~410k verified records | Ours. The generator, Qwen3.6-35B-A3B, is Apache-2.0 |
| Procedural “universe” data (templated, programmatic) | Ours | |
| SNIPS CC0 release (own split, test overlap removed) | 13.7k | CC0 |
| Mobile Actions train split, When2Call, MASSIVE, CLINC | CC BY |
How we generate it
Every generated record goes through five steps, and about two thirds are thrown away along the way.
Under the hoodEach step in detailThe scenarios, the Android functions and the extra filters at merge time.
- Forge. The model invents realistic tool families, with schemas that have types, enums and formats, across 23 domains and 179 subdomains. Android tools are not invented. We wrote 36 Android functions in 8 families by hand from the public AOSP documentation, with format checkers for URIs, ISO datetimes, phone numbers, email addresses and package names.
- Write. The model writes requests for a target scenario: single calls, many tools, parallel calls, multi-step sequences with references, “ask” cases where the user wants a tool but gives none of the required details, “none” cases where no tool fits, dialogue turns, dense slot filling and oddly worded requests.
- Verify. Every argument must be expressible as a value program over the request text, so there are no invented values. Values must pass their format checker. In dialogue and slot data, strings must be verbatim spans of the request.
- Solve round-trip. A fresh call to the model has to solve the request from scratch and agree with the label.
- Filter for contamination. Anything close to a test, battle or sealed item is dropped.
At merge time we also drop labels filled with an unstated default, “meta” requests that talk about parameter names, and dialogue turns labelled “no value” that still state a date, number or name.
Learning from errors without overfitting
For the last round we split six of the benchmarks in two. Half A was for analysis: we studied Relay’s errors there, such as over-abstaining on oddly worded requests, values on question-only dialogue turns, role swaps and invented defaults. Half B stayed untouched until the final check. We then generated new, similar data for those skills, never benchmark data, and the gains held on half B:
So the improvement is a real skill, not a fit to the examples we looked at.
Speed
Almost all of a request’s time, about 96%, is the encoder, and it grows with the amount of tool text.
Under the hoodGust, the runtimeOne C codebase, native first and compiled to WebAssembly for the browser: packed 4-bit weights with int16 activations, encoder operators, bit-identical results across every build and thread count.
Relay ships as .gust files and one C host that does the tokenizing, features, value programs and decoding. Host and runtime are plain C with no dependencies: they build natively for x86-64 and ARM64, and compile to one WebAssembly module of 492 KB (192 KB gzipped; 496 KB with threads) for the browser, which holds the runtime, the host and the model interface, so the JavaScript wrapper only moves bytes. We built Gust ourselves because no off-the-shelf runtime did what we needed.
- Packed low-bit weights with int16 activations. The inner loops are exact int32 sums of 4-bit codes times int16 values:
pmaddwdon x86, NEON on ARM64,i32x4.dot_i16x8_sin WebAssembly. - Encoder-specific operations. ModernBERT’s banded (±64 token) sliding-window attention, rotary position embeddings, GeGLU, and fast float32 attention built on the same tile layout as the convolutions.
- Bit-identical results across the native x86-64 builds (base, AVX2, AVX-512 VNNI), the native ARM64 build on a Raspberry Pi, WebAssembly single- and multi-threaded, and 1 to 12 threads. We test this exactly, so a phone and a laptop give the same answer.
- Multi-threading with native threads, and over shared memory in WebAssembly, where workers park on
memory.atomic.waitand claim tasks with compare-and-swap. - Profile-driven tuning in V8. Four-row tiles instead of six, because V8 spills registers. A vector absmax built on
pmaxcut quantisation from 49 ms to 4.8 ms per request. - Tested exactly. The C host gives byte-identical output to our JavaScript reference host on all 8,015 benchmark rows, with the decoding fixes on and off. (Re-checked on 2026-10-22 on 1,800 rows, 150 from each of the 12 evaluations: identical on all but one sealed-set request, where the calls and values are equal and only the order of one call’s argument keys differs.) With them off, the JavaScript host matches the Python reference on 1,011 golden records and 3,021 tokenizer cases, bit for bit.
Under the hoodThe retrieval prefilterHow 100 tools went from about 10 seconds to under 100 ms, and why accuracy rose too.
Long tool lists used to make Relay slow, about 6.5 seconds for 100 tools on one thread, because every tool is read with every request. A small head on Relay’s own encoder, 2 MB, now ranks the tools first, from vectors cached once per tool, and Relay reads only the best ones: up to 10 tools or 600 tokens of tool text. On held-out data the right tools are in its top 10 99% of the time.
With 8 threads and the tool vectors cached, every benchmark and the sealed set stay under 100 ms at the 90th percentile (81 ms at most), and 20 to 100 tools take 74 to 77 ms at the median. The first request with a new tool list is slower: the median is 80 to 104 ms for 20 to 100 tools, and on BFCL and the sealed set, where nearly every request brings new tools, the 90th percentile is 180 to 225 ms, because each new tool is read once. It helps accuracy too: cutting look-alike tools raised the sealed set from 82.8 to 83.6, and our stress test of 100 near-duplicate tools from 50.5 to 59.2. NexusRaven rose from 36.8 to 37.4 and the other benchmarks moved by 0.1 point or less.
Under the hoodRelay 1 FastA variant for single-core devices that cached each app's tools: about twice as fast, better on DSTC8, worse where abstaining matters. Since retired.
For single-core devices we also built a variant that caches the tools themselves. Its first six encoder layers read each tool on its own, once per app. The request reads those cached states in every layer, through a small new one-way attention operator in Gust, and the last four layers read the request and the tools together. That makes the encoder about 2.2 times faster on one thread, and a whole request about twice as fast: 92 to 118 ms in the browser, against 170 to 210 ms.
It is better on DSTC8 (65.4 against 59.6) and on the six benchmarks overall (73.9 against 72.8), but worse where judging a tool needs the request while reading it: NexusRaven by 6 points, BitAgent by 5 and the sealed set by 1, mostly cases where the right answer is to call nothing. We retired it on 2026-10-20: faster runtime kernels made Relay 1 itself fast enough, and its judgement losses and extra memory were not worth it.
Where Relay falls short
- General tool calling still trails 2B+ models. BFCL, ToolACE and NexusRaven reward reasoning over long, unfamiliar API documentation, and computing or rephrasing values.
- Some values cannot be expressed. Relay cannot write text that is not in the request: a composed email body, a reworded search query, or “13-minute” turned into a different format. On DroidCall roughly 15 to 25% of rows need such a value, depending on how strictly you count.
- DroidCall conventions. Models fine-tuned on DroidCall’s own training data reach 74.5 to 85 in its paper. We do not train on it, because its data were generated with GPT-4-turbo, so we miss some of its labelling conventions.
- Many tools. Without the prefilter, accuracy degrades with long tool lists and the time grows with the tool text. The prefilter fixes both when threads are available; on one thread, a list of 20 to 100 tools still takes about 0.3 s once its vectors are cached and 1 to 2 s the first time, against 0.16 s for a typical seven-tool request.
- At most 32 parameters per tool are read.
- Benchmark noise. DroidCall has 200 rows, so each row is 0.5 points, and BitAgent has 37 distinct requests.
- Harness differences. Our scorer closely matches Cactus’s numbers for Needle 3, within 1.6 points (87.5, 45.5 and 51.8 for the three sets we share, against their published 86.0, 47.0 and 50.2). Our extraction protocol differs from Cactus’s chart, so Needle 3 scores lower on DSTC8 and SNIPS here than in its own published table. And one DeepSeek request per model failed at the API (the same ToolACE row for both, rejected for an empty tool name) and is scored as no call.
What we learned along the way
- Reading plus value programs is a strong prior. A 17M-parameter encoder was ahead of Needle 3 on five of its six benchmarks the first time we ran it, after one hour of training. The distilled 17M model was then ahead on all six. Both were early versions trained partly on the training splits of three of those benchmarks.
- Size helps extraction most. Moving from 17M to 32M lifted BFCL by 4 points and SNIPS 7-way by 6. A second training stage added about 1 and 2 more, and fixed most of the remaining slot-filling gap.
- Clean data cost us first, then paid back. The first build trained only on our own data lost SNIPS (94 down to 23) and DSTC8 (78 down to 44), because it had learned short-utterance slots and dialogue state from third-party sets. Targeted generation for dialogue, dense slots and Android brought SNIPS back to 93, lifted ATIS from 13 to 81 (the third-party-data version had 46), and beat the old model on four of five battle sets and the sealed set. DSTC8 and DroidCall are still below the third-party-data version, because those benchmarks reward their own labelling conventions.
- More “don’t call” data is not free. Teaching the model to hold back helped on Glaive but cost NexusRaven 8 points, because it also abstained on real requests. The simple required-argument rule did better than more data.
- Fair harnesses matter. In our harness LFM2.5-230M scores 62.7 on BFCL, against 46.3 in Cactus’s published chart (Liquid’s own figures are 43.3 on BFCL v3 and 21.0 on v4). That is why we ran every model, Needle 3 included, ourselves, in the same harness, rather than quoting anyone’s chart.
What’s next
Relay 2 is aimed at the gaps above: judgement over long, unfamiliar tools, multi-call requests, composed values and phone actions. It has to stay as fast as Relay 1 on a low-end device. Our first experiments toward it shaped the plan:
- Data before size. Every regression we have seen so far came from data, not from the model. We will clean the “no call” examples in our new Android data, which made a retrained teacher abstain less often. We will add contrast data for the errors our demo audit found: negation, corrections (“20, no, 22”), conditionals, opposite actions such as lock and unlock, and paraphrased requests. We will not train on DroidCall’s own data: in a test it lifted DroidCall by 6 to 9 points while costing 5 on abstaining.
- A teacher that passes our checks first. The student can only learn what the teacher knows. The teacher must hold NexusRaven, BitAgent and abstaining before we distil from it.
- About twice the student, trained for longer. On the same benchmarks our 150M teacher scores 4.6 points above Relay 1, most of it on DSTC8 and SNIPS 7-way. That gap is the student’s size, not its 4-bit weights. Students were still improving when our test runs stopped, so Relay 2 distils for much longer. It stays at 4-bit, at about 17 to 18 MB. 2-bit weights and a full 68M encoder are out: 3-bit already cost accuracy, and a 68M encoder would be about twice as slow on a low-end device.
- Architecture fixes for known limits. Instance-aware anchors, or set prediction, for requests that need several calls. A calibrated call versus no-call score. A tiny copy-or-generate decoder used only for composed text such as message bodies.
- The same speed on low-end hardware. A word-matching tool shortlist and integer kernels each make Relay faster, together enough to pay for the larger student. The shortlist keeps Relay 1’s accuracy and is 1.4 to 2 times faster on a Raspberry Pi with 24 tools.
- A much larger licence-clean Android world. 332 more hand-written Android functions, about 33k new verified training records, function-name masking, and error-driven data rounds.
- A vocabulary built only from our own data (see the caveat above).
We will report it the same way: one harness for every model, the sealed set, an untouched half of every benchmark we study, and, new for Relay 2, a larger abstention test, because the sealed set’s 102 no-call requests cannot separate a two-point difference from noise.