Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Production SDK patterns for Kling AI: client wrapper, retry logic, async polling, and error handling. Use when building robust integrations. Trigger with phrases like 'klingai sdk', 'kling ai client', 'klingai patterns', 'kling ai wrapper'.
.claude/skills/jeremylongshore-klingai-sdk-patterns/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 93% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 56% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 77% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 43% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 57% | 0% |
Production-ready client patterns for the Kling AI API. Covers auto-refreshing JWT, typed request/response models, exponential backoff polling, async batch submission, and structured error handling.
pythonimport jwt import time import os import requests from dataclasses import dataclass, field from typing import Optional @dataclass class KlingConfig: access_key: str = field(default_factory=lambda: os.environ["KLING_ACCESS_KEY"]) secret_key: str = field(default_factory=lambda: os.environ["KLING_SECRET_KEY"]) base_url: str = "https://api.klingai.com/v1" token_buffer_sec: int = 300 poll_interval_sec: int = 10 max_poll_attempts: int = 120 # 20 minutes max timeout_sec: int = 30 class KlingClient: """Production Kling AI client with auto-refreshing JWT.""" def __init__(self, config: Optional[KlingConfig] = None): self.config = config or KlingConfig() self._token = None self._token_expires = 0 @property def _headers(self) -> dict: now = int(time.time()) if now >= (self._token_expires - self.config.token_buffer_sec): payload = {"iss": self.config.access_key, "exp": now + 1800, "nbf": now - 5} self._token = jwt.encode(payload, self.config.secret_key, algorithm="HS256", headers={"alg": "HS256", "typ": "JWT"}) self._token_expires = now + 1800 return {"Authorization": f"Bearer {self._token}", "Content-Type": "application/json"} def _post(self, path: str, body: dict) -> dict: r = requests.post(f"{self.config.base_url}{path}", headers=self._headers, json=body, timeout=self.config.timeout_sec) r.raise_for_status() return r.json() def _get(self, path: str) -> dict: r = requests.get(f"{self.config.base_url}{path}", headers=self._headers, timeout=self.config.timeout_sec) r.raise_for_status() return r.json() def _poll_task(self, endpoint: str, task_id: str) -> dict: """Poll with exponential backoff until task completes.""" interval = self.config.poll_interval_sec for attempt in range(self.config.max_poll_attempts): time.sleep(interval) result = self._get(f"{endpoint}/{task_id}") status = result["data"]["task_status"] if status == "succeed": return result["data"]["task_result"] elif status == "failed": raise KlingGenerationError(result["data"].get("task_status_msg", "Unknown")) # Increase interval up to 30s max interval = min(interval * 1.2, 30) raise KlingTimeoutError(f"Task {task_id} did not complete in time") # --- Public API --- def text_to_video(self, prompt: str, **kwargs) -> dict: body = {"model_name": kwargs.get("model", "kling-v2-master"), "prompt": prompt, "duration": str(kwargs.get("duration", 5)), "aspect_ratio": kwargs.get("aspect_ratio", "16:9"), "mode": kwargs.get("mode", "standard")} if kwargs.get("negative_prompt"): body["negative_prompt"] = kwargs["negative_prompt"] if kwargs.get("cfg_scale") is not None: body["cfg_scale"] = kwargs["cfg_scale"] if kwargs.get("callback_url"): body["callback_url"] = kwargs["callback_url"] task = self._post("/videos/text2video", body) task_id = task["data"]["task_id"] if kwargs.get("wait", True): return self._poll_task("/videos/text2video", task_id) return {"task_id": task_id} def image_to_video(self, image_url: str, **kwargs) -> dict: body = {"model_name": kwargs.get("model", "kling-v2-1"), "image": image_url, "duration": str(kwargs.get("duration", 5)), "mode": kwargs.get("mode", "standard")} if kwargs.get("prompt"): body["prompt"] = kwargs["prompt"] task = self._post("/videos/image2video", body) task_id = task["data"]["task_id"] if kwargs.get("wait", True): return self._poll_task("/videos/image2video", task_id) return {"task_id": task_id} def extend_video(self, task_id: str, **kwargs) -> dict: body = {"task_id": task_id, "prompt": kwargs.get("prompt", ""), "duration": str(kwargs.get("duration", 5)), "mode": kwargs.get("mode", "standard")} result = self._post("/videos/video-extend", body) new_task_id = result["data"]["task_id"] if kwargs.get("wait", True): return self._poll_task("/videos/video-extend", new_task_id) return {"task_id": new_task_id} class KlingError(Exception): pass class KlingGenerationError(KlingError): pass class KlingTimeoutError(KlingError): pass
pythonclient = KlingClient() # Synchronous (waits for result) result = client.text_to_video( "A cat playing piano in a jazz club", model="kling-v2-6", mode="professional", duration=5, ) print(result["videos"][0]["url"]) # Fire-and-forget (returns task_id) task = client.text_to_video("Ocean waves at sunset", wait=False) print(f"Submitted: {task['task_id']}")
javascriptimport jwt from "jsonwebtoken"; class KlingClient { #token = null; #tokenExp = 0; constructor(ak = process.env.KLING_ACCESS_KEY, sk = process.env.KLING_SECRET_KEY) { this.ak = ak; this.sk = sk; this.base = "https://api.klingai.com/v1"; } #getHeaders() { const now = Math.floor(Date.now() / 1000); if (now >= this.#tokenExp - 300) { this.#token = jwt.sign( { iss: this.ak, exp: now + 1800, nbf: now - 5 }, this.sk, { algorithm: "HS256", header: { typ: "JWT" } } ); this.#tokenExp = now + 1800; } return { Authorization: `Bearer ${this.#token}`, "Content-Type": "application/json" }; } async textToVideo(prompt, opts = {}) { const res = await fetch(`${this.base}/videos/text2video`, { method: "POST", headers: this.#getHeaders(), body: JSON.stringify({ model_name: opts.model ?? "kling-v2-master", prompt, duration: String(opts.duration ?? 5), aspect_ratio: opts.aspectRatio ?? "16:9", mode: opts.mode ?? "standard", }), }); const { data } = await res.json(); return opts.wait === false ? data : this.#poll("/videos/text2video", data.task_id); } async #poll(endpoint, taskId, interval = 10000) { for (let i = 0; i < 120; i++) { await new Promise((r) => setTimeout(r, interval)); const res = await fetch(`${this.base}${endpoint}/${taskId}`, { headers: this.#getHeaders(), }); const { data } = await res.json(); if (data.task_status === "succeed") return data.task_result; if (data.task_status === "failed") throw new Error(data.task_status_msg); interval = Math.min(interval * 1.2, 30000); } throw new Error(`Timeout: task ${taskId}`); } }
pythonimport functools def retry_on_transient(max_retries=3, backoff_base=2): """Retry on 429 (rate limit) and 5xx (server) errors.""" def decorator(fn): @functools.wraps(fn) def wrapper(*args, **kwargs): for attempt in range(max_retries + 1): try: return fn(*args, **kwargs) except requests.HTTPError as e: if e.response.status_code in (429, 500, 502, 503) and attempt < max_retries: wait = backoff_base ** attempt time.sleep(wait) continue raise return wrapper return decorator # Apply to client methods KlingClient._post = retry_on_transient()(KlingClient._post)
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-08 | fail→fail | 19,786 | 13,542 | -32% | 1 | 1 | 0% | 2,572 | 5,044 | +96% | 0 | 0 | — |
case-01 | fail→fail | 26,165 | 32,870 | +26% | 1 | 1 | 0% | 5,728 | 8,665 | +51% | 0 | 0 | — |
case-02 | fail→pass | 56,825 | 21,872 | -62% | 1 | 1 | 0% | 3,887 | 7,483 | +93% | 0 | 0 | — |
case-03 | fail→pass | 59,036 | 28,144 | -52% | 1 | 1 | 0% | 4,995 | 7,774 | +56% | 0 | 0 | — |
case-04 | pass→pass | 20,248 | 11,167 | -45% | 1 | 1 | 0% | 2,988 | 4,763 | +59% | 0 | 0 | — |
case-05 | pass→pass | 13,219 | 21,156 | +60% | 1 | 1 | 0% | 2,594 | 5,576 | +115% | 0 | 0 | — |
case-06 | pass→pass | 14,746 | 22,709 | +54% | 1 | 1 | 0% | 2,165 | 6,287 | +190% | 0 | 0 | — |
case-07 | pass→pass | 14,211 | 15,439 | +9% | 1 | 1 | 0% | 2,719 | 4,626 | +70% | 0 | 0 | — |
case-09 | fail→fail | 19,413 | 10,546 | -46% | 1 | 1 | 0% | 2,206 | 4,470 | +103% | 0 | 0 | — |
case-10 | fail→pass | 10,283 | 9,507 | -8% | 1 | 1 | 0% | 1,887 | 3,343 | +77% | 0 | 0 | — |
case-11 | fail→pass | 26,729 | 16,338 | -39% | 1 | 1 | 0% | 3,368 | 4,822 | +43% | 0 | 0 | — |
case-12 | fail→fail | 20,962 | 15,093 | -28% | 1 | 1 | 0% | 2,909 | 4,403 | +51% | 0 | 0 | — |
case-13 | pass→pass | 17,313 | 15,408 | -11% | 1 | 1 | 0% | 2,372 | 4,349 | +83% | 0 | 0 | — |
case-14 | pass→pass | 7,558 | 10,013 | +32% | 1 | 1 | 0% | 1,438 | 3,450 | +140% | 0 | 0 | — |
case-15 | fail→pass | 18,554 | 6,419 | -65% | 1 | 1 | 0% | 2,406 | 3,779 | +57% | 0 | 0 | — |
case-16 | pass→pass | 19,141 | 11,807 | -38% | 1 | 1 | 0% | 2,583 | 4,299 | +66% | 0 | 0 | — |
case-17 | fail→pass | 15,056 | 4,323 | -71% | 1 | 1 | 0% | 2,565 | 3,288 | +28% | 0 | 0 | — |
case-18 | pass→pass | 23,052 | 25,856 | +12% | 1 | 1 | 0% | 3,548 | 5,730 | +61% | 0 | 0 | — |
case-19 | fail→pass | 18,306 | 4,420 | -76% | 1 | 1 | 0% | 2,073 | 3,324 | +60% | 0 | 0 | — |
case-20 | fail→pass | 15,839 | 15,265 | -4% | 1 | 1 | 0% | 2,961 | 4,322 | +46% | 0 | 0 | — |
case-21 | fail→pass | 10,140 | 1,971 | -81% | 1 | 1 | 0% | 906 | 2,767 | +205% | 0 | 0 | — |
case-22 | fail→fail | 19,230 | 13,107 | -32% | 1 | 1 | 0% | 2,628 | 4,964 | +89% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +41 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.