Como integrar a API da OpenAI utilizando stream no Next.js

Vinnicius Gomes
8 min readFeb 14, 2024
Foto de Kenny Eliason na Unsplash

Se você não vive em uma caverna e está por dentro dos assuntos mais comentados dos últimos tempos, provavelmente já ouviu falar do ChatGPT. Este é um sistema de chat que se utiliza do modelo GPT (Generative Pre-trained Transformer) da OpenAI para interagir com usuários de maneira semelhante a uma conversa com uma pessoa real. O funcionamento é simples: o usuário digita o texto, que é então alimentado ao modelo GPT, responsável por gerar respostas contextuais com base no que foi fornecido. Essas respostas podem variar desde a simples resposta a perguntas até a realização de tarefas específicas ou a manutenção de uma conversa casual.

Para contextualizar, os modelos GPT são Modelos de Linguagem de Grande Escala (LLM) desenvolvidos pela OpenAI. Eles se destacam em uma ampla gama de tarefas linguísticas, incluindo a conclusão de texto, a sumarização e até mesmo a escrita criativa. Esses modelos foram projetados para compreender e gerar texto de maneira sofisticada e semelhante à humana, tornando-se extremamente úteis para uma variedade de aplicativos no processamento de linguagem natural (NLP).

E se você já se perguntou como os chatbots são criados, ou quer experimentar a magia do GPT em ação, você está no lugar certo! Vou te mostrar como criar uma aplicação semelhante ao ChatGPT do zero, utilizando Next.js, Vercel AI SDK e a API da OpenAI.

Antes de começar…

Como o intuito desse post é aprender como integrar o GPT em uma aplicação React, vou pular toda a parte de criação da interface, no final desse post vou deixar o repositório com o código completo criado nesse artigo, você vai poder baixar e utilizar a UI como quiser.

O projeto foi criado utilizando a CLI do Next.js com TypeScript, Tailwind e Shadcn UI.

Então vamos ao que importa!

Como eu disse no começo desse post, a ideia não é te ensinar como começar uma aplicação React, então vou seguir direto ao ponto, que é a integração!

Para esse projeto funcionar precisamos instalar algumas libs: pnpm install ai openai

OpenAI: É a SDK para JavaScript oficial da OpenAI, você pode ler mais sobre ela aqui: https://platform.openai.com/docs/libraries/typescript-javascript-library

AI: É a Vercel AI SDK, uma biblioteca de código aberto projetada para ajudar os desenvolvedores a construir interfaces de usuário de streaming conversacional em JavaScript e TypeScript. Você pode ler mais sobre ela aqui: https://sdk.vercel.ai/docs (Essa SDK funciona perfeitamente no React sem o Next.js)

Depois de instalar as libs, precisamos criar uma variável de ambiente com a OPENAI_API_KEY

Crie um arquivo .env na raiz do seu projeto:

OPENAI_API_KEY=xxxxxxxxx

Você consegue gerar essa key pelo portal da OpenAI. Se você já tem uma API_KEY pode pular os passos abaixo

Gerando chave da API

1. Navegue até a seção de API
Após fazer o login, no menu a esquerda, clique em “API keys”.

https://platform.openai.com/api-keys

2. Gerar uma nova chave da API
Agora que você está na seção de chaves da API, você deverá ver um botão “Create new secret key”. Clique nesse botão para gerar uma nova chave da API.

https://platform.openai.com/api-keys

Um modal irá aparecer solicitando que você dê um nome à sua chave da API. É recomendável ter chaves diferentes para aplicativos e sites distintos, então certifique-se de nomeá-la de uma forma que você se lembre para que serve quando olhar para ela no futuro.

Após inserir um nome para sua chave, clique no botão “Create secret key”.

3. Guarde sua chave da API
Em seguida, você verá sua chave secreta que foi gerada. Certifique-se de copiar sua chave secreta e colá-la no aplicativo em que você precisa utilizá-la.

É extremamente importante que você copie essa chave e a guarde em um local seguro, pois você não será capaz de recuperá-la novamente por motivos de segurança. Você precisará desta chave para autenticar seus aplicativos com os serviços da OpenAI.

