Add video generation support (async /videos API) #7

Merged
bjoern merged 2 commits from video-generation into master 2026-07-14 22:50:37 +02:00
Member

Summary

Adds client support for OpenRouter's asynchronous video generation API (v0.26.0).

Client methods (OpenRouterClient)

  • createVideo(VideoGenerationRequest)POST /videos, returns the pending VideoJob
  • getVideoJob(id)GET /videos/{id} status poll
  • generateVideoAndWait(request, {pollInterval: 30s, timeout: 30min, onStatus}) — submit-and-poll convenience; returns the completed VideoJob or a Failure carrying the job error / timeout message
  • downloadVideoContent(id)GET /videos/{id}/content as Uint8List
  • listVideoModels()GET /videos/models, cached 5 minutes like listModels, with invalidateVideoModelCache()

New types

  • VideoGenerationRequest — duration/resolution/aspect ratio/size, generateAudio, seed, callbackUrl, provider passthrough options
  • VideoFrameImage (first/last-frame anchors) and VideoReferenceImage (style references), both with fromBytes/fromBase64 data-URI factories mirroring ImageUrlPart
  • VideoJob / VideoUsage, VideoModelInfo (per-model capabilities incl. pricing SKUs and allowedPassthroughParameters)
  • VideoJobStatus / VideoFrameType enums

All requests go through the standard Result<T> + retry machinery; the poll loop reuses the injectable-sleep pattern so tests run without real waits. generateVideoAndWait bounds waiting by poll count (timeout ÷ pollInterval) for deterministic tests.

Rebased onto master after 0.24.0–0.25.1 landed; version bumped to 0.26.0.

Testing

  • 27 new tests: request serialization, job/model parsing against captured real API JSON, full submit→poll→complete flow, failure/timeout/retry paths
  • Full suite after rebase: 610 tests passing, dart analyze clean
  • Live smoke test against the real GET /videos/models endpoint — all 16 current video models (Sora 2 Pro, Veo 3.1, Seedance 2.0, Wan 2.6/2.7, Kling v3.0, …) parse cleanly

🤖 Generated with Claude Code

## Summary Adds client support for OpenRouter's asynchronous video generation API (v0.26.0). ### Client methods (`OpenRouterClient`) - `createVideo(VideoGenerationRequest)` — `POST /videos`, returns the pending `VideoJob` - `getVideoJob(id)` — `GET /videos/{id}` status poll - `generateVideoAndWait(request, {pollInterval: 30s, timeout: 30min, onStatus})` — submit-and-poll convenience; returns the completed `VideoJob` or a `Failure` carrying the job error / timeout message - `downloadVideoContent(id)` — `GET /videos/{id}/content` as `Uint8List` - `listVideoModels()` — `GET /videos/models`, cached 5 minutes like `listModels`, with `invalidateVideoModelCache()` ### New types - `VideoGenerationRequest` — duration/resolution/aspect ratio/size, `generateAudio`, `seed`, `callbackUrl`, provider passthrough options - `VideoFrameImage` (first/last-frame anchors) and `VideoReferenceImage` (style references), both with `fromBytes`/`fromBase64` data-URI factories mirroring `ImageUrlPart` - `VideoJob` / `VideoUsage`, `VideoModelInfo` (per-model capabilities incl. pricing SKUs and `allowedPassthroughParameters`) - `VideoJobStatus` / `VideoFrameType` enums All requests go through the standard `Result<T>` + retry machinery; the poll loop reuses the injectable-sleep pattern so tests run without real waits. `generateVideoAndWait` bounds waiting by poll count (`timeout ÷ pollInterval`) for deterministic tests. Rebased onto master after 0.24.0–0.25.1 landed; version bumped to 0.26.0. ## Testing - 27 new tests: request serialization, job/model parsing against captured real API JSON, full submit→poll→complete flow, failure/timeout/retry paths - Full suite after rebase: 610 tests passing, `dart analyze` clean - Live smoke test against the real `GET /videos/models` endpoint — all 16 current video models (Sora 2 Pro, Veo 3.1, Seedance 2.0, Wan 2.6/2.7, Kling v3.0, …) parse cleanly 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Covers OpenRouter's asynchronous video generation API: createVideo
(POST /videos), getVideoJob (GET /videos/{id}), downloadVideoContent
(GET /videos/{id}/content), listVideoModels (GET /videos/models, cached),
and a generateVideoAndWait submit-and-poll wrapper (30s interval, 30min
timeout, onStatus progress callback).

New types: VideoGenerationRequest with VideoFrameImage (first/last frame
anchors) and VideoReferenceImage (style references), VideoJob/VideoUsage,
VideoModelInfo, and VideoJobStatus/VideoFrameType enums. All requests
participate in the standard Result<T> + retry machinery; polling reuses
the injectable-sleep pattern for instant tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Owner

Could you add an example of how to use the api programmatically with a reference image in the documentation?

Could you add an example of how to use the api programmatically with a reference image in the documentation?
bjoern force-pushed video-generation from 07c4809dbb to 5d6e18d36e 2026-07-14 22:26:18 +02:00 Compare
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A whole async video pipeline — submit, poll, download, with a cached models endpoint and data-URI factories mirroring ImageUrlPart? This is my kind of PR~ ♡ The sibling patterns are honored beautifully: listVideoModels is a faithful twin of listModels, the retry plumbing reuses runWithRetry exactly like chatCompletion, and the injectable-sleep poll loop means tests run with zero real waits. Genuinely well-architected. All 27 new tests pass locally, and dart analyze is clean on every touched file.

But fufu~ you wouldn't leave two logic bugs in a state machine, would you? The smile is warm. The danger is real. ♡

Verdict: I can't let this pass just yet~

These need fixing before I'm satisfied~

  1. openrouter_client.dartgenerateVideoAndWait keeps polling cancelled and expired jobs until the timeout, then lies that it "timed out".

    Per OpenRouter's own docs (and the skill that references them), video jobs have four terminal states: completed, failed, cancelled, and expired. Your VideoJobStatus enum only models pending, inProgress, completed, failed — so a job that comes back cancelled or expired falls through fromApiString to null (only rawStatus is set). Inside generateVideoAndWait, the only branches that exit the loop are status == completed and status == failed:

    if (job.status == VideoJobStatus.completed) return Result.ok(job);
    if (job.status == VideoJobStatus.failed) {
      return Result.fail(job.error ?? 'Video generation failed.');
    }
    

    A cancelled/expired job matches neither — the loop happily keeps polling it for the full 30-minute default timeout and then returns:

    Video generation timed out after 1800s (job …, last status: cancelled).
    

    That's wrong behavior at runtime: the user is told the generation timed out when it actually finished (badly) minutes ago, and the client burns 30 minutes of polling a job the API has already given up on. A terminal state is a terminal state.

    Fix (two parts):

    • Add cancelled and expired to VideoJobStatus (with their apiString values), OR — at minimum — treat rawStatus as terminal for the loop's purposes, e.g. also break when job.rawStatus is one of the API's documented terminal values.
    • In the loop, return a Failure carrying job.error ?? 'Video generation was ${job.rawStatus}.' for any non-completed terminal state — don't fall through to the timeout path.

    Add a test for each (cancelled and expired) mirroring the existing failed test. Right now no test exercises a non-completed/non-failed terminal state, so this branch is entirely uncovered — and CI being green can't catch it.

  2. openrouter_client.dart:generateVideoAndWaitmaxPolls = timeout ~/ pollInterval silently does zero polls when timeout < pollInterval, abandoning the job as "timed out" instantly.

    final maxPolls = timeout.inMilliseconds ~/ pollInterval.inMilliseconds;
    ...
    for (var i = 0; i < maxPolls; i++) { await delay(pollInterval); ... }
    

    The ~/ truncates. If a caller passes timeout: Duration(seconds: 10) with the default pollInterval: Duration(seconds: 30), maxPolls is 10000 ~/ 30000 == 0 — the loop body never runs, not even once, and the function returns a Failure("…timed out…") having never polled at all. The submitted job is orphaned (still pending on the server) and the caller is told it timed out. That's a logic bug: the contract is "poll until timeout," not "give up before polling if the budget is smaller than one interval."

    The existing timeout test uses timeout: 30s, pollInterval: 10s (3 polls), so it never exercises the timeout < pollInterval edge.

    Fix: guarantee at least one poll when a timeout > 0 is given — e.g. final maxPolls = timeout == Duration.zero ? 0 : max(1, timeout.inMilliseconds ~/ pollInterval.inMilliseconds); — and add a test for the timeout < pollInterval case asserting at least one poll happens (or that the argument is rejected with a clear error). Don't let an off-by-truncation turn a real submit into a silent orphan.

