openai
Version:
The official TypeScript library for the OpenAI API
309 lines • 12.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createForm = exports.multipartFormRequestOptions = exports.maybeMultipartFormRequestOptions = exports.isAsyncIterable = exports.checkFileSupport = void 0;
exports.toStreamingFile = toStreamingFile;
exports.makeFile = makeFile;
exports.getName = getName;
const headers_1 = require("./headers.js");
const shims_1 = require("./shims.js");
const bytes_1 = require("./utils/bytes.js");
const brand_privateStreamingFile = /* @__PURE__ */ Symbol('brand.privateStreamingFile');
/**
* Wrap a stream as an uploadable file without reading it into memory.
*
* Unlike {@link toFile}, this helper does not create a web `File`, because the `File` constructor
* must consume all of its contents up front. The stream is instead encoded lazily as multipart
* form data when the request is sent.
*/
function toStreamingFile(data, name, options) {
if (!name) {
throw new TypeError('toStreamingFile requires a non-empty file name');
}
return {
[brand_privateStreamingFile]: true,
data,
name,
...(options?.type ? { type: options.type } : {}),
};
}
const checkFileSupport = () => {
if (typeof File === 'undefined') {
const { process } = globalThis;
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`."
: ''));
}
};
exports.checkFileSupport = checkFileSupport;
/**
* Construct a `File` instance. This is used to ensure a helpful error is thrown
* for environments that don't define a global `File` yet.
*/
function makeFile(fileBits, fileName, options) {
(0, exports.checkFileSupport)();
return new File(fileBits, fileName ?? 'unknown_file', options);
}
function getName(value) {
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);
}
const isAsyncIterable = (value) => value != null && typeof value === 'object' && typeof value[Symbol.asyncIterator] === 'function';
exports.isAsyncIterable = isAsyncIterable;
/**
* 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.
*/
const maybeMultipartFormRequestOptions = async (opts, fetch) => {
if (!hasUploadableValue(opts.body))
return opts;
if (hasStreamingUploadableValue(opts.body)) {
return createStreamingFormRequestOptions(opts);
}
return { ...opts, body: await (0, exports.createForm)(opts.body, fetch) };
};
exports.maybeMultipartFormRequestOptions = maybeMultipartFormRequestOptions;
const multipartFormRequestOptions = async (opts, fetch) => {
if (hasStreamingUploadableValue(opts.body)) {
return createStreamingFormRequestOptions(opts);
}
return { ...opts, body: await (0, exports.createForm)(opts.body, fetch) };
};
exports.multipartFormRequestOptions = multipartFormRequestOptions;
const supportsFormDataMap = /* @__PURE__ */ new WeakMap();
/**
* 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) {
const fetch = typeof fetchObject === 'function' ? fetchObject : fetchObject.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);
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;
}
const createForm = async (body, fetch) => {
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;
};
exports.createForm = createForm;
// 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) => value instanceof Blob && 'name' in value;
const isReadableStream = (value) => typeof value === 'object' &&
value !== null &&
'getReader' in value &&
typeof value.getReader === 'function';
const isStreamingFile = (value) => typeof value === 'object' && value !== null && brand_privateStreamingFile in value;
const isUploadable = (value) => typeof value === 'object' &&
value !== null &&
(value instanceof Response ||
(0, exports.isAsyncIterable)(value) ||
isReadableStream(value) ||
isStreamingFile(value) ||
isNamedBlob(value));
const hasStreamingUploadableValue = (value) => {
if (isStreamingFile(value) || (0, exports.isAsyncIterable)(value) || isReadableStream(value))
return true;
if (Array.isArray(value))
return value.some(hasStreamingUploadableValue);
if (value && typeof value === 'object' && !isNamedBlob(value) && !(value instanceof Response)) {
for (const k in value) {
if (hasStreamingUploadableValue(value[k]))
return true;
}
}
return false;
};
const hasUploadableValue = (value) => {
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[k]))
return true;
}
}
return false;
};
const createStreamingFormRequestOptions = (opts) => {
const boundary = `openai-${Math.random().toString(36).slice(2)}`;
const body = (0, shims_1.ReadableStreamFrom)(iterateMultipartBody(opts.body, boundary));
return {
...opts,
body,
headers: (0, headers_1.buildHeaders)([{ 'content-type': `multipart/form-data; boundary=${boundary}` }, opts.headers]),
};
};
async function* iterateMultipartBody(body, boundary) {
for await (const { key, value } of iterateFormEntries(body)) {
yield (0, bytes_1.encodeUTF8)(`--${boundary}\r\n`);
if (isUploadable(value)) {
const filename = getStreamingFileName(value);
const type = getStreamingFileType(value);
yield (0, bytes_1.encodeUTF8)(`Content-Disposition: form-data; name="${escapeHeaderValue(key)}"; filename="${escapeHeaderValue(filename)}"\r\n` + `Content-Type: ${type}\r\n\r\n`);
yield* iterateBytes(getStreamingFileData(value));
}
else {
yield (0, bytes_1.encodeUTF8)(`Content-Disposition: form-data; name="${escapeHeaderValue(key)}"\r\n\r\n${String(value)}`);
}
yield (0, bytes_1.encodeUTF8)('\r\n');
}
yield (0, bytes_1.encodeUTF8)(`--${boundary}--\r\n`);
}
async function* iterateFormEntries(body) {
if (!body || typeof body !== 'object')
return;
for (const [key, value] of Object.entries(body)) {
yield* iterateFormValue(key, value);
}
}
async function* iterateFormValue(key, value) {
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'`);
}
if (typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean' ||
isUploadable(value)) {
yield { key, value };
}
else if (Array.isArray(value)) {
for (const entry of value) {
yield* iterateFormValue(key + '[]', entry);
}
}
else if (typeof value === 'object') {
for (const [name, prop] of Object.entries(value)) {
yield* iterateFormValue(`${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`);
}
}
function getStreamingFileName(value) {
return isStreamingFile(value) ? value.name : getName(value) ?? 'unknown_file';
}
function getStreamingFileType(value) {
if (isStreamingFile(value))
return value.type || 'application/octet-stream';
if (isNamedBlob(value) && value.type)
return value.type;
if (value instanceof Response)
return value.headers.get('content-type') || 'application/octet-stream';
return 'application/octet-stream';
}
function getStreamingFileData(value) {
if (isStreamingFile(value))
return value.data;
return value;
}
async function* iterateBytes(value) {
if (typeof value === 'string') {
yield (0, bytes_1.encodeUTF8)(value);
}
else if (ArrayBuffer.isView(value)) {
yield new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
}
else if (value instanceof ArrayBuffer) {
yield new Uint8Array(value);
}
else if (value instanceof Response) {
if (value.body) {
yield* iterateBytes(value.body);
}
else {
yield* iterateBytes(await value.blob());
}
}
else if (value instanceof Blob) {
if (typeof value.stream === 'function') {
yield* iterateBytes(value.stream());
}
else {
yield new Uint8Array(await value.arrayBuffer());
}
}
else if (isReadableStream(value)) {
for await (const chunk of (0, shims_1.ReadableStreamToAsyncIterable)(value)) {
yield* iterateBytes(chunk);
}
}
else if ((0, exports.isAsyncIterable)(value)) {
for await (const chunk of value) {
yield* iterateBytes(chunk);
}
}
else {
throw new TypeError(`Invalid streaming file chunk: ${String(value)}`);
}
}
function escapeHeaderValue(value) {
return value.replace(/["\\\r\n]/g, (character) => encodeURIComponent(character));
}
const addFormValue = async (form, key, value) => {
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) {
form.append(key, makeFile([await value.blob()], getName(value)));
}
else if ((0, exports.isAsyncIterable)(value)) {
form.append(key, makeFile([await new Response((0, shims_1.ReadableStreamFrom)(value)).blob()], getName(value)));
}
else if (isNamedBlob(value)) {
form.append(key, value, getName(value));
}
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`);
}
};
//# sourceMappingURL=uploads.js.map