openaiのGPTでギリギリ最大のtoken数を指定したい

# 背景
## トークン数の上限について
openaiのAPIを利用してリクエストを投げる際、modelによって異なりますがトークン数の上限が存在します。
この上限数はmodelに与えるinputのトークン数と、modelから返ってくるoutputのトークンの合計です。
APIを出す時に、通常max_tokensというパラメータを指定する必要があります。これはoutputのトークン数の最大値を指定するものです。指定しない場合はデフォルト値が適用されますがそれは非常に短いので利用する際には大体指定します。

## 上限指定する上で困ること
このデフォルト値が、`modelの最大トークン数 - inputのトークン数`で最初から扱ってくれていれば問題ないのですが、非常に短い固定値になっています。
なので、最初APIを触る際はみんな適当に1000とか2000とかで設定すると思います。
しかし、なるべくトークンは1つのリクエストで使い切りたいので、max_tokensはなるべく最大値に近い値で利用したいです。
しかしトークン数=文字数ではないので、まずはトークン数を数える必要があります。

# トークンの数え方を調べてみる
## 方針
Openaiの公式によるとpythonの場合は[tiktoken](https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them)というライブラリを使ってトークン数を数えているそうです。
なので、ちょっとサンプルコードを組んで調べてみます

## 検証用コード
下記のように組んでみました。
```python
import tiktoken
import openai

model_name = "gpt-3.5-turbo"
system_prompt = "これから与える昔話を完成させてください"
user_prompt = "むかしむかしあるところに、おじいさんとおばあさんが"

# tiktokenを使ってtokenを数える
enc = tiktoken.encoding_for_model(model_name)
system_prompt_tokens = enc.encode(system_prompt)
user_prompt_tokens = enc.encode(user_prompt)
print("system_prompt_tokens: ", len(system_prompt_tokens))
print("user_prompt_tokens: ", len(user_prompt_tokens))
print("total_tokens: ", len(system_prompt_tokens) + len(user_prompt_tokens))

# APIリクエストを投げる(OPENAI_API_KEYは環境変数で設定してある前提)
response = openai.ChatCompletion.create(
    model=model_name,
    messages=[
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": user_prompt},
    ],
    max_tokens=1,
)

print("response_prompt_tokens:", response.usage.prompt_tokens)
```

### 実行結果
実行結果は下記のようになりました。total_tokens = response_prompt_tokensになってほしいところだったのですが。

```
system_prompt_tokens:  14
user_prompt_tokens:  25
total_tokens:  39
response_prompt_tokens: 50
```

## なぜトークン数が一致しないのかを調べる

まず、openaiから公式に出されている[tokenカウンターツール](https://platform.openai.com/tokenizer)を試してみますが、これを使って数えてみたところ下記のような結果になりました。
```
これから与える昔話を完成させてください ... 27
むかしむかしあるところに、おじいさんとおばあさんが ... 32
```
tiktokenを使った結果にも、chat apiが算出したものともマッチしないですね。

さらに調べてみると、[openaiコミュニティの書き込み](https://community.openai.com/t/counting-tokens-for-chat-api-calls-gpt-3-5-turbo/81974)がありました。
結構みんな苦労してますね。このPostから参照されている[openai-cookbookのexample](https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb)を見てみると、確かにこんなことが書いてあります（というか最初からこれを見ればよかった）。


> ChatGPT models like gpt-3.5-turbo and gpt-4 use tokens in the same way as older completions models, but because of their message-based formatting, it's more difficult to count how many tokens will be used by a conversation.
>
>Below is an example function for counting tokens for messages passed to gpt-3.5-turbo or gpt-4.
>
>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.

なるほど、つまりtokenのカウント方法はmodel毎に異なり、しかも同じgpt-3.5でも、2023/8/30時点で
- gpt-3.5-turbo-0301
- gpt-3.5-turbo-0613
- gpt-3.5-turbo

と3種類のmodelがあり、それぞれでtokenのカウント数が微妙に違う（場合がある）のが分かります。

2023年8月現在では四半期毎にopenaiが提供するmodelはアップデートされており、最新版を示す `gpt-3.5-turbo`についてもかなり頻繁にアップデートされているということになります。
tiktokenからはちゃんと `gpt-3.5-turbo`のモデルに対応するものを指定しているのですが、もしかするとtiktoken内部のエンコーダーのアップデートが追いついてないのかもしれません。

# 結論
正確なtoken数をmodelに投げる前にカウントして、ギリギリのmax_tokensを指定して投げたいと思っていたわけですが、正確なtoken数を継続的に計測するのは結構難しいのでやめた方が良さそう、というのが今のところの結論です。

ただ、概算は計算できるため下記のようなworkaroundが可能そうです。
- 概算のmax_tokenの90%をmax_tokensとする
- finish_reasonがlength（max_tokensの制限により本来出すべき出力が全て出なかった）場合に追加の出力を求める仕組み
- 予めmax_tokens=1で投げて、トークン数をカウントしてからmax_tokensを指定して投げる