feat(studio): local-server discovery + model picker for every provider - #167
Open
Yona-Appletree wants to merge 12 commits into
Open
feat(studio): local-server discovery + model picker for every provider#167Yona-Appletree wants to merge 12 commits into
Yona-Appletree wants to merge 12 commits into
Conversation
Setting up a local model against lightplayer.app dead-ends in ways a
browser reports identically: a server answering without CORS headers and
a port with nothing on it both surface as the same opaque TypeError. Two
features close that gap, plus model discovery for all four providers.
Test connection / Find my server (Custom provider)
- Every failed probe is retried in `no-cors` mode. An opaque response
resolves whenever the connection was accepted, so opaque-success after
a normal failure means CORS, and both-fail means nothing is there.
- Diagnoses carry the fix: the exact `OLLAMA_ORIGINS=<this origin>`
command, LM Studio's CORS toggle, vLLM's --allowed-origins, plus 401
(needs a key), 404 (base URL likely missing /v1), empty list (pull a
model), and a served list that lacks the configured id.
- Base URLs normalize (scheme + /v1), and a dead `localhost` is retried
at 127.0.0.1 — Ollama binds IPv4 only, so name resolution to ::1 is a
silent miss.
- An opaque probe proves a listener, not a product, so the copy never
claims "Ollama is running". Port 5000 is not scanned: macOS
ControlCenter owns it and would hit on every Mac.
Model picker (all providers)
- One `{"data":[{"id"}]}` parser; only the request differs per provider.
Anthropic needs the same anthropic-dangerous-direct-browser-access
header the agent sends; OpenRouter's list is public, so it browses
before Connect — which is exactly when a model id is needed.
- Long lists get a filter box, rows show published $/MTok, and embedding
/speech/image ids are filtered with the count shown (the field still
accepts anything typed).
- Picking a model also sets the cost-estimate rates from its published
pricing, and clears them when the provider publishes none — leftover
rates would silently mis-price the newly chosen model.
Decisions and copy live in lpa-studio-core (30 host tests); the fetches
are wasm-only glue. Credentials come from settings_io rather than the
view DTO, which deliberately carries only a masked key.
Verified live: Anthropic's real API, and OpenRouter's real 341-model /
533 KB payload replayed through a local CORS server (all 11 filtered ids
are genuinely image/audio). That live pass caught three bugs unit tests
could not: a timed-out body read reported as "not JSON", a stale
provider's list surviving a provider switch, and a filter query
outliving its own input box.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Captured in the pinned CI environment by run: https://github.com/light-player/lightplayer/actions/runs/30342573973
Contributor
CI refreshed the story baselines on this branchThe Review every PNG in the PR's Files changed view (swipe / onion-skin). 10 more file(s)
|
…scovery-test-eda2eb # Conflicts: # lp-app/lpa-studio-web/story-images/studio__layout__studio-settings-popover__overview__lg.png # lp-app/lpa-studio-web/story-images/studio__layout__studio-settings-popover__overview__md.png # lp-app/lpa-studio-web/story-images/studio__layout__studio-settings-popover__overview__sm.png # lp-app/lpa-studio-web/story-images/studio__node__agent-chat__overview__lg.png # lp-app/lpa-studio-web/story-images/studio__node__agent-chat__overview__md.png # lp-app/lpa-studio-web/story-images/studio__node__agent-chat__overview__sm.png
The auto-commit path only fires on a push-triggered run, so a branch whose baselines are stale with nothing new to push — after resolving PNG conflicts by hand, most notably — had no way to ask CI for a recapture short of an empty commit. `CI` now accepts `workflow_dispatch`. The three validate jobs skip themselves on that event (the branch's code was already validated by the run that reviewed it), leaving `validate-stories` alone: ~10 minutes, and it commits any refresh to the selected branch exactly as the PR path does. `main` is excluded from dispatch auto-commit so a mis-aimed run cannot push baselines straight to it, and the sticky PR comment stays PR-only since a dispatch has no PR number. GitHub only offers the Run-workflow button for workflows on the default branch, so this becomes usable once it lands on main. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Alphabetical order made OpenRouter's ~340-model list useless: `ai21/…` and `aion-labs/…` led, and every model worth choosing was buried. There is no popularity signal to sort by. `/api/v1/models` carries none, `?order=top-weekly` is accepted and ignored (byte-identical response), and the endpoint openrouter.ai's own rankings page uses is CORS-locked to their origin, so a browser page cannot read it. The payload does carry quality, though: `benchmarks.artificial_analysis` publishes coding / agentic / intelligence indices (117 of 341 models), and `supported_parameters` says whether a model can call tools at all. Both come from the provider, so nothing here is a list we maintain. - Order: published coding score first (falling back to agentic, then intelligence), then newest by `created`, then id. The head of the OpenRouter list becomes claude-opus-5, gpt-5.6-sol, gpt-5.6-terra, claude-fable-5, kimi-k3 — the models someone would actually pick. - Hide models the provider marks tool-incapable: the agent is a single-tool loop, so those fail on turn one. 66 of 341 on OpenRouter, counted in the hidden line beside the embedding/speech ids. Silence about tool support is not a no — only OpenRouter publishes the field. - Rows show the score, and a legend names the ordering, so a model's position is explained rather than mysterious. Providers that publish neither (Anthropic, OpenAI, local servers) fall through to id order exactly as before. Verified against the real 341-model payload replayed through a local CORS server: 272 offered, 69 hidden, top rows as above. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Captured in the pinned CI environment by run: https://github.com/light-player/lightplayer/actions/runs/30382618633
#158 landed model discovery in parallel with this branch: a `/models` listing in lpa-agent behind an `HttpGetTransport` seam, typed `ListModelsError`, per-provider fetch state keyed by a credential fingerprint, and a `<select>` in the settings popover. That is the better home for it — this branch's version fetched from the web edge and never reached the store — so the duplicate is deleted rather than merged, and its four differences are ported onto main's pipeline instead. Deleted: `settings/model_catalog.rs` (core) and `model_catalog.rs` (web). Ported into the surviving implementation: - `data: null` is an empty listing, not a parse failure. Ollama with nothing pulled answers exactly that, and serde's `default` covers a MISSING field, not a null one — so the commonest local-server state read as "model list unavailable". `data` is now required-but-nullable, which also keeps the useful opposite signal: a body with no `data` at all (Ollama's native `{"models":[…]}`) means the base URL is missing its `/v1`, and the probe still says so. - `ModelInfo` carries the metadata OpenRouter publishes — per-token pricing, the Artificial Analysis indices, tool-calling support, `created` — so the listing can be ranked and priced without a maintained list. - `model_options` ranks by published coding score, then recency, then id, and drops models the provider marks tool-incapable (the agent is a single-tool loop) alongside the embedding/speech ids. Listings with no scores keep the provider's own order. - Picking a model adopts its published rates, and clears them when the listing prices nothing — now in the store, so it covers the dropdown and hand-typed ids alike. The local-server probe (Test connection / Find my server) is unchanged apart from reading bodies through the shared discovery parser, so a diagnosis and the dropdown can never disagree about what a body means. Baseline PNGs and tailwind.css take main's bytes; CI recaptures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…thub.com/light-player/lightplayer into claude/local-model-discovery-test-eda2eb
Those stories captured this branch's own model picker, which the merge of #158 replaced with main's dropdown. Their baselines have no story to compare against; CI recaptures everything else.
Captured in the pinned CI environment by run: https://github.com/light-player/lightplayer/actions/runs/30385620403
# Conflicts: # .github/workflows/pre-merge.yml
…dule Merging #158 moved the last non-test use of `UiModelOption` out of settings_store, leaving the top-level import used only by the assertions in `mod tests` — which reach it through `use super::*` and so kept compiling under `cargo test` while failing `-D warnings` on the lib. That is what turned Validate (x64) red on 2026-07-28. Import it where it is actually used instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
















Setting up a local model against lightplayer.app dead-ends in ways a browser reports identically: a server answering without CORS headers and a port with nothing listening both surface as the same opaque
TypeError. This adds the two affordances that close that gap, plus model discovery for all four providers.Test connection / Find my server (Custom provider)
Two buttons under the Base URL field. Test probes the address in the field; Find my server tries Ollama :11434, LM Studio :1234, llama.cpp :8080, vLLM :8000 and Jan :1337 concurrently.
The load-bearing trick: every failed probe is retried in
no-corsmode. An opaque response resolves whenever the connection was accepted, so opaque success after a normal failure means CORS, and both failing means nothing is there. Diagnoses carry the remedy — the exactOLLAMA_ORIGINS=<this page's origin> ollama serveline, LM Studio's CORS toggle, vLLM's--allowed-origins— plus 401 (wants a key), 404 (base URL likely missing/v1), an empty list (pull a model first), and a served list that lacks the configured model id.Smaller things that each fix a real dead end:
/v1), solocalhost:11434— the address Ollama prints — works.localhostis retried at127.0.0.1: Ollama binds IPv4 only, so a name resolving to::1is a silent miss.Model picker (all four providers)
The Model field gained Browse models ▾. One
{"data":[{"id":…}]}parser covers every provider; only the request differs. Anthropic needs the sameanthropic-dangerous-direct-browser-accessheader the agent already sends; OpenRouter's list is public, so it browses before Connect — exactly when a model id is needed; Custom reuses the probe's URL.$in/$outper MTok; embedding/speech/image ids are filtered with the count shown, and the field still accepts anything typed.Shape
Decisions and copy live in
lpa-studio-core(local_model_probe.rs,model_catalog.rs; 30 host tests); the fetches are wasm-only glue inlpa-studio-web. Credentials are read throughsettings_iorather than the view DTO, which deliberately carries only a masked key.Validation
just checkandjust testgreen locally. 7 new stories cover the discovery and picker states — their baselines are CI's to capture.Verified live in a browser, not just against fixtures:
{"data":null}when nothing is pulled) plus stand-in servers with and without CORS headers.That live pass caught three bugs unit tests could not, all fixed here: a timed-out body read reported as "the response was not JSON", a stale provider's model list surviving a provider switch, and a filter query outliving its own input box.
🤖 Generated with Claude Code