Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 48 additions & 2 deletions src/crates/adapters/ai-adapters/src/client/image_generation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ const REQUEST_TIMEOUT_SECS: u64 = 180;
/// Guard against a provider streaming an unbounded body.
const MAX_BODY_BYTES: usize = 64 * 1024 * 1024;
const DEFAULT_MIME: &str = "image/png";
/// The Responses API has a separate image-tool model. Keep this independent
/// from the subscription's top-level chat model, which is only responsible for
/// orchestrating the tool call and revising the prompt.
const DEFAULT_CODEX_IMAGE_MODEL: &str = "gpt-image-2.5-sunburst";
/// `high` is supported by the current GPT Image models and older GPT Image
/// models, so existing subscription configurations can opt into a quality
/// setting without a persisted-config migration.
const DEFAULT_CODEX_IMAGE_QUALITY: &str = "high";

/// Substrings that mean the provider refused on safety grounds rather than
/// failing. Reported separately so the UI can say "blocked" instead of "failed".
Expand Down Expand Up @@ -282,8 +290,16 @@ fn build_body(target: &ImageGenerationTarget<'_>, request: &ImageGenerationReque
/// output item. `store: false` is required by the Codex backend, and `stream`
/// is what produces the SSE events the caller parses.
fn chatgpt_codex_body(model: &str, request: &ImageGenerationRequest) -> Value {
let mut tool = json!({ "type": "image_generation" });
// The tool accepts the same knobs as the image API, but only when set.
let mut tool = json!({
"type": "image_generation",
"model": DEFAULT_CODEX_IMAGE_MODEL,
"quality": request
.quality
.as_deref()
.unwrap_or(DEFAULT_CODEX_IMAGE_QUALITY),
});
// The tool accepts the same knobs as the image API. Quality defaults to a
// high-quality setting above, while an explicit service setting wins.
if let Some(size) = request.size.as_deref() {
tool["size"] = json!(normalize_size(size, 'x'));
}
Expand Down Expand Up @@ -1077,6 +1093,36 @@ mod tests {
assert_eq!(body["quality"], "high");
}

#[test]
fn codex_body_selects_explicit_high_quality_image_model() {
let body = chatgpt_codex_body(
"gpt-6-astra",
&ImageGenerationRequest {
prompt: "a red fox".to_string(),
..ImageGenerationRequest::default()
},
);

assert_eq!(body["model"], "gpt-6-astra");
assert_eq!(body["tools"][0]["type"], "image_generation");
assert_eq!(body["tools"][0]["model"], "gpt-image-2.5-sunburst");
assert_eq!(body["tools"][0]["quality"], "high");
}

#[test]
fn codex_body_preserves_an_explicit_quality_override() {
let body = chatgpt_codex_body(
"gpt-6-astra",
&ImageGenerationRequest {
prompt: "a red fox".to_string(),
quality: Some("max".to_string()),
..ImageGenerationRequest::default()
},
);

assert_eq!(body["tools"][0]["quality"], "max");
}

#[test]
fn dashscope_body_nests_prompt_and_uses_star_separator() {
let body = dashscope_body(
Expand Down
3 changes: 2 additions & 1 deletion src/crates/contracts/config-contracts/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -907,7 +907,8 @@ pub struct ImageGenerationParams {
pub n: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub negative_prompt: Option<String>,
/// OpenAI Images quality preset (`auto` / `low` / `medium` / `high`).
/// OpenAI Images quality preset (`auto` / `low` / `medium` / `high` /
/// `xhigh` / `max`).
#[serde(skip_serializing_if = "Option::is_none")]
pub quality: Option<String>,
/// OpenAI Images background (`auto` / `opaque` / `transparent`).
Expand Down
Loading