Skip to content

Model downloads: a rate limit is said out loud, a shared env build survives a cancel, and sizes stop disagreeing - #752

Merged
iamsdas merged 10 commits into
mainfrom
download-throttle-and-progress
Aug 23, 2026
Merged

Model downloads: a rate limit is said out loud, a shared env build survives a cancel, and sizes stop disagreeing#752
iamsdas merged 10 commits into
mainfrom
download-throttle-and-progress

Conversation

@iamsdas

@iamsdas iamsdas commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Summary

A user reported downloading two models and seeing only one move — the other sat at 0. Downloads already run in parallel (a probe proved it), so this fixes the things that made a working parallel download look stuck, and makes the numbers on the page honest.

  • A Hugging Face rate limit is now waited out and said on the row. A 429 used to be indistinguishable from a dead link: it spent the segment's whole fault budget in about seven seconds and handed the repo to hf's own snapshot_download, which is slower — so a throttled user watched a download crawl, having been throttled and never told. Throttles now get their own allowance, separate from the fault budget, and the row reads "Hugging Face is limiting this download", with a sign-in prompt when no token is stored.
  • The wait comes from the header the Hub actually sends. HF signals a limit with RateLimit: "resolvers";r=0;t=87 — the IETF scheme — not Retry-After. Parsing only Retry-After meant guessing the wait on every real Hub throttle. Both are read now, RateLimit first, capped per-wait at THROTTLE_WAIT_MAX_S and in aggregate at THROTTLE_TOTAL_MAX_S (600s) so a host that 429s forever still gives up rather than hanging a job indefinitely.
  • A 429 on the resolve call is now recognised too. _hub_file_meta raises a requests-shaped HfHubHTTPError, not urllib.error.HTTPError, so a throttle on the metadata hop bypassed the new branch entirely — and that hop is the one the Hub actually meters (quota is counted per URL carrying a /resolve/ segment; our ranged GETs go to the presigned CDN and cost nothing). Both call sites funnel through _resolved_meta, which reads either exception shape without importing requests into this stdlib-only module.
  • Cancelling one download no longer kills an environment build the others are waiting on. Worker.install_key names a shared single-flight install, so cancelling on the key alone tore down the build every other download of that runner had joined — and before an interpreter is pinned that key is machine-global, so the blast radius was every runner. Installs are refcounted now: the last waiter out is the one that cancels, and the count is written under _lock so a cancel and a _terminate can't interleave.
  • A download parked behind someone else's env build says so, instead of reading "Preparing Fake — sync…" identically to the one doing the work — which was indistinguishable from a row that had died.
  • A live download's own total replaces the catalog's advertised size. This is the "68 GB out of 64 GB" report: the two numbers were never in conflict, they were two different numbers shown side by side — a hand-written catalog.py estimate next to a row counting real listing bytes. One rule now decides, in one unit, at every site: never understate. A live total larger than the advertised figure means a stale constant and the row wins; a smaller one is either one phase of a multi-part fetch or a conservative constant, and quoting it would promise a cheaper download than the user will get.
  • A single file is now capped at 500 chunks. CHUNK_BYTES becomes a floor: chunk = max(CHUNK_BYTES, ceil(size / 500)). Comfy-Org/MiniMax-H3 has single files up to 66.3GB, which planned 1,976 segments in one file and 14,057 across the repo — and every segment's cursor lives in a sidecar rewritten whole every second, so that download serialised ~2,000 dicts a second per file in flight, for hours. This is a bookkeeping measure and the comment says so out loud: chunk count consumes no Hub quota. Files below ~16.8GB are chunked exactly as before; SIDECAR_VERSION goes to 3 because a v2 sidecar for a larger file is internally consistent and wrong, which only a version number can reject.

Not changed: no download queue and no concurrency cap were added. Downloads are already concurrent; the perceived serialisation was the shared env build (fixed here), the per-process connection split, and throttling (also fixed here).

Visuals

What a segment does with an HTTP error — the throttle branch is the delta.

