Skip to content

UN-2646 [FEAT] OSS half of the VLM image-answer feature (path contract, loader, vision policy, bridge) - #2218

Merged
chandrasekharan-zipstack merged 12 commits into
feat/llmwhisperer-image-output-adapterfrom
feat/vlm-shared-page-path
Aug 5, 2026
Merged

UN-2646 [FEAT] OSS half of the VLM image-answer feature (path contract, loader, vision policy, bridge)#2218
chandrasekharan-zipstack merged 12 commits into
feat/llmwhisperer-image-output-adapterfrom
feat/vlm-shared-page-path

Conversation

@pk-zipstack

@pk-zipstack pk-zipstack commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

What

The complete OSS half of the VLM image-answer feature (the cloud-only consumer that answers prompts against LLMWhisperer image-output documents by sending the persisted page images to a vision LLM). Four commits, reviewable independently:

  1. Shared page-path contract — promotes build_page_store_dir ({extract_dir}/{stem}/pages) and the page-naming constants (PAGE_GLOB_PATTERN, PAGE_NUMBER_REGEX) from the LLMWhisperer helper to the shared x2text surface; the adapter binds the shared function by identity. The tolerant ZIP-ingest pattern stays adapter-local as a named ZIP_PAGE_MEMBER_REGEX (service wire contract ≠ storage contract).
  2. sdk1 reader utilities — a FileStorage-backed page-image loader (natural sort by integer page index, fail-fast page cap, base64 + complete_vision content blocks with "Page N" labels, typed empty/partial/over-cap errors with cache-bypass + re-billing remediation copy) and a vision-capability policy (LiteLLM local-registry classification: SUPPORTED / UNSUPPORTED / UNKNOWN; hard-block only definitive UNSUPPORTED, warn-and-allow unknown/self-hosted — never get_model_info, which can make network calls for Ollama).
  3. OSS bridge (vlm_image_answer, mirroring the lookup_enrichment bridge with the opposite error policy): detects image mode in the answer_prompt flow by resolving the x2text adapter config via the platform service (payload carries only the instance id), dispatches to the cloud vlm-image-answer executor plugin, and raises structured code-prefixed errors (IMAGE_OUTPUT_REQUIRES_CLOUD, IMAGE_OUTPUT_MISSING, IMAGE_PAGE_CAP_EXCEEDED, VISION_LLM_REQUIRED) when it cannot serve — never a silent fallthrough to the text path. Single-pass extraction is rejected for image-mode profiles. Backend vlm_utils no-op bridge wires: profile vision_warning, deploy-time guard, and re-extraction invalidation.
  4. FE: profile save surfaces a backend-computed vision_warning toast (field never present in OSS responses).

Why

  • Image mode produces per-page PNGs and no text; without a consumer the executor would answer prompts against the one-line extraction summary — the prohibited silent-wrong-answer mode. The bridge guarantees image-mode prompts either reach the vision path (cloud) or fail loudly with an actionable error (OSS).
  • Writer (adapter) and reader (consumer) share one path derivation and one naming contract by identity, so they can never drift — this is what makes the no-transport/no-manifest design safe.
  • The consumer itself is a paid cloud feature; OSS ships only neutral sdk1 utilities, the bridge, and gating (companion cloud PR: Zipstack/unstract-cloud — feat/vlm-image-answer).

How

  • Hook placement: inside _execute_single_prompt, the image-mode branch replaces only retrieval + completion; type conversion, lookup enrichment, webhooks, challenge, and output persistence run unchanged, so image-mode answers behave identically downstream.
  • Detection cache: per (execution_id, adapter_instance_id), bounded — one platform-service call per adapter per run, and adapter edits are picked up on the next execution.
  • Error codes are prefixed onto messages because ExecutionResult.error is string-only; the code survives verbatim to the Prompt Studio socket events and deployment API responses.

Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)

  • No. Text/layout-mode prompts take the vlm_answer is None branch and behave byte-identically (the bridge returns None without a platform call when no x2text adapter id is present, and after one cached resolution otherwise). The path-contract refactor is behavior-preserving — all 57 pre-existing sdk1 image tests pass unmodified. The backend hooks are no-ops without the cloud package. The FE change keys off a response field OSS never emits.

