Skip to content

Measure local models across every runtime, and give Models its own nav section - #4729

Merged
atomantic merged 3 commits into
mainfrom
cos/task-mt2msvsr/agent-8a8920ac
Aug 21, 2026
Merged

Measure local models across every runtime, and give Models its own nav section#4729
atomantic merged 3 commits into
mainfrom
cos/task-mt2msvsr/agent-8a8920ac

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

Measured Model Assessments could only benchmark Ollama and LM Studio, because its only measurement path resolved a configured PortOS provider. It now reaches every local daemon PortOS knows how to talk to — llama.cpp, MTPLX, and vLLM as well — and it records what configuration each reading describes, so "is -ub 512 worth it on this machine?" is a question you can actually answer.

Model management also stops being a scroll position on a Settings page: it gets its own top-level Models section.

Every runtime, not just the managed pair

ASSESSABLE_RUNTIMES (server/lib/localProviderRuntime.js) is the roster. The split is about how PortOS reaches the model, not about measurement quality:

Kind Runtimes Model list Measurement
Managed ollama, lmstudio listModels() — a durable catalog on disk runLocalLlmTest (provider path, lands in /runs)
Endpoint llama, mtplx, vllm GET /v1/models on the live daemon runEndpointLlmTest — direct, no /runs record

That distinction is the whole sentinel contract for this feature. A managed backend's model list survives the daemon being down; an endpoint runtime has no catalog at all, so a stopped daemon means the list could not be read — an error, never an empty catalog. Reporting "0 models" for a daemon the user only needs to start would silently hide every model behind it.

The SSE read loop moved to server/lib/openAiChatStream.js so both paths share one implementation of the reasoning-channel handling, the skip-a-malformed-frame rule, and partial-output-on-abort.

Tuning: what PortOS can actually do with each knob

A throughput number for a GGUF is meaningless without the launch line that produced it — the same model on the same machine streams at wildly different rates depending on micro-batch size, flash attention, and KV-cache quantization. Two readings looked like noise when they were actually two different setups.

