Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5954afef55 | ||
|
|
929a7a172c | ||
|
|
16149260c7 | ||
|
|
c72c43aaaa | ||
|
|
84aea01b5b | ||
|
|
293ef24bb7 | ||
|
|
0f1765b226 | ||
|
|
1fa58ba901 | ||
|
|
55595570c2 | ||
|
|
fba9759bc7 | ||
|
|
3d3460ce63 | ||
|
|
000ee1bbfd | ||
|
|
d0cfd0a385 |
+2
-2
@@ -36,7 +36,7 @@ require (
|
|||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
github.com/tidwall/sjson v1.2.5 // indirect
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
go.uber.org/goleak v1.3.0 // indirect
|
go.uber.org/goleak v1.3.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.21.0 // indirect
|
||||||
golang.org/x/text v0.37.0 // indirect
|
golang.org/x/text v0.39.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
+4
-4
@@ -73,10 +73,10 @@ golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
|||||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
|
||||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
|||||||
@@ -1306,6 +1306,25 @@ func TestGeminiClientImageGenerateBuildsNativeImageBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGeminiGenerationConfigInitializesMissingImageConfig(t *testing.T) {
|
||||||
|
config := geminiGenerationConfig(map[string]any{
|
||||||
|
"aspect_ratio": "16:9",
|
||||||
|
"resolution": "4K",
|
||||||
|
}, true)
|
||||||
|
|
||||||
|
imageConfig, ok := config["imageConfig"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("imageConfig should be initialized, got %+v", config)
|
||||||
|
}
|
||||||
|
if imageConfig["aspectRatio"] != "16:9" || imageConfig["imageSize"] != "4K" {
|
||||||
|
t.Fatalf("unexpected imageConfig: %+v", imageConfig)
|
||||||
|
}
|
||||||
|
modalities, ok := config["responseModalities"].([]any)
|
||||||
|
if !ok || len(modalities) != 1 || modalities[0] != "IMAGE" {
|
||||||
|
t.Fatalf("image response modality should be initialized, got %+v", config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGeminiClientImageEditPreservesNativeContentsAndFileData(t *testing.T) {
|
func TestGeminiClientImageEditPreservesNativeContentsAndFileData(t *testing.T) {
|
||||||
var captured map[string]any
|
var captured map[string]any
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -1539,10 +1558,39 @@ func TestGeminiClientChatConvertsFunctionCallResponse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGeminiURLAcceptsVersionedBaseURL(t *testing.T) {
|
func TestGeminiURLAcceptsVersionedBaseURL(t *testing.T) {
|
||||||
got := geminiURL("https://generativelanguage.googleapis.com/v1beta", "gemini-2.5-flash", "test-key")
|
tests := []struct {
|
||||||
want := "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=test-key"
|
name string
|
||||||
if got != want {
|
baseURL string
|
||||||
t.Fatalf("unexpected gemini url: %s", got)
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default version",
|
||||||
|
baseURL: "https://generativelanguage.googleapis.com",
|
||||||
|
want: "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=test-key",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "google beta version",
|
||||||
|
baseURL: "https://generativelanguage.googleapis.com/v1beta",
|
||||||
|
want: "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=test-key",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "compatible v1 endpoint",
|
||||||
|
baseURL: "https://cloud.dataeyes.ai/v1",
|
||||||
|
want: "https://cloud.dataeyes.ai/v1/models/gemini-2.5-flash:generateContent?key=test-key",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "openai suffix after version",
|
||||||
|
baseURL: "https://generativelanguage.googleapis.com/v1beta/openai",
|
||||||
|
want: "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=test-key",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
got := geminiURL(test.baseURL, "gemini-2.5-flash", "test-key")
|
||||||
|
if got != test.want {
|
||||||
|
t.Fatalf("unexpected gemini url: %s", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,11 +61,13 @@ func geminiURL(baseURL string, model string, apiKey string) string {
|
|||||||
base = "https://generativelanguage.googleapis.com"
|
base = "https://generativelanguage.googleapis.com"
|
||||||
}
|
}
|
||||||
base = strings.TrimSuffix(base, "/openai")
|
base = strings.TrimSuffix(base, "/openai")
|
||||||
if strings.HasSuffix(base, "/v1beta") {
|
if !strings.HasSuffix(base, "/v1") &&
|
||||||
base = strings.TrimSuffix(base, "/v1beta")
|
!strings.HasSuffix(base, "/v1beta") &&
|
||||||
|
!strings.HasSuffix(base, "/v1alpha") {
|
||||||
|
base += "/v1beta"
|
||||||
}
|
}
|
||||||
escapedModel := url.PathEscape(model)
|
escapedModel := url.PathEscape(model)
|
||||||
return fmt.Sprintf("%s/v1beta/models/%s:generateContent?key=%s", base, escapedModel, url.QueryEscape(apiKey))
|
return fmt.Sprintf("%s/models/%s:generateContent?key=%s", base, escapedModel, url.QueryEscape(apiKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
func geminiBody(request Request) map[string]any {
|
func geminiBody(request Request) map[string]any {
|
||||||
@@ -195,14 +197,23 @@ func geminiApplyRequestOptions(body map[string]any, request Request, imageRespon
|
|||||||
func geminiGenerationConfig(body map[string]any, imageResponse bool) map[string]any {
|
func geminiGenerationConfig(body map[string]any, imageResponse bool) map[string]any {
|
||||||
source := mapFromAny(firstPresent(body["generationConfig"], body["generation_config"]))
|
source := mapFromAny(firstPresent(body["generationConfig"], body["generation_config"]))
|
||||||
out := cloneMapAny(source)
|
out := cloneMapAny(source)
|
||||||
|
if out == nil {
|
||||||
|
out = map[string]any{}
|
||||||
|
}
|
||||||
if aspectRatio := firstNonEmptyString(body["aspect_ratio"], body["aspectRatio"]); aspectRatio != "" {
|
if aspectRatio := firstNonEmptyString(body["aspect_ratio"], body["aspectRatio"]); aspectRatio != "" {
|
||||||
imageConfig := cloneMapAny(mapFromAny(firstPresent(out["imageConfig"], out["image_config"])))
|
imageConfig := cloneMapAny(mapFromAny(firstPresent(out["imageConfig"], out["image_config"])))
|
||||||
|
if imageConfig == nil {
|
||||||
|
imageConfig = map[string]any{}
|
||||||
|
}
|
||||||
imageConfig["aspectRatio"] = aspectRatio
|
imageConfig["aspectRatio"] = aspectRatio
|
||||||
out["imageConfig"] = imageConfig
|
out["imageConfig"] = imageConfig
|
||||||
delete(out, "image_config")
|
delete(out, "image_config")
|
||||||
}
|
}
|
||||||
if imageSize := firstNonEmptyString(body["resolution"], body["imageSize"], body["image_size"], body["size"]); imageSize != "" {
|
if imageSize := firstNonEmptyString(body["resolution"], body["imageSize"], body["image_size"], body["size"]); imageSize != "" {
|
||||||
imageConfig := cloneMapAny(mapFromAny(firstPresent(out["imageConfig"], out["image_config"])))
|
imageConfig := cloneMapAny(mapFromAny(firstPresent(out["imageConfig"], out["image_config"])))
|
||||||
|
if imageConfig == nil {
|
||||||
|
imageConfig = map[string]any{}
|
||||||
|
}
|
||||||
imageConfig["imageSize"] = imageSize
|
imageConfig["imageSize"] = imageSize
|
||||||
out["imageConfig"] = imageConfig
|
out["imageConfig"] = imageConfig
|
||||||
delete(out, "image_config")
|
delete(out, "image_config")
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ func (c UniversalClient) Run(ctx context.Context, request Request) (Response, er
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return Response{}, annotateResponseError(err, submitRequestID, startedAt, time.Now())
|
return Response{}, annotateResponseError(err, submitRequestID, startedAt, time.Now())
|
||||||
}
|
}
|
||||||
|
submitResult = universalEffectiveResult(submitResult)
|
||||||
|
submitRequestID = firstNonEmptyString(submitRequestID, requestIDFromResult(submitResult))
|
||||||
if isUniversalSuccess(submitResult) && submitResult["data"] != nil {
|
if isUniversalSuccess(submitResult) && submitResult["data"] != nil {
|
||||||
return Response{
|
return Response{
|
||||||
Result: normalizeUniversalResult(request, submitResult, ""),
|
Result: normalizeUniversalResult(request, submitResult, ""),
|
||||||
@@ -157,6 +159,7 @@ func (c UniversalClient) universalPollUntilDone(ctx context.Context, executor *s
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", annotateResponseError(err, firstNonEmptyString(pollRequestID, requestID, upstreamTaskID), pollStarted, pollFinished)
|
return nil, "", annotateResponseError(err, firstNonEmptyString(pollRequestID, requestID, upstreamTaskID), pollStarted, pollFinished)
|
||||||
}
|
}
|
||||||
|
result = universalEffectiveResult(result)
|
||||||
lastResult = result
|
lastResult = result
|
||||||
requestID = firstNonEmptyString(pollRequestID, requestID, requestIDFromResult(result), upstreamTaskID)
|
requestID = firstNonEmptyString(pollRequestID, requestID, requestIDFromResult(result), upstreamTaskID)
|
||||||
if isUniversalSuccess(result) {
|
if isUniversalSuccess(result) {
|
||||||
@@ -239,6 +242,14 @@ func universalScriptContext(request Request, modelType string, payload map[strin
|
|||||||
return selectedBase + "/" + strings.TrimLeft(path, "/")
|
return selectedBase + "/" + strings.TrimLeft(path, "/")
|
||||||
}
|
}
|
||||||
context["creatRequestURL"] = context["createRequestURL"]
|
context["creatRequestURL"] = context["createRequestURL"]
|
||||||
|
processedParams := cloneBody(request.Body)
|
||||||
|
processedParams["model"] = upstreamModelName(request.Candidate)
|
||||||
|
context["processedParams"] = processedParams
|
||||||
|
context["preProcessParams"] = func(params map[string]any, _ ...string) map[string]any {
|
||||||
|
processed := cloneMapAny(params)
|
||||||
|
processed["model"] = upstreamModelName(request.Candidate)
|
||||||
|
return processed
|
||||||
|
}
|
||||||
context["resolveGetTaskURL"] = func(taskID string) string {
|
context["resolveGetTaskURL"] = func(taskID string) string {
|
||||||
return resolveUniversalTaskURL(request.Candidate.PlatformConfig, taskID)
|
return resolveUniversalTaskURL(request.Candidate.PlatformConfig, taskID)
|
||||||
}
|
}
|
||||||
@@ -389,6 +400,20 @@ func universalStatus(result map[string]any) string {
|
|||||||
return strings.ToLower(strings.TrimSpace(firstNonEmptyString(result["status"], result["state"], result["task_status"])))
|
return strings.ToLower(strings.TrimSpace(firstNonEmptyString(result["status"], result["state"], result["task_status"])))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func universalEffectiveResult(result map[string]any) map[string]any {
|
||||||
|
nested, ok := result["result"].(map[string]any)
|
||||||
|
if !ok || nested == nil || isUniversalFailure(result) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
out := cloneMapAny(nested)
|
||||||
|
for _, key := range []string{"status", "request_id", "requestId", "upstream_task_id", "task_id", "taskId", "id"} {
|
||||||
|
if out[key] == nil && result[key] != nil {
|
||||||
|
out[key] = result[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func universalTaskID(result map[string]any) string {
|
func universalTaskID(result map[string]any) string {
|
||||||
return firstNonEmptyString(result["upstream_task_id"], result["task_id"], result["taskId"], result["id"])
|
return firstNonEmptyString(result["upstream_task_id"], result["task_id"], result["taskId"], result["id"])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,47 @@ func TestUniversalClientDefaultSubmitAndPoll(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUniversalClientSupportsServerMainScriptContextAndNestedResult(t *testing.T) {
|
||||||
|
request := Request{
|
||||||
|
Kind: "images.generations",
|
||||||
|
ModelType: "image_generate",
|
||||||
|
Model: "custom-image",
|
||||||
|
Body: map[string]any{"model": "custom-image", "prompt": "hello"},
|
||||||
|
Candidate: testUniversalCandidate(map[string]any{
|
||||||
|
"customGetParamsScript": map[string]any{
|
||||||
|
"image_generate": `async function getParams(params, context) {
|
||||||
|
const processed = await context.preProcessParams(params, context.type);
|
||||||
|
return { prompt: processed.prompt + "-" + processed.model + "-" + context.processedParams.model };
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
"customSubmitScript": map[string]any{
|
||||||
|
"image_generate": `async function submitTask(payload) {
|
||||||
|
return {
|
||||||
|
status: "success",
|
||||||
|
result: {
|
||||||
|
status: "success",
|
||||||
|
data: [{ url: "https://cdn.example/" + payload.prompt + ".png" }]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := (UniversalClient{}).Run(context.Background(), request)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("run failed: %v", err)
|
||||||
|
}
|
||||||
|
data, ok := response.Result["data"].([]any)
|
||||||
|
if !ok || len(data) != 1 {
|
||||||
|
t.Fatalf("unexpected nested result: %#v", response.Result)
|
||||||
|
}
|
||||||
|
image, ok := data[0].(map[string]any)
|
||||||
|
if !ok || image["url"] != "https://cdn.example/hello-provider-model-provider-model.png" {
|
||||||
|
t.Fatalf("unexpected image result: %#v", response.Result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUniversalClientResumeSkipsSubmit(t *testing.T) {
|
func TestUniversalClientResumeSkipsSubmit(t *testing.T) {
|
||||||
request := Request{
|
request := Request{
|
||||||
Kind: "videos.generations",
|
Kind: "videos.generations",
|
||||||
|
|||||||
@@ -29,6 +29,23 @@ func TestKlingCompatibilitySimulationFlow(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
|
var upgradedBaseModels int
|
||||||
|
if err := db.Pool().QueryRow(ctx, `
|
||||||
|
SELECT count(*)
|
||||||
|
FROM base_model_catalog
|
||||||
|
WHERE provider_key = 'keling'
|
||||||
|
AND provider_model_name IN ('kling-video-o1', 'kling-v3-omni')
|
||||||
|
AND model_type @> '["video_generate","image_to_video","omni_video"]'::jsonb
|
||||||
|
AND capabilities ? 'video_generate'
|
||||||
|
AND capabilities ? 'image_to_video'
|
||||||
|
AND capabilities ? 'omni_video'
|
||||||
|
AND metadata->'rawModel'->'types' @> '["video_generate","image_to_video","omni_video"]'::jsonb`).Scan(&upgradedBaseModels); err != nil {
|
||||||
|
t.Fatalf("read upgraded Kling Omni base model capabilities: %v", err)
|
||||||
|
}
|
||||||
|
if upgradedBaseModels != 2 {
|
||||||
|
t.Fatalf("expected both Kling Omni base models to expose base video capabilities, got %d", upgradedBaseModels)
|
||||||
|
}
|
||||||
|
|
||||||
serverCtx, cancelServer := context.WithCancel(ctx)
|
serverCtx, cancelServer := context.WithCancel(ctx)
|
||||||
defer cancelServer()
|
defer cancelServer()
|
||||||
server := httptest.NewServer(NewServerWithContext(serverCtx, config.Config{
|
server := httptest.NewServer(NewServerWithContext(serverCtx, config.Config{
|
||||||
@@ -89,6 +106,55 @@ func TestKlingCompatibilitySimulationFlow(t *testing.T) {
|
|||||||
"displayName": model,
|
"displayName": model,
|
||||||
}, http.StatusCreated, nil)
|
}, http.StatusCreated, nil)
|
||||||
}
|
}
|
||||||
|
var upgradedPlatformModels int
|
||||||
|
if err := db.Pool().QueryRow(ctx, `
|
||||||
|
SELECT count(*)
|
||||||
|
FROM platform_models
|
||||||
|
WHERE platform_id = $1::uuid
|
||||||
|
AND model_type @> '["video_generate","image_to_video","omni_video"]'::jsonb
|
||||||
|
AND capabilities ? 'video_generate'
|
||||||
|
AND capabilities ? 'image_to_video'
|
||||||
|
AND capabilities ? 'omni_video'`, platform.ID).Scan(&upgradedPlatformModels); err != nil {
|
||||||
|
t.Fatalf("read upgraded Kling Omni platform model capabilities: %v", err)
|
||||||
|
}
|
||||||
|
if upgradedPlatformModels != 2 {
|
||||||
|
t.Fatalf("expected both Kling Omni platform models to expose base video capabilities, got %d", upgradedPlatformModels)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertGenericVideoGeneration := func(name string, model string, image string, expectedModelType string) {
|
||||||
|
t.Helper()
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
request := map[string]any{
|
||||||
|
"model": model,
|
||||||
|
"prompt": "通用视频接口模拟任务",
|
||||||
|
"duration": 5,
|
||||||
|
"resolution": "720p",
|
||||||
|
"runMode": "simulation",
|
||||||
|
"simulation": true,
|
||||||
|
"simulationDurationMs": 5,
|
||||||
|
}
|
||||||
|
if image != "" {
|
||||||
|
request["image"] = image
|
||||||
|
}
|
||||||
|
var response struct {
|
||||||
|
Task struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
ModelType string `json:"modelType"`
|
||||||
|
ResolvedModel string `json:"resolvedModel"`
|
||||||
|
} `json:"task"`
|
||||||
|
}
|
||||||
|
doJSON(t, server.URL, http.MethodPost, "/api/v1/videos/generations", apiKeyResponse.Secret, request, http.StatusAccepted, &response)
|
||||||
|
resolvedModel, resolved := klingV2ProviderModel(response.Task.ResolvedModel)
|
||||||
|
if response.Task.Status != "succeeded" || response.Task.ModelType != expectedModelType || !resolved || resolvedModel != model {
|
||||||
|
t.Fatalf("generic video request without modelType should use inferred capability: %+v", response.Task)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, model := range []string{klingO1Model, klingV3OmniModel} {
|
||||||
|
assertGenericVideoGeneration(model+"-text-to-video", model, "", "video_generate")
|
||||||
|
assertGenericVideoGeneration(model+"-image-to-video", model, "https://example.com/first.png", "image_to_video")
|
||||||
|
}
|
||||||
|
|
||||||
createV1 := func(model string, duration int, externalID string) string {
|
createV1 := func(model string, duration int, externalID string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|||||||
@@ -488,6 +488,8 @@ func modelTypeAliases(value string) []string {
|
|||||||
return []string{"image_edit"}
|
return []string{"image_edit"}
|
||||||
case "video", "videos.generations":
|
case "video", "videos.generations":
|
||||||
return []string{"video_generate"}
|
return []string{"video_generate"}
|
||||||
|
case "omni_video":
|
||||||
|
return []string{"video_generate", "image_to_video", "omni_video"}
|
||||||
case "song", "music", "song.generations", "music.generations", "music_generate":
|
case "song", "music", "song.generations", "music.generations", "music_generate":
|
||||||
return []string{"audio_generate"}
|
return []string{"audio_generate"}
|
||||||
case "speech", "speech.generations", "tts":
|
case "speech", "speech.generations", "tts":
|
||||||
|
|||||||
@@ -9,6 +9,19 @@ func TestNormalizeModelMatchKeyRemovesWhitespace(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNormalizeModelTypeListExpandsOmniVideoBaseCapabilities(t *testing.T) {
|
||||||
|
got := normalizeModelTypeList([]string{"omni_video"})
|
||||||
|
want := StringList{"video_generate", "image_to_video", "omni_video"}
|
||||||
|
if len(got) != len(want) {
|
||||||
|
t.Fatalf("omni_video should include text-to-video and image-to-video capabilities: got=%v want=%v", got, want)
|
||||||
|
}
|
||||||
|
for index := range want {
|
||||||
|
if got[index] != want[index] {
|
||||||
|
t.Fatalf("omni_video capability mismatch at %d: got=%v want=%v", index, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTaskBillingModelIdentityKeepsRequestedModelPrimary(t *testing.T) {
|
func TestTaskBillingModelIdentityKeepsRequestedModelPrimary(t *testing.T) {
|
||||||
identity := taskBillingModelIdentity(GatewayTask{
|
identity := taskBillingModelIdentity(GatewayTask{
|
||||||
Model: "doubao-5.0 图像编辑",
|
Model: "doubao-5.0 图像编辑",
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
WITH keling_omni_models(provider_model_name, model_type, capabilities) AS (
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
'kling-video-o1',
|
||||||
|
'["video_generate","image_to_video","omni_video"]'::jsonb,
|
||||||
|
'{
|
||||||
|
"video_generate": {
|
||||||
|
"output_resolutions": ["720p", "1080p"],
|
||||||
|
"aspect_ratio_allowed": ["16:9", "1:1", "9:16"],
|
||||||
|
"duration_options": [3, 4, 5, 6, 7, 8, 9, 10],
|
||||||
|
"output_audio": false,
|
||||||
|
"input_audio": false,
|
||||||
|
"prompt_length_limit": {
|
||||||
|
"max": 2500,
|
||||||
|
"count_mode": "non_ascii_weighted",
|
||||||
|
"label": "可灵口径"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"image_to_video": {
|
||||||
|
"output_resolutions": ["720p", "1080p"],
|
||||||
|
"aspect_ratio_allowed": ["16:9", "1:1", "9:16"],
|
||||||
|
"duration_options": [3, 4, 5, 6, 7, 8, 9, 10],
|
||||||
|
"input_first_frame": true,
|
||||||
|
"input_last_frame": false,
|
||||||
|
"input_first_last_frame": true,
|
||||||
|
"input_reference_generate_single": true,
|
||||||
|
"input_reference_generate_multiple": true,
|
||||||
|
"max_images": 7,
|
||||||
|
"max_images_for_last_frame": 2,
|
||||||
|
"support_video_effect_template": false,
|
||||||
|
"output_audio": false,
|
||||||
|
"input_audio": false,
|
||||||
|
"prompt_length_limit": {
|
||||||
|
"max": 2500,
|
||||||
|
"count_mode": "non_ascii_weighted",
|
||||||
|
"label": "可灵口径"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"omni_video": {
|
||||||
|
"supported_modes": ["text_to_video", "image_reference", "element_reference", "first_last_frame", "video_reference", "video_edit"],
|
||||||
|
"output_resolutions": ["720p", "1080p"],
|
||||||
|
"aspect_ratio_allowed": ["16:9", "1:1", "9:16"],
|
||||||
|
"duration_options": [3, 4, 5, 6, 7, 8, 9, 10],
|
||||||
|
"output_audio": false,
|
||||||
|
"input_audio": false,
|
||||||
|
"max_videos": 1,
|
||||||
|
"max_audios": 0,
|
||||||
|
"max_images": 7,
|
||||||
|
"max_elements": 7,
|
||||||
|
"max_images_and_elements": 7,
|
||||||
|
"limits_with_video": {"max_images_and_elements": 4},
|
||||||
|
"max_images_for_last_frame": 2,
|
||||||
|
"support_instruction_edit": true,
|
||||||
|
"prompt_length_limit": {
|
||||||
|
"max": 2500,
|
||||||
|
"count_mode": "non_ascii_weighted",
|
||||||
|
"label": "可灵口径"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"originalTypes": ["video_generate", "image_to_video", "omni_video"]
|
||||||
|
}'::jsonb
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'kling-v3-omni',
|
||||||
|
'["video_generate","image_to_video","omni_video"]'::jsonb,
|
||||||
|
'{
|
||||||
|
"video_generate": {
|
||||||
|
"output_resolutions": ["720p", "1080p", "2160p"],
|
||||||
|
"aspect_ratio_allowed": ["16:9", "1:1", "9:16"],
|
||||||
|
"duration_options": [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
||||||
|
"output_audio": true,
|
||||||
|
"input_audio": false,
|
||||||
|
"prompt_length_limit": {
|
||||||
|
"max": 2500,
|
||||||
|
"count_mode": "non_ascii_weighted",
|
||||||
|
"label": "可灵口径"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"image_to_video": {
|
||||||
|
"output_resolutions": ["720p", "1080p", "2160p"],
|
||||||
|
"aspect_ratio_allowed": ["16:9", "1:1", "9:16"],
|
||||||
|
"duration_options": [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
||||||
|
"input_first_frame": true,
|
||||||
|
"input_last_frame": false,
|
||||||
|
"input_first_last_frame": true,
|
||||||
|
"input_reference_generate_single": true,
|
||||||
|
"input_reference_generate_multiple": true,
|
||||||
|
"max_images": 7,
|
||||||
|
"max_images_for_last_frame": 2,
|
||||||
|
"support_video_effect_template": false,
|
||||||
|
"output_audio": true,
|
||||||
|
"input_audio": false,
|
||||||
|
"prompt_length_limit": {
|
||||||
|
"max": 2500,
|
||||||
|
"count_mode": "non_ascii_weighted",
|
||||||
|
"label": "可灵口径"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"omni_video": {
|
||||||
|
"supported_modes": ["text_to_video", "image_reference", "element_reference", "first_last_frame", "video_reference", "video_edit", "multi_shot"],
|
||||||
|
"output_resolutions": ["720p", "1080p", "2160p"],
|
||||||
|
"aspect_ratio_allowed": ["16:9", "1:1", "9:16"],
|
||||||
|
"duration_options": [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
||||||
|
"output_audio": true,
|
||||||
|
"input_audio": false,
|
||||||
|
"max_videos": 1,
|
||||||
|
"max_audios": 0,
|
||||||
|
"max_images": 7,
|
||||||
|
"max_elements": 7,
|
||||||
|
"max_images_and_elements": 7,
|
||||||
|
"limits_with_video": {
|
||||||
|
"max_images_and_elements": 4,
|
||||||
|
"duration_options": [3, 4, 5, 6, 7, 8, 9, 10]
|
||||||
|
},
|
||||||
|
"max_images_for_last_frame": 2,
|
||||||
|
"support_instruction_edit": true,
|
||||||
|
"prompt_length_limit": {
|
||||||
|
"max": 2500,
|
||||||
|
"count_mode": "non_ascii_weighted",
|
||||||
|
"label": "可灵口径"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"originalTypes": ["video_generate", "image_to_video", "omni_video"]
|
||||||
|
}'::jsonb
|
||||||
|
)
|
||||||
|
)
|
||||||
|
UPDATE base_model_catalog model
|
||||||
|
SET model_type = defs.model_type,
|
||||||
|
capabilities = defs.capabilities,
|
||||||
|
metadata = COALESCE(model.metadata, '{}'::jsonb)
|
||||||
|
|| jsonb_build_object(
|
||||||
|
'originalTypes', defs.model_type,
|
||||||
|
'rawModel', COALESCE(model.metadata->'rawModel', '{}'::jsonb)
|
||||||
|
|| jsonb_build_object(
|
||||||
|
'types', defs.model_type,
|
||||||
|
'capabilities', defs.capabilities
|
||||||
|
)
|
||||||
|
),
|
||||||
|
default_snapshot = CASE
|
||||||
|
WHEN COALESCE(model.default_snapshot, '{}'::jsonb) = '{}'::jsonb THEN model.default_snapshot
|
||||||
|
ELSE jsonb_set(
|
||||||
|
jsonb_set(
|
||||||
|
jsonb_set(model.default_snapshot, '{modelType}', defs.model_type, true),
|
||||||
|
'{capabilities}',
|
||||||
|
defs.capabilities,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
'{metadata}',
|
||||||
|
COALESCE(model.default_snapshot->'metadata', '{}'::jsonb)
|
||||||
|
|| jsonb_build_object(
|
||||||
|
'originalTypes', defs.model_type,
|
||||||
|
'rawModel', COALESCE(model.default_snapshot->'metadata'->'rawModel', '{}'::jsonb)
|
||||||
|
|| jsonb_build_object(
|
||||||
|
'types', defs.model_type,
|
||||||
|
'capabilities', defs.capabilities
|
||||||
|
)
|
||||||
|
),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
END,
|
||||||
|
updated_at = now()
|
||||||
|
FROM keling_omni_models defs
|
||||||
|
WHERE model.provider_model_name = defs.provider_model_name
|
||||||
|
AND model.model_type @> '["omni_video"]'::jsonb;
|
||||||
|
|
||||||
|
WITH keling_omni_models(provider_model_name, model_type, capabilities) AS (
|
||||||
|
SELECT DISTINCT ON (model.provider_model_name)
|
||||||
|
model.provider_model_name,
|
||||||
|
model.model_type,
|
||||||
|
model.capabilities
|
||||||
|
FROM base_model_catalog model
|
||||||
|
WHERE model.provider_model_name IN ('kling-video-o1', 'kling-v3-omni')
|
||||||
|
AND model.model_type @> '["omni_video"]'::jsonb
|
||||||
|
ORDER BY model.provider_model_name, (model.provider_key = 'keling') DESC, model.created_at ASC
|
||||||
|
)
|
||||||
|
UPDATE platform_models model
|
||||||
|
SET model_type = defs.model_type,
|
||||||
|
capabilities = defs.capabilities,
|
||||||
|
updated_at = now()
|
||||||
|
FROM keling_omni_models defs
|
||||||
|
WHERE COALESCE(NULLIF(model.provider_model_name, ''), model.model_name) = defs.provider_model_name
|
||||||
|
AND model.model_type @> '["omni_video"]'::jsonb;
|
||||||
@@ -1 +1 @@
|
|||||||
2d6c16fec0bec9c0288e5cb142b458af982fff8f
|
fba9759bc7a5fbca5e9d1465bdd7a91de6a10928
|
||||||
|
|||||||
Generated
+8
-7
@@ -6,17 +6,18 @@ settings:
|
|||||||
|
|
||||||
overrides:
|
overrides:
|
||||||
'@babel/core@<=7.29.0': 7.29.6
|
'@babel/core@<=7.29.0': 7.29.6
|
||||||
'@nx/js>picomatch': 4.0.5
|
|
||||||
'@nx/vite>picomatch': 4.0.5
|
|
||||||
'@nx/workspace>picomatch': 4.0.5
|
|
||||||
axios@<1.18.0: 1.18.0
|
axios@<1.18.0: 1.18.0
|
||||||
brace-expansion@>=2.0.0 <2.1.2: 2.1.2
|
brace-expansion@>=2.0.0 <2.1.2: 2.1.2
|
||||||
dompurify@<=3.4.10: 3.4.11
|
dompurify@<=3.4.10: 3.4.11
|
||||||
esbuild@>=0.27.3 <0.28.1: 0.28.1
|
esbuild@>=0.27.3 <0.28.1: 0.28.1
|
||||||
|
fast-uri@>=3.0.0 <3.1.3: 3.1.3
|
||||||
form-data@>=4.0.0 <4.0.6: 4.0.6
|
form-data@>=4.0.0 <4.0.6: 4.0.6
|
||||||
js-yaml@<4.3.0: 4.3.0
|
js-yaml@<4.3.0: 4.3.0
|
||||||
mermaid@>=11.0.0-alpha.1 <=11.14.0: 11.15.0
|
mermaid@>=11.0.0-alpha.1 <=11.14.0: 11.15.0
|
||||||
minimatch@>=9.0.0 <9.0.7: 9.0.7
|
minimatch@>=9.0.0 <9.0.7: 9.0.7
|
||||||
|
'@nx/js>picomatch': 4.0.5
|
||||||
|
'@nx/vite>picomatch': 4.0.5
|
||||||
|
'@nx/workspace>picomatch': 4.0.5
|
||||||
tmp@<0.2.7: 0.2.7
|
tmp@<0.2.7: 0.2.7
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
@@ -3034,8 +3035,8 @@ packages:
|
|||||||
fast-deep-equal@3.1.3:
|
fast-deep-equal@3.1.3:
|
||||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||||
|
|
||||||
fast-uri@3.1.2:
|
fast-uri@3.1.3:
|
||||||
resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==}
|
resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==}
|
||||||
|
|
||||||
fdir@6.5.0:
|
fdir@6.5.0:
|
||||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||||
@@ -7181,7 +7182,7 @@ snapshots:
|
|||||||
ajv@8.20.0:
|
ajv@8.20.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-deep-equal: 3.1.3
|
fast-deep-equal: 3.1.3
|
||||||
fast-uri: 3.1.2
|
fast-uri: 3.1.3
|
||||||
json-schema-traverse: 1.0.0
|
json-schema-traverse: 1.0.0
|
||||||
require-from-string: 2.0.2
|
require-from-string: 2.0.2
|
||||||
|
|
||||||
@@ -7850,7 +7851,7 @@ snapshots:
|
|||||||
|
|
||||||
fast-deep-equal@3.1.3: {}
|
fast-deep-equal@3.1.3: {}
|
||||||
|
|
||||||
fast-uri@3.1.2: {}
|
fast-uri@3.1.3: {}
|
||||||
|
|
||||||
fdir@6.5.0(picomatch@4.0.4):
|
fdir@6.5.0(picomatch@4.0.4):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ overrides:
|
|||||||
'brace-expansion@>=2.0.0 <2.1.2': 2.1.2
|
'brace-expansion@>=2.0.0 <2.1.2': 2.1.2
|
||||||
'dompurify@<=3.4.10': 3.4.11
|
'dompurify@<=3.4.10': 3.4.11
|
||||||
'esbuild@>=0.27.3 <0.28.1': 0.28.1
|
'esbuild@>=0.27.3 <0.28.1': 0.28.1
|
||||||
|
'fast-uri@>=3.0.0 <3.1.3': 3.1.3
|
||||||
'form-data@>=4.0.0 <4.0.6': 4.0.6
|
'form-data@>=4.0.0 <4.0.6': 4.0.6
|
||||||
'js-yaml@<4.3.0': 4.3.0
|
'js-yaml@<4.3.0': 4.3.0
|
||||||
'mermaid@>=11.0.0-alpha.1 <=11.14.0': 11.15.0
|
'mermaid@>=11.0.0-alpha.1 <=11.14.0': 11.15.0
|
||||||
|
|||||||
Reference in New Issue
Block a user