diff --git a/backend/internal/pkg/apicompat/chatcompletions_responses_test.go b/backend/internal/pkg/apicompat/chatcompletions_responses_test.go index bf5c23d5652..103258556b9 100644 --- a/backend/internal/pkg/apicompat/chatcompletions_responses_test.go +++ b/backend/internal/pkg/apicompat/chatcompletions_responses_test.go @@ -225,6 +225,56 @@ func TestChatCompletionsToResponses_WhitespaceOnlyBase64ImageURLSkipped(t *testi assert.Equal(t, "Describe this", parts[0].Text) } +func TestChatCompletionsToResponses_FileData(t *testing.T) { + content := `[{"type":"text","text":"Analyze this file"},{"type":"file","file":{"filename":"example-report.pdf","file_data":"data:application/pdf;base64,abc123"}}]` + req := &ChatCompletionsRequest{ + Model: "gpt-4o", + Messages: []ChatMessage{ + {Role: "user", Content: json.RawMessage(content)}, + }, + } + resp, err := ChatCompletionsToResponses(req) + require.NoError(t, err) + + var items []ResponsesInputItem + require.NoError(t, json.Unmarshal(resp.Input, &items)) + require.Len(t, items, 1) + + var parts []ResponsesContentPart + require.NoError(t, json.Unmarshal(items[0].Content, &parts)) + require.Len(t, parts, 2) + assert.Equal(t, "input_text", parts[0].Type) + assert.Equal(t, "Analyze this file", parts[0].Text) + assert.Equal(t, "input_file", parts[1].Type) + assert.Equal(t, "example-report.pdf", parts[1].Filename) + assert.Equal(t, "data:application/pdf;base64,abc123", parts[1].FileData) + assert.Empty(t, parts[1].FileURL) +} + +func TestChatCompletionsToResponses_FileURL(t *testing.T) { + content := `[{"type":"file","file":{"filename":"report.pdf","file_url":"https://example.com/report.pdf"}}]` + req := &ChatCompletionsRequest{ + Model: "gpt-4o", + Messages: []ChatMessage{ + {Role: "user", Content: json.RawMessage(content)}, + }, + } + resp, err := ChatCompletionsToResponses(req) + require.NoError(t, err) + + var items []ResponsesInputItem + require.NoError(t, json.Unmarshal(resp.Input, &items)) + require.Len(t, items, 1) + + var parts []ResponsesContentPart + require.NoError(t, json.Unmarshal(items[0].Content, &parts)) + require.Len(t, parts, 1) + assert.Equal(t, "input_file", parts[0].Type) + assert.Equal(t, "report.pdf", parts[0].Filename) + assert.Equal(t, "https://example.com/report.pdf", parts[0].FileURL) + assert.Empty(t, parts[0].FileData) +} + func TestChatCompletionsToResponses_SystemArrayContent(t *testing.T) { req := &ChatCompletionsRequest{ Model: "gpt-4o", diff --git a/backend/internal/pkg/apicompat/chatcompletions_to_responses.go b/backend/internal/pkg/apicompat/chatcompletions_to_responses.go index 64ef5781585..338d88f8207 100644 --- a/backend/internal/pkg/apicompat/chatcompletions_to_responses.go +++ b/backend/internal/pkg/apicompat/chatcompletions_to_responses.go @@ -346,6 +346,29 @@ func convertChatContentPartsToResponses(parts []ChatContentPart) []ResponsesCont ImageURL: p.ImageURL.URL, }) } + case "file": + fileData := p.FileData + fileURL := p.FileURL + filename := p.Filename + if p.File != nil { + if fileData == "" { + fileData = p.File.FileData + } + if fileURL == "" { + fileURL = p.File.FileURL + } + if filename == "" { + filename = p.File.Filename + } + } + if fileData != "" || fileURL != "" { + responseParts = append(responseParts, ResponsesContentPart{ + Type: "input_file", + FileData: fileData, + FileURL: fileURL, + Filename: filename, + }) + } } } return responseParts diff --git a/backend/internal/pkg/apicompat/types.go b/backend/internal/pkg/apicompat/types.go index f9cd5a1c7f9..164f73915e6 100644 --- a/backend/internal/pkg/apicompat/types.go +++ b/backend/internal/pkg/apicompat/types.go @@ -219,9 +219,12 @@ type ResponsesInputItem struct { // ResponsesContentPart is a typed content part in a Responses message. type ResponsesContentPart struct { - Type string `json:"type"` // "input_text" | "output_text" | "input_image" + Type string `json:"type"` // "input_text" | "output_text" | "input_image" | "input_file" Text string `json:"text,omitempty"` ImageURL string `json:"image_url,omitempty"` // data URI for input_image + FileData string `json:"file_data,omitempty"` // data URI for input_file + FileURL string `json:"file_url,omitempty"` // URL for input_file + Filename string `json:"filename,omitempty"` // original filename for input_file } // ResponsesTool describes a tool in the Responses API. @@ -401,9 +404,15 @@ type ChatMessage struct { // ChatContentPart is a typed content part in a multi-modal message. type ChatContentPart struct { - Type string `json:"type"` // "text" | "image_url" + Type string `json:"type"` // "text" | "image_url" | "file" Text string `json:"text,omitempty"` ImageURL *ChatImageURL `json:"image_url,omitempty"` + File *ChatFile `json:"file,omitempty"` + + // Optional flat file fields for compatibility with different client payloads. + FileData string `json:"file_data,omitempty"` + FileURL string `json:"file_url,omitempty"` + Filename string `json:"filename,omitempty"` } // ChatImageURL contains the URL for an image content part. @@ -412,6 +421,13 @@ type ChatImageURL struct { Detail string `json:"detail,omitempty"` // "auto" | "low" | "high" } +// ChatFile contains file metadata and source for a file content part. +type ChatFile struct { + Filename string `json:"filename,omitempty"` + FileData string `json:"file_data,omitempty"` + FileURL string `json:"file_url,omitempty"` +} + // ChatTool describes a tool available to the model. type ChatTool struct { Type string `json:"type"` // "function"