Skip to content

feat(ai): add abort signals and timeouts to media generation activities #981

Description

@AdemBenAbdallah

Problem

The media-generation activities, such as generateImage(), can await a provider request indefinitely. This is especially problematic for durable job and workflow systems: a hung provider connection leaves the job running forever, so its retry policy never activates.

The cancellation path is currently incomplete:

  • ImageAdapterConfig and equivalent adapter configs declare timeout?: number, but do not consume it.
  • ImageActivityOptions and ImageGenerationOptions expose neither a timeout nor an AbortSignal.
  • GenerationMiddleware is deliberately observe-only, so it cannot interrupt an in-flight adapter call.
  • @fal-ai/client supports abortSignal on fal.subscribe(), but @tanstack/ai-fal does not receive or forward one.

Chat already composes cancellation signals internally; media activities need the equivalent shared contract.

Proposed public API

Add optional execution controls to media activity options:

const controller = new AbortController()

await generateImage({
  adapter: falImage("fal-ai/nano-banana-2"),
  prompt: "A cinematic landscape",
  timeout: 10 * 60 * 1000,
  abortSignal: controller.signal,
})
  • timeout?: number is the maximum duration of one activity invocation.
  • abortSignal?: AbortSignal supports caller cancellation, request disconnects, and job/runtime cancellation.
  • Core composes the caller signal and timeout signal, clears timeout resources when settled, and passes the effective signal to the adapter.
  • Adapters forward the signal to provider SDKs where supported, for example fal.subscribe(model, { input, abortSignal }).

This should cover generateImage, generateAudio, generateVideo, generateSpeech, generateTranscription, and likely summarize for a consistent activity contract. A first PR could intentionally scope to generateImage plus @tanstack/ai-fal.

Middleware

A generic timeout middleware is useful, but it cannot solve this alone today: GenerationMiddleware only observes onStart, onFinish, and onError, and has no signal or abort() capability.

Once core owns an effective activity signal, middleware could optionally receive:

interface GenerationMiddlewareContext {
  // Existing fields
  signal: AbortSignal
  abort(reason?: unknown): void
}

That would enable a composable timeoutMiddleware(ms), but direct activity options should remain the baseline execution-control API.

Expected behavior

  • No SDK-wide default timeout; callers choose values suitable for each provider or job.
  • The first of caller cancellation or timeout wins and preserves its abort reason.
  • A timeout should be treated as an abort in lifecycle middleware (onAbort), while the activity promise rejects so callers and workflows can retry.
  • A successful completion clears the timer.
  • Request-specific signals must be passed to the provider call, not stored in global provider client configuration. This matters for Fal because fal.config() is global and concurrent generations must remain isolated.

Tests

  • Core: a timeout aborts and is forwarded to a media adapter.
  • Core: a caller-provided signal is forwarded and the first abort reason wins.
  • Core: a successful completion clears the timer.
  • Core: a timeout triggers onAbort exactly once rather than onError.
  • Fal adapter: fal.subscribe() receives the request-specific abortSignal.

Context

This was found while investigating a Fal image-generation request that did not resolve or reject. An application-level adapter-fetch timeout is a temporary safeguard, but a core activity-level cancellation contract would make this behavior reusable across providers and media activities.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions