feat(ai): add a timeout option to chat(), matching the media activities
メンテナーはふだん 1 日以内に返信
@AlemTuzlak がすでに取り組んでいます。
2026年9月26日 から。
評価
この issue はまだ評価されていません。
説明
Problem
#981 / #1047 gave the media activities and summarize a shared timeout contract: timeout + abortSignal, composed by createActivityAbortControls. chat() never got one.
packages/ai/src/utilities/activity-abort.ts provides createActivityAbortControls({ abortSignal, timeout }), combineAbortSignals, raceWithAbort and isActivityAbortError. Together they compose the timeout with the caller signal into one effective signal and race the adapter call, so a hung provider still rejects. They also clear the timer on settle and route the abort to middleware onAbort rather than onError. Every generate* activity and summarize uses them.
chat() takes only abortController (activities/chat/index.ts L1019-L1022), keeps its own private copy of combineAbortSignals, and has no timeout option. The abortController doc in types.ts suggests building a timeout by hand with setTimeout(() => abortController.abort(), 5000). So a chat stream that never sends headers, or starts and then goes silent, is bounded only by whatever the caller aborts with.
Provider SDK timeouts don't reliably fill the gap either. Some of them drop their own timeout once a signal is passed, and adapters pass the chat signal down. For example, @openrouter/sdk applies timeoutMs only when no signal is present (OpenRouterTeam/typescript-sdk#1123). A core-level timeout composed into the signal avoids that for every adapter.
Verified in @tanstack/ai 0.58.0 and on main at 3e30cde.
Proposal
await chat({
adapter,
messages,
timeout: 30_000,
abortController,
})
timeout?: number: the maximum duration in ms of onechat()invocation, with no default. It would be built oncreateActivityAbortControls, and the effective signal passed to the adapter the same way the media activities do it.chat()would also use the sharedcombineAbortSignals.- Because adapters may ignore the signal, the stream's reads should also be raced against it (the
raceWithAbortidea applied pernext()), so a stalled iterator still rejects. - A timeout is an abort for middleware (
onAbort, notonError), with a distinguishable reason: the existingTimeoutErrorfromcreateTimeoutReason. Callers can then tell "timed out" from "caller aborted" and fall back to another model or retry. - The timer is cleared when the run settles.
Follow-up (or an option in the same change): streaming deadlines
For long reasoning or streaming outputs, a total cap has to be set high enough not to cut off legitimate slow answers, and then it catches stalls late. Two opt-in deadlines would complement timeout:
- a first-chunk timeout: from request start to the first parsed stream chunk;
- an idle timeout: the maximum gap between parsed chunks.
"Chunk" here means a parsed StreamChunk reaching core. SSE keep-alive comments may be dropped by the adapter or SDK before core sees them, so they neither count as progress nor mask a stall. For tool loops, these deadlines would re-arm for each model call.
Motivation
In a production app running many parallel structured chat() calls per request, a rare provider hang held one call until the whole request's deadline (80-118s observed) instead of failing fast and falling back to another model. The workaround today is an app-level AbortController + setTimeout at every call site. That doesn't get onAbort timeout classification, and it duplicates what createActivityAbortControls already does.
Tests (mirroring #1047)
- A timeout aborts a hung
chatStream, including one that ignores the signal. - The first of caller abort or timeout wins and keeps its reason.
- A timeout triggers
onAbortexactly once, notonError. - Completion clears the timer.
- 主要言語
- TypeScript
- スター
- 3.2k
- フォーク
- 372
- 平均マージ
- 1日 16時間
- マージ済み PR(30日)
- 244
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
TanStack/ai のほかの issue
-
難易度 4/5 3〜5日 初心者へのやさしさ 35/100
メンテナーはふだん 1 日以内に返信
-
Durable resume through withPersistence never fills ctx.inputResponse for mcp_input interrupts対応中かも @tombeckenham が 3 日前に担当しました。 オープンwaiting-on: maintainer
メンテナーはふだん 1 日以内に返信
-
ai, ai-persistence: a nested subagent that waits runs again from the start under withPersistence対応中かも @tombeckenham が 4 日前に担当しました。 オープンwaiting-on: maintainer
メンテナーはふだん 1 日以内に返信
-
ai-ollama: streamed chat() reports finishReason "stop" when done_reason is "length"対応中かも @AlemTuzlak が 4 日前に担当しました。 オープンwaiting-on: maintainer
メンテナーはふだん 1 日以内に返信
-
Discriminate status-dependent record and option types (RunRecord, SessionIndexEntry, InputState, durable-only options)対応中かも @jherr が 10 日前に担当しました。 オープンenhancement waiting-on: maintainer
メンテナーはふだん 1 日以内に返信
似ている issue
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
outline/outline#14348 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
bug needs-triage
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
メンテナーはふだん 1 日以内に返信
-
[Bug]: Agent import retains quotes in .env API keys followed by inline comments対応中かも @yxr-2025 が今日担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
elie222/rakazo#2322 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
awslabs/visual-asset-management-system#804 ·
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
dsh-market/dsh-market#1027 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信