Merge pull request #5487 from an-epiphany/fix/file-part-min

fix(apicompat): chat/completions 的 file part 转换为 Responses input_file,不再静默丢弃
This commit is contained in:
Wesley Liddick
2026-08-22 13:34:18 +08:00
committed by GitHub
3 changed files with 97 additions and 2 deletions
@@ -407,6 +407,79 @@ func TestChatCompletionsToResponses_WhitespaceOnlyBase64ImageURLSkipped(t *testi
assert.Equal(t, "Describe this", parts[0].Text)
}
func TestChatCompletionsToResponses_FilePartFileData(t *testing.T) {
content := `[{"type":"text","text":"Summarize the attached document"},{"type":"file","file":{"filename":"document.pdf","file_data":"data:application/pdf;base64,JVBERi0xLjQ="}}]`
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, "Summarize the attached document", parts[0].Text)
assert.Equal(t, "input_file", parts[1].Type)
assert.Equal(t, "document.pdf", parts[1].Filename)
assert.Equal(t, "data:application/pdf;base64,JVBERi0xLjQ=", parts[1].FileData)
assert.Empty(t, parts[1].FileID)
}
func TestChatCompletionsToResponses_FilePartFileID(t *testing.T) {
content := `[{"type":"file","file":{"file_id":"file-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, 1)
assert.Equal(t, "input_file", parts[0].Type)
assert.Equal(t, "file-abc123", parts[0].FileID)
assert.Empty(t, parts[0].FileData)
}
func TestChatCompletionsToResponses_EmptyFilePartSkipped(t *testing.T) {
// A file part with neither file_data nor file_id carries nothing the
// Responses API can use; dropping it (like empty image URLs) avoids an
// upstream 400 on an empty input_file part.
content := `[{"type":"text","text":"Describe this"},{"type":"file","file":{"filename":"empty.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_text", parts[0].Type)
}
func TestChatCompletionsToResponses_EmptyContentNeverNull(t *testing.T) {
// Regression for #2515: the upstream Responses API rejects an input item
// whose content field is JSON null. Any chat-completions message that
@@ -378,6 +378,15 @@ func convertChatContentPartsToResponses(parts []ChatContentPart) []ResponsesCont
ImageURL: p.ImageURL.URL,
})
}
case "file":
if p.File != nil && (p.File.FileData != "" || p.File.FileID != "") {
responseParts = append(responseParts, ResponsesContentPart{
Type: "input_file",
Filename: p.File.Filename,
FileData: p.File.FileData,
FileID: p.File.FileID,
})
}
}
}
return responseParts
+15 -2
View File
@@ -295,9 +295,14 @@ func (i *ResponsesInputItem) UnmarshalJSON(data []byte) error {
// 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
// input_file fields.
Filename string `json:"filename,omitempty"`
FileData string `json:"file_data,omitempty"` // data URI
FileID string `json:"file_id,omitempty"`
}
// ResponsesTool describes a tool in the Responses API.
@@ -670,9 +675,10 @@ 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"`
}
// ChatImageURL contains the URL for an image content part.
@@ -681,6 +687,13 @@ type ChatImageURL struct {
Detail string `json:"detail,omitempty"` // "auto" | "low" | "high"
}
// ChatFile contains the payload of a "file" content part (e.g. PDF input).
type ChatFile struct {
Filename string `json:"filename,omitempty"`
FileData string `json:"file_data,omitempty"` // data URI
FileID string `json:"file_id,omitempty"`
}
// ChatTool describes a tool available to the model.
type ChatTool struct {
Type string `json:"type"` // "function" | "x_search"