Skip to content

Commit 9d9a959

Browse files
committed
Support Azure chat completions file attachments
1 parent bd08e31 commit 9d9a959

4 files changed

Lines changed: 178 additions & 1 deletion

File tree

apps/sim/providers/azure-openai/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
1515
import type { StreamingExecution } from '@/executor/types'
1616
import { MAX_TOOL_ITERATIONS } from '@/providers'
1717
import {
18+
appendFileAttachmentsToChatCompletionMessages,
1819
checkForForcedToolUsage,
1920
createReadableStreamFromAzureOpenAIStream,
2021
extractApiVersionFromUrl,
@@ -93,6 +94,8 @@ async function executeChatCompletionsRequest(
9394
allMessages.push(...(request.messages as ChatCompletionMessageParam[]))
9495
}
9596

97+
appendFileAttachmentsToChatCompletionMessages(allMessages, request.fileAttachments)
98+
9699
const tools: ChatCompletionTool[] | undefined = request.tools?.length
97100
? request.tools.map((tool) => ({
98101
type: 'function' as const,
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import type { ChatCompletionMessageParam } from 'openai/resources/chat/completions'
2+
import { describe, expect, it } from 'vitest'
3+
import { appendFileAttachmentsToChatCompletionMessages } from '@/providers/azure-openai/utils'
4+
import type { ProviderFileAttachment } from '@/providers/types'
5+
6+
describe('appendFileAttachmentsToChatCompletionMessages', () => {
7+
it('adds supported file attachments to the latest user message', () => {
8+
const messages: ChatCompletionMessageParam[] = [
9+
{ role: 'user', content: 'Earlier request' },
10+
{ role: 'assistant', content: 'Earlier response' },
11+
{ role: 'user', content: 'Use these attachments' },
12+
]
13+
const fileAttachments: ProviderFileAttachment[] = [
14+
{
15+
name: 'brief.pdf',
16+
type: 'application/pdf',
17+
base64: 'cGRm',
18+
},
19+
{
20+
name: 'photo.png',
21+
type: 'image/png',
22+
base64: 'cG5n',
23+
},
24+
]
25+
26+
appendFileAttachmentsToChatCompletionMessages(messages, fileAttachments)
27+
28+
expect(messages[0]).toEqual({ role: 'user', content: 'Earlier request' })
29+
expect(messages[1]).toEqual({ role: 'assistant', content: 'Earlier response' })
30+
expect(messages[2]).toEqual({
31+
role: 'user',
32+
content: [
33+
{ type: 'text', text: 'Use these attachments' },
34+
{
35+
type: 'file',
36+
file: {
37+
file_data: 'cGRm',
38+
filename: 'brief.pdf',
39+
},
40+
},
41+
{
42+
type: 'image_url',
43+
image_url: {
44+
url: 'data:image/png;base64,cG5n',
45+
detail: 'auto',
46+
},
47+
},
48+
],
49+
})
50+
})
51+
52+
it('adds a user message when there is no existing user message', () => {
53+
const messages: ChatCompletionMessageParam[] = [{ role: 'system', content: 'You are helpful' }]
54+
55+
appendFileAttachmentsToChatCompletionMessages(messages, [
56+
{
57+
name: 'brief.pdf',
58+
type: 'application/pdf',
59+
base64: 'cGRm',
60+
},
61+
])
62+
63+
expect(messages).toEqual([
64+
{ role: 'system', content: 'You are helpful' },
65+
{
66+
role: 'user',
67+
content: [
68+
{ type: 'text', text: 'Please use the attached files.' },
69+
{
70+
type: 'file',
71+
file: {
72+
file_data: 'cGRm',
73+
filename: 'brief.pdf',
74+
},
75+
},
76+
],
77+
},
78+
])
79+
})
80+
81+
it('leaves messages unchanged when attachments have unsupported MIME types', () => {
82+
const messages: ChatCompletionMessageParam[] = [{ role: 'user', content: 'Text only' }]
83+
84+
appendFileAttachmentsToChatCompletionMessages(messages, [
85+
{
86+
name: 'spreadsheet.xlsx',
87+
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
88+
base64: 'eGxzeA==',
89+
},
90+
])
91+
92+
expect(messages).toEqual([{ role: 'user', content: 'Text only' }])
93+
})
94+
})

apps/sim/providers/azure-openai/utils.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import type { Logger } from '@sim/logger'
22
import type OpenAI from 'openai'
3-
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
3+
import type {
4+
ChatCompletionChunk,
5+
ChatCompletionContentPart,
6+
ChatCompletionMessageParam,
7+
ChatCompletionUserMessageParam,
8+
} from 'openai/resources/chat/completions'
49
import type { CompletionUsage } from 'openai/resources/completions'
510
import type { Stream } from 'openai/streaming'
11+
import { buildChatCompletionFileParts } from '@/providers/openai/utils'
12+
import type { ProviderFileAttachment } from '@/providers/types'
613
import { checkForForcedToolUsageOpenAI, createOpenAICompatibleStream } from '@/providers/utils'
714

815
/**
@@ -37,6 +44,43 @@ export function checkForForcedToolUsage(
3744
)
3845
}
3946

47+
function isUserMessage(
48+
message: ChatCompletionMessageParam
49+
): message is ChatCompletionUserMessageParam {
50+
return message.role === 'user'
51+
}
52+
53+
function createTextPart(text: string): ChatCompletionContentPart {
54+
return {
55+
type: 'text',
56+
text,
57+
}
58+
}
59+
60+
/**
61+
* Adds supported file attachments to the latest user message for Azure Chat Completions.
62+
*/
63+
export function appendFileAttachmentsToChatCompletionMessages(
64+
messages: ChatCompletionMessageParam[],
65+
fileAttachments?: ProviderFileAttachment[]
66+
): void {
67+
const fileParts = buildChatCompletionFileParts(fileAttachments)
68+
if (fileParts.length === 0) return
69+
70+
const lastUserMessage = [...messages].reverse().find(isUserMessage)
71+
if (!lastUserMessage) {
72+
messages.push({
73+
role: 'user',
74+
content: [createTextPart('Please use the attached files.'), ...fileParts],
75+
})
76+
return
77+
}
78+
79+
lastUserMessage.content = Array.isArray(lastUserMessage.content)
80+
? [...lastUserMessage.content, ...fileParts]
81+
: [createTextPart(lastUserMessage.content || 'Please use the attached files.'), ...fileParts]
82+
}
83+
4084
/**
4185
* Determines if an Azure OpenAI endpoint URL is for the chat completions API.
4286
* Returns true for URLs containing /chat/completions pattern.

apps/sim/providers/openai/utils.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createLogger } from '@sim/logger'
22
import type OpenAI from 'openai'
3+
import type { ChatCompletionContentPart } from 'openai/resources/chat/completions'
34
import type { Message, ProviderFileAttachment } from '@/providers/types'
45

56
const logger = createLogger('ResponsesUtils')
@@ -116,6 +117,41 @@ function buildResponsesFileParts(
116117
})
117118
}
118119

120+
export function buildChatCompletionFileParts(
121+
fileAttachments?: ProviderFileAttachment[]
122+
): ChatCompletionContentPart[] {
123+
if (!fileAttachments?.length) return []
124+
125+
return fileAttachments.flatMap<ChatCompletionContentPart>((file) => {
126+
const type = file.type.toLowerCase()
127+
if (OPENAI_SUPPORTED_IMAGE_MIME_TYPES.has(type)) {
128+
return [
129+
{
130+
type: 'image_url',
131+
image_url: {
132+
url: toDataUrl(file),
133+
detail: 'auto',
134+
},
135+
},
136+
]
137+
}
138+
139+
if (!OPENAI_SUPPORTED_FILE_MIME_TYPES.has(type)) {
140+
return []
141+
}
142+
143+
return [
144+
{
145+
type: 'file',
146+
file: {
147+
file_data: file.base64,
148+
filename: file.name,
149+
},
150+
},
151+
]
152+
})
153+
}
154+
119155
/**
120156
* Converts chat-style messages into Responses API input items.
121157
*/

0 commit comments

Comments
 (0)