Skip to content

Commit 6aa0550

Browse files
fix: support input_file for chat completions when possible (#735)
Co-authored-by: Kazuhiro Sera <seratch@openai.com>
1 parent 1300121 commit 6aa0550

File tree

3 files changed

+169
-5
lines changed

3 files changed

+169
-5
lines changed

.changeset/sharp-towns-cheer.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@openai/agents-openai': patch
3+
---
4+
5+
fix: support input_file for chat completions when possible

packages/agents-openai/src/openaiChatCompletionsConverter.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,40 @@ export function extractAllUserContent(
9696
...rest,
9797
});
9898
} else if (c.type === 'input_file') {
99-
throw new Error(
100-
`File uploads are not supported for chat completions: ${JSON.stringify(
101-
c,
102-
)}`,
103-
);
99+
// Chat Completions API supports file inputs via the "file" content part type.
100+
// See: https://platform.openai.com/docs/guides/pdf-files?api-mode=chat
101+
const file: ChatCompletionContentPart.File['file'] = {};
102+
103+
if (typeof c.file === 'string') {
104+
const value = c.file.trim();
105+
if (value.startsWith('data:')) {
106+
file.file_data = value;
107+
} else {
108+
throw new UserError(
109+
`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)}`,
110+
);
111+
}
112+
} else if (c.file && typeof c.file === 'object' && 'id' in c.file) {
113+
file.file_id = (c.file as { id: string }).id;
114+
} else {
115+
throw new UserError(
116+
`File input requires a data URL or file ID: ${JSON.stringify(c)}`,
117+
);
118+
}
119+
120+
// Handle filename from the content item or providerData
121+
if (c.filename) {
122+
file.filename = c.filename;
123+
} else if (c.providerData?.filename) {
124+
file.filename = c.providerData.filename;
125+
}
126+
127+
const { filename: _filename, ...rest } = c.providerData || {};
128+
out.push({
129+
type: 'file',
130+
file,
131+
...rest,
132+
});
104133
} else if (c.type === 'audio') {
105134
const { input_audio, ...rest } = c.providerData || {};
106135
out.push({

packages/agents-openai/test/openaiChatCompletionsConverter.test.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,136 @@ describe('content extraction helpers', () => {
8787
expect(() => extractAllUserContent(bad)).toThrow();
8888
});
8989

90+
test('extractAllUserContent converts input_file with data URL', () => {
91+
const userContent: protocol.UserMessageItem['content'] = [
92+
{
93+
type: 'input_file',
94+
file: 'data:application/pdf;base64,JVBER...',
95+
filename: 'document.pdf',
96+
},
97+
];
98+
const converted = extractAllUserContent(userContent);
99+
expect(converted).toEqual([
100+
{
101+
type: 'file',
102+
file: {
103+
file_data: 'data:application/pdf;base64,JVBER...',
104+
filename: 'document.pdf',
105+
},
106+
},
107+
]);
108+
});
109+
110+
test('extractAllUserContent throws on https URL (not supported in Chat Completions)', () => {
111+
const userContent: protocol.UserMessageItem['content'] = [
112+
{
113+
type: 'input_file',
114+
file: 'https://example.com/document.pdf',
115+
},
116+
];
117+
expect(() => extractAllUserContent(userContent)).toThrow(
118+
/Chat Completions only supports data URLs/,
119+
);
120+
});
121+
122+
test('extractAllUserContent converts input_file with file ID object', () => {
123+
const userContent: protocol.UserMessageItem['content'] = [
124+
{
125+
type: 'input_file',
126+
file: { id: 'file-abc123' },
127+
},
128+
];
129+
const converted = extractAllUserContent(userContent);
130+
expect(converted).toEqual([
131+
{
132+
type: 'file',
133+
file: {
134+
file_id: 'file-abc123',
135+
},
136+
},
137+
]);
138+
});
139+
140+
test('extractAllUserContent throws on file URL object (not supported in Chat Completions)', () => {
141+
const userContent: protocol.UserMessageItem['content'] = [
142+
{
143+
type: 'input_file',
144+
file: { url: 'https://example.com/document.pdf' },
145+
},
146+
];
147+
expect(() => extractAllUserContent(userContent)).toThrow(
148+
/requires a data URL or file ID/,
149+
);
150+
});
151+
152+
test('extractAllUserContent gets filename from providerData', () => {
153+
const userContent: protocol.UserMessageItem['content'] = [
154+
{
155+
type: 'input_file',
156+
file: 'data:application/pdf;base64,JVBER...',
157+
providerData: {
158+
filename: 'from-provider.pdf',
159+
},
160+
},
161+
];
162+
const converted = extractAllUserContent(userContent);
163+
expect(converted).toEqual([
164+
{
165+
type: 'file',
166+
file: {
167+
file_data: 'data:application/pdf;base64,JVBER...',
168+
filename: 'from-provider.pdf',
169+
},
170+
},
171+
]);
172+
});
173+
174+
test('extractAllUserContent prefers content filename over providerData', () => {
175+
const userContent: protocol.UserMessageItem['content'] = [
176+
{
177+
type: 'input_file',
178+
file: 'data:application/pdf;base64,JVBER...',
179+
filename: 'content-filename.pdf',
180+
providerData: {
181+
filename: 'from-provider.pdf',
182+
},
183+
},
184+
];
185+
const converted = extractAllUserContent(userContent);
186+
expect(converted).toEqual([
187+
{
188+
type: 'file',
189+
file: {
190+
file_data: 'data:application/pdf;base64,JVBER...',
191+
filename: 'content-filename.pdf',
192+
},
193+
},
194+
]);
195+
});
196+
197+
test('extractAllUserContent throws on unsupported file string format', () => {
198+
const userContent: protocol.UserMessageItem['content'] = [
199+
{
200+
type: 'input_file',
201+
file: 'not-a-valid-url-or-data',
202+
},
203+
];
204+
expect(() => extractAllUserContent(userContent)).toThrow(
205+
/use an object with the id property/,
206+
);
207+
});
208+
209+
test('extractAllUserContent throws when file is missing', () => {
210+
const userContent: protocol.UserMessageItem['content'] = [
211+
{
212+
type: 'input_file',
213+
},
214+
];
215+
expect(() => extractAllUserContent(userContent)).toThrow(
216+
/requires a data URL or file ID/,
217+
);
218+
});
219+
90220
test('extractAllAssistantContent converts supported entries and ignores images/audio', () => {
91221
const assistantContent: protocol.AssistantMessageItem['content'] = [
92222
{ type: 'output_text', text: 'hi', providerData: { b: 2 } },

0 commit comments

Comments
 (0)