diff --git a/internal/translator/codex/claude/codex_claude_request.go b/internal/translator/codex/claude/codex_claude_request.go index 20241bf24..6b5bfb0bc 100644 --- a/internal/translator/codex/claude/codex_claude_request.go +++ b/internal/translator/codex/claude/codex_claude_request.go @@ -26,7 +26,7 @@ import ( // The function performs the following transformations: // 1. Sets up a template with the model name and empty instructions field // 2. Processes system messages and converts them to developer input content -// 3. Transforms message contents (text, image, tool_use, tool_result) to appropriate formats +// 3. Transforms message contents (text, image, document, tool_use, tool_result) to appropriate formats // 4. Converts tools declarations to the expected format // 5. Adds additional configuration parameters for the Codex API // 6. Maps Claude thinking configuration to Codex reasoning settings @@ -129,6 +129,12 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) contentItems = append(contentItems, content) } + appendDocumentContent := func(dataURL string) { + content := []byte(`{"type":"input_file","file_data":"","filename":"document.pdf"}`) + content, _ = sjson.SetBytes(content, "file_data", dataURL) + contentItems = append(contentItems, content) + } + appendReasoningContent := func(part gjson.Result) { if messageRole != "assistant" { return @@ -182,6 +188,22 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) appendImageContent(dataURL) } } + case "document": + sourceResult := messageContentResult.Get("source") + if sourceResult.Get("type").String() != "base64" { + continue + } + mediaType := strings.TrimSpace(sourceResult.Get("media_type").String()) + if !strings.EqualFold(mediaType, "application/pdf") { + continue + } + data := sourceResult.Get("data").String() + if data == "" { + data = sourceResult.Get("base64").String() + } + if data != "" { + appendDocumentContent(fmt.Sprintf("data:%s;base64,%s", mediaType, data)) + } case "tool_use": flushMessage() functionCallMessage := []byte(`{"type":"function_call"}`) diff --git a/internal/translator/codex/claude/codex_claude_request_test.go b/internal/translator/codex/claude/codex_claude_request_test.go index 446e1376e..9db9c069f 100644 --- a/internal/translator/codex/claude/codex_claude_request_test.go +++ b/internal/translator/codex/claude/codex_claude_request_test.go @@ -508,6 +508,44 @@ func TestConvertClaudeRequestToCodex_AssistantThinkingSignatureToReasoningItem(t } } +func TestConvertClaudeRequestToCodex_PreservesBase64PDFDocumentContent(t *testing.T) { + inputJSON := `{ + "messages": [{ + "role": "user", + "content": [ + {"type": "text", "text": "before"}, + {"type": "document", "source": {"type": "base64", "media_type": "application/pdf", "data": "JVBERi0xLjQK"}}, + {"type": "text", "text": "after"} + ] + }] + }` + + result := ConvertClaudeRequestToCodex("gpt-5.6-sol", []byte(inputJSON), false) + content := gjson.GetBytes(result, "input.0.content").Array() + if len(content) != 3 { + t.Fatalf("got %d content items, want 3. Output: %s", len(content), result) + } + + wantTypes := []string{"input_text", "input_file", "input_text"} + for i, wantType := range wantTypes { + if got := content[i].Get("type").String(); got != wantType { + t.Fatalf("content[%d].type = %q, want %q. Output: %s", i, got, wantType, result) + } + } + if got := content[0].Get("text").String(); got != "before" { + t.Fatalf("content[0].text = %q, want %q", got, "before") + } + if got := content[1].Get("file_data").String(); got != "data:application/pdf;base64,JVBERi0xLjQK" { + t.Fatalf("content[1].file_data = %q, want PDF data URL", got) + } + if got := content[1].Get("filename").String(); got != "document.pdf" { + t.Fatalf("content[1].filename = %q, want %q", got, "document.pdf") + } + if got := content[2].Get("text").String(); got != "after" { + t.Fatalf("content[2].text = %q, want %q", got, "after") + } +} + func TestConvertClaudeRequestToCodex_PreservesContentOrderAcrossToolAndReasoningItems(t *testing.T) { signature := validCodexReasoningSignature() inputJSON := `{