Skip to content

feat: discover compatible Hugging Face embedding models - #61

Merged
sjvans merged 6 commits into
AISQLiteServicefrom
feat/huggingface-model-discovery
Aug 28, 2026
Merged

feat: discover compatible Hugging Face embedding models#61
sjvans merged 6 commits into
AISQLiteServicefrom
feat/huggingface-model-discovery

Conversation

@sjvans

@sjvans sjvans commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • replace handwritten Hugging Face API calls with the official @huggingface/hub client
  • discover conventional, root-level, nested, and uniquely named ONNX exports
  • prefer tokenizer/configuration metadata adjacent to nested ONNX exports, with repository-root fallback
  • derive dimensions and input limits from common Transformers configuration aliases
  • preserve Sentence Transformers pooling/normalization semantics and reject explicitly incompatible Hub tasks
  • load and probe the staged model with ONNX Runtime before publishing an installation
  • support public Hugging Face-compatible Hub URLs and models with a subset of the standard encoder inputs
  • expose @huggingface/hub as a lazy-loaded optional peer dependency
  • add npx @cap-js/ai check-model <model> for a metadata-only, cache-free compatibility pre-check

Review hardening

  • bound each Hub operation with a 30-second timeout and retry transient network failures plus HTTP 408, 429, and 5xx responses
  • exercise the installed @huggingface/hub library against a fake HTTP transport, including normalized .task, file/LFS metadata, and downloads
  • use the official client's normalized .task field; missing task metadata is explicitly allowed, while declared non-embedding tasks are rejected
  • remove HF_TOKEN/Bearer handling and the obsolete origin alias; discovery currently supports public repositories only
  • document and test that only conventional adjacent ONNX external-data sidecars are supported
  • retain the trust-on-first-use wording from feat: add AI-enabled SQLite service #53: pinned revisions and checksums detect later drift/corruption but do not authenticate a publisher

Validation

  • npm test — 102 tests passed
  • npm run lint
  • Prettier check passed
  • git diff --check

The local checkout does not provide the separate format-cds executable, but no CDS source files are changed by this PR.

Larger architectural follow-ups are tracked together in #62. ONNX Runtime version compatibility remains tracked separately in #54.

@sjvans sjvans mentioned this pull request Aug 27, 2026
1 task
@sjvans
sjvans force-pushed the feat/huggingface-model-discovery branch from a8367b6 to 0f5d650 Compare August 28, 2026 10:05
@sjvans
sjvans marked this pull request as ready for review August 28, 2026 10:10
@sjvans
sjvans requested review from a team as code owners August 28, 2026 10:10
@sjvans
sjvans merged commit 549df5b into AISQLiteService Aug 28, 2026
1 check passed
@sjvans
sjvans deleted the feat/huggingface-model-discovery branch August 28, 2026 10:11
@hyperspace-pr-bot

Copy link
Copy Markdown
Contributor

Summary

The following content is AI-generated and provides a summary of the pull request:


feat: Discover compatible Hugging Face embedding models via official @huggingface/hub client

Summary

This PR replaces hand-written Hugging Face API calls in the model discovery layer with the official @huggingface/hub client, adds a new check-model CLI command for metadata-only pre-checks, and hardens the overall discovery and download pipeline.

What Changed

New Feature: npx @cap-js/ai check-model <model>

A new CLI command that reads repository, configuration, and tokenizer metadata from the Hugging Face Hub without downloading ONNX artifacts or writing to the model cache. Its output is a likely-compatibility report; install-model remains the definitive validation since it also loads and probes the model with ONNX Runtime.

@huggingface/hub as a lazy-loaded optional peer

  • A new huggingface-hub.js module wraps the official client, exposing getModelInfo, getFiles, and getFile
  • Exposed as an optional peer dependency; a missing package produces an actionable installation error
  • Each Hub operation is bounded by a 30-second timeout and retries on transient failures (HTTP 408, 429, 5xx, and transport errors)

Layout-aware model discovery (model-discovery.js)

  • Prefers onnx/model.onnx, then model.onnx, a uniquely named nested model.onnx, or a sole .onnx file
  • For nested models, prefers tokenizer/config files adjacent to the ONNX export with a repository-root fallback
  • Recognizes common Transformers dimension aliases: hidden_size, n_embd, d_model, dim
  • Recognizes additional input-length aliases: n_positions, n_ctx
  • Rejects repositories explicitly tagged for incompatible tasks (e.g., text-generation, fill-mask)
  • External ONNX tensor data supported only for conventional adjacent sidecars

Two-phase discovery: checkModel + discoverModel

discoverModelMetadata is now shared; checkModel stops after metadata, discoverModel continues to fetch file integrity (size + SHA-256).

embedding.js / model-utils.js

  • createFeeds now filters inputs against the session's actual inputNames, enabling models that don't expose all three standard inputs
  • processEmbedding and validateSession exported for unit testing
  • downloadModelIfNeeded respects a configurable hubUrl (trailing slashes normalized)

Tests

  • New tests/huggingface-hub.test.js exercises the Hub adapter against a fake HTTP transport
  • tests/model-discovery.test.js migrated to a typed createHub / hubFor test double replacing raw fetch mocks
  • New unit tests for validateSession, processEmbedding, and custom Hub URL behavior — 102 tests pass

