From bd0a06e3d3ca62e4211999f8f58334be302655fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Sat, 19 Sep 2026 13:55:30 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=BC=E5=AE=B9=E5=9B=BE=E7=89=87=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E7=9A=84=E5=8E=86=E5=8F=B2=20gpt-image-2=20=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E5=88=AB=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit api-server 的图片类 Agent 工具在反序列化前把 gpt-image-2 / gpt-image-2-c 归一为 gpt-image-2.5,旧持久化工具调用不再被校验直接拒绝 EditImageTool 计价改用 gpt-image-2.5 业务模型常量,并新增别名归一单测、更新受影响的校验与计价断言 --- .../api-server/src/editor_agent/tool.rs | 133 ++++++++++++++---- 1 file changed, 103 insertions(+), 30 deletions(-) diff --git a/server-rs/crates/api-server/src/editor_agent/tool.rs b/server-rs/crates/api-server/src/editor_agent/tool.rs index 0805fb4d4..a1ec7b5c0 100644 --- a/server-rs/crates/api-server/src/editor_agent/tool.rs +++ b/server-rs/crates/api-server/src/editor_agent/tool.rs @@ -28,7 +28,7 @@ use platform_editor_agent::agent::tools::generate_video::{ }; use platform_editor_agent::framework::error::PromptError; use platform_editor_agent::framework::tool::{Tool, ToolDyn, null_tool_args_as_missing}; -use platform_image::GPT_IMAGE_2_MODEL; +use platform_image::{GPT_IMAGE_2_5_BUSINESS_NAME, GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_MODEL}; use serde::Serialize; use serde::de::DeserializeOwned; use serde_json::{Value, json}; @@ -224,6 +224,45 @@ fn parse_internal( }) } +/// 把持久化的历史 GPT Image 2 模型别名(`gpt-image-2` / `gpt-image-2-c`)改写为当前 +/// 业务模型 `gpt-image-2.5`。 +/// +/// 平台层图片工具只接受 `gemini-3.1-flash-image-preview` 与 `gpt-image-2.5`,但历史会话 +/// 里仍可能保存着 `gpt-image-2` 的工具调用参数。这里在进入强类型校验前做一次别名归一, +/// 既让旧调用继续可用,也把计价统一到当前业务模型档位。 +fn normalize_legacy_image_model_args(value: &Value) -> Value { + let Some(model) = value.get("model").and_then(Value::as_str) else { + return value.clone(); + }; + if !matches!(model.trim(), GPT_IMAGE_2_MODEL | GPT_IMAGE_2_C_MODEL) { + return value.clone(); + } + let mut normalized = value.clone(); + if let Some(fields) = normalized.as_object_mut() { + fields.insert( + "model".to_string(), + Value::String(GPT_IMAGE_2_5_BUSINESS_NAME.to_string()), + ); + } + normalized +} + +/// 图片类工具的 `validate_args` 入口:先归一历史模型别名,再走强类型反序列化。 +fn parse_invalid_image_args( + tool_name: &str, + value: &Value, +) -> Result { + parse_invalid_args(tool_name, &normalize_legacy_image_model_args(value)) +} + +/// 图片类工具的 `pricing` 入口:即使调用方直接传入原始 args,也要先归一历史模型别名。 +fn parse_internal_image_args( + label: &str, + value: &Value, +) -> Result { + parse_internal(label, &normalize_legacy_image_model_args(value)) +} + /// SFX V2 的显式 `duration: null` 表示自动时长,必须原样保留,不能被通用的 /// “顶层 null 当缺省”兼容层恢复成手动 5 秒。但其余字段仍要走该兼容层: /// `model` 是无 Option 的 String,LLM 传 `model: null` 时若不剥离会直接反序列化失败。 @@ -311,7 +350,7 @@ fn editor_agent_image_mud_points( impl EditorAgentTool for GenerateImageTool { fn validate_args(&self, args: &Value) -> Result { - let args: GenerateImageToolArgs = parse_invalid_args(Self::NAME, args)?; + let args: GenerateImageToolArgs = parse_invalid_image_args(Self::NAME, args)?; GenerateImageTool::validate_args(self, &args) .map_err(|error| EditorAgentToolError::invalid_args(error.to_string()))?; serialize_normalized_args(Self::NAME, &args) @@ -322,7 +361,7 @@ impl EditorAgentTool for GenerateImageTool { pricing: &EditorGenerationPricingConfig, args: &Value, ) -> Result { - let args: GenerateImageToolArgs = parse_internal("generate image args", args)?; + let args: GenerateImageToolArgs = parse_internal_image_args("generate image args", args)?; Ok(editor_agent_image_mud_points( pricing, None, @@ -337,7 +376,8 @@ impl EditorAgentTool for GenerateImageTool { pricing: &EditorGenerationPricingConfig, ) -> Result { let price_mud_points = self.pricing(pricing, args)?; - let args: GenerateImageToolArgs = parse_internal("generate image display args", args)?; + let args: GenerateImageToolArgs = + parse_internal_image_args("generate image display args", args)?; let mut display_args = EditorAgentToolCallDisplayArgs::default(); push_image_generation_display_args( &mut display_args, @@ -358,7 +398,7 @@ impl EditorAgentTool for GenerateImageTool { context: &EditorAgentPrepareJobContext<'_>, ) -> Result { let price_mud_points = self.pricing(context.pricing, args)?; - let args: GenerateImageToolArgs = parse_invalid_args(Self::NAME, args)?; + let args: GenerateImageToolArgs = parse_invalid_image_args(Self::NAME, args)?; let title = args.prompt.clone(); let reference_image_srcs = resolve_image_ids(&args.reference_image_ids, &self.context)?; let payload = EditorImageGenerationRequest { @@ -397,7 +437,7 @@ impl EditorAgentTool for GenerateImageTool { args: &Value, result: &Value, ) -> Result { - let args: GenerateImageToolArgs = parse_internal("generate image args", args)?; + let args: GenerateImageToolArgs = parse_internal_image_args("generate image args", args)?; let result: EditorImageGenerationResult = parse_internal("generate image result", result)?; Ok(GenerateImageTool::format_execute_message( self, &args, &result, @@ -418,7 +458,7 @@ impl EditorAgentTool for GenerateImageTool { impl EditorAgentTool for GenerateCharacterTool { fn validate_args(&self, args: &Value) -> Result { - let args: GenerateCharacterToolArgs = parse_invalid_args(Self::NAME, args)?; + let args: GenerateCharacterToolArgs = parse_invalid_image_args(Self::NAME, args)?; GenerateCharacterTool::validate_args(self, &args) .map_err(|error| EditorAgentToolError::invalid_args(error.to_string()))?; serialize_normalized_args(Self::NAME, &args) @@ -429,7 +469,8 @@ impl EditorAgentTool for GenerateCharacterTool { pricing: &EditorGenerationPricingConfig, args: &Value, ) -> Result { - let args: GenerateCharacterToolArgs = parse_internal("generate character args", args)?; + let args: GenerateCharacterToolArgs = + parse_internal_image_args("generate character args", args)?; Ok(editor_agent_image_mud_points( pricing, Some("character"), @@ -445,7 +486,7 @@ impl EditorAgentTool for GenerateCharacterTool { ) -> Result { let price_mud_points = self.pricing(pricing, args)?; let args: GenerateCharacterToolArgs = - parse_internal("generate character display args", args)?; + parse_internal_image_args("generate character display args", args)?; let mut display_args = EditorAgentToolCallDisplayArgs::default(); push_image_generation_display_args( &mut display_args, @@ -466,7 +507,7 @@ impl EditorAgentTool for GenerateCharacterTool { context: &EditorAgentPrepareJobContext<'_>, ) -> Result { let price_mud_points = self.pricing(context.pricing, args)?; - let args: GenerateCharacterToolArgs = parse_invalid_args(Self::NAME, args)?; + let args: GenerateCharacterToolArgs = parse_invalid_image_args(Self::NAME, args)?; let title = args.prompt.clone(); let reference_image_srcs = resolve_image_ids(&args.reference_image_ids, &self.context)?; let payload = EditorImageGenerationRequest { @@ -505,7 +546,8 @@ impl EditorAgentTool for GenerateCharacterTool { args: &Value, result: &Value, ) -> Result { - let args: GenerateCharacterToolArgs = parse_internal("generate character args", args)?; + let args: GenerateCharacterToolArgs = + parse_internal_image_args("generate character args", args)?; let result: EditorImageGenerationResult = parse_internal("generate character result", result)?; Ok(GenerateCharacterTool::format_execute_message( @@ -528,7 +570,7 @@ impl EditorAgentTool for GenerateCharacterTool { impl EditorAgentTool for GenerateUiDesignTool { fn validate_args(&self, args: &Value) -> Result { - let args: GenerateUiDesignToolArgs = parse_invalid_args(Self::NAME, args)?; + let args: GenerateUiDesignToolArgs = parse_invalid_image_args(Self::NAME, args)?; GenerateUiDesignTool::validate_args(self, &args) .map_err(|error| EditorAgentToolError::invalid_args(error.to_string()))?; serialize_normalized_args(Self::NAME, &args) @@ -539,7 +581,8 @@ impl EditorAgentTool for GenerateUiDesignTool { pricing: &EditorGenerationPricingConfig, args: &Value, ) -> Result { - let args: GenerateUiDesignToolArgs = parse_internal("generate UI design args", args)?; + let args: GenerateUiDesignToolArgs = + parse_internal_image_args("generate UI design args", args)?; Ok(editor_agent_image_mud_points( pricing, Some("ui-design"), @@ -555,7 +598,7 @@ impl EditorAgentTool for GenerateUiDesignTool { ) -> Result { let price_mud_points = self.pricing(pricing, args)?; let args: GenerateUiDesignToolArgs = - parse_internal("generate UI design display args", args)?; + parse_internal_image_args("generate UI design display args", args)?; let mut display_args = EditorAgentToolCallDisplayArgs::default(); push_image_generation_display_args( &mut display_args, @@ -576,7 +619,7 @@ impl EditorAgentTool for GenerateUiDesignTool { context: &EditorAgentPrepareJobContext<'_>, ) -> Result { let price_mud_points = self.pricing(context.pricing, args)?; - let args: GenerateUiDesignToolArgs = parse_invalid_args(Self::NAME, args)?; + let args: GenerateUiDesignToolArgs = parse_invalid_image_args(Self::NAME, args)?; let title = args.prompt.clone(); let reference_image_srcs = resolve_image_ids(&args.reference_image_ids, &self.context)?; let payload = EditorImageGenerationRequest { @@ -615,7 +658,8 @@ impl EditorAgentTool for GenerateUiDesignTool { args: &Value, result: &Value, ) -> Result { - let args: GenerateUiDesignToolArgs = parse_internal("generate UI design args", args)?; + let args: GenerateUiDesignToolArgs = + parse_internal_image_args("generate UI design args", args)?; let result: EditorImageGenerationResult = parse_internal("generate UI design result", result)?; Ok(GenerateUiDesignTool::format_execute_message( @@ -638,7 +682,7 @@ impl EditorAgentTool for GenerateUiDesignTool { impl EditorAgentTool for EditImageTool { fn validate_args(&self, args: &Value) -> Result { - let args: EditImageToolArgs = parse_invalid_args(Self::NAME, args)?; + let args: EditImageToolArgs = parse_invalid_image_args(Self::NAME, args)?; if let Some(error) = EditImageTool::validate_args(self, &args) { return Err(EditorAgentToolError::invalid_args(error.to_string())); } @@ -650,11 +694,11 @@ impl EditorAgentTool for EditImageTool { pricing: &EditorGenerationPricingConfig, args: &Value, ) -> Result { - let _: EditImageToolArgs = parse_internal("edit image args", args)?; + let _: EditImageToolArgs = parse_internal_image_args("edit image args", args)?; Ok(editor_agent_image_mud_points( pricing, Some("quick-edit"), - GPT_IMAGE_2_MODEL, + GPT_IMAGE_2_5_BUSINESS_NAME, Some("1K"), )) } @@ -665,7 +709,7 @@ impl EditorAgentTool for EditImageTool { pricing: &EditorGenerationPricingConfig, ) -> Result { let price_mud_points = self.pricing(pricing, args)?; - let args: EditImageToolArgs = parse_internal("edit image display args", args)?; + let args: EditImageToolArgs = parse_internal_image_args("edit image display args", args)?; let mut display_args = EditorAgentToolCallDisplayArgs::default(); push_string_display_arg(&mut display_args, "prompt", "修改要求", args.prompt); push_string_display_arg(&mut display_args, "model", "模型", args.model); @@ -694,7 +738,7 @@ impl EditorAgentTool for EditImageTool { context: &EditorAgentPrepareJobContext<'_>, ) -> Result { let price_mud_points = self.pricing(context.pricing, args)?; - let args: EditImageToolArgs = parse_invalid_args(Self::NAME, args)?; + let args: EditImageToolArgs = parse_invalid_image_args(Self::NAME, args)?; let source_reference_id = self .context .image_metadata(&args.object_image_id) @@ -739,7 +783,7 @@ impl EditorAgentTool for EditImageTool { args: &Value, result: &Value, ) -> Result { - let args: EditImageToolArgs = parse_internal("edit image args", args)?; + let args: EditImageToolArgs = parse_internal_image_args("edit image args", args)?; let result: EditorImageEditResult = parse_internal("edit image result", result)?; Ok(EditImageTool::format_execute_message(self, args, result)) } @@ -785,7 +829,7 @@ fn resolve_icon_spec_reference_id<'a>( impl EditorAgentTool for GenerateIconSpritesheetTool { fn validate_args(&self, args: &Value) -> Result { - let args: GenerateIconSpritesheetToolArgs = parse_invalid_args(Self::NAME, args)?; + let args: GenerateIconSpritesheetToolArgs = parse_invalid_image_args(Self::NAME, args)?; let args = GenerateIconSpritesheetTool::normalize_args(self, args) .map_err(|error| EditorAgentToolError::invalid_args(error.to_string()))?; resolve_icon_spec_reference_id(&self.context, &args.reference_image_id)?; @@ -798,7 +842,7 @@ impl EditorAgentTool for GenerateIconSpritesheetTool { args: &Value, ) -> Result { let args: GenerateIconSpritesheetToolArgs = - parse_internal("generate icon spritesheet args", args)?; + parse_internal_image_args("generate icon spritesheet args", args)?; Ok(editor_agent_image_mud_points( pricing, Some("icon"), @@ -814,7 +858,7 @@ impl EditorAgentTool for GenerateIconSpritesheetTool { ) -> Result { let price_mud_points = self.pricing(pricing, args)?; let args: GenerateIconSpritesheetToolArgs = - parse_internal("generate icon spritesheet display args", args)?; + parse_internal_image_args("generate icon spritesheet display args", args)?; let mut display_args = EditorAgentToolCallDisplayArgs::default(); push_string_display_arg( &mut display_args, @@ -855,7 +899,7 @@ impl EditorAgentTool for GenerateIconSpritesheetTool { context: &EditorAgentPrepareJobContext<'_>, ) -> Result { let price_mud_points = self.pricing(context.pricing, args)?; - let args: GenerateIconSpritesheetToolArgs = parse_invalid_args(Self::NAME, args)?; + let args: GenerateIconSpritesheetToolArgs = parse_invalid_image_args(Self::NAME, args)?; let reference_id = resolve_icon_spec_reference_id(&self.context, &args.reference_image_id)?.to_string(); let reference_image_srcs = resolve_image_ids(&args.reference_image_ids, &self.context)?; @@ -901,7 +945,7 @@ impl EditorAgentTool for GenerateIconSpritesheetTool { result: &Value, ) -> Result { let args: GenerateIconSpritesheetToolArgs = - parse_internal("generate icon spritesheet args", args)?; + parse_internal_image_args("generate icon spritesheet args", args)?; let result: EditorIconSpritesheetResult = parse_internal("generate icon spritesheet result", result)?; Ok(GenerateIconSpritesheetTool::format_execute_message( @@ -1248,7 +1292,9 @@ mod tests { use spacetime_client::{EditorCanvasRecord, EditorCanvasViewportRecord}; use super::*; - use crate::editor_generation_config::load_editor_generation_pricing_from_paths; + use crate::editor_generation_config::{ + EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION, load_editor_generation_pricing_from_paths, + }; fn pricing() -> EditorGenerationPricingConfig { load_editor_generation_pricing_from_paths(None).expect("default editor pricing should load") @@ -1348,7 +1394,7 @@ mod tests { "unknown": "drop-me" })) .expect("typed UI validation should pass"); - assert_eq!(normalized["model"], GPT_IMAGE_2_MODEL); + assert_eq!(normalized["model"], GPT_IMAGE_2_5_BUSINESS_NAME); assert_eq!(normalized["aspect_ratio"], "1:1"); assert_eq!(normalized["image_size"], "1K"); assert!(normalized.get("unknown").is_none()); @@ -1493,6 +1539,31 @@ mod tests { ); } + #[test] + fn dyn_validation_normalizes_legacy_gpt_image_2_alias() { + let edit = editor_agent_tool(EditImageTool::NAME, &context_with_image("image-1")) + .expect("edit tool should resolve"); + let normalized = edit + .validate_args(&json!({ + "object_image_id": "image-1", + "prompt": "把背景换成夜晚", + "model": GPT_IMAGE_2_MODEL + })) + .expect("legacy edit args should validate"); + assert_eq!(normalized["model"], GPT_IMAGE_2_5_BUSINESS_NAME); + + let image = editor_agent_tool(GenerateImageTool::NAME, &EditorToolContext::default()) + .expect("image tool should resolve"); + let normalized = image + .validate_args(&json!({ + "prompt": "生成图片", + "model": GPT_IMAGE_2_C_MODEL, + "image_size": "2K" + })) + .expect("legacy generation alias should validate"); + assert_eq!(normalized["model"], GPT_IMAGE_2_5_BUSINESS_NAME); + } + #[test] fn dyn_pricing_keeps_all_existing_tool_formulas() { let pricing = pricing(); @@ -1510,6 +1581,7 @@ mod tests { ), ( GenerateUiDesignTool::NAME, + // 历史持久化的 `gpt-image-2` 会先归一为 `gpt-image-2.5`,所以计价仍走当前档位。 json!({ "prompt": "界面", "model": platform_image::GPT_IMAGE_2_MODEL, "image_size": "2K" }), 5, ), @@ -1552,9 +1624,10 @@ mod tests { #[test] fn dyn_pricing_uses_the_supplied_runtime_snapshot() { let mut pricing = pricing(); + // 历史 `gpt-image-2` 输入会归一为 `gpt-image-2.5-flare-c`,所以要改这个档位的价格。 pricing .models - .get_mut(GPT_IMAGE_2_MODEL) + .get_mut(EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION) .expect("gpt image pricing should exist") .prices .insert("2K".to_string(), 37);