From 124b75f464309ae02c7a87c29bc4bf827d350f1f Mon Sep 17 00:00:00 2001 From: Railly Date: Sat, 2 May 2026 04:15:38 -0500 Subject: [PATCH] feat: add --timeout flag to image, text, and video commands The hardcoded 120s default for image and text aborts complex prompts before the AI Gateway has a chance to respond. AI Gateway accepts up to ~13min per provider; the CLI just never let users reach for it. Adds --timeout to all three generation commands. Defaults are unchanged (120s for image/text, 300s for video), so this is purely additive. Updates README, commands docs, and troubleshooting docs to document the flag. --- apps/web/docs/commands.mdx | 10 +++++++++- apps/web/docs/troubleshooting.mdx | 10 ++++++++-- packages/ai-cli/README.md | 9 ++++++++- packages/ai-cli/src/commands/image.ts | 11 ++++++++++- packages/ai-cli/src/commands/text.ts | 11 ++++++++++- packages/ai-cli/src/commands/video.ts | 11 ++++++++++- 6 files changed, 55 insertions(+), 7 deletions(-) diff --git a/apps/web/docs/commands.mdx b/apps/web/docs/commands.mdx index 6ae5c02..510c078 100644 --- a/apps/web/docs/commands.mdx +++ b/apps/web/docs/commands.mdx @@ -174,7 +174,7 @@ Models are fetched live from the AI Gateway. Requests that exceed the per-command timeout are aborted automatically: -| Command | Timeout | +| Command | Default timeout | | --- | --- | | `text` | 120 seconds | | `image` | 120 seconds | @@ -182,6 +182,14 @@ Requests that exceed the per-command timeout are aborted automatically: Video generation uses a longer timeout because models typically need more processing time. +Override the default with `--timeout `. Useful for complex prompts where the default is too tight: + +```bash +ai image "complex sprite atlas with 72 cells..." --timeout 600 +ai text "long structured response..." --timeout 300 +ai video "extended scene..." --timeout 900 +``` + ## Exit codes | Code | Meaning | diff --git a/apps/web/docs/troubleshooting.mdx b/apps/web/docs/troubleshooting.mdx index dc45500..efc5ae0 100644 --- a/apps/web/docs/troubleshooting.mdx +++ b/apps/web/docs/troubleshooting.mdx @@ -23,13 +23,19 @@ Add the export to your shell profile (`~/.zshrc`, `~/.bashrc`) to persist across Requests are aborted if they exceed the per-command timeout: -| Command | Timeout | +| Command | Default timeout | | --- | --- | | `text` | 120 seconds | | `image` | 120 seconds | | `video` | 300 seconds | -If you're consistently hitting timeouts, try a faster model variant (e.g. `bytedance/seedance-2.0-fast` instead of `bytedance/seedance-2.0`). +Two ways to handle them: + +1. Bump the timeout per command with `--timeout `: + ```bash + ai image "complex sprite atlas..." --timeout 600 + ``` +2. Try a faster model variant (e.g. `bytedance/seedance-2.0-fast` instead of `bytedance/seedance-2.0`). ## `--quality` / `--style` warning diff --git a/packages/ai-cli/README.md b/packages/ai-cli/README.md index 4c8181a..9191c6e 100644 --- a/packages/ai-cli/README.md +++ b/packages/ai-cli/README.md @@ -128,12 +128,19 @@ The `-m` flag always takes priority over `AI_CLI_*_MODEL` env vars. The `-o` fla Requests that exceed the timeout are aborted automatically: -| Command | Timeout | +| Command | Default timeout | |---|---| | `text` | 120 seconds | | `image` | 120 seconds | | `video` | 300 seconds | +Override with `--timeout ` per command. Useful for complex prompts (large images, long sprite atlases) where the default is too tight: + +```bash +ai image "complex sprite atlas..." --timeout 600 +ai video "longer scene..." --timeout 900 +``` + ### Exit Codes | Code | Meaning | diff --git a/packages/ai-cli/src/commands/image.ts b/packages/ai-cli/src/commands/image.ts index 0b0d8dc..7390a3a 100644 --- a/packages/ai-cli/src/commands/image.ts +++ b/packages/ai-cli/src/commands/image.ts @@ -21,6 +21,7 @@ interface ImageOptions { json?: boolean; concurrency?: string; preview?: boolean; + timeout?: string; } export function registerImageCommand(program: Command) { @@ -48,6 +49,10 @@ export function registerImageCommand(program: Command) { "-p, --concurrency ", `Max parallel generations (default: ${DEFAULT_CONCURRENCY})` ) + .option( + "--timeout ", + `Per-request timeout in seconds (default: ${DEFAULT_TIMEOUT_MS / 1000})` + ) .action(async (rawPrompt: string | undefined, opts: ImageOptions) => { const prompt = rawPrompt?.trim() || undefined; const stdin = await readStdin(); @@ -88,10 +93,14 @@ export function registerImageCommand(program: Command) { const jobs = buildJobs(models, countPerModel); + const timeoutMs = opts.timeout + ? parsePositiveInt(opts.timeout, "timeout") * 1000 + : DEFAULT_TIMEOUT_MS; + const { total, failed } = await runJobs( jobs, async (modelId) => { - const abort = AbortSignal.timeout(DEFAULT_TIMEOUT_MS); + const abort = AbortSignal.timeout(timeoutMs); if (gatewayModels.languageImageModelIds.has(modelId)) { const messageContent: Array< diff --git a/packages/ai-cli/src/commands/text.ts b/packages/ai-cli/src/commands/text.ts index efbcfda..70c23ba 100644 --- a/packages/ai-cli/src/commands/text.ts +++ b/packages/ai-cli/src/commands/text.ts @@ -21,6 +21,7 @@ interface TextOptions { concurrency?: string; quiet?: boolean; json?: boolean; + timeout?: string; } function resolveFormat(fmt?: string): OutputFormat { @@ -50,6 +51,10 @@ export function registerTextCommand(program: Command) { .option("-t, --temperature ", "Temperature (0-2)") .option("-q, --quiet", "Suppress progress output") .option("--json", "Output metadata as JSON") + .option( + "--timeout ", + `Per-request timeout in seconds (default: ${DEFAULT_TIMEOUT_MS / 1000})` + ) .action(async (rawPrompt: string | undefined, opts: TextOptions) => { const prompt = rawPrompt?.trim() || undefined; const stdin = await readStdin(); @@ -83,10 +88,14 @@ export function registerTextCommand(program: Command) { const jobs = buildJobs(models, countPerModel); + const timeoutMs = opts.timeout + ? parsePositiveInt(opts.timeout, "timeout") * 1000 + : DEFAULT_TIMEOUT_MS; + const { total, failed } = await runJobs( jobs, async (modelId) => { - const abort = AbortSignal.timeout(DEFAULT_TIMEOUT_MS); + const abort = AbortSignal.timeout(timeoutMs); const result = await generateText({ headers: { "http-referer": "https://github.com/vercel-labs/ai-cli", diff --git a/packages/ai-cli/src/commands/video.ts b/packages/ai-cli/src/commands/video.ts index 6f8bbe8..f6a78d7 100644 --- a/packages/ai-cli/src/commands/video.ts +++ b/packages/ai-cli/src/commands/video.ts @@ -23,6 +23,7 @@ interface VideoOptions { json?: boolean; concurrency?: string; preview?: boolean; + timeout?: string; } export function registerVideoCommand(program: Command) { @@ -48,6 +49,10 @@ export function registerVideoCommand(program: Command) { "-p, --concurrency ", `Max parallel generations (default: ${DEFAULT_CONCURRENCY})` ) + .option( + "--timeout ", + `Per-request timeout in seconds (default: ${DEFAULT_TIMEOUT_MS / 1000})` + ) .action(async (rawPrompt: string | undefined, opts: VideoOptions) => { const prompt = rawPrompt?.trim() || undefined; const stdin = await readStdin(); @@ -79,10 +84,14 @@ export function registerVideoCommand(program: Command) { const jobs = buildJobs(models, countPerModel); + const timeoutMs = opts.timeout + ? parsePositiveInt(opts.timeout, "timeout") * 1000 + : DEFAULT_TIMEOUT_MS; + const { total, failed } = await runJobs( jobs, async (modelId) => { - const abort = AbortSignal.timeout(DEFAULT_TIMEOUT_MS); + const abort = AbortSignal.timeout(timeoutMs); const result = await generateVideo({ headers: { "http-referer": "https://github.com/vercel-labs/ai-cli",