Skip to content

Commit 72a8143

Browse files
authored
fix: preserve image_url parts in OpenAI converter on message retrieval (#357)
The OpenAI converter silently dropped image parts when retrieving multimodal messages. URL-based images (stored in Part.Meta["url"]) were lost because the converter only checked GetAssetURL() which returns empty for non-uploaded assets. Added Meta URL fallback matching the Anthropic and Gemini converters. Also fixed GetAssetURL key mismatch: lookup used asset.S3Key but the publicURLs map is keyed by asset.SHA256. Made-with: Cursor
1 parent d9aeef8 commit 72a8143

File tree

4 files changed

+133
-1
lines changed

4 files changed

+133
-1
lines changed

src/server/api/go/internal/pkg/converter/converter_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,41 @@ func TestGetConvertedMessagesOutput_MixedMetas(t *testing.T) {
585585
assert.Equal(t, map[string]interface{}{}, result.Metas[2])
586586
}
587587

588+
func TestGetAssetURL_LookupBySHA256(t *testing.T) {
589+
t.Run("resolves by SHA256 key", func(t *testing.T) {
590+
asset := &model.Asset{
591+
S3Key: "parts/project-id/abc.json",
592+
SHA256: "deadbeef1234",
593+
}
594+
publicURLs := map[string]service.PublicURL{
595+
"deadbeef1234": {URL: "https://cdn.example.com/signed-url"},
596+
}
597+
598+
result := GetAssetURL(asset, publicURLs)
599+
assert.Equal(t, "https://cdn.example.com/signed-url", result)
600+
})
601+
602+
t.Run("nil asset returns empty", func(t *testing.T) {
603+
result := GetAssetURL(nil, map[string]service.PublicURL{
604+
"key": {URL: "https://example.com"},
605+
})
606+
assert.Equal(t, "", result)
607+
})
608+
609+
t.Run("missing SHA256 in map returns empty", func(t *testing.T) {
610+
asset := &model.Asset{
611+
S3Key: "parts/project-id/abc.json",
612+
SHA256: "not-in-map",
613+
}
614+
publicURLs := map[string]service.PublicURL{
615+
"deadbeef1234": {URL: "https://cdn.example.com/signed-url"},
616+
}
617+
618+
result := GetAssetURL(asset, publicURLs)
619+
assert.Equal(t, "", result)
620+
})
621+
}
622+
588623
func TestGetConvertedMessagesOutput_EmptyMessages_HasEmptyMetas(t *testing.T) {
589624
messages := []model.Message{}
590625

src/server/api/go/internal/pkg/converter/openai.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ func (c *OpenAIConverter) convertToUserMessage(msg model.Message, publicURLs map
6666
contentParts = append(contentParts, openai.TextContentPart(part.Text))
6767
case model.PartTypeImage:
6868
imageURL := GetAssetURL(part.Asset, publicURLs)
69+
if imageURL == "" && part.Meta != nil {
70+
if url := part.GetMetaString(model.MetaKeyURL); url != "" {
71+
imageURL = url
72+
}
73+
}
6974
if imageURL != "" {
7075
detail := part.GetMetaString(model.MetaKeyDetail)
7176
imgParam := openai.ChatCompletionContentPartImageImageURLParam{

src/server/api/go/internal/pkg/converter/openai_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,98 @@ func TestOpenAIConverter_Convert_ThinkingDowngradedToText(t *testing.T) {
111111
})
112112
}
113113

114+
func TestOpenAIConverter_Convert_ImagePartFromMetaURL(t *testing.T) {
115+
converter := &OpenAIConverter{}
116+
117+
t.Run("image with external URL in meta", func(t *testing.T) {
118+
messages := []model.Message{
119+
createTestMessage(model.RoleUser, []model.Part{
120+
{Type: model.PartTypeText, Text: "What is in this image?"},
121+
{
122+
Type: model.PartTypeImage,
123+
Meta: map[string]any{
124+
model.MetaKeyURL: "https://example.com/cat.png",
125+
model.MetaKeyDetail: "high",
126+
},
127+
},
128+
}, nil),
129+
}
130+
131+
result, err := converter.Convert(messages, nil)
132+
require.NoError(t, err)
133+
134+
msgs := result.([]openai.ChatCompletionMessageParamUnion)
135+
require.Len(t, msgs, 1)
136+
137+
user := msgs[0].OfUser
138+
require.NotNil(t, user)
139+
140+
parts := user.Content.OfArrayOfContentParts
141+
require.Len(t, parts, 2, "should have text + image parts")
142+
143+
assert.NotNil(t, parts[0].OfText)
144+
assert.Equal(t, "What is in this image?", parts[0].OfText.Text)
145+
146+
assert.NotNil(t, parts[1].OfImageURL)
147+
assert.Equal(t, "https://example.com/cat.png", parts[1].OfImageURL.ImageURL.URL)
148+
assert.Equal(t, "high", parts[1].OfImageURL.ImageURL.Detail)
149+
})
150+
151+
t.Run("image with data URL in meta", func(t *testing.T) {
152+
dataURL := "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
153+
messages := []model.Message{
154+
createTestMessage(model.RoleUser, []model.Part{
155+
{
156+
Type: model.PartTypeImage,
157+
Meta: map[string]any{
158+
model.MetaKeyURL: dataURL,
159+
model.MetaKeyDetail: "low",
160+
},
161+
},
162+
}, nil),
163+
}
164+
165+
result, err := converter.Convert(messages, nil)
166+
require.NoError(t, err)
167+
168+
msgs := result.([]openai.ChatCompletionMessageParamUnion)
169+
require.Len(t, msgs, 1)
170+
171+
user := msgs[0].OfUser
172+
require.NotNil(t, user)
173+
174+
parts := user.Content.OfArrayOfContentParts
175+
require.Len(t, parts, 1, "should have image part")
176+
177+
assert.NotNil(t, parts[0].OfImageURL)
178+
assert.Equal(t, dataURL, parts[0].OfImageURL.ImageURL.URL)
179+
assert.Equal(t, "low", parts[0].OfImageURL.ImageURL.Detail)
180+
})
181+
182+
t.Run("image with nil asset and no meta URL is skipped", func(t *testing.T) {
183+
messages := []model.Message{
184+
createTestMessage(model.RoleUser, []model.Part{
185+
{Type: model.PartTypeText, Text: "Hello"},
186+
{Type: model.PartTypeImage, Meta: map[string]any{}},
187+
}, nil),
188+
}
189+
190+
result, err := converter.Convert(messages, nil)
191+
require.NoError(t, err)
192+
193+
msgs := result.([]openai.ChatCompletionMessageParamUnion)
194+
require.Len(t, msgs, 1)
195+
196+
user := msgs[0].OfUser
197+
require.NotNil(t, user)
198+
199+
parts := user.Content.OfArrayOfContentParts
200+
require.Len(t, parts, 1, "empty image should be skipped, leaving only text")
201+
assert.NotNil(t, parts[0].OfText)
202+
assert.Equal(t, "Hello", parts[0].OfText.Text)
203+
})
204+
}
205+
114206
func TestOpenAIConverter_Convert_ToolResult(t *testing.T) {
115207
converter := &OpenAIConverter{}
116208

src/server/api/go/internal/pkg/converter/utils.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func GetAssetURL(asset *model.Asset, publicURLs map[string]service.PublicURL) st
2323
if asset == nil {
2424
return ""
2525
}
26-
if publicURL, ok := publicURLs[asset.S3Key]; ok {
26+
if publicURL, ok := publicURLs[asset.SHA256]; ok {
2727
return publicURL.URL
2828
}
2929
return ""

0 commit comments

Comments
 (0)