Skip to content

Model hub: resolve unlisted Ollama models by exact name - #56

Open
shaun-ws-yap wants to merge 1 commit into
NVIDIA:developfrom
shaun-ws-yap:ollama-hub-exact-name-lookup
Open

shaun-ws-yap wants to merge 1 commit into
NVIDIA:developfrom
shaun-ws-yap:ollama-hub-exact-name-lookup

Conversation

@shaun-ws-yap

Copy link
Copy Markdown

Description

Add model browses a catalog PAIR ships with the application —
desktop/src/electron/model-hub/ollama-models.json, a committed snapshot
regenerated by hand. Anything Ollama publishes after that snapshot is invisible
in the hub even though the engine can pull it, because pull_model forwards
whatever name it is given and never consulted the catalog. The snapshot on
main is dated 2026-07-21 and holds 232 base models; ollama.com/library
currently lists 239.

This makes a search that matches nothing fall back to asking Ollama's registry
about that one exact name, and shows the answer as an ordinary result row.
Selecting it downloads it through the existing path.

registry.ollama.ai speaks the same OCI distribution API ollama pull uses, so
this asks the source the engine would ask, rather than scraping rendered HTML —
the fragility this project deliberately removed when it froze the catalog. The
registry answers 404 for /tags/list, so a catalog still cannot be enumerated
from it; browsing continues to come from the snapshot, and this is an exact-name
lookup rather than a search.

Refs #21, which reports the symptom (qwen3.8 not appearing) and names the same
model this was verified against.

User-visible change for the release notes: searching the Ollama model hub for
a model published after your PAIR release now finds it, instead of reporting
"No models found".

Scope

In scope

  • A new engine:lookup-hub-model channel, mirroring the existing
    engine:search-hub, dispatching per engine.
  • ollama-registry.ts: reads one manifest for the download size, then the config
    blob it points at for the model's own parameter size, quantization and format.
    Bounded cache holding negative results, AbortSignal timeout, model reference
    validated in main.
  • The renderer fires the lookup only once the local list is known to be empty,
    debounced by 400 ms, with a generation guard against races.
  • ModelEntry.updatedAt becomes optional. A manifest carries no publication date
    and the registry sends no last-modified, so a resolved row omits its age
    rather than claiming it was updated just now. The lastModified comparator
    sorts an unknown date oldest instead of returning NaN.

Deliberately out of scope

  • Listing a model's other tags. Not possible without scraping ollama.com
    HTML at runtime. Left alone rather than reintroducing that.
  • LM Studio. Its catalog is fetched live on a six-hour TTL, so it never falls
    behind a release and has no gap to close.
  • References to other registries, such as hf.co/<user>/<repo>.
    registry.ollama.ai cannot answer for them, and asking would report a real
    model as missing.
  • Refreshing or repairing the snapshot. Separate change; see Risk.

Validation

macOS 26.6, Apple silicon, Node 26.8.1, Ollama running locally.

$ npm run lint
✖ 1 problem (0 errors, 1 warning)
# the one warning is pre-existing, in src/electron/service-bridge/node-info-poller.ts:289,
# and is untouched by this change. Clean on every file this PR modifies.

$ npm run typecheck
> tsc --noEmit -p tsconfig.node.json --composite false
> tsc --noEmit -p tsconfig.web.json --composite false
> tsc --noEmit -p tsconfig.test.json
# no errors

$ npm run test:unit
 Test Files  39 passed (39)
      Tests  228 passed (228)

$ npm run dead-code:check
  0 unused files | 0 unused exports | 0 unused types | 0 unused deps | 0 unused devDeps | 0 strict-tsc findings
✅ check passed: no dead code.

$ npm run service-contracts:check
✅ check passed: doc fresh, no hardcoded versions, no unignored drift.

$ node scripts/spdx-headers.mjs
SPDX headers: 875 checked, 0 missing, 0 to review, 0 unclassified, 94 skipped

New coverage:

  • tests/modular/ollama-registry-lookup.test.ts — size accumulation, config
    enrichment and its fallback when a config omits model_type, vision detection,
    namespace and tag defaulting, 404 and offline handling, caching, and
    URL-safety of the parsed reference.
  • tests/modular/model-hub-lookup.test.ts — which queries are worth a request,
    including cloud-tag and non-reference rejection and matcher linearity.

Manual, against the live registry:

Query Result
qwen3.8 resolves — 16.5 GB, 27.3B, vision
gemma4 resolves — 9.0 GB, 8.0B
qwen3.5:4b already in the snapshot; no lookup fires
qwen3-vl:235b-cloud declined, cloud tags cannot run locally
a model already installed no row, the hub hides installed models
offline no row, no error surfaced

