From 01e16a8509c1e65ff55daf68230bf87b2c7169be Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Mon, 27 Apr 2026 16:31:26 +0800 Subject: [PATCH] feat(codex): handle thinking-signature conversion for reasoning content - Implemented `appendReasoningContent` to support processing of `thinking` signature and text as reasoning input. - Added test cases to validate reasoning content conversion with and without text. --- .../codex/claude/codex_claude_request.go | 27 +++++++ .../codex/claude/codex_claude_request_test.go | 72 +++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/internal/translator/codex/claude/codex_claude_request.go b/internal/translator/codex/claude/codex_claude_request.go index adff9a038..0a034d6eb 100644 --- a/internal/translator/codex/claude/codex_claude_request.go +++ b/internal/translator/codex/claude/codex_claude_request.go @@ -120,6 +120,30 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) hasContent = true } + appendReasoningContent := func(part gjson.Result) { + if messageRole != "assistant" { + return + } + + thinkingText := thinking.GetThinkingText(part) + signature := part.Get("signature").String() + if strings.TrimSpace(thinkingText) == "" && signature == "" { + return + } + + reasoningItem := []byte(`{"type":"reasoning","summary":[]}`) + if signature != "" { + reasoningItem, _ = sjson.SetBytes(reasoningItem, "encrypted_content", signature) + } + if strings.TrimSpace(thinkingText) != "" { + summary := []byte(`{"type":"summary_text","text":""}`) + summary, _ = sjson.SetBytes(summary, "text", thinkingText) + reasoningItem, _ = sjson.SetRawBytes(reasoningItem, "summary.-1", summary) + } + + template, _ = sjson.SetRawBytes(template, "input.-1", reasoningItem) + } + messageContentsResult := messageResult.Get("content") if messageContentsResult.IsArray() { messageContentResults := messageContentsResult.Array() @@ -130,6 +154,9 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) switch contentType { case "text": appendTextContent(messageContentResult.Get("text").String()) + case "thinking": + flushMessage() + appendReasoningContent(messageContentResult) case "image": sourceResult := messageContentResult.Get("source") if sourceResult.Exists() { diff --git a/internal/translator/codex/claude/codex_claude_request_test.go b/internal/translator/codex/claude/codex_claude_request_test.go index 3cf023696..21df206e1 100644 --- a/internal/translator/codex/claude/codex_claude_request_test.go +++ b/internal/translator/codex/claude/codex_claude_request_test.go @@ -133,3 +133,75 @@ func TestConvertClaudeRequestToCodex_ParallelToolCalls(t *testing.T) { }) } } + +func TestConvertClaudeRequestToCodex_ThinkingSignatureToEncryptedContent(t *testing.T) { + result := ConvertClaudeRequestToCodex("test-model", []byte(`{ + "model": "claude-3-opus", + "messages": [{ + "role": "assistant", + "content": [ + {"type": "thinking", "thinking": "Internal reasoning.", "signature": "sig_123"}, + {"type": "text", "text": "Visible answer."} + ] + }] + }`), false) + resultJSON := gjson.ParseBytes(result) + inputs := resultJSON.Get("input").Array() + + if len(inputs) != 2 { + t.Fatalf("got %d input items, want 2. Output: %s", len(inputs), string(result)) + } + + reasoning := inputs[0] + if got := reasoning.Get("type").String(); got != "reasoning" { + t.Fatalf("input[0].type = %q, want %q. Output: %s", got, "reasoning", string(result)) + } + if got := reasoning.Get("encrypted_content").String(); got != "sig_123" { + t.Fatalf("encrypted_content = %q, want %q. Output: %s", got, "sig_123", string(result)) + } + if got := reasoning.Get("summary.0.type").String(); got != "summary_text" { + t.Fatalf("summary.0.type = %q, want %q. Output: %s", got, "summary_text", string(result)) + } + if got := reasoning.Get("summary.0.text").String(); got != "Internal reasoning." { + t.Fatalf("summary.0.text = %q, want %q. Output: %s", got, "Internal reasoning.", string(result)) + } + + message := inputs[1] + if got := message.Get("type").String(); got != "message" { + t.Fatalf("input[1].type = %q, want %q. Output: %s", got, "message", string(result)) + } + if got := message.Get("role").String(); got != "assistant" { + t.Fatalf("input[1].role = %q, want %q. Output: %s", got, "assistant", string(result)) + } + if got := message.Get("content.0.type").String(); got != "output_text" { + t.Fatalf("content.0.type = %q, want %q. Output: %s", got, "output_text", string(result)) + } + if got := message.Get("content.0.text").String(); got != "Visible answer." { + t.Fatalf("content.0.text = %q, want %q. Output: %s", got, "Visible answer.", string(result)) + } +} + +func TestConvertClaudeRequestToCodex_ThinkingSignatureWithoutText(t *testing.T) { + result := ConvertClaudeRequestToCodex("test-model", []byte(`{ + "model": "claude-3-opus", + "messages": [{ + "role": "assistant", + "content": [{"type": "thinking", "thinking": "", "signature": "sig_empty_text"}] + }] + }`), false) + resultJSON := gjson.ParseBytes(result) + inputs := resultJSON.Get("input").Array() + + if len(inputs) != 1 { + t.Fatalf("got %d input items, want 1. Output: %s", len(inputs), string(result)) + } + if got := inputs[0].Get("type").String(); got != "reasoning" { + t.Fatalf("input[0].type = %q, want %q. Output: %s", got, "reasoning", string(result)) + } + if got := inputs[0].Get("encrypted_content").String(); got != "sig_empty_text" { + t.Fatalf("encrypted_content = %q, want %q. Output: %s", got, "sig_empty_text", string(result)) + } + if got := len(inputs[0].Get("summary").Array()); got != 0 { + t.Fatalf("summary length = %d, want 0. Output: %s", got, string(result)) + } +}