Skip to content

Commit 92ee1ac

Browse files
committed
fix: improve image URL parsing robustness across providers
1 parent 8fe0288 commit 92ee1ac

File tree

7 files changed

+57
-8
lines changed

7 files changed

+57
-8
lines changed

core/llm/llms/Anthropic.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
} from "../../index.js";
2929
import { safeParseToolCallArgs } from "../../tools/parseArgs.js";
3030
import { renderChatMessage, stripImages } from "../../util/messageContent.js";
31+
import { extractBase64FromDataUrl } from "../../util/url.js";
3132
import { DEFAULT_REASONING_TOKENS } from "../constants.js";
3233
import { BaseLLM } from "../index.js";
3334

@@ -110,7 +111,7 @@ class Anthropic extends BaseLLM {
110111
source: {
111112
type: "base64",
112113
media_type: getAnthropicMediaTypeFromDataUrl(part.imageUrl.url),
113-
data: part.imageUrl.url.split(",")[1],
114+
data: extractBase64FromDataUrl(part.imageUrl.url),
114115
},
115116
});
116117
}

core/llm/llms/Bedrock.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type { CompletionOptions } from "../../index.js";
2121
import { ChatMessage, Chunk, LLMOptions, MessageContent } from "../../index.js";
2222
import { safeParseToolCallArgs } from "../../tools/parseArgs.js";
2323
import { renderChatMessage, stripImages } from "../../util/messageContent.js";
24+
import { parseDataUrl } from "../../util/url.js";
2425
import { BaseLLM } from "../index.js";
2526
import { PROVIDER_TOOL_SUPPORT } from "../toolSupport.js";
2627
import { getSecureID } from "../utils/getSecureID.js";
@@ -546,7 +547,7 @@ class Bedrock extends BaseLLM {
546547
blocks.push({ text: part.text });
547548
} else if (part.type === "imageUrl" && part.imageUrl) {
548549
try {
549-
const [mimeType, base64Data] = part.imageUrl.url.split(",");
550+
const { mimeType, base64Data } = parseDataUrl(part.imageUrl.url);
550551
const format = mimeType.split("/")[1]?.split(";")[0] || "jpeg";
551552
if (
552553
format === ImageFormat.JPEG ||

core/llm/llms/Gemini.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from "../../index.js";
1212
import { safeParseToolCallArgs } from "../../tools/parseArgs.js";
1313
import { renderChatMessage, stripImages } from "../../util/messageContent.js";
14+
import { extractBase64FromDataUrl } from "../../util/url.js";
1415
import { BaseLLM } from "../index.js";
1516
import {
1617
GeminiChatContent,
@@ -191,7 +192,9 @@ class Gemini extends BaseLLM {
191192
: {
192193
inlineData: {
193194
mimeType: "image/jpeg",
194-
data: part.imageUrl?.url.split(",")[1],
195+
data: part.imageUrl?.url
196+
? extractBase64FromDataUrl(part.imageUrl.url)
197+
: "",
195198
},
196199
};
197200
}

core/llm/llms/Ollama.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from "../../index.js";
1414
import { renderChatMessage } from "../../util/messageContent.js";
1515
import { getRemoteModelInfo } from "../../util/ollamaHelper.js";
16+
import { extractBase64FromDataUrl } from "../../util/url.js";
1617
import { BaseLLM } from "../index.js";
1718

1819
type OllamaChatMessage = {
@@ -303,7 +304,9 @@ class Ollama extends BaseLLM implements ModelInstaller {
303304
const images: string[] = [];
304305
message.content.forEach((part) => {
305306
if (part.type === "imageUrl" && part.imageUrl) {
306-
const image = part.imageUrl?.url.split(",").at(-1);
307+
const image = part.imageUrl?.url
308+
? extractBase64FromDataUrl(part.imageUrl.url)
309+
: undefined;
307310
if (image) {
308311
images.push(image);
309312
}

core/util/url.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,42 @@ export function canParseUrl(url: string): boolean {
99
return false;
1010
}
1111
}
12+
13+
export function parseDataUrl(dataUrl: string): {
14+
mimeType: string;
15+
base64Data: string;
16+
} {
17+
const urlParts = dataUrl.split(",");
18+
19+
if (urlParts.length < 2) {
20+
throw new Error(
21+
"Invalid data URL format: expected 'data:type;base64,data' format",
22+
);
23+
}
24+
25+
const [mimeType, ...base64Parts] = urlParts;
26+
const base64Data = base64Parts.join(",");
27+
28+
return { mimeType, base64Data };
29+
}
30+
31+
export function extractBase64FromDataUrl(dataUrl: string): string {
32+
return parseDataUrl(dataUrl).base64Data;
33+
}
34+
35+
export function safeSplit(
36+
input: string,
37+
delimiter: string,
38+
expectedParts: number,
39+
errorContext: string = "input",
40+
): string[] {
41+
const parts = input.split(delimiter);
42+
43+
if (parts.length !== expectedParts) {
44+
throw new Error(
45+
`Invalid ${errorContext} format: expected ${expectedParts} parts separated by "${delimiter}", got ${parts.length}`,
46+
);
47+
}
48+
49+
return parts;
50+
}

packages/openai-adapters/src/apis/Anthropic.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
CompletionUsage,
2626
} from "openai/resources/index";
2727
import { ChatCompletionCreateParams } from "openai/resources/index.js";
28+
import { extractBase64FromDataUrl } from "../../../../core/util/url.js";
2829
import { AnthropicConfig } from "../types.js";
2930
import {
3031
chatChunk,
@@ -199,7 +200,7 @@ export class AnthropicApi implements BaseLlmApi {
199200
source: {
200201
type: "base64",
201202
media_type: getAnthropicMediaTypeFromDataUrl(dataUrl),
202-
data: dataUrl.split(",")[1],
203+
data: extractBase64FromDataUrl(dataUrl),
203204
},
204205
});
205206
}

packages/openai-adapters/src/apis/Bedrock.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131

3232
import { fromNodeProviderChain } from "@aws-sdk/credential-providers";
3333
import { fromStatic } from "@aws-sdk/token-providers";
34+
import { parseDataUrl } from "../../../../core/util/url.js";
3435
import { BedrockConfig } from "../types.js";
3536
import { chatChunk, chatChunkFromDelta, embedding, rerank } from "../util.js";
3637
import { safeParseArgs } from "../util/parseArgs.js";
@@ -135,9 +136,9 @@ export class BedrockApi implements BaseLlmApi {
135136
case "image_url":
136137
default:
137138
try {
138-
const [mimeType, base64Data] = (
139-
part as ChatCompletionContentPartImage
140-
).image_url.url.split(",");
139+
const { mimeType, base64Data } = parseDataUrl(
140+
(part as ChatCompletionContentPartImage).image_url.url,
141+
);
141142
const format = mimeType.split("/")[1]?.split(";")[0] || "jpeg";
142143
if (
143144
format === ImageFormat.JPEG ||

0 commit comments

Comments
 (0)