Database Migrations

  • None.

Env Config

  • None required. (VLM_IMAGE_ANSWER_PAGE_CAP is read by the cloud plugin, not OSS.)

Relevant Docs

Related Issues or PRs

Dependencies Versions

  • None added.

Notes on Testing

Unit suites

  • sdk1: 123 pass (26 path-contract + 23 loader incl. stale-listing/TOCTOU regressions + 13 vision-capability + pre-existing image suite with 5 new PDF-sniffing cases, rest unmodified).
  • workers: 13 pass (executor/tests/test_vlm_image_answer_bridge.py — detection, per-execution caching, plugin-absent structured error, error-code mapping, single-pass guard).
  • backend: 20 pass (8 vlm_utils bridge + 12 image-output gating).
  • All with pinned ruff v0.3.4 clean.

Live end-to-end validation (merged-tree Docker stack, MinIO object storage)

Run against a local rig with the companion cloud plugin installed, then removed — every path below verified in the running product:

Path Result
Prompt Studio happy path (1-page invoice) ✅ Correct VLM answer with spatial reasoning ("upper right corner of the invoice…") — conclusive proof the answer came from the page image, since image mode has no extracted text
Multi-page (11-page doc, stem with spaces) ✅ Ordered "Page N" delivery; token cost linear (1,559/page vs 1,570 single-page)
API deployment E2E (7-page PDF via POST /deployment/api/...) ✅ After fixing the extension-less input bug (below); pages persisted under the execution dir on MinIO, answered by the vision LLM
Missing images — empty pages/ IMAGE_OUTPUT_MISSING with cache-bypass + per-page re-billing remediation
Missing images — partial (2 of 11 purged) ✅ Distinct copy with exact missing list [6, 9]; zero VLM cost on failure
Page cap (cap=2 vs 7-page doc) IMAGE_PAGE_CAP_EXCEEDED; zero image reads (fail-fast before any bytes)
Vision gate — run time (Bedrock nova-micro, non-vision) VISION_LLM_REQUIRED naming the model
Vision gate — config time (same model) ✅ Non-blocking vision_warning toast on profile save
Single-pass rejection (with the cloud single-pass executor installed) IMAGE_OUTPUT_UNSUPPORTED_OPERATION before the executor is invoked
Service-side failure (password-protected PDF) ✅ Fail-closed poll surfaced the PDFium error verbatim on the first error status
Text/layout-mode regression ✅ No behavior change
Plugin-absent (pure OSS) image stripped from the served schema (enum + enumNames + conditional block); existing image-mode profile → IMAGE_OUTPUT_REQUIRES_CLOUD (never a silent answer from the summary); adapter save backstop → clean 400

Bugs found and fixed by the live testing (each with regression tests)

  1. Stale object-store listings / TOCTOU (ff7a4774): fsspec's directory cache in the long-lived worker served a stale page listing, so a purged page passed discovery and blew up at read time as a raw FileNotFoundError. Discovery now refreshes the backend listing cache, and a read-time miss maps to the typed PageImageSetIncompleteError.
  2. Extension-less deployment inputs (801ce759): workflow executions store the source file as SOURCE (no extension), so the extension-only PDF guard false-rejected every API-deployment input. The guard now falls back to %PDF- magic-byte content sniffing (fail-closed; no extra read on .pdf-named paths).
  3. Gating 500 on adapter save (4d40c473): the rejection was raised with a bare-string ValidationError from inside to_internal_value, which crashes DRF's error collection into a 500. Now a field-keyed dict detail → clean 400 with the "available only on Unstract Cloud" message.

Known accepted edge

An adapter saved in image mode on cloud and later opened on a plugin-less deployment renders a misleading dropdown state (RJSF displays the schema default while formData still holds the removed image value); submit is correctly rejected with the clean 400, and actively re-picking any mode recovers. Cloud→OSS downgrade only; documented rather than fixed (a fix would mean mutating stored data on read or custom widget work for a rare corner).

Checklist

I have read and understood the Contribution Guidelines.

🤖 Generated with Claude Code

…surface

