yo-router-270m

A 270M-parameter tool router for macOS. Takes one plain-English sentence, emits one function call. That is the whole job — it never writes prose, never sees command output, and never chooses more than one tool.

It is a LoRA fine-tune of google/functiongemma-270m-it, fused back into full weights. Its 10 tool declarations are baked into the weights, so the prompt does not carry them: 33 prompt tokens instead of 655, a 20x reduction.

tool accuracy arg accuracy prompt tokens
gemma-3-270m-it, no fine-tune 11.2% 4.8% 657
functiongemma-270m-it, no fine-tune 56.2% 33.9% 655
this model (baked) 95.2% 82.3% 33
sibling, declarations in prompt 96.4% 90.3% 655

Gemma is provided under and subject to the Gemma Terms of Use found at ai.google.dev/gemma/terms. These files have been modified — see Licence below. This is a Model Derivative, not Google's original FunctionGemma.


What it does

$ yo whats eating my disk
→ disk_usage(path='~')
Downloads is the biggest folder in /Users/lagna360 at 47 GB — that's 31% of everything there.
  [disk_usage · 33 prompt tok · 0.25s]

The model is one stage of a three-stage pipeline:

utterance → ROUTER (this model) → picks 1 tool + typed args
          → EXECUTOR (argv allowlist, never a shell) → structured data
          → RENDERER (templates) → one plain-English sentence

Because the model only classifies and extracts, numbers in the output are always exact and a filename can never become an instruction.

The 10 tools

All are read-only macOS system queries.

tool args what it answers
disk_usage path which folders are eating space
storage_summary how much space is left
largest_files path biggest individual files
find_files name, path find a file by name
top_processes sort_by (cpu|mem) what's hogging CPU/RAM
network_listeners port what's using a port
network_info IP, wifi SSID, gateway
battery_status charge level, plugged in?
system_info macOS version, host, user, uptime
datetime current time and date

Usage

Requires Apple Silicon and mlx-lm.

Via the yo CLI (what these weights are built for)

pip install yo-macos
yo pull yo-router-270m      # downloads this repo into ~/.cache/yo/models/
yo whats eating my disk

The repo carries four files the CLI reads beyond the weights themselves: yo-model.json (declares prompt: baked, so the client knows not to send declarations), MANIFEST.json (sha256 + size for every runtime file, checked after download), and NOTICE + LICENSE, which put the Gemma Terms on disk next to the weights.

Directly with mlx-lm

from mlx_lm import load, generate

model, tok = load("lagna360/yo-router-270m")

# The baked prompt shape. Note: NO tools= argument — that is the point.
msgs = [
    {"role": "developer",
     "content": "You are a model that can do function calling with the following functions"},
    {"role": "user", "content": "whats eating my disk"},
]
prompt = tok.apply_chat_template(msgs, add_generation_prompt=True, tokenize=False)

print(generate(model, tok, prompt=prompt, max_tokens=96, verbose=False))
# <start_function_call>call:disk_usage{path:<escape>~<escape>}<end_function_call>

The prompt shape is load-bearing. The developer-role trigger string above is exactly what the model was trained on. Change it, or pass a tools= list, and accuracy degrades — the model was never shown declarations during training.

Output format:

no args:   <start_function_call>call:datetime{}<end_function_call>
with args: <start_function_call>call:find_files{name:<escape>invoice<escape>}<end_function_call>

Arguments are key:<escape>value<escape>, comma-separated, keys sorted. Use greedy decoding — every number reported here is from greedy decoding and is deterministic.

The full CLI, the parser for this format, and the executor are at github.com/lagna360/yo.


Training recipe

This is the exact configuration that produced these weights. It is also the recommended starting point for pointing the same pipeline at a different domain — see RECIPE.md in the repo.

base:            google/functiongemma-270m-it
fine_tune_type:  lora
rank:            8
scale:           16.0        # alpha = 2 x rank
dropout:         0.0
learning_rate:   5e-5
optimizer:       adam
num_layers:      16
targets:         [q_proj, k_proj, v_proj, o_proj, gate_proj, down_proj, up_proj]
epochs:          3           # = 518 iters at this dataset/batch size
batch_size:      16
max_seq_length:  160
mask_prompt:     true        # loss on the completion only
grad_checkpoint: true
seed:            17

Trained with mlx_lm.lora, then mlx_lm.fuse to merge the adapter into the base weights.

  • Data: 3,000 synthetic examples (2,760 train / 240 val), ~300 per tool, 1,840 carrying arguments. See the dataset card.
  • Hardware / time: Apple M1 Pro, 16 GB. 8m 14s of training.
  • Final loss: train 0.001, val 0.03.
  • Adapter size: 6.5 MB before fusing. The fused model is 543 MB (bf16), of which 33 MB is the 262k-entry tokenizer.

The three cliffs

Each of these is the difference between working and zero, not a tunable trade-off:

cliff wrong choice result
base model gemma-3-270m-it 0% — no <start_function_call> control tokens, so they fragment into BPE pieces the model never learns to emit
adapter size rank 64, lr 1e-4 0% — diverges, stops emitting calls entirely
argument supervision omit args from training data 0% arg accuracy (tool selection unaffected)

Everything else is a plateau.


Evaluation

249 hand-written test cases across 10 tools, written by hand and independently of the training-data generator. The generator hard-fails if any training utterance exactly matches a test utterance — it caught one real collision during development.

Greedy decoding, so every accuracy figure is deterministic. Latency varies ~7% run to run.

metric value
tool accuracy 95.2% (237/249)
argument accuracy 82.3% (over the 62 arg-bearing cases)
no-call rate 0.0%
hallucinated tool name 1.6%
mean prompt tokens 33
latency p50 / p95 0.238s / 0.277s (M1 Pro)

