Repository navigation
feat(ai): add Kling video generation - #63684
rekram1-node wants to merge 2 commits into
Conversation
9dd4b2c to
41b3438
Compare
There was a problem hiding this comment.
Looks sound. Tests pass (62 pass, 1 skip: the unrecorded live test, which skips without KLINGAI_API_KEY). tsc --noEmit is clean on packages/ai and packages/core. I couldn't check anything against the live Kling API.
Public API: additions only, backwards compatible. New package exports @opencode/ai/providers/klingai, @opencode/ai/providers/klingai/videos and @opencode/ai/protocols/klingai-video; a KlingAI export from providers; the KLINGAI_API_KEY environment variable; and @opencode/ai/providers/klingai/videos registered in core's built-in packages.
One small inline note: providerOptions.contents is dropped silently on an explicit text-to-video endpoint.
| `${route.name} ${request.model.id} has no reference input; use a model with an Omni endpoint`, | ||
| ) | ||
| if (endpoint === "text-to-video" && (request.start !== undefined || request.end !== undefined)) | ||
| return yield* route.unsupported("video.frames", `${route.name} text-to-video has no start or end frame input`) |
There was a problem hiding this comment.
With providerOptions.endpoint: "text-to-video", any providerOptions.contents is dropped silently: extra is only used in the contents branch below. Frames get a clear unsupported error on this endpoint, so it might be worth rejecting contents here the same way (or sending them through) instead of losing them without a word.
There was a problem hiding this comment.
Fixed in the latest push: providerOptions.contents on an explicit text-to-video endpoint now fails as unsupported, like frames do.
There was a problem hiding this comment.
I checked the fix in ae8591c and it works. fromRequest now rejects providerOptions.contents on the text-to-video endpoint as unsupported("video.contents", …). That check runs right after the frames check and before anything is built, so the contents can no longer be dropped without an error.
- Explicit endpoint: your new table case (
providerOptions: { endpoint: "text-to-video", contents: [...] }→ "no contents input") covers it, and nothing is sent to Kling. - Inferred endpoint: this case was already fine. When
contentsis set and no endpoint is given,endpointForroutes toimage-to-video, so text-to-video only getscontentswhen the endpoint is set explicitly.
I ran bun test ./test/klingai-video.test.ts against pr/head in a temporary worktree and all 19 tests passed. I removed the worktree afterwards. The shared repo/ checkout is still on the older commit 41b3438, and I didn't touch it or its existing bun.lock change.
There was a problem hiding this comment.
The earlier point is fixed: providerOptions.contents on an explicit text-to-video endpoint is now rejected before anything is sent, instead of being dropped silently. Nothing new to raise. bun test test/klingai-video.test.ts passes and tsc --noEmit is clean on packages/ai; nothing has run against the live Kling API. This push leaves the public API unchanged; the additions noted in the earlier review still apply.
Issue for this PR
None. This is independent of the video tool in #63666. Like #63680 and #63681, it adds the shared
videoscases tomedia-entrypoints.test.tsandvideosto the package regex invariant.test.ts, so whichever lands second has a trivial conflict.Type of change
What does this PR do?
I added Kling video generation to
@opencode/ai: aklingai-videoprotocol, a minimalKlingAIfacade with.video(id)andvideoModel, the@opencode/ai/providers/klingai/videosentrypoint, and its registration in core's built-in packages, so models.dev's Kling video models can load it (anomalyco/models.dev#13545).Kling has one endpoint per mode and model. The protocol picks it from the inputs:
models.dev lists some models under Kling's older
model_namevalues, so a small table maps them to path segments (kling-v2-6tokling-2.6,kling-v2-5-turbotokling-2.5-turbo,kling-video-o1tokling-o1). Unknown IDs are used as the segment as-is. Every model, including 2.6 and 2.5 Turbo, has these bearer-key endpoints in the current docs, so I didn't implement the legacy/v1/videos/*routes, which need an AK/SK-signed JWT.promptprompt(text-to-video) orcontents[{ type: "prompt", text }]start/endcontentsfirst_frame/last_frame(first frame first, last frame last)referencescontentsrefer_imagewith idsimage_1,image_2, ...referencescontentsfeature_videowith idsvideo_1, ... (URL only)durationsettings.duration(seconds)aspectsettings.aspect_ratio;autoisadaptive, left out on image-to-video where the frame sets the shaperesolutionsettings.resolutionsoundsettings.audio:native/offInline images go as bare base64, as Kling asks.
providerOptions.endpointoverrides the endpoint,providerOptions.contentsadds native items (Elements, voices,base_video,middle_frame) before the last frame, andproviderOptions.settings/optionsmerge into the native objects (settings.audio: "original",settings.multi_shot,options.callback_url,options.external_task_id,options.watermark_info).Rejected locally, because the protocol has no field for them: reference audio (Kling takes voices only as
voice_id), inline reference videos (URL only), references on models without an Omni endpoint (kling-3.0,kling-3.0-turbo, Kling 2.x), and frames orproviderOptions.contentson an explicit text-to-video endpoint. Everything else, such as durations or aspect ratios a model doesn't list, goes to Kling and its error comes back typed.Status and result read
GET /tasks?task_ids=<id>.submittedis queued,processingrunning,succeeded(and the legacysucceed) completed,failedfailed, and anything else stays running. The token is{ taskID, createdAt }. Kling keeps results for 30 days, so video URLs getexpiresAt30 days after the task'screate_time. A task missing from the response after that window reads asexpired; before it, it's an invalid-output error. Failed tasks carry Kling'smessageasProviderInternal. Kling documents no failure codes on tasks and no exact moderation wording (only thatmessagedescribes the reason, "such as triggering the content risk control"), so I don't map any text toContentPolicy. A submit that Kling's content policy blocks (error code 1301, HTTP 400) comes back as Kling's HTTP error. Results carrydurationSeconds,usagefromusage.output_duration(Kling 4.0), and the task id, external id and billing inproviderMetadata. Kling has no cancel endpoint, socancelfails withUnsupportedOperationwithout a request.No new flag: this only adds a library protocol and entrypoint; nothing in opencode loads video models until the video tool lands.
Sources: https://kling.ai/document-api/api/video/4-0/video-omni, https://kling.ai/document-api/api/video/3-0-omni/video-omni, https://kling.ai/document-api/api/video/o1/video-omni, https://kling.ai/document-api/api/video/2-6/text-to-video, https://kling.ai/document-api/api/get-started/authentication, https://kling.ai/document-api/api/get-started/error-codes
How did you verify your code works?
packages/ai/test/klingai-video.test.ts: body mapping for every shared field, endpoint and segment routing, the rejected inputs (no network call), errors returned typed, token JSON round trip, status table with unknown statuses, result decoding (URL, expiry, duration, usage, metadata), failed, pending, empty and expired tasks, and cancel.klingaicase inmedia-entrypoints.test.tsandexports.test.ts.klingai-video.recorded.test.tsuseskling-v2-5-turbo, 5 seconds at 720p. I had noKLINGAI_API_KEY, so it isn't recorded and skips without a key. Nothing here ran against the live API.bun typecheckandbun testinpackages/ai,bun typecheckandtest/variant.test.tsinpackages/core,bun run checkat the root.Checklist
Added by the opencode bot (AI-generated).
Public API: adds the @opencode/ai/providers/klingai(/videos) and protocols/klingai-video entrypoints and KLINGAI_API_KEY (backwards compatible)