feat: Ollama models command, with load/pull/delete and unload #16

Merged
adrianbonpin merged 3 commits from feat/ollama-models-command into prod 2026-10-05 09:43:03 +00:00
Owner

New Ollama Models command: list what the configured host can reach, filter with the search bar, pick the model Lam uses, unload one that is holding memory, and on a server you run, pull and delete models.

Also fixes two bugs and a prompt gap found while testing this against a local server.

Unload

GET /api/ps reports what is loaded right now, and those rows get a green loaded tag, so "why is my GPU busy" is answered in the same place as "what do I have". Loaded rows offer Unload Model, which frees the memory without deleting anything.

No confirmation, unlike deleting: nothing leaves the disk, and the worst case is that the next question pays to load the model again. The toast reports what was freed.

It is POST /api/generate with keep_alive: 0 and no prompt, which is how Ollama says "release this now" and what its own ollama stop does. Verified against 0.35.1 with the model genuinely loaded:

1. before:                     []
2. after loading:              [ "ornith-1.5:9b (11.0 GB in memory)" ]
3. unloaded ornith-1.5:9b, freed 11.0 GB
4. after unloading:            []

The reply is {"done":true,"done_reason":"unload"}. The DELETE form of the route answers 405, so there is no delete-shaped alternative.

A host that does not serve /api/ps (older builds answer 404) shows no loaded tags and nothing else changes. That check resolving to nothing is not an error, and it must not take the model list down with it.

Fix: web tools were 404ing on a local host

The extension posted to the cloud path on every host. The two kinds of host serve the same operations at different paths, probed directly:

/api/web_search /api/experimental/web_search
ollama.com 401 (exists, needs a key) 404
localhost:11434 404 200, with real results

So there is no path that works for both. The path is now chosen by host, and the other is tried if the first answers 404 (a 404 means the route is absent, so nothing is ever sent twice). Verified through the real tool layer against a local server: web_search and web_fetch both return real content.

The error hint was also wrong ("The web tools require an Ollama Cloud host"), so a failing model was given bad advice on top of a bad request.

Fix: the model had no idea what it was

describeRuntime appends this to the system prompt, after the user's own prompt if set, because none of it is a style choice:

What you are, for questions about your own setup. Do not speculate beyond this:
- You are Lam, running inside the Vicinae launcher on the user's desktop, and answering as
  `ornith-1.5:9b` served by the Ollama server at http://localhost:11434, which is reachable
  on this machine.
- You see only what the user types or attaches. You cannot read their screen, files,
  clipboard, calendar or anything else on the machine, and you cannot act on it.
- A model name ending in `:cloud` is proxied by that server to ollama.com, so the conversation
  leaves this machine when it is used. Anything else is running locally.
- You have `web_search` and `web_fetch`. Use them rather than guessing about anything recent,
  and say so if they fail.
- You can read images. A screenshot attached to a question is yours to describe, so describe it.

Before this, a local model described Vicinae as "a desktop launcher for Ollama", reported its own web tools as not configured, and answered a question about an attached screenshot by reasoning out loud about whether it was able to see, instead of describing it.

The :cloud line states Ollama's naming convention rather than looking it up. The authoritative field is remote_host on /api/tags, which this command reads and shows as a cloud tag, but pulling it into the chat would cost a second request per model change for one sentence.

Why a pick is not written to the preference

An extension cannot write its own preferences: @vicinae/api has getPreferenceValues and no setter. The only way to change the preference would be to write Vicinae's config file, and that does not work. Measured, not assumed:

  1. Pointed lam-agent's ollamaHost at a local listener that logged every request, in ~/.config/vicinae/settings.json
  2. Launched the setup command with vicinae-cli cmd launch
  3. The listener received nothing; the requests went to the old host
  4. Restored the file, verified byte-identical

Preferences are read once at startup and held in memory, so a write applies only after a Vicinae restart. It would also be a lost update (Vicinae owns and rewrites that file) and undocumented, so it would break on an update.

So the pick goes to LocalStorage and loadPreferences layers it over the preference. The cost is surfaced rather than hidden: while a pick is active, editing the Model preference has no effect. The setup screen shows both values and offers Reset to the Preference.