Foundation for the VLM consumer (MUNS-206): the writer (adapter) and the
upcoming page-image reader must agree on the storage layout without
metadata persistence or a manifest sidecar.

- Promote build_page_store_dir ({extract_dir}/{stem}/pages) from the
  LLMWhisperer helper to unstract.sdk1.adapters.x2text.constants; the
  helper now binds the shared function directly (identity, not a copy)
- Add page-naming constants to ImageOutputConstants: PAGES_SUBFOLDER,
  PAGE_IMAGE_PREFIX/EXTENSION/PADDING, PAGE_GLOB_PATTERN and
  PAGE_NUMBER_REGEX (integer capture for natural sort — lexicographic
  ordering misorders past page 999)
- ImageOutputConfig now aliases the shared layout constants; the
  tolerant ZIP-member pattern moves to a named ZIP_PAGE_MEMBER_REGEX
  (service wire contract, distinct from the storage contract)
- New writer/reader agreement tests: path determinism across stem
  shapes, glob/regex contract incl. >999-page fixtures, and the
  writer's filename builder round-tripping through the reader's
  glob + regex

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: aa070157-05ff-4811-97ad-47e89ca409f9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/vlm-shared-page-path

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

pk-zipstack and others added 3 commits July 28, 2026 18:20
…licy

Reader-side sdk1 utilities for the VLM consumer (MUNS-207 + the sdk1
half of MUNS-208):

- page_image_loader: discovers persisted page_NNN.png files through the
  FileStorage abstraction using the shared naming constants, orders by
  integer page index, enforces a fail-fast page cap before any bytes are
  read, base64-encodes, and shapes complete_vision content blocks with
  'Page N' labels before each image. Typed failures keep empty
  (never-extracted/purged), incomplete (post-write loss, with found vs
  missing pages), and over-cap cases distinct — remediation copy steers
  to cache-bypass re-extraction with the per-page re-billing warning.
- vision_capability: classifies a model as SUPPORTED / UNSUPPORTED /
  UNKNOWN from litellm's local model_cost registry only — never
  get_model_info, which can make network calls for self-hosted
  providers, and never bare supports_vision, which returns False for
  both non-vision and unknown models and would wrongly hard-block
  custom vision models (Ollama, proxies). Policy: hard-block only a
  definitive UNSUPPORTED; UNKNOWN warns and allows.

Tests: 33 new (in-memory S3-like double + real local FileStorage
backend, >999-page ordering, cap-before-read assertion, registry fakes
+ real-registry smoke checks). Full sdk1 image suite: 104 pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Image-mode prompts are answered by a vision LLM via the cloud-only
'vlm-image-answer' executor plugin. This adds the OSS half (mirrors the
lookup_enrichment bridge, with the opposite error policy — lookups
degrade, image mode fails loudly):

- workers vlm_image_answer bridge: detects image mode by resolving the
  x2text adapter config through the platform service (the payload
  carries only the instance id), cached per (execution, adapter);
  dispatches to the plugin with the deterministic pages dir from the
  shared path helper; raises structured, code-prefixed errors
  (IMAGE_OUTPUT_REQUIRES_CLOUD / IMAGE_OUTPUT_MISSING /
  IMAGE_PAGE_CAP_EXCEEDED / VISION_LLM_REQUIRED) that survive the
  string-only error propagation to PS and deployment responses —
  never a silent fallthrough to the text path
- legacy_executor: image-mode branch replaces retrieval + completion
  only; type conversion, lookups, webhooks and challenge run unchanged.
  Single-pass extraction is rejected for image-mode profiles before
  delegating to the cloud single-pass plugin
- backend vlm_utils no-op bridge (lookup_utils pattern) wired into:
  profile serialization (non-blocking vision_warning), API deployment
  creation (deploy-time guard), and the extraction choke point
  (VLM answer invalidation after a fresh extraction rewrites pages/)