Argument accuracy is graded by loose containment, not exact match.

Versus the un-tuned base

56.2% → 95.2%. Paired exact McNemar against the un-tuned functiongemma-270m-it: p = 5.38e-28.

Much of what fine-tuning does here is suppression, not new knowledge. The base model's instinct is to apologise and explain; refusals go 14.9% → 0.0%.

Versus keeping declarations in the prompt

The sibling run trained identically but with the 10 declarations in the prompt scores 96.4% tool / 90.3% arg at 655 prompt tokens. Baking costs 1.2 points of tool accuracy — McNemar p = 0.549, not significant at n=249 — and about 8 points of argument accuracy.

"Not significant" is not "no difference". With few discordant pairs the test is underpowered; both a real small cost and true equivalence remain consistent with this data.

The argument cost is the honest one: the declarations describe the argument schema, so deleting them hurts extraction more than classification.

Where the errors are

The 12 remaining failures are semantically adjacent pairs — tools whose descriptions overlap:

storage_summary  vs  disk_usage       "space left"  vs  "what's using space"
find_files       vs  largest_files    a named file  vs  big files
top_processes    vs  system_info      "what's slow" vs  "what is this machine"

Plus 4 cases (1.6%) where it invents a name that is not in the menu at all (battery, disk_split, loud_top_percent, disk_top_processes).


Intended use

In scope:

  • Routing plain-English macOS system questions to one of the 10 read-only tools above.
  • A reference implementation / teaching artifact for narrow-domain tool routing on small models.
  • A starting point to re-run the same pipeline on your own tool set.

Out of scope:

  • General-purpose chat, question answering, reasoning, or text generation. It will not do these.
  • Any tool set other than the 10 baked in. Baked tools are immutable — adding, removing or changing a tool means retraining.
  • Non-macOS platforms. The tools shell out to macOS-specific binaries.
  • Any use where a misroute is costly without a human in the loop.

Limitations

These are real and you should design around them.

  • No chitchat escape hatch. There is no none / no-tool option. Say "hi" and it will force a tool call onto it. Fine-tuning drove the no-call rate to 0.0%, which is a feature for in-domain utterances and a bug for everything else. Fixing this means adding a chitchat tool to the schema and retraining.
  • Argument accuracy is 82.3%. Roughly one in six arg-bearing calls has a wrong or missing argument. Paths and filenames are the common misses. Validate arguments before executing.
  • 1.6% hallucinated tool names. Your parser must handle a name that is not in your registry — do not assume the output is in-menu.
  • 10 tools, macOS, English, single call. No chaining, no multi-call, no other language tested.
  • Slot-value diversity is the ceiling on arguments. The generator drew paths and filenames from fixed pools, so scaling the dataset to 10k reduced argument accuracy to 77.4% while tool accuracy rose. If you re-run this, scale your slot pools with your example count.
  • Synthetic training data. Utterances come from hand-written stems plus programmatic prefix/suffix/typo expansion. Real user phrasing has a longer tail.
  • Prompt-shape brittleness. It expects the exact developer-role trigger above and no declarations. This is the cost of baking.
  • Inherits FunctionGemma's biases and failure modes. No additional safety tuning was done.

Safety

The model's output is untrusted input and the surrounding system treats it as such. In the reference implementation the model can only name a tool; it cannot reach anything the executor does not already implement. The executor builds argv arrays and never invokes a shell, paths are canonicalised against a denylist, and rendering is done by templates rather than by the model. If you build on these weights, do the same — do not pipe the output into a shell.

Environmental / compute footprint

8m 14s on a single M1 Pro laptop for this run; ~5.2 GPU-hours across all 24 runs in the study. Inference is ~0.24s per query on the same laptop, entirely local — no network, no API call.


Licence

These weights are NOT MIT-licensed and cannot be relicensed.

Gemma is provided under and subject to the Gemma Terms of Use found at ai.google.dev/gemma/terms

Modification notice, as required by Section 3.1 of those terms: these files have been modified. The base weights of google/functiongemma-270m-it were LoRA fine-tuned (rank 8, alpha 16, lr 5e-5, 3 epochs) on a synthetic macOS tool-routing dataset by Pankaj Upreti, and the adapter was fused back in. Every weight tensor differs from Google's original. FunctionGemma is listed in the Appendix to the Gemma Terms of Use, so those terms govern this derivative.

Use restrictions carry forward. Your use of these weights is subject to the restrictions in Section 3.2 of the Agreement, which incorporate the Gemma Prohibited Use Policy by reference. If you redistribute these weights or anything derived from them, you must pass on a copy of the Agreement, this modification notice, and these restrictions.

A full copy of the Gemma Terms of Use ships with this repository as MODEL_LICENSE, and the required notice as NOTICE.

"Open weights" is not "open source." These weights do not carry an OSI-approved licence, and describing them as open source would be wrong.

The code around them — CLI, tool registry, data generator, eval harness, experiment ladder — is MIT-licensed and lives at github.com/lagna360/yo.

Not affiliated with, endorsed by, or sponsored by Google. "Gemma", "FunctionGemma" and "Google" are trademarks of Google LLC, used here only to identify the base model as the Agreement requires.

Citation

@software{upreti2026yo,
  author = {Upreti, Pankaj},
  title  = {yo: fine-tuning a 270M model for macOS tool routing},
  year   = {2026},
  url    = {https://github.com/lagna360/yo}
}
Downloads last month
388
Safetensors
Model size
0.3B params
Tensor type
BF16
·
MLX
Hardware compatibility
Log In to add your hardware

Quantized

Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for lagna360/yo-router-270m

Finetuned
(450)
this model