gpt-image-2 multi-image + fix: tools need reasoning_effort none on gpt-5.6 (ggg mute bug)
This commit is contained in:
@@ -102,33 +102,6 @@ You always try to say something positive about the current day and the Fjærkroa
|
||||
# Skip this test due to Mock iteration issues - functionality works in practice
|
||||
self.skipTest("Mock iteration issue - test works in real usage")
|
||||
|
||||
async def test_translate1(self) -> None:
|
||||
self.bot.airesponder.config["fix-model"] = "gpt-4o-mini"
|
||||
|
||||
# Mock translation responses
|
||||
def translation_side_effect(*args, **kwargs):
|
||||
mock_resp = Mock()
|
||||
mock_resp.choices = [Mock()]
|
||||
mock_resp.choices[0].message = Mock()
|
||||
|
||||
# Check the input text to return appropriate translation
|
||||
user_content = kwargs["messages"][1]["content"]
|
||||
if user_content == "Das ist ein komischer Text.":
|
||||
mock_resp.choices[0].message.content = "This is a strange text."
|
||||
elif user_content == "This is a strange text.":
|
||||
mock_resp.choices[0].message.content = "Dies ist ein seltsamer Text."
|
||||
else:
|
||||
mock_resp.choices[0].message.content = user_content
|
||||
|
||||
return mock_resp
|
||||
|
||||
self.mock_openai_chat.side_effect = translation_side_effect
|
||||
|
||||
response = await self.bot.airesponder.translate("Das ist ein komischer Text.")
|
||||
self.assertEqual(response, "This is a strange text.")
|
||||
response = await self.bot.airesponder.translate("This is a strange text.", language="german")
|
||||
self.assertEqual(response, "Dies ist ein seltsamer Text.")
|
||||
|
||||
async def test_fix1(self) -> None:
|
||||
# Skip this test due to Mock iteration issues - functionality works in practice
|
||||
self.skipTest("Mock iteration issue - test works in real usage")
|
||||
|
||||
@@ -49,9 +49,6 @@ class FakeModelResponder(AIResponder):
|
||||
async def classify(self, message, history_tail):
|
||||
return getattr(self, "scripted_classification", None)
|
||||
|
||||
async def translate(self, text: str, language: str = "english") -> str:
|
||||
return text
|
||||
|
||||
|
||||
@given(parsers.parse("a responder with history limit {limit:d}"), target_fixture="responder")
|
||||
def responder(limit):
|
||||
|
||||
@@ -30,16 +30,6 @@ class TestOpenAIResponderSimple(unittest.IsolatedAsyncioTestCase):
|
||||
"""ENV-18: the repair path is gone — no fix() on the responder."""
|
||||
self.assertFalse(hasattr(self.responder, "fix"))
|
||||
|
||||
async def test_translate_no_fix_model(self):
|
||||
"""Test translate when no fix-model is configured."""
|
||||
config_no_fix = {"openai-key": "test", "model": "gpt-4"}
|
||||
responder = OpenAIResponder(config_no_fix)
|
||||
|
||||
original_text = "Hello world"
|
||||
result = await responder.translate(original_text)
|
||||
|
||||
self.assertEqual(result, original_text)
|
||||
|
||||
async def test_consolidate_no_memory_model(self):
|
||||
"""MEM-10: without memory-model, consolidation is a no-op returning None."""
|
||||
config_no_memory = {"openai-key": "test", "model": "gpt-4"}
|
||||
|
||||
@@ -121,7 +121,7 @@ class TestSplitSends(OpsBase):
|
||||
self.bot.config["split-threshold"] = 50
|
||||
answer = "Første del.\n\nAndre del som også er ganske lang her."
|
||||
response = AIResponse(answer, True, "chat", None, "a cat", False, False)
|
||||
self.bot.airesponder.draw = AsyncMock(return_value=__import__("io").BytesIO(b"png"))
|
||||
self.bot.airesponder.draw = AsyncMock(return_value=[__import__("io").BytesIO(b"png")])
|
||||
channel = MagicMock(spec=TextChannel)
|
||||
channel.send = AsyncMock()
|
||||
await self.bot.send_answer_with_typing(response, channel, self.bot.airesponder, factual=True)
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Unit coverage for SPEC-004 image generation (IMG-01..05)."""
|
||||
|
||||
import base64
|
||||
import unittest
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
|
||||
from discord import TextChannel
|
||||
|
||||
from fjerkroa_bot.ai_responder import AIMessage, AIResponder, AIResponse
|
||||
from fjerkroa_bot.openai_responder import OpenAIResponder
|
||||
|
||||
from .test_bdd_envelope import FakeModelResponder, envelope
|
||||
from .test_spec_ops import OpsBase
|
||||
|
||||
RESPONDER_CONFIG = {"openai-token": "t", "model": "m", "system": "s", "history-limit": 5}
|
||||
|
||||
|
||||
def image_api_result(count):
|
||||
return Mock(data=[Mock(b64_json=base64.b64encode(f"png{i}".encode()).decode()) for i in range(count)])
|
||||
|
||||
|
||||
class TestBase64Generation(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_draw_returns_decoded_buffers_and_meters(self):
|
||||
"""IMG-01: count images decoded from b64_json, each metered in the ledger."""
|
||||
responder = OpenAIResponder(RESPONDER_CONFIG, "chat")
|
||||
with patch("fjerkroa_bot.openai_responder.openai_image", new_callable=AsyncMock) as image_mock:
|
||||
image_mock.return_value = image_api_result(2)
|
||||
buffers = await responder.draw_openai("en katt på brygga", 2)
|
||||
self.assertEqual([buf.read() for buf in buffers], [b"png0", b"png1"])
|
||||
self.assertEqual(responder.ledger.images_today(), 2)
|
||||
self.assertEqual(image_mock.await_args.kwargs["n"], 2)
|
||||
self.assertEqual(image_mock.await_args.kwargs["model"], "gpt-image-2")
|
||||
self.assertNotIn("response_format", image_mock.await_args.kwargs)
|
||||
|
||||
async def test_legacy_model_clamped_single_b64(self):
|
||||
"""IMG-04: dall-e-3 -> n=1 and explicit response_format=b64_json."""
|
||||
config = dict(RESPONDER_CONFIG, **{"image-model": "dall-e-3"})
|
||||
responder = OpenAIResponder(config, "chat")
|
||||
with patch("fjerkroa_bot.openai_responder.openai_image", new_callable=AsyncMock) as image_mock:
|
||||
image_mock.return_value = image_api_result(1)
|
||||
buffers = await responder.draw_openai("a cat", 3)
|
||||
self.assertEqual(len(buffers), 1)
|
||||
self.assertEqual(image_mock.await_args.kwargs["n"], 1)
|
||||
self.assertEqual(image_mock.await_args.kwargs["response_format"], "b64_json")
|
||||
|
||||
|
||||
class TestPictureCountEnvelope(unittest.IsolatedAsyncioTestCase):
|
||||
async def clamp(self, raw):
|
||||
responder = FakeModelResponder({"system": "s", "history-limit": 5}, "chat")
|
||||
payload = {"answer": "ok", "answer_needed": True, "channel": "chat", "picture": "katt"}
|
||||
if raw is not None:
|
||||
payload["picture_count"] = raw
|
||||
return await responder.post_process(AIMessage("alice", "tegn", "chat"), payload)
|
||||
|
||||
async def test_clamped_and_defaulted(self):
|
||||
"""IMG-02: picture_count clamps to 1..4, defaults to 1 when absent."""
|
||||
self.assertEqual((await self.clamp(3)).picture_count, 3)
|
||||
self.assertEqual((await self.clamp(9)).picture_count, 4)
|
||||
self.assertEqual((await self.clamp(0)).picture_count, 1)
|
||||
self.assertEqual((await self.clamp(None)).picture_count, 1)
|
||||
|
||||
|
||||
class TestMultiImageSend(OpsBase):
|
||||
async def test_files_attached_to_single_send(self):
|
||||
"""IMG-03: picture_count images ride as multiple files on one send."""
|
||||
response = AIResponse("her er kattene", True, "chat", None, "to katter", False, False)
|
||||
response.picture_count = 2
|
||||
import io
|
||||
|
||||
self.bot.airesponder.draw = AsyncMock(return_value=[io.BytesIO(b"a"), io.BytesIO(b"b")])
|
||||
channel = MagicMock(spec=TextChannel)
|
||||
channel.send = AsyncMock()
|
||||
await self.bot.send_answer_with_typing(response, channel, self.bot.airesponder, factual=True)
|
||||
self.bot.airesponder.draw.assert_awaited_once_with("to katter", 2)
|
||||
files = channel.send.await_args.kwargs["files"]
|
||||
self.assertEqual(len(files), 2)
|
||||
|
||||
|
||||
class TestNoTranslateStep(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_translate_is_gone_prompt_untouched(self):
|
||||
"""IMG-05: no translate() anywhere; the picture prompt survives verbatim."""
|
||||
responder = FakeModelResponder({"system": "s", "history-limit": 5}, "chat")
|
||||
self.assertFalse(hasattr(responder, "translate"))
|
||||
self.assertFalse(hasattr(AIResponder, "translate"))
|
||||
responder.scripted.append(envelope(answer="ok", answer_needed=True, picture="en rød katt på brygga"))
|
||||
result = await responder.send(AIMessage("alice", "tegn en katt", "chat"))
|
||||
self.assertEqual(result.picture, "en rød katt på brygga")
|
||||
@@ -43,7 +43,7 @@ class TestEnvelopeSchema(unittest.IsolatedAsyncioTestCase):
|
||||
"""ENV-19: strict envelope schema — exact fields, all required, closed object."""
|
||||
json_schema = ENVELOPE_RESPONSE_FORMAT["json_schema"]
|
||||
schema = json_schema["schema"]
|
||||
expected = {"answer", "answer_needed", "channel", "staff", "picture", "picture_edit", "hack"}
|
||||
expected = {"answer", "answer_needed", "channel", "staff", "picture", "picture_count", "picture_edit", "hack"}
|
||||
self.assertEqual(set(schema["properties"]), expected)
|
||||
self.assertEqual(set(schema["required"]), expected)
|
||||
self.assertFalse(schema["additionalProperties"])
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Unit coverage for ENV-21 (tools + reasoning_effort, found live on ggg)."""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from fjerkroa_bot.openai_responder import OpenAIResponder
|
||||
|
||||
from .test_bdd_envelope import envelope
|
||||
|
||||
|
||||
def ok_result():
|
||||
message = Mock(content=envelope(answer="x", answer_needed=True), role="assistant", tool_calls=None, refusal=None)
|
||||
return Mock(choices=[Mock(message=message)], usage="usage")
|
||||
|
||||
|
||||
class TestToolsReasoningEffort(unittest.IsolatedAsyncioTestCase):
|
||||
async def chat_kwargs(self, with_tools):
|
||||
config = {"openai-token": "t", "model": "gpt-5.6-luna", "system": "s", "history-limit": 5, "enable-game-info": with_tools}
|
||||
responder = OpenAIResponder(config, "chat")
|
||||
if with_tools:
|
||||
responder.igdb = Mock()
|
||||
responder.igdb.get_openai_functions = Mock(return_value=[{"name": "search_games", "parameters": {}}])
|
||||
with patch("fjerkroa_bot.openai_responder.openai_chat", new_callable=AsyncMock) as chat_mock:
|
||||
chat_mock.return_value = ok_result()
|
||||
await responder.chat([{"role": "user", "content": "hi"}], 10)
|
||||
return chat_mock.await_args.kwargs
|
||||
|
||||
async def test_tools_carry_reasoning_effort_none(self):
|
||||
"""ENV-21: tools attached -> reasoning_effort 'none' rides along."""
|
||||
kwargs = await self.chat_kwargs(with_tools=True)
|
||||
self.assertIn("tools", kwargs)
|
||||
self.assertEqual(kwargs["reasoning_effort"], "none")
|
||||
|
||||
async def test_toolless_calls_untouched(self):
|
||||
"""ENV-21: without tools no reasoning_effort is sent."""
|
||||
kwargs = await self.chat_kwargs(with_tools=False)
|
||||
self.assertNotIn("tools", kwargs)
|
||||
self.assertNotIn("reasoning_effort", kwargs)
|
||||
Reference in New Issue
Block a user