Tests: 13 bridge (detection/cache/dispatch/error mapping/single-pass
guard) + 8 vlm_utils (OSS no-ops + cloud delegation policies). Full
sdk1 image suite (104) and gating suite (12) stay green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Profile save responses may carry a backend-computed vision_warning
(image output mode selected with an LLM that is not verifiably
vision-capable — populated by the cloud vlm_image_answer hooks, never
present in OSS responses). Show it as a non-blocking warning toast so
the user learns about the mismatch at config time instead of at run
time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pk-zipstack pk-zipstack changed the title UN-2646 [FEAT] Promote page-image path contract to the shared x2text surface UN-2646 [FEAT] OSS half of the VLM image-answer feature (path contract, loader, vision policy, bridge) Jul 28, 2026
pk-zipstack and others added 3 commits July 29, 2026 14:39
Live-testing regression: with pages purged from object storage, the
loader raised a raw FileNotFoundError instead of the typed
incomplete-set error — fsspec's directory cache served discovery a
stale listing in the long-lived worker, so the contiguity check passed
and the purge only surfaced at read time, bypassing the error mapping.

- discover_page_images now refreshes the backend's listing cache first
  (duck-typed invalidate_cache; no-op for backends without one)
- load_page_images maps a read-time FileNotFoundError (TOCTOU: page
  vanished after discovery) onto PageImageSetIncompleteError with the
  cache-bypass + re-billing remediation
- in-memory test double now raises FileNotFoundError like real backends

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live deployment-path regression: workflow executions store the source
file under an extension-less name (e.g. SOURCE), so the extension-only
PDF guard false-rejected every API-deployment input — a real .pdf
upload failed with the PDF-only error.

_validate_pdf_only now checks the extension first and falls back to
content sniffing (%PDF- magic bytes via FileStorage) when the name has
no .pdf suffix; unverifiable content still rejects (fail-closed). No
extra read on the common .pdf-named path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…re string

Live-testing regression: validate_image_output_allowed raises from
inside AdapterInstanceSerializer.to_internal_value, where DRF folds the
detail into its per-field error mapping — a bare-string detail crashed
error collection (ValueError: dictionary update sequence) and surfaced
as a 500 'Something went wrong' instead of the clean 400 with the
'available only on Unstract Cloud' message. Use a field-keyed dict
detail, and log the rejected adapter/mode for diagnosability.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pk-zipstack
pk-zipstack marked this pull request as ready for review July 29, 2026 10:09
Comment thread workers/executor/executors/vlm_image_answer.py Outdated
Comment thread unstract/sdk1/src/unstract/sdk1/adapters/x2text/page_image_loader.py Outdated
@greptile-apps

greptile-apps Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Implements the OSS-side bridge and shared SDK infrastructure for answering image-output documents with a cloud vision plugin.

  • Adds a shared page-image storage contract, bounded FileStorage loader, and vision-capability policy.
  • Routes image-mode prompts through the optional VLM executor plugin with structured failure codes and downstream processing preserved.
  • Adds backend deployment/profile hooks, re-extraction invalidation, image-mode gating, and a frontend compatibility warning.
  • Fixes the previously reported cache-scoping and page-image byte-bound issues.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; both previously reported issues are addressed by run-scoped cache handling and bounded aggregate page-image reads.

Important Files Changed

Filename Overview
workers/executor/executors/vlm_image_answer.py Adds image-mode detection, run-scoped caching, cloud-plugin dispatch, and structured error mapping; the previously reported stale-cache issue is fixed by run scoping and no-scope cache bypass.
workers/executor/executors/legacy_executor.py Integrates VLM answers into the existing prompt execution path while retaining normal conversion, enrichment, webhook, and persistence behavior.
unstract/sdk1/src/unstract/sdk1/adapters/x2text/page_image_loader.py Adds ordered page discovery, completeness and count checks, bounded aggregate reads, base64 encoding, and typed loader failures; the previously reported unbounded-byte issue is addressed.
unstract/sdk1/src/unstract/sdk1/adapters/x2text/constants.py Defines the shared page-path and naming contract used by both image writers and readers.
unstract/sdk1/src/unstract/sdk1/utils/vision_capability.py Classifies vision support using LiteLLM's local registry and blocks only definitively unsupported models.
backend/prompt_studio/vlm_utils.py Adds optional cloud-hook bridges for compatibility warnings, deployment validation, and answer invalidation.
backend/prompt_studio/prompt_studio_core_v2/prompt_studio_helper.py Stamps image-mode metadata, preserves the original extraction path, and invokes answer invalidation after successful re-extraction.
frontend/src/components/custom-tools/add-llm-profile/AddLlmProfile.jsx Displays the backend-provided vision compatibility warning after a successful profile save.

Sequence Diagram

sequenceDiagram
    participant UI as Prompt Studio / Deployment
    participant Backend
    participant Worker as Executor Worker
    participant Bridge as VLM Bridge
    participant Plugin as Cloud VLM Plugin
    participant Store as FileStorage
    participant LLM as Vision LLM

    UI->>Backend: Run image-mode prompt
    Backend->>Worker: Dispatch answer_prompt
    Worker->>Bridge: Resolve image output mode
    Bridge->>Plugin: run_with_metrics(...)
    Plugin->>Store: Discover and bounded-read page images
    Store-->>Plugin: Ordered page PNGs
    Plugin->>LLM: Prompt and page-image blocks
    LLM-->>Plugin: Answer
    Plugin-->>Worker: VLM answer and usage
    Worker-->>Backend: Persist and return normal prompt result
    Backend-->>UI: Answer or structured image-mode error
Loading

Reviews (5): Last reviewed commit: "UN-2646 [FIX] Bound page-image reads to ..." | Re-trigger Greptile

pk-zipstack and others added 2 commits July 29, 2026 16:42
… budget

Two review findings on the consumer path:

- Run-scoped detection cache (P1): IDE payloads carry no execution_id,
  so every IDE run shared one ('', adapter) cache entry — an adapter
  switched between text and image output kept serving the stale mode
  until worker restart. The cache key now scopes to execution_id or,
  for IDE runs, the run_id (deduplicating the N per-prompt resolutions
  within one run — the cache's actual purpose — with no cross-run
  reuse), and skips caching entirely when no scope id exists.
- Aggregate byte budget on image loading (P2): the page cap bounds the
  COUNT of images, not their size. load_page_images now enforces a
  50MB (default, disable-able) raw-byte budget while reading, raising
  the typed PageImageSetTooLargeError with page-range remediation; the
  bridge maps it to a distinct IMAGE_PAGES_TOO_LARGE error code.

Tests: run-scoped vs no-scope cache behavior, byte-budget stop-at-page
accounting, and the new error-code mapping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y read

Greptile follow-up: the read-time budget still pulled each full object
into worker memory before checking cumulative size, so one pathological
page could spike RAM before rejection. The budget is now enforced twice:
first from storage size metadata (object HEAD — zero bytes transferred)
before any read, so an oversized set is rejected with no image bytes in
memory; the read-time accounting stays as a belt-and-braces guard for
backends without size metadata and for stat/read races.

Tests: metadata pre-check rejects with zero reads; no-size() backends
still enforced at read time; unreadable metadata falls back cleanly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pk-zipstack

Copy link
Copy Markdown
Contributor Author

Re: Confidence 3/5 — "an oversized individual page can still cause an uncontrolled worker-memory allocation before the new byte limit rejects it"

Addressed in 46c34f2. The byte budget is now enforced from storage size metadata (object HEAD) before any read — an oversized page set is rejected with zero image bytes entering worker memory. The read-time accounting remains as a belt-and-braces guard for backends without a size() API and for stat/read races. Regression tests pin all three paths: metadata pre-check rejects with reads == [], no-size() backends still enforce at read time, and unreadable metadata falls back cleanly.

@chandrasekharan-zipstack chandrasekharan-zipstack 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.

