Skip to content

feat: add s3 model provider#440

Open
Broduker wants to merge 3 commits into
ai-dynamo:mainfrom
Broduker:feat/s3-provider
Open

feat: add s3 model provider#440
Broduker wants to merge 3 commits into
ai-dynamo:mainfrom
Broduker:feat/s3-provider

Conversation

@Broduker

@Broduker Broduker commented Jun 17, 2026

Copy link
Copy Markdown

Overview:

Adds S3-compatible object storage as a ModelExpress model provider for metadata downloads and provider-aware request routing.

Details:

  • Adds ModelProvider::S3 to the protobuf and Rust provider model.
  • Adds an S3 provider backed by object_store for metadata-only downloads from s3://bucket/prefix into the ModelExpress cache.
  • Adds ModelProvider::resolve_for_model_name so clients resolve s3://, gs://, and ngc:// URIs before sending requests.
  • Registers S3 in cache listing, direct download routing, Redis registry records, Kubernetes registry records, and CLI URI normalization.
  • Adds tests for provider resolution, provider routing, canonicalization, and S3 request provider propagation.

Where should the reviewer start?

Start with modelexpress_common/src/providers/s3.rs for the new provider implementation, then modelexpress_common/src/models.rs and modelexpress_client/src/lib.rs for provider resolution and request routing.

Related Issues

  • Confirmed — no related issue

Summary by CodeRabbit

  • New Features
    • Added support for Amazon S3 as a model provider
    • Models can now be downloaded directly from S3 buckets using s3:// URLs
    • Provider is automatically inferred from model URLs for seamless S3 integration
    • S3 models are managed with the same caching system as other providers

Signed-off-by: shenls <shenlinshan@kanzhun.com>
@copy-pr-bot

copy-pr-bot Bot commented Jun 17, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds S3 as a new model provider across the full stack: extends the ModelProvider proto enum and Rust types, introduces S3Provider and S3ProviderCache using the object_store crate for object listing and streaming downloads, wires S3 into common download/cache dispatch, updates client provider inference from s3:// URLs, and registers S3 in the Kubernetes and Redis registry backends.

Changes

S3 Model Provider End-to-End

Layer / File(s) Summary
ModelProvider::S3 enum variant and gRPC contract
Cargo.toml, modelexpress_common/Cargo.toml, modelexpress_common/proto/model.proto, modelexpress_common/src/models.rs, modelexpress_common/src/lib.rs
Adds S3 = 3 to the proto ModelProvider enum, the Rust ModelProvider::S3 variant with as_str/resolve_for_model_name/ValueEnum wiring, bidirectional gRPC ↔ internal conversions, workspace object_store dependency with aws feature, and all related unit tests.
S3Provider and S3ProviderCache implementation
modelexpress_common/src/providers/s3.rs
Implements S3ModelName URL parsing/validation, S3Provider building an object_store client from env vars, downloadable-path filtering, chunked async streaming downloads, ModelProviderTrait methods, S3ProviderCache ProviderCache methods, recursive cache discovery, and unit tests for parsing, enum recognition, and ignore_weights enforcement.
Wire S3Provider into common dispatch
modelexpress_common/src/providers.rs, modelexpress_common/src/download.rs, modelexpress_common/src/cache.rs
Adds the public s3 module and S3Provider re-export, extends get_provider to return S3Provider, maps ModelProvider::S3 to S3ProviderCache in cache_for_provider, includes S3 in get_cache_stats iteration and provider_sort_key, with updated tests.
Client-side S3 provider inference
modelexpress_client/src/bin/modules/handlers.rs, modelexpress_client/src/lib.rs
Adds s3:// recognition to handler inference and normalization, calls ModelProvider::resolve_for_model_name in four Client methods, updates RecordingModelService to capture seen_provider, and adds/updates tests asserting GCS and S3 provider inference.
Server registry backend S3 registration
modelexpress_server/src/registry/backend/kubernetes.rs, modelexpress_server/src/registry/backend/redis.rs
Extends ALL_PROVIDERS, provider_str, and provider_from_str in both Kubernetes and Redis backends to include ModelProvider::S3.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A bucket of models, a region away,
s3:// paths now find their way.
The rabbit parses bucket and prefix with care,
Streams metadata files through the cloud-chilly air.
No weights allowed unless you say so — it's true!
The marker is written, the cache folder's new. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add s3 model provider' accurately summarizes the primary objective of the PR, which is to introduce S3 as a new model provider throughout the system.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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


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: 2

