feat: refine wooden fish runtime generation
This commit is contained in:
@@ -380,17 +380,41 @@ pub(crate) async fn create_openai_image_edit(
|
||||
reference_image: &OpenAiReferenceImage,
|
||||
failure_context: &str,
|
||||
) -> Result<OpenAiGeneratedImages, AppError> {
|
||||
create_openai_image_edit_with_references(
|
||||
http_client,
|
||||
settings,
|
||||
prompt,
|
||||
negative_prompt,
|
||||
size,
|
||||
std::slice::from_ref(reference_image),
|
||||
failure_context,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn create_openai_image_edit_with_references(
|
||||
http_client: &reqwest::Client,
|
||||
settings: &OpenAiImageSettings,
|
||||
prompt: &str,
|
||||
negative_prompt: Option<&str>,
|
||||
size: &str,
|
||||
reference_images: &[OpenAiReferenceImage],
|
||||
failure_context: &str,
|
||||
) -> Result<OpenAiGeneratedImages, AppError> {
|
||||
if reference_images.is_empty() {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": VECTOR_ENGINE_PROVIDER,
|
||||
"message": format!("{failure_context}:缺少参考图"),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
let task_id = format!("vector-engine-edit-{}", current_utc_micros());
|
||||
let request_url = vector_engine_images_edit_url(settings);
|
||||
let normalized_size = normalize_image_size(size);
|
||||
let image_part = reqwest::multipart::Part::bytes(reference_image.bytes.clone())
|
||||
.file_name(reference_image.file_name.clone())
|
||||
.mime_str(reference_image.mime_type.as_str())
|
||||
.map_err(|error| {
|
||||
map_openai_image_request_error(format!("{failure_context}:构造参考图失败:{error}"))
|
||||
})?;
|
||||
let form = reqwest::multipart::Form::new()
|
||||
.part("image", image_part)
|
||||
|
||||
let mut form = reqwest::multipart::Form::new()
|
||||
.text("model", GPT_IMAGE_2_MODEL.to_string())
|
||||
.text(
|
||||
"prompt",
|
||||
@@ -398,7 +422,20 @@ pub(crate) async fn create_openai_image_edit(
|
||||
)
|
||||
.text("n", "1")
|
||||
.text("size", normalized_size.clone());
|
||||
for reference_image in reference_images {
|
||||
let image_part = reqwest::multipart::Part::bytes(reference_image.bytes.clone())
|
||||
.file_name(reference_image.file_name.clone())
|
||||
.mime_str(reference_image.mime_type.as_str())
|
||||
.map_err(|error| {
|
||||
map_openai_image_request_error(format!(
|
||||
"{failure_context}:构造参考图失败:{error}"
|
||||
))
|
||||
})?;
|
||||
form = form.part("image", image_part);
|
||||
}
|
||||
|
||||
let started_at = std::time::Instant::now();
|
||||
let reference_image_count = reference_images.len();
|
||||
let response = match http_client
|
||||
.post(request_url.as_str())
|
||||
.header(
|
||||
@@ -432,7 +469,7 @@ pub(crate) async fn create_openai_image_edit(
|
||||
None,
|
||||
Some(latency_ms),
|
||||
Some(prompt.chars().count()),
|
||||
Some(1),
|
||||
Some(reference_image_count),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
@@ -450,7 +487,7 @@ pub(crate) async fn create_openai_image_edit(
|
||||
status = response_status.as_u16(),
|
||||
prompt_chars = prompt.chars().count(),
|
||||
size = %normalized_size,
|
||||
reference_image_count = 1usize,
|
||||
reference_image_count,
|
||||
elapsed_ms = started_at.elapsed().as_millis() as u64,
|
||||
failure_context,
|
||||
"VectorEngine 图片编辑 HTTP 返回"
|
||||
@@ -478,7 +515,7 @@ pub(crate) async fn create_openai_image_edit(
|
||||
None,
|
||||
Some(latency_ms),
|
||||
Some(prompt.chars().count()),
|
||||
Some(1),
|
||||
Some(reference_image_count),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
@@ -505,7 +542,7 @@ pub(crate) async fn create_openai_image_edit(
|
||||
Some(truncate_raw(response_text.as_str())),
|
||||
Some(started_at.elapsed().as_millis() as u64),
|
||||
Some(prompt.chars().count()),
|
||||
Some(1),
|
||||
Some(reference_image_count),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
@@ -534,7 +571,7 @@ pub(crate) async fn create_openai_image_edit(
|
||||
Some(truncate_raw(response_text.as_str())),
|
||||
Some(started_at.elapsed().as_millis() as u64),
|
||||
Some(prompt.chars().count()),
|
||||
Some(1),
|
||||
Some(reference_image_count),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
@@ -565,7 +602,7 @@ pub(crate) async fn create_openai_image_edit(
|
||||
None,
|
||||
Some(download_started_at.elapsed().as_millis() as u64),
|
||||
Some(prompt.chars().count()),
|
||||
Some(1),
|
||||
Some(reference_image_count),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
@@ -597,7 +634,7 @@ pub(crate) async fn create_openai_image_edit(
|
||||
Some(truncate_raw(response_text.as_str())),
|
||||
Some(started_at.elapsed().as_millis() as u64),
|
||||
Some(prompt.chars().count()),
|
||||
Some(1),
|
||||
Some(reference_image_count),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
@@ -1100,6 +1137,32 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn vector_engine_multi_reference_edit_rejects_empty_references() {
|
||||
let settings = OpenAiImageSettings {
|
||||
base_url: "https://vector.example".to_string(),
|
||||
api_key: "test-key".to_string(),
|
||||
request_timeout_ms: 1_000_000,
|
||||
external_api_audit_state: None,
|
||||
};
|
||||
let http_client = reqwest::Client::new();
|
||||
|
||||
let result = create_openai_image_edit_with_references(
|
||||
&http_client,
|
||||
&settings,
|
||||
"提示词",
|
||||
None,
|
||||
"1:1",
|
||||
&[],
|
||||
"测试图片编辑失败",
|
||||
)
|
||||
.await;
|
||||
|
||||
let error = result.expect_err("empty references should be rejected locally");
|
||||
assert_eq!(error.status_code(), StatusCode::BAD_REQUEST);
|
||||
assert!(error.body_text().contains("缺少参考图"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn b64_json_response_decodes_png_image() {
|
||||
let images = images_from_base64(
|
||||
|
||||
Reference in New Issue
Block a user