Risk

  • New outbound host. registry.ollama.ai, contacted from the main process
    only, only for a name the user typed, and only after a local search returned
    nothing. No credentials and no telemetry. This is the part worth an explicit
    decision: PAIR is local-first and documents no outbound hosts today.
    CONTRIBUTING.md asks for maintainer alignment on a new model source, which is
    why this is a draft — happy to change the approach or drop it.
  • Renderer is not trusted. A lookup arrives over IPC, so parseReference
    validates every segment and the tag in main against a character class. Without
    it foo/.. walks out of the namespace and foo/bar#z truncates the request
    path. Covered by a test.
  • Sizes read lower than ollama.com on resolved rows. This PR stores the
    manifest's true byte count. formatBytes divides by 1024³ while labelling the
    result "GB", so a 17.74 GB model renders as 16.52 GB. Existing rows agree with
    ollama.com only because the scraper parses its decimal "4.9GB" strings as
    binary, inflating every stored size by ~6.9%; two errors that cancel. I have
    not touched either here — correcting the formatter requires correcting the
    snapshot in the same change, and that is its own PR. Happy to open it.
  • details.family carries a capability, not a model family. Pre-existing:
    the scraper puts the capability chip there. Resolved rows follow that
    convention so the chip matches sibling rows, which means this PR propagates the
    misnomer rather than fixing it.
  • Failure modes are quiet by design. A lookup is speculative, so every
    failure resolves to "no row" rather than an error banner for a request the user
    never made. A console.warn keeps a broken channel visible in DevTools.
  • No compatibility or migration impact. Additive channel, no persisted data,
    no change to pull_model, no service binary touched — so no
    services/versions.json bump applies.

Checklist

  • I have read the Contributing Guidelines.
  • Every commit is signed off (git commit -s), certifying the Developer Certificate of Origin.
  • New or existing tests cover the change.
  • Relevant documentation is updated. — not yet. docs/getting-started.mdx
    should describe this before the draft is marked ready; holding until the
    approach is settled, since the wording depends on it.
  • I checked the diff, changed filenames, and commit messages for credentials, private data, internal URLs, internal issue identifiers, and generated artifacts.
  • I recorded the validation commands and results above.
  • I bumped any affected component in services/versions.json, and described user-visible changes above so they reach the release notes. — no bump applies; desktop-only, no service binary output changed.

The Ollama model hub browses a committed snapshot
(desktop/src/electron/model-hub/ollama-models.json) regenerated by hand, so a
model published after that snapshot is invisible in the hub even though the
engine can pull it. The pull path never had this limit: pull_model forwards
whatever name it is given and never consulted the catalog, so the snapshot
gated discovery alone.

When a search matches nothing locally and the query is a well-formed model
reference, resolve that one exact name against registry.ollama.ai and fold the
answer into the list as an ordinary result row. The registry speaks the same
OCI distribution API ollama pull uses, so this asks the source the engine
would ask rather than scraping rendered HTML. It answers 404 for /tags/list,
so browsing still comes from the snapshot and this is an exact-name lookup
rather than a search.

A manifest carries no publication date and the registry sends no
last-modified, so ModelEntry.updatedAt becomes optional and a resolved row
omits its age rather than claiming it was updated just now.

Lookups arrive over IPC, so the reference is validated in the main process
rather than trusting the renderer's own check: without it "foo/.." walks out
of the namespace and "foo/bar#z" truncates the request path.

Refs NVIDIA#21

Signed-off-by: Shaun Y <me@shaunyap.ca>
@shaun-ws-yap
shaun-ws-yap force-pushed the ollama-hub-exact-name-lookup branch from 6c906f9 to 3fefdb1 Compare September 9, 2026 21:25
@shaun-ws-yap
shaun-ws-yap marked this pull request as ready for review September 9, 2026 21:27
@shaun-ws-yap shaun-ws-yap mentioned this pull request Sep 9, 2026
2 tasks
@shaun-ws-yap
shaun-ws-yap changed the base branch from main to develop September 9, 2026 21:30
@Noah-Tervalon-Nvidia

Copy link
Copy Markdown
Collaborator

@ckelseynv to have a look. We used to scrape the website and don't really want to go back to that. I haven't looked at the code here, so not sure on implementation, but Chris will be able to add context and review this! We do want to find a way moving forward of making sure that we don't get stuck with old model lists.

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