Record Token Usage after Streaming User Interfaces
Learn how to record token usage after streaming user interfaces using the AI TOOLKIT and React Server Components
When you're streaming structured data with `streamUI`, you may want to record the token usage for billing purposes.
Run it locally
$ npm install ai`onFinish` Callback
You can use the onFinish callback to record token usage.
It is called when the stream is finished.
'use client';
import { useState } from 'react';
import { ClientMessage } from './actions';
import { useActions, useUIState } from '@ai-toolkit/rsc';
import { generateId } from 'ai-toolkit';
// Allow streaming responses up to 30 seconds
export const maxDuration = 30;
export default function Home() {
const [input, setInput] = useState<string>('');
const [conversation, setConversation] = useUIState();
const { continueConversation } = useActions();
return (
<div>
<div>
{conversation.map((message: ClientMessage) => (
<div key={message.id}>
{message.role}: {message.display}
</div>
))}
</div>
<div>
<input
type="text"
value={input}
onChange={event => {
setInput(event.target.value);
}}
/>
<button
onClick={async () => {
setConversation((currentConversation: ClientMessage[]) => [
...currentConversation,
{ id: generateId(), role: 'user', display: input },
]);
const message = await continueConversation(input);
setConversation((currentConversation: ClientMessage[]) => [
...currentConversation,
message,
]);
}}
>
Send Message
</button>
</div>
</div>
);
}
- 1'use client';
- 2import { useState } from 'react';
- 3import { ClientMessage } from './actions';
- 4import { useActions, useUIState } from '@ai-toolkit/rsc';
- 5import { generateId } from 'ai-toolkit';
- 6// Allow streaming responses up to 30 seconds
- 7export const maxDuration = 30;
- 8export default function Home() {
- 9 const [input, setInput] = useState<string>('');
- 10 const [conversation, setConversation] = useUIState();
- 11 const { continueConversation } = useActions();
- 12 return (
- 13 <div>
- 14 <div>
- 15 {conversation.map((message: ClientMessage) => (
- 16 <div key={message.id}>
- 17 {message.role}: {message.display}
- 18 </div>
- 19 ))}
- 20 </div>
- 21 <div>
- 22 <input
- 23 type="text"
- 24 value={input}
- 25 onChange={event => {
- 26 setInput(event.target.value);
- 27 }}
- 28 />
- 29 <button
- 30 onClick={async () => {
- 31 setConversation((currentConversation: ClientMessage[]) => [
- 32 ...currentConversation,
- 33 { id: generateId(), role: 'user', display: input },
- 34 ]);
- 35 const message = await continueConversation(input);
- 36 setConversation((currentConversation: ClientMessage[]) => [
- 37 ...currentConversation,
- 38 message,
- 39 ]);
- 40 }}
- 41 >
- 42 Send Message
- 43 </button>
- 44 </div>
- 45 </div>
- 46 );
- 47}
Server
'use server';
import { createAI, getMutableAIState, streamUI } from '@ai-toolkit/rsc';
import { openai } from '@ai-toolkit/openai';
import { ReactNode } from 'react';
import { z } from 'zod';
import { generateId } from 'ai-toolkit';
export interface ServerMessage {
role: 'user' | 'assistant';
content: string;
}
export interface ClientMessage {
id: string;
role: 'user' | 'assistant';
display: ReactNode;
}
export async function continueConversation(
input: string,
): Promise<ClientMessage> {
'use server';
const history = getMutableAIState();
const result = await streamUI({
model: openai('gpt-3.5-turbo'),
messages: [...history.get(), { role: 'user', content: input }],
text: ({ content, done }) => {
if (done) {
history.done((messages: ServerMessage[]) => [
...messages,
{ role: 'assistant', content },
]);
}
return <div>{content}</div>;
},
tools: {
deploy: {
description: 'Deploy repository to vercel',
inputSchema: z.object({
repositoryName: z
.string()
.describe('The name of the repository, example: vercel/ai-chatbot'),
}),
generate: async function* ({ repositoryName }) {
yield <div>Cloning repository {repositoryName}...</div>; // [!code highlight:5]
await new Promise(resolve => setTimeout(resolve, 3000));
yield <div>Building repository {repositoryName}...</div>;
await new Promise(resolve => setTimeout(resolve, 2000));
return <div>{repositoryName} deployed!</div>;
},
},
},
onFinish: ({ usage }) => {
const { promptTokens, completionTokens, totalTokens } = usage;
// your own logic, e.g. for saving the chat history or recording usage
console.log('Prompt tokens:', promptTokens);
console.log('Completion tokens:', completionTokens);
console.log('Total tokens:', totalTokens);
},
});
return {
id: generateId(),
role: 'assistant',
display: result.value,
};
}
import { createAI } from '@ai-toolkit/rsc';
import { ServerMessage, ClientMessage, continueConversation } from './actions';
export const AI = createAI<ServerMessage[], ClientMessage[]>({
actions: {
continueConversation,
},
initialAIState: [],
initialUIState: [],
});
- 1'use server';
- 2import { createAI, getMutableAIState, streamUI } from '@ai-toolkit/rsc';
- 3import { openai } from '@ai-toolkit/openai';
- 4import { ReactNode } from 'react';
- 5import { z } from 'zod';
- 6import { generateId } from 'ai-toolkit';
- 7export interface ServerMessage {
- 8 role: 'user' | 'assistant';
- 9 content: string;
- 10}
- 11export interface ClientMessage {
- 12 id: string;
- 13 role: 'user' | 'assistant';
- 14 display: ReactNode;
- 15}
- 16export async function continueConversation(
- 17 input: string,
- 18): Promise<ClientMessage> {
- 19 'use server';
- 20 const history = getMutableAIState();
- 21 const result = await streamUI({
- 22 model: openai('gpt-3.5-turbo'),
- 23 messages: [...history.get(), { role: 'user', content: input }],
- 24 text: ({ content, done }) => {
- 25 if (done) {
- 26 history.done((messages: ServerMessage[]) => [
- 27 ...messages,
- 28 { role: 'assistant', content },
- 29 ]);
- 30 }
- 31 return <div>{content}</div>;
- 32 },
- 33 tools: {
- 34 deploy: {
- 35 description: 'Deploy repository to vercel',
- 36 inputSchema: z.object({
- 37 repositoryName: z
- 38 .string()
- 39 .describe('The name of the repository, example: vercel/ai-chatbot'),
- 40 }),
- 41 generate: async function* ({ repositoryName }) {
- 42 yield <div>Cloning repository {repositoryName}...</div>; // [!code highlight:5]
- 43 await new Promise(resolve => setTimeout(resolve, 3000));
- 44 yield <div>Building repository {repositoryName}...</div>;
- 45 await new Promise(resolve => setTimeout(resolve, 2000));
- 46 return <div>{repositoryName} deployed!</div>;
- 47 },
- 48 },
- 49 },
- 50 onFinish: ({ usage }) => {
- 51 const { promptTokens, completionTokens, totalTokens } = usage;
- 52 // your own logic, e.g. for saving the chat history or recording usage
- 53 console.log('Prompt tokens:', promptTokens);
- 54 console.log('Completion tokens:', completionTokens);
- 55 console.log('Total tokens:', totalTokens);
- 56 },
- 57 });
- 58 return {
- 59 id: generateId(),
- 60 role: 'assistant',
- 61 display: result.value,
- 62 };
- 63}
- 1import { createAI } from '@ai-toolkit/rsc';
- 2import { ServerMessage, ClientMessage, continueConversation } from './actions';
- 3export const AI = createAI<ServerMessage[], ClientMessage[]>({
- 4 actions: {
- 5 continueConversation,
- 6 },
- 7 initialAIState: [],
- 8 initialUIState: [],
- 9});