@@ -6,6 +6,11 @@ import (
66 "strings"
77)
88
9+ type chatMessageContent struct {
10+ Text * string
11+ Parts []ChatContentPart
12+ }
13+
914// ChatCompletionsToResponses converts a Chat Completions request into a
1015// Responses API request. The upstream always streams, so Stream is forced to
1116// true. store is always false and reasoning.encrypted_content is always
@@ -113,11 +118,11 @@ func chatMessageToResponsesItems(m ChatMessage) ([]ResponsesInputItem, error) {
113118
114119// chatSystemToResponses converts a system message.
115120func chatSystemToResponses (m ChatMessage ) ([]ResponsesInputItem , error ) {
116- text , err := parseChatContent (m .Content )
121+ parsed , err := parseChatMessageContent (m .Content )
117122 if err != nil {
118123 return nil , err
119124 }
120- content , err := json . Marshal ( text )
125+ content , err := marshalChatInputContent ( parsed )
121126 if err != nil {
122127 return nil , err
123128 }
@@ -127,39 +132,11 @@ func chatSystemToResponses(m ChatMessage) ([]ResponsesInputItem, error) {
127132// chatUserToResponses converts a user message, handling both plain strings and
128133// multi-modal content arrays.
129134func chatUserToResponses (m ChatMessage ) ([]ResponsesInputItem , error ) {
130- // Try plain string first.
131- var s string
132- if err := json .Unmarshal (m .Content , & s ); err == nil {
133- content , _ := json .Marshal (s )
134- return []ResponsesInputItem {{Role : "user" , Content : content }}, nil
135- }
136-
137- var parts []ChatContentPart
138- if err := json .Unmarshal (m .Content , & parts ); err != nil {
135+ parsed , err := parseChatMessageContent (m .Content )
136+ if err != nil {
139137 return nil , fmt .Errorf ("parse user content: %w" , err )
140138 }
141-
142- var responseParts []ResponsesContentPart
143- for _ , p := range parts {
144- switch p .Type {
145- case "text" :
146- if p .Text != "" {
147- responseParts = append (responseParts , ResponsesContentPart {
148- Type : "input_text" ,
149- Text : p .Text ,
150- })
151- }
152- case "image_url" :
153- if p .ImageURL != nil && p .ImageURL .URL != "" {
154- responseParts = append (responseParts , ResponsesContentPart {
155- Type : "input_image" ,
156- ImageURL : p .ImageURL .URL ,
157- })
158- }
159- }
160- }
161-
162- content , err := json .Marshal (responseParts )
139+ content , err := marshalChatInputContent (parsed )
163140 if err != nil {
164141 return nil , err
165142 }
@@ -312,16 +289,79 @@ func chatFunctionToResponses(m ChatMessage) ([]ResponsesInputItem, error) {
312289}
313290
314291// parseChatContent returns the string value of a ChatMessage Content field.
315- // Content must be a JSON string. Returns "" if content is null or empty.
292+ // Content can be a JSON string or an array of typed parts. Array content is
293+ // flattened to text by concatenating text parts and ignoring non-text parts.
316294func parseChatContent (raw json.RawMessage ) (string , error ) {
295+ parsed , err := parseChatMessageContent (raw )
296+ if err != nil {
297+ return "" , err
298+ }
299+ if parsed .Text != nil {
300+ return * parsed .Text , nil
301+ }
302+ return flattenChatContentParts (parsed .Parts ), nil
303+ }
304+
305+ func parseChatMessageContent (raw json.RawMessage ) (chatMessageContent , error ) {
317306 if len (raw ) == 0 {
318- return "" , nil
307+ return chatMessageContent { Text : stringPtr ( "" )} , nil
319308 }
309+
320310 var s string
321- if err := json .Unmarshal (raw , & s ); err ! = nil {
322- return "" , fmt . Errorf ( "parse content as string: %w" , err )
311+ if err := json .Unmarshal (raw , & s ); err = = nil {
312+ return chatMessageContent { Text : & s }, nil
323313 }
324- return s , nil
314+
315+ var parts []ChatContentPart
316+ if err := json .Unmarshal (raw , & parts ); err == nil {
317+ return chatMessageContent {Parts : parts }, nil
318+ }
319+
320+ return chatMessageContent {}, fmt .Errorf ("parse content as string or parts array" )
321+ }
322+
323+ func marshalChatInputContent (content chatMessageContent ) (json.RawMessage , error ) {
324+ if content .Text != nil {
325+ return json .Marshal (* content .Text )
326+ }
327+ return json .Marshal (convertChatContentPartsToResponses (content .Parts ))
328+ }
329+
330+ func convertChatContentPartsToResponses (parts []ChatContentPart ) []ResponsesContentPart {
331+ var responseParts []ResponsesContentPart
332+ for _ , p := range parts {
333+ switch p .Type {
334+ case "text" :
335+ if p .Text != "" {
336+ responseParts = append (responseParts , ResponsesContentPart {
337+ Type : "input_text" ,
338+ Text : p .Text ,
339+ })
340+ }
341+ case "image_url" :
342+ if p .ImageURL != nil && p .ImageURL .URL != "" {
343+ responseParts = append (responseParts , ResponsesContentPart {
344+ Type : "input_image" ,
345+ ImageURL : p .ImageURL .URL ,
346+ })
347+ }
348+ }
349+ }
350+ return responseParts
351+ }
352+
353+ func flattenChatContentParts (parts []ChatContentPart ) string {
354+ var textParts []string
355+ for _ , p := range parts {
356+ if p .Type == "text" && p .Text != "" {
357+ textParts = append (textParts , p .Text )
358+ }
359+ }
360+ return strings .Join (textParts , "" )
361+ }
362+
363+ func stringPtr (s string ) * string {
364+ return & s
325365}
326366
327367// convertChatToolsToResponses maps Chat Completions tool definitions and legacy
0 commit comments