gpt-image-2 multi-image + fix: tools need reasoning_effort none on gpt-5.6 (ggg mute bug)

This commit is contained in:
Oleksandr Kozachuk
2026-07-13 16:17:00 +02:00
parent ae870db181
commit 7e6eae10ee
15 changed files with 220 additions and 88 deletions
+23 -33
View File
@@ -1,14 +1,14 @@
import asyncio
import base64
import hashlib
import json
import logging
from io import BytesIO
from typing import Any, Dict, List, Optional, Tuple
import aiohttp
import openai
from .ai_responder import AIResponder, exponential_backoff, pp, sanitize_external_text
from .ai_responder import AIResponder, exponential_backoff, sanitize_external_text
from .igdblib import IGDBQuery
from .leonardo_draw import LeonardoAIDrawMixIn
from .quota import QuotaLedger
@@ -24,10 +24,11 @@ ENVELOPE_SCHEMA = {
"channel": {"type": ["string", "null"], "description": "Target channel name, or null for the origin channel."},
"staff": {"type": ["string", "null"], "description": "Alert text for the staff channel, or null."},
"picture": {"type": ["string", "null"], "description": "Image generation prompt, or null."},
"picture_count": {"type": "integer", "description": "How many images to generate (1-4), 1 unless more were asked for."},
"picture_edit": {"type": "boolean", "description": "Whether the picture refers to an earlier image."},
"hack": {"type": "boolean", "description": "Whether the user tried to manipulate the assistant."},
},
"required": ["answer", "answer_needed", "channel", "staff", "picture", "picture_edit", "hack"],
"required": ["answer", "answer_needed", "channel", "staff", "picture", "picture_count", "picture_edit", "hack"],
"additionalProperties": False,
}
ENVELOPE_RESPONSE_FORMAT = {"type": "json_schema", "json_schema": {"name": "envelope", "strict": True, "schema": ENVELOPE_SCHEMA}}
@@ -92,10 +93,7 @@ async def openai_chat(client, *args, **kwargs):
async def openai_image(client, *args, **kwargs):
response = await client.images.generate(*args, **kwargs)
async with aiohttp.ClientSession() as session:
async with session.get(response.data[0].url) as image:
return BytesIO(await image.read())
return await client.images.generate(*args, **kwargs)
class OpenAIResponder(AIResponder, LeonardoAIDrawMixIn):
@@ -126,15 +124,26 @@ class OpenAIResponder(AIResponder, LeonardoAIDrawMixIn):
else:
logging.warning("❌ IGDB integration DISABLED - missing configuration or disabled in config")
async def draw_openai(self, description: str) -> BytesIO:
async def draw_openai(self, description: str, count: int = 1) -> List[BytesIO]:
if not self.ledger.budget_ok():
raise RuntimeError("daily budget exhausted - refusing image call")
model = self.config.get("image-model", "gpt-image-2")
kwargs: Dict[str, Any] = {"model": model, "prompt": description, "size": self.config.get("image-size", "1024x1024")}
if "image-quality" in self.config:
kwargs["quality"] = self.config["image-quality"]
if model.startswith("gpt-image"):
kwargs["n"] = max(1, min(int(count), 4))
else:
# legacy models: single image, base64 must be requested (IMG-04)
kwargs["n"] = 1
kwargs["response_format"] = "b64_json"
for _ in range(3):
try:
response = await openai_image(self.client, prompt=description, n=1, size="1024x1024", model="dall-e-3")
self.ledger.add_images(1)
logging.info(f"Drawed a picture with DALL-E on this description: {repr(description)}")
return response
response = await openai_image(self.client, **kwargs)
buffers = [BytesIO(base64.b64decode(item.b64_json)) for item in response.data]
self.ledger.add_images(len(buffers))
logging.info(f"generated {len(buffers)} image(s) on {model} for: {repr(description)}")
return buffers
except Exception as err:
logging.warning(f"Failed to generate image {repr(description)}: {repr(err)}")
raise RuntimeError(f"Failed to generate image {repr(description)} after multiple retries")
@@ -209,6 +218,8 @@ class OpenAIResponder(AIResponder, LeonardoAIDrawMixIn):
if igdb_functions and isinstance(igdb_functions, list):
chat_kwargs["tools"] = [{"type": "function", "function": func} for func in igdb_functions]
chat_kwargs["tool_choice"] = "auto"
# gpt-5.6 rejects tools + reasoning on chat/completions (ENV-21)
chat_kwargs["reasoning_effort"] = self.config.get("reasoning-effort", "none")
logging.info(f"🎮 IGDB functions available to AI: {[f['name'] for f in igdb_functions]}")
logging.debug(f" Full chat_kwargs with tools: {list(chat_kwargs.keys())}")
except (TypeError, AttributeError) as e:
@@ -343,27 +354,6 @@ class OpenAIResponder(AIResponder, LeonardoAIDrawMixIn):
logging.debug(f"Full traceback: {traceback.format_exc()}")
return None, limit
async def translate(self, text: str, language: str = "english") -> str:
if "fix-model" not in self.config:
return text
message = [
{
"role": "system",
"content": f"You are an professional translator to {language} language,"
f" you translate everything you get directly to {language}"
f" if it is not already in {language}, otherwise you just copy it.",
},
{"role": "user", "content": text},
]
try:
result = await openai_chat(self.client, model=self.config["fix-model"], messages=message)
response = result.choices[0].message.content
logging.info(f"got this translated message:\n{pp(response)}")
return response
except Exception as err:
logging.warning(f"failed to translate the text: {repr(err)}")
return text
async def classify(self, message: Any, history_tail: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
"""~100-token reply/factual/emoji verdict on classifier-model (BEH-01/03)."""
if "classifier-model" not in self.config or not self.ledger.budget_ok():