From 16e8cffa8c5c27b89800a2f88d285cb7c52a4fcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20J=2E=20Nu=C3=B1ez=20Madrazo?= Date: Sat, 26 Jul 2025 13:52:37 +0100 Subject: [PATCH 1/6] Add file part support to chat message structure Introduces ChatMessagePartFile struct and ChatMessagePartTypeFile constant to support file attachments in chat messages. Updates ChatMessagePart to include file parts and adds comprehensive tests for serialization, deserialization, and constant definitions. --- chat.go | 9 ++++ chat_test.go | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 120 insertions(+), 2 deletions(-) diff --git a/chat.go b/chat.go index 0bb2e98ee..ac2a186b0 100644 --- a/chat.go +++ b/chat.go @@ -81,17 +81,26 @@ type ChatMessageImageURL struct { Detail ImageURLDetail `json:"detail,omitempty"` } +// ChatMessagePartFile is a placeholder for file parts in chat messages. +type ChatMessagePartFile struct { + FileID string `json:"file_id,omitempty"` + FileName string `json:"filename,omitempty"` + FileData string `json:"file_data,omitempty"` // Base64 encoded file data +} + type ChatMessagePartType string const ( ChatMessagePartTypeText ChatMessagePartType = "text" ChatMessagePartTypeImageURL ChatMessagePartType = "image_url" + ChatMessagePartTypeFile ChatMessagePartType = "file" ) type ChatMessagePart struct { Type ChatMessagePartType `json:"type,omitempty"` Text string `json:"text,omitempty"` ImageURL *ChatMessageImageURL `json:"image_url,omitempty"` + File *ChatMessagePartFile `json:"file,omitempty"` } type ChatCompletionMessage struct { diff --git a/chat_test.go b/chat_test.go index 172ce0740..e18454e0c 100644 --- a/chat_test.go +++ b/chat_test.go @@ -677,6 +677,14 @@ func TestMultipartChatCompletions(t *testing.T) { Detail: openai.ImageURLDetailLow, }, }, + { + Type: openai.ChatMessagePartTypeFile, + File: &openai.ChatMessagePartFile{ + FileID: "file-123", + FileName: "test.txt", + FileData: "dGVzdCBmaWxlIGNvbnRlbnQ=", // base64 encoded "test file content" + }, + }, }, }, }, @@ -687,7 +695,8 @@ func TestMultipartChatCompletions(t *testing.T) { func TestMultipartChatMessageSerialization(t *testing.T) { jsonText := `[{"role":"system","content":"system-message"},` + `{"role":"user","content":[{"type":"text","text":"nice-text"},` + - `{"type":"image_url","image_url":{"url":"URL","detail":"high"}}]}]` + `{"type":"image_url","image_url":{"url":"URL","detail":"high"}},` + + `{"type":"file","file":{"file_id":"file-123","filename":"test.txt","file_data":"dGVzdA=="}}]}]` var msgs []openai.ChatCompletionMessage err := json.Unmarshal([]byte(jsonText), &msgs) @@ -700,7 +709,7 @@ func TestMultipartChatMessageSerialization(t *testing.T) { if msgs[0].Role != "system" || msgs[0].Content != "system-message" || msgs[0].MultiContent != nil { t.Errorf("invalid user message: %v", msgs[0]) } - if msgs[1].Role != "user" || msgs[1].Content != "" || len(msgs[1].MultiContent) != 2 { + if msgs[1].Role != "user" || msgs[1].Content != "" || len(msgs[1].MultiContent) != 3 { t.Errorf("invalid user message") } parts := msgs[1].MultiContent @@ -710,6 +719,9 @@ func TestMultipartChatMessageSerialization(t *testing.T) { if parts[1].Type != "image_url" || parts[1].ImageURL.URL != "URL" || parts[1].ImageURL.Detail != "high" { t.Errorf("invalid image_url part") } + if parts[2].Type != "file" || parts[2].File.FileID != "file-123" || parts[2].File.FileName != "test.txt" || parts[2].File.FileData != "dGVzdA==" { + t.Errorf("invalid file part: %v", parts[2]) + } s, err := json.Marshal(msgs) if err != nil { @@ -756,6 +768,103 @@ func TestMultipartChatMessageSerialization(t *testing.T) { } } +func TestChatMessagePartFile(t *testing.T) { + // Test file part with FileID + filePart := openai.ChatMessagePart{ + Type: openai.ChatMessagePartTypeFile, + File: &openai.ChatMessagePartFile{ + FileID: "file-abc123", + }, + } + + // Test serialization + data, err := json.Marshal(filePart) + if err != nil { + t.Fatalf("Expected no error: %s", err) + } + + expected := `{"type":"file","file":{"file_id":"file-abc123"}}` + result := strings.ReplaceAll(string(data), " ", "") + if result != expected { + t.Errorf("Expected %s, got %s", expected, result) + } + + // Test deserialization + var parsedPart openai.ChatMessagePart + err = json.Unmarshal(data, &parsedPart) + if err != nil { + t.Fatalf("Expected no error: %s", err) + } + + if parsedPart.Type != openai.ChatMessagePartTypeFile { + t.Errorf("Expected type %s, got %s", openai.ChatMessagePartTypeFile, parsedPart.Type) + } + if parsedPart.File == nil { + t.Fatal("Expected File to be non-nil") + } + if parsedPart.File.FileID != "file-abc123" { + t.Errorf("Expected FileID %s, got %s", "file-abc123", parsedPart.File.FileID) + } + + // Test file part with all fields + filePartComplete := openai.ChatMessagePart{ + Type: openai.ChatMessagePartTypeFile, + File: &openai.ChatMessagePartFile{ + FileID: "file-xyz789", + FileName: "document.pdf", + FileData: "JVBERi0xLjQK", // base64 for "%PDF-1.4\n" + }, + } + + data, err = json.Marshal(filePartComplete) + if err != nil { + t.Fatalf("Expected no error: %s", err) + } + + expected = `{"type":"file","file":{"file_id":"file-xyz789","filename":"document.pdf","file_data":"JVBERi0xLjQK"}}` + result = strings.ReplaceAll(string(data), " ", "") + if result != expected { + t.Errorf("Expected %s, got %s", expected, result) + } + + // Test deserialization of complete file part + var parsedCompleteFile openai.ChatMessagePart + err = json.Unmarshal(data, &parsedCompleteFile) + if err != nil { + t.Fatalf("Expected no error: %s", err) + } + + if parsedCompleteFile.File.FileID != "file-xyz789" { + t.Errorf("Expected FileID %s, got %s", "file-xyz789", parsedCompleteFile.File.FileID) + } + if parsedCompleteFile.File.FileName != "document.pdf" { + t.Errorf("Expected FileName %s, got %s", "document.pdf", parsedCompleteFile.File.FileName) + } + if parsedCompleteFile.File.FileData != "JVBERi0xLjQK" { + t.Errorf("Expected FileData %s, got %s", "JVBERi0xLjQK", parsedCompleteFile.File.FileData) + } +} + +func TestChatMessagePartTypeConstants(t *testing.T) { + // Test that the new file constant is properly defined + if openai.ChatMessagePartTypeFile != "file" { + t.Errorf("Expected ChatMessagePartTypeFile to be 'file', got %s", openai.ChatMessagePartTypeFile) + } + + // Test all part type constants + expectedTypes := map[openai.ChatMessagePartType]string{ + openai.ChatMessagePartTypeText: "text", + openai.ChatMessagePartTypeImageURL: "image_url", + openai.ChatMessagePartTypeFile: "file", + } + + for constant, expected := range expectedTypes { + if string(constant) != expected { + t.Errorf("Expected %s to be %s, got %s", constant, expected, string(constant)) + } + } +} + // handleChatCompletionEndpoint Handles the ChatGPT completion endpoint by the test server. func handleChatCompletionEndpoint(w http.ResponseWriter, r *http.Request) { var err error From 5ebd6148944f284f88ead13a50067f6743ec180c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20J=2E=20Nu=C3=B1ez=20Madrazo?= Date: Sat, 26 Jul 2025 14:00:26 +0100 Subject: [PATCH 2/6] Rename ChatMessagePartFile to ChatMessageFile Refactored struct name for file parts in chat messages from ChatMessagePartFile to ChatMessageFile for consistency and clarity. --- chat.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chat.go b/chat.go index ac2a186b0..06dcd0053 100644 --- a/chat.go +++ b/chat.go @@ -82,7 +82,7 @@ type ChatMessageImageURL struct { } // ChatMessagePartFile is a placeholder for file parts in chat messages. -type ChatMessagePartFile struct { +type ChatMessageFile struct { FileID string `json:"file_id,omitempty"` FileName string `json:"filename,omitempty"` FileData string `json:"file_data,omitempty"` // Base64 encoded file data @@ -100,7 +100,7 @@ type ChatMessagePart struct { Type ChatMessagePartType `json:"type,omitempty"` Text string `json:"text,omitempty"` ImageURL *ChatMessageImageURL `json:"image_url,omitempty"` - File *ChatMessagePartFile `json:"file,omitempty"` + File *ChatMessageFile `json:"file,omitempty"` } type ChatCompletionMessage struct { From d970292680ca1a47e36a8fdc419e5aff62499d68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20J=2E=20Nu=C3=B1ez=20Madrazo?= Date: Sat, 26 Jul 2025 14:01:31 +0100 Subject: [PATCH 3/6] Fix indentation in ChatMessagePart struct Corrected the indentation of the File field in the ChatMessagePart struct for improved code readability and consistency. --- chat.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat.go b/chat.go index 06dcd0053..b69e20471 100644 --- a/chat.go +++ b/chat.go @@ -100,7 +100,7 @@ type ChatMessagePart struct { Type ChatMessagePartType `json:"type,omitempty"` Text string `json:"text,omitempty"` ImageURL *ChatMessageImageURL `json:"image_url,omitempty"` - File *ChatMessageFile `json:"file,omitempty"` + File *ChatMessageFile `json:"file,omitempty"` } type ChatCompletionMessage struct { From dfe5ae2ea2db0fc66352a2c1f962749ca223323e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20J=2E=20Nu=C3=B1ez=20Madrazo?= Date: Sat, 26 Jul 2025 14:02:34 +0100 Subject: [PATCH 4/6] Update tests to use ChatMessageFile type Replaces usage of ChatMessagePartFile with ChatMessageFile in chat_test.go to reflect updated type naming in the openai package. Also renames related test function for consistency. --- chat_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/chat_test.go b/chat_test.go index e18454e0c..7648333e8 100644 --- a/chat_test.go +++ b/chat_test.go @@ -679,7 +679,7 @@ func TestMultipartChatCompletions(t *testing.T) { }, { Type: openai.ChatMessagePartTypeFile, - File: &openai.ChatMessagePartFile{ + File: &openai.ChatMessageFile{ FileID: "file-123", FileName: "test.txt", FileData: "dGVzdCBmaWxlIGNvbnRlbnQ=", // base64 encoded "test file content" @@ -768,11 +768,11 @@ func TestMultipartChatMessageSerialization(t *testing.T) { } } -func TestChatMessagePartFile(t *testing.T) { +func TestChatMessageFile(t *testing.T) { // Test file part with FileID filePart := openai.ChatMessagePart{ Type: openai.ChatMessagePartTypeFile, - File: &openai.ChatMessagePartFile{ + File: &openai.ChatMessageFile{ FileID: "file-abc123", }, } @@ -809,7 +809,7 @@ func TestChatMessagePartFile(t *testing.T) { // Test file part with all fields filePartComplete := openai.ChatMessagePart{ Type: openai.ChatMessagePartTypeFile, - File: &openai.ChatMessagePartFile{ + File: &openai.ChatMessageFile{ FileID: "file-xyz789", FileName: "document.pdf", FileData: "JVBERi0xLjQK", // base64 for "%PDF-1.4\n" From 1000a61326f04dc1accbe98702fabb9ca68666b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20J=2E=20Nu=C3=B1ez=20Madrazo?= Date: Sat, 26 Jul 2025 14:04:27 +0100 Subject: [PATCH 5/6] Fix formatting in multipart chat message test Split a long conditional statement in TestMultipartChatMessageSerialization for improved readability. --- chat_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/chat_test.go b/chat_test.go index 7648333e8..ad9e77315 100644 --- a/chat_test.go +++ b/chat_test.go @@ -719,7 +719,8 @@ func TestMultipartChatMessageSerialization(t *testing.T) { if parts[1].Type != "image_url" || parts[1].ImageURL.URL != "URL" || parts[1].ImageURL.Detail != "high" { t.Errorf("invalid image_url part") } - if parts[2].Type != "file" || parts[2].File.FileID != "file-123" || parts[2].File.FileName != "test.txt" || parts[2].File.FileData != "dGVzdA==" { + if parts[2].Type != "file" || parts[2].File.FileID != "file-123" || + parts[2].File.FileName != "test.txt" || parts[2].File.FileData != "dGVzdA==" { t.Errorf("invalid file part: %v", parts[2]) } From fa787121847fd8fee006cade64a134c6c7ea8864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20J=2E=20Nu=C3=B1ez=20Madrazo?= Date: Wed, 30 Jul 2025 14:44:09 +0100 Subject: [PATCH 6/6] Update chat.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- chat.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat.go b/chat.go index b69e20471..f364c4b7e 100644 --- a/chat.go +++ b/chat.go @@ -81,7 +81,7 @@ type ChatMessageImageURL struct { Detail ImageURLDetail `json:"detail,omitempty"` } -// ChatMessagePartFile is a placeholder for file parts in chat messages. +// ChatMessageFile is a placeholder for file parts in chat messages. type ChatMessageFile struct { FileID string `json:"file_id,omitempty"` FileName string `json:"filename,omitempty"`