mirror of
https://github.com/THU-MAIC/OpenMAIC.git
synced 2026-10-02 01:15:18 +08:00
fix(orchestration): strip data: prefix before passing image to AI SDK
Vercel AI SDK's streamText/generateText treats ImagePart.image as a URL to fetch when it's a string. data: URLs fail the http/https scheme check and throw AI_DownloadError. Strip the data URL prefix and pass raw base64 with mediaType separately — AI SDK treats base64 strings as data content.
This commit is contained in:
@@ -18,7 +18,12 @@ export interface ConvertedMessage {
|
||||
|
||||
/**
|
||||
* Extract image URLs from UIMessage parts.
|
||||
* Returns data-URLs or http(s) URLs where `mediaType` starts with `image/`.
|
||||
* Returns image parts in AI SDK's `ImagePart` shape.
|
||||
*
|
||||
* AI SDK's `streamText`/`generateText` treats strings as URLs to download.
|
||||
* data URLs fail that download check, so we unwrap `data:image/...;base64,<payload>`
|
||||
* into a raw base64 string and carry the media type separately.
|
||||
* http(s) URLs pass through unchanged (AI SDK will fetch them).
|
||||
*/
|
||||
function extractImageParts(parts: unknown[] | undefined): ImagePart[] {
|
||||
if (!parts) return [];
|
||||
@@ -26,12 +31,23 @@ function extractImageParts(parts: unknown[] | undefined): ImagePart[] {
|
||||
for (const part of parts) {
|
||||
const p = part as Record<string, unknown>;
|
||||
if (
|
||||
p.type === 'file' &&
|
||||
typeof p.mediaType === 'string' &&
|
||||
p.mediaType.startsWith('image/') &&
|
||||
typeof p.url === 'string'
|
||||
p.type !== 'file' ||
|
||||
typeof p.mediaType !== 'string' ||
|
||||
!p.mediaType.startsWith('image/') ||
|
||||
typeof p.url !== 'string'
|
||||
) {
|
||||
out.push({ type: 'image', image: p.url, mediaType: p.mediaType });
|
||||
continue;
|
||||
}
|
||||
|
||||
const url = p.url;
|
||||
// Match data URLs: data:<mime>[;base64],<payload>
|
||||
const dataUrlMatch = /^data:([^;,]+)(?:;base64)?,(.*)$/.exec(url);
|
||||
if (dataUrlMatch) {
|
||||
const payload = dataUrlMatch[2];
|
||||
out.push({ type: 'image', image: payload, mediaType: p.mediaType });
|
||||
} else {
|
||||
// http / https URL — AI SDK will fetch it
|
||||
out.push({ type: 'image', image: url, mediaType: p.mediaType });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('convertMessagesToOpenAI — text-only (backwards compatibility)', () =
|
||||
});
|
||||
|
||||
describe('convertMessagesToOpenAI — image attachments', () => {
|
||||
test('user message with a single image/png file part → multimodal content array', () => {
|
||||
test('user message with a data: URL image → base64 payload + mediaType (data prefix stripped)', () => {
|
||||
const msgs: UIMsg[] = [
|
||||
userMsg([
|
||||
{ type: 'text', text: 'Please look at this.' },
|
||||
@@ -88,14 +88,16 @@ describe('convertMessagesToOpenAI — image attachments', () => {
|
||||
const parts = out[0].content as Array<Record<string, unknown>>;
|
||||
expect(parts).toHaveLength(2);
|
||||
expect(parts[0]).toEqual({ type: 'text', text: 'Please look at this.' });
|
||||
// The data URL prefix must be stripped — AI SDK rejects `data:` strings
|
||||
// as invalid download URLs.
|
||||
expect(parts[1]).toEqual({
|
||||
type: 'image',
|
||||
image: 'data:image/png;base64,iVBORw0KGgo=',
|
||||
image: 'iVBORw0KGgo=',
|
||||
mediaType: 'image/png',
|
||||
});
|
||||
});
|
||||
|
||||
test('image-only user message (no text) still emits content array with just the image', () => {
|
||||
test('http URL image passes through unchanged', () => {
|
||||
const msgs: UIMsg[] = [
|
||||
userMsg([
|
||||
{
|
||||
@@ -109,7 +111,11 @@ describe('convertMessagesToOpenAI — image attachments', () => {
|
||||
expect(out).toHaveLength(1);
|
||||
const parts = out[0].content as Array<Record<string, unknown>>;
|
||||
expect(parts).toHaveLength(1);
|
||||
expect(parts[0]).toMatchObject({ type: 'image', image: 'https://example.com/board.png' });
|
||||
expect(parts[0]).toMatchObject({
|
||||
type: 'image',
|
||||
image: 'https://example.com/board.png',
|
||||
mediaType: 'image/png',
|
||||
});
|
||||
});
|
||||
|
||||
test('non-image file parts (pdf) are ignored (image-only experiment scope)', () => {
|
||||
|
||||
Reference in New Issue
Block a user