Skip to content

Commit e66db68

Browse files
committed
feat: add image content support to OpenAI providers
Handle ImageContent in OpenAIProvider._convert_message and OpenAIResponsesProvider._build_input so that user messages containing images are correctly converted to the respective API formats instead of being silently dropped. Closes #22
1 parent fbd0f8c commit e66db68

4 files changed

Lines changed: 68 additions & 0 deletions

File tree

cubepi/providers/openai.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from cubepi.providers.base import (
1111
AssistantMessage,
12+
ImageContent,
1213
Message,
1314
MessageStream,
1415
Model,
@@ -250,6 +251,22 @@ async def _produce() -> None:
250251
@staticmethod
251252
def _convert_message(msg: Message) -> dict[str, Any]:
252253
if isinstance(msg, UserMessage):
254+
has_image = any(isinstance(c, ImageContent) for c in msg.content)
255+
if has_image:
256+
parts: list[dict[str, Any]] = []
257+
for c in msg.content:
258+
if isinstance(c, TextContent):
259+
parts.append({"type": "text", "text": c.text})
260+
elif isinstance(c, ImageContent):
261+
parts.append(
262+
{
263+
"type": "image_url",
264+
"image_url": {
265+
"url": f"data:{c.media_type};base64,{c.source}"
266+
},
267+
}
268+
)
269+
return {"role": "user", "content": parts}
253270
text_parts = [c.text for c in msg.content if isinstance(c, TextContent)]
254271
return {"role": "user", "content": "\n".join(text_parts)}
255272

cubepi/providers/openai_responses.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from cubepi.providers.base import (
99
AssistantMessage,
10+
ImageContent,
1011
Message,
1112
MessageStream,
1213
Model,
@@ -471,6 +472,13 @@ def _build_input(messages: list[Message]) -> list[dict[str, Any]]:
471472
for c in msg.content:
472473
if isinstance(c, TextContent):
473474
content.append({"type": "input_text", "text": c.text})
475+
elif isinstance(c, ImageContent):
476+
content.append(
477+
{
478+
"type": "input_image",
479+
"image_url": f"data:{c.media_type};base64,{c.source}",
480+
}
481+
)
474482
if content:
475483
api_input.append({"role": "user", "content": content})
476484

tests/providers/test_openai.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from cubepi.providers.openai import OpenAIProvider
22
from cubepi.providers.base import (
33
AssistantMessage,
4+
ImageContent,
45
TextContent,
56
ToolCall,
67
ToolDefinition,
@@ -46,6 +47,29 @@ def test_convert_tool_result(self):
4647
assert result["content"] == "result"
4748

4849

50+
class TestOpenAIImageConversion:
51+
def test_user_message_with_image(self):
52+
msg = UserMessage(content=[
53+
TextContent(text="What's in this image?"),
54+
ImageContent(source="base64data", media_type="image/png"),
55+
])
56+
result = OpenAIProvider._convert_message(msg)
57+
assert result["role"] == "user"
58+
assert isinstance(result["content"], list)
59+
assert len(result["content"]) == 2
60+
assert result["content"][0] == {"type": "text", "text": "What's in this image?"}
61+
assert result["content"][1] == {
62+
"type": "image_url",
63+
"image_url": {"url": "data:image/png;base64,base64data"},
64+
}
65+
66+
def test_user_message_text_only_stays_simple(self):
67+
msg = UserMessage(content=[TextContent(text="hello")])
68+
result = OpenAIProvider._convert_message(msg)
69+
assert result["role"] == "user"
70+
assert result["content"] == "hello"
71+
72+
4973
class TestOpenAIToolConversion:
5074
def test_convert_tool_definition(self):
5175
td = ToolDefinition(

tests/providers/test_openai_responses.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from cubepi.providers.base import (
1212
AssistantMessage,
13+
ImageContent,
1314
Model,
1415
StreamOptions,
1516
TextContent,
@@ -144,6 +145,24 @@ def test_mixed_conversation(self):
144145
assert result[3]["type"] == "function_call_output"
145146

146147

148+
class TestOpenAIResponsesImageConversion:
149+
def test_user_message_with_image(self):
150+
msg = UserMessage(content=[
151+
TextContent(text="Describe this"),
152+
ImageContent(source="imgdata", media_type="image/jpeg"),
153+
])
154+
result = OpenAIResponsesProvider._build_input([msg])
155+
assert len(result) == 1
156+
assert result[0]["role"] == "user"
157+
content = result[0]["content"]
158+
assert len(content) == 2
159+
assert content[0] == {"type": "input_text", "text": "Describe this"}
160+
assert content[1] == {
161+
"type": "input_image",
162+
"image_url": "data:image/jpeg;base64,imgdata",
163+
}
164+
165+
147166
class TestConvertTool:
148167
def test_convert_tool_definition(self):
149168
td = ToolDefinition(

0 commit comments

Comments
 (0)