Initial project import
This commit is contained in:
@@ -0,0 +1,643 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from unittest import mock
|
||||
import json
|
||||
import base64
|
||||
|
||||
from services.config import config
|
||||
from services.protocol import openai_v1_chat_complete, openai_v1_response
|
||||
from services.protocol.chat_completion_cache import cache_key, chat_completion_cache
|
||||
from services.protocol.conversation import iter_conversation_payloads, sanitize_output_text
|
||||
from utils.helper import extract_image_from_message_content
|
||||
|
||||
|
||||
PNG_1X1 = base64.b64decode(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADUlEQVR4nGP8z8BQDwAFgwJ/luzl4wAAAABJRU5ErkJggg=="
|
||||
)
|
||||
PNG_1X1_DATA_URL = "data:image/png;base64," + base64.b64encode(PNG_1X1).decode("ascii")
|
||||
|
||||
|
||||
class ChatCompletionCacheTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.old_cache_settings = config.data.get("chat_completion_cache")
|
||||
config.data["chat_completion_cache"] = {
|
||||
"enabled": True,
|
||||
"ttl_seconds": 60,
|
||||
"max_entries": 32,
|
||||
"dedupe_inflight": True,
|
||||
"stream_cache": True,
|
||||
"normalize_messages": True,
|
||||
"drop_adjacent_duplicates": True,
|
||||
"drop_assistant_history": False,
|
||||
}
|
||||
chat_completion_cache.clear()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
if self.old_cache_settings is None:
|
||||
config.data.pop("chat_completion_cache", None)
|
||||
else:
|
||||
config.data["chat_completion_cache"] = self.old_cache_settings
|
||||
chat_completion_cache.clear()
|
||||
|
||||
def test_repeated_non_stream_text_completion_uses_cache(self) -> None:
|
||||
calls = 0
|
||||
|
||||
def fake_collect_text(_backend, _request):
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
return f"cached answer {calls}"
|
||||
|
||||
body = {
|
||||
"model": "auto",
|
||||
"messages": [{"role": "user", "content": "cache this exact prompt"}],
|
||||
}
|
||||
|
||||
with (
|
||||
mock.patch("services.protocol.openai_v1_chat_complete.text_backend", return_value=object()),
|
||||
mock.patch("services.protocol.openai_v1_chat_complete.collect_text", side_effect=fake_collect_text),
|
||||
):
|
||||
first = openai_v1_chat_complete.handle(body)
|
||||
second = openai_v1_chat_complete.handle(body)
|
||||
|
||||
self.assertEqual(calls, 1)
|
||||
self.assertEqual(
|
||||
first["choices"][0]["message"]["content"],
|
||||
second["choices"][0]["message"]["content"],
|
||||
)
|
||||
|
||||
def test_cache_key_distinguishes_thinking_effort_inputs(self) -> None:
|
||||
messages = [{"role": "user", "content": "same prompt"}]
|
||||
base = {"model": "auto", "messages": messages}
|
||||
|
||||
default_key = cache_key(base, messages, stream=False)
|
||||
thinking_key = cache_key({**base, "thinking_effort": "high"}, messages, stream=False)
|
||||
reasoning_key = cache_key({**base, "reasoning": {"effort": "high"}}, messages, stream=False)
|
||||
|
||||
self.assertNotEqual(default_key, thinking_key)
|
||||
self.assertNotEqual(default_key, reasoning_key)
|
||||
self.assertNotEqual(thinking_key, reasoning_key)
|
||||
|
||||
def test_chat_completion_reasoning_effort_reaches_conversation_request(self) -> None:
|
||||
captured_efforts: list[str] = []
|
||||
|
||||
def fake_collect_text(_backend, request):
|
||||
captured_efforts.append(request.thinking_effort)
|
||||
return "ok"
|
||||
|
||||
body = {
|
||||
"model": "auto",
|
||||
"reasoning_effort": "xhigh",
|
||||
"messages": [{"role": "user", "content": "use more reasoning"}],
|
||||
}
|
||||
|
||||
with (
|
||||
mock.patch("services.protocol.openai_v1_chat_complete.text_backend", return_value=object()),
|
||||
mock.patch("services.protocol.openai_v1_chat_complete.collect_text", side_effect=fake_collect_text),
|
||||
):
|
||||
openai_v1_chat_complete.handle(body)
|
||||
|
||||
self.assertEqual(captured_efforts, ["extended"])
|
||||
|
||||
def test_responses_reasoning_effort_reaches_conversation_request(self) -> None:
|
||||
captured_efforts: list[str] = []
|
||||
|
||||
def fake_stream_text_deltas(_backend, request):
|
||||
captured_efforts.append(request.thinking_effort)
|
||||
yield "ok"
|
||||
|
||||
body = {
|
||||
"model": "auto",
|
||||
"input": "use more reasoning",
|
||||
"reasoning": {"effort": "xhigh"},
|
||||
}
|
||||
|
||||
with (
|
||||
mock.patch("services.protocol.openai_v1_response.text_backend", return_value=object()),
|
||||
mock.patch("services.protocol.openai_v1_response.stream_text_deltas", side_effect=fake_stream_text_deltas),
|
||||
):
|
||||
openai_v1_response.handle(body)
|
||||
|
||||
self.assertEqual(captured_efforts, ["extended"])
|
||||
|
||||
def test_repeated_stream_text_completion_replays_cached_chunks(self) -> None:
|
||||
calls = 0
|
||||
|
||||
def fake_stream_text_deltas(_backend, _request):
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
yield "streamed"
|
||||
yield " answer"
|
||||
|
||||
body = {
|
||||
"model": "auto",
|
||||
"stream": True,
|
||||
"messages": [{"role": "user", "content": "stream cache this exact prompt"}],
|
||||
}
|
||||
|
||||
with (
|
||||
mock.patch("services.protocol.openai_v1_chat_complete.text_backend", return_value=object()),
|
||||
mock.patch(
|
||||
"services.protocol.openai_v1_chat_complete.stream_text_deltas",
|
||||
side_effect=fake_stream_text_deltas,
|
||||
),
|
||||
):
|
||||
first = list(openai_v1_chat_complete.handle(body))
|
||||
second = list(openai_v1_chat_complete.handle(body))
|
||||
|
||||
self.assertEqual(calls, 1)
|
||||
self.assertEqual(first, second)
|
||||
content = "".join(str(chunk["choices"][0]["delta"].get("content") or "") for chunk in second)
|
||||
self.assertEqual(content, "streamed answer")
|
||||
|
||||
def test_adjacent_duplicate_messages_are_removed_before_upstream_call(self) -> None:
|
||||
captured_messages = []
|
||||
|
||||
def fake_collect_text(_backend, request):
|
||||
captured_messages.extend(request.messages or [])
|
||||
return "ok"
|
||||
|
||||
body = {
|
||||
"model": "auto",
|
||||
"messages": [
|
||||
{"role": "user", "content": "repeat me"},
|
||||
{"role": "user", "content": "repeat me"},
|
||||
{"role": "assistant", "content": "old answer"},
|
||||
{"role": "user", "content": "next prompt"},
|
||||
],
|
||||
}
|
||||
|
||||
with (
|
||||
mock.patch("services.protocol.openai_v1_chat_complete.text_backend", return_value=object()),
|
||||
mock.patch("services.protocol.openai_v1_chat_complete.collect_text", side_effect=fake_collect_text),
|
||||
):
|
||||
openai_v1_chat_complete.handle(body)
|
||||
|
||||
self.assertEqual(
|
||||
captured_messages,
|
||||
[
|
||||
{"role": "user", "content": "repeat me"},
|
||||
{"role": "assistant", "content": "old answer"},
|
||||
{"role": "user", "content": "next prompt"},
|
||||
],
|
||||
)
|
||||
|
||||
def test_chat_completion_usage_includes_cached_tokens(self) -> None:
|
||||
with (
|
||||
mock.patch("services.protocol.openai_v1_chat_complete.text_backend", return_value=object()),
|
||||
mock.patch("services.protocol.openai_v1_chat_complete.collect_text", return_value="ok"),
|
||||
):
|
||||
response = openai_v1_chat_complete.handle({
|
||||
"model": "auto",
|
||||
"messages": [{"role": "user", "content": "usage shape"}],
|
||||
})
|
||||
|
||||
details = response["usage"]["prompt_tokens_details"]
|
||||
self.assertEqual(details["cached_tokens"], 0)
|
||||
output_details = response["usage"]["completion_tokens_details"]
|
||||
self.assertEqual(output_details["reasoning_tokens"], 0)
|
||||
|
||||
def test_responses_completed_usage_includes_cached_tokens(self) -> None:
|
||||
with (
|
||||
mock.patch("services.protocol.openai_v1_response.text_backend", return_value=object()),
|
||||
mock.patch("services.protocol.openai_v1_response.stream_text_deltas", return_value=iter(["ok"])),
|
||||
):
|
||||
response = openai_v1_response.handle({
|
||||
"model": "auto",
|
||||
"input": "usage shape",
|
||||
})
|
||||
|
||||
details = response["usage"]["input_tokens_details"]
|
||||
self.assertEqual(details["cached_tokens"], 0)
|
||||
output_details = response["usage"]["output_tokens_details"]
|
||||
self.assertEqual(output_details["reasoning_tokens"], 0)
|
||||
|
||||
def test_repeated_responses_text_request_uses_cache(self) -> None:
|
||||
calls = 0
|
||||
|
||||
def fake_stream_text_deltas(_backend, _request):
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
yield f"response cache {calls}"
|
||||
|
||||
body = {
|
||||
"model": "auto",
|
||||
"input": "cache this responses prompt",
|
||||
"stream": True,
|
||||
}
|
||||
|
||||
with (
|
||||
mock.patch("services.protocol.openai_v1_response.text_backend", return_value=object()),
|
||||
mock.patch("services.protocol.openai_v1_response.stream_text_deltas", side_effect=fake_stream_text_deltas),
|
||||
):
|
||||
first = list(openai_v1_response.handle(body))
|
||||
second = list(openai_v1_response.handle(body))
|
||||
|
||||
self.assertEqual(calls, 1)
|
||||
self.assertEqual(first, second)
|
||||
|
||||
def test_output_sanitizer_removes_chatgpt_annotation_markup(self) -> None:
|
||||
text = (
|
||||
"Repo: \ue200url\ue202basketikun/chatgpt2api"
|
||||
"\ue202https://github.com/basketikun/chatgpt2api\ue201 "
|
||||
"details \ue200cite\ue202turn0search0\ue201."
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
sanitize_output_text(text),
|
||||
"Repo: basketikun/chatgpt2api (https://github.com/basketikun/chatgpt2api) details.",
|
||||
)
|
||||
|
||||
def test_output_sanitizer_preserves_annotated_entity_text(self) -> None:
|
||||
text = (
|
||||
"The character is from \ue200entity\ue202Invincible\ue201, "
|
||||
"which is based on the comic series \ue200entity\ue202Invincible\ue201."
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
sanitize_output_text(text),
|
||||
"The character is from Invincible, which is based on the comic series Invincible.",
|
||||
)
|
||||
|
||||
def test_output_sanitizer_preserves_readable_cite_label(self) -> None:
|
||||
text = "The character is \ue200cite\ue202Invincible\ue202turn0search0\ue201."
|
||||
|
||||
self.assertEqual(sanitize_output_text(text), "The character is Invincible.")
|
||||
|
||||
def test_output_sanitizer_preserves_code_spaces_before_punctuation(self) -> None:
|
||||
self.assertEqual(sanitize_output_text("find ."), "find .")
|
||||
self.assertEqual(sanitize_output_text("if ! test -f x; then"), "if ! test -f x; then")
|
||||
|
||||
def test_stream_sanitizer_does_not_emit_partial_annotation_or_repeat_prefix(self) -> None:
|
||||
events = [
|
||||
{"p": "/message/content/parts/0", "o": "append", "v": "Repo: \ue200url\ue202chat"},
|
||||
{"p": "/message/content/parts/0", "o": "append", "v": "gpt2api\ue202turn0search0\ue201 done \ue200cite\ue202turn0\ue201."},
|
||||
"[DONE]",
|
||||
]
|
||||
payloads = [json.dumps(event, ensure_ascii=False) if isinstance(event, dict) else event for event in events]
|
||||
deltas = [
|
||||
str(event.get("delta") or "")
|
||||
for event in iter_conversation_payloads(iter(payloads))
|
||||
if event.get("type") == "conversation.delta"
|
||||
]
|
||||
|
||||
self.assertEqual("".join(deltas), "Repo: chatgpt2api done.")
|
||||
self.assertFalse(any("\ue200" in delta or "\ue202" in delta or "\ue201" in delta for delta in deltas))
|
||||
|
||||
def test_stream_ignores_internal_tool_recipient_messages(self) -> None:
|
||||
events = [
|
||||
{
|
||||
"message": {
|
||||
"author": {"role": "assistant"},
|
||||
"content": {
|
||||
"content_type": "code",
|
||||
"text": 'search("\\u56fd\\u5bb6\\u6d77\\u6d0b")',
|
||||
},
|
||||
"recipient": "web",
|
||||
"status": "finished_successfully",
|
||||
},
|
||||
},
|
||||
{
|
||||
"message": {
|
||||
"author": {"name": "web.run", "role": "tool"},
|
||||
"content": {"content_type": "text", "parts": [""]},
|
||||
"recipient": "all",
|
||||
"status": "finished_successfully",
|
||||
},
|
||||
},
|
||||
{
|
||||
"v": {
|
||||
"message": {
|
||||
"author": {"role": "assistant"},
|
||||
"content": {
|
||||
"content_type": "code",
|
||||
"text": '{"aspect_ratio":"16:9","query":["museum"],"num_per_query":2}',
|
||||
},
|
||||
"recipient": "web",
|
||||
"status": "finished_successfully",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"message": {
|
||||
"author": {"role": "assistant"},
|
||||
"channel": "final",
|
||||
"content": {"content_type": "text", "parts": ["Final answer."]},
|
||||
"recipient": "all",
|
||||
"status": "finished_successfully",
|
||||
},
|
||||
},
|
||||
"[DONE]",
|
||||
]
|
||||
payloads = [json.dumps(event, ensure_ascii=False) if isinstance(event, dict) else event for event in events]
|
||||
deltas = [
|
||||
str(event.get("delta") or "")
|
||||
for event in iter_conversation_payloads(iter(payloads))
|
||||
if event.get("type") == "conversation.delta"
|
||||
]
|
||||
|
||||
self.assertEqual("".join(deltas), "Final answer.")
|
||||
|
||||
def test_stream_ignores_hidden_and_non_final_assistant_messages(self) -> None:
|
||||
events = [
|
||||
{
|
||||
"message": {
|
||||
"author": {"role": "assistant"},
|
||||
"channel": "final",
|
||||
"content": {"content_type": "text", "parts": ["Hidden text."]},
|
||||
"metadata": {"is_visually_hidden_from_conversation": True},
|
||||
"recipient": "all",
|
||||
},
|
||||
},
|
||||
{
|
||||
"message": {
|
||||
"author": {"role": "assistant"},
|
||||
"channel": "analysis",
|
||||
"content": {"content_type": "text", "parts": ["Private reasoning."]},
|
||||
"recipient": "all",
|
||||
},
|
||||
},
|
||||
{
|
||||
"message": {
|
||||
"author": {"role": "assistant"},
|
||||
"channel": "final",
|
||||
"content": {"content_type": "text", "parts": ["Visible text."]},
|
||||
"recipient": "all",
|
||||
},
|
||||
},
|
||||
"[DONE]",
|
||||
]
|
||||
payloads = [json.dumps(event, ensure_ascii=False) if isinstance(event, dict) else event for event in events]
|
||||
deltas = [
|
||||
str(event.get("delta") or "")
|
||||
for event in iter_conversation_payloads(iter(payloads))
|
||||
if event.get("type") == "conversation.delta"
|
||||
]
|
||||
|
||||
self.assertEqual("".join(deltas), "Visible text.")
|
||||
|
||||
def test_stream_preserves_user_visible_code_messages(self) -> None:
|
||||
events = [
|
||||
{
|
||||
"message": {
|
||||
"author": {"role": "assistant"},
|
||||
"channel": "final",
|
||||
"content": {"content_type": "code", "text": 'print("hello")'},
|
||||
"recipient": "all",
|
||||
},
|
||||
},
|
||||
"[DONE]",
|
||||
]
|
||||
payloads = [json.dumps(event, ensure_ascii=False) if isinstance(event, dict) else event for event in events]
|
||||
deltas = [
|
||||
str(event.get("delta") or "")
|
||||
for event in iter_conversation_payloads(iter(payloads))
|
||||
if event.get("type") == "conversation.delta"
|
||||
]
|
||||
|
||||
self.assertEqual("".join(deltas), 'print("hello")')
|
||||
|
||||
def test_responses_tools_add_honest_no_tool_guard(self) -> None:
|
||||
model, messages = openai_v1_response.text_response_parts({
|
||||
"model": "auto",
|
||||
"input": "run echo hi",
|
||||
"tools": [{"type": "function", "name": "shell"}],
|
||||
})
|
||||
|
||||
self.assertEqual(model, "auto")
|
||||
self.assertEqual(messages[0]["role"], "system")
|
||||
self.assertIn("cannot execute local tools", str(messages[0]["content"]))
|
||||
|
||||
def test_responses_web_search_tool_returns_search_output(self) -> None:
|
||||
search_result = {
|
||||
"answer": "Latest answer.",
|
||||
"sources": [{"title": "Example", "url": "https://example.com/news", "snippet": "Snippet"}],
|
||||
}
|
||||
body = {
|
||||
"model": "auto",
|
||||
"input": "latest example news",
|
||||
"tools": [{"type": "web_search"}],
|
||||
}
|
||||
|
||||
with mock.patch("services.protocol.openai_v1_response.run_web_search", return_value=search_result) as search:
|
||||
response = openai_v1_response.handle(body)
|
||||
|
||||
search.assert_called_once_with("latest example news")
|
||||
self.assertEqual(response["output"][0]["type"], "web_search_call")
|
||||
self.assertEqual(response["output"][0]["status"], "completed")
|
||||
self.assertEqual(response["output"][0]["action"]["query"], "latest example news")
|
||||
message = response["output"][1]
|
||||
self.assertEqual(message["type"], "message")
|
||||
content = message["content"][0]
|
||||
self.assertIn("Latest answer.", content["text"])
|
||||
self.assertEqual(content["annotations"][0]["type"], "url_citation")
|
||||
self.assertEqual(content["annotations"][0]["url"], "https://example.com/news")
|
||||
|
||||
def test_responses_web_search_tool_streams_search_events(self) -> None:
|
||||
search_result = {
|
||||
"answer": "Streamed search answer.",
|
||||
"sources": [{"title": "Example", "url": "https://example.com/stream", "snippet": ""}],
|
||||
}
|
||||
body = {
|
||||
"model": "auto",
|
||||
"stream": True,
|
||||
"input": "stream search",
|
||||
"tools": [{"type": "web_search_preview"}],
|
||||
}
|
||||
|
||||
with mock.patch("services.protocol.openai_v1_response.run_web_search", return_value=search_result):
|
||||
events = list(openai_v1_response.handle(body))
|
||||
|
||||
event_types = [event["type"] for event in events]
|
||||
self.assertIn("response.web_search_call.in_progress", event_types)
|
||||
self.assertIn("response.web_search_call.searching", event_types)
|
||||
self.assertIn("response.web_search_call.completed", event_types)
|
||||
completed = events[-1]["response"]
|
||||
self.assertEqual(completed["output"][0]["type"], "web_search_call")
|
||||
self.assertEqual(completed["output"][1]["type"], "message")
|
||||
|
||||
def test_responses_versioned_web_search_tool_returns_search_output(self) -> None:
|
||||
search_result = {
|
||||
"answer": "Versioned search answer.",
|
||||
"sources": [{"title": "Example", "url": "https://example.com/versioned", "snippet": ""}],
|
||||
}
|
||||
body = {
|
||||
"model": "auto",
|
||||
"input": "versioned search",
|
||||
"tools": [{"type": "web_search_preview_2025_03_11"}],
|
||||
}
|
||||
|
||||
with mock.patch("services.protocol.openai_v1_response.run_web_search", return_value=search_result) as search:
|
||||
response = openai_v1_response.handle(body)
|
||||
|
||||
search.assert_called_once_with("versioned search")
|
||||
self.assertEqual(response["output"][0]["type"], "web_search_call")
|
||||
self.assertIn("Versioned search answer.", response["output"][1]["content"][0]["text"])
|
||||
|
||||
def test_chat_completions_web_search_tool_returns_search_answer(self) -> None:
|
||||
search_result = {
|
||||
"answer": "Chat search answer.",
|
||||
"sources": [{"title": "Example", "url": "https://example.com/chat", "snippet": ""}],
|
||||
}
|
||||
body = {
|
||||
"model": "auto",
|
||||
"messages": [{"role": "user", "content": "search chat"}],
|
||||
"tools": [{"type": "web_search"}],
|
||||
}
|
||||
|
||||
with mock.patch("services.protocol.openai_v1_chat_complete.run_web_search", return_value=search_result) as search:
|
||||
response = openai_v1_chat_complete.handle(body)
|
||||
|
||||
search.assert_called_once_with("search chat")
|
||||
message = response["choices"][0]["message"]
|
||||
self.assertIn("Chat search answer.", message["content"])
|
||||
self.assertEqual(message["annotations"][0]["type"], "url_citation")
|
||||
self.assertEqual(message["annotations"][0]["url_citation"]["url"], "https://example.com/chat")
|
||||
|
||||
def test_chat_completions_web_search_options_trigger_search(self) -> None:
|
||||
search_result = {
|
||||
"answer": "Options search answer.",
|
||||
"sources": [{"title": "Example", "url": "https://example.com/options", "snippet": ""}],
|
||||
}
|
||||
body = {
|
||||
"model": "auto",
|
||||
"messages": [{"role": "user", "content": "search options"}],
|
||||
"web_search_options": {"search_context_size": "low"},
|
||||
}
|
||||
|
||||
with mock.patch("services.protocol.openai_v1_chat_complete.run_web_search", return_value=search_result) as search:
|
||||
response = openai_v1_chat_complete.handle(body)
|
||||
|
||||
search.assert_called_once_with("search options")
|
||||
self.assertIn("Options search answer.", response["choices"][0]["message"]["content"])
|
||||
|
||||
def test_chat_completions_search_model_triggers_search(self) -> None:
|
||||
search_result = {
|
||||
"answer": "Search model answer.",
|
||||
"sources": [{"title": "Example", "url": "https://example.com/model", "snippet": ""}],
|
||||
}
|
||||
body = {
|
||||
"model": "gpt-5-search-api-2026-06-01",
|
||||
"messages": [{"role": "user", "content": "search model"}],
|
||||
}
|
||||
|
||||
with mock.patch("services.protocol.openai_v1_chat_complete.run_web_search", return_value=search_result) as search:
|
||||
response = openai_v1_chat_complete.handle(body)
|
||||
|
||||
search.assert_called_once_with("search model")
|
||||
self.assertEqual(response["model"], "gpt-5-search-api-2026-06-01")
|
||||
self.assertIn("Search model answer.", response["choices"][0]["message"]["content"])
|
||||
|
||||
def test_chat_completions_search_like_model_does_not_trigger_search(self) -> None:
|
||||
body = {
|
||||
"model": "gpt-5-search-apiary",
|
||||
"messages": [{"role": "user", "content": "not actually a search model"}],
|
||||
}
|
||||
|
||||
with (
|
||||
mock.patch("services.protocol.openai_v1_chat_complete.run_web_search") as search,
|
||||
mock.patch("services.protocol.openai_v1_chat_complete.text_backend", return_value=object()),
|
||||
mock.patch("services.protocol.openai_v1_chat_complete.collect_text", return_value="plain text answer"),
|
||||
):
|
||||
response = openai_v1_chat_complete.handle(body)
|
||||
|
||||
search.assert_not_called()
|
||||
self.assertIn("plain text answer", response["choices"][0]["message"]["content"])
|
||||
|
||||
def test_chat_completions_accepts_remote_image_url(self) -> None:
|
||||
class FakeImageResponse:
|
||||
status_code = 200
|
||||
headers = {"content-type": "image/png", "content-length": str(len(PNG_1X1))}
|
||||
content = PNG_1X1
|
||||
|
||||
with mock.patch("utils.helper.requests.get", return_value=FakeImageResponse()) as request_get:
|
||||
model, messages = openai_v1_chat_complete.text_chat_parts({
|
||||
"model": "auto",
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "Describe this"},
|
||||
{"type": "image_url", "image_url": {"url": "https://example.test/image.png"}},
|
||||
],
|
||||
}],
|
||||
})
|
||||
|
||||
request_get.assert_called_once()
|
||||
self.assertEqual(model, "auto")
|
||||
content = messages[0]["content"]
|
||||
self.assertEqual(content[0], {"type": "text", "text": "Describe this"})
|
||||
self.assertEqual(content[1]["type"], "image")
|
||||
self.assertEqual(content[1]["data"], PNG_1X1)
|
||||
self.assertEqual(content[1]["mime"], "image/png")
|
||||
|
||||
def test_responses_text_request_preserves_input_image(self) -> None:
|
||||
captured = {}
|
||||
|
||||
def fake_stream_text_deltas(_backend, request):
|
||||
captured["messages"] = request.messages
|
||||
yield "red"
|
||||
|
||||
body = {
|
||||
"model": "auto",
|
||||
"input": [
|
||||
{"type": "input_text", "text": "What color is this image?"},
|
||||
{"type": "input_image", "image_url": PNG_1X1_DATA_URL},
|
||||
],
|
||||
}
|
||||
|
||||
with (
|
||||
mock.patch("services.protocol.openai_v1_response.text_backend", return_value=object()),
|
||||
mock.patch("services.protocol.openai_v1_response.stream_text_deltas", side_effect=fake_stream_text_deltas),
|
||||
):
|
||||
response = openai_v1_response.handle(body)
|
||||
|
||||
self.assertEqual(response["output"][0]["content"][0]["text"], "red")
|
||||
content = captured["messages"][0]["content"]
|
||||
self.assertEqual(content[0], {"type": "text", "text": "What color is this image?"})
|
||||
self.assertEqual(content[1]["type"], "image")
|
||||
self.assertEqual(content[1]["mime"], "image/png")
|
||||
self.assertEqual(content[1]["data"], PNG_1X1)
|
||||
self.assertGreater(response["usage"]["input_tokens_details"]["image_tokens"], 0)
|
||||
|
||||
def test_responses_text_request_accepts_remote_input_image_url(self) -> None:
|
||||
class FakeImageResponse:
|
||||
status_code = 200
|
||||
headers = {"content-type": "image/png", "content-length": str(len(PNG_1X1))}
|
||||
content = PNG_1X1
|
||||
|
||||
with mock.patch("utils.helper.requests.get", return_value=FakeImageResponse()) as request_get:
|
||||
_model, messages = openai_v1_response.text_response_parts({
|
||||
"model": "auto",
|
||||
"input": [{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "input_text", "text": "Describe this"},
|
||||
{"type": "input_image", "image_url": {"url": "https://example.test/image.png"}},
|
||||
],
|
||||
}],
|
||||
})
|
||||
|
||||
request_get.assert_called_once()
|
||||
content = messages[0]["content"]
|
||||
self.assertEqual(content[0], {"type": "text", "text": "Describe this"})
|
||||
self.assertEqual(content[1]["type"], "image")
|
||||
self.assertEqual(content[1]["data"], PNG_1X1)
|
||||
self.assertEqual(content[1]["mime"], "image/png")
|
||||
|
||||
def test_image_extractor_supports_extra_image_object_shapes(self) -> None:
|
||||
encoded = base64.b64encode(PNG_1X1).decode("ascii")
|
||||
|
||||
images = extract_image_from_message_content([
|
||||
{"type": "image", "data": PNG_1X1, "mime": "image/png"},
|
||||
{"type": "input_image", "base64": encoded, "mime_type": "image/png"},
|
||||
{"type": "input_image", "source": {"type": "base64", "data": encoded, "media_type": "image/png"}},
|
||||
])
|
||||
|
||||
self.assertEqual(len(images), 3)
|
||||
self.assertEqual([image[1] for image in images], ["image/png", "image/png", "image/png"])
|
||||
self.assertTrue(all(image[0] == PNG_1X1 for image in images))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user