Skip to content
Open
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
50 changes: 50 additions & 0 deletions backend/internal/pkg/apicompat/chatcompletions_responses_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
23 changes: 23 additions & 0 deletions backend/internal/pkg/apicompat/chatcompletions_to_responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Comment on lines +350 to +363
Comment on lines +350 to +363
if fileData != "" || fileURL != "" {
responseParts = append(responseParts, ResponsesContentPart{
Type: "input_file",
FileData: fileData,
FileURL: fileURL,
Filename: filename,
})
}
Comment on lines +349 to +371
}
}
return responseParts
Expand Down
20 changes: 18 additions & 2 deletions backend/internal/pkg/apicompat/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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"
Expand Down
Loading