Skip to content

fix(models): validate mmproj GGUF integrity on download and move-failure#18

Merged
fl4p merged 1 commit into
mainfrom
fix/mmproj-download-reliability
Jul 4, 2026
Merged

fix(models): validate mmproj GGUF integrity on download and move-failure#18
fl4p merged 1 commit into
mainfrom
fix/mmproj-download-reliability

Conversation

@fl4p

@fl4p fl4p commented Jul 3, 2026

Copy link
Copy Markdown
Owner

What & why

Multimodal vision models ship a separate mmproj (multimodal projector) sidecar next to the main GGUF. A corrupt, truncated, or wrong sidecar makes the model load as broken-vision. This hardens the sidecar lifecycle so a bad file is caught and a good one is never destroyed.

Changes (src/services/modelManager/download.ts)

  • GGUF magic-byte validation (hasGgufMagic) added alongside the existing size check. A read that throws or returns a short/empty string is treated as inconclusive (iOS RNFS.read has a known NSInteger bridging bug that can manifest either way), so a misread never deletes a valid file.
  • Pre-download gate (checkMmProjExists): a present-but-invalid sidecar (missing / partial / bad magic) is deleted so it re-downloads clean. This path exclusively owns the file.
  • Move-failure recovery: when the mmproj move fails (a valid file may already sit at the path — a re-download, or another model sharing it), validate the existing target read-only (mmProjFileValid) — reuse it if valid, downgrade to text-only if not, but never delete it. llama.rn re-validates on load.

Scope note

This ports only the useful reliability logic from upstream off-grid-ai#316. Dropped from that PR: its stale DownloadManagerModule.kt rewrite (would regress main's newer download-id/observer refactor) and unrelated website churn. Its shared-mmproj delete guard is already present in modelManager (index.ts), so it's not re-added here.

Tests

__tests__/unit/services/parallelMmproj.test.ts — added move-failure recovery coverage: valid GGUF (reuse), bad magic (downgrade, no delete), size mismatch + good magic (downgrade, no delete), read throws / short read (inconclusive → keep vision), missing target (downgrade). npm test for the download suites: 202 passing; eslint clean.

Guard multimodal projector (mmproj) sidecars against corrupt/partial files:

- Add GGUF magic-byte validation (hasGgufMagic) alongside the size check.
  A read that throws or returns a short string is inconclusive (iOS
  RNFS.read has a known NSInteger bridging bug), so a misread never
  destroys a valid file.
- Pre-download gate (checkMmProjExists) deletes a present-but-invalid
  sidecar so it re-downloads clean.
- mmproj move-failure recovery validates the existing target READ-ONLY
  (mmProjFileValid): reuse if valid, downgrade to text-only if not, but
  never delete it - the target may be shared with another model or be
  valid-but-misread, and llama.rn re-validates on load.

Ports the reliability logic from upstream off-grid-ai#316 (dropping its stale Kotlin
and website churn); the shared-mmproj delete guard it also proposed is
already present in modelManager.

Adds move-failure coverage for valid / bad-magic / short-read /
size-mismatch / missing-target paths (parallelMmproj: 42 tests).
@fl4p
fl4p merged commit 578e669 into main Jul 4, 2026
fl4p added a commit that referenced this pull request Jul 4, 2026
#23)

Two defects in the mmproj GGUF integrity validation (#18), found in post-merge review:

1. The catch-up guard (mmproj completes natively before the onComplete listener
   registers) only checked target existence on move-failure, silently keeping a
   corrupt-but-present sidecar as a vision projector. It now runs the same
   read-only validation as the onComplete handler. Both paths share a new
   reconcileMmProjMoveFailure() so they can't drift again.

2. mmProjFileValid / checkMmProjExists skipped the size check when expectedSize
   was 0/undefined (reachable on crash-restore), so a 0-byte/truncated stub with
   an inconclusive magic read passed as valid. Added an absolute MIN_MMPROJ_FILE_SIZE
   (1 KB) floor, independent of expectedSize, mirroring llmSafetyChecks.

Tests: catch-up now downgrades-without-deleting on a corrupt present target, and
rejects a sub-1KB stub when expectedSize is unknown.
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