Repository: envoyproxy/ai-gateway
Pull request #486 (2025-03-13 04:05:11)

feat:  openai: allow assistant content to be string
**Commit Message**

Content type should be able to be string so adding this for compatibility. It's already done for the other types - keeping it consistent.

mathetake: LGTM from me

Patch:
diff --git a/internal/apischema/openai/openai.go b/internal/apischema/openai/openai.go
index c8600ed..3065952 100644
--- a/internal/apischema/openai/openai.go
+++ b/internal/apischema/openai/openai.go
@@ -10,6 +10,7 @@ package openai
 
 import (
 	"encoding/json"
+	"errors"
 	"fmt"
 	"strconv"
 	"strings"
@@ -149,6 +150,28 @@ func (c *ChatCompletionContentPartUserUnionParam) UnmarshalJSON(data []byte) err
 	return nil
 }
 
+type StringOrAssistantRoleContentUnion struct {
+	Value interface{}
+}
+
+func (s *StringOrAssistantRoleContentUnion) UnmarshalJSON(data []byte) error {
+	var str string
+	err := json.Unmarshal(data, &str)
+	if err == nil {
+		s.Value = str
+		return nil
+	}
+
+	var content ChatCompletionAssistantMessageParamContent
+	err = json.Unmarshal(data, &content)
+	if err == nil {
+		s.Value = content
+		return nil
+	}
+
+	return errors.New("cannot unmarshal JSON data as string or assistant content parts")
+}
+
 type StringOrArray struct {
 	Value interface{}
 }
