Skip to content

Commit 7b7b258

Browse files
committed
test(translator): add tests for handling Claude system messages as string and array
1 parent cf74ed2 commit 7b7b258

File tree

2 files changed

+110
-12
lines changed

2 files changed

+110
-12
lines changed

internal/translator/codex/claude/codex_claude_request.go

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,23 +43,32 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
4343

4444
// Process system messages and convert them to input content format.
4545
systemsResult := rootResult.Get("system")
46-
if systemsResult.IsArray() {
47-
systemResults := systemsResult.Array()
46+
if systemsResult.Exists() {
4847
message := `{"type":"message","role":"developer","content":[]}`
4948
contentIndex := 0
50-
for i := 0; i < len(systemResults); i++ {
51-
systemResult := systemResults[i]
52-
systemTypeResult := systemResult.Get("type")
53-
if systemTypeResult.String() == "text" {
54-
text := systemResult.Get("text").String()
55-
if strings.HasPrefix(text, "x-anthropic-billing-header: ") {
56-
continue
49+
50+
appendSystemText := func(text string) {
51+
if text == "" || strings.HasPrefix(text, "x-anthropic-billing-header: ") {
52+
return
53+
}
54+
55+
message, _ = sjson.Set(message, fmt.Sprintf("content.%d.type", contentIndex), "input_text")
56+
message, _ = sjson.Set(message, fmt.Sprintf("content.%d.text", contentIndex), text)
57+
contentIndex++
58+
}
59+
60+
if systemsResult.Type == gjson.String {
61+
appendSystemText(systemsResult.String())
62+
} else if systemsResult.IsArray() {
63+
systemResults := systemsResult.Array()
64+
for i := 0; i < len(systemResults); i++ {
65+
systemResult := systemResults[i]
66+
if systemResult.Get("type").String() == "text" {
67+
appendSystemText(systemResult.Get("text").String())
5768
}
58-
message, _ = sjson.Set(message, fmt.Sprintf("content.%d.type", contentIndex), "input_text")
59-
message, _ = sjson.Set(message, fmt.Sprintf("content.%d.text", contentIndex), text)
60-
contentIndex++
6169
}
6270
}
71+
6372
if contentIndex > 0 {
6473
template, _ = sjson.SetRaw(template, "input.-1", message)
6574
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package claude
2+
3+
import (
4+
"testing"
5+
6+
"github.com/tidwall/gjson"
7+
)
8+
9+
func TestConvertClaudeRequestToCodex_SystemMessageScenarios(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
inputJSON string
13+
wantHasDeveloper bool
14+
wantTexts []string
15+
}{
16+
{
17+
name: "No system field",
18+
inputJSON: `{
19+
"model": "claude-3-opus",
20+
"messages": [{"role": "user", "content": "hello"}]
21+
}`,
22+
wantHasDeveloper: false,
23+
},
24+
{
25+
name: "Empty string system field",
26+
inputJSON: `{
27+
"model": "claude-3-opus",
28+
"system": "",
29+
"messages": [{"role": "user", "content": "hello"}]
30+
}`,
31+
wantHasDeveloper: false,
32+
},
33+
{
34+
name: "String system field",
35+
inputJSON: `{
36+
"model": "claude-3-opus",
37+
"system": "Be helpful",
38+
"messages": [{"role": "user", "content": "hello"}]
39+
}`,
40+
wantHasDeveloper: true,
41+
wantTexts: []string{"Be helpful"},
42+
},
43+
{
44+
name: "Array system field with filtered billing header",
45+
inputJSON: `{
46+
"model": "claude-3-opus",
47+
"system": [
48+
{"type": "text", "text": "x-anthropic-billing-header: tenant-123"},
49+
{"type": "text", "text": "Block 1"},
50+
{"type": "text", "text": "Block 2"}
51+
],
52+
"messages": [{"role": "user", "content": "hello"}]
53+
}`,
54+
wantHasDeveloper: true,
55+
wantTexts: []string{"Block 1", "Block 2"},
56+
},
57+
}
58+
59+
for _, tt := range tests {
60+
t.Run(tt.name, func(t *testing.T) {
61+
result := ConvertClaudeRequestToCodex("test-model", []byte(tt.inputJSON), false)
62+
resultJSON := gjson.ParseBytes(result)
63+
inputs := resultJSON.Get("input").Array()
64+
65+
hasDeveloper := len(inputs) > 0 && inputs[0].Get("role").String() == "developer"
66+
if hasDeveloper != tt.wantHasDeveloper {
67+
t.Fatalf("got hasDeveloper = %v, want %v. Output: %s", hasDeveloper, tt.wantHasDeveloper, resultJSON.Get("input").Raw)
68+
}
69+
70+
if !tt.wantHasDeveloper {
71+
return
72+
}
73+
74+
content := inputs[0].Get("content").Array()
75+
if len(content) != len(tt.wantTexts) {
76+
t.Fatalf("got %d system content items, want %d. Content: %s", len(content), len(tt.wantTexts), inputs[0].Get("content").Raw)
77+
}
78+
79+
for i, wantText := range tt.wantTexts {
80+
if gotType := content[i].Get("type").String(); gotType != "input_text" {
81+
t.Fatalf("content[%d] type = %q, want %q", i, gotType, "input_text")
82+
}
83+
if gotText := content[i].Get("text").String(); gotText != wantText {
84+
t.Fatalf("content[%d] text = %q, want %q", i, gotText, wantText)
85+
}
86+
}
87+
})
88+
}
89+
}

0 commit comments

Comments
 (0)