mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-06 15:03:54 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user