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

3 min readrscusageView source

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>

);

}

app/page.tsx
tsx
  1. 1'use client';
  2. 2import { useState } from 'react';
  3. 3import { ClientMessage } from './actions';
  4. 4import { useActions, useUIState } from '@ai-toolkit/rsc';
  5. 5import { generateId } from 'ai-toolkit';
  6. 6// Allow streaming responses up to 30 seconds
  7. 7export const maxDuration = 30;
  8. 8export default function Home() {
  9. 9 const [input, setInput] = useState<string>('');
  10. 10 const [conversation, setConversation] = useUIState();
  11. 11 const { continueConversation } = useActions();
  12. 12 return (
  13. 13 <div>
  14. 14 <div>
  15. 15 {conversation.map((message: ClientMessage) => (
  16. 16 <div key={message.id}>
  17. 17 {message.role}: {message.display}
  18. 18 </div>
  19. 19 ))}
  20. 20 </div>
  21. 21 <div>
  22. 22 <input
  23. 23 type="text"
  24. 24 value={input}
  25. 25 onChange={event => {
  26. 26 setInput(event.target.value);
  27. 27 }}
  28. 28 />
  29. 29 <button
  30. 30 onClick={async () => {
  31. 31 setConversation((currentConversation: ClientMessage[]) => [
  32. 32 ...currentConversation,
  33. 33 { id: generateId(), role: 'user', display: input },
  34. 34 ]);
  35. 35 const message = await continueConversation(input);
  36. 36 setConversation((currentConversation: ClientMessage[]) => [
  37. 37 ...currentConversation,
  38. 38 message,
  39. 39 ]);
  40. 40 }}
  41. 41 >
  42. 42 Send Message
  43. 43 </button>
  44. 44 </div>
  45. 45 </div>
  46. 46 );
  47. 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: [],

});

app/actions.tsx
tsx
  1. 1'use server';
  2. 2import { createAI, getMutableAIState, streamUI } from '@ai-toolkit/rsc';
  3. 3import { openai } from '@ai-toolkit/openai';
  4. 4import { ReactNode } from 'react';
  5. 5import { z } from 'zod';
  6. 6import { generateId } from 'ai-toolkit';
  7. 7export interface ServerMessage {
  8. 8 role: 'user' | 'assistant';
  9. 9 content: string;
  10. 10}
  11. 11export interface ClientMessage {
  12. 12 id: string;
  13. 13 role: 'user' | 'assistant';
  14. 14 display: ReactNode;
  15. 15}
  16. 16export async function continueConversation(
  17. 17 input: string,
  18. 18): Promise<ClientMessage> {
  19. 19 'use server';
  20. 20 const history = getMutableAIState();
  21. 21 const result = await streamUI({
  22. 22 model: openai('gpt-3.5-turbo'),
  23. 23 messages: [...history.get(), { role: 'user', content: input }],
  24. 24 text: ({ content, done }) => {
  25. 25 if (done) {
  26. 26 history.done((messages: ServerMessage[]) => [
  27. 27 ...messages,
  28. 28 { role: 'assistant', content },
  29. 29 ]);
  30. 30 }
  31. 31 return <div>{content}</div>;
  32. 32 },
  33. 33 tools: {
  34. 34 deploy: {
  35. 35 description: 'Deploy repository to vercel',
  36. 36 inputSchema: z.object({
  37. 37 repositoryName: z
  38. 38 .string()
  39. 39 .describe('The name of the repository, example: vercel/ai-chatbot'),
  40. 40 }),
  41. 41 generate: async function* ({ repositoryName }) {
  42. 42 yield <div>Cloning repository {repositoryName}...</div>; // [!code highlight:5]
  43. 43 await new Promise(resolve => setTimeout(resolve, 3000));
  44. 44 yield <div>Building repository {repositoryName}...</div>;
  45. 45 await new Promise(resolve => setTimeout(resolve, 2000));
  46. 46 return <div>{repositoryName} deployed!</div>;
  47. 47 },
  48. 48 },
  49. 49 },
  50. 50 onFinish: ({ usage }) => {
  51. 51 const { promptTokens, completionTokens, totalTokens } = usage;
  52. 52 // your own logic, e.g. for saving the chat history or recording usage
  53. 53 console.log('Prompt tokens:', promptTokens);
  54. 54 console.log('Completion tokens:', completionTokens);
  55. 55 console.log('Total tokens:', totalTokens);
  56. 56 },
  57. 57 });
  58. 58 return {
  59. 59 id: generateId(),
  60. 60 role: 'assistant',
  61. 61 display: result.value,
  62. 62 };
  63. 63}
app/ai.ts
typescript
  1. 1import { createAI } from '@ai-toolkit/rsc';
  2. 2import { ServerMessage, ClientMessage, continueConversation } from './actions';
  3. 3export const AI = createAI<ServerMessage[], ClientMessage[]>({
  4. 4 actions: {
  5. 5 continueConversation,
  6. 6 },
  7. 7 initialAIState: [],
  8. 8 initialUIState: [],
  9. 9});