Skip to content

Commit c945e35

Browse files
committed
feat(translator): improve Claude request handling with enhanced content processing
- Introduced helper functions (`appendTextContent`, `appendImageContent`, etc.) for structured content construction. - Refactored message generation logic for better clarity, supporting mixed content scenarios (text, images, and function calls). - Added `flushMessage` to ensure proper grouping of message contents.
1 parent 1cd275f commit c945e35

File tree

2 files changed

+129
-44
lines changed

2 files changed

+129
-44
lines changed

internal/translator/claude/openai/responses/claude_openai-responses_request.go

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -143,21 +143,63 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
143143
}
144144
switch typ {
145145
case "message":
146-
// Determine role from content type (input_text=user, output_text=assistant)
146+
// Determine role and construct Claude-compatible content parts.
147147
var role string
148-
var text strings.Builder
148+
var textAggregate strings.Builder
149+
var partsJSON []string
150+
hasImage := false
149151
if parts := item.Get("content"); parts.Exists() && parts.IsArray() {
150152
parts.ForEach(func(_, part gjson.Result) bool {
151153
ptype := part.Get("type").String()
152-
if ptype == "input_text" || ptype == "output_text" {
154+
switch ptype {
155+
case "input_text", "output_text":
153156
if t := part.Get("text"); t.Exists() {
154-
text.WriteString(t.String())
157+
txt := t.String()
158+
textAggregate.WriteString(txt)
159+
contentPart := `{"type":"text","text":""}`
160+
contentPart, _ = sjson.Set(contentPart, "text", txt)
161+
partsJSON = append(partsJSON, contentPart)
155162
}
156163
if ptype == "input_text" {
157164
role = "user"
158-
} else if ptype == "output_text" {
165+
} else {
159166
role = "assistant"
160167
}
168+
case "input_image":
169+
url := part.Get("image_url").String()
170+
if url == "" {
171+
url = part.Get("url").String()
172+
}
173+
if url != "" {
174+
var contentPart string
175+
if strings.HasPrefix(url, "data:") {
176+
trimmed := strings.TrimPrefix(url, "data:")
177+
mediaAndData := strings.SplitN(trimmed, ";base64,", 2)
178+
mediaType := "application/octet-stream"
179+
data := ""
180+
if len(mediaAndData) == 2 {
181+
if mediaAndData[0] != "" {
182+
mediaType = mediaAndData[0]
183+
}
184+
data = mediaAndData[1]
185+
}
186+
if data != "" {
187+
contentPart = `{"type":"image","source":{"type":"base64","media_type":"","data":""}}`
188+
contentPart, _ = sjson.Set(contentPart, "source.media_type", mediaType)
189+
contentPart, _ = sjson.Set(contentPart, "source.data", data)
190+
}
191+
} else {
192+
contentPart = `{"type":"image","source":{"type":"url","url":""}}`
193+
contentPart, _ = sjson.Set(contentPart, "source.url", url)
194+
}
195+
if contentPart != "" {
196+
partsJSON = append(partsJSON, contentPart)
197+
if role == "" {
198+
role = "user"
199+
}
200+
hasImage = true
201+
}
202+
}
161203
}
162204
return true
163205
})
@@ -174,15 +216,25 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
174216
}
175217
}
176218

177-
if text.Len() > 0 || role == "system" {
178-
msg := `{"role":"","content":""}`
219+
if len(partsJSON) > 0 {
220+
msg := `{"role":"","content":[]}`
179221
msg, _ = sjson.Set(msg, "role", role)
180-
if text.Len() > 0 {
181-
msg, _ = sjson.Set(msg, "content", text.String())
222+
if len(partsJSON) == 1 && !hasImage {
223+
// Preserve legacy behavior for single text content
224+
msg, _ = sjson.Delete(msg, "content")
225+
textPart := gjson.Parse(partsJSON[0])
226+
msg, _ = sjson.Set(msg, "content", textPart.Get("text").String())
182227
} else {
183-
msg, _ = sjson.Set(msg, "content", "")
228+
for _, partJSON := range partsJSON {
229+
msg, _ = sjson.SetRaw(msg, "content.-1", partJSON)
230+
}
184231
}
185232
out, _ = sjson.SetRaw(out, "messages.-1", msg)
233+
} else if textAggregate.Len() > 0 || role == "system" {
234+
msg := `{"role":"","content":""}`
235+
msg, _ = sjson.Set(msg, "role", role)
236+
msg, _ = sjson.Set(msg, "content", textAggregate.String())
237+
out, _ = sjson.SetRaw(out, "messages.-1", msg)
186238
}
187239

188240
case "function_call":

internal/translator/codex/claude/codex_claude_request.go

Lines changed: 67 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -68,36 +68,79 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
6868

6969
for i := 0; i < len(messageResults); i++ {
7070
messageResult := messageResults[i]
71+
messageRole := messageResult.Get("role").String()
72+
73+
newMessage := func() string {
74+
msg := `{"type": "message","role":"","content":[]}`
75+
msg, _ = sjson.Set(msg, "role", messageRole)
76+
return msg
77+
}
78+
79+
message := newMessage()
80+
contentIndex := 0
81+
hasContent := false
82+
83+
flushMessage := func() {
84+
if hasContent {
85+
template, _ = sjson.SetRaw(template, "input.-1", message)
86+
message = newMessage()
87+
contentIndex = 0
88+
hasContent = false
89+
}
90+
}
91+
92+
appendTextContent := func(text string) {
93+
partType := "input_text"
94+
if messageRole == "assistant" {
95+
partType = "output_text"
96+
}
97+
message, _ = sjson.Set(message, fmt.Sprintf("content.%d.type", contentIndex), partType)
98+
message, _ = sjson.Set(message, fmt.Sprintf("content.%d.text", contentIndex), text)
99+
contentIndex++
100+
hasContent = true
101+
}
102+
103+
appendImageContent := func(dataURL string) {
104+
message, _ = sjson.Set(message, fmt.Sprintf("content.%d.type", contentIndex), "input_image")
105+
message, _ = sjson.Set(message, fmt.Sprintf("content.%d.image_url", contentIndex), dataURL)
106+
contentIndex++
107+
hasContent = true
108+
}
71109

72110
messageContentsResult := messageResult.Get("content")
73111
if messageContentsResult.IsArray() {
74112
messageContentResults := messageContentsResult.Array()
75113
for j := 0; j < len(messageContentResults); j++ {
76114
messageContentResult := messageContentResults[j]
77-
messageContentTypeResult := messageContentResult.Get("type")
78-
contentType := messageContentTypeResult.String()
79-
80-
if contentType == "text" {
81-
// Handle text content by creating appropriate message structure.
82-
message := `{"type": "message","role":"","content":[]}`
83-
messageRole := messageResult.Get("role").String()
84-
message, _ = sjson.Set(message, "role", messageRole)
115+
contentType := messageContentResult.Get("type").String()
85116

86-
partType := "input_text"
87-
if messageRole == "assistant" {
88-
partType = "output_text"
117+
switch contentType {
118+
case "text":
119+
appendTextContent(messageContentResult.Get("text").String())
120+
case "image":
121+
sourceResult := messageContentResult.Get("source")
122+
if sourceResult.Exists() {
123+
data := sourceResult.Get("data").String()
124+
if data == "" {
125+
data = sourceResult.Get("base64").String()
126+
}
127+
if data != "" {
128+
mediaType := sourceResult.Get("media_type").String()
129+
if mediaType == "" {
130+
mediaType = sourceResult.Get("mime_type").String()
131+
}
132+
if mediaType == "" {
133+
mediaType = "application/octet-stream"
134+
}
135+
dataURL := fmt.Sprintf("data:%s;base64,%s", mediaType, data)
136+
appendImageContent(dataURL)
137+
}
89138
}
90-
91-
currentIndex := len(gjson.Get(message, "content").Array())
92-
message, _ = sjson.Set(message, fmt.Sprintf("content.%d.type", currentIndex), partType)
93-
message, _ = sjson.Set(message, fmt.Sprintf("content.%d.text", currentIndex), messageContentResult.Get("text").String())
94-
template, _ = sjson.SetRaw(template, "input.-1", message)
95-
} else if contentType == "tool_use" {
96-
// Handle tool use content by creating function call message.
139+
case "tool_use":
140+
flushMessage()
97141
functionCallMessage := `{"type":"function_call"}`
98142
functionCallMessage, _ = sjson.Set(functionCallMessage, "call_id", messageContentResult.Get("id").String())
99143
{
100-
// Shorten tool name if needed based on declared tools
101144
name := messageContentResult.Get("name").String()
102145
toolMap := buildReverseMapFromClaudeOriginalToShort(rawJSON)
103146
if short, ok := toolMap[name]; ok {
@@ -109,28 +152,18 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
109152
}
110153
functionCallMessage, _ = sjson.Set(functionCallMessage, "arguments", messageContentResult.Get("input").Raw)
111154
template, _ = sjson.SetRaw(template, "input.-1", functionCallMessage)
112-
} else if contentType == "tool_result" {
113-
// Handle tool result content by creating function call output message.
155+
case "tool_result":
156+
flushMessage()
114157
functionCallOutputMessage := `{"type":"function_call_output"}`
115158
functionCallOutputMessage, _ = sjson.Set(functionCallOutputMessage, "call_id", messageContentResult.Get("tool_use_id").String())
116159
functionCallOutputMessage, _ = sjson.Set(functionCallOutputMessage, "output", messageContentResult.Get("content").String())
117160
template, _ = sjson.SetRaw(template, "input.-1", functionCallOutputMessage)
118161
}
119162
}
163+
flushMessage()
120164
} else if messageContentsResult.Type == gjson.String {
121-
// Handle string content by creating appropriate message structure.
122-
message := `{"type": "message","role":"","content":[]}`
123-
messageRole := messageResult.Get("role").String()
124-
message, _ = sjson.Set(message, "role", messageRole)
125-
126-
partType := "input_text"
127-
if messageRole == "assistant" {
128-
partType = "output_text"
129-
}
130-
131-
message, _ = sjson.Set(message, "content.0.type", partType)
132-
message, _ = sjson.Set(message, "content.0.text", messageContentsResult.String())
133-
template, _ = sjson.SetRaw(template, "input.-1", message)
165+
appendTextContent(messageContentsResult.String())
166+
flushMessage()
134167
}
135168
}
136169

0 commit comments

Comments
 (0)