Skip to content

feat(local-model): a library of models on this Mac, with sizes, delete, add and switch - #804

Merged
Ark0N merged 5 commits into
masterfrom
feat/local-model-library
Nov 12, 2026
Merged

Ark0N merged 5 commits into
masterfrom
feat/local-model-library

Conversation

@Ark0N

@Ark0N Ark0N commented Nov 10, 2026 •

Copy link
Copy Markdown
Owner

What this changes

"Run a local model" is the window that installs a coding model on the Mac the Codeman server runs on, through a private Ollama runtime under the data directory. Until now it knew exactly one model: a second install was refused, and a model that Remove left on disk could never be freed from the window again.

This PR turns that into a library of models with one in use:

  • See what is on this Mac. The installed screen lists every model in the store with its size on disk, plus the total the store uses.
  • Delete one. Each model that is not in use has a Delete button that asks once and says how much it frees.
  • Add another. "Add a model" opens the chooser again. The new model is downloaded, checked and then becomes the one in use; the previous one stays in the library.
  • Switch. "Use" on a checked model loads and checks it again and makes it the one in use. Nothing is downloaded.
  • Free leftovers. With nothing installed but models still on disk (Remove can keep them), the window shows the size and offers to delete them.

In the chooser opened from an installed state, a row that is already on this Mac says so and choosing it switches without a download. A failed add shows the way back to the previous model. On a phone each model row is two lines.

What stays exactly as it was

The feature's safety rules are unchanged, on purpose:

  • One model is loaded and connected at a time. The managed endpoint record local-ollama still lists exactly one model, the runtime still runs with OLLAMA_MAX_LOADED_MODELS=1, and the launch guard still starts a coding agent only on the model in use, after comparing its stored digest.
  • Switching and adding are refused while sessions use the model (409 sessions-open with the session names), like Stop and Remove already were. Their coding agents were started on the current model, and two models would make the runtime swap tens of gigabytes per request.
  • Nothing outside the local model folder is touched beyond the state file and the two writes of the connect step. The state file still holds no path.
  • Admin only in multi-user mode on every new route, checked before anything is parsed or probed.

API (experimental routes, outside the stable /api/v1 contract)

Route Body Refusals
POST /api/local-model/install (now also from installed) unchanged adds 409 sessions-open; already-installed now means this model at this context is the one in use
POST /api/local-model/use { modelId } 400 not a checked model of the library, or its coding agent is gone; 409 job-running, wrong-state, already-installed, sessions-open
DELETE /api/local-model/models { modelId } 400 no such model on this Mac; 409 job-running, wrong-state, model-active, runtime-not-running; 500 the runtime did not delete it
DELETE /api/local-model unchanged now also offered in idle while the store holds something

GET /api/local-model gains, inside install:

  • library[]: { id, label, context?, sizeBytes?, sizeGb?, active, verified, status, actions }. status is ready, changed, missing or unverified; actions is a subset of use, delete.
  • store: { bytes, gb }, the real disk use of the model store.
  • a recommendation row that is already a checked model on this Mac carries library: 'active' | 'ready'.

