UNPKG

@anthropic-ai/bedrock-sdk

Version:

The official TypeScript library for the Anthropic Bedrock API

194 lines (173 loc) 6.98 kB
import { type RequestOptions } from './request-options'; import type { FilePropertyBag, Fetch } from './builtin-types'; import type { BaseAnthropic } from '../client'; import { ReadableStreamFrom } from './shims'; export type BlobPart = string | ArrayBuffer | ArrayBufferView | Blob | DataView; type FsReadStream = AsyncIterable<Uint8Array> & { path: string | { toString(): string } }; // https://github.com/oven-sh/bun/issues/5980 interface BunFile extends Blob { readonly name?: string | undefined; } export const checkFileSupport = () => { if (typeof File === 'undefined') { const { process } = globalThis as any; const isOldNode = typeof process?.versions?.node === 'string' && parseInt(process.versions.node.split('.')) < 20; throw new Error( '`File` is not defined as a global, which is required for file uploads.' + (isOldNode ? " Update to Node 20 LTS or newer, or set `globalThis.File` to `import('node:buffer').File`." : ''), ); } }; /** * Typically, this is a native "File" class. * * We provide the {@link toFile} utility to convert a variety of objects * into the File class. * * For convenience, you can also pass a fetch Response, or in Node, * the result of fs.createReadStream(). */ export type Uploadable = File | Response | FsReadStream | BunFile; /** * Construct a `File` instance. This is used to ensure a helpful error is thrown * for environments that don't define a global `File` yet. */ export function makeFile( fileBits: BlobPart[], fileName: string | undefined, options?: FilePropertyBag, ): File { checkFileSupport(); return new File(fileBits as any, fileName ?? 'unknown_file', options); } export function getName(value: any): string | undefined { return ( ( (typeof value === 'object' && value !== null && (('name' in value && value.name && String(value.name)) || ('url' in value && value.url && String(value.url)) || ('filename' in value && value.filename && String(value.filename)) || ('path' in value && value.path && String(value.path)))) || '' ) .split(/[\\/]/) .pop() || undefined ); } export const isAsyncIterable = (value: any): value is AsyncIterable<any> => value != null && typeof value === 'object' && typeof value[Symbol.asyncIterator] === 'function'; /** * Returns a multipart/form-data request if any part of the given request body contains a File / Blob value. * Otherwise returns the request as is. */ export const maybeMultipartFormRequestOptions = async ( opts: RequestOptions, fetch: BaseAnthropic | Fetch, ): Promise<RequestOptions> => { if (!hasUploadableValue(opts.body)) return opts; return { ...opts, body: await createForm(opts.body, fetch) }; }; type MultipartFormRequestOptions = Omit<RequestOptions, 'body'> & { body: unknown }; export const multipartFormRequestOptions = async ( opts: MultipartFormRequestOptions, fetch: BaseAnthropic | Fetch, ): Promise<RequestOptions> => { return { ...opts, body: await createForm(opts.body, fetch) }; }; const supportsFormDataMap = new WeakMap<Fetch, Promise<boolean>>(); /** * node-fetch doesn't support the global FormData object in recent node versions. Instead of sending * properly-encoded form data, it just stringifies the object, resulting in a request body of "[object FormData]". * This function detects if the fetch function provided supports the global FormData object to avoid * confusing error messages later on. */ function supportsFormData(fetchObject: BaseAnthropic | Fetch): Promise<boolean> { const fetch: Fetch = typeof fetchObject === 'function' ? fetchObject : (fetchObject as any).fetch; const cached = supportsFormDataMap.get(fetch); if (cached) return cached; const promise = (async () => { try { const FetchResponse = ( 'Response' in fetch ? fetch.Response : (await fetch('data:,')).constructor) as typeof Response; const data = new FormData(); if (data.toString() === (await new FetchResponse(data).text())) { return false; } return true; } catch { // avoid false negatives return true; } })(); supportsFormDataMap.set(fetch, promise); return promise; } export const createForm = async <T = Record<string, unknown>>( body: T | undefined, fetch: BaseAnthropic | Fetch, ): Promise<FormData> => { if (!(await supportsFormData(fetch))) { throw new TypeError( 'The provided fetch function does not support file uploads with the current global FormData class.', ); } const form = new FormData(); await Promise.all(Object.entries(body || {}).map(([key, value]) => addFormValue(form, key, value))); return form; }; // We check for Blob not File because Bun.File doesn't inherit from File, // but they both inherit from Blob and have a `name` property at runtime. const isNamedBlob = (value: object): value is Blob => value instanceof Blob && 'name' in value; const isUploadable = (value: unknown) => typeof value === 'object' && value !== null && (value instanceof Response || isAsyncIterable(value) || isNamedBlob(value)); const hasUploadableValue = (value: unknown): boolean => { if (isUploadable(value)) return true; if (Array.isArray(value)) return value.some(hasUploadableValue); if (value && typeof value === 'object') { for (const k in value) { if (hasUploadableValue((value as any)[k])) return true; } } return false; }; const addFormValue = async (form: FormData, key: string, value: unknown): Promise<void> => { if (value === undefined) return; if (value == null) { throw new TypeError( `Received null for "${key}"; to pass null in FormData, you must use the string 'null'`, ); } // TODO: make nested formats configurable if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { form.append(key, String(value)); } else if (value instanceof Response) { let options = {} as FilePropertyBag; const contentType = value.headers.get('Content-Type'); if (contentType) { options = { type: contentType }; } form.append(key, makeFile([await value.blob()], getName(value), options)); } else if (isAsyncIterable(value)) { form.append(key, makeFile([await new Response(ReadableStreamFrom(value)).blob()], getName(value))); } else if (isNamedBlob(value)) { form.append(key, makeFile([value], getName(value), { type: value.type })); } else if (Array.isArray(value)) { await Promise.all(value.map((entry) => addFormValue(form, key + '[]', entry))); } else if (typeof value === 'object') { await Promise.all( Object.entries(value).map(([name, prop]) => addFormValue(form, `${key}[${name}]`, prop)), ); } else { throw new TypeError( `Invalid value given to form, expected a string, number, boolean, object, Array, File or Blob but got ${value} instead`, ); } };