Repository: cloudflare/workerd
Pull request #3604: Add new AI.toMarkdown() method
Author: login: G4brym
Created: 2025-02-25T19:55:53Z | Merged: 2025-03-05T21:37:47Z



Diff:
diff --git a/src/cloudflare/internal/ai-api.ts b/src/cloudflare/internal/ai-api.ts
index 74e747bced7..4634b2ea4b8 100644
--- a/src/cloudflare/internal/ai-api.ts
+++ b/src/cloudflare/internal/ai-api.ts
@@ -33,6 +33,14 @@ export type AiOptions = {
   sessionOptions?: SessionOptions;
 };
 
+export type ConversionResponse = {
+  name: string;
+  mimeType: string;
+  format: 'markdown';
+  tokens: number;
+  data: string;
+};
+
 export type AiModelsSearchParams = {
   author?: string;
   hide_experimental?: boolean;
@@ -74,6 +82,26 @@ export class AiInternalError extends Error {
   }
 }
 
+async function blobToBase64(blob: Blob): Promise<string> {
+  // TODO(soon): This is better implemented using the node::buffer API
+  // but we cannot get to that from here currently. Once the node:buffer
+  // API (actually, `node-internal:internal_buffer`) is available to be imported
+  // here we should update this code to use it instead.
+  const arrayBuffer = await blob.arrayBuffer();
+  const uint8Array = new Uint8Array(arrayBuffer);
+
+  let binary = '';
+  const chunk = 1024;
+  for (let i = 0; i < uint8Array.length; i += chunk) {
+    binary += String.fromCharCode.apply(
+      null,
+      uint8Array.subarray(i, i + chunk) as unknown as number[]
+    );
+  }
+
+  return btoa(binary);
+}
+
 export class Ai {
   private readonly fetcher: Fetcher;
 
@@ -221,6 +249,82 @@ export class Ai {
     }
   }
 
+  public async toMarkdown(
+    files: { name: string; blob: Blob }[],
+    options?: { gateway?: GatewayOptions }
+  ): Promise<ConversionResponse[]>;
+  public async toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    },
+    options?: { gateway?: GatewayOptions }
+  ): Promise<ConversionResponse>;
+  public async toMarkdown(
+    files: { name: string; blob: Blob } | { name: string; blob: Blob }[],
+    options?: { gateway?: GatewayOptions }
+  ): Promise<ConversionResponse | ConversionResponse[]> {
+    const input = Array.isArray(files) ? files : [files];
+
+    const processedFiles = [];
+    for (const file of input) {
+      processedFiles.push({
+        name: file.name,
+        mimeType: file.blob.type,
+        data: await blobToBase64(file.blob),
+      });
+    }
+
+    const fetchOptions = {
+      method: 'POST',
+      body: JSON.stringify({
+        files: processedFiles,
+        options: options,
+      }),
+      headers: {
+        'content-type': 'application/json',
+      },
+    };
+
+    const endpointUrl =
+      'https://workers-binding.ai/to-everything/markdown/transformer';
+
+    const res = await this.fetcher.fetch(endpointUrl, fetchOptions);
+
+    switch (res.status) {
+      case 200: {
+        const data = (await res.json()) as { result: ConversionResponse[] };
+
+        if (data.result.length === 0) {
+          throw new AiInternalError(
+            'Internal Error Converting files into Markdown'
+          );
+        }
+
+        // If the user sent a list of files, return an array of results, otherwise, return just the first object
+        if (Array.isArray(files)) {
+          return data.result;
+        }
+
+        const obj = data.result.at(0);
+        if (!obj) {
+          throw new AiInternalError(
+            'Internal Error Converting files into Markdown'
+          );
+        }
+
+        return obj;
+      }
+      default: {
+        const data = (await res.json()) as { errors: { message: string }[] };
+
+        throw new AiInternalError(
+          data.errors.at(0)?.message || 'Internal Error'
+        );
+      }
+    }
+  }
+
   public gateway(gatewayId: string): AiGateway {
     return new AiGateway(this.fetcher, gatewayId);
   }
diff --git a/src/cloudflare/internal/test/ai/ai-api-test.js b/src/cloudflare/internal/test/ai/ai-api-test.js
index 359cb97264d..30bfc3cf795 100644
--- a/src/cloudflare/internal/test/ai/ai-api-test.js
+++ b/src/cloudflare/internal/test/ai/ai-api-test.js
@@ -207,5 +207,60 @@ export const tests = {
 
       assert.ok(resp instanceof Response);
     }
+
+    {
+      // Test toMarkdown with single file
+      const resp = await env.ai.toMarkdown(
+        {
+          name: 'random.md',
+          blob: new Blob([`# Random Markdown`], { type: 'text/markdown' }),
+        },
+        { gateway: { id: 'my-gateway' } }
+      );
+
+      assert.deepStrictEqual(resp, {
+        name: 'random.md',
+        mimeType: 'text/markdown',
+        format: 'markdown',
+        tokens: 0,
+        data: '# Random Markdown',
+      });
+    }
+
+    {
+      // Test toMarkdown with multiple file
+      const resp = await env.ai.toMarkdown([
+        {
+          name: 'random.md',
+          blob: new Blob([`# Random Markdown`], { type: 'text/markdown' }),
+        },
+        {
+          name: 'cat.png',
+          blob: new Blob(
+            [
+              `The image shows a white and orange cat standing on a beige floor`,
+            ],
+            { type: 'image/png' }
+          ),
+        },
+      ]);
+
+      assert.deepStrictEqual(resp, [
+        {
+          name: 'random.md',
+          mimeType: 'text/markdown',
+          format: 'markdown',
+          tokens: 0,
+          data: '# Random Markdown',
+        },
+        {
+          name: 'cat.png',
+          mimeType: 'image/png',
+          format: 'markdown',
+          tokens: 0,
+          data: 'The image shows a white and orange cat standing on a beige floor',
+        },
+      ]);
+    }
   },
 };
diff --git a/src/cloudflare/internal/test/ai/ai-mock.js b/src/cloudflare/internal/test/ai/ai-mock.js
index b6789855d30..bccfa501bf8 100644
--- a/src/cloudflare/internal/test/ai/ai-mock.js
+++ b/src/cloudflare/internal/test/ai/ai-mock.js
@@ -2,10 +2,44 @@
 // Licensed under the Apache 2.0 license found in the LICENSE file or at:
 //     https://opensource.org/licenses/Apache-2.0
 
