Skip to content

feat: add NGC model provider to support NGC Downloads (#222) - #232

Merged
sunil1511 merged 4 commits into
mainfrom
sunil1511/add-ngc-provider
Apr 15, 2026
Merged

sunil1511 merged 4 commits into
mainfrom
sunil1511/add-ngc-provider

Conversation

@sunil1511

@sunil1511 sunil1511 commented Apr 13, 2026 •

Copy link
Copy Markdown
Contributor

Implement NgcProvider and NgcProviderCache to download models from NVIDIA NGC alongside the existing HuggingFace provider.

  • Parse ngc:// URIs and org/[team/][type/]name/version path formats
  • Fetch scoped OAuth2 Bearer token via authn.nvidia.com before each download
  • Download via NGC V2 API for team artifacts (Bearer-authenticated per-file URLs) and V1 presigned S3 URLs for org-level artifacts
  • Fall back to checksums.blake3 manifest enumeration when bulk file listing returns 400 (orgs with access controls)
  • Resolve NGC API key from NGC_API_KEY, NGC_CLI_API_KEY, or ~/.ngc/config
  • Atomic download via .tmp + rename to avoid partial files on interrupted downloads
  • list_models emits round-trip-safe names: 3-segment for org-level artifacts, 5-segment for team artifacts
  • Wire NgcProvider into ModelProvider enum, proto, cache, download, and database layers
  • Update ARCHITECTURE.md and DEPLOYMENT.md

Few Caveats:

  • Currently it supports parallel file downloads but no per-file chunking. Per-file byte-range chunking can be follow-up optimization.
  • NGC bulk /files endpoint is gated behind UAM org membership. Legacy/service keys from external orgs get 400; personal api keys work fine. So I added a fall back to checksums.blake3 manifest to enumerate files. Individual file downloads are not gated.

Summary by CodeRabbit

  • New Features

    • Added NVIDIA NGC model provider support, enabling downloads from NGC alongside HuggingFace
  • Documentation

    • Updated deployment guides with NGC authentication and cache configuration options
    • Added Helm examples for configuring NGC API keys via Kubernetes secrets

sunil1511 and others added 2 commits April 10, 2026 20:34
Add NVIDIA NGC as a model provider alongside HuggingFace, enabling
ModelExpress to download, cache, and manage models from the NGC catalog.
NGC provider:
- Implement NgcProvider (ModelProviderTrait) and NgcProviderCache (ProviderCache)
- NGC API integration: token exchange via authn.nvidia.com, paginated file
  listing, streaming downloads with concurrent file transfers
- Support V1 (presigned URL) and V2 (Bearer auth) NGC storage backends
- Support all NGC API key types: personal (nvapi-), legacy, and service keys
- UAM fallback: when bulk file listing is gated (HTTP 400/401), enumerate
  files via checksums.blake3 manifest
- Model name parsing: org/name/version, org/team/name/version,
  org/team/type/name/version, and ngc:// URI scheme
- API key resolution: NGC_API_KEY, NGC_CLI_API_KEY env vars, ~/.ngc/config
  (INI and JSON formats)
- Parallel file downloads using buffer_unordered with min(num_cpus, 10)
  concurrency, matching nim-sdk patterns
- Thread-safe Bearer token refresh via RwLock with double-checked locking
  to handle tokens expiring during large model downloads
- Atomic writes (temp file + rename) to prevent partial files on interruption
- Path traversal protection in model name parsing
- Input whitespace trimming for robustness
Integration:
- Extend ModelProvider enum, proto (NGC = 1), gRPC conversions, database
  serialization
- Wire into cache.rs, download.rs, lib.rs, providers.rs
Helm:
- Document HF_TOKEN and NGC_API_KEY secret injection in values.yaml and
  values-local-storage.yaml via extraEnv
Documentation:
- Update ARCHITECTURE.md and DEPLOYMENT.md for NGC provider
- Fix provider directory tree to reflect providers.rs + providers/ layout
Tests:
- 31 unit tests covering parsing, cache operations, API response handling,
  V1/V2 storage, round-trip model names, path traversal rejection

Signed-off-by: Sunil Upare <sunilupare45@gmail.com>
@coderabbitai

coderabbitai Bot commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This PR extends model download infrastructure to support NVIDIA NGC models alongside existing HuggingFace support. Changes include new provider implementation, domain model extensions, provider routing, deployment configuration documentation, and server-side database integration.

Changes

Cohort / File(s) Summary
Workspace Dependencies
Cargo.toml, modelexpress_common/Cargo.toml
Added reqwest (v0.12 with json, rustls-tls, stream features) and futures as workspace dependencies to enable HTTP operations and async provider implementations.
Core Domain Models
modelexpress_common/src/models.rs, modelexpress_common/proto/model.proto
Added Ngc variant to ModelProvider enum with string representation "ngc" and Clap ValueEnum support; proto defines corresponding NGC = 1 enum value with tests for serialization/deserialization.
NGC Provider Implementation
modelexpress_common/src/providers/ngc.rs
Comprehensive new NGC provider with artifact ID parsing (org/name/version formats, NGC URI prefix mapping), API key resolution from environment/CLI config, Bearer token exchange, file enumeration with V1/V2 API support, blake3 checksum fallback, concurrent streaming downloads with atomic rename, and cache management (clear, resolve paths, list cached models).
Provider Routing & Conversion
modelexpress_common/src/download.rs, modelexpress_common/src/cache.rs, modelexpress_common/src/lib.rs, modelexpress_common/src/providers.rs
Extended get_provider factory and cache routing to dispatch ModelProvider::Ngc requests; added bidirectional gRPC conversion mappings; updated sort keys for consistent provider ordering; added NGC provider module re-export.
Deployment & Configuration
docs/ARCHITECTURE.md, docs/DEPLOYMENT.md, helm/values.yaml, helm/values-local-storage.yaml
Updated architecture diagrams and documentation describing NGC support in cache service; added Kubernetes secret configuration examples for NGC_API_KEY and HF_TOKEN; documented NGC API key precedence (NGC_API_KEY → NGC_CLI_API_KEY → ~/.ngc/config) and cache directory resolution.
Server Database Integration
modelexpress_server/src/database.rs
Extended provider string mapping in database layer to serialize/deserialize ModelProvider::Ngc as "Ngc" alongside existing "HuggingFace" handling.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Poem

🐰 wiggles nose with delight ✨
HuggingFace was splendid, NGC's the new guest,
With tokens and caches and V2 at best,
A bunny's swift paws add providers with flair—
Two model sources now dancing in air! 🚀

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: add NGC model provider to support NGC Downloads' directly and clearly describes the main change: adding a new NGC model provider implementation alongside the existing HuggingFace provider.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (4)
docs/DEPLOYMENT.md (1)

192-197: Document NGC_CLI_HOME override in key resolution order.

The precedence list is almost complete; the implementation also supports overriding the config directory via NGC_CLI_HOME before reading .ngc/config. Adding that note will keep docs fully aligned with runtime behavior.

Suggested doc tweak
-3. `~/.ngc/config` (written by `ngc config set`)
+3. `${NGC_CLI_HOME}/.ngc/config` if `NGC_CLI_HOME` is set
+4. `~/.ngc/config` (written by `ngc config set`)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/DEPLOYMENT.md` around lines 192 - 197, The docs list NGC API key
resolution order but omit the NGC_CLI_HOME override; update the precedence
description to mention that before reading ~/.ngc/config (created by ngc config
set) the server will respect an alternate config directory set via the
NGC_CLI_HOME environment variable, so the full resolution order becomes:
NGC_API_KEY env var, NGC_CLI_API_KEY env var, then ~/.ngc/config (or the
directory pointed to by NGC_CLI_HOME) — mention NGC_CLI_HOME explicitly and that
it overrides the default .ngc config path.
modelexpress_common/src/lib.rs (1)

140-155: Add an explicit Ngc round-trip conversion test.

The new match arms are correct, but the current provider conversion test only asserts HuggingFace. Adding Ngc in test_model_provider_conversion_both_ways will lock in this mapping.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modelexpress_common/src/lib.rs` around lines 140 - 155, Add an explicit
assertion for the Ngc variant in the existing round-trip conversion test: update
test_model_provider_conversion_both_ways to also assert that
models::ModelProvider::Ngc converts to grpc::model::ModelProvider::Ngc and back
to models::ModelProvider::Ngc, verifying the impl From conversions between
models::ModelProvider and grpc::model::ModelProvider both ways.
modelexpress_common/src/providers/ngc.rs (2)

648-668: Consider adding temp file cleanup on error.

If an error occurs after creating the temp file (e.g., during write or flush), the .tmp file will be left behind. While this isn't critical (subsequent downloads will overwrite it), explicit cleanup on error would be cleaner.

Optional: Add cleanup on error
     let temp_path = dest.with_extension("tmp");
     let mut file = tokio::fs::File::create(&temp_path)
         .await
         .with_context(|| format!("Failed to create {temp_path:?}"))?;

     let mut stream = response.bytes_stream();
-    while let Some(chunk) = stream.next().await {
-        let bytes = chunk.with_context(|| format!("Error reading response body for {rel_path}"))?;
-        file.write_all(&bytes)
-            .await
-            .with_context(|| format!("Failed to write to {temp_path:?}"))?;
+    let write_result = async {
+        while let Some(chunk) = stream.next().await {
+            let bytes = chunk.with_context(|| format!("Error reading response body for {rel_path}"))?;
+            file.write_all(&bytes)
+                .await
+                .with_context(|| format!("Failed to write to {temp_path:?}"))?;
+        }
+        file.flush()
+            .await
+            .with_context(|| format!("Failed to flush {temp_path:?}"))?;
+        Ok::<_, anyhow::Error>(())
+    }.await;
+
+    if let Err(e) = write_result {
+        let _ = tokio::fs::remove_file(&temp_path).await;
+        return Err(e);
     }
-
-    file.flush()
-        .await
-        .with_context(|| format!("Failed to flush {temp_path:?}"))?;
     drop(file);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modelexpress_common/src/providers/ngc.rs` around lines 648 - 668, The temp
file at temp_path can be left behind if an error occurs after creation; wrap the
download/write/flush/rename sequence in a block that captures the result, and on
Err call tokio::fs::remove_file(&temp_path).await.ok() before returning the
error. Concretely, after creating temp_path and file, perform the stream loop,
flush, and rename inside a let result = (async { ... }).await or a match so you
can do if let Err(e) = result { tokio::fs::remove_file(&temp_path).await.ok();
return Err(e); } — reference symbols: temp_path, dest, file, stream,
response.bytes_stream(), and the final tokio::fs::rename call.

965-971: Edge case: unreadable directory returns as cached.

If read_dir fails (e.g., permission denied), the code treats the directory as non-empty and returns success. This might mask permission issues, though such cases are rare in practice.

Optional: Propagate read_dir errors
-    let is_empty = match tokio::fs::read_dir(&path).await {
-        Ok(mut rd) => rd.next_entry().await.ok().flatten().is_none(),
-        Err(_) => false,
-    };
+    let mut rd = tokio::fs::read_dir(&path)
+        .await
+        .with_context(|| format!("Failed to read cache directory {path:?}"))?;
+    let is_empty = rd.next_entry().await.ok().flatten().is_none();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modelexpress_common/src/providers/ngc.rs` around lines 965 - 971, The current
is_empty check swallows tokio::fs::read_dir errors and treats unreadable
directories as non-empty; instead surface failures from tokio::fs::read_dir so
permission/IO errors aren’t masked. Change the match around
tokio::fs::read_dir(&path).await in ngc.rs (the is_empty calculation) to
propagate the Err (e.g., return/bail with the error using anyhow::Context or map
the error into anyhow::Error) rather than converting Err(_) to false, so
read_dir failures cause the caller to see the error and bail.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@helm/values-local-storage.yaml`:
- Around line 81-94: Update the commented secret names to match the repo
convention by replacing usages of hf-token and ngc-api-key with hf-token-secret
and ngc-api-key-secret: change the example kubectl create secret commands and
the corresponding commented env var blocks (the secretKeyRef.name values in the
HF_TOKEN and NGC_API_KEY sections) so they reference hf-token-secret and
ngc-api-key-secret instead of hf-token / ngc-api-key, ensuring consistent naming
across examples.

---

Nitpick comments:
In `@docs/DEPLOYMENT.md`:
- Around line 192-197: The docs list NGC API key resolution order but omit the
NGC_CLI_HOME override; update the precedence description to mention that before
reading ~/.ngc/config (created by ngc config set) the server will respect an
alternate config directory set via the NGC_CLI_HOME environment variable, so the
full resolution order becomes: NGC_API_KEY env var, NGC_CLI_API_KEY env var,
then ~/.ngc/config (or the directory pointed to by NGC_CLI_HOME) — mention
NGC_CLI_HOME explicitly and that it overrides the default .ngc config path.

In `@modelexpress_common/src/lib.rs`:
- Around line 140-155: Add an explicit assertion for the Ngc variant in the
existing round-trip conversion test: update
test_model_provider_conversion_both_ways to also assert that
models::ModelProvider::Ngc converts to grpc::model::ModelProvider::Ngc and back
to models::ModelProvider::Ngc, verifying the impl From conversions between
models::ModelProvider and grpc::model::ModelProvider both ways.

In `@modelexpress_common/src/providers/ngc.rs`:
- Around line 648-668: The temp file at temp_path can be left behind if an error
occurs after creation; wrap the download/write/flush/rename sequence in a block
that captures the result, and on Err call
tokio::fs::remove_file(&temp_path).await.ok() before returning the error.
Concretely, after creating temp_path and file, perform the stream loop, flush,
and rename inside a let result = (async { ... }).await or a match so you can do
if let Err(e) = result { tokio::fs::remove_file(&temp_path).await.ok(); return
Err(e); } — reference symbols: temp_path, dest, file, stream,
response.bytes_stream(), and the final tokio::fs::rename call.
- Around line 965-971: The current is_empty check swallows tokio::fs::read_dir
errors and treats unreadable directories as non-empty; instead surface failures
from tokio::fs::read_dir so permission/IO errors aren’t masked. Change the match
around tokio::fs::read_dir(&path).await in ngc.rs (the is_empty calculation) to
propagate the Err (e.g., return/bail with the error using anyhow::Context or map
the error into anyhow::Error) rather than converting Err(_) to false, so
read_dir failures cause the caller to see the error and bail.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 80d8e3b9-2777-4819-91a8-c700a5c5224b

📥 Commits

Reviewing files that changed from the base of the PR and between 434855d and 8327743.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • Cargo.toml
  • docs/ARCHITECTURE.md
  • docs/DEPLOYMENT.md
  • helm/values-local-storage.yaml
  • helm/values.yaml
  • modelexpress_common/Cargo.toml
  • modelexpress_common/proto/model.proto
  • modelexpress_common/src/cache.rs
  • modelexpress_common/src/download.rs
  • modelexpress_common/src/lib.rs
  • modelexpress_common/src/models.rs
  • modelexpress_common/src/providers.rs
  • modelexpress_common/src/providers/ngc.rs
  • modelexpress_server/src/database.rs

Comment thread helm/values-local-storage.yaml
… fix helm secret names

Signed-off-by: Sunil Upare <sunilupare45@gmail.com>

This branch was previously deployed

1 inactive deployment
GITLAB — b48295bc Deployed Apr 13, 2026 by sunil1511 via trigger / mirror_and_trigger #807
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants