Skip to content

Commit 0ee595e

Browse files
transport: move /api/chat default from core to Vercel layer
The default HTTP endpoint "/api/chat" is the Vercel AI SDK's default (set by DefaultChatTransport), but we were setting it in our generic core transport. This violates the two-layer architecture principle that the generic layer should know nothing about Vercel. Make `api` required on core `ClientTransportOptions` and apply the "/api/chat" default in the Vercel layer's `createClientTransport` factory. To avoid forcing useChat users to call `useClientTransport` (where `api` would now be required), `useChatTransport` now returns a `ChatTransportHandle` containing both the `ChatTransport` and the underlying `ClientTransport`. Previously it only returned the `ChatTransport`, so callers had no way to access the transport it created internally — they needed a separate `useClientTransport` call. The handle lets useChat users get the Vercel default without extra configuration, while still having access to the transport for `useMessageSync`, `useActiveTurns`, `useView`, etc. [AIT-676] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c28c65f commit 0ee595e

File tree

20 files changed

+138
-74
lines changed

20 files changed

+138
-74
lines changed

README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,15 +144,13 @@ export async function POST(req: Request) {
144144

145145
import { useChat } from '@ai-sdk/react';
146146
import { useChannel } from 'ably/react';
147-
import { useClientTransport, useActiveTurns, useView } from '@ably/ai-transport/react';
147+
import { useActiveTurns, useView } from '@ably/ai-transport/react';
148148
import { useChatTransport, useMessageSync } from '@ably/ai-transport/vercel/react';
149-
import { UIMessageCodec } from '@ably/ai-transport/vercel';
150149

151150
function Chat({ chatId, clientId }: { chatId: string; clientId?: string }) {
152151
const { channel } = useChannel({ channelName: chatId });
153152

154-
const transport = useClientTransport({ channel, codec: UIMessageCodec, clientId });
155-
const chatTransport = useChatTransport(transport);
153+
const { chatTransport, transport } = useChatTransport({ channel, clientId });
156154

157155
const { messages, setMessages, sendMessage, stop } = useChat({
158156
id: chatId,

demo/vercel/react/use-chat/src/app/chat.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22

33
import { useChat } from '@ai-sdk/react';
44
import { useChannel } from 'ably/react';
5-
import { useClientTransport, useActiveTurns, useView, useAblyMessages } from '@ably/ai-transport/react';
5+
import { useActiveTurns, useView, useAblyMessages } from '@ably/ai-transport/react';
66
import { useChatTransport, useMessageSync } from '@ably/ai-transport/vercel/react';
7-
import { UIMessageCodec } from '@ably/ai-transport/vercel';
87
import { useState } from 'react';
98
import { MessageList } from './components/message-list';
109
import { DebugPane } from './components/debug-pane';
@@ -18,8 +17,7 @@ export function Chat({ chatId, clientId, historyLimit }: { chatId: string; clien
1817
const { channel } = useChannel({ channelName: chatId });
1918

2019
// Create transport immediately (subscribes before attach — RTL7g)
21-
const transport = useClientTransport({ channel, codec: UIMessageCodec, clientId });
22-
const chatTransport = useChatTransport(transport);
20+
const { chatTransport, transport } = useChatTransport({ channel, clientId });
2321

2422
const {
2523
setMessages,

demo/vercel/react/use-client-transport/src/app/components/chat.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export function Chat({ chatId, clientId, historyLimit }: ChatProps) {
3737
channel,
3838
codec: UIMessageCodec,
3939
clientId,
40+
api: '/api/chat',
4041
body: () => ({ id: chatId }),
4142
});
4243

docs/concepts/transport.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ In React, the hooks handle subscriptions and state management:
8383
import { useClientTransport, useView } from '@ably/ai-transport/react';
8484
import { UIMessageCodec } from '@ably/ai-transport/vercel';
8585

86-
const transport = useClientTransport({ channel, codec: UIMessageCodec, clientId });
86+
const transport = useClientTransport({ channel, codec: UIMessageCodec, clientId, api: '/api/chat' });
8787
const { nodes, send } = useView(transport);
8888
```
8989

docs/features/branching.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ With a single view, navigating to a different branch in one part of the UI chang
135135
```typescript
136136
import { useClientTransport, useCreateView, useView } from '@ably/ai-transport/react';
137137

138-
const transport = useClientTransport({ channel, codec, clientId });
138+
const transport = useClientTransport({ channel, codec, clientId, api: '/api/chat' });
139139

140140
// Default view for the left pane
141141
const left = useView(transport, { limit: 50 });

docs/features/multi-client.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ No special API is needed. Connect two clients to the same channel name, and mess
1515

1616
```typescript
1717
// Client A
18-
const transportA = useClientTransport({ channel, codec: UIMessageCodec, clientId: 'user-a' });
18+
const transportA = useClientTransport({ channel, codec: UIMessageCodec, clientId: 'user-a', api: '/api/chat' });
1919

2020
// Client B (different browser tab, device, or user)
21-
const transportB = useClientTransport({ channel, codec: UIMessageCodec, clientId: 'user-b' });
21+
const transportB = useClientTransport({ channel, codec: UIMessageCodec, clientId: 'user-b', api: '/api/chat' });
2222

2323
// When Client A sends a message and the server streams a response,
2424
// Client B sees both the user message and the assistant response

docs/frameworks/vercel-ai-sdk.md

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,10 @@ The Vercel AI SDK provides model abstraction, streaming primitives, and React ho
2020
Wrap the transport in a `ChatTransport` adapter and pass it to Vercel's `useChat()`. Message state is managed by `useChat()` - the transport delivers messages over Ably instead of HTTP.
2121

2222
```typescript
23-
import { useClientTransport } from '@ably/ai-transport/react';
2423
import { useChatTransport, useMessageSync } from '@ably/ai-transport/vercel/react';
25-
import { UIMessageCodec } from '@ably/ai-transport/vercel';
2624
import { useChat } from '@ai-sdk/react';
2725

28-
const transport = useClientTransport({ channel, codec: UIMessageCodec, clientId });
29-
const chatTransport = useChatTransport(transport);
26+
const { chatTransport, transport } = useChatTransport({ channel, clientId });
3027

3128
const { messages, setMessages, sendMessage, stop } = useChat({
3229
id: chatId,
@@ -37,7 +34,7 @@ const { messages, setMessages, sendMessage, stop } = useChat({
3734
useMessageSync(transport, setMessages);
3835
```
3936

40-
`useChatTransport()` wraps the core transport into the `ChatTransport` interface that `useChat()` expects. `useMessageSync()` pushes the transport's authoritative message list into `useChat()`'s state - this is how messages from other clients appear.
37+
`useChatTransport()` creates the core transport and wraps it into the `ChatTransport` interface that `useChat()` expects, returning both as `{ chatTransport, transport }`. `useMessageSync()` pushes the transport's authoritative message list into `useChat()`'s state - this is how messages from other clients appear.
4138

4239
### Generic hooks path (more control)
4340

@@ -47,7 +44,7 @@ Use the generic React hooks directly. You manage message state through the trans
4744
import { useClientTransport, useView, useActiveTurns } from '@ably/ai-transport/react';
4845
import { UIMessageCodec } from '@ably/ai-transport/vercel';
4946

50-
const transport = useClientTransport({ channel, codec: UIMessageCodec, clientId });
47+
const transport = useClientTransport({ channel, codec: UIMessageCodec, clientId, api: '/api/chat' });
5148
const {
5249
nodes,
5350
hasOlder,

docs/get-started/vercel-use-chat.md

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -155,36 +155,32 @@ Wire up `useChat()` with the AI Transport hooks:
155155

156156
import { useChat } from '@ai-sdk/react';
157157
import { useChannel, ChannelProvider } from 'ably/react';
158-
import { useClientTransport, useActiveTurns, useView } from '@ably/ai-transport/react';
158+
import { useActiveTurns, useView } from '@ably/ai-transport/react';
159159
import { useChatTransport, useMessageSync } from '@ably/ai-transport/vercel/react';
160-
import { UIMessageCodec } from '@ably/ai-transport/vercel';
161160
import { useState } from 'react';
162161

163162
function ChatInner({ chatId, clientId }: { chatId: string; clientId?: string }) {
164163
const { channel } = useChannel({ channelName: chatId });
165164
const [input, setInput] = useState('');
166165

167-
// 1. Create the core transport - subscribes to the Ably channel and decodes
168-
// incoming messages through UIMessageCodec
169-
const transport = useClientTransport({ channel, codec: UIMessageCodec, clientId });
166+
// 1. Create the transport and wrap it for useChat - subscribes to the
167+
// Ably channel and decodes incoming messages through UIMessageCodec
168+
const { chatTransport, transport } = useChatTransport({ channel, clientId });
170169

171-
// 2. Wrap it for useChat compatibility
172-
const chatTransport = useChatTransport(transport);
173-
174-
// 3. Use Vercel's useChat with the wrapped transport
170+
// 2. Use Vercel's useChat with the wrapped transport
175171
const { messages, setMessages, sendMessage, stop } = useChat({
176172
id: chatId,
177173
transport: chatTransport,
178174
});
179175

180-
// 4. Sync transport messages into useChat's state (for observer messages)
176+
// 3. Sync transport messages into useChat's state (for observer messages)
181177
useMessageSync(transport, setMessages);
182178

183-
// 5. Track active turns for loading state
179+
// 4. Track active turns for loading state
184180
const activeTurns = useActiveTurns(transport);
185181
const isStreaming = activeTurns.size > 0;
186182

187-
// 6. Load history on mount
183+
// 5. Load history on mount
188184
useView(transport, { limit: 30 });
189185

190186
return (
@@ -245,12 +241,11 @@ Open `http://localhost:3000`. Type a message - you'll see tokens stream in real
245241

246242
## What's happening
247243

248-
1. `useClientTransport()` creates a transport that subscribes to the Ably channel before it attaches - no messages are lost.
249-
2. `useChatTransport()` wraps the transport into Vercel's `ChatTransport` interface, which `useChat()` expects.
250-
3. When you send a message, `useChat()` calls the chat transport's `sendMessages()`, which fires an HTTP POST to `/api/chat` and opens a stream on the Ably channel.
251-
4. The server creates a turn, publishes user messages, streams the LLM response through the encoder to the channel, and publishes a turn-end event.
252-
5. The client transport decodes incoming Ably messages through `UIMessageCodec` and routes them to the stream.
253-
6. `useMessageSync()` syncs messages from the transport (including messages from other clients) into `useChat()`'s state.
244+
1. `useChatTransport()` creates a transport that subscribes to the Ably channel before it attaches - no messages are lost. It wraps the transport into Vercel's `ChatTransport` interface, which `useChat()` expects, and returns both.
245+
2. When you send a message, `useChat()` calls the chat transport's `sendMessages()`, which fires an HTTP POST to `/api/chat` and opens a stream on the Ably channel.
246+
3. The server creates a turn, publishes user messages, streams the LLM response through the encoder to the channel, and publishes a turn-end event.
247+
4. The client transport decodes incoming Ably messages through `UIMessageCodec` and routes them to the stream.
248+
5. `useMessageSync()` syncs messages from the transport (including messages from other clients) into `useChat()`'s state.
254249

255250
For the conceptual details, see [Client and server transport](../concepts/transport.md) and [Turns](../concepts/turns.md).
256251

docs/get-started/vercel-use-client-transport.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ function ChatInner({ chatId, clientId }: { chatId: string; clientId?: string })
3737
channel,
3838
codec: UIMessageCodec,
3939
clientId,
40+
api: '/api/chat',
4041
body: () => ({ id: chatId }),
4142
});
4243

docs/reference/react-hooks.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const transport = useClientTransport<TEvent, TMessage>(options: ClientTransportO
2121
| `options.channel` | `Ably.RealtimeChannel` | The Ably channel to subscribe to |
2222
| `options.codec` | `Codec<TEvent, TMessage>` | The codec for encoding/decoding |
2323
| `options.clientId` | `string?` | Client identity, sent to the server in POST body |
24-
| `options.api` | `string?` | Server endpoint URL. Default: `"/api/chat"` |
24+
| `options.api` | `string` | Server endpoint URL (required) |
2525
| `options.headers` | `Record<string, string> \| (() => Record<string, string>)?` | HTTP POST headers. Function form for dynamic values |
2626
| `options.body` | `Record<string, unknown> \| (() => Record<string, unknown>)?` | Additional POST body fields. Function form for dynamic values |
2727
| `options.credentials` | `RequestCredentials?` | Fetch credentials mode |
@@ -243,10 +243,10 @@ Import from `@ably/ai-transport/vercel/react`.
243243
244244
### useChatTransport
245245
246-
Create and memoize a `ChatTransport` for Vercel's `useChat()` hook.
246+
Create and memoize a `ChatTransport` and its underlying `ClientTransport` for Vercel's `useChat()` hook.
247247
248248
```typescript
249-
const chatTransport = useChatTransport(
249+
const { chatTransport, transport } = useChatTransport(
250250
transportOrOptions: ClientTransport<UIMessageChunk, UIMessage> | VercelClientTransportOptions,
251251
chatOptions?: ChatTransportOptions,
252252
);
@@ -257,7 +257,12 @@ const chatTransport = useChatTransport(
257257
| `transportOrOptions` | `ClientTransport \| VercelClientTransportOptions` | An existing transport, or options to create one |
258258
| `chatOptions` | `ChatTransportOptions?` | Optional hooks for customizing request construction |
259259
260-
**Returns:** `ChatTransport` - compatible with `useChat()`'s `transport` option.
260+
**Returns:** `ChatTransportHandle`
261+
262+
| Property | Type | Description |
263+
| --------------- | -------------------------------------------- | ----------------------------------------------------------------------------------------- |
264+
| `chatTransport` | `ChatTransport` | Compatible with `useChat()`'s `transport` option |
265+
| `transport` | `ClientTransport<UIMessageChunk, UIMessage>` | The underlying transport for use with `useMessageSync`, `useActiveTurns`, `useView`, etc. |
261266
262267
Two usage patterns:
263268
@@ -267,7 +272,7 @@ Two usage patterns:
267272
`ChatTransportOptions.prepareSendMessagesRequest` lets you customize the HTTP POST body and headers:
268273
269274
```typescript
270-
const chatTransport = useChatTransport(transport, {
275+
const { chatTransport } = useChatTransport(transport, {
271276
prepareSendMessagesRequest: (context) => ({
272277
body: { history: context.history, sessionId: mySessionId },
273278
headers: { 'x-custom': 'value' },

0 commit comments

Comments
 (0)