Preserve Codex reasoning signatures for Claude
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package claude
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
@@ -134,74 +136,143 @@ func TestConvertClaudeRequestToCodex_ParallelToolCalls(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertClaudeRequestToCodex_ThinkingSignatureToEncryptedContent(t *testing.T) {
|
||||
result := ConvertClaudeRequestToCodex("test-model", []byte(`{
|
||||
func TestConvertClaudeRequestToCodex_AssistantThinkingSignatureToReasoningItem(t *testing.T) {
|
||||
signature := validCodexReasoningSignature()
|
||||
inputJSON := `{
|
||||
"model": "claude-3-opus",
|
||||
"messages": [{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "thinking", "thinking": "Internal reasoning.", "signature": "sig_123"},
|
||||
{"type": "text", "text": "Visible answer."}
|
||||
]
|
||||
}]
|
||||
}`), false)
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "thinking",
|
||||
"thinking": "visible summary must not be replayed",
|
||||
"signature": "` + signature + `"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "visible answer"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "continue"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
result := ConvertClaudeRequestToCodex("test-model", []byte(inputJSON), 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))
|
||||
if len(inputs) != 3 {
|
||||
t.Fatalf("got %d input items, want 3. 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))
|
||||
t.Fatalf("first input type = %q, want reasoning. Output: %s", got, 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("encrypted_content").String(); got != signature {
|
||||
t.Fatalf("encrypted_content = %q, want %q", got, signature)
|
||||
}
|
||||
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").Raw; got != "[]" {
|
||||
t.Fatalf("summary = %s, want []", got)
|
||||
}
|
||||
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))
|
||||
if got := reasoning.Get("content").Raw; got != "null" {
|
||||
t.Fatalf("content = %s, want null", got)
|
||||
}
|
||||
|
||||
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))
|
||||
assistantMessage := inputs[1]
|
||||
if got := assistantMessage.Get("role").String(); got != "assistant" {
|
||||
t.Fatalf("second input role = %q, want assistant. Output: %s", got, 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 := assistantMessage.Get("content.0.type").String(); got != "output_text" {
|
||||
t.Fatalf("assistant content type = %q, want output_text", got)
|
||||
}
|
||||
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 := assistantMessage.Get("content.0.text").String(); got != "visible answer" {
|
||||
t.Fatalf("assistant text = %q, want visible answer", got)
|
||||
}
|
||||
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))
|
||||
if strings.Contains(string(result), "visible summary must not be replayed") {
|
||||
t.Fatalf("thinking text should not be replayed into Codex input. Output: %s", 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()
|
||||
func TestConvertClaudeRequestToCodex_IgnoresNonCodexThinkingSignatures(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
inputJSON string
|
||||
}{
|
||||
{
|
||||
name: "Ignore user thinking even with Codex-shaped signature",
|
||||
inputJSON: `{
|
||||
"model": "claude-3-opus",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "thinking",
|
||||
"thinking": "user supplied thinking",
|
||||
"signature": "` + validCodexReasoningSignature() + `"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "hello"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "Ignore Anthropic native signature",
|
||||
inputJSON: `{
|
||||
"model": "claude-3-opus",
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "thinking",
|
||||
"thinking": "anthropic thinking",
|
||||
"signature": "Eo8Canthropic-state"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "visible answer"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`,
|
||||
},
|
||||
}
|
||||
|
||||
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))
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ConvertClaudeRequestToCodex("test-model", []byte(tt.inputJSON), false)
|
||||
if got := countRequestInputItemsByType(result, "reasoning"); got != 0 {
|
||||
t.Fatalf("got %d reasoning items, want 0. Output: %s", got, string(result))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func countRequestInputItemsByType(result []byte, itemType string) int {
|
||||
count := 0
|
||||
gjson.GetBytes(result, "input").ForEach(func(_, item gjson.Result) bool {
|
||||
if item.Get("type").String() == itemType {
|
||||
count++
|
||||
}
|
||||
return true
|
||||
})
|
||||
return count
|
||||
}
|
||||
|
||||
func validCodexReasoningSignature() string {
|
||||
raw := make([]byte, 1+8+16+16+32)
|
||||
raw[0] = 0x80
|
||||
raw[8] = 1
|
||||
return base64.URLEncoding.EncodeToString(raw)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user