generateVideo polling timeout does not cancel in-flight status requests
Description
experimental_generateVideo checks poll.timeoutMs between polls, but does not apply it to an in-flight doStatus request.
With timeoutMs: 50, a status request that waits for its abort signal is still pending after 200 ms and its signal has not been aborted. If that request instead returns a completed result after the deadline, the SDK accepts it as a successful generation.
Expected: the polling deadline stops waiting and cancels the pending status request. This concerns the local status request, not cancelling the remote video-generation job.
The reference documentation describes timeoutMs as the maximum wait for completion. In executeStartStatusFlow, elapsed time is checked before await retry(() => model.doStatus(...)), but the request only receives the caller's signal. There is no deadline signal or elapsed-time check when the status request completes.
Reproduction
No API key or network calls needed. Install [email protected] and [email protected], save as repro.mts, and run node repro.mts with Node 24:
import { experimental_generateVideo as generateVideo } from 'ai';
import { MockVideoModelV4 } from 'ai/test';
const caller = new AbortController();
const entered = Promise.withResolvers<void>();
let statusSignal: AbortSignal | undefined;
let state = 'pending';
const model = new MockVideoModelV4({
doGenerate: undefined,
doStart: async () => ({
operation: 'synthetic-operation', warnings: [],
response: { timestamp: new Date(), modelId: 'synthetic-video', headers: {} },
}),
doStatus: ({ abortSignal }) => {
statusSignal = abortSignal;
return new Promise((_resolve, reject) => {
if (abortSignal?.aborted) reject(abortSignal.reason);
else abortSignal?.addEventListener('abort', () => reject(abortSignal.reason), { once: true });
entered.resolve();
});
},
});
const generation = generateVideo({
model, prompt: 'Synthetic prompt.', maxRetries: 0,
abortSignal: caller.signal,
poll: { intervalMs: 0, timeoutMs: 50 },
}).then(() => { state = 'resolved'; }, () => { state = 'rejected'; });
await entered.promise;
await new Promise(resolve => setTimeout(resolve, 200));
console.log({ state, statusRequestAborted: statusSignal?.aborted });
// Expected after the 50 ms deadline: rejected, true.
// Actual after 200 ms: pending, false.
caller.abort(new Error('Synthetic cleanup'));
await generation;Actual output after the deadline:
{ state: 'pending', statusRequestAborted: false }Expected: { state: 'rejected', statusRequestAborted: true }.
The model honors cancellation: the final caller.abort() immediately releases the request. The missing part is applying the polling deadline to that signal.
On Node 22.20.0 and 24.13.0, local regression checks reproduce both the stalled-status and late-success cases. Four controls pass: immediate completion, completion before the deadline, repeated pending polls timing out, and caller cancellation during a status request. The reproduction passes strict TypeScript checking.
The relevant source and documentation are unchanged on main at 12845693d7a6a517dc633d6b6a2e4f5bfd24d2ec (source comparison; runtime tests used the published package). I checked #12515 and the xAI polling reports #17308 / #18048; those do not address the core deadline around doStatus.
I used AI assistance to investigate and prepare the reproduction and tests.
AI SDK Version
ai:7.0.101zod:4.4.3- Node.js:
22.20.0and24.13.0
Code of Conduct
- I agree to follow this project's Code of Conduct
Source: vercel/ai