【OpenAI】API プロンプトトークン数(利用料金)をカウントする

通常のAPIリクエストではレスポンスボディに利用トークンのカウント数は含まれる。


"usage": {
   "prompt_tokens": 9,
   "completion_tokens": 12,
   "total_tokens": 21
 }

👉 API Reference - OpenAI API hatena-bookmark

しかし「Server-Sent Events(SSE)」では、それをレスポンスで返さない。



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

従量制のAPIを利用している限り料金は気になりますよね。

プロンプトのトークンの数をアプリ側で独自にカウントすれば料金(利用トークン数から)は算定できるのでは?

 

🤖 JTokkit

Java のライブラリありますね。OpenAI公式でも紹介されているやつ。


👉 openai-cookbook/How_to_count_tokens_with_tiktoken.ipynb at main · openai/openai-cookbook · GitHub hatena-bookmark

豊富な Java ライブラリを使えるのは Kotlin の便利なところです。

👉 Counting Tokens for ChatML | JTokkit hatena-bookmark

とりあえずテストで動かしてみます。


dependencies {
  testImplementation 'com.knuddels:jtokkit:0.4.0'
}


@Test
fun test_jtokkit() {

  val messages = listOf(
    Message("user", "こんにちはと言ってください。"),
    Message("assistant", "こんにちは!")
  )

  val encoding = Encodings.newDefaultEncodingRegistry()
    .getEncodingForModel(ModelType.GPT_3_5_TURBO)

  val count = messages.sumOf { message ->

    // GPT_3_5_TURBO without name
    listOf(
      4, // per message
      encoding.countTokens(message.role),
      encoding.countTokens(message.content)
    ).sum()

  } + 3 // every reply is primed with <|start|>assistant<|message|>

  println(count)

}


@Serializable
data class Message(
  val role: String,
  val content: String
)

メッセージごと、リプライごとのカウント追加が間違ってるのか、なんなのか。

少しカウント数ずれるんだけども。

 

🤖 まとめ

てか、返せよ Usage。

従量制の有料サービスなんだから。

👉 How to get total_tokens from a stream of CompletionCreateRequests - API - OpenAI Developer Forum hatena-bookmark
👉 How do you get token count when streaming - API - OpenAI Developer Forum hatena-bookmark
👉 How to determine the token usage for a session when using stream:true? - API - OpenAI Developer Forum hatena-bookmark

まあ、Webページで確認すればいいけども。

👉 Usage - OpenAI API hatena-bookmark

大して使ってないからどうでもいいか。


【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