Skip to content

Commit e3729c8

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 bd3175e commit e3729c8

File tree

20 files changed

+160
-82
lines changed

20 files changed

+160
-82
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';
@@ -17,8 +16,7 @@ export function Chat({ chatId, clientId, historyLimit }: { chatId: string; clien
1716
const { channel } = useChannel({ channelName: chatId });
1817

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

2321
const { setMessages, sendMessage, stop, status, regenerate } = useChat({
2422
id: chatId,

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
@@ -28,6 +28,7 @@ export function Chat({ chatId, clientId, historyLimit }: ChatProps) {
2828
channel,
2929
codec: UIMessageCodec,
3030
clientId,
31+
api: '/api/chat',
3132
body: () => ({ id: chatId }),
3233
});
3334

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

@@ -51,7 +48,7 @@ import {
5148
} from '@ably/ai-transport/react';
5249
import { UIMessageCodec } from '@ably/ai-transport/vercel';
5350

54-
const transport = useClientTransport({ channel, codec: UIMessageCodec, clientId });
51+
const transport = useClientTransport({ channel, codec: UIMessageCodec, clientId, api: '/api/chat' });
5552
const { nodes, hasOlder, loading, loadOlder, send, regenerate, edit, select, getSelectedIndex, getSiblings, hasSiblings } = useView(transport, { limit: 30 });
5653
const activeTurns = useActiveTurns(transport);
5754
```

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 chat adapter in one step - 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 chat transport adapter
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), and wraps it into Vercel's `ChatTransport` interface, which `useChat` expects. It returns both the `chatTransport` adapter and the underlying `transport`.
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: 20 additions & 12 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 |
@@ -239,30 +239,38 @@ Import from `@ably/ai-transport/vercel/react`.
239239
240240
### useChatTransport
241241
242-
Create and memoize a `ChatTransport` for Vercel's `useChat` hook.
242+
Create and memoize a `ChatTransport` and its underlying `ClientTransport` for Vercel's `useChat` hook.
243243
244244
```typescript
245-
const chatTransport = useChatTransport(
246-
transportOrOptions: ClientTransport<UIMessageChunk, UIMessage> | VercelClientTransportOptions,
245+
// From options — creates the transport internally
246+
const { chatTransport, transport } = useChatTransport(
247+
options: VercelClientTransportOptions,
248+
chatOptions?: ChatTransportOptions,
249+
);
250+
251+
// From an existing transport — wraps it
252+
const { chatTransport, transport } = useChatTransport(
253+
transport: ClientTransport<UIMessageChunk, UIMessage>,
247254
chatOptions?: ChatTransportOptions,
248255
);
249256
```
250257
251258
| Parameter | Type | Description |
252259
|---|---|---|
253-
| `transportOrOptions` | `ClientTransport \| VercelClientTransportOptions` | An existing transport, or options to create one |
254-
| `chatOptions` | `ChatTransportOptions?` | Optional hooks for customizing request construction |
260+
| `transportOrOptions` | `ClientTransport \| VercelClientTransportOptions` | An existing transport to wrap, or options to create one |
261+
| `chatOptions` | `ChatTransportOptions?` | Optional hooks for customising request construction |
255262
256-
**Returns:** `ChatTransport` - compatible with `useChat`'s `transport` option.
263+
**Returns:** `ChatTransportHandle`
257264
258-
Two usage patterns:
259-
1. **Wrap an existing transport** - pass a `ClientTransport` created by `useClientTransport`
260-
2. **Create internally** - pass `VercelClientTransportOptions` and the hook creates the transport with `UIMessageCodec`
265+
| Property | Type | Description |
266+
|---|---|---|
267+
| `chatTransport` | `ChatTransport` | Compatible with `useChat`'s `transport` option |
268+
| `transport` | `ClientTransport<UIMessageChunk, UIMessage>` | The underlying transport for use with `useMessageSync`, `useActiveTurns`, `useView`, etc. |
261269
262-
`ChatTransportOptions.prepareSendMessagesRequest` lets you customize the HTTP POST body and headers:
270+
`ChatTransportOptions.prepareSendMessagesRequest` lets you customise the HTTP POST body and headers:
263271
264272
```typescript
265-
const chatTransport = useChatTransport(transport, {
273+
const { chatTransport } = useChatTransport({ channel, clientId }, {
266274
prepareSendMessagesRequest: (context) => ({
267275
body: { history: context.history, sessionId: mySessionId },
268276
headers: { 'x-custom': 'value' },

0 commit comments

Comments
 (0)