Measure local models across every runtime, and give Models its own nav section - #4729
Merged
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 512worth 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:listModels()— a durable catalog on diskrunLocalLlmTest(provider path, lands in/runs)GET /v1/modelson the live daemonrunEndpointLlmTest— direct, no/runsrecordThat 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.jsso 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.jsholds 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'snum_ctx)record— PortOS cannot set it; the user states how the daemon was launched so two readings stay comparable (LM Studio, MTPLX, vLLM)A
recordknob changes nothing about the run and the UI says so. What it must never do is claim to have been applied — which is whyappliesis on the spec rather than implied, why arequestknob must also declare the wire field it maps to (both enforced by test), and whytuningAppliedisnull(nottrue) when there was nothing settable.relaunchLlamaServerWithTuningis 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 thellamaprovider 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)returnsbackend:modelIdwhen the tuning signature is empty andbackend: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.compareTuningsreports 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:/models/llms) — backends, install catalog, llama.cpp launcher/models/performance) — measured assessments + tuning comparison/models/status) — what is resident in memory right now/local-llm/playground) — unchanged path, now renders the section's tab bar so it doesn't strand you outside it/settings/local-llmredirects to/models/llms, so bookmarks and stale ⌘K history keep working, andLocal LLMsleaves 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.statusadded;nav.settings.local-llmkeeps its opaque id — it's in palette history — and moves to theModelssection).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 declaresapplies; everyrequestknob declares its wire name; only llama.cpp carries launch knobs), normalize/clamp/coerce, signature stability, comparison rulesserver/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,tuningAppliedfalse/nullrecording, runtime roster'snull-vs-0model countserver/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 answersserver/lib/openAiChatStream.test.js— SSE frame parsing, malformed-frame skip, reasoning fallbackclient/src/pages/Models.test.jsx,LocalModelAssessments.test.jsx,LocalLlmTab.test.jsx,Layout.test.jsx— tab routing from the route param, runtime roster (unreachablevs0 models), tuning payload, warn-not-celebrate on unapplied tuning,/modelsfull-width classificationFollow-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.