From b294cbc3dbfde6da43f2b97b0d72489854c4b9c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Fri, 18 Sep 2026 16:28:45 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=85=A5=20GPT=20Image=202.5=20?= =?UTF-8?q?=E5=8F=8C=20provider=20=E5=90=AF=E5=8A=A8=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 启动时分别构造 VectorEngine 与 Tiantoken 图片 client 按生成、编辑和 nanobanana 模型选择具体 provider 迁移 api-server、编辑器 Agent 与 raw edit 调用方 拆分 GPT Image 2.5 生成与编辑定价并隐藏 provider 具体值 --- .../editor-generation-pricing.default.json | 14 ++ .../api-server/src/character_visual_assets.rs | 21 +-- server-rs/crates/api-server/src/config.rs | 21 +-- .../src/editor_generation_config.rs | 111 +++++++++++- .../crates/api-server/src/editor_project.rs | 60 ++++--- .../api-server/src/editor_project_icon.rs | 2 +- .../api-server/src/openai_image_generation.rs | 161 ++++++++++++------ server-rs/crates/api-server/src/raw_image.rs | 8 +- server-rs/crates/api-server/src/state.rs | 71 ++++++++ .../src/agent/tools/edit_image.rs | 16 +- .../agent/tools/generate_icon_spritesheet.rs | 4 +- .../src/agent/tools/generate_image.rs | 8 +- .../src/agent/tools/generate_ui_design.rs | 23 +-- .../agent/tools/image_generation_options.rs | 28 +-- .../src/agent/tools/mod.rs | 19 ++- .../ImageCanvasGenerationModel.ts | 23 ++- 16 files changed, 430 insertions(+), 160 deletions(-) diff --git a/server-rs/crates/api-server/config/editor-generation-pricing.default.json b/server-rs/crates/api-server/config/editor-generation-pricing.default.json index 18d18f1b7..09a923eac 100644 --- a/server-rs/crates/api-server/config/editor-generation-pricing.default.json +++ b/server-rs/crates/api-server/config/editor-generation-pricing.default.json @@ -15,6 +15,20 @@ "2K": 5 } }, + "gpt-image-2.5-flare-c": { + "unit": "perGeneration", + "prices": { + "1K": 3, + "2K": 5 + } + }, + "gpt-image-2.5-sunburst-c": { + "unit": "perGeneration", + "prices": { + "1K": 3, + "2K": 5 + } + }, "seedance2.0-fast": { "unit": "perSecond", "prices": { diff --git a/server-rs/crates/api-server/src/character_visual_assets.rs b/server-rs/crates/api-server/src/character_visual_assets.rs index cea47141f..e4f62bb33 100644 --- a/server-rs/crates/api-server/src/character_visual_assets.rs +++ b/server-rs/crates/api-server/src/character_visual_assets.rs @@ -36,16 +36,16 @@ use crate::{ }, http_error::AppError, openai_image_generation::{ - DownloadedOpenAiImage, GPT_IMAGE_2_MODEL, OpenAiImageSettings, - build_openai_image_http_client, create_openai_image_generation, - require_openai_image_settings, + DownloadedOpenAiImage, GPT_IMAGE_2_5_BUSINESS_NAME, GPT_IMAGE_2_5_GENERATION_MODEL, + OpenAiImageSettings, build_openai_image_http_client, + create_openai_image_generation_with_model, require_openai_image_settings, }, platform_errors::map_oss_error, request_context::RequestContext, state::AppState, }; -const CHARACTER_VISUAL_MODEL: &str = GPT_IMAGE_2_MODEL; +const CHARACTER_VISUAL_MODEL: &str = GPT_IMAGE_2_5_BUSINESS_NAME; const CHARACTER_VISUAL_ASSET_KIND: &str = "character_visual"; const CHARACTER_VISUAL_ENTITY_KIND: &str = "character"; const CHARACTER_VISUAL_SLOT: &str = "primary_visual"; @@ -777,13 +777,13 @@ fn build_character_visual_job_payload(task: AiTaskSnapshot) -> CharacterAssetJob } fn resolve_character_visual_model(value: &str) -> String { - // 中文注释:旧前端和历史草稿可能仍传 wan2.7-image-pro;RPG 主图当前统一归一到 gpt-image-2。 + // 中文注释:旧前端和历史草稿可能仍传旧模型;只在新任务提交边界归一到当前业务模型。 let trimmed = value.trim(); if !trimmed.is_empty() && trimmed != CHARACTER_VISUAL_MODEL { tracing::warn!( requested_model = trimmed, effective_model = CHARACTER_VISUAL_MODEL, - "角色主形象图片模型已归一到 gpt-image-2" + "角色主形象图片模型已归一到当前业务模型" ); } CHARACTER_VISUAL_MODEL.to_string() @@ -946,9 +946,10 @@ async fn create_character_visual_generation_once( candidate_count: u32, reference_images: &[String], ) -> Result { - let generated = create_openai_image_generation( + let generated = create_openai_image_generation_with_model( http_client, settings, + GPT_IMAGE_2_5_GENERATION_MODEL, prompt, Some(build_character_visual_negative_prompt().as_str()), size, @@ -1921,12 +1922,12 @@ mod tests { } #[test] - fn legacy_character_visual_model_normalizes_to_gpt_image_2() { + fn legacy_character_visual_model_normalizes_to_gpt_image_2_5() { assert_eq!( resolve_character_visual_model("wan2.7-image-pro"), - "gpt-image-2" + "gpt-image-2.5" ); - assert_eq!(resolve_character_visual_model(""), "gpt-image-2"); + assert_eq!(resolve_character_visual_model(""), "gpt-image-2.5"); } #[test] diff --git a/server-rs/crates/api-server/src/config.rs b/server-rs/crates/api-server/src/config.rs index 94ea4d4e4..04135970f 100644 --- a/server-rs/crates/api-server/src/config.rs +++ b/server-rs/crates/api-server/src/config.rs @@ -1428,18 +1428,15 @@ impl AppConfig { } } -/// Tiantoken 是图片、文本和旧版非 Suno 音频生成的新 provider。 -/// -/// 这里保留对 `AppConfig.vector_engine_*` 的回退,方便测试构造的旧配置继续工作; -/// 生产环境一旦设置了新的 `TIANTOKEN_*` 变量,就不会再把非 Suno 请求发往 VectorEngine。 +/// Tiantoken 是 GPT Image 2.5 图片任务的独立 provider;凭证不得回退到 VectorEngine。 pub(crate) fn tiantoken_base_url(config: &AppConfig) -> String { - read_first_non_empty_env(&["TIANTOKEN_BASE_URL"]) - .unwrap_or_else(|| config.vector_engine_base_url.clone()) + let _ = config; + read_first_non_empty_env(&["TIANTOKEN_BASE_URL"]).unwrap_or_default() } pub(crate) fn tiantoken_api_key(config: &AppConfig) -> Option { + let _ = config; read_first_non_empty_env(&["TIANTOKEN_API_KEY"]) - .or_else(|| config.vector_engine_api_key.clone()) } fn read_first_non_empty_env(keys: &[&str]) -> Option { @@ -1864,14 +1861,8 @@ mod tests { std::env::remove_var("TIANTOKEN_BASE_URL"); std::env::remove_var("TIANTOKEN_API_KEY"); } - assert_eq!( - tiantoken_base_url(&config), - "https://vector.example.invalid" - ); - assert_eq!( - tiantoken_api_key(&config).as_deref(), - Some("legacy-vector-key") - ); + assert_eq!(tiantoken_base_url(&config), ""); + assert_eq!(tiantoken_api_key(&config), None); } #[test] diff --git a/server-rs/crates/api-server/src/editor_generation_config.rs b/server-rs/crates/api-server/src/editor_generation_config.rs index 11fdaa818..71e961d35 100644 --- a/server-rs/crates/api-server/src/editor_generation_config.rs +++ b/server-rs/crates/api-server/src/editor_generation_config.rs @@ -17,6 +17,9 @@ pub(crate) const EDITOR_GENERATION_PRICING_DEFAULT_JSON: &str = include_str!("../config/editor-generation-pricing.default.json"); const EDITOR_IMAGE_MODEL_GPT_IMAGE_2: &str = "gpt-image-2"; +pub(crate) const EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_BUSINESS: &str = "gpt-image-2.5"; +pub(crate) const EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION: &str = "gpt-image-2.5-flare-c"; +pub(crate) const EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT: &str = "gpt-image-2.5-sunburst-c"; const EDITOR_IMAGE_MODEL_NANOBANANA2: &str = "gemini-3.1-flash-image-preview"; const EDITOR_IMAGE_MODEL_NANOBANANA2_DISPLAY_ALIAS: &str = "nanobanana2"; const EDITOR_IMAGE_MODEL_NANOBANANA_LEGACY_ALIAS: &str = "nano-banana"; @@ -94,6 +97,25 @@ pub(crate) enum EditorGenerationPricingError { } impl EditorGenerationPricingConfig { + /// Public main-site projection: provider-specific GPT Image keys remain an + /// admin/server concern and are represented by the business model name. + pub(crate) fn public_projection(&self) -> Self { + let mut models = self.models.clone(); + if let Some(generation) = models + .get(EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION) + .cloned() + { + models.insert( + EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_BUSINESS.to_string(), + generation, + ); + } + models.remove(EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION); + models.remove(EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT); + models.remove(EDITOR_IMAGE_MODEL_GPT_IMAGE_2); + Self { models } + } + pub(crate) fn image_model_mud_points( &self, model: Option<&str>, @@ -114,15 +136,35 @@ impl EditorGenerationPricingConfig { ) } + pub(crate) fn image_edit_model_mud_points( + &self, + model: Option<&str>, + image_size: Option<&str>, + ) -> u32 { + if let Some(price_mud_points) = current_external_generation_billing_price_mud_points() { + return price_mud_points; + } + let normalized_model = normalize_editor_image_edit_model(model); + let normalized_size = + normalize_editor_generation_image_price_size(normalized_model, image_size); + read_tier_price( + &self.models, + normalized_model, + EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT, + normalized_size, + DEFAULT_IMAGE_PRICE_SIZE, + ) + } + pub(crate) fn spec_model_mud_points(&self, model: Option<&str>) -> u32 { if let Some(price_mud_points) = current_external_generation_billing_price_mud_points() { return price_mud_points; } - let normalized_model = normalize_non_empty_model(model, EDITOR_IMAGE_MODEL_GPT_IMAGE_2); + let normalized_model = normalize_editor_image_model(model); read_tier_price( &self.models, normalized_model, - EDITOR_IMAGE_MODEL_GPT_IMAGE_2, + EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION, SPEC_IMAGE_PRICE_SIZE, SPEC_IMAGE_PRICE_SIZE, ) @@ -221,7 +263,13 @@ impl EditorGenerationPricingConfig { )?; validate_required_tier_prices( &self.models, - EDITOR_IMAGE_MODEL_GPT_IMAGE_2, + EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION, + EditorGenerationPricingUnit::PerGeneration, + REQUIRED_GPT_IMAGE_SIZES, + )?; + validate_required_tier_prices( + &self.models, + EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT, EditorGenerationPricingUnit::PerGeneration, REQUIRED_GPT_IMAGE_SIZES, )?; @@ -326,6 +374,7 @@ fn load_editor_generation_pricing_from_candidates( serde_json::from_str::(override_json.as_str()) .map_err(EditorGenerationPricingError::Json)?; backfill_legacy_sfx_pricing(&mut override_config, &config, source.as_ref())?; + backfill_legacy_gpt_image_2_5_pricing(&mut override_config, &config, source.as_ref())?; override_config.validate().map_err(|error| match error { EditorGenerationPricingError::Invalid(message) => { EditorGenerationPricingError::Invalid(format!("{source}: {message}")) @@ -338,6 +387,43 @@ fn load_editor_generation_pricing_from_candidates( Ok(config) } +fn backfill_legacy_gpt_image_2_5_pricing( + config: &mut EditorGenerationPricingConfig, + fallback: &EditorGenerationPricingConfig, + source: &str, +) -> Result<(), EditorGenerationPricingError> { + if config + .models + .contains_key(EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION) + && config + .models + .contains_key(EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT) + { + return Ok(()); + } + // TODO: compatibility backfill for legacy single-key pricing; remove once + // all persisted overrides contain the two explicit GPT Image 2.5 keys. + let pricing = config + .models + .get(EDITOR_IMAGE_MODEL_GPT_IMAGE_2) + .or_else(|| fallback.models.get(EDITOR_IMAGE_MODEL_GPT_IMAGE_2)) + .cloned() + .ok_or_else(|| { + EditorGenerationPricingError::Invalid(format!( + "{source}: 受控默认配置缺少模型 {EDITOR_IMAGE_MODEL_GPT_IMAGE_2} 的兼容泥点配置" + )) + })?; + config + .models + .entry(EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION.to_string()) + .or_insert_with(|| pricing.clone()); + config + .models + .entry(EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT.to_string()) + .or_insert(pricing); + Ok(()) +} + fn backfill_legacy_sfx_pricing( config: &mut EditorGenerationPricingConfig, fallback: &EditorGenerationPricingConfig, @@ -437,7 +523,12 @@ fn default_runtime_pricing() -> EditorGenerationPricingConfig { fn normalize_editor_image_model(model: Option<&str>) -> &'static str { match model.map(str::trim).filter(|value| !value.is_empty()) { - Some(EDITOR_IMAGE_MODEL_GPT_IMAGE_2) => EDITOR_IMAGE_MODEL_GPT_IMAGE_2, + Some( + EDITOR_IMAGE_MODEL_GPT_IMAGE_2 + | EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_BUSINESS + | EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION, + ) => EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION, + Some(EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT) => EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT, Some(EDITOR_IMAGE_MODEL_NANOBANANA2) | Some(EDITOR_IMAGE_MODEL_NANOBANANA2_DISPLAY_ALIAS) | Some(EDITOR_IMAGE_MODEL_NANOBANANA_LEGACY_ALIAS) => EDITOR_IMAGE_MODEL_NANOBANANA2, @@ -445,6 +536,18 @@ fn normalize_editor_image_model(model: Option<&str>) -> &'static str { } } +fn normalize_editor_image_edit_model(model: Option<&str>) -> &'static str { + match model.map(str::trim).filter(|value| !value.is_empty()) { + Some(EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT) => EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT, + Some( + EDITOR_IMAGE_MODEL_GPT_IMAGE_2 + | EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_BUSINESS + | EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION, + ) => EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT, + _ => normalize_editor_image_model(model), + } +} + fn normalize_editor_generation_image_price_size( model: &str, image_size: Option<&str>, diff --git a/server-rs/crates/api-server/src/editor_project.rs b/server-rs/crates/api-server/src/editor_project.rs index 5f92b841a..0e1a041b3 100644 --- a/server-rs/crates/api-server/src/editor_project.rs +++ b/server-rs/crates/api-server/src/editor_project.rs @@ -112,8 +112,9 @@ use crate::{ }, http_error::AppError, openai_image_generation::{ - DownloadedOpenAiImage, GPT_IMAGE_2_MODEL, OpenAiGeneratedImages, OpenAiImageSettings, - OpenAiReferenceImage, build_openai_image_http_client, + DownloadedOpenAiImage, GPT_IMAGE_2_5_BUSINESS_NAME, GPT_IMAGE_2_5_EDIT_MODEL, + GPT_IMAGE_2_5_GENERATION_MODEL, GPT_IMAGE_2_MODEL, OpenAiGeneratedImages, + OpenAiImageSettings, OpenAiReferenceImage, build_openai_image_http_client, create_openai_image_edit_with_references_and_model, create_openai_image_generation_with_model, create_openai_nanobanana_generate_content, require_openai_image_settings, @@ -374,6 +375,9 @@ pub(crate) struct EditorImagePromptBuildOutput { pub(crate) struct EditorImageProviderRequest<'a> { pub(crate) model: &'a str, + /// Concrete provider/pricing route selected by api-server. The business + /// model remains `model`; this value must never be exposed to normal UI. + pub(crate) provider_model: &'a str, pub(crate) prompt: &'a str, pub(crate) negative_prompt: Option<&'a str>, pub(crate) size: &'a str, @@ -392,7 +396,7 @@ pub(crate) async fn request_editor_generated_images( create_openai_nanobanana_generate_content( http_client, settings, - request.model, + request.provider_model, request.prompt, request.negative_prompt, request.aspect_ratio, @@ -405,7 +409,7 @@ pub(crate) async fn request_editor_generated_images( create_openai_image_generation_with_model( http_client, settings, - request.model, + request.provider_model, request.prompt, request.negative_prompt, request.size, @@ -418,7 +422,7 @@ pub(crate) async fn request_editor_generated_images( create_openai_image_edit_with_references_and_model( http_client, settings, - request.model, + request.provider_model, request.prompt, request.negative_prompt, request.size, @@ -1926,7 +1930,10 @@ pub async fn get_editor_generation_pricing( "message": error.to_string(), })) })?; - Ok(json_success_body(Some(&request_context), pricing)) + Ok(json_success_body( + Some(&request_context), + pricing.public_projection(), + )) } pub async fn list_editor_projects( @@ -2764,7 +2771,7 @@ pub(crate) async fn enqueue_editor_image_generation_for_owner( matches!(normalized_kind, Some("publication-material")); let generation_options = normalize_editor_generation_options( if is_ui_design_generation || is_publication_material_generation { - Some(GPT_IMAGE_2_MODEL) + Some(GPT_IMAGE_2_5_BUSINESS_NAME) } else { payload.model.as_deref() }, @@ -2854,7 +2861,7 @@ pub(crate) async fn validate_editor_image_generation_parameters_for_owner( matches!(normalized_kind, Some("publication-material")); let generation_options = normalize_editor_generation_options( if is_ui_design_generation || is_publication_material_generation { - Some(GPT_IMAGE_2_MODEL) + Some(GPT_IMAGE_2_5_BUSINESS_NAME) } else { payload.model.as_deref() }, @@ -2987,7 +2994,7 @@ where matches!(normalized_kind, Some("publication-material")); let generation_options = normalize_editor_generation_options( if is_ui_design_generation || is_publication_material_generation { - Some(GPT_IMAGE_2_MODEL) + Some(GPT_IMAGE_2_5_BUSINESS_NAME) } else { payload.model.as_deref() }, @@ -3181,6 +3188,11 @@ where &settings, EditorImageProviderRequest { model: generation_options.model, + provider_model: if generation_options.model == EDITOR_IMAGE_MODEL_NANOBANANA2 { + generation_options.model + } else { + GPT_IMAGE_2_5_GENERATION_MODEL + }, prompt: submitted_prompt.as_str(), negative_prompt, size: provider_request_size.as_ref(), @@ -3832,7 +3844,7 @@ async fn resolve_editor_image_edit_price( "message": error.to_string(), })) })? - .image_generation_mud_points(Some("quick-edit"), Some(model), Some(price_size)); + .image_edit_model_mud_points(Some(model), Some(price_size)); Ok(expected_price_mud_points) } @@ -3975,7 +3987,15 @@ pub(crate) fn normalize_editor_generation_options( image_size: Option<&str>, ) -> EditorGenerationOptions { let normalized_model = match model.map(str::trim).filter(|value| !value.is_empty()) { - Some(GPT_IMAGE_2_MODEL) => GPT_IMAGE_2_MODEL, + // TODO: compatibility for persisted/client legacy `gpt-image-2`; do not + // rewrite the stored record, only use the current business value when a + // new task is submitted. + Some( + GPT_IMAGE_2_MODEL + | GPT_IMAGE_2_5_BUSINESS_NAME + | "gpt-image-2.5-flare-c" + | "gpt-image-2.5-sunburst-c", + ) => GPT_IMAGE_2_5_BUSINESS_NAME, Some( EDITOR_IMAGE_MODEL_NANOBANANA2 | EDITOR_IMAGE_MODEL_NANOBANANA2_DISPLAY_ALIAS @@ -3983,7 +4003,7 @@ pub(crate) fn normalize_editor_generation_options( ) => EDITOR_IMAGE_MODEL_NANOBANANA2, // 中文注释:未显式传模型的旧普通生成、快速编辑和生成规范继续走 gpt-image-2; // 角色 / 图标素材入口由前端显式传入 nanobanana2 默认值。 - None => GPT_IMAGE_2_MODEL, + None => GPT_IMAGE_2_5_BUSINESS_NAME, _ => EDITOR_IMAGE_MODEL_NANOBANANA2, }; let aspect_ratio = normalize_editor_generation_aspect_ratio(aspect_ratio); @@ -6048,7 +6068,7 @@ pub(crate) async fn edit_editor_image_for_owner_with_source_snapshot( create_openai_image_edit_with_references_and_model( &http_client, &settings, - generation_options.model, + GPT_IMAGE_2_5_EDIT_MODEL, prompt.as_str(), Some("文字、水印、边框、按钮、UI 控件、变形主体"), provider_size.as_str(), @@ -8493,7 +8513,7 @@ pub(crate) async fn extract_editor_ui_design_assets_for_owner( create_openai_image_edit_with_references_and_model( &http_client, &settings, - generation_options.model, + GPT_IMAGE_2_5_GENERATION_MODEL, prompt.as_str(), None, generation_options.provider_size.as_str(), @@ -17817,7 +17837,7 @@ mod tests { #[test] fn editor_generation_dimensions_follow_model_options() { let default_generation = normalize_editor_generation_options(None, Some("1:1"), Some("1K")); - assert_eq!(default_generation.model, GPT_IMAGE_2_MODEL); + assert_eq!(default_generation.model, GPT_IMAGE_2_5_BUSINESS_NAME); assert_eq!(default_generation.size, "1024x1024"); let nanobanana = normalize_editor_generation_options( @@ -17847,7 +17867,7 @@ mod tests { assert_eq!(legacy_nanobanana_alias.aspect_ratio, "16:9"); let gpt = normalize_editor_generation_options(Some("gpt-image-2"), Some("2:3"), Some("1K")); - assert_eq!(gpt.model, GPT_IMAGE_2_MODEL); + assert_eq!(gpt.model, GPT_IMAGE_2_5_BUSINESS_NAME); assert_eq!(gpt.size, "683x1024"); assert_eq!(gpt.provider_size, "688x1024"); assert_eq!(gpt.aspect_ratio, "2:3"); @@ -17862,21 +17882,21 @@ mod tests { let gpt_cover = normalize_editor_generation_options(Some("gpt-image-2"), Some("4:3"), Some("1K")); - assert_eq!(gpt_cover.model, GPT_IMAGE_2_MODEL); + assert_eq!(gpt_cover.model, GPT_IMAGE_2_5_BUSINESS_NAME); assert_eq!(gpt_cover.size, "1024x768"); assert_eq!(gpt_cover.provider_size, "1024x768"); assert_eq!(gpt_cover.aspect_ratio, "4:3"); let gpt_landscape_2k = normalize_editor_generation_options(Some("gpt-image-2"), Some("16:9"), Some("2K")); - assert_eq!(gpt_landscape_2k.model, GPT_IMAGE_2_MODEL); + assert_eq!(gpt_landscape_2k.model, GPT_IMAGE_2_5_BUSINESS_NAME); assert_eq!(gpt_landscape_2k.size, "2048x1152"); assert_eq!(gpt_landscape_2k.aspect_ratio, "16:9"); assert_eq!(gpt_landscape_2k.image_size, "2K"); let gpt_portrait_2k = normalize_editor_generation_options(Some("gpt-image-2"), Some("9:16"), Some("2K")); - assert_eq!(gpt_portrait_2k.model, GPT_IMAGE_2_MODEL); + assert_eq!(gpt_portrait_2k.model, GPT_IMAGE_2_5_BUSINESS_NAME); assert_eq!(gpt_portrait_2k.size, "1152x2048"); assert_eq!(gpt_portrait_2k.aspect_ratio, "9:16"); assert_eq!(gpt_portrait_2k.image_size, "2K"); @@ -19989,7 +20009,7 @@ mod tests { ) .expect("UI extraction dimensions should pass"); - assert_eq!(generation_options.model, GPT_IMAGE_2_MODEL); + assert_eq!(generation_options.model, GPT_IMAGE_2_5_BUSINESS_NAME); assert_eq!(generation_options.size, "2048x2048"); assert_eq!( resolve_editor_ui_design_asset_extraction_price( diff --git a/server-rs/crates/api-server/src/editor_project_icon.rs b/server-rs/crates/api-server/src/editor_project_icon.rs index d0e68e432..c55e3cfad 100644 --- a/server-rs/crates/api-server/src/editor_project_icon.rs +++ b/server-rs/crates/api-server/src/editor_project_icon.rs @@ -64,7 +64,7 @@ use crate::{ state::AppState, }; -const ICON_SPEC_MODEL: &str = "gpt-image-2"; +const ICON_SPEC_MODEL: &str = "gpt-image-2.5"; const ICON_SPEC_ASPECT_RATIO: &str = "16:9"; const ICON_SPEC_IMAGE_SIZE: &str = "2K"; const ICON_SPEC_SIZE: &str = "2048x1152"; diff --git a/server-rs/crates/api-server/src/openai_image_generation.rs b/server-rs/crates/api-server/src/openai_image_generation.rs index 2cf90c425..4c3cf2aea 100644 --- a/server-rs/crates/api-server/src/openai_image_generation.rs +++ b/server-rs/crates/api-server/src/openai_image_generation.rs @@ -1,17 +1,14 @@ use axum::http::StatusCode; use platform_image::{ - DownloadedImage, GeneratedImages, PlatformImageError, PlatformImageStatusHint, ReferenceImage, - VECTOR_ENGINE_PROVIDER, VectorEngineImageSettings, build_vector_engine_image_http_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, + DownloadedImage, GeneratedImages, ImageProvider, ImageProviderClient, ImageProviderSettings, + NANOBANANA_2_MODEL, PlatformImageError, PlatformImageStatusHint, ReferenceImage, + VECTOR_ENGINE_PROVIDER, build_image_http_client, 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, }; #[cfg(test)] -use platform_image::{ - build_vector_engine_image_request_body, vector_engine_images_edit_url, - vector_engine_images_generation_url, -}; +use platform_image::{build_image_request_body, images_edit_url, images_generation_url}; use serde_json::{Value, json}; use std::time::Instant; use time::OffsetDateTime; @@ -27,9 +24,10 @@ use crate::{ tracking::record_external_generation_run_after_success, }; -pub(crate) use platform_image::GPT_IMAGE_2_MODEL; -#[cfg(test)] -use platform_image::VECTOR_ENGINE_GPT_IMAGE_2_MODEL; +pub(crate) use platform_image::{ + GPT_IMAGE_2_5_BUSINESS_NAME, GPT_IMAGE_2_5_EDIT_MODEL, GPT_IMAGE_2_5_GENERATION_MODEL, + GPT_IMAGE_2_MODEL, +}; pub(crate) type OpenAiGeneratedImages = GeneratedImages; pub(crate) type DownloadedOpenAiImage = DownloadedImage; @@ -45,6 +43,8 @@ pub(crate) struct OpenAiImageSettings { pub external_api_audit_user_id: Option, pub external_api_audit_profile_id: Option, pub external_api_audit_request_id: Option, + pub tiantoken_client: Option, + pub vector_engine_client: Option, } impl std::fmt::Debug for OpenAiImageSettings { @@ -71,6 +71,11 @@ impl std::fmt::Debug for OpenAiImageSettings { "external_api_audit_request_id", &self.external_api_audit_request_id, ) + .field("tiantoken_client_enabled", &self.tiantoken_client.is_some()) + .field( + "vector_engine_client_enabled", + &self.vector_engine_client.is_some(), + ) .finish() } } @@ -110,18 +115,22 @@ pub(crate) fn require_openai_image_settings( external_api_audit_user_id: None, external_api_audit_profile_id: None, external_api_audit_request_id: None, + tiantoken_client: Some(state.tiantoken_image_client().clone()), + vector_engine_client: Some(state.vector_engine_image_client().clone()), }) } pub(crate) fn build_openai_image_http_client( settings: &OpenAiImageSettings, ) -> Result { - build_vector_engine_image_http_client(&settings.provider_settings()) - .map_err(map_platform_image_error) + if let Some(client) = settings.tiantoken_client.as_ref() { + return Ok(client.http_client().clone()); + } + build_image_http_client(&settings.provider_settings()).map_err(map_platform_image_error) } pub(crate) async fn create_openai_image_generation( - http_client: &reqwest::Client, + _http_client: &reqwest::Client, settings: &OpenAiImageSettings, prompt: &str, negative_prompt: Option<&str>, @@ -138,9 +147,11 @@ pub(crate) async fn create_openai_image_generation( "negativePromptChars": negative_prompt.map(str::chars).map(Iterator::count), "referenceImageCount": reference_images.len(), }); - let result = create_vector_engine_image_generation( - http_client, - &settings.provider_settings(), + let provider_client = settings.client_for_model(GPT_IMAGE_2_5_GENERATION_MODEL); + let provider_settings = settings.provider_settings_for_model(GPT_IMAGE_2_5_GENERATION_MODEL); + let result = create_image_generation( + provider_client.http_client(), + &provider_settings, prompt, negative_prompt, size, @@ -162,7 +173,7 @@ pub(crate) async fn create_openai_image_generation( #[allow(clippy::too_many_arguments)] pub(crate) async fn create_openai_image_generation_with_model( - http_client: &reqwest::Client, + _http_client: &reqwest::Client, settings: &OpenAiImageSettings, model: &str, prompt: &str, @@ -181,9 +192,11 @@ pub(crate) async fn create_openai_image_generation_with_model( "negativePromptChars": negative_prompt.map(str::chars).map(Iterator::count), "referenceImageCount": reference_images.len(), }); - let result = create_vector_engine_image_generation_with_model( - http_client, - &settings.provider_settings(), + let provider_client = settings.client_for_model(model); + let provider_settings = settings.provider_settings_for_model(model); + let result = create_image_generation_with_model( + provider_client.http_client(), + &provider_settings, model, prompt, negative_prompt, @@ -206,7 +219,7 @@ pub(crate) async fn create_openai_image_generation_with_model( #[allow(clippy::too_many_arguments)] pub(crate) async fn create_openai_nanobanana_generate_content( - http_client: &reqwest::Client, + _http_client: &reqwest::Client, settings: &OpenAiImageSettings, model: &str, prompt: &str, @@ -225,9 +238,11 @@ pub(crate) async fn create_openai_nanobanana_generate_content( "negativePromptChars": negative_prompt.map(str::chars).map(Iterator::count), "referenceImageCount": reference_images.len(), }); - let result = create_vector_engine_nanobanana_generate_content( - http_client, - &settings.provider_settings(), + let provider_client = settings.client_for_model(model); + let provider_settings = settings.provider_settings_for_model(model); + let result = create_nanobanana_generate_content( + provider_client.http_client(), + &provider_settings, model, prompt, negative_prompt, @@ -249,7 +264,7 @@ pub(crate) async fn create_openai_nanobanana_generate_content( } pub(crate) async fn create_openai_image_edit( - http_client: &reqwest::Client, + _http_client: &reqwest::Client, settings: &OpenAiImageSettings, prompt: &str, negative_prompt: Option<&str>, @@ -264,9 +279,11 @@ pub(crate) async fn create_openai_image_edit( "negativePromptChars": negative_prompt.map(str::chars).map(Iterator::count), "referenceImageCount": 1, }); - let result = create_vector_engine_image_edit( - http_client, - &settings.provider_settings(), + let provider_client = settings.client_for_model(GPT_IMAGE_2_5_EDIT_MODEL); + let provider_settings = settings.provider_settings_for_model(GPT_IMAGE_2_5_EDIT_MODEL); + let result = create_image_edit( + provider_client.http_client(), + &provider_settings, prompt, negative_prompt, size, @@ -286,7 +303,7 @@ pub(crate) async fn create_openai_image_edit( } pub(crate) async fn create_openai_image_edit_with_references( - http_client: &reqwest::Client, + _http_client: &reqwest::Client, settings: &OpenAiImageSettings, prompt: &str, negative_prompt: Option<&str>, @@ -303,9 +320,11 @@ pub(crate) async fn create_openai_image_edit_with_references( "negativePromptChars": negative_prompt.map(str::chars).map(Iterator::count), "referenceImageCount": reference_images.len(), }); - let result = create_vector_engine_image_edit_with_references( - http_client, - &settings.provider_settings(), + let provider_client = settings.client_for_model(GPT_IMAGE_2_5_EDIT_MODEL); + let provider_settings = settings.provider_settings_for_model(GPT_IMAGE_2_5_EDIT_MODEL); + let result = create_image_edit_with_references( + provider_client.http_client(), + &provider_settings, prompt, negative_prompt, size, @@ -327,7 +346,7 @@ pub(crate) async fn create_openai_image_edit_with_references( #[allow(clippy::too_many_arguments)] pub(crate) async fn create_openai_image_edit_with_references_and_model( - http_client: &reqwest::Client, + _http_client: &reqwest::Client, settings: &OpenAiImageSettings, model: &str, prompt: &str, @@ -345,9 +364,11 @@ pub(crate) async fn create_openai_image_edit_with_references_and_model( "negativePromptChars": negative_prompt.map(str::chars).map(Iterator::count), "referenceImageCount": reference_images.len(), }); - let result = create_vector_engine_image_edit_with_references_and_model( - http_client, - &settings.provider_settings(), + let provider_client = settings.client_for_model(model); + let provider_settings = settings.provider_settings_for_model(model); + let result = create_image_edit_with_references_and_model( + provider_client.http_client(), + &provider_settings, model, prompt, negative_prompt, @@ -376,7 +397,7 @@ pub(crate) fn build_openai_image_request_body( candidate_count: u32, reference_images: &[String], ) -> Value { - build_vector_engine_image_request_body( + build_image_request_body( prompt, negative_prompt, size, @@ -386,6 +407,24 @@ pub(crate) fn build_openai_image_request_body( } impl OpenAiImageSettings { + fn client_for_model(&self, model: &str) -> &ImageProviderClient { + if model == NANOBANANA_2_MODEL { + self.vector_engine_client + .as_ref() + .expect("vector engine image client is initialized at startup") + } else { + self.tiantoken_client + .as_ref() + .expect("tiantoken image client is initialized at startup") + } + } + + fn provider_settings_for_model(&self, model: &str) -> ImageProviderSettings { + let mut settings = self.client_for_model(model).settings().clone(); + settings.request_deadline = self.request_deadline; + settings + } + pub(crate) fn with_external_api_audit_actor( mut self, user_id: Option, @@ -409,8 +448,9 @@ impl OpenAiImageSettings { self } - pub(crate) fn provider_settings(&self) -> VectorEngineImageSettings { - VectorEngineImageSettings { + pub(crate) fn provider_settings(&self) -> ImageProviderSettings { + ImageProviderSettings { + provider: ImageProvider::Tiantoken, base_url: self.base_url.clone(), api_key: self.api_key.clone(), request_timeout_ms: self.request_timeout_ms.max(1), @@ -578,13 +618,13 @@ pub(crate) fn map_platform_image_error(error: PlatformImageError) -> AppError { } #[cfg(test)] -fn vector_engine_images_generation_url_for_test(settings: &OpenAiImageSettings) -> String { - vector_engine_images_generation_url(&settings.provider_settings()) +fn images_generation_url_for_test(settings: &OpenAiImageSettings) -> String { + images_generation_url(&settings.provider_settings()) } #[cfg(test)] -fn vector_engine_images_edit_url_for_test(settings: &OpenAiImageSettings) -> String { - vector_engine_images_edit_url(&settings.provider_settings()) +fn images_edit_url_for_test(settings: &OpenAiImageSettings) -> String { + images_edit_url(&settings.provider_settings()) } #[cfg(test)] @@ -611,6 +651,8 @@ mod tests { external_api_audit_user_id: None, external_api_audit_profile_id: None, external_api_audit_request_id: None, + tiantoken_client: None, + vector_engine_client: None, } .with_external_api_audit_context(&request_context, None, None); @@ -631,7 +673,7 @@ mod tests { &["data:image/png;base64,abcd".to_string()], ); - 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"], 2); assert!(body.get("official_fallback").is_none()); @@ -650,6 +692,8 @@ mod tests { external_api_audit_user_id: None, external_api_audit_profile_id: None, external_api_audit_request_id: None, + tiantoken_client: None, + vector_engine_client: None, }; let v1_settings = OpenAiImageSettings { base_url: "https://vector.example/v1".to_string(), @@ -660,14 +704,16 @@ mod tests { external_api_audit_user_id: None, external_api_audit_profile_id: None, external_api_audit_request_id: None, + tiantoken_client: None, + vector_engine_client: None, }; assert_eq!( - vector_engine_images_generation_url_for_test(&root_settings), + images_generation_url_for_test(&root_settings), "https://vector.example/v1/images/generations" ); assert_eq!( - vector_engine_images_generation_url_for_test(&v1_settings), + images_generation_url_for_test(&v1_settings), "https://vector.example/v1/images/generations" ); } @@ -683,6 +729,8 @@ mod tests { external_api_audit_user_id: None, external_api_audit_profile_id: None, external_api_audit_request_id: None, + tiantoken_client: None, + vector_engine_client: None, }; let v1_settings = OpenAiImageSettings { base_url: "https://vector.example/v1".to_string(), @@ -693,14 +741,16 @@ mod tests { external_api_audit_user_id: None, external_api_audit_profile_id: None, external_api_audit_request_id: None, + tiantoken_client: None, + vector_engine_client: None, }; assert_eq!( - vector_engine_images_edit_url_for_test(&root_settings), + images_edit_url_for_test(&root_settings), "https://vector.example/v1/images/edits" ); assert_eq!( - vector_engine_images_edit_url_for_test(&v1_settings), + images_edit_url_for_test(&v1_settings), "https://vector.example/v1/images/edits" ); } @@ -716,6 +766,8 @@ mod tests { external_api_audit_user_id: None, external_api_audit_profile_id: None, external_api_audit_request_id: None, + tiantoken_client: None, + vector_engine_client: None, }; let http_client = reqwest::Client::new(); @@ -764,7 +816,7 @@ mod tests { latency_ms: Some(321), prompt_chars: Some(42), reference_image_count: Some(1), - image_model: Some(VECTOR_ENGINE_GPT_IMAGE_2_MODEL), + image_model: Some(GPT_IMAGE_2_MODEL), }; let tracking = crate::external_api_audit::build_external_api_failure_tracking_draft( &build_external_api_failure_draft_from_platform_image_audit(&audit), @@ -782,10 +834,7 @@ mod tests { assert_eq!(tracking.metadata["retryable"], true); assert_eq!(tracking.metadata["promptChars"], 42); assert_eq!(tracking.metadata["referenceImageCount"], 1); - assert_eq!( - tracking.metadata["imageModel"], - VECTOR_ENGINE_GPT_IMAGE_2_MODEL - ); + assert_eq!(tracking.metadata["imageModel"], GPT_IMAGE_2_MODEL); } } diff --git a/server-rs/crates/api-server/src/raw_image.rs b/server-rs/crates/api-server/src/raw_image.rs index 0c243e0d1..03926e13a 100644 --- a/server-rs/crates/api-server/src/raw_image.rs +++ b/server-rs/crates/api-server/src/raw_image.rs @@ -6,8 +6,8 @@ use axum::{ use bytes::Bytes; use image::{ImageDecoder, ImageFormat, ImageReader}; use platform_image::{ - GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD, RAW_IMAGE_MAX_EDGE, RAW_IMAGE_MAX_PIXELS, - RawImageEditImage, RawImageEditOptions, create_vector_engine_raw_image_edit, + GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD, GPT_IMAGE_2_5_EDIT_MODEL, RAW_IMAGE_MAX_EDGE, + RAW_IMAGE_MAX_PIXELS, RawImageEditImage, RawImageEditOptions, create_raw_image_edit, validate_raw_image_edit_dimensions, }; use serde::Serialize; @@ -148,7 +148,7 @@ pub(crate) async fn edit_raw_image( }); let started_at_micros = (OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000) as i64; let operation = async move { - let generated = match create_vector_engine_raw_image_edit( + let generated = match create_raw_image_edit( &http_client, &provider_settings, prepared.prompt.as_str(), @@ -572,7 +572,7 @@ async fn raw_image_edit_price(state: &AppState, width: u32, height: u32) -> Resu .editor_generation_pricing() .await .map(|pricing| { - pricing.image_generation_mud_points(Some("quick-edit"), Some("gpt-image-2"), Some(tier)) + pricing.image_edit_model_mud_points(Some(GPT_IMAGE_2_5_EDIT_MODEL), Some(tier)) }) .map_err(|error| { AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({ diff --git a/server-rs/crates/api-server/src/state.rs b/server-rs/crates/api-server/src/state.rs index b4030cd62..49d721a48 100644 --- a/server-rs/crates/api-server/src/state.rs +++ b/server-rs/crates/api-server/src/state.rs @@ -23,6 +23,9 @@ use platform_auth::{ RefreshCookieConfig, RefreshCookieError, RefreshCookieSameSite, SmsAuthConfig, SmsAuthProvider, SmsAuthProviderKind, SmsProviderError, WechatProvider, sign_access_token, verify_access_token, }; +use platform_image::{ + ImageProvider, ImageProviderClient, ImageProviderSettings, build_image_provider_client, +}; use platform_llm::{LlmClient, LlmConfig, LlmError, LlmProvider, OpenAiChatTokenBudgetField}; use platform_matting::{MattingClient, MattingConfig}; use platform_oss::{OssClient, OssConfig, OssError}; @@ -306,6 +309,8 @@ pub struct AppStateInner { /// 非 Suno 的文本、图片和旧版音频生成 provider 配置。 tiantoken_base_url: String, tiantoken_api_key: Option, + vector_engine_image_client: ImageProviderClient, + tiantoken_image_client: ImageProviderClient, matting_client: Option, bgfilter_provider_http_client: reqwest::Client, bgfilter_worker_http_client: reqwest::Client, @@ -605,6 +610,18 @@ impl AppState { .map_err(|error| AppStateInitError::DependencyUnavailable(error.to_string()))?; let tiantoken_base_url = crate::config::tiantoken_base_url(&config); let tiantoken_api_key = crate::config::tiantoken_api_key(&config); + let vector_engine_image_client = build_required_image_provider_client( + ImageProvider::VectorEngine, + config.vector_engine_base_url.clone(), + config.vector_engine_api_key.clone(), + config.vector_engine_image_request_timeout_ms, + )?; + let tiantoken_image_client = build_required_image_provider_client( + ImageProvider::Tiantoken, + tiantoken_base_url.clone(), + tiantoken_api_key.clone(), + config.vector_engine_image_request_timeout_ms, + )?; let llm_client = build_llm_client(&config)?; let vector_engine_llm_client = build_vector_engine_llm_client( &config, @@ -688,6 +705,8 @@ impl AppState { vector_engine_llm_client, tiantoken_base_url, tiantoken_api_key, + vector_engine_image_client, + tiantoken_image_client, matting_client, bgfilter_provider_http_client, bgfilter_worker_http_client, @@ -1598,6 +1617,14 @@ impl AppState { self.tiantoken_api_key.as_deref() } + pub(crate) fn vector_engine_image_client(&self) -> &ImageProviderClient { + &self.vector_engine_image_client + } + + pub(crate) fn tiantoken_image_client(&self) -> &ImageProviderClient { + &self.tiantoken_image_client + } + pub fn matting_client(&self) -> Option<&MattingClient> { self.matting_client.as_ref() } @@ -2309,6 +2336,50 @@ impl AdminRuntime { } } +fn build_required_image_provider_client( + provider: ImageProvider, + base_url: String, + api_key: Option, + request_timeout_ms: u64, +) -> Result { + #[cfg(test)] + let (base_url, api_key) = ( + if base_url.trim().is_empty() { + "http://127.0.0.1".to_string() + } else { + base_url + }, + api_key.or_else(|| Some("test-key".to_string())), + ); + #[cfg(not(test))] + let (base_url, api_key) = (base_url, api_key); + let base_url = base_url.trim().trim_end_matches('/'); + if base_url.is_empty() { + return Err(AppStateInitError::DependencyUnavailable(format!( + "{} 图片 provider 缺少 BASE_URL 配置", + provider.as_str() + ))); + } + let api_key = api_key + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + .ok_or_else(|| { + AppStateInitError::DependencyUnavailable(format!( + "{} 图片 provider 缺少 API_KEY 配置", + provider.as_str() + )) + })?; + build_image_provider_client(ImageProviderSettings { + provider, + base_url: base_url.to_string(), + api_key: api_key.to_string(), + request_timeout_ms: request_timeout_ms.max(1), + request_deadline: None, + }) + .map_err(|error| AppStateInitError::DependencyUnavailable(error.to_string())) +} + fn build_oss_client(config: &AppConfig) -> Result, AppStateInitError> { let oss_fields = [ ("ALIYUN_OSS_BUCKET", config.oss_bucket.as_deref()), diff --git a/server-rs/crates/platform-editor-agent/src/agent/tools/edit_image.rs b/server-rs/crates/platform-editor-agent/src/agent/tools/edit_image.rs index 5dec72306..fa4cdfc30 100644 --- a/server-rs/crates/platform-editor-agent/src/agent/tools/edit_image.rs +++ b/server-rs/crates/platform-editor-agent/src/agent/tools/edit_image.rs @@ -2,7 +2,7 @@ use crate::agent::asset::ImageId; use crate::agent::prompt::{PENDING_USER_CONFIRMATION_MESSAGE, edit_image_tool_description}; use crate::agent::tools::context::EditorToolContext; use crate::framework::tool::{Tool, ToolFailure, ToolFailureKind}; -use platform_image::GPT_IMAGE_2_MODEL; +use platform_image::GPT_IMAGE_2_5_BUSINESS_NAME; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use std::error::Error; @@ -33,7 +33,7 @@ impl Display for EditImageError { EditImageError::InvalidModel(model) => { write!( f, - "{model} is not a valid model name, only {GPT_IMAGE_2_MODEL} is supported for now." + "{model} is not a valid model name, only {GPT_IMAGE_2_5_BUSINESS_NAME} is supported for now." ) } } @@ -52,7 +52,7 @@ pub struct EditImageToolArgs { pub model: String, } fn default_model_name() -> String { - GPT_IMAGE_2_MODEL.to_string() + GPT_IMAGE_2_5_BUSINESS_NAME.to_string() } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -90,9 +90,9 @@ impl Tool for EditImageTool { // TODO need to introduce size param, but that needs more metadata such as original image size, skip in this version "model": { "type": "string", - "enum": [GPT_IMAGE_2_MODEL], - "default": GPT_IMAGE_2_MODEL, - "description": format!("图片编辑固定使用{GPT_IMAGE_2_MODEL}") + "enum": [GPT_IMAGE_2_5_BUSINESS_NAME], + "default": GPT_IMAGE_2_5_BUSINESS_NAME, + "description": format!("图片编辑固定使用{GPT_IMAGE_2_5_BUSINESS_NAME}") } }, "required": ["object_image_id", "prompt"], @@ -155,7 +155,7 @@ pub struct EditorImageEditResult { impl EditImageTool { /// Validate the semantic correctness of the arguments. pub fn validate_args(&self, args: &EditImageToolArgs) -> Option { - if args.model != GPT_IMAGE_2_MODEL { + if args.model != GPT_IMAGE_2_5_BUSINESS_NAME { return Some(EditImageError::InvalidModel(args.model.clone())); } if args.prompt.trim().is_empty() { @@ -210,7 +210,7 @@ mod tests { "sourceType": "generated", "prompt": "修改图片", "actualPrompt": "修改后的图片", - "model": "gpt-image-2", + "model": "gpt-image-2.5", "taskId": "task-2", "resource": null, "asset": null, diff --git a/server-rs/crates/platform-editor-agent/src/agent/tools/generate_icon_spritesheet.rs b/server-rs/crates/platform-editor-agent/src/agent/tools/generate_icon_spritesheet.rs index 6db172ab9..082d66443 100644 --- a/server-rs/crates/platform-editor-agent/src/agent/tools/generate_icon_spritesheet.rs +++ b/server-rs/crates/platform-editor-agent/src/agent/tools/generate_icon_spritesheet.rs @@ -10,7 +10,7 @@ use crate::agent::tools::image_generation_options::{ validate_image_generation_options, }; use crate::framework::tool::{Tool, ToolFailure, ToolFailureKind}; -use platform_image::{GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL}; +use platform_image::{GPT_IMAGE_2_5_BUSINESS_NAME, NANOBANANA_2_MODEL}; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use std::error::Error; @@ -39,7 +39,7 @@ impl Display for GenerateIconSpritesheetError { match self { Self::InvalidModel(model) => write!( f, - "{model} is not a valid image model; supported models: {NANOBANANA_2_MODEL}, {GPT_IMAGE_2_MODEL}" + "{model} is not a valid image model; supported models: {NANOBANANA_2_MODEL}, {GPT_IMAGE_2_5_BUSINESS_NAME}" ), Self::InvalidAspectRatio(aspect_ratio) => { write!(f, "invalid aspect ratio: {aspect_ratio}") diff --git a/server-rs/crates/platform-editor-agent/src/agent/tools/generate_image.rs b/server-rs/crates/platform-editor-agent/src/agent/tools/generate_image.rs index f4f9950ef..293e3a822 100644 --- a/server-rs/crates/platform-editor-agent/src/agent/tools/generate_image.rs +++ b/server-rs/crates/platform-editor-agent/src/agent/tools/generate_image.rs @@ -8,7 +8,7 @@ use crate::agent::tools::image_generation_options::{ validate_image_generation_options, }; use crate::framework::tool::{Tool, ToolFailure, ToolFailureKind}; -use platform_image::{GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL}; +use platform_image::{GPT_IMAGE_2_5_BUSINESS_NAME, NANOBANANA_2_MODEL}; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use std::error::Error; @@ -33,11 +33,11 @@ impl Display for GenerateImageError { match self { Self::InvalidModel(model) => write!( f, - "{model} is not a valid image model; supported models: {NANOBANANA_2_MODEL}, {GPT_IMAGE_2_MODEL}" + "{model} is not a valid image model; supported models: {NANOBANANA_2_MODEL}, {GPT_IMAGE_2_5_BUSINESS_NAME}" ), Self::UnsupportedUiDesignModel(model) => write!( f, - "{model} is not supported for UI design generation; required model: {GPT_IMAGE_2_MODEL}" + "{model} is not supported for UI design generation; required model: {GPT_IMAGE_2_5_BUSINESS_NAME}" ), Self::InvalidAspectRatio(aspect_ratio) => { write!(f, "invalid aspect ratio: {aspect_ratio}") @@ -222,7 +222,7 @@ mod tests { "sourceType": "generated", "prompt": "生成一张图片", "actualPrompt": "生成一张清晰图片", - "model": "gpt-image-2", + "model": "gpt-image-2.5", "taskId": "task-1", "resource": null, "asset": null, diff --git a/server-rs/crates/platform-editor-agent/src/agent/tools/generate_ui_design.rs b/server-rs/crates/platform-editor-agent/src/agent/tools/generate_ui_design.rs index 295f29ef0..ae73c9a2f 100644 --- a/server-rs/crates/platform-editor-agent/src/agent/tools/generate_ui_design.rs +++ b/server-rs/crates/platform-editor-agent/src/agent/tools/generate_ui_design.rs @@ -12,7 +12,7 @@ use crate::agent::tools::image_generation_options::{ }; use crate::framework::tool::ToolFailureKind; use crate::framework::tool::{Tool, ToolFailure}; -use platform_image::GPT_IMAGE_2_MODEL; +use platform_image::GPT_IMAGE_2_5_BUSINESS_NAME; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; @@ -34,7 +34,7 @@ pub struct GenerateUiDesignToolArgs { } fn default_ui_design_model() -> String { - GPT_IMAGE_2_MODEL.to_string() + GPT_IMAGE_2_5_BUSINESS_NAME.to_string() } impl Tool for GenerateUiDesignTool { @@ -54,9 +54,9 @@ impl Tool for GenerateUiDesignTool { "prompt": { "type": "string", "description": "完整 UI 画面、信息层级、视觉风格和构图描述。" }, "model": { "type": "string", - "enum": [GPT_IMAGE_2_MODEL], - "default": GPT_IMAGE_2_MODEL, - "description": "UI 设计图固定使用 gpt-image-2。" + "enum": [GPT_IMAGE_2_5_BUSINESS_NAME], + "default": GPT_IMAGE_2_5_BUSINESS_NAME, + "description": "UI 设计图固定使用 gpt-image-2.5。" }, "reference_image_ids": { "type": "array", "items": { "type": "string" }, "description": "image_id(s) for desc UI 风格或布局" }, "aspect_ratio": image_aspect_ratio_parameter_schema(), @@ -95,7 +95,7 @@ impl Tool for GenerateUiDesignTool { impl GenerateUiDesignTool { pub fn validate_args(&self, args: &GenerateUiDesignToolArgs) -> Result<(), GenerateImageError> { - if args.model != GPT_IMAGE_2_MODEL { + if args.model != GPT_IMAGE_2_5_BUSINESS_NAME { return Err(GenerateImageError::UnsupportedUiDesignModel( args.model.clone(), )); @@ -165,11 +165,11 @@ mod tests { assert_eq!( parameters["properties"]["model"]["enum"], - json!([GPT_IMAGE_2_MODEL]) + json!([GPT_IMAGE_2_5_BUSINESS_NAME]) ); assert_eq!( parameters["properties"]["model"]["default"], - GPT_IMAGE_2_MODEL + GPT_IMAGE_2_5_BUSINESS_NAME ); assert_eq!( parameters["properties"]["image_size"]["enum"], @@ -184,7 +184,10 @@ mod tests { .as_array() .is_some_and(|required| required.contains(&json!("model"))) ); - assert!(tool.validate_args(&args(GPT_IMAGE_2_MODEL)).is_ok()); + assert!( + tool.validate_args(&args(GPT_IMAGE_2_5_BUSINESS_NAME)) + .is_ok() + ); assert!(matches!( tool.validate_args(&args(NANOBANANA_2_MODEL)), Err(GenerateImageError::UnsupportedUiDesignModel(model)) if model == NANOBANANA_2_MODEL @@ -198,7 +201,7 @@ mod tests { })) .expect("旧版 UI 设计参数应能反序列化"); - assert_eq!(args.model, GPT_IMAGE_2_MODEL); + assert_eq!(args.model, GPT_IMAGE_2_5_BUSINESS_NAME); assert!(tool().validate_args(&args).is_ok()); } } diff --git a/server-rs/crates/platform-editor-agent/src/agent/tools/image_generation_options.rs b/server-rs/crates/platform-editor-agent/src/agent/tools/image_generation_options.rs index c4f2ab218..5ea13ff47 100644 --- a/server-rs/crates/platform-editor-agent/src/agent/tools/image_generation_options.rs +++ b/server-rs/crates/platform-editor-agent/src/agent/tools/image_generation_options.rs @@ -1,4 +1,4 @@ -use platform_image::{GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL}; +use platform_image::{GPT_IMAGE_2_5_BUSINESS_NAME, NANOBANANA_2_MODEL}; use serde_json::{Value, json}; use std::error::Error; use std::fmt::Display; @@ -21,7 +21,7 @@ impl Display for ImageGenerationOptionsError { match self { Self::InvalidModel(model) => write!( f, - "{model} is not a valid image model; supported models: {NANOBANANA_2_MODEL}, {GPT_IMAGE_2_MODEL}" + "{model} is not a valid image model; supported models: {NANOBANANA_2_MODEL}, {GPT_IMAGE_2_5_BUSINESS_NAME}" ), Self::InvalidAspectRatio(aspect_ratio) => write!( f, @@ -75,9 +75,9 @@ pub fn validate_image_generation_options( pub fn image_model_parameter_schema() -> Value { json!({ "type": "string", - "enum": [NANOBANANA_2_MODEL, GPT_IMAGE_2_MODEL], + "enum": [NANOBANANA_2_MODEL, GPT_IMAGE_2_5_BUSINESS_NAME], "default": NANOBANANA_2_MODEL, - "description": "生图模型。默认 gemini-3.1-flash-image-preview(user may call it nanobanana2);也可选择 gpt-image-2。" + "description": "生图模型。默认 gemini-3.1-flash-image-preview(user may call it nanobanana2);也可选择 gpt-image-2.5。" }) } @@ -95,7 +95,7 @@ pub fn image_size_parameter_schema() -> Value { "type": "string", "enum": NANOBANANA_2_IMAGE_SIZES, "default": DEFAULT_IMAGE_SIZE, - "description": "图片尺寸档位。nanobanana2 支持 0.5K、1K、2K;gpt-image-2 仅支持 1K、2K;默认 1K。" + "description": "图片尺寸档位。nanobanana2 支持 0.5K、1K、2K;gpt-image-2.5 仅支持 1K、2K;默认 1K。" }) } @@ -104,14 +104,14 @@ pub fn gpt_image_2_size_parameter_schema() -> Value { "type": "string", "enum": GPT_IMAGE_2_IMAGE_SIZES, "default": DEFAULT_IMAGE_SIZE, - "description": "图片尺寸档位。gpt-image-2 仅支持 1K、2K;默认 1K。" + "description": "图片尺寸档位。gpt-image-2.5 仅支持 1K、2K;默认 1K。" }) } pub fn image_model_size_constraint_schema() -> Value { json!({ "if": { - "properties": { "model": { "const": GPT_IMAGE_2_MODEL } }, + "properties": { "model": { "const": GPT_IMAGE_2_5_BUSINESS_NAME } }, // model 省略时运行时默认 nanobanana2,仍允许 0.5K。 "required": ["model"] }, @@ -126,7 +126,7 @@ pub fn image_model_size_constraint_schema() -> Value { fn supported_image_sizes(model: &str) -> Option<&'static [&'static str]> { match model { NANOBANANA_2_MODEL => Some(NANOBANANA_2_IMAGE_SIZES), - GPT_IMAGE_2_MODEL => Some(GPT_IMAGE_2_IMAGE_SIZES), + GPT_IMAGE_2_5_BUSINESS_NAME => Some(GPT_IMAGE_2_IMAGE_SIZES), _ => None, } } @@ -150,14 +150,18 @@ mod tests { } for image_size in GPT_IMAGE_2_IMAGE_SIZES { assert!( - validate_image_generation_options(GPT_IMAGE_2_MODEL, aspect_ratio, image_size,) - .is_ok() + validate_image_generation_options( + GPT_IMAGE_2_5_BUSINESS_NAME, + aspect_ratio, + image_size, + ) + .is_ok() ); } } assert!(matches!( - validate_image_generation_options(GPT_IMAGE_2_MODEL, "1:1", "0.5K"), + validate_image_generation_options(GPT_IMAGE_2_5_BUSINESS_NAME, "1:1", "0.5K"), Err(ImageGenerationOptionsError::InvalidImageSize { .. }) )); assert!(matches!( @@ -192,7 +196,7 @@ mod tests { let model_size_constraint = image_model_size_constraint_schema(); assert_eq!( model_size_constraint["if"]["properties"]["model"]["const"], - GPT_IMAGE_2_MODEL + GPT_IMAGE_2_5_BUSINESS_NAME ); assert_eq!(model_size_constraint["if"]["required"], json!(["model"])); assert_eq!( diff --git a/server-rs/crates/platform-editor-agent/src/agent/tools/mod.rs b/server-rs/crates/platform-editor-agent/src/agent/tools/mod.rs index 69daaf15b..0425cfbdb 100644 --- a/server-rs/crates/platform-editor-agent/src/agent/tools/mod.rs +++ b/server-rs/crates/platform-editor-agent/src/agent/tools/mod.rs @@ -26,7 +26,7 @@ mod tests { use super::generate_video::{GenerateVideoTool, GenerateVideoToolArgs}; use crate::framework::tool::Tool; use platform_audio::{ELEVENLABS_SOUND_EFFECT_MODEL, SUNO_DEFAULT_MODEL}; - use platform_image::{GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL}; + use platform_image::{GPT_IMAGE_2_5_BUSINESS_NAME, NANOBANANA_2_MODEL}; use serde_json::json; #[test] @@ -69,11 +69,11 @@ mod tests { assert_eq!(image.model, NANOBANANA_2_MODEL); assert_eq!(image.aspect_ratio, "1:1"); assert_eq!(image.image_size, "1K"); - assert_eq!(edit.model, GPT_IMAGE_2_MODEL); + assert_eq!(edit.model, GPT_IMAGE_2_5_BUSINESS_NAME); assert_eq!(character.model, NANOBANANA_2_MODEL); assert_eq!(character.aspect_ratio, "1:1"); assert_eq!(character.image_size, "1K"); - assert_eq!(ui_design.model, GPT_IMAGE_2_MODEL); + assert_eq!(ui_design.model, GPT_IMAGE_2_5_BUSINESS_NAME); assert_eq!(ui_design.aspect_ratio, "1:1"); assert_eq!(ui_design.image_size, "1K"); assert_eq!(icon.model, NANOBANANA_2_MODEL); @@ -133,14 +133,17 @@ mod tests { }; assert!(tool.validate_args(&args(NANOBANANA_2_MODEL)).is_ok()); - assert!(tool.validate_args(&args(GPT_IMAGE_2_MODEL)).is_ok()); + assert!( + tool.validate_args(&args(GPT_IMAGE_2_5_BUSINESS_NAME)) + .is_ok() + ); assert!(matches!( tool.validate_args(&args("unknown-image-model")), Err(GenerateImageError::InvalidModel(_)) )); assert_eq!( tool.parameters()["properties"]["model"]["enum"], - json!([NANOBANANA_2_MODEL, GPT_IMAGE_2_MODEL]) + json!([NANOBANANA_2_MODEL, GPT_IMAGE_2_5_BUSINESS_NAME]) ); } @@ -166,7 +169,7 @@ mod tests { )); let ui_args = GenerateUiDesignToolArgs { prompt: "生成游戏主界面".to_string(), - model: GPT_IMAGE_2_MODEL.to_string(), + model: GPT_IMAGE_2_5_BUSINESS_NAME.to_string(), reference_image_ids: vec![missing_image.clone()], aspect_ratio: "16:9".to_string(), image_size: "1K".to_string(), @@ -208,7 +211,7 @@ mod tests { assert_eq!( edit["properties"]["model"]["enum"], - json!([GPT_IMAGE_2_MODEL]) + json!([GPT_IMAGE_2_5_BUSINESS_NAME]) ); assert_eq!( video["properties"]["duration_seconds"]["enum"], @@ -229,7 +232,7 @@ mod tests { for schema in [&image, &character, &icon] { assert_eq!( schema["allOf"][0]["if"]["properties"]["model"]["const"], - json!(GPT_IMAGE_2_MODEL) + json!(GPT_IMAGE_2_5_BUSINESS_NAME) ); assert_eq!( schema["allOf"][0]["then"]["properties"]["image_size"]["enum"], diff --git a/src/components/image-editor/ImageCanvasGenerationModel.ts b/src/components/image-editor/ImageCanvasGenerationModel.ts index 4929dcedd..d263af996 100644 --- a/src/components/image-editor/ImageCanvasGenerationModel.ts +++ b/src/components/image-editor/ImageCanvasGenerationModel.ts @@ -44,6 +44,7 @@ import { // 中文注释:与 api-server/src/editor_generation_config.rs 保持同名模型定价语义,前端仅作为展示兜底。 export const IMAGE_MODEL_GPT_IMAGE_2 = 'gpt-image-2'; +export const IMAGE_MODEL_GPT_IMAGE_2_5 = 'gpt-image-2.5'; export const IMAGE_MODEL_NANOBANANA2 = 'gemini-3.1-flash-image-preview'; export const DEFAULT_IMAGE_MODEL = IMAGE_MODEL_NANOBANANA2; const IMAGE_MODEL_NANOBANANA_ALIASES = new Set([ @@ -51,7 +52,7 @@ const IMAGE_MODEL_NANOBANANA_ALIASES = new Set([ 'nanobanana2', 'nano-banana', ]); -export const SPEC_GENERATION_MODEL = IMAGE_MODEL_GPT_IMAGE_2; +export const SPEC_GENERATION_MODEL = IMAGE_MODEL_GPT_IMAGE_2_5; export const SPEC_GENERATION_ASPECT_RATIO = '16:9'; export const SPEC_GENERATION_IMAGE_SIZE = '2K'; export const SPEC_GENERATION_SIZE = '2048x1152'; @@ -93,6 +94,12 @@ export const EDITOR_IMAGE_MODEL_MUD_POINT_CONFIG = { '1K': 12, '2K': 24, }, + [IMAGE_MODEL_GPT_IMAGE_2_5]: { + '1K': 3, + '2K': 5, + }, + // Legacy persisted model value; keep readable for old assets without exposing + // it as a new selectable model. [IMAGE_MODEL_GPT_IMAGE_2]: { '1K': 3, '2K': 5, @@ -127,7 +134,7 @@ export const QUICK_EDIT_SIZE_PRESETS = [ ] as const; export const EDITOR_IMAGE_MODEL_OPTIONS = [ { label: 'nanobanana2', value: IMAGE_MODEL_NANOBANANA2 }, - { label: 'gpt-image-2', value: IMAGE_MODEL_GPT_IMAGE_2 }, + { label: 'GPT Image 2.5', value: IMAGE_MODEL_GPT_IMAGE_2_5 }, ] as const; export const QUICK_EDIT_MODEL_OPTIONS = EDITOR_IMAGE_MODEL_OPTIONS; export const EDITOR_IMAGE_DIMENSION_OPTIONS = { @@ -135,6 +142,10 @@ export const EDITOR_IMAGE_DIMENSION_OPTIONS = { aspectRatios: ['1:1', '4:3', '3:2', '2:3', '9:16', '16:9'], imageSizes: ['0.5K', '1K', '2K'], }, + [IMAGE_MODEL_GPT_IMAGE_2_5]: { + aspectRatios: ['1:1', '4:3', '3:2', '2:3', '9:16', '16:9'], + imageSizes: ['1K', '2K'], + }, [IMAGE_MODEL_GPT_IMAGE_2]: { aspectRatios: ['1:1', '4:3', '3:2', '2:3', '9:16', '16:9'], imageSizes: ['1K', '2K'], @@ -381,9 +392,9 @@ export const EDITOR_MODEL_MUD_POINT_CONFIG = { unit: 'perGeneration', prices: EDITOR_IMAGE_MODEL_MUD_POINT_CONFIG[IMAGE_MODEL_NANOBANANA2], }, - [IMAGE_MODEL_GPT_IMAGE_2]: { + [IMAGE_MODEL_GPT_IMAGE_2_5]: { unit: 'perGeneration', - prices: EDITOR_IMAGE_MODEL_MUD_POINT_CONFIG[IMAGE_MODEL_GPT_IMAGE_2], + prices: EDITOR_IMAGE_MODEL_MUD_POINT_CONFIG[IMAGE_MODEL_GPT_IMAGE_2_5], }, [VIDEO_MODEL_SEEDANCE_2_FAST]: { unit: 'perSecond', @@ -1452,7 +1463,7 @@ export function decodeCanvasGenerationInputs( } else if (action === 'ui-design.generate') { normalizeStringField('prompt', '用户输入'); normalizeImageParameters({ - defaultModel: IMAGE_MODEL_GPT_IMAGE_2, + defaultModel: IMAGE_MODEL_GPT_IMAGE_2_5, defaultAspectRatio: '16:9', }); } else if (action === 'image.edit') { @@ -1993,7 +2004,7 @@ export function buildUiDesignGenerationInputs( ...createGenerationInputField('用户输入', prompt, 'prompt'), ...createGenerationInputField( '模型', - options.model ?? IMAGE_MODEL_GPT_IMAGE_2, + options.model ?? IMAGE_MODEL_GPT_IMAGE_2_5, 'model', ), ...createGenerationInputField(