flowchart TD
    A[Segment gets an HTTP error] --> B{429, or 503<br/>with a wait header?}
    B -->|no| C[Count a fault<br/>backoff, retry]
    C --> D{Fault budget spent?}
    D -->|yes| E[Fall back to<br/>snapshot_download]
    B -->|yes| F[Say it on the job row]
    F --> G[Sleep in slices,<br/>separate allowance]
    G --> A
Loading

Before this PR the yes branch did not exist: a 429 took the no path, exhausted the fault budget, and reached the fallback silently.

Usage

Nothing new is user-invocable — these are behaviour changes on paths already in use.

  • Throttling: start a model download while over a Hub quota. The row reads Hugging Face is limiting this download — waiting 42s, or …— sign in to Hugging Face in Preferences → AI for a higher limit when no token is stored. The sign-in it points at is the existing device-code flow (D402); a 429 from a FUSED_MODEL_MIRROR host says only This download is being rate-limited, since neither the Hub nor a Hub sign-in is involved.
  • Shared env build: start two downloads for the same runner before its venv exists, then cancel either one. The other keeps building. Previously the cancelled one took it down with the install was cancelled.
  • Joined install: the same two downloads — the joiner's row reads Waiting for the <runner> environment — another download is building it…. No percentage, because nothing here knows how far another worker's uv sync has got.
  • Sizes: any AI Models or Playground surface showing a size. While a download runs, its measured total shows if it exceeds the catalog figure; otherwise the catalog estimate — hedged with ~ on the Local tab's recommended card, which is the one surface that has always carried the hedge.
  • Chunking: transparent, except that an in-flight .fusedpart for a file over ~16.8GB will be re-planned rather than resumed (sidecar v2 → v3).

