Skip to content

Commit 6f86534

Browse files
chitalianJustin Torreclaude
authored
fix: render Anthropic base64 images in request viewer (#5555)
* fix: render Anthropic base64 images in request viewer The Anthropic parser was extracting raw base64 data from message.source.data without including the data: URL prefix or mime_type. The ChatMessage ImageContent component requires either a proper data URL (with 'base64,' in it) or an http(s) URL. Changes: - Extract mime_type from message.source.media_type (default to image/png) - Format base64 data as proper data URL: data:{mimeType};base64,{data} - Store base64 data in content field and mime_type for fallback rendering Fixes image rendering in browser automation sessions and other Anthropic multimodal requests. * debug: add logging for Anthropic image parsing * fix: add unoptimized flag for data URL images and debug logging * fix: move useMemo hook before early return to fix React hooks order violation The groupedItems useMemo was being called after an early return statement, which violated React's rules of hooks (hooks must be called in the same order every render). This caused 'Rendered more hooks than during the previous render' error when clicking on requests in the detail panel. * chore: remove debug logging from Anthropic image parser and ImageContent component Cleanup after verifying the parser correctly generates data:image/png;base64 URLs for Anthropic image messages. * fix: render Anthropic images in request viewer - Fix contentArray filtering to include image types (was checking content.content but images use image_url) - Add unoptimized flag for data URL images in Next.js Image component - Remove debug logging * test: add mime_type field to Anthropic image test expectations Update test assertions to include the mime_type field that was added to image content objects in the Anthropic request parser fix. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix: make mime_type optional and only set for base64 source data The mime_type field should only be set when we have actual base64 data from the source field. For images that are already URLs (http/https or data URLs), the mime_type is not relevant and should be omitted. Changes: - Updated requestParser to conditionally include mime_type only when base64Data exists - Updated existing test expectations to not include mime_type for URL images - Added new test case specifically for base64 source images that verifies mime_type is correctly included with the proper media type This addresses the AI review feedback about mime_type being optional and ensures we only set it when it's actually needed for base64 content. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Justin Torre <justin@Justins-MacBook-Pro.local> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 4345e74 commit 6f86534

File tree

4 files changed

+82
-18
lines changed

4 files changed

+82
-18
lines changed

packages/__tests__/llm-mapper/anthropic.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,56 @@ describe("mapAnthropicRequest", () => {
302302
});
303303
});
304304

