Skip to content

Commit 75c08db

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 3e04e96 commit 75c08db

File tree

20 files changed

+139
-74
lines changed

20 files changed

+139
-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

@@ -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 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 |
@@ -242,10 +242,10 @@ Import from `@ably/ai-transport/vercel/react`.
242242
243243
### useChatTransport
244244
245-
Create and memoize a `ChatTransport` for Vercel's `useChat` hook.
245+
Create and memoize a `ChatTransport` and its underlying `ClientTransport` for Vercel's `useChat` hook.
246246
247247
```typescript
248-
const chatTransport = useChatTransport(
248+
const { chatTransport, transport } = useChatTransport(
249249
transportOrOptions: ClientTransport<UIMessageChunk, UIMessage> | VercelClientTransportOptions,
250250
chatOptions?: ChatTransportOptions,
251251
);
@@ -256,7 +256,12 @@ const chatTransport = useChatTransport(
256256
| `transportOrOptions` | `ClientTransport \| VercelClientTransportOptions` | An existing transport, or options to create one |
257257
| `chatOptions` | `ChatTransportOptions?` | Optional hooks for customizing request construction |
258258
259-
**Returns:** `ChatTransport` - compatible with `useChat`'s `transport` option.
259+
**Returns:** `ChatTransportHandle`
260+
261+
| Property | Type | Description |
262+
|---|---|---|
263+
| `chatTransport` | `ChatTransport` | Compatible with `useChat`'s `transport` option |
264+
| `transport` | `ClientTransport<UIMessageChunk, UIMessage>` | The underlying transport for use with `useMessageSync`, `useActiveTurns`, `useView`, etc. |
260265
261266
Two usage patterns:
262267
1. **Wrap an existing transport** - pass a `ClientTransport` created by `useClientTransport`
@@ -265,7 +270,7 @@ Two usage patterns:
265270
`ChatTransportOptions.prepareSendMessagesRequest` lets you customize the HTTP POST body and headers:
266271
267272
```typescript
268-
const chatTransport = useChatTransport(transport, {
273+
const { chatTransport } = useChatTransport(transport, {
269274
prepareSendMessagesRequest: (context) => ({
270275
body: { history: context.history, sessionId: mySessionId },
271276
headers: { 'x-custom': 'value' },

0 commit comments

Comments
 (0)