From 568cd38203677da8567359b5b09f6d03fa897c53 Mon Sep 17 00:00:00 2001 From: Daniel Klein Date: Fri, 5 Dec 2025 12:29:12 -0600 Subject: [PATCH 1/3] fix: support input_file for chat completions when possible --- .../src/openaiChatCompletionsConverter.ts | 39 +++++- .../openaiChatCompletionsConverter.test.ts | 130 ++++++++++++++++++ 2 files changed, 164 insertions(+), 5 deletions(-) diff --git a/packages/agents-openai/src/openaiChatCompletionsConverter.ts b/packages/agents-openai/src/openaiChatCompletionsConverter.ts index 509d1b15..cca63ae9 100644 --- a/packages/agents-openai/src/openaiChatCompletionsConverter.ts +++ b/packages/agents-openai/src/openaiChatCompletionsConverter.ts @@ -96,11 +96,40 @@ export function extractAllUserContent( ...rest, }); } else if (c.type === 'input_file') { - throw new Error( - `File uploads are not supported for chat completions: ${JSON.stringify( - c, - )}`, - ); + // Chat Completions API supports file inputs via the "file" content part type. + // See: https://platform.openai.com/docs/guides/pdf-files?api-mode=chat + const file: ChatCompletionContentPart.File['file'] = {}; + + if (typeof c.file === 'string') { + const value = c.file.trim(); + if (value.startsWith('data:')) { + file.file_data = value; + } else { + throw new UserError( + `Chat Completions only supports data URLs for file input. If you're trying to pass an uploaded file's ID, use an object with the id property instead: ${JSON.stringify(c)}`, + ); + } + } else if (c.file && typeof c.file === 'object' && 'id' in c.file) { + file.file_id = (c.file as { id: string }).id; + } else { + throw new UserError( + `File input requires a data URL or file ID: ${JSON.stringify(c)}`, + ); + } + + // Handle filename from the content item or providerData + if (c.filename) { + file.filename = c.filename; + } else if (c.providerData?.filename) { + file.filename = c.providerData.filename; + } + + const { filename: _filename, ...rest } = c.providerData || {}; + out.push({ + type: 'file', + file, + ...rest, + }); } else if (c.type === 'audio') { const { input_audio, ...rest } = c.providerData || {}; out.push({ diff --git a/packages/agents-openai/test/openaiChatCompletionsConverter.test.ts b/packages/agents-openai/test/openaiChatCompletionsConverter.test.ts index 88881a72..c7c32de0 100644 --- a/packages/agents-openai/test/openaiChatCompletionsConverter.test.ts +++ b/packages/agents-openai/test/openaiChatCompletionsConverter.test.ts @@ -87,6 +87,136 @@ describe('content extraction helpers', () => { expect(() => extractAllUserContent(bad)).toThrow(); }); + test('extractAllUserContent converts input_file with data URL', () => { + const userContent: protocol.UserMessageItem['content'] = [ + { + type: 'input_file', + file: 'data:application/pdf;base64,JVBER...', + filename: 'document.pdf', + }, + ]; + const converted = extractAllUserContent(userContent); + expect(converted).toEqual([ + { + type: 'file', + file: { + file_data: 'data:application/pdf;base64,JVBER...', + filename: 'document.pdf', + }, + }, + ]); + }); + + test('extractAllUserContent throws on https URL (not supported in Chat Completions)', () => { + const userContent: protocol.UserMessageItem['content'] = [ + { + type: 'input_file', + file: 'https://example.com/document.pdf', + }, + ]; + expect(() => extractAllUserContent(userContent)).toThrow( + /Chat Completions only supports data URLs/, + ); + }); + + test('extractAllUserContent converts input_file with file ID object', () => { + const userContent: protocol.UserMessageItem['content'] = [ + { + type: 'input_file', + file: { id: 'file-abc123' }, + }, + ]; + const converted = extractAllUserContent(userContent); + expect(converted).toEqual([ + { + type: 'file', + file: { + file_id: 'file-abc123', + }, + }, + ]); + }); + + test('extractAllUserContent throws on file URL object (not supported in Chat Completions)', () => { + const userContent: protocol.UserMessageItem['content'] = [ + { + type: 'input_file', + file: { url: 'https://example.com/document.pdf' }, + }, + ]; + expect(() => extractAllUserContent(userContent)).toThrow( + /requires a data URL or file ID/, + ); + }); + + test('extractAllUserContent gets filename from providerData', () => { + const userContent: protocol.UserMessageItem['content'] = [ + { + type: 'input_file', + file: 'data:application/pdf;base64,JVBER...', + providerData: { + filename: 'from-provider.pdf', + }, + }, + ]; + const converted = extractAllUserContent(userContent); + expect(converted).toEqual([ + { + type: 'file', + file: { + file_data: 'data:application/pdf;base64,JVBER...', + filename: 'from-provider.pdf', + }, + }, + ]); + }); + + test('extractAllUserContent prefers content filename over providerData', () => { + const userContent: protocol.UserMessageItem['content'] = [ + { + type: 'input_file', + file: 'data:application/pdf;base64,JVBER...', + filename: 'content-filename.pdf', + providerData: { + filename: 'from-provider.pdf', + }, + }, + ]; + const converted = extractAllUserContent(userContent); + expect(converted).toEqual([ + { + type: 'file', + file: { + file_data: 'data:application/pdf;base64,JVBER...', + filename: 'content-filename.pdf', + }, + }, + ]); + }); + + test('extractAllUserContent throws on unsupported file string format', () => { + const userContent: protocol.UserMessageItem['content'] = [ + { + type: 'input_file', + file: 'not-a-valid-url-or-data', + }, + ]; + expect(() => extractAllUserContent(userContent)).toThrow( + /use an object with the id property/, + ); + }); + + test('extractAllUserContent throws when file is missing', () => { + const userContent: protocol.UserMessageItem['content'] = [ + { + type: 'input_file', + }, + ]; + expect(() => extractAllUserContent(userContent)).toThrow( + /requires a data URL or file ID/, + ); + }); + test('extractAllAssistantContent converts supported entries and ignores images/audio', () => { const assistantContent: protocol.AssistantMessageItem['content'] = [ { type: 'output_text', text: 'hi', providerData: { b: 2 } }, From 8ad436d853da0f7c8b67dfc42cb66db7f8dfb702 Mon Sep 17 00:00:00 2001 From: Daniel Klein Date: Fri, 5 Dec 2025 13:19:29 -0600 Subject: [PATCH 2/3] add changeset --- .changeset/sharp-towns-cheer.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/sharp-towns-cheer.md diff --git a/.changeset/sharp-towns-cheer.md b/.changeset/sharp-towns-cheer.md new file mode 100644 index 00000000..9844e4b5 --- /dev/null +++ b/.changeset/sharp-towns-cheer.md @@ -0,0 +1,5 @@ +--- +'@openai/agents-openai': minor +--- + +fix: support input_file for chat completions when possible From 7698a2e6de364fe2ab990744b4f5c5fd19913e8e Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Sat, 6 Dec 2025 08:39:31 +0900 Subject: [PATCH 3/3] Update .changeset/sharp-towns-cheer.md --- .changeset/sharp-towns-cheer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/sharp-towns-cheer.md b/.changeset/sharp-towns-cheer.md index 9844e4b5..046935e8 100644 --- a/.changeset/sharp-towns-cheer.md +++ b/.changeset/sharp-towns-cheer.md @@ -1,5 +1,5 @@ --- -'@openai/agents-openai': minor +'@openai/agents-openai': patch --- fix: support input_file for chat completions when possible