feat: 完善模型请求适配与输出限制
This commit is contained in:
@@ -2294,6 +2294,256 @@ func TestKelingClientVideoSubmitsAndPollsImageTask(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestKelingClient30TurboUsesModelEndpointAndTasksAPI(t *testing.T) {
|
||||
var submitPath string
|
||||
var pollPath string
|
||||
var pollQuery string
|
||||
var gotAuth string
|
||||
var submittedPayload map[string]any
|
||||
var submittedTaskPayload map[string]any
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotAuth = r.Header.Get("Authorization")
|
||||
switch r.Method + " " + r.URL.Path {
|
||||
case "POST /text-to-video/kling-3.0-turbo":
|
||||
submitPath = r.URL.Path
|
||||
if err := json.NewDecoder(r.Body).Decode(&submittedPayload); err != nil {
|
||||
t.Fatalf("decode keling 3.0 turbo submit: %v", err)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"code": 0,
|
||||
"request_id": "req-turbo-submit",
|
||||
"data": map[string]any{
|
||||
"id": "turbo-task-1",
|
||||
"status": "submitted",
|
||||
},
|
||||
})
|
||||
case "GET /tasks":
|
||||
pollPath = r.URL.Path
|
||||
pollQuery = r.URL.RawQuery
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"code": 0,
|
||||
"request_id": "req-turbo-poll",
|
||||
"data": []any{
|
||||
map[string]any{
|
||||
"id": "turbo-task-1",
|
||||
"status": "succeeded",
|
||||
"create_time": 789,
|
||||
"outputs": []any{
|
||||
map[string]any{
|
||||
"type": "video",
|
||||
"url": "https://example.com/turbo.mp4",
|
||||
"watermark_url": "https://example.com/turbo-watermark.mp4",
|
||||
"duration": "8",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
default:
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
response, err := (KelingClient{HTTPClient: server.Client()}).Run(context.Background(), Request{
|
||||
Kind: "videos.generations",
|
||||
ModelType: "video_generate",
|
||||
Model: "可灵3.0 Turbo",
|
||||
Body: map[string]any{
|
||||
"prompt": "A cinematic city reveal",
|
||||
"duration": 8,
|
||||
"resolution": "1080p",
|
||||
"aspect_ratio": "9:16",
|
||||
"callback_url": "https://example.com/callback",
|
||||
"external_task_id": "external-1",
|
||||
},
|
||||
Candidate: store.RuntimeModelCandidate{
|
||||
BaseURL: server.URL + "/v1",
|
||||
Provider: "keling",
|
||||
AuthType: "APIKey",
|
||||
ModelName: "可灵3.0 Turbo",
|
||||
ProviderModelName: "kling-3.0-turbo",
|
||||
Credentials: map[string]any{"apiKey": "kling-api-key"},
|
||||
PlatformConfig: map[string]any{
|
||||
"kelingPollIntervalMs": 100,
|
||||
"kelingPollTimeoutSeconds": 1,
|
||||
},
|
||||
},
|
||||
OnRemoteTaskSubmitted: func(remoteTaskID string, payload map[string]any) error {
|
||||
if remoteTaskID != "turbo-task-1" {
|
||||
t.Fatalf("unexpected remote task id: %s", remoteTaskID)
|
||||
}
|
||||
submittedTaskPayload = payload
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("run keling 3.0 turbo video: %v", err)
|
||||
}
|
||||
if submitPath != "/text-to-video/kling-3.0-turbo" ||
|
||||
pollPath != "/tasks" ||
|
||||
pollQuery != "task_ids=turbo-task-1" ||
|
||||
gotAuth != "Bearer kling-api-key" {
|
||||
t.Fatalf("unexpected keling 3.0 turbo paths/auth submit=%s poll=%s?%s auth=%s", submitPath, pollPath, pollQuery, gotAuth)
|
||||
}
|
||||
if submittedTaskPayload["endpoint"] != "/text-to-video/kling-3.0-turbo" ||
|
||||
submittedTaskPayload["taskApi"] != "keling_tasks_v2" {
|
||||
t.Fatalf("unexpected submitted task payload: %+v", submittedTaskPayload)
|
||||
}
|
||||
settings, _ := submittedPayload["settings"].(map[string]any)
|
||||
options, _ := submittedPayload["options"].(map[string]any)
|
||||
if submittedPayload["prompt"] != "A cinematic city reveal" ||
|
||||
numericValue(settings["duration"], 0) != 8 ||
|
||||
settings["resolution"] != "1080p" ||
|
||||
settings["aspect_ratio"] != "9:16" ||
|
||||
options["callback_url"] != "https://example.com/callback" ||
|
||||
options["external_task_id"] != "external-1" {
|
||||
t.Fatalf("unexpected keling 3.0 turbo payload: %+v", submittedPayload)
|
||||
}
|
||||
data, _ := response.Result["data"].([]any)
|
||||
item, _ := data[0].(map[string]any)
|
||||
if response.Result["upstream_task_id"] != "turbo-task-1" ||
|
||||
item["url"] != "https://example.com/turbo.mp4" ||
|
||||
item["watermark_url"] != "https://example.com/turbo-watermark.mp4" {
|
||||
t.Fatalf("unexpected keling 3.0 turbo response: %+v", response.Result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKelingClient30TurboRejectsLegacyCredentials(t *testing.T) {
|
||||
_, err := (KelingClient{}).Run(context.Background(), Request{
|
||||
Kind: "videos.generations",
|
||||
Body: map[string]any{"prompt": "A cinematic city reveal"},
|
||||
Candidate: store.RuntimeModelCandidate{
|
||||
Provider: "keling",
|
||||
AuthType: "AccessKey-SecretKey",
|
||||
ProviderModelName: "kling-3.0-turbo",
|
||||
Credentials: map[string]any{"accessKey": "ak", "secretKey": "sk"},
|
||||
},
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "new API key") {
|
||||
t.Fatalf("expected keling 3.0 turbo API key requirement, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKelingClient30TurboResumePollsWithoutSubmitting(t *testing.T) {
|
||||
var submitCalled bool
|
||||
var pollQuery string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method + " " + r.URL.Path {
|
||||
case "POST /text-to-video/kling-3.0-turbo":
|
||||
submitCalled = true
|
||||
t.Fatalf("resume should not submit a new keling 3.0 turbo task")
|
||||
case "GET /tasks":
|
||||
pollQuery = r.URL.RawQuery
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"code": 0,
|
||||
"request_id": "req-turbo-resume",
|
||||
"data": []any{
|
||||
map[string]any{
|
||||
"id": "turbo-existing",
|
||||
"status": "succeeded",
|
||||
"outputs": []any{
|
||||
map[string]any{"type": "video", "url": "https://example.com/resumed-turbo.mp4"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
default:
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
response, err := (KelingClient{HTTPClient: server.Client()}).Run(context.Background(), Request{
|
||||
Kind: "videos.generations",
|
||||
ModelType: "video_generate",
|
||||
RemoteTaskID: "turbo-existing",
|
||||
Body: map[string]any{},
|
||||
Candidate: store.RuntimeModelCandidate{
|
||||
BaseURL: server.URL + "/v1",
|
||||
Provider: "keling",
|
||||
AuthType: "APIKey",
|
||||
ProviderModelName: "kling-3.0-turbo",
|
||||
Credentials: map[string]any{"apiKey": "kling-api-key"},
|
||||
PlatformConfig: map[string]any{
|
||||
"kelingPollIntervalMs": 100,
|
||||
"kelingPollTimeoutSeconds": 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("resume keling 3.0 turbo video: %v", err)
|
||||
}
|
||||
if submitCalled || pollQuery != "task_ids=turbo-existing" {
|
||||
t.Fatalf("resume should only poll existing task, submit=%v query=%s", submitCalled, pollQuery)
|
||||
}
|
||||
data, _ := response.Result["data"].([]any)
|
||||
item, _ := data[0].(map[string]any)
|
||||
if item["url"] != "https://example.com/resumed-turbo.mp4" {
|
||||
t.Fatalf("unexpected resumed keling 3.0 turbo response: %+v", response.Result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeling30TurboPayloadBuildsFirstFrameAndMultiShotRequests(t *testing.T) {
|
||||
imagePayload, endpoint, err := keling30TurboPayload(Request{
|
||||
Body: map[string]any{
|
||||
"duration": 5,
|
||||
"resolution": "720p",
|
||||
"content": []any{
|
||||
map[string]any{"type": "text", "text": "The subject looks toward the camera"},
|
||||
map[string]any{
|
||||
"type": "image_url",
|
||||
"role": "first_frame",
|
||||
"image_url": map[string]any{"url": "https://example.com/first.png"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("build keling 3.0 turbo image payload: %v", err)
|
||||
}
|
||||
if endpoint != "/image-to-video/kling-3.0-turbo" {
|
||||
t.Fatalf("unexpected image endpoint: %s", endpoint)
|
||||
}
|
||||
if _, ok := mapFromAny(imagePayload["settings"])["aspect_ratio"]; ok {
|
||||
t.Fatalf("image-to-video settings should not contain aspect_ratio: %+v", imagePayload)
|
||||
}
|
||||
contents, _ := imagePayload["contents"].([]any)
|
||||
frame := mapFromAny(contents[1])
|
||||
if frame["type"] != "first_frame" || frame["url"] != "https://example.com/first.png" {
|
||||
t.Fatalf("unexpected image contents: %+v", imagePayload["contents"])
|
||||
}
|
||||
|
||||
shotPayload, _, err := keling30TurboPayload(Request{
|
||||
Body: map[string]any{
|
||||
"resolution": "720p",
|
||||
"content": []any{
|
||||
map[string]any{"type": "text", "role": "shot_prompt", "shot_index": 1, "duration": 4, "text": "A car enters the tunnel"},
|
||||
map[string]any{"type": "text", "role": "shot_prompt", "shot_index": 2, "duration": 3, "text": "The headlights fill the frame"},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("build keling 3.0 turbo shot payload: %v", err)
|
||||
}
|
||||
if shotPayload["prompt"] != "shot 1, 4s, A car enters the tunnel; shot 2, 3s, The headlights fill the frame;" ||
|
||||
numericValue(mapFromAny(shotPayload["settings"])["duration"], 0) != 7 {
|
||||
t.Fatalf("unexpected shot payload: %+v", shotPayload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeling30TurboPayloadRejectsLastFrame(t *testing.T) {
|
||||
_, _, err := keling30TurboPayload(Request{
|
||||
Body: map[string]any{
|
||||
"prompt": "Move forward",
|
||||
"last_frame": "https://example.com/last.png",
|
||||
},
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "last frame") {
|
||||
t.Fatalf("expected unsupported last frame error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKelingOmniPayloadConvertsGatewayContent(t *testing.T) {
|
||||
payload, cleanupIDs, err := (KelingClient{}).kelingOmniPayload(context.Background(), Request{
|
||||
Kind: "videos.generations",
|
||||
|
||||
Reference in New Issue
Block a user