Fix vision captioning on OpenAI-compatible backends: media parsing + image format - #67
Open
LegalInspiration wants to merge 2 commits into
Conversation
…image format Two independent bugs meant Vision mode was effectively non-functional on any non-Ollama backend (OpenAI-compatible, e.g. local llama.cpp llama-server): 1. Media JSON parsing was case-sensitive (LLMAPICalls.cs, MagicPromptPhoneHome). The real browser client always sends lowercase keys (type/data/mediaType), but the media list was bound via ToObject<List<MediaContent>>() against the PascalCase MediaContent class, which silently failed to bind and left Media empty - meaning no real UI request ever actually attached an image. 2. The OpenAI-compatible path defaulted to WEBP for vision images (BackendSchema.cs, CompressImageForVision / OpenAICompatibleRequestBody). Confirmed via isolated A/B testing (same image, same model, only the format varied) that at least one real vision model's decode pipeline mishandles WEBP even at near-lossless quality, producing confident, detailed, wrong descriptions. JPEG was reliable in the same test, and is what OllamaRequestBody already uses for its own vision path - this brings the OpenAI-compatible path in line with that already-proven choice. Also tuned two settings already flagged with TODOs in the original code: - maxDimension 256 -> 1024 (too aggressive a downscale for modern encoders) - compression quality 40/60 -> 90 (quite lossy on top of that downscale) - vision-call temperature 1.0 -> 0.2 (factual captioning wants low/deterministic sampling, not the creative-chat default; scoped to Vision messages only) Tested via direct calls to /API/MagicPromptPhoneHome using the exact lowercase-key JSON shape the real browser sends, plus through the live SwarmUI UI in all three MagicPrompt modes (chat, vision prompt, auto-caption) against a local OpenAIAPI-compatible backend (llama.cpp llama-server). Known caveat (not fixed here): JPEG has no alpha channel, so an image with real transparency will have its alpha silently dropped, and any RGB data sitting under fully-transparent pixels can "leak through" into the encoded JPEG. Verified with an isolated ImageSharp unit test: a PNG built with a solid blue background and a circular region at alpha=0 but RGB=(0,255,0) round-tripped through the same SaveAsJpeg call this fix uses and came out with that hidden green visible; sending it through the live vision pipeline produced a confident, detailed, and entirely wrong description of "a green circle on a blue background." A clean fix would explicitly composite alpha onto a solid background (or preserve PNG) before the JPEG encode - straightforward, but out of scope for this PR since it wasn't hit in the reporter's own use case.
📝 WalkthroughWalkthroughVision requests now use larger JPEG-compressed images and lower temperature values. ChangesVision request updates
Media payload parsing
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@BackendSchema.cs`:
- Around line 166-171: Update the image URL construction in the media content
flow around CompressImageForVision so fallback data retains its actual MIME type
instead of always being labeled according to preferPngForBase64. Return the
compressed/unchanged encoded data together with the resolved MIME type, or use
media.MediaType for fallback URLs, while preserving the current preferred
PNG/JPEG behavior for successful conversions.
- Around line 77-91: Update the XML documentation and return comment for the
image compression method containing maxDimension and quality so they document
the new JPG format alongside the supported PNG and WEBP formats, and remove the
stale statement that the method returns WEBP specifically. Keep the
documentation aligned with the method’s actual format-selection behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ab7eaef7-5cc6-4a10-842d-58da84ae5df6
📒 Files selected for processing (2)
BackendSchema.csWebAPI/LLMAPICalls.cs
CompressImageForVision now returns the compressed data together with its actual resulting MIME type, instead of letting callers assume one from targetFormat. The fallback paths (non-image media, or a conversion exception) return the original untouched bytes - previously the OpenAI- compatible caller would still label those bytes as image/png or image/jpeg regardless of their real format, which could cause a backend to reject or misdecode the payload. All three callers (Ollama, OpenAI- compatible, Anthropic) updated to use the returned MIME type. Also updated the method's XML doc comment, which still described only PNG/WEBP as valid targetFormat values and referenced a WEBP-specific return prefix, both stale since the previous commit added JPG as a real, used value. Verified via a clean rebuild and a live vision test against the same OpenAIAPI-compatible backend used for the original fix - no regression.
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.
NOTE: An AI model, specifically Claude Code, was used to investigate this bug, test fixes, and generate the first draft of this report. A human being read every single line in it, so if there's something wrong with it, blame the meatsack.
ALSO NOTE: CodeRabbit found two issues related to the code I touched. While I didn't cause any new issues, I didn't want to leave them hanging, especially one flagged as major, so there is a second commit which addresses them.
Summary
After several attempts and different configuration pass, I could not get captioning or vision mode to work while using my local llama.cpp instance, a local llama.cpp
llama-serverexposing an OpenAI-style endpoint. Symptoms: either the model insisted that no image was uploaded, or it confidently returned a detailed description that doesn't match the actual uploaded image, differently wrong on every attempt. In my limited experience, this usually indicates that the vision model doesn't have an actual/parsable image to work from, so it basically hallucinates one from its weights.Investigation, for which I also used Claude Code, found two independent, confirmed bugs, both reproduced directly against
/API/MagicPromptPhoneHome(bypassing the browser) and fixes for both are presented here.Please note: the README specifically already flagged "If you are using any LLM service other than Ollama, I cannot guarantee that it will work." This PR is exactly that unsupported path — filed with a full diagnosis and working patch in case it's useful, not a complaint.
Bug 1 — media parsing was case-sensitive, but the client always sent lowercase keys
WebAPI/LLMAPICalls.cs,MagicPromptPhoneHome:MediaContentis PascalCase (Type/Data/MediaType). The actual browser JS (Assets/magicprompt.js,createRequestPayload) always sends lowercase:Confirmed via direct testing: lowercase keys always produced an empty/unbound
Medialist — no exception, request just proceeded as if no image was ever attached. Sending an Identical payload with PascalCase keys parsed fine. Since the real client never sent PascalCase, every real vision request from the UI hit this — no image ever actually reached the vision backend in normal use, regardless of anything downstream.Fix: explicit case-insensitive property lookup instead of the case-sensitive
ToObject<T>()call.Bug 2 — WEBP is mishandled by at least one real vision model's decode pipeline, independent of quality/resolution
Once bug 1 is fixed and an image actually attaches, captioning becomes unreliable rather than absent — confident, detailed, differently-wrong descriptions each time. Isolated via systematic A/B testing, holding every variable constant except one at a time:
This lines up with something already in the codebase —
OllamaRequestBodyalready hardcodes JPEG:— while
OpenAICompatibleRequestBodydefaulted to WEBP foropenai/openaiapi/openrouter. My read: Ollama's choice wasn't a deliberate fix for this, it just never touched WEBP in the first place, so it never hit whatever's going on in that decode path (plausibly a llama.cppclip.cpp/mtmd-side WEBP handling gap, not something fixable in this extension beyond avoiding the format).Fix: OpenAI-compatible path now defaults to JPEG, matching Ollama's already-working choice.
Also tuned: two settings already flagged with your own TODOs
CompressImageForVision:maxDimension256→1024,quality40/60→90. 256px/q40 is likely fine for older CLIP-ViT-era encoders (224–336px native) but throws away real detail for newer higher-resolution-capable models.OpenAICompatibleRequestBody, vision branch only:temperature1.0→0.2. Factual captioning wants low/deterministic sampling; high temperature turned a marginal image into confidently-wrong, differently-wrong-each-time answers. Text-only chat and Ollama's own path are untouched, deliberately out of scope.Testing
Unit-level (isolated, no SwarmUI involved): built a synthetic PNG (blue background, alpha=0 circular region with hidden RGB=(0,255,0)) and ran it through the exact same ImageSharp
SaveAsJpegcall this fix uses, in isolation. Confirmed the alpha channel is silently dropped and the hidden green shows through (see caveat below) — this is a genuine ImageSharp/JPEG behavior, not something introduced by this patch, but worth knowing about given the format change.Integration-level (real, live SwarmUI, actual vision model): applied the fix to a running install, rebuilt the extension, and tested through the real UI in all three MagicPrompt modes — chat, vision-prompt, and auto-caption — against a local OpenAIAPI-compatible backend. Then re-verified directly via
/API/MagicPromptPhoneHomeusing the exact lowercase-key JSON shape the real browser sends: 3/3 clean, accurate, consistent descriptions of a real test image, versus repeated failures/wrong descriptions before the fix, across multiple structural variations.Known caveat — not fixed here, flagging for visibility
JPEG has no alpha channel. Once bug 2's fix lands, any image with real transparency will have its alpha silently dropped by the encoder, and whatever RGB happens to sit under fully-transparent pixels can "leak through" into the encoded JPEG — potentially producing a confidently-described object that was never actually visible in the source image. Confirmed both in isolation (unit test above) and end-to-end: sending that same synthetic image through the live vision pipeline produced a detailed, confident description of "a bright green circle on a solid blue background" — an object that doesn't exist in the actual image; the green was invisible authoring data hidden under alpha=0.
This isn't something this PR introduces (WEBP has the same limitation for lossy mode, and was already broken for an unrelated reason), and it wasn't hit in my own use case, so I left it out of scope. A real fix looks straightforward, though: detect an alpha channel and explicitly composite it onto a solid background (or fall back to PNG) before the JPEG encode, rather than leaving the dropped-alpha behavior undefined. Happy to take a pass at that too if it's wanted — didn't want to bundle an untested fix for a problem I didn't personally hit into the same PR as the confirmed, tested one.
Summary by CodeRabbit