mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
fix(gateway): 兼容 OpenAI 图像 multipart boundary 大小写 (#331)
This commit is contained in:
@@ -866,11 +866,7 @@ fn parse_multipart_fields_from_base64(
|
||||
.headers
|
||||
.get(http::header::CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok())?;
|
||||
let boundary = content_type
|
||||
.split(';')
|
||||
.find_map(|segment| segment.trim().strip_prefix("boundary="))?
|
||||
.trim_matches('"')
|
||||
.to_string();
|
||||
let boundary = multipart_boundary(content_type)?;
|
||||
let body_bytes = base64::engine::general_purpose::STANDARD
|
||||
.decode(body_base64)
|
||||
.ok()?;
|
||||
@@ -905,6 +901,17 @@ fn parse_multipart_fields(body: &[u8], boundary: &str) -> Vec<MultipartField> {
|
||||
parts
|
||||
}
|
||||
|
||||
fn multipart_boundary(content_type: &str) -> Option<String> {
|
||||
content_type.split(';').find_map(|segment| {
|
||||
let (key, value) = segment.trim().split_once('=')?;
|
||||
if !key.trim().eq_ignore_ascii_case("boundary") {
|
||||
return None;
|
||||
}
|
||||
let boundary = value.trim().trim_matches('"').trim();
|
||||
(!boundary.is_empty()).then(|| boundary.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_multipart_field(raw: &[u8]) -> Option<MultipartField> {
|
||||
let header_end = find_subslice(raw, b"\r\n\r\n")?;
|
||||
let headers = &raw[..header_end];
|
||||
@@ -1035,6 +1042,44 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_edit_multipart_request_accepts_mixed_case_boundary() {
|
||||
let boundary = "------------------------OYNWsMZCt0ILTwn8naP4Gb";
|
||||
let body = format!(
|
||||
concat!(
|
||||
"--{boundary}\r\n",
|
||||
"Content-Disposition: form-data; name=\"model\"\r\n\r\n",
|
||||
"gpt-image-2\r\n",
|
||||
"--{boundary}\r\n",
|
||||
"Content-Disposition: form-data; name=\"prompt\"\r\n\r\n",
|
||||
"edit this image\r\n",
|
||||
"--{boundary}\r\n",
|
||||
"Content-Disposition: form-data; name=\"image\"; filename=\"image.jpg\"\r\n",
|
||||
"Content-Type: image/jpeg\r\n\r\n",
|
||||
"image-bytes\r\n",
|
||||
"--{boundary}--\r\n"
|
||||
),
|
||||
boundary = boundary,
|
||||
);
|
||||
let body_base64 = base64::engine::general_purpose::STANDARD.encode(body.as_bytes());
|
||||
let parts = request_parts(
|
||||
"/v1/images/edits",
|
||||
Some(&format!("multipart/form-data; boundary={boundary}")),
|
||||
);
|
||||
|
||||
let request = normalize_openai_image_request(&parts, &json!({}), Some(&body_base64))
|
||||
.expect("edit request should normalize");
|
||||
|
||||
assert_eq!(request.operation, OpenAiImageOperation::Edit);
|
||||
assert_eq!(request.requested_model.as_deref(), Some("gpt-image-2"));
|
||||
assert_eq!(request.prompt.as_deref(), Some("edit this image"));
|
||||
assert_eq!(request.images.len(), 1);
|
||||
assert_eq!(
|
||||
request.tool.get("action").and_then(|value| value.as_str()),
|
||||
Some("edit")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_edit_json_request_maps_mask_to_input_image_mask() {
|
||||
let parts = request_parts("/v1/images/edits", Some("application/json"));
|
||||
|
||||
@@ -318,9 +318,12 @@ fn parse_openai_image_validation_input(
|
||||
});
|
||||
}
|
||||
|
||||
let content_type = content_type.unwrap_or_default().to_ascii_lowercase();
|
||||
if content_type.contains("multipart/form-data") {
|
||||
parse_openai_image_validation_input_from_multipart(request_body, &content_type)
|
||||
let content_type = content_type.unwrap_or_default();
|
||||
if content_type
|
||||
.to_ascii_lowercase()
|
||||
.contains("multipart/form-data")
|
||||
{
|
||||
parse_openai_image_validation_input_from_multipart(request_body, content_type)
|
||||
} else {
|
||||
parse_openai_image_validation_input_from_json(request_body)
|
||||
}
|
||||
@@ -403,11 +406,7 @@ fn parse_openai_image_validation_input_from_multipart(
|
||||
request_body: &Bytes,
|
||||
content_type: &str,
|
||||
) -> Result<OpenAiImageValidationInput, &'static str> {
|
||||
let boundary = content_type
|
||||
.split(';')
|
||||
.find_map(|segment| segment.trim().strip_prefix("boundary="))
|
||||
.map(|value| value.trim_matches('"').to_string())
|
||||
.ok_or(OPENAI_IMAGE_INVALID_MULTIPART_DETAIL)?;
|
||||
let boundary = multipart_boundary(content_type).ok_or(OPENAI_IMAGE_INVALID_MULTIPART_DETAIL)?;
|
||||
let fields = parse_multipart_fields(request_body, &boundary);
|
||||
if fields.is_empty() {
|
||||
return Err(OPENAI_IMAGE_INVALID_MULTIPART_DETAIL);
|
||||
@@ -536,6 +535,17 @@ fn parse_multipart_fields(body: &[u8], boundary: &str) -> Vec<MultipartField> {
|
||||
parts
|
||||
}
|
||||
|
||||
fn multipart_boundary(content_type: &str) -> Option<String> {
|
||||
content_type.split(';').find_map(|segment| {
|
||||
let (key, value) = segment.trim().split_once('=')?;
|
||||
if !key.trim().eq_ignore_ascii_case("boundary") {
|
||||
return None;
|
||||
}
|
||||
let boundary = value.trim().trim_matches('"').trim();
|
||||
(!boundary.is_empty()).then(|| boundary.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_multipart_field(raw: &[u8]) -> Option<MultipartField> {
|
||||
let header_end = find_subslice(raw, b"\r\n\r\n")?;
|
||||
let headers = &raw[..header_end];
|
||||
@@ -1081,4 +1091,36 @@ mod tests {
|
||||
|
||||
assert_eq!(validation.model.as_deref(), Some("Custom/Image-Model:V1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_validation_accepts_multipart_with_mixed_case_boundary() {
|
||||
let boundary = "------------------------OYNWsMZCt0ILTwn8naP4Gb";
|
||||
let body = Bytes::from(format!(
|
||||
concat!(
|
||||
"--{boundary}\r\n",
|
||||
"Content-Disposition: form-data; name=\"model\"\r\n\r\n",
|
||||
"gpt-image-2\r\n",
|
||||
"--{boundary}\r\n",
|
||||
"Content-Disposition: form-data; name=\"prompt\"\r\n\r\n",
|
||||
"edit this image\r\n",
|
||||
"--{boundary}\r\n",
|
||||
"Content-Disposition: form-data; name=\"image\"; filename=\"image.jpg\"\r\n",
|
||||
"Content-Type: image/jpeg\r\n\r\n",
|
||||
"image-bytes\r\n",
|
||||
"--{boundary}--\r\n"
|
||||
),
|
||||
boundary = boundary,
|
||||
));
|
||||
|
||||
let validation = parse_openai_image_validation_input(
|
||||
OpenAiImageOperation::Edit,
|
||||
Some(&format!("multipart/form-data; boundary={boundary}")),
|
||||
&body,
|
||||
)
|
||||
.expect("multipart image edit should validate");
|
||||
|
||||
assert_eq!(validation.model.as_deref(), Some("gpt-image-2"));
|
||||
assert_eq!(validation.prompt.as_deref(), Some("edit this image"));
|
||||
assert_eq!(validation.image_count, 1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user