何が起きたか
本番で LLM-as-a-judge の採点パイプラインを回しています。判定モデルを gpt-4o-mini から gpt-5-mini に差し替えることにしました。価格は同じ($0.15 / $0.60 per 1M トークン)で判定品質だけ上がる、コスト中立のアップグレードのはずでした。
モデル名を書き換えて、ユニットテストは全部緑。デプロイしました。
その直後に手が止まりました。テストは全部モックで書いてあります。実 API がパラメータを拒否するケースは、一件も検証していない。慌てて実 API に投げて確認したところ、全リクエストが 400 で落ちる状態でした。次の定期実行が来る前にロールバックして事なきを得ましたが、あと数十分遅ければ本番の採点が全部止まっていました。
gpt-5 系(と o 系)は Chat Completions の互換モデルではありません。ハマった非互換は 3 つです。
非互換 1: max_tokens が使えない
gpt-5 系に max_tokens を渡すと 400 が返ります。
Unsupported parameter: 'max_tokens' is not supported with this model.
Use 'max_completion_tokens' instead.
max_completion_tokens に置き換えれば通ります。機械的な置換で済むので、これは一番軽傷です。
非互換 2: temperature が既定値以外を受け付けない
判定の再現性のために temperature: 0 を渡していました。これも 400 です。
Unsupported value: 'temperature' does not support 0 with this model.
Only the default (1) value is supported.
gpt-5 系は temperature の指定自体を捨てる必要があります。「judge は temperature 0 で固定」という定石が使えなくなるので、判定のブレは実測で確認するしかありません(後述しますが、実測では実用上問題ありませんでした)。
非互換 3(最重要): reasoning トークンが出力予算を食い尽くす
これが一番怖い挙動でした。gpt-5 系は応答の前に内部で reasoning トークンを消費します。このトークンは max_completion_tokens の予算から引かれ、課金対象にもなります。
つまり max_completion_tokens: 100 のような小さい上限で JSON を返させようとすると、reasoning が予算を使い切り、HTTP 200 なのに content が空文字で返ってきます。finish_reason は length です。
このパターンの何が怖いかというと、エラーにならないことです。うちでは別の監視系で response_format: { type: "json_object" } を使っていて、空文字が JSON.parse に渡って例外になり、「判定不能」という中間的な状態として現れました。400 なら気付きますが、200 + 空応答は「モデルの調子が悪い」ように見えて原因究明が遅れます。
対処は 2 つの合わせ技です。
-
max_completion_tokensを余裕を持って確保する(体感で従来の 3〜5 倍。うちは空応答になっていた箇所を 2000 にして解消しました) - 深い推論が不要な用途では
reasoning_effort: "minimal"を渡して reasoning の消費自体を抑える
対処: パラメータの出し分けをヘルパー 1 本に集約する
呼び出し箇所ごとに if を書くと必ず漏れます。うちは移行時にヘルパー 1 本に集約しました。
/** モデル世代に応じた Chat Completions パラメータを返す */
export function chatCompletionParams(
model: string,
opts: { maxOutputTokens: number; temperature?: number },
): Record<string, unknown> {
const isReasoningFamily = /^(gpt-5|o\d)/.test(model);
const params: Record<string, unknown> = {
// max_tokens は gpt-5 系で 400 になるため、常に新パラメータ側に寄せる
max_completion_tokens: opts.maxOutputTokens,
};
if (!isReasoningFamily && opts.temperature !== undefined) {
params.temperature = opts.temperature;
}
if (/^gpt-5/.test(model)) {
params.reasoning_effort = "minimal";
}
return params;
}
呼び出し側は ...chatCompletionParams(model, { maxOutputTokens: 2000, temperature: 0 }) を spread するだけです。モデル名を将来また差し替えても、呼び出し箇所を触らずに済みます。
実際このヘルパーは移行の翌日、別の監視系(gpt-5-mini + response_format: json_object で空応答になっていた箇所)にもそのまま適用して直せました。集約した意味が 1 日で回収できた形です。
移行後に起きる「良い変化」にも注意
差し替え後の初回実行で、採点結果の分布が変わりました。gpt-4o-mini はほぼ全項目に満点を付けていたのに対し、gpt-5-mini は項目ごとに差を付けてきます(例: 正確性 3.5、簡潔さ 5)。
judge が厳しくなるのは望ましい変化ですが、スコアの基線が入れ替わるので、移行直後は品質スコアが下がって見えます。ダッシュボードやアラートの閾値を旧基線のままにしていると偽アラートが出ます。移行日をメモしておいて、数日〜1 週間は基線の入れ替わり期間として読む必要があります。
チェックリスト
gpt-4o 系から gpt-5 系への差し替えでやることをまとめます。
-
max_tokens→max_completion_tokensに全置換 -
temperature指定を gpt-5 系では出さない(出すと 400) -
max_completion_tokensを従来の 3〜5 倍に増やす(reasoning が予算を食う) - 深い推論が不要なら
reasoning_effort: "minimal" -
response_format: json_objectを使っている箇所は「200 + 空 content」のハンドリングを確認 - パラメータの出し分けはヘルパー 1 本に集約(呼び出し箇所ごとの if は漏れる)
- モックのテストだけで判断せず、実 API に 1 本投げてから deploy する
- 移行日を記録し、スコア基線の入れ替わり期間(数日〜1 週間)を想定する
7 番が本質です。今回の障害は、ユニットテストが全部緑のままデプロイして起きました。パラメータの互換性は実 API しか教えてくれません。
まとめ
gpt-5-mini は gpt-4o-mini と同価格で判定品質が上がる、移行する価値のあるモデルです。ただし Chat Completions のパラメータ互換性はなく、「モデル名の書き換えだけ」で移行すると 400 か、より怖い「200 + 空応答」で静かに壊れます。ヘルパー集約と実 API での事前検証をセットでやれば、移行自体は半日で終わります。
筆者は LLM API の呼び出しを記録・監視する SaaS を個人開発しており、本記事の障害と対処はその本番環境で実際に起きたものです。