diff --git a/src/internal/uploads.ts b/src/internal/uploads.ts index 15073983b..7b0c59202 100644 --- a/src/internal/uploads.ts +++ b/src/internal/uploads.ts @@ -75,10 +75,11 @@ export const isAsyncIterable = (value: any): value is AsyncIterable => export const maybeMultipartFormRequestOptions = async ( opts: RequestOptions, fetch: OpenAI | Fetch, + { stripFilenames = true }: { stripFilenames?: boolean } = {}, ): Promise => { if (!hasUploadableValue(opts.body)) return opts; - return { ...opts, body: await createForm(opts.body, fetch) }; + return { ...opts, body: await createForm(opts.body, fetch, { stripFilenames }) }; }; type MultipartFormRequestOptions = Omit & { body: unknown }; @@ -86,8 +87,9 @@ type MultipartFormRequestOptions = Omit & { body: unknow export const multipartFormRequestOptions = async ( opts: MultipartFormRequestOptions, fetch: OpenAI | Fetch, + { stripFilenames = true }: { stripFilenames?: boolean } = {}, ): Promise => { - return { ...opts, body: await createForm(opts.body, fetch) }; + return { ...opts, body: await createForm(opts.body, fetch, { stripFilenames }) }; }; const supportsFormDataMap = /* @__PURE__ */ new WeakMap>(); @@ -125,6 +127,7 @@ function supportsFormData(fetchObject: OpenAI | Fetch): Promise { export const createForm = async >( body: T | undefined, fetch: OpenAI | Fetch, + { stripFilenames = true }: { stripFilenames?: boolean } = {}, ): Promise => { if (!(await supportsFormData(fetch))) { throw new TypeError( @@ -132,7 +135,9 @@ export const createForm = async >( ); } const form = new FormData(); - await Promise.all(Object.entries(body || {}).map(([key, value]) => addFormValue(form, key, value))); + await Promise.all( + Object.entries(body || {}).map(([key, value]) => addFormValue(form, key, value, { stripFilenames })), + ); return form; }; @@ -156,7 +161,12 @@ const hasUploadableValue = (value: unknown): boolean => { return false; }; -const addFormValue = async (form: FormData, key: string, value: unknown): Promise => { +const addFormValue = async ( + form: FormData, + key: string, + value: unknown, + { stripFilenames = true }: { stripFilenames?: boolean } = {}, +): Promise => { if (value === undefined) return; if (value == null) { throw new TypeError( @@ -172,12 +182,16 @@ const addFormValue = async (form: FormData, key: string, value: unknown): Promis } else if (isAsyncIterable(value)) { form.append(key, makeFile([await new Response(ReadableStreamFrom(value)).blob()], getName(value))); } else if (isNamedBlob(value)) { - form.append(key, value, getName(value)); + form.append( + key, + value, + stripFilenames ? getName(value) : (('name' in value && value.name && String(value.name)) || getName(value)), + ); } else if (Array.isArray(value)) { - await Promise.all(value.map((entry) => addFormValue(form, key + '[]', entry))); + await Promise.all(value.map((entry) => addFormValue(form, key + '[]', entry, { stripFilenames }))); } else if (typeof value === 'object') { await Promise.all( - Object.entries(value).map(([name, prop]) => addFormValue(form, `${key}[${name}]`, prop)), + Object.entries(value).map(([name, prop]) => addFormValue(form, `${key}[${name}]`, prop, { stripFilenames })), ); } else { throw new TypeError( diff --git a/src/resources/responses/responses.ts b/src/resources/responses/responses.ts index d43489f21..dd811e372 100644 --- a/src/resources/responses/responses.ts +++ b/src/resources/responses/responses.ts @@ -5935,7 +5935,7 @@ export interface ResponseRefusalDoneEvent { export type ResponseStatus = 'completed' | 'failed' | 'in_progress' | 'cancelled' | 'queued' | 'incomplete'; /** - * Emitted when there is a partial audio response. + * A streamed event emitted by the Responses API. */ export type ResponseStreamEvent = | ResponseAudioDeltaEvent diff --git a/src/resources/skills/skills.ts b/src/resources/skills/skills.ts index c7d9ff89c..bcf798474 100644 --- a/src/resources/skills/skills.ts +++ b/src/resources/skills/skills.ts @@ -30,7 +30,10 @@ export class Skills extends APIResource { * Create a new skill. */ create(body: SkillCreateParams | null | undefined = {}, options?: RequestOptions): APIPromise { - return this._client.post('/skills', maybeMultipartFormRequestOptions({ body, ...options }, this._client)); + return this._client.post( + '/skills', + maybeMultipartFormRequestOptions({ body, ...options }, this._client, { stripFilenames: false }), + ); } /** diff --git a/src/resources/skills/versions/versions.ts b/src/resources/skills/versions/versions.ts index 19ab483f4..21ea47241 100644 --- a/src/resources/skills/versions/versions.ts +++ b/src/resources/skills/versions/versions.ts @@ -23,7 +23,7 @@ export class Versions extends APIResource { ): APIPromise { return this._client.post( path`/skills/${skillID}/versions`, - maybeMultipartFormRequestOptions({ body, ...options }, this._client), + maybeMultipartFormRequestOptions({ body, ...options }, this._client, { stripFilenames: false }), ); } diff --git a/tests/api-resources/skills/skills.test.ts b/tests/api-resources/skills/skills.test.ts index 0d4a5d80d..afb261aec 100644 --- a/tests/api-resources/skills/skills.test.ts +++ b/tests/api-resources/skills/skills.test.ts @@ -1,6 +1,8 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import OpenAI, { toFile } from 'openai'; +import { mockFetch } from '../../utils/mock-fetch'; +import { File } from 'node:buffer'; const client = new OpenAI({ apiKey: 'My API Key', @@ -29,6 +31,25 @@ describe('resource skills', () => { ).rejects.toThrow(OpenAI.NotFoundError); }); + test('create: preserves nested skill file paths', async () => { + const { fetch, handleRequest } = mockFetch(); + const client = new OpenAI({ apiKey: 'My API Key', fetch }); + + handleRequest(async (_, init) => { + const files = (init!.body as FormData).getAll('files[]'); + expect(files).toHaveLength(1); + expect(files[0]).toBeInstanceOf(File); + expect((files[0] as File).name).toEqual('my-skill/SKILL.md'); + + return new Response(JSON.stringify({ id: 'skill_123', created_at: 0, default_version: '1', description: '', latest_version: '1', name: 'my-skill', object: 'skill' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + }); + + await client.skills.create({ files: [new File(['# skill'], 'my-skill/SKILL.md')] }); + }); + test('retrieve', async () => { const responsePromise = client.skills.retrieve('skill_123'); const rawResponse = await responsePromise.asResponse(); diff --git a/tests/api-resources/skills/versions/versions.test.ts b/tests/api-resources/skills/versions/versions.test.ts index 486ecd9df..cce8165f7 100644 --- a/tests/api-resources/skills/versions/versions.test.ts +++ b/tests/api-resources/skills/versions/versions.test.ts @@ -1,6 +1,8 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import OpenAI, { toFile } from 'openai'; +import { mockFetch } from '../../../utils/mock-fetch'; +import { File } from 'node:buffer'; const client = new OpenAI({ apiKey: 'My API Key', @@ -30,6 +32,36 @@ describe('resource versions', () => { ).rejects.toThrow(OpenAI.NotFoundError); }); + test('create: preserves nested skill file paths', async () => { + const { fetch, handleRequest } = mockFetch(); + const client = new OpenAI({ apiKey: 'My API Key', fetch }); + + handleRequest(async (_, init) => { + const files = (init!.body as FormData).getAll('files[]'); + expect(files).toHaveLength(1); + expect(files[0]).toBeInstanceOf(File); + expect((files[0] as File).name).toEqual('my-skill/SKILL.md'); + + return new Response( + JSON.stringify({ + id: 'skillver_123', + created_at: 0, + description: '', + name: 'my-skill', + object: 'skill.version', + skill_id: 'skill_123', + version: '1', + }), + { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }, + ); + }); + + await client.skills.versions.create('skill_123', { files: [new File(['# skill'], 'my-skill/SKILL.md')] }); + }); + test('retrieve: only required params', async () => { const responsePromise = client.skills.versions.retrieve('version', { skill_id: 'skill_123' }); const rawResponse = await responsePromise.asResponse();