diff --git a/internal/translator/codex/claude/codex_claude_request.go b/internal/translator/codex/claude/codex_claude_request.go index 6373e693..4bc116b9 100644 --- a/internal/translator/codex/claude/codex_claude_request.go +++ b/internal/translator/codex/claude/codex_claude_request.go @@ -43,23 +43,32 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) // Process system messages and convert them to input content format. systemsResult := rootResult.Get("system") - if systemsResult.IsArray() { - systemResults := systemsResult.Array() + if systemsResult.Exists() { message := `{"type":"message","role":"developer","content":[]}` contentIndex := 0 - for i := 0; i < len(systemResults); i++ { - systemResult := systemResults[i] - systemTypeResult := systemResult.Get("type") - if systemTypeResult.String() == "text" { - text := systemResult.Get("text").String() - if strings.HasPrefix(text, "x-anthropic-billing-header: ") { - continue + + appendSystemText := func(text string) { + if text == "" || strings.HasPrefix(text, "x-anthropic-billing-header: ") { + return + } + + message, _ = sjson.Set(message, fmt.Sprintf("content.%d.type", contentIndex), "input_text") + message, _ = sjson.Set(message, fmt.Sprintf("content.%d.text", contentIndex), text) + contentIndex++ + } + + if systemsResult.Type == gjson.String { + appendSystemText(systemsResult.String()) + } else if systemsResult.IsArray() { + systemResults := systemsResult.Array() + for i := 0; i < len(systemResults); i++ { + systemResult := systemResults[i] + if systemResult.Get("type").String() == "text" { + appendSystemText(systemResult.Get("text").String()) } - message, _ = sjson.Set(message, fmt.Sprintf("content.%d.type", contentIndex), "input_text") - message, _ = sjson.Set(message, fmt.Sprintf("content.%d.text", contentIndex), text) - contentIndex++ } } + if contentIndex > 0 { template, _ = sjson.SetRaw(template, "input.-1", message) } diff --git a/internal/translator/codex/claude/codex_claude_request_test.go b/internal/translator/codex/claude/codex_claude_request_test.go new file mode 100644 index 00000000..bdd41639 --- /dev/null +++ b/internal/translator/codex/claude/codex_claude_request_test.go @@ -0,0 +1,89 @@ +package claude + +import ( + "testing" + + "github.com/tidwall/gjson" +) + +func TestConvertClaudeRequestToCodex_SystemMessageScenarios(t *testing.T) { + tests := []struct { + name string + inputJSON string + wantHasDeveloper bool + wantTexts []string + }{ + { + name: "No system field", + inputJSON: `{ + "model": "claude-3-opus", + "messages": [{"role": "user", "content": "hello"}] + }`, + wantHasDeveloper: false, + }, + { + name: "Empty string system field", + inputJSON: `{ + "model": "claude-3-opus", + "system": "", + "messages": [{"role": "user", "content": "hello"}] + }`, + wantHasDeveloper: false, + }, + { + name: "String system field", + inputJSON: `{ + "model": "claude-3-opus", + "system": "Be helpful", + "messages": [{"role": "user", "content": "hello"}] + }`, + wantHasDeveloper: true, + wantTexts: []string{"Be helpful"}, + }, + { + name: "Array system field with filtered billing header", + inputJSON: `{ + "model": "claude-3-opus", + "system": [ + {"type": "text", "text": "x-anthropic-billing-header: tenant-123"}, + {"type": "text", "text": "Block 1"}, + {"type": "text", "text": "Block 2"} + ], + "messages": [{"role": "user", "content": "hello"}] + }`, + wantHasDeveloper: true, + wantTexts: []string{"Block 1", "Block 2"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ConvertClaudeRequestToCodex("test-model", []byte(tt.inputJSON), false) + resultJSON := gjson.ParseBytes(result) + inputs := resultJSON.Get("input").Array() + + hasDeveloper := len(inputs) > 0 && inputs[0].Get("role").String() == "developer" + if hasDeveloper != tt.wantHasDeveloper { + t.Fatalf("got hasDeveloper = %v, want %v. Output: %s", hasDeveloper, tt.wantHasDeveloper, resultJSON.Get("input").Raw) + } + + if !tt.wantHasDeveloper { + return + } + + content := inputs[0].Get("content").Array() + if len(content) != len(tt.wantTexts) { + t.Fatalf("got %d system content items, want %d. Content: %s", len(content), len(tt.wantTexts), inputs[0].Get("content").Raw) + } + + for i, wantText := range tt.wantTexts { + if gotType := content[i].Get("type").String(); gotType != "input_text" { + t.Fatalf("content[%d] type = %q, want %q", i, gotType, "input_text") + } + if gotText := content[i].Get("text").String(); gotText != wantText { + t.Fatalf("content[%d] text = %q, want %q", i, gotText, wantText) + } + } + }) + } +}