先に断っておくと、この記事の入口は既知の話です。GPT-5 系と o 系の推論モデルが temperature を受け付けないことは、OpenAI のドキュメントに書かれています。
書くのは、そこで踏んだあとの話です。**最初に書いた修正が筋の悪いもので、動いていたのに作り直した。**その判断と、捨てなかった部分をどう残したか。ここは自分のコードの中にしかありません。
何が起きたか
自作の WordPress 用チャットボットプラグインで、モデル設定を GPT-5 系に切り替えたら、会話が全部エラーになりました。HTTP 400 です。gpt-4o では何も起きません。同じリクエストボディなのに、モデルを変えただけで弾かれました。
原因は temperature でした。プラグインは設定値(既定 0.7)を常に付けていたので、これらのモデルでは一発アウトです。エラー本文は、このモデルでは temperature に既定値以外を指定できない、という趣旨の内容を返してきます。
ここまでは、調べれば分かる話でした。
失敗編:400 を受けてから、文字列マッチで再試行する
最初の修正はこうです。
リクエストを送る。400 が返る。エラーメッセージに temperature 関連の文言が含まれているかを部分文字列マッチで調べる。含まれていたら temperature を外して再送する。
動きました。ただ、書いた直後から嫌な予感がありました。
- 判定の根拠が API のエラーメッセージの文言で、OpenAI 側の文言変更ひとつで壊れます
- temperature と無関係な 400(別パラメーターの invalid など)を誤検知して、無意味な再送をする余地があります
- 対象モデルでは常に2往復になります。ユーザーの体感で、応答が1リクエスト分遅くなります
「とりあえず動く」と「設計として正しい」の差を、そのまま形にしたようなコードでした。
学び編:知っていることは、送る前に判定する
考えてみれば、どのモデルが temperature を受け付けないかは、リクエストを送る前から分かっています。なら送る前に分ければいい。
現行版はモデル名のプレフィックス判定で、対象モデルには temperature を最初から送りません。
// GPT-5 系と o 系(o1 / o3 / o4)は temperature=1 固定のため送らない
if (!$is_reasoning && !$this->is_gpt5_model()) {
$body['temperature'] = (float) ($options['temperature'] ?? 0.7);
}
再試行も、余計な往復も消えました。
トレードオフは、新モデルが出るたびにプレフィックスのリストを更新する必要があることです。
| 事後判定(文字列マッチ) | 事前判定(プレフィックス) | |
|---|---|---|
| 未知のモデル | 自動で追従する | 追従しない |
| 文言が変わったとき | 壊れる | 影響なし |
| 往復数 | 対象モデルで常に2回 | 1回 |
| 保守 | 不要に見えて、実は文言監視 | リストの更新が要る |
未知のモデルに追従できるのは、事後判定の本物の利点です。それでも事前判定を取りました。追従して「動く」状態と、追従せず「落ちて気づく」状態なら、後者のほうが手当てしやすいと判断したからです。
リストの更新は、リリース前の手順に組み込みました。
文字列マッチは、捨てていません
面白いのはここからです。文字列マッチで再試行するパターン自体は、用途を変えて生き残っています。
Chat Completions と Responses API のどちらに送るべきかは、モデルによって違います。外した場合の 400 / 404 を検出して、もう片方へフォールバックする処理があります。web search ツール起因の 400 で、ツールを外して再送する処理も同じ系統です。
ただし、判定の根拠に順位を付けました。
| 優先度 | 根拠 | 使いどころ |
|---|---|---|
| 1 | 構造化されたエラーコード | まずここを見る |
| 2 | 文言+文脈条件(モデル名や endpoint への言及があるとき) | コードで判別できないとき |
| 3 | 文言のみ | 使わない |
temperature の件で踏んだのは、3番でした。**文言だけを根拠にすると、相手が言葉を変えた瞬間に壊れます。**2番まで落とせば、誤検知の余地がかなり減ります。
言い換えると、文字列マッチが悪いのではなく、文字列マッチ「だけ」が悪かったという整理になりました。最初の修正を全部捨てなくて済んだのは、ここが分かったからです。
事前に分けられるものと、分けられないもの
今回の切り分けを、一般化するとこうなりました。
**事前に知れることは、コードに知識として持たせる。**どのモデルが何を受け付けないか、どの endpoint が要るか。これは調べれば分かるので、送る前に分岐します。そのかわり、知識の更新をリリース手順に入れます。
**事前に知れないことだけ、事後に拾う。**一時的な障害、レート制限、こちらが想定していない組み合わせ。ここは送ってみないと分かりません。
境目は「ドキュメントに書いてあるかどうか」です。書いてあることを実行時に探り当てようとすると、今回のようなコードになります。
次の自分に渡すメモ
- エラーメッセージの文字列マッチは最終手段。使うなら、誤検知を抑える文脈条件(モデル名への言及など)とセットにする
- 構造化されたエラーコードがあるなら、そちらを最優先にする
- 事前に知れることはコードに持たせて、リクエストを送る前に分岐する
- そのかわり、プレフィックスのリスト更新をリリース前の手順に入れる。次の新モデルで最初に壊れるのは、たぶんここです
事後の賢いリカバリーより、事前のつまらない if 文。
プラグイン本体: https://wordpress.org/plugins/rapls-ai-chatbot/
質問・不具合報告: https://wordpress.org/support/plugin/rapls-ai-chatbot/
ふだんはraplsworks.comで、WordPressプラグイン開発やClaude Codeまわりのことを書いています。