Skip to content

Commit fc0380b

Browse files
feat (ui): resolvable header, body, credentials in http chat transport (#7154)
## Background For use cases such as credentials, it is important that the values of body, headers, credentials can get resolved at request execution time if needed. ## Summary Switch from fixed object to resolvables. ## Related Issues Resolves #7109 Addresses #7147 --------- Co-authored-by: nicoalbanese <gcalbanese96@gmail.com>
1 parent e03f635 commit fc0380b

File tree

5 files changed

+198
-18
lines changed

5 files changed

+198
-18
lines changed

‎.changeset/flat-falcons-happen.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'ai': patch
3+
---
4+
5+
feat (ui): resolvable header, body, credentials in http chat transport

‎content/docs/04-ai-sdk-ui/02-chatbot.mdx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,25 @@ const { messages, sendMessage } = useChat({
369369
});
370370
```
371371

372+
#### Dynamic Hook-Level Configuration
373+
374+
You can also provide functions that return configuration values. This is useful for authentication tokens that need to be refreshed, or for configuration that depends on runtime conditions:
375+
376+
```tsx
377+
const { messages, sendMessage } = useChat({
378+
api: '/api/chat',
379+
headers: () => ({
380+
Authorization: `Bearer ${getAuthToken()}`,
381+
'X-User-ID': getCurrentUserId(),
382+
}),
383+
body: () => ({
384+
sessionId: getCurrentSessionId(),
385+
preferences: getUserPreferences(),
386+
}),
387+
credentials: () => 'include',
388+
});
389+
```
390+
372391
#### Request-Level Configuration (Recommended)
373392

374393
<Note>

‎content/docs/04-ai-sdk-ui/21-transport.mdx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,27 @@ const { messages, sendMessage } = useChat({
5151
});
5252
```
5353

54+
### Dynamic Configuration
55+
56+
You can also provide functions that return configuration values. This is useful for authentication tokens that need to be refreshed, or for configuration that depends on runtime conditions:
57+
58+
```tsx
59+
const { messages, sendMessage } = useChat({
60+
transport: new DefaultChatTransport({
61+
api: '/api/chat',
62+
headers: () => ({
63+
Authorization: `Bearer ${getAuthToken()}`,
64+
'X-User-ID': getCurrentUserId(),
65+
}),
66+
body: () => ({
67+
sessionId: getCurrentSessionId(),
68+
preferences: getUserPreferences(),
69+
}),
70+
credentials: () => 'include',
71+
}),
72+
});
73+
```
74+
5475
### Request Transformation
5576

5677
Transform requests before sending to your API:

‎packages/ai/src/ui/http-chat-transport.test.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,114 @@ describe('HttpChatTransport', () => {
6969
}
7070
`);
7171
});
72+
73+
it('should include the body in the request when a function is provided', async () => {
74+
server.urls['http://localhost/api/chat'].response = {
75+
type: 'stream-chunks',
76+
chunks: [],
77+
};
78+
79+
const transport = new MockHttpChatTransport({
80+
api: 'http://localhost/api/chat',
81+
body: () => ({ someData: true }),
82+
});
83+
84+
await transport.sendMessages({
85+
chatId: 'c123',
86+
messageId: 'm123',
87+
trigger: 'submit-user-message',
88+
messages: [
89+
{
90+
id: 'm123',
91+
role: 'user',
92+
parts: [{ text: 'Hello, world!', type: 'text' }],
93+
},
94+
],
95+
abortSignal: new AbortController().signal,
96+
});
97+
98+
expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(`
99+
{
100+
"id": "c123",
101+
"messageId": "m123",
102+
"messages": [
103+
{
104+
"id": "m123",
105+
"parts": [
106+
{
107+
"text": "Hello, world!",
108+
"type": "text",
109+
},
110+
],
111+
"role": "user",
112+
},
113+
],
114+
"someData": true,
115+
"trigger": "submit-user-message",
116+
}
117+
`);
118+
});
119+
});
120+
121+
describe('headers', () => {
122+
it('should include headers in the request by default', async () => {
123+
server.urls['http://localhost/api/chat'].response = {
124+
type: 'stream-chunks',
125+
chunks: [],
126+
};
127+
128+
const transport = new MockHttpChatTransport({
129+
api: 'http://localhost/api/chat',
130+
headers: { 'X-Test-Header': 'test-value' },
131+
});
132+
133+
await transport.sendMessages({
134+
chatId: 'c123',
135+
messageId: 'm123',
136+
trigger: 'submit-user-message',
137+
messages: [
138+
{
139+
id: 'm123',
140+
role: 'user',
141+
parts: [{ text: 'Hello, world!', type: 'text' }],
142+
},
143+
],
144+
abortSignal: new AbortController().signal,
145+
});
146+
147+
expect(server.calls[0].requestHeaders['x-test-header']).toBe(
148+
'test-value',
149+
);
150+
});
151+
152+
it('should include headers in the request when a function is provided', async () => {
153+
server.urls['http://localhost/api/chat'].response = {
154+
type: 'stream-chunks',
155+
chunks: [],
156+
};
157+
158+
const transport = new MockHttpChatTransport({
159+
api: 'http://localhost/api/chat',
160+
headers: () => ({ 'X-Test-Header': 'test-value-fn' }),
161+
});
162+
163+
await transport.sendMessages({
164+
chatId: 'c123',
165+
messageId: 'm123',
166+
trigger: 'submit-user-message',
167+
messages: [
168+
{
169+
id: 'm123',
170+
role: 'user',
171+
parts: [{ text: 'Hello, world!', type: 'text' }],
172+
},
173+
],
174+
abortSignal: new AbortController().signal,
175+
});
176+
177+
expect(server.calls[0].requestHeaders['x-test-header']).toBe(
178+
'test-value-fn',
179+
);
180+
});
72181
});
73182
});

‎packages/ai/src/ui/http-chat-transport.ts

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FetchFunction } from '@ai-sdk/provider-utils';
1+
import { FetchFunction, Resolvable, resolve } from '@ai-sdk/provider-utils';
22
import { UIMessageChunk } from '../ui-message-stream/ui-message-chunks';
33
import { ChatTransport } from './chat-transport';
44
import { UIMessage } from './ui-messages';
@@ -52,20 +52,29 @@ export type PrepareReconnectToStreamRequest = (options: {
5252
api?: string;
5353
}>;
5454

55+
/**
56+
* Options for the `HttpChatTransport` class.
57+
*
58+
* @param UI_MESSAGE - The type of message to be used in the chat.
59+
*/
5560
export type HttpChatTransportInitOptions<UI_MESSAGE extends UIMessage> = {
61+
/**
62+
* The API URL to be used for the chat transport.
63+
* Defaults to '/api/chat'.
64+
*/
5665
api?: string;
5766

5867
/**
5968
* The credentials mode to be used for the fetch request.
6069
* Possible values are: 'omit', 'same-origin', 'include'.
6170
* Defaults to 'same-origin'.
6271
*/
63-
credentials?: RequestCredentials;
72+
credentials?: Resolvable<RequestCredentials>;
6473

6574
/**
6675
* HTTP headers to be sent with the API request.
6776
*/
68-
headers?: Record<string, string> | Headers;
77+
headers?: Resolvable<Record<string, string> | Headers>;
6978

7079
/**
7180
* Extra body object to be sent with the API request.
@@ -79,7 +88,7 @@ export type HttpChatTransportInitOptions<UI_MESSAGE extends UIMessage> = {
7988
* })
8089
* ```
8190
*/
82-
body?: object;
91+
body?: Resolvable<object>;
8392

8493
/**
8594
Custom fetch implementation. You can use it as a middleware to intercept requests,
@@ -98,16 +107,25 @@ export type HttpChatTransportInitOptions<UI_MESSAGE extends UIMessage> = {
98107
*/
99108
prepareSendMessagesRequest?: PrepareSendMessagesRequest<UI_MESSAGE>;
100109

110+
/**
111+
* When a function is provided, it will be used
112+
* to prepare the request body for the chat API. This can be useful for
113+
* customizing the request body based on the messages and data in the chat.
114+
*
115+
* @param id The id of the chat.
116+
* @param messages The current messages in the chat.
117+
* @param requestBody The request body object passed in the chat request.
118+
*/
101119
prepareReconnectToStreamRequest?: PrepareReconnectToStreamRequest;
102120
};
103121

104122
export abstract class HttpChatTransport<UI_MESSAGE extends UIMessage>
105123
implements ChatTransport<UI_MESSAGE>
106124
{
107125
protected api: string;
108-
protected credentials?: RequestCredentials;
109-
protected headers?: Record<string, string> | Headers;
110-
protected body?: object;
126+
protected credentials: HttpChatTransportInitOptions<UI_MESSAGE>['credentials'];
127+
protected headers: HttpChatTransportInitOptions<UI_MESSAGE>['headers'];
128+
protected body: HttpChatTransportInitOptions<UI_MESSAGE>['body'];
111129
protected fetch?: FetchFunction;
112130
protected prepareSendMessagesRequest?: PrepareSendMessagesRequest<UI_MESSAGE>;
113131
protected prepareReconnectToStreamRequest?: PrepareReconnectToStreamRequest;
@@ -134,13 +152,17 @@ export abstract class HttpChatTransport<UI_MESSAGE extends UIMessage>
134152
abortSignal,
135153
...options
136154
}: Parameters<ChatTransport<UI_MESSAGE>['sendMessages']>[0]) {
155+
const resolvedBody = await resolve(this.body);
156+
const resolvedHeaders = await resolve(this.headers);
157+
const resolvedCredentials = await resolve(this.credentials);
158+
137159
const preparedRequest = await this.prepareSendMessagesRequest?.({
138160
api: this.api,
139161
id: options.chatId,
140162
messages: options.messages,
141-
body: { ...this.body, ...options.body },
142-
headers: { ...this.headers, ...options.headers },
143-
credentials: this.credentials,
163+
body: { ...resolvedBody, ...options.body },
164+
headers: { ...resolvedHeaders, ...options.headers },
165+
credentials: resolvedCredentials,
144166
requestMetadata: options.metadata,
145167
trigger: options.trigger,
146168
messageId: options.messageId,
@@ -150,19 +172,19 @@ export abstract class HttpChatTransport<UI_MESSAGE extends UIMessage>
150172
const headers =
151173
preparedRequest?.headers !== undefined
152174
? preparedRequest.headers
153-
: { ...this.headers, ...options.headers };
175+
: { ...resolvedHeaders, ...options.headers };
154176
const body =
155177
preparedRequest?.body !== undefined
156178
? preparedRequest.body
157179
: {
158-
...this.body,
180+
...resolvedBody,
159181
...options.body,
160182
id: options.chatId,
161183
messages: options.messages,
162184
trigger: options.trigger,
163185
messageId: options.messageId,
164186
};
165-
const credentials = preparedRequest?.credentials ?? this.credentials;
187+
const credentials = preparedRequest?.credentials ?? resolvedCredentials;
166188

167189
// avoid caching globalThis.fetch in case it is patched by other libraries
168190
const fetch = this.fetch ?? globalThis.fetch;
@@ -194,21 +216,25 @@ export abstract class HttpChatTransport<UI_MESSAGE extends UIMessage>
194216
async reconnectToStream(
195217
options: Parameters<ChatTransport<UI_MESSAGE>['reconnectToStream']>[0],
196218
): Promise<ReadableStream<UIMessageChunk> | null> {
219+
const resolvedBody = await resolve(this.body);
220+
const resolvedHeaders = await resolve(this.headers);
221+
const resolvedCredentials = await resolve(this.credentials);
222+
197223
const preparedRequest = await this.prepareReconnectToStreamRequest?.({
198224
api: this.api,
199225
id: options.chatId,
200-
body: { ...this.body, ...options.body },
201-
headers: { ...this.headers, ...options.headers },
202-
credentials: this.credentials,
226+
body: { ...resolvedBody, ...options.body },
227+
headers: { ...resolvedHeaders, ...options.headers },
228+
credentials: resolvedCredentials,
203229
requestMetadata: options.metadata,
204230
});
205231

206232
const api = preparedRequest?.api ?? `${this.api}/${options.chatId}/stream`;
207233
const headers =
208234
preparedRequest?.headers !== undefined
209235
? preparedRequest.headers
210-
: { ...this.headers, ...options.headers };
211-
const credentials = preparedRequest?.credentials ?? this.credentials;
236+
: { ...resolvedHeaders, ...options.headers };
237+
const credentials = preparedRequest?.credentials ?? resolvedCredentials;
212238

213239
// avoid caching globalThis.fetch in case it is patched by other libraries
214240
const fetch = this.fetch ?? globalThis.fetch;

0 commit comments

Comments
 (0)