Skip to content

[Feat]: Pluggable model sources — unified search across HF, OCI, and remote registries #648

@a-ghorbani

Description

@a-ghorbani

Problem

Today, model discovery is tightly coupled to specific backends:

  • HuggingFace: HFStore handles search, pagination, and file details via api/hf.ts
  • Preset models: Hardcoded in defaultModels.ts (~440 lines, MODEL_LIST_VERSION bumped per release)
  • Remote servers: ServerStore fetches /v1/models from OpenAI-compatible endpoints
  • Local files: User picks a .gguf file from device storage

There's no common interface between these. Adding a new source (OCI registries, custom catalogs, etc.) means writing another bespoke store, another UI sheet, and more origin-specific branches in ModelStore.

PR #458 (OCI support) illustrates the problem: it adds ~700 lines of hardcoded ModelOrigin.OCI entries to defaultModels.ts with size: 0, params: 0, and guessed chat templates — metadata that should come from the registry dynamically.

Proposal: ModelSource interface

Abstract model discovery behind a common interface. Each source implements search, metadata fetching, and Model conversion:

interface ModelSourceSearchResult {
  id: string;                    // source-specific unique ID
  name: string;                  // display name
  author?: string;
  description?: string;
  size?: number;                 // bytes (0 if unknown until file selection)
  params?: number;
  tags?: string[];               // e.g. ['gguf', 'vision', 'code']
  variants?: ModelSourceVariant[]; // quant variants / tags
}

interface ModelSourceVariant {
  id: string;          // e.g. filename for HF, tag for OCI
  label: string;       // e.g. "Q4_K_M" or "latest"
  size?: number;
}

interface ModelSource {
  readonly id: string;           // 'hf', 'oci', 'preset', etc.
  readonly displayName: string;  // 'Hugging Face', 'Docker Hub', etc.
  readonly icon?: string;

  // Discovery
  search(query: string, options?: SearchOptions): Promise<ModelSourceSearchResult[]>;
  fetchMore?(): Promise<ModelSourceSearchResult[]>;  // pagination
  hasMore?: boolean;

  // Detail / variant selection
  fetchDetails(resultId: string): Promise<ModelSourceDetails>;

  // Conversion — turns a user selection into a Model object
  toModel(result: ModelSourceSearchResult, variant: ModelSourceVariant): Model;

  // Download URL resolution (may need async for OCI manifest fetching, HF auth, etc.)
  resolveDownloadUrl(model: Model): Promise<{ url: string; headers?: Record<string, string>; size?: number }>;
}

How existing sources map

Source search() fetchDetails() resolveDownloadUrl()
HF GET /api/models?search=...&filter=gguf fetchGGUFSpecs + fetchModelFilesDetails Direct HF CDN URL (already known)
OCI Docker Hub API: GET /v2/repositories/ai/?name=... Fetch OCI manifest → extract layer annotations (size, filename) Authenticate → manifest → blob URL
Preset Filter in-memory list No-op (already complete) Direct HF CDN URL (already in model def)
Remote Fetch /v1/models from server No-op (no download) N/A (streamed from server)

OCI-specific details

Docker Hub exposes a search API:

GET https://hub.docker.com/v2/repositories/ai/?page_size=25&name=gemma

This returns repo names, descriptions, star counts, last updated dates. Combined with the OCI manifest (which has layer annotations for .gguf filenames and sizes), this gives full dynamic discovery — no hardcoded list needed.

For non-Docker-Hub OCI registries, the OCI Distribution Spec provides:

  • GET /v2/_catalog — list repositories
  • GET /v2/{name}/tags/list — list tags
  • GET /v2/{name}/manifests/{reference} — get manifest with layer metadata

Chat template resolution

The hardcoded list in PR #458 guesses chat templates. Better approach:

  1. GGUF metadata: After download, llama.cpp reads the GGUF header which contains tokenizer.chat_template. PocketPal already does this via loadMissingGGUFMetadata().
  2. Pre-download hint: OCI manifest annotations or Docker Hub description could include model family. Map family → default template as a best-effort hint (same as getHFDefaultSettings does for HF models).
  3. Fallback: Use a generic chat template until GGUF metadata is loaded post-download.

UI changes

Unified search (future)

Replace the current FAB → "Search HuggingFace" flow with a source-tabbed or source-filtered search:

[Search models...]
[HuggingFace ▾] [Docker Hub] [All Sources]

Results:
  gemma-3-4b-it (HuggingFace · google · 2.5GB)
  gemma3 (Docker Hub · ai · tag: latest)
  ...

Minimal first step

Keep the current HF search sheet but add an "OCI" tab or toggle. The ModelSource interface means both tabs use the same result → detail → download flow.

Migration path

  1. Phase 1: Extract ModelSource interface. Refactor HFStore to implement it (or wrap it). Ship OCI source with Docker Hub search. Remove hardcoded OCI models from defaultModels.ts.
  2. Phase 2: Unified search UI with source selector. Preset models become a "Recommended" source that's just a curated in-memory list.
  3. Phase 3: User-configurable registries (add your own OCI endpoint, private HF orgs, etc.).

Relationship to PR #458

PR #458's OCIRegistryClient is a reasonable low-level building block for the OCI ModelSource implementation. The hardcoded model list and ModelOrigin.OCI additions should be replaced with dynamic discovery.

Open questions

  • Should the remote catalog (recommended models) be fetched from a GitHub-hosted JSON instead of compiled into the app? This would allow updating recommendations without app releases.
  • Should we support private OCI registries from the start, or only public Docker Hub initially?
  • How should we handle models that exist on both HF and Docker Hub? Deduplicate, or show both?

🤖 Generated by PocketPal Dev Team

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions