接入 GPT Image 2.5 双 provider 启动路由
启动时分别构造 VectorEngine 与 Tiantoken 图片 client 按生成、编辑和 nanobanana 模型选择具体 provider 迁移 api-server、编辑器 Agent 与 raw edit 调用方 拆分 GPT Image 2.5 生成与编辑定价并隐藏 provider 具体值
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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<GeneratedCharacterVisuals, AppError> {
|
||||
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]
|
||||
|
||||
@@ -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<String> {
|
||||
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<String> {
|
||||
@@ -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]
|
||||
|
||||
@@ -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::<EditorGenerationPricingConfig>(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>,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<String>,
|
||||
pub external_api_audit_profile_id: Option<String>,
|
||||
pub external_api_audit_request_id: Option<String>,
|
||||
pub tiantoken_client: Option<ImageProviderClient>,
|
||||
pub vector_engine_client: Option<ImageProviderClient>,
|
||||
}
|
||||
|
||||
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<reqwest::Client, AppError> {
|
||||
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<String>,
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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!({
|
||||
|
||||
@@ -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<String>,
|
||||
vector_engine_image_client: ImageProviderClient,
|
||||
tiantoken_image_client: ImageProviderClient,
|
||||
matting_client: Option<MattingClient>,
|
||||
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<String>,
|
||||
request_timeout_ms: u64,
|
||||
) -> Result<ImageProviderClient, AppStateInitError> {
|
||||
#[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<Option<OssClient>, AppStateInitError> {
|
||||
let oss_fields = [
|
||||
("ALIYUN_OSS_BUCKET", config.oss_bucket.as_deref()),
|
||||
|
||||
@@ -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<EditImageError> {
|
||||
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,
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
+16
-12
@@ -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!(
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user