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
165 // Process messages and transform them to Claude Code format165 // Process messages and transform them to Claude Code format
166 if messages := root.Get("messages"); messages.Exists() && messages.IsArray() {166 if messages := root.Get("messages"); messages.Exists() && messages.IsArray() {
167 messageIndex := 0167 messageIndex := 0
168 systemMessageIndex := -1
169 messages.ForEach(func(_, message gjson.Result) bool {168 messages.ForEach(func(_, message gjson.Result) bool {
170 role := message.Get("role").String()169 role := message.Get("role").String()
171 contentResult := message.Get("content")170 contentResult := message.Get("content")
172 171
173 switch role {172 switch role {
174 case "system":173 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 }
181 if contentResult.Exists() && contentResult.Type == gjson.String && contentResult.String() != "" {174 if contentResult.Exists() && contentResult.Type == gjson.String && contentResult.String() != "" {
182 textPart := []byte(`{"type":"text","text":""}`)175 textPart := []byte(`{"type":"text","text":""}`)
183 textPart, _ = sjson.SetBytes(textPart, "text", contentResult.String())176 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)
185 } else if contentResult.Exists() && contentResult.IsArray() {178 } else if contentResult.Exists() && contentResult.IsArray() {
186 contentResult.ForEach(func(_, part gjson.Result) bool {179 contentResult.ForEach(func(_, part gjson.Result) bool {
187 if part.Get("type").String() == "text" {180 if part.Get("type").String() == "text" {
188 textPart := []byte(`{"type":"text","text":""}`)181 textPart := []byte(`{"type":"text","text":""}`)
189 textPart, _ = sjson.SetBytes(textPart, "text", part.Get("text").String())182 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)
191 }184 }
192 return true185 return true
193 })186 })
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
135 t.Fatalf("Unexpected image URL: %q", got)135 t.Fatalf("Unexpected image URL: %q", got)
136 }136 }
137}137}
138
139func 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
177func 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