mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-05 00:47:48 +08:00
fix(gateway): support OpenAI image edit requests
This commit is contained in:
@@ -187,6 +187,7 @@ async fn execute_chatgpt_web_image(
|
|||||||
request_id = %plan.request_id,
|
request_id = %plan.request_id,
|
||||||
candidate_id = ?plan.candidate_id,
|
candidate_id = ?plan.candidate_id,
|
||||||
base_url = %base_url,
|
base_url = %base_url,
|
||||||
|
operation = %request.operation,
|
||||||
image_count = request.images.len(),
|
image_count = request.images.len(),
|
||||||
size = %request.size,
|
size = %request.size,
|
||||||
ratio = %request.ratio,
|
ratio = %request.ratio,
|
||||||
@@ -311,6 +312,7 @@ async fn execute_chatgpt_web_image(
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct ChatGptWebImageRequest {
|
struct ChatGptWebImageRequest {
|
||||||
|
operation: String,
|
||||||
model: String,
|
model: String,
|
||||||
web_model: String,
|
web_model: String,
|
||||||
prompt: String,
|
prompt: String,
|
||||||
@@ -342,6 +344,7 @@ impl ChatGptWebImageRequest {
|
|||||||
.map(ToOwned::to_owned)
|
.map(ToOwned::to_owned)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
operation: chatgpt_web_image_operation(body.get("operation")),
|
||||||
model: text("model").unwrap_or_else(|| "gpt-image-2".to_string()),
|
model: text("model").unwrap_or_else(|| "gpt-image-2".to_string()),
|
||||||
web_model: text("web_model").unwrap_or_else(|| "gpt-5-5-thinking".to_string()),
|
web_model: text("web_model").unwrap_or_else(|| "gpt-5-5-thinking".to_string()),
|
||||||
prompt: text("prompt").unwrap_or_else(|| "Generate a high quality image.".to_string()),
|
prompt: text("prompt").unwrap_or_else(|| "Generate a high quality image.".to_string()),
|
||||||
@@ -2434,10 +2437,23 @@ fn json_u64(value: Option<&Value>) -> Option<u64> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn chatgpt_web_image_operation(value: Option<&Value>) -> String {
|
||||||
|
value
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(str::trim)
|
||||||
|
.map(str::to_ascii_lowercase)
|
||||||
|
.filter(|value| matches!(value.as_str(), "generate" | "edit"))
|
||||||
|
.unwrap_or_else(|| "generate".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
fn build_failed_sse(request: &ChatGptWebImageRequest, failure: &Value) -> String {
|
fn build_failed_sse(request: &ChatGptWebImageRequest, failure: &Value) -> String {
|
||||||
let failed = if failure.get("type").and_then(Value::as_str) == Some("response.failed") {
|
let failed = if failure.get("type").and_then(Value::as_str) == Some("response.failed") {
|
||||||
failure.clone()
|
failure.clone()
|
||||||
} else {
|
} else {
|
||||||
|
let operation = match request.operation.as_str() {
|
||||||
|
"edit" => "edit",
|
||||||
|
_ => "generation",
|
||||||
|
};
|
||||||
json!({
|
json!({
|
||||||
"type": "response.failed",
|
"type": "response.failed",
|
||||||
"response": {
|
"response": {
|
||||||
@@ -2445,7 +2461,7 @@ fn build_failed_sse(request: &ChatGptWebImageRequest, failure: &Value) -> String
|
|||||||
"model": request.model,
|
"model": request.model,
|
||||||
"error": failure.get("error").cloned().unwrap_or_else(|| json!({
|
"error": failure.get("error").cloned().unwrap_or_else(|| json!({
|
||||||
"code": "chatgpt_web_image_failed",
|
"code": "chatgpt_web_image_failed",
|
||||||
"message": "ChatGPT-Web image generation failed"
|
"message": format!("ChatGPT-Web image {operation} failed")
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -2641,7 +2657,7 @@ fn chatgpt_web_image_request_context(plan: &ExecutionPlan) -> Option<Value> {
|
|||||||
let mut image_request = Map::new();
|
let mut image_request = Map::new();
|
||||||
image_request.insert(
|
image_request.insert(
|
||||||
"operation".to_string(),
|
"operation".to_string(),
|
||||||
Value::String("generate".to_string()),
|
Value::String(chatgpt_web_image_operation(body.get("operation"))),
|
||||||
);
|
);
|
||||||
for key in [
|
for key in [
|
||||||
"model",
|
"model",
|
||||||
@@ -3223,6 +3239,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn chatgpt_web_success_sse_includes_estimated_image_usage() {
|
fn chatgpt_web_success_sse_includes_estimated_image_usage() {
|
||||||
let request = ChatGptWebImageRequest {
|
let request = ChatGptWebImageRequest {
|
||||||
|
operation: "generate".to_string(),
|
||||||
model: "gpt-image-2".to_string(),
|
model: "gpt-image-2".to_string(),
|
||||||
web_model: "gpt-5-5-thinking".to_string(),
|
web_model: "gpt-5-5-thinking".to_string(),
|
||||||
prompt: "draw a test image".to_string(),
|
prompt: "draw a test image".to_string(),
|
||||||
@@ -3276,6 +3293,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn chatgpt_web_success_sse_uses_image_dimensions_not_output_text() {
|
fn chatgpt_web_success_sse_uses_image_dimensions_not_output_text() {
|
||||||
let request = ChatGptWebImageRequest {
|
let request = ChatGptWebImageRequest {
|
||||||
|
operation: "generate".to_string(),
|
||||||
model: "gpt-image-2".to_string(),
|
model: "gpt-image-2".to_string(),
|
||||||
web_model: "gpt-5-5-thinking".to_string(),
|
web_model: "gpt-5-5-thinking".to_string(),
|
||||||
prompt: "draw a test image".to_string(),
|
prompt: "draw a test image".to_string(),
|
||||||
@@ -3334,6 +3352,32 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn chatgpt_web_image_request_context_preserves_edit_operation() {
|
||||||
|
let plan = sample_plan(
|
||||||
|
CHATGPT_WEB_DEFAULT_BASE_URL,
|
||||||
|
json!({
|
||||||
|
"operation": "edit",
|
||||||
|
"model": "gpt-image-2",
|
||||||
|
"web_model": "gpt-5-5-thinking",
|
||||||
|
"prompt": "adjust this image",
|
||||||
|
"size": "512x512",
|
||||||
|
"ratio": "1:1",
|
||||||
|
"images": ["data:image/png;base64,aW1hZ2U="],
|
||||||
|
"count": 1,
|
||||||
|
"output_format": "png"
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
let context = chatgpt_web_stream_observer_context(&plan, None);
|
||||||
|
|
||||||
|
assert_eq!(context["image_request"]["operation"], json!("edit"));
|
||||||
|
assert_eq!(context["image_request"]["model"], json!("gpt-image-2"));
|
||||||
|
assert_eq!(context["image_request"]["size"], json!("512x512"));
|
||||||
|
assert_eq!(context["provider_api_format"], json!("openai:image"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn chatgpt_web_image_quota_refresh_plan_uses_conversation_init() {
|
fn chatgpt_web_image_quota_refresh_plan_uses_conversation_init() {
|
||||||
let plan = sample_plan(
|
let plan = sample_plan(
|
||||||
|
|||||||
@@ -1577,6 +1577,47 @@ mod tests {
|
|||||||
assert!(provider_request_body.get("tools").is_none());
|
assert!(provider_request_body.get("tools").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_image_api_provider_edit_request_body_keeps_images_edit_shape() {
|
||||||
|
let parts = request_parts("/v1/images/edits", Some("application/json"));
|
||||||
|
let request = normalize_openai_image_request(
|
||||||
|
&parts,
|
||||||
|
&json!({
|
||||||
|
"model": "gpt-image-2",
|
||||||
|
"prompt": "replace the background",
|
||||||
|
"image": "data:image/png;base64,aW1hZ2U=",
|
||||||
|
"mask": "data:image/png;base64,bWFzaw==",
|
||||||
|
"input_fidelity": "high",
|
||||||
|
"output_format": "png",
|
||||||
|
"response_format": "url",
|
||||||
|
"user": "user-123"
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.expect("edit request should normalize");
|
||||||
|
|
||||||
|
let provider_request_body =
|
||||||
|
build_openai_image_api_provider_request_body(&request, Some("mapped-edit-model"));
|
||||||
|
|
||||||
|
assert_eq!(provider_request_body["model"], "mapped-edit-model");
|
||||||
|
assert_eq!(provider_request_body["prompt"], "replace the background");
|
||||||
|
assert_eq!(provider_request_body["input_fidelity"], "high");
|
||||||
|
assert_eq!(provider_request_body["output_format"], "png");
|
||||||
|
assert_eq!(provider_request_body["response_format"], "url");
|
||||||
|
assert_eq!(provider_request_body["user"], "user-123");
|
||||||
|
assert_eq!(
|
||||||
|
provider_request_body["image"]["image_url"],
|
||||||
|
"data:image/png;base64,aW1hZ2U="
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
provider_request_body["mask"]["image_url"],
|
||||||
|
"data:image/png;base64,bWFzaw=="
|
||||||
|
);
|
||||||
|
assert!(provider_request_body.get("input").is_none());
|
||||||
|
assert!(provider_request_body.get("tools").is_none());
|
||||||
|
assert!(provider_request_body.get("action").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn chatgpt_web_accepts_1k_tier_and_1024_size() {
|
fn chatgpt_web_accepts_1k_tier_and_1024_size() {
|
||||||
let parts = request_parts("/v1/images/generations", Some("application/json"));
|
let parts = request_parts("/v1/images/generations", Some("application/json"));
|
||||||
@@ -1630,6 +1671,40 @@ mod tests {
|
|||||||
assert_eq!(body["output_format"], "png");
|
assert_eq!(body["output_format"], "png");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn chatgpt_web_accepts_openai_image_edit_requests() {
|
||||||
|
let parts = request_parts("/v1/images/edits", Some("application/json"));
|
||||||
|
let body = build_chatgpt_web_image_request_body(
|
||||||
|
&parts,
|
||||||
|
&json!({
|
||||||
|
"model": "gpt-image-2",
|
||||||
|
"prompt": "make the sky brighter",
|
||||||
|
"image": {
|
||||||
|
"b64_json": "aW1hZ2U=",
|
||||||
|
"mime_type": "image/png"
|
||||||
|
},
|
||||||
|
"size": "1024x1024",
|
||||||
|
"quality": "high",
|
||||||
|
"response_format": "b64_json",
|
||||||
|
"output_format": "png",
|
||||||
|
"user": "user-123"
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.expect("ChatGPT-Web edit request should pass");
|
||||||
|
|
||||||
|
assert_eq!(body["operation"], "edit");
|
||||||
|
assert_eq!(body["model"], "gpt-image-2");
|
||||||
|
assert_eq!(body["prompt"], "make the sky brighter");
|
||||||
|
assert_eq!(body["size"], "1024x1024");
|
||||||
|
assert_eq!(body["quality"], "high");
|
||||||
|
assert_eq!(body["response_format"], "b64_json");
|
||||||
|
assert_eq!(body["output_format"], "png");
|
||||||
|
assert_eq!(body["user"], "user-123");
|
||||||
|
assert_eq!(body["count"], 1);
|
||||||
|
assert_eq!(body["images"], json!(["data:image/png;base64,aW1hZ2U="]));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn chatgpt_web_rejects_oversized_resolution_or_size() {
|
fn chatgpt_web_rejects_oversized_resolution_or_size() {
|
||||||
let parts = request_parts("/v1/images/generations", Some("application/json"));
|
let parts = request_parts("/v1/images/generations", Some("application/json"));
|
||||||
|
|||||||
@@ -189,6 +189,17 @@ mod tests {
|
|||||||
assert_eq!(url, "https://api.openai.com/v1/images/generations?trace=1");
|
assert_eq!(url, "https://api.openai.com/v1/images/generations?trace=1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn standard_openai_image_url_preserves_edit_surface() {
|
||||||
|
let mut transport = sample_transport();
|
||||||
|
transport.provider.provider_type = "openai".to_string();
|
||||||
|
|
||||||
|
let url =
|
||||||
|
build_openai_image_upstream_url(&transport, Some("/v1/images/edits"), Some("trace=1"));
|
||||||
|
|
||||||
|
assert_eq!(url, "https://api.openai.com/v1/images/edits?trace=1");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn chatgpt_web_is_supported_by_dedicated_openai_image_transport_policy() {
|
fn chatgpt_web_is_supported_by_dedicated_openai_image_transport_policy() {
|
||||||
let mut transport = sample_transport();
|
let mut transport = sample_transport();
|
||||||
|
|||||||
Reference in New Issue
Block a user