Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sharp-towns-cheer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openai/agents-openai': patch
---

fix: support input_file for chat completions when possible
39 changes: 34 additions & 5 deletions packages/agents-openai/src/openaiChatCompletionsConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
130 changes: 130 additions & 0 deletions packages/agents-openai/test/openaiChatCompletionsConverter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } },
Expand Down