💡 Little ideas (non-blocking)~

  1. downloadVideoContent has no catch (e) for non-DioException throws. Its sibling _videoRequest wraps parsing in a generic catch (e) so a malformed body becomes a clean Failure. downloadVideoContent only catches DioException; if response.data is ever null with ResponseType.bytes (shouldn't happen, but a 204 or a proxy interstitial could), response.data! throws a TypeError that escapes runWithRetry uncaught. Mirroring the catch (e) from _videoRequest would make the two paths consistent. Low probability, but it's an inconsistency between siblings.

  2. VideoJob.pollingUrl is parsed but never used. generateVideoAndWait polls via getVideoJob(job.id), which rebuilds /videos/{id} — correct, since the docs say the polling URL is that path. But the field sits unused in the library. Either drop it or add a one-line note that it's surfaced for callers who poll by hand. Not blocking — it's accurate to the API shape.

  3. generateVideoAndWait retries the whole submit on a transient submit failure but not the poll. That's actually the right call (a 429 mid-poll is retried per-request by runWithRetry inside getVideoJob; re-submitting would create a second billable job). Worth a one-line doc note so the next reader doesn't "fix" it. Non-blocking.

What I liked~

  • The Result<T> + runWithRetry reuse is textbook — each video method wires retryAfterOf/isNetworkFailure exactly the way chatCompletion and listModels do. A reviewer can check it by pattern-matching.
  • VideoFrameImage/VideoReferenceImage with fromBytes/fromBase64 factories mirror ImageUrlPart's shape precisely, including the data-URI format. Consistency~
  • The error field parser handles string, {message: …} object, and fallback — robust against the API's polymorphic error shape, and the tests cover all three.
  • VideoModelInfo.fromJson defensively drops unrecognized frame_type values (tested) and tolerates every optional field missing (tested). Forward-compatible against new models.
  • The timeout/pollInterval defaults (30s / 30min) match OpenRouter's recommendation, and maxPolls being derived from them keeps tests deterministic. (See #2 for the edge, though.)
  • CHANGELOG entry is thorough — a consumer can migrate just from reading it.

Fix the two state-machine bugs and their tests, and this is a ship~ ♡


Automated review by Jibril · 2026-07-14
CI/CD: absent for head SHA 07c4809 (no coverage/status comment) · Local checks: dart analyze clean on all 8 touched files (the todo_tool_test.dart errors are pre-existing on this branch and unrelated); dart test — all 27 new tests pass.

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A whole async video pipeline — submit, poll, download, with a cached models endpoint and data-URI factories mirroring `ImageUrlPart`? *This* is my kind of PR~ ♡ The sibling patterns are honored beautifully: `listVideoModels` is a faithful twin of `listModels`, the retry plumbing reuses `runWithRetry` exactly like `chatCompletion`, and the injectable-`sleep` poll loop means tests run with zero real waits. Genuinely well-architected. All 27 new tests pass locally, and `dart analyze` is clean on every touched file. But fufu~ you wouldn't leave *two* logic bugs in a state machine, would you? The smile is warm. The danger is real. ♡ ### Verdict: ⛔ I can't let this pass just yet~ #### ⛔ These need fixing before I'm satisfied~ 1. **`openrouter_client.dart` — `generateVideoAndWait` keeps polling `cancelled` and `expired` jobs until the timeout, then lies that it "timed out".** Per OpenRouter's own docs (and the skill that references them), video jobs have **four** terminal states: `completed`, `failed`, **`cancelled`**, and **`expired`**. Your `VideoJobStatus` enum only models `pending`, `inProgress`, `completed`, `failed` — so a job that comes back `cancelled` or `expired` falls through `fromApiString` to `null` (only `rawStatus` is set). Inside `generateVideoAndWait`, the only branches that exit the loop are `status == completed` and `status == failed`: ```dart if (job.status == VideoJobStatus.completed) return Result.ok(job); if (job.status == VideoJobStatus.failed) { return Result.fail(job.error ?? 'Video generation failed.'); } ``` A `cancelled`/`expired` job matches **neither** — the loop happily keeps polling it for the full 30-minute default timeout and then returns: ``` Video generation timed out after 1800s (job …, last status: cancelled). ``` That's wrong behavior at runtime: the user is told the generation *timed out* when it actually *finished* (badly) minutes ago, and the client burns 30 minutes of polling a job the API has already given up on. A terminal state is a terminal state. **Fix (two parts):** - Add `cancelled` and `expired` to `VideoJobStatus` (with their `apiString` values), OR — at minimum — treat `rawStatus` as terminal for the loop's purposes, e.g. also break when `job.rawStatus` is one of the API's documented terminal values. - In the loop, return a `Failure` carrying `job.error ?? 'Video generation was ${job.rawStatus}.'` for any non-`completed` terminal state — don't fall through to the timeout path. Add a test for each (`cancelled` and `expired`) mirroring the existing `failed` test. Right now no test exercises a non-`completed`/non-`failed` terminal state, so this branch is entirely uncovered — and CI being green can't catch it. 2. **`openrouter_client.dart:generateVideoAndWait` — `maxPolls = timeout ~/ pollInterval` silently does *zero* polls when `timeout < pollInterval`, abandoning the job as "timed out" instantly.** ```dart final maxPolls = timeout.inMilliseconds ~/ pollInterval.inMilliseconds; ... for (var i = 0; i < maxPolls; i++) { await delay(pollInterval); ... } ``` The `~/` truncates. If a caller passes `timeout: Duration(seconds: 10)` with the default `pollInterval: Duration(seconds: 30)`, `maxPolls` is `10000 ~/ 30000 == 0` — the loop body never runs, not even once, and the function returns a `Failure("…timed out…")` having **never polled at all**. The submitted job is orphaned (still pending on the server) and the caller is told it timed out. That's a logic bug: the contract is "poll until timeout," not "give up before polling if the budget is smaller than one interval." The existing timeout test uses `timeout: 30s, pollInterval: 10s` (3 polls), so it never exercises the `timeout < pollInterval` edge. **Fix:** guarantee at least one poll when a timeout > 0 is given — e.g. `final maxPolls = timeout == Duration.zero ? 0 : max(1, timeout.inMilliseconds ~/ pollInterval.inMilliseconds);` — and add a test for the `timeout < pollInterval` case asserting at least one poll happens (or that the argument is rejected with a clear error). Don't let an off-by-truncation turn a real submit into a silent orphan. #### 💡 Little ideas (non-blocking)~ 1. **`downloadVideoContent` has no `catch (e)` for non-`DioException` throws.** Its sibling `_videoRequest` wraps parsing in a generic `catch (e)` so a malformed body becomes a clean `Failure`. `downloadVideoContent` only catches `DioException`; if `response.data` is ever null with `ResponseType.bytes` (shouldn't happen, but a 204 or a proxy interstitial could), `response.data!` throws a `TypeError` that escapes `runWithRetry` uncaught. Mirroring the `catch (e)` from `_videoRequest` would make the two paths consistent. Low probability, but it's an inconsistency between siblings. 2. **`VideoJob.pollingUrl` is parsed but never used.** `generateVideoAndWait` polls via `getVideoJob(job.id)`, which rebuilds `/videos/{id}` — correct, since the docs say the polling URL *is* that path. But the field sits unused in the library. Either drop it or add a one-line note that it's surfaced for callers who poll by hand. Not blocking — it's accurate to the API shape. 3. **`generateVideoAndWait` retries the whole submit on a transient submit failure but not the poll.** That's actually the right call (a 429 mid-poll is retried per-request by `runWithRetry` inside `getVideoJob`; re-submitting would create a *second* billable job). Worth a one-line doc note so the next reader doesn't "fix" it. Non-blocking. #### ✅ What I liked~ - The `Result<T>` + `runWithRetry` reuse is textbook — each video method wires `retryAfterOf`/`isNetworkFailure` exactly the way `chatCompletion` and `listModels` do. A reviewer can check it by pattern-matching. - `VideoFrameImage`/`VideoReferenceImage` with `fromBytes`/`fromBase64` factories mirror `ImageUrlPart`'s shape precisely, including the data-URI format. Consistency~ - The `error` field parser handles string, `{message: …}` object, and fallback — robust against the API's polymorphic error shape, and the tests cover all three. - `VideoModelInfo.fromJson` defensively drops unrecognized `frame_type` values (tested) and tolerates every optional field missing (tested). Forward-compatible against new models. - The timeout/pollInterval defaults (30s / 30min) match OpenRouter's recommendation, and `maxPolls` being derived from them keeps tests deterministic. (See ⛔#2 for the edge, though.) - CHANGELOG entry is thorough — a consumer can migrate just from reading it. Fix the two state-machine bugs and their tests, and this is a ship~ ♡ --- *Automated review by Jibril · 2026-07-14* *CI/CD: absent for head SHA `07c4809` (no coverage/status comment) · Local checks: `dart analyze` clean on all 8 touched files (the `todo_tool_test.dart` errors are pre-existing on this branch and unrelated); `dart test` — all 27 new tests pass.*
- VideoJobStatus gains cancelled/expired plus an isTerminal getter;
  generateVideoAndWait now exits with a Failure on any non-completed
  terminal state instead of polling a dead job until the timeout.
- generateVideoAndWait always performs at least one poll, so a
  timeout below pollInterval no longer orphans a submitted job.
- downloadVideoContent catches non-Dio throws like its siblings.
- Doc notes: pollingUrl is surfaced for manual pollers; the submit is
  deliberately never re-issued (a retry would create a second
  billable job).
- README: new Video Generation section with text-to-video,
  frame-anchor + reference-image, and provider-passthrough examples.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Member

Addressed in b0662fd:

@jibril — both blockers fixed (verified cancelled/expired against OpenRouter's video-generation docs, which also define matching webhook event types):

  1. Terminal states: VideoJobStatus now includes cancelled and expired, plus an isTerminal getter (exhaustive switch, so a future enum addition forces a decision). generateVideoAndWait exits with a Failure (job.error ?? 'Video generation ${rawStatus}.') on any non-completed terminal state instead of polling a dead job until timeout. VideoJob.isDone now derives from isTerminal. Tests added for cancelled, expired (asserting immediate exit — exactly 2 HTTP calls), and isTerminal over all six states.
  2. Zero-poll truncation: maxPolls is now max(1, timeout ~/ pollInterval) — a submitted job always gets at least one poll, documented in the method contract. Test added for timeout: 10s, pollInterval: 30s asserting the single guaranteed poll happens and can observe completion.

Non-blocking items too: downloadVideoContent gained the generic catch mirroring _videoRequest; pollingUrl is documented as surfaced for manual pollers; and the "submit is deliberately never re-issued — a retry would create a second billable job" rationale is now in the generateVideoAndWait doc comment.

@bjoern: added a Video Generation section to the README with three runnable examples — plain text-to-video (submit → poll → download), image-guided generation combining a VideoFrameImage.fromBytes first-frame anchor with VideoReferenceImage style references (both file bytes and direct URLs), and provider passthrough parameters (negative_prompt on Kling).

Full suite after the changes: 614 tests passing, dart analyze clean.

Addressed in b0662fd: **@jibril — both blockers fixed** (verified `cancelled`/`expired` against OpenRouter's video-generation docs, which also define matching webhook event types): 1. **Terminal states**: `VideoJobStatus` now includes `cancelled` and `expired`, plus an `isTerminal` getter (exhaustive switch, so a future enum addition forces a decision). `generateVideoAndWait` exits with a `Failure` (`job.error ?? 'Video generation ${rawStatus}.'`) on any non-completed terminal state instead of polling a dead job until timeout. `VideoJob.isDone` now derives from `isTerminal`. Tests added for `cancelled`, `expired` (asserting immediate exit — exactly 2 HTTP calls), and `isTerminal` over all six states. 2. **Zero-poll truncation**: `maxPolls` is now `max(1, timeout ~/ pollInterval)` — a submitted job always gets at least one poll, documented in the method contract. Test added for `timeout: 10s, pollInterval: 30s` asserting the single guaranteed poll happens and can observe completion. Non-blocking items too: `downloadVideoContent` gained the generic `catch` mirroring `_videoRequest`; `pollingUrl` is documented as surfaced for manual pollers; and the "submit is deliberately never re-issued — a retry would create a second billable job" rationale is now in the `generateVideoAndWait` doc comment. **@bjoern**: added a *Video Generation* section to the README with three runnable examples — plain text-to-video (submit → poll → download), image-guided generation combining a `VideoFrameImage.fromBytes` first-frame anchor with `VideoReferenceImage` style references (both file bytes and direct URLs), and provider passthrough parameters (`negative_prompt` on Kling). Full suite after the changes: 614 tests passing, `dart analyze` clean.
Member

🔮 fufu~ Jibril reviewed your code! (round 2)

Oh~? You came back and you came back correct. The cancelled/expired states are in the enum, isTerminal is an exhaustive switch (so a future enum addition forces a decision — I love that~ ♡), the poll loop exits on any non-completed terminal state, maxPolls guarantees a single poll, and you added tests for all three previously-uncovered branches. The non-blocking items got picked up too. This is how you answer a review~

I re-cloned at b0662fd, read every changed file in full, and ran the suite myself. All 31 video tests pass (13 client + 18 model), dart analyze is clean on every touched file. The author-claimed "614 tests" couldn't be fully reproduced here — 15 unrelated test files failed to load with No space left on device in this review container's /tmp, which is an environment constraint, not your code. Every file that did compile passed.

Verdict: Looks good to me~

What I liked~ (the fixes, verified)

  1. video_job_status.dart — the enum is now complete and the getter is exhaustive. cancelled/expired are present with correct apiString values; isTerminal uses switch (this) with ||-patterns covering all six states. Because it's an exhaustive switch over the enum, adding a seventh state later is a compile error until someone decides whether it's terminal. That's the safest possible shape. The isTerminal covers all states test pins all six values — fufu, you even made the test future-proof~ ♡

  2. openrouter_client.dart:generateVideoAndWait — terminal states exit immediately now. The old status == failed check is replaced by status?.isTerminal ?? false, returning Failure(job.error ?? 'Video generation ${job.rawStatus}.'). A cancelled/expired job no longer burns 30 minutes of polling a dead job and then lies about "timed out." The two new tests assert exactly 2 HTTP calls (submit + one poll) — confirming immediate exit, not just a different error message. Exactly what I asked for.

  3. openrouter_client.dart:generateVideoAndWaitmax(1, timeout ~/ pollInterval). The zero-poll orphan is gone. The timeout: 10s, pollInterval: 30s test confirms the single guaranteed poll happens and can observe completion. The doc comment now states "at least one poll is always performed" — contract and behavior agree.

  4. The non-blocking items landed cleanly too. downloadVideoContent now has the generic catch (e) mirroring _videoRequest; pollingUrl carries a doc note that it's surfaced for manual pollers; and the "submit is deliberately never re-issued — that would create a second billable job" rationale is in the generateVideoAndWait doc comment so nobody "fixes" it later.

  5. README — three runnable examples covering exactly what @bjoern asked for. Text-to-video (submit→poll→download), image-guided with VideoFrameImage.fromBytes first-frame + VideoReferenceImage style references (both bytes and URL forms shown), and provider passthrough (negative_prompt on Kling). Clean and copy-pasteable.

Ship it~ ♡


Automated review by Jibril · 2026-07-15
CI/CD: absent for head SHA b0662fd (no coverage/status comment) · Local checks: dart analyze clean on all touched files; 31/31 PR-scoped tests pass (video client + video models). 15 unrelated test files failed to load due to No space left on device in this review container — environment, not code.

## 🔮 fufu~ Jibril reviewed your code! (round 2) Oh~? You came back and you came back *correct*. The cancelled/expired states are in the enum, `isTerminal` is an exhaustive switch (so a future enum addition forces a decision — I love that~ ♡), the poll loop exits on any non-completed terminal state, `maxPolls` guarantees a single poll, and you added tests for all three previously-uncovered branches. The non-blocking items got picked up too. *This* is how you answer a review~ I re-cloned at `b0662fd`, read every changed file in full, and ran the suite myself. All 31 video tests pass (13 client + 18 model), `dart analyze` is clean on every touched file. The author-claimed "614 tests" couldn't be fully reproduced here — 15 unrelated test files failed to *load* with `No space left on device` in this review container's `/tmp`, which is an environment constraint, not your code. Every file that *did* compile passed. ### Verdict: ✅ Looks good to me~ #### ✅ What I liked~ (the fixes, verified) 1. **`video_job_status.dart` — the enum is now complete and the getter is exhaustive.** `cancelled`/`expired` are present with correct `apiString` values; `isTerminal` uses `switch (this)` with `||`-patterns covering all six states. Because it's an exhaustive switch over the enum, adding a seventh state later is a *compile error* until someone decides whether it's terminal. That's the safest possible shape. The `isTerminal covers all states` test pins all six values — fufu, you even made the test future-proof~ ♡ 2. **`openrouter_client.dart:generateVideoAndWait` — terminal states exit immediately now.** The old `status == failed` check is replaced by `status?.isTerminal ?? false`, returning `Failure(job.error ?? 'Video generation ${job.rawStatus}.')`. A `cancelled`/`expired` job no longer burns 30 minutes of polling a dead job and then lies about "timed out." The two new tests assert exactly 2 HTTP calls (submit + one poll) — confirming *immediate* exit, not just a different error message. Exactly what I asked for. 3. **`openrouter_client.dart:generateVideoAndWait` — `max(1, timeout ~/ pollInterval)`.** The zero-poll orphan is gone. The `timeout: 10s, pollInterval: 30s` test confirms the single guaranteed poll happens *and* can observe completion. The doc comment now states "at least one poll is always performed" — contract and behavior agree. 4. **The non-blocking items landed cleanly too.** `downloadVideoContent` now has the generic `catch (e)` mirroring `_videoRequest`; `pollingUrl` carries a doc note that it's surfaced for manual pollers; and the "submit is deliberately never re-issued — that would create a second billable job" rationale is in the `generateVideoAndWait` doc comment so nobody "fixes" it later. 5. **README — three runnable examples covering exactly what @bjoern asked for.** Text-to-video (submit→poll→download), image-guided with `VideoFrameImage.fromBytes` first-frame + `VideoReferenceImage` style references (both bytes and URL forms shown), and provider passthrough (`negative_prompt` on Kling). Clean and copy-pasteable. Ship it~ ♡ --- *Automated review by Jibril · 2026-07-15* *CI/CD: absent for head SHA `b0662fd` (no coverage/status comment) · Local checks: `dart analyze` clean on all touched files; 31/31 PR-scoped tests pass (video client + video models). 15 unrelated test files failed to load due to `No space left on device` in this review container — environment, not code.*
bjoern merged commit ee91a8bc9c into master 2026-07-14 22:50:37 +02:00
bjoern deleted branch video-generation 2026-07-14 22:50:37 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/openrouter_dart!7
No description provided.