fix(agent): 媒体准入 - 能力校验拒绝的参数改为可纠正的参数错误 (#674)
复现:generate_media 把 size 写成 9:16 这类比例值(模型只接受像素尺寸),或 durationSeconds 低于模型下限时,工具结果只有一句「工具执行失败」,整轮终止;模型不知道错在哪个参数,用户要求重试时原样再错。 原因:尺寸、时长、比例、分辨率不在模型支持范围时,能力校验的拒绝和其它准入失败一样被包成不可重试的 media_admission_failed。 修法:这类拒绝改报字段级参数错误,带上原因与取值来源,走已有的 fix_arguments 纠正通道;仍然没有任务被提交,也不产生费用。另外把准入拒绝的原始原因写进服务端日志(工具结果里的原因会脱敏,日志内容是服务端校验错误文本,不含用户内容或密钥)。
This commit is contained in:
@@ -291,6 +291,10 @@ func cloudAgentWrapMediaAdmissionError(err error) error {
|
||||
if errors.As(err, &admissionErr) {
|
||||
return err
|
||||
}
|
||||
// 能力校验拒绝的参数可以由模型自己改正,见 cloud_agent_media_capability_error.go。
|
||||
if argument := cloudAgentMediaCapabilityArgumentError(err); argument != nil {
|
||||
return argument
|
||||
}
|
||||
retryable := cloudAgentMediaAdmissionRetryable(err)
|
||||
action := "report_to_user"
|
||||
if retryable {
|
||||
|
||||
39
backend/internal/app/cloud_agent_media_capability_error.go
Normal file
39
backend/internal/app/cloud_agent_media_capability_error.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package app
|
||||
|
||||
import "strings"
|
||||
|
||||
// 模型能力校验(model_capability_validate.go)拒绝的生成参数属于「模型填错了参数」,
|
||||
// 不是模型、权限或预算问题。此前它们和其它准入失败一样被包成不可重试的
|
||||
// media_admission_failed:工具结果只有一句「工具执行失败」,整轮直接终止,
|
||||
// 模型既不知道错在哪个参数,也没有机会改正;用户要求重试时它只能原样再错一次。
|
||||
//
|
||||
// 这里把这类错误改报为字段级参数错误:结果里带着具体原因和取值来源,
|
||||
// 走已有的 fix_arguments 纠正通道(次数仍受 cloudAgentToolAttemptLimit 限制),
|
||||
// 没有任何任务被提交,也不会产生费用。
|
||||
var cloudAgentMediaCapabilityArgumentHints = []struct {
|
||||
marker string
|
||||
field string
|
||||
hint string
|
||||
}{
|
||||
{"图片尺寸不在当前模型支持范围内", "size", "size 必须取 model_list 返回的该模型 options.image.size.values 中的一个值(通常是像素尺寸,例如 1536x2752),不要写 9:16 这样的比例"},
|
||||
{"当前分辨率不支持所选图片宽高比", "size", "所选 quality 档位下没有这个宽高比;按 model_list 返回的该模型 options.image.size.presets 选一组匹配的比例与分辨率"},
|
||||
{"图片质量不在当前模型支持范围内", "quality", "quality 必须取 model_list 返回的该模型 options.image.quality.values 中的一个值,或省略"},
|
||||
{"视频时长不在当前模型支持范围内", "durationSeconds", "durationSeconds 必须落在 model_list 返回的该模型 options.video.duration 给出的范围或固定值内"},
|
||||
{"画面比例不在当前模型支持范围内", "size", "视频的 size 必须取 model_list 返回的该模型 options.video.ratios 中的一个比例"},
|
||||
{"输出分辨率不在当前模型支持范围内", "quality", "视频的 quality 必须取 model_list 返回的该模型 options.video.resolutions 中的一个值,大小写保持一致"},
|
||||
}
|
||||
|
||||
// cloudAgentMediaCapabilityArgumentError 识别能力校验错误并转成可纠正的参数错误;
|
||||
// 其它错误返回 nil,由调用方按原口径处理。
|
||||
func cloudAgentMediaCapabilityArgumentError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
message := err.Error()
|
||||
for _, item := range cloudAgentMediaCapabilityArgumentHints {
|
||||
if strings.Contains(message, item.marker) {
|
||||
return cloudAgentFieldError(item.field, "unsupported_value", item.marker+";"+item.hint+"。未提交生成任务,改正后可直接重新调用")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 复现:generate_media 把 size 写成 "9:16" 这类比例值,而模型只接受像素尺寸;
|
||||
// 修复前结果只有「工具执行失败」,整轮终止,重试时模型原样再错。
|
||||
func TestCloudAgentMediaCapabilityErrorBecomesCorrectableArgumentError(t *testing.T) {
|
||||
wrapped := cloudAgentWrapMediaAdmissionError(BadAuthRequest("图片尺寸不在当前模型支持范围内"))
|
||||
|
||||
var argumentErr *cloudAgentArgumentError
|
||||
if !errors.As(wrapped, &argumentErr) {
|
||||
t.Fatalf("capability rejection is not an argument error: %#v", wrapped)
|
||||
}
|
||||
var fieldErr *cloudAgentFieldArgumentError
|
||||
if !errors.As(wrapped, &fieldErr) || fieldErr.Field != "size" {
|
||||
t.Fatalf("field = %#v", fieldErr)
|
||||
}
|
||||
class, retryable, action := cloudAgentToolErrorClass(CloudAgentRequest{}, cloudAgentCall{}, wrapped, true)
|
||||
if class != cloudAgentToolErrorSchemaError || !retryable || action != "fix_arguments" {
|
||||
t.Fatalf("class=%s retryable=%v action=%s", class, retryable, action)
|
||||
}
|
||||
text := cloudAgentSafeToolError(wrapped)
|
||||
if !strings.Contains(text, "图片尺寸不在当前模型支持范围内") || !strings.Contains(text, "model_list") {
|
||||
t.Fatalf("model does not see the reason: %q", text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudAgentMediaCapabilityErrorCoversDurationRatioAndResolution(t *testing.T) {
|
||||
for message, field := range map[string]string{
|
||||
"视频时长不在当前模型支持范围内": "durationSeconds",
|
||||
"画面比例不在当前模型支持范围内": "size",
|
||||
"输出分辨率不在当前模型支持范围内": "quality",
|
||||
"图片质量不在当前模型支持范围内": "quality",
|
||||
} {
|
||||
var fieldErr *cloudAgentFieldArgumentError
|
||||
if !errors.As(cloudAgentWrapMediaAdmissionError(BadAuthRequest(message)), &fieldErr) || fieldErr.Field != field {
|
||||
t.Fatalf("%s → %#v, want field %s", message, fieldErr, field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 模型、权限、预算等其它准入失败保持原口径:不可自动重试,交给用户。
|
||||
func TestCloudAgentMediaAdmissionErrorStaysTerminalForOtherFailures(t *testing.T) {
|
||||
wrapped := cloudAgentWrapMediaAdmissionError(BadAuthRequest("已超过本轮视频时长预算"))
|
||||
var admissionErr *cloudAgentMediaAdmissionError
|
||||
if !errors.As(wrapped, &admissionErr) || admissionErr.Retryable || admissionErr.RequiredAction != "report_to_user" {
|
||||
t.Fatalf("budget failure changed class: %#v", wrapped)
|
||||
}
|
||||
var argumentErr *cloudAgentArgumentError
|
||||
if errors.As(wrapped, &argumentErr) {
|
||||
t.Fatal("budget failure became a correctable argument error")
|
||||
}
|
||||
}
|
||||
@@ -216,6 +216,8 @@ func (s *Service) cloudAgentMediaError(run *model.CloudAgentExecution, state *cl
|
||||
return cloudAgentSave(current, state)
|
||||
}
|
||||
if phase == "admission" && !submitted {
|
||||
// 工具结果里的原因会脱敏;原始原因只进服务端日志,排查准入失败时以它为准。
|
||||
log.Printf("[Agent] media admission rejected run=%s: %v", run.ID, err)
|
||||
err = cloudAgentWrapMediaAdmissionError(err)
|
||||
}
|
||||
toolName := state.Calls[state.CallIndex].Function.Name
|
||||
|
||||
Reference in New Issue
Block a user