Test Plan

  • tests/test_ai_hub_fetch.py — throttle recognition (_is_throttled's 503-needs-a-wait-header distinction), RateLimit parsing (t=, malformed, absent → fall back to Retry-After), all four Retry-After forms (delta-seconds, HTTP-date, naive-as-GMT, past-as-zero), per-wait and aggregate caps, cancel-during-a-throttle, mirror-vs-Hub wording, sign-in text only without a token, a 429 on the resolve hop, and the regression: SEGMENT_ATTEMPTS * 4 consecutive 429s now complete the download (on stashed code it failed with exactly gave up at byte 0 — HTTP 429). Plus chunk-plan cases driving _chunks with sizes as parameters — nothing allocated — covering the floor, the ceiling, the 16.8GB meeting point, and the SEGMENT_MIN_BYTES short-circuit.
  • tests/test_env_install.pystart() reports claimed on the spawn path only.
  • tests/test_ai_runtime.py — new shared_install fixture (real _ensure_venv, faked envinstall whose cancel marks the record done-with-error like the real one, claim-once/join-after). Either side's cancel leaves the other's install alive; the joiner's row carries the waiting detail; a re-hold landing before the cancel call stops it. All were red before the fix (assert ['shared-key'] == [], and 'Preparing Fake — sync…').
  • frontend/src/apps/ai_models/shared/modelSize.test.ts — cases over liveModelTotal / modelSizeLabel / modelSizeHint / catalogSizeBytes, mutation-checked.
  • 40 consecutive stress runs of the install-cancel mechanism, zero related failures — the original flake reproduced at roughly 1 in 11.
  • Full local suite: failure-ID set identical to main in both directions (this repo's macOS baseline is not zero — see the caveats below).
  • bun run typecheck clean and bun test src/apps/ai_models/shared/modelSize.test.ts green after merging origin/main (D428 reshaped this exact file).
  • CI green.
  • Manual: reviewer reproduces the throttle row against a rate-limited Hub account, and the two-download cancel above.

Known residuals, stated rather than hidden

  • The cancel-then-instantly-reload window is narrowed, not closed. Between dropping the last refcount and calling envinstall.cancel, a new waiter can re-hold the key; a second lock hold re-checks and bails, which shrinks the window to a few bytecodes but does not remove it. Closing it properly needs a generation token at the envinstall layer — deliberately out of scope. Worst case is one spurious the install was cancelled on a fast cancel-then-retry.
  • A card now reads 4.3 GB where catalog.py writes 4.6. The catalog's decimal GB is converted to bytes and formatted base-1024 like every other size on the page, including the progress row it sits beside. The card and its own progress bar agreeing was chosen over agreeing with the constant.
  • The Playground polls job rows again. D428's sidebar rewrite removed this page's jobs polling along with the progress bar it fed; the size rule needs a live pull's total, so the poll is back — gated on isBusy(runtime), so it runs only while something is actually live and clears its state otherwise. The progress bar was not resurrected.
  • /api/env/install gains a "claimed" field on the record. Additive; no client reads it.
  • test_a_refused_body_this_cannot_frame_ends_the_connection is a pre-existing ConnectionResetError flake (~17 of 25 runs, a different parametrisation each time) that reproduces on main. Not from this branch, not fixed here.

🤖 Generated with Claude Code

iamsdas and others added 4 commits August 22, 2026 22:42
A 429 from the Hub was indistinguishable from a broken link. It fell into
the generic `HTTP <code>` branch of the segment retry loop, spent the
whole `SEGMENT_ATTEMPTS` budget in about seven seconds of backoff, and
handed the repo to hf's own `snapshot_download` — a SLOWER download, with
nothing anywhere saying why it had become slow, and no hint that signing
in to the Hub would raise the limit.

Three halves to the fix:

  - a throttle (429, or 503 WITH a `Retry-After`) is recognised as its
    own case and gets its own generous allowance (`THROTTLE_ATTEMPTS`),
    so waiting it out no longer aborts into the fallback;
  - `Retry-After` is honoured in both permitted forms, clamped to
    `THROTTLE_WAIT_MAX_S` and slept in slices so a ✕ still lands;
  - the row says it. A process-global notice (one worker process serves
    one download) carries the fact from the segment threads to
    `fetch_with_progress`'s tick, which is the only channel to the row —
    with the sign-in half only when `_hf_token()` finds no token, and
    without naming Hugging Face at all on the mirror path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`envinstall.start` is single-flight per key: the first caller spawns
`uv sync`, every later caller joins and polls. `_ensure_venv` and
`_terminate` both cancelled on that KEY — which names the shared install,
not this job's share of it — so a ✕ on a download that was merely WAITING
tore the build down for every other download of that runner, each of
which then failed with a cancellation it never asked for. With no pinned
interpreter on the machine yet, `start` swaps in the machine-global
`PYTHON_BOOTSTRAP_KEY`, so the blast radius was every runner.

Ownership is now a fact rather than a guess: `envinstall.start` reports
`claimed` (True only on the path that reached `_spawn`, and deliberately
NOT part of `progress()`'s polled shape), `Worker.install_owned` records
it beside the key, and both cancel sites are conditional on it. A joiner
stops waiting, reports its own row cancelled, and leaves the install
running for whoever owns it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both progress surfaces are already honest about an unknown total — no bar
is drawn without one — but the DETAIL text could not tell "this download
is building an environment for itself" from "this download is parked
waiting for another download's build". Both read "Preparing <runner> —
<stage>…", so a download blocked on someone else looked identical to one
doing the work, and to one that had died.

The joiner's row now names the wait, using the ownership fact from the
previous commit. `_JOINED_INSTALL_DETAIL` sits beside `_QUEUED_DETAIL`,
which is the same shape of answer the transcription queue already gives.
No percentage: nothing here knows how far another worker's install has
got, and `ModelProgress` refuses to invent one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two numbers described one download and the cards showed both at once: the
catalog's hand-written `size_gb` (approximate, and it says so) beside the
job row's `total`, which the worker sums from the live listing for exactly
the files it is fetching. A card could read `~64 GB` next to
`68 GB / 68 GB`, and the only available reading is a download overrunning
its own size. The row is right; the constant is stale.

The rule now lives in one module, `shared/modelSize`, rather than at each
call site: once a RUNNING job for this model reports a positive byte
total, that total is the size shown — otherwise the catalog figure,
otherwise the em-dash the page has always used for an unmeasured model.
`modelSizeHint` also says whether the number is approximate, so "~" and
"About" are dropped over a measured figure rather than hedging it.

`RecommendedCard` and Playground (state line, sidebar cell, Download
button) call it. `liveSizeOverride` carries the same rule to the Hub
search results, whose fallback figure is the Hub's own estimate rather
than the catalog's (a third number, `hubSize.ts`) — that card draws
`ModelProgress` in the slot right below its size, so it had the same
"≈16 GB beside 17 GB / 17 GB" problem. Playground grows a `jobByModel`
map built the way the rest of the page does — by job TITLE, never by
re-deriving job ids in TypeScript. `catalog.py`'s numbers are untouched,
and the Size sort's key still reads the Hub's figure: a sort that
re-ordered the grid mid-download would be a different change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  fused_render
  envinstall.py 1629, 1656, 1745-1749
  fused_render/ai
  supervisor.py 930
  fused_render/ai/runners
  worker_base.py 1164, 1202-1203, 1205, 1209, 1797
Project Total  

This report was generated by python-coverage-comment-action

iamsdas and others added 5 commits August 22, 2026 23:05
Review finding: the single-flight fix covered joiners and left the
symmetric case broken. `envinstall.cancel` does not merely kill the
installer pid — it writes `error: "the install was cancelled"` into the
SHARED record every joiner is polling. So an OWNER's ✕ reached the
joiners just as surely as a joiner's ✕ used to reach the owner: their
next poll read the error and raised past the `_VENV_ROUNDS` loop, and a
download nobody had touched died saying "the install was cancelled".

Ownership is the wrong condition in both directions, so it stops being
the condition. `_install_waiters` counts the bring-ups waiting on each
`envinstall` key; `_hold_install` takes a share (under `_lock`, with the
key that is also the token) and `_release_install` gives it back exactly
once per worker however many threads call it. The install is cancelled by
the LAST waiter to leave, whatever made it leave — a ✕, an eviction,
`unload_all` at shutdown — which keeps the property the cancel exists for
(nothing multi-GB outlives the app; an install nobody wants is not left
running) without ever taking down work somebody else is waiting on.
`Worker.install_owned` survives, but now words the ROW and nothing else.

`_release_install(cancel=False)` covers leaving an install that is already
over, built or failed: there is no installer to stop, and a genuine
resolver error must reach every row verbatim rather than be overwritten or
retried. That is the distinction the code has to keep, and it now comes
from what happened rather than from parsing the error text.

Second review finding, and the reason the first one shipped: the
`shared_install` fixture's `cancel` only appended to a list, where the
real one also marks the shared record done-with-an-error. That fake
certified behaviour production does not have — it let a joiner sail past a
cancellation that would really have killed it. It now does what the real
one does, including refusing an already-`done` record, and with it the
four new/changed tests fail against the previous commit for exactly the
right reasons (the joiner reaches `error`, not `cancelled`).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ait in time

Three review findings on the throttle path, plus what the Hub's own
rate-limit documentation says it sends.

**We parsed the wrong header.** The Hub does not answer a rate limit with
`Retry-After`. It meters request COUNTS over five-minute fixed windows in
three buckets and answers a 429 with the IETF
`RateLimit: "resolvers";r=0;t=N`, `t` being the exact seconds left in the
window — which `huggingface_hub` has parsed since 1.2.0, meaning the
FALLBACK was better informed about the wait than our own fast path.
`_ratelimit_reset_s` reads it, tolerantly (quoted names, any parameter
order, several buckets in one header, and the bucket whose `r=0` is the
one that named the wait we care about), and anything it cannot make sense
of falls through rather than raising or inventing a zero. Precedence is
`RateLimit` `t=` → `Retry-After` → a computed backoff.

**The bounds did not match their own rationale.** 60 attempts at a 60s
ceiling is an hour of one segment sitting still, which is exactly what the
ceiling says it exists to prevent. `THROTTLE_TOTAL_MAX_S` (600s, two of
the Hub's windows) is now the real guarantee; the attempt count stays to
bound the REQUESTS for the case the clock cannot see — a host naming a
wait of a millisecond over and over.

**A stated wait of zero is not a wait.** `t=0`/`Retry-After: 0` means "the
window resets about now" and taken literally it turned the allowance into
an immediate re-request loop against a host that had just refused us. Zero
falls through to the exponential backoff, which is the floor.

**The allowance now comes back on progress**, like `tries` always has.
Without it the 61st 429 of a healthy multi-hour download — an hour and
gigabytes after the 60th — became an ordinary fault and spent the retry
budget falling back to hf.

All of it lives in one `_Throttle`, because the chunk loop is not the only
thing that gets throttled — and on the Hub it is not even the likely one.
The metered bucket is URLs with a `/resolve/` segment; our ranged GETs go
to the presigned CDN location, which has none. So the METADATA call is
where a 429 realistically lands, and it used to raise straight out of
`_segmented_fetch` into the fallback with none of this waiting and none of
the disclosure. `_resolved_meta` is now the single funnel for every Hub
metadata call — the pre-flight resolve and the mid-download re-resolve —
and `_http_status`/`_http_headers` read a status and headers off both
client shapes (urllib's `HTTPError`, huggingface_hub's requests-shaped
one) without this stdlib-only module having to import either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three review findings on the size cell, all of them about the number
meaning what it says.

**A row's total is one PHASE, not the download.** A single download can be
several sequential fetches with a scoped total each — `torch_image.py`'s
GGUF recipe pulls an allow-listed snapshot and then a quantized
transformer out of a second repo — while `catalog.py` documents `size_gb`
as every byte the download fetches across every repo it touches. So
"prefer the live total" would have walked such a card from the whole
figure down to phase one's and then to phase two's, understating the cost
and changing twice, under a tooltip claiming it was "the size this
download is actually fetching". The rule is now that the shown figure
NEVER UNDERSTATES: a live total LARGER than the advertised one is a stale
constant and the row is right; a smaller one is a phase, or a
conservative constant, and either way quoting it would promise a cheaper
download than the user is getting. (No curated entry hits the recipe path
today — the one `_GGUF_RECIPES` row is not in any shortlist — so this is
the rule being right rather than a visible bug being fixed.)

**The two figures were in different units.** `size_gb` is decimal
(4.62e9 → 4.6); `formatSize` is base-1024, which is what every other size
on the page uses, the progress row included. Pressing Download therefore
moved FLUX.2-Klein-4B-4bit's announced size from "4.6 GB" to "4.3 GB" — a
7% drop in the exact number this change exists to stop contradicting
itself. The catalog figure is now converted to bytes and formatted like
everything else, so the cell agrees with the row before and after. The
visible cost, stated: a card reads 4.3 GB where `catalog.py` writes 4.6.
The alternative was a cell in decimal beside a bar in binary, which is
the same defect wearing different digits.

**The Hub search cards keep the Hub's own estimate.** The override added
there during the rebase broke a documented invariant: the size SORT ranks
by `hubSizeBytes`, defined to match exactly what the cell shows, because
"the number beside a name is the only evidence a reader has that a size
sort worked". A running card showing a different figure sits visibly out
of order in a size-sorted grid, so it is dropped — one measurement per
column beats a more-accurate one on whichever row happens to be busy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… file

    chunk = max(CHUNK_BYTES, ceil(size / MAX_CHUNKS_PER_FILE))

`Comfy-Org/MiniMax-H3` is 30 files and 471GB with single files up to
66.3GB, which planned 1,976 segments in ONE file and 14,057 across the
repo. Every segment's cursor lives in the sidecar, and the sidecar is
rewritten whole every `FLUSH_EVERY_S` — so that download serialises ~2,000
dicts a second, per file in flight, for the several hours it runs. Capped,
the same file is 500 × 133MB.

That is the whole justification: sidecar bookkeeping. It is NOT a
rate-limit measure and the comment says so out loud — the Hub meters URLs
carrying a `/resolve/` segment and our ranged GETs go to the presigned CDN
location, so chunk count consumes no quota at all. The metered cost is
about one metadata resolve per file (280 for the largest MiniMax repo,
against 3,000 per five minutes anonymously), which no chunking decision
changes.

Kept deliberately:

  - `_chunks` stays a pure function of `size` — the cap is PER FILE, never
    per repo, because a repo-wide budget would make one file's boundaries
    depend on the file SET, and a resume after any change to that set (a
    scoped download, an `allow_patterns` fetch, a repo that gained a file)
    would re-plan a file whose own bytes never moved and discard its
    recorded progress. The tighter bound is not worth that;
  - the `SEGMENT_MIN_BYTES` short-circuit and the single-segment
    no-range-support path, untouched;
  - the tail fix. 500 units against `MAX_CONNECTIONS = 8` is still sixty
    times more work than connections, which is the property that mattered
    — unlike `_RETIRED_MAX_SEGMENTS_PER_FILE`'s 4, which was BELOW the
    connection count and put a big file back on static shares.

`SIDECAR_VERSION` goes to 3. Boundaries change for every file above
500 × CHUNK_BYTES (~16.8GB), and a version-2 sidecar for such a file has
the right etag, the right size and an internally consistent layout at
every 32MB — the exact input that turns a resume into a silently wrong
blob, so only the version number can reject it.

Accepted cost, stated in the comment: at the 66.3GB extreme a failed chunk
re-fetches up to 133MB instead of 32MB. Every model in today's catalog,
and every file of the 280-file `MiniMaxAI/MiniMax-H3`, is planned exactly
as before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… joined

`_release_install` popped `_install_waiters` under `_lock`, then called
`envinstall.cancel(key)` after releasing it. A fresh `_hold_install` for the
same key — an ordinary reload landing right after a cancel — could complete
its whole `envinstall.start()` + join round trip in that gap, and the
departing worker's cancel would still fire, killing the pid and writing
"the install was cancelled" into the record the reload had just joined.

Fixed with a re-check immediately before the call: `key in _install_waiters`
is true again the instant a new claim registers, so a cancel that arrives
after one is a no-op. Deliberately a second, separate `with _lock:` rather
than one continuous hold across the `envinstall.cancel` call (the "hold the
lock" alternative) — this module never holds `_lock` over I/O (see
`ready_worker`, `_claim_for_removal`), and `cancel` signals a pid and writes
a file. That leaves a much smaller gap than the one being closed (a few
bytecodes vs. an entire `envinstall.start` round trip), which is called out
in the added comment rather than left implicit.

Added a test that reproduces the exact window deterministically: it hooks
`_install_waiters.pop` (the last thing `_release_install` does before
dropping the lock, present in both the old and new code) to run a second
thread's full `_hold_install` rejoin to completion right there, using the
real lock rather than a sleep to force the ordering. Confirmed red against
the unmodified source and green after, 5/5 runs each way with no flakes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@iamsdas
iamsdas marked this pull request as ready for review August 22, 2026 18:45
… sidebar

origin/main's af8d109 (D428, "Playground sidebar: capability sections +
four-fact model cards") rewrote the sidebar cards and stage header —
pg-model-foot/pg-model-size markup, the pg-hero header, and a new four-fact
definition list — replacing the structure our modelSize.ts routing had been
laid over. Took upstream's JSX/markup/comments and re-applied our rule
(modelSizeHint/modelSizeLabel, "never understate") at each size-naming site:
the state line, the sidebar card, the Download button, and the four-fact
list's new Download row (a site that didn't exist when our branch was
written and was left printing a raw size_gb GB before this commit).

The merge also had to restore the jobs-polling scaffolding (isBusy, the
jobs state/effect, fetchJobs) that our size-hint lookups (jobForSelected,
jobByModel) depend on — upstream's rewrite dropped it along with the
ModelProgress bar it fed, which the size rule does not need back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cursor

cursor Bot commented Aug 23, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@iamsdas
iamsdas merged commit 0e767a5 into main Aug 23, 2026
6 of 13 checks passed
@iamsdas
iamsdas deleted the download-throttle-and-progress branch August 23, 2026 05:59
@AkshilVT AkshilVT mentioned this pull request Aug 23, 2026
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