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
18 changes: 14 additions & 4 deletions src/cloudflare/internal/ai-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export type AiOptions = {
};

export type AiInputReadableStream = {
body: ReadableStream;
body: ReadableStream | FormData;
contentType: string;
};

Expand Down Expand Up @@ -87,7 +87,6 @@ export class AiInternalError extends Error {
}
}

// TODO: merge this function with the one with images-api.ts
function isReadableStream(obj: unknown): obj is ReadableStream {
return !!(
obj &&
Expand All @@ -97,6 +96,17 @@ function isReadableStream(obj: unknown): obj is ReadableStream {
);
}

function isFormData(obj: unknown): obj is FormData {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not use something like this?

function isFormData(obj: unknown): obj is FormData {
  return obj instanceof FormData;
}

Copy link
Contributor Author

@thatsKevinJain thatsKevinJain Nov 26, 2025

Choose a reason for hiding this comment

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

"instanceof" depends on how the object was constructed and can fail to work sometimes.

Referring to the SO article here:
https://stackoverflow.com/a/47204039

Screenshot 2025-11-26 at 9 28 31 AM

The approach used in PR above looks verbose, but it is trying to check if some specific functions of FormData object are present as well.

This is similar to how we check for readable streams:
https://github.com/cloudflare/workerd/pull/5590/files#diff-2fe1e717c62f1145cade283ccc2dabf84edd49d4517103ddc75171c17e5e710fR90

Copy link
Contributor

Choose a reason for hiding this comment

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

If we are saying that we accept FormData then the users should build the object as a FormData and not a random object with append and entries methods. This means that the following object will pass the validation

const fakeFormData = {
  append(key: string, value: string) {
    console.log(`Appending ${key}: ${value}`);
  },
  entries() {
    return [['key1', 'value1']];
  }
};

On a side note: I also don't agree how we check for readable streams either

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@danlapid Do you have ideas on what can be right approach here?

return !!(
obj &&
typeof obj === 'object' &&
'append' in obj &&
typeof obj.append === 'function' &&
'entries' in obj &&
typeof obj.entries === 'function'
);
}

/**
* Find keys in inputs that have a ReadableStream
* */
Expand All @@ -111,9 +121,9 @@ function findReadableStreamKeys(
value &&
typeof value === 'object' &&
'body' in value &&
isReadableStream(value.body);
(isReadableStream(value.body) || isFormData(value.body));

if (hasReadableStreamBody || isReadableStream(value)) {
if (hasReadableStreamBody || isReadableStream(value) || isFormData(value)) {
readableStreamKeys.push(key);
}
}
Expand Down
19 changes: 19 additions & 0 deletions src/cloudflare/internal/test/ai/ai-api-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,25 @@ export const tests = {
);
}

{
// Test form data input
const form = new FormData();
form.append('prompt', 'cat');
const resp = await env.ai.run('formDataInputs', {
audio: {
body: form,
contentType: 'multipart/form-data',
},
});

assert.deepStrictEqual(resp, {
inputs: {},
options: { userInputs: '{}', version: '3' },
requestUrl:
'https://workers-binding.ai/run?version=3&userInputs=%7B%7D',
});
}

{
// Test gateway option
const resp = await env.ai.run(
Expand Down
13 changes: 13 additions & 0 deletions src/cloudflare/internal/test/ai/ai-mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,19 @@ export default {
);
}

if (modelName === 'formDataInputs') {
return Response.json(
{
inputs: {},
options: { ...data.options },
requestUrl: request.url,
},
{
headers: respHeaders,
}
);
}

if (modelName === 'inputErrorModel') {
return Response.json(
{
Expand Down
Loading