Fix sticker generation for newer GPT image models

Sticker generation was failing when using newer GPT image models
(gpt-image-1, gpt-image-1-mini, gpt-image-1.5). The issue occurred
because stickers requested 256x256 size, but these models only support
1024x1024, 1536x1024, 1024x1536, and auto.

To reproduce, send `!bai sticker Something` to an agent configured
with a GPT image model. The error was:

  invalid_request_error: Invalid value: '256x256'. Supported values
  are: '1024x1024', '1024x1536', '1536x1024', and 'auto'. (param: size)
  (code: invalid_value)

The fix replaces the hardcoded 256x256 size override with a
`smallest_size_possible` flag, letting each provider determine the
appropriate sticker size based on the model being used.

The `openai_compat` provider still defaults to requesting 256x256 in all cases
(regardless of model name).
This commit is contained in:
Slavi Pantaleev
2026-02-04 02:36:50 +02:00
parent 38516f2e17
commit c70387b0c3
6 changed files with 37 additions and 42 deletions

View File

@@ -2,7 +2,7 @@ use mxlink::mime;
#[derive(Default)] #[derive(Default)]
pub struct ImageGenerationParams { pub struct ImageGenerationParams {
pub size_override: Option<String>, pub smallest_size_possible: bool,
pub cheaper_model_switching_allowed: bool, pub cheaper_model_switching_allowed: bool,
@@ -10,8 +10,8 @@ pub struct ImageGenerationParams {
} }
impl ImageGenerationParams { impl ImageGenerationParams {
pub fn with_size_override(mut self, value: Option<String>) -> Self { pub fn with_smallest_size_possible(mut self, value: bool) -> Self {
self.size_override = value; self.smallest_size_possible = value;
self self
} }

View File

@@ -31,12 +31,9 @@ use crate::{
use crate::{ use crate::{
agent::{ agent::{
AgentPurpose, AgentPurpose,
provider::{ provider::entity::{
entity::{ ImageEditResult, ImageGenerationResult, ImageSource, PingResult,
ImageEditResult, ImageGenerationResult, ImageSource, PingResult, TextToSpeechParams, TextToSpeechResult,
TextToSpeechParams, TextToSpeechResult,
},
openai::utils::convert_string_to_enum,
}, },
}, },
strings, strings,
@@ -312,10 +309,11 @@ impl ControllerTrait for Controller {
image_generation_config.quality.clone() image_generation_config.quality.clone()
}; };
let size = params let size = if params.smallest_size_possible {
.size_override Some(get_sticker_size(&model))
.map(|s| convert_string_to_enum::<async_openai::types::images::ImageSize>(&s).unwrap()) } else {
.or(image_generation_config.size); image_generation_config.size
};
let response_format = match model.clone() { let response_format = match model.clone() {
ImageModel::DallE2 => Some(ImageResponseFormat::B64Json), ImageModel::DallE2 => Some(ImageResponseFormat::B64Json),
@@ -638,3 +636,17 @@ fn audio_mime_type_to_file_name(mime_type: &mxlink::mime::Mime) -> Option<String
Some(format!("audio.{}", file_extension)) Some(format!("audio.{}", file_extension))
} }
/// Returns the smallest supported size for stickers based on what the image model supports.
fn get_sticker_size(model: &ImageModel) -> async_openai::types::images::ImageSize {
use async_openai::types::images::ImageSize;
match model {
ImageModel::DallE2 => ImageSize::S256x256,
ImageModel::DallE3 => ImageSize::S1024x1024,
ImageModel::GptImage1 => ImageSize::S1024x1024,
ImageModel::GptImage1Mini => ImageSize::S1024x1024,
ImageModel::GptImage1dot5 => ImageSize::S1024x1024,
ImageModel::Other(_) => ImageSize::S1024x1024,
}
}

View File

@@ -1,8 +1,6 @@
use async_openai::types::{ use async_openai::types::responses::{
responses::{ EasyInputContent, EasyInputMessage, ImageDetail, InputContent, InputImageContent, InputItem,
EasyInputContent, EasyInputMessage, ImageDetail, InputContent, InputImageContent, InputParam, MessageType, Role,
InputItem, InputParam, MessageType, Role,
},
}; };
use crate::conversation::llm::{ use crate::conversation::llm::{
@@ -48,19 +46,3 @@ pub fn convert_llm_messages_to_openai_response_input(
InputParam::Items(items) InputParam::Items(items)
} }
pub(super) fn convert_string_to_enum<T>(value: &str) -> Result<T, String>
where
T: serde::de::DeserializeOwned,
{
// This is a hacky way to construct an enum from the string we have.
let enum_result: serde_json::Result<T> = serde_json::from_str(&format!("\"{}\"", value));
match enum_result {
Ok(enum_result) => Ok(enum_result),
Err(err) => {
tracing::debug!(?err, "Failed to parse into enum");
Err(format!("The value ({}) is not supported.", value))
}
}
}

View File

@@ -3,6 +3,8 @@ use etke_openai_api_rust::chat::{ChatApi, ChatBody};
use etke_openai_api_rust::images::{ImagesApi, ImagesBody}; use etke_openai_api_rust::images::{ImagesApi, ImagesBody};
use etke_openai_api_rust::{Auth, Message, OpenAI}; use etke_openai_api_rust::{Auth, Message, OpenAI};
const SMALLEST_IMAGE_SIZE: &str = "256x256";
use super::super::ControllerTrait; use super::super::ControllerTrait;
use crate::utils::base64::base64_decode; use crate::utils::base64::base64_decode;
use crate::{ use crate::{
@@ -303,9 +305,11 @@ impl ControllerTrait for Controller {
// when they span multiple lines. // when they span multiple lines.
let prompt = prompt.replace("\n", " "); let prompt = prompt.replace("\n", " ");
let size: Option<String> = params let size: Option<String> = if params.smallest_size_possible {
.size_override Some(SMALLEST_IMAGE_SIZE.to_owned())
.or_else(|| image_generation_config.size.clone()); } else {
image_generation_config.size.clone()
};
let request = ImagesBody { let request = ImagesBody {
model: Some(image_generation_config.model_id.to_owned()), model: Some(image_generation_config.model_id.to_owned()),

View File

@@ -12,9 +12,6 @@ use crate::strings;
use crate::utils::mime::get_file_extension; use crate::utils::mime::get_file_extension;
use crate::{Bot, entity::MessageContext}; use crate::{Bot, entity::MessageContext};
// We may make this configurable (per room, etc.) in the future, but for now it's hardcoded.
const STICKER_SIZE: &str = "256x256";
pub async fn handle_image( pub async fn handle_image(
bot: &Bot, bot: &Bot,
matrix_link: MatrixLink, matrix_link: MatrixLink,
@@ -177,7 +174,7 @@ pub async fn handle_sticker(
); );
let params = ImageGenerationParams::default() let params = ImageGenerationParams::default()
.with_size_override(Some(STICKER_SIZE.to_owned())) .with_smallest_size_possible(true)
.with_cheaper_model_switching_allowed(true) .with_cheaper_model_switching_allowed(true)
.with_cheaper_quality_switching_allowed(true); .with_cheaper_quality_switching_allowed(true);

View File

@@ -64,7 +64,7 @@ To create a sticker, send a command like `%command_prefix% sticker A huge bowl o
The difference from **creating images** is that the bot will: The difference from **creating images** is that the bot will:
- create a smaller-resolution image (`256x256`) - smaller/quicker, but still good enough for a sticker - create a smaller-resolution image (as small as the model allows) - smaller/quicker, but still good enough for a sticker
- potentially switch to a different (cheaper or otherwise more suitable) model, if available - potentially switch to a different (cheaper or otherwise more suitable) model, if available
- post the image directly to the room (as a reply to your message), without starting a threaded conversation - post the image directly to the room (as a reply to your message), without starting a threaded conversation
"#; "#;