diff --git a/server-rs/crates/platform-image/src/vector_engine/audit.rs b/server-rs/crates/platform-image/src/image_provider/audit.rs similarity index 100% rename from server-rs/crates/platform-image/src/vector_engine/audit.rs rename to server-rs/crates/platform-image/src/image_provider/audit.rs diff --git a/server-rs/crates/platform-image/src/vector_engine/budget.rs b/server-rs/crates/platform-image/src/image_provider/budget.rs similarity index 98% rename from server-rs/crates/platform-image/src/vector_engine/budget.rs rename to server-rs/crates/platform-image/src/image_provider/budget.rs index 02f099344..75831c03b 100644 --- a/server-rs/crates/platform-image/src/vector_engine/budget.rs +++ b/server-rs/crates/platform-image/src/image_provider/budget.rs @@ -86,7 +86,7 @@ pub(crate) fn request_budget_exhausted_error( reference_image_count, image_model, operation, - "VectorEngine 图片请求执行预算已耗尽" + "ImageProvider 图片请求执行预算已耗尽" ); PlatformImageError::Request { provider: VECTOR_ENGINE_PROVIDER, diff --git a/server-rs/crates/platform-image/src/vector_engine/client.rs b/server-rs/crates/platform-image/src/image_provider/client.rs similarity index 72% rename from server-rs/crates/platform-image/src/vector_engine/client.rs rename to server-rs/crates/platform-image/src/image_provider/client.rs index 24b10196a..9987f97bc 100644 --- a/server-rs/crates/platform-image/src/vector_engine/client.rs +++ b/server-rs/crates/platform-image/src/image_provider/client.rs @@ -10,31 +10,30 @@ use super::{ retry_delay_fits_request_deadline, }, constants::{ - GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_MODEL, VECTOR_ENGINE_IMAGE_EDIT_MAX_REFERENCE_IMAGES, + GPT_IMAGE_2_5_EDIT_MODEL, GPT_IMAGE_2_5_GENERATION_MODEL, GPT_IMAGE_2_C_MODEL, + GPT_IMAGE_2_MODEL, VECTOR_ENGINE_IMAGE_EDIT_MAX_REFERENCE_IMAGES, VECTOR_ENGINE_NANOBANANA_MAX_REFERENCE_IMAGES, VECTOR_ENGINE_PROVIDER, }, curl_transport::{ - map_curl_error, send_vector_engine_json_request_with_curl, - send_vector_engine_multipart_edit_request_with_curl, + map_curl_error, send_image_json_request_with_curl, + send_image_multipart_edit_request_with_curl, }, error::PlatformImageError, image_source::resolve_reference_images, request::{ - build_vector_engine_image_edit_request_log_params, - build_vector_engine_image_request_body_with_model, - build_vector_engine_nanobanana_generate_content_request_body, is_gpt_image_2_family_model, - normalize_image_size_for_model, normalize_vector_engine_image_model, - vector_engine_images_edit_url, vector_engine_images_generation_url, - vector_engine_nanobanana_generate_content_url, + build_image_edit_request_log_params, build_image_request_body_with_model, + build_nanobanana_generate_content_request_body, images_edit_url, images_generation_url, + nanobanana_generate_content_url, normalize_image_model, normalize_image_size_for_model, + resolve_image_provider, }, - response::handle_vector_engine_response, - types::{GeneratedImages, ReferenceImage, VectorEngineImageSettings}, + response::handle_image_response, + types::{GeneratedImages, ImageProviderSettings, ReferenceImage}, util::truncate_raw, }; -pub async fn create_vector_engine_image_generation( +pub async fn create_image_generation( http_client: &reqwest::Client, - settings: &VectorEngineImageSettings, + settings: &ImageProviderSettings, prompt: &str, negative_prompt: Option<&str>, size: &str, @@ -42,10 +41,10 @@ pub async fn create_vector_engine_image_generation( reference_images: &[String], failure_context: &str, ) -> Result { - create_vector_engine_image_generation_with_model( + create_image_generation_with_model( http_client, settings, - GPT_IMAGE_2_MODEL, + GPT_IMAGE_2_5_GENERATION_MODEL, prompt, negative_prompt, size, @@ -57,9 +56,9 @@ pub async fn create_vector_engine_image_generation( } #[allow(clippy::too_many_arguments)] -pub async fn create_vector_engine_image_generation_with_model( +pub async fn create_image_generation_with_model( http_client: &reqwest::Client, - settings: &VectorEngineImageSettings, + settings: &ImageProviderSettings, model: &str, prompt: &str, negative_prompt: Option<&str>, @@ -68,7 +67,13 @@ pub async fn create_vector_engine_image_generation_with_model( reference_images: &[String], failure_context: &str, ) -> Result { - let requested_model = normalize_vector_engine_image_model(model); + let requested_model = normalize_image_model(model); + resolve_image_provider(requested_model).map_err(|message| { + PlatformImageError::InvalidRequest { + provider: settings.provider.as_str(), + message: format!("{failure_context}:{message}:{requested_model}"), + } + })?; if !reference_images.is_empty() { let resolved_references = resolve_reference_images( http_client, @@ -77,7 +82,7 @@ pub async fn create_vector_engine_image_generation_with_model( settings.request_deadline, ) .await?; - return create_vector_engine_image_edit_with_references_and_model( + return create_image_edit_with_references_and_model( http_client, settings, requested_model, @@ -91,14 +96,14 @@ pub async fn create_vector_engine_image_generation_with_model( .await; } - let request_url = vector_engine_images_generation_url(settings); + let request_url = images_generation_url(settings); let normalized_size = normalize_image_size_for_model(requested_model, size); let started_at = std::time::Instant::now(); - let mut upstream_model = preferred_vector_engine_upstream_model(requested_model); + let mut upstream_model = preferred_image_upstream_model(requested_model); let mut recovered_failure_audits = Vec::new(); let mut attempt = 1; loop { - let request_body = build_vector_engine_image_request_body_with_model( + let request_body = build_image_request_body_with_model( upstream_model, prompt, negative_prompt, @@ -109,11 +114,11 @@ pub async fn create_vector_engine_image_generation_with_model( let Some(attempt_timeout_ms) = effective_request_timeout_ms(settings.request_timeout_ms, settings.request_deadline) else { - return Err(finish_vector_engine_model_fallback_error( + return Err(finish_image_model_fallback_error( request_budget_exhausted_error( request_url.as_str(), failure_context, - auditable_vector_engine_image_model(upstream_model), + auditable_image_model(upstream_model), Some(started_at.elapsed().as_millis() as u64), Some(prompt.chars().count()), Some(reference_images.len()), @@ -121,7 +126,7 @@ pub async fn create_vector_engine_image_generation_with_model( &mut recovered_failure_audits, )); }; - let response = match send_vector_engine_json_request_with_curl( + let response = match send_image_json_request_with_curl( request_url.as_str(), settings.api_key.as_str(), &request_body, @@ -130,18 +135,18 @@ pub async fn create_vector_engine_image_generation_with_model( .await { Ok(response) => { - if should_retry_vector_engine_upstream_response( + if should_retry_image_upstream_response( response.status, response.body.as_str(), attempt, ) { let primary_error = if upstream_model == GPT_IMAGE_2_MODEL { - handle_vector_engine_response( + handle_image_response( http_client, request_url.as_str(), response.status, response.body.as_str(), - auditable_vector_engine_image_model(upstream_model), + auditable_image_model(upstream_model), failure_context, started_at.elapsed().as_millis() as u64, Some(prompt.chars().count()), @@ -165,7 +170,7 @@ pub async fn create_vector_engine_image_generation_with_model( ) }) { let error = primary_error.expect("primary error checked above"); - record_vector_engine_model_fallback( + record_image_model_fallback( "generation", request_url.as_str(), upstream_model, @@ -180,7 +185,7 @@ pub async fn create_vector_engine_image_generation_with_model( attempt += 1; continue; } - if retry_vector_engine_upstream_status_after_delay( + if retry_image_upstream_status_after_delay( "generation", request_url.as_str(), attempt, @@ -201,8 +206,8 @@ pub async fn create_vector_engine_image_generation_with_model( response } Err(error) => { - if should_retry_vector_engine_curl_send_error(&error, attempt) { - if retry_vector_engine_send_after_delay( + if should_retry_image_curl_send_error(&error, attempt) { + if retry_image_send_after_delay( "generation", request_url.as_str(), "request_send", @@ -224,12 +229,12 @@ pub async fn create_vector_engine_image_generation_with_model( continue; } } - return Err(finish_vector_engine_model_fallback_error( + return Err(finish_image_model_fallback_error( map_curl_error( format!("{failure_context}:创建图片生成任务失败").as_str(), request_url.as_str(), "request_send", - auditable_vector_engine_image_model(upstream_model), + auditable_image_model(upstream_model), error, started_at.elapsed().as_millis() as u64, Some(prompt.chars().count()), @@ -253,15 +258,15 @@ pub async fn create_vector_engine_image_generation_with_model( attempt, elapsed_ms = started_at.elapsed().as_millis() as u64, failure_context, - "VectorEngine 图片生成 HTTP 返回" + "ImageProvider 图片生成 HTTP 返回" ); let response_text = response.body; - match handle_vector_engine_response( + match handle_image_response( http_client, request_url.as_str(), response_status, response_text.as_str(), - auditable_vector_engine_image_model(upstream_model), + auditable_image_model(upstream_model), failure_context, started_at.elapsed().as_millis() as u64, Some(prompt.chars().count()), @@ -287,7 +292,7 @@ pub async fn create_vector_engine_image_generation_with_model( settings, ) => { - record_vector_engine_model_fallback( + record_image_model_fallback( "generation", request_url.as_str(), upstream_model, @@ -302,7 +307,7 @@ pub async fn create_vector_engine_image_generation_with_model( attempt += 1; } Err(error) => { - return Err(finish_vector_engine_model_fallback_error( + return Err(finish_image_model_fallback_error( error, &mut recovered_failure_audits, )); @@ -312,9 +317,9 @@ pub async fn create_vector_engine_image_generation_with_model( } #[allow(clippy::too_many_arguments)] -pub async fn create_vector_engine_nanobanana_generate_content( +pub async fn create_nanobanana_generate_content( http_client: &reqwest::Client, - settings: &VectorEngineImageSettings, + settings: &ImageProviderSettings, model: &str, prompt: &str, negative_prompt: Option<&str>, @@ -323,6 +328,11 @@ pub async fn create_vector_engine_nanobanana_generate_content( reference_images: &[ReferenceImage], failure_context: &str, ) -> Result { + let model = normalize_image_model(model); + resolve_image_provider(model).map_err(|message| PlatformImageError::InvalidRequest { + provider: settings.provider.as_str(), + message: format!("{failure_context}:{message}:{model}"), + })?; if reference_images.len() > VECTOR_ENGINE_NANOBANANA_MAX_REFERENCE_IMAGES { return Err(PlatformImageError::InvalidRequest { provider: VECTOR_ENGINE_PROVIDER, @@ -332,9 +342,8 @@ pub async fn create_vector_engine_nanobanana_generate_content( ), }); } - let model = normalize_vector_engine_image_model(model); - let request_url = vector_engine_nanobanana_generate_content_url(settings, model); - let request_body = build_vector_engine_nanobanana_generate_content_request_body( + let request_url = nanobanana_generate_content_url(settings, model); + let request_body = build_nanobanana_generate_content_request_body( prompt, negative_prompt, aspect_ratio, @@ -373,7 +382,7 @@ pub async fn create_vector_engine_nanobanana_generate_content( Some(reference_image_count), )); }; - match send_vector_engine_json_request_with_curl( + match send_image_json_request_with_curl( request_url.as_str(), settings.api_key.as_str(), &request_body, @@ -382,12 +391,12 @@ pub async fn create_vector_engine_nanobanana_generate_content( .await { Ok(response) => { - if should_retry_vector_engine_upstream_response( + if should_retry_image_upstream_response( response.status, response.body.as_str(), attempt, ) { - if retry_vector_engine_upstream_status_after_delay( + if retry_image_upstream_status_after_delay( "nanobanana_generate_content", request_url.as_str(), attempt, @@ -408,8 +417,8 @@ pub async fn create_vector_engine_nanobanana_generate_content( break response; } Err(error) => { - if should_retry_vector_engine_curl_send_error(&error, attempt) { - if retry_vector_engine_send_after_delay( + if should_retry_image_curl_send_error(&error, attempt) { + if retry_image_send_after_delay( "nanobanana_generate_content", request_url.as_str(), "request_send", @@ -460,10 +469,10 @@ pub async fn create_vector_engine_nanobanana_generate_content( attempt, elapsed_ms = started_at.elapsed().as_millis() as u64, failure_context, - "VectorEngine nanobanana2 图片生成 HTTP 返回" + "ImageProvider nanobanana2 图片生成 HTTP 返回" ); let response_text = response.body; - handle_vector_engine_response( + handle_image_response( http_client, request_url.as_str(), response_status, @@ -480,16 +489,16 @@ pub async fn create_vector_engine_nanobanana_generate_content( .await } -pub async fn create_vector_engine_image_edit( +pub async fn create_image_edit( http_client: &reqwest::Client, - settings: &VectorEngineImageSettings, + settings: &ImageProviderSettings, prompt: &str, negative_prompt: Option<&str>, size: &str, reference_image: &ReferenceImage, failure_context: &str, ) -> Result { - create_vector_engine_image_edit_with_references( + create_image_edit_with_references( http_client, settings, prompt, @@ -502,9 +511,9 @@ pub async fn create_vector_engine_image_edit( .await } -pub async fn create_vector_engine_image_edit_with_references( +pub async fn create_image_edit_with_references( http_client: &reqwest::Client, - settings: &VectorEngineImageSettings, + settings: &ImageProviderSettings, prompt: &str, negative_prompt: Option<&str>, size: &str, @@ -512,10 +521,10 @@ pub async fn create_vector_engine_image_edit_with_references( reference_images: &[ReferenceImage], failure_context: &str, ) -> Result { - create_vector_engine_image_edit_with_references_and_model( + create_image_edit_with_references_and_model( http_client, settings, - GPT_IMAGE_2_MODEL, + GPT_IMAGE_2_5_EDIT_MODEL, prompt, negative_prompt, size, @@ -527,9 +536,9 @@ pub async fn create_vector_engine_image_edit_with_references( } #[allow(clippy::too_many_arguments)] -pub async fn create_vector_engine_image_edit_with_references_and_model( +pub async fn create_image_edit_with_references_and_model( http_client: &reqwest::Client, - settings: &VectorEngineImageSettings, + settings: &ImageProviderSettings, model: &str, prompt: &str, negative_prompt: Option<&str>, @@ -538,7 +547,13 @@ pub async fn create_vector_engine_image_edit_with_references_and_model( reference_images: &[ReferenceImage], failure_context: &str, ) -> Result { - let requested_model = normalize_vector_engine_image_model(model); + let requested_model = normalize_image_model(model); + resolve_image_provider(requested_model).map_err(|message| { + PlatformImageError::InvalidRequest { + provider: settings.provider.as_str(), + message: format!("{failure_context}:{message}:{requested_model}"), + } + })?; if reference_images.is_empty() { return Err(PlatformImageError::InvalidRequest { provider: VECTOR_ENGINE_PROVIDER, @@ -555,18 +570,18 @@ pub async fn create_vector_engine_image_edit_with_references_and_model( }); } - let request_url = vector_engine_images_edit_url(settings); + let request_url = images_edit_url(settings); let normalized_size = normalize_image_size_for_model(requested_model, size); let reference_image_count = reference_images.len(); let reference_image_bytes_total: usize = reference_images.iter().map(|image| image.bytes.len()).sum(); let started_at = std::time::Instant::now(); - let mut upstream_model = preferred_vector_engine_upstream_model(requested_model); + let mut upstream_model = preferred_image_upstream_model(requested_model); let mut recovered_failure_audits = Vec::new(); let mut attempt = 1; loop { - let request_params = build_vector_engine_image_edit_request_log_params( + let request_params = build_image_edit_request_log_params( upstream_model, prompt, negative_prompt, @@ -594,16 +609,16 @@ pub async fn create_vector_engine_image_edit_with_references_and_model( request_params = %request_params, attempt, failure_context, - "VectorEngine 图片编辑请求参数" + "ImageProvider 图片编辑请求参数" ); let Some(attempt_timeout_ms) = effective_request_timeout_ms(settings.request_timeout_ms, settings.request_deadline) else { - return Err(finish_vector_engine_model_fallback_error( + return Err(finish_image_model_fallback_error( request_budget_exhausted_error( request_url.as_str(), failure_context, - auditable_vector_engine_image_model(upstream_model), + auditable_image_model(upstream_model), Some(started_at.elapsed().as_millis() as u64), Some(prompt.chars().count()), Some(reference_image_count), @@ -611,7 +626,7 @@ pub async fn create_vector_engine_image_edit_with_references_and_model( &mut recovered_failure_audits, )); }; - let response = match send_vector_engine_multipart_edit_request_with_curl( + let response = match send_image_multipart_edit_request_with_curl( request_url.as_str(), settings.api_key.as_str(), upstream_model, @@ -625,18 +640,18 @@ pub async fn create_vector_engine_image_edit_with_references_and_model( .await { Ok(response) => { - if should_retry_vector_engine_upstream_response( + if should_retry_image_upstream_response( response.status, response.body.as_str(), attempt, ) { let primary_error = if upstream_model == GPT_IMAGE_2_MODEL { - handle_vector_engine_response( + handle_image_response( http_client, request_url.as_str(), response.status, response.body.as_str(), - auditable_vector_engine_image_model(upstream_model), + auditable_image_model(upstream_model), failure_context, started_at.elapsed().as_millis() as u64, Some(prompt.chars().count()), @@ -660,7 +675,7 @@ pub async fn create_vector_engine_image_edit_with_references_and_model( ) }) { let error = primary_error.expect("primary error checked above"); - record_vector_engine_model_fallback( + record_image_model_fallback( "edit", request_url.as_str(), upstream_model, @@ -675,7 +690,7 @@ pub async fn create_vector_engine_image_edit_with_references_and_model( attempt += 1; continue; } - if retry_vector_engine_upstream_status_after_delay( + if retry_image_upstream_status_after_delay( "edit", request_url.as_str(), attempt, @@ -696,8 +711,8 @@ pub async fn create_vector_engine_image_edit_with_references_and_model( response } Err(error) => { - if should_retry_vector_engine_curl_send_error(&error, attempt) { - if retry_vector_engine_send_after_delay( + if should_retry_image_curl_send_error(&error, attempt) { + if retry_image_send_after_delay( "edit", request_url.as_str(), "request_send", @@ -719,12 +734,12 @@ pub async fn create_vector_engine_image_edit_with_references_and_model( continue; } } - return Err(finish_vector_engine_model_fallback_error( + return Err(finish_image_model_fallback_error( map_curl_error( format!("{failure_context}:创建图片编辑任务失败").as_str(), request_url.as_str(), "request_send", - auditable_vector_engine_image_model(upstream_model), + auditable_image_model(upstream_model), error, started_at.elapsed().as_millis() as u64, Some(prompt.chars().count()), @@ -750,15 +765,15 @@ pub async fn create_vector_engine_image_edit_with_references_and_model( attempt, elapsed_ms = started_at.elapsed().as_millis() as u64, failure_context, - "VectorEngine 图片编辑 HTTP 返回" + "ImageProvider 图片编辑 HTTP 返回" ); let response_text = response.body; - match handle_vector_engine_response( + match handle_image_response( http_client, request_url.as_str(), response_status, response_text.as_str(), - auditable_vector_engine_image_model(upstream_model), + auditable_image_model(upstream_model), failure_context, started_at.elapsed().as_millis() as u64, Some(prompt.chars().count()), @@ -784,7 +799,7 @@ pub async fn create_vector_engine_image_edit_with_references_and_model( settings, ) => { - record_vector_engine_model_fallback( + record_image_model_fallback( "edit", request_url.as_str(), upstream_model, @@ -799,7 +814,7 @@ pub async fn create_vector_engine_image_edit_with_references_and_model( attempt += 1; } Err(error) => { - return Err(finish_vector_engine_model_fallback_error( + return Err(finish_image_model_fallback_error( error, &mut recovered_failure_audits, )); @@ -808,22 +823,21 @@ pub async fn create_vector_engine_image_edit_with_references_and_model( } } -fn preferred_vector_engine_upstream_model(requested_model: &str) -> &str { - if is_gpt_image_2_family_model(requested_model) { - GPT_IMAGE_2_MODEL - } else { - requested_model - } +fn preferred_image_upstream_model(requested_model: &str) -> &str { + // Provider routing is selected by api-server at the task boundary. Keep the + // concrete value intact here so retries stay on the same model and legacy + // persisted values are never silently rewritten. + requested_model } -fn finish_vector_engine_model_fallback_error( +fn finish_image_model_fallback_error( error: PlatformImageError, recovered_failure_audits: &mut Vec, ) -> PlatformImageError { error.with_recovered_failure_audits(std::mem::take(recovered_failure_audits)) } -fn auditable_vector_engine_image_model(model: &str) -> Option<&'static str> { +fn auditable_image_model(model: &str) -> Option<&'static str> { match model { GPT_IMAGE_2_C_MODEL => Some(GPT_IMAGE_2_C_MODEL), GPT_IMAGE_2_MODEL => Some(GPT_IMAGE_2_MODEL), @@ -836,74 +850,15 @@ fn should_fallback_to_gpt_image_2_c( upstream_model: &str, attempt: u32, error: &PlatformImageError, - settings: &VectorEngineImageSettings, + settings: &ImageProviderSettings, ) -> bool { - if !is_gpt_image_2_family_model(requested_model) - || upstream_model != GPT_IMAGE_2_MODEL - || attempt >= VECTOR_ENGINE_SEND_MAX_ATTEMPTS - || effective_request_timeout_ms(settings.request_timeout_ms, settings.request_deadline) - .is_none() - { - return false; - } - - match error { - PlatformImageError::Upstream { - upstream_status, - message, - raw_excerpt, - .. - } => match *upstream_status { - 408 => true, - 429 => !contains_vector_engine_content_rejection(message, raw_excerpt), - status if status >= 500 => true, - 400 | 404 | 422 => contains_vector_engine_model_unavailable(message, raw_excerpt), - _ => false, - }, - PlatformImageError::ResponseParse { - message, - raw_excerpt, - .. - } => !contains_vector_engine_content_rejection(message, raw_excerpt), - PlatformImageError::MissingImage { message, audit, .. } => { - let raw_excerpt = audit - .as_ref() - .and_then(|audit| audit.raw_excerpt.as_deref()) - .unwrap_or_default(); - !contains_vector_engine_content_rejection(message, raw_excerpt) - } - PlatformImageError::InvalidConfig { .. } - | PlatformImageError::InvalidRequest { .. } - | PlatformImageError::Request { .. } - | PlatformImageError::FallbackFailed { .. } => false, - } + // Compatibility function retained for old call sites/tests. New dispatch + // never crosses models; retries are handled by the same-model loop only. + let _ = (requested_model, upstream_model, attempt, error, settings); + false } -fn contains_vector_engine_model_unavailable(message: &str, raw_excerpt: &str) -> bool { - let haystack = format!("{message}\n{raw_excerpt}").to_ascii_lowercase(); - let mentions_model = haystack.contains("model") - || haystack.contains("模型") - || haystack.contains(GPT_IMAGE_2_MODEL) - || haystack.contains(GPT_IMAGE_2_C_MODEL); - let unavailable = [ - "not found", - "not supported", - "unsupported", - "unavailable", - "does not exist", - "invalid model", - "unknown model", - "不存在", - "不支持", - "不可用", - "未开通", - ] - .iter() - .any(|marker| haystack.contains(marker)); - mentions_model && unavailable -} - -fn contains_vector_engine_content_rejection(message: &str, raw_excerpt: &str) -> bool { +fn contains_image_provider_content_rejection(message: &str, raw_excerpt: &str) -> bool { let haystack = format!("{message}\n{raw_excerpt}").to_ascii_lowercase(); [ "invalid_prompt", @@ -928,7 +883,7 @@ fn contains_vector_engine_content_rejection(message: &str, raw_excerpt: &str) -> .any(|marker| haystack.contains(marker)) } -fn record_vector_engine_model_fallback( +fn record_image_model_fallback( request_kind: &'static str, request_url: &str, from_model: &str, @@ -949,26 +904,26 @@ fn record_vector_engine_model_fallback( failure_stage = audit.map(|audit| audit.failure_stage).unwrap_or("unknown"), status = audit.and_then(|audit| audit.status_code).unwrap_or_default(), error = %error.message(), - "VectorEngine 首选图片模型失败,切换兼容模型" + "ImageProvider 首选图片模型失败,切换兼容模型" ); } -fn should_retry_vector_engine_curl_send_error( - error: &super::curl_transport::VectorEngineCurlError, +fn should_retry_image_curl_send_error( + error: &super::curl_transport::ImageProviderCurlError, attempt: u32, ) -> bool { attempt < VECTOR_ENGINE_SEND_MAX_ATTEMPTS && (error.is_timeout() || error.is_connect() || error.is_transient_transport()) } -fn should_retry_vector_engine_upstream_response(status: u16, raw_body: &str, attempt: u32) -> bool { +fn should_retry_image_upstream_response(status: u16, raw_body: &str, attempt: u32) -> bool { attempt < VECTOR_ENGINE_SEND_MAX_ATTEMPTS && (status == 408 || status >= 500 - || (status == 429 && !contains_vector_engine_content_rejection("", raw_body))) + || (status == 429 && !contains_image_provider_content_rejection("", raw_body))) } -async fn retry_vector_engine_send_after_delay( +async fn retry_image_send_after_delay( request_kind: &'static str, request_url: &str, failure_stage: &'static str, @@ -984,7 +939,8 @@ async fn retry_vector_engine_send_after_delay( request_params: Option<&serde_json::Value>, request_deadline: Option, ) -> bool { - let delay_ms = vector_engine_send_retry_delay_ms(attempt, vector_engine_send_retry_jitter_ms()); + let delay_ms = + image_provider_send_retry_delay_ms(attempt, image_provider_send_retry_jitter_ms()); if !retry_delay_fits_request_deadline(request_deadline, delay_ms) { tracing::warn!( provider = VECTOR_ENGINE_PROVIDER, @@ -1003,7 +959,7 @@ async fn retry_vector_engine_send_after_delay( elapsed_ms, prompt_chars, reference_image_count, - "VectorEngine 图片请求剩余预算不足,停止重试" + "ImageProvider 图片请求剩余预算不足,停止重试" ); return false; } @@ -1027,13 +983,13 @@ async fn retry_vector_engine_send_after_delay( request_params = %request_params .map(|value| value.to_string()) .unwrap_or_default(), - "VectorEngine 图片请求发送失败,准备重试" + "ImageProvider 图片请求发送失败,准备重试" ); tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await; true } -async fn retry_vector_engine_upstream_status_after_delay( +async fn retry_image_upstream_status_after_delay( request_kind: &'static str, request_url: &str, attempt: u32, @@ -1045,7 +1001,8 @@ async fn retry_vector_engine_upstream_status_after_delay( request_params: Option<&serde_json::Value>, request_deadline: Option, ) -> bool { - let delay_ms = vector_engine_send_retry_delay_ms(attempt, vector_engine_send_retry_jitter_ms()); + let delay_ms = + image_provider_send_retry_delay_ms(attempt, image_provider_send_retry_jitter_ms()); if !retry_delay_fits_request_deadline(request_deadline, delay_ms) { tracing::warn!( provider = VECTOR_ENGINE_PROVIDER, @@ -1060,7 +1017,7 @@ async fn retry_vector_engine_upstream_status_after_delay( elapsed_ms, prompt_chars, reference_image_count, - "VectorEngine 图片请求剩余预算不足,停止上游状态重试" + "ImageProvider 图片请求剩余预算不足,停止上游状态重试" ); return false; } @@ -1081,19 +1038,19 @@ async fn retry_vector_engine_upstream_status_after_delay( request_params = %request_params .map(|value| value.to_string()) .unwrap_or_default(), - "VectorEngine 图片上游状态可重试,准备重试" + "ImageProvider 图片上游状态可重试,准备重试" ); tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await; true } -fn vector_engine_send_retry_delay_ms(attempt: u32, jitter_ms: u64) -> u64 { +fn image_provider_send_retry_delay_ms(attempt: u32, jitter_ms: u64) -> u64 { let exponential_factor = 1_u64 << attempt.saturating_sub(1).min(10); let bounded_jitter_ms = jitter_ms.min(VECTOR_ENGINE_SEND_RETRY_MAX_JITTER_MS); VECTOR_ENGINE_SEND_RETRY_BASE_DELAY_MS * exponential_factor + bounded_jitter_ms } -fn vector_engine_send_retry_jitter_ms() -> u64 { +fn image_provider_send_retry_jitter_ms() -> u64 { let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|duration| duration.subsec_nanos()) @@ -1103,6 +1060,7 @@ fn vector_engine_send_retry_jitter_ms() -> u64 { #[cfg(test)] mod tests { + use super::super::types::ImageProvider; use super::*; fn reference_image(index: usize) -> ReferenceImage { @@ -1113,8 +1071,9 @@ mod tests { } } - fn test_settings() -> VectorEngineImageSettings { - VectorEngineImageSettings { + fn test_settings() -> ImageProviderSettings { + ImageProviderSettings { + provider: ImageProvider::VectorEngine, base_url: "http://127.0.0.1:9".to_string(), api_key: "test-key".to_string(), request_timeout_ms: 1_000, @@ -1125,7 +1084,7 @@ mod tests { #[tokio::test] async fn gpt_image_edit_rejects_six_references_before_network_send() { let references = (0..6).map(reference_image).collect::>(); - let error = create_vector_engine_image_edit_with_references_and_model( + let error = create_image_edit_with_references_and_model( &reqwest::Client::new(), &test_settings(), GPT_IMAGE_2_MODEL, @@ -1145,7 +1104,7 @@ mod tests { #[tokio::test] async fn nanobanana_rejects_fifteen_references_before_network_send() { let references = (0..15).map(reference_image).collect::>(); - let error = create_vector_engine_nanobanana_generate_content( + let error = create_nanobanana_generate_content( &reqwest::Client::new(), &test_settings(), super::super::constants::NANOBANANA_2_MODEL, @@ -1164,13 +1123,14 @@ mod tests { #[tokio::test] async fn expired_deadline_stops_generation_before_network_send() { - let settings = VectorEngineImageSettings { + let settings = ImageProviderSettings { + provider: ImageProvider::VectorEngine, base_url: "http://127.0.0.1:9".to_string(), api_key: "test-key".to_string(), request_timeout_ms: 1_000, request_deadline: Some(Instant::now()), }; - let error = create_vector_engine_image_generation( + let error = create_image_generation( &reqwest::Client::new(), &settings, "测试提示词", @@ -1197,132 +1157,77 @@ mod tests { } #[test] - fn vector_engine_send_retry_policy_allows_four_retries_before_final_attempt() { + fn image_provider_send_retry_policy_allows_four_retries_before_final_attempt() { assert_eq!(VECTOR_ENGINE_SEND_MAX_ATTEMPTS, 5); } #[test] - fn vector_engine_send_retry_policy_treats_ssl_reset_as_transient_transport() { - let error = super::super::curl_transport::VectorEngineCurlError::Curl(curl::Error::new(35)); + fn image_provider_send_retry_policy_treats_ssl_reset_as_transient_transport() { + let error = + super::super::curl_transport::ImageProviderCurlError::Curl(curl::Error::new(35)); assert!(error.is_transient_transport()); - assert!(should_retry_vector_engine_curl_send_error(&error, 1)); - assert!(!should_retry_vector_engine_curl_send_error(&error, 5)); + assert!(should_retry_image_curl_send_error(&error, 1)); + assert!(!should_retry_image_curl_send_error(&error, 5)); } #[test] - fn vector_engine_send_retry_policy_treats_recv_eof_as_transient_transport() { - let error = super::super::curl_transport::VectorEngineCurlError::Curl(curl::Error::new(56)); + fn image_provider_send_retry_policy_treats_recv_eof_as_transient_transport() { + let error = + super::super::curl_transport::ImageProviderCurlError::Curl(curl::Error::new(56)); assert!(error.is_transient_transport()); - assert!(should_retry_vector_engine_curl_send_error(&error, 1)); - assert!(!should_retry_vector_engine_curl_send_error(&error, 5)); + assert!(should_retry_image_curl_send_error(&error, 1)); + assert!(!should_retry_image_curl_send_error(&error, 5)); } #[test] - fn vector_engine_send_retry_policy_treats_upstream_502_as_retryable() { - assert!(should_retry_vector_engine_upstream_response(502, "", 1)); - assert!(should_retry_vector_engine_upstream_response(429, "", 1)); - assert!(should_retry_vector_engine_upstream_response( + fn image_provider_send_retry_policy_treats_upstream_502_as_retryable() { + assert!(should_retry_image_upstream_response(502, "", 1)); + assert!(should_retry_image_upstream_response(429, "", 1)); + assert!(should_retry_image_upstream_response( 429, "request rejected due to rate limit", 1, )); - assert!(should_retry_vector_engine_upstream_response(408, "", 1)); - assert!(!should_retry_vector_engine_upstream_response( + assert!(should_retry_image_upstream_response(408, "", 1)); + assert!(!should_retry_image_upstream_response( 429, "内容审核拒绝", 1, )); - assert!(!should_retry_vector_engine_upstream_response(400, "", 1)); - assert!(!should_retry_vector_engine_upstream_response(502, "", 5)); + assert!(!should_retry_image_upstream_response(400, "", 1)); + assert!(!should_retry_image_upstream_response(502, "", 5)); } #[test] - fn model_fallback_only_accepts_eligible_provider_failures() { - let settings = VectorEngineImageSettings { + fn image_provider_never_falls_back_across_image_models() { + let settings = ImageProviderSettings { + provider: ImageProvider::VectorEngine, base_url: "https://vector.example/v1".to_string(), api_key: "test-key".to_string(), request_timeout_ms: 1_000, request_deadline: None, }; - let unsupported_model = PlatformImageError::Upstream { + let error = PlatformImageError::InvalidRequest { provider: VECTOR_ENGINE_PROVIDER, - message: "model gpt-image-2 is not supported".to_string(), - upstream_status: 400, - raw_excerpt: "unknown model".to_string(), - audit: None, + message: "model unavailable".to_string(), }; - let content_rejection = PlatformImageError::Upstream { - provider: VECTOR_ENGINE_PROVIDER, - message: "moderation blocked".to_string(), - upstream_status: 429, - raw_excerpt: "invalid_prompt".to_string(), - audit: None, - }; - let uncertain_send_failure = PlatformImageError::Request { - provider: VECTOR_ENGINE_PROVIDER, - message: "send failed".to_string(), - endpoint: None, - timeout: true, - connect: false, - request: true, - body: false, - status_code: None, - source: None, - audit: None, - }; - - assert!(should_fallback_to_gpt_image_2_c( - GPT_IMAGE_2_MODEL, - GPT_IMAGE_2_MODEL, - 1, - &unsupported_model, - &settings, - )); assert!(!should_fallback_to_gpt_image_2_c( GPT_IMAGE_2_MODEL, GPT_IMAGE_2_MODEL, 1, - &content_rejection, - &settings, - )); - let rate_limit_rejection = PlatformImageError::Upstream { - provider: VECTOR_ENGINE_PROVIDER, - message: "request rejected due to rate limit".to_string(), - upstream_status: 429, - raw_excerpt: "rate_limit_exceeded".to_string(), - audit: None, - }; - assert!(should_fallback_to_gpt_image_2_c( - GPT_IMAGE_2_MODEL, - GPT_IMAGE_2_MODEL, - 1, - &rate_limit_rejection, - &settings, - )); - assert!(!should_fallback_to_gpt_image_2_c( - GPT_IMAGE_2_MODEL, - GPT_IMAGE_2_MODEL, - 1, - &uncertain_send_failure, - &settings, - )); - assert!(!should_fallback_to_gpt_image_2_c( - GPT_IMAGE_2_MODEL, - GPT_IMAGE_2_MODEL, - VECTOR_ENGINE_SEND_MAX_ATTEMPTS, - &unsupported_model, + &error, &settings, )); } #[test] - fn vector_engine_send_retry_delay_uses_exponential_backoff_with_bounded_jitter() { - assert_eq!(vector_engine_send_retry_delay_ms(1, 0), 500); - assert_eq!(vector_engine_send_retry_delay_ms(2, 0), 1_000); - assert_eq!(vector_engine_send_retry_delay_ms(3, 0), 2_000); - assert_eq!(vector_engine_send_retry_delay_ms(4, 0), 4_000); - assert_eq!(vector_engine_send_retry_delay_ms(4, 999), 4_999); + fn image_provider_send_retry_delay_uses_exponential_backoff_with_bounded_jitter() { + assert_eq!(image_provider_send_retry_delay_ms(1, 0), 500); + assert_eq!(image_provider_send_retry_delay_ms(2, 0), 1_000); + assert_eq!(image_provider_send_retry_delay_ms(3, 0), 2_000); + assert_eq!(image_provider_send_retry_delay_ms(4, 0), 4_000); + assert_eq!(image_provider_send_retry_delay_ms(4, 999), 4_999); } } diff --git a/server-rs/crates/platform-image/src/vector_engine/constants.rs b/server-rs/crates/platform-image/src/image_provider/constants.rs similarity index 56% rename from server-rs/crates/platform-image/src/vector_engine/constants.rs rename to server-rs/crates/platform-image/src/image_provider/constants.rs index 2ae789387..8c101ae71 100644 --- a/server-rs/crates/platform-image/src/vector_engine/constants.rs +++ b/server-rs/crates/platform-image/src/image_provider/constants.rs @@ -1,8 +1,14 @@ pub const GPT_IMAGE_2_MODEL: &str = "gpt-image-2"; pub const GPT_IMAGE_2_C_MODEL: &str = "gpt-image-2-c"; +/// Current business model exposed to callers for new image tasks. +pub const GPT_IMAGE_2_5_BUSINESS_NAME: &str = "gpt-image-2.5"; +/// Provider/pricing key for new generation tasks. +pub const GPT_IMAGE_2_5_GENERATION_MODEL: &str = "gpt-image-2.5-flare-c"; +/// Provider/pricing key for explicit image-edit tasks. +pub const GPT_IMAGE_2_5_EDIT_MODEL: &str = "gpt-image-2.5-sunburst-c"; pub const NANOBANANA_2_MODEL: &str = "gemini-3.1-flash-image-preview"; -pub const VECTOR_ENGINE_GPT_IMAGE_2_MODEL: &str = GPT_IMAGE_2_MODEL; pub const VECTOR_ENGINE_PROVIDER: &str = "vector-engine"; +pub const TIANTOKEN_PROVIDER: &str = "tiantoken"; pub const VECTOR_ENGINE_IMAGE_EDIT_MAX_REFERENCE_IMAGES: usize = 5; pub const VECTOR_ENGINE_NANOBANANA_MAX_REFERENCE_IMAGES: usize = 14; pub const GPT_IMAGE_2_MIN_PIXELS: u64 = 655_360; diff --git a/server-rs/crates/platform-image/src/vector_engine/curl_transport.rs b/server-rs/crates/platform-image/src/image_provider/curl_transport.rs similarity index 88% rename from server-rs/crates/platform-image/src/vector_engine/curl_transport.rs rename to server-rs/crates/platform-image/src/image_provider/curl_transport.rs index fbe94e1b1..b1865fd02 100644 --- a/server-rs/crates/platform-image/src/vector_engine/curl_transport.rs +++ b/server-rs/crates/platform-image/src/image_provider/curl_transport.rs @@ -12,19 +12,19 @@ use super::{ }; #[derive(Debug)] -pub(crate) struct VectorEngineCurlResponse { +pub(crate) struct ImageProviderCurlResponse { pub(crate) status: u16, pub(crate) body: String, } #[derive(Debug)] -pub(crate) enum VectorEngineCurlError { +pub(crate) enum ImageProviderCurlError { Curl(curl::Error), Form(FormError), WorkerJoin(tokio::task::JoinError), } -impl VectorEngineCurlError { +impl ImageProviderCurlError { pub(crate) fn is_timeout(&self) -> bool { match self { Self::Curl(error) => error.is_operation_timedout(), @@ -63,7 +63,7 @@ impl VectorEngineCurlError { } } -impl fmt::Display for VectorEngineCurlError { +impl fmt::Display for ImageProviderCurlError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Curl(error) => write!(formatter, "{error}"), @@ -73,26 +73,26 @@ impl fmt::Display for VectorEngineCurlError { } } -impl Error for VectorEngineCurlError {} +impl Error for ImageProviderCurlError {} -impl From for VectorEngineCurlError { +impl From for ImageProviderCurlError { fn from(error: curl::Error) -> Self { Self::Curl(error) } } -impl From for VectorEngineCurlError { +impl From for ImageProviderCurlError { fn from(error: FormError) -> Self { Self::Form(error) } } -pub(crate) async fn send_vector_engine_json_request_with_curl( +pub(crate) async fn send_image_json_request_with_curl( request_url: &str, api_key: &str, request_body: &Value, timeout_ms: u64, -) -> Result { +) -> Result { let request_url = request_url.to_string(); let api_key = api_key.to_string(); let request_body = request_body.to_string(); @@ -105,11 +105,11 @@ pub(crate) async fn send_vector_engine_json_request_with_curl( ) }) .await - .map_err(VectorEngineCurlError::WorkerJoin)? + .map_err(ImageProviderCurlError::WorkerJoin)? } #[allow(clippy::too_many_arguments)] -pub(crate) async fn send_vector_engine_multipart_edit_request_with_curl( +pub(crate) async fn send_image_multipart_edit_request_with_curl( request_url: &str, api_key: &str, model: &str, @@ -119,7 +119,7 @@ pub(crate) async fn send_vector_engine_multipart_edit_request_with_curl( candidate_count: u32, reference_images: &[ReferenceImage], timeout_ms: u64, -) -> Result { +) -> Result { let request_url = request_url.to_string(); let api_key = api_key.to_string(); let model = model.to_string(); @@ -141,7 +141,7 @@ pub(crate) async fn send_vector_engine_multipart_edit_request_with_curl( ) }) .await - .map_err(VectorEngineCurlError::WorkerJoin)? + .map_err(ImageProviderCurlError::WorkerJoin)? } pub(crate) fn map_curl_error( @@ -149,7 +149,7 @@ pub(crate) fn map_curl_error( request_url: &str, failure_stage: &'static str, image_model: Option<&'static str>, - error: VectorEngineCurlError, + error: ImageProviderCurlError, latency_ms: u64, prompt_chars: Option, reference_image_count: Option, @@ -195,7 +195,7 @@ pub(crate) fn map_curl_error( request_params = %request_params .map(|value| value.to_string()) .unwrap_or_default(), - "VectorEngine 图片 libcurl 请求失败" + "ImageProvider 图片 libcurl 请求失败" ); PlatformImageError::Request { @@ -217,8 +217,8 @@ fn send_json_request_with_curl_blocking( api_key: &str, request_body: &str, timeout_ms: u64, -) -> Result { - let mut headers = vector_engine_curl_headers(api_key)?; +) -> Result { + let mut headers = image_curl_headers(api_key)?; headers.append("Content-Type: application/json")?; let mut easy = Easy::new(); easy.url(request_url)?; @@ -240,7 +240,7 @@ fn send_multipart_edit_request_with_curl_blocking( candidate_count: u32, reference_images: &[ReferenceImage], timeout_ms: u64, -) -> Result { +) -> Result { let mut form = Form::new(); form.part("model").contents(model.as_bytes()).add()?; form.part("prompt") @@ -263,7 +263,7 @@ fn send_multipart_edit_request_with_curl_blocking( .add()?; } - let headers = vector_engine_curl_headers(api_key)?; + let headers = image_curl_headers(api_key)?; let mut easy = Easy::new(); easy.url(request_url)?; easy.httppost(form)?; @@ -272,14 +272,14 @@ fn send_multipart_edit_request_with_curl_blocking( Ok(perform_curl_request(easy)?) } -fn vector_engine_curl_headers(api_key: &str) -> Result { +fn image_curl_headers(api_key: &str) -> Result { let mut headers = List::new(); headers.append(format!("Authorization: Bearer {api_key}").as_str())?; headers.append("Accept: application/json")?; Ok(headers) } -fn perform_curl_request(mut easy: Easy) -> Result { +fn perform_curl_request(mut easy: Easy) -> Result { let mut body = Vec::new(); { let mut transfer = easy.transfer(); @@ -291,13 +291,13 @@ fn perform_curl_request(mut easy: Easy) -> Result Result { validate_raw_image_edit_dimensions(options.width, options.height) .map_err(|error| invalid_input(failure_context, error.to_string()))?; - let url = vector_engine_images_edit_url(settings); + let url = images_edit_url(settings); let started_at = Instant::now(); let prompt_chars = Some(prompt.chars().count()); let reference_image_count = Some(1_usize + usize::from(options.mask.is_some())); @@ -137,7 +137,7 @@ pub async fn create_vector_engine_raw_image_edit( return Err(request_budget_exhausted_error( url.as_str(), failure_context, - Some(GPT_IMAGE_2_MODEL), + Some(GPT_IMAGE_2_5_EDIT_MODEL), Some(started_at.elapsed().as_millis() as u64), prompt_chars, reference_image_count, @@ -149,7 +149,7 @@ pub async fn create_vector_engine_raw_image_edit( mime_type: image_mime_type, } = image; let mut form = Form::new() - .text("model", GPT_IMAGE_2_MODEL.to_string()) + .text("model", GPT_IMAGE_2_5_EDIT_MODEL.to_string()) .text("n", "1".to_string()) .text("prompt", prompt.to_string()) .text("size", format!("{}x{}", options.width, options.height)) @@ -209,7 +209,7 @@ pub async fn create_vector_engine_raw_image_edit( reference_image_count, elapsed_ms = started_at.elapsed().as_millis() as u64, failure_context, - "VectorEngine Raw 图片编辑 HTTP 返回" + "ImageProvider Raw 图片编辑 HTTP 返回" ); let body = response.text().await.map_err(|error| { request_error( @@ -242,7 +242,7 @@ pub async fn create_vector_engine_raw_image_edit( Some(started_at.elapsed().as_millis() as u64), prompt_chars, reference_image_count, - Some(GPT_IMAGE_2_MODEL), + Some(GPT_IMAGE_2_5_EDIT_MODEL), ); return Err(PlatformImageError::Upstream { provider: VECTOR_ENGINE_PROVIDER, @@ -271,7 +271,7 @@ pub async fn create_vector_engine_raw_image_edit( Some(started_at.elapsed().as_millis() as u64), prompt_chars, reference_image_count, - Some(GPT_IMAGE_2_MODEL), + Some(GPT_IMAGE_2_5_EDIT_MODEL), ); return Err(PlatformImageError::ResponseParse { provider: VECTOR_ENGINE_PROVIDER, @@ -298,7 +298,7 @@ pub async fn create_vector_engine_raw_image_edit( Some(started_at.elapsed().as_millis() as u64), prompt_chars, reference_image_count, - Some(GPT_IMAGE_2_MODEL), + Some(GPT_IMAGE_2_5_EDIT_MODEL), ); return Err(PlatformImageError::MissingImage { provider: VECTOR_ENGINE_PROVIDER, @@ -354,7 +354,7 @@ fn request_error( elapsed_ms = started_at.elapsed().as_millis() as u64, failure_context = context, error = %source, - "VectorEngine Raw 图片编辑请求失败" + "ImageProvider Raw 图片编辑请求失败" ); let audit = build_failure_audit( url, @@ -370,7 +370,7 @@ fn request_error( Some(started_at.elapsed().as_millis() as u64), prompt_chars, reference_image_count, - Some(GPT_IMAGE_2_MODEL), + Some(GPT_IMAGE_2_5_EDIT_MODEL), ); PlatformImageError::Request { provider: VECTOR_ENGINE_PROVIDER, diff --git a/server-rs/crates/platform-image/src/vector_engine/request.rs b/server-rs/crates/platform-image/src/image_provider/request.rs similarity index 84% rename from server-rs/crates/platform-image/src/vector_engine/request.rs rename to server-rs/crates/platform-image/src/image_provider/request.rs index 0b008d2a2..530ec64cc 100644 --- a/server-rs/crates/platform-image/src/vector_engine/request.rs +++ b/server-rs/crates/platform-image/src/image_provider/request.rs @@ -2,21 +2,22 @@ use serde_json::{Map, Value, json}; use super::{ constants::{ + GPT_IMAGE_2_5_BUSINESS_NAME, GPT_IMAGE_2_5_EDIT_MODEL, GPT_IMAGE_2_5_GENERATION_MODEL, GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_DIMENSION_ALIGNMENT, GPT_IMAGE_2_MAX_EDGE, GPT_IMAGE_2_MAX_PIXELS, GPT_IMAGE_2_MIN_PIXELS, GPT_IMAGE_2_MODEL, }, - types::{ReferenceImage, VectorEngineImageSettings}, + types::{ImageProvider, ImageProviderSettings, ReferenceImage}, }; -pub fn build_vector_engine_image_request_body( +pub fn build_image_request_body( prompt: &str, negative_prompt: Option<&str>, size: &str, candidate_count: u32, _reference_images: &[String], ) -> Value { - build_vector_engine_image_request_body_with_model( - GPT_IMAGE_2_MODEL, + build_image_request_body_with_model( + GPT_IMAGE_2_5_GENERATION_MODEL, prompt, negative_prompt, size, @@ -25,7 +26,7 @@ pub fn build_vector_engine_image_request_body( ) } -pub fn build_vector_engine_image_request_body_with_model( +pub fn build_image_request_body_with_model( model: &str, prompt: &str, negative_prompt: Option<&str>, @@ -33,7 +34,7 @@ pub fn build_vector_engine_image_request_body_with_model( candidate_count: u32, _reference_images: &[String], ) -> Value { - let model = normalize_vector_engine_image_model(model); + let model = normalize_image_model(model); let body = Map::from_iter([ ("model".to_string(), Value::String(model.to_string())), ( @@ -50,7 +51,7 @@ pub fn build_vector_engine_image_request_body_with_model( Value::Object(body) } -pub fn build_vector_engine_nanobanana_generate_content_request_body( +pub fn build_nanobanana_generate_content_request_body( prompt: &str, negative_prompt: Option<&str>, aspect_ratio: &str, @@ -86,17 +87,36 @@ pub fn build_vector_engine_nanobanana_generate_content_request_body( }) } -pub fn normalize_vector_engine_image_model(model: &str) -> &str { +pub fn normalize_image_model(model: &str) -> &str { match model.trim() { "" => GPT_IMAGE_2_MODEL, value => value, } } +/// Resolves a persisted/concrete model to its owning provider without rewriting +/// the model string. Legacy GPT Image 2 values remain readable, but are routed +/// to the current Tiantoken task boundary instead of VectorEngine. +pub fn resolve_image_provider(model: &str) -> Result { + match normalize_image_model(model) { + GPT_IMAGE_2_MODEL + | GPT_IMAGE_2_C_MODEL + | GPT_IMAGE_2_5_BUSINESS_NAME + | GPT_IMAGE_2_5_GENERATION_MODEL + | GPT_IMAGE_2_5_EDIT_MODEL => Ok(ImageProvider::Tiantoken), + "gemini-3.1-flash-image-preview" => Ok(ImageProvider::VectorEngine), + _ => Err("不支持的图片模型"), + } +} + pub(crate) fn is_gpt_image_2_family_model(model: &str) -> bool { matches!( - normalize_vector_engine_image_model(model), - GPT_IMAGE_2_MODEL | GPT_IMAGE_2_C_MODEL + normalize_image_model(model), + GPT_IMAGE_2_MODEL + | GPT_IMAGE_2_C_MODEL + | GPT_IMAGE_2_5_BUSINESS_NAME + | GPT_IMAGE_2_5_GENERATION_MODEL + | GPT_IMAGE_2_5_EDIT_MODEL ) } @@ -134,7 +154,7 @@ fn normalize_explicit_pixel_size(value: &str) -> String { fn clamp_gpt_image_2_pixel_size(size: &str) -> String { const MAX_ASPECT_RATIO: f64 = 3.0; - // 中文注释:这里是 VectorEngine 的共享发送边界,只处理 gpt-image-2 的显式像素尺寸。 + // 中文注释:这里是 ImageProvider 的共享发送边界,只处理 gpt-image-2 的显式像素尺寸。 let Some((width, height)) = parse_explicit_pixel_size(size) else { return size.to_string(); }; @@ -232,7 +252,7 @@ fn normalize_nanobanana_aspect_ratio(aspect_ratio: &str) -> &str { fn normalize_nanobanana_image_size(image_size: &str) -> &str { match image_size.trim() { - // 中文注释:nanobanana / Gemini 3.1 的 0.5K 在 VectorEngine 文档中要求传 512。 + // 中文注释:nanobanana / Gemini 3.1 的 0.5K 在 ImageProvider 文档中要求传 512。 "512" | "0.5K" => "512", "2K" => "2K", "4K" => "4K", @@ -240,7 +260,7 @@ fn normalize_nanobanana_image_size(image_size: &str) -> &str { } } -pub fn vector_engine_images_generation_url(settings: &VectorEngineImageSettings) -> String { +pub fn images_generation_url(settings: &ImageProviderSettings) -> String { if settings.base_url.ends_with("/v1") { format!("{}/images/generations", settings.base_url) } else { @@ -248,7 +268,7 @@ pub fn vector_engine_images_generation_url(settings: &VectorEngineImageSettings) } } -pub fn vector_engine_images_edit_url(settings: &VectorEngineImageSettings) -> String { +pub fn images_edit_url(settings: &ImageProviderSettings) -> String { if settings.base_url.ends_with("/v1") { format!("{}/images/edits", settings.base_url) } else { @@ -256,10 +276,7 @@ pub fn vector_engine_images_edit_url(settings: &VectorEngineImageSettings) -> St } } -pub fn vector_engine_nanobanana_generate_content_url( - settings: &VectorEngineImageSettings, - model: &str, -) -> String { +pub fn nanobanana_generate_content_url(settings: &ImageProviderSettings, model: &str) -> String { let base_url = settings .base_url .trim_end_matches("/v1") @@ -267,11 +284,11 @@ pub fn vector_engine_nanobanana_generate_content_url( format!( "{}/v1beta/models/{}:generateContent", base_url, - normalize_vector_engine_image_model(model) + normalize_image_model(model) ) } -pub(crate) fn build_vector_engine_image_edit_request_log_params( +pub(crate) fn build_image_edit_request_log_params( model: &str, prompt: &str, negative_prompt: Option<&str>, @@ -279,7 +296,7 @@ pub(crate) fn build_vector_engine_image_edit_request_log_params( candidate_count: u32, reference_images: &[ReferenceImage], ) -> Value { - let model = normalize_vector_engine_image_model(model); + let model = normalize_image_model(model); let prompt = prompt.trim(); let negative_prompt = negative_prompt .map(str::trim) @@ -330,11 +347,11 @@ pub(crate) fn build_prompt_with_negative(prompt: &str, negative_prompt: Option<& #[cfg(test)] mod tests { use super::*; - use crate::vector_engine::types::ReferenceImage; + use crate::image_provider::types::ReferenceImage; #[test] fn edit_request_log_params_include_reference_image_sizes_without_secrets_or_bytes() { - let params = build_vector_engine_image_edit_request_log_params( + let params = build_image_edit_request_log_params( GPT_IMAGE_2_MODEL, " 拼图参考图重绘 ", Some(" 文字,水印 "), @@ -389,7 +406,7 @@ mod tests { "1152x2048", "2048x1152", ] { - let body = build_vector_engine_image_request_body_with_model( + let body = build_image_request_body_with_model( GPT_IMAGE_2_MODEL, "测试", None, diff --git a/server-rs/crates/platform-image/src/vector_engine/response.rs b/server-rs/crates/platform-image/src/image_provider/response.rs similarity index 93% rename from server-rs/crates/platform-image/src/vector_engine/response.rs rename to server-rs/crates/platform-image/src/image_provider/response.rs index 438b5b3a6..aa88b1205 100644 --- a/server-rs/crates/platform-image/src/vector_engine/response.rs +++ b/server-rs/crates/platform-image/src/image_provider/response.rs @@ -12,7 +12,7 @@ use super::{ }; use std::time::Instant; -pub(crate) async fn handle_vector_engine_response( +pub(crate) async fn handle_image_response( http_client: &reqwest::Client, request_url: &str, response_status: u16, @@ -53,7 +53,7 @@ pub(crate) async fn handle_vector_engine_response( retryable = audit.retryable, message = %message, raw_excerpt = %raw_excerpt, - "VectorEngine 图片生成上游错误" + "ImageProvider 图片生成上游错误" ); return Err(PlatformImageError::Upstream { provider: VECTOR_ENGINE_PROVIDER, @@ -89,7 +89,7 @@ pub(crate) async fn handle_vector_engine_response( status = response_status, raw_excerpt = %truncate_raw(response_text), message = %error.message(), - "VectorEngine 图片响应解析失败" + "ImageProvider 图片响应解析失败" ); return Err(error.with_audit(audit)); } @@ -151,7 +151,7 @@ pub(crate) async fn handle_vector_engine_response( image_count = generated.images.len(), elapsed_ms = download_started_at.elapsed().as_millis() as u64, failure_context, - "VectorEngine 图片下载完成" + "ImageProvider 图片下载完成" ); return Ok(generated); } @@ -159,7 +159,7 @@ pub(crate) async fn handle_vector_engine_response( if !b64_images.is_empty() { let mut generated = images_from_base64(task_id, b64_images, candidate_count); if generated.images.is_empty() { - let message = format!("{failure_context}:VectorEngine 返回的 base64 图片无法解码"); + let message = format!("{failure_context}:ImageProvider 返回的 base64 图片无法解码"); let raw_excerpt = truncate_raw(response_text); let audit = build_failure_audit( request_url, @@ -183,7 +183,7 @@ pub(crate) async fn handle_vector_engine_response( status = response_status, image_model, raw_excerpt = %raw_excerpt, - "VectorEngine 图片 base64 解码失败" + "ImageProvider 图片 base64 解码失败" ); return Err(PlatformImageError::ResponseParse { provider: VECTOR_ENGINE_PROVIDER, @@ -198,12 +198,12 @@ pub(crate) async fn handle_vector_engine_response( endpoint = %request_url, image_count = generated.images.len(), failure_context, - "VectorEngine 图片 base64 解码完成" + "ImageProvider 图片 base64 解码完成" ); return Ok(generated); } - let message = format!("{failure_context}:VectorEngine 未返回图片地址"); + let message = format!("{failure_context}:ImageProvider 未返回图片地址"); let audit = build_failure_audit( request_url, failure_context, @@ -225,7 +225,7 @@ pub(crate) async fn handle_vector_engine_response( endpoint = %request_url, status = response_status, raw_excerpt = %truncate_raw(response_text), - "VectorEngine 图片响应未返回图片" + "ImageProvider 图片响应未返回图片" ); Err(PlatformImageError::MissingImage { provider: VECTOR_ENGINE_PROVIDER, diff --git a/server-rs/crates/platform-image/src/vector_engine/tests.rs b/server-rs/crates/platform-image/src/image_provider/tests.rs similarity index 92% rename from server-rs/crates/platform-image/src/vector_engine/tests.rs rename to server-rs/crates/platform-image/src/image_provider/tests.rs index febcd539f..3d0acb0c6 100644 --- a/server-rs/crates/platform-image/src/vector_engine/tests.rs +++ b/server-rs/crates/platform-image/src/image_provider/tests.rs @@ -6,7 +6,7 @@ mod tests { #[test] fn request_body_normalizes_size_prompt_and_candidate_count() { - let body = build_vector_engine_image_request_body( + let body = build_image_request_body( " 风雨夜里的街道 ", Some(" 低清,水印 "), " 1:1 ", @@ -14,7 +14,7 @@ mod tests { &["data:image/png;base64,AAAA".to_string()], ); - assert_eq!(body["model"], GPT_IMAGE_2_MODEL); + assert_eq!(body["model"], GPT_IMAGE_2_5_GENERATION_MODEL); assert_eq!(body["size"], "1024x1024"); assert_eq!(body["n"], 4); assert_eq!(body["prompt"], "风雨夜里的街道\n避免:低清,水印"); @@ -23,13 +23,15 @@ mod tests { #[test] fn provider_urls_normalize_root_and_v1_base_urls() { - let root_settings = VectorEngineImageSettings { + let root_settings = ImageProviderSettings { + provider: ImageProvider::VectorEngine, base_url: "https://vector.example".to_string(), api_key: "test-key".to_string(), request_timeout_ms: 1_000, request_deadline: None, }; - let v1_settings = VectorEngineImageSettings { + let v1_settings = ImageProviderSettings { + provider: ImageProvider::VectorEngine, base_url: "https://vector.example/v1".to_string(), api_key: "test-key".to_string(), request_timeout_ms: 1_000, @@ -37,19 +39,19 @@ mod tests { }; assert_eq!( - vector_engine_images_generation_url(&root_settings), + images_generation_url(&root_settings), "https://vector.example/v1/images/generations" ); assert_eq!( - vector_engine_images_generation_url(&v1_settings), + images_generation_url(&v1_settings), "https://vector.example/v1/images/generations" ); assert_eq!( - vector_engine_images_edit_url(&root_settings), + images_edit_url(&root_settings), "https://vector.example/v1/images/edits" ); assert_eq!( - vector_engine_images_edit_url(&v1_settings), + images_edit_url(&v1_settings), "https://vector.example/v1/images/edits" ); } @@ -94,7 +96,7 @@ mod tests { latency_ms: Some(987), prompt_chars: Some(64), reference_image_count: Some(2), - image_model: Some(VECTOR_ENGINE_GPT_IMAGE_2_MODEL), + image_model: Some(GPT_IMAGE_2_MODEL), }; let request_error = PlatformImageError::Request { @@ -162,7 +164,7 @@ mod tests { assert!(audit_ref.timeout); assert!(audit_ref.retryable); assert_eq!(audit_ref.reference_image_count, Some(2)); - assert_eq!(audit_ref.image_model, Some(VECTOR_ENGINE_GPT_IMAGE_2_MODEL)); + assert_eq!(audit_ref.image_model, Some(GPT_IMAGE_2_MODEL)); assert!(invalid_config.audit().is_none()); assert!(invalid_request.audit().is_none()); } diff --git a/server-rs/crates/platform-image/src/image_provider/transport.rs b/server-rs/crates/platform-image/src/image_provider/transport.rs new file mode 100644 index 000000000..69117c14d --- /dev/null +++ b/server-rs/crates/platform-image/src/image_provider/transport.rs @@ -0,0 +1,30 @@ +use std::time::Duration; + +use super::{ + error::PlatformImageError, + types::{ImageProviderClient, ImageProviderSettings}, +}; + +pub fn build_image_http_client( + settings: &ImageProviderSettings, +) -> Result { + reqwest::Client::builder() + .timeout(Duration::from_millis(settings.request_timeout_ms.max(1))) + .http1_only() + .pool_max_idle_per_host(0) + .build() + .map_err(|error| PlatformImageError::InvalidConfig { + provider: settings.provider.as_str(), + message: format!( + "构造 {} 图片生成 HTTP 客户端失败:{error}", + settings.provider.as_str() + ), + }) +} + +pub fn build_image_provider_client( + settings: ImageProviderSettings, +) -> Result { + let http_client = build_image_http_client(&settings)?; + Ok(ImageProviderClient::new(settings, http_client)) +} diff --git a/server-rs/crates/platform-image/src/image_provider/types.rs b/server-rs/crates/platform-image/src/image_provider/types.rs new file mode 100644 index 000000000..593102047 --- /dev/null +++ b/server-rs/crates/platform-image/src/image_provider/types.rs @@ -0,0 +1,85 @@ +use super::audit::PlatformImageFailureAudit; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ImageProvider { + VectorEngine, + Tiantoken, +} + +impl ImageProvider { + pub const fn as_str(self) -> &'static str { + match self { + Self::VectorEngine => super::constants::VECTOR_ENGINE_PROVIDER, + Self::Tiantoken => super::constants::TIANTOKEN_PROVIDER, + } + } +} + +#[derive(Clone, Debug)] +pub struct ImageProviderSettings { + pub provider: ImageProvider, + pub base_url: String, + pub api_key: String, + pub request_timeout_ms: u64, + pub request_deadline: Option, +} + +#[derive(Clone)] +pub struct ImageProviderClient { + settings: ImageProviderSettings, + http_client: reqwest::Client, +} + +impl std::fmt::Debug for ImageProviderClient { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("ImageProviderClient") + .field("provider", &self.settings.provider) + .field("base_url", &self.settings.base_url) + .field("api_key", &"") + .finish() + } +} + +impl ImageProviderClient { + pub(crate) fn new(settings: ImageProviderSettings, http_client: reqwest::Client) -> Self { + Self { + settings, + http_client, + } + } + + pub fn provider(&self) -> ImageProvider { + self.settings.provider + } + + pub fn settings(&self) -> &ImageProviderSettings { + &self.settings + } + + pub fn http_client(&self) -> &reqwest::Client { + &self.http_client + } +} + +#[derive(Clone, Debug)] +pub struct GeneratedImages { + pub task_id: String, + pub actual_prompt: Option, + pub images: Vec, + pub recovered_failure_audits: Vec, +} + +#[derive(Clone, Debug)] +pub struct DownloadedImage { + pub bytes: Vec, + pub mime_type: String, + pub extension: String, +} + +#[derive(Clone, Debug)] +pub struct ReferenceImage { + pub bytes: Vec, + pub mime_type: String, + pub file_name: String, +} diff --git a/server-rs/crates/platform-image/src/vector_engine/util.rs b/server-rs/crates/platform-image/src/image_provider/util.rs similarity index 100% rename from server-rs/crates/platform-image/src/vector_engine/util.rs rename to server-rs/crates/platform-image/src/image_provider/util.rs diff --git a/server-rs/crates/platform-image/src/lib.rs b/server-rs/crates/platform-image/src/lib.rs index 9ef40103c..d30a32c09 100644 --- a/server-rs/crates/platform-image/src/lib.rs +++ b/server-rs/crates/platform-image/src/lib.rs @@ -1,26 +1,26 @@ pub mod generated_asset_sheets; pub mod generated_assets; +pub mod image_provider; pub mod pixel_art_snapper; -pub mod vector_engine; +pub use image_provider::{ + DownloadedImage, GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD, GPT_IMAGE_2_5_BUSINESS_NAME, + GPT_IMAGE_2_5_EDIT_MODEL, GPT_IMAGE_2_5_GENERATION_MODEL, GPT_IMAGE_2_C_MODEL, + GPT_IMAGE_2_MODEL, GeneratedImages, ImageProvider, ImageProviderClient, ImageProviderSettings, + NANOBANANA_2_MODEL, PlatformImageError, PlatformImageFailureAudit, PlatformImageStatusHint, + RAW_IMAGE_DIMENSION_ALIGNMENT, RAW_IMAGE_MAX_EDGE, RAW_IMAGE_MAX_PIXELS, RAW_IMAGE_MIN_PIXELS, + RawImageEditDimensionError, RawImageEditImage, RawImageEditOptions, RawImageEditResult, + ReferenceImage, TIANTOKEN_PROVIDER, VECTOR_ENGINE_PROVIDER, build_image_http_client, + build_image_provider_client, build_image_request_body, + build_nanobanana_generate_content_request_body, create_image_edit, + create_image_edit_with_references, create_image_edit_with_references_and_model, + create_image_generation, create_image_generation_with_model, + create_nanobanana_generate_content, create_raw_image_edit, download_remote_image, + images_edit_url, images_generation_url, nanobanana_generate_content_url, + resolve_image_provider, validate_raw_image_edit_dimensions, +}; pub use pixel_art_snapper::{ PIXEL_ART_ALPHA_COVERAGE_THRESHOLD, PIXEL_ART_ANALYSIS_COLORS, PIXEL_ART_KMEANS_SAMPLE_LIMIT, PIXEL_ART_MAX_IMAGE_PIXELS, PixelArtSnapError, snap_pixel_art_strict_with_deadline, snap_pixel_art_with_deadline, }; -pub use vector_engine::{ - DownloadedImage, GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD, GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_MODEL, - GeneratedImages, NANOBANANA_2_MODEL, PlatformImageError, PlatformImageFailureAudit, - PlatformImageStatusHint, RAW_IMAGE_DIMENSION_ALIGNMENT, RAW_IMAGE_MAX_EDGE, - RAW_IMAGE_MAX_PIXELS, RAW_IMAGE_MIN_PIXELS, RawImageEditDimensionError, RawImageEditImage, - RawImageEditOptions, RawImageEditResult, ReferenceImage, VECTOR_ENGINE_GPT_IMAGE_2_MODEL, - VECTOR_ENGINE_PROVIDER, VectorEngineImageSettings, build_vector_engine_image_http_client, - build_vector_engine_image_request_body, - build_vector_engine_nanobanana_generate_content_request_body, create_vector_engine_image_edit, - create_vector_engine_image_edit_with_references, - create_vector_engine_image_edit_with_references_and_model, - create_vector_engine_image_generation, create_vector_engine_image_generation_with_model, - create_vector_engine_nanobanana_generate_content, create_vector_engine_raw_image_edit, - download_remote_image, validate_raw_image_edit_dimensions, vector_engine_images_edit_url, - vector_engine_images_generation_url, vector_engine_nanobanana_generate_content_url, -}; diff --git a/server-rs/crates/platform-image/src/vector_engine/mod.rs b/server-rs/crates/platform-image/src/vector_engine/mod.rs deleted file mode 100644 index 25e984180..000000000 --- a/server-rs/crates/platform-image/src/vector_engine/mod.rs +++ /dev/null @@ -1,42 +0,0 @@ -mod audit; -mod budget; -mod client; -mod constants; -mod curl_transport; -mod error; -mod image_source; -mod payload; -mod raw_edit; -mod request; -mod response; -mod transport; -mod types; -mod util; - -pub use audit::PlatformImageFailureAudit; -pub use client::{ - create_vector_engine_image_edit, create_vector_engine_image_edit_with_references, - create_vector_engine_image_edit_with_references_and_model, - create_vector_engine_image_generation, create_vector_engine_image_generation_with_model, - create_vector_engine_nanobanana_generate_content, -}; -pub use constants::{ - GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL, VECTOR_ENGINE_GPT_IMAGE_2_MODEL, - VECTOR_ENGINE_PROVIDER, -}; -pub use error::{PlatformImageError, PlatformImageStatusHint}; -pub use image_source::download_remote_image; -pub use raw_edit::{ - GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD, RAW_IMAGE_DIMENSION_ALIGNMENT, RAW_IMAGE_MAX_EDGE, - RAW_IMAGE_MAX_PIXELS, RAW_IMAGE_MIN_PIXELS, RawImageEditDimensionError, RawImageEditImage, - RawImageEditOptions, RawImageEditResult, create_vector_engine_raw_image_edit, - validate_raw_image_edit_dimensions, -}; -pub use request::{ - build_vector_engine_image_request_body, build_vector_engine_image_request_body_with_model, - build_vector_engine_nanobanana_generate_content_request_body, normalize_image_size_for_model, - vector_engine_images_edit_url, vector_engine_images_generation_url, - vector_engine_nanobanana_generate_content_url, -}; -pub use transport::build_vector_engine_image_http_client; -pub use types::{DownloadedImage, GeneratedImages, ReferenceImage, VectorEngineImageSettings}; diff --git a/server-rs/crates/platform-image/src/vector_engine/transport.rs b/server-rs/crates/platform-image/src/vector_engine/transport.rs deleted file mode 100644 index 6a63878bd..000000000 --- a/server-rs/crates/platform-image/src/vector_engine/transport.rs +++ /dev/null @@ -1,19 +0,0 @@ -use std::time::Duration; - -use super::{ - constants::VECTOR_ENGINE_PROVIDER, error::PlatformImageError, types::VectorEngineImageSettings, -}; - -pub fn build_vector_engine_image_http_client( - settings: &VectorEngineImageSettings, -) -> Result { - reqwest::Client::builder() - .timeout(Duration::from_millis(settings.request_timeout_ms.max(1))) - .http1_only() - .pool_max_idle_per_host(0) - .build() - .map_err(|error| PlatformImageError::InvalidConfig { - provider: VECTOR_ENGINE_PROVIDER, - message: format!("构造 VectorEngine 图片生成 HTTP 客户端失败:{error}"), - }) -} diff --git a/server-rs/crates/platform-image/src/vector_engine/types.rs b/server-rs/crates/platform-image/src/vector_engine/types.rs deleted file mode 100644 index 77fbd19f9..000000000 --- a/server-rs/crates/platform-image/src/vector_engine/types.rs +++ /dev/null @@ -1,31 +0,0 @@ -use super::audit::PlatformImageFailureAudit; - -#[derive(Clone, Debug)] -pub struct VectorEngineImageSettings { - pub base_url: String, - pub api_key: String, - pub request_timeout_ms: u64, - pub request_deadline: Option, -} - -#[derive(Clone, Debug)] -pub struct GeneratedImages { - pub task_id: String, - pub actual_prompt: Option, - pub images: Vec, - pub recovered_failure_audits: Vec, -} - -#[derive(Clone, Debug)] -pub struct DownloadedImage { - pub bytes: Vec, - pub mime_type: String, - pub extension: String, -} - -#[derive(Clone, Debug)] -pub struct ReferenceImage { - pub bytes: Vec, - pub mime_type: String, - pub file_name: String, -} diff --git a/server-rs/crates/platform-image/tests/vector_engine.rs b/server-rs/crates/platform-image/tests/image_provider.rs similarity index 72% rename from server-rs/crates/platform-image/tests/vector_engine.rs rename to server-rs/crates/platform-image/tests/image_provider.rs index f1bd4470b..cae9ed173 100644 --- a/server-rs/crates/platform-image/tests/vector_engine.rs +++ b/server-rs/crates/platform-image/tests/image_provider.rs @@ -1,11 +1,10 @@ -use platform_image::vector_engine::{ - GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_MODEL, PlatformImageError, ReferenceImage, - VECTOR_ENGINE_PROVIDER, VectorEngineImageSettings, build_vector_engine_image_http_client, - build_vector_engine_image_request_body, build_vector_engine_image_request_body_with_model, - build_vector_engine_nanobanana_generate_content_request_body, create_vector_engine_image_edit, - create_vector_engine_image_generation, create_vector_engine_nanobanana_generate_content, - vector_engine_images_edit_url, vector_engine_images_generation_url, - vector_engine_nanobanana_generate_content_url, +use platform_image::image_provider::{ + GPT_IMAGE_2_5_GENERATION_MODEL, GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_MODEL, ImageProvider, + ImageProviderSettings, PlatformImageError, ReferenceImage, VECTOR_ENGINE_PROVIDER, + build_image_http_client, build_image_request_body, build_image_request_body_with_model, + build_nanobanana_generate_content_request_body, create_image_edit, create_image_generation, + create_nanobanana_generate_content, images_edit_url, images_generation_url, + nanobanana_generate_content_url, }; use std::{ sync::{ @@ -21,39 +20,39 @@ use tokio::{ }; #[test] -fn vector_engine_module_exposes_provider_protocol_helpers() { - let settings = VectorEngineImageSettings { +fn image_provider_module_exposes_provider_protocol_helpers() { + let settings = ImageProviderSettings { + provider: ImageProvider::VectorEngine, base_url: "https://vector.example/v1".to_string(), api_key: "test-key".to_string(), request_timeout_ms: 1_000, request_deadline: None, }; - let body = - build_vector_engine_image_request_body("雾海神殿", Some("文字,水印"), "16:9", 9, &[]); + let body = build_image_request_body("雾海神殿", Some("文字,水印"), "16:9", 9, &[]); assert_eq!(GPT_IMAGE_2_MODEL, "gpt-image-2"); assert_eq!(GPT_IMAGE_2_C_MODEL, "gpt-image-2-c"); assert_eq!(VECTOR_ENGINE_PROVIDER, "vector-engine"); - assert_eq!(body["model"], GPT_IMAGE_2_MODEL); + assert_eq!(body["model"], GPT_IMAGE_2_5_GENERATION_MODEL); assert_eq!(body["size"], "1536x1024"); assert_eq!(body["n"], 4); assert_eq!(body["prompt"], "雾海神殿\n避免:文字,水印"); assert_eq!( - vector_engine_images_generation_url(&settings), + images_generation_url(&settings), "https://vector.example/v1/images/generations" ); assert_eq!( - vector_engine_images_edit_url(&settings), + images_edit_url(&settings), "https://vector.example/v1/images/edits" ); } #[test] -fn vector_engine_clamps_gpt_image_2_explicit_pixel_sizes_to_its_supported_pixel_budget() { - let cover = build_vector_engine_image_request_body("宣发首图", None, "720x540", 1, &[]); - let detail = build_vector_engine_image_request_body("详情单图", None, "720x1280", 1, &[]); - let poster = build_vector_engine_image_request_body("运营海报", None, "1280x720", 1, &[]); +fn image_provider_clamps_gpt_image_2_explicit_pixel_sizes_to_its_supported_pixel_budget() { + let cover = build_image_request_body("宣发首图", None, "720x540", 1, &[]); + let detail = build_image_request_body("详情单图", None, "720x1280", 1, &[]); + let poster = build_image_request_body("运营海报", None, "1280x720", 1, &[]); assert_eq!(cover["size"], "944x704"); assert_eq!(detail["size"], "720x1280"); @@ -61,17 +60,17 @@ fn vector_engine_clamps_gpt_image_2_explicit_pixel_sizes_to_its_supported_pixel_ } #[test] -fn vector_engine_normalizes_2k_landscape_spec_size() { - let body = build_vector_engine_image_request_body("生成规范图", None, "2048x1152", 1, &[]); +fn image_provider_normalizes_2k_landscape_spec_size() { + let body = build_image_request_body("生成规范图", None, "2048x1152", 1, &[]); - assert_eq!(body["model"], GPT_IMAGE_2_MODEL); + assert_eq!(body["model"], GPT_IMAGE_2_5_GENERATION_MODEL); assert_eq!(body["size"], "2048x1152"); assert_eq!(body["n"], 1); } #[test] -fn vector_engine_request_body_can_use_nanobanana2_model() { - let body = build_vector_engine_image_request_body_with_model( +fn image_provider_request_body_can_use_nanobanana2_model() { + let body = build_image_request_body_with_model( "gemini-3.1-flash-image-preview", "生成图标 spritesheet", None, @@ -86,16 +85,10 @@ fn vector_engine_request_body_can_use_nanobanana2_model() { } #[test] -fn vector_engine_only_enforces_the_gpt_image_2_pixel_budget_for_that_model() { - let gpt_body = build_vector_engine_image_request_body_with_model( - GPT_IMAGE_2_MODEL, - "小尺寸图", - None, - "640x640", - 1, - &[], - ); - let nanobanana_body = build_vector_engine_image_request_body_with_model( +fn image_provider_only_enforces_the_gpt_image_2_pixel_budget_for_that_model() { + let gpt_body = + build_image_request_body_with_model(GPT_IMAGE_2_MODEL, "小尺寸图", None, "640x640", 1, &[]); + let nanobanana_body = build_image_request_body_with_model( "gemini-3.1-flash-image-preview", "小尺寸图", None, @@ -103,7 +96,7 @@ fn vector_engine_only_enforces_the_gpt_image_2_pixel_budget_for_that_model() { 1, &[], ); - let oversized_gpt_body = build_vector_engine_image_request_body_with_model( + let oversized_gpt_body = build_image_request_body_with_model( GPT_IMAGE_2_MODEL, "大尺寸图", None, @@ -111,7 +104,7 @@ fn vector_engine_only_enforces_the_gpt_image_2_pixel_budget_for_that_model() { 1, &[], ); - let fallback_gpt_body = build_vector_engine_image_request_body_with_model( + let fallback_gpt_body = build_image_request_body_with_model( GPT_IMAGE_2_C_MODEL, "小尺寸图", None, @@ -127,7 +120,7 @@ fn vector_engine_only_enforces_the_gpt_image_2_pixel_budget_for_that_model() { } #[test] -fn vector_engine_gpt_image_2_sizes_always_meet_the_full_provider_envelope() { +fn image_provider_gpt_image_2_sizes_always_meet_the_full_provider_envelope() { for size in [ "1x1", "720x540", @@ -137,7 +130,7 @@ fn vector_engine_gpt_image_2_sizes_always_meet_the_full_provider_envelope() { "16x4096", "3840x3840", ] { - let body = build_vector_engine_image_request_body("约束测试", None, size, 1, &[]); + let body = build_image_request_body("约束测试", None, size, 1, &[]); let normalized = body["size"].as_str().expect("size should be a string"); let (width, height) = normalized .split_once('x') @@ -154,8 +147,8 @@ fn vector_engine_gpt_image_2_sizes_always_meet_the_full_provider_envelope() { } #[test] -fn vector_engine_request_body_can_use_nanobanana2_half_k() { - let body = build_vector_engine_image_request_body_with_model( +fn image_provider_request_body_can_use_nanobanana2_half_k() { + let body = build_image_request_body_with_model( "gemini-3.1-flash-image-preview", "生成图标 spritesheet", None, @@ -170,7 +163,7 @@ fn vector_engine_request_body_can_use_nanobanana2_half_k() { #[test] fn nanobanana_generate_content_body_carries_aspect_ratio_and_image_size() { - let body = build_vector_engine_nanobanana_generate_content_request_body( + let body = build_nanobanana_generate_content_request_body( "生成角色图", Some("文字、水印"), "2:3", @@ -195,7 +188,8 @@ fn nanobanana_generate_content_body_carries_aspect_ratio_and_image_size() { #[test] fn nanobanana_generate_content_url_uses_model_path() { - let settings = VectorEngineImageSettings { + let settings = ImageProviderSettings { + provider: ImageProvider::VectorEngine, base_url: "https://vector.example/v1".to_string(), api_key: "test-key".to_string(), request_timeout_ms: 1_000, @@ -203,13 +197,13 @@ fn nanobanana_generate_content_url_uses_model_path() { }; assert_eq!( - vector_engine_nanobanana_generate_content_url(&settings, "gemini-3.1-flash-image-preview"), + nanobanana_generate_content_url(&settings, "gemini-3.1-flash-image-preview"), "https://vector.example/v1beta/models/gemini-3.1-flash-image-preview:generateContent" ); } #[tokio::test] -async fn vector_engine_image_edit_retries_send_timeout_once_and_succeeds() { +async fn image_edit_retries_send_timeout_once_and_succeeds() { let listener = TcpListener::bind("127.0.0.1:0") .await .expect("mock server should bind"); @@ -250,28 +244,28 @@ async fn vector_engine_image_edit_retries_send_timeout_once_and_succeeds() { } }); - let settings = VectorEngineImageSettings { + let settings = ImageProviderSettings { + provider: ImageProvider::VectorEngine, base_url: format!("http://{server_addr}/v1"), api_key: "test-key".to_string(), request_timeout_ms: 40, request_deadline: None, }; - let http_client = - build_vector_engine_image_http_client(&settings).expect("client should build"); + let http_client = build_image_http_client(&settings).expect("client should build"); let reference_image = ReferenceImage { bytes: b"reference".to_vec(), mime_type: "image/png".to_string(), file_name: "reference.png".to_string(), }; - let generated = create_vector_engine_image_edit( + let generated = create_image_edit( &http_client, &settings, "测试提示词", None, "1024x1024", &reference_image, - "测试 VectorEngine 图片编辑失败", + "测试 ImageProvider 图片编辑失败", ) .await .expect("second attempt should return generated image"); @@ -284,7 +278,7 @@ async fn vector_engine_image_edit_retries_send_timeout_once_and_succeeds() { assert!( requests .iter() - .all(|request| request.contains("\r\n\r\ngpt-image-2\r\n")) + .all(|request| request.contains("\r\n\r\ngpt-image-2.5-sunburst-c\r\n")) ); server.abort(); } @@ -328,7 +322,7 @@ async fn read_http_request(stream: &mut tokio::net::TcpStream) -> Vec { } #[tokio::test] -async fn vector_engine_deadline_clips_stalled_attempt_and_prevents_retry() { +async fn image_provider_deadline_clips_stalled_attempt_and_prevents_retry() { let listener = TcpListener::bind("127.0.0.1:0") .await .expect("mock server should bind"); @@ -351,18 +345,18 @@ async fn vector_engine_deadline_clips_stalled_attempt_and_prevents_retry() { } }); - let mut settings = VectorEngineImageSettings { + let mut settings = ImageProviderSettings { + provider: ImageProvider::VectorEngine, base_url: format!("http://{server_addr}/v1"), api_key: "test-key".to_string(), request_timeout_ms: 5_000, request_deadline: None, }; - let http_client = - build_vector_engine_image_http_client(&settings).expect("client should build"); + let http_client = build_image_http_client(&settings).expect("client should build"); let started_at = Instant::now(); settings.request_deadline = Some(started_at + Duration::from_secs(1)); - let error = create_vector_engine_image_generation( + let error = create_image_generation( &http_client, &settings, "测试提示词", @@ -370,7 +364,7 @@ async fn vector_engine_deadline_clips_stalled_attempt_and_prevents_retry() { "1024x1024", 1, &[], - "测试 VectorEngine 图片生成失败", + "测试 ImageProvider 图片生成失败", ) .await .expect_err("stalled request should exhaust the shared deadline"); @@ -435,15 +429,16 @@ async fn nanobanana_generate_content_posts_native_body_and_reads_inline_data() { ); let _ = stream.write_all(response.as_bytes()).await; }); - let settings = VectorEngineImageSettings { + let settings = ImageProviderSettings { + provider: ImageProvider::VectorEngine, base_url: format!("http://{}", server_addr), api_key: "test-key".to_string(), request_timeout_ms: 1_000, request_deadline: None, }; - let client = build_vector_engine_image_http_client(&settings).expect("client should build"); + let client = build_image_http_client(&settings).expect("client should build"); - let generated = create_vector_engine_nanobanana_generate_content( + let generated = create_nanobanana_generate_content( &client, &settings, "gemini-3.1-flash-image-preview", @@ -463,7 +458,7 @@ async fn nanobanana_generate_content_posts_native_body_and_reads_inline_data() { } #[tokio::test] -async fn vector_engine_image_generation_falls_back_after_upstream_502_and_succeeds() { +async fn image_generation_stays_on_model_after_upstream_502() { let listener = TcpListener::bind("127.0.0.1:0") .await .expect("mock server should bind"); @@ -510,16 +505,16 @@ async fn vector_engine_image_generation_falls_back_after_upstream_502_and_succee } }); - let settings = VectorEngineImageSettings { + let settings = ImageProviderSettings { + provider: ImageProvider::VectorEngine, base_url: format!("http://{server_addr}/v1"), api_key: "test-key".to_string(), request_timeout_ms: 1_000, request_deadline: None, }; - let http_client = - build_vector_engine_image_http_client(&settings).expect("client should build"); + let http_client = build_image_http_client(&settings).expect("client should build"); - let generated = create_vector_engine_image_generation( + let generated = create_image_generation( &http_client, &settings, "测试提示词", @@ -527,38 +522,31 @@ async fn vector_engine_image_generation_falls_back_after_upstream_502_and_succee "1024x1024", 1, &[], - "测试 VectorEngine 图片生成失败", + "测试 ImageProvider 图片生成失败", ) .await - .expect("second attempt should return generated image"); + .expect("same-model retry should recover"); assert_eq!(generated.images.len(), 1); - assert_eq!(generated.images[0].mime_type, "image/png"); - assert_eq!(generated.recovered_failure_audits.len(), 1); - assert_eq!( - generated.recovered_failure_audits[0].image_model, - Some(GPT_IMAGE_2_MODEL) - ); assert_eq!(request_count.load(Ordering::SeqCst), 2); let requests = requests.lock().await; - assert!(requests[0].contains("\"model\":\"gpt-image-2\"")); - assert!(requests[1].contains("\"model\":\"gpt-image-2-c\"")); + assert!(requests[0].contains("\"model\":\"gpt-image-2.5-flare-c\"")); + assert!(requests[1].contains("\"model\":\"gpt-image-2.5-flare-c\"")); server.abort(); } #[tokio::test] -async fn vector_engine_image_generation_uses_gpt_image_2_without_fallback_on_success() { +async fn image_generation_uses_gpt_image_2_without_fallback_on_success() { let (base_url, server, requests) = start_http_response_sequence(vec![MockResponse { status: "200 OK", content_type: "application/json", body: r#"{"data":[{"b64_json":"iVBORw0KGgpyZXN0"}]}"#, }]) .await; - let settings = test_vector_engine_settings(base_url); - let http_client = - build_vector_engine_image_http_client(&settings).expect("client should build"); + let settings = test_image_provider_settings(base_url); + let http_client = build_image_http_client(&settings).expect("client should build"); - let generated = create_vector_engine_image_generation( + let generated = create_image_generation( &http_client, &settings, "测试提示词", @@ -566,7 +554,7 @@ async fn vector_engine_image_generation_uses_gpt_image_2_without_fallback_on_suc "1024x1024", 1, &[], - "测试 VectorEngine 图片生成失败", + "测试 ImageProvider 图片生成失败", ) .await .expect("preferred model should generate image"); @@ -575,12 +563,12 @@ async fn vector_engine_image_generation_uses_gpt_image_2_without_fallback_on_suc assert!(generated.recovered_failure_audits.is_empty()); let requests = requests.lock().await; assert_eq!(requests.len(), 1); - assert!(requests[0].contains("\"model\":\"gpt-image-2\"")); + assert!(requests[0].contains("\"model\":\"gpt-image-2.5-flare-c\"")); server.abort(); } #[tokio::test] -async fn vector_engine_image_edit_falls_back_when_preferred_model_is_unsupported() { +async fn image_edit_does_not_cross_model_when_unsupported() { let (base_url, server, requests) = start_http_response_sequence(vec![ MockResponse { status: "400 Bad Request", @@ -594,53 +582,45 @@ async fn vector_engine_image_edit_falls_back_when_preferred_model_is_unsupported }, ]) .await; - let settings = test_vector_engine_settings(base_url); - let http_client = - build_vector_engine_image_http_client(&settings).expect("client should build"); + let settings = test_image_provider_settings(base_url); + let http_client = build_image_http_client(&settings).expect("client should build"); let reference = ReferenceImage { bytes: b"reference".to_vec(), mime_type: "image/png".to_string(), file_name: "reference.png".to_string(), }; - let generated = create_vector_engine_image_edit( + let error = create_image_edit( &http_client, &settings, "测试提示词", None, "1024x1024", &reference, - "测试 VectorEngine 图片编辑失败", + "测试 ImageProvider 图片编辑失败", ) .await - .expect("fallback model should recover unsupported preferred model"); + .expect_err("unsupported model must remain terminal"); - assert_eq!(generated.images.len(), 1); - assert_eq!(generated.recovered_failure_audits.len(), 1); - assert_eq!( - generated.recovered_failure_audits[0].image_model, - Some(GPT_IMAGE_2_MODEL) - ); + assert!(matches!(error, PlatformImageError::Upstream { .. })); let requests = requests.lock().await; - assert_eq!(requests.len(), 2); - assert!(requests[0].contains("\r\n\r\ngpt-image-2\r\n")); - assert!(requests[1].contains("\r\n\r\ngpt-image-2-c\r\n")); + assert_eq!(requests.len(), 1); + assert!(requests[0].contains("\r\n\r\ngpt-image-2.5-sunburst-c\r\n")); server.abort(); } #[tokio::test] -async fn vector_engine_image_generation_does_not_fallback_on_auth_failure() { +async fn image_generation_does_not_fallback_on_auth_failure() { let (base_url, server, requests) = start_http_response_sequence(vec![MockResponse { status: "401 Unauthorized", content_type: "application/json", body: r#"{"error":{"message":"invalid api key"}}"#, }]) .await; - let settings = test_vector_engine_settings(base_url); - let http_client = - build_vector_engine_image_http_client(&settings).expect("client should build"); + let settings = test_image_provider_settings(base_url); + let http_client = build_image_http_client(&settings).expect("client should build"); - let error = create_vector_engine_image_generation( + let error = create_image_generation( &http_client, &settings, "测试提示词", @@ -648,7 +628,7 @@ async fn vector_engine_image_generation_does_not_fallback_on_auth_failure() { "1024x1024", 1, &[], - "测试 VectorEngine 图片生成失败", + "测试 ImageProvider 图片生成失败", ) .await .expect_err("authentication failure should remain terminal"); @@ -666,7 +646,7 @@ async fn vector_engine_image_generation_does_not_fallback_on_auth_failure() { } #[tokio::test] -async fn vector_engine_image_generation_falls_back_after_non_image_base64_response() { +async fn image_generation_does_not_cross_model_after_invalid_response() { let (base_url, server, requests) = start_http_response_sequence(vec![ MockResponse { status: "200 OK", @@ -680,11 +660,10 @@ async fn vector_engine_image_generation_falls_back_after_non_image_base64_respon }, ]) .await; - let settings = test_vector_engine_settings(base_url); - let http_client = - build_vector_engine_image_http_client(&settings).expect("client should build"); + let settings = test_image_provider_settings(base_url); + let http_client = build_image_http_client(&settings).expect("client should build"); - let generated = create_vector_engine_image_generation( + let error = create_image_generation( &http_client, &settings, "测试提示词", @@ -692,26 +671,20 @@ async fn vector_engine_image_generation_falls_back_after_non_image_base64_respon "1024x1024", 1, &[], - "测试 VectorEngine 图片生成失败", + "测试 ImageProvider 图片生成失败", ) .await - .expect("fallback model should recover invalid preferred response"); + .expect_err("invalid image response must remain terminal"); - assert_eq!(generated.images.len(), 1); - assert_eq!(generated.recovered_failure_audits.len(), 1); - assert_eq!( - generated.recovered_failure_audits[0].failure_stage, - "response_parse" - ); + assert!(matches!(error, PlatformImageError::ResponseParse { .. })); let requests = requests.lock().await; - assert_eq!(requests.len(), 2); - assert!(requests[0].contains("\"model\":\"gpt-image-2\"")); - assert!(requests[1].contains("\"model\":\"gpt-image-2-c\"")); + assert_eq!(requests.len(), 1); + assert!(requests[0].contains("\"model\":\"gpt-image-2.5-flare-c\"")); server.abort(); } #[tokio::test] -async fn vector_engine_image_generation_preserves_primary_audit_when_fallback_also_fails() { +async fn image_generation_does_not_retry_as_another_model() { let (base_url, server, requests) = start_http_response_sequence(vec![ MockResponse { status: "502 Bad Gateway", @@ -725,11 +698,10 @@ async fn vector_engine_image_generation_preserves_primary_audit_when_fallback_al }, ]) .await; - let settings = test_vector_engine_settings(base_url); - let http_client = - build_vector_engine_image_http_client(&settings).expect("client should build"); + let settings = test_image_provider_settings(base_url); + let http_client = build_image_http_client(&settings).expect("client should build"); - let error = create_vector_engine_image_generation( + let error = create_image_generation( &http_client, &settings, "测试提示词", @@ -737,38 +709,29 @@ async fn vector_engine_image_generation_preserves_primary_audit_when_fallback_al "1024x1024", 1, &[], - "测试 VectorEngine 图片生成失败", + "测试 ImageProvider 图片生成失败", ) .await - .expect_err("fallback authentication failure should remain terminal"); + .expect_err("upstream failure should remain terminal"); - assert_eq!(error.recovered_failure_audits().len(), 1); - assert_eq!( - error.recovered_failure_audits()[0].image_model, - Some(GPT_IMAGE_2_MODEL) - ); - assert_eq!( - error.audit().and_then(|audit| audit.image_model), - Some(GPT_IMAGE_2_C_MODEL) - ); + assert!(error.recovered_failure_audits().is_empty()); let requests = requests.lock().await; assert_eq!(requests.len(), 2); server.abort(); } #[tokio::test] -async fn vector_engine_image_generation_does_not_fallback_on_safety_parse_failure() { +async fn image_generation_does_not_fallback_on_safety_parse_failure() { let (base_url, server, requests) = start_http_response_sequence(vec![MockResponse { status: "200 OK", content_type: "application/json", body: "safety refusal: 内容审核拒绝", }]) .await; - let settings = test_vector_engine_settings(base_url); - let http_client = - build_vector_engine_image_http_client(&settings).expect("client should build"); + let settings = test_image_provider_settings(base_url); + let http_client = build_image_http_client(&settings).expect("client should build"); - let error = create_vector_engine_image_generation( + let error = create_image_generation( &http_client, &settings, "测试提示词", @@ -776,7 +739,7 @@ async fn vector_engine_image_generation_does_not_fallback_on_safety_parse_failur "1024x1024", 1, &[], - "测试 VectorEngine 图片生成失败", + "测试 ImageProvider 图片生成失败", ) .await .expect_err("content rejection should not switch models"); @@ -828,8 +791,9 @@ async fn start_http_response_sequence( (format!("http://{server_addr}/v1"), server, requests) } -fn test_vector_engine_settings(base_url: String) -> VectorEngineImageSettings { - VectorEngineImageSettings { +fn test_image_provider_settings(base_url: String) -> ImageProviderSettings { + ImageProviderSettings { + provider: ImageProvider::VectorEngine, base_url, api_key: "test-key".to_string(), request_timeout_ms: 1_000,