Other API details verified against Ollama 0.35.1

  • DELETE /api/delete, not POST: the POST form answers 405 method not allowed.
  • POST /api/pull reports a bad name as HTTP 200 with an error in the body, so the stream is checked and not just the status. Same trap the chat stream already had.
  • A local daemon's list mixes real weights with :cloud entries proxied to ollama.com (a few hundred bytes, remote_model/remote_host), so the two are told apart rather than both being called installed. Deleting a cloud entry drops a reference, not gigabytes.
  • /api/tags includes capabilities and size; both are optional, so a server that omits them shows nothing rather than implying "no vision".
  • formatBytes uses decimal units and matches ollama list exactly (6550813657 bytes prints as 6.6 GB in both).

Also

  • loadPreferences is now the single preferences reader. getLamPreferences is no longer exported, because a second synchronous reader would look right and silently ignore the pick. Views use hooks/usePreferences.
  • ChatView is split into an outer loader and ChatSession, so useChat is only created once the model is known.
  • "unknown" and "no" are kept distinct wherever vision is reported: canSeeImages stays fail-closed for the snip action, but the prompt gets the tri-state, so a failed check is never reported to the model as an inability.

Verification

  • 487 tests pass.
  • tsc --noEmit, eslint ., vici lint, vici build all clean.
  • Real end-to-end against a local daemon using only deliberate, reversible calls: the full load/ps/unload cycle above, listModels parsing, deleteModel on a missing name (which proves the DELETE route without deleting anything), pullModel on a bad name (which proves the inline-error path having downloaded nothing), and both web tools returning real content.
  • Command launched via vicinae-cli cmd launch: /api/ps and /api/tags both requested, 200, no worker errors.
