Sonoe z1

Presenting the Sonoe z1 model

# Introducing z1

**Sonoe · Small Open Neural Operations Ecosystem lab**
October 2026 · model `z1-0.1.0`

> A small model that doesn't talk: it **decides**. Give it a state and typed questions; it returns answers with
> honest probabilities, a confidence that means something, and the humility to say "I'm not sure."

*[Leer en español](presentando-z1.md)*

---

## In one sentence

z1 is a 118M-parameter decision model that runs on a laptop (or a $20 server with no GPU), understands Spanish,
English and the mix of both, and answers three kinds of questions (**choice**, **scale** and **yes/no**) with
calibrated probabilities and built-in abstention. No free text: every answer is data.

```json
POST /v1/decide
{
  "state": {"canal": "chat", "mensaje": "me cobraron dos veces y quiero mi dinero"},
  "questions": {
    "equipo":    {"type": "choice", "ask": "¿Qué equipo lo atiende?",
                  "options": {"facturación": "Cobros y pagos", "envíos": "Entregas", "ventas": null}},
    "urgencia":  {"type": "scale",  "ask": "¿Qué tan urgente es?", "levels": ["baja", "media", "alta"]},
    "reembolso": {"type": "binary", "ask": "¿Pide un reembolso?"}
  }
}
```

---

## The problem: deciding is not chatting