@@ -335,7 +358,7 @@ type ChatCompletionAssistantMessageParam struct {
 	Audio ChatCompletionAssistantMessageParamAudio `json:"audio,omitempty"`
 	// The contents of the assistant message. Required unless `tool_calls` or
 	// `function_call` is specified.
-	Content ChatCompletionAssistantMessageParamContent `json:"content"`
+	Content StringOrAssistantRoleContentUnion `json:"content"`
 	// An optional name for the participant. Provides the model information to
 	// differentiate between participants of the same role.
 	Name string `json:"name,omitempty"`
diff --git a/internal/extproc/translator/openai_awsbedrock.go b/internal/extproc/translator/openai_awsbedrock.go
index b007953..0a255af 100644
--- a/internal/extproc/translator/openai_awsbedrock.go
+++ b/internal/extproc/translator/openai_awsbedrock.go
@@ -245,11 +245,14 @@ func (o *openAIToAWSBedrockTranslatorV1ChatCompletion) openAIMessageToBedrockMes
 ) (*awsbedrock.Message, error) {
 	var bedrockMessage *awsbedrock.Message
 	contentBlocks := make([]*awsbedrock.ContentBlock, 0)
-	if openAiMessage.Content.Type == openai.ChatCompletionAssistantMessageParamContentTypeRefusal {
-		contentBlocks = append(contentBlocks, &awsbedrock.ContentBlock{Text: openAiMessage.Content.Refusal})
-	} else if openAiMessage.Content.Text != nil {
-		// TODO: we are sometimes missing the content (should fix)
-		contentBlocks = append(contentBlocks, &awsbedrock.ContentBlock{Text: openAiMessage.Content.Text})
+	if v, ok := openAiMessage.Content.Value.(string); ok {
+		contentBlocks = append(contentBlocks, &awsbedrock.ContentBlock{Text: &v})
+	} else if content, ok := openAiMessage.Content.Value.(openai.ChatCompletionAssistantMessageParamContent); ok {
+		if content.Type == openai.ChatCompletionAssistantMessageParamContentTypeRefusal {
+			contentBlocks = append(contentBlocks, &awsbedrock.ContentBlock{Text: content.Refusal})
+		} else if content.Text != nil {
+			contentBlocks = append(contentBlocks, &awsbedrock.ContentBlock{Text: content.Text})
+		}
 	}
 	bedrockMessage = &awsbedrock.Message{
 		Role:    role,


Test patch:
diff --git a/internal/apischema/openai/openai_test.go b/internal/apischema/openai/openai_test.go
index ed0fd83..e0b621d 100644
--- a/internal/apischema/openai/openai_test.go
+++ b/internal/apischema/openai/openai_test.go
@@ -153,7 +153,35 @@ func TestOpenAIChatCompletionMessageUnmarshal(t *testing.T) {
 					{
 						Value: ChatCompletionAssistantMessageParam{
 							Role:    ChatMessageRoleAssistant,
-							Content: ChatCompletionAssistantMessageParamContent{Text: ptr.To("you are a helpful assistant")},
+							Content: StringOrAssistantRoleContentUnion{Value: ChatCompletionAssistantMessageParamContent{Text: ptr.To("you are a helpful assistant")}},
+						},
+						Type: ChatMessageRoleAssistant,
+					},
+				},
+			},
+		},
+		{
+			name: "assistant message string",
+			in: []byte(`{"model": "gpu-o4",
+                        "messages": [
+                         {"role": "assistant", "content": "you are a helpful assistant"},
+                         {"role": "assistant", "content": "{'text': 'you are a helpful assistant'}"}
+						 ]}
+`),
+			out: &ChatCompletionRequest{
+				Model: "gpu-o4",
+				Messages: []ChatCompletionMessageParamUnion{
+					{
+						Value: ChatCompletionAssistantMessageParam{
+							Role:    ChatMessageRoleAssistant,
+							Content: StringOrAssistantRoleContentUnion{Value: "you are a helpful assistant"},
+						},
+						Type: ChatMessageRoleAssistant,
+					},
+					{
+						Value: ChatCompletionAssistantMessageParam{
+							Role:    ChatMessageRoleAssistant,
+							Content: StringOrAssistantRoleContentUnion{Value: "{'text': 'you are a helpful assistant'}"},
 						},
 						Type: ChatMessageRoleAssistant,
 					},
diff --git a/internal/extproc/translator/openai_awsbedrock_test.go b/internal/extproc/translator/openai_awsbedrock_test.go
index b39b5a2..313c3eb 100644
--- a/internal/extproc/translator/openai_awsbedrock_test.go
+++ b/internal/extproc/translator/openai_awsbedrock_test.go
@@ -82,8 +82,11 @@ func TestOpenAIToAWSBedrockTranslatorV1ChatCompletion_RequestBody(t *testing.T)
 					},
 					{
 						Value: openai.ChatCompletionAssistantMessageParam{
-							Content: openai.ChatCompletionAssistantMessageParamContent{
-								Text: ptr.To("I dunno"),
+							Content: openai.StringOrAssistantRoleContentUnion{
+								Value: openai.ChatCompletionAssistantMessageParamContent{
+									Type: openai.ChatCompletionAssistantMessageParamContentTypeText,
+									Text: ptr.To("I dunno"),
+								},
 							},
 							ToolCalls: []openai.ChatCompletionMessageToolCallParam{
 								{
@@ -97,6 +100,13 @@ func TestOpenAIToAWSBedrockTranslatorV1ChatCompletion_RequestBody(t *testing.T)
 							},
 						}, Type: openai.ChatMessageRoleAssistant,
 					},
+					{
+						Value: openai.ChatCompletionAssistantMessageParam{
+							Content: openai.StringOrAssistantRoleContentUnion{
+								Value: "I also dunno",
+							},
+						}, Type: openai.ChatMessageRoleAssistant,
+					},
 				},
 			},
 			output: awsbedrock.ConverseInput{
@@ -164,6 +174,14 @@ func TestOpenAIToAWSBedrockTranslatorV1ChatCompletion_RequestBody(t *testing.T)
 							},
 						},
 					},
+					{
+						Role: openai.ChatMessageRoleAssistant,
+						Content: []*awsbedrock.ContentBlock{
+							{
+								Text: ptr.To("I also dunno"),
+							},
+						},
+					},
 				},
 			},
 		},