From 0cbfe7f4575b9df16f31d61c513bd660682367af Mon Sep 17 00:00:00 2001 From: Alexey Yanchenko Date: Fri, 20 Feb 2026 10:25:44 +0700 Subject: [PATCH] Pass file input from /chat/completions and /responses to codex and claude --- .../chat-completions/claude_openai_request.go | 15 +++++++++++ .../claude_openai-responses_request.go | 27 ++++++++++++++++++- .../chat-completions/codex_openai_request.go | 14 +++++++++- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/internal/translator/claude/openai/chat-completions/claude_openai_request.go b/internal/translator/claude/openai/chat-completions/claude_openai_request.go index 3cad1882..f94825b2 100644 --- a/internal/translator/claude/openai/chat-completions/claude_openai_request.go +++ b/internal/translator/claude/openai/chat-completions/claude_openai_request.go @@ -199,6 +199,21 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream msg, _ = sjson.SetRaw(msg, "content.-1", imagePart) } } + + case "file": + fileData := part.Get("file.file_data").String() + if strings.HasPrefix(fileData, "data:") { + semicolonIdx := strings.Index(fileData, ";") + commaIdx := strings.Index(fileData, ",") + if semicolonIdx != -1 && commaIdx != -1 && commaIdx > semicolonIdx { + mediaType := strings.TrimPrefix(fileData[:semicolonIdx], "data:") + data := fileData[commaIdx+1:] + docPart := `{"type":"document","source":{"type":"base64","media_type":"","data":""}}` + docPart, _ = sjson.Set(docPart, "source.media_type", mediaType) + docPart, _ = sjson.Set(docPart, "source.data", data) + msg, _ = sjson.SetRaw(msg, "content.-1", docPart) + } + } } return true }) diff --git a/internal/translator/claude/openai/responses/claude_openai-responses_request.go b/internal/translator/claude/openai/responses/claude_openai-responses_request.go index 337f9be9..33a81124 100644 --- a/internal/translator/claude/openai/responses/claude_openai-responses_request.go +++ b/internal/translator/claude/openai/responses/claude_openai-responses_request.go @@ -155,6 +155,7 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte var textAggregate strings.Builder var partsJSON []string hasImage := false + hasFile := false if parts := item.Get("content"); parts.Exists() && parts.IsArray() { parts.ForEach(func(_, part gjson.Result) bool { ptype := part.Get("type").String() @@ -207,6 +208,30 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte hasImage = true } } + case "input_file": + fileData := part.Get("file_data").String() + if fileData != "" { + mediaType := "application/octet-stream" + data := fileData + if strings.HasPrefix(fileData, "data:") { + trimmed := strings.TrimPrefix(fileData, "data:") + mediaAndData := strings.SplitN(trimmed, ";base64,", 2) + if len(mediaAndData) == 2 { + if mediaAndData[0] != "" { + mediaType = mediaAndData[0] + } + data = mediaAndData[1] + } + } + contentPart := `{"type":"document","source":{"type":"base64","media_type":"","data":""}}` + contentPart, _ = sjson.Set(contentPart, "source.media_type", mediaType) + contentPart, _ = sjson.Set(contentPart, "source.data", data) + partsJSON = append(partsJSON, contentPart) + if role == "" { + role = "user" + } + hasFile = true + } } return true }) @@ -228,7 +253,7 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte if len(partsJSON) > 0 { msg := `{"role":"","content":[]}` msg, _ = sjson.Set(msg, "role", role) - if len(partsJSON) == 1 && !hasImage { + if len(partsJSON) == 1 && !hasImage && !hasFile { // Preserve legacy behavior for single text content msg, _ = sjson.Delete(msg, "content") textPart := gjson.Parse(partsJSON[0]) diff --git a/internal/translator/codex/openai/chat-completions/codex_openai_request.go b/internal/translator/codex/openai/chat-completions/codex_openai_request.go index e79f97cd..1ea9ca4b 100644 --- a/internal/translator/codex/openai/chat-completions/codex_openai_request.go +++ b/internal/translator/codex/openai/chat-completions/codex_openai_request.go @@ -180,7 +180,19 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b msg, _ = sjson.SetRaw(msg, "content.-1", part) } case "file": - // Files are not specified in examples; skip for now + if role == "user" { + fileData := it.Get("file.file_data").String() + filename := it.Get("file.filename").String() + if fileData != "" { + part := `{}` + part, _ = sjson.Set(part, "type", "input_file") + part, _ = sjson.Set(part, "file_data", fileData) + if filename != "" { + part, _ = sjson.Set(part, "filename", filename) + } + msg, _ = sjson.SetRaw(msg, "content.-1", part) + } + } } } }