UNPKG

ai

Version:

AI SDK by Vercel - build apps like ChatGPT, Claude, Gemini, and more with a single interface for any model using the Vercel AI Gateway or go direct to OpenAI, Anthropic, Google, or any other model provider.

183 lines (170 loc) 6 kB
import type { FilesV4, FilesV4UploadFileCallOptions, ProviderV4, } from '@ai-sdk/provider'; import { convertBase64ToUint8Array, detectMediaType, } from '@ai-sdk/provider-utils'; import type { ProviderMetadata } from '../types/provider-metadata'; import type { ProviderReference } from '../types/provider-reference'; import type { Warning } from '../types/warning'; import type { UploadFileResult } from './upload-file-result'; /** * Uploads a file using a files API interface. * * @param api - The Files API interface to use for uploading. * @param data - The file data to upload (tagged `{ type: 'data' | 'text' | 'stream' }`). * Stream data is sent without buffering by providers that support streaming * uploads (others reject with `UnsupportedFunctionalityError`); the provider * consumes the stream — any failed upload, including validation failures * before a request is made, cancels it. Do not reuse it. * @param mediaType - Optional IANA media type. Auto-detected from file bytes * when omitted (falls back to `text/plain` for the `text` variant and * `application/octet-stream` for the `stream` variant, which cannot be sniffed). * @param filename - Optional filename for the uploaded file. Multipart-based * providers default it to `"blob"` when omitted. * @param abortSignal - Optional signal to cancel the upload. * @param headers - Optional additional HTTP headers for the request. * @param providerOptions - Additional provider-specific options. * * @returns A result object containing the provider reference, optional * metadata, and — when reported by the provider — `byteSize`, `createdAt`, * and `expiresAt` (the provider-applied retention expiry). */ export async function uploadFile({ api, data: dataArg, mediaType: mediaTypeArg, filename, abortSignal, headers, providerOptions, }: { /** * The files API interface to use for uploading. * Can be a `FilesV4` instance or a `ProviderV4` instance with a `files()` method. */ api: FilesV4 | ProviderV4; } & Omit<FilesV4UploadFileCallOptions, 'mediaType' | 'data'> & { /** * The file data. Accepts the tagged `{ type: 'data' | 'text' }` shapes, or * the shorthand `Uint8Array | string` (treated as `{ type: 'data', data }`). */ data: FilesV4UploadFileCallOptions['data'] | Uint8Array | string; /** * Optional IANA media type of the file. Auto-detected from file bytes when * omitted; falls back to `text/plain` for the `text` variant. */ mediaType?: string; }): Promise<UploadFileResult> { const data: FilesV4UploadFileCallOptions['data'] = dataArg instanceof Uint8Array || typeof dataArg === 'string' ? { type: 'data', data: dataArg } : dataArg; // stream data cannot be sniffed without consuming it const mediaType = mediaTypeArg ?? (data.type === 'text' ? 'text/plain' : data.type === 'stream' ? 'application/octet-stream' : (detectMediaType({ data: data.data }) ?? (isLikelyText(data.data) ? 'text/plain' : 'application/octet-stream'))); let result; try { const filesApi: FilesV4 = 'uploadFile' in api ? api : typeof api.files === 'function' ? api.files() : (() => { throw new Error( 'The provider does not support file uploads. Make sure it exposes a files() method.', ); })(); result = await filesApi.uploadFile({ data, mediaType, filename, abortSignal, headers, providerOptions, }); } catch (error) { // ownership guarantee: a failed upload releases the stream, even when // the provider rejected before (or without) consuming it if (data.type === 'stream') { await data.stream.cancel(error).catch(() => {}); } throw error; } return new DefaultUploadFileResult({ providerReference: result.providerReference, mediaType: result.mediaType, filename: result.filename, byteSize: result.byteSize, createdAt: result.createdAt, expiresAt: result.expiresAt, providerMetadata: result.providerMetadata, warnings: result.warnings, }); } class DefaultUploadFileResult implements UploadFileResult { readonly providerReference: ProviderReference; readonly mediaType?: string; readonly filename?: string; readonly byteSize?: number; readonly createdAt?: Date; readonly expiresAt?: Date; readonly providerMetadata?: ProviderMetadata; readonly warnings: Array<Warning>; constructor(options: { providerReference: ProviderReference; mediaType?: string; filename?: string; byteSize?: number; createdAt?: Date; expiresAt?: Date; providerMetadata?: ProviderMetadata; warnings: Array<Warning>; }) { this.providerReference = options.providerReference; this.mediaType = options.mediaType; this.filename = options.filename; this.byteSize = options.byteSize; this.createdAt = options.createdAt; this.expiresAt = options.expiresAt; this.providerMetadata = options.providerMetadata; this.warnings = options.warnings; } } function isLikelyText(data: Uint8Array | string): boolean { /* * Limit checks to 512 bytes for performance. * 4 base64 characters represent 3 bytes, and we use a small margin of 4 bytes just to be safe. */ const CHECK_LENGTH = 512; const BASE64_CHECK_LENGTH = Math.ceil((CHECK_LENGTH + 4) / 3) * 4; const bytes = typeof data === 'string' ? convertBase64ToUint8Array( data.substring(0, Math.min(data.length, BASE64_CHECK_LENGTH)), ) : data; const checkLength = Math.min(bytes.length, CHECK_LENGTH); if (checkLength === 0) return false; for (let i = 0; i < checkLength; i++) { const byte = bytes[i]; if ( byte === 0x00 || (byte < 0x20 && byte !== 0x09 && byte !== 0x0a && byte !== 0x0d) ) { return false; } } return true; }