History

fix: map OpenAI system messages to Claude top-level system

GeJiaXiang committed 7 months ago

Commit 000e4ce1 parent 96f55572 files · +70 −9Browse files

Unified · Split

2 files
  1. internal/translator/claude/openai/chat-completions/claude_openai_request.go +2 −9
  2. internal/translator/claude/openai/chat-completions/claude_openai_request_test.go +68 −0
internal/translator/claude/openai/chat-completions/claude_openai_request.go+2−9
@@ -165,29 +165,22 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream · 164 unmodified lines
165165 // Process messages and transform them to Claude Code format
166166 if messages := root.Get("messages"); messages.Exists() && messages.IsArray() {
167167 messageIndex := 0
168− systemMessageIndex := -1
169168 messages.ForEach(func(_, message gjson.Result) bool {
170169 role := message.Get("role").String()
171170 contentResult := message.Get("content")
172171
173172 switch role {
174173 case "system":
175− if systemMessageIndex == -1 {
176− systemMsg := []byte(`{"role":"user","content":[]}`)
177− out, _ = sjson.SetRawBytes(out, "messages.-1", systemMsg)
178− systemMessageIndex = messageIndex
179− messageIndex++
180− }
181174 if contentResult.Exists() && contentResult.Type == gjson.String && contentResult.String() != "" {
182175 textPart := []byte(`{"type":"text","text":""}`)
183176 textPart, _ = sjson.SetBytes(textPart, "text", contentResult.String())
184− out, _ = sjson.SetRawBytes(out, fmt.Sprintf("messages.%d.content.-1", systemMessageIndex), textPart)
177+ out, _ = sjson.SetRawBytes(out, "system.-1", textPart)
185178 } else if contentResult.Exists() && contentResult.IsArray() {
186179 contentResult.ForEach(func(_, part gjson.Result) bool {
187180 if part.Get("type").String() == "text" {
188181 textPart := []byte(`{"type":"text","text":""}`)
189182 textPart, _ = sjson.SetBytes(textPart, "text", part.Get("text").String())
190− out, _ = sjson.SetRawBytes(out, fmt.Sprintf("messages.%d.content.-1", systemMessageIndex), textPart)
183+ out, _ = sjson.SetRawBytes(out, "system.-1", textPart)
191184 }
192185 return true
193186 })
internal/translator/claude/openai/chat-completions/claude_openai_request_test.go+68−0
@@ -135,3 +135,71 @@ func TestConvertOpenAIRequestToClaude_ToolResultURLImageOnly(t *testing.T) { · 134 unmodified lines
135135 t.Fatalf("Unexpected image URL: %q", got)
136136 }
137137 }
138+
139+func TestConvertOpenAIRequestToClaude_SystemRoleBecomesTopLevelSystem(t *testing.T) {
140+ inputJSON := `{
141+ "model": "gpt-4.1",
142+ "messages": [
143+ {"role": "system", "content": "You are a helpful assistant."},
144+ {"role": "user", "content": "Hello"}
145+ ]
146+ }`
147+
148+ result := ConvertOpenAIRequestToClaude("claude-sonnet-4-5", []byte(inputJSON), false)
149+ resultJSON := gjson.ParseBytes(result)
150+
151+ system := resultJSON.Get("system")
152+ if !system.IsArray() {
153+ t.Fatalf("Expected top-level system array, got %s", system.Raw)
154+ }
155+ if len(system.Array()) != 1 {
156+ t.Fatalf("Expected 1 system block, got %d. System: %s", len(system.Array()), system.Raw)
157+ }
158+ if got := system.Get("0.type").String(); got != "text" {
159+ t.Fatalf("Expected system block type %q, got %q", "text", got)
160+ }
161+ if got := system.Get("0.text").String(); got != "You are a helpful assistant." {
162+ t.Fatalf("Expected system text %q, got %q", "You are a helpful assistant.", got)
163+ }
164+
165+ messages := resultJSON.Get("messages").Array()
166+ if len(messages) != 1 {
167+ t.Fatalf("Expected 1 non-system message, got %d. Messages: %s", len(messages), resultJSON.Get("messages").Raw)
168+ }
169+ if got := messages[0].Get("role").String(); got != "user" {
170+ t.Fatalf("Expected remaining message role %q, got %q", "user", got)
171+ }
172+ if got := messages[0].Get("content.0.text").String(); got != "Hello" {
173+ t.Fatalf("Expected user text %q, got %q", "Hello", got)
174+ }
175+}
176+
177+func TestConvertOpenAIRequestToClaude_MultipleSystemMessagesMergedIntoTopLevelSystem(t *testing.T) {
178+ inputJSON := `{
179+ "model": "gpt-4.1",
180+ "messages": [
181+ {"role": "system", "content": "Rule 1"},
182+ {"role": "system", "content": [{"type": "text", "text": "Rule 2"}]},
183+ {"role": "user", "content": "Hello"}
184+ ]
185+ }`
186+
187+ result := ConvertOpenAIRequestToClaude("claude-sonnet-4-5", []byte(inputJSON), false)
188+ resultJSON := gjson.ParseBytes(result)
189+
190+ system := resultJSON.Get("system").Array()
191+ if len(system) != 2 {
192+ t.Fatalf("Expected 2 system blocks, got %d. System: %s", len(system), resultJSON.Get("system").Raw)
193+ }
194+ if got := system[0].Get("text").String(); got != "Rule 1" {
195+ t.Fatalf("Expected first system text %q, got %q", "Rule 1", got)
196+ }
197+ if got := system[1].Get("text").String(); got != "Rule 2" {
198+ t.Fatalf("Expected second system text %q, got %q", "Rule 2", got)
199+ }
200+
201+ messages := resultJSON.Get("messages").Array()
202+ if len(messages) != 1 {
203+ t.Fatalf("Expected 1 non-system message, got %d. Messages: %s", len(messages), resultJSON.Get("messages").Raw)
204+ }
205+}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t