Category

🆕 New Feature / ♻️ Refactor

Related: #53, #54, #62

Have you...

  • Added relevant entry to the change log?

  • 🔄 Regenerate and Update Summary
  • ✏️ Insert as PR Description (deletes this comment)
  • 🗑️ Delete comment
PR Bot Information

Version: 1.29.54

@hyperspace-pr-bot hyperspace-pr-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pull request is well-structured and the overall logic is sound, with good test coverage for the new check-model command, the layout-aware ONNX discovery, and the Hub client retry/timeout behaviour. The four comments above flag a null-dereference in discoverFileIntegrity when a file has no metadata and is not a known JSON file, a misleading error message in selectOnnxModel when multiple nested model.onnx exports are present, a potential TypeError in formatModelCheck if the model file entry is absent, and an eager optional-peer import at client-construction time rather than at first use.

PR Bot Information

Version: 1.29.54

  • Correlation ID: bca38cf0-a2c8-11f1-869c-5c31b4f61c80
  • LLM: anthropic--claude-4.6-sonnet
  • Event Trigger: pull_request.ready_for_review
  • File Content Strategy: Full file content

retries: options.requestRetries ?? HUB_REQUEST_RETRIES,
retryDelayMs: options.requestRetryMs ?? HUB_RETRY_DELAY_MS
};
const loadedBindings = resolveBindings(bindings);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: resolveBindings(bindings) is called eagerly at client-construction time. When bindings is absent or incomplete, this immediately starts a dynamic import('@huggingface/hub'), even if no Hub operation is ever performed (e.g. a caller that only uses checkModel with a pre-built hubClient). The resulting Promise is stored and re-awaited on each call, so correctness is preserved, but the eager import side-effects (including the friendly error for the missing peer) happen at construction rather than at first use. Consider calling resolveBindings lazily inside each method, or memoising it only on the first actual call.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

Comment on lines +200 to 205
if (conventional.length === 1) return conventional[0];
if (conventional.length > 1 || paths.length > 1) {
throw new Error(
`Hugging Face model '${repository}' contains ambiguous ONNX exports: ${paths.join(', ')}`
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: When conventional.length > 1 the error message interpolates the full paths array (all .onnx files), but the ambiguity is among the conventional entries (multiple nested model.onnx files). Listing paths instead of conventional will include unrelated non-model.onnx files and misrepresent the conflict. Consider using conventional in the error message for that branch.

Suggested change
if (conventional.length === 1) return conventional[0];
if (conventional.length > 1 || paths.length > 1) {
throw new Error(
`Hugging Face model '${repository}' contains ambiguous ONNX exports: ${paths.join(', ')}`
);
}
if (conventional.length === 1) return conventional[0];
if (conventional.length > 1) {
throw new Error(
`Hugging Face model '${repository}' contains ambiguous ONNX exports: ${conventional.join(', ')}`
);
}
if (paths.length > 1) {
throw new Error(
`Hugging Face model '${repository}' contains ambiguous ONNX exports: ${paths.join(', ')}`
);
}

Double-check suggestion before committing. Edit this comment for amendments.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

}

const file = knownFile ?? (await fetchFile(context, repository, revision, sibling.rfilename));
const downloaded = knownFile ?? (await fetchFile(context, repository, revision, file.path));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: file is sourced from filesByPath.get(entry.path), which returns undefined for files not present in the Hub file listing (e.g. JSON files that were only fetched directly). When metadataChecksum and metadataSize are both absent and file is undefined, the fallback file.path on the next line throws a TypeError: Cannot read properties of undefined instead of a useful error. The knownFile parameter covers JSON files, so this only affects non-JSON files without LFS metadata, but the crash is still confusing. Should guard against undefined or ensure the caller always passes a valid file object.

Suggested change
const downloaded = knownFile ?? (await fetchFile(context, repository, revision, file.path));
if (!file?.path && !knownFile) {
throw new Error(`Cannot derive integrity for a file with no metadata or known content`);
}
const downloaded = knownFile ?? (await fetchFile(context, repository, revision, file.path));

Double-check suggestion before committing. Edit this comment for amendments.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

Comment on lines +99 to +103
const modelFile = model.files.find(({ role }) => role === 'model');
return `Likely compatible: ${model.repository}
Revision: ${model.revision}
Task: ${model.task ?? 'not declared'}
ONNX: ${modelFile.path}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: modelFile will be undefined if model.files contains no entry with role === 'model', causing modelFile.path to throw a TypeError. While validateModelCandidate in checkModel validates the descriptor shape, the task field causes the shape to differ from what validateModelDescriptor expects. Should guard against a missing model file.

Suggested change
const modelFile = model.files.find(({ role }) => role === 'model');
return `Likely compatible: ${model.repository}
Revision: ${model.revision}
Task: ${model.task ?? 'not declared'}
ONNX: ${modelFile.path}
const modelFile = model.files.find(({ role }) => role === 'model');
if (!modelFile) throw new Error(`check-model result for '${model.repository}' has no model file`);
return `Likely compatible: ${model.repository}
Revision: ${model.revision}
Task: ${model.task ?? 'not declared'}
ONNX: ${modelFile.path}

Double-check suggestion before committing. Edit this comment for amendments.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

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.

1 participant