Reviewed both halves (#2218 + Zipstack/unstract-cloud#1690) against the live code paths.

The architecture holds up well. Binding writer and reader to one build_page_store_dir (with a test asserting is identity, not equality) is the right way to make a two-sided storage contract undriftable, and the fail-loud-never-fall-through error policy is applied consistently across the bridge, the loader and the gating. Gate-before-read with a test asserting fs.read_calls == [], and the error-code prefixing to survive string-only ExecutionResult.error propagation, are both nicely done.

Verified and holding: fs.ls / fs.size / fs.read(length=) all exist on FileStorage; complete_vision / get_model_name / get_metrics match the SDK1 signatures; the TOCTOU except FileNotFoundError works because skip_local_cache re-raises FileNotFoundError after its invalidate-and-retry, and the new invalidate_cache in discovery covers the stale-positive listing case that decorator does not; the DRF dict-detail fix is the correct shape for a raise inside to_internal_value.

Six comments inline plus one NIT roll-up. My ranking: #1 is a blocker — user-reachable, misleading, and costs money to discover. #2 and #3 are design decisions I would like stated rather than left as defaults. Everything else is cleanup.

Comment thread workers/executor/executors/vlm_image_answer.py Outdated
Comment thread workers/executor/executors/vlm_image_answer.py
Comment thread workers/executor/executors/legacy_executor.py
Comment thread workers/executor/executors/legacy_executor.py Outdated
Comment thread backend/prompt_studio/vlm_utils.py
Comment thread unstract/sdk1/src/unstract/sdk1/adapters/x2text/constants.py Outdated
pk-zipstack and others added 3 commits July 31, 2026 12:47
… image-mode scoping

Addresses Chandrasekharan's review on the consumer path:

1. Pages directory keys on a never-rewritten extract path (blocker):
   summarize-as-source and smart-table runs rewrite the payload
   file_path before the answer step, so the reader looked in the wrong
   directory and surfaced IMAGE_OUTPUT_MISSING with a paid remediation
   that could not help. The payload builders (IDE single/bulk and the
   structure tool task) now stamp an explicit extract_file_path,
   captured before any rewrite; the bridge derives pages/ from it, with
   the old file_path as fallback for older payloads.

2. Text-mode prompts no longer depend on the platform service: the
   backend stamps the x2text adapter's output_mode per prompt (it
   already holds the decrypted metadata), so detection is payload-only
   for stamped runs; the run-scoped platform resolution remains as the
   fallback for unstamped payloads (API deployments, older payloads).

3. Challenge and evaluation are skipped in image mode with a visible
   log line — both verify an answer against retrieval context, which
   image mode does not have; running them billed a doomed second LLM
   call. A vision-aware challenge is a later-phase decision.

4. Retrieval adapters are no longer constructed for image-mode
   prompts: detection now runs before adapter init, which proceeds
   LLM-only (embedding/vector DB skipped) when image mode is detected.

5. Profile-save toasts no longer swallow each other: one alert renders
   — 'Saved — check LLM compatibility' with the warning when present,
   plain success otherwise (the alert store holds a single entry, so
   two synchronous calls batched into showing only the last).

6. vlm_utils logs a distinct warning when plugins.vlm_image_answer is
   present but backend_hooks fails to import — 'cloud hooks broken' is
   no longer indistinguishable from 'running OSS'.

Bridge API split accordingly: detect_image_mode_config (stamp fast
path + platform fallback) and run_vlm_image_answer (dispatch only,
keyed on extract_file_path). Tests: 23 bridge (stamped/unstamped
detection, cache scoping, extract-path keying) all green; sdk1 130;
backend 20.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Greptile's follow-up was right: the metadata pre-pass fell back to an
unbounded full-object read when the size lookup failed, so one
pathological object could still spike worker memory before the budget
check. Replaced both layers with a single stronger mechanism — bounded
reads: every page is read with length = remaining budget + 1 via
FileStorage.read, giving a hard allocation ceiling of budget + 1 bytes
for the whole loop, independent of object sizes or the backend's size
metadata. The metadata pre-pass is gone (also Chandrasekharan's call —
one mechanism, less code).

Also folds in the review nits: dead PAGE_GLOB_PATTERN removed (readers
list + regex, never glob) and the duplicated not-found message
extracted to one helper.

Tests: bounded-read guarantees pinned (single 10MB object → exactly 51
bytes read; aggregate reads <= budget + 1) — 127 sdk1 + 23 bridge pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Aug 5, 2026

Copy link
Copy Markdown

@chandrasekharan-zipstack
chandrasekharan-zipstack merged commit a92f347 into feat/llmwhisperer-image-output-adapter Aug 5, 2026
6 checks passed
@chandrasekharan-zipstack
chandrasekharan-zipstack deleted the feat/vlm-shared-page-path branch August 5, 2026 11:36
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.

2 participants