6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【ChatGPT API / stream】chatGPTのツールstream対応してみた javascript / typescript

Last updated at Posted at 2023-04-25

今話題のChatGPTですが、apiはstreamオプションを使用することで、リアルタイムに対話を行うことができます。
現代人は待てない人が増えている(ってのは感想ですが)、streamすることでストレスの少ないUXを提供できるのも確かです。
本記事では、TypeScriptでChat Completion APIのストリームを実装する方法を紹介します。

なお、この記事の内容で先日作ったchatgptのツールをstream対応させています!

streamをcurlで投げてみる

なにもともあれ、streamオプションを付けたときにどんな感じでレスポンスがかえってくるのか確認しましょう。

curl https://api.openai.com/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $YOUR_API_KEY" \
  -d '{
    "model": "gpt-3.5-turbo",
    "stream": true,
    "messages": [{"role": "user", "content": "Hello!"}]
}'
data: {"id":"chatcmpl-798yxjuKJ40j94poEGypdvY1vmNQF","object":"chat.completion.chunk","created":1682413835,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"role":"assistant"},"index":0,"finish_reason":null}]}

data: {"id":"chatcmpl-798yxjuKJ40j94poEGypdvY1vmNQF","object":"chat.completion.chunk","created":1682413835,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":"Hello"},"index":0,"finish_reason":null}]}

data: {"id":"chatcmpl-798yxjuKJ40j94poEGypdvY1vmNQF","object":"chat.completion.chunk","created":1682413835,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":"!"},"index":0,"finish_reason":null}]}

data: {"id":"chatcmpl-798yxjuKJ40j94poEGypdvY1vmNQF","object":"chat.completion.chunk","created":1682413835,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":" How"},"index":0,"finish_reason":null}]}

data: {"id":"chatcmpl-798yxjuKJ40j94poEGypdvY1vmNQF","object":"chat.completion.chunk","created":1682413835,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":" can"},"index":0,"finish_reason":null}]}

data: {"id":"chatcmpl-798yxjuKJ40j94poEGypdvY1vmNQF","object":"chat.completion.chunk","created":1682413835,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":" I"},"index":0,"finish_reason":null}]}

data: {"id":"chatcmpl-798yxjuKJ40j94poEGypdvY1vmNQF","object":"chat.completion.chunk","created":1682413835,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":" assist"},"index":0,"finish_reason":null}]}

data: {"id":"chatcmpl-798yxjuKJ40j94poEGypdvY1vmNQF","object":"chat.completion.chunk","created":1682413835,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":" you"},"index":0,"finish_reason":null}]}

data: {"id":"chatcmpl-798yxjuKJ40j94poEGypdvY1vmNQF","object":"chat.completion.chunk","created":1682413835,"model":"gpt-3.5-turbo-0301","choices":[{"delta"hoices":[{"delta":{"content":" today"},"index":0,"finish_reason":null}]}

data: {"id":"chatcmpl-798yxjuKJ40j94poEGypdvY1vmNQF","object":"chat.completion.chunk","created":1682413835,"model":"gpt-3.5-turbo-0301","choices":[{"delta"hoices":[{"delta":{"content":"?"},"index":0,"finish_reason":null}]}

data: {"id":"chatcmpl-798yxjuKJ40j94poEGypdvY1vmNQF","object":"chat.completion.chunk","created":1682413835,"model":"gpt-3.5-turbo-0301","choices":[{"delta"hoices":[{"delta":{},"index":0,"finish_reason":"stop"}]}

data: [DONE]

さて何やらたくさん返ってきました。
どうなっているか見ていきましょう

streamオプションでのresponse

responseはdata: {...}というデータが何行も来ていることがわかります。
3パターンに分かれています

1つ目は最初のdataで"role": "assistant"が返ってきています
本当は改行ありませんが、見やすくすると以下のような内容です。

data: {
  "id":"chatcmpl-798yxjuKJ40j94poEGypdvY1vmNQF",
  "object":"chat.completion.chunk",
  "created":1682413835,
  "model":"gpt-3.5-turbo-0301",
  "choices":[
    {
      "delta":{"role":"assistant"},
      "index":0,
      "finish_reason":null
    }
  ]
}

2つ目は最初以降のdataで、contentがおそらくトークンごとに入ってきています。
roleはassistantと決まりきっているので、このcontentをうまく取り出すのが目標になってきます。

data: {
  "id":"chatcmpl-798yxjuKJ40j94poEGypdvY1vmNQF",
  "object":"chat.completion.chunk",
  "created":1682413835,
  "model":"gpt-3.5-turbo-0301",
  "choices":[
    {
      "delta":{"content":"Hello"},
      "index":0,
      "finish_reason":null
    }
  ]
}

3つ目は最後のdataで[DONE]とだけ入っています。
これが来たら処理終了という認識でいいでしょう。

data: [DONE]

なので方針としては、

  • responseを行ごとに処理して
  • data.choices[0].delta.contentを取り出す
  • [DONE]で終了する

ことができればよさそうです

Chat Completion APIのstream実装

以上を踏まえて、Chat Completion APIのストリームを実装するには、以下のような TypeScript コードを使用することができます。

// 型定義
type ChatCompletionMessage = {
  role: "system" | "user" | "assistant";
  content: string
}

const getChatCompletionStreaming = async (
  sendMessages: ChatCompletionMessage[]
): Promise<ChatCompletionMessage | void> => {
  try {
    let content = ''

    // APIにリクエストを送信する
    const res = await fetch("https://api.openai.com/v1/chat/completions", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${openAIApiKey}`,
      },
      body: JSON.stringify({
        model: "gpt-3.5-turbo",
        stream: true, // streamオプションを付ける
        messages: sendMessages,
      }),
    });
    if (!res.ok || !res.body) throw new Error();

    // レスポンスの本文を読み取るための準備をする
    const reader = res.body.getReader();
    const textDecoder = new TextDecoder();
    let buffer = "";
    let end = false;

    // ストリームからデータを読み取り、処理を行う
    while (!end) {
      const { value, done } = await reader.read();
      if (done) break;

      buffer += textDecoder.decode(value, { stream: true });
      while (true) {
        // bufferからlineをpopしてlineごとに処理していく
        const newlineIndex = buffer.indexOf("\n");
        if (newlineIndex === -1) break;
        const line = buffer.slice(0, newlineIndex);
        buffer = buffer.slice(newlineIndex + 1);

        // データに"[DONE]"が含まれていれば終了
        if (line.includes("[DONE]")) {
          end = true;
          break;
        }

        // 受信したデータが"data:"で始まっているかどうかを確認する
        if (!line.startsWith("data:")) continue;
        // lineは`data: {}`の形状をしているため5文字目以降をparseする
        const jsonData = JSON.parse(line.slice(5));
        if (!jsonData.choices[0].delta.content) continue;

        // contentを取り出す
        const newContent = jsonData.choices[0].delta.content;
        content += newContent
      }
    }
    return {role: "assistant", content}
  } catch (e) {
    // エラー処理をする
    // ...
  }
};

実際にこれを使ってみるとこんな感じで、chatGPTのUIのように徐々に表示することを実現できました。

最後に

chatGPTに限らずstreamingは待てない現代人にとってUX上大切でしょう。
今回その一端に触れることができたので、今後streamingの知識をよりつけていきたいと思います。

今回の記事が皆さんに参考になれば幸いです!

なお、冒頭に書いた通り、この記事を書くにあたって学んだ内容で、先日作ったchatGPT関連のツールをstream対応してみたので、よかったら見てください!
AI同士で会話させることで、プロンプトエンジニアリングするときなどに入力を考えなくてよくなるツールです!

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?