Skip to content

Commit 3d20bdd

Browse files
committed
Pin the MIME detection of Ollama images
toImageDataUrl labels a bare base64 image by its magic bytes (JPEG, GIF, WebP, else PNG) and passes an existing data URL through, but the only test sent a PNG: swapping the JPEG and GIF literals or dropping the data: check kept the suite green. Two unit tests now assert each label and the passthrough. The passthrough is the case a client would notice, since llama.cpp rejects a doubled data: prefix while it never reads the subtype. Swapped literals, a missing WebP branch and a missing passthrough each turn them red (e.g. "but: was \"data:image/png;base64,data:image/jpeg;base64,/9j/4AAQ\""); unchanged, 46/46 in OllamaApiSupportTest are green. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
1 parent 1543695 commit 3d20bdd

2 files changed

Lines changed: 40 additions & 0 deletions

File tree

‎BUGHUNT-FOUND.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ Repos plus eins. Geprüfte Klassen ohne Fund stehen in [`BUGHUNT-CHECKED.md`](BU
235235
- **Beschreibung:** Ollama-Clients schicken Bilder als `"images": ["<base64>", …]` neben `"content"`. `toOpenAiMessage` kopierte die Nachricht unverändert; der OpenAI-Parser von llama.cpp liest nur `image_url`-Content-Parts, das Feld `images` wurde also ignoriert. Das Vision-Modell antwortete ohne Bild, kein Fehler. Gleichzeitig meldet `/api/show` die Fähigkeit `vision`, wenn `--mmproj` gesetzt ist. Fix: `images` wird entfernt und als `image_url`-Parts (Data-URL, MIME-Typ aus den Magic-Bytes: JPEG/GIF/WebP, sonst PNG – llama.cpp dekodiert nach Inhalt) hinter dem Text angehängt.
236236
- **Reproduktion:** `OllamaApiSupportTest#toOpenAiChatRequestTurnsOllamaImagesIntoImageUrlParts` (vorher rot, danach grün); `OllamaApiSupportTest` 20/20, `OpenAiCompatServerHttpTest` 43/43. Ende-zu-Ende mit echtem Vision-Modell nicht geprüft (kein Modell im Sandbox).
237237
- **Nebenbefund (nicht geändert):** Die gestreamte Abschlusszeile (`toOllamaDoneLine`) setzt `done_reason` immer auf `"stop"`, auch bei Abbruch durch `length`; die nicht-gestreamte Antwort übernimmt `finish_reason` korrekt.
238+
- **Nachtrag (Branch-Review 2026-10-20):** Die MIME-Erkennung aus den Magic-Bytes und der Durchlass einer fertigen `data:`-URL hatten keinen Test; der PNG-Test oben nimmt nur den `else`-Zweig (Fund J3-6). `OllamaApiSupportTest#ollamaImageMimeTypeFollowsTheMagicBytes` pinnt `/9j/` → `image/jpeg`, `R0lGOD` → `image/gif`, `UklGR` → `image/webp` und den PNG-Rückfall; `#ollamaImageThatIsAlreadyADataUrlIsPassedThrough` den Durchlass — der einzige Zweig mit sichtbarer Wirkung, denn ein doppeltes `data:` lehnt llama.cpp ab, während es den Untertyp nie liest. Gegenprobe rot: vertauschte JPEG/GIF-Literale (`Expected: is "data:image/jpeg;base64,/9j/4AAQ" but: was "data:image/gif;base64,/9j/4AAQ"`), fehlender WebP-Zweig (`… but: was "data:image/png;base64,UklGRh4A"`), fehlender Durchlass (`… but: was "data:image/png;base64,data:image/jpeg;base64,/9j/4AAQ"`); unverändert 46/46 in der Klasse grün.
238239
- **Status:** gefixt
239240
- **Commit:** java-llama.cpp `ef7295c`
240241

‎llama/src/test/java/net/ladenthin/llama/server/OllamaApiSupportTest.java‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,45 @@ public void toOpenAiChatRequestTurnsOllamaImagesIntoImageUrlParts() throws IOExc
146146
assertThat(message.has("images"), is(false));
147147
}
148148

149+
/**
150+
* The MIME type of a bare base64 image follows its magic bytes. llama.cpp decodes the payload by
151+
* content, so a wrong subtype would still work; these cases keep the labels honest.
152+
*/
153+
@Test
154+
public void ollamaImageMimeTypeFollowsTheMagicBytes() throws IOException {
155+
String[][] cases = {
156+
{"/9j/4AAQ", "image/jpeg"},
157+
{"R0lGODlh", "image/gif"},
158+
{"UklGRh4A", "image/webp"},
159+
{"AAAA", "image/png"}
160+
};
161+
for (String[] imageAndMimeType : cases) {
162+
assertThat(
163+
imageAndMimeType[0],
164+
imageUrlOf(imageAndMimeType[0]),
165+
is("data:" + imageAndMimeType[1] + ";base64," + imageAndMimeType[0]));
166+
}
167+
}
168+
169+
/**
170+
* An image that already is a data URL passes through unchanged: a second {@code data:} prefix makes
171+
* llama.cpp reject the whole request ("Invalid uri-encoded base64 value"), so this is the case a
172+
* client would notice.
173+
*/
174+
@Test
175+
public void ollamaImageThatIsAlreadyADataUrlIsPassedThrough() throws IOException {
176+
String dataUrl = "data:image/jpeg;base64,/9j/4AAQ";
177+
assertThat(imageUrlOf(dataUrl), is(dataUrl));
178+
}
179+
180+
private static String imageUrlOf(String image) throws IOException {
181+
JsonNode openAi = OllamaApiSupport.toOpenAiChatRequest(read(
182+
"{\"messages\":[{\"role\":\"user\",\"content\":\"what is this?\",\"images\":[\"" + image + "\"]}]}"));
183+
JsonNode part = openAi.path("messages").get(0).path("content").get(1);
184+
assertThat(part.path("type").asText(), is("image_url"));
185+
return part.path("image_url").path("url").asText();
186+
}
187+
149188
@Test
150189
public void toOpenAiChatRequestMapsFormatJsonToResponseFormat() throws IOException {
151190
JsonNode openAi = OllamaApiSupport.toOpenAiChatRequest(

0 commit comments

Comments
 (0)