+function base64ToBlob(base64, mimeType) {
+  const binaryString = atob(base64); // Decode Base64
+  const len = binaryString.length;
+  const bytes = new Uint8Array(len);
+  for (let i = 0; i < len; i++) {
+    bytes[i] = binaryString.charCodeAt(i);
+  }
+  return new Blob([bytes], { type: mimeType });
+}
+
 export default {
   async fetch(request, env, ctx) {
     const url = new URL(request.url);
 
+    if (url.pathname === '/to-everything/markdown/transformer') {
+      const body = await request.json();
+      const decoder = new TextDecoder('utf-8');
+
+      const result = [];
+
+      for (const file of body.files) {
+        const fileblob = await base64ToBlob(file.data, file.mimeType);
+        const arr = await fileblob.arrayBuffer();
+        result.push({
+          name: file.name,
+          mimeType: file.mimeType,
+          format: 'markdown',
+          tokens: 0,
+          data: decoder.decode(arr),
+        });
+      }
+
+      return Response.json({
+        success: true,
+        result: result,
+      });
+    }
+
     if (url.pathname === '/ai-api/models/search') {
       return Response.json({
         success: true,
diff --git a/types/defines/ai.d.ts b/types/defines/ai.d.ts
index 2ac5438251c..92f886f8b00 100644
--- a/types/defines/ai.d.ts
+++ b/types/defines/ai.d.ts
@@ -820,6 +820,13 @@ export type AiOptions = {
   prefix?: string;
   extraHeaders?: object;
 };
+export type ConversionResponse = {
+  name: string;
+  mimeType: string;
+  format: "markdown";
+  tokens: number;
+  data: string;
+};
 export type AiModelsSearchParams = {
   author?: string;
   hide_experimental?: boolean;
@@ -856,7 +863,7 @@ export declare abstract class Ai<
   run<Name extends keyof AiModelList, Options extends AiOptions>(
     model: Name,
     inputs: AiModelList[Name]["inputs"],
-    options?: Options,
+    options?: Options
   ): Promise<
     Options extends {
       returnRawResponse: true;
@@ -865,4 +872,15 @@ export declare abstract class Ai<
       : AiModelList[Name]["postProcessedOutputs"]
   >;
   public models(params?: AiModelsSearchParams): Promise<AiModelsSearchObject[]>;
+  public toMarkdown(
+    files: { name: string; blob: Blob }[],
+    options?: { gateway?: GatewayOptions }
+  ): Promise<ConversionResponse[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    },
+    options?: { gateway?: GatewayOptions }
+  ): Promise<ConversionResponse>;
 }
diff --git a/types/generated-snapshot/2021-11-03/index.d.ts b/types/generated-snapshot/2021-11-03/index.d.ts
index 5cf57ccb94b..312d0f1493f 100755
--- a/types/generated-snapshot/2021-11-03/index.d.ts
+++ b/types/generated-snapshot/2021-11-03/index.d.ts
@@ -4239,6 +4239,13 @@ type AiOptions = {
   prefix?: string;
   extraHeaders?: object;
 };
+type ConversionResponse = {
+  name: string;
+  mimeType: string;
+  format: "markdown";
+  tokens: number;
+  data: string;
+};
 type AiModelsSearchParams = {
   author?: string;
   hide_experimental?: boolean;
@@ -4282,6 +4289,24 @@ declare abstract class Ai<AiModelList extends AiModelListType = AiModels> {
       : AiModelList[Name]["postProcessedOutputs"]
   >;
   public models(params?: AiModelsSearchParams): Promise<AiModelsSearchObject[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    }[],
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    },
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse>;
 }
 type GatewayOptions = {
   id: string;
diff --git a/types/generated-snapshot/2021-11-03/index.ts b/types/generated-snapshot/2021-11-03/index.ts
index 4a9f8adc0dc..7a503a1eeb8 100755
--- a/types/generated-snapshot/2021-11-03/index.ts
+++ b/types/generated-snapshot/2021-11-03/index.ts
@@ -4251,6 +4251,13 @@ export type AiOptions = {
   prefix?: string;
   extraHeaders?: object;
 };
+export type ConversionResponse = {
+  name: string;
+  mimeType: string;
+  format: "markdown";
+  tokens: number;
+  data: string;
+};
 export type AiModelsSearchParams = {
   author?: string;
   hide_experimental?: boolean;
@@ -4296,6 +4303,24 @@ export declare abstract class Ai<
       : AiModelList[Name]["postProcessedOutputs"]
   >;
   public models(params?: AiModelsSearchParams): Promise<AiModelsSearchObject[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    }[],
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    },
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse>;
 }
 export type GatewayOptions = {
   id: string;
diff --git a/types/generated-snapshot/2022-01-31/index.d.ts b/types/generated-snapshot/2022-01-31/index.d.ts
index ec20ed38259..6ac899d45ef 100755
--- a/types/generated-snapshot/2022-01-31/index.d.ts
+++ b/types/generated-snapshot/2022-01-31/index.d.ts
@@ -4265,6 +4265,13 @@ type AiOptions = {
   prefix?: string;
   extraHeaders?: object;
 };
+type ConversionResponse = {
+  name: string;
+  mimeType: string;
+  format: "markdown";
+  tokens: number;
+  data: string;
+};
 type AiModelsSearchParams = {
   author?: string;
   hide_experimental?: boolean;
@@ -4308,6 +4315,24 @@ declare abstract class Ai<AiModelList extends AiModelListType = AiModels> {
       : AiModelList[Name]["postProcessedOutputs"]
   >;
   public models(params?: AiModelsSearchParams): Promise<AiModelsSearchObject[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    }[],
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    },
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse>;
 }
 type GatewayOptions = {
   id: string;
diff --git a/types/generated-snapshot/2022-01-31/index.ts b/types/generated-snapshot/2022-01-31/index.ts
index cc72e9706fe..c597e1a404d 100755
--- a/types/generated-snapshot/2022-01-31/index.ts
+++ b/types/generated-snapshot/2022-01-31/index.ts
@@ -4277,6 +4277,13 @@ export type AiOptions = {
   prefix?: string;
   extraHeaders?: object;
 };
+export type ConversionResponse = {
+  name: string;
+  mimeType: string;
+  format: "markdown";
+  tokens: number;
+  data: string;
+};
 export type AiModelsSearchParams = {
   author?: string;
   hide_experimental?: boolean;
@@ -4322,6 +4329,24 @@ export declare abstract class Ai<
       : AiModelList[Name]["postProcessedOutputs"]
   >;
   public models(params?: AiModelsSearchParams): Promise<AiModelsSearchObject[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    }[],
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    },
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse>;
 }
 export type GatewayOptions = {
   id: string;
diff --git a/types/generated-snapshot/2022-03-21/index.d.ts b/types/generated-snapshot/2022-03-21/index.d.ts
index 003c6ac8fc7..62049ad8929 100755
--- a/types/generated-snapshot/2022-03-21/index.d.ts
+++ b/types/generated-snapshot/2022-03-21/index.d.ts
@@ -4290,6 +4290,13 @@ type AiOptions = {
   prefix?: string;
   extraHeaders?: object;
 };
+type ConversionResponse = {
+  name: string;
+  mimeType: string;
+  format: "markdown";
+  tokens: number;
+  data: string;
+};
 type AiModelsSearchParams = {
   author?: string;
   hide_experimental?: boolean;
@@ -4333,6 +4340,24 @@ declare abstract class Ai<AiModelList extends AiModelListType = AiModels> {
       : AiModelList[Name]["postProcessedOutputs"]
   >;
   public models(params?: AiModelsSearchParams): Promise<AiModelsSearchObject[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    }[],
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    },
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse>;
 }
 type GatewayOptions = {
   id: string;
diff --git a/types/generated-snapshot/2022-03-21/index.ts b/types/generated-snapshot/2022-03-21/index.ts
index b69406711b6..46aa59fa01d 100755
--- a/types/generated-snapshot/2022-03-21/index.ts
+++ b/types/generated-snapshot/2022-03-21/index.ts
@@ -4302,6 +4302,13 @@ export type AiOptions = {
   prefix?: string;
   extraHeaders?: object;
 };
+export type ConversionResponse = {
+  name: string;
+  mimeType: string;
+  format: "markdown";
+  tokens: number;
+  data: string;
+};
 export type AiModelsSearchParams = {
   author?: string;
   hide_experimental?: boolean;
@@ -4347,6 +4354,24 @@ export declare abstract class Ai<
       : AiModelList[Name]["postProcessedOutputs"]
   >;
   public models(params?: AiModelsSearchParams): Promise<AiModelsSearchObject[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    }[],
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    },
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse>;
 }
 export type GatewayOptions = {
   id: string;
diff --git a/types/generated-snapshot/2022-08-04/index.d.ts b/types/generated-snapshot/2022-08-04/index.d.ts
index a05e73e5bef..c5b4ade1423 100755
--- a/types/generated-snapshot/2022-08-04/index.d.ts
+++ b/types/generated-snapshot/2022-08-04/index.d.ts
@@ -4291,6 +4291,13 @@ type AiOptions = {
   prefix?: string;
   extraHeaders?: object;
 };
+type ConversionResponse = {
+  name: string;
+  mimeType: string;
+  format: "markdown";
+  tokens: number;
+  data: string;
+};
 type AiModelsSearchParams = {
   author?: string;
   hide_experimental?: boolean;
@@ -4334,6 +4341,24 @@ declare abstract class Ai<AiModelList extends AiModelListType = AiModels> {
       : AiModelList[Name]["postProcessedOutputs"]
   >;
   public models(params?: AiModelsSearchParams): Promise<AiModelsSearchObject[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    }[],
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    },
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse>;
 }
 type GatewayOptions = {
   id: string;
diff --git a/types/generated-snapshot/2022-08-04/index.ts b/types/generated-snapshot/2022-08-04/index.ts
index fb3606d90d2..2755264e772 100755
--- a/types/generated-snapshot/2022-08-04/index.ts
+++ b/types/generated-snapshot/2022-08-04/index.ts
@@ -4303,6 +4303,13 @@ export type AiOptions = {
   prefix?: string;
   extraHeaders?: object;
 };
+export type ConversionResponse = {
+  name: string;
+  mimeType: string;
+  format: "markdown";
+  tokens: number;
+  data: string;
+};
 export type AiModelsSearchParams = {
   author?: string;
   hide_experimental?: boolean;
@@ -4348,6 +4355,24 @@ export declare abstract class Ai<
       : AiModelList[Name]["postProcessedOutputs"]
   >;
   public models(params?: AiModelsSearchParams): Promise<AiModelsSearchObject[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    }[],
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    },
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse>;
 }
 export type GatewayOptions = {
   id: string;
diff --git a/types/generated-snapshot/2022-10-31/index.d.ts b/types/generated-snapshot/2022-10-31/index.d.ts
index e8234f29ddc..07f97536bb5 100755
--- a/types/generated-snapshot/2022-10-31/index.d.ts
+++ b/types/generated-snapshot/2022-10-31/index.d.ts
@@ -4295,6 +4295,13 @@ type AiOptions = {
   prefix?: string;
   extraHeaders?: object;
 };
+type ConversionResponse = {
+  name: string;
+  mimeType: string;
+  format: "markdown";
+  tokens: number;
+  data: string;
+};
 type AiModelsSearchParams = {
   author?: string;
   hide_experimental?: boolean;
@@ -4338,6 +4345,24 @@ declare abstract class Ai<AiModelList extends AiModelListType = AiModels> {
       : AiModelList[Name]["postProcessedOutputs"]
   >;
   public models(params?: AiModelsSearchParams): Promise<AiModelsSearchObject[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    }[],
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    },
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse>;
 }
 type GatewayOptions = {
   id: string;
diff --git a/types/generated-snapshot/2022-10-31/index.ts b/types/generated-snapshot/2022-10-31/index.ts
index 3afe45d8de9..2aca9998126 100755
--- a/types/generated-snapshot/2022-10-31/index.ts
+++ b/types/generated-snapshot/2022-10-31/index.ts
@@ -4307,6 +4307,13 @@ export type AiOptions = {
   prefix?: string;
   extraHeaders?: object;
 };
+export type ConversionResponse = {
+  name: string;
+  mimeType: string;
+  format: "markdown";
+  tokens: number;
+  data: string;
+};
 export type AiModelsSearchParams = {
   author?: string;
   hide_experimental?: boolean;
@@ -4352,6 +4359,24 @@ export declare abstract class Ai<
       : AiModelList[Name]["postProcessedOutputs"]
   >;
   public models(params?: AiModelsSearchParams): Promise<AiModelsSearchObject[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    }[],
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    },
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse>;
 }
 export type GatewayOptions = {
   id: string;
diff --git a/types/generated-snapshot/2022-11-30/index.d.ts b/types/generated-snapshot/2022-11-30/index.d.ts
index 7a0b970e389..bf8d1e1a770 100755
--- a/types/generated-snapshot/2022-11-30/index.d.ts
+++ b/types/generated-snapshot/2022-11-30/index.d.ts
@@ -4300,6 +4300,13 @@ type AiOptions = {
   prefix?: string;
   extraHeaders?: object;
 };
+type ConversionResponse = {
+  name: string;
+  mimeType: string;
+  format: "markdown";
+  tokens: number;
+  data: string;
+};
 type AiModelsSearchParams = {
   author?: string;
   hide_experimental?: boolean;
@@ -4343,6 +4350,24 @@ declare abstract class Ai<AiModelList extends AiModelListType = AiModels> {
       : AiModelList[Name]["postProcessedOutputs"]
   >;
   public models(params?: AiModelsSearchParams): Promise<AiModelsSearchObject[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    }[],
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    },
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse>;
 }
 type GatewayOptions = {
   id: string;
diff --git a/types/generated-snapshot/2022-11-30/index.ts b/types/generated-snapshot/2022-11-30/index.ts
index f996b199f49..04bbda2e4fb 100755
--- a/types/generated-snapshot/2022-11-30/index.ts
+++ b/types/generated-snapshot/2022-11-30/index.ts
@@ -4312,6 +4312,13 @@ export type AiOptions = {
   prefix?: string;
   extraHeaders?: object;
 };
+export type ConversionResponse = {
+  name: string;
+  mimeType: string;
+  format: "markdown";
+  tokens: number;
+  data: string;
+};
 export type AiModelsSearchParams = {
   author?: string;
   hide_experimental?: boolean;
@@ -4357,6 +4364,24 @@ export declare abstract class Ai<
       : AiModelList[Name]["postProcessedOutputs"]
   >;
   public models(params?: AiModelsSearchParams): Promise<AiModelsSearchObject[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    }[],
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    },
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse>;
 }
 export type GatewayOptions = {
   id: string;
diff --git a/types/generated-snapshot/2023-03-01/index.d.ts b/types/generated-snapshot/2023-03-01/index.d.ts
index 61d887de236..d6beff75691 100755
--- a/types/generated-snapshot/2023-03-01/index.d.ts
+++ b/types/generated-snapshot/2023-03-01/index.d.ts
@@ -4302,6 +4302,13 @@ type AiOptions = {
   prefix?: string;
   extraHeaders?: object;
 };
+type ConversionResponse = {
+  name: string;
+  mimeType: string;
+  format: "markdown";
+  tokens: number;
+  data: string;
+};
 type AiModelsSearchParams = {
   author?: string;
   hide_experimental?: boolean;
@@ -4345,6 +4352,24 @@ declare abstract class Ai<AiModelList extends AiModelListType = AiModels> {
       : AiModelList[Name]["postProcessedOutputs"]
   >;
   public models(params?: AiModelsSearchParams): Promise<AiModelsSearchObject[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    }[],
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    },
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse>;
 }
 type GatewayOptions = {
   id: string;
diff --git a/types/generated-snapshot/2023-03-01/index.ts b/types/generated-snapshot/2023-03-01/index.ts
index aa724db89d7..fe633a73078 100755
--- a/types/generated-snapshot/2023-03-01/index.ts
+++ b/types/generated-snapshot/2023-03-01/index.ts
@@ -4314,6 +4314,13 @@ export type AiOptions = {
   prefix?: string;
   extraHeaders?: object;
 };
+export type ConversionResponse = {
+  name: string;
+  mimeType: string;
+  format: "markdown";
+  tokens: number;
+  data: string;
+};
 export type AiModelsSearchParams = {
   author?: string;
   hide_experimental?: boolean;
@@ -4359,6 +4366,24 @@ export declare abstract class Ai<
       : AiModelList[Name]["postProcessedOutputs"]
   >;
   public models(params?: AiModelsSearchParams): Promise<AiModelsSearchObject[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    }[],
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    },
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse>;
 }
 export type GatewayOptions = {
   id: string;
diff --git a/types/generated-snapshot/2023-07-01/index.d.ts b/types/generated-snapshot/2023-07-01/index.d.ts
index 732118310bc..a9302fe2b69 100755
--- a/types/generated-snapshot/2023-07-01/index.d.ts
+++ b/types/generated-snapshot/2023-07-01/index.d.ts
@@ -4302,6 +4302,13 @@ type AiOptions = {
   prefix?: string;
   extraHeaders?: object;
 };
+type ConversionResponse = {
+  name: string;
+  mimeType: string;
+  format: "markdown";
+  tokens: number;
+  data: string;
+};
 type AiModelsSearchParams = {
   author?: string;
   hide_experimental?: boolean;
@@ -4345,6 +4352,24 @@ declare abstract class Ai<AiModelList extends AiModelListType = AiModels> {
       : AiModelList[Name]["postProcessedOutputs"]
   >;
   public models(params?: AiModelsSearchParams): Promise<AiModelsSearchObject[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    }[],
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    },
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse>;
 }
 type GatewayOptions = {
   id: string;
diff --git a/types/generated-snapshot/2023-07-01/index.ts b/types/generated-snapshot/2023-07-01/index.ts
index 65d09e7ffaa..3196937d18b 100755
--- a/types/generated-snapshot/2023-07-01/index.ts
+++ b/types/generated-snapshot/2023-07-01/index.ts
@@ -4314,6 +4314,13 @@ export type AiOptions = {
   prefix?: string;
   extraHeaders?: object;
 };
+export type ConversionResponse = {
+  name: string;
+  mimeType: string;
+  format: "markdown";
+  tokens: number;
+  data: string;
+};
 export type AiModelsSearchParams = {
   author?: string;
   hide_experimental?: boolean;
@@ -4359,6 +4366,24 @@ export declare abstract class Ai<
       : AiModelList[Name]["postProcessedOutputs"]
   >;
   public models(params?: AiModelsSearchParams): Promise<AiModelsSearchObject[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    }[],
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    },
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse>;
 }
 export type GatewayOptions = {
   id: string;
diff --git a/types/generated-snapshot/experimental/index.d.ts b/types/generated-snapshot/experimental/index.d.ts
index 86cf340dc9d..ac6a897d54e 100755
--- a/types/generated-snapshot/experimental/index.d.ts
+++ b/types/generated-snapshot/experimental/index.d.ts
@@ -4383,6 +4383,13 @@ type AiOptions = {
   prefix?: string;
   extraHeaders?: object;
 };
+type ConversionResponse = {
+  name: string;
+  mimeType: string;
+  format: "markdown";
+  tokens: number;
+  data: string;
+};
 type AiModelsSearchParams = {
   author?: string;
   hide_experimental?: boolean;
@@ -4426,6 +4433,24 @@ declare abstract class Ai<AiModelList extends AiModelListType = AiModels> {
       : AiModelList[Name]["postProcessedOutputs"]
   >;
   public models(params?: AiModelsSearchParams): Promise<AiModelsSearchObject[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    }[],
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    },
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse>;
 }
 type GatewayOptions = {
   id: string;
diff --git a/types/generated-snapshot/experimental/index.ts b/types/generated-snapshot/experimental/index.ts
index 66ac40c3392..c6ca6fcdcc1 100755
--- a/types/generated-snapshot/experimental/index.ts
+++ b/types/generated-snapshot/experimental/index.ts
@@ -4395,6 +4395,13 @@ export type AiOptions = {
   prefix?: string;
   extraHeaders?: object;
 };
+export type ConversionResponse = {
+  name: string;
+  mimeType: string;
+  format: "markdown";
+  tokens: number;
+  data: string;
+};
 export type AiModelsSearchParams = {
   author?: string;
   hide_experimental?: boolean;
@@ -4440,6 +4447,24 @@ export declare abstract class Ai<
       : AiModelList[Name]["postProcessedOutputs"]
   >;
   public models(params?: AiModelsSearchParams): Promise<AiModelsSearchObject[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    }[],
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    },
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse>;
 }
 export type GatewayOptions = {
   id: string;
diff --git a/types/generated-snapshot/oldest/index.d.ts b/types/generated-snapshot/oldest/index.d.ts
index 6c04c6d5628..b90d6903005 100755
--- a/types/generated-snapshot/oldest/index.d.ts
+++ b/types/generated-snapshot/oldest/index.d.ts
@@ -4239,6 +4239,13 @@ type AiOptions = {
   prefix?: string;
   extraHeaders?: object;
 };
+type ConversionResponse = {
+  name: string;
+  mimeType: string;
+  format: "markdown";
+  tokens: number;
+  data: string;
+};
 type AiModelsSearchParams = {
   author?: string;
   hide_experimental?: boolean;
@@ -4282,6 +4289,24 @@ declare abstract class Ai<AiModelList extends AiModelListType = AiModels> {
       : AiModelList[Name]["postProcessedOutputs"]
   >;
   public models(params?: AiModelsSearchParams): Promise<AiModelsSearchObject[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    }[],
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    },
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse>;
 }
 type GatewayOptions = {
   id: string;
diff --git a/types/generated-snapshot/oldest/index.ts b/types/generated-snapshot/oldest/index.ts
index eb847e588c6..ca142846977 100755
--- a/types/generated-snapshot/oldest/index.ts
+++ b/types/generated-snapshot/oldest/index.ts
@@ -4251,6 +4251,13 @@ export type AiOptions = {
   prefix?: string;
   extraHeaders?: object;
 };
+export type ConversionResponse = {
+  name: string;
+  mimeType: string;
+  format: "markdown";
+  tokens: number;
+  data: string;
+};
 export type AiModelsSearchParams = {
   author?: string;
   hide_experimental?: boolean;
@@ -4296,6 +4303,24 @@ export declare abstract class Ai<
       : AiModelList[Name]["postProcessedOutputs"]
   >;
   public models(params?: AiModelsSearchParams): Promise<AiModelsSearchObject[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    }[],
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse[]>;
+  public toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    },
+    options?: {
+      gateway?: GatewayOptions;
+    },
+  ): Promise<ConversionResponse>;
 }
 export type GatewayOptions = {
   id: string;


Reviews:
user: login: jasnell
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-25T21:02:00Z
user: login: anonrig
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T02:10:12Z
user: login: G4brym
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-26T10:57:36Z
user: login: G4brym
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-26T11:02:49Z
user: login: anonrig
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T14:35:01Z
user: login: anonrig
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T14:37:49Z
user: login: G4brym
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-26T14:50:03Z
user: login: G4brym
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-26T14:51:42Z
user: login: G4brym
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-27T11:15:48Z
user: login: jasnell
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T15:09:16Z
user: login: jasnell
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T15:11:33Z
user: login: jasnell
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T15:14:23Z
user: login: jasnell
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-04T15:15:07Z
user: login: G4brym
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T15:56:42Z
user: login: jasnell
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-05T21:37:37Z

Review comments:
diff_hunk: @@ -221,6 +229,68 @@ export class Ai {
     }
   }
 
+  public async toMarkdown(
+    files: { name: string; blob: Blob }[]
+  ): Promise<ConversionResponse[]>;
+  public async toMarkdown(files: {
+    name: string;
+    blob: Blob;
+  }): Promise<ConversionResponse>;
+  public async toMarkdown(
+    files: { name: string; blob: Blob } | { name: string; blob: Blob }[]
+  ): Promise<ConversionResponse | ConversionResponse[]> {
+    const input = Array.isArray(files) ? files : [files];
+
+    const processedFiles = [];
+    for (const file of input) {
+      processedFiles.push({
+        name: file.name,
+        mimeType: file.blob.type,
+        data: btoa(
+          String.fromCharCode(...new Uint8Array(await file.blob.arrayBuffer()))
+        ),
path: src/cloudflare/internal/ai-api.ts
user: login: jasnell
body: This feels extremely convoluted. I believe you ought to be able to make use of `internal-node:internal-buffer` here and just use `Buffer.from(await file.blob.arrayBuffer()).toString('base64'))` here.
created_at: 2025-02-25T21:02:00Z
author_association: COLLABORATOR
diff_hunk: @@ -221,6 +229,68 @@ export class Ai {
     }
   }
 
+  public async toMarkdown(
+    files: { name: string; blob: Blob }[]
+  ): Promise<ConversionResponse[]>;
+  public async toMarkdown(files: {
+    name: string;
+    blob: Blob;
+  }): Promise<ConversionResponse>;
+  public async toMarkdown(
+    files: { name: string; blob: Blob } | { name: string; blob: Blob }[]
+  ): Promise<ConversionResponse | ConversionResponse[]> {
+    const input = Array.isArray(files) ? files : [files];
+
+    const processedFiles = [];
+    for (const file of input) {
+      processedFiles.push({
+        name: file.name,
+        mimeType: file.blob.type,
+        data: btoa(
+          String.fromCharCode(...new Uint8Array(await file.blob.arrayBuffer()))
+        ),
+      });
+    }
+
+    const fetchOptions = {
+      method: 'POST',
+      body: JSON.stringify({
+        files: processedFiles,
+      }),
+      headers: {
+        'content-type': 'application/json',
+      },
+    };
+
+    let endpointUrl =
path: src/cloudflare/internal/ai-api.ts
user: login: anonrig
body: ```suggestion
    const endpointUrl =
```

created_at: 2025-02-26T02:05:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -221,6 +229,68 @@ export class Ai {
     }
   }
 
+  public async toMarkdown(
+    files: { name: string; blob: Blob }[]
+  ): Promise<ConversionResponse[]>;
+  public async toMarkdown(files: {
+    name: string;
+    blob: Blob;
+  }): Promise<ConversionResponse>;
+  public async toMarkdown(
+    files: { name: string; blob: Blob } | { name: string; blob: Blob }[]
+  ): Promise<ConversionResponse | ConversionResponse[]> {
+    const input = Array.isArray(files) ? files : [files];
+
+    const processedFiles = [];
+    for (const file of input) {
+      processedFiles.push({
+        name: file.name,
+        mimeType: file.blob.type,
+        data: btoa(
+          String.fromCharCode(...new Uint8Array(await file.blob.arrayBuffer()))
+        ),
+      });
+    }
+
+    const fetchOptions = {
+      method: 'POST',
+      body: JSON.stringify({
+        files: processedFiles,
+      }),
+      headers: {
+        'content-type': 'application/json',
+      },
+    };
+
+    let endpointUrl =
+      'https://workers-binding.ai/to-everything/markdown/transformer';
+
+    const res = await this.fetcher.fetch(endpointUrl, fetchOptions);
+
+    switch (res.status) {
+      case 200: {
+        const data = (await res.json()) as { result: ConversionResponse[] };
+
+        if (!data.result || data.result.length === 0) {
+          throw new AiInternalError(
+            'Internal Error Converting files into Markdown'
+          );
+        }
+
+        if (Array.isArray(files)) {
+          return data.result;
+        }
+
+        return data.result[0] as ConversionResponse;
path: src/cloudflare/internal/ai-api.ts
user: login: anonrig
body: I recommend removing this "as" and add an assertion for length or [0] to exist rather than forcing compiler
created_at: 2025-02-26T02:06:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -221,6 +229,68 @@ export class Ai {
     }
   }
 
+  public async toMarkdown(
+    files: { name: string; blob: Blob }[]
+  ): Promise<ConversionResponse[]>;
+  public async toMarkdown(files: {
+    name: string;
+    blob: Blob;
+  }): Promise<ConversionResponse>;
+  public async toMarkdown(
+    files: { name: string; blob: Blob } | { name: string; blob: Blob }[]
+  ): Promise<ConversionResponse | ConversionResponse[]> {
+    const input = Array.isArray(files) ? files : [files];
+
+    const processedFiles = [];
+    for (const file of input) {
+      processedFiles.push({
+        name: file.name,
+        mimeType: file.blob.type,
+        data: btoa(
+          String.fromCharCode(...new Uint8Array(await file.blob.arrayBuffer()))
+        ),
+      });
+    }
+
+    const fetchOptions = {
+      method: 'POST',
+      body: JSON.stringify({
+        files: processedFiles,
+      }),
+      headers: {
+        'content-type': 'application/json',
+      },
+    };
+
+    let endpointUrl =
+      'https://workers-binding.ai/to-everything/markdown/transformer';
+
+    const res = await this.fetcher.fetch(endpointUrl, fetchOptions);
+
+    switch (res.status) {
+      case 200: {
+        const data = (await res.json()) as { result: ConversionResponse[] };
+
+        if (!data.result || data.result.length === 0) {
+          throw new AiInternalError(
+            'Internal Error Converting files into Markdown'
+          );
+        }
+
+        if (Array.isArray(files)) {
path: src/cloudflare/internal/ai-api.ts
user: login: anonrig
body: I'm not following. Where is this defined?
created_at: 2025-02-26T02:08:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -221,6 +229,68 @@ export class Ai {
     }
   }
 
+  public async toMarkdown(
+    files: { name: string; blob: Blob }[]
+  ): Promise<ConversionResponse[]>;
+  public async toMarkdown(files: {
+    name: string;
+    blob: Blob;
+  }): Promise<ConversionResponse>;
+  public async toMarkdown(
+    files: { name: string; blob: Blob } | { name: string; blob: Blob }[]
+  ): Promise<ConversionResponse | ConversionResponse[]> {
+    const input = Array.isArray(files) ? files : [files];
+
+    const processedFiles = [];
+    for (const file of input) {
+      processedFiles.push({
+        name: file.name,
+        mimeType: file.blob.type,
+        data: btoa(
+          String.fromCharCode(...new Uint8Array(await file.blob.arrayBuffer()))
+        ),
+      });
+    }
+
+    const fetchOptions = {
+      method: 'POST',
+      body: JSON.stringify({
+        files: processedFiles,
+      }),
+      headers: {
+        'content-type': 'application/json',
+      },
+    };
+
+    let endpointUrl =
+      'https://workers-binding.ai/to-everything/markdown/transformer';
+
+    const res = await this.fetcher.fetch(endpointUrl, fetchOptions);
+
+    switch (res.status) {
+      case 200: {
+        const data = (await res.json()) as { result: ConversionResponse[] };
+
+        if (!data.result || data.result.length === 0) {
+          throw new AiInternalError(
+            'Internal Error Converting files into Markdown'
+          );
+        }
+
+        if (Array.isArray(files)) {
+          return data.result;
+        }
+
+        return data.result[0] as ConversionResponse;
+      }
+      default: {
+        const data = (await res.json()) as { errors: { message: string }[] };
+
+        throw new AiInternalError(data.errors[0]?.message || 'Internal Error');
path: src/cloudflare/internal/ai-api.ts
user: login: anonrig
body: Using .at() returns nullable response which is better than index based access which doesn't return nullable value in typescript

```suggestion
        throw new AiInternalError(data.errors.at(0)?.message || 'Internal Error');
```

created_at: 2025-02-26T02:09:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -221,6 +229,68 @@ export class Ai {
     }
   }
 
+  public async toMarkdown(
+    files: { name: string; blob: Blob }[]
+  ): Promise<ConversionResponse[]>;
+  public async toMarkdown(files: {
+    name: string;
+    blob: Blob;
+  }): Promise<ConversionResponse>;
+  public async toMarkdown(
+    files: { name: string; blob: Blob } | { name: string; blob: Blob }[]
+  ): Promise<ConversionResponse | ConversionResponse[]> {
+    const input = Array.isArray(files) ? files : [files];
+
+    const processedFiles = [];
+    for (const file of input) {
+      processedFiles.push({
+        name: file.name,
+        mimeType: file.blob.type,
+        data: btoa(
+          String.fromCharCode(...new Uint8Array(await file.blob.arrayBuffer()))
+        ),
path: src/cloudflare/internal/ai-api.ts
user: login: G4brym
body: this import as you are suggesting `import { Buffer } from 'internal-node:internal-buffer';` 
throws this error: `src/cloudflare/internal/ai-api.ts(6,24): error TS2307: Cannot find module 'internal-node:internal-buffer' or its corresponding type declarations.`
I didn't find any other `cloudflare/*` file importing anything from `internal-node` so it might be a simple configuration missing
created_at: 2025-02-26T10:57:36Z
author_association: MEMBER
diff_hunk: @@ -221,6 +229,68 @@ export class Ai {
     }
   }
 
+  public async toMarkdown(
+    files: { name: string; blob: Blob }[]
+  ): Promise<ConversionResponse[]>;
+  public async toMarkdown(files: {
+    name: string;
+    blob: Blob;
+  }): Promise<ConversionResponse>;
+  public async toMarkdown(
+    files: { name: string; blob: Blob } | { name: string; blob: Blob }[]
+  ): Promise<ConversionResponse | ConversionResponse[]> {
+    const input = Array.isArray(files) ? files : [files];
+
+    const processedFiles = [];
+    for (const file of input) {
+      processedFiles.push({
+        name: file.name,
+        mimeType: file.blob.type,
+        data: btoa(
+          String.fromCharCode(...new Uint8Array(await file.blob.arrayBuffer()))
+        ),
+      });
+    }
+
+    const fetchOptions = {
+      method: 'POST',
+      body: JSON.stringify({
+        files: processedFiles,
+      }),
+      headers: {
+        'content-type': 'application/json',
+      },
+    };
+
+    let endpointUrl =
+      'https://workers-binding.ai/to-everything/markdown/transformer';
+
+    const res = await this.fetcher.fetch(endpointUrl, fetchOptions);
+
+    switch (res.status) {
+      case 200: {
+        const data = (await res.json()) as { result: ConversionResponse[] };
+
+        if (!data.result || data.result.length === 0) {
+          throw new AiInternalError(
+            'Internal Error Converting files into Markdown'
+          );
+        }
+
+        if (Array.isArray(files)) {
path: src/cloudflare/internal/ai-api.ts
user: login: G4brym
body: I didn't understand the question, `files` is the function parameter, and this condition is just checking if the user sent one or multiple files as input
created_at: 2025-02-26T11:02:49Z
author_association: MEMBER
diff_hunk: @@ -221,6 +229,68 @@ export class Ai {
     }
   }
 
+  public async toMarkdown(
+    files: { name: string; blob: Blob }[]
+  ): Promise<ConversionResponse[]>;
+  public async toMarkdown(files: {
+    name: string;
+    blob: Blob;
+  }): Promise<ConversionResponse>;
+  public async toMarkdown(
+    files: { name: string; blob: Blob } | { name: string; blob: Blob }[]
+  ): Promise<ConversionResponse | ConversionResponse[]> {
+    const input = Array.isArray(files) ? files : [files];
+
+    const processedFiles = [];
+    for (const file of input) {
+      processedFiles.push({
+        name: file.name,
+        mimeType: file.blob.type,
+        data: btoa(
+          String.fromCharCode(...new Uint8Array(await file.blob.arrayBuffer()))
+        ),
+      });
+    }
+
+    const fetchOptions = {
+      method: 'POST',
+      body: JSON.stringify({
+        files: processedFiles,
+      }),
+      headers: {
+        'content-type': 'application/json',
+      },
+    };
+
+    let endpointUrl =
+      'https://workers-binding.ai/to-everything/markdown/transformer';
+
+    const res = await this.fetcher.fetch(endpointUrl, fetchOptions);
+
+    switch (res.status) {
+      case 200: {
+        const data = (await res.json()) as { result: ConversionResponse[] };
+
+        if (!data.result || data.result.length === 0) {
+          throw new AiInternalError(
+            'Internal Error Converting files into Markdown'
+          );
+        }
+
+        if (Array.isArray(files)) {
path: src/cloudflare/internal/ai-api.ts
user: login: anonrig
body: Ah I see. Can you add a comment to this line so the intended behavior is documented?
created_at: 2025-02-26T14:35:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -221,6 +229,77 @@ export class Ai {
     }
   }
 
+  public async toMarkdown(
+    files: { name: string; blob: Blob }[]
+  ): Promise<ConversionResponse[]>;
+  public async toMarkdown(files: {
+    name: string;
+    blob: Blob;
+  }): Promise<ConversionResponse>;
path: src/cloudflare/internal/ai-api.ts
user: login: anonrig
body: I don't think these are necessary since the output value doesn't change according to the parameter type.

```suggestion
```
created_at: 2025-02-26T14:35:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -221,6 +229,68 @@ export class Ai {
     }
   }
 
+  public async toMarkdown(
+    files: { name: string; blob: Blob }[]
+  ): Promise<ConversionResponse[]>;
+  public async toMarkdown(files: {
+    name: string;
+    blob: Blob;
+  }): Promise<ConversionResponse>;
+  public async toMarkdown(
+    files: { name: string; blob: Blob } | { name: string; blob: Blob }[]
+  ): Promise<ConversionResponse | ConversionResponse[]> {
+    const input = Array.isArray(files) ? files : [files];
+
+    const processedFiles = [];
+    for (const file of input) {
+      processedFiles.push({
+        name: file.name,
+        mimeType: file.blob.type,
+        data: btoa(
+          String.fromCharCode(...new Uint8Array(await file.blob.arrayBuffer()))
+        ),
path: src/cloudflare/internal/ai-api.ts
user: login: anonrig
body: The current implementation limits the length of the string to 65k. Using spread operator means that you are bounded by the V8's argument limitation.
created_at: 2025-02-26T14:36:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -221,6 +229,68 @@ export class Ai {
     }
   }
 
+  public async toMarkdown(
+    files: { name: string; blob: Blob }[]
+  ): Promise<ConversionResponse[]>;
+  public async toMarkdown(files: {
+    name: string;
+    blob: Blob;
+  }): Promise<ConversionResponse>;
+  public async toMarkdown(
+    files: { name: string; blob: Blob } | { name: string; blob: Blob }[]
+  ): Promise<ConversionResponse | ConversionResponse[]> {
+    const input = Array.isArray(files) ? files : [files];
+
+    const processedFiles = [];
+    for (const file of input) {
+      processedFiles.push({
+        name: file.name,
+        mimeType: file.blob.type,
+        data: btoa(
+          String.fromCharCode(...new Uint8Array(await file.blob.arrayBuffer()))
+        ),
path: src/cloudflare/internal/ai-api.ts
user: login: G4brym
body: i would prefer to use the  `internal-node` version, but I'm unable to import it using `import { Buffer } from 'internal-node:internal-buffer';`
created_at: 2025-02-26T14:50:03Z
author_association: MEMBER
diff_hunk: @@ -221,6 +229,77 @@ export class Ai {
     }
   }
 
+  public async toMarkdown(
+    files: { name: string; blob: Blob }[]
+  ): Promise<ConversionResponse[]>;
+  public async toMarkdown(files: {
+    name: string;
+    blob: Blob;
+  }): Promise<ConversionResponse>;
path: src/cloudflare/internal/ai-api.ts
user: login: G4brym
body: the output is dependent on the input, if the user sends a single file, the function returns an object, if the user sends a list, then an array is return
See `src/cloudflare/internal/test/ai/ai-api-test.js` for example usage
created_at: 2025-02-26T14:51:42Z
author_association: MEMBER
diff_hunk: @@ -221,6 +229,68 @@ export class Ai {
     }
   }
 
+  public async toMarkdown(
+    files: { name: string; blob: Blob }[]
+  ): Promise<ConversionResponse[]>;
+  public async toMarkdown(files: {
+    name: string;
+    blob: Blob;
+  }): Promise<ConversionResponse>;
+  public async toMarkdown(
+    files: { name: string; blob: Blob } | { name: string; blob: Blob }[]
+  ): Promise<ConversionResponse | ConversionResponse[]> {
+    const input = Array.isArray(files) ? files : [files];
+
+    const processedFiles = [];
+    for (const file of input) {
+      processedFiles.push({
+        name: file.name,
+        mimeType: file.blob.type,
+        data: btoa(
+          String.fromCharCode(...new Uint8Array(await file.blob.arrayBuffer()))
+        ),
path: src/cloudflare/internal/ai-api.ts
user: login: G4brym
body: I wasn't able to import the Buffer class, so i changed the implementation to stop using the spread operator, this should bypass the 65k limit
created_at: 2025-02-27T11:15:47Z
author_association: MEMBER
diff_hunk: @@ -221,6 +229,68 @@ export class Ai {
     }
   }
 
+  public async toMarkdown(
+    files: { name: string; blob: Blob }[]
+  ): Promise<ConversionResponse[]>;
+  public async toMarkdown(files: {
+    name: string;
+    blob: Blob;
+  }): Promise<ConversionResponse>;
+  public async toMarkdown(
+    files: { name: string; blob: Blob } | { name: string; blob: Blob }[]
+  ): Promise<ConversionResponse | ConversionResponse[]> {
+    const input = Array.isArray(files) ? files : [files];
+
+    const processedFiles = [];
+    for (const file of input) {
+      processedFiles.push({
+        name: file.name,
+        mimeType: file.blob.type,
+        data: btoa(
+          String.fromCharCode(...new Uint8Array(await file.blob.arrayBuffer()))
+        ),
path: src/cloudflare/internal/ai-api.ts
user: login: jasnell
body: hmm.. yeah it looks like there's a bit more we need to do in the runtime to make importing `node-internal:*` stuff available to `cloudflare-internal:*` ... dang. Ok, will have to fix that then we can update this soon.
created_at: 2025-03-04T15:09:16Z
author_association: COLLABORATOR
diff_hunk: @@ -74,6 +82,22 @@ export class AiInternalError extends Error {
   }
 }
 
+async function blobToBase64(blob: Blob): Promise<string> {
+  const arrayBuffer = await blob.arrayBuffer();
path: src/cloudflare/internal/ai-api.ts
user: login: jasnell
body: Let's add a todo comment here...
```suggestion
  // TODO(soon): This is better implemented using the node::buffer API
  // but we cannot get to that from here currently. Once the node:buffer
  // API (actually, `node-internal:internal_buffer`) is available to be imported
  // here we should update this code to use it instead.
  const arrayBuffer = await blob.arrayBuffer();
```
created_at: 2025-03-04T15:11:33Z
author_association: COLLABORATOR
diff_hunk: @@ -221,6 +245,82 @@ export class Ai {
     }
   }
 
+  public async toMarkdown(
+    files: { name: string; blob: Blob }[],
+    options?: { gateway?: GatewayOptions }
+  ): Promise<ConversionResponse[]>;
+  public async toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    },
+    options?: { gateway?: GatewayOptions }
+  ): Promise<ConversionResponse>;
+  public async toMarkdown(
+    files: { name: string; blob: Blob } | { name: string; blob: Blob }[],
+    options?: { gateway?: GatewayOptions }
+  ): Promise<ConversionResponse | ConversionResponse[]> {
+    const input = Array.isArray(files) ? files : [files];
+
+    const processedFiles = [];
+    for (const file of input) {
+      processedFiles.push({
+        name: file.name,
+        mimeType: file.blob.type,
+        data: await blobToBase64(file.blob),
+      });
+    }
+
+    const fetchOptions = {
+      method: 'POST',
+      body: JSON.stringify({
+        files: processedFiles,
+        options: options,
+      }),
+      headers: {
+        'content-type': 'application/json',
+      },
+    };
+
+    const endpointUrl =
+      'https://workers-binding.ai/to-everything/markdown/transformer';
+
+    const res = await this.fetcher.fetch(endpointUrl, fetchOptions);
+
+    switch (res.status) {
+      case 200: {
path: src/cloudflare/internal/ai-api.ts
user: login: jasnell
body: A better practice would be to check for `if (res.ok) { ...` which covers the full range of 2xx status codes, not just 200, but this is fine if you know your service would only ever produce 200 codes.
created_at: 2025-03-04T15:14:23Z
author_association: COLLABORATOR
diff_hunk: @@ -221,6 +245,82 @@ export class Ai {
     }
   }
 
+  public async toMarkdown(
+    files: { name: string; blob: Blob }[],
+    options?: { gateway?: GatewayOptions }
+  ): Promise<ConversionResponse[]>;
+  public async toMarkdown(
+    files: {
+      name: string;
+      blob: Blob;
+    },
+    options?: { gateway?: GatewayOptions }
+  ): Promise<ConversionResponse>;
+  public async toMarkdown(
+    files: { name: string; blob: Blob } | { name: string; blob: Blob }[],
+    options?: { gateway?: GatewayOptions }
+  ): Promise<ConversionResponse | ConversionResponse[]> {
+    const input = Array.isArray(files) ? files : [files];
+
+    const processedFiles = [];
+    for (const file of input) {
+      processedFiles.push({
+        name: file.name,
+        mimeType: file.blob.type,
+        data: await blobToBase64(file.blob),
+      });
+    }
+
+    const fetchOptions = {
+      method: 'POST',
+      body: JSON.stringify({
+        files: processedFiles,
+        options: options,
+      }),
+      headers: {
+        'content-type': 'application/json',
+      },
+    };
+
+    const endpointUrl =
+      'https://workers-binding.ai/to-everything/markdown/transformer';
+
+    const res = await this.fetcher.fetch(endpointUrl, fetchOptions);
+
+    switch (res.status) {
+      case 200: {
path: src/cloudflare/internal/ai-api.ts
user: login: G4brym
body: this service only produces 200
I used the switch statement, because other methods in this file produce several codes and use it already
created_at: 2025-03-04T15:56:42Z
author_association: MEMBER

PR comments:
user: login: G4brym
created_at: 2025-03-04T11:06:12Z
body: Hey @jasnell or @anonrig can you take a look at this pr again?
author_association: MEMBER
user: login: G4brym
created_at: 2025-03-04T22:57:03Z
body: Can someone merge this pr? i don't have permissions
author_association: MEMBER