Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion core/llm/llms/Anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
} from "../../index.js";
import { safeParseToolCallArgs } from "../../tools/parseArgs.js";
import { renderChatMessage, stripImages } from "../../util/messageContent.js";
import { extractBase64FromDataUrl } from "../../util/url.js";
import { DEFAULT_REASONING_TOKENS } from "../constants.js";
import { BaseLLM } from "../index.js";

Expand Down Expand Up @@ -110,7 +111,7 @@ class Anthropic extends BaseLLM {
source: {
type: "base64",
media_type: getAnthropicMediaTypeFromDataUrl(part.imageUrl.url),
data: part.imageUrl.url.split(",")[1],
data: extractBase64FromDataUrl(part.imageUrl.url),
},
});
}
Expand Down
3 changes: 2 additions & 1 deletion core/llm/llms/Bedrock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type { CompletionOptions } from "../../index.js";
import { ChatMessage, Chunk, LLMOptions, MessageContent } from "../../index.js";
import { safeParseToolCallArgs } from "../../tools/parseArgs.js";
import { renderChatMessage, stripImages } from "../../util/messageContent.js";
import { parseDataUrl } from "../../util/url.js";
import { BaseLLM } from "../index.js";
import { PROVIDER_TOOL_SUPPORT } from "../toolSupport.js";
import { getSecureID } from "../utils/getSecureID.js";
Expand Down Expand Up @@ -546,7 +547,7 @@ class Bedrock extends BaseLLM {
blocks.push({ text: part.text });
} else if (part.type === "imageUrl" && part.imageUrl) {
try {
const [mimeType, base64Data] = part.imageUrl.url.split(",");
const { mimeType, base64Data } = parseDataUrl(part.imageUrl.url);
const format = mimeType.split("/")[1]?.split(";")[0] || "jpeg";
if (
format === ImageFormat.JPEG ||
Expand Down
5 changes: 4 additions & 1 deletion core/llm/llms/Gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "../../index.js";
import { safeParseToolCallArgs } from "../../tools/parseArgs.js";
import { renderChatMessage, stripImages } from "../../util/messageContent.js";
import { extractBase64FromDataUrl } from "../../util/url.js";
import { BaseLLM } from "../index.js";
import {
GeminiChatContent,
Expand Down Expand Up @@ -191,7 +192,9 @@ class Gemini extends BaseLLM {
: {
inlineData: {
mimeType: "image/jpeg",
data: part.imageUrl?.url.split(",")[1],
data: part.imageUrl?.url
? extractBase64FromDataUrl(part.imageUrl.url)
: "",
},
};
}
Expand Down
5 changes: 4 additions & 1 deletion core/llm/llms/Ollama.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "../../index.js";
import { renderChatMessage } from "../../util/messageContent.js";
import { getRemoteModelInfo } from "../../util/ollamaHelper.js";
import { extractBase64FromDataUrl } from "../../util/url.js";
import { BaseLLM } from "../index.js";

type OllamaChatMessage = {
Expand Down Expand Up @@ -303,7 +304,9 @@ class Ollama extends BaseLLM implements ModelInstaller {
const images: string[] = [];
message.content.forEach((part) => {
if (part.type === "imageUrl" && part.imageUrl) {
const image = part.imageUrl?.url.split(",").at(-1);
const image = part.imageUrl?.url
? extractBase64FromDataUrl(part.imageUrl.url)
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling extractBase64FromDataUrl here will throw on malformed data URLs and bubble up, regressing the previous behavior that tolerated bad input; wrap this in error handling or preserve the non-throwing fallback so malformed URLs don’t crash message conversion.

Prompt for AI agents
Address the following comment on core/llm/llms/Ollama.ts at line 308:

<comment>Calling extractBase64FromDataUrl here will throw on malformed data URLs and bubble up, regressing the previous behavior that tolerated bad input; wrap this in error handling or preserve the non-throwing fallback so malformed URLs don’t crash message conversion.</comment>

<file context>
@@ -303,7 +304,9 @@ class Ollama extends BaseLLM implements ModelInstaller {
         if (part.type === &quot;imageUrl&quot; &amp;&amp; part.imageUrl) {
-          const image = part.imageUrl?.url.split(&quot;,&quot;).at(-1);
+          const image = part.imageUrl?.url
+            ? extractBase64FromDataUrl(part.imageUrl.url)
+            : undefined;
           if (image) {
</file context>
Fix with Cubic

: undefined;
if (image) {
images.push(image);
}
Expand Down
39 changes: 39 additions & 0 deletions core/util/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,42 @@ export function canParseUrl(url: string): boolean {
return false;
}
}

export function parseDataUrl(dataUrl: string): {
mimeType: string;
base64Data: string;
} {
const urlParts = dataUrl.split(",");

if (urlParts.length < 2) {
throw new Error(
"Invalid data URL format: expected 'data:type;base64,data' format",
);
}

const [mimeType, ...base64Parts] = urlParts;
const base64Data = base64Parts.join(",");

return { mimeType, base64Data };
}

export function extractBase64FromDataUrl(dataUrl: string): string {
return parseDataUrl(dataUrl).base64Data;
}

export function safeSplit(
input: string,
delimiter: string,
expectedParts: number,
errorContext: string = "input",
): string[] {
const parts = input.split(delimiter);

if (parts.length !== expectedParts) {
throw new Error(
`Invalid ${errorContext} format: expected ${expectedParts} parts separated by "${delimiter}", got ${parts.length}`,
);
}

return parts;
}
3 changes: 2 additions & 1 deletion packages/openai-adapters/src/apis/Anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
CompletionUsage,
} from "openai/resources/index";
import { ChatCompletionCreateParams } from "openai/resources/index.js";
import { extractBase64FromDataUrl } from "../../../../core/util/url.js";
import { AnthropicConfig } from "../types.js";
import {
chatChunk,
Expand Down Expand Up @@ -199,7 +200,7 @@ export class AnthropicApi implements BaseLlmApi {
source: {
type: "base64",
media_type: getAnthropicMediaTypeFromDataUrl(dataUrl),
data: dataUrl.split(",")[1],
data: extractBase64FromDataUrl(dataUrl),
},
});
}
Expand Down
7 changes: 4 additions & 3 deletions packages/openai-adapters/src/apis/Bedrock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {

import { fromNodeProviderChain } from "@aws-sdk/credential-providers";
import { fromStatic } from "@aws-sdk/token-providers";
import { parseDataUrl } from "../../../../core/util/url.js";
import { BedrockConfig } from "../types.js";
import { chatChunk, chatChunkFromDelta, embedding, rerank } from "../util.js";
import { safeParseArgs } from "../util/parseArgs.js";
Expand Down Expand Up @@ -135,9 +136,9 @@ export class BedrockApi implements BaseLlmApi {
case "image_url":
default:
try {
const [mimeType, base64Data] = (
part as ChatCompletionContentPartImage
).image_url.url.split(",");
const { mimeType, base64Data } = parseDataUrl(
(part as ChatCompletionContentPartImage).image_url.url,
);
const format = mimeType.split("/")[1]?.split(";")[0] || "jpeg";
if (
format === ImageFormat.JPEG ||
Expand Down
Loading