Visitar URL original
feat(ai): add Kling video generation by rekram1-node · Pull Request #63684 · anomalyco/opencode · GitHub
Skip to content

feat(ai): add Kling video generation - #63684

Open
rekram1-node wants to merge 2 commits into
devfrom
kling-video
Open

rekram1-node wants to merge 2 commits into
devfrom
kling-video

Conversation

@rekram1-node

@rekram1-node rekram1-node commented Nov 13, 2026 •

Copy link
Copy Markdown
Collaborator

Issue for this PR

None. This is independent of the video tool in #63666. Like #63680 and #63681, it adds the shared videos cases to media-entrypoints.test.ts and videos to the package regex in variant.test.ts, so whichever lands second has a trivial conflict.

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

I added Kling video generation to @opencode/ai: a klingai-video protocol, a minimal KlingAI facade with .video(id) and videoModel, the @opencode/ai/providers/klingai/videos entrypoint, 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:

endpoint = providerOptions.endpoint
        ?? omni-only model (kling-3.0-omni, kling-video-o1)   -> /omni-video/<segment>
        ?? references and the model has an Omni endpoint       -> /omni-video/<segment>
        ?? start or end                                        -> /image-to-video/<segment>
        ?? otherwise                                           -> /text-to-video/<segment>

models.dev lists some models under Kling's older model_name values, so a small table maps them to path segments (kling-v2-6 to kling-2.6, kling-v2-5-turbo to kling-2.5-turbo, kling-video-o1 to kling-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.

Shared field Kling
prompt prompt (text-to-video) or contents[{ type: "prompt", text }]
start / end contents first_frame / last_frame (first frame first, last frame last)
image references contents refer_image with ids image_1, image_2, ...
video references contents feature_video with ids video_1, ... (URL only)
duration settings.duration (seconds)
aspect settings.aspect_ratio; auto is adaptive, left out on image-to-video where the frame sets the shape
resolution settings.resolution
sound settings.audio: native / off

Inline images go as bare base64, as Kling asks. providerOptions.endpoint overrides the endpoint, providerOptions.contents adds native items (Elements, voices, base_video, middle_frame) before the last frame, and providerOptions.settings / options merge 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 or providerOptions.contents on 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>. submitted is queued, processing running, succeeded (and the legacy succeed) completed, failed failed, and anything else stays running. The token is { taskID, createdAt }. Kling keeps results for 30 days, so video URLs get expiresAt 30 days after the task's create_time. A task missing from the response after that window reads as expired; before it, it's an invalid-output error. Failed tasks carry Kling's message as ProviderInternal. Kling documents no failure codes on tasks and no exact moderation wording (only that message describes the reason, "such as triggering the content risk control"), so I don't map any text to ContentPolicy. A submit that Kling's content policy blocks (error code 1301, HTTP 400) comes back as Kling's HTTP error. Results carry durationSeconds, usage from usage.output_duration (Kling 4.0), and the task id, external id and billing in providerMetadata. Kling has no cancel endpoint, so cancel fails with UnsupportedOperation without 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.
  • A klingai case in media-entrypoints.test.ts and exports.test.ts.
  • klingai-video.recorded.test.ts uses kling-v2-5-turbo, 5 seconds at 720p. I had no KLINGAI_API_KEY, so it isn't recorded and skips without a key. Nothing here ran against the live API.
  • bun typecheck and bun test in packages/ai, bun typecheck and test/variant.test.ts in packages/core, bun run check at the root.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

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)

@rekram1-node
rekram1-node requested a review from thdxr as a code owner November 13, 2026 05:29
@rekram1-node
rekram1-node changed the base branch from video-tool to dev November 13, 2026 05:29

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in the latest push: providerOptions.contents on an explicit text-to-video endpoint now fails as unsupported, like frames do.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 contents is set and no endpoint is given, endpointFor routes to image-to-video, so text-to-video only gets contents when 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.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant