Instructions to use lagna360/yo-router-270m with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use lagna360/yo-router-270m with MLX:
# Make sure mlx-lm is installed # pip install --upgrade mlx-lm # Generate text with mlx-lm from mlx_lm import load, generate model, tokenizer = load("lagna360/yo-router-270m") prompt = "Write a story about Einstein" messages = [{"role": "user", "content": prompt}] prompt = tokenizer.apply_chat_template( messages, add_generation_prompt=True ) text = generate(model, tokenizer, prompt=prompt, verbose=True) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- Pi
How to use lagna360/yo-router-270m with Pi:
Start the MLX server
# Install MLX LM: uv tool install mlx-lm # Start a local OpenAI-compatible server: mlx_lm.server --model "lagna360/yo-router-270m"
Configure the model in Pi
# Install Pi: npm install -g @earendil-works/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "mlx-lm": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "lagna360/yo-router-270m" } ] } } }Run Pi
# Start Pi in your project directory: pi
- MLX LM
How to use lagna360/yo-router-270m with MLX LM:
Generate or start a chat session
# Install MLX LM uv tool install mlx-lm # Interactive chat REPL mlx_lm.chat --model "lagna360/yo-router-270m"
Run an OpenAI-compatible server
# Install MLX LM uv tool install mlx-lm # Start the server mlx_lm.server --model "lagna360/yo-router-270m" # Calling the OpenAI-compatible server with curl curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "lagna360/yo-router-270m", "messages": [ {"role": "user", "content": "Hello"} ] }' - Hermes Agent
How to use lagna360/yo-router-270m with Hermes Agent:
Start the MLX server
# Install MLX LM: uv tool install mlx-lm # Start a local OpenAI-compatible server: mlx_lm.server --model "lagna360/yo-router-270m"
Configure Hermes
# Install Hermes: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash hermes setup # Point Hermes at the local server: hermes config set model.provider custom hermes config set model.base_url http://127.0.0.1:8080/v1 hermes config set model.default lagna360/yo-router-270m
Run Hermes
hermes
- Atomic Chat
- OpenClaw
How to use lagna360/yo-router-270m with OpenClaw:
Start the MLX server
# Install MLX LM: uv tool install mlx-lm # Start a local OpenAI-compatible server: mlx_lm.server --model "lagna360/yo-router-270m"
Configure OpenClaw
# Install OpenClaw: npm install -g openclaw@latest # Register the local server and set it as the default model: openclaw onboard --non-interactive --mode local \ --auth-choice custom-api-key \ --custom-base-url http://127.0.0.1:8080/v1 \ --custom-model-id "lagna360/yo-router-270m" \ --custom-provider-id mlx-lm \ --custom-compatibility openai \ --custom-text-input \ --accept-risk \ --skip-health
Run OpenClaw
openclaw agent --local --agent main --message "Hello from Hugging Face"
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 achitchattool 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
Quantized
Model tree for lagna360/yo-router-270m
Base model
google/functiongemma-270m-it