[DE-8270] Model weights upload & download (SDK side) - #469
Open
luke-e-schaefer wants to merge 3 commits into
Open
[DE-8270] Model weights upload & download (SDK side)#469luke-e-schaefer wants to merge 3 commits into
luke-e-schaefer wants to merge 3 commits into
Conversation
Base automatically changed from
update-nuc-sdk-for-new-eval-stuff-pt1
to
master
August 11, 2026 14:24
Mirrors the REST surface shipped in scaleapi#149063 so users can attach a weights artifact to a model and fetch it back from Python. - `NucleusClient.upload_model_weights` / `download_model_weights` — the two methods the server PR's API-docs pages already document — plus `get_model_weights` and `delete_model_weights` for the remaining routes. - `Model.upload_weights()` / `download_weights()` / `weights()` / `delete_weights()` delegate to the client, matching how `Benchmark` does it. - New `ModelWeights` metadata type parsed from the weights DTO. The weights routes serialize camelCase both ways, unlike most of this SDK's endpoints, so the new payload keys are grouped and labelled in `constants.py`. - Transfers go straight to storage via presigned URLs and never through the API, so artifacts aren't subject to API request-size limits. Over 5 GB the server hands back multipart parts, which upload 4 at a time; `on_progress` reports `(bytes_transferred, total_bytes)`. - Size is checked against the server's 10 GB cap before presign, so an oversized file fails without a network round-trip. Two things worth knowing for review: part PUTs must be sent with *no* headers (they're signed without the Content-Type condition, so forwarding `requiredHeaders` makes S3 reject the signature), and download resolves the signed URL via `?json=1` rather than following the 302, so the API's auth headers are never sent to storage. 24 mock-based unit tests in `tests/test_model_weights.py`; version bumped to 0.19.1 (additive, per CLAUDE.md). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The docstrings are what users read, so they shouldn't describe how the artifact gets stored or moved. Dropped the presign/multipart/direct-to-storage narration from every public docstring, the `ModelWeights` attribute docs, and the CHANGELOG, leaving what a caller actually needs: what the method does, who can call it, the size limit, and the arguments. Also made the transfer helpers private (`_presign_payload`, `_transfer_weights_to_storage`, `_stream_weights_to_file`, `_finalize_payload`) so the mechanics don't show up in the generated API docs at all, rather than only being reworded. Kept the two in-body comments that explain why part uploads send no headers and why the download URL is fetched as JSON — those aren't user-visible and each one guards a real footgun. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
luke-e-schaefer
force-pushed
the
lukeschaefer/de-8270-upload-download-model-weights
branch
from
August 11, 2026 14:35
df6ede7 to
83c25f7
Compare
…progress Addresses two review comments: - transferred += len(chunk) ran unsynchronized across the part-upload pool, so concurrent workers could drop updates. The counter and the value handed to on_progress are now taken under a lock. - A single PUT reported nothing until it finished, then jumped to 100%. When a callback is supplied the body is wrapped so progress comes from the read side; the wrapper delegates everything but read(), so requests still sizes the body from fileno()/tell() and sends Content-Length as before.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
SDK side of model weights upload / download, mirroring the REST surface merged in scaleapi#149063.
The two primary methods are the ones the server PR's API-docs pages (
ApiDocsPage/models-python/{upload,download}-model-weights.md) already document, so the published docs and the SDK agree:Added
NucleusClient.upload_model_weights(model, path, *, content_type=None, original_filename=None, checksum_sha256=None, on_progress=None)— presign → PUT direct to storage → finalize. ReturnsModelWeights.NucleusClient.download_model_weights(model, path, *, on_progress=None)— resolves the signed URL and streams to disk (creating parent dirs). Returns the path written.get_model_weights(model)/delete_model_weights(model)for the remaining two routes.Model.upload_weights()/.download_weights()/.weights()/.delete_weights()— thin delegation to the client, matching howBenchmarkwraps its client methods.ModelWeightsmetadata type:present,status,size_bytes,original_filename,content_type,download_url.All model arguments accept either a
Modelor a bare model id (prj_*).Notes for review
requiredHeaders(which the single PUT does need) makes S3 reject the signature. There's a test pinning this in both directions — it's the easiest thing to get wrong here, and the frontend hook in 149063 has the same split.?json=1to fetch the signed URL rather than following the 302, so the API's auth headers are never sent to storage.constants.py.Tests / Version
tests/test_model_weights.py— 24 mock-based unit tests (no live API, no real S3): DTO parsing, payload builders, single vs. multipart transfer, header split, ETag/failure handling, progress callbacks, download streaming, all four client methods, and theModelwrappers.pylint nucleus10.00/10,mypy --ignore-missing-imports nucleusclean, ruff clean, black + isort clean, 61 mock-based tests passing across the eval/benchmark/leaderboard/weights suites.pyproject.toml→ 0.19.1 + CHANGELOG entry (patch bump: additive new methods, perCLAUDE.md).resolves https://linear.app/scale-epd/issue/DE-8270
🤖 Generated with Claude Code
Greptile Summary
This PR adds model weights upload and download to the Nucleus Python SDK. Bytes flow directly between the caller and storage over presigned URLs (presign → PUT → finalize), so multi-GB artifacts never transit the Nucleus API.
NucleusClientgainsupload_model_weights,download_model_weights,get_model_weights, anddelete_model_weights;Modelgets matching thin wrappers (upload_weights,download_weights,weights,delete_weights)._ProgressReaderwrapper to deliver incremental progress callbacks; multipart uploads (≥ 5 GB) run up to 4 concurrent part PUTs with athreading.Lock-protected counter, correctly sending no headers on part PUTs to avoid S3 signature rejection.ModelWeightsdataclass represents artifact metadata; 24 mock-based unit tests cover all paths including the header-split invariant, ETag failure handling, and progress callbacks.Confidence Score: 5/5
_clientfield inModelWeightsparticipates in__eq__, which is a minor design issue but not a correctness problem for any current caller.Important Files Changed
ModelWeights._clientfield participates in dataclass__eq__by default, which can cause unexpected inequality between logically identical objects.Sequence Diagram
sequenceDiagram participant User participant SDK as NucleusClient participant API as Nucleus API participant S3 as Storage (S3) Note over User,S3: Upload flow User->>SDK: upload_model_weights(model, path) SDK->>SDK: check file size ≤ 10 GB SDK->>API: "POST model/{id}/weights/presign" API-->>SDK: presign response alt Single PUT (uploadUrl present) SDK->>S3: PUT presignedUrl (with requiredHeaders) S3-->>SDK: ETag else Multipart (parts[] present) par Up to 4 concurrent part uploads SDK->>S3: PUT part[1].url (no headers) SDK->>S3: PUT part[2].url (no headers) SDK->>S3: PUT part[N].url (no headers) end S3-->>SDK: ETags per part end SDK->>API: "POST model/{id}/weights/finalize" API-->>SDK: ModelWeights DTO SDK-->>User: ModelWeights Note over User,S3: Download flow User->>SDK: download_model_weights(model, path) SDK->>API: "GET model/{id}/weights/download?json=1" API-->>SDK: "{"url": signedUrl}" SDK->>S3: GET signedUrl (streaming) S3-->>SDK: file bytes (chunked) SDK-->>User: path writtenReviews (5): Last reviewed commit: "fix(weights): lock the multipart progres..." | Re-trigger Greptile