A 9B I told to log a bug went and found it instead
Ornith-1.5-9B as a gap-filler — clean infra answers, and one run that died on the context wall
TL;DR
- The gap-filler over-delivers. I added a 9B to cover chores while the 24 GB box was down. It stayed: it answers real infrastructure questions and does small, self-directed tasks with a full box’s composure.
- The numbers. 40.7 ± 0.2 tok/s decode and ~1000 tok/s prefill (llama-bench, build c589f0e, Vulkan,
-ngl 99 -fa on -t 16) on a 5,780,090,816-byte Q4_K_M file, on an 8 GiB card. - The story. Told to “just log a bug,” it ran a 43-tool-call root-cause hunt, got the diagnosis right, and was on track to fix it — then died at ~58,773 total tokens, a wall set by half of its context being eaten by the standing prompt. A larger model still had to finish and log the bug.
- The caveat. It’s a 9B, not a 27B or 80B. Every “impressive” claim here rests on a handful of short burst sessions from one box, one build, and one person’s workload. The vendor’s agentic benchmarks are self-reported; I did not run them.
The setup
Ornith-1.5-9B, by ornith-ai. Dense 9.65B parameters (llama.cpp reports 9.20B effective), architecture tag qwen35 9B, MIT, released 2026-08-18. It’s a Qwen3.5 (+Gemma 4) base with continued pretraining plus agentic-RL self-improvement (GRPO) — the “agentic” in the name is the training, not a marketing word.
The story
This is the session that made me stop and write it down. The ask was deliberately small: “log a bug for me in my bug log for a pi extension I’m working on.” The job was to write a bug entry. That’s it.
What the 9B did instead was go and find it. Forty-three tool calls, almost all bash — grep and find, hunting for where the extension lives — it’s not in the obvious place. It chased false leads first, then found the actual throw site, then reached a root cause that checked out. The last recorded action was a degenerate bare cd tool call. The stream errored twice, then aborted. The run had reached ~58,773 total tokens — ~35K of it the standing prompt, roughly half the window before a single word of the task — and one more exchange would have crossed the 64K ceiling. It never wrote the bug entry. It never delivered the conclusion. It just stopped.
The result
The larger model did the rescue. I fed the 9B’s full transcript to a bigger model: “review it, tell me if the diagnosis is right, and actually fix it.” The verdict: the 9B’s diagnosis was correct, with two honest gaps. It overclaimed the scope, and it never established the exact trigger precondition that makes the failure reachable. The bigger model verified each claim against the source, wrote the minimal fix, added a regression test (suite 16/16 green, tsc clean), updated the docs, and finally logged the bug the 9B was told to log.
That is the useful boundary here. The 9B found the bug and was on track to fix it, but a larger model still had to finish the run. The story is not that a 9B replaced a 27B or 80B; it is that a small model can do most of the reasoning while the standing prompt consumes the context budget that keeps the work going.
I serve it as Ornith-1.5-9B-Q4_K_M.gguf, 5,780,090,816 bytes (~5.78 GB). I checked the sha256 against the Hugging Face lfs.oid of the official ornith-ai/Ornith-1.5-9B-GGUF repo; the file is bit-identical to upstream. That’s the whole integrity story for a downloaded weights file: one hash, one match.
It’s a thinking model, so it runs with the enable_thinking: false kwarg. Leave thinking on and the entire completion budget goes into hidden reasoning and the reply comes back empty — the same trap as the other Qwen3.5-family models I run. One flag, or an empty answer.
Hardware: an 8 GiB discrete-GPU inference laptop (RX 7600M XT), Vulkan. The 5.37 GiB of weights sit on the dGPU with their compute — ~5.4–5.7 GiB of the 8. The KV cache doesn’t fit there: at the 64K context the agent harness needs, q8_0 KV is ~5 GiB, so it lives in system RAM (--no-kv-offload, 56 GiB available). The 1-bit vision model I also keep on that box wants ~5.7 GiB of VRAM, so the two never co-reside — 5.4 + 5.7 is more than 8. A small front-door proxy (llama-swap) lazy-loads one model at a time and unloads the other, so “one at a time” is the design, not a workaround.
The bench (llama-bench, build c589f0e, Vulkan, -ngl 99 -fa on -t 16, discrete GPU pinned via GGML_VK_VISIBLE_DEVICES=1):
| metric | Ornith-1.5-9B Q4_K_M | Qwen3-8B Q4_K_M (same build) | Qwen3.5-4B |
|---|---|---|---|
| pp512 | 928.0 ± 34.1 tok/s | ~1007 | — |
| pp2048 | 998.0 ± 1.6 tok/s | — | — |
| tg128 | 40.7 ± 0.2 tok/s | 47.6 | 60–64 |
One caveat on the table: those are small-context bench numbers. The agent entry runs at 64K, and at that size the same build measured pp ~390 / tg ~7.2 t/s on a 28.5K-ctx probe (2026-09-08) — a q4_0 KV alternative bought only +15%, so it stayed q8_0. The hunt above ran at that speed, not the table’s.
It’s a 9B, so it’s slower than the 4B and a touch behind the 8B on decode. That’s the honest read of the table. What I installed it for was not raw speed — it was to cover the gap while the bigger box was down: hold the setup knowledge in context, answer fleet-architecture questions, and do small command work like managing the inference stack.
I put it through the same agentic battery the box’s 1-bit 27B and 80B already passed:
- T1, a multi-round tool loop (list two directories, calculate, answer): passed in a clean 3-of-6-round path, all tool-argument JSON valid.
- T2, strict-JSON null-safety (emit
null, don’t invent a default): passed in 1.24 s, where the 27B took 2.2 s and the 80B 6.3 s. - T3, an abstention profile (propose code but don’t volunteer to write 400 lines inline): passed the profile.
What the run actually showed
The agentic-RL training is a plausible explanation, not proof. In this one run, what I can actually verify is the behavior: the model identified the missing location, chased false leads, found the throw site, and checked a root cause. That is the transferable result — not the exact throughput, and not an attribution to GRPO.
The first measurable part is clean tool use. A 9B that emits valid tool-argument JSON, in the right order, with no invented defaults, is already ahead of most models in the class. The battery is boring by design — it’s the difference between “it called the right tool” and “it called the right tool with parseable arguments every round.”
The second is self-directed follow-through. The model took the task and extended it on its own. That behavior is useful, but the context failure still matters: the model’s intelligence was not the only limit on the task.
What it’s been used for
Three more ordinary, short, zero-cost runs, each a small task rather than a toy:
- An inference-stack question. Why the model manager’s UI wouldn’t load — it dug into the config and found the actual cause, not a symptom.
- A reload procedure. How to reset a box’s model without disturbing the rest of the stack, with the “don’t act, just walk me through it” constraint honored.
- A cloud-availability check. A GPU read as “unavailable”; it checked whether that was a real stockout or a config mismatch and reported the actual cause.
The kind of thing you’d normally keep for a bigger model — but it handled each on its own, and it cost nothing.
A grain of salt
The n is small and the selection is not random: four real sessions, one box, one build, one person’s workload, and I picked the four that make the model look good. That’s the honest frame.
The context caveat is real and it’s the one I keep coming back to. In the agent harness, ornith is registered at a 64K context window — but the standing system prompt (the tool descriptions, project instructions, and memory index) eats roughly half of that before a single word of the task. So the effective working room for a 9B burst session is ~30K, and that’s exactly the wall that killed the bug run. I’m still working out how to shrink that standing overhead. Until I do, the honest read is: the model is impressive within the short burst windows it’s actually given, not as a general replacement for the big box.
I did not verify the vendor’s agentic benchmarks. SWE-bench Verified 70.6, Terminal-Bench 2.1 46.2, SWE-bench Pro 47.5, Toolathlon 41.2, MCP-Atlas 54.2, GPQA-D 86.4 — those are their numbers, self-reported, and I did not run any of them. I’m not claiming the 9B beats the 27B or the 80B at real work; the 27B and 80B were the ones that rescued the one ornith run.
One last color note, because it’s true: this post is being written the house way, with four writers — the 80B, ornith-9b itself, a 26B MoE, and me — drafting from the same brief and me merging the best of each. In the first such run, ornith-9b and the 26B came in with zero number errors under the no-invention rule while the 80B (best voice) invented a couple of plausible details. One data point that the 9B’s discipline is a feature, not just its speed.
If you’ve measured the effective working window of a 9B-class agentic model under a heavy standing prompt — or a healthy decode number on an 8 GiB card with a different build — I’d genuinely like to know what a healthy number looks like.
The effort, honestly
This post was written the house way. The forensics, the brief, the merge, and the number-verification all ran on the rented-pod 27B (me), while three models on an 8 GiB inference laptop each drafted from the same full-contract brief. Measured from the session records at time of writing:
- The coordinator — the 27B that kept the gates, read the session logs, and verified every number: 93 messages, 110 tool calls, ~192k tokens, about 20 minutes of active model time (the session sat idle while the writers ran).
- The writers — three local models on one laptop, one identical brief each: an 80B (Qwen3-Next-80B-A3B), ornith-9b itself, and a 26B MoE (Gemma 4, QAT at Q4_K_XL): 3 calls, 5,571 completion tokens, ~16 minutes summed, first loads included.
The 80B had the best voice and invented a couple of plausible details — a port number that wasn’t there, and a claim that the 80B itself did the reviewing — that the merge had to cut. The other two drafts got every number right. No draft goes to the log unchecked.
~1,800 words · 6 prompts || 93 messages · ~192k tokens || ~20 minutes