From d37fb3ea60575172cc331065ea323ab549ad9dbc Mon Sep 17 00:00:00 2001 From: easyai Date: Wed, 22 Jul 2026 10:00:30 +0800 Subject: [PATCH] =?UTF-8?q?fix(keling):=20=E7=BB=9F=E4=B8=80=20Omni=20?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=88=AB=E5=90=8D=E4=B8=8E=E7=94=9F=E4=BA=A7?= =?UTF-8?q?=E7=BB=91=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/internal/httpapi/handlers.go | 17 +++++++++++++- .../httpapi/keling_compat_handlers.go | 16 +++++++------- .../httpapi/keling_compat_handlers_test.go | 22 +++++++++++++++++-- .../httpapi/keling_compat_integration_test.go | 4 ++-- apps/api/internal/httpapi/kling_compat.go | 6 ++++- docs/kling-omni-compatible.md | 10 ++++----- 6 files changed, 56 insertions(+), 19 deletions(-) diff --git a/apps/api/internal/httpapi/handlers.go b/apps/api/internal/httpapi/handlers.go index 5ef7fb9..ecd68a1 100644 --- a/apps/api/internal/httpapi/handlers.go +++ b/apps/api/internal/httpapi/handlers.go @@ -1070,11 +1070,15 @@ func (s *Server) createTask(kind string, compatible bool) http.Handler { return } } - model := requestModelName(body) + requestedModel := requestModelName(body) + model := canonicalTaskModelName(kind, requestedModel) if model == "" { writeError(w, http.StatusBadRequest, "model is required") return } + if model != requestedModel { + body["model"] = model + } if !apiKeyScopeAllowed(user, kind) { writeError(w, http.StatusForbidden, "api key scope does not allow this capability") return @@ -1402,6 +1406,17 @@ func requestModelName(body map[string]any) string { return modelNameFromValue(body["model"]) } +func canonicalTaskModelName(kind string, model string) string { + model = strings.TrimSpace(model) + if kind != "videos.generations" { + return model + } + if canonical, ok := canonicalKlingOmniModel(model); ok { + return canonical + } + return model +} + func modelNameFromValue(value any) string { switch typed := value.(type) { case string: diff --git a/apps/api/internal/httpapi/keling_compat_handlers.go b/apps/api/internal/httpapi/keling_compat_handlers.go index eb1ef30..1ab322c 100644 --- a/apps/api/internal/httpapi/keling_compat_handlers.go +++ b/apps/api/internal/httpapi/keling_compat_handlers.go @@ -269,7 +269,7 @@ func normalizeKelingOmniRequest(input map[string]any) (map[string]any, *kelingCo if sound != "on" && sound != "off" { return nil, newKelingCompatError(http.StatusBadRequest, 1201, "sound must be on or off") } - if model == "kling-o1" && sound == "on" { + if model == klingO1Model && sound == "on" { return nil, newKelingCompatError(http.StatusBadRequest, 1201, "kling-video-o1 does not support generated audio; sound must be off") } @@ -347,7 +347,7 @@ func normalizeKelingOmniRequest(input map[string]any) (map[string]any, *kelingCo if duration < 3 || duration > maxDuration { return nil, newKelingCompatError(http.StatusBadRequest, 1201, fmt.Sprintf("duration for %s must be an integer between 3 and %d seconds", requestedModel, maxDuration)) } - if model == "kling-o1" && (len(images) == 0 || hasFirstFrame) && duration != 5 && duration != 10 { + if model == klingO1Model && (len(images) == 0 || hasFirstFrame) && duration != 5 && duration != 10 { return nil, newKelingCompatError(http.StatusBadRequest, 1201, "kling-video-o1 text-to-video and first-frame generation only support 5 or 10 seconds") } } @@ -532,14 +532,14 @@ func normalizeKelingMultiPrompts(value any) ([]any, int, *kelingCompatError) { } func kelingCompatModel(value string) (string, int, bool) { - switch strings.ToLower(strings.TrimSpace(value)) { - case "kling-video-o1", "kling-o1": - return "kling-o1", 10, true - case "kling-v3-omni", "kling-3.0-omni": - return "kling-3.0-omni", 15, true - default: + model, ok := canonicalKlingOmniModel(value) + if !ok { return "", 0, false } + if model == klingO1Model { + return model, 10, true + } + return model, 15, true } func kelingCompatObjectList(value any, field string) ([]map[string]any, *kelingCompatError) { diff --git a/apps/api/internal/httpapi/keling_compat_handlers_test.go b/apps/api/internal/httpapi/keling_compat_handlers_test.go index 0a59403..5446e21 100644 --- a/apps/api/internal/httpapi/keling_compat_handlers_test.go +++ b/apps/api/internal/httpapi/keling_compat_handlers_test.go @@ -27,7 +27,7 @@ func TestNormalizeKelingOmniRequestMapsOfficialFields(t *testing.T) { if err != nil { t.Fatalf("normalize Kling request: %v", err) } - if normalized["model"] != "kling-3.0-omni" || + if normalized["model"] != "kling-v3-omni" || normalized["modelType"] != "omni_video" || normalized["resolution"] != "1080p" || normalized["aspect_ratio"] != "9:16" || @@ -49,6 +49,24 @@ func TestNormalizeKelingOmniRequestMapsOfficialFields(t *testing.T) { } } +func TestCanonicalTaskModelNameNormalizesKelingOmniAliases(t *testing.T) { + tests := map[string]string{ + "kling-o1": "kling-video-o1", + "kling-video-o1": "kling-video-o1", + "kling-3.0-omni": "kling-v3-omni", + "kling-3-omni": "kling-v3-omni", + "kling-v3-omni": "kling-v3-omni", + } + for input, expected := range tests { + if got := canonicalTaskModelName("videos.generations", input); got != expected { + t.Fatalf("canonicalTaskModelName(%q) = %q, want %q", input, got, expected) + } + } + if got := canonicalTaskModelName("chat.completions", "kling-o1"); got != "kling-o1" { + t.Fatalf("non-video model must not be rewritten, got %q", got) + } +} + func TestNormalizeKelingOmniRequestBuildsMultiShotMedia(t *testing.T) { normalized, err := normalizeKelingOmniRequest(map[string]any{ "model_name": "kling-3.0-omni", @@ -70,7 +88,7 @@ func TestNormalizeKelingOmniRequestBuildsMultiShotMedia(t *testing.T) { if err != nil { t.Fatalf("normalize multi-shot request: %v", err) } - if normalized["model"] != "kling-3.0-omni" || normalized["duration"] != 5 || normalized["multi_shot"] != true || normalized["shot_type"] != "customize" { + if normalized["model"] != "kling-v3-omni" || normalized["duration"] != 5 || normalized["multi_shot"] != true || normalized["shot_type"] != "customize" { t.Fatalf("unexpected multi-shot fields: %+v", normalized) } content, _ := normalized["content"].([]any) diff --git a/apps/api/internal/httpapi/keling_compat_integration_test.go b/apps/api/internal/httpapi/keling_compat_integration_test.go index 89ac4fe..42907bb 100644 --- a/apps/api/internal/httpapi/keling_compat_integration_test.go +++ b/apps/api/internal/httpapi/keling_compat_integration_test.go @@ -100,9 +100,9 @@ func TestKelingOmniCompatibleHTTPFlow(t *testing.T) { _, err = db.CreatePlatformModel(ctx, store.CreatePlatformModelInput{ PlatformID: platform.ID, CanonicalModelKey: "keling:kling-video-o1", - ModelName: "kling-o1", + ModelName: "kling-video-o1", ProviderModelName: "kling-video-o1", - ModelAlias: "kling-o1", + ModelAlias: "", ModelType: store.StringList{"omni_video", "video_generate"}, DisplayName: "Kling O1 Compatible Test", Capabilities: map[string]any{ diff --git a/apps/api/internal/httpapi/kling_compat.go b/apps/api/internal/httpapi/kling_compat.go index fc49613..e5e9ca7 100644 --- a/apps/api/internal/httpapi/kling_compat.go +++ b/apps/api/internal/httpapi/kling_compat.go @@ -695,7 +695,11 @@ func klingV2Status(status string) string { } func klingV2ProviderModel(pathModel string) (string, bool) { - switch strings.ToLower(strings.TrimSpace(pathModel)) { + return canonicalKlingOmniModel(pathModel) +} + +func canonicalKlingOmniModel(value string) (string, bool) { + switch strings.ToLower(strings.TrimSpace(value)) { case "kling-o1", "kling-video-o1": return klingO1Model, true case "kling-v3-omni", "kling-3.0-omni", "kling-3-omni": diff --git a/docs/kling-omni-compatible.md b/docs/kling-omni-compatible.md index dd01753..dfbbe9b 100644 --- a/docs/kling-omni-compatible.md +++ b/docs/kling-omni-compatible.md @@ -10,12 +10,12 @@ export GATEWAY_API_KEY="" ## 模型与参数映射 -| 请求 `model_name` | 网关模型别名 | TranStreams 原生 `model_name` | 时长范围 | +| 请求 `model_name` | 网关候选模型 | 可灵原生 `model_name` | 时长范围 | | --- | --- | --- | --- | -| `kling-video-o1`、`kling-o1` | `kling-o1` | `kling-video-o1` | 3–10 秒 | -| `kling-v3-omni`、`kling-3.0-omni` | `kling-3.0-omni` | `kling-v3-omni` | 3–15 秒 | +| `kling-video-o1`、`kling-o1` | `kling-video-o1` | `kling-video-o1` | 3–10 秒 | +| `kling-v3-omni`、`kling-3.0-omni`、`kling-3-omni` | `kling-v3-omni` | `kling-v3-omni` | 3–15 秒 | -网关别名用于候选匹配,原生模型名用于发往 TranStreams 的 Kling Omni 请求;两类名称不会混用。 +上述旧别名会在入口处自动归一为可灵原生模型名,候选匹配不依赖平台模型额外配置别名。 `kling-video-o1` 的纯文生视频和首帧生视频只接受 5 或 10 秒;3–10 秒中的其他整数需要使用普通参考图等支持该时长的 Omni 输入。`kling-v3-omni` 接受 3–15 秒。 @@ -109,7 +109,7 @@ curl -sS -X POST "$GATEWAY_PUBLIC_API_BASE/videos/generations" \ "resolution": "1080p", "aspect_ratio": "9:16", "duration": 5, - "audio": true, + "audio": false, "watermark": false, "runMode": "real" }'