🧹 Nitpick comments (4)
modelexpress_common/src/providers/s3.rs (1)

208-212: 💤 Low value

Blocking I/O in async context.

fs::create_dir_all is a blocking call within an async function. While acceptable for quick operations, consider using tokio::fs::create_dir_all for consistency with the async operations below (e.g., tokio::fs::write at line 240).

♻️ Suggested refactor
-        fs::create_dir_all(&cache_dir).with_context(|| {
+        tokio::fs::create_dir_all(&cache_dir).await.with_context(|| {
             format!("Failed to create cache directory: {}", cache_dir.display())
         })?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modelexpress_common/src/providers/s3.rs` around lines 208 - 212, Replace the
blocking fs::create_dir_all call with its async equivalent
tokio::fs::create_dir_all to avoid blocking I/O in the async context. Since
tokio::fs::create_dir_all is an async function, you will need to await it. This
ensures consistency with other async file operations used later in the function,
such as tokio::fs::write.
modelexpress_client/src/bin/modules/handlers.rs (2)

62-63: ⚡ Quick win

Add a dedicated S3 validation-path regression test.

The new S3 inference/canonicalization branches are untested in this module, while GCS has explicit coverage. Add a sibling test to lock in behavior for mixed-case S3://... inputs.

✅ Suggested test
+    #[test]
+    fn test_validate_resolves_s3_model_name_to_s3_cache_path() {
+        let temp_dir = TempDir::new().expect("Failed to create temp dir");
+        let model_name = "S3://bucket/foo/bar";
+        let expected_path = temp_dir
+            .path()
+            .join("s3")
+            .join("bucket")
+            .join("foo")
+            .join("bar");
+        fs::create_dir_all(&expected_path).expect("Failed to create S3 cache path");
+
+        let (provider, model_path) = resolve_validation_model_path(temp_dir.path(), model_name);
+
+        assert_eq!(provider, ModelProvider::S3);
+        assert_eq!(model_path, expected_path);
+        assert!(model_path.exists());
+        assert!(!temp_dir.path().join("S3://bucket/foo/bar").exists());
+    }

Also applies to: 84-85

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modelexpress_client/src/bin/modules/handlers.rs` around lines 62 - 63, The
new S3 inference/canonicalization branches (ModelProvider::S3 handling) in the
handlers module lack test coverage while GCS has explicit regression tests. Add
a sibling test to the existing GCS test cases that validates the behavior for
mixed-case S3:// prefixes (such as S3://, s3://, S3://, etc.) to ensure the
strip_ascii_prefix_ignore_case function correctly identifies and canonicalizes
S3 model provider inputs, mirroring the test coverage pattern already
established for GCS.

58-69: ⚡ Quick win

Use the shared provider resolver to prevent contract drift.

Line 58-Line 69 duplicates scheme-to-provider logic that already lives in modelexpress_common/src/models.rs (ModelProvider::resolve_for_model_name). Reusing it here keeps client validation aligned with the core provider contract as new schemes are added.

♻️ Suggested refactor
 fn infer_provider_from_model_name(model_name: &str) -> ModelProvider {
-    let model_name = model_name.trim_start();
-    if strip_ascii_prefix_ignore_case(model_name, "gs://").is_some() {
-        ModelProvider::Gcs
-    } else if strip_ascii_prefix_ignore_case(model_name, "s3://").is_some() {
-        ModelProvider::S3
-    } else if strip_ascii_prefix_ignore_case(model_name, "ngc://").is_some() {
-        ModelProvider::Ngc
-    } else {
-        ModelProvider::HuggingFace
-    }
+    ModelProvider::resolve_for_model_name(model_name, ModelProvider::HuggingFace)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modelexpress_client/src/bin/modules/handlers.rs` around lines 58 - 69, The
function infer_provider_from_model_name duplicates scheme-to-provider resolution
logic that is already implemented in the shared
ModelProvider::resolve_for_model_name function. Remove the duplicated if-else
chain in infer_provider_from_model_name and instead directly call
ModelProvider::resolve_for_model_name with the trimmed model_name, returning its
result. This prevents contract drift and ensures the client validation stays
synchronized with the core provider contract as new schemes are added.
modelexpress_client/src/lib.rs (1)

281-281: ⚡ Quick win

Deduplicate effective-provider resolution across client entry points.

The same ModelProvider::resolve_for_model_name call is repeated in four methods. A small helper keeps this contract in one place and reduces future drift.

♻️ Suggested refactor
 impl Client {
+    #[inline]
+    fn resolve_provider_for_model_name(model_name: &str, provider: ModelProvider) -> ModelProvider {
+        ModelProvider::resolve_for_model_name(model_name, provider)
+    }
+
     pub async fn get_model_path(
         &self,
         model_name: &str,
         provider: ModelProvider,
     ) -> anyhow::Result<PathBuf> {
-        let provider = ModelProvider::resolve_for_model_name(model_name, provider);
+        let provider = Self::resolve_provider_for_model_name(model_name, provider);
@@
     pub async fn request_model_on_server(
@@
-        let provider = ModelProvider::resolve_for_model_name(&model_name, provider);
+        let provider = Self::resolve_provider_for_model_name(&model_name, provider);
@@
     pub async fn request_model(
@@
-        let provider = ModelProvider::resolve_for_model_name(&model_name, provider);
+        let provider = Self::resolve_provider_for_model_name(&model_name, provider);
@@
     pub async fn request_model_with_smart_fallback(
@@
-        let provider = ModelProvider::resolve_for_model_name(&model_name, provider);
+        let provider = Self::resolve_provider_for_model_name(&model_name, provider);

Also applies to: 635-635, 706-706, 741-741

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modelexpress_client/src/lib.rs` at line 281, The
ModelProvider::resolve_for_model_name call is duplicated across four client
entry point methods (appearing at lines 281, 635, 706, and 741). Create a
private helper method in the client that wraps this
ModelProvider::resolve_for_model_name call with the model_name and provider
parameters, then replace all four duplicate calls with invocations of this new
helper method. This centralizes the contract in one place and prevents future
inconsistencies across these entry points.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@modelexpress_server/src/registry/backend/kubernetes.rs`:
- Around line 31-36: The ALL_PROVIDERS constant now includes ModelProvider::S3,
expanding the array to 4 providers which means candidate_cr_names() will now
return 5 entries (4 providers plus a legacy entry). Update the test that
validates candidate_cr_names() output (currently expecting 4 entries) to expect
5 entries instead, and add membership assertions to verify that
ModelProvider::S3 is included in the candidate names returned by the function.
This ensures the test expectations align with the expanded provider list.

In `@modelexpress_server/src/registry/backend/redis.rs`:
- Around line 42-47: The test for the `candidate_keys()` function needs to be
updated to reflect the addition of S3 to the `ALL_PROVIDERS` constant. Update
the test assertions around line 594 to expect 5 keys instead of 4 (accounting
for the 4 providers in `ALL_PROVIDERS` plus the legacy key), and add an
assertion to verify that the S3 provider key is included in the returned
candidate keys list.

---

Nitpick comments:
In `@modelexpress_client/src/bin/modules/handlers.rs`:
- Around line 62-63: The new S3 inference/canonicalization branches
(ModelProvider::S3 handling) in the handlers module lack test coverage while GCS
has explicit regression tests. Add a sibling test to the existing GCS test cases
that validates the behavior for mixed-case S3:// prefixes (such as S3://, s3://,
S3://, etc.) to ensure the strip_ascii_prefix_ignore_case function correctly
identifies and canonicalizes S3 model provider inputs, mirroring the test
coverage pattern already established for GCS.
- Around line 58-69: The function infer_provider_from_model_name duplicates
scheme-to-provider resolution logic that is already implemented in the shared
ModelProvider::resolve_for_model_name function. Remove the duplicated if-else
chain in infer_provider_from_model_name and instead directly call
ModelProvider::resolve_for_model_name with the trimmed model_name, returning its
result. This prevents contract drift and ensures the client validation stays
synchronized with the core provider contract as new schemes are added.

In `@modelexpress_client/src/lib.rs`:
- Line 281: The ModelProvider::resolve_for_model_name call is duplicated across
four client entry point methods (appearing at lines 281, 635, 706, and 741).
Create a private helper method in the client that wraps this
ModelProvider::resolve_for_model_name call with the model_name and provider
parameters, then replace all four duplicate calls with invocations of this new
helper method. This centralizes the contract in one place and prevents future
inconsistencies across these entry points.

In `@modelexpress_common/src/providers/s3.rs`:
- Around line 208-212: Replace the blocking fs::create_dir_all call with its
async equivalent tokio::fs::create_dir_all to avoid blocking I/O in the async
context. Since tokio::fs::create_dir_all is an async function, you will need to
await it. This ensures consistency with other async file operations used later
in the function, such as tokio::fs::write.
🪄 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: Enterprise

Run ID: b44da810-a72d-4969-b574-15ea710dad44

📥 Commits

Reviewing files that changed from the base of the PR and between 0c45617 and b39343a.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • Cargo.toml
  • modelexpress_client/src/bin/modules/handlers.rs
  • modelexpress_client/src/lib.rs
  • 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/s3.rs
  • modelexpress_server/src/registry/backend/kubernetes.rs
  • modelexpress_server/src/registry/backend/redis.rs

Comment on lines +31 to 36
const ALL_PROVIDERS: [ModelProvider; 4] = [
ModelProvider::HuggingFace,
ModelProvider::Ngc,
ModelProvider::Gcs,
ModelProvider::S3,
];

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Update candidate-name test expectations after expanding ALL_PROVIDERS.

Adding ModelProvider::S3 here increases candidate_cr_names() output to 5 entries (4 providers + legacy), but the test at Line 705 still expects 4 and omits S3 membership checks. This will fail tests consistently.

Suggested test fix
-        assert_eq!(candidates.len(), 4);
+        assert_eq!(candidates.len(), 5);
         for p in [
             ModelProvider::HuggingFace,
             ModelProvider::Ngc,
             ModelProvider::Gcs,
+            ModelProvider::S3,
         ] {
             assert!(candidates.contains(&KubernetesRegistryBackend::cr_name_for(p, n)));
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modelexpress_server/src/registry/backend/kubernetes.rs` around lines 31 - 36,
The ALL_PROVIDERS constant now includes ModelProvider::S3, expanding the array
to 4 providers which means candidate_cr_names() will now return 5 entries (4
providers plus a legacy entry). Update the test that validates
candidate_cr_names() output (currently expecting 4 entries) to expect 5 entries
instead, and add membership assertions to verify that ModelProvider::S3 is
included in the candidate names returned by the function. This ensures the test
expectations align with the expanded provider list.

Comment on lines +42 to 47
const ALL_PROVIDERS: [ModelProvider; 4] = [
ModelProvider::HuggingFace,
ModelProvider::Ngc,
ModelProvider::Gcs,
ModelProvider::S3,
];

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix stale candidate-key test after adding S3 to provider fan-out.

With S3 added to ALL_PROVIDERS, candidate_keys() now returns 5 keys (4 providers + legacy), but the test at Line 594 still asserts 4 and misses the S3 key assertion.

Suggested test fix
-        assert_eq!(keys.len(), 4);
+        assert_eq!(keys.len(), 5);
         assert!(keys.contains(&"mx:model:HuggingFace:org/model".to_string()));
         assert!(keys.contains(&"mx:model:Ngc:org/model".to_string()));
         assert!(keys.contains(&"mx:model:Gcs:org/model".to_string()));
+        assert!(keys.contains(&"mx:model:S3:org/model".to_string()));
         assert!(keys.contains(&"mx:model:org/model".to_string())); // legacy
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modelexpress_server/src/registry/backend/redis.rs` around lines 42 - 47, The
test for the `candidate_keys()` function needs to be updated to reflect the
addition of S3 to the `ALL_PROVIDERS` constant. Update the test assertions
around line 594 to expect 5 keys instead of 4 (accounting for the 4 providers in
`ALL_PROVIDERS` plus the legacy key), and add an assertion to verify that the S3
provider key is included in the returned candidate keys list.

Broduker added 2 commits June 17, 2026 15:42
Signed-off-by: shenls <shenlinshan@kanzhun.com>
Signed-off-by: shenls <shenlinshan@kanzhun.com>
@Broduker

Copy link
Copy Markdown
Author

coud anyone review it?

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.

1 participant