How it is built

  • State file (schema still 1, old files read as before): library holds every model that passed the check through Codeman, with its context, digest, size and coding agent. An installed file from an older version counts its one model.
  • A switch is a job of two of the existing steps, verify and connect, started by a new reducer event use. Every new state and event pair is a row in the transition table (45 pairs, 22 listed), and the "every state with every event" test covers all of them.
  • A model the runtime lists that Codeman never checked (kept by an earlier Remove, or pulled by another program through the runtime's port, which has no login) is shown as "Not checked": it can be deleted, never started.
  • Delete takes a model name, never a path. The schema admits only name:tag, the name must be in the server's own listing, and only the runtime's own delete removes it.
  • The window never asks the runtime on its one-second poll. The listing and the store size are cached for a few seconds and refreshed in the background.
  • A job that fails after an add or a switch leaves the state failed with the new model. The view then offers "Use" on the other checked models so the person gets back to the previous one. Sessions stay refused until the state is installed again, with a new sentence (job-unfinished) instead of the misleading "not running, start it first".

Testing

  • The full gate (npm test): see the CI run on this PR.
  • 43 targeted test files, 1,900 tests on the final tree: every test/local-model-* and test/routes/local-model-* file, the i18n harvest, the custom model tests and both CLI id-branching guards. The end-to-end test drives the real modules over the fake runtime: install A, add B, switch back to A, delete B, the refusal for the model in use, the sessions-open refusals.
  • npm run test:browser -- test/local-model-ui.browser.test.ts (not part of the gate): 10 passed in Chromium, 4 of them new (sizes on one edge, no sideways jump when the Delete question opens, a long list scrolling from its top at 1440 and 390 px, two-line phone rows).
  • Typecheck, lint, format check, frontend syntax and public assets checks: clean.
  • One check against a real runtime: a private Ollama on Linux (own port, store and home). /api/tags sizes summed to exactly the size of the blobs folder, and after a delete the listing dropped the model and the blobs were freed at once.

Run on a real Mac (2026-11-12)

A MacBook Pro (M5 Pro, 48 GB, macOS 27.2) with the pinned runtime, Ollama 0.40.15. Its Codeman ran the tree of this stack (#803 to #808) merged onto 1.50.1. The window was driven by real clicks, from a Chromium on another machine through an SSH forward to the Mac's own server, so every state on screen was the Mac's.

  • Upgrade from the 1.50.1 release: the release installed Qwen3.5 9B there (129 seconds), then the same data directory was given this build. The window listed the model with 6.6 GB, in use and checked, and a session started on it. The state file the release wrote needed nothing.
  • A first install through the window: Qwen3.6 35B-A3B (the MLX build), 23.1 GB in 6 minutes 22 seconds. The consent total was accepted, and the model loaded at 23.2 GB, all of it on the GPU.
  • Add a model: Ornith 1.0 9B, 5.6 GB in 88 seconds. It became the model in use, the earlier one stayed, and the list showed both sizes and their total (28.7 GB).
  • Use: back to Qwen3.6 35B-A3B in 9 seconds, nothing downloaded. Two more switches between a GGUF model and the MLX build took 6 and 12 seconds.
  • Delete: the question named the model and the 5.6 GB it frees. Afterwards the store read 23.1 GB and the disk agreed.
  • Refusals with a session open on the model: Use, Stop and Remove each answered 409 sessions-open. The model in use could not be deleted, with or without sessions.
  • Server restarts (three, for rebuilds) kept the list and brought the runtime back with the model in use.
  • Remove with the downloads ticked put the Mac back to nothing installed: folder gone, connection gone, the settings flag back at its earlier value.

Two bugs were found there and are fixed in this PR, each in its own commit, and each was seen fixed on the same Mac:

  1. db81d55f: with a worker of the codeman skill open, the refusal read "Sessions are still using this model: . Close them first." A session an agent starts has no name of its own. The sentence now uses what the session's tab shows (its folder, else the short id). This sentence has shipped since 1.50.0 for Stop and Remove.
  2. 9c2ba35f: after pressing Use, the window showed the one-button screen that ends an install, so the list was gone and deleting the other model meant closing the window and opening it again. A switch now ends on the list.

Not proven, stated plainly

  • One Mac only (M5 Pro, 48 GB). Nothing ran on a 16 GB or a 96 GB Mac.
  • A switch to a model with another context length was not run: every model in these runs was loaded at 131,072 tokens.
  • Models that share layers were not exercised: their rows would sum above the store total, which is the exact figure.
  • The window was driven in Chromium, against the Mac's server. It was not looked at inside the macOS app's WKWebView.

Known rough edges, left for a follow-up

  • A model under 50 MB would print as "0.0 GB" (no catalogue model is that small; an unchecked leftover could be).
  • In the chooser, a row that is on this Mac and also carries a server tag shows two tags and wraps to a taller row at 640 px.
  • A library model offered at another context than it was checked with goes through the normal install, whose confirm screen shows the catalogue download size although nothing is downloaded.
  • Downloading a model in the background while sessions keep using the current one is not possible yet: adding is refused up front while sessions are open.

The local model window could hold one model. A second install was refused
(409 already-installed), and a model a Remove kept could never be freed
from the window again. The store can now hold several models, and exactly
one of them is the model in use: the one the endpoint record lists, the
runtime is started for and a session may be launched on.

Server
- LocalModelStateFile.library lists every model that passed the check
  (id, label, context, digest, size, coding agent). The service writes
  the entry when verify passes; the reducer carries it and remove drops
  it. An installed file of an earlier build counts its one model.
- POST /api/local-model/install is allowed from installed: it adds a
  model, and the earlier one stays. A replaced job no longer deletes a
  model of the library, only an unchecked or partial download.
- POST /api/local-model/use switches to a checked model: a job of the
  two steps verify and connect. The stored digest is compared before
  anything is loaded (model-changed otherwise). Nothing is downloaded.
- DELETE /api/local-model/models deletes one model by its name, through
  the runtime's own delete, never by a path, never the model in use.
- Adding and switching answer 409 sessions-open while a live session
  uses the endpoint. Until such a job ends installed, a session start is
  refused with the new code job-unfinished.
- The view carries library (the state's entries joined with what the
  running runtime lists: ready, changed, missing, unverified) and store
  (the size of the blob folder). Both are cached and refreshed in the
  background, never read on the one-second poll.
- With nothing installed, remove is offered while the store still holds
  models an earlier Remove kept.

Window
- The installed screen lists the models on this Mac with the context
  each was checked with, its state and its size on disk, with Use and
  Delete per row (Delete asks once, in the row) and Add a model.
- In the chooser a model that is on this Mac already says so and is
  switched to without a download or a confirm screen.
- A paused or failed job that has a checked model beside it shows the
  way back to that model.

Every route is admin only in multi-user mode, like the existing ones.

Not run on a Mac: the tests use fakes and the fake runtime, and the
listing, store size and delete were checked against a real Ollama 0.13.0
on Linux, not the pinned 0.40.15 build for macOS.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The guide, the API reference, the invariants and the two CLAUDE files
describe the library with one model in use: the state file's list, the
three verbs and their refusals, the two cached listings of the view, and
the window's list with its row actions.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
… the Stop

A job started from a model the person had stopped runs the runtime, and
the model it ends on is in use and running. The stored "stopped" flag
stayed set, so that model would not have started with the server again.
Both verbs now set it back once their job started, the way Start does.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…his model" refusal

Seen on a MacBook (M5 Pro, 2026-11-12): with a worker of the codeman skill open on the
local model, Stop, Remove and a switch of models answered

    Sessions are still using this model: . Close them first.

A session an agent starts has no name of its own (a quick-start without sessionName), and
the refusal listed session.name. The tab of such a session shows the last folder of its
working directory, else the first eight characters of its id, so the sentence now uses the
same label (sessionLabel() beside sessionNamesUsingEndpoint()).

The sentence has shipped since 1.50.0 for Stop and Remove. This branch adds it to Use and
to the install of another model, which is where a person with local workers open meets it.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…-button screen

Seen on a MacBook (M5 Pro, 2026-11-12): pressing Use on a model in "Models on this Mac"
ran the switch and then showed the screen that ends an install, "X is ready on this Mac"
with Start a session alone. The list was gone, so deleting the model just switched away
from meant closing the window and opening it again.

That screen is right after a download: the person installed a model and wants a session.
A switch is pressed IN the list by someone who is tidying it. A switch is a job without a
`model` step, so the window now tells the two apart from the job it just watched and ends a
switch on the whole installed screen: the list with "In use" on the other row, Add a model,
Stop and Remove.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@Ark0N
Ark0N merged commit b724f2d into master Nov 12, 2026
2 checks passed
Ark0N pushed a commit that referenced this pull request Nov 12, 2026
…and switch

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Ark0N pushed a commit that referenced this pull request Nov 12, 2026
… and agent verbs (#804, #806)

POST /api/local-model/use, DELETE /api/local-model/models and
POST /api/local-model/agent are experimental like the routes beside them,
and so are the install object's library, store and harnesses lists.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Ark0N pushed a commit that referenced this pull request Nov 12, 2026
…f models, and a session without a name is named in the refusal

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Ark0N pushed a commit that referenced this pull request Nov 12, 2026
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Ark0N pushed a commit that referenced this pull request Nov 12, 2026
… run (#804, #807)

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants