[Bug]: ModelScope: a truncated shard is renamed to its final name — HEAD gives no Content-Length (total=0 in download())
Maintainers usually reply within 3 days
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 70/100
Research direction
Start in download() in setup.py, where total stays 0 because the ModelScope HEAD response has no Content-Length, then read ms_meta() and verify_sha256(). The suggested fix uses the published size from ms_meta() as total, so the existing resume loop and the final size check apply again. Done when a stream that ends early after partial data resumes with a Range request instead of being renamed to the final name, and a short file is never promoted to dst.
Written by the indexing model from the issue text.
Description
What happened
Installing with STRATA_SOURCE=modelscope on a mainland-China network (Windows), --family coder --model IQ1_M --context 65536 --vision no. Step 5, downloading IQ1_M/Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M-00001-of-00002.gguf (29,608,446,496 bytes per ModelScope's file API): the stream ended cleanly at 5,466,520,256 bytes after ~7 minutes (~13 MB/s). No download interrupted (...); retrying in 10 s ... line appeared — the socket simply reached EOF — and setup then printed:
Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M-00001-of-00002.gguf: 5466.5 MB
[X] Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M-00001-of-00002.gguf is 5,466,520,256 bytes, not 29,608,446,496
delete it and run setup again (the download restarts)
Expected: an interrupted download resumes (as it already does when total is known), and a short file is never renamed to its final name.
What made this possible: ModelScope's /resolve/ HEAD responses carry no Content-Length (verified with curl, with and without an HTTP proxy):
$ curl -sI https://www.modelscope.cn/models/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-Coder-GGUF/resolve/master/IQ1_M/Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M-00001-of-00002.gguf
HTTP/1.1 200 OK
Date: Sun, 11 Oct 2026 02:46:47 GMT
Set-Cookie: acw_tc=...
X-Linked-Etag: e11083ba855e7666b48ea3f2db6a9c3a20c18751a012cc24f948de91b7087fad
...no Content-Length anywhere in the headers
So in download() (setup.py @ 61b3fb5) total stays 0, and with total == 0 the whole safety net is off:
- the progress line shows only megabytes —
5466.5 MBwith no denominator — which is the visible tell; - the first clean EOF counts as finished:
if not total or have >= total: break(line 1522). A connection reset would raiseOSErrorand be retried withRange, but a clean FIN after partial data looks exactly like a complete download; - the end-of-transfer size check is skipped:
if total and part.stat().st_size != total:(line 1528); part.replace(dst)(line 1531) promotes the 5.4 GB fragment to the shard's real filename.
Only afterwards does verify_sha256()'s size check against ms_meta() (line 1634) fail. And because the .part has already been renamed away, the "delete it and run setup again (the download restarts)" instruction discards the 5.4 GB that a .part-aware message could have resumed. (Manual recovery does work: rename the fragment back to …gguf.part and rerun — I did exactly that after the fix below, and the download resumed from 5.47 GB, ~17 MB/s on a direct connection.)
This is the same total == 0 defect as #1797 (GitHub's codeload endpoint), a third trigger host and a different aftermath: there, a truncated archive's .done mark sticks and every rerun dies with BadZipFile; here, a truncated GGUF shard silently receives its final name and the user is told to restart a 29.6 GB download that could have resumed.
Suggested fix. ms_meta() already knows the file's published size before the download starts (and download() consults it right after, for the hash). Use it as total when HEAD has none:
part = dst.with_name(dst.name + ".part")
- total = 0
- for attempt in range(5):
+ meta = ms_meta(*ms) if ms is not None else None
+ total = meta[0] if meta and meta[0] else 0 # ModelScope's HEAD gives no Content-Length: use its published size
+ for attempt in range(5 if not total else 0):
and reuse meta at the end of the function instead of fetching it again:
part.replace(dst)
- meta = ms_meta(*ms) if ms is not None else None
if meta is not None and meta[1]:
With total set, the existing retry loop already does the right thing on a premature EOF (it re-requests with Range: bytes={have}- and appends), so no other change is needed — the three skipped defences (progress denominator, resume-on-early-EOF, final size check) all come back. A more general variant for any host that strips the length: when total is still 0, parse it from the first 206 response's Content-Range: bytes 0-…/29608446496.
Nice-to-have: when the final check does fail, the message could offer "keep it as <name>.part to resume" instead of "delete it".
Strata version, GPU, OS
setup.py @ 61b3fb5 (main, 2026-10-10; ready-made engine 0.1.42) · RTX 5070 Ti 16 GB · Windows 10.0.26200 · Python 3.11.15 · STRATA_SOURCE=modelscope, --family coder --model IQ1_M --context 65536 --vision no
Log
The excerpt above is the whole failure: the progress line without a denominator, then the size check. The run's setup.log contains no download interrupted line — the early close was a clean EOF. For comparison, the same file after the suggested fix (resumed, minutes into the rerun):
Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M-00001-of-00002.gguf: 5.97 / 29.61 GB (20%)
- Dominant language
- C++
- Stars
- 11.6k
- Forks
- 1k
- PR merge metrics
- No merged PRs in 30d
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from Niko1221/Strata
-
[Other] error detected in the compilation of Strata/src/qwen35/q36_engine.cuPossibly taken A pull request linked to this issue is open or already merged. Openneeds-format
Difficulty 2/5 1-3 hours Newbie friendliness 69/100
Niko1221/Strata#3368 · 1 comment ·
Maintainers usually reply within 3 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
Maintainers usually reply within 3 days
-
Difficulty 2/5 1-2 days Newbie friendliness 75/100
Maintainers usually reply within 3 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
Niko1221/Strata#3269 · 2 comments ·
Maintainers usually reply within 3 days
-
Difficulty 1/5 Under an hour Newbie friendliness 64/100
Niko1221/Strata#3197 · 3 comments ·
Maintainers usually reply within 3 days
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
category:port-update
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 2 days
-
(S1 - Need confirmation)
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
CleverRaven/Cataclysm-DDA#89194 ·
Maintainers usually reply within 1 day
-
reproduced
Difficulty 1/5 Under an hour Newbie friendliness 67/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day