305+
it("should handle images with base64 source data and include mime_type", () => {
306+
const result = mapAnthropicRequest({
307+
request: {
308+
messages: [
309+
{
310+
role: "user",
311+
content: [
312+
{
313+
type: "text",
314+
text: "What's in this image?",
315+
},
316+
{
317+
type: "image",
318+
source: {
319+
type: "base64",
320+
media_type: "image/jpeg",
321+
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
322+
},
323+
},
324+
],
325+
},
326+
],
327+
},
328+
response: {
329+
type: "message",
330+
role: "assistant",
331+
content: [
332+
{
333+
type: "text",
334+
text: "I see an image",
335+
},
336+
],
337+
},
338+
statusCode: 200,
339+
model: "claude-3-sonnet",
340+
});
341+
342+
const contentArray = result.schema.request.messages![0].contentArray!;
343+
expect(contentArray).toHaveLength(2);
344+
345+
// Image with base64 source should include mime_type
346+
expect(contentArray[1]).toMatchObject({
347+
role: "user",
348+
_type: "image",
349+
content: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
350+
mime_type: "image/jpeg",
351+
image_url: "data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
352+
});
353+
});
354+
305355
it("should handle streamed responses with undefined values", () => {
306356
const result = mapAnthropicRequest({
307357
request: {

packages/llm-mapper/mappers/anthropic/requestParser.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,17 @@ const anthropicMessageToMessage = (message: any, role?: string): Message => {
5454
};
5555
}
5656
if (message.type === "image" || message.type === "image_url") {
57+
const imageUrl = message.image_url?.url;
58+
const base64Data = message.source?.data;
59+
const mimeType = message.source?.media_type || "image/png";
60+
const generatedImageUrl = imageUrl || (base64Data ? `data:${mimeType};base64,${base64Data}` : undefined);
61+
5762
return {
58-
content: getMessageContent(message),
63+
content: base64Data || "",
5964
role: messageRole,
6065
_type: "image",
61-
image_url:
62-
message.type === "image" || message.type === "image_url"
63-
? message.image_url?.url || message.source?.data
64-
: undefined,
66+
image_url: generatedImageUrl,
67+
...(base64Data && { mime_type: mimeType }),
6568
id: randomId(),
6669
};
6770
}

web/components/templates/requests/components/ChatOnlyView.tsx

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -358,17 +358,8 @@ export default function ChatOnlyView({ mappedRequest }: ChatOnlyViewProps) {
358358
(item) => item.type === "message"
359359
).length;
360360

361-
if (messageCount === 0) {
362-
return (
363-
<div className="flex h-full items-center justify-center p-8">
364-
<p className="text-sm text-muted-foreground">
365-
No user or assistant messages found in this request.
366-
</p>
367-
</div>
368-
);
369-
}
370-
371361
// Group consecutive tool calls together
362+
// NOTE: This useMemo must be called BEFORE any early returns to comply with React's rules of hooks
372363
const groupedItems = useMemo(() => {
373364
const result: (
374365
| { type: "message"; message: Message; isUser: boolean }
@@ -398,6 +389,16 @@ export default function ChatOnlyView({ mappedRequest }: ChatOnlyViewProps) {
398389
return result;
399390
}, [chatItems]);
400391

392+
if (messageCount === 0) {
393+
return (
394+
<div className="flex h-full items-center justify-center p-8">
395+
<p className="text-sm text-muted-foreground">
396+
No user or assistant messages found in this request.
397+
</p>
398+
</div>
399+
);
400+
}
401+
401402
let messageIndex = 0;
402403

403404
return (

web/components/templates/requests/components/chatComponent/ChatMessage.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ const ImageContent: React.FC<{
192192
const [isModalOpen, setIsModalOpen] = useState(false);
193193

194194
let imageSrc = message.image_url;
195+
195196
if (message.content && message.mime_type?.startsWith("image/")) {
196197
imageSrc = `data:${message.mime_type};base64,${message.content}`;
197198
} else if (message.content && !message.mime_type) {
@@ -204,16 +205,23 @@ const ImageContent: React.FC<{
204205
imageSrc = `data:image/png;base64,${message.content}`;
205206
}
206207

207-
if (!imageSrc) return null;
208+
if (!imageSrc) {
209+
return null;
210+
}
208211

209212
const processedImageSrc = imageSrc.includes("base64,")
210213
? base64UrlToBase64(imageSrc)
211214
: imageSrc.includes("https://") || imageSrc.includes("http://")
212215
? imageSrc
213216
: null;
214217

215-
if (!processedImageSrc) return null;
218+
if (!processedImageSrc) {
219+
return null;
220+
}
216221

222+
// Use unoptimized for data URLs to avoid Next.js image optimization issues
223+
const isDataUrl = processedImageSrc.startsWith("data:");
224+
217225
const imageElement = (
218226
<div className="relative w-full max-w-md">
219227
<Image
@@ -225,6 +233,7 @@ const ImageContent: React.FC<{
225233
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
226234
onClick={() => setIsModalOpen(true)}
227235
title="Click to view full size"
236+
unoptimized={isDataUrl}
228237
/>
229238
</div>
230239
);
@@ -822,8 +831,9 @@ export default function ChatMessage({
822831
<div className="flex flex-col gap-4">
823832
{message.contentArray?.map((content, index) => {
824833
const contentType = getMessageType(content);
834+
// Images have data in image_url, not content
825835
const shouldShowContent =
826-
chatMode === "PLAYGROUND_INPUT" || content.content;
836+
chatMode === "PLAYGROUND_INPUT" || content.content || content.image_url || (content._type === "image");
827837

828838
return shouldShowContent ? (
829839
<div key={index}>

0 commit comments

Comments
 (0)