Azure OpenAI APIの正しいトークン数の計算方法
はじめに
OpenAIのAPI利用する際、OpenAI内のGPTなどのLLMはトークンと呼ばれる文字列の単位で処理を行い、APIの利用料金は処理したトークン数にもとづいて課金されます。
そのため、OpenAI APIの利用料金を算出するにあたって、トークン数を試算する必要があります。
この記事では、OpenAI APIにおいてトークン数の算出をする方法について紹介します。
OpenAI APIのレスポンスボディからトークン数を算出
OpenAIのAPIをコールすると通常は、レスポンスボディのusageの項目からトークン数を取得することができます。
// OpenAI APIのレスポンスボディの例
{
"id": "chatcmpl-123",
"object": "chat.completion",
"created": 1677652288,
"model": "gpt-4o-mini",
"system_fingerprint": "fp_44709d6fcb",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "\n\nHello there, how may I assist you today?",
},
"logprobs": null,
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 9,
"completion_tokens": 12,
"total_tokens": 21,
"completion_tokens_details": {
"reasoning_tokens": 0
}
}
}
usage配下のパラメータはそれぞれ以下の意味を持ちます。
- prompt_tokens: 入力されたトークン数
- completion_tokens: 出力されたトークン数
- total_tokens: 入力と出力の合計トークン数
- completion_tokens_details: 出力時に生成されたトークンの内訳
- reasoning_tokens: 推論で使用されたトークンの内訳
通常はAPIで応答されるtotal_tokensの値をカウントしていけば、使用したトークン数のカウントができるのですが、APIのリクエスト時のオプションでstreamを有効にして、ストリーミング形式で分割してレスポンスを取得するようにすると、APIのレスポンスからトークン数を取得することができません。
そのため、ストリーミング時はトークナイザーとよばれる算出ツールを使って、トークン数を自前で算出する必要があります。
トークナイザーを使ったトークン数試算
トークン試算をする場合、OpenAI公式からWEBブラウザ経由で該当の文字列のトークン数を算出できるトークナイザーが提供されています。
ブラウザ経由ではなく、プログラミング言語でトークン算出をする場合、 OpenAIのAPIを算出できるトークナイザーは実装するプログラミング言語ごとに異なり、以下のサイトで確認できます。
Pythonの場合は、以下のtiktokenというトークナイザーを使用することができます。
tiktokenは以下のように使用することができます。
pip install tiktoken
import tiktoken
# モデルに応じたエンコーダーを作成
enc = tiktoken.get_encoding("gpt-4")
# トークンに分割
tokens = enc.encode("Hello World!")
# Hello World!という文字列のトークン数を表示
print(len(tokens))
但し、上記のOpenAIのクックブックに記載されている通り、tiktokenでの算出結果は試算値に過ぎず、APIをコールしたときに実際に消費されるトークンと一致しない可能性があるので、注意が必要です。
Note that the exact way that tokens are counted from messages may change from model to model. Consider the counts from the function below an estimate, not a timeless guarantee.
トークン数試算時の計算式
トークン数を試算する場合、ユーザーが入力したテキストとLLMの生成テキスト以外にも、APIのメッセージに含まれるメタ情報もカウントが必要になり、計算式は以下になります。
トークン数 = M * TPM + N * TPN + ΣV + 3
* M = メッセージの総数
* TPM = メッセージ当たりのトークン(モデルによって3または4)
* TPN = `name`キーをもつメッセージ当たりのトークン(モデルによって1または-1)
* N = `name`キーを持つメッセージの数
* V = メッセージの各バリューのトークン数( `content`キー、`role`キーなどに対するバリューのトークン数)
* 3 = メッセージの最後に追加されるトークン数( APIの全ての応答に対して付与される `<|start|>assistant<|message|>` の分、APIコール前の場合は0 )
例えば、モデルがgpt-35-turbo-16k-0613で、会話履歴のメッセージが以下の形式の場合、計算結果は以下のようになります。
// 会話履歴メッセージリスト
[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "こんにちは", "name": "John"},
{"role": "assistant", "content": "こんにちは!今日はどのようにお手伝いできますか?"}
]
M = 3 ※配列の中のメッセージが3つなので、3
TPM = 3, TPN = 1 ※gpt-35-turbo-16k-0613の場合は3と1
N = 1 ※nameキーを含むメッセージが1つなので1
ΣV = 7 + 3 + 21 = 31 ※各メッセージのトークンの合計
"system" + "You are a helpful assistant." → V = 1 + 6 = 7
"user" + "こんにちは" + "John" → V = 1 + 1 + 1 = 3
"assistant" + "こんにちは!今日はどのようにお手伝いできますか?" → V = 1 + 20 = 21
トークン数 = M * TPM + N * TPN + ΣV + 3
= 3 * 3 + 1 * 1 + 31 + 3
= 44
上記の計算式をPythonで記述すると以下のコードになります。
import tiktoken
import os
from openai import AzureOpenAI
client = AzureOpenAI(
api_key = os.getenv("AZURE_OPENAI_API_KEY"),
api_version = "2024-02-01",
azure_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT") # Your Azure OpenAI resource's endpoint value.
)
system_message = {"role": "system", "content": "You are a helpful assistant."}
max_response_tokens = 250
token_limit = 4096
conversation = []
conversation.append(system_message)
def num_tokens_from_messages(messages, model="gpt-3.5-turbo-0613"):
"""Return the number of tokens used by a list of messages."""
try:
encoding = tiktoken.encoding_for_model(model)
except KeyError:
print("Warning: model not found. Using cl100k_base encoding.")
encoding = tiktoken.get_encoding("cl100k_base")
if model in {
"gpt-3.5-turbo-0613",
"gpt-3.5-turbo-16k-0613",
"gpt-4-0314",
"gpt-4-32k-0314",
"gpt-4-0613",
"gpt-4-32k-0613",
}:
tokens_per_message = 3
tokens_per_name = 1
elif model == "gpt-3.5-turbo-0301":
tokens_per_message = 4 # every message follows <|start|>{role/name}\n{content}<|end|>\n
tokens_per_name = -1 # if there's a name, the role is omitted
elif "gpt-3.5-turbo" in model:
print("Warning: gpt-3.5-turbo may update over time. Returning num tokens assuming gpt-3.5-turbo-0613.")
return num_tokens_from_messages(messages, model="gpt-3.5-turbo-0613")
elif "gpt-4" in model:
print("Warning: gpt-4 may update over time. Returning num tokens assuming gpt-4-0613.")
return num_tokens_from_messages(messages, model="gpt-4-0613")
else:
raise NotImplementedError(
f"""num_tokens_from_messages() is not implemented for model {model}."""
)
num_tokens = 0
for message in messages:
num_tokens += tokens_per_message
for key, value in message.items():
num_tokens += len(encoding.encode(value))
if key == "name":
num_tokens += tokens_per_name
num_tokens += 3 # every reply is primed with <|start|>assistant<|message|>
return num_tokens
while True:
user_input = input("Q:")
conversation.append({"role": "user", "content": user_input})
conv_history_tokens = num_tokens_from_messages(conversation)
while conv_history_tokens + max_response_tokens >= token_limit:
del conversation[1]
conv_history_tokens = num_tokens_from_messages(conversation)
response = client.chat.completions.create(
model="gpt-35-turbo", # model = "deployment_name".
messages=conversation,
temperature=0.7,
max_tokens=max_response_tokens
)
conversation.append({"role": "assistant", "content": response.choices[0].message.content})
print("\n" + response.choices[0].message.content + "\n")
マルチモーダル利用時のトークン算出
マルチモーダルで画像を入力情報としてOpenAIにリクエストした場合は、画像使用分のトークンが別に発生します。
以下が、画像入力時のトークン数の計算は以下の記事が参考になります。
高解像度モードがオフなのかオンなのかによってトークン数の計算式が変わってきます。
- low:高解像度モードがオフの場合、1画像あたり85トークン
- high:高解像度モードがオンの場合、特別な計算式により計算
高解像度モードがオンの場合の計算の流れ
- 入力画像は縦横比を取得
- 縦横比を維持したまま、2,048 × 2,048 ピクセルの正方形に収まるように拡大縮小
- 縦横比を維持したまま、短い辺の長さが 768 ピクセルになるように縮小
- 512 ピクセルの正方形のタイルに分割
- トークン数 = タイル数 x 170 トークン + 85
- タイルの数(部分的なタイルは切り上げ)によって最終的なコストが決まる。
高解像度モードのトークン計算例
2,048 x 4,096 ピクセルの画像入力を行う場合、以下のようになります。
- 縦横比 = 1:2
- 2,048 × 4,096 -> 1,024 × 2,048 ピクセルにリサイズ
- 2,048 ピクセルの正方形に収まるように拡大縮小
- 1,024 × 2,048 -> 768 x 1,536 ピクセルに縮小
- 短い辺の長さが 768 ピクセルになるように縮小
- 512 ピクセルのタイルに分割 -> タイル6枚
- トークン数 170 × 6 + 85 = 1,105 トークン
マルチモーダル対応のPythonコード
Microsoftのサンプルコードをもとに画像入力時のトークン数を算出する非公式のコードを作成しました。
pillow
tiktoken
import requests
import tiktoken
from PIL import Image
from math import ceil
class OpenAiTokenizer:
@staticmethod
def calc_tokens_from_messages(messages, model="gpt-35-turbo-0613", response_flag=False):
"""トークン数を計算する
Note:
参考
https://learn.microsoft.com/ja-jp/azure/ai-services/openai/how-to/chatgpt?pivots=programming-language-chat-completions&tabs=python-new#manage-conversations
計算式
M = メッセージの総数
Tpm = メッセージ当たりのトークン(モデルによって3または4)
Tpn = `name`キーをもつメッセージ当たりのトークン(モデルによって1または-1)
N = `name`キーを持つメッセージの数
V = メッセージの各バリューのトークン数( `content`キー、`role`キーのトークン数)
3 = メッセージの最後に追加されるトークン数( APIの全ての応答に対して付与される `<|start|>assistant<|message|>` の分、APIコール前の場合は0 )
T = M * Tpm + N * Tpn + ΣV + 3
※modelがgpt-4系でサポート外の場合は、gpt-4-0613の計算式を使用する
Args:
messages (list[dict[str, str]]): メッセージリスト
model (str): モデル名
response_flag (bool): レスポンスフラグ (最後に追加されるトークン数を追加するかどうか: Trueの場合追加する)
Returns:
int: トークン数
"""
try:
encoding = tiktoken.encoding_for_model(model)
except KeyError:
encoding = tiktoken.get_encoding("cl100k_base")
if model in {
"gpt-35-turbo-0613",
"gpt-35-turbo-16k-0613",
"gpt-4-0314",
"gpt-4-32k-0314",
"gpt-4-0613",
"gpt-4-32k-0613",
}:
tokens_per_message = 3
tokens_per_name = 1
elif model == "gpt-35-turbo-0301":
tokens_per_message = 4 # every message follows <|start|>{role/name}\n{content}<|end|>\n
tokens_per_name = -1 # if there's a name, the role is omitted
elif model.startswith("gpt-35-turbo"):
return OpenAiTokenizer.calc_tokens_from_messages(messages, model="gpt-35-turbo-0613", response_flag=response_flag)
elif model.startswith("gpt-4"):
return OpenAiTokenizer.calc_tokens_from_messages(messages, model="gpt-4-0613", response_flag=response_flag)
else:
raise NotImplementedError(
f"""num_tokens_from_messages() is not implemented for model {model}."""
)
num_tokens = 0
for message in messages:
num_tokens += tokens_per_message
for key, value in message.items():
if type(value) == list:
for item in value:
# 画像の場合は、画像のURLと詳細度をトークン数に変換
if item.get("type") == "image_url":
image_url = item.get("image_url").get("url")
detail = item.get("image_url").get("detail")
image_text_token = len(encoding.encode(image_url)) + len(encoding.encode(detail))
num_tokens += image_text_token
# urlからファイルをダウンロードし、ファイルサイズを特定
width, height = OpenAiTokenizer.get_image_size(image_url)
image_token = OpenAiTokenizer.count_image_tokens(width, height, detail)
num_tokens += image_token
# テキストの場合は、テキストをエンコードしてトークン数を算出
elif item.get("type") == "text":
num_tokens += len(encoding.encode(item.get("text")))
else:
num_tokens += len(encoding.encode(value))
if key == "name":
num_tokens += tokens_per_name
if response_flag:
num_tokens += 3 # every reply is primed with <|start|>assistant<|message|>
return num_tokens
@staticmethod
def get_image_size(image_url):
"""画像のサイズを取得する
Args:
image_url (str): 画像URL
Returns:
int: 画像の幅
int: 画像の高さ
"""
response = requests.get(image_url)
image = Image.open(io.BytesIO(response.content))
width, height = image.size
image.close()
return width, height
@staticmethod
def count_image_tokens(width: int, height: int, detail: str = "auto"):
"""画像のトークン数を計算する
Notes:
以下のステップで計算を行う。
1. 縦横比を保ちながら2048x2048以内に収まるようにリサイズ (2048 x 4096 -> 縦横比は1:2 -> 1024 x 2048)
2. 最短辺のサイズを768pxにリサイズ (1024 x 2048 -> 768 x 1536)
3. リサイズ後の画像を覆うのに必要な512pxの正方形タイルの数をカウント(部分的なタイルは切り上げ) (768 / 512 = 1.5 -> 2, 1536 / 512 = 3, 2 x 3 = 6)
4. トークンコストを計算 (170 * タイル数 + 85)
参考
- https://learn.microsoft.com/ja-jp/azure/ai-services/openai/overview#image-tokens-gpt-4-turbo-with-vision-and-gpt-4o
- https://zenn.dev/microsoft/articles/cd3060cbcf0303
- https://community.openai.com/t/how-do-i-calculate-image-tokens-in-gpt4-vision/492318
Args:
width (int): 画像の幅
height (int): 画像の高さ
detail (str): 画像の詳細度 : default "auto"
Returns:
int: 画像のトークン数
"""
cost = 0
if detail == "low":
cost = 85
else:
# detailがhighの場合の計算
# ステップ1: 縦横比取得
aspect_ratio = width / height
# ステップ2: 縦横比を保ちながら2048x2048の範囲にリサイズ
# 横幅の方が大きい場合
if aspect_ratio > 1:
new_width = 2048
new_height = int(2048 / aspect_ratio)
# 縦幅の方が大きい場合
else:
new_height = 2048
new_width = int(2048 * aspect_ratio)
# ステップ3: 最短辺を768pxにリサイズ
if new_width < new_height:
scale_factor = 768 / new_width
else:
scale_factor = 768 / new_height
final_width = int(new_width * scale_factor)
final_height = int(new_height * scale_factor)
# ステップ4: 512pxの正方形タイルの数をカウント
# math.ceil: 切り上げ
num_tiles = math.ceil(final_width / 512) * math.ceil(final_height / 512)
# ステップ5: トークンコストを計算
cost = 170 * num_tiles + 85
return cost
おわりに
この記事では、OpenAIのAPI利用時のトークン数の算出方法について紹介しました。
- OpenAI APIの利用料金は処理したトークン数に基づいて課金される
- トークン数の算出はレスポンスの usage 項目で確認可能だが、ストリーミング形式では自分で算出が必要
- トークン数を計算するツール「トークナイザー」が提供されており、Pythonでは tiktoken ライブラリを使用できる
- トークナイザーでの計算は推定値のため、実際の値と一致しない可能性がある
- トークン数の試算式が提示され、メッセージ内の各要素ごとにトークン数がカウントされる仕組みが説明されている。
- 画像を含むリクエストでは、画像の解像度に応じてトークンが追加される
この記事がトークン数の試算を検討されている方の参考になれば幸いです。