seal
← Research

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

Try Relay in your browser

Relay 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.

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-needle 3.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
    Fig. 1The evaluations. Rows are the number of test items. Battle sets are public tool-calling data that we converted to one format.

    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.

    304050607010 MB100 MB1 GB10 GB100 GBDeepSeek V4.1DeepSeek V4Qwen3.6-35BQwen3.5-9BQwen3.5-4BQwen3.5-2BQwen3.5-0.8BLFM2.5-1.2BLFM2.5-350MLFM2.5-230MFunctionGemmaNeedle 3Relay 1mean score, 12 evaluations · up is betterweights size, log scale
    Fig. 2Mean score against weights size. The dotted line carries Relay 1's mean to the right: every model that scores above it is between 19 GB and 510 GB. Needle 3 is plotted at the 35 MB of the 20-layer file we ran; Cactus quotes 8 to 29 MB for its shallower depths. Point at or tap a model for its numbers.

    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

    1. SNIPS93.366.8DeepSeek V4.1 Flash+26.5
    2. SNIPS 7-way83.266.2DeepSeek V4.1 Flash+17.0
    3. Mobile Actions92.292.2Qwen3.5-4Blevel
    4. BitAgent38.639.0Qwen3.5-2B−0.4
    5. Sealed set83.691.0Qwen3.5-9B−7.4
    6. DSTC859.668.4Qwen3.6-35B-A3B−8.8
    7. ATIS80.890.2Qwen3.6-35B-A3B−9.4
    8. Glaive85.095.2Qwen3.5-4B−10.2
    9. DroidCall44.557.0Qwen3.5-9B−12.5
    10. BFCL63.981.6Qwen3.5-9B−17.7
    11. ToolACE44.369.1Qwen3.5-4B−24.8
    12. NexusRaven37.470.4Qwen3.5-9B−33.0

    Point at a row for what it measures and who set the mark.

    Fig. 3Relay 1 against the best score the other models reached on each set. The right-hand column is the gap in points. Switch to the on-device models to see the narrower comparison, and point at a row for what the set 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 MB92.244.563.959.693.383.244.337.485.038.680.883.667.2
    Needle 335 MB87.545.551.818.514.910.937.517.382.332.62.267.139.0
    FunctionGemma536 MB59.320.549.017.518.19.046.723.079.426.26.557.534.4
    LFM2.5-230M459 MB70.629.062.715.715.010.447.436.593.426.019.669.641.3
    LFM2.5-350M709 MB77.038.063.914.620.916.853.833.693.632.819.974.044.9
    LFM2.5-1.2B2.3 GB81.636.068.425.127.226.953.732.188.038.625.481.048.7
    Qwen3.5-0.8B1.7 GB73.726.564.531.930.625.753.945.388.838.430.671.148.4
    Qwen3.5-2B4.5 GB86.251.571.133.537.233.565.059.780.939.066.381.058.7
    Qwen3.5-4B9.3 GB92.256.080.042.251.046.769.165.195.233.184.689.867.1
    Qwen3.5-9B19.3 GB91.457.081.651.153.752.368.770.494.538.687.691.069.8
    Qwen3.6-35B-A3B72 GB89.854.580.468.456.454.767.768.989.538.590.289.170.7
    DeepSeek V4 Flash160 GB87.057.073.155.966.662.764.854.192.236.188.079.868.1
    DeepSeek V4.1 Flash510 GB81.555.079.168.266.866.269.061.694.538.875.788.970.4

    Point at a cell for its rank. Click a column head to sort by it.

    Fig. 4Scores on all 12 evaluations and the mean, one cell per model and set. Bold marks the best in each column. Switch to rank to light each cell by its place in its column; click a column head to sort the models by it. Sizes are the weights as published (see the table below).
    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
    Scores on all 12 evaluations. Bold marks the best in each column. Parameters and sizes are for the weights as published on Hugging Face: bf16 for the open models, Cactus's 2-bit file for Needle 3 and DeepSeek's mixed FP8/FP4 releases. Parameter counts are the vendors' own where they publish one. Quantised community builds of the open models are smaller. Relay 1's 11.5 MB is its four model files (the tokenizer, the 2 MB prefilter and the runtime add about 2.6 MB), scored as it ships: the JavaScript host with its decoding rules and the retrieval prefilter, on the Gust WebAssembly runtime.

    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.

    higher is better
    1. Relay 111.5 MB67.2
    2. LFM2.5-1.2B2.3 GB48.7
    3. Qwen3.5-0.8B1.7 GB48.4
    4. LFM2.5-350M709 MB44.9
    5. LFM2.5-230M459 MB41.3
    6. Needle 335 MB39.0
    7. FunctionGemma536 MB34.4
    8. Qwen3.5-4B9.3 GB, not on-device67.1

    Point at a row for its numbers.

    Fig. 5Mean over the 12 evaluations for every model small enough to ship inside an app, with Qwen3.5-4B, the nearest model in score, for scale.
    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.

    Request, tool schemas and context"Set an alarm for 6:45 tomorrow, then text Sam I'm running late" · set_alarm, send_message, get_weather, …
    Retrieval prefilter2 MBRanks the tools from cached vectors and keeps the best: up to 10 tools or 600 tokens of tool text.
    Encoder12.5M · 4-bitReads the request and every kept schema together, in one pass.
    which tools
    RouterCall each tool or not, and how many times. A learned none score abstains.
    from where
    AnchorsWhich part of the request each call comes from.
    with what
    Argument headsA value program and its inputs for each parameter.
    Value programscopy, number, datetime, enum, lookup, ref, … run deterministically over the request text.
    Decoding rulesNo call with an empty required argument; a span fills at most one argument; calls in order.
    Callsset_alarm(time="2026-10-18T06:45:00") · send_message(recipient="Sam", body="I'm running late")

    a network plain code: rules, search or signal processing

    Fig. 6Relay from request to calls. The encoder reads everything once; the three heads only point, and the value programs build each argument from the request's own words. The decoding rules are plain code.
    1. 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.
    2. 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.
    3. 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
    Fig. 7The value programs. Each one turns a stretch of the request into a typed value, or says that nothing was stated.

    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
    Fig. 8Relay 1's parameters by part. The embeddings were the largest part before the vocabulary was pruned from 50k tokens to 16k.
    Under the hoodThe five-step squeeze, and what did not make the cutTeacher, student, vocabulary pruning, 4-bit training, Gust export.
    1. Teacher. An ettin-encoder-150M (MIT licence), trained on all our data.
    2. Student. An ettin-encoder-32M, distilled from the teacher on its outputs and its value-program distributions, over three training stages.
    3. 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.
    4. Quantisation-aware training. 4-bit per-channel weights in every linear layer, and 4-bit embeddings.
    5. 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.

    1 · ForgeInvent realistic tool families with typed schemas across 23 domains and 179 subdomains. 36 Android functions are hand-written from AOSP documentation instead.
    2 · WriteRequests for a target scenario: single, parallel and multi-step calls, ask and none cases, dialogue turns, dense slots, odd wording.
    3 · VerifyEvery argument must be a value program over the request text and pass its format checker. In dialogue and slot data, strings must be verbatim spans.
    4 · Solve round-tripA fresh call has to solve the request from scratch and agree with the label.
    5 · Filter for contaminationAnything close to a test, battle or sealed item is dropped.
    Fig. 9The generator's five steps. About two thirds of all records are dropped on the way, most at the verify and round-trip steps.
    Under the hoodEach step in detailThe scenarios, the Android functions and the extra filters at merge time.
    1. 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.
    2. 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.
    3. 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.
    4. Solve round-trip. A fresh call to the model has to solve the request from scratch and agree with the label.
    5. 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:

    NexusRaven38.4before 30.2 +8.2
    DroidCall44.4before 38.9 +5.6
    ToolACE43.6before 38.9 +4.7
    BitAgent35.3before 30.5 +4.7
    Fig. 10The untouched half B of four benchmarks, before and after the last round of targeted generation. Every one moved up. Of the other two split sets, DSTC8 rose 1.4 points and BFCL was flat (−0.2).

    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.

    lower is better
    1. Relay 11 thread156 ms
    2. Relay 1 Fast1 thread80–95 ms
    3. Relay 14 threads68 ms
    4. Relay 18 threads39 ms
    5. Relay 111 threads30 ms
    6. Needle 3native, ~11 coresabout 160 ms

    Point at a row for its numbers.

    Fig. 11Median time per Mobile Actions request (350 tokens at the median), in WebAssembly under Node, on one desktop PC with a Ryzen 7 7800X3D. Relay 1's rows are the shipped files with the retrieval prefilter, re-measured on 2026-10-22 over all 961 requests; the Relay 1 Fast row was measured earlier. Needle 3 runs its native engine, which used 11.5 cores on average in our run.
    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: pmaddwd on x86, NEON on ARM64, i32x4.dot_i16x8_s in 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.wait and 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 pmax cut 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.