Today, when an app needs a decision ("which team gets this message?", "what kind of place is this person looking
for?"), it asks a large language model and reads the answer as text. That works in a demo and breaks in
production for four reasons:

1. **Text is not data.** The answer has to be parsed; sometimes it uses another word, adds an explanation, or
   says nothing useful.
2. **There are no honest probabilities.** An LLM says "billing" with the same certainty when it's right as when
   it's guessing.
3. **It can't stay quiet.** It always answers. The most valuable thing a decision system can do is know when to
   hand the case to a rule, another model or a person.
4. **It's expensive and far away.** Every decision is a cloud call, with its cost, its latency and the user's
   data traveling.

z1 starts from a simple idea: **separate deciding from talking**. Decisions have a shape, and that shape can be
written down as a contract.

---

## The idea: a typed contract

Every question to z1 is one of three types:

| Type | What it is | What it returns |
|---|---|---|
| `choice` | pick one of 2 to 256 options, each with an optional description | the option, the full distribution, confidence |
| `scale` | place the state on 2–10 ordered levels | the level, the **expected value** (can fall between two), confidence |
| `binary` | yes or no | the probability of yes, confidence |

Every answer also carries `abstain`: **true when confidence falls below the calibrated threshold** for that
question type. Abstaining is not an error: it's a first-class output.

Three design decisions that matter:

- **The state can be text, an object or a list.** Objects are read as `key: value` lines. The rule: *pass facts
  already computed* ("5 min walk", "open now: yes"). z1 decides; it doesn't do arithmetic.
- **Questions are independent.** None sees another's answer, and all are answered in a single pass through the
  network.
- **Options are made up at request time.** z1 has no fixed list of classes: it reads each option as text, so it
  accepts categories it has never seen.

---

## The architecture

```
state + question ──┐
                   ├─► multilingual encoder ─► mean pool ─► MLP head ─► 1 score
option i (+ desc.)─┘     (multilingual-e5-small)

scores of all options ─► ÷ temperature of the type ─► softmax ─► probabilities
                                                                └► confidence = 1 − normalized entropy
```

- **Cross-encoder, one option at a time.** Each option is read *together with* the state and the question and
  gets its own score. There's no bias from list order, any number of options fits, and the model truly compares
  the state against each alternative instead of against a precomputed vector.
- **Scales know where they are.** Each level travels with its position ("media (nivel 2 de 3)"), and training
  spreads a little probability to neighboring levels: being off by one costs less than being off by two.
- **Small, multilingual base.** `intfloat/multilingual-e5-small`: 118M parameters, 96M of which are the word
  table for ~100 languages. The actual compute is that of a ~21M model. That's why z1 is fast without a GPU.
- **Comparable confidence.** 1 − normalized entropy: 0 = no idea, 1 = certain, and it means the same with 2
  options as with 200.

---

## The core: probabilities that tell the truth

A model that says "80%" should be right 80% of the time. Almost no model does that on its own. z1 works on it in
three layers:

1. **Training with proper scoring rules:** log loss + Brier, per question. Both reward stating the real
   probability, not exaggerating. Soft labels (`probs`) are accepted for cases with more than one right answer
   and for distilling from large models.
2. **One temperature per question type**, fitted after training on data the model never saw. Choice, scale and
   yes/no fail in different ways; each gets its own correction.
3. **One abstention threshold per type:** the lowest confidence at which answered questions reach the target
   accuracy (90% by default). Below it, z1 abstains.

Measured with **ECE** (expected calibration error), **NLL** and **Brier**, before and after calibration.

---

## How we measure it: phrasings it never saw

A model that memorizes phrasings looks brilliant and fails with the first real person. So across all of z1's
data, **the last 30% of every phrasing list is never trained on**: validation (to calibrate) and test (to
measure) come from there, with different examples between them.

This first version's data is synthetic and bilingual, in six families:

| Family | Type | Example |
|---|---|---|
| routing | choice | «el artículo no es lo que pedí, lo voy a devolver» → returns |
| need | choice | `{"necesidad": "busco dónde estudiar gratis"}` → Library |
| sentiment | scale | «increíble, cinco estrellas» → very positive |
| urgency | scale | «por curiosidad, sin apuro» → low |
| refund | yes/no | «my order arrived on time» → no |
| fact | yes/no | `{"abierto ahora": "no", …}` + «¿Puedo ir ahora mismo?» → no |

---

## Results

Trained on a MacBook (Apple M5): 2,400 examples, 2 epochs, 300 steps. Test: 600 examples with phrasings never
seen.

### It learned

| | untrained | **z1-0.1.0** |
|---|---|---|
| Overall accuracy | 32.5% | **67.3%** |
| Choice | — | **91.0%** |
| Routing messages | — | **91%** |
| Need → place | — | **91%** |

### It calibrated

| | uncalibrated | **calibrated** |
|---|---|---|
| Overall ECE | 0.130 | **0.105** |
| Overall NLL | 0.753 | **0.684** |
| Yes/no ECE | 0.168 | **0.072** (−57%) |
| Scale ECE | 0.365 | **0.263** |

### It speaks two languages (and the mix)

No changes, same model:

| State | Answer |
|---|---|
| "I was charged twice and I want my money back" | billing · 99.8% |
| "my package never arrived, where is it?" | shipping · 99.97% |
| "I need to print some documents" | Library · 99.3% |
| "I need medicine for a headache" | Pharmacy · 88% *(less sure, and it says so)* |
| state in English, options in Spanish | Farmacia · 98.8% |

### Small and local

| Where | Time per request |
|---|---|
| Mac GPU (MPS), 3–4 options | **15–20 ms** |
| CPU only, 2 threads, 12 options | **37 ms** (worst case 48 ms) |

No cloud, no keys, no per-decision cost. The model fits in a 449 MB file, and in ONNX int8 it's expected to be
around 120 MB.

---

## What we learned building it

**1. Calibrating on familiar phrasings breeds arrogance.** The first calibration used validation data with the
same phrasings as training. The model looked sure of everything and, on new phrasings, calibration *worsened*
ECE (0.130 → 0.137) and produced "billing: 100%". Calibrating on held-out phrasings fixed it (0.130 → 0.105).
The lesson: **calibration is only worth the distance between its data and the world.**

**2. An optimizer can silently break honesty.** Fitting the temperature with LBFGS diverged to `NaN` on yes/no
questions and hit the bound on choice. We replaced it with a log-spaced grid search in double precision, which
cannot diverge, and a test now guards it.

**3. Abstaining is a product, not a failure.** Faced with "I want my money back" and "is the customer asking for
a refund?", z1-0.1.0 isn't sure, and says so. An app can turn that abstention into a question for the user
("Looking for…?") instead of an error.

**4. You have to see training.** We built a live dashboard alongside the model: loss curve, log, a red alert if
the process stops, step-by-step calibration, server start-up, an end-to-end test and a playground. Thanks to it
we caught in time that the Mac was out of memory (training went from ~3.5 s to ~100 s per step) and the
calibration bugs.

---

## Limits, no makeup

- **Scales and yes/no don't work yet:** 55% and 56% accuracy. Calibrated, they almost always abstain, which is
  correct but not useful. The main hypothesis: few phrasings and little contrast in those families, and yes/no
  options that differ by very few words (`expandir` in `modelo.py`). That's issue #7.
- **Synthetic data.** Everything measured is on phrasings we wrote. The real world will be stranger.
- **The choice abstention threshold came out very low** (0.0188): it meets 90% on this data, but on a new domain
  it would answer too much. That's issue #8.
- **One evaluation domain.** It still needs to be measured on families it never saw, not just new phrasings.

---

## First use: Lados's search bar

[Lados](lados.md) answers "what's near me and which stations is it by?". Its search only finds places whose name
contains the typed words: "necesito imprimir unos papeles" returns nothing.

z1 will answer a single question: **what kind of place solves this?** It will choose among Lados's 11
categories plus "Ninguna de estas" (for a hospital or a plumber), in Puerto Rico Spanish, English or a mix. Lados
keeps computing distances, routes and schedules; z1 only understands the intent. If it abstains, Lados shows
"Looking for…?" chips.

The data is ready (2,550 searches, with ambiguities and soft labels) and so is the baseline: **51%** before
training on it. The goal for `z1-0.2.0`: **≥ 90% on what it answers, in all three languages**, running as ONNX
inside Next.js on a $20 droplet.

---

## What's next

1. **z1-0.2.0:** trained on Lados and on data distilled from a large LLM, with probabilities.
2. **ONNX int8:** in Node, in the browser and on the phone.
3. **Scales and yes/no** on par with choice.
4. **Learning from use:** recalibrate with what people actually pick (`z1.recalibrar` already allows it without
   retraining).
5. **Larger bases** (multilingual mmBERT / ModernBERT) for a production version, compared on the same test.

---

## Reproduce it

```bash
python3.12 -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/python -m z1.panel                         # http://localhost:2050
.venv/bin/python -m z1.entrenar                      # trains, calibrates and evaluates; the dashboard follows live
gh release download z1-0.1.0 --repo sonoeai/z1 --dir artefactos   # or use the trained model
.venv/bin/python -m z1.evaluar --datos artefactos/lados.jsonl
```

All the code, synthetic data, tests and dashboard are in this repository.
**z1 is Sonoe's: small, local and honest about what it knows.**