From 85891ec8f53cb1cad953f9087b91ade568b04e3a Mon Sep 17 00:00:00 2001 From: Kevin Jain Date: Tue, 25 Nov 2025 20:43:23 -0600 Subject: [PATCH] Allow FormData to be passed as input to AI bindings ``` Models like bfl/flux-2-dev accept FormData as inputs, where the user can pass multiple images as style references. This PR allows AI bindings to pass FormData as-is to the model. ``` --- src/cloudflare/internal/ai-api.ts | 18 ++++++++++++++---- .../internal/test/ai/ai-api-test.js | 19 +++++++++++++++++++ src/cloudflare/internal/test/ai/ai-mock.js | 13 +++++++++++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/cloudflare/internal/ai-api.ts b/src/cloudflare/internal/ai-api.ts index 5e6b51f9c65..2a65b9bb2ad 100644 --- a/src/cloudflare/internal/ai-api.ts +++ b/src/cloudflare/internal/ai-api.ts @@ -42,7 +42,7 @@ export type AiOptions = { }; export type AiInputReadableStream = { - body: ReadableStream; + body: ReadableStream | FormData; contentType: string; }; @@ -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 && @@ -97,6 +96,17 @@ function isReadableStream(obj: unknown): obj is ReadableStream { ); } +function isFormData(obj: unknown): obj is FormData { + 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 * */ @@ -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); } } diff --git a/src/cloudflare/internal/test/ai/ai-api-test.js b/src/cloudflare/internal/test/ai/ai-api-test.js index ceea08d1761..20e2302dfe5 100644 --- a/src/cloudflare/internal/test/ai/ai-api-test.js +++ b/src/cloudflare/internal/test/ai/ai-api-test.js @@ -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( diff --git a/src/cloudflare/internal/test/ai/ai-mock.js b/src/cloudflare/internal/test/ai/ai-mock.js index 2095d162967..77f29da8504 100644 --- a/src/cloudflare/internal/test/ai/ai-mock.js +++ b/src/cloudflare/internal/test/ai/ai-mock.js @@ -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( {