若干更新迭代

This commit is contained in:
andy committed 2026-10-02 19:56:32 +08:00
1 parent deb9cc5bcf
commit 4ac786aafb
64 files changed
+3251 -291

No files matched your search

+36 -3
View File
@@ -3,6 +3,7 @@ package providers
import (
"context"
"fmt"
"math"
"net/http"
"strings"
)
@@ -64,10 +65,17 @@ func NewBailian(c Config, client HTTPClient) *Bailian {
return "/api/v1/services/aigc/image-generation/generation"
}, queryPath: func(id string) string { return "/api/v1/tasks/" + id }, payload: func(r Request) any {
if r.Capability == "video.generate" {
model := requestModel(r, c.Model)
mode := stringValue(r.Settings["inputMode"])
if mode == "" && model == "wan3.0-video" {
mode = "reference"
}
media := make([]any, 0, len(r.InputURLs))
for index, inputURL := range r.InputURLs {
frameType := "first_frame"
if index > 0 {
if mode == "reference" {
frameType = "reference_image"
} else if index > 0 {
frameType = "last_frame"
}
media = append(media, map[string]any{"type": frameType, "url": inputURL})
@@ -84,7 +92,16 @@ func NewBailian(c Config, client HTTPClient) *Bailian {
if parameters["duration"] == nil {
parameters["duration"] = 10
}
return map[string]any{"model": requestModel(r, c.Model), "input": map[string]any{"prompt": r.Prompt, "media": media}, "parameters": parameters}
if model == "wan3.0-video" {
if ratio := stringValue(r.Settings["ratio"]); ratio != "" {
parameters["ratio"] = ratio
}
}
input := map[string]any{"prompt": r.Prompt}
if len(media) > 0 {
input["media"] = media
}
return map[string]any{"model": model, "input": input, "parameters": parameters}
}
content := make([]any, 0, len(r.InputURLs)+1)
for _, inputURL := range r.InputURLs {
@@ -126,7 +143,23 @@ func decodeBailian(raw []byte) Result {
}
}
collectURLs(o["video_url"], &out)
return Result{TaskID: stringValue(o["task_id"], r["task_id"]), Status: status(o["task_status"]), OutputURLs: out, ErrorMessage: stringValue(r["message"])}
usage := map[string]int{}
for _, value := range []any{object(r["usage"])["output_video_duration"], object(r["usage"])["duration"], object(o["usage"])["output_video_duration"], object(o["usage"])["duration"]} {
if milliseconds, ok := positiveDurationMilliseconds(value); ok {
usage["outputMilliseconds"] = milliseconds
usage["outputSeconds"] = int(math.Ceil(float64(milliseconds) / 1000))
break
}
}
return Result{TaskID: stringValue(o["task_id"], r["task_id"]), Status: status(o["task_status"]), OutputURLs: out, ErrorMessage: stringValue(r["message"]), Usage: usage}
}
func positiveDurationMilliseconds(value any) (int, bool) {
seconds, ok := value.(float64)
if !ok || math.IsNaN(seconds) || math.IsInf(seconds, 0) || seconds <= 0 || seconds > math.MaxInt32/1000 {
return 0, false
}
return int(math.Ceil(seconds * 1000)), true
}
type Seedance struct{ *httpAdapter }
@@ -0,0 +1,150 @@
package providers
import (
"encoding/json"
"net/http"
"regexp"
"strings"
)
var providerRequestIDInMessage = regexp.MustCompile(`(?i)\brequest[ _-]?id\s*[:=]\s*([A-Za-z0-9_.:-]{3,128})\b`)
// SanitizeFailureDetail retains a short provider explanation while removing
// submitted text, media links, credentials, and unrecognized quoted values.
// It is safe to call again after the original request has been discarded.
func SanitizeFailureDetail(message string, requestBody []byte, secrets ...string) string {
if strings.TrimSpace(message) == "" {
return ""
}
privateValues := append([]string(nil), secrets...)
var request any
if json.Unmarshal(requestBody, &request) == nil {
collectPrivateRequestValues(request, "", &privateValues)
}
for _, value := range privateValues {
if value == "" {
continue
}
message = strings.ReplaceAll(message, value, "[redacted]")
encoded, _ := json.Marshal(value)
if len(encoded) > 2 {
message = strings.ReplaceAll(message, string(encoded[1:len(encoded)-1]), "[redacted]")
}
}
message = minimaxDiagnosticURL.ReplaceAllString(message, "[redacted]")
message = minimaxDiagnosticCredential.ReplaceAllString(message, "[redacted]")
message = minimaxDiagnosticQuoted.ReplaceAllStringFunc(message, func(quoted string) string {
value := quoted[1 : len(quoted)-1]
if minimaxDiagnosticValue(value) != "unrecognized" {
return quoted
}
return "[redacted]"
})
message = minimaxDiagnosticLongToken.ReplaceAllString(message, "[redacted]")
message = strings.Join(strings.Fields(message), " ")
runes := []rune(message)
if len(runes) > 512 {
return string(runes[:512]) + "...[truncated]"
}
return message
}
func collectPrivateRequestValues(value any, key string, values *[]string) {
switch typed := value.(type) {
case map[string]any:
for childKey, childValue := range typed {
collectPrivateRequestValues(childValue, strings.ToLower(childKey), values)
}
case []any:
for _, child := range typed {
collectPrivateRequestValues(child, key, values)
}
case string:
if strings.Contains(key, "prompt") || strings.Contains(key, "text") || strings.Contains(key, "url") || strings.Contains(key, "image") || strings.Contains(key, "video") || strings.Contains(key, "audio") || strings.Contains(key, "token") || strings.Contains(key, "secret") || strings.Contains(key, "key") || strings.Contains(key, "signature") || strings.Contains(key, "password") || strings.Contains(key, "credential") {
*values = append(*values, typed)
}
}
}
func requestIDFromMessage(message string) string {
match := providerRequestIDInMessage.FindStringSubmatch(message)
if len(match) < 2 {
return ""
}
return safeHTTPProviderDiagnosticToken(match[1], 128)
}
func safeProviderResponseDetail(raw, requestBody []byte, secrets ...string) string {
root := record(raw)
providerError := object(first(root["error"], root["Error"]))
output := object(root["output"])
data := object(root["data"])
task := object(first(root["task"], data["task"]))
nestedError := object(first(data["error"], task["error"], output["error"]))
metadataError := object(first(object(first(root["ResponseMetadata"], root["response_metadata"]))["Error"], object(first(root["ResponseMetadata"], root["response_metadata"]))["error"]))
message := stringValue(providerError["message"], providerError["Message"], nestedError["message"], nestedError["Message"], task["message"], output["message"], data["message"], metadataError["Message"], metadataError["message"], root["message"], root["Message"])
return SanitizeFailureDetail(message, requestBody, secrets...)
}
func successBusinessCode(code string) bool {
switch strings.ToLower(strings.TrimSpace(code)) {
case "", "0", "200", "ok", "success", "10000":
return true
}
return false
}
// Only explicit top-level or output error indicators count as a business
// rejection. A successful task can carry a normal informational message.
func inspectProviderBusinessRejection(raw []byte, headers http.Header, requestBody []byte, secrets ...string) (code, requestID, detail string, rejected bool) {
root := record(raw)
providerError := object(first(root["error"], root["Error"]))
output := object(root["output"])
data := object(root["data"])
task := object(first(root["task"], data["task"]))
nestedError := object(first(data["error"], task["error"], output["error"]))
code, requestID, _ = inspectHTTPProviderFailure(raw, headers)
message := stringValue(providerError["message"], providerError["Message"], nestedError["message"], nestedError["Message"], output["message"], data["message"], root["message"])
if (len(providerError) > 0 || len(nestedError) > 0) && (code != "" || message != "") {
rejected = true
} else if rootCode := safeHTTPProviderDiagnosticToken(first(root["code"], root["Code"]), 64); rootCode != "" && !successBusinessCode(rootCode) {
rejected = true
} else if outputCode := safeHTTPProviderDiagnosticToken(first(output["code"], output["Code"]), 64); outputCode != "" && !successBusinessCode(outputCode) {
rejected = true
} else if strings.EqualFold(stringValue(root["type"]), "error") && message != "" {
rejected = true
}
if !rejected {
return "", "", "", false
}
return code, requestID, SanitizeFailureDetail(message, requestBody, secrets...), true
}
func annotateTerminalFailure(result Result, raw []byte, headers http.Header, requestBody []byte, secrets ...string) Result {
if result.Status != StatusFailed && result.Status != StatusExpired && result.Status != StatusCancelled {
return result
}
root := record(raw)
business := object(first(root["Result"], root["result"]))
data := object(root["data"])
businessData := object(first(business["data"], business["Data"]))
output := object(root["output"])
task := object(root["task"])
providerError := object(first(task["error"], data["error"], businessData["error"], businessData["Error"], business["error"], business["Error"], output["error"], root["error"], root["Error"]))
code, requestID, _ := inspectHTTPProviderFailure(raw, headers)
if nestedCode := firstSafeHTTPProviderDiagnosticToken(64, providerError["code"], providerError["Code"], task["error_code"], data["error_code"], businessData["error_code"], business["error_code"]); nestedCode != "" {
code = nestedCode
}
if nestedID := firstSafeHTTPProviderDiagnosticToken(128, providerError["request_id"], providerError["requestId"], providerError["RequestId"], task["request_id"], data["request_id"], businessData["request_id"], business["request_id"]); nestedID != "" {
requestID = nestedID
}
message := stringValue(providerError["message"], providerError["Message"], task["error_message"], task["message"], output["message"], data["error_message"], data["message"], businessData["error_message"], businessData["message"], business["message"], root["message"], result.ErrorMessage)
if requestID == "" {
requestID = requestIDFromMessage(message)
}
result.ErrorCode = code
result.RequestID = requestID
result.ErrorDetail = SanitizeFailureDetail(message, requestBody, secrets...)
result.ErrorMessage = SanitizeFailureDetail(result.ErrorMessage, requestBody, secrets...)
return result
}
@@ -0,0 +1,185 @@
package providers
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"strings"
"testing"
"time"
)
func TestAdaptersPreserveExplicitProviderRejections(t *testing.T) {
tests := []struct {
name, body, wantCode, wantDetail string
newAdapter func(HTTPClient) Adapter
}{
{"seedance", `{"error":{"code":"InputImageSensitiveContentDetected.PrivacyInformation","message":"Input image may contain a real person. Request ID: req-safe-1"}}`, "InputImageSensitiveContentDetected.PrivacyInformation", "real person", func(c HTTPClient) Adapter {
return NewSeedance(Config{BaseURL: "https://test.invalid", APIKey: "private-key"}, c)
}},
{"seedream", `{"error":{"code":"InvalidImage","message":"Image format is invalid"}}`, "InvalidImage", "Image format", func(c HTTPClient) Adapter {
return NewSeedream(Config{BaseURL: "https://test.invalid", APIKey: "private-key"}, c)
}},
{"evolink", `{"data":{"error":{"code":"InvalidPrompt","message":"Prompt is invalid"}}}`, "InvalidPrompt", "Prompt is invalid", func(c HTTPClient) Adapter {
return NewEvoLink(Config{BaseURL: "https://test.invalid", APIKey: "private-key"}, c)
}},
{"bailian", `{"code":"InvalidParameter","message":"Invalid duration"}`, "InvalidParameter", "Invalid duration", func(c HTTPClient) Adapter {
return NewBailian(Config{BaseURL: "https://test.invalid", APIKey: "private-key"}, c)
}},
{"minimax", `{"type":"error","error":{"code":"1008","message":"Account balance is insufficient"}}`, "1008", "balance", func(c HTTPClient) Adapter {
return NewMinimax(Config{BaseURL: "https://test.invalid", APIKey: "private-key"}, c)
}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
for _, httpStatus := range []int{http.StatusBadRequest, http.StatusOK} {
client := roundTripFunc(func(*http.Request) (*http.Response, error) {
return &http.Response{StatusCode: httpStatus, Header: http.Header{}, Body: io.NopCloser(strings.NewReader(tc.body))}, nil
})
result, err := tc.newAdapter(client).Submit(context.Background(), Request{Prompt: "private prompt"})
if tc.name == "seedream" && httpStatus == http.StatusOK {
if err != nil || result.Status != StatusFailed || result.ErrorCode != tc.wantCode || !strings.Contains(result.ErrorDetail, tc.wantDetail) {
t.Fatalf("status=%d result=%#v err=%v", httpStatus, result, err)
}
continue
}
var failure *ProviderError
if !errors.As(err, &failure) || failure.Status != httpStatus || failure.Code != tc.wantCode || !strings.Contains(failure.Detail, tc.wantDetail) {
t.Fatalf("status=%d failure=%#v err=%v", httpStatus, failure, err)
}
if tc.name == "seedance" && failure.RequestID != "req-safe-1" {
t.Fatalf("request id=%q", failure.RequestID)
}
}
})
}
}
func TestSuccessfulBusinessCodesAndInformationalMessagesDoNotReject(t *testing.T) {
tests := []struct {
name, response, taskID string
wantURL bool
adapter func(HTTPClient) Adapter
}{
{"evolink", `{"code":0,"message":"request accepted","data":{"id":"task-ev","status":"queued"}}`, "task-ev", false, func(c HTTPClient) Adapter { return NewEvoLink(Config{BaseURL: "https://test.invalid"}, c) }},
{"bailian", `{"code":"200","message":"task submitted","output":{"task_id":"task-ba","task_status":"PENDING"}}`, "task-ba", false, func(c HTTPClient) Adapter { return NewBailian(Config{BaseURL: "https://test.invalid"}, c) }},
{"seedance", `{"code":"success","message":"task completed","id":"task-se","status":"succeeded","content":{"video_url":"https://cdn.example/video.mp4"}}`, "task-se", true, func(c HTTPClient) Adapter { return NewSeedance(Config{BaseURL: "https://test.invalid"}, c) }},
{"minimax", `{"code":0,"message":"request accepted","task_id":"task-mi"}`, "task-mi", false, func(c HTTPClient) Adapter { return NewMinimax(Config{BaseURL: "https://test.invalid"}, c) }},
{"seedream", `{"code":0,"message":"generation complete","data":[{"url":"https://cdn.example/image.png"}]}`, "", true, func(c HTTPClient) Adapter { return NewSeedream(Config{BaseURL: "https://test.invalid"}, c) }},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := roundTripFunc(func(*http.Request) (*http.Response, error) {
return &http.Response{StatusCode: 200, Header: http.Header{}, Body: io.NopCloser(strings.NewReader(tc.response))}, nil
})
result, err := tc.adapter(client).Submit(context.Background(), Request{Prompt: "safe prompt"})
if err != nil || result.TaskID != tc.taskID || (tc.wantURL && len(result.OutputURLs) == 0) || (!tc.wantURL && len(result.OutputURLs) != 0) {
t.Fatalf("result=%#v err=%v", result, err)
}
})
}
}
func TestAsyncTerminalFailurePersistsSafeDiagnostics(t *testing.T) {
client := roundTripFunc(func(*http.Request) (*http.Response, error) {
return &http.Response{StatusCode: 200, Header: http.Header{}, Body: io.NopCloser(strings.NewReader(`{"output":{"task_id":"task-1","task_status":"FAILED","code":"InvalidImage","message":"Input image was rejected"},"request_id":"req-bailian-1"}`))}, nil
})
result, err := NewBailian(Config{BaseURL: "https://test.invalid", APIKey: "secret"}, client).Query(context.Background(), "task-1")
if err != nil || result.Status != StatusFailed || result.ErrorCode != "InvalidImage" || result.RequestID != "req-bailian-1" || result.ErrorDetail != "Input image was rejected" {
t.Fatalf("result=%#v err=%v", result, err)
}
encoded, err := EncodeResult(result)
if err != nil {
t.Fatal(err)
}
var persisted HTTPResult
if err := json.Unmarshal(encoded, &persisted); err != nil || persisted.ErrorCode != result.ErrorCode || persisted.RequestID != result.RequestID || persisted.ErrorDetail != result.ErrorDetail {
t.Fatalf("persisted=%#v err=%v", persisted, err)
}
}
func TestAsyncAdaptersReadTerminalFailureDetails(t *testing.T) {
tests := []struct {
name, response, code, detail string
adapter func(HTTPClient) Adapter
}{
{"evolink", `{"id":"task-1","status":"failed","data":{"error":{"code":"ContentRejected","message":"Reference image rejected"}}}`, "ContentRejected", "Reference image rejected", func(c HTTPClient) Adapter { return NewEvoLink(Config{BaseURL: "https://test.invalid"}, c) }},
{"seedance", `{"id":"task-1","status":"failed","error":{"code":"InputImageSensitiveContentDetected.PrivacyInformation","message":"Input image may contain a real person"}}`, "InputImageSensitiveContentDetected.PrivacyInformation", "real person", func(c HTTPClient) Adapter { return NewSeedance(Config{BaseURL: "https://test.invalid"}, c) }},
{"minimax", `{"task":{"id":"task-1","status":"failed","error":{"code":"1026","message":"Input content rejected"}}}`, "1026", "Input content rejected", func(c HTTPClient) Adapter { return NewMinimax(Config{BaseURL: "https://test.invalid"}, c) }},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := roundTripFunc(func(*http.Request) (*http.Response, error) {
return &http.Response{StatusCode: 200, Header: http.Header{}, Body: io.NopCloser(strings.NewReader(tc.response))}, nil
})
result, err := tc.adapter(client).Query(context.Background(), "task-1")
if err != nil || result.Status != StatusFailed || result.ErrorCode != tc.code || !strings.Contains(result.ErrorDetail, tc.detail) {
t.Fatalf("result=%#v err=%v", result, err)
}
})
}
}
func TestSanitizeFailureDetailRemovesSubmittedMaterialsCredentialsAndUnknownQuotes(t *testing.T) {
request := []byte(`{"prompt":"private portrait","content":[{"text":"private portrait"},{"image_url":{"url":"https://private.example/photo.png"}}],"materials":[{"url":"https://private.example/video.mp4"}],"settings":{"token":"very-secret-token"}}`)
message := `invalid "private portrait" from https://private.example/photo.png; video https://private.example/video.mp4; token=very-secret-token; Bearer other-secret; field "person's private name" rejected`
got := SanitizeFailureDetail(message, request, "api-secret")
for _, private := range []string{"private portrait", "private.example", "very-secret-token", "other-secret", "person's private name"} {
if strings.Contains(got, private) {
t.Fatalf("detail leaks %q: %q", private, got)
}
}
if !strings.Contains(got, "invalid") || !strings.Contains(got, "rejected") {
t.Fatalf("detail lost context: %q", got)
}
if got := SanitizeFailureDetail(strings.Repeat("x", 1000), nil); len([]rune(got)) > 527 {
t.Fatalf("detail unbounded: %d", len([]rune(got)))
}
}
func TestUnknownSubmitOutcomeKeepsStatusZero(t *testing.T) {
for name, client := range map[string]HTTPClient{
"transport": roundTripFunc(func(*http.Request) (*http.Response, error) { return nil, io.ErrUnexpectedEOF }),
"unreadable-response": roundTripFunc(func(*http.Request) (*http.Response, error) {
return &http.Response{StatusCode: 400, Header: http.Header{}, Body: &failingBody{}}, nil
}),
} {
t.Run(name, func(t *testing.T) {
_, err := NewSeedance(Config{BaseURL: "https://test.invalid"}, client).Submit(context.Background(), Request{Prompt: "test"})
var failure *ProviderError
if !errors.As(err, &failure) || failure.Status != 0 {
t.Fatalf("failure=%#v err=%v", failure, err)
}
})
}
}
func TestVolcengineRejectionAndTerminalFailurePreserveSafeDiagnostics(t *testing.T) {
responses := []string{
`{"ResponseMetadata":{"RequestId":"req-volc-1","Error":{"Code":"50413","Message":"Input image rejected"}}}`,
`{"code":10000,"Result":{"data":{"task_id":"task-volc","status":"failed","error":{"code":"InvalidImage","message":"Input image rejected"}}}}`,
}
call := 0
client := roundTripFunc(func(*http.Request) (*http.Response, error) {
body := responses[call]
call++
return &http.Response{StatusCode: 200, Header: http.Header{}, Body: io.NopCloser(strings.NewReader(body))}, nil
})
adapter := NewVolcengine(Config{BaseURL: "https://test.invalid", AccessKeyID: "private-ak", SecretAccessKey: "private-sk"}, client, func() time.Time { return time.Unix(0, 0) })
_, err := adapter.Submit(context.Background(), Request{Prompt: "private prompt"})
var failure *ProviderError
if !errors.As(err, &failure) || failure.Status != 200 || failure.Code != "50413" || failure.RequestID != "req-volc-1" || failure.Detail != "Input image rejected" {
t.Fatalf("failure=%#v err=%v", failure, err)
}
result, err := adapter.Query(context.Background(), "task-volc")
if err != nil || result.Status != StatusFailed || result.ErrorCode != "InvalidImage" || result.ErrorDetail != "Input image rejected" {
t.Fatalf("terminal result=%#v err=%v", result, err)
}
}
type failingBody struct{}
func (*failingBody) Read([]byte) (int, error) { return 0, io.ErrUnexpectedEOF }
func (*failingBody) Close() error { return nil }
@@ -0,0 +1,197 @@
package providers
import "strings"
// FailureMessage returns a user-facing reason for a confirmed provider rejection
// or a failed asynchronous task. The code must come from the named provider;
// provider response prose must never be passed through to the user.
//
// Error-code references:
// Volcengine Ark: https://docs.volcengine.com/docs/82379/1299023
// Bailian: https://help.aliyun.com/zh/model-studio/error-code/
// EvoLink API codes: https://evolink.ai/zh/blog/wan-2-7-api-guide
// Jimeng visual codes: https://docs.volcengine.com/docs/AIGCAIzhongtaigongyongwendang/69728
// and https://www.volcengine.com/docs/85621/1791184
func FailureMessage(provider string, status int, code string) string {
code = strings.TrimSpace(code)
switch strings.ToLower(strings.TrimSpace(provider)) {
case "seedance", "seedream":
if message := arkFailureMessage(code); message != "" {
return message
}
case "minimax":
if message := minimaxFailureMessage(code); message != "" {
return message
}
case "bailian":
if message := bailianFailureMessage(code); message != "" {
return message
}
case "evolink":
if message := evolinkFailureMessage(code); message != "" {
return message
}
case "volcengine-visual":
if message := jimengFailureMessage(code); message != "" {
return message
}
}
return failureMessageForStatus(status)
}
func arkFailureMessage(code string) string {
if code == "InputImageSensitiveContentDetected.PrivacyInformation" {
return "参考图片可能包含真人,未通过服务商审核;请更换符合要求的图片后重试。"
}
if code == "InputVideoSensitiveContentDetected.PrivacyInformation" {
return "参考视频可能包含真人,未通过服务商审核;请更换符合要求的视频后重试。"
}
for _, item := range []struct{ code, material string }{
{"InputTextSensitiveContentDetected", "提示词"},
{"InputImageSensitiveContentDetected", "参考图片"},
{"InputVideoSensitiveContentDetected", "参考视频"},
{"InputAudioSensitiveContentDetected", "参考音频"},
{"OutputTextSensitiveContentDetected", "生成的文字"},
{"OutputImageSensitiveContentDetected", "生成的图片"},
{"OutputVideoSensitiveContentDetected", "生成的视频"},
{"OutputAudioSensitiveContentDetected", "生成的音频"},
} {
if code == item.code+".PolicyViolation" {
return item.material + "可能涉及版权限制,请调整内容后重试。"
}
if code == item.code || strings.HasPrefix(code, item.code+".") {
return item.material + "未通过服务商内容审核,请调整内容后重试。"
}
}
switch code {
case "InvalidImageURL.EmptyURL", "InvalidImageURL.InvalidFormat", "InvalidParameter.TosURLInvalid":
return "参考图片无法读取,请重新上传图片后重试。"
case "OperationDenied.ServiceNotOpen", "OperationDenied.PermissionDenied":
return "当前模型服务尚未开通或无权使用,请联系管理员处理。"
case "OperationDenied.ServiceOverdue":
return "生成服务账户已逾期,请联系管理员处理。"
case "InvalidAccountStatus":
return "生成服务账户状态异常,请联系管理员处理。"
case "ContentSecurityDetectionError":
return "服务商内容审核暂时不可用,请稍后重试。"
}
if code == "MissingParameter" || strings.HasPrefix(code, "MissingParameter.") || code == "InvalidParameter" || strings.HasPrefix(code, "InvalidParameter.") {
return "当前生成参数不符合模型要求,请调整设置或切换模型后重试。"
}
return ""
}
func minimaxFailureMessage(code string) string {
switch code {
case "1002":
return "MiniMax 请求过于频繁,请稍后重试。"
case "1004", "2049":
return "MiniMax 服务凭证无效,请联系管理员处理。"
case "1008":
return "MiniMax 生成服务账户余额不足,请联系管理员处理。"
case "1026":
return "视频描述或参考素材未通过 MiniMax 内容审核,请调整后重试。"
case "2013":
return "当前生成参数不符合 MiniMax 模型要求,请调整设置后重试。"
}
return ""
}
func bailianFailureMessage(code string) string {
switch code {
case "Arrearage", "isv.OUT_OF_SERVICE":
return "百炼生成服务账户余额不足或逾期,请联系管理员处理。"
case "DataInspectionFailed", "data_inspection_failed":
return "输入或生成内容未通过百炼审核,请调整提示词或素材后重试。"
case "IPInfringementSuspect":
return "输入内容可能涉及知识产权限制,请调整提示词或素材后重试。"
case "InvalidParameter", "InternalError.Algo.InvalidParameter":
return "当前生成参数不符合百炼模型要求,请调整设置后重试。"
case "InvalidURL", "InvalidURL.ConnectionRefused", "InvalidURL.Timeout", "InvalidFile.DownloadFailed", "InvalidParameter.FileDownload":
return "参考素材无法下载,请重新上传可访问的素材后重试。"
case "InvalidParameter.DataInspection":
return "参考素材在安全审核时无法访问,请重新上传素材;若仍失败请联系管理员。"
case "InvalidFile.Size", "InvalidFile.Format", "InvalidFile.Resolution", "InvalidFile.FPS", "InvalidFile.Duration", "InvalidFile.ImageSize", "InvalidFile.Openerror":
return "参考素材的格式、大小或时长不符合模型要求,请更换素材后重试。"
case "InvalidApiKey", "AccessDenied", "Resource.AccessDenied", "Workspace.AccessDenied":
return "百炼生成服务鉴权或使用权限异常,请联系管理员处理。"
case "Throttling":
return "百炼请求过于频繁,请稍后重试。"
case "InternalError", "InternalError.Algo":
return "百炼生成服务暂时异常,请稍后重试。"
}
return ""
}
func evolinkFailureMessage(code string) string {
switch code {
case "invalid_request":
return "当前生成参数不符合模型要求,请检查提示词、素材和设置后重试。"
case "unauthorized":
return "EvoLink 生成服务鉴权失败,请联系管理员处理。"
case "insufficient_quota":
return "EvoLink 生成服务账户额度不足,请联系管理员处理。"
case "model_access_denied":
return "当前 EvoLink 模型无调用权限,请联系管理员处理。"
case "rate_limit_exceeded":
return "EvoLink 请求频率过高,请稍后重试。"
case "internal_error":
return "EvoLink 生成服务暂时异常,请稍后重试。"
}
return ""
}
func jimengFailureMessage(code string) string {
switch code {
case "50200", "50201", "50204":
return "当前生成参数不符合即梦模型要求,请调整设置后重试。"
case "50205", "50206", "50207":
return "参考图片过大、为空或无法读取,请更换图片后重试。"
case "50209", "50210", "50211", "50214":
return "参考视频的内容、大小或时长不符合要求,请更换视频后重试。"
case "50213":
return "提交的素材总大小超过即梦限制,请减少或压缩素材后重试。"
case "60102":
return "参考图片中未检测到人脸,请更换清晰的人像图片后重试。"
case "60208", "50411":
return "参考图片未通过即梦内容审核,请更换图片后重试。"
case "50511":
return "生成的图片未通过即梦内容审核,请调整提示词或素材后重试。"
case "50412", "50413":
return "提示词未通过即梦内容审核,请调整内容后重试。"
case "50512":
return "生成的文字未通过即梦内容审核,请调整提示词或素材后重试。"
case "50400":
return "即梦服务鉴权或调用权限异常,请联系管理员处理。"
case "50402":
return "即梦模型接口不可用,请联系管理员检查配置。"
case "50429", "50430":
return "即梦请求频率或并发达到上限,请稍后重试。"
case "50500", "50501":
return "即梦生成服务暂时异常,请稍后重试。"
}
return ""
}
func failureMessageForStatus(status int) string {
switch status {
case 0:
return "服务商未能完成生成,请联系管理员查看错误详情。"
case 401:
return "生成服务鉴权失败,请联系管理员处理。"
case 402:
return "生成服务账户额度不足,请联系管理员处理。"
case 403:
return "当前生成服务无权使用,请联系管理员处理。"
case 404:
return "模型或生成任务不存在,请联系管理员检查配置。"
case 408, 504:
return "服务商请求超时,无法确认是否已接收,请联系管理员核查任务状态。"
case 429:
return "生成服务请求频率过高,请稍后重试。"
case 500, 502, 503:
return "生成服务暂时异常,请稍后重试。"
default:
return "服务商拒绝了本次请求,请联系管理员查看错误详情。"
}
}
@@ -0,0 +1,80 @@
package providers
import (
"strings"
"testing"
)
func TestFailureMessageUsesProviderSpecificCodes(t *testing.T) {
cases := []struct {
name, provider, code, want string
status int
}{
{"seedance real-person image", "seedance", "InputImageSensitiveContentDetected.PrivacyInformation", "参考图片可能包含真人,未通过服务商审核;请更换符合要求的图片后重试。", 400},
{"seedream real-person image", "seedream", "InputImageSensitiveContentDetected.PrivacyInformation", "参考图片可能包含真人,未通过服务商审核;请更换符合要求的图片后重试。", 400},
{"seedance real-person video", "seedance", "InputVideoSensitiveContentDetected.PrivacyInformation", "参考视频可能包含真人,未通过服务商审核;请更换符合要求的视频后重试。", 400},
{"seedance image copyright", "seedance", "InputImageSensitiveContentDetected.PolicyViolation", "参考图片可能涉及版权限制,请调整内容后重试。", 400},
{"seedance image subcode", "seedance", "InputImageSensitiveContentDetected.OtherRestriction", "参考图片未通过服务商内容审核,请调整内容后重试。", 400},
{"seedance parameter subcode", "seedance", "InvalidParameter.NewConstraint", "当前生成参数不符合模型要求,请调整设置或切换模型后重试。", 400},
{"seedance missing parameter subcode", "seedance", "MissingParameter.ImageURL", "当前生成参数不符合模型要求,请调整设置或切换模型后重试。", 400},
{"seedance audio moderation", "seedance", "InputAudioSensitiveContentDetected", "参考音频未通过服务商内容审核,请调整内容后重试。", 400},
{"seedance output video moderation", "seedance", "OutputVideoSensitiveContentDetected", "生成的视频未通过服务商内容审核,请调整内容后重试。", 400},
{"seedream old output image moderation", "seedream", "OutputImageSensitiveContentDetected", "生成的图片未通过服务商内容审核,请调整内容后重试。", 400},
{"seedance account overdue", "seedance", "OperationDenied.ServiceOverdue", "生成服务账户已逾期,请联系管理员处理。", 403},
{"seedance service unopened", "seedance", "OperationDenied.ServiceNotOpen", "当前模型服务尚未开通或无权使用,请联系管理员处理。", 403},
{"minimax balance", "minimax", "1008", "MiniMax 生成服务账户余额不足,请联系管理员处理。", 400},
{"minimax moderation", "minimax", "1026", "视频描述或参考素材未通过 MiniMax 内容审核,请调整后重试。", 400},
{"minimax rate limit", "minimax", "1002", "MiniMax 请求过于频繁,请稍后重试。", 400},
{"minimax invalid parameters", "minimax", "2013", "当前生成参数不符合 MiniMax 模型要求,请调整设置后重试。", 400},
{"bailian arrearage", "bailian", "Arrearage", "百炼生成服务账户余额不足或逾期,请联系管理员处理。", 400},
{"bailian copyright", "bailian", "IPInfringementSuspect", "输入内容可能涉及知识产权限制,请调整提示词或素材后重试。", 400},
{"bailian inspection", "bailian", "DataInspectionFailed", "输入或生成内容未通过百炼审核,请调整提示词或素材后重试。", 400},
{"bailian reference download", "bailian", "InvalidFile.DownloadFailed", "参考素材无法下载,请重新上传可访问的素材后重试。", 400},
{"bailian inspection access", "bailian", "InvalidParameter.DataInspection", "参考素材在安全审核时无法访问,请重新上传素材;若仍失败请联系管理员。", 400},
{"evolink balance by status", "evolink", "unrecognized", "生成服务账户额度不足,请联系管理员处理。", 402},
{"evolink invalid request by code", "evolink", "invalid_request", "当前生成参数不符合模型要求,请检查提示词、素材和设置后重试。", 400},
{"evolink quota by code", "evolink", "insufficient_quota", "EvoLink 生成服务账户额度不足,请联系管理员处理。", 400},
{"evolink model access by code", "evolink", "model_access_denied", "当前 EvoLink 模型无调用权限,请联系管理员处理。", 400},
{"jimeng text audit by business code", "volcengine-visual", "50413", "提示词未通过即梦内容审核,请调整内容后重试。", 200},
{"jimeng image audit by business code", "volcengine-visual", "50411", "参考图片未通过即梦内容审核,请更换图片后重试。", 200},
{"jimeng image too large by business code", "volcengine-visual", "50205", "参考图片过大、为空或无法读取,请更换图片后重试。", 200},
{"jimeng concurrency by business code", "volcengine-visual", "50430", "即梦请求频率或并发达到上限,请稍后重试。", 200},
{"jimeng forbidden by status", "volcengine-visual", "unrecognized", "当前生成服务无权使用,请联系管理员处理。", 403},
{"unknown authorization", "evolink", "unrecognized", "生成服务鉴权失败,请联系管理员处理。", 401},
{"unknown model", "evolink", "unrecognized", "模型或生成任务不存在,请联系管理员检查配置。", 404},
{"unknown validation", "evolink", "unrecognized", "服务商拒绝了本次请求,请联系管理员查看错误详情。", 422},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := FailureMessage(tc.provider, tc.status, tc.code); got != tc.want {
t.Errorf("FailureMessage(%q, %d, %q) = %q; want %q", tc.provider, tc.status, tc.code, got, tc.want)
}
})
}
}
func TestFailureMessageUnknownCodesStayGenericAndDoNotCrossProvider(t *testing.T) {
cases := []struct {
provider, code string
status int
want string
}{
{"seedance", "InputImageSensitiveContentDetectedEvil.PrivacyInformation", 400, "服务商拒绝了本次请求,请联系管理员查看错误详情。"},
{"evolink", "InputImageSensitiveContentDetected.PrivacyInformation", 400, "服务商拒绝了本次请求,请联系管理员查看错误详情。"},
{"minimax", "OperationDenied.ServiceOverdue", 400, "服务商拒绝了本次请求,请联系管理员查看错误详情。"},
{"unknown", "1008", 400, "服务商拒绝了本次请求,请联系管理员查看错误详情。"},
{"bailian", "unknown", 0, "服务商未能完成生成,请联系管理员查看错误详情。"},
{"seedream", "unknown", 429, "生成服务请求频率过高,请稍后重试。"},
{"seedream", "unknown", 504, "服务商请求超时,无法确认是否已接收,请联系管理员核查任务状态。"},
{"seedream", "unknown", 500, "生成服务暂时异常,请稍后重试。"},
}
for _, tc := range cases {
got := FailureMessage(tc.provider, tc.status, tc.code)
if got != tc.want {
t.Errorf("FailureMessage(%q, %d, %q) = %q; want %q", tc.provider, tc.status, tc.code, got, tc.want)
}
if strings.Contains(got, tc.code) && tc.code != "unknown" {
t.Errorf("user message leaked provider code %q: %q", tc.code, got)
}
}
}
+13 -6
View File
@@ -9,8 +9,8 @@ import (
const MinimaxH3Model = "MiniMax-H3"
// Minimax adapts the MiniMax H3 V2 asynchronous video API to the common
// provider contract. The first release intentionally supports text-to-video
// and a single first-frame image only; request validation lives in jobs.
// provider contract. Request validation for first-frame and multi-reference
// modes lives in jobs.
type Minimax struct{ *httpAdapter }
func NewMinimax(c Config, client HTTPClient) *Minimax {
@@ -30,13 +30,20 @@ func NewMinimax(c Config, client HTTPClient) *Minimax {
content := []any{map[string]any{"type": "text", "text": r.Prompt}}
materials := r.Materials
if len(materials) == 0 && len(r.InputURLs) > 0 {
materials = []Material{{URL: r.InputURLs[0], Type: MaterialImage}}
materials = make([]Material, 0, len(r.InputURLs))
for _, inputURL := range r.InputURLs {
materials = append(materials, Material{URL: inputURL, Type: MaterialImage})
}
}
if len(materials) > 0 {
role := "first_frame"
if r.Settings["inputMode"] == "reference" {
role = "reference_image"
}
for _, material := range materials {
content = append(content, map[string]any{
"type": "image_url",
"image_url": map[string]any{"url": materials[0].URL},
"role": "first_frame",
"image_url": map[string]any{"url": material.URL},
"role": role,
})
}
return map[string]any{
+51 -10
View File
@@ -66,6 +66,9 @@ type Result struct {
OutputURLs []string
Raw json.RawMessage
ErrorMessage string
ErrorCode string
RequestID string
ErrorDetail string
Usage map[string]int
}
@@ -78,6 +81,9 @@ type HTTPResult struct {
OutputURLs []string `json:"outputUrls"`
Raw json.RawMessage `json:"raw,omitempty"`
ErrorMessage string `json:"errorMessage,omitempty"`
ErrorCode string `json:"errorCode,omitempty"`
RequestID string `json:"requestId,omitempty"`
ErrorDetail string `json:"errorDetail,omitempty"`
Usage map[string]int `json:"usage,omitempty"`
}
@@ -86,7 +92,7 @@ func EncodeResult(result Result) (json.RawMessage, error) {
if urls == nil {
urls = []string{}
}
return json.Marshal(HTTPResult{TaskID: result.TaskID, Status: result.Status, OutputURLs: urls, Raw: result.Raw, ErrorMessage: result.ErrorMessage, Usage: result.Usage})
return json.Marshal(HTTPResult{TaskID: result.TaskID, Status: result.Status, OutputURLs: urls, Raw: result.Raw, ErrorMessage: result.ErrorMessage, ErrorCode: result.ErrorCode, RequestID: result.RequestID, ErrorDetail: SanitizeFailureDetail(result.ErrorDetail, nil), Usage: result.Usage})
}
type Adapter interface {
@@ -211,10 +217,15 @@ func (a *httpAdapter) call(ctx context.Context, method, path string, body []byte
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
code, requestID, errorType := inspectHTTPProviderFailure(raw, resp.Header)
var detail string
// Only a submission carries the original content needed for redaction.
detail := safeProviderResponseDetail(raw, body, a.config.APIKey)
if operation == "submit" && a.failureDetail != nil {
detail = a.failureDetail(raw, body)
if adapterDetail := a.failureDetail(raw, body); adapterDetail != "" {
detail = SanitizeFailureDetail(adapterDetail, body, a.config.APIKey)
}
}
loggedDetail := ""
if a.name == "minimax" {
loggedDetail = detail
}
logHTTPProviderFailure(httpProviderDiagnostic{
Provider: a.name,
@@ -225,7 +236,7 @@ func (a *httpAdapter) call(ctx context.Context, method, path string, body []byte
ErrorType: errorType,
ErrorClass: "service",
ElapsedMS: elapsedMilliseconds(startedAt),
Detail: detail,
Detail: loggedDetail,
})
return Result{}, &ProviderError{Operation: a.name + " " + operation, Status: resp.StatusCode, Code: code, RequestID: requestID, Detail: detail}
}
@@ -236,6 +247,13 @@ func (a *httpAdapter) call(ctx context.Context, method, path string, body []byte
return Result{}, &ProviderError{Operation: a.name + " " + operation}
}
result := a.decode(raw)
if result.Status != StatusFailed && result.Status != StatusExpired && result.Status != StatusCancelled {
if code, requestID, detail, rejected := inspectProviderBusinessRejection(raw, resp.Header, body, a.config.APIKey); rejected {
logHTTPProviderFailure(httpProviderDiagnostic{Provider: a.name, Operation: operation, Status: resp.StatusCode, Code: code, RequestID: requestID, ErrorClass: "service", ElapsedMS: elapsedMilliseconds(startedAt)})
return Result{}, &ProviderError{Operation: a.name + " " + operation, Status: resp.StatusCode, Code: code, RequestID: requestID, Detail: detail}
}
}
result = annotateTerminalFailure(result, raw, resp.Header, body, a.config.APIKey)
result.Raw = append(json.RawMessage(nil), raw...)
return result, nil
}
@@ -283,21 +301,35 @@ func inspectHTTPProviderFailure(raw []byte, headers http.Header) (code, requestI
}
metadata := object(first(root["ResponseMetadata"], root["response_metadata"]))
metadataError := object(first(metadata["Error"], metadata["error"]))
output := object(root["output"])
data := object(root["data"])
task := object(first(root["task"], data["task"]))
nestedError := object(first(data["error"], task["error"], output["error"]))
code = safeHTTPProviderDiagnosticToken(first(
code = firstSafeHTTPProviderDiagnosticToken(64,
providerError["code"], providerError["Code"],
nestedError["code"], nestedError["Code"],
metadataError["code"], metadataError["Code"],
output["code"], output["Code"],
data["code"], data["Code"],
root["code"], root["Code"],
), 64)
)
if code == "" {
code = parenthesizedNumericCode(stringValue(providerError["message"], root["message"]))
code = parenthesizedNumericCode(stringValue(providerError["message"], nestedError["message"], output["message"], data["message"], root["message"]))
}
requestID = safeHTTPProviderDiagnosticToken(first(
requestID = firstSafeHTTPProviderDiagnosticToken(128,
providerError["request_id"], providerError["requestId"], providerError["RequestId"], providerError["RequestID"],
nestedError["request_id"], nestedError["requestId"], nestedError["RequestId"], nestedError["RequestID"],
root["request_id"], root["requestId"], root["RequestId"], root["RequestID"],
data["request_id"], data["requestId"], data["RequestId"], data["RequestID"],
task["request_id"], task["requestId"], task["RequestId"], task["RequestID"],
metadata["request_id"], metadata["requestId"], metadata["RequestId"], metadata["RequestID"],
output["request_id"], output["requestId"], output["RequestId"], output["RequestID"],
headers.Get("X-Tt-Logid"), headers.Get("X-Request-Id"),
), 128)
)
if requestID == "" {
requestID = requestIDFromMessage(stringValue(providerError["message"], nestedError["message"], output["message"], data["message"], root["message"]))
}
errorType = safeHTTPProviderDiagnosticToken(first(
providerError["type"], providerError["Type"],
metadataError["type"], metadataError["Type"],
@@ -354,6 +386,15 @@ func safeHTTPProviderDiagnosticToken(value any, maxLength int) string {
return token
}
func firstSafeHTTPProviderDiagnosticToken(maxLength int, values ...any) string {
for _, value := range values {
if token := safeHTTPProviderDiagnosticToken(value, maxLength); token != "" {
return token
}
}
return ""
}
func classifyHTTPProviderTransportError(err error) string {
if errors.Is(err, context.Canceled) {
return "canceled"
@@ -738,6 +738,33 @@ func TestBailianDecodesCompatibleModeChoiceImages(t *testing.T) {
}
}
func TestBailianWan30ReferencePayloadAndActualDuration(t *testing.T) {
adapter := NewBailian(Config{Model: "wan2.7-i2v-2026-04-25"}, nil)
request := Request{Capability: "video.generate", Model: "wan3.0-video", Prompt: "combine", InputURLs: []string{"https://cdn.test/a.png", "https://cdn.test/b.png"}, Settings: map[string]any{"resolution": "480P", "duration": 20, "ratio": "9:16"}}
payload := adapter.payload(request).(map[string]any)
media := payload["input"].(map[string]any)["media"].([]any)
if payload["model"] != "wan3.0-video" || len(media) != 2 || media[0].(map[string]any)["type"] != "reference_image" || media[1].(map[string]any)["type"] != "reference_image" {
t.Fatalf("payload=%#v", payload)
}
if payload["parameters"].(map[string]any)["ratio"] != "9:16" {
t.Fatalf("parameters=%#v", payload["parameters"])
}
request.Settings["inputMode"] = "frames"
media = adapter.payload(request).(map[string]any)["input"].(map[string]any)["media"].([]any)
if media[0].(map[string]any)["type"] != "first_frame" || media[1].(map[string]any)["type"] != "last_frame" {
t.Fatalf("media=%#v", media)
}
request.InputURLs = nil
request.Settings["inputMode"] = "reference"
if _, exists := adapter.payload(request).(map[string]any)["input"].(map[string]any)["media"]; exists {
t.Fatal("Wan 3.0 text-only request should omit media")
}
result := decodeBailian([]byte(`{"output":{"task_id":"task-1","task_status":"SUCCEEDED","video_url":"https://cdn.test/v.mp4"},"usage":{"output_video_duration":5.25,"duration":8}}`))
if result.Usage["outputMilliseconds"] != 5250 || result.Usage["outputSeconds"] != 6 {
t.Fatalf("usage=%#v", result.Usage)
}
}
func TestSeedance20PayloadMatchesOfficialMultimodalContract(t *testing.T) {
client := roundTripFunc(func(request *http.Request) (*http.Response, error) {
var body map[string]any
+12 -6
View File
@@ -146,11 +146,11 @@ func (v *Volcengine) call(ctx context.Context, action, version string, payload a
raw, err := io.ReadAll(io.LimitReader(resp.Body, limit+1))
if err != nil {
logVolcengineFailure(volcengineDiagnostic{Operation: operation, Status: resp.StatusCode, ErrorClass: "response_read", ElapsedMS: elapsedMilliseconds(startedAt)})
return Result{}, &ProviderError{Operation: "volcengine request", Status: resp.StatusCode}
return Result{}, &ProviderError{Operation: "volcengine request", Cause: err}
}
if int64(len(raw)) > limit {
logVolcengineFailure(volcengineDiagnostic{Operation: operation, Status: resp.StatusCode, ErrorClass: "response_too_large", ElapsedMS: elapsedMilliseconds(startedAt)})
return Result{}, &ProviderError{Operation: "volcengine request", Status: resp.StatusCode}
return Result{}, &ProviderError{Operation: "volcengine request"}
}
validJSON := json.Valid(raw)
r := map[string]any{}
@@ -169,7 +169,7 @@ func (v *Volcengine) call(ctx context.Context, action, version string, payload a
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
diagnostic.ErrorClass = "service"
logVolcengineFailure(diagnostic)
return Result{}, &ProviderError{Operation: "volcengine request", Status: resp.StatusCode, Code: diagnostic.Code}
return Result{}, &ProviderError{Operation: "volcengine request", Status: resp.StatusCode, Code: diagnostic.Code, RequestID: diagnostic.RequestID, Detail: safeProviderResponseDetail(raw, body, v.config.AccessKeyID, v.config.SecretAccessKey)}
}
if !validJSON {
diagnostic.ErrorClass = "invalid_json"
@@ -179,14 +179,19 @@ func (v *Volcengine) call(ctx context.Context, action, version string, payload a
if response.code != nil && !volcengineRequestSucceeded(response.code) {
diagnostic.ErrorClass = "service"
logVolcengineFailure(diagnostic)
return Result{}, &ProviderError{Operation: "volcengine request", Code: diagnostic.Code}
return Result{}, &ProviderError{Operation: "volcengine request", Status: resp.StatusCode, Code: diagnostic.Code, RequestID: diagnostic.RequestID, Detail: safeProviderResponseDetail(raw, body, v.config.AccessKeyID, v.config.SecretAccessKey)}
}
d := object(first(response.business["data"], response.business["Data"]))
out := []string{}
for _, value := range []any{d["image_urls"], d["image_url"], d["url"], d["result_url"], d["output"], d["outputs"]} {
collectURLs(value, &out)
}
return Result{TaskID: stringValue(response.business["task_id"], response.business["TaskId"], d["task_id"], d["TaskId"]), Status: status(first(d["status"], d["Status"], response.business["status"], response.business["Status"])), OutputURLs: out, Raw: raw}, nil
result := Result{TaskID: stringValue(response.business["task_id"], response.business["TaskId"], d["task_id"], d["TaskId"]), Status: status(first(d["status"], d["Status"], response.business["status"], response.business["Status"])), OutputURLs: out, Raw: raw}
result = annotateTerminalFailure(result, raw, resp.Header, body, v.config.AccessKeyID, v.config.SecretAccessKey)
if result.Status == StatusFailed && result.ErrorDetail == "" {
result.ErrorDetail = SanitizeFailureDetail(stringValue(d["message"], response.business["message"]), body, v.config.AccessKeyID, v.config.SecretAccessKey)
}
return result, nil
}
type volcengineResponse struct {
@@ -215,11 +220,12 @@ func inspectVolcengineResponse(root map[string]any, headers http.Header) volceng
business["code_n"], business["codeN"], business["CodeN"],
root["code_n"], root["codeN"], root["CodeN"],
),
requestID: first(
requestID: firstSafeHTTPProviderDiagnosticToken(128,
business["request_id"], business["requestId"], business["RequestId"], business["RequestID"],
root["request_id"], root["requestId"], root["RequestId"], root["RequestID"],
metadata["RequestId"], metadata["RequestID"], metadata["request_id"], metadata["requestId"],
headers.Get("X-Tt-Logid"), headers.Get("X-Request-Id"),
requestIDFromMessage(stringValue(gatewayError["Message"], gatewayError["message"], business["message"], root["message"])),
),
}
}