takataka430’s blog

.NET系を中心に勉強したことのまとめを書きます

Azure Functions(Node.js)がStreamに対応したのでOpenAIのStreamと組み合わせてみた

少し前ですが、Azure FunctionsのNode.jsでstream対応になりました。

techcommunity.microsoft.com

stream対応したということは、OpenAIのstreamの返信をFunctions経由で返すことができるということなので、このブログで検証しようと思います。

※OpenAIのAPIを使います。AzureOpenAIではないです。

環境

@azure/functions 4.0.0
openai 4.38.1
FunctionsはHTTPトリガーを使用

Functions実装

最初に環境変数を設定します。
名前:"OpenAIApiKey"
値:OpenAIのAPIキー
local.setting.jsonに記載する場合は以下のようになります。

{
  "Values": {
      "OpenAIApiKey": "[OpenAIのAPIキー]"
  }
}

ソースコードを書いていきます。
まずOpenAIのパッケージをインストールします。

npm install openai

コードを書いていきます。
まず以下でFunctionsのstreamを有効にします。

app.setup({ enableHttpStream: true });

APIキーを環境変数から取得してOpenAIのクライアントを作成します。

const apiKey = process.env["OpenAIApiKey"];
const openai = new OpenAI({ apiKey: apiKey});

HTTPリクエストを受けてreturnする関数は以下のようにします。

async function httpTrigger1(req: HttpRequest){
    //本文から値を取得
    //想定しているJSONは以下の通り
    //{"prompt": "Hello, World!"}
    const prompt = req.params['prompt'];

    //OpenAIのAPIを呼び出す
    const completion = await openai.chat.completions.create({
        messages: [{ role: "user", content: prompt }],
        model: "gpt-3.5-turbo",
        stream: true
    });

    //streamを生成するgenerator
    const generator = async function* () {
        for await (const chunk of completion) {
            if(chunk.choices[0].finish_reason === "stop"){
                break;
            }
            yield chunk.choices[0].delta.content + '\n';
        }
    };

    //streamを取得
    const stream = Readable.from(generator());
    
    //streamを返す
    return {
        body: stream,
        headers: {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-store'
        }
    };
};

ポイントとしては以下の通りです。

  • OpenAIにリクエストを送るときはstream: trueにします
  • レスポンスのヘッダーに'Content-Type': 'text/event-stream','Cache-Control': 'no-store'を設定します
    参考: developer.mozilla.org

  • generatorのあたりはあまりよくわかっていませんが、見よう見まねで実装しました

全体のコードは以下のようになります。

import { app, HttpRequest } from "@azure/functions";
import OpenAI from "openai";
import { Readable } from "stream";

//streamを使うために必要
app.setup({ enableHttpStream: true });

// APIキーを環境変数から取得
const apiKey = process.env["OpenAIApiKey"];
const openai = new OpenAI({ apiKey: apiKey});

async function httpTrigger1(req: HttpRequest){
    //本文から値を取得
    //想定しているJSONは以下の通り
    //{"prompt": "Hello, World!"}
    const prompt = req.params['prompt'];

    //OpenAIのAPIを呼び出す
    const completion = await openai.chat.completions.create({
        messages: [{ role: "user", content: prompt }],
        model: "gpt-3.5-turbo",
        stream: true
    });

    //streamを生成するgenerator
    const generator = async function* () {
        for await (const chunk of completion) {
            if(chunk.choices[0].finish_reason === "stop"){
                break;
            }
            yield chunk.choices[0].delta.content + '\n';
        }
    };

    //streamを取得
    const stream = Readable.from(generator());
    
    //streamを返す
    return {
        body: stream,
        headers: {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-store'
        }
    };
};

app.http('httpTrigger1', {
    methods: ['POST'],
    authLevel: 'function',
    handler: httpTrigger1
});

Functions側はこれで完了です。

クライアントの実装

次にFunctionsにアクセスするクライアントをC#のコンソールアプリで実装します。

using System.Text;
using System.Text.Json;

var endpointUrl = "[FunctionsのURL]";
var promptString = "[OpenAIに送信するプロンプト]";

//HttpClientの作成
var client = new HttpClient();

//HTTPリクエストの作成
var request = new HttpRequestMessage(HttpMethod.Post, endpointUrl);

//JSON形式でデータを送信
using StringContent jsonContent = new(
        JsonSerializer.Serialize(new
        {
            prompt = promptString
        }),
        Encoding.UTF8,
        "application/json");
request.Content = jsonContent;

//リクエストの送信
var response = await client.SendAsync(request,HttpCompletionOption.ResponseHeadersRead);

//レスポンスの読み取って表示
using var stream = await response.Content.ReadAsStreamAsync();
using var streamReader = new StreamReader(stream);
while (!streamReader.EndOfStream)
{
    var line = await streamReader.ReadLineAsync();
    if (string.IsNullOrEmpty(line))
        continue;
    Console.Write(line);
}

promptStringにこんにちは。お元気ですか?と設定してプログラムを実行すると以下のようになります。

想定通り少しずつ回答を取得して表示していますね!

参考URL

Azure Functions の Node.js v4 に追加された HTTP Streaming サポートを深掘りする - しばやん雑記

https://platform.openai.com/docs/api-reference/chat/create?lang=node.js

Stream | Node.js v21.7.3 Documentation