Skip to content

Commit 01e16a8

Browse files
committed
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.
1 parent 04a336f commit 01e16a8

2 files changed

Lines changed: 99 additions & 0 deletions

File tree

internal/translator/codex/claude/codex_claude_request.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,30 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
120120
hasContent = true
121121
}
122122

123+
appendReasoningContent := func(part gjson.Result) {
124+
if messageRole != "assistant" {
125+
return
126+
}
127+
128+
thinkingText := thinking.GetThinkingText(part)
129+
signature := part.Get("signature").String()
130+
if strings.TrimSpace(thinkingText) == "" && signature == "" {
131+
return
132+
}
133+
134+
reasoningItem := []byte(`{"type":"reasoning","summary":[]}`)
135+
if signature != "" {
136+
reasoningItem, _ = sjson.SetBytes(reasoningItem, "encrypted_content", signature)
137+
}
138+
if strings.TrimSpace(thinkingText) != "" {
139+
summary := []byte(`{"type":"summary_text","text":""}`)
140+
summary, _ = sjson.SetBytes(summary, "text", thinkingText)
141+
reasoningItem, _ = sjson.SetRawBytes(reasoningItem, "summary.-1", summary)
142+
}
143+
144+
template, _ = sjson.SetRawBytes(template, "input.-1", reasoningItem)
145+
}
146+
123147
messageContentsResult := messageResult.Get("content")
124148
if messageContentsResult.IsArray() {
125149
messageContentResults := messageContentsResult.Array()
@@ -130,6 +154,9 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
130154
switch contentType {
131155
case "text":
132156
appendTextContent(messageContentResult.Get("text").String())
157+
case "thinking":
158+
flushMessage()
159+
appendReasoningContent(messageContentResult)
133160
case "image":
134161
sourceResult := messageContentResult.Get("source")
135162
if sourceResult.Exists() {

internal/translator/codex/claude/codex_claude_request_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,75 @@ func TestConvertClaudeRequestToCodex_ParallelToolCalls(t *testing.T) {
133133
})
134134
}
135135
}
136+
137+
func TestConvertClaudeRequestToCodex_ThinkingSignatureToEncryptedContent(t *testing.T) {
138+
result := ConvertClaudeRequestToCodex("test-model", []byte(`{
139+
"model": "claude-3-opus",
140+
"messages": [{
141+
"role": "assistant",
142+
"content": [
143+
{"type": "thinking", "thinking": "Internal reasoning.", "signature": "sig_123"},
144+
{"type": "text", "text": "Visible answer."}
145+
]
146+
}]
147+
}`), false)
148+
resultJSON := gjson.ParseBytes(result)
149+
inputs := resultJSON.Get("input").Array()
150+
151+
if len(inputs) != 2 {
152+
t.Fatalf("got %d input items, want 2. Output: %s", len(inputs), string(result))
153+
}
154+
155+
reasoning := inputs[0]
156+
if got := reasoning.Get("type").String(); got != "reasoning" {
157+
t.Fatalf("input[0].type = %q, want %q. Output: %s", got, "reasoning", string(result))
158+
}
159+
if got := reasoning.Get("encrypted_content").String(); got != "sig_123" {
160+
t.Fatalf("encrypted_content = %q, want %q. Output: %s", got, "sig_123", string(result))
161+
}
162+
if got := reasoning.Get("summary.0.type").String(); got != "summary_text" {
163+
t.Fatalf("summary.0.type = %q, want %q. Output: %s", got, "summary_text", string(result))
164+
}
165+
if got := reasoning.Get("summary.0.text").String(); got != "Internal reasoning." {
166+
t.Fatalf("summary.0.text = %q, want %q. Output: %s", got, "Internal reasoning.", string(result))
167+
}
168+
169+
message := inputs[1]
170+
if got := message.Get("type").String(); got != "message" {
171+
t.Fatalf("input[1].type = %q, want %q. Output: %s", got, "message", string(result))
172+
}
173+
if got := message.Get("role").String(); got != "assistant" {
174+
t.Fatalf("input[1].role = %q, want %q. Output: %s", got, "assistant", string(result))
175+
}
176+
if got := message.Get("content.0.type").String(); got != "output_text" {
177+
t.Fatalf("content.0.type = %q, want %q. Output: %s", got, "output_text", string(result))
178+
}
179+
if got := message.Get("content.0.text").String(); got != "Visible answer." {
180+
t.Fatalf("content.0.text = %q, want %q. Output: %s", got, "Visible answer.", string(result))
181+
}
182+
}
183+
184+
func TestConvertClaudeRequestToCodex_ThinkingSignatureWithoutText(t *testing.T) {
185+
result := ConvertClaudeRequestToCodex("test-model", []byte(`{
186+
"model": "claude-3-opus",
187+
"messages": [{
188+
"role": "assistant",
189+
"content": [{"type": "thinking", "thinking": "", "signature": "sig_empty_text"}]
190+
}]
191+
}`), false)
192+
resultJSON := gjson.ParseBytes(result)
193+
inputs := resultJSON.Get("input").Array()
194+
195+
if len(inputs) != 1 {
196+
t.Fatalf("got %d input items, want 1. Output: %s", len(inputs), string(result))
197+
}
198+
if got := inputs[0].Get("type").String(); got != "reasoning" {
199+
t.Fatalf("input[0].type = %q, want %q. Output: %s", got, "reasoning", string(result))
200+
}
201+
if got := inputs[0].Get("encrypted_content").String(); got != "sig_empty_text" {
202+
t.Fatalf("encrypted_content = %q, want %q. Output: %s", got, "sig_empty_text", string(result))
203+
}
204+
if got := len(inputs[0].Get("summary").Array()); got != 0 {
205+
t.Fatalf("summary length = %d, want 0. Output: %s", got, string(result))
206+
}
207+
}

0 commit comments

Comments
 (0)