diff --git a/src/backend/drivers/ai-chat/providers/claude/ClaudeProvider.test.ts b/src/backend/drivers/ai-chat/providers/claude/ClaudeProvider.test.ts
index 6e5ac5b515..e1bf7f27d1 100644
--- a/src/backend/drivers/ai-chat/providers/claude/ClaudeProvider.test.ts
+++ b/src/backend/drivers/ai-chat/providers/claude/ClaudeProvider.test.ts
@@ -253,6 +253,42 @@ describe('ClaudeProvider.complete request shape', () => {
expect(args.max_tokens).toBe(0);
});
+ it('converts OpenAI-shaped image_url parts into Anthropic image blocks', async () => {
+ const { provider } = makeProvider();
+ messagesCreateMock.mockResolvedValueOnce(baseResponse);
+
+ await withTestActor(() =>
+ provider.complete({
+ model: 'claude-sonnet-4-6',
+ messages: [
+ {
+ role: 'user',
+ content: [
+ { type: 'text', text: 'What do you see?' },
+ {
+ image_url: {
+ url: 'https://assets.puter.site/doge.jpeg',
+ },
+ },
+ ],
+ },
+ ],
+ }),
+ );
+
+ const [args] = messagesCreateMock.mock.calls[0]!;
+ expect(args.messages[0].content).toEqual([
+ { type: 'text', text: 'What do you see?' },
+ {
+ type: 'image',
+ source: {
+ type: 'url',
+ url: 'https://assets.puter.site/doge.jpeg',
+ },
+ },
+ ]);
+ });
+
it('extracts system messages and forwards them as the top-level `system` field', async () => {
const { provider } = makeProvider();
messagesCreateMock.mockResolvedValueOnce(baseResponse);
diff --git a/src/backend/drivers/ai-chat/providers/claude/ClaudeProvider.ts b/src/backend/drivers/ai-chat/providers/claude/ClaudeProvider.ts
index f67970e26a..b31520daf6 100644
--- a/src/backend/drivers/ai-chat/providers/claude/ClaudeProvider.ts
+++ b/src/backend/drivers/ai-chat/providers/claude/ClaudeProvider.ts
@@ -46,6 +46,7 @@ import type {
AIChatToolUseStream,
} from '../../utils/Streaming.js';
import { FILES_API_BETA, processPuterPathUploads } from './fileUpload.js';
+import { coerceImageContentParts } from './imageHandling.js';
import { CLAUDE_MODELS } from './models.js';
// Anthropic inline-compaction beta. The vendored SDK (0.68.0) doesn't type the
@@ -290,6 +291,8 @@ export class ClaudeProvider implements IChatProvider {
const actor = Context.get('actor');
+ coerceImageContentParts(messages);
+
// Upload any `puter_path` parts to Anthropic's Files API and rewrite
// them in-place to reference the returned `file_id`. Must happen
// before sdkParams snapshots `messages`.
diff --git a/src/backend/drivers/ai-chat/providers/claude/imageHandling.test.ts b/src/backend/drivers/ai-chat/providers/claude/imageHandling.test.ts
new file mode 100644
index 0000000000..a4272a20f4
--- /dev/null
+++ b/src/backend/drivers/ai-chat/providers/claude/imageHandling.test.ts
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2024-present Puter Technologies Inc.
+ *
+ * This file is part of Puter.
+ *
+ * Puter is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { describe, expect, it } from 'vitest';
+import { coerceImageContentParts } from './imageHandling.js';
+
+describe('coerceImageContentParts', () => {
+ it('converts puter.js vision shorthand `{ image_url: { url } }` blocks', () => {
+ const messages = [
+ {
+ role: 'user',
+ content: [
+ { type: 'text', text: 'What do you see?' },
+ {
+ image_url: {
+ url: 'https://assets.puter.site/doge.jpeg',
+ },
+ },
+ ],
+ },
+ ];
+
+ coerceImageContentParts(messages);
+
+ expect(messages[0]!.content).toEqual([
+ { type: 'text', text: 'What do you see?' },
+ {
+ type: 'image',
+ source: {
+ type: 'url',
+ url: 'https://assets.puter.site/doge.jpeg',
+ },
+ },
+ ]);
+ });
+
+ it('converts typed OpenAI `image_url` parts with nested url objects', () => {
+ const messages = [
+ {
+ content: [
+ {
+ type: 'image_url',
+ image_url: { url: 'https://example.com/a.png' },
+ },
+ ],
+ },
+ ];
+
+ coerceImageContentParts(messages);
+
+ expect(messages[0]!.content[0]).toEqual({
+ type: 'image',
+ source: { type: 'url', url: 'https://example.com/a.png' },
+ });
+ });
+
+ it('converts flat-string `image_url` parts', () => {
+ const messages = [
+ {
+ content: [
+ {
+ type: 'image_url',
+ image_url: 'https://example.com/flat.png',
+ },
+ ],
+ },
+ ];
+
+ coerceImageContentParts(messages);
+
+ expect(messages[0]!.content[0]).toEqual({
+ type: 'image',
+ source: { type: 'url', url: 'https://example.com/flat.png' },
+ });
+ });
+
+ it('parses data URIs into base64 image sources', () => {
+ const messages = [
+ {
+ content: [
+ {
+ image_url: {
+ url: 'data:image/png;base64,QUJD',
+ },
+ },
+ ],
+ },
+ ];
+
+ coerceImageContentParts(messages);
+
+ expect(messages[0]!.content[0]).toEqual({
+ type: 'image',
+ source: {
+ type: 'base64',
+ media_type: 'image/png',
+ data: 'QUJD',
+ },
+ });
+ });
+
+ it('leaves native Anthropic image blocks unchanged', () => {
+ const native = {
+ type: 'image',
+ source: { type: 'file', file_id: 'file_1' },
+ };
+ const messages = [{ content: [native] }];
+
+ coerceImageContentParts(messages);
+
+ expect(messages[0]!.content[0]).toBe(native);
+ });
+
+ it('leaves non-image content parts unchanged', () => {
+ const messages = [
+ {
+ content: [
+ { type: 'text', text: 'hello' },
+ { type: 'tool_use', id: 'call_1', name: 'lookup', input: {} },
+ ],
+ },
+ ];
+
+ coerceImageContentParts(messages);
+
+ expect(messages[0]!.content).toEqual([
+ { type: 'text', text: 'hello' },
+ { type: 'tool_use', id: 'call_1', name: 'lookup', input: {} },
+ ]);
+ });
+});
diff --git a/src/backend/drivers/ai-chat/providers/claude/imageHandling.ts b/src/backend/drivers/ai-chat/providers/claude/imageHandling.ts
new file mode 100644
index 0000000000..3406f0fb2a
--- /dev/null
+++ b/src/backend/drivers/ai-chat/providers/claude/imageHandling.ts
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2024-present Puter Technologies Inc.
+ *
+ * This file is part of Puter.
+ *
+ * Puter is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+interface ImageUrlPart {
+ type?: string;
+ image_url?: string | { url?: string };
+ source?: unknown;
+ [key: string]: unknown;
+}
+
+type AnthropicImagePart = {
+ type: 'image';
+ source:
+ | { type: 'url'; url: string }
+ | { type: 'base64'; media_type: string; data: string };
+};
+
+const extractImageUrl = (part: ImageUrlPart): string | undefined => {
+ const raw = part.image_url;
+ if (typeof raw === 'string') return raw;
+ if (raw && typeof raw === 'object' && typeof raw.url === 'string') {
+ return raw.url;
+ }
+ return undefined;
+};
+
+const toAnthropicImagePart = (url: string): AnthropicImagePart => {
+ const dataUriMatch = url.match(/^data:([^;,]+);base64,(.+)$/);
+ if (dataUriMatch) {
+ return {
+ type: 'image',
+ source: {
+ type: 'base64',
+ media_type: dataUriMatch[1]!,
+ data: dataUriMatch[2]!,
+ },
+ };
+ }
+ return {
+ type: 'image',
+ source: { type: 'url', url },
+ };
+};
+
+/**
+ * Rewrite OpenAI-style `image_url` content parts into Anthropic `image`
+ * blocks. The puter.js vision shorthand sends `{ image_url: { url } }`
+ * without a `type`, which Anthropic rejects (`content.N.type: Field required`).
+ */
+export function coerceImageContentParts(
+ messages: Array<{ content?: unknown }>,
+): void {
+ for (const message of messages) {
+ if (!Array.isArray(message.content)) continue;
+ message.content = (message.content as ImageUrlPart[]).map((part) => {
+ if (!part || typeof part !== 'object') return part;
+ if (part.type === 'image' && part.source) return part;
+ const url = extractImageUrl(part);
+ if (url === undefined) return part;
+ return toAnthropicImagePart(url);
+ });
+ }
+}
diff --git a/src/puter-js/test/ai.test.js b/src/puter-js/test/ai.test.js
index e97af2b6fd..63f11ac7bb 100644
--- a/src/puter-js/test/ai.test.js
+++ b/src/puter-js/test/ai.test.js
@@ -278,10 +278,7 @@ const generateAllTests = function() {
// per-model tests above, each of these targets a specific known-good model.
const TEST_IMAGE_URL = "https://assets.puter.site/doge.jpeg";
-// Direct-claude models are excluded: the claude provider is the one chat
-// provider that doesn't infer the `type` on the SDK's `{ image_url }`
-// media blocks, so the vision shorthand 400s against Anthropic today.
-const VISION_MODELS = ["gpt-5-nano", "gemini-2.5-pro"];
+const VISION_MODELS = ["gpt-5-nano", "gemini-2.5-pro", "claude-sonnet-4-6"];
// The test image is a Shiba Inu; any vision-capable model should say so.
const assertMentionsDog = function(result) {