【OpenAI】Kotlin OkHttp で Server-Sent Events (SSE)

ChatGPT API で stream 受信したいですよね。

stream boolean Optional Defaults to false

If set, partial message deltas will be sent, like in ChatGPT. Tokens will be sent as data-only server-sent events as they become available, with the stream terminated by a data: [DONE] message.

👉 API Reference - OpenAI API hatena-bookmark

簡単にできそうなのでやってみました。

 

📡 curl

curl で簡単に取得できる。


curl "https://api.openai.com/v1/chat/completions" \
  -H "accept: text/event-stream" \
  -H "authorization: Bearer $OPENAI_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "model": "gpt-3.5-turbo",
    "messages": [{"role": "user", "content": "Server-Sent Events とは"}],
    "stream": true
  }'


...

data: {"id":"chatcmpl-7OQ1xuBFfam8Ct","object":"chat.completion.chunk","created":1686054286,"model":"gpt-3.5-turbo","choices":[{"delta":{"content":"て"},"index":0,"finish_reason":null}]}

data: {"id":"chatcmpl-7OQ1xuBFfam8Ct","object":"chat.completion.chunk","created":1686054286,"model":"gpt-3.5-turbo","choices":[{"delta":{"content":"います"},"index":0,"finish_reason":null}]}

data: {"id":"chatcmpl-7OQ1xuBFfam8Ct","object":"chat.completion.chunk","created":1686054286,"model":"gpt-3.5-turbo","choices":[{"delta":{"content":"。"},"index":0,"finish_reason":null}]}

data: {"id":"chatcmpl-7OQ1xuBFfam8Ct","object":"chat.completion.chunk","created":1686054286,"model":"gpt-3.5-turbo","choices":[{"delta":{},"index":0,"finish_reason":"stop"}]}

data: [DONE]

パースはだるそうですが、取得はできてるようです!

では、いざ、Kotlin で。

 

📡 okhttp-sse

壊れてるのか何なのか微妙。


testImplementation("com.squareup.okhttp3:okhttp-sse:4.11.0")

👉 okhttp/okhttp-sse at master · square/okhttp · GitHub hatena-bookmark

現在の安定版 4.11.0 ではあやしい。

5.0.0-alpha.11 ではまあまあ動いてるのでそれで。



SSEは、日本語で表現するとサーバ送信イベントと表現されるもので、サーバからクライアントに対してリアルタイムでイベントを送信することができる機能です。 コネクションを張っておいて、サーバからイベントを好きなタイミングで送れるイメージです

 

📡 まとめ

カンファレンスでの公演資料とか、時間が経つと、まあ動かないこと多いですよね!


👉 Server-Sent Events in Android (with Node.js) | by Rahul Ray | Jun, 2023 | ProAndroidDev hatena-bookmark


【OpenAI】API「That model is currently overloaded with other requests. You can retry your request, or contact us through our help center at help.openai.com if the error persists.」

最近、頻繁に出てます。レスポンスボディ。


{
  "error": {
    "message": "That model is currently overloaded with other requests. You can retry your request, or contact us through our help center at help.openai.com if the error persists. (Please include the request ID XXXXXXXXXXXXXXXXXXXXX in your message.)",
    "type": "server_error",
    "param": null,
    "code": null
  }
}

エラーコードが null なので、API クライアントの実装によっては影響あるか。

こんなの出ました。

Expected string literal but 'null' literal was found at path: $.error.code
Use 'coerceInputValues = true' in 'Json {}` builder to coerce nulls to default values.

👉 Kotlin Serialization ガイド 第5章 JSONの機能 - Qiita hatena-bookmark

Error codes - OpenAI API
👉 Error codes - OpenAI API hatena-bookmark

レスポンスボディでなく、レスポンスヘッダーから見るべきか。

👉 Status code 503: That model is currently overloaded with other requests - API - OpenAI Developer Forum hatena-bookmark

一応、サーバーステータスのページでも確認するといいですね。

OpenAI Status

👉 OpenAI Status hatena-bookmark


【Kotlin】ChatGPT のようなメッセージングアプリのスクロールする吹き出し部分にローディングインジケーターを実装する

 

🎞️ ローディングインジケーターの作成

まずは、簡単にテキストで「読み込み中」を表示するViewを作ります。

JetpackCompose なら簡単に実装できます。




こんな無限ループでさえ、Jetpack Compose はメモリーリークを起こしません。きちんと捨ててます。

上の動画内コードから抜粋。


  val texts = listOf("○○○", "️●○○", "️●●○", "️●●●", "️●●○", "️●○○")
  var dots by remember { mutableStateOf("") }

  LaunchedEffect(Unit) {
    while (true) {
      texts.indices.forEach { index ->
        delay(500)
        dots = texts[index]
      }
    }
  }

これを LazyColumn の item の表示に使います。

 

🎞️ Flow<List<Item>> の一時的な付け替え

まず、ローディング表示のよくあるパターン。

リクエストの開始時に _loadingtrue に、レスポンス取得時に false にすることで List<Item> を更新します。


private val _loading = MutableStateFlow(false)
val loading = _listening.asStateFlow()

val items: Flow<List<Item>> = _loading.flatMapLatest { loading ->
  repository.items
}

これを少し変更します。

List<Item> は、レスポンスが帰ってくるまで変化がないですが、この間にダミーの「読み込み中を表す Item」を最後に追加しておきます。

もちろん読み込みが終われば元に戻す。


val items: Flow<List<Item>> = _loading.flatMapLatest { loading ->
  repository.items.map { list ->
    list.toMutableList().apply {
      if (loading) add(dummyLoadingItem)
    }.toList()
  }
}

あとは、各メッセージを表示する Composable で Item を確認して表示を切り替えれば良いです。

 

🎞️ 出来上がり



👉 【Kotlin】Flow flatMap* を ネストするか チェインするか【coroutine】 hatena-bookmark