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:
- GGUF metadata: After download,
llama.cpp reads the GGUF header which contains tokenizer.chat_template. PocketPal already does this via loadMissingGGUFMetadata().
- 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).
- 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
- 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.
- Phase 2: Unified search UI with source selector. Preset models become a "Recommended" source that's just a curated in-memory list.
- 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
Problem
Today, model discovery is tightly coupled to specific backends:
HFStorehandles search, pagination, and file details viaapi/hf.tsdefaultModels.ts(~440 lines,MODEL_LIST_VERSIONbumped per release)ServerStorefetches/v1/modelsfrom OpenAI-compatible endpoints.gguffile from device storageThere'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.OCIentries todefaultModels.tswithsize: 0,params: 0, and guessed chat templates — metadata that should come from the registry dynamically.Proposal:
ModelSourceinterfaceAbstract model discovery behind a common interface. Each source implements search, metadata fetching, and Model conversion:
How existing sources map
search()fetchDetails()resolveDownloadUrl()GET /api/models?search=...&filter=gguffetchGGUFSpecs+fetchModelFilesDetailsGET /v2/repositories/ai/?name=.../v1/modelsfrom serverOCI-specific details
Docker Hub exposes a search API:
This returns repo names, descriptions, star counts, last updated dates. Combined with the OCI manifest (which has layer annotations for
.gguffilenames 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 repositoriesGET /v2/{name}/tags/list— list tagsGET /v2/{name}/manifests/{reference}— get manifest with layer metadataChat template resolution
The hardcoded list in PR #458 guesses chat templates. Better approach:
llama.cppreads the GGUF header which containstokenizer.chat_template. PocketPal already does this vialoadMissingGGUFMetadata().getHFDefaultSettingsdoes for HF models).UI changes
Unified search (future)
Replace the current FAB → "Search HuggingFace" flow with a source-tabbed or source-filtered search:
Minimal first step
Keep the current HF search sheet but add an "OCI" tab or toggle. The
ModelSourceinterface means both tabs use the same result → detail → download flow.Migration path
ModelSourceinterface. RefactorHFStoreto implement it (or wrap it). Ship OCI source with Docker Hub search. Remove hardcoded OCI models fromdefaultModels.ts.Relationship to PR #458
PR #458's
OCIRegistryClientis a reasonable low-level building block for the OCIModelSourceimplementation. The hardcoded model list andModelOrigin.OCIadditions should be replaced with dynamic discovery.Open questions
🤖 Generated by PocketPal Dev Team