Commit ad23da9
authored
feat(ai-client,ai-react): add
* feat(ai-client,ai-react): add `fetcher` option to ChatClient/useChat
Mirrors the `fetcher` option on the multimedia hooks (useGenerateSpeech /
useSummarize / useTranscription / useGenerateImage). Pass either
`connection` (a ConnectionAdapter) or `fetcher` (a direct async function
— typically a TanStack Start server function) — runtime XOR validation.
The fetcher may return either a Response (parsed as SSE) or an
AsyncIterable<StreamChunk> (yielded directly). Internally, fetcher is
wrapped via `fetcherToConnectionAdapter` and reuses the same subscribe/
send queue plumbing as every other connection adapter — no new code
paths in ChatClient itself.
Purely additive: stream(), rpcStream(), fetchServerSentEvents(), and
fetchHttpStream() are unchanged. Other framework wrappers (ai-solid,
ai-vue, ai-svelte) untouched in this branch — same shape can be added
to each in a follow-up if this design is preferred.
Sketch alternative to #508 (the stream() connection-adapter approach)
for design comparison.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: apply automated fixes
* fix(ai-client): port #508 robustness fixes onto fetcher-alt branch
The fetcher path uses the same SSE parsing and connect-wrapper plumbing
as the stream() path on #508, so the polish that landed during #508's
review applies directly here. Carry it over so this branch has the same
robustness.
- Skip SSE control lines (`:` comments, `event:` / `id:` / `retry:`) in
responseToSSEChunks. Proxies and CDNs inject these as keepalives;
letting them through would feed JSON.parse a non-payload line.
- Drop unterminated trailing buffer in readStreamLines. A non-empty
buffer at stream end means the connection was cut mid-line, so the
data is partial — yielding it would surface a misleading RUN_ERROR
for what is really a transport-layer issue.
- Surface JSON.parse failures in responseToSSEChunks and fetchHttpStream.
Stop swallowing them behind console.warn; let SyntaxError propagate so
the connect-wrapper turns it into a visible RUN_ERROR.
- Drop unsafe `as unknown as StreamChunk` casts in
normalizeConnectionAdapter's synthesized RUN_FINISHED / RUN_ERROR
events. Use EventType + RunFinishedEvent / RunErrorEvent so missing
required fields are caught by the compiler. Track upstream
threadId/runId from chunks and reuse them in the synthesis instead of
fabricating both ids unconditionally.
- Forward optional abortSignal third arg through stream() and rpcStream()
factory signatures. Backwards-compatible for existing callers; lets
long-running factories cancel when useChat aborts. Mirrors what
fetcherToConnectionAdapter already does.
Tests:
- Update the two `should handle malformed JSON gracefully` tests to
assert SyntaxError throws instead of silent drop.
- Update stream() / rpcStream() factory mock assertions to expect the new
third arg.
- Add chat-fetcher test asserting a fetcher returning a malformed-SSE
Response surfaces as a RUN_ERROR via onError.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(ai-client): enforce connection/fetcher XOR via ChatTransport type
Promote `ChatClientOptions` to a discriminated union so exactly one of
`connection` or `fetcher` is required at the type level, surface stream
truncation as a `StreamTruncatedError` instead of a silent warn, synthesize
RUN_FINISHED on legacy `[DONE]` sentinels, and abort fetcher-returned
async iterables that ignore their signal. Update framework wrappers
(react/preact/solid/svelte/vue) and the e2e route to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(hooks): branch on connection/fetcher in useChat instead of spreading a partial transport
Mirrors the pattern in `useGeneration` (multimedia hooks): build a `baseOptions`
literal once, then call `new ChatClient(...)` in two narrow branches with the
matching transport. Drops the `optionsRef.current.fetcher!` non-null assertion
and the awkward discriminated-union spread, and provides a clear hook-level
error when neither `connection` nor `fetcher` is provided.
Applied to all five chat hooks: ai-react, ai-preact, ai-solid, ai-vue,
ai-svelte.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(release): bump framework hooks to minor for fetcher option
Add `@tanstack/ai-preact`, `ai-solid`, `ai-svelte`, and `ai-vue` to the
fetcher changeset as minor bumps — they all expose the new `fetcher`
option transitively via `ChatClientOptions`. Also simplify the hooks
to pick the transport into a single object before constructing
`ChatClient`, instead of duplicating the option bag in if/else branches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(release): downgrade non-react framework hooks to patch
`ai-preact`, `ai-solid`, `ai-svelte`, and `ai-vue` don't add any new
public exports — they only adjust internal plumbing to handle the new
connection/fetcher XOR shape from `ai-client`. `ai-react` stays minor
because it genuinely re-exports new symbols (`rpcStream`, `ChatFetcher`,
`ChatFetcherInput`, `ChatFetcherOptions`).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Merge branch 'main' into claude/usechat-fetcher-alternative
Resolves conflicts in 9 files arising from main's changes since the PR
was opened (#511 AG-UI compliance, #545 openai-base refactor, #564
stricter TS, #576 useChat partial/final reset, #577 structured-output).
Notable resolutions:
- chat-client.ts: Kept PR's `resolveTransport()` for connection/fetcher
XOR; combined with main's two-slot `bodyOption`/`forwardedPropsOption`
+ AG-UI `threadId`. Removed `!` non-null assertions to satisfy main's
tightened lint.
- connection-adapters.ts: Kept PR's `responseToSSEChunks` /
`fetcherToConnectionAdapter` / `StreamTruncatedError` alongside main's
new `RunAgentInputContext`. Synthesized RUN_FINISHED/RUN_ERROR now
prefer upstream-observed IDs, fall back to runContext, then synthesize.
- Framework hooks (react/preact/solid/vue/svelte): Combined PR's
`transport` ternary with main's `exactOptionalPropertyTypes` conditional
spreads.
- chat-fetcher.test.ts: Dropped `conversationId` expectation from
`data` — AG-UI now sends `threadId` at the wire top level, not in
the body.
Verified:
- @tanstack/ai-client: 228/228 tests pass, tsc clean
- @tanstack/ai-react: 118/118 tests pass, tsc clean
- Other framework hooks: tsc clean (svelte pre-existing $state errors
unrelated to merge area)
* fix(ai-client): expose threadId/runId on ChatFetcherInput
After merging main (AG-UI compliance, #511), the chat client stopped
injecting `conversationId` into the request body — `threadId` is now sent
at the AG-UI wire root via `RunAgentInputContext`. But the fetcher path
(PR #512's whole point) only receives `{ messages, data }`, so
fetcher-using consumers had no way to access any correlation identifier
for the turn.
Thread `runContext.threadId` / `runContext.runId` through
`fetcherToConnectionAdapter` and surface them as required fields on
`ChatFetcherInput`. Users wrapping a `fetch()` call can now forward the
ids to their server like the connection adapters already do.
Also throws if `runContext` is missing — chat-client always supplies it,
so a missing value indicates a bug worth surfacing immediately rather
than silently fabricating a date-based id.
Restores the test assertion against the new fields (the previous
`conversationId` assertion was removed in the merge because the field
moved to the wire root — but for fetcher consumers, neither path was
left).
* fix(ai-client,hooks): tighten error handling and preserve ChatTransport XOR in useChat options
Addresses four items from the merge review:
1. Drop the deprecated `error: { message }` nested field from
synthesized RUN_ERROR events. The top-level `message` field already
satisfies AG-UI's `RunErrorEventSchema`; the nested form is
`@deprecated` per the type doc.
2. Replace the `${Date.now()}` fallback in synthesized RUN_FINISHED /
RUN_ERROR with an explicit throw. The chat client always supplies
`runContext.threadId` / `runId`, so a missing value indicates a
wiring bug worth surfacing instead of fabricating a date-based id
that breaks `activeRunIds` correlation downstream.
3. Replace the `(chunk as { runId?: string }).runId` structural cast in
chat-client with the `'runId' in chunk` narrowing pattern already
used elsewhere in connection-adapters. Removes the cast without
adding a runtime check.
4. Distribute `Omit` over `ChatClientOptions` in every framework hook's
`UseChatOptions` / `CreateChatOptions` (via new `DistributedOmit`
helper exported from `@tanstack/ai-client`). Plain `Omit` collapsed
the `ChatTransport` discriminated union into a flat shape, forcing a
load-bearing `optionsRef.current.fetcher!` non-null assertion in all
five hooks. After this change:
- `useChat({})` is a compile error
- `useChat({ connection, fetcher })` is a compile error
- Narrowing on `!opts.connection` types `opts.fetcher` as
`ChatFetcher` (non-undefined) — the `!` is removed in
react/preact/solid/vue/svelte hooks.
Adds a type-only test in ai-react probing the XOR so a future
refactor that drops `DistributedOmit` would fail compile.
Tests updated:
- connection-adapters.test.ts: two synthesis tests now pass `runContext`
to `adapter.send()` (previously relied on the date-fallback behavior).
* fix(e2e): post AG-UI wire envelope from fetcher-mode chat route
The e2e fetcher was posting `{ messages, data, threadId, runId, provider,
feature, testId, aimockPort }` — a pre-AG-UI shape that `chatParamsFromRequestBody`
rejects (it validates against AG-UI's `RunAgentInputSchema`). The connection
adapter avoids this because `fetchServerSentEvents` builds the full
envelope internally; the fetcher path bypasses that.
Mirror what `fetchServerSentEvents` posts:
- Convert messages with `uiMessagesToWire` (parts → string content).
- Include `state: {}`, `tools: []`, `context: []` (required by schema).
- Forward `input.data` as `forwardedProps` (already contains the merged
body fields from `useChat({ body })`).
Fixes 7 failing fetcher-mode tests across all chat providers.
* refactor(e2e): narrow $feature.tsx route params via type predicates
Route.useParams() returns provider/feature as raw strings. Previously
the file used an `as { provider: Provider; feature: Feature }` cast on
the destructure, which an earlier autofix removed — leaving TS to flag
each downstream usage as TS2345.
Replace with two small `isProvider` / `isFeature` type predicates over
the existing ALL_PROVIDERS / ALL_FEATURES arrays. The existing
"return <NotSupported />" guard now narrows the strings while still
performing the runtime "is this a known provider/feature?" check.
* ci: apply automated fixes
* fix: address CodeRabbit review comments on PR #512
Closes 5 review items in one batch:
- ChatFetcher return type now accepts Response/AsyncIterable directly
(not just Promise<...>), so async generator fetchers work without
`as unknown as ChatFetcher` casts. Drops 6 such casts from tests.
- Rename DistributedOmit type parameters O/K → TObject/TKeys to satisfy
the project's @typescript-eslint/naming-convention rule (2 CI lint
errors).
- StreamTruncatedError in readStreamLines no longer fires when the
consumer aborts mid-line — user-initiated stop() is expected, not a
truncation bug.
- validateSearch in $feature.tsx whitelists mode against the allowed
Mode union instead of casting arbitrary URL strings.
- Fix rendered code snippet in server-fn-chat.tsx — the documented
shape now matches the actual `chatFn({ data: { messages }, signal })`
call.
- Add a fetcher-specific e2e assertion: the route sends a sentinel
`x-tanstack-ai-transport: fetcher` header, and chat.spec.ts waits
for a request carrying it. Without this, a silent fallback to the
connection adapter would still pass the response assertion.
Verified:
- pnpm test:lib: 26/26
- pnpm test:eslint: 26/26
- pnpm --filter @tanstack/ai-e2e test:e2e: 196/196fetcher option to ChatClient/useChat (#512)1 parent 3c6e616 commit ad23da9
27 files changed
Lines changed: 1159 additions & 144 deletions
File tree
- .changeset
- examples/ts-react-chat/src
- components
- lib
- routes
- packages/typescript
- ai-client
- src
- tests
- ai-preact/src
- ai-react
- src
- tests
- ai-solid/src
- ai-svelte/src
- ai-vue/src
- testing/e2e
- src/routes/$provider
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
14 | 15 | | |
15 | 16 | | |
16 | 17 | | |
| |||
210 | 211 | | |
211 | 212 | | |
212 | 213 | | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
213 | 227 | | |
214 | 228 | | |
215 | 229 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
| |||
10 | 11 | | |
11 | 12 | | |
12 | 13 | | |
13 | | - | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
14 | 21 | | |
15 | 22 | | |
16 | 23 | | |
| |||
375 | 382 | | |
376 | 383 | | |
377 | 384 | | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
12 | 13 | | |
13 | 14 | | |
14 | 15 | | |
| |||
34 | 35 | | |
35 | 36 | | |
36 | 37 | | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
37 | 43 | | |
38 | 44 | | |
39 | 45 | | |
| |||
163 | 169 | | |
164 | 170 | | |
165 | 171 | | |
| 172 | + | |
166 | 173 | | |
167 | 174 | | |
168 | 175 | | |
| |||
189 | 196 | | |
190 | 197 | | |
191 | 198 | | |
| 199 | + | |
192 | 200 | | |
193 | 201 | | |
194 | 202 | | |
| |||
216 | 224 | | |
217 | 225 | | |
218 | 226 | | |
| 227 | + | |
219 | 228 | | |
220 | 229 | | |
221 | 230 | | |
| |||
244 | 253 | | |
245 | 254 | | |
246 | 255 | | |
| 256 | + | |
247 | 257 | | |
248 | 258 | | |
249 | 259 | | |
| |||
270 | 280 | | |
271 | 281 | | |
272 | 282 | | |
| 283 | + | |
273 | 284 | | |
274 | 285 | | |
275 | 286 | | |
| |||
296 | 307 | | |
297 | 308 | | |
298 | 309 | | |
| 310 | + | |
299 | 311 | | |
300 | 312 | | |
301 | 313 | | |
| |||
323 | 335 | | |
324 | 336 | | |
325 | 337 | | |
| 338 | + | |
326 | 339 | | |
327 | 340 | | |
328 | 341 | | |
| |||
347 | 360 | | |
348 | 361 | | |
349 | 362 | | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
350 | 370 | | |
351 | 371 | | |
352 | 372 | | |
| |||
523 | 543 | | |
524 | 544 | | |
525 | 545 | | |
| 546 | + | |
526 | 547 | | |
527 | 548 | | |
528 | 549 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
0 commit comments