Skip to content

Instantly share code, notes, and snippets.

@joematthews
Created July 29, 2026 20:51
Show Gist options
  • Select an option

  • Save joematthews/a3c5986247fa8ca3d37a8467454adf24 to your computer and use it in GitHub Desktop.

Select an option

Save joematthews/a3c5986247fa8ca3d37a8467454adf24 to your computer and use it in GitHub Desktop.
Humble Pi - Ornith Edition

Humble Pi (Ornith Edition) -- agentic coding on a 16 GB laptop

Ornith 1.0 9B is an MIT-licensed reasoning model built for agentic coding. It runs pi through llama.cpp on a 16 GB MacBook Air. No API key, no cloud, works offline.

download 5.98 GB (UD-Q4_K_XL)
resident about 7 GiB at 64k context
generation 13 to 15 tokens/second on an M4
SWE-bench Verified 69.4
licence MIT

1. Install llama.cpp

This gives you the llama binary. Confirm with llama version.

macOS -- Homebrew:

brew install llama.cpp

Linux -- installama.sh detects your CPU and GPU (CUDA / ROCm / Vulkan) and drops llama into ~/.local/bin:

curl -fsSL https://angt.github.io/installama.sh | sh

2. Start the server

No chat template to download or patch. Unsloth's GGUF already carries a fixed one, and llama.cpp reads it out of the model file.

Put this in ~/.zshrc (macOS) or ~/.bashrc (Linux):

alias ornith-9b='mkdir -p ~/.llama-logs && llama serve -hf unsloth/Ornith-1.0-9B-GGUF:UD-Q4_K_XL -c 65536 -fa 1 -ctk q8_0 -ctv q8_0 -ctxcp 8 --prio 2 --no-ui --jinja --parallel 1 --cache-ram 0 --no-mmproj --temp 1.0 --top-p 0.95 --top-k 20 --min-p 0 --reasoning on 2>&1 | tee ~/.llama-logs/ornith-9b-$(date +%Y%m%d-%H%M%S).log'
source ~/.zshrc
ornith-9b

The first run downloads 5.98 GB with no output for several minutes. The server is up when the log prints server is listening on http://127.0.0.1:8080.

What the flags do

flag what it does
-c 65536 Context. 64k leaves memory free; the model trains to 262144 and -fit will grant all of it on 16 GB if you ask.
-fa 1 Flash attention. Required for a quantized KV cache.
-ctk q8_0 -ctv q8_0 8-bit KV cache, roughly half the size of f16.
-ctxcp 8 Cap context checkpoints. Each is 50 MiB and the default of 32 can hold 1.5 GiB.
--prio 2 High thread priority, so the OS interrupts inference less.
--parallel 1 One slot, so the whole context serves one conversation.
--cache-ram 0 No cross-chat KV cache in RAM, which keeps memory pressure low.
--no-mmproj Skip the 922 MB vision projector. Text only.
--temp 1.0 --top-p 0.95 --top-k 20 --min-p 0 Ornith's recommended sampling. Low temperature sends this model into repetition loops.
--reasoning on Parse the model's <think> blocks.

Only 8 of Ornith's 32 layers use full attention; the rest are linear. That is why 256k of context costs 4.25 GiB rather than the ~17 GiB a conventional 9B would need.


3. Install pi and its packages

curl -fsSL https://pi.dev/install.sh | sh
pi install npm:pi-llama-cpp
pi install npm:pi-smart-fetch
pi install npm:pi-smart-web-search
pi install npm:pi-plate

4. Run it

# terminal 1
source ~/.zshrc && ornith-9b
# terminal 2
mkdir -p ~/Code/demo && cd ~/Code/demo && pi

5. A prompt that works

Build a todo app in ./todo with separate index.html, style.css and app.js. No dependencies, no build step.

Features: add, toggle complete, delete, filter all/active/done, clear completed, item count, persist to localStorage.

Use one delegated click handler. Keep ids as strings everywhere. When the files are written, re-read app.js and trace each feature once to confirm the logic holds.

Dark theme, readable at half-screen width. Then run npx serve.

About 15 turns and 5 minutes. The last paragraph earns its place: without it the first attempt compared a numeric id against dataset.id, which is always a string, so delete and toggle silently did nothing.


BONUS: strict linting the model has to satisfy

A small model writes plausible TypeScript. Type-aware linting is what tells it when plausible is wrong. This turns on typescript-eslint's strictest type-checked rules, its stylistic rules, and eslint-plugin-repertoire, which catches characters nobody typed -- em dashes, curly quotes, invisible zero-width joiners.

npm install --save-dev eslint typescript typescript-eslint eslint-config-prettier eslint-plugin-repertoire
// eslint.config.mjs
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import prettier from "eslint-config-prettier";
import repertoire from "eslint-plugin-repertoire";

export default tseslint.config(
  { ignores: ["dist/**", "node_modules/**"] },
  {
    files: ["**/*.ts"],
    extends: [
      eslint.configs.recommended,
      ...tseslint.configs.strictTypeChecked,
      ...tseslint.configs.stylisticTypeChecked,
    ],
    languageOptions: {
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
    plugins: { repertoire },
    rules: { "repertoire/no-undeclared-characters": "error" },
  },
  prettier,
);

strictTypeChecked reads the type checker rather than the syntax, so it catches a floating promise, an unnecessary condition, or an unsafe any that parsing alone cannot see. Those are the mistakes a 9B model makes most.

Point the agent at the output:

Run npx eslint . and fix every error.

Be warned that ESLint flat config is the hardest thing to ask a small local model to write. It is recent enough that training data is dominated by the old .eslintrc format, and models regress to it even with the current documentation in front of them. Write the config yourself and let the model fix what it reports.

Add markdown, JSON and YAML to the character check with their parsers:

npm install --save-dev @eslint/markdown jsonc-eslint-parser yaml-eslint-parser
{
  files: ["**/*.md"],
  language: "markdown/gfm",
  plugins: { markdown, repertoire },
  rules: { "repertoire/no-undeclared-characters": "error" },
},

Writing in another language? Name it, and its letters are permitted:

"repertoire/no-undeclared-characters": ["error", { languages: ["de", "pl"] }]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment