runapi-kling 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,25 +1,25 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runapi-kling
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: RunAPI Kling SDK for text-to-video, image-to-video, avatar, and motion-control workflows in JavaScript, Python, Ruby, Go, Java, and PHP
5
5
  Project-URL: Homepage, https://runapi.ai/models/kling
6
- Project-URL: Documentation, https://runapi.ai/docs#sdk-kling
6
+ Project-URL: Documentation, https://runapi.ai/docs/resources/sdks
7
7
  Project-URL: Source, https://github.com/runapi-ai/kling-sdk
8
8
  Project-URL: Issues, https://github.com/runapi-ai/kling-sdk/issues
9
9
  Project-URL: Changelog, https://github.com/runapi-ai/kling-sdk/blob/main/CHANGELOG.md
10
- Project-URL: Release Notes, https://github.com/runapi-ai/kling-sdk/releases/tag/python%2Fv0.2.0
10
+ Project-URL: Release Notes, https://github.com/runapi-ai/kling-sdk/releases/tag/python%2Fv0.2.2
11
11
  Author-email: RunAPI <contact@runapi.ai>
12
12
  License-Expression: Apache-2.0
13
13
  Keywords: api,golang,gradle,java,kling,kling-ai-api,maven,python,ruby,runapi,runapi-ai,sdk,text-to-video,typescript,video-api,video-generation
14
14
  Requires-Python: >=3.9
15
- Requires-Dist: runapi-core>=0.3.0
15
+ Requires-Dist: runapi-core>=0.3.3
16
16
  Description-Content-Type: text/markdown
17
17
 
18
18
  # Kling API Python SDK for RunAPI
19
19
 
20
20
  The Kling Python SDK is the language-specific package for Kling on RunAPI. Use this package for video generation, animation, and video editing workflows when your application needs request bodies, task status lookup, and consistent RunAPI errors in Python.
21
21
 
22
- This README is the Python package guide inside the public `kling-sdk` repository. For the repository overview, start at `../README.md`; for model details, use https://runapi.ai/models/kling; for API reference, use https://runapi.ai/docs#kling; for SDK docs, use https://runapi.ai/docs#sdk-kling.
22
+ This README is the Python package guide inside the public `kling-sdk` repository. For the repository overview, start at `../README.md`; for model details, use https://runapi.ai/models/kling; for API reference, use https://runapi.ai/docs/api/kling/text-to-video; for SDK docs, use https://runapi.ai/docs/resources/sdks.
23
23
 
24
24
  ## Install
25
25
 
@@ -48,6 +48,23 @@ avatar = client.ai_avatar.create(
48
48
  )
49
49
  ```
50
50
 
51
+ ## Kling O1 reference media
52
+
53
+ ```python
54
+ result = client.text_to_video.run(
55
+ model="kling-o1",
56
+ prompt="Keep <<<image_1>>> beside the performer from <<<video_1>>>",
57
+ reference_image_urls=["https://cdn.runapi.ai/public/samples/portrait.jpg"],
58
+ reference_video_url="https://cdn.runapi.ai/public/samples/video.mp4",
59
+ reference_video_type="feature",
60
+ preserve_reference_video_audio=True,
61
+ mode="pro",
62
+ duration_seconds=5,
63
+ )
64
+ ```
65
+
66
+ Number reference images in prompt order as `<<<image_1>>>`, `<<<image_2>>>`, and so on; the optional video is `<<<video_1>>>`. With a video, send at most four images. Do not combine `last_frame_image_url` with reference images or a reference video. A `feature` reference video may be used with the required first frame; `base` cannot be combined with frame inputs. O1 requests are five seconds and keep sound disabled. Pricing and limits: https://runapi.ai/models/kling/o1.
67
+
51
68
  Use `create` when you want to submit a task and return quickly, `get` when you need the latest task state, and `run` when a script should create and poll until completion:
52
69
 
53
70
  ```python
@@ -69,8 +86,8 @@ Pass parameters as keyword arguments and catch the `runapi.kling` error classes
69
86
  ## Links
70
87
 
71
88
  - Model page: https://runapi.ai/models/kling
72
- - SDK docs: https://runapi.ai/docs#sdk-kling
73
- - Product docs: https://runapi.ai/docs#kling
89
+ - SDK docs: https://runapi.ai/docs/resources/sdks
90
+ - Product docs: https://runapi.ai/docs/api/kling/text-to-video
74
91
  - Pricing and rate limits: https://runapi.ai/models/kling/3.0
75
92
  - Provider comparison: https://runapi.ai/providers/kuaishou
76
93
  - Full catalog: https://runapi.ai/models
@@ -2,7 +2,7 @@
2
2
 
3
3
  The Kling Python SDK is the language-specific package for Kling on RunAPI. Use this package for video generation, animation, and video editing workflows when your application needs request bodies, task status lookup, and consistent RunAPI errors in Python.
4
4
 
5
- This README is the Python package guide inside the public `kling-sdk` repository. For the repository overview, start at `../README.md`; for model details, use https://runapi.ai/models/kling; for API reference, use https://runapi.ai/docs#kling; for SDK docs, use https://runapi.ai/docs#sdk-kling.
5
+ This README is the Python package guide inside the public `kling-sdk` repository. For the repository overview, start at `../README.md`; for model details, use https://runapi.ai/models/kling; for API reference, use https://runapi.ai/docs/api/kling/text-to-video; for SDK docs, use https://runapi.ai/docs/resources/sdks.
6
6
 
7
7
  ## Install
8
8
 
@@ -31,6 +31,23 @@ avatar = client.ai_avatar.create(
31
31
  )
32
32
  ```
33
33
 
34
+ ## Kling O1 reference media
35
+
36
+ ```python
37
+ result = client.text_to_video.run(
38
+ model="kling-o1",
39
+ prompt="Keep <<<image_1>>> beside the performer from <<<video_1>>>",
40
+ reference_image_urls=["https://cdn.runapi.ai/public/samples/portrait.jpg"],
41
+ reference_video_url="https://cdn.runapi.ai/public/samples/video.mp4",
42
+ reference_video_type="feature",
43
+ preserve_reference_video_audio=True,
44
+ mode="pro",
45
+ duration_seconds=5,
46
+ )
47
+ ```
48
+
49
+ Number reference images in prompt order as `<<<image_1>>>`, `<<<image_2>>>`, and so on; the optional video is `<<<video_1>>>`. With a video, send at most four images. Do not combine `last_frame_image_url` with reference images or a reference video. A `feature` reference video may be used with the required first frame; `base` cannot be combined with frame inputs. O1 requests are five seconds and keep sound disabled. Pricing and limits: https://runapi.ai/models/kling/o1.
50
+
34
51
  Use `create` when you want to submit a task and return quickly, `get` when you need the latest task state, and `run` when a script should create and poll until completion:
35
52
 
36
53
  ```python
@@ -52,8 +69,8 @@ Pass parameters as keyword arguments and catch the `runapi.kling` error classes
52
69
  ## Links
53
70
 
54
71
  - Model page: https://runapi.ai/models/kling
55
- - SDK docs: https://runapi.ai/docs#sdk-kling
56
- - Product docs: https://runapi.ai/docs#kling
72
+ - SDK docs: https://runapi.ai/docs/resources/sdks
73
+ - Product docs: https://runapi.ai/docs/api/kling/text-to-video
57
74
  - Pricing and rate limits: https://runapi.ai/models/kling/3.0
58
75
  - Provider comparison: https://runapi.ai/providers/kuaishou
59
76
  - Full catalog: https://runapi.ai/models
@@ -4,22 +4,22 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "runapi-kling"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "RunAPI Kling SDK for text-to-video, image-to-video, avatar, and motion-control workflows in JavaScript, Python, Ruby, Go, Java, and PHP"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
11
11
  license = "Apache-2.0"
12
12
  authors = [{ name = "RunAPI", email = "contact@runapi.ai" }]
13
13
  keywords = ["runapi", "runapi-ai", "kling", "api", "sdk", "typescript", "python", "ruby", "golang", "java", "maven", "gradle", "video-generation", "text-to-video", "video-api", "kling-ai-api"]
14
- dependencies = ["runapi-core>=0.3.0"]
14
+ dependencies = ["runapi-core>=0.3.3"]
15
15
 
16
16
  [project.urls]
17
17
  Homepage = "https://runapi.ai/models/kling"
18
- Documentation = "https://runapi.ai/docs#sdk-kling"
18
+ Documentation = "https://runapi.ai/docs/resources/sdks"
19
19
  Source = "https://github.com/runapi-ai/kling-sdk"
20
20
  Issues = "https://github.com/runapi-ai/kling-sdk/issues"
21
21
  Changelog = "https://github.com/runapi-ai/kling-sdk/blob/main/CHANGELOG.md"
22
- "Release Notes" = "https://github.com/runapi-ai/kling-sdk/releases/tag/python%2Fv0.2.0"
22
+ "Release Notes" = "https://github.com/runapi-ai/kling-sdk/releases/tag/python%2Fv0.2.2"
23
23
 
24
24
  [tool.hatch.build.targets.wheel]
25
25
  packages = ["src/runapi"]
@@ -82,8 +82,39 @@ CONTRACT = {
82
82
  }
83
83
  },
84
84
  "image-to-video": {
85
- "models": ["kling-v2.1-master-image-to-video", "kling-v2.1-pro", "kling-v2.1-standard", "kling-v2.5-turbo-image-to-video-pro", "kling-v2.6", "kling-v3-omni", "kling-v3-turbo-image-to-video"],
85
+ "models": ["kling-o1", "kling-v2.1-master-image-to-video", "kling-v2.1-pro", "kling-v2.1-standard", "kling-v2.5-turbo-image-to-video-pro", "kling-v2.6", "kling-v3-omni", "kling-v3-turbo-image-to-video"],
86
86
  "fields_by_model": {
87
+ "kling-o1": {
88
+ "aspect_ratio": {
89
+ "enum": ["16:9", "9:16", "1:1"]
90
+ },
91
+ "duration_seconds": {
92
+ "enum": [5],
93
+ "type": "integer"
94
+ },
95
+ "first_frame_image_url": {
96
+ "required": True
97
+ },
98
+ "mode": {
99
+ "enum": ["std", "pro"]
100
+ },
101
+ "model": {
102
+ "required": True
103
+ },
104
+ "prompt": {
105
+ "required": True,
106
+ "min": 1,
107
+ "max": 2500,
108
+ "length": True
109
+ },
110
+ "reference_image_urls": {
111
+ "min_items": 1,
112
+ "max_items": 7
113
+ },
114
+ "reference_video_type": {
115
+ "enum": ["base", "feature"]
116
+ }
117
+ },
87
118
  "kling-v2.1-master-image-to-video": {
88
119
  "duration_seconds": {
89
120
  "enum": [5, 10],
@@ -215,40 +246,45 @@ CONTRACT = {
215
246
  }
216
247
  },
217
248
  "rules": [{
249
+ "when": {
250
+ "model": "kling-o1"
251
+ },
252
+ "forbidden": ["output_resolution", "negative_prompt", "cfg_scale"]
253
+ }, {
218
254
  "when": {
219
255
  "model": "kling-v2.1-master-image-to-video"
220
256
  },
221
- "forbidden": ["output_resolution", "enable_sound"]
257
+ "forbidden": ["output_resolution", "enable_sound", "reference_image_urls", "reference_video_url", "reference_video_type", "preserve_reference_video_audio"]
222
258
  }, {
223
259
  "when": {
224
260
  "model": "kling-v2.1-pro"
225
261
  },
226
- "forbidden": ["output_resolution", "enable_sound"]
262
+ "forbidden": ["output_resolution", "enable_sound", "reference_image_urls", "reference_video_url", "reference_video_type", "preserve_reference_video_audio"]
227
263
  }, {
228
264
  "when": {
229
265
  "model": "kling-v2.1-standard"
230
266
  },
231
- "forbidden": ["output_resolution", "enable_sound"]
267
+ "forbidden": ["output_resolution", "enable_sound", "reference_image_urls", "reference_video_url", "reference_video_type", "preserve_reference_video_audio"]
232
268
  }, {
233
269
  "when": {
234
270
  "model": "kling-v2.5-turbo-image-to-video-pro"
235
271
  },
236
- "forbidden": ["output_resolution", "enable_sound"]
272
+ "forbidden": ["output_resolution", "enable_sound", "reference_image_urls", "reference_video_url", "reference_video_type", "preserve_reference_video_audio"]
237
273
  }, {
238
274
  "when": {
239
275
  "model": "kling-v2.6"
240
276
  },
241
- "forbidden": ["output_resolution", "negative_prompt", "cfg_scale"]
277
+ "forbidden": ["output_resolution", "negative_prompt", "cfg_scale", "reference_image_urls", "reference_video_url", "reference_video_type", "preserve_reference_video_audio"]
242
278
  }, {
243
279
  "when": {
244
280
  "model": "kling-v3-omni"
245
281
  },
246
- "forbidden": ["negative_prompt", "cfg_scale"]
282
+ "forbidden": ["negative_prompt", "cfg_scale", "reference_image_urls", "reference_video_url", "reference_video_type", "preserve_reference_video_audio"]
247
283
  }, {
248
284
  "when": {
249
285
  "model": "kling-v3-turbo-image-to-video"
250
286
  },
251
- "forbidden": ["enable_sound", "aspect_ratio", "negative_prompt", "cfg_scale", "last_frame_image_url"]
287
+ "forbidden": ["enable_sound", "aspect_ratio", "negative_prompt", "cfg_scale", "last_frame_image_url", "reference_image_urls", "reference_video_url", "reference_video_type", "preserve_reference_video_audio"]
252
288
  }]
253
289
  },
254
290
  "motion-control": {
@@ -306,7 +342,7 @@ CONTRACT = {
306
342
  }]
307
343
  },
308
344
  "text-to-video": {
309
- "models": ["kling-3.0", "kling-v2.1-master-text-to-video", "kling-v2.5-turbo-text-to-video-pro", "kling-v2.6", "kling-v3-omni", "kling-v3-turbo-text-to-video"],
345
+ "models": ["kling-3.0", "kling-o1", "kling-v2.1-master-text-to-video", "kling-v2.5-turbo-text-to-video-pro", "kling-v2.6", "kling-v3-omni", "kling-v3-turbo-text-to-video"],
310
346
  "fields_by_model": {
311
347
  "kling-3.0": {
312
348
  "aspect_ratio": {
@@ -323,6 +359,34 @@ CONTRACT = {
323
359
  "enum": ["720p", "1080p", "4k"]
324
360
  }
325
361
  },
362
+ "kling-o1": {
363
+ "aspect_ratio": {
364
+ "enum": ["16:9", "9:16", "1:1"]
365
+ },
366
+ "duration_seconds": {
367
+ "enum": [5],
368
+ "type": "integer"
369
+ },
370
+ "mode": {
371
+ "enum": ["std", "pro"]
372
+ },
373
+ "model": {
374
+ "required": True
375
+ },
376
+ "prompt": {
377
+ "required": True,
378
+ "min": 1,
379
+ "max": 2500,
380
+ "length": True
381
+ },
382
+ "reference_image_urls": {
383
+ "min_items": 1,
384
+ "max_items": 7
385
+ },
386
+ "reference_video_type": {
387
+ "enum": ["base", "feature"]
388
+ }
389
+ },
326
390
  "kling-v2.1-master-text-to-video": {
327
391
  "aspect_ratio": {
328
392
  "enum": ["16:9", "9:16", "1:1"]
@@ -412,25 +476,40 @@ CONTRACT = {
412
476
  }
413
477
  },
414
478
  "rules": [{
479
+ "when": {
480
+ "model": "kling-3.0"
481
+ },
482
+ "forbidden": ["reference_image_urls", "reference_video_url", "reference_video_type", "preserve_reference_video_audio"]
483
+ }, {
484
+ "when": {
485
+ "model": "kling-o1"
486
+ },
487
+ "forbidden": ["output_resolution", "negative_prompt", "cfg_scale", "multi_shots", "multi_prompt", "first_frame_image_url", "last_frame_image_url", "kling_elements"]
488
+ }, {
489
+ "when": {
490
+ "model": "kling-v2.1-master-text-to-video"
491
+ },
492
+ "forbidden": ["mode", "reference_image_urls", "reference_video_url", "reference_video_type", "preserve_reference_video_audio"]
493
+ }, {
415
494
  "when": {
416
495
  "model": "kling-v2.5-turbo-text-to-video-pro"
417
496
  },
418
- "forbidden": ["mode"]
497
+ "forbidden": ["mode", "reference_image_urls", "reference_video_url", "reference_video_type", "preserve_reference_video_audio"]
419
498
  }, {
420
499
  "when": {
421
500
  "model": "kling-v2.6"
422
501
  },
423
- "forbidden": ["output_resolution", "negative_prompt", "cfg_scale", "multi_shots", "multi_prompt", "first_frame_image_url", "last_frame_image_url", "kling_elements"]
502
+ "forbidden": ["output_resolution", "negative_prompt", "cfg_scale", "multi_shots", "multi_prompt", "first_frame_image_url", "last_frame_image_url", "kling_elements", "reference_image_urls", "reference_video_url", "reference_video_type", "preserve_reference_video_audio"]
424
503
  }, {
425
504
  "when": {
426
505
  "model": "kling-v3-omni"
427
506
  },
428
- "forbidden": ["negative_prompt", "cfg_scale", "multi_shots", "multi_prompt", "first_frame_image_url", "last_frame_image_url", "kling_elements"]
507
+ "forbidden": ["negative_prompt", "cfg_scale", "multi_shots", "multi_prompt", "first_frame_image_url", "last_frame_image_url", "kling_elements", "reference_image_urls", "reference_video_url", "reference_video_type", "preserve_reference_video_audio"]
429
508
  }, {
430
509
  "when": {
431
510
  "model": "kling-v3-turbo-text-to-video"
432
511
  },
433
- "forbidden": ["enable_sound", "negative_prompt", "cfg_scale", "multi_shots", "multi_prompt", "first_frame_image_url", "last_frame_image_url", "kling_elements"]
512
+ "forbidden": ["enable_sound", "negative_prompt", "cfg_scale", "multi_shots", "multi_prompt", "first_frame_image_url", "last_frame_image_url", "kling_elements", "reference_image_urls", "reference_video_url", "reference_video_type", "preserve_reference_video_audio"]
434
513
  }]
435
514
  }
436
515
  }
@@ -11,6 +11,7 @@ from ..types import (
11
11
  CompletedImageToVideoResponse,
12
12
  ImageToVideoResponse,
13
13
  )
14
+ from .o1_reference_validation import O1_MODEL, validate_kling_o1_references
14
15
 
15
16
  V26_MODEL = "kling-v2.6"
16
17
  V3_OMNI_MODEL = "kling-v3-omni"
@@ -70,6 +71,7 @@ class ImageToVideo(Resource):
70
71
  def _validate_params(self, params: Dict[str, Any]) -> None:
71
72
  self._reject_unsupported_v3_turbo_fields(params)
72
73
  self._validate_contract(CONTRACT["image-to-video"], params)
74
+ validate_kling_o1_references(params)
73
75
 
74
76
  # Bespoke last-frame rules that the generated contract cannot express.
75
77
  model = params.get("model")
@@ -82,6 +84,7 @@ class ImageToVideo(Resource):
82
84
  f"last_frame_image_url requires duration_seconds 5 for {V3_OMNI_MODEL}"
83
85
  )
84
86
  elif last_frame_image_url and model not in (
87
+ O1_MODEL,
85
88
  "kling-v2.5-turbo-image-to-video-pro",
86
89
  "kling-v2.1-pro",
87
90
  ):
@@ -0,0 +1,178 @@
1
+ """Cross-field validation shared by Kling O1 video resources."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ipaddress
6
+ import re
7
+ from pathlib import PurePosixPath
8
+ from typing import Any, Dict
9
+ from urllib.parse import urlparse
10
+
11
+ from runapi.core import ValidationError
12
+
13
+ O1_MODEL = "kling-o1"
14
+ IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png"}
15
+ VIDEO_EXTENSIONS = {".mp4", ".mov"}
16
+ NONCANONICAL_IPV4_LITERAL = re.compile(
17
+ r"(?:0x[0-9a-f]+|\d+)(?:\.(?:0x[0-9a-f]+|\d+)){0,3}",
18
+ re.IGNORECASE,
19
+ )
20
+ BLOCKED_IP_NETWORKS = tuple(
21
+ ipaddress.ip_network(cidr)
22
+ for cidr in (
23
+ "0.0.0.0/8",
24
+ "10.0.0.0/8",
25
+ "100.64.0.0/10",
26
+ "127.0.0.0/8",
27
+ "169.254.0.0/16",
28
+ "172.16.0.0/12",
29
+ "192.0.0.0/24",
30
+ "192.0.2.0/24",
31
+ "192.88.99.0/24",
32
+ "192.168.0.0/16",
33
+ "198.18.0.0/15",
34
+ "198.51.100.0/24",
35
+ "203.0.113.0/24",
36
+ "224.0.0.0/4",
37
+ "240.0.0.0/4",
38
+ "255.255.255.255/32",
39
+ "::/128",
40
+ "::1/128",
41
+ "64:ff9b::/96",
42
+ "64:ff9b:1::/48",
43
+ "100::/64",
44
+ "2001::/32",
45
+ "2001:2::/48",
46
+ "2001:db8::/32",
47
+ "2002::/16",
48
+ "3fff::/20",
49
+ "fc00::/7",
50
+ "fe80::/10",
51
+ "ff00::/8",
52
+ )
53
+ )
54
+
55
+
56
+ def validate_kling_o1_references(params: Dict[str, Any]) -> None:
57
+ if params.get("model") != O1_MODEL:
58
+ return
59
+ if params.get("enable_sound") is True:
60
+ raise ValidationError(f"enable_sound is not supported by {O1_MODEL}")
61
+
62
+ for field in ("first_frame_image_url", "last_frame_image_url"):
63
+ value = params.get(field)
64
+ if value and not _is_public_http_url(value):
65
+ raise ValidationError(f"{field} must be a public HTTP or HTTPS URL")
66
+ if value and _url_extension(value) not in IMAGE_EXTENSIONS:
67
+ raise ValidationError(f"{field} must use a JPG, JPEG, or PNG URL")
68
+
69
+ prompt = params.get("prompt")
70
+ if not isinstance(prompt, str):
71
+ raise ValidationError("prompt must be a string")
72
+ reference_images = params.get("reference_image_urls") or []
73
+ reference_video_url = params.get("reference_video_url")
74
+ if reference_video_url is not None and not isinstance(reference_video_url, str):
75
+ raise ValidationError("reference_video_url must be a string")
76
+ if params.get("last_frame_image_url") and (
77
+ reference_images or reference_video_url
78
+ ):
79
+ raise ValidationError(
80
+ "last_frame_image_url cannot be combined with "
81
+ "reference_image_urls or reference_video_url"
82
+ )
83
+ if reference_video_url and len(reference_images) > 4:
84
+ raise ValidationError(
85
+ "reference_image_urls must contain at most 4 items when "
86
+ "reference_video_url is present"
87
+ )
88
+
89
+ for index, image_url in enumerate(reference_images):
90
+ if not isinstance(image_url, str):
91
+ raise ValidationError(
92
+ f"reference_image_urls[{index}] must be a string"
93
+ )
94
+ if not _is_public_http_url(image_url):
95
+ raise ValidationError(
96
+ f"reference_image_urls[{index}] must be a public HTTP or HTTPS URL"
97
+ )
98
+ if _url_extension(image_url) not in IMAGE_EXTENSIONS:
99
+ raise ValidationError(
100
+ f"reference_image_urls[{index}] must use a JPG, JPEG, or PNG URL"
101
+ )
102
+ marker = f"<<<image_{index + 1}>>>"
103
+ if marker not in prompt:
104
+ raise ValidationError(
105
+ f"prompt must reference reference_image_urls[{index}] as {marker}"
106
+ )
107
+
108
+ for marker_index in re.findall(r"<<<image_(\d+)>>>", prompt):
109
+ index = int(marker_index)
110
+ if index < 1 or index > len(reference_images):
111
+ raise ValidationError(f"prompt references missing image_{index}")
112
+
113
+ if not reference_video_url:
114
+ if "reference_video_type" in params:
115
+ raise ValidationError("reference_video_type requires reference_video_url")
116
+ if "preserve_reference_video_audio" in params:
117
+ raise ValidationError(
118
+ "preserve_reference_video_audio requires reference_video_url"
119
+ )
120
+ missing_marker = re.search(r"<<<video_([^>]+)>>>", prompt)
121
+ if missing_marker:
122
+ raise ValidationError(
123
+ f"prompt references missing video_{missing_marker.group(1)}"
124
+ )
125
+ return
126
+
127
+ if not _is_public_http_url(reference_video_url):
128
+ raise ValidationError(
129
+ "reference_video_url must be a public HTTP or HTTPS URL"
130
+ )
131
+ if _url_extension(reference_video_url) not in VIDEO_EXTENSIONS:
132
+ raise ValidationError("reference_video_url must use an MP4 or MOV URL")
133
+ if "<<<video_1>>>" not in prompt:
134
+ raise ValidationError(
135
+ "prompt must reference reference_video_url as <<<video_1>>>"
136
+ )
137
+ if any(index != "1" for index in re.findall(r"<<<video_([^>]+)>>>", prompt)):
138
+ raise ValidationError("prompt may only reference video_1")
139
+
140
+ reference_video_type = params.get("reference_video_type") or "base"
141
+ if reference_video_type == "base" and (
142
+ params.get("first_frame_image_url") or params.get("last_frame_image_url")
143
+ ):
144
+ raise ValidationError(
145
+ "reference_video_type base cannot be combined with "
146
+ "first_frame_image_url or last_frame_image_url"
147
+ )
148
+
149
+
150
+ def _url_extension(value: str) -> str:
151
+ return PurePosixPath(urlparse(str(value)).path).suffix.lower()
152
+
153
+
154
+ def _is_public_http_url(value: str) -> bool:
155
+ try:
156
+ parsed = urlparse(value)
157
+ return (
158
+ parsed.scheme.lower() in {"http", "https"}
159
+ and bool(parsed.hostname)
160
+ and parsed.username is None
161
+ and parsed.password is None
162
+ and not _is_blocked_host(parsed.hostname)
163
+ )
164
+ except (TypeError, ValueError):
165
+ return False
166
+
167
+
168
+ def _is_blocked_host(hostname: str) -> bool:
169
+ host = hostname.lower().rstrip(".")
170
+ if host == "localhost" or host.endswith(".localhost"):
171
+ return True
172
+ try:
173
+ address = ipaddress.ip_address(host)
174
+ except ValueError:
175
+ return NONCANONICAL_IPV4_LITERAL.fullmatch(host) is not None
176
+ if isinstance(address, ipaddress.IPv6Address) and address.ipv4_mapped:
177
+ address = address.ipv4_mapped
178
+ return any(address in network for network in BLOCKED_IP_NETWORKS)
@@ -13,6 +13,7 @@ from ..types import (
13
13
  CompletedTextToVideoResponse,
14
14
  TextToVideoResponse,
15
15
  )
16
+ from .o1_reference_validation import validate_kling_o1_references
16
17
 
17
18
  V26_MODEL = "kling-v2.6"
18
19
  V3_TURBO_MODEL = "kling-v3-turbo-text-to-video"
@@ -76,6 +77,7 @@ class TextToVideo(Resource):
76
77
  self._reject_unsupported_v3_turbo_fields(params)
77
78
  self._validate_contract(CONTRACT["text-to-video"], params)
78
79
  self._validate_v26_params(params)
80
+ validate_kling_o1_references(params)
79
81
 
80
82
  # Bespoke cross-field rules the contract cannot express.
81
83
  multi_shots = params.get("multi_shots") is True
@@ -410,6 +410,57 @@ def test_text_to_video_v26_rejects_sound_outside_pro_mode():
410
410
  assert fake.calls == []
411
411
 
412
412
 
413
+ def test_text_to_video_o1_posts_reference_fields():
414
+ fake = FakeHttp({"id": "t1", "status": "pending"})
415
+ client = KlingClient(api_key="k", http_client=fake)
416
+
417
+ client.text_to_video.create(
418
+ model="kling-o1",
419
+ prompt="Keep <<<image_1>>> beside <<<video_1>>>",
420
+ reference_image_urls=["https://cdn.runapi.ai/public/samples/portrait.jpg"],
421
+ reference_video_url="https://cdn.runapi.ai/public/samples/video.mp4",
422
+ reference_video_type="feature",
423
+ preserve_reference_video_audio=True,
424
+ duration_seconds=5,
425
+ )
426
+
427
+ assert fake.calls == [
428
+ (
429
+ "post",
430
+ "/api/v1/kling/text_to_video",
431
+ {
432
+ "model": "kling-o1",
433
+ "prompt": "Keep <<<image_1>>> beside <<<video_1>>>",
434
+ "reference_image_urls": [
435
+ "https://cdn.runapi.ai/public/samples/portrait.jpg"
436
+ ],
437
+ "reference_video_url": "https://cdn.runapi.ai/public/samples/video.mp4",
438
+ "reference_video_type": "feature",
439
+ "preserve_reference_video_audio": True,
440
+ "duration_seconds": 5,
441
+ },
442
+ )
443
+ ]
444
+
445
+
446
+ def test_text_to_video_o1_rejects_reference_without_prompt_marker():
447
+ fake = FakeHttp()
448
+ client = KlingClient(api_key="k", http_client=fake)
449
+
450
+ with pytest.raises(
451
+ ValidationError,
452
+ match=r"prompt must reference reference_image_urls\[0\] as <<<image_1>>>",
453
+ ):
454
+ client.text_to_video.create(
455
+ model="kling-o1",
456
+ prompt="Keep the same subject",
457
+ reference_image_urls=[
458
+ "https://cdn.runapi.ai/public/samples/portrait.jpg"
459
+ ],
460
+ )
461
+ assert fake.calls == []
462
+
463
+
413
464
  # --- ai_avatar ------------------------------------------------------------
414
465
 
415
466
 
@@ -756,6 +807,83 @@ def test_image_to_video_v3_omni_rejects_final_frame_outside_five_seconds():
756
807
  last_frame_image_url="https://cdn.runapi.ai/public/samples/image.jpg",
757
808
  duration_seconds=7,
758
809
  )
810
+
811
+
812
+ def test_image_to_video_o1_rejects_base_video_with_frame_input():
813
+ fake = FakeHttp()
814
+ client = KlingClient(api_key="k", http_client=fake)
815
+
816
+ with pytest.raises(
817
+ ValidationError,
818
+ match="reference_video_type base cannot be combined with first_frame_image_url or last_frame_image_url",
819
+ ):
820
+ client.image_to_video.create(
821
+ model="kling-o1",
822
+ prompt="Use <<<video_1>>> as the base",
823
+ first_frame_image_url="https://cdn.runapi.ai/public/samples/image-to-video.jpg",
824
+ reference_video_url="https://cdn.runapi.ai/public/samples/video.mp4",
825
+ reference_video_type="base",
826
+ )
827
+ assert fake.calls == []
828
+
829
+
830
+ def test_image_to_video_o1_rejects_tail_frame_with_reference_media():
831
+ fake = FakeHttp()
832
+ client = KlingClient(api_key="k", http_client=fake)
833
+
834
+ with pytest.raises(
835
+ ValidationError,
836
+ match="last_frame_image_url cannot be combined with reference_image_urls or reference_video_url",
837
+ ):
838
+ client.image_to_video.create(
839
+ model="kling-o1",
840
+ prompt="Move toward <<<image_1>>>",
841
+ first_frame_image_url="https://cdn.runapi.ai/public/samples/image-to-video.jpg",
842
+ last_frame_image_url="https://cdn.runapi.ai/public/samples/last-frame.jpg",
843
+ reference_image_urls=["https://cdn.runapi.ai/public/samples/portrait.jpg"],
844
+ )
845
+ assert fake.calls == []
846
+
847
+
848
+ def test_text_to_video_o1_rejects_missing_video_reference():
849
+ fake = FakeHttp()
850
+ client = KlingClient(api_key="k", http_client=fake)
851
+
852
+ with pytest.raises(ValidationError, match="prompt references missing video_1"):
853
+ client.text_to_video.create(
854
+ model="kling-o1",
855
+ prompt="Follow <<<video_1>>>",
856
+ )
857
+ assert fake.calls == []
858
+
859
+
860
+ @pytest.mark.parametrize(
861
+ "reference_url",
862
+ [
863
+ "file:///etc/passwd.jpg",
864
+ "http://localhost/reference.jpg",
865
+ "http://127.0.0.1/reference.jpg",
866
+ "http://169.254.169.254/reference.jpg",
867
+ "http://[::ffff:127.0.0.1]/reference.jpg",
868
+ "http://2130706433/reference.jpg",
869
+ "http://127.1/reference.jpg",
870
+ "http://0177.0.0.1/reference.jpg",
871
+ "http://0x7f000001/reference.jpg",
872
+ ],
873
+ )
874
+ def test_text_to_video_o1_rejects_non_public_reference_media(reference_url):
875
+ fake = FakeHttp()
876
+ client = KlingClient(api_key="k", http_client=fake)
877
+
878
+ with pytest.raises(
879
+ ValidationError,
880
+ match=r"reference_image_urls\[0\] must be a public HTTP or HTTPS URL",
881
+ ):
882
+ client.text_to_video.create(
883
+ model="kling-o1",
884
+ prompt="Use <<<image_1>>>",
885
+ reference_image_urls=[reference_url],
886
+ )
759
887
  assert fake.calls == []
760
888
 
761
889