4. Salve no arquivo .env
Copie e cole essa chave no arquivo .env que criamos anteriormente:

OPENAI_API_KEY=sk-DUgtJ2cYEZQRl7nVw9HX8aCcgyM1ftObiEsRWKplVNh6KP30

Agora vamos para a parte legal, fazer isso funcionar!

Integrando a OpenAI com o Next.js

Agora que temos tudo configurado, podemos começar nossa integração, para isso criaremos uma rota usando o API Routes do Next.js:

/* 
Path: app/api/chat/route.ts
Código no GitHub: https://github.com/vinniciusgomes/blog/blob/master/openai-integration/src/app/api/chat/route.ts
*/

// Criando uma instância da classe OpenAI, passando a chave da API como parâmetro
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});

export async function POST(req: Request) {
const {
messages, // Mensagens a serem enviadas para a API
model, // Modelo de IA a ser usado para geração de texto
temperature, // Parâmetro de "temperatura" para controlar a aleatoriedade das previsões da IA
max_length: max_tokens, // Número máximo de tokens (ou comprimento) permitidos na saída
top_p, // Valor de "top-p" para controlar a diversidade de respostas da IA
} = await req.json();

// Chamando a API do OpenAI para completar as mensagens em um modo de streaming
const response = await openai.chat.completions.create({
model, // Especificando o modelo a ser usado
stream: true, // Ativando o modo de streaming
messages, // Passando as mensagens para a API
temperature, // Passando a temperatura
max_tokens, // Passando o número máximo de tokens
top_p, // Passando o valor de top-p
});

// Criando um fluxo de streaming de texto com base na resposta da API
const stream = OpenAIStream(response);

// Retornando uma resposta de texto em streaming
return new StreamingTextResponse(stream);
}

O projeto foi criado com a nova App Router do Next.js, por isso o arquivo da API está desse jeito. Você pode ler mais sobre isso aqui.

Agora temos uma rota que utiliza a SDK da OpenAI em POST/api/chat.

Criando nossa UI

Para construir a UI, utilizei o shadcn/ui, então os componentes utilizados para construir a tela vem dessa lib, você pode ler a documentação clicando aqui.

O foco é na integração, então vamos ignorar a parte do layout. No arquivo app/page.tsx é onde vamos chamar a rota que criamos no passo anterior utilizando a SDK da Vercel:

/* 
Path: app/page.tsx
Código no GitHub: https://github.com/vinniciusgomes/blog/blob/master/openai-integration/src/app/api/chat/route.ts
*/

"use client";
import { useState } from "react";
import { useChat } from "ai/react";
import { Wand } from "lucide-react";

import { AIChat } from "@/components/ai-chat";
import { Header } from "@/components/header";
import { MaxLengthSelector } from "@/components/max-length-selector";
import { ModelSelector } from "@/components/model-selector";
import { TemperatureSelector } from "@/components/temperature-selector";
import { TopPSelector } from "@/components/top-p-selector";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { Textarea } from "@/components/ui/textarea";
import { UserChat } from "@/components/user-chat";

export default function Home() {
const [model, setModel] = useState("gpt-3.5-turbo");
const [temperature, setTemperature] = useState([1]);
const [maxLength, setMaxLength] = useState([256]);
const [topP, setTopP] = useState([1]);

// Utilizando o hook useChat que vem de dentro da SDK da Vercel:
const { messages, input, handleInputChange, handleSubmit } = useChat({
api: "/api/chat",
body: {
model,
temperature: temperature[0],
max_length: maxLength[0],
top_p: topP[0],
},
});

return (
<main className="flex min-h-screen flex-col">
<div className="h-full flex-col md:flex">
<Header />
<Separator />
<div className="mt-4 py-4 container h-full md:mt-6 md:py-6">
<div className="grid h-full items-stretch gap-6 md:grid-cols-[1fr_200px]">
<div className="flex-col space-y-6 sm:flex md:order-2">
<div className="grid gap-2">
<span className="font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
Settings
</span>
</div>
<ModelSelector
defaultValue={model}
onValueChange={(model) => setModel(model)}
/>
<TemperatureSelector
defaultValue={temperature}
value={temperature}
onValueChange={(value) => setTemperature(value)}
/>
<MaxLengthSelector
defaultValue={maxLength}
value={maxLength}
onValueChange={(value) => setMaxLength(value)}
/>
<TopPSelector
defaultValue={topP}
value={temperature}
onValueChange={(value) => setTopP(value)}
/>
</div>
<div className="md:order-1">
<form className="flex flex-col space-y-4" onSubmit={handleSubmit}>
<div className="grid h-full grid-rows-2 gap-6 lg:grid-cols-2 lg:grid-rows-1">
<Textarea
placeholder="Say something..."
className="h-full min-h-[300px] lg:min-h-[700px] xl:min-h-[700px] p-4 resize-none"
onChange={handleInputChange}
value={input}
/>
<ScrollArea className="rounded-md border bg-muted p-6 h-full max-h-[300px] lg:max-h-[700px] xl:max-h-[700px]">
<div className="flex flex-col gap-4">
{messages.map((m) => (
<div className="flex flex-col gap-2" key={m.id}>
{m.role === "user" ? (
<UserChat
avatar="https://github.com/vinniciusgomes.png"
username="vinniciusgomes"
/>
) : (
<AIChat />
)}

<span>{m.content}</span>
</div>
))}
</div>
</ScrollArea>
</div>
<div className="flex items-center space-x-2">
<Button type="submit">
Generate with A.I
<Wand size={16} className="ml-2" />
</Button>
</div>
</form>
</div>
</div>
</div>
</div>
</main>
);
}

Este componente utiliza o hook useChat, usará a rota POST que criamos anteriormente. O hook fornece funções e estado para lidar com a entrada do usuário e a submissão do formulário. O hook useChat fornece várias funções utilitárias e variáveis de estado:

messages — As mensagens de chat atuais, uma matriz de objetos com propriedades id, role e content (entre outras).
input — Este é o valor atual do campo de entrada do usuário.
handleInputChange e handleSubmit — Estas funções lidam com as interações do usuário, como digitar no campo de entrada e enviar o formulário, respectivamente.
isLoading — Este booleano indica se a solicitação à API está em andamento ou não.

Nesse trecho do código é onde vamos renderizar nossas mensagens:

{messages.map((m) => (
<div className="flex flex-col gap-2" key={m.id}>
{m.role === "user" ? (
<UserChat
avatar="https://github.com/vinniciusgomes.png"
username="vinniciusgomes"
/>
) : (
<AIChat />
)}

<span>{m.content}</span>
</div>
))}

O objeto message segue essa estrutura:

type Message = {
id: string;
tool_call_id?: string;
createdAt?: Date;
content: string;
ui?: string | JSX.Element | JSX.Element[] | null | undefined;
role: 'system' | 'user' | 'assistant' | 'function' | 'data' | 'tool';
/**
* If the message has a role of `function`, the `name` field is the name of the function.
* Otherwise, the name field should not be set.
*/
name?: string;
/**
* If the assistant role makes a function call, the `function_call` field
* contains the function call name and arguments. Otherwise, the field should
* not be set. (Deprecated and replaced by tool_calls.)
*/
function_call?: string | FunctionCall;
data?: JSONValue;
/**
* If the assistant role makes a tool call, the `tool_calls` field contains
* the tool call name and arguments. Otherwise, the field should not be set.
*/
tool_calls?: string | ToolCall[];
/**
* Additional message-specific information added on the server via StreamData
*/
annotations?: JSONValue[] | undefined;
}

E é isso. Simples assim! 🎉

Esse é o resultado final da nossa aplicação:

E o backend funciona dessa forma:

Brincadeiras a parte, eu criei um repositório no GitHub com a aplicação criada nesse post, se você quiser dar uma olhada mais a fundo é só clicar aqui:

Cuidado! Os robôs vão dominar o mundo… 🤖

Espero que tenha gostado! E se tiver alguma sugestão ou dúvida deixe aí nos comentários 💬

Se gostou, dê 1 ou 50 claps (Só clicar 50x na 👏)

Obrigado pela leitura!

Me acompanhe por aí! 😜

--

--

Vinnicius Gomes

Senior Software Engineer who love to write about Frontend, JavaScript and Web development. See more about me — vinniciusgomes.dev