server/lib/localModelTuning.js holds the knob catalog, and every knob declares what PortOS can do with it:

  • launch — PortOS puts it on the daemon's command line (llama.cpp: -b, -ub, -t, --flash-attn, --cache-type-k|v, --draft-max)
  • request — sent with each measurement request (Ollama's num_ctx)
  • record — PortOS cannot set it; the user states how the daemon was launched so two readings stay comparable (LM Studio, MTPLX, vLLM)

A record knob changes nothing about the run and the UI says so. What it must never do is claim to have been applied — which is why applies is on the spec rather than implied, why a request knob must also declare the wire field it maps to (both enforced by test), and why tuningApplied is null (not true) when there was nothing settable.

relaunchLlamaServerWithTuning is the launch half. It refuses rather than guesses when nothing is running (no model path to reuse) or when the process was started outside PortOS (stopping it would kill something the user owns). And because a tuning sweep is expected to produce launch lines llama.cpp rejects, a failed tuned launch restores the previous configuration before reporting — that daemon fronts the llama provider for the whole install, so leaving it down would break every later request, not just the measurement.

Store identity: no migration needed

assessmentKey(backend, modelId, tuningKey) returns backend:modelId when the tuning signature is empty and backend:modelId@<signature> otherwise. An untuned run therefore keys byte-identically to the pre-tuning key, so every record already on disk keeps resolving with no migration, while a tuned run lands beside it instead of overwriting it. Re-running the same tuning still supersedes — a stale reading of one configuration is worse than none.

compareTunings reports each variant's throughput as a percentage of the winner. Models measured under only one tuning are omitted: one reading is not a comparison, and presenting it as "the best tuning" would dress a single measurement up as a conclusion.

Models navigation

A top-level Models section at /models/:tab:

  • LLMs (/models/llms) — backends, install catalog, llama.cpp launcher
  • Performance (/models/performance) — measured assessments + tuning comparison
  • Status (/models/status) — what is resident in memory right now
  • Playground (/local-llm/playground) — unchanged path, now renders the section's tab bar so it doesn't strand you outside it

/settings/local-llm redirects to /models/llms, so bookmarks and stale ⌘K history keep working, and Local LLMs leaves the Settings sub-nav. Each tab is a route param, so all four are deep-linkable and reachable from ⌘K and voice (nav.models.performance / nav.models.status added; nav.settings.local-llm keeps its opaque id — it's in palette history — and moves to the Models section).

The llama.cpp launcher gained the same tuning flags, so a configuration can be launched and then measured. An untouched field is stripped from the payload rather than sent as 0 — llama.cpp's own default is not a value PortOS can name.

Test plan

  • server/lib/localModelTuning.test.js (30 cases) — catalog invariants (every knob declares applies; every request knob declares its wire name; only llama.cpp carries launch knobs), normalize/clamp/coerce, signature stability, comparison rules
  • server/services/localModelAssessments.test.js (62 cases) — endpoint-runtime listing and measurement, endpoint resolution from the live llama-server, two tunings coexisting, same-tuning replacement, per-tuning delete, tuningApplied false/null recording, runtime roster's null-vs-0 model count
  • server/services/llamaServerManager.test.js — relaunch refuses when nothing is running / on an empty tuning, puts knobs on the line keeping the served model, restores the previous config when the tuned line exits, and reports not-applied when the relaunch never answers
  • server/lib/openAiChatStream.test.js — SSE frame parsing, malformed-frame skip, reasoning fallback
  • client/src/pages/Models.test.jsx, LocalModelAssessments.test.jsx, LocalLlmTab.test.jsx, Layout.test.jsx — tab routing from the route param, runtime roster (unreachable vs 0 models), tuning payload, warn-not-celebrate on unapplied tuning, /models full-width classification
  • Full suites: server 1541 files / 32,140 tests, client 701 files / 8,866 tests, biome lint clean, client build succeeds

Follow-up

Only LLM model management lives under Models so far. LoRAs, image/video checkpoints, and embeddings are tracked in #4728. Design record: docs/plans/2026-08-21-models-navigation.md.

…v section

Measured Model Assessments could only benchmark Ollama and LM Studio, because
its only measurement path resolved a configured PortOS provider. It now reaches
every local daemon PortOS knows how to talk to — llama.cpp, MTPLX, and vLLM as
well — by talking to their OpenAI-compatible endpoints directly, listing their
models from GET /v1/models rather than from a catalog they don't have.

An assessment also now records the configuration it describes. A throughput
number for a GGUF is meaningless without the launch line that produced it: the
same model on the same machine streams at wildly different rates depending on
the micro-batch size, whether flash attention is on, and how much of the KV
cache is quantized. Each runtime declares its tuning knobs and, crucially, what
PortOS can actually DO with each one — put it on the launch line, send it with
the request, or merely record it so two readings stay comparable. For llama.cpp
PortOS relaunches the server with the requested flags, restoring the previous
configuration if that launch line turns out not to work. Two tunings of one
model are two records, so they rank against each other and the panel reports
which configuration this machine actually prefers.

Model management also stops being a scroll position on a Settings page. Memory
residency, measured assessments, and backend/catalog management now live under
a top-level Models section as Status, Performance, and LLMs, each at its own
URL and reachable from Cmd+K and voice. /settings/local-llm redirects, so
bookmarks and stale palette history keep working.

The llama.cpp launcher gained the same tuning flags (-b, -ub, -t, --flash-attn,
--cache-type-k/-v), so a configuration can be launched and then measured.

Follow-up for the remaining model kinds — LoRAs, image/video checkpoints,
embeddings: #4728. Design record: docs/plans/2026-08-21-models-navigation.md.
The assessments read path runs on every Performance page load, and it called
getLlamaServerStatus purely to learn which port llama-server is on. That call
answers a much bigger question and pays for it with a network probe AND an
execPm2 logs subprocess — whose output this caller then threw away.

getLlamaServerEndpoint reads the same recovered-config path (so a PortOS
restart that left the PM2 process online still resolves the real port rather
than the default) and stops there. Host/port defaulting is shared with the
status call so the two can't drift.
…s evidence it isn't

Four findings, three of them the same shape — a tuning that says one thing while
the record means another.

- rankByIntent dropped tuningKey/tuning/tuningLabel from the ranked projection,
  so every variant of a model collapsed onto the backend-defaults record: two
  rows shared a React key, both rendered "backend defaults", Discard deleted the
  wrong measurement (or 404'd), and Measure-again silently lost the settings it
  was supposed to reproduce. The tuning is the row's identity, so it now travels
  with the row — on excluded rows too, and as the tie-break that keeps two
  variants of one model in a stable order.

- A run whose launch knobs never reached the daemon was still saved under the
  requested tuning key and then ranked and compared as evidence FOR it — the
  throughput of whatever was actually running, credited to knobs nobody applied.
  The record stays on disk (the run cost real minutes, and the reason is what
  explains itself) but it is excluded from the ranking and the comparison, and
  surfaced with what went wrong instead.

- The relaunch treated "hasn't answered yet" as "wedged". startLlamaServer polls
  for four seconds and a large GGUF routinely takes longer to load, so a launch
  that was about to succeed could be reported as failed — and, worse, left
  running while the previous configuration was already gone. It now waits out a
  real readiness budget and, only if the endpoint is still silent, restores the
  previous configuration exactly as a rejected launch line does.

- A vLLM container from the shipped compose stack is started behind
  VLLM_API_KEY and 401s an unauthenticated request, so both the model listing
  and every sample failed on an otherwise-healthy stack. The key is resolved off
  the matching provider record, keyed on the same localRuntimeKind classifier
  providerReadiness already uses so the two can't disagree.

Each fix is pinned by a test that fails without it.
@atomantic
atomantic merged commit e3ed7c1 into main Aug 21, 2026
7 checks passed
@atomantic
atomantic deleted the cos/task-mt2msvsr/agent-8a8920ac branch August 21, 2026 08:55
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.

1 participant