New **Ollama Models** command: list what the configured host can reach, filter with the search bar, pick the model Lam uses, unload one that is holding memory, and on a server you run, pull and delete models. Also fixes two bugs and a prompt gap found while testing this against a local server. ## Unload `GET /api/ps` reports what is loaded right now, and those rows get a green `loaded` tag, so "why is my GPU busy" is answered in the same place as "what do I have". Loaded rows offer **Unload Model**, which frees the memory without deleting anything. No confirmation, unlike deleting: nothing leaves the disk, and the worst case is that the next question pays to load the model again. The toast reports what was freed. It is `POST /api/generate` with `keep_alive: 0` and no prompt, which is how Ollama says "release this now" and what its own `ollama stop` does. Verified against 0.35.1 with the model genuinely loaded: ``` 1. before: [] 2. after loading: [ "ornith-1.5:9b (11.0 GB in memory)" ] 3. unloaded ornith-1.5:9b, freed 11.0 GB 4. after unloading: [] ``` The reply is `{"done":true,"done_reason":"unload"}`. The `DELETE` form of the route answers 405, so there is no delete-shaped alternative. A host that does not serve `/api/ps` (older builds answer 404) shows no loaded tags and nothing else changes. That check resolving to nothing is not an error, and it must not take the model list down with it. ## Fix: web tools were 404ing on a local host The extension posted to the cloud path on every host. The two kinds of host serve the same operations at **different paths**, probed directly: | | `/api/web_search` | `/api/experimental/web_search` | | --- | --- | --- | | `ollama.com` | **401** (exists, needs a key) | 404 | | `localhost:11434` | 404 | **200**, with real results | So there is no path that works for both. The path is now chosen by host, and the other is tried if the first answers 404 (a 404 means the route is absent, so nothing is ever sent twice). Verified through the real tool layer against a local server: `web_search` and `web_fetch` both return real content. The error hint was also wrong ("The web tools require an Ollama Cloud host"), so a failing model was given bad advice on top of a bad request. ## Fix: the model had no idea what it was `describeRuntime` appends this to the system prompt, after the user's own prompt if set, because none of it is a style choice: ``` What you are, for questions about your own setup. Do not speculate beyond this: - You are Lam, running inside the Vicinae launcher on the user's desktop, and answering as `ornith-1.5:9b` served by the Ollama server at http://localhost:11434, which is reachable on this machine. - You see only what the user types or attaches. You cannot read their screen, files, clipboard, calendar or anything else on the machine, and you cannot act on it. - A model name ending in `:cloud` is proxied by that server to ollama.com, so the conversation leaves this machine when it is used. Anything else is running locally. - You have `web_search` and `web_fetch`. Use them rather than guessing about anything recent, and say so if they fail. - You can read images. A screenshot attached to a question is yours to describe, so describe it. ``` Before this, a local model described Vicinae as "a desktop launcher for Ollama", reported its own web tools as not configured, and answered a question about an attached screenshot by reasoning out loud about whether it was able to see, instead of describing it. The `:cloud` line states Ollama's naming convention rather than looking it up. The authoritative field is `remote_host` on `/api/tags`, which this command reads and shows as a `cloud` tag, but pulling it into the chat would cost a second request per model change for one sentence. ## Why a pick is not written to the preference An extension cannot write its own preferences: `@vicinae/api` has `getPreferenceValues` and no setter. The only way to change the preference would be to write Vicinae's config file, and **that does not work**. Measured, not assumed: 1. Pointed lam-agent's `ollamaHost` at a local listener that logged every request, in `~/.config/vicinae/settings.json` 2. Launched the setup command with `vicinae-cli cmd launch` 3. The listener received nothing; the requests went to the old host 4. Restored the file, verified byte-identical Preferences are read once at startup and held in memory, so a write applies only after a Vicinae restart. It would also be a lost update (Vicinae owns and rewrites that file) and undocumented, so it would break on an update. So the pick goes to LocalStorage and `loadPreferences` layers it over the preference. The cost is surfaced rather than hidden: while a pick is active, editing the `Model` preference has no effect. The setup screen shows both values and offers **Reset to the Preference**. ## Other API details verified against Ollama 0.35.1 - `DELETE /api/delete`, not `POST`: the POST form answers `405 method not allowed`. - `POST /api/pull` reports a bad name as **HTTP 200 with an `error` in the body**, so the stream is checked and not just the status. Same trap the chat stream already had. - A local daemon's list mixes real weights with `:cloud` entries proxied to ollama.com (a few hundred bytes, `remote_model`/`remote_host`), so the two are told apart rather than both being called installed. Deleting a cloud entry drops a reference, not gigabytes. - `/api/tags` includes `capabilities` and `size`; both are optional, so a server that omits them shows nothing rather than implying "no vision". - `formatBytes` uses decimal units and matches `ollama list` exactly (6550813657 bytes prints as 6.6 GB in both). ## Also - `loadPreferences` is now the single preferences reader. `getLamPreferences` is no longer exported, because a second synchronous reader would look right and silently ignore the pick. Views use `hooks/usePreferences`. - `ChatView` is split into an outer loader and `ChatSession`, so `useChat` is only created once the model is known. - `"unknown"` and `"no"` are kept distinct wherever vision is reported: `canSeeImages` stays fail-closed for the snip action, but the prompt gets the tri-state, so a failed check is never reported to the model as an inability. ## Verification - 487 tests pass. - `tsc --noEmit`, `eslint .`, `vici lint`, `vici build` all clean. - Real end-to-end against a local daemon using only deliberate, reversible calls: the full load/ps/unload cycle above, `listModels` parsing, `deleteModel` on a missing name (which proves the DELETE route without deleting anything), `pullModel` on a bad name (which proves the inline-error path having downloaded nothing), and both web tools returning real content. - Command launched via `vicinae-cli cmd launch`: `/api/ps` and `/api/tags` both requested, 200, no worker errors.
adrianbonpin added 1 commit 2026-10-05 09:27:21 +00:00
adrianbonpin added 1 commit 2026-10-05 09:35:25 +00:00
adrianbonpin added 1 commit 2026-10-05 09:41:39 +00:00
adrianbonpin changed title from feat: list, pick and manage Ollama models from a command to feat: Ollama models command, with load/pull/delete and unload 2026-10-05 09:41:55 +00:00
adrianbonpin merged commit 26e05dea5b into prod 2026-10-05 09:43:03 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: adrianbonpin/lam-agent-vicinae#16