兼容图片工具的历史 gpt-image-2 模型别名

api-server 的图片类 Agent 工具在反序列化前把 gpt-image-2 / gpt-image-2-c 归一为 gpt-image-2.5,旧持久化工具调用不再被校验直接拒绝

EditImageTool 计价改用 gpt-image-2.5 业务模型常量,并新增别名归一单测、更新受影响的校验与计价断言
This commit is contained in:
2026-09-19 13:55:30 +08:00
parent 939c35a9b9
commit bd0a06e3d3
@@ -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<T: DeserializeOwned>(
})
}
/// 把持久化的历史 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<T: DeserializeOwned>(
tool_name: &str,
value: &Value,
) -> Result<T, EditorAgentToolError> {
parse_invalid_args(tool_name, &normalize_legacy_image_model_args(value))
}
/// 图片类工具的 `pricing` 入口:即使调用方直接传入原始 args,也要先归一历史模型别名。
fn parse_internal_image_args<T: DeserializeOwned>(
label: &str,
value: &Value,
) -> Result<T, EditorAgentToolError> {
parse_internal(label, &normalize_legacy_image_model_args(value))
}
/// SFX V2 的显式 `duration: null` 表示自动时长,必须原样保留,不能被通用的
/// “顶层 null 当缺省”兼容层恢复成手动 5 秒。但其余字段仍要走该兼容层:
/// `model` 是无 Option 的 StringLLM 传 `model: null` 时若不剥离会直接反序列化失败。
@@ -311,7 +350,7 @@ fn editor_agent_image_mud_points(
impl EditorAgentTool for GenerateImageTool {
fn validate_args(&self, args: &Value) -> Result<Value, EditorAgentToolError> {
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<u32, EditorAgentToolError> {
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<EditorAgentToolCallDisplayArgs, EditorAgentToolError> {
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<PreparedEditorAgentJob, EditorAgentToolError> {
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<String, EditorAgentToolError> {
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<Value, EditorAgentToolError> {
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<u32, EditorAgentToolError> {
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<EditorAgentToolCallDisplayArgs, EditorAgentToolError> {
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<PreparedEditorAgentJob, EditorAgentToolError> {
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<String, EditorAgentToolError> {
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<Value, EditorAgentToolError> {
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<u32, EditorAgentToolError> {
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<EditorAgentToolCallDisplayArgs, EditorAgentToolError> {
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<PreparedEditorAgentJob, EditorAgentToolError> {
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<String, EditorAgentToolError> {
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<Value, EditorAgentToolError> {
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<u32, EditorAgentToolError> {
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<EditorAgentToolCallDisplayArgs, EditorAgentToolError> {
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<PreparedEditorAgentJob, EditorAgentToolError> {
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<String, EditorAgentToolError> {
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<Value, EditorAgentToolError> {
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<u32, EditorAgentToolError> {
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<EditorAgentToolCallDisplayArgs, EditorAgentToolError> {
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<PreparedEditorAgentJob, EditorAgentToolError> {
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<String, EditorAgentToolError> {
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);