Skip to content
File

Blob: src/cloudflare/internal/to-markdown-api.ts

typescript175 lines
1// Copyright (c) 2026 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5import { AiInternalError } from 'cloudflare-internal:ai-api';
6import type { GatewayOptions } from 'cloudflare-internal:aig-api';
7import base64 from 'cloudflare-internal:base64';
8 
9interface Fetcher {
10 fetch: typeof fetch;
11}
12 
13export type MarkdownDocument = {
14 name: string;
15 blob: Blob;
16};
17 
18export type ConversionResponse =
19 | {
20 id: string;
21 name: string;
22 mimeType: string;
23 format: 'markdown';
24 tokens: number;
25 data: string;
26 }
27 | {
28 id: string;
29 name: string;
30 mimeType: string;
31 format: 'error';
32 error: string;
33 };
34 
35export type ImageConversionOptions = {
36 descriptionLanguage?: 'en' | 'es' | 'fr' | 'it' | 'pt' | 'de';
37};
38 
39export type EmbeddedImageConversionOptions = ImageConversionOptions & {
40 convert?: boolean;
41 maxConvertedImages?: number;
42};
43 
44export type ConversionOptions = {
45 html?: {
46 images?: EmbeddedImageConversionOptions & { convertOGImage?: boolean };
47 hostname?: string;
48 cssSelector?: string;
49 };
50 docx?: {
51 images?: EmbeddedImageConversionOptions;
52 };
53 image?: ImageConversionOptions;
54 pdf?: {
55 images?: EmbeddedImageConversionOptions;
56 metadata?: boolean;
57 };
58};
59 
60export type ConversionRequestOptions = {
61 gateway?: GatewayOptions;
62 extraHeaders?: object;
63 conversionOptions?: ConversionOptions;
64};
65 
66export type SupportedFileFormat = {
67 mimeType: string;
68 extension: string;
69};
70 
71async function blobToBase64(blob: Blob): Promise<string> {
72 return base64.encodeArrayToString(await blob.arrayBuffer());
73}
74 
75export class ToMarkdownService {
76 #fetcher: Fetcher;
77 #endpointURL = 'https://workers-binding.ai';
78 
79 constructor(fetcher: Fetcher) {
80 this.#fetcher = fetcher;
81 }
82 
83 async transform(
84 files: MarkdownDocument[],
85 options?: ConversionRequestOptions
86 ): Promise<ConversionResponse[]>;
87 async transform(
88 files: MarkdownDocument,
89 options?: ConversionRequestOptions
90 ): Promise<ConversionResponse>;
91 async transform(
92 files: MarkdownDocument | MarkdownDocument[],
93 options?: ConversionRequestOptions
94 ): Promise<ConversionResponse | ConversionResponse[]> {
95 const input = Array.isArray(files) ? files : [files];
96 
97 const processedFiles = [];
98 for (const file of input) {
99 processedFiles.push({
100 name: file.name,
101 mimeType: file.blob.type,
102 data: await blobToBase64(file.blob),
103 });
104 }
105 
106 const fetchOptions = {
107 method: 'POST',
108 body: JSON.stringify({
109 files: processedFiles,
110 options: options,
111 }),
112 headers: {
113 ...options?.extraHeaders,
114 'content-type': 'application/json',
115 },
116 };
117 
118 const endpointUrl = `${this.#endpointURL}/to-everything/markdown/transformer`;
119 
120 const res = await this.#fetcher.fetch(endpointUrl, fetchOptions);
121 
122 if (!res.ok) {
123 const content = (await res.json()) as {
124 errors: { message: string }[];
125 };
126 
127 throw new AiInternalError(
128 content.errors.at(0)?.message || 'Internal Error'
129 );
130 }
131 
132 const data = (await res.json()) as { result: ConversionResponse[] };
133 
134 // If the user sent a list of files, return an array of results, otherwise, return just the first object
135 if (Array.isArray(files)) {
136 return data.result;
137 }
138 
139 if (data.result.length === 0) {
140 throw new AiInternalError(
141 'Internal Error Converting files into Markdown'
142 );
143 }
144 
145 const obj = data.result.at(0);
146 if (!obj) {
147 throw new AiInternalError(
148 'Internal Error Converting files into Markdown'
149 );
150 }
151 
152 return obj;
153 }
154 
155 async supported(): Promise<SupportedFileFormat[]> {
156 const fetchOptions = {
157 method: 'GET',
158 headers: {
159 'content-type': 'application/json',
160 },
161 };
162 
163 const endpointUrl = `${this.#endpointURL}/to-everything/markdown/supported`;
164 
165 const res = await this.#fetcher.fetch(endpointUrl, fetchOptions);
166 
167 // This endpoint never fails so just parse the output
168 const { result: extensions } = (await res.json()) as {
169 result: SupportedFileFormat[];
170 };
171 
172 return extensions;
173 }
174}