diff --git a/.github/workflows/publish-ollama.yml b/.github/workflows/publish-ollama.yml new file mode 100644 index 0000000..69c819d --- /dev/null +++ b/.github/workflows/publish-ollama.yml @@ -0,0 +1,111 @@ +name: publish-ollama +# Every change to main that touches a Modelfile or an Ollama page is published on ollama.com: +# the changed models are rebuilt and pushed as Tromset/, then the pages are updated from docs/ollama/. +# Needs two repository secrets (Settings > Secrets and variables > Actions): +# OLLAMA_KEY the private key ~/.ollama/id_ed25519 of a machine linked to the Tromset account (`ollama signin`) +# OLLAMA_API_KEY an API key from https://ollama.com/settings/keys +on: + push: + branches: [main] + paths: + - 'Modelfile*' + - 'docs/ollama/**' + - 'scripts/publish.sh' + - 'scripts/update-ollama-pages.sh' + - '.github/workflows/publish-ollama.yml' + workflow_dispatch: + inputs: + models: + description: 'Models to publish (space-separated names, "all", or "none" for pages only)' + default: all +concurrency: + group: publish-ollama + cancel-in-progress: false +jobs: + publish: + if: github.repository == 'Tromset/flycoder' + runs-on: ubuntu-latest + timeout-minutes: 120 + env: + OLLAMA_KEY: ${{ secrets.OLLAMA_KEY }} + OLLAMA_API_KEY: ${{ secrets.OLLAMA_API_KEY }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Pick the models to publish + id: pick + env: + BEFORE: ${{ github.event.before }} + REQUESTED: ${{ inputs.models }} + run: | + if [ "$GITHUB_EVENT_NAME" = workflow_dispatch ]; then + models=$REQUESTED + elif ! changed=$(git diff --name-only "$BEFORE" "$GITHUB_SHA" 2>/dev/null); then + models=all + else + models= + for file in $changed; do + case $file in + Modelfile) models="$models flycoder0.3" ;; + Modelfile.fast) models="$models flycoder0.3fast" ;; + Modelfile.pro) models="$models flycoder0.3pro" ;; + Modelfile.lite) models="$models flycoder0.3:lite" ;; + Modelfile.router) models="$models flycoder0.3:router" ;; + scripts/publish.sh|.github/workflows/publish-ollama.yml) models=all; break ;; + esac + done + fi + [ -n "$models" ] || models=none + echo "Models to publish: $models" + echo "models=$models" >> "$GITHUB_OUTPUT" + + - name: Check the secrets + run: | + [ -n "$OLLAMA_KEY" ] || { echo "::error::Add the OLLAMA_KEY secret (contents of ~/.ollama/id_ed25519 from a machine signed in to the Tromset account)."; exit 1; } + [ -n "$OLLAMA_API_KEY" ] || echo "::warning::OLLAMA_API_KEY is not set: the model pages will not be updated." + + - name: Free disk space for the base models + if: steps.pick.outputs.models != 'none' + run: | + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL + sudo docker image prune --all --force >/dev/null + df -h / + + - name: Start Ollama with the Tromset key + if: steps.pick.outputs.models != 'none' + run: | + curl -fsSL https://ollama.com/install.sh | sh + sudo systemctl disable --now ollama || true + # Pushes are signed by the server with ~/.ollama/id_ed25519: run it as this user. + mkdir -p ~/.ollama && chmod 700 ~/.ollama + printf '%s\n' "$OLLAMA_KEY" > ~/.ollama/id_ed25519 && chmod 600 ~/.ollama/id_ed25519 + ssh-keygen -y -f ~/.ollama/id_ed25519 > ~/.ollama/id_ed25519.pub + nohup ollama serve > "$RUNNER_TEMP/ollama.log" 2>&1 & + for _ in $(seq 30); do ollama list >/dev/null 2>&1 && break; sleep 1; done + ollama --version + + - name: Build and push the models + if: steps.pick.outputs.models != 'none' + env: + FLYCODER_PRUNE: '1' + MODELS: ${{ steps.pick.outputs.models }} + run: | + # MODELS is a space-separated list of names: split it on purpose. + # shellcheck disable=SC2086 + if [ "$MODELS" = all ]; then sh scripts/publish.sh Tromset; else sh scripts/publish.sh Tromset $MODELS; fi + + - name: Update the model pages + if: env.OLLAMA_API_KEY != '' + run: | + if sh scripts/update-ollama-pages.sh Tromset; then + echo "Model pages updated from docs/ollama/." >> "$GITHUB_STEP_SUMMARY" + else + echo "::warning::Some ollama.com pages were not updated (see the log). Paste docs/ollama/.md into the page by hand." + echo "Some ollama.com pages were not updated: paste docs/ollama/.md by hand." >> "$GITHUB_STEP_SUMMARY" + fi + + - name: Remove the key + if: always() + run: rm -f ~/.ollama/id_ed25519 ~/.ollama/id_ed25519.pub diff --git a/Documentation.md b/Documentation.md index 24c9f92..ea01baf 100644 --- a/Documentation.md +++ b/Documentation.md @@ -1,6 +1,6 @@ -# FlyCoder 0.3 beta documentation +# FlyCoder 0.3 documentation -FlyCoder is a local coding model that runs in [Ollama](https://ollama.com), tuned for Apple Silicon MacBooks and Mac minis. This documentation covers installation, usage, settings, the benchmark, publishing and troubleshooting. For a quick overview, see the [README](readme.md). +FlyCoder is a local coding model that runs in [Ollama](https://ollama.com), tuned for MacBooks and Mac minis. This documentation covers installation, usage, settings, the benchmark, publishing and troubleshooting. For a quick overview, see the [README](readme.md). ## Contents @@ -11,7 +11,7 @@ FlyCoder is a local coding model that runs in [Ollama](https://ollama.com), tune 5. [Usage](#5-usage) 6. [Settings](#6-settings) 7. [Benchmark](#7-benchmark) -8. [Publishing a version on ollama.com](#8-publishing-a-version-on-ollamacom) +8. [Publishing on ollama.com](#8-publishing-on-ollamacom) 9. [Troubleshooting](#9-troubleshooting) 10. [Development](#10-development) 11. [Known limitations](#11-known-limitations) @@ -20,43 +20,41 @@ FlyCoder is a local coding model that runs in [Ollama](https://ollama.com), tune ## 1. What FlyCoder is -FlyCoder 0.2 beta is an **Ollama profile**: an open base model, a quantization suited to Macs, the official sampling settings, an explicit context window and a system prompt dedicated to code. You use it like any Ollama model: in the terminal, through the API, or behind a coding agent. +FlyCoder 0.3 is an **Ollama profile**: an open base model, the sampling settings its authors recommend for code, a large context window and a short system prompt dedicated to code. You use it like any Ollama model: in the terminal, through the API, or behind a coding agent. -FlyCoder does not retrain the weights. The gains over 0.1 come from the choice of base and settings, and are measured by the repository's benchmark (section 7). +It comes in three sizes, each published as its own model on ollama.com: -Since 0.2, FlyCoder no longer includes a workshop (CLI, web interface, Electron app, FlyBrain controller): only the model ships. +| Model | Base | Weights | Mac | Context | +|---|---|---|---|---| +| `Tromset/flycoder0.3` | Qwen3.5 9B | 6.6 GB | 16 GB | 65,536 tokens | +| `Tromset/flycoder0.3fast` | Qwen3.5 4B | 3.3 GB | 8 GB | 32,768 tokens | +| `Tromset/flycoder0.3pro` | Qwen3.8 27B | 18 GB | 32 GB or more | 65,536 tokens | + +FlyCoder does not retrain the weights. The gains come from the choice of base and settings, and are measured by the repository's benchmark (section 7). ## 2. Requirements | Item | Minimum | Note | |---|---|---| -| Mac | Apple Silicon (M1 or later) | Intel Macs, Linux and Windows: GGUF variants | -| Memory | 8 GB for `fast`, 16 GB for the main variant | The installer picks for you | -| Ollama | 0.31 or later | Main variant on MLX; `fast` on MLX needs 0.19 | -| Disk | 4.0 GB (`fast`) or 7.7 GB (main) | Shared with the base models, no duplicate copy | -| Node.js | 22 | Only for the benchmark and the tests | +| Computer | Any Mac (Apple Silicon or Intel), Linux or Windows | The published models are GGUF: they run everywhere | +| Memory | 8 GB (`fast`), 16 GB (`flycoder0.3`), 32 GB (`pro`) | The installer picks for you | +| Ollama | 0.30 (`flycoder0.3`, `fast`), 0.32.12 (`pro`) | `ollama --version`; update from https://ollama.com/download | +| Node.js | 22 | Only for FlyBrain, the benchmark and the tests | | Python 3 | The one shipped with macOS | Only for the benchmark | -Check the Ollama version: - -```sh -ollama --version -``` - ## 3. Installation ### 3.1 From ollama.com -FlyCoder is published at [ollama.com/Tromset/flycoder](https://ollama.com/Tromset/flycoder). One command is enough: +One command is enough: ```sh -ollama run Tromset/flycoder # main variant (Gemma 4 12B, MLX) -ollama run Tromset/flycoder:fast # fast variant (Qwen3.5 4B, MLX) -ollama run Tromset/flycoder:0.2-beta-gguf # main, Intel Mac, Linux, Windows -ollama run Tromset/flycoder:0.2-beta-fast-gguf # fast, Intel Mac, Linux, Windows +ollama run Tromset/flycoder0.3 # Macs with 16 GB +ollama run Tromset/flycoder0.3fast # Macs with 8 GB +ollama run Tromset/flycoder0.3pro # Macs with 32 GB or more ``` -Available tags: `latest` and `0.2-beta` (main), `fast` and `0.2-beta-fast` (fast), `0.2-beta-gguf` and `0.2-beta-fast-gguf` (portable). For now only the GGUF tags (`0.2-beta-gguf`, `0.2-beta-fast-gguf`, `0.2-beta-lite-gguf`, `router-gguf`) are online; the MLX tags arrive once they are pushed from an Apple Silicon Mac. Until then, Mac users install with `install.sh`. To call it simply `flycoder`: `ollama cp Tromset/flycoder flycoder`. +To call it simply `flycoder`: `ollama cp Tromset/flycoder0.3 flycoder`. ### 3.2 One-command installer @@ -67,23 +65,21 @@ ollama run flycoder The installer: -1. checks that Ollama is installed, running and at version 0.31 or later; +1. checks that Ollama is installed, running and recent enough for the chosen variant; 2. detects the system and the memory; -3. picks `flycoder:0.2-beta` from 15 GB of memory up, otherwise `flycoder:0.2-beta-fast`; -4. downloads the base model, creates the variant and copies it to `flycoder:latest`. +3. picks `flycoder0.3pro` from 30 GB of memory up, `flycoder0.3` from 15 GB, otherwise `flycoder0.3fast`; +4. downloads the base model, creates the variant and copies it to the short name `flycoder`; +5. installs FlyBrain's experts and server (section 5.5), except with `pro`. Options, placed after `sh -s --` when the script is read from `curl`: | Option | Effect | |---|---| -| `--fast` | installs only the fast variant | -| `--all` | installs both variants; `latest` points to the main one | -| `--gguf` | uses the portable GGUF weights (Intel Mac, Linux, Windows) instead of MLX | -| `--no-brain` | skips the FlyBrain experts (`flycoder:router`, `flycoder:0.2-beta-lite`) | - -```sh -curl -fsSL https://raw.githubusercontent.com/Tromset/flycoder/main/install.sh | sh -s -- --all -``` +| `--fast` | installs `flycoder0.3fast` | +| `--pro` | installs `flycoder0.3pro` | +| `--all` | installs all three; `flycoder` points to `flycoder0.3` | +| `--mlx` | uses Ollama's MLX engine on Apple Silicon (faster, needs Ollama 0.31) instead of the portable GGUF weights | +| `--no-brain` | skips the FlyBrain experts (`flycoder0.3:router`, `flycoder0.3:lite`) | From a copy of the repository, `sh install.sh` uses the local Modelfiles. The `FLYCODER_REF` variable picks another GitHub branch or tag to download them from. @@ -91,56 +87,48 @@ From a copy of the repository, `sh install.sh` uses the local Modelfiles. The `F ```sh git clone https://github.com/Tromset/flycoder.git && cd flycoder -ollama create flycoder:0.2-beta -f Modelfile -ollama create flycoder:0.2-beta-fast -f Modelfile.fast -ollama cp flycoder:0.2-beta flycoder:latest +ollama create flycoder0.3 -f Modelfile +ollama create flycoder0.3fast -f Modelfile.fast +ollama create flycoder0.3pro -f Modelfile.pro ``` -On a machine without Apple Silicon, first replace the `FROM` line: `gemma4:12b-mlx` with `gemma4:12b`, and `qwen3.5:4b-mlx` with `qwen3.5:4b`. - ### 3.4 Checking the installation ```sh -ollama list # flycoder:latest, flycoder:0.2-beta... -ollama show flycoder # base, quantization, capabilities -ollama show flycoder --parameters # applied settings -ollama run flycoder "Write a Python function that reverses a string." -ollama ps # memory used, GPU share, allocated context +ollama list # flycoder0.3, flycoder... +ollama show flycoder0.3 # base, quantization, capabilities +ollama show flycoder0.3 --parameters # applied settings +ollama run flycoder0.3 "Write a Python function that reverses a string." +ollama ps # memory used, GPU share, allocated context ``` -In `ollama ps`, the PROCESSOR column should show `100% GPU`. A CPU/GPU split means memory is short (section 9). +On a Mac, the PROCESSOR column of `ollama ps` should show `100% GPU`. A CPU/GPU split means memory is short (section 9). ### 3.5 Uninstalling ```sh -ollama rm flycoder:latest flycoder:0.2-beta flycoder:0.2-beta-fast -ollama rm gemma4:12b-mlx qwen3.5:4b-mlx # bases, if no other model uses them +ollama rm flycoder flycoder0.3 flycoder0.3fast flycoder0.3pro flycoder0.3:lite flycoder0.3:router +ollama rm qwen3.5:9b qwen3.5:4b qwen3.5:2b qwen3.5:0.8b qwen3.8:27b # bases, if no other model uses them ``` ## 4. Choosing a variant -| Tag | Base | Weights | Context | For | -|---|---|---|---|---| -| `flycoder:0.2-beta`, `latest` | Gemma 4 12B (Google DeepMind), NVFP4 on MLX | 7.7 GB | 32,768 | Macs with 16 GB or more, maximum quality | -| `flycoder:0.2-beta-fast`, `fast` | Qwen3.5 4B (Alibaba Qwen), NVFP4 on MLX | 4.0 GB | 16,384 | 8 GB Macs, 0.1's speed | -| `flycoder:0.2-beta-gguf` | Gemma 4 12B, GGUF Q4_K_M | 8.0 GB | 32,768 | Intel Mac, Linux, Windows | -| `flycoder:0.2-beta-fast-gguf` | Qwen3.5 4B, GGUF Q4_K_M | 3.4 GB | 16,384 | Intel Mac, Linux, Windows | +Take the largest variant your memory allows: -Both bases support tool calling, thinking and images. +- **`flycoder0.3`** (Qwen3.5 9B, 16 GB Macs): the default. With its 64K context it uses about 9 GB in memory (measured: 8.8 GB). +- **`flycoder0.3fast`** (Qwen3.5 4B, 8 GB Macs): about twice as fast per token, a lower pass rate on hard problems. +- **`flycoder0.3pro`** (Qwen3.8 27B, 32 GB Macs or more): the latest Qwen generation with its own FlyCoder recipe, for hard bugs, algorithms and coding agents (section 6.5). -Rule of thumb: take the main variant if your Mac has 16 GB or more and code quality matters more than latency; take `fast` on 8 GB or if you want exactly 0.1's responsiveness. To change the default model: +Every Modelfile declares the Ollama version it needs (`REQUIRES`): an older Ollama names the version to install instead of failing to load the model. -```sh -ollama cp flycoder:0.2-beta-fast flycoder:latest # the fast variant becomes "flycoder" -ollama cp flycoder:0.2-beta flycoder:latest # back to the main variant -``` +All three bases support tool calling, thinking and images. ## 5. Usage ### 5.1 In the terminal ```sh -ollama run flycoder +ollama run flycoder0.3 ``` Type your request, then press Enter. For multi-line text, wrap it in `"""`. Useful commands in a conversation: @@ -148,7 +136,7 @@ Type your request, then press Enter. For multi-line text, wrap it in `"""`. Usef | Command | Effect | |---|---| | `/set nothink` / `/set think` | turns thinking off or back on | -| `/set parameter num_ctx 65536` | enlarges the context for the session | +| `/set parameter num_ctx 131072` | enlarges the context for the session | | `/set system "…"` | replaces the system prompt for the session | | `/show parameters` | shows the current settings | | `/clear` | clears the conversation | @@ -159,16 +147,16 @@ Launch options: `--think=false` (immediate answer), `--hidethinking` (hides the Giving the model a file: ```sh -ollama run flycoder "Find the bugs in this file and suggest a fix: $(cat src/parser.py)" +ollama run flycoder0.3 "Find the bugs in this file and suggest a fix: $(cat src/parser.py)" ``` -Giving it an image (screenshot, diagram): write its path in the request, for example `ollama run flycoder "Write the HTML for this mockup: ./mockup.png"`. +Giving it an image (screenshot, diagram): write its path in the request, for example `ollama run flycoder0.3 "Write the HTML for this mockup: ./mockup.png"`. ### 5.2 Ollama REST API ```sh curl http://localhost:11434/api/chat -d '{ - "model": "flycoder", + "model": "Tromset/flycoder0.3", "messages": [{"role": "user", "content": "Write a JavaScript function debounce(fn, ms)."}], "think": false, "stream": false @@ -177,7 +165,7 @@ curl http://localhost:11434/api/chat -d '{ - `"think": true` or `false` turns thinking on or off; the thinking comes back separately in `message.thinking`. - `"stream": true` (the default) sends the answer as it is generated, as JSON lines. -- `"options": {"num_ctx": 65536}` changes a setting for one request. +- `"options": {"num_ctx": 131072}` changes a setting for one request. - A `{"role": "system", ...}` message replaces FlyCoder's system prompt for that conversation. ### 5.3 OpenAI-compatible API @@ -189,7 +177,7 @@ from openai import OpenAI client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama") reply = client.chat.completions.create( - model="flycoder", + model="Tromset/flycoder0.3", messages=[{"role": "user", "content": "Write a pytest test for a slugify function."}], ) print(reply.choices[0].message.content) @@ -200,84 +188,89 @@ print(reply.choices[0].message.content) `ollama launch` sets up and opens an agent with FlyCoder: ```sh -ollama launch claude --model flycoder # Claude Code -ollama launch codex --model flycoder # Codex -ollama launch opencode --model flycoder # OpenCode +ollama launch claude --model Tromset/flycoder0.3 # Claude Code +ollama launch codex --model Tromset/flycoder0.3 # Codex +ollama launch opencode --model Tromset/flycoder0.3 # OpenCode ``` -If you got FlyCoder from ollama.com without `install.sh`, use the published name: `ollama launch claude --model Tromset/flycoder`. - -`ollama launch --help` lists the other integrations, including VS Code, Copilot CLI, Cline, Qwen Code, Pi and Droid. Agents send long instructions and many files: Ollama recommends at least 64,000 tokens of context for them (section 6.2). +`ollama launch --help` lists the other integrations. Ollama recommends at least 64,000 tokens of context for agents: `flycoder0.3` and `flycoder0.3pro` have 65,536 out of the box. For agents, `flycoder0.3pro` gives the best results if your Mac can hold it. ### 5.5 FlyBrain: the router that lowers RAM use -FlyBrain is a small, dependency-free Node 22 server that sits in front of Ollama on port 11435. It answers to the names `flycoder` and `flycoder:fast`, picks an expert for each request and keeps only one expert in memory. Other models and other routes pass through unchanged. +FlyBrain is a small, dependency-free Node 22 server that sits in front of Ollama on port 11435. It answers to the names `flycoder0.3` (and `flycoder`) and `flycoder0.3fast`, picks an expert for each request and keeps only one expert in memory. Other models, `flycoder0.3pro` included, and other routes pass through unchanged. + +| You call | Simple request | Hard request, tools (agents) or long context | Unclear request | +|---|---|---|---| +| `flycoder0.3` | `flycoder0.3fast` (4B), with thinking | `flycoder0.3` (9B), with thinking | the `flycoder0.3:router` micro-model (Qwen3.5 0.8B) decides | +| `flycoder0.3fast` | `flycoder0.3:lite` (Qwen3.5 2B) | `flycoder0.3fast` (4B) | the 4B (no micro-model) | ```sh node ~/.flycoder/brain/flybrain.mjs # copied by install.sh; or npm run brain in this repository -node brain/flybrain.mjs --prefix Tromset/ # with the models published on ollama.com +node brain/flybrain.mjs --prefix Tromset/ # with the models pulled from ollama.com ``` -Options: `--port` (11435), `--ollama` (http://127.0.0.1:11434), `--prefix`, `--max-expert auto|full|fast`. With `auto`, the default, a Mac with less than 16 GB caps `flycoder` at the 4B. +With `--prefix Tromset/`, first pull the experts: `ollama pull Tromset/flycoder0.3:router`, `Tromset/flycoder0.3:lite`, `Tromset/flycoder0.3fast` and `Tromset/flycoder0.3`. + +Options: `--port` (11435), `--ollama` (http://127.0.0.1:11434), `--prefix`, `--max-expert auto|full|fast`. With `auto`, the default, a machine with less than 16 GB caps `flycoder0.3` at the 4B. How a request is routed: 1. **Rules** (free): a request is hard if it carries tools (agents), more than 6,000 characters, several code blocks, a bulleted specification, or a word such as debug, algorithm, optimize, security, error. It is simple if it is a short question without code ("what is", "how do I", "explain"…). The rules recognize these cues in both English and French. -2. **The `flycoder:router` micro-model** (Qwen3.5 0.8B, 1 GB) for the rest. It answers `{"level":"simple"}` or `{"level":"hard"}` in an enforced JSON format. If it fails, takes longer than 8 s or answers anything else, the request goes to the large model. It is not called when the large model is already loaded. `flycoder:fast` has no micro-model: an unclear request keeps the 4B. +2. **The micro-model** for the rest. It answers `{"level":"simple"}` or `{"level":"hard"}` in an enforced JSON format. If it fails, takes longer than 8 s or answers anything else, the request goes to the large model. It is not called when the large model is already loaded. 3. **Conversation memory**: a conversation keeps its expert and can only move up to the stronger one. 4. **A single expert**: before loading another expert, FlyBrain waits for the running answers to finish, then unloads the old one. It also unloads the micro-model before the large model. -The normal variant forces thinking (`think: true`) unless the client turns it off. Each response carries the `x-flybrain-expert` and `x-flybrain-reason` headers, and FlyBrain's terminal prints one line per decision. +On 60 labelled requests (the 20 bench problems plus [tests/fixtures/route-prompts.json](tests/fixtures/route-prompts.json)), the rules and the micro-model route 59 correctly; no hard request goes to the small model. Measure it on your own requests with `node brain/eval-router.mjs`. Connecting tools: ```sh -OLLAMA_HOST=127.0.0.1:11435 ollama run flycoder -ANTHROPIC_BASE_URL=http://127.0.0.1:11435 ANTHROPIC_AUTH_TOKEN=ollama ANTHROPIC_API_KEY="" claude --model flycoder -curl http://127.0.0.1:11435/v1/chat/completions -d '{"model":"flycoder","messages":[{"role":"user","content":"Hello"}]}' +OLLAMA_HOST=127.0.0.1:11435 ollama run flycoder0.3 +ANTHROPIC_BASE_URL=http://127.0.0.1:11435 ANTHROPIC_AUTH_TOKEN=ollama ANTHROPIC_API_KEY="" claude --model flycoder0.3 +curl http://127.0.0.1:11435/v1/chat/completions -d '{"model":"flycoder0.3","messages":[{"role":"user","content":"Hello"}]}' ``` -To measure the router's accuracy on your own requests: add them to `tests/fixtures/route-prompts.json`, then run `node brain/eval-router.mjs`. The memory and accuracy measurements are in the [README](readme.md#flybrain-less-ram-same-quality-on-hard-requests). +Each response carries the `x-flybrain-expert` and `x-flybrain-reason` headers, and FlyBrain's terminal prints one line per decision. ## 6. Settings ### 6.1 What the Modelfiles set -| Parameter | Main | `fast` | Why | -|---|---|---|---| -| `num_ctx` | 32,768 | 16,384 | Otherwise Ollama limits machines under 24 GB of memory to 4,096 tokens | -| `temperature` | 1 | 0.6 | Official values from Google and Qwen for coding | -| `top_k` | 64 | 20 | Same | -| `top_p` | 0.95 | 0.95 | Same | -| `min_p`, `presence_penalty`, `repeat_penalty` | inherited | 0, 0, 1 | Qwen's "coding in thinking mode" settings | +| Parameter | `flycoder0.3` | `fast` | `pro` | Why | +|---|---|---|---|---| +| `num_ctx` | 65,536 | 32,768 | 65,536 | Otherwise Ollama limits machines under 24 GB of memory to 4,096 tokens | +| `temperature` | 0.6 | 0.6 | 0.6 | Qwen's settings for coding in thinking mode | +| `top_k`, `top_p`, `min_p` | 20, 0.95, 0 | same | same | Same | +| `presence_penalty`, `repeat_penalty` | 0, 1 | same | same | Same | +| `draft_num_predict` | — | — | 4 | Multi-token prediction: Qwen3.8's built-in draft layer proposes 4 tokens per pass | -The main variant also inherits Gemma 4's multi-token prediction draft model, enabled by Ollama. Avoid lowering the temperature: Qwen and Google recommend it as is, and 0.1 (temperature 0.2) produced looping answers. +Avoid lowering the temperature: Qwen advises against it, and FlyCoder 0.1 (temperature 0.2) produced looping answers. ### 6.2 Enlarging the context -For one session: `/set parameter num_ctx 65536`. For a permanent model, create a variant: +The bases support up to 262,144 tokens. For one session: `/set parameter num_ctx 131072`. For a permanent model, create a variant: ```sh -printf 'FROM flycoder:0.2-beta\nPARAMETER num_ctx 65536\n' > Modelfile.64k -ollama create flycoder:64k -f Modelfile.64k -ollama launch claude --model flycoder:64k +printf 'FROM Tromset/flycoder0.3\nPARAMETER num_ctx 131072\n' > Modelfile.128k +ollama create flycoder0.3:128k -f Modelfile.128k ``` -Memory cost of the cache, on top of the weights: about 0.5 GB per 32,768 tokens for the main variant, and about 0.5 GB per 16,384 tokens for `fast`. On a 16 GB Mac, 65,536 tokens should fit with the main variant if few other applications are running (not measured): check with `ollama ps` that the model stays at `100% GPU`. +Qwen3.5 and Qwen3.8 use a hybrid attention where only one layer in four keeps a growing cache, so a large context costs little memory: `flycoder0.3` uses 8.8 GB in total at 65,536 tokens (measured with `ollama ps`). For `flycoder0.3pro`, the GGUF header gives 16 cache layers of 4 KB per token each: about 4.4 GB at 65,536 tokens, 22 GB in total with the weights (computed, not measured). On a 32 GB Mac, if `ollama ps` shows a CPU share, use `/set parameter num_ctx 32768`. Check with `ollama ps` that the model stays at `100% GPU` after a change. ### 6.3 Thinking -Thinking is on by default: the model reasons before answering, which clearly improves code on hard problems but lengthens the wait. Turn it off for simple questions (`--think=false`, `/set nothink`, or `"think": false` in the API). Without thinking, the benchmark showed that models sometimes leave an abandoned first attempt in the returned code; keep it on for code you ship. +Thinking is on by default: the model reasons before answering, which clearly improves code on hard problems but lengthens the wait. Turn it off for simple questions (`--think=false`, `/set nothink`, or `"think": false` in the API). ### 6.4 System prompt -FlyCoder's system prompt (`ollama show flycoder --system`) asks the model to: +The system prompt of `flycoder0.3`, `fast` and `lite` (`ollama show flycoder0.3 --system`) asks the model to: - keep the requested names, signatures and files exactly; - deliver complete, runnable code, without "TODO"; - handle edge cases; - prefer the standard library and never invent APIs; - choose a suitable algorithm; +- check its code once more before answering, and give a single final version; - show only the changed parts of an existing file; - never claim to have run tests; - avoid common vulnerabilities; @@ -285,7 +278,18 @@ FlyCoder's system prompt (`ollama show flycoder --system`) asks the model to: Replace it for a session with `/set system`, or with a `system` message in the API. -### 6.5 Ollama server settings +### 6.5 What makes `flycoder0.3pro` different + +`flycoder0.3pro` is not Qwen3.8 with a new name. Its Modelfile gives it: + +- **Its own workflow** in the system prompt, in six steps: contract (exact names, inputs, errors; one question only if an ambiguity changes the code), plan (at most five lines for multi-file work), code, tests that prove the code, a check of the tricky cases before answering, and a one-line report on how to run the tests. +- **Agent rules**: read files before editing them, change as little as the task needs, run the project's tests and report their real output, never run a destructive command without the user's agreement. +- **A worked example** (`MESSAGE`): one request and the expected answer (assumption, code, pytest tests, run command), so the model sees the format instead of only reading about it. `npm test` runs the example's code against its own test cases. +- **Multi-token prediction** (`draft_num_predict 4`) and 64K tokens of context. + +The weights are Qwen3.8's: FlyCoder does not retrain them. The recipe was measured on the 9B base, because the test server cannot hold the 27B (section 7). + +### 6.6 Ollama server settings These variables apply to every model. On a Mac running the Ollama app, set them with `launchctl setenv`, then restart Ollama: @@ -294,8 +298,6 @@ launchctl setenv OLLAMA_KV_CACHE_TYPE q8_0 # context cache half the size, s launchctl setenv OLLAMA_KEEP_ALIVE 30m # keeps the model loaded for 30 minutes ``` -`OLLAMA_KV_CACHE_TYPE=q8_0` halves the context cache memory of GGUF models (`-gguf` tags); Ollama's documentation does not say how it affects the MLX engine. `OLLAMA_KEEP_ALIVE` avoids reloading the model between two widely spaced questions. - ## 7. Benchmark The `bench/` folder measures the pass rate and speed of any Ollama model on 20 coding problems: @@ -305,8 +307,7 @@ The `bench/` folder measures the pass rate and speed of any Ollama model on 20 c - each test is proven by a reference solution and rejects an empty solution (`npm test`). ```sh -ollama create flycoder0.1beta -f bench/baselines/flycoder-0.1-beta.Modelfile # to compare with 0.1 -npm run bench -- --models flycoder0.1beta,flycoder:0.2-beta,flycoder:0.2-beta-fast +npm run bench -- --models flycoder0.3,flycoder0.3fast ``` | Option | Effect | @@ -320,48 +321,46 @@ npm run bench -- --models flycoder0.1beta,flycoder:0.2-beta,flycoder:0.2-beta-fa | `--out file.json` | report location; `bench/results/` by default | | `--no-sandbox` | disables the macOS sandbox | -The bench prints a table (passes, generation rate, prompt processing speed, tokens per answer, seconds per problem) and records every answer, every test error and every counter in the JSON, updated after each problem. +The bench runs the code written by the models on your machine, in a temporary folder, with a time limit. On macOS, `sandbox-exec` blocks network access and writes outside that folder. -It runs the code written by the models on your machine, in a temporary folder, with a time limit. On macOS, `sandbox-exec` blocks network access and writes outside that folder. +The 0.3 reference run (`docs/bench/cpu-nothink-0.3.json`) is being recorded. The 0.2 run stays in [docs/bench/cpu-nothink.json](docs/bench/cpu-nothink.json). -Reference results, recorded in [docs/bench/cpu-nothink.json](docs/bench/cpu-nothink.json) on a Linux server without a GPU, with GGUF weights, without thinking and one attempt per problem: +## 8. Publishing on ollama.com -| Profile | Passed | Generation | Tokens per answer | Time per problem | -|---|---|---|---|---| -| `flycoder0.1beta` (0.1) | 5/20 | 6.6 tok/s | 581 | 94 s | -| `flycoder:0.2-beta-fast` | 5/20 | 6.5 tok/s | 537 | 89 s | -| `flycoder:0.2-beta` | 14/20 | 3.4 tok/s | 739 | 235 s | +Publishing is automatic. On every change to `main` that touches a Modelfile, a page in `docs/ollama/` or the publishing scripts, the [publish-ollama](.github/workflows/publish-ollama.yml) workflow: -On this server, the main variant solves almost three times as many problems, but takes about 2.5 times as long per problem. The fast variant keeps 0.1's speed and pass rate. No measurement has been made on a Mac yet, where the MLX engine and multi-token prediction should narrow the speed gap. +1. rebuilds the changed models from their Modelfiles and pushes them as `Tromset/flycoder0.3`, `Tromset/flycoder0.3fast`, `Tromset/flycoder0.3pro`, `Tromset/flycoder0.3:lite` and `Tromset/flycoder0.3:router`; +2. updates each model page on ollama.com from `docs/ollama/.md`. -## 8. Publishing a version on ollama.com +It can also be started by hand from the Actions tab (Run workflow), with a list of models or `none` to update only the pages. -The GGUF tags of 0.2 beta are published under the `Tromset` account; the MLX tags must be pushed from an Apple Silicon Mac, because Ollama cannot build MLX models on Linux. To publish a new version, follow these steps. The script refuses to push non-MLX builds under the main tags: build them on an Apple Silicon Mac. +One-time setup, in the repository's Settings > Secrets and variables > Actions: -1. Create an account on [ollama.com](https://ollama.com/signup). The username becomes the model's prefix (`/flycoder`). -2. Link the machine to the account: `ollama signin`, then confirm in the browser. Authorized machines are managed in the account settings, under Ollama keys; remove the ones you no longer use. -3. Build both variants: `sh install.sh --all`. -4. Publish: `sh scripts/publish.sh `. Add `--gguf` to also publish the GGUF tags (`0.2-beta-gguf`, `0.2-beta-fast-gguf`, `0.2-beta-lite-gguf`, `router-gguf`). -5. On the `https://ollama.com//flycoder` page, paste the text of [docs/ollama-model-page.md](docs/ollama-model-page.md). +| Secret | Value | +|---|---| +| `OLLAMA_KEY` | the private key `~/.ollama/id_ed25519` of a machine linked to the Tromset account (`ollama signin` on that machine first) | +| `OLLAMA_API_KEY` | an API key created at https://ollama.com/settings/keys, used to update the pages | -Published tags: `0.2-beta` and `latest` (main), `0.2-beta-fast` and `fast` (fast), `0.2-beta-lite` and `router` (FlyBrain experts), plus the GGUF tags with `--gguf`. Weights already on Ollama's registry are not uploaded again: for 0.2, whose bases come from the official library, publishing the six tags took only a few minutes. +Updating a page uses the same request as the page's Edit button; Ollama does not document it. If it is refused, the workflow shows a warning and the page can still be pasted by hand. -For a new version, update the version in `package.json`, `install.sh`, `scripts/publish.sh` and the system prompt of the Modelfiles. `npm test` checks that they agree. +By hand, from any machine signed in to the account: `sh scripts/publish.sh` (all models) or `sh scripts/publish.sh Tromset flycoder0.3fast` (one model), then `OLLAMA_API_KEY=... sh scripts/update-ollama-pages.sh`. + +For a new version, update the version in `package.json`, `install.sh`, `scripts/publish.sh`, the system prompt of the Modelfiles and the pages. `npm test` checks that they agree. ## 9. Troubleshooting | Symptom | Likely cause | Fix | |---|---|---| -| `this model requires MLX support` | machine without Apple Silicon, or Ollama too old | update Ollama; otherwise `sh install.sh --gguf` or the `-gguf` tags | -| `requires a newer version of Ollama` | Ollama older than 0.31 | update from https://ollama.com/download | +| `pull model manifest: file does not exist` | wrong name | the names are `Tromset/flycoder0.3`, `Tromset/flycoder0.3fast` and `Tromset/flycoder0.3pro` | +| `requires a newer version of Ollama` or `unknown model architecture` | Ollama too old | update from https://ollama.com/download (0.30 or later; 0.32.12 for `pro`) | | The installer says "Ollama is not responding" | the app is closed | open Ollama, or run `ollama serve` | -| Very slow answers, `ollama ps` shows a CPU share | not enough memory, the model spills out of the GPU | close applications, lower `num_ctx`, or switch to `flycoder:fast` | +| Very slow answers, `ollama ps` shows a CPU share | not enough memory, the model spills out of the GPU | close applications, lower `num_ctx`, or switch to `flycoder0.3fast` | | The model stops or crashes while loading | memory full | same; only one large model loaded at a time | | The model forgets the start of a long file | context too small | enlarge `num_ctx` (section 6.2) | | Thinking takes too long | simple question handled in thinking mode | `--think=false` or `/set nothink` | | Endless repetition | temperature changed to a value that is too low | go back to the Modelfile's settings | | `npm run bench`: "Reference solution failed" | `python3` or `node` missing, or sandbox refused | `xcode-select --install`, Node 22, or `--no-sandbox` | -| `publish.sh`: "is not the MLX build" | variants built as GGUF | rerun `sh install.sh --all` on an Apple Silicon Mac | +| The publish workflow fails at "Check the secrets" | `OLLAMA_KEY` is missing | add it (section 8) | ## 10. Development @@ -369,18 +368,18 @@ For a new version, update the version in `package.json`, `install.sh`, `scripts/ | Path | Role | |---|---| -| `Modelfile`, `Modelfile.fast` | definitions of the two variants | +| `Modelfile`, `Modelfile.fast`, `Modelfile.pro` | the three variants | | `Modelfile.lite`, `Modelfile.router` | FlyBrain's 2B expert and micro-model | | `brain/flybrain.mjs`, `brain/router.mjs` | FlyBrain server and routing rules | | `brain/eval-router.mjs`, `tests/fixtures/route-prompts.json` | router accuracy measurement | | `install.sh` | one-command installer | -| `scripts/publish.sh` | publishing on ollama.com | +| `scripts/publish.sh`, `scripts/update-ollama-pages.sh` | publishing models and pages on ollama.com | +| `docs/ollama/` | the ollama.com pages, one file per model | | `bench/bench.mjs`, `bench/problems.mjs` | benchmark and problems | | `bench/baselines/` | 0.1 profile, for comparison | | `docs/bench/` | reference results | -| `docs/ollama-model-page.md` | text of the ollama.com page | -| `tests/` | tests for the bench, the Modelfiles and the scripts | -| `.github/workflows/test.yml` | CI on Linux and macOS | +| `tests/` | tests for the bench, the Modelfiles, the scripts, the pages and FlyBrain | +| `.github/workflows/test.yml`, `.github/workflows/publish-ollama.yml` | CI on Linux and macOS, automatic publishing | | `training/`, `scripts/build_language_dataset.js`, `server/fly-language.js` | former FlyBrain voice pipeline, independent of FlyCoder; the voice speaks French by design | ### 10.2 Tests @@ -393,50 +392,40 @@ The tests check: - that each reference solution passes its hidden tests, and that an empty solution fails; - code extraction from answers and the statistics; -- reading streamed answers and the execution time limit; - that the Modelfiles start from the right bases, with the expected settings and an identical system prompt apart from the base name; -- that the installer and the publishing script agree with the Modelfiles and the version; -- FlyBrain's rules (the 20 bench problems go to the large model), the fallback to the large model when the micro-model fails, conversation memory and the scheduler; -- the FlyBrain server against a fake Ollama: model rewriting on the native, OpenAI and Anthropic APIs, streaming passed through, a single expert loaded at a time. +- that the installer, the publishing script and the workflow agree with the Modelfiles and the version; +- that each ollama.com page shows the right name, size, memory and context; +- that every Modelfile declares its minimum Ollama version, and that the code of the pro example passes its own test cases; +- FlyBrain's rules, the fallback to the large model when the micro-model fails, conversation memory, the scheduler, and the server against a fake Ollama. CI runs them on Linux and on macOS, where the reference solutions run inside the sandbox. ### 10.3 Changing the system prompt -Edit the `SYSTEM` block of the Modelfiles identically (only the sentence naming the base differs), run `npm test`, rebuild with `ollama create`, then compare before and after with `npm run bench`. +Edit the `SYSTEM` block of the Modelfiles identically (only the sentence naming the base differs), run `npm test`, rebuild with `ollama create`, then compare before and after with `npm run bench`. Once merged into `main`, the new prompt is published automatically. ### 10.4 Adding a problem to the bench -Add an object to `bench/problems.mjs` with: - -- `id` (`py-` or `js-` prefix); -- `language` (`python` or `javascript`); -- `entry` (expected function or class); -- `prompt` (the only text sent to the model); -- `reference` (correct solution); -- `test` (assertions). - -`npm test` rejects the problem if the reference fails or if an empty solution passes. +Add an object to `bench/problems.mjs` with `id` (`py-` or `js-` prefix), `language` (`python` or `javascript`), `entry` (expected function or class), `prompt` (the only text sent to the model), `reference` (correct solution) and `test` (assertions). `npm test` rejects the problem if the reference fails or if an empty solution passes. ## 11. Known limitations - The weights are not retrained: FlyCoder is a profile of existing models. -- The LiveCodeBench figures (72.0% for Gemma 4 12B, 55.8% for Qwen3.5 4B) are the ones published by Google and Qwen, not FlyCoder measurements. - The bench has 20 problems and one attempt per problem: it separates profiles without replacing a public benchmark. -- Speed on a Mac has not been measured for this version. The MLX (about +20%) and multi-token (about +90% on Apple Silicon) gains are those reported by Ollama; on CPU, the measured multi-token gain is 49%. -- Thinking mode has not been measured by the bench. -- The MLX tags only work on Apple Silicon. -- FlyBrain does not lower the memory peak of hard requests, and switching experts costs a few seconds. Its measurements (memory, accuracy on 60 requests) come from a Linux server running GGUF, not from a Mac running MLX. The rules were adjusted once after a first measurement on these same requests: the score on new requests may be a little lower. +- The reference measurements come from a Linux server without a GPU; speed on a Mac has not been measured for this version. +- `flycoder0.3pro` was not run by the bench: its 27B base needs more memory than the test server has. Its recipe was measured on the 9B base instead. +- The pro example conversation is sent with every conversation: about 600 tokens of the 64K context. +- FlyBrain does not lower the memory peak of hard requests, and switching experts costs a few seconds. ## 12. Version history | Version | Contents | |---|---| | 0.1 beta | Full workshop: Qwen3.5 4B, trainable FlyBrain controller, CLI, web and Electron interfaces, architect, coder and reviewer agents. | -| 0.2 beta | FlyCoder becomes an Ollama model only: Gemma 4 12B variant (MLX, multi-token) and a fixed Qwen3.5 4B variant, one-command install, ollama.com publishing, hidden-test benchmark, workshop removed. | -| 0.3 beta | Optional FlyBrain router: rules and a Qwen3.5 0.8B micro-model, a 2B expert for `flycoder:fast`, a single expert in memory. Less RAM for simple requests. The 0.2 models are unchanged. | +| 0.2 beta | FlyCoder becomes an Ollama model only: Gemma 4 12B variant (MLX, multi-token) and a fixed Qwen3.5 4B variant, one-command install, hidden-test benchmark. Then the optional FlyBrain router. | +| 0.3 | Three models under clear names (`Tromset/flycoder0.3`, `fast`, `pro`), all portable GGUF that run on any recent Ollama; Qwen3.5 9B replaces Gemma 4 12B, which did not run on every 16 GB Mac; Qwen3.8 27B for `pro`; 64K context; automatic publishing from GitHub; short ollama.com pages. | ## 13. Licenses - Repository code: MIT, see [license.md](license.md). -- Weights: Apache 2.0 for Gemma 4 and for Qwen3.5. Each base's terms are shown by `ollama show flycoder --license`. +- Weights: Apache 2.0 for Qwen3.5 and Qwen3.8. Each base's terms are shown by `ollama show flycoder0.3 --license`. diff --git a/Modelfile b/Modelfile index d410636..fcd5302 100644 --- a/Modelfile +++ b/Modelfile @@ -1,17 +1,21 @@ -# FlyCoder 0.2 beta: the default model for MacBook and Mac mini with 16 GB of memory or more. -# Base: Gemma 4 12B (Apache 2.0), NVFP4 weights on Ollama's MLX engine for Apple Silicon, -# with multi-token prediction enabled by Ollama. Build: ollama create flycoder:0.2-beta -f Modelfile -FROM gemma4:12b-mlx +# FlyCoder 0.3: the default model, for Macs with 16 GB of memory. +# Base: Qwen3.5 9B (Apache 2.0), GGUF Q4_K_M: runs on Apple Silicon, Intel Macs, Linux and Windows. +# Published as Tromset/flycoder0.3. Build: ollama create flycoder0.3 -f Modelfile +FROM qwen3.5:9b +REQUIRES 0.30.0 -# 32K tokens of context: about 0.5 GB of cache on top of the 7.7 GB of weights. -PARAMETER num_ctx 32768 +# 64K tokens of context: Qwen3.5's hybrid attention keeps the cache small, about 8.5 GB in total. +PARAMETER num_ctx 65536 -# Sampling recommended by Google DeepMind for every Gemma 4 use case, coding included. -PARAMETER temperature 1 -PARAMETER top_k 64 +# Qwen's recommended sampling for precise coding in thinking mode. +PARAMETER temperature 0.6 +PARAMETER top_k 20 PARAMETER top_p 0.95 +PARAMETER min_p 0 +PARAMETER presence_penalty 0 +PARAMETER repeat_penalty 1 -SYSTEM """You are FlyCoder 0.2 beta, a coding model running locally on the user's computer through Ollama. You are built on Gemma 4 12B by Google DeepMind, configured by the FlyCoder project. +SYSTEM """You are FlyCoder 0.3, a coding model running locally on the user's computer through Ollama. You are built on Qwen3.5 9B by the Qwen team at Alibaba, configured by the FlyCoder project. Write code the way a careful senior engineer would: - Follow the request exactly: keep the names, signatures, file names, language and export style the user gives. @@ -19,6 +23,7 @@ Write code the way a careful senior engineer would: - Handle edge cases on purpose: empty input, missing values, boundaries, invalid arguments (raise or return exactly as specified), Unicode. - Prefer the standard library and the existing style of the project. Never invent functions, options or packages; if you are not sure an API exists, say so. - Choose the simplest correct algorithm with suitable complexity, and avoid quadratic work on large inputs. +- Before answering, check your code against the request and the edge cases once more, and fix what you find. Give one final version, never an abandoned first attempt. - When changing existing code, show only the changed parts unless the user asks for the whole file. - Never claim that you ran code or tests. When useful, give a short command or test the user can run. - Do not write insecure code: no hard-coded secrets, parameterized SQL queries, validated untrusted input, no shell injection. diff --git a/Modelfile.fast b/Modelfile.fast index a5abb8e..90980c1 100644 --- a/Modelfile.fast +++ b/Modelfile.fast @@ -1,13 +1,13 @@ -# FlyCoder 0.2 beta fast: same speed class as FlyCoder 0.1, for 8 GB Macs or maximum speed. -# Base: Qwen3.5 4B (Apache 2.0), NVFP4 weights on Ollama's MLX engine for Apple Silicon. -# Build: ollama create flycoder:0.2-beta-fast -f Modelfile.fast -FROM qwen3.5:4b-mlx +# FlyCoder 0.3 fast: for 8 GB Macs, or when speed matters most. +# Base: Qwen3.5 4B (Apache 2.0), GGUF Q4_K_M. Published as Tromset/flycoder0.3fast. +# Build: ollama create flycoder0.3fast -f Modelfile.fast +FROM qwen3.5:4b +REQUIRES 0.30.0 -# Same context as FlyCoder 0.1, so memory use and speed stay comparable on 8 GB machines. -PARAMETER num_ctx 16384 +# 32K tokens of context keep an 8 GB Mac comfortable. +PARAMETER num_ctx 32768 -# Qwen's recommended sampling for precise coding (thinking mode). FlyCoder 0.1 used -# temperature 0.2, which Qwen warns can cause repetition and lower quality. +# Qwen's recommended sampling for precise coding in thinking mode. PARAMETER temperature 0.6 PARAMETER top_k 20 PARAMETER top_p 0.95 @@ -15,7 +15,7 @@ PARAMETER min_p 0 PARAMETER presence_penalty 0 PARAMETER repeat_penalty 1 -SYSTEM """You are FlyCoder 0.2 beta, a coding model running locally on the user's computer through Ollama. You are built on Qwen3.5 4B by the Qwen team at Alibaba, configured by the FlyCoder project. +SYSTEM """You are FlyCoder 0.3, a coding model running locally on the user's computer through Ollama. You are built on Qwen3.5 4B by the Qwen team at Alibaba, configured by the FlyCoder project. Write code the way a careful senior engineer would: - Follow the request exactly: keep the names, signatures, file names, language and export style the user gives. @@ -23,6 +23,7 @@ Write code the way a careful senior engineer would: - Handle edge cases on purpose: empty input, missing values, boundaries, invalid arguments (raise or return exactly as specified), Unicode. - Prefer the standard library and the existing style of the project. Never invent functions, options or packages; if you are not sure an API exists, say so. - Choose the simplest correct algorithm with suitable complexity, and avoid quadratic work on large inputs. +- Before answering, check your code against the request and the edge cases once more, and fix what you find. Give one final version, never an abandoned first attempt. - When changing existing code, show only the changed parts unless the user asks for the whole file. - Never claim that you ran code or tests. When useful, give a short command or test the user can run. - Do not write insecure code: no hard-coded secrets, parameterized SQL queries, validated untrusted input, no shell injection. diff --git a/Modelfile.lite b/Modelfile.lite index 52f0e87..87689fe 100644 --- a/Modelfile.lite +++ b/Modelfile.lite @@ -1,13 +1,13 @@ -# FlyCoder 0.2 beta lite: the small expert FlyBrain uses for simple requests in flycoder:fast. -# Base: Qwen3.5 2B (Apache 2.0), NVFP4 weights on Ollama's MLX engine for Apple Silicon. -# Build: ollama create flycoder:0.2-beta-lite -f Modelfile.lite -FROM qwen3.5:2b-nvfp4 +# FlyCoder 0.3 lite: the small expert FlyBrain uses for simple requests to flycoder0.3fast. +# Base: Qwen3.5 2B (Apache 2.0), GGUF Q4_K_M. Published as Tromset/flycoder0.3:lite. +# Build: ollama create flycoder0.3:lite -f Modelfile.lite +FROM qwen3.5:2b +REQUIRES 0.30.0 -# Simple requests are short: 8K tokens keeps the cache small next to 2.5 GB of weights. +# Simple requests are short: 8K tokens keeps the cache small. PARAMETER num_ctx 8192 -# Qwen's recommended sampling for precise coding (thinking mode). FlyCoder 0.1 used -# temperature 0.2, which Qwen warns can cause repetition and lower quality. +# Qwen's recommended sampling for precise coding in thinking mode. PARAMETER temperature 0.6 PARAMETER top_k 20 PARAMETER top_p 0.95 @@ -15,7 +15,7 @@ PARAMETER min_p 0 PARAMETER presence_penalty 0 PARAMETER repeat_penalty 1 -SYSTEM """You are FlyCoder 0.2 beta, a coding model running locally on the user's computer through Ollama. You are built on Qwen3.5 2B by the Qwen team at Alibaba, configured by the FlyCoder project. +SYSTEM """You are FlyCoder 0.3, a coding model running locally on the user's computer through Ollama. You are built on Qwen3.5 2B by the Qwen team at Alibaba, configured by the FlyCoder project. Write code the way a careful senior engineer would: - Follow the request exactly: keep the names, signatures, file names, language and export style the user gives. @@ -23,6 +23,7 @@ Write code the way a careful senior engineer would: - Handle edge cases on purpose: empty input, missing values, boundaries, invalid arguments (raise or return exactly as specified), Unicode. - Prefer the standard library and the existing style of the project. Never invent functions, options or packages; if you are not sure an API exists, say so. - Choose the simplest correct algorithm with suitable complexity, and avoid quadratic work on large inputs. +- Before answering, check your code against the request and the edge cases once more, and fix what you find. Give one final version, never an abandoned first attempt. - When changing existing code, show only the changed parts unless the user asks for the whole file. - Never claim that you ran code or tests. When useful, give a short command or test the user can run. - Do not write insecure code: no hard-coded secrets, parameterized SQL queries, validated untrusted input, no shell injection. diff --git a/Modelfile.pro b/Modelfile.pro new file mode 100644 index 0000000..987a3be --- /dev/null +++ b/Modelfile.pro @@ -0,0 +1,87 @@ +# FlyCoder 0.3 pro: the strongest variant, for hard coding work and coding agents, on Macs with 32 GB or more. +# Base: Qwen3.8 27B (Apache 2.0), GGUF Q4_K_M, with its built-in multi-token prediction layer. +# Published as Tromset/flycoder0.3pro. Build: ollama create flycoder0.3pro -f Modelfile.pro +FROM qwen3.8:27b +REQUIRES 0.32.12 + +# 64K tokens: only 16 of the 64 layers keep a growing cache (about 68 KB per token), +# so the cache takes about 4.4 GB on top of 17.7 GB of weights. +PARAMETER num_ctx 65536 + +# Qwen's recommended sampling for precise coding in thinking mode. +PARAMETER temperature 0.6 +PARAMETER top_k 20 +PARAMETER top_p 0.95 +PARAMETER min_p 0 +PARAMETER presence_penalty 0 +PARAMETER repeat_penalty 1 +# Multi-token prediction: the built-in draft layer proposes 4 tokens that the model checks in one pass. +PARAMETER draft_num_predict 4 + +SYSTEM """You are FlyCoder 0.3 pro, the strongest FlyCoder, a coding model running locally on the user's computer through Ollama. You are built on Qwen3.8 27B by the Qwen team at Alibaba, configured by the FlyCoder project for hard coding work and coding agents. + +Work in this order: +1. Contract: pin down the exact names, signatures, inputs, outputs and errors. If an ambiguity would change the code, ask one short question; otherwise state your assumption in one line and go on. +2. Plan: for anything larger than one function, give a plan of at most five lines: the files you touch and the approach. +3. Code: complete, runnable code with every import. No placeholders, no "TODO", no "rest of the code here". Keep the user's names, language, export style and the project's conventions. Prefer the standard library; never invent functions, options or packages. +4. Tests: write the tests that prove the code, with the project's test framework (otherwise pytest or node:test), covering the edge cases you handled: empty input, boundaries, invalid arguments, Unicode, large inputs. +5. Check: before answering, trace the tricky cases through your code by hand and fix what you find. Give one final version, never an abandoned first attempt. +6. Report: one line on how to run the tests, and any risk or limit the user must know. + +Choose the simplest correct algorithm and avoid quadratic work on large inputs. Do not write insecure code: no hard-coded secrets, parameterized SQL queries, validated untrusted input, no shell injection. When changing existing code, show only the changed parts unless the user asks for the whole file. + +As an agent with tools: read the relevant files before editing them, change as little code as the task needs, run the project's tests when you can and report their real output. Never claim a result you did not see. Never run a destructive command (deleting files, force-pushing, dropping data) without the user's explicit agreement. + +Format: each file goes in one fenced code block with a language tag, preceded by its path. Keep explanations short and write them in the user's language.""" + +# One worked exchange that shows the expected shape of an answer: contract, code, tests, how to run them. +MESSAGE user """Write a Python function parse_duration(text) that turns durations like "1h30m" or "45s" into seconds. Invalid input must raise ValueError.""" +MESSAGE assistant """Assumption: units are h, m and s, each used at most once and in that order, with no spaces between parts ("1h 30m" is rejected); a non-string raises TypeError. + +`duration.py` +```python +import re + +_SECONDS = {"h": 3600, "m": 60, "s": 1} +_PART = re.compile(r"(\d+)([hms])") + + +def parse_duration(text: str) -> int: + '''Return the number of seconds in a duration such as "1h30m" or "45s".''' + if not isinstance(text, str): + raise TypeError("text must be a string") + value = text.strip().lower() + parts = _PART.findall(value) + units = [unit for _, unit in parts] + # Every character must belong to a part, and units must appear once, in h-m-s order. + if not parts or _PART.sub("", value) or units != sorted(set(units), key="hms".index): + raise ValueError(f"invalid duration: {text!r}") + return sum(int(number) * _SECONDS[unit] for number, unit in parts) +``` + +`test_duration.py` +```python +import pytest + +from duration import parse_duration + + +@pytest.mark.parametrize("text, seconds", [ + ("45s", 45), ("1h30m", 5400), ("2h", 7200), (" 10M ", 600), ("1h0m5s", 3605), ("0s", 0), +]) +def test_valid(text, seconds): + assert parse_duration(text) == seconds + + +@pytest.mark.parametrize("text", ["", " ", "abc", "10", "1h1h", "30m1h", "1.5h", "-5s", "1h 30m"]) +def test_invalid(text): + with pytest.raises(ValueError): + parse_duration(text) + + +def test_not_a_string(): + with pytest.raises(TypeError): + parse_duration(90) +``` + +Run: `python -m pytest test_duration.py`. Limit: no days or fractional values; add them to `_SECONDS` and the pattern if you need them.""" diff --git a/Modelfile.router b/Modelfile.router index 7dd66a2..f0a0e45 100644 --- a/Modelfile.router +++ b/Modelfile.router @@ -1,7 +1,9 @@ # FlyCoder router: the micro-model FlyBrain asks when a request has no clear cue. # It only answers {"level":"simple"} or {"level":"hard"}; FlyBrain forces that JSON format. -# Base: Qwen3.5 0.8B (Apache 2.0), NVFP4 on MLX. Build: ollama create flycoder:router -f Modelfile.router -FROM qwen3.5:0.8b-nvfp4 +# Base: Qwen3.5 0.8B (Apache 2.0), GGUF. Published as Tromset/flycoder0.3:router. +# Build: ollama create flycoder0.3:router -f Modelfile.router +FROM qwen3.5:0.8b +REQUIRES 0.30.0 # The router reads at most 2,000 characters of the request. PARAMETER num_ctx 2048 diff --git a/bench/bench.mjs b/bench/bench.mjs index 4eb70a8..93a8c13 100644 --- a/bench/bench.mjs +++ b/bench/bench.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node // FlyCoder bench: pass rate and speed of Ollama models on hidden-test coding problems. -// Usage: node bench/bench.mjs --models flycoder:0.2-beta,flycoder:0.2-beta-fast [--think off] +// Usage: node bench/bench.mjs --models flycoder0.3,flycoder0.3fast [--think off] import { spawn } from 'node:child_process'; import fs from 'node:fs'; import os from 'node:os'; @@ -132,7 +132,7 @@ export function table(summaries) { async function main() { const { values } = parseArgs({ options: { - models: { type: 'string', default: 'flycoder:0.2-beta,flycoder:0.2-beta-fast' }, host: { type: 'string', default: process.env.OLLAMA_HOST ? `http://${process.env.OLLAMA_HOST.replace(/^https?:\/\//, '')}` : 'http://127.0.0.1:11434' }, + models: { type: 'string', default: 'flycoder0.3,flycoder0.3fast' }, host: { type: 'string', default: process.env.OLLAMA_HOST ? `http://${process.env.OLLAMA_HOST.replace(/^https?:\/\//, '')}` : 'http://127.0.0.1:11434' }, think: { type: 'string', default: 'default' }, samples: { type: 'string', default: '1' }, only: { type: 'string' }, 'max-tokens': { type: 'string', default: '8192' }, 'num-ctx': { type: 'string' }, out: { type: 'string' }, 'no-sandbox': { type: 'boolean' }, help: { type: 'boolean', short: 'h' } } }); if (values.help) return console.log('node bench/bench.mjs --models a,b [--think on|off|default] [--samples n] [--only id,id] [--max-tokens n] [--num-ctx n] [--out file.json] [--no-sandbox]'); diff --git a/brain/eval-router.mjs b/brain/eval-router.mjs index 4c33daa..71f7611 100644 --- a/brain/eval-router.mjs +++ b/brain/eval-router.mjs @@ -1,13 +1,13 @@ #!/usr/bin/env node // Measures how well FlyBrain routes labelled prompts: rules alone, then rules + micro-model. -// Usage: node brain/eval-router.mjs [--ollama http://127.0.0.1:11434] [--router flycoder:router] +// Usage: node brain/eval-router.mjs [--ollama http://127.0.0.1:11434] [--router flycoder0.3:router] import fs from 'node:fs'; import { parseArgs } from 'node:util'; import { digest, ruleRoute } from './router.mjs'; import { PROBLEMS } from '../bench/problems.mjs'; import { instruction } from '../bench/bench.mjs'; -const { values } = parseArgs({ options: { ollama: { type: 'string', default: 'http://127.0.0.1:11434' }, router: { type: 'string', default: 'flycoder:router' } } }); +const { values } = parseArgs({ options: { ollama: { type: 'string', default: 'http://127.0.0.1:11434' }, router: { type: 'string', default: 'flycoder0.3:router' } } }); const labelled = JSON.parse(fs.readFileSync(new URL('../tests/fixtures/route-prompts.json', import.meta.url), 'utf8')); const cases = [...labelled.simple.map(text => ({ text, want: 'simple' })), ...labelled.hard.map(text => ({ text, want: 'hard' })), ...PROBLEMS.map(p => ({ text: instruction(p), want: 'hard' }))]; diff --git a/brain/flybrain.mjs b/brain/flybrain.mjs index c8d376d..611f7a1 100644 --- a/brain/flybrain.mjs +++ b/brain/flybrain.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -// FlyBrain: an Ollama-compatible server that answers to `flycoder` and `flycoder:fast`, +// FlyBrain: an Ollama-compatible server that answers to `flycoder0.3` and `flycoder0.3fast`, // routes each request to one expert model and keeps a single expert in memory. // Usage: node brain/flybrain.mjs [--port 11435] [--ollama http://127.0.0.1:11434] [--prefix Tromset/] [--max-expert auto|full|fast] import http from 'node:http'; @@ -87,7 +87,7 @@ export function createFlyBrain({ ollama = 'http://127.0.0.1:11434', prefix = '', })); } -// Below 16 GB the large expert does not fit next to the system: cap at the 4B. +// Below 16 GB the 9B does not fit next to the system and the 4B: cap at the 4B. export function defaultMaxExpert(totalBytes = os.totalmem()) { return totalBytes / 2 ** 30 >= 15 ? 'full' : 'fast'; } function main() { @@ -98,7 +98,7 @@ function main() { const maxExpert = values['max-expert'] === 'auto' ? defaultMaxExpert() : values['max-expert']; if (!['full', 'fast'].includes(maxExpert)) throw new Error('--max-expert must be auto, full or fast'); const server = createFlyBrain({ ollama: values.ollama, prefix: values.prefix, maxExpert }); - server.listen(Number(values.port), values.host, () => console.error(`FlyBrain listening on http://${values.host}:${values.port} (Ollama: ${values.ollama}, max expert: ${maxExpert}). Models: flycoder, flycoder:fast`)); + server.listen(Number(values.port), values.host, () => console.error(`FlyBrain listening on http://${values.host}:${values.port} (Ollama: ${values.ollama}, max expert: ${maxExpert}). Models: flycoder0.3, flycoder0.3fast`)); } if (process.argv[1] && import.meta.url === pathToFileURL(fs.realpathSync(process.argv[1])).href) { diff --git a/brain/router.mjs b/brain/router.mjs index 3f9f784..2144020 100644 --- a/brain/router.mjs +++ b/brain/router.mjs @@ -37,13 +37,17 @@ export function ruleRoute(d) { } // Experts behind each name FlyBrain answers to. `prefix` lets the published -// models be used directly, e.g. prefix 'Tromset/' for Tromset/flycoder:0.2-beta. +// models be used directly, e.g. prefix 'Tromset/' for Tromset/flycoder0.3. +// flycoder0.3pro is not routed: FlyBrain passes it through like any other model. export function profiles(prefix = '', { maxExpert = 'full' } = {}) { - const tag = t => `${prefix}flycoder:${t}`; - const normal = { simple: tag('0.2-beta-fast'), hard: tag(maxExpert === 'fast' ? '0.2-beta-fast' : '0.2-beta'), router: tag('router'), think: true }; + const name = n => `${prefix}${n}`; + const normal = { simple: name('flycoder0.3fast'), hard: name(maxExpert === 'fast' ? 'flycoder0.3fast' : 'flycoder0.3'), router: name('flycoder0.3:router'), think: true }; // The fast profile has no micro-model: an unclear request keeps the 4B, so it never answers worse than before. - const fast = { simple: tag('0.2-beta-lite'), hard: tag('0.2-beta-fast'), router: null, think: false }; - return { flycoder: normal, 'flycoder:latest': normal, 'flycoder:fast': fast }; + const fast = { simple: name('flycoder0.3:lite'), hard: name('flycoder0.3fast'), router: null, think: false }; + const table = {}; + for (const n of ['flycoder', 'flycoder0.3']) table[n] = table[`${n}:latest`] = table[name(n)] = table[`${name(n)}:latest`] = normal; + for (const n of ['flycoder0.3fast']) table[n] = table[`${n}:latest`] = table[name(n)] = table[`${name(n)}:latest`] = fast; + return table; } export class Memory { diff --git a/docs/bench/cpu-nothink-0.3.json b/docs/bench/cpu-nothink-0.3.json new file mode 100644 index 0000000..0d1fbce --- /dev/null +++ b/docs/bench/cpu-nothink-0.3.json @@ -0,0 +1,72 @@ +{ + "at": "2026-10-08T18:52:53.880Z", + "host": "http://127.0.0.1:11434", + "think": "off", + "samples": 1, + "maxTokens": 2048, + "numCtx": "model default", + "platform": "linux x64 Intel(R) Xeon(R) Processor @ 2.80GHz", + "memoryGiB": 16, + "models": [ + { + "summary": { + "model": "flycoder0.3", + "passed": 1, + "total": 3, + "passRate": 0.3333333333333333, + "decodeTokensPerSecond": 3.7353407093832978, + "prefillTokensPerSecond": 21.755343865522967, + "meanOutputTokens": 588, + "meanSecondsPerProblem": 180.223, + "truncated": 0 + }, + "results": [ + { + "problem": "py-merge-intervals", + "sample": 0, + "ok": true, + "error": null, + "truncated": false, + "wallMs": 157293, + "loadMs": 31763, + "outputTokens": 392, + "decodeMs": 103828.51, + "promptTokens": 468, + "prefillMs": 21682.139, + "thinkingChars": 0, + "answer": "```python\nfrom typing import List, Tuple\n\ndef merge_intervals(intervals: List[Tuple[int, int]]) -> List[Tuple[int, int]]:\n \"\"\"\n Merges a list of closed intervals.\n \n - Intervals are (start, end) tuples of integers.\n - Overlapping or touching intervals (where one ends exactly where the next starts) are merged.\n - Returns a new list of merged intervals sorted by start time.\n - Raises ValueError if any interval has start > end.\n - Does not mutate the input.\n \"\"\"\n # Validate input and check for invalid intervals\n for start, end in intervals:\n if start > end:\n raise ValueError(f\"Invalid interval: start ({start}) must not be greater than end ({end})\")\n \n # Handle empty input\n if not intervals:\n return []\n \n # Sort intervals by start time (and end time as secondary key for stability)\n sorted_intervals = sorted(intervals, key=lambda x: (x[0], x[1]))\n \n # Initialize the merged list with the first interval\n merged: List[Tuple[int, int]] = [sorted_intervals[0]]\n \n # Iterate through the sorted intervals and merge as needed\n for current_start, current_end in sorted_intervals[1:]:\n last_start, last_end = merged[-1]\n \n # Check if the current interval overlaps or touches the last merged interval\n if current_start <= last_end:\n # Merge by updating the end of the last interval\n merged[-1] = (last_start, max(last_end, current_end))\n else:\n # No overlap, add the current interval to the merged list\n merged.append((current_start, current_end))\n \n return merged\n```" + }, + { + "problem": "py-roman-strict", + "sample": 0, + "ok": false, + "error": "Traceback (most recent call last):\n File \"/tmp/flycoder-bench-nRLqs2/test_solution.py\", line 7, in \n raise AssertionError(\"accepted \" + repr(bad))\nAssertionError: accepted 'IIII'\n", + "truncated": false, + "wallMs": 226807, + "loadMs": 10, + "outputTokens": 763, + "decodeMs": 204187.883, + "promptTokens": 477, + "prefillMs": 22455.957, + "thinkingChars": 0, + "answer": "```python\ndef roman_to_int(s: str) -> int:\n \"\"\"\n Convert a Roman numeral string to an integer.\n\n Valid input: canonical Roman numerals for 1 to 3999.\n Raises ValueError for:\n - empty string\n - lowercase letters\n - unknown characters\n - non-canonical forms (e.g., II IV, IC, XM, MMMM)\n\n Mapping:\n I = 1\n V = 5\n X = 10\n L = 50\n C = 100\n D = 500\n M = 1000\n Subtractive pairs: IV, IX, XL, XC, CD, CM\n \"\"\"\n # Validate non-empty\n if not s:\n raise ValueError(\"Input string must not be empty.\")\n\n # Validate characters: must be uppercase and in {I, V, X, L, C, D, M}\n valid_chars = set(\"IVXLCDM\")\n for ch in s:\n if ch not in valid_chars:\n raise ValueError(f\"Invalid Roman numeral character: {ch!r}\")\n\n # Quick lowercase check (redundant with the set check, but explicit per spec)\n if s != s.upper():\n raise ValueError(\"Input must be uppercase.\")\n\n # Value map for each symbol\n values = {\n \"I\": 1,\n \"V\": 5,\n \"X\": 10,\n \"L\": 50,\n \"C\": 100,\n \"D\": 500,\n \"M\": 1000,\n }\n\n # Maximum allowed value for 3999\n max_value = 3999\n\n # Subtractive pairs that are allowed in canonical form\n allowed_subtractive = {\"IV\", \"IX\", \"XL\", \"XC\", \"CD\", \"CM\"}\n\n # We will parse left-to-right, looking ahead to detect subtractive pairs.\n # If a subtractive pair is NOT present where it should be, or if the\n # resulting number would be non-canonical, we raise.\n\n i = 0\n n = len(s)\n total = 0\n\n while i < n:\n cur_val = values[s[i]]\n if i + 1 < n:\n next_val = values[s[i + 1]]\n else:\n next_val = 0\n\n # Case 1: Subtractive pair (current < next)\n if cur_val < next_val:\n # Check if this two-character sequence is an allowed subtractive pair\n pair = s[i : i + 2]\n if pair not in allowed_subtractive:\n raise ValueError(f\"Non-canonical subtractive form: {pair}\")\n total += next_val - cur_val\n i += 2\n continue\n\n # Case 2: Normal addition (current >= next or last character)\n total += cur_val\n i += 1\n\n # Ensure the result is within the canonical range 1..3999\n if total == 0:\n raise ValueError(\"Invalid Roman numeral: empty or invalid combination.\")\n if total > max_value:\n raise ValueError(f\"Value {total} exceeds maximum canonical Roman numeral (3999).\")\n\n return total\n```" + }, + { + "problem": "py-parse-duration", + "sample": 0, + "ok": false, + "error": "Traceback (most recent call last):\n File \"/tmp/flycoder-bench-Yln79e/test_solution.py\", line 3, in \n assert parse_duration(\"1h30m\") == 5400\n ~~~~~~~~~~~~~~^^^^^^^^^\n File \"/tmp/flycoder-bench-Yln79e/solution.py\", line 35, in parse_duration\n raise ValueError(f\"Invalid part: '{part}'\")\nValueError: Invalid part: '1h30m'\n", + "truncated": false, + "wallMs": 188347, + "loadMs": 5, + "outputTokens": 609, + "decodeMs": 164229.685, + "promptTokens": 537, + "prefillMs": 23983.098, + "thinkingChars": 0, + "answer": "```python\nimport re\n\ndef parse_duration(text: str) -> int:\n \"\"\"\n Parse a duration string into total seconds.\n \n Supports units: d (days), h (hours), m (minutes), s (seconds).\n Units must appear in order (d, h, m, s) and at most once each.\n Spaces are allowed between parts or around the string.\n \n Raises ValueError for invalid input.\n \"\"\"\n # Strip leading/trailing whitespace\n text = text.strip()\n \n if not text:\n raise ValueError(\"Empty string is invalid\")\n \n # Split by spaces to handle \"1h 15m 10s\"\n parts = text.split()\n \n if not parts:\n raise ValueError(\"Invalid input after stripping\")\n \n total_seconds = 0\n seen_units = set()\n expected_order = ['d', 'h', 'm', 's']\n \n # Regex to match number followed by a unit\n pattern = re.compile(r'^(\\d+)([dhms])$')\n \n for part in parts:\n match = pattern.match(part)\n if not match:\n raise ValueError(f\"Invalid part: '{part}'\")\n \n num = int(match.group(1))\n unit = match.group(2)\n \n if num < 0:\n raise ValueError(f\"Negative number not allowed in part: {part}\")\n \n # Check unit validity\n if unit not in expected_order:\n raise ValueError(f\"Invalid unit: '{unit}'\")\n \n if unit in seen_units:\n raise ValueError(f\"Duplicate unit: '{unit}'\")\n \n seen_units.add(unit)\n \n # Validate order: if a unit is seen, next valid unit must follow in order\n # We track the last seen unit index to ensure order\n unit_index = expected_order.index(unit)\n for prev_unit in seen_units:\n if expected_order.index(prev_unit) > unit_index:\n raise ValueError(f\"Units out of order: '{unit}' appears after '{prev_unit}'\")\n \n # Add to total\n unit_to_seconds = {'d': 86400, 'h': 3600, 'm': 60, 's': 1}\n total_seconds += num * unit_to_seconds[unit]\n \n # Final validation: ensure all expected units were not required but some were missing?\n # The problem says \"one or more ... parts\", so missing units are OK as long as order is respected.\n # However, we must ensure no extra characters remain.\n \n if len(seen_units) == 0:\n raise ValueError(\"No valid unit found in input\")\n \n return total_seconds\n```" + } + ] + } + ] +} diff --git a/docs/ollama-model-page.md b/docs/ollama-model-page.md deleted file mode 100644 index 8266c58..0000000 --- a/docs/ollama-model-page.md +++ /dev/null @@ -1,41 +0,0 @@ -# FlyCoder 0.2 beta - -A local coding model tuned for MacBook and Mac mini (Apple Silicon), packaged for Ollama. - -```sh -ollama run Tromset/flycoder # Gemma 4 12B base, Macs with 16 GB or more -ollama run Tromset/flycoder:fast # Qwen3.5 4B base, Macs with 8 GB -``` - -## Tags - -| Tag | Base | Size | Context | -|---|---|---|---| -| `latest`, `0.2-beta` | Gemma 4 12B (Google DeepMind), NVFP4 on Ollama's MLX engine | 7.7 GB | 32,768 | -| `fast`, `0.2-beta-fast` | Qwen3.5 4B (Alibaba Qwen), NVFP4 on Ollama's MLX engine | 4.0 GB | 16,384 | -| `0.2-beta-lite` | Qwen3.5 2B, NVFP4 on MLX: FlyBrain's expert for simple requests in `flycoder:fast` | 2.5 GB | 8,192 | -| `router` | Qwen3.5 0.8B, NVFP4 on MLX: FlyBrain's micro-router, answers only `simple` or `hard` | 1.0 GB | 2,048 | -| `*-gguf` | Same bases as GGUF Q4_K_M, for Intel Macs, Linux and Windows | 8.0 / 3.4 / 2.7 / 1.0 GB | same | - -MLX tags need Ollama 0.31 or later on Apple Silicon. - -## What FlyCoder sets up - -- A stronger base for the same hardware: Gemma 4 12B scores 72.0% on LiveCodeBench v6 (Google's published figure, thinking mode), against 55.8% for Qwen3.5 4B (Qwen's published figure). -- Multi-token prediction on Gemma 4: a small draft model proposes several tokens that the main model verifies in one pass, which Ollama reports as nearly 90% faster generation on Apple Silicon. -- The sampling settings recommended by each base model's authors for coding. -- A real context window: Ollama otherwise defaults to 4,096 tokens on machines with less than 24 GB. -- A short system prompt focused on complete, correct code: exact names and signatures, edge cases, no invented APIs, no claims of having run tests, secure defaults, answers in the user's language. - -## Less memory with FlyBrain (optional) - -FlyBrain is a small Node server from the GitHub repository that sits in front of Ollama. It answers to `flycoder` and `flycoder:fast`, sends simple questions to a smaller expert, sends hard requests (and every agent request with tools) to the strong one, and keeps a single expert in memory. Measured on a 16 GB Linux server (GGUF, CPU): a simple request to `flycoder` uses 6.1 GiB instead of 10.5. Hard requests still use the strong model, so their quality is unchanged. - -```sh -node brain/flybrain.mjs --prefix Tromset/ -OLLAMA_HOST=127.0.0.1:11435 ollama run flycoder -``` - -FlyCoder does not retrain weights. Weights keep their base license (Apache 2.0 for both bases). - -Source, installer and benchmark: https://github.com/Tromset/flycoder diff --git a/docs/ollama/flycoder.md b/docs/ollama/flycoder.md new file mode 100644 index 0000000..a51ed93 --- /dev/null +++ b/docs/ollama/flycoder.md @@ -0,0 +1,11 @@ +# FlyCoder has moved + +This page holds the old 0.2 beta builds. FlyCoder 0.3 lives here: + +```sh +ollama run Tromset/flycoder0.3 # Macs with 16 GB +ollama run Tromset/flycoder0.3fast # Macs with 8 GB +ollama run Tromset/flycoder0.3pro # Macs with 32 GB or more +``` + +Source: https://github.com/Tromset/flycoder diff --git a/docs/ollama/flycoder0.3.md b/docs/ollama/flycoder0.3.md new file mode 100644 index 0000000..5596d20 --- /dev/null +++ b/docs/ollama/flycoder0.3.md @@ -0,0 +1,20 @@ +# FlyCoder 0.3 + +A local coding model for Macs with 16 GB of memory. Complete, runnable code, a large context, and it answers in your language. + +```sh +ollama run Tromset/flycoder0.3 +``` + +| Base | Size | Mac | Context | +|---|---|---|---| +| Qwen3.5 9B (Apache 2.0) | 6.6 GB | 16 GB | 64K tokens | + +- **Code that runs**: exact names and signatures, every import, edge cases handled, no "TODO", no invented APIs. +- **Large context**: 64K tokens out of the box, enough for whole files and coding agents (`ollama launch claude --model Tromset/flycoder0.3`). +- **Thinks first**: reasoning is on by default; `/set nothink` for instant answers. + +Other sizes: [`Tromset/flycoder0.3fast`](https://ollama.com/Tromset/flycoder0.3fast) for 8 GB Macs, [`Tromset/flycoder0.3pro`](https://ollama.com/Tromset/flycoder0.3pro) for 32 GB Macs or more. + +Needs Ollama 0.30 or later. Runs on Apple Silicon, Intel Macs, Linux and Windows. +Source, benchmark and installer: https://github.com/Tromset/flycoder diff --git a/docs/ollama/flycoder0.3fast.md b/docs/ollama/flycoder0.3fast.md new file mode 100644 index 0000000..99e88c9 --- /dev/null +++ b/docs/ollama/flycoder0.3fast.md @@ -0,0 +1,20 @@ +# FlyCoder 0.3 fast + +The fast FlyCoder, for Macs with 8 GB of memory or when speed matters most. + +```sh +ollama run Tromset/flycoder0.3fast +``` + +| Base | Size | Mac | Context | +|---|---|---|---| +| Qwen3.5 4B (Apache 2.0) | 3.3 GB | 8 GB | 32K tokens | + +- **Fast**: about twice the speed of `Tromset/flycoder0.3`, with the same coding rules. +- **Code that runs**: exact names and signatures, every import, edge cases handled, no "TODO", no invented APIs. +- **Light**: fits next to your other apps on an 8 GB Mac. + +Other sizes: [`Tromset/flycoder0.3`](https://ollama.com/Tromset/flycoder0.3) for 16 GB Macs, [`Tromset/flycoder0.3pro`](https://ollama.com/Tromset/flycoder0.3pro) for 32 GB Macs or more. + +Needs Ollama 0.30 or later. Runs on Apple Silicon, Intel Macs, Linux and Windows. +Source, benchmark and installer: https://github.com/Tromset/flycoder diff --git a/docs/ollama/flycoder0.3pro.md b/docs/ollama/flycoder0.3pro.md new file mode 100644 index 0000000..2456dea --- /dev/null +++ b/docs/ollama/flycoder0.3pro.md @@ -0,0 +1,20 @@ +# FlyCoder 0.3 pro + +The strongest FlyCoder, for hard coding work and coding agents, on Macs with 32 GB of memory or more. + +```sh +ollama run Tromset/flycoder0.3pro +``` + +| Base | Size | Mac | Context | +|---|---|---|---| +| Qwen3.8 27B (Apache 2.0) | 18 GB | 32 GB | 64K tokens | + +- **Works like a senior engineer**: pins down the contract, plans multi-file changes, writes the code and the tests that prove it, checks the tricky cases before answering, then tells you how to run the tests. +- **Built for agents**: reads files before editing, makes minimal changes, reports real test output, never runs a destructive command without asking (`ollama launch claude --model Tromset/flycoder0.3pro`). +- **Fast for its size**: multi-token prediction is on, and 64K tokens of context cost only about 4.4 GB thanks to Qwen3.8's hybrid attention. + +Other sizes: [`Tromset/flycoder0.3`](https://ollama.com/Tromset/flycoder0.3) for 16 GB Macs, [`Tromset/flycoder0.3fast`](https://ollama.com/Tromset/flycoder0.3fast) for 8 GB Macs. + +Needs Ollama 0.32.12 or later. Runs on Apple Silicon, Intel Macs, Linux and Windows. +Source, benchmark and installer: https://github.com/Tromset/flycoder diff --git a/install.sh b/install.sh index 24a73bb..62c0b9e 100755 --- a/install.sh +++ b/install.sh @@ -1,15 +1,16 @@ #!/bin/sh -# FlyCoder 0.2 beta installer: builds the FlyCoder model inside your local Ollama. +# FlyCoder 0.3 installer: builds FlyCoder inside your local Ollama, picked from your memory. # curl -fsSL https://raw.githubusercontent.com/Tromset/flycoder/main/install.sh | sh -# sh install.sh [--fast | --all] [--gguf] [--no-brain] -# --fast install only flycoder:0.2-beta-fast (Qwen3.5 4B, 8 GB Macs, maximum speed) -# --all install both variants -# --gguf use the portable GGUF weights instead of MLX (Intel Macs, Linux, Windows) -# --no-brain skip the FlyBrain experts (flycoder:router, flycoder:0.2-beta-lite) +# sh install.sh [--fast | --pro | --all] [--mlx] [--no-brain] +# --fast install flycoder0.3fast (Qwen3.5 4B, 8 GB Macs, maximum speed) +# --pro install flycoder0.3pro (Qwen3.8 27B, 32 GB Macs or more) +# --all install all three variants +# --mlx use Ollama's MLX engine on Apple Silicon (faster, needs Ollama 0.31+); default: portable GGUF +# --no-brain skip the FlyBrain experts (flycoder0.3:router, flycoder0.3:lite) +# Without installing anything, the published models also work: ollama run Tromset/flycoder0.3 set -eu -VERSION=0.2-beta -MIN_OLLAMA=0.31.0 +VERSION=0.3 REF=${FLYCODER_REF:-main} RAW=https://raw.githubusercontent.com/Tromset/flycoder/$REF @@ -17,15 +18,16 @@ say() { printf '%s\n' "$@"; } die() { printf 'FlyCoder: %s\n' "$*" >&2; exit 1; } choice=auto -engine=auto +engine=gguf brain=yes for arg in "$@"; do case $arg in --fast) choice=fast ;; + --pro) choice=pro ;; --all) choice=all ;; - --gguf) engine=gguf ;; + --mlx) engine=mlx ;; --no-brain) brain=no ;; - -h|--help) sed -n '2,9p' "$0" 2>/dev/null || true; exit 0 ;; + -h|--help) sed -n '2,10p' "$0" 2>/dev/null || true; exit 0 ;; *) die "unknown option: $arg" ;; esac done @@ -39,13 +41,15 @@ version_ge() { } current=$(ollama --version 2>/dev/null | sed -n 's/.*version is \([0-9][0-9.]*\).*/\1/p' | head -n 1) [ -n "$current" ] || die "could not read the Ollama version." -version_ge "$current" "$MIN_OLLAMA" || die "Ollama $current is too old. FlyCoder $VERSION needs Ollama $MIN_OLLAMA or later: https://ollama.com/download" +need() { # need + version_ge "$current" "$1" || die "Ollama $current is too old: $2 needs Ollama $1 or later. Update it from https://ollama.com/download, then run this command again." +} +need 0.30.0 "FlyCoder $VERSION (Qwen3.5)" +[ "$engine" = mlx ] && need 0.31.0 "the MLX engine" os=$(uname -s) arch=$(uname -m) -if [ "$engine" = auto ]; then - if [ "$os" = Darwin ] && [ "$arch" = arm64 ]; then engine=mlx; else engine=gguf; fi -fi +[ "$engine" = gguf ] || { [ "$os" = Darwin ] && [ "$arch" = arm64 ]; } || die "--mlx needs an Apple Silicon Mac." case $os in Darwin) memory_gb=$(( $(sysctl -n hw.memsize) / 1073741824 )) ;; @@ -55,8 +59,11 @@ esac if [ "$choice" = auto ]; then # 16 GB machines report a little less than 16 GiB of usable memory. - if [ "$memory_gb" -ge 15 ]; then choice=default; else choice=fast; fi + if [ "$memory_gb" -ge 30 ]; then choice=pro + elif [ "$memory_gb" -ge 15 ]; then choice=default + else choice=fast; fi fi +case $choice in pro|all) need 0.32.12 "flycoder0.3pro (Qwen3.8)" ;; esac # Modelfiles come from this checkout when present, otherwise from GitHub. here=$(cd "$(dirname "$0")" 2>/dev/null && pwd || echo .) @@ -68,35 +75,39 @@ fetch() { else curl -fsSL "$RAW/$1" -o "$workdir/$1" || die "download failed: $RAW/$1"; fi } -build() { # build +build() { # build fetch "$1" base=$3 - if [ "$engine" = gguf ]; then + if [ "$engine" = mlx ]; then base=$4 - sed "s|^FROM $3\$|FROM $4|" "$workdir/$1" > "$workdir/$1.gguf" && mv "$workdir/$1.gguf" "$workdir/$1" + sed "s|^FROM $3\$|FROM $4|" "$workdir/$1" > "$workdir/$1.mlx" && mv "$workdir/$1.mlx" "$workdir/$1" fi grep -q "^FROM $base\$" "$workdir/$1" || die "$1 does not start from $base." say "" "==> Downloading base model $base" ollama pull "$base" - say "==> Creating flycoder:$2" - ollama create "flycoder:$2" -f "$workdir/$1" + say "==> Creating $2" + ollama create "$2" -f "$workdir/$1" } +main() { build Modelfile flycoder0.3 qwen3.5:9b qwen3.5:9b-mtp-nvfp4; } +fast() { build Modelfile.fast flycoder0.3fast qwen3.5:4b qwen3.5:4b-nvfp4; } +pro() { build Modelfile.pro flycoder0.3pro qwen3.8:27b qwen3.8:27b-nvfp4; } -say "FlyCoder $VERSION · Ollama $current · $os $arch · ${memory_gb} GB · $engine engine" +say "FlyCoder $VERSION · Ollama $current · $os $arch · ${memory_gb} GB · $engine weights" +# `flycoder` is a short alias for the variant that fits this machine. case $choice in - default) build Modelfile "$VERSION" gemma4:12b-mlx gemma4:12b; ollama cp "flycoder:$VERSION" flycoder:latest ;; - fast) build Modelfile.fast "$VERSION-fast" qwen3.5:4b-mlx qwen3.5:4b; ollama cp "flycoder:$VERSION-fast" flycoder:latest ;; - all) - build Modelfile "$VERSION" gemma4:12b-mlx gemma4:12b - build Modelfile.fast "$VERSION-fast" qwen3.5:4b-mlx qwen3.5:4b - ollama cp "flycoder:$VERSION" flycoder:latest ;; + default) main; ollama cp flycoder0.3 flycoder ;; + fast) fast; ollama cp flycoder0.3fast flycoder ;; + pro) pro; ollama cp flycoder0.3pro flycoder ;; + all) main; fast; pro; ollama cp flycoder0.3 flycoder ;; esac -# FlyBrain experts: the micro-router (1 GB) and the 2B expert for simple requests in flycoder:fast. +# FlyBrain routes flycoder0.3 and flycoder0.3fast; flycoder0.3pro is never routed. +[ "$choice" = pro ] && brain=no if [ "$brain" = yes ]; then - build Modelfile.router router qwen3.5:0.8b-nvfp4 qwen3.5:0.8b - build Modelfile.lite "$VERSION-lite" qwen3.5:2b-nvfp4 qwen3.5:2b - if [ "$choice" = default ]; then build Modelfile.fast "$VERSION-fast" qwen3.5:4b-mlx qwen3.5:4b; fi + # The micro-router (1 GB), the 2B expert, and the 4B for simple requests to flycoder0.3. + build Modelfile.router flycoder0.3:router qwen3.5:0.8b qwen3.5:0.8b-nvfp4 + build Modelfile.lite flycoder0.3:lite qwen3.5:2b qwen3.5:2b-nvfp4 + [ "$choice" = default ] && fast # The FlyBrain server itself: two dependency-free Node files. brain_dir=${FLYCODER_HOME:-$HOME/.flycoder}/brain mkdir -p "$brain_dir" @@ -109,8 +120,7 @@ fi say "" "FlyCoder is installed. Run: ollama run flycoder" if [ "$brain" = yes ]; then say "With the FlyBrain router (less RAM, Node 22): node $brain_dir/flybrain.mjs" \ - "then, in another terminal: OLLAMA_HOST=127.0.0.1:11435 ollama run flycoder" - if [ "$choice" = fast ]; then say "(without the 12B model, start FlyBrain with --max-expert fast)"; fi + "then, in another terminal: OLLAMA_HOST=127.0.0.1:11435 ollama run flycoder0.3" + [ "$choice" = fast ] && say "(without the 9B model, start FlyBrain with --max-expert fast)" fi -[ "$choice" = default ] && say "Fastest version (8 GB): sh install.sh --fast" exit 0 diff --git a/package-lock.json b/package-lock.json index 4d0431c..a4a2632 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "flycoder", - "version": "0.2.0-beta.1", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "flycoder", - "version": "0.2.0-beta.1", + "version": "0.3.0", "engines": { "node": ">=22" } diff --git a/package.json b/package.json index 1ab73c0..ff5ed77 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flycoder", - "version": "0.3.0-beta.1", + "version": "0.3.0", "private": true, "description": "FlyCoder: a local coding model for Ollama, tuned for MacBook and Mac mini", "scripts": { diff --git a/readme.md b/readme.md index 0a0ff6e..0877f59 100644 --- a/readme.md +++ b/readme.md @@ -1,163 +1,69 @@ -# FlyCoder 0.3 beta +# FlyCoder 0.3 -FlyCoder is a local coding model for Ollama, tuned for Apple Silicon MacBooks and Mac minis. Use it directly with `ollama run`, or from any Ollama-compatible tool. Since 0.3, the optional **FlyBrain** router only activates the part of FlyCoder a request needs, which lowers memory use (see below). - -Full documentation (installation, usage, API, settings, benchmark, publishing, troubleshooting): [Documentation.md](Documentation.md). - -## One-command install - -You need [Ollama](https://ollama.com/download) 0.31 or later, running. FlyCoder is published at [ollama.com/Tromset/flycoder](https://ollama.com/Tromset/flycoder): +FlyCoder is a local coding model for [Ollama](https://ollama.com), tuned for MacBooks and Mac minis: complete, runnable code, a large context, answers in your language. ```sh -ollama run Tromset/flycoder # Gemma 4 12B, Macs with 16 GB or more -ollama run Tromset/flycoder:fast # Qwen3.5 4B, 8 GB Macs +ollama run Tromset/flycoder0.3 # Macs with 16 GB +ollama run Tromset/flycoder0.3fast # Macs with 8 GB +ollama run Tromset/flycoder0.3pro # Macs with 32 GB or more ``` -For now only the portable GGUF tags are online (`ollama run Tromset/flycoder:0.2-beta-gguf`); the Mac (MLX) tags above come next. Until then, Macs install with `install.sh` below. +You need Ollama 0.30 or later (0.32.12 for `pro`). Full documentation: [Documentation.md](Documentation.md). -Or, to install it under the short name `flycoder`, with the variant picked from your Mac's memory: +## The three models -```sh -curl -fsSL https://raw.githubusercontent.com/Tromset/flycoder/main/install.sh | sh -ollama run flycoder -``` - -The installer detects your Mac's memory and builds the right variant in your local Ollama: `flycoder:0.2-beta` from 16 GB up, `flycoder:0.2-beta-fast` below that. Either way, `ollama run flycoder` works. Options: `sh -s -- --fast` (fast variant), `sh -s -- --all` (both), `sh -s -- --gguf` (GGUF weights for Intel Macs, Linux or Windows). - -## The two variants - -| Tag | Base | Weights | Recommended Mac | Context | +| Model | Base | Weights | Mac | Context | |---|---|---|---|---| -| `flycoder:0.2-beta` (`latest`) | Gemma 4 12B, Google DeepMind | 7.7 GB, NVFP4 on MLX | 16 GB or more | 32,768 tokens | -| `flycoder:0.2-beta-fast` (`fast`) | Qwen3.5 4B, Alibaba Qwen | 4.0 GB, NVFP4 on MLX | 8 GB or more | 16,384 tokens | - -Both bases are licensed under Apache 2.0 and support tool calling, thinking and images. +| [`Tromset/flycoder0.3`](https://ollama.com/Tromset/flycoder0.3) | Qwen3.5 9B | 6.6 GB | 16 GB | 65,536 tokens | +| [`Tromset/flycoder0.3fast`](https://ollama.com/Tromset/flycoder0.3fast) | Qwen3.5 4B | 3.3 GB | 8 GB | 32,768 tokens | +| [`Tromset/flycoder0.3pro`](https://ollama.com/Tromset/flycoder0.3pro) | Qwen3.8 27B | 18 GB | 32 GB or more | 65,536 tokens | -## FlyBrain: less RAM, same quality on hard requests +All three are portable GGUF builds: they run on Apple Silicon and Intel Macs, Linux and Windows, with tool calling, thinking and images. The bases are licensed under Apache 2.0. -FlyBrain takes its cue from the fly's brain: small hints drawn from the request (like the Kenyon cells of the mushroom body) are enough to settle the clear cases. When nothing is clear, a 0.8-billion-parameter micro-model decides. Only one expert "lobe" is then loaded into memory. +What FlyCoder sets up on top of the base: -| You call | Simple request | Hard request, tools (agents) or long context | Unclear request | -|---|---|---|---| -| `flycoder` | Qwen3.5 4B, with thinking | Gemma 4 12B, with thinking | the `flycoder:router` micro-model (Qwen3.5 0.8B) decides | -| `flycoder:fast` | `flycoder:0.2-beta-lite` (Qwen3.5 2B) | Qwen3.5 4B | Qwen3.5 4B (no micro-model) | +- **A large context**: 64K tokens instead of Ollama's 4,096 default on machines under 24 GB. Qwen's hybrid attention keeps it cheap: `flycoder0.3` uses 8.8 GB in total at 64K. +- **The right sampling**: Qwen's recommended settings for coding in thinking mode. +- **A short system prompt for code**: exact names and signatures, complete code without "TODO", edge cases, no invented APIs, a final check before answering, secure defaults, answers in the user's language. +- **`flycoder0.3pro` has its own recipe**: a six-step workflow (contract, plan, code, tests, check, report), rules for coding agents, a worked example that shows the expected answer, and multi-token prediction. Details: [Documentation.md, section 6.5](Documentation.md#65-what-makes-flycoder03pro-different). -Measured memory (resident memory of the model processes after one answer, 16 GB Linux server without a GPU, GGUF weights; on a Mac with MLX the absolute values differ): - -| Case | Without FlyBrain | With FlyBrain | -|---|---|---| -| `flycoder`, simple request | 10.5 GiB | **6.1 GiB** (4B + micro-model) | -| `flycoder`, hard request | 10.5 GiB | 10.5 GiB (unchanged: quality is kept) | -| `flycoder:fast`, simple request | 4.6 GiB | **3.7 GiB** | -| `flycoder:fast`, hard request | 4.6 GiB | 4.6 GiB | - -The router is rarely wrong: out of 60 labelled requests (the 20 bench problems plus 40 requests written for the purpose in [tests/fixtures/route-prompts.json](tests/fixtures/route-prompts.json)), it routes 59 correctly. No hard request went to the small model; one simple git question went to the large one. The rules decide on their own 39 times; the micro-model answers in about 0.6 s (median, on CPU). If the micro-model fails or hesitates, the request goes to the large model. A conversation keeps its expert and can only move up to the stronger one. Below 16 GB of memory, FlyBrain caps `flycoder` at the 4B. - -```sh -node ~/.flycoder/brain/flybrain.mjs # installed by install.sh; or: npm run brain from this repository -OLLAMA_HOST=127.0.0.1:11435 ollama run flycoder -``` - -FlyBrain speaks the Ollama API on port 11435 and passes every other model through. For a coding agent, point it at FlyBrain instead of Ollama: +## Install under a short name ```sh -ANTHROPIC_BASE_URL=http://127.0.0.1:11435 ANTHROPIC_AUTH_TOKEN=ollama ANTHROPIC_API_KEY="" claude --model flycoder # Claude Code -# Codex, OpenCode, Cline…: OpenAI-compatible URL http://127.0.0.1:11435/v1, model flycoder -``` - -With the models published on ollama.com rather than installed by `install.sh`: `ollama pull Tromset/flycoder:router` (and `:0.2-beta-lite`, `:0.2-beta-fast`, `:0.2-beta`), then `node brain/flybrain.mjs --prefix Tromset/`. - -## What changed since 0.1 - -**A much stronger base model.** 0.1 was built on Qwen3.5 4B. On LiveCodeBench v6, using the figures published by the authors in thinking mode, Gemma 4 12B scores 72.0% against 55.8% for Qwen3.5 4B (and 65.6% for Qwen3.5 9B, which would be twice as slow). Its published Codeforces Elo is 1659. - -**Multi-token prediction keeps the cost of 12 billion parameters in check.** Gemma 4 ships a small draft model that proposes several tokens ahead; the main model verifies them in a single pass. Measured here over 14 coding answers: 77% of the 6,594 proposed tokens accepted, 3.35 tokens validated per pass, and generation 49% faster than with the draft disabled (2.7 → 4.1 tokens/s on the same example). Ollama reports about +90% on Apple Silicon. - -**Apple Silicon's MLX engine.** Both variants use NVFP4 weights on Ollama's MLX engine. According to Ollama, this format generates about 20% faster than 0.1's Q4_K_M and roughly halves the quality loss from quantization. - -**Fixed settings.** 0.1 ran at a temperature of 0.2, which Qwen advises against (risk of endless repetition and lower quality). 0.2 applies the official settings: Google's for Gemma 4, Qwen's for coding in thinking mode. - -**Enough context for real code.** Out of the box, Ollama limits machines with less than 24 GB to 4,096 tokens. FlyCoder sets 32,768 tokens (main variant) and 16,384 (fast variant). - -**A short system prompt dedicated to code**: keep the requested names and signatures exactly, deliver complete code without "TODO", handle edge cases, never invent APIs, never claim to have run tests, avoid common vulnerabilities, answer in the user's language. - -## Measured speed and quality - -The `bench/` benchmark holds 20 coding problems (13 in Python, 7 in JavaScript) with hidden tests: intervals, strict Roman numerals, topological sort, LRU cache, SemVer, CSV, Dijkstra on 100,000 edges, expression evaluator, `*`/`?` wildcard matching that withstands pathological cases, and more. Each test is validated against a reference solution and rejects an empty solution (`npm test`). - -Run recorded in [docs/bench/cpu-nothink.json](docs/bench/cpu-nothink.json), on a 4-core Linux server **without a GPU** (Ollama 0.35, GGUF Q4_K_M weights, thinking disabled as in the former workshop, one attempt per problem, 2,048 tokens maximum). Absolute speeds are much lower than on a Mac; only the comparisons between profiles matter: - -| Profile | Base | Problems solved | Generation | Tokens per answer | Time per problem | -|---|---|---|---|---|---| -| `flycoder0.1beta` (0.1) | Qwen3.5 4B, temperature 0.2 | 5/20 (25%) | 6.6 tok/s | 581 | 94 s | -| `flycoder:0.2-beta-fast` | Qwen3.5 4B, Qwen settings | 5/20 (25%) | 6.5 tok/s | 537 | 89 s | -| `flycoder:0.2-beta` | Gemma 4 12B, multi-token | **14/20 (70%)** | 3.4 tok/s | 739 | 235 s | - -What these numbers say: - -- **The main variant solves almost three times as many problems** as 0.1: 14 against 5, including the expression evaluator, the CSV parser, the LRU cache, strict Roman numerals and spelling numbers out in words, all failed by 0.1. -- **It is slower per problem on this server**: about 2.5 times 0.1's time, because it writes more complete code (validation, docstrings) and each token costs more on CPU. On a Mac, the MLX engine and the multi-token gain, which is larger on GPU, should narrow that gap; this is not measured yet. -- **The fast variant keeps exactly 0.1's speed** (6.5 against 6.6 tokens/s, 89 against 94 s per problem) with the same pass rate without thinking, and 8% fewer tokens. On a Mac, it also gains the MLX engine. - -Seven problems for the main variant and one for the fast variant were rerun: on the first pass, the server had run out of memory or the bench had cut the request after 5 minutes. These were infrastructure failures, not answers; the bench now streams answers and retries a crashed model once. - -To measure on your Mac: - -```sh -ollama create flycoder0.1beta -f bench/baselines/flycoder-0.1-beta.Modelfile # to compare with 0.1 -npm run bench -- --models flycoder0.1beta,flycoder:0.2-beta,flycoder:0.2-beta-fast +curl -fsSL https://raw.githubusercontent.com/Tromset/flycoder/main/install.sh | sh +ollama run flycoder ``` -The bench runs the code the models produce on your machine, in a temporary folder, with a time limit; on macOS, `sandbox-exec` blocks network access and writes outside that folder. Useful options: `--think on|off`, `--samples 3`, `--only py-lcs,js-evaluate`. +The installer picks the variant from your memory and names it `flycoder`. Options after `sh -s --`: `--fast`, `--pro`, `--all`, `--mlx` (Ollama's MLX engine on Apple Silicon), `--no-brain`. -## Usage +## Measured quality and speed -```sh -ollama run flycoder # thinking on by default: best quality -ollama run flycoder --think=false # immediate answer, for simple questions -ollama run flycoder:fast # 4B variant if you installed it -``` +The 0.3 reference run (`docs/bench/cpu-nothink-0.3.json`) is being recorded. The 0.2 run stays in [docs/bench/cpu-nothink.json](docs/bench/cpu-nothink.json). -In a conversation, `/set nothink` turns thinking off and `/set parameter num_ctx 65536` enlarges the context (about 0.5 GB of extra memory for the main variant). Ollama recommends at least 64,000 tokens for coding agents; FlyCoder also works with `ollama launch` (Claude Code, Codex, OpenCode): `ollama launch claude --model flycoder` if you installed it with `install.sh`, or `ollama launch claude --model Tromset/flycoder` from ollama.com. `ollama launch` talks to Ollama directly; to go through FlyBrain, see the FlyBrain section. +## FlyBrain: less RAM for simple requests (optional) -From an application, the Ollama API is all you need: +FlyBrain is a small Node server in front of Ollama. It answers to `flycoder0.3` and `flycoder0.3fast`, sends simple questions to a smaller expert and hard requests (and every agent request with tools) to the strong one, and keeps a single expert in memory. On 60 labelled requests it routes 59 correctly, and no hard request goes to the small model. ```sh -curl http://localhost:11434/api/chat -d '{"model":"flycoder","messages":[{"role":"user","content":"Write a Python function that merges intervals."}]}' +node ~/.flycoder/brain/flybrain.mjs # installed by install.sh; or: npm run brain +OLLAMA_HOST=127.0.0.1:11435 ollama run flycoder0.3 ``` -## Publishing FlyCoder on ollama.com - -Done once, from an Apple Silicon Mac: +Details: [Documentation.md, section 5.5](Documentation.md#55-flybrain-the-router-that-lowers-ram-use). -1. Create an account on [ollama.com](https://ollama.com/signup). The username becomes part of the model name. -2. Link this Mac to the account: `ollama signin`. -3. Build both variants: `sh install.sh --all`. -4. Publish: `sh scripts/publish.sh ` (add `--gguf` to also publish the Intel Mac, Linux and Windows builds). -5. Paste the text of [docs/ollama-model-page.md](docs/ollama-model-page.md) into the model page description. +## Publishing -After that, anyone can run `ollama run /flycoder` or `ollama run /flycoder:fast`. The GGUF tags of 0.2 beta are published under `Tromset`; the MLX tags (`latest`, `fast`) still have to be pushed from an Apple Silicon Mac. +Every change to `main` that touches a Modelfile or a page in `docs/ollama/` is published on ollama.com by the [publish-ollama](.github/workflows/publish-ollama.yml) workflow: models first, then their pages. It needs two repository secrets, `OLLAMA_KEY` and `OLLAMA_API_KEY` ([Documentation.md, section 8](Documentation.md#8-publishing-on-ollamacom)). ## Repository contents -- `Modelfile`, `Modelfile.fast`: definitions of the two variants (`ollama create flycoder -f Modelfile`). -- `Modelfile.lite`, `Modelfile.router`: FlyBrain's 2B expert and micro-model. -- `brain/`: the FlyBrain server (`flybrain.mjs`), its rules (`router.mjs`) and the router accuracy measurement (`eval-router.mjs`). -- `install.sh`: one-command install. `scripts/publish.sh`: publishing on ollama.com. -- `bench/`: quality and speed benchmark; `bench/baselines/` keeps the 0.1 profile for comparison. -- `tests/`: `npm test` checks the bench tests and the consistency of the Modelfiles and scripts. -- `training/`, `scripts/build_language_dataset.js`, `server/fly-language.js`: QLoRA pipeline for the former FlyBrain voice (Qwen 2.5 1.5B, MLX), independent of FlyCoder. That voice speaks French by design, so its prompts and dataset stay in French. - -## Limitations of this beta - -- The weights are not retrained: FlyCoder 0.2 is an Ollama profile (base, quantization, settings, context, system prompt). The quality gains come from the change of base and settings, not from fine-tuning. -- The LiveCodeBench and Codeforces figures are the ones published by Google and Qwen, not FlyCoder measurements. The bundled bench is small (20 problems): it separates profiles, it does not replace a public benchmark. -- Speed on a Mac has not been measured for this version: the MLX and multi-token gains quoted are those reported by Ollama. On CPU, the main variant is about 2.5 times slower per problem than 0.1; if the gap stays too large on your Mac, `ollama cp flycoder:0.2-beta-fast flycoder:latest` makes the fast variant the default model. -- Without thinking, all three profiles sometimes leave an abandoned first attempt in the returned code (3 answers out of 20 each): keep thinking on for code you ship. -- The bench ran without thinking. With thinking on (the `ollama run` default), quality goes up for both bases but answers are longer; measure it with `npm run bench -- --think on`. -- The MLX variants need an Apple Silicon Mac; elsewhere, use `--gguf`. -- FlyBrain lowers memory for simple requests, not for hard ones: the peak stays that of the large model. Switching experts takes a few seconds of reloading. Agents (which send tools) always go to the large model. The memory and router measurements come from a Linux server running GGUF, not yet from a Mac running MLX. +- `Modelfile`, `Modelfile.fast`, `Modelfile.pro`: the three variants. `Modelfile.lite`, `Modelfile.router`: FlyBrain's experts. +- `docs/ollama/`: the ollama.com page of each model. +- `brain/`: the FlyBrain server and its routing rules. +- `install.sh`: one-command install. `scripts/publish.sh`, `scripts/update-ollama-pages.sh`: publishing. +- `bench/`: quality and speed benchmark. `tests/`: `npm test`. +- `training/`, `scripts/build_language_dataset.js`, `server/fly-language.js`: the former FlyBrain voice pipeline, independent of FlyCoder. ## License -Repository code: MIT, see [license.md](license.md). The weights keep the license of their base: Apache 2.0 for Gemma 4 and for Qwen3.5, shown by `ollama show flycoder --license`. +Repository code: MIT, see [license.md](license.md). The weights keep the license of their base: Apache 2.0 for Qwen3.5 and Qwen3.8. diff --git a/scripts/publish.sh b/scripts/publish.sh index a7c47e2..29d87d9 100755 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -1,60 +1,49 @@ #!/bin/sh -# Publishes FlyCoder on ollama.com so anyone can run: ollama run /flycoder -# sh scripts/publish.sh [--gguf] -# Prerequisites: an ollama.com account, then `ollama signin` once on this Mac. -# The models must exist locally: run `sh install.sh --all` first. -# --gguf also publish portable GGUF tags (0.2-beta-gguf, 0.2-beta-fast-gguf, 0.2-beta-lite-gguf, router-gguf) for Intel Macs, Linux and Windows +# Publishes FlyCoder on ollama.com: /flycoder0.3, /flycoder0.3fast, /flycoder0.3pro +# and FlyBrain's experts /flycoder0.3:lite and /flycoder0.3:router. +# sh scripts/publish.sh [] [ ...] +# Default user: Tromset. Default names: every model below. Example: sh scripts/publish.sh Tromset flycoder0.3fast +# Prerequisite: this machine's Ollama key is linked to the account (`ollama signin` once). +# GitHub Actions runs this script on every change to main (.github/workflows/publish-ollama.yml). +# FLYCODER_PRUNE=1 removes each model and its base after the push, to keep the disk free (used by CI) set -eu -VERSION=0.2-beta +VERSION=0.3 +# Modelfile, published name. Keep in sync with install.sh and docs/ollama/. +MODELS='Modelfile flycoder0.3 +Modelfile.fast flycoder0.3fast +Modelfile.pro flycoder0.3pro +Modelfile.lite flycoder0.3:lite +Modelfile.router flycoder0.3:router' + die() { printf 'FlyCoder: %s\n' "$*" >&2; exit 1; } -user=${1:-} -[ -n "$user" ] && [ "${user#-}" = "$user" ] || die "usage: sh scripts/publish.sh [--gguf]" -gguf=${2:-} -[ -z "$gguf" ] || [ "$gguf" = --gguf ] || die "unknown option: $gguf" +user=${1:-Tromset} +[ "${user#-}" = "$user" ] || die "usage: sh scripts/publish.sh [] [ ...]" +[ $# -gt 0 ] && shift command -v ollama >/dev/null 2>&1 || die "Ollama is not installed." +ollama list >/dev/null 2>&1 || die "Ollama is not responding. Start it with 'ollama serve'." -require() { ollama show "$1" >/dev/null 2>&1 || die "$1 is missing. First run: sh install.sh --all"; } -format() { ollama show "$1" 2>/dev/null | awk '/quantization/ {print $2}' | head -n 1; } -push() { # push - ollama cp "$1" "$user/flycoder:$2" - ollama push "$user/flycoder:$2" +here=$(cd "$(dirname "$0")/.." && pwd) +wanted() { # wanted [ ...]: true when nothing was requested or was + n=$1; shift + [ $# -eq 0 ] && return 0 + for w in "$@"; do [ "$w" = "$n" ] && return 0; done + return 1 } -here=$(cd "$(dirname "$0")/.." && pwd) -require "flycoder:$VERSION" -require "flycoder:$VERSION-fast" -require "flycoder:$VERSION-lite" -require flycoder:router -# The main tags must hold the Apple Silicon (MLX) builds, never a GGUF fallback. -for model in "flycoder:$VERSION" "flycoder:$VERSION-fast" "flycoder:$VERSION-lite" flycoder:router; do - [ "$(format "$model")" = nvfp4 ] || die "$model is not the MLX build. Publish from an Apple Silicon Mac after: sh install.sh --all" +echo "$MODELS" | while read -r file name; do + wanted "$name" "$@" || continue + base=$(sed -n 's/^FROM //p' "$here/$file" | head -n 1) + [ -n "$base" ] || die "$file has no FROM line." + printf '\n==> %s/%s (%s, base %s)\n' "$user" "$name" "$file" "$base" + ollama pull "$base" + ollama create "$user/$name" -f "$here/$file" + ollama push "$user/$name" + if [ "${FLYCODER_PRUNE:-0}" = 1 ]; then ollama rm "$user/$name" "$base" >/dev/null; fi done -push "flycoder:$VERSION" "$VERSION" -push "flycoder:$VERSION" latest -push "flycoder:$VERSION-fast" "$VERSION-fast" -push "flycoder:$VERSION-fast" fast -# FlyBrain experts, used by: node brain/flybrain.mjs --prefix / -push "flycoder:$VERSION-lite" "$VERSION-lite" -push flycoder:router router - -if [ "$gguf" = --gguf ]; then - tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT - sed 's|^FROM gemma4:12b-mlx$|FROM gemma4:12b|' "$here/Modelfile" > "$tmp/Modelfile" - sed 's|^FROM qwen3.5:4b-mlx$|FROM qwen3.5:4b|' "$here/Modelfile.fast" > "$tmp/Modelfile.fast" - sed 's|^FROM qwen3.5:2b-nvfp4$|FROM qwen3.5:2b|' "$here/Modelfile.lite" > "$tmp/Modelfile.lite" - sed 's|^FROM qwen3.5:0.8b-nvfp4$|FROM qwen3.5:0.8b|' "$here/Modelfile.router" > "$tmp/Modelfile.router" - ollama create "flycoder:$VERSION-gguf" -f "$tmp/Modelfile" - ollama create "flycoder:$VERSION-fast-gguf" -f "$tmp/Modelfile.fast" - ollama create "flycoder:$VERSION-lite-gguf" -f "$tmp/Modelfile.lite" - ollama create flycoder:router-gguf -f "$tmp/Modelfile.router" - push "flycoder:$VERSION-gguf" "$VERSION-gguf" - push "flycoder:$VERSION-fast-gguf" "$VERSION-fast-gguf" - push "flycoder:$VERSION-lite-gguf" "$VERSION-lite-gguf" - push flycoder:router-gguf router-gguf -fi - -printf '\nPublished. Anyone can now run:\n ollama run %s/flycoder (Gemma 4 12B, Macs with 16 GB or more)\n ollama run %s/flycoder:fast (Qwen3.5 4B, 8 GB Macs)\n' "$user" "$user" -printf 'Model page: https://ollama.com/%s/flycoder (paste docs/ollama-model-page.md there)\n' "$user" +printf '\nPublished FlyCoder %s. Anyone can now run:\n' "$VERSION" +printf ' ollama run %s/flycoder0.3 (Macs with 16 GB)\n' "$user" +printf ' ollama run %s/flycoder0.3fast (Macs with 8 GB)\n' "$user" +printf ' ollama run %s/flycoder0.3pro (Macs with 32 GB or more)\n' "$user" diff --git a/scripts/update-ollama-pages.sh b/scripts/update-ollama-pages.sh new file mode 100755 index 0000000..bf9578a --- /dev/null +++ b/scripts/update-ollama-pages.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# Updates the model pages on ollama.com from docs/ollama/.md. +# OLLAMA_API_KEY=... sh scripts/update-ollama-pages.sh [] +# Uses the same request as the page's Edit button (POST https://ollama.com//, field readme), +# authenticated with an ollama.com API key (https://ollama.com/settings/keys). +# A page that cannot be updated is reported, and the script exits with status 1 at the end. +set -eu + +user=${1:-Tromset} +[ -n "${OLLAMA_API_KEY:-}" ] || { printf 'FlyCoder: OLLAMA_API_KEY is not set.\n' >&2; exit 1; } +here=$(cd "$(dirname "$0")/.." && pwd) + +failed=0 +for page in "$here"/docs/ollama/*.md; do + model=$(basename "$page" .md) + # No redirect following: a redirect to the sign-in page means the key was refused. + status=$(curl -sS -o /dev/null -w '%{http_code}' -X POST "https://ollama.com/$user/$model" \ + -H "Authorization: Bearer $OLLAMA_API_KEY" --data-urlencode "readme@$page") || status=000 + case $status in + 2??) printf 'Updated https://ollama.com/%s/%s\n' "$user" "$model" ;; + *) printf 'FlyCoder: could not update https://ollama.com/%s/%s (HTTP %s)\n' "$user" "$model" "$status" >&2; failed=1 ;; + esac +done +exit "$failed" diff --git a/tests/modelfile.test.mjs b/tests/modelfile.test.mjs index 9c1e047..034a8ea 100644 --- a/tests/modelfile.test.mjs +++ b/tests/modelfile.test.mjs @@ -4,30 +4,38 @@ import fs from 'node:fs'; import { execFileSync } from 'node:child_process'; const read = file => fs.readFileSync(new URL(`../${file}`, import.meta.url), 'utf8'); -// Minimal Modelfile reader: FROM, PARAMETER and the triple-quoted SYSTEM block. +// Minimal Modelfile reader: FROM, REQUIRES, PARAMETER, and the triple-quoted SYSTEM and MESSAGE blocks. function parse(text) { const system = text.match(/^SYSTEM """([\s\S]*?)"""/m)?.[1]; - const lines = text.replace(/^SYSTEM """[\s\S]*?"""/m, '').split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#')); + const messages = [...text.matchAll(/^MESSAGE (user|assistant) """([\s\S]*?)"""/gm)].map(m => ({ role: m[1], content: m[2] })); + const lines = text.replace(/^(SYSTEM|MESSAGE \w+) """[\s\S]*?"""/gm, '').split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#')); const from = lines.filter(l => /^FROM /i.test(l)).map(l => l.slice(5).trim()); + const requires = lines.filter(l => /^REQUIRES /i.test(l)).map(l => l.slice(9).trim()); const params = Object.fromEntries(lines.filter(l => /^PARAMETER /i.test(l)).map(l => l.split(/\s+/).slice(1))); - const unknown = lines.filter(l => !/^(FROM|PARAMETER) /i.test(l)); - return { from, params, system, unknown }; + const unknown = lines.filter(l => !/^(FROM|REQUIRES|PARAMETER) /i.test(l)); + return { from, requires, params, system, messages, unknown }; } +const QWEN = { temperature: '0.6', top_k: '20', top_p: '0.95', min_p: '0', presence_penalty: '0', repeat_penalty: '1' }; +// page: the Ollama page of the model; size and mac are what that page must say. const VARIANTS = [ - { file: 'Modelfile', tag: '0.2-beta', mlx: 'gemma4:12b-mlx', gguf: 'gemma4:12b', base: 'Gemma 4 12B by Google DeepMind', params: { num_ctx: '32768', temperature: '1', top_k: '64', top_p: '0.95' } }, - { file: 'Modelfile.fast', tag: '0.2-beta-fast', mlx: 'qwen3.5:4b-mlx', gguf: 'qwen3.5:4b', base: 'Qwen3.5 4B by the Qwen team at Alibaba', - params: { num_ctx: '16384', temperature: '0.6', top_k: '20', top_p: '0.95', min_p: '0', presence_penalty: '0', repeat_penalty: '1' } }, - { file: 'Modelfile.lite', tag: '0.2-beta-lite', mlx: 'qwen3.5:2b-nvfp4', gguf: 'qwen3.5:2b', base: 'Qwen3.5 2B by the Qwen team at Alibaba', - params: { num_ctx: '8192', temperature: '0.6', top_k: '20', top_p: '0.95', min_p: '0', presence_penalty: '0', repeat_penalty: '1' } } + { file: 'Modelfile', name: 'flycoder0.3', gguf: 'qwen3.5:9b', mlx: 'qwen3.5:9b-mtp-nvfp4', base: 'Qwen3.5 9B by the Qwen team at Alibaba', + params: { num_ctx: '65536', ...QWEN }, page: 'docs/ollama/flycoder0.3.md', size: '6.6 GB', mac: '16 GB', ctx: '64K' }, + { file: 'Modelfile.fast', name: 'flycoder0.3fast', gguf: 'qwen3.5:4b', mlx: 'qwen3.5:4b-nvfp4', base: 'Qwen3.5 4B by the Qwen team at Alibaba', + params: { num_ctx: '32768', ...QWEN }, page: 'docs/ollama/flycoder0.3fast.md', size: '3.3 GB', mac: '8 GB', ctx: '32K' }, + { file: 'Modelfile.pro', name: 'flycoder0.3pro', gguf: 'qwen3.8:27b', mlx: 'qwen3.8:27b-nvfp4', base: 'Qwen3.8 27B by the Qwen team at Alibaba', + params: { num_ctx: '65536', ...QWEN, draft_num_predict: '4' }, requires: '0.32.12', page: 'docs/ollama/flycoder0.3pro.md', size: '18 GB', mac: '32 GB', ctx: '64K' }, + { file: 'Modelfile.lite', name: 'flycoder0.3:lite', gguf: 'qwen3.5:2b', mlx: 'qwen3.5:2b-nvfp4', base: 'Qwen3.5 2B by the Qwen team at Alibaba', + params: { num_ctx: '8192', ...QWEN } } ]; -const ROUTER = { file: 'Modelfile.router', tag: 'router', mlx: 'qwen3.5:0.8b-nvfp4', gguf: 'qwen3.5:0.8b', params: { num_ctx: '2048', temperature: '0' } }; +const ROUTER = { file: 'Modelfile.router', name: 'flycoder0.3:router', gguf: 'qwen3.5:0.8b', mlx: 'qwen3.5:0.8b-nvfp4', params: { num_ctx: '2048', temperature: '0' } }; const KNOWN = new Set(['num_ctx', 'temperature', 'top_k', 'top_p', 'min_p', 'presence_penalty', 'repeat_penalty', 'repeat_last_n', 'seed', 'stop', 'num_predict', 'draft_num_predict']); for (const v of VARIANTS) { - test(`${v.file} builds ${v.tag} from ${v.mlx} with the recommended sampling`, () => { + test(`${v.file} builds ${v.name} from ${v.gguf} with the recommended sampling`, () => { const m = parse(read(v.file)); - assert.deepEqual(m.from, [v.mlx]); + assert.deepEqual(m.from, [v.gguf]); + assert.deepEqual(m.requires, [v.requires ?? '0.30.0'], 'Ollama names the version to install instead of failing to load'); assert.deepEqual(m.unknown, []); assert.deepEqual(m.params, v.params); for (const key of Object.keys(m.params)) assert.ok(KNOWN.has(key), `unknown parameter ${key}`); @@ -35,16 +43,44 @@ for (const v of VARIANTS) { }); } -test('every expert shares one system prompt, apart from the base model sentence', () => { - const [a, ...others] = VARIANTS.map(v => parse(read(v.file)).system.replace(v.base, '')); +const PRO = VARIANTS.find(v => v.name === 'flycoder0.3pro'); +const SHARED = VARIANTS.filter(v => v !== PRO); + +test('the 0.3, fast and lite experts share one system prompt, apart from the base model sentence', () => { + const [a, ...others] = SHARED.map(v => parse(read(v.file)).system.replace(v.base, '')); for (const b of others) assert.equal(b, a); - assert.match(a, /^You are FlyCoder 0\.2 beta/); + assert.match(a, /^You are FlyCoder 0\.3,/); assert.ok(a.length < 2500, 'keep the system prompt short: it is processed on every new conversation'); + for (const v of SHARED) assert.deepEqual(parse(read(v.file)).messages, [], `${v.file} has no example conversation`); +}); + +test('flycoder0.3pro has its own agent workflow on top of the shared rules', () => { + const pro = parse(read(PRO.file)).system; + assert.match(pro, /^You are FlyCoder 0\.3 pro,/); + for (const step of ['1. Contract', '2. Plan', '3. Code', '4. Tests', '5. Check', '6. Report', 'As an agent with tools']) assert.ok(pro.includes(step), `pro prompt has ${step}`); + for (const rule of ['No placeholders, no "TODO"', 'never invent functions', 'parameterized SQL', 'Never claim a result you did not see', "write them in the user's language"]) + assert.ok(pro.includes(rule), `pro prompt keeps the rule: ${rule}`); + assert.ok(pro.length < 3000, 'keep the pro prompt short'); +}); + +test("the pro example conversation shows code that passes its own tests", () => { + const messages = parse(read(PRO.file)).messages; + assert.deepEqual(messages.map(m => m.role), ['user', 'assistant']); + assert.ok(!messages[1].content.includes('"""'), 'triple double quotes would end the MESSAGE block'); + const [code, tests] = [...messages[1].content.matchAll(/```python\n([\s\S]*?)```/g)].map(m => m[1]); + assert.match(tests, /pytest/); + // Run the example with plain asserts (pytest is not needed): the valid and invalid cases of its tests. + const valid = [...tests.matchAll(/\("([^"]*)", (\d+)\)/g)].map(m => [m[1], Number(m[2])]); + const invalid = JSON.parse(tests.match(/"text", (\[[^\]]*\])/)[1]); + assert.ok(valid.length >= 5 && invalid.length >= 5); + const check = `${code}\nfor text, seconds in ${JSON.stringify(valid)}:\n assert parse_duration(text) == seconds, text\n` + + `for text in ${JSON.stringify(invalid)}:\n try:\n parse_duration(text)\n except ValueError:\n continue\n raise AssertionError(text)\nprint("ok")\n`; + assert.equal(execFileSync('python3', ['-c', check], { encoding: 'utf8' }).trim(), 'ok'); }); test('Modelfile.router is a tiny deterministic classifier that answers simple or hard', () => { const m = parse(read(ROUTER.file)); - assert.deepEqual(m.from, [ROUTER.mlx]); + assert.deepEqual(m.from, [ROUTER.gguf]); assert.deepEqual(m.unknown, []); assert.deepEqual(m.params, ROUTER.params); assert.match(m.system, /"simple"/); assert.match(m.system, /"hard"/); @@ -53,16 +89,33 @@ test('Modelfile.router is a tiny deterministic classifier that answers simple or test('installer and publisher agree with the Modelfiles and the package version', () => { const install = read('install.sh'), publish = read('scripts/publish.sh'), pkg = JSON.parse(read('package.json')); - assert.match(pkg.version, /^0\.3\.0-beta\./); - for (const script of [install, publish]) assert.match(script, /^VERSION=0\.2-beta$/m); + assert.match(pkg.version, /^0\.3\.\d+$/); + for (const script of [install, publish]) assert.match(script, /^VERSION=0\.3$/m); for (const v of [...VARIANTS, ROUTER]) { - const tag = v === ROUTER ? 'router' : `"$VERSION${v.tag.slice('0.2-beta'.length)}"`; - assert.ok(install.includes(`build ${v.file} ${tag} ${v.mlx} ${v.gguf}`), `install.sh builds ${v.file}`); - assert.ok(publish.includes(`FROM ${v.mlx}$|FROM ${v.gguf}`), `publish.sh maps ${v.mlx} to ${v.gguf}`); + assert.ok(install.includes(`build ${v.file} ${v.name} ${v.gguf} ${v.mlx}`), `install.sh builds ${v.name} from ${v.file}`); + assert.match(publish, new RegExp(`^(MODELS=')?${v.file.replace('.', '\\.')} ${v.name}'?$`, 'm'), `publish.sh publishes ${v.file} as ${v.name}`); } for (const file of ['brain/router.mjs', 'brain/flybrain.mjs']) assert.ok(install.includes(file), `install.sh installs ${file}`); }); +test('every published model has a short, up-to-date Ollama page', () => { + for (const v of VARIANTS.filter(v => v.page)) { + const page = read(v.page); + assert.ok(page.includes(`ollama run Tromset/${v.name}\n`), `${v.page} shows how to run Tromset/${v.name}`); + for (const fact of [v.size, v.mac, v.ctx, v.base.split(' by ')[0]]) assert.ok(page.includes(fact), `${v.page} mentions ${fact}`); + assert.ok(!/0\.2|Gemma/.test(page), `${v.page} has no stale 0.2 or Gemma mention`); + assert.ok(page.split('\n').length <= 40, `${v.page} stays short`); + } +}); + +test('the publish workflow runs the publisher and updates every page', () => { + const workflow = read('.github/workflows/publish-ollama.yml'); + assert.match(workflow, /branches: \[main\]/); + assert.match(workflow, /sh scripts\/publish\.sh Tromset/); + for (const v of VARIANTS.filter(v => v.page)) assert.ok(workflow.includes(v.page.split('/').pop()) || workflow.includes('docs/ollama/'), `workflow uploads ${v.page}`); + for (const secret of ['OLLAMA_KEY', 'OLLAMA_API_KEY']) assert.ok(workflow.includes(`secrets.${secret}`), `workflow uses ${secret}`); +}); + test('shell scripts parse with POSIX sh', () => { - for (const file of ['install.sh', 'scripts/publish.sh']) execFileSync('sh', ['-n', new URL(`../${file}`, import.meta.url).pathname]); + for (const file of ['install.sh', 'scripts/publish.sh', 'scripts/update-ollama-pages.sh']) execFileSync('sh', ['-n', new URL(`../${file}`, import.meta.url).pathname]); }); diff --git a/tests/router.test.mjs b/tests/router.test.mjs index 98ab30a..8b8f861 100644 --- a/tests/router.test.mjs +++ b/tests/router.test.mjs @@ -32,15 +32,15 @@ test('digest reads native, OpenAI and Anthropic shapes', () => { test('route: the micro-model settles unclear requests, and any failure keeps quality', async () => { const profile = profiles().flycoder, body = user('Écris une fonction qui met une chaîne en majuscules'); - assert.equal((await route('flycoder', body, { profile, askRouter: async () => 'simple' })).expert, 'flycoder:0.2-beta-fast'); - assert.equal((await route('flycoder', body, { profile, askRouter: async () => 'hard' })).expert, 'flycoder:0.2-beta'); + assert.equal((await route('flycoder', body, { profile, askRouter: async () => 'simple' })).expert, 'flycoder0.3fast'); + assert.equal((await route('flycoder', body, { profile, askRouter: async () => 'hard' })).expert, 'flycoder0.3'); assert.equal((await route('flycoder', body, { profile, askRouter: async () => 'maybe' })).level, 'hard'); assert.equal((await route('flycoder', body, { profile, askRouter: async () => { throw new Error('down'); } })).level, 'hard'); // No micro-model call when the hard expert is already in memory. const skipped = await route('flycoder', body, { profile, loaded: profile.hard, askRouter: () => assert.fail('router called') }); assert.equal(skipped.reason, 'hard expert already loaded'); - // flycoder:fast has no micro-model: unclear requests keep the 4B. - assert.equal((await route('flycoder:fast', body, { profile: profiles()['flycoder:fast'] })).expert, 'flycoder:0.2-beta-fast'); + // flycoder0.3fast has no micro-model: unclear requests keep the 4B. + assert.equal((await route('flycoder0.3fast', body, { profile: profiles()['flycoder0.3fast'] })).expert, 'flycoder0.3fast'); }); test('route: a conversation keeps its expert and can only move up', async () => { @@ -56,8 +56,12 @@ test('route: a conversation keeps its expert and can only move up', async () => }); test('profiles: prefix for published models and the 8 GB cap', () => { - assert.equal(profiles('Tromset/').flycoder.hard, 'Tromset/flycoder:0.2-beta'); - assert.equal(profiles('', { maxExpert: 'fast' }).flycoder.hard, 'flycoder:0.2-beta-fast'); + const published = profiles('Tromset/'); + assert.equal(published.flycoder.hard, 'Tromset/flycoder0.3'); + assert.equal(published['Tromset/flycoder0.3'].router, 'Tromset/flycoder0.3:router'); + assert.equal(published['Tromset/flycoder0.3fast:latest'].simple, 'Tromset/flycoder0.3:lite'); + assert.equal(published['flycoder0.3pro'], undefined, 'the pro model is passed through, never routed'); + assert.equal(profiles('', { maxExpert: 'fast' }).flycoder.hard, 'flycoder0.3fast'); assert.equal(defaultMaxExpert(8 * 2 ** 30), 'fast'); assert.equal(defaultMaxExpert(16 * 2 ** 30), 'full'); }); @@ -110,7 +114,7 @@ test('FlyBrain server rewrites the model, streams the answer and keeps one exper const post = (path, body) => fetch(base + path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }); try { const simple = await post('/api/chat', { model: 'flycoder', ...user('How do I reverse a list in Python?') }); - assert.equal(simple.headers.get('x-flybrain-expert'), 'flycoder:0.2-beta-fast'); + assert.equal(simple.headers.get('x-flybrain-expert'), 'flycoder0.3fast'); assert.match(await simple.text(), /"done":true/); assert.equal(ollama.seen.at(-1).think, true, 'normal profile thinks by default'); @@ -119,13 +123,13 @@ test('FlyBrain server rewrites the model, streams the answer and keeps one exper await unclear.text(); const hard = await post('/v1/messages', { model: 'flycoder', max_tokens: 10, tools: [{ name: 'read', input_schema: {} }], messages: [{ role: 'user', content: 'hi' }] }); - assert.equal(hard.headers.get('x-flybrain-expert'), 'flycoder:0.2-beta'); + assert.equal(hard.headers.get('x-flybrain-expert'), 'flycoder0.3'); await hard.text(); - assert.deepEqual([...ollama.loaded], ['flycoder:0.2-beta'], 'small expert and micro-model unloaded before the 12B'); + assert.deepEqual([...ollama.loaded], ['flycoder0.3'], 'small expert and micro-model unloaded before the 9B'); assert.equal(ollama.maxLoaded(), 1); - const show = await post('/api/show', { model: 'flycoder:fast' }); - assert.equal((await show.json()).model, 'flycoder:0.2-beta-fast'); + const show = await post('/api/show', { model: 'flycoder0.3fast' }); + assert.equal((await show.json()).model, 'flycoder0.3fast'); const other = await post('/api/chat', { model: 'llama3', ...user('hi') }); assert.equal(other.headers.get('x-flybrain-expert'), null); await other.text();