功能增加

This commit is contained in:
andy committed 2026-09-11 15:33:18 +08:00
1 parent b340fc7ad7
commit f1993eb388
69 files changed
+2243 -162

No files matched your search

+51
View File
@@ -0,0 +1,51 @@
package providers
import (
"context"
"encoding/json"
"io"
"net/http"
"strings"
"testing"
)
func TestA4ImageAdapterDimensions(t *testing.T) {
for _, size := range []struct {
width, height int
evoSize, bailianSize string
}{
{1680, 2376, "848x1200", "1680*2376"},
{2376, 1680, "1200x848", "2376*1680"},
{848, 1200, "848x1200", "848*1200"},
{1200, 848, "1200x848", "1200*848"},
} {
request := Request{Capability: "image.generate", Prompt: "A4 itinerary", Settings: map[string]any{"width": size.width, "height": size.height}}
evo := NewEvoLink(Config{}, nil).payload(request).(map[string]any)
if evo["size"] != size.evoSize || evo["resolution"] != "1K" {
t.Fatalf("evolink=%#v", evo)
}
bailian := NewBailian(Config{}, nil).payload(request).(map[string]any)
if object(bailian["parameters"])["size"] != size.bailianSize {
t.Fatalf("bailian=%#v", bailian)
}
}
}
func TestA4JimengDimensionsAreSubmittedUnchanged(t *testing.T) {
for _, size := range [][2]int{{1680, 2376}, {2376, 1680}} {
client := roundTripFunc(func(request *http.Request) (*http.Response, error) {
var body map[string]any
if err := json.NewDecoder(request.Body).Decode(&body); err != nil {
t.Fatal(err)
}
if body["width"] != float64(size[0]) || body["height"] != float64(size[1]) {
t.Fatalf("body=%#v", body)
}
return &http.Response{StatusCode: 200, Header: http.Header{}, Body: io.NopCloser(strings.NewReader(`{"code":10000,"data":{"task_id":"test-a4"}}`))}, nil
})
adapter := NewVolcengine(Config{BaseURL: "https://visual.test", Model: "jimeng_seedream46_cvtob", AccessKeyID: "test-key", SecretAccessKey: "test-secret"}, client, nil)
if _, err := adapter.Submit(context.Background(), Request{Capability: "image.generate", Prompt: "A4 itinerary", Settings: map[string]any{"width": size[0], "height": size[1]}}); err != nil {
t.Fatal(err)
}
}
}
+7
View File
@@ -25,6 +25,13 @@ func NewEvoLink(c Config, client HTTPClient) *EvoLink {
payload["size"] = size
} else if width, widthOK := positiveInteger(r.Settings["width"]); widthOK {
if height, heightOK := positiveInteger(r.Settings["height"]); heightOK {
// A4 custom pixels ignore the resolution shortcut. Match the
// frontend's 1K budget rather than silently requesting a 2K image.
if width == 1680 && height == 2376 {
width, height = 848, 1200
} else if width == 2376 && height == 1680 {
width, height = 1200, 848
}
payload["size"] = supportedRatio(width, height)
}
}
+124
View File
@@ -0,0 +1,124 @@
package providers
import (
"context"
"net/http"
"strings"
)
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.
type Minimax struct{ *httpAdapter }
func NewMinimax(c Config, client HTTPClient) *Minimax {
if client == nil {
client = http.DefaultClient
}
if strings.TrimSpace(c.Model) == "" {
c.Model = MinimaxH3Model
}
a := &httpAdapter{
name: "minimax",
config: c,
client: client,
submitPath: func(Request) string { return "/v2/video_generation" },
queryPath: func(id string) string { return "/v2/query/video_generation/" + id },
payload: func(r Request) any {
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}}
}
if len(materials) > 0 {
content = append(content, map[string]any{
"type": "image_url",
"image_url": map[string]any{"url": materials[0].URL},
"role": "first_frame",
})
}
return map[string]any{
"model": requestModel(r, c.Model),
"content": content,
"resolution": r.Settings["resolution"],
"duration": r.Settings["duration"],
"ratio": r.Settings["ratio"],
"aigc_watermark": false,
}
},
decode: decodeMinimax,
failureDetail: func(responseBody, requestBody []byte) string {
return minimaxSafeFailureDetail(responseBody, requestBody, c.APIKey)
},
}
return &Minimax{a}
}
func (a *Minimax) Submit(ctx context.Context, request Request) (Result, error) {
result, err := a.submit(ctx, request)
if err != nil {
logMinimaxFailedRequest(request, a.config, err)
}
return result, err
}
func (a *Minimax) Query(ctx context.Context, id string) (Result, error) {
return a.query(ctx, id)
}
func decodeMinimax(raw []byte) Result {
root := record(raw)
task := object(root["task"])
if len(task) == 0 {
return Result{TaskID: stringValue(root["task_id"]), Status: StatusQueued}
}
outputURLs := []string{}
collectURLs(object(task["content"])["url"], &outputURLs)
usage := map[string]int{}
for source, target := range map[string]string{
"total_seconds": "totalSeconds",
"input_seconds": "inputSeconds",
"output_seconds": "outputSeconds",
"input_image_count": "inputImageCount",
"input_audio_seconds": "inputAudioSeconds",
"total_tokens": "totalTokens",
"prompt_tokens": "promptTokens",
"completion_tokens": "completionTokens",
} {
if number, ok := object(task["usage"])[source].(float64); ok && number >= 0 {
usage[target] = int(number)
}
}
providerError := object(task["error"])
resultStatus := status(task["status"])
errorMessage := minimaxTaskFailureMessage(safeHTTPProviderDiagnosticToken(providerError["code"], 16), stringValue(providerError["message"]))
if resultStatus == StatusFailed && errorMessage == "" {
errorMessage = "MiniMax 视频生成失败,请调整提示词或素材后重试。"
}
return Result{
TaskID: stringValue(task["id"], root["task_id"]),
Status: resultStatus,
OutputURLs: outputURLs,
ErrorMessage: errorMessage,
Usage: usage,
}
}
func minimaxTaskFailureMessage(code, message string) string {
switch strings.TrimSpace(code) {
case "1008":
return "MiniMax API 账户余额不足,请充值后重试。"
case "1026":
return "视频描述或输入素材触发内容安全审核,请调整后重试。"
case "1002":
return "MiniMax 请求频率过高,请稍后重试。"
}
if strings.TrimSpace(message) != "" {
return "MiniMax 视频生成失败,请调整提示词或素材后重试。"
}
return ""
}
var _ Adapter = (*Minimax)(nil)
@@ -0,0 +1,114 @@
package providers
import (
"encoding/json"
"errors"
"log"
"net/url"
"regexp"
"strconv"
"strings"
"unicode/utf8"
)
var (
minimaxDiagnosticURL = regexp.MustCompile(`(?i)(?:https?://|data:)[^\s"'<>]+`)
minimaxDiagnosticCredential = regexp.MustCompile(`(?i)(?:bearer\s+|(?:authorization|api[_-]?key|access[_-]?key|secret|token|password|signature|credential)\s*["']?\s*[:=]\s*["']?(?:bearer\s+)?)[^\s,"';}]+`)
minimaxDiagnosticQuoted = regexp.MustCompile("\"(?:[^\"\\\\]|\\\\.)*\"|'[^']*'|`[^`]*`")
minimaxDiagnosticLongToken = regexp.MustCompile(`[A-Za-z0-9_+/=.-]{32,}`)
)
// Preserve the upstream validation explanation, but remove request content,
// credentials, URLs and arbitrary quoted values before it reaches logs or DB.
func minimaxSafeFailureDetail(responseBody, requestBody []byte, apiKey string) string {
root := record(responseBody)
message := stringValue(object(root["error"])["message"], root["message"])
if message == "" {
return ""
}
privateValues := []string{apiKey}
request := record(requestBody)
content, _ := request["content"].([]any)
for _, item := range content {
entry := object(item)
privateValues = append(privateValues, stringValue(entry["text"]))
for _, kind := range []string{"image_url", "video_url", "audio_url"} {
privateValues = append(privateValues, stringValue(object(entry[kind])["url"]))
}
}
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 {
message = string(runes[:512]) + "...[truncated]"
}
return message
}
func logMinimaxFailedRequest(request Request, config Config, err error) {
var failure *ProviderError
if !errors.As(err, &failure) {
return
}
// All strings except the request ID are fixed enums. In particular, never
// log custom endpoints, prompts or even the hosts of reference image URLs.
endpointClass := "custom"
basePathClass := "custom"
if endpoint, parseErr := url.Parse(config.BaseURL); parseErr == nil {
if endpoint.Hostname() == "api.minimax.cn" {
endpointClass = "official-cn"
}
switch strings.Trim(endpoint.Path, "/") {
case "":
basePathClass = "root"
case "v1", "v2":
basePathClass = "version-prefix"
}
}
imageItems := 0
if len(request.Materials) > 0 || len(request.InputURLs) > 0 {
imageItems = 1
}
duration := "invalid"
if encoded, encodeErr := json.Marshal(request.Settings["duration"]); encodeErr == nil {
if value, parseErr := strconv.Atoi(string(encoded)); parseErr == nil && value >= 4 && value <= 15 {
duration = strconv.Itoa(value)
}
}
log.Printf(
"zhinian-api MiniMax submit request summary requestId=%q model=%q resolution=%q duration=%s ratio=%q textItems=1 promptChars=%d imageItems=%d endpointClass=%s basePathClass=%s",
failure.RequestID, minimaxDiagnosticValue(requestModel(request, config.Model)),
minimaxDiagnosticValue(stringValue(request.Settings["resolution"])), duration,
minimaxDiagnosticValue(stringValue(request.Settings["ratio"])), utf8.RuneCountInString(request.Prompt),
imageItems, endpointClass, basePathClass,
)
}
func minimaxDiagnosticValue(value string) string {
switch value {
case MinimaxH3Model, "MiniMax-H3-Max", "768P", "2K", "480P", "adaptive", "21:9", "16:9", "4:3", "1:1", "3:4", "9:16",
"model", "content", "text", "prompt", "resolution", "duration", "ratio", "image_url", "video_url", "audio_url", "role", "first_frame", "last_frame", "aigc_watermark":
return value
default:
return "unrecognized"
}
}
@@ -0,0 +1,98 @@
package providers
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"log"
"net/http"
"reflect"
"strings"
"testing"
"unicode/utf8"
)
func TestMinimaxPreservesRejectionReasonForPersistedTextVideoRequest(t *testing.T) {
var output bytes.Buffer
previousOutput := log.Writer()
log.SetOutput(&output)
t.Cleanup(func() { log.SetOutput(previousOutput) })
var request Request
if err := json.Unmarshal([]byte(`{"model":"MiniMax-H3","prompt":"一段盛开的烟花","settings":{"ratio":"1:1","duration":4,"resolution":"768P"},"capability":"video.generate"}`), &request); err != nil {
t.Fatal(err)
}
client := roundTripFunc(func(r *http.Request) (*http.Response, error) {
if r.Method != http.MethodPost || r.URL.Path != "/v2/video_generation" {
t.Fatalf("unexpected endpoint: %s %s", r.Method, r.URL.Path)
}
var payload map[string]any
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatal(err)
}
want := map[string]any{
"model": "MiniMax-H3", "resolution": "768P", "duration": float64(4), "ratio": "1:1", "aigc_watermark": false,
"content": []any{map[string]any{"type": "text", "text": "一段盛开的烟花"}},
}
if !reflect.DeepEqual(payload, want) {
t.Fatalf("wire payload=%#v want=%#v", payload, want)
}
// A synthetic validation response: this does not assert that 4 seconds
// is unsupported by the live API, only that its explanation survives.
return &http.Response{StatusCode: 400, Header: http.Header{}, Body: io.NopCloser(strings.NewReader(`{"error":{"type":"bad_request_error","message":"invalid params, duration is not supported (2013)"},"request_id":"request-minimax-1"}`))}, nil
})
adapter := NewMinimax(Config{BaseURL: "https://api.minimax.cn", APIKey: "private-api-key"}, client)
_, err := adapter.Submit(context.Background(), request)
var failure *ProviderError
if !errors.As(err, &failure) || failure.Code != "2013" || failure.RequestID != "request-minimax-1" || failure.Detail != "invalid params, duration is not supported (2013)" {
t.Fatalf("failure=%#v err=%v", failure, err)
}
if strings.Contains(err.Error(), "invalid params") {
t.Fatalf("generic error exposed diagnostic: %v", err)
}
for _, expected := range []string{`detail="invalid params, duration is not supported (2013)"`, `requestId="request-minimax-1"`, `model="MiniMax-H3"`, `resolution="768P"`, "duration=4", `ratio="1:1"`, "promptChars=7", "imageItems=0", "endpointClass=official-cn", "basePathClass=root"} {
if !strings.Contains(output.String(), expected) {
t.Errorf("missing %q from %s", expected, output.String())
}
}
for _, private := range []string{"一段盛开的烟花", "private-api-key"} {
if strings.Contains(output.String(), private) {
t.Errorf("log leaked %q", private)
}
}
}
func TestMinimaxFailureDetailRedactsEchoedInputsCredentialsAndURLs(t *testing.T) {
const prompt = "private prompt\nsecond line"
const imageURL = "https://private.test/a.png?Signature=private-signature"
request, _ := json.Marshal(map[string]any{"content": []any{
map[string]any{"type": "text", "text": prompt},
map[string]any{"type": "image_url", "image_url": map[string]any{"url": imageURL}},
}})
encodedPrompt, _ := json.Marshal(prompt)
response, _ := json.Marshal(map[string]any{"error": map[string]any{"message": "invalid params, 'model' \"MiniMax-H3\" not supported; " + prompt + " " + string(encodedPrompt) + " " + imageURL + " Bearer private-api-key api_key=another-key Authorization: Bearer unrelated-token 'private-user-name' https://other-private.test/a.png?token=other-secret (2013)"}})
detail := minimaxSafeFailureDetail(response, request, "private-api-key")
for _, expected := range []string{"invalid params", "model", "MiniMax-H3", "not supported", "(2013)", "[redacted]"} {
if !strings.Contains(detail, expected) {
t.Fatalf("missing %q in %q", expected, detail)
}
}
for _, private := range []string{"private", "second line", "another-key", "unrelated-token", "other-secret", "Signature=", "https://", "Bearer", "\n"} {
if strings.Contains(detail, private) {
t.Fatalf("detail leaked %q: %s", private, detail)
}
}
}
func TestMinimaxFailureDetailIsBoundedAndIgnoresOtherResponseFields(t *testing.T) {
response, _ := json.Marshal(map[string]any{"error": map[string]any{"message": strings.Repeat("参数无效 ", 200), "request": "private request body"}, "secret": "private secret"})
detail := minimaxSafeFailureDetail(response, nil, "")
if utf8.RuneCountInString(detail) > 512+len("...[truncated]") || !strings.HasSuffix(detail, "...[truncated]") || strings.Contains(detail, "private") {
t.Fatalf("unsafe/unbounded detail: %s", detail)
}
if detail := minimaxSafeFailureDetail([]byte("not json"), nil, ""); detail != "" {
t.Fatalf("invalid JSON detail=%q", detail)
}
}
+51 -10
View File
@@ -112,7 +112,11 @@ type ProviderError struct {
Operation string
Status int
Code string
Cause error
RequestID string
// Detail is opt-in, bounded and redacted by the provider-specific adapter.
// Error() deliberately never includes it.
Detail string
Cause error
}
func (e *ProviderError) Error() string {
@@ -125,14 +129,15 @@ func (e *ProviderError) Error() string {
func (e *ProviderError) Unwrap() error { return e.Cause }
type httpAdapter struct {
name string
config Config
client HTTPClient
submitPath func(Request) string
queryPath func(string) string
payload func(Request) any
headers func(*http.Request)
decode func([]byte) Result
name string
config Config
client HTTPClient
submitPath func(Request) string
queryPath func(string) string
payload func(Request) any
headers func(*http.Request)
decode func([]byte) Result
failureDetail func(responseBody, requestBody []byte) string
}
func (a *httpAdapter) submit(ctx context.Context, input Request) (Result, error) {
@@ -206,6 +211,11 @@ 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.
if operation == "submit" && a.failureDetail != nil {
detail = a.failureDetail(raw, body)
}
logHTTPProviderFailure(httpProviderDiagnostic{
Provider: a.name,
Operation: operation,
@@ -215,8 +225,9 @@ func (a *httpAdapter) call(ctx context.Context, method, path string, body []byte
ErrorType: errorType,
ErrorClass: "service",
ElapsedMS: elapsedMilliseconds(startedAt),
Detail: detail,
})
return Result{}, &ProviderError{Operation: a.name + " " + operation, Status: resp.StatusCode, Code: code}
return Result{}, &ProviderError{Operation: a.name + " " + operation, Status: resp.StatusCode, Code: code, RequestID: requestID, Detail: detail}
}
if !json.Valid(raw) {
logHTTPProviderFailure(httpProviderDiagnostic{
@@ -238,9 +249,18 @@ type httpProviderDiagnostic struct {
ErrorType string
ErrorClass string
ElapsedMS int64
Detail string
}
func logHTTPProviderFailure(diagnostic httpProviderDiagnostic) {
if diagnostic.Detail != "" {
log.Printf(
"zhinian-api provider operation failed provider=%s operation=%s status=%d code=%q requestId=%q errorType=%q errorClass=%s elapsedMs=%d detail=%q",
diagnostic.Provider, diagnostic.Operation, diagnostic.Status, diagnostic.Code, diagnostic.RequestID,
diagnostic.ErrorType, diagnostic.ErrorClass, diagnostic.ElapsedMS, diagnostic.Detail,
)
return
}
log.Printf(
"zhinian-api provider operation failed provider=%s operation=%s status=%d code=%q requestId=%q errorType=%q errorClass=%s elapsedMs=%d",
diagnostic.Provider,
@@ -269,6 +289,9 @@ func inspectHTTPProviderFailure(raw []byte, headers http.Header) (code, requestI
metadataError["code"], metadataError["Code"],
root["code"], root["Code"],
), 64)
if code == "" {
code = parenthesizedNumericCode(stringValue(providerError["message"], root["message"]))
}
requestID = safeHTTPProviderDiagnosticToken(first(
providerError["request_id"], providerError["requestId"], providerError["RequestId"], providerError["RequestID"],
root["request_id"], root["requestId"], root["RequestId"], root["RequestID"],
@@ -283,6 +306,24 @@ func inspectHTTPProviderFailure(raw []byte, headers http.Header) (code, requestI
return code, requestID, errorType
}
func parenthesizedNumericCode(message string) string {
message = strings.TrimSpace(message)
if len(message) < 3 || message[len(message)-1] != ')' {
return ""
}
start := strings.LastIndexByte(message, '(')
if start < 0 || start == len(message)-2 {
return ""
}
code := message[start+1 : len(message)-1]
for _, character := range code {
if character < '0' || character > '9' {
return ""
}
}
return safeHTTPProviderDiagnosticToken(code, 16)
}
func safeHTTPProviderDiagnosticToken(value any, maxLength int) string {
var token string
switch typed := value.(type) {
@@ -27,6 +27,7 @@ func TestHTTPAdaptersMapRequestsAndResponses(t *testing.T) {
{"evolink", NewEvoLink(Config{BaseURL: "https://e.test", APIKey: "secret", Model: "gpt-image-2"}, nil), "/v1/images/generations", "/v1/tasks/task-1", `{"id":"task-1","status":"completed","results":[{"url":"https://cdn.test/a.png"}]}`},
{"bailian", NewBailian(Config{BaseURL: "https://b.test", APIKey: "secret", Model: "wan"}, nil), "/api/v1/services/aigc/image-generation/generation", "/api/v1/tasks/task-1", `{"output":{"task_id":"task-1","task_status":"SUCCEEDED","results":[{"url":"https://cdn.test/a.png"}]}}`},
{"seedance", NewSeedance(Config{BaseURL: "https://s.test/api/v3", APIKey: "secret", Model: "seed"}, nil), "/api/v3/contents/generations/tasks", "/api/v3/contents/generations/tasks/task-1", `{"id":"task-1","status":"succeeded","content":{"video_url":"https://cdn.test/a.mp4"}}`},
{"minimax", NewMinimax(Config{BaseURL: "https://m.test", APIKey: "secret", Model: MinimaxH3Model}, nil), "/v2/video_generation", "/v2/query/video_generation/task-1", `{"task":{"id":"task-1","status":"succeeded","content":{"url":"https://cdn.test/a.mp4"}}}`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -52,6 +53,8 @@ func TestHTTPAdaptersMapRequestsAndResponses(t *testing.T) {
a.client = client
case *Seedance:
a.client = client
case *Minimax:
a.client = client
}
submitted, err := tt.adapter.Submit(context.Background(), Request{Capability: "image.generate", Prompt: "hello"})
if err != nil || submitted.TaskID != "task-1" {
@@ -65,6 +68,50 @@ func TestHTTPAdaptersMapRequestsAndResponses(t *testing.T) {
}
}
func TestMinimaxH3PayloadAndUsageMatchOfficialV2Contract(t *testing.T) {
var calls int
client := roundTripFunc(func(request *http.Request) (*http.Response, error) {
calls++
if request.Header.Get("Authorization") != "Bearer secret" {
t.Fatalf("authorization=%q", request.Header.Get("Authorization"))
}
if calls == 1 {
var body map[string]any
if err := json.NewDecoder(request.Body).Decode(&body); err != nil {
t.Fatal(err)
}
content := body["content"].([]any)
image := content[1].(map[string]any)
imageURL := image["image_url"].(map[string]any)
if body["model"] != MinimaxH3Model || body["resolution"] != "2K" || body["duration"] != float64(8) || body["ratio"] != "adaptive" || body["aigc_watermark"] != false || len(content) != 2 || image["role"] != "first_frame" || imageURL["url"] != "https://cdn.test/first.png" {
t.Fatalf("body=%#v", body)
}
return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(`{"task_id":"task-h3"}`)), Header: http.Header{}}, nil
}
return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(`{"task":{"id":"task-h3","model":"MiniMax-H3","status":"succeeded","content":{"url":"https://cdn.test/h3.mp4"},"resolution":"2K","duration":8,"usage":{"total_seconds":8,"input_seconds":0,"output_seconds":8,"input_image_count":1,"completion_tokens":1234},"ratio":"16:9"}}`)), Header: http.Header{}}, nil
})
adapter := NewMinimax(Config{BaseURL: "https://m.test", APIKey: "secret", Model: MinimaxH3Model}, client)
submitted, err := adapter.Submit(context.Background(), Request{
Capability: "video.generate", Prompt: "animate", Materials: []Material{{URL: "https://cdn.test/first.png", Type: MaterialImage}},
Settings: map[string]any{"resolution": "2K", "duration": float64(8), "ratio": "adaptive"},
})
if err != nil || submitted.TaskID != "task-h3" || submitted.Status != StatusQueued {
t.Fatalf("submitted=%#v err=%v", submitted, err)
}
queried, err := adapter.Query(context.Background(), submitted.TaskID)
if err != nil || queried.Status != StatusSucceeded || !reflect.DeepEqual(queried.OutputURLs, []string{"https://cdn.test/h3.mp4"}) || queried.Usage["outputSeconds"] != 8 || queried.Usage["inputImageCount"] != 1 {
t.Fatalf("queried=%#v err=%v", queried, err)
}
}
func TestMinimaxErrorDiagnosticsExtractSafeNumericCode(t *testing.T) {
raw := []byte(`{"type":"error","error":{"type":"insufficient_balance_error","message":"insufficient balance (1008)","http_code":"402"},"request_id":"request-safe"}`)
code, requestID, errorType := inspectHTTPProviderFailure(raw, http.Header{})
if code != "1008" || requestID != "request-safe" || errorType != "insufficient_balance_error" {
t.Fatalf("diagnostics=%q/%q/%q", code, requestID, errorType)
}
}
func TestBailianOnlyEnablesAsyncModeWhenSubmitting(t *testing.T) {
var requests []*http.Request
client := roundTripFunc(func(request *http.Request) (*http.Response, error) {