Claude Media
「role system must precede」400エラーをClaude Codeで切り分ける

「role system must precede」400エラーをClaude Codeで切り分ける

400「role 'system' must precede an 'assistant' message」の意味と、v2.1.280以降も残る場合の切り分けです。プロキシやゲートウェイが挟んだsystemメッセージを疑う手順を示します。

API Error: 400 messages.6: role 'system' must precede an 'assistant' message or end the array; ... と出て、同じ会話が毎ターン失敗する。これは、会話の途中に置かれたsystemメッセージの位置をAPIが拒否した表示です。Claude Code自身が送った分ならv2.1.280以降で自動回復します。それでも出続けるなら、間に挟まるプロキシかLLMゲートウェイが自前のsystemメッセージを足している可能性が高くなります。

このエラーは何を拒否されているのか

APIが拒否したのは、messages 配列の途中に置かれた role: "system" のメッセージです。位置が許される場所から外れていました。

Claude Codeは、リマインダーや添付テキストの一部を、会話の中のsystemメッセージとして送ります。トップレベルの system フィールドを書き換えると、プロンプトキャッシュの先頭側が崩れるためです。履歴の末尾側に足せば、それまでのキャッシュはそのまま使えます。

この仕組みと位置の規則は、mid-conversation system messageの配置制約で表にしています。要点は、中身のあるsystemメッセージを置けるのが「user ターンの直後で、次が assistant か配列の末尾」という場所だけという点です。先頭や、tool_use と対応する tool_result の間は拒否されます。

エラー文の messages.6 は、messages 配列のうち拒否された要素を指す番号です。この番号を、あとで述べる切り分けで使います。

並びで見ると、拒否される形と通る形の違いが分かりやすくなります。左端の数字が messages の番号で、ここでは番号6が system です。

通る例                拒否される例
4 user                4 user
5 assistant           5 assistant
6 user                6 system     <- 直前がassistant、直後がuser
7 system              7 user
8 assistant           8 assistant

左の例では、番号7のsystemは user の直後にあり、次が assistant なので規則に合います。右の例の番号6は、次が user です。「assistant の前に来るか、配列の末尾で終わるか」のどちらにも当たらないため、must precede an 'assistant' message or end the array と拒否されます。エラー文は、この「どちらかでなければならない」という規則をそのまま述べています。

Claude Codeが送った分は自動で回復する

errorsページによると、APIが位置を拒否したとき、Claude Codeはそのテキストを通常のuserメッセージに変えて、リクエストを1回だけ再送します。次のような別の言い回しの拒否も、同じ回復の対象です。

use the top-level 'system' parameter for the initial system prompt

つまり、最新のClaude Codeを直接APIにつないでいる限り、この400は利用者の画面にほとんど出ません。出たとしたら、次のどちらかです。

くらべる

エラーが画面に出る2つの経路

v2.1.279以前

古いバージョン

この文言を認識できず、Claude Code自身が送ったsystemメッセージでも回復しません。会話の以降のターンが、すべて同じ形で失敗します。

v2.1.280以降

経路上の第三者

拒否されたsystemメッセージがClaude Codeの送った分ではないため、Claude Codeには取り除けません。プロキシやゲートウェイが足した分が典型です。

まずバージョンを確かめる

最初に確認するのはバージョンです。v2.1.280より前なら、原因がプロキシであっても、まず更新して再度試す価値があります。

claude --version
claude update

更新して会話を再開しても同じ400が出るなら、Claude Code自身の分は回復されている前提で、次へ進みます。

プロキシかゲートウェイが原因かを切り分ける

errorsページは、この状態を「refused system message isn't one Claude Code can remove」と説明し、間に挟まるプロキシかLLMゲートウェイが自前のsystemメッセージを足している可能性が高いとしています。切り分けは3段階です。

手順

経路を疑うときの切り分け

  1. 1

    ANTHROPIC_BASE_URLが設定されているか見る

    値が空でなければ、そのシェルで起動したClaude Codeはプロキシやゲートウェイを通っています。

  2. 2

    プロキシを外して同じ操作を試す

    ANTHROPIC_BASE_URL を外した状態で起動し、エラーが消えるかを確かめます。消えれば、原因は経路上にあります。

  3. 3

    新しい会話でも出るか見る

    /clear で新しい会話にしても出るなら、原因は保存された会話ではなく、リクエストの経路にあります。

1段目の確認は、次のコマンドで足ります。

echo "${ANTHROPIC_BASE_URL:-未設定}"

2段目は、環境変数を外して起動するだけです。

env -u ANTHROPIC_BASE_URL claude

この方法は、変数をシェルで設定している場合に効きます。settings.json の env ブロックで値を入れている場合は、設定側を一時的に外す必要があります。管理者がマネージド設定で allowedProviders に customEndpoint だけを許可している環境では、ゲートウェイ以外に向けたセッションをClaude Codeが拒否します(v2.1.285以降)。その場合は直接つないで試せないので、運営者に再現を依頼する流れになります。

ANTHROPIC_BASE_URL の役割は、ANTHROPIC_BASE_URLの設定で扱っています。

ゲートウェイ側で何を見ればよいか

プロキシの運営者に渡す材料があると、報告が早く済みます。リクエスト本文を記録できる環境なら、messages の role の並びを番号つきで出せます。

jq -r '.messages | to_entries[] | "\(.key) \(.value.role)"' request.json

出力のうち、エラー文にある番号(上の例なら6)の行が system です。その直前の行が user でない、または次の行が user(tool_result を含む)なら、位置の規則に当たります。ゲートウェイが自前のsystemメッセージを足したのか、Claude Codeが送った分の位置をずらしたのかは、元のリクエストとの差分で判別できます。

運営者側で見直す点は、LLMゲートウェイのプロトコルを扱うページにも載っています。

  • system の配列は、受け取ったとおりに転送します。先頭に別のsystemブロックを足す、順序を入れ替える、1本の文字列にまとめるといった操作は、属性ブロックの除去を無効にします
  • 属性ブロックは配列の独立した1要素のまま残します。別のsystemテキストを混ぜると、混ぜた分まで属性として捨てられ、システムプロンプトの残りも消えます
  • ゲートウェイが system の内容を整形する必要があるなら、クライアント側で CLAUDE_CODE_ATTRIBUTION_HEADER=0 を設定し、ブロックを出さない運用が案内されています
  • プロンプトキャッシュを使うなら、role: "system" の途中追加メッセージを含め、cache_control はそのまま転送します。ブロック形式のコンテンツを文字列に変換しません

最後の項目(cache_control の転送)は、このエラーそのものではなくキャッシュの話です。ただし、systemメッセージに手を入れるゲートウェイが踏みやすい別の落とし穴として並びます。症状はエラーではなく、毎ターン入力トークンが未キャッシュ扱いで課金される形で現れます。

プロトコル全体の考え方は、LLMゲートウェイのプロトコル解説が詳しいです。

繰り返す場合の対処と報告

経路が原因と分かったら、次の順で動きます。

  • ゲートウェイの運営者に、エラー文の全文とバージョン、再現したタイミングを渡して報告する
  • 報告の返事を待つ間は、/clear で新しい会話を始める。ゲートウェイが毎ターン同じ位置にsystemメッセージを足す構成なら再発するが、履歴が原因の場合は直る
  • 直接つなげる環境があるなら、その間だけ ANTHROPIC_BASE_URL を外して作業する

ゲートウェイを経由する会話では、上流がsystemメッセージ自体を拒否した場合の動きも別にあります。上流が会話途中のsystemメッセージ、またはそのメッセージの cache_control マーカーを拒否すると、Claude Codeは再試行し、その会話の残りの間はその機能を無効にします。つまり、上流がmid-conversation system messageに対応していないだけの構成では、会話は最終的に進みます。毎ターン失敗し続けるのは、位置の拒否が、Claude Codeの取り除けないsystemメッセージに対して起きている場合です。

似た400系エラーとの見分け方

プロキシ経由の400は、文言ごとに原因と回復の形が違います。Unsupported tool content removed を含む近い項目との見比べは、Unsupported tool content removedの解説の切り分け表にまとめています。履歴の食い違いで出る別の400は、thinking blockの食い違いを直す手順が受け持ちます。

本記事のエラーを見分ける手がかりは、文言に role 'system' と must precede が両方入っている点と、メッセージ番号が付いている点です。

errorsページの「エラー文言から探す」一覧には、この項目が role 'system' must precede an 'assistant' message の形で載っています。実際の表示は末尾に or end the array; ... と続くため、一覧を文字列検索するときは前半だけで探すと見つかります。エラー文の中の番号は会話ごとに変わるので、検索語には番号を含めません。

設定を疑う順番の早見表

状況疑う場所最初の一手
v2.1.279以前で出る疑う場所Claude Code自身のsystemメッセージ最初の一手claude update
v2.1.280以降、ゲートウェイなしで出る疑う場所経路上の別の仕組み(社内プロキシなど)最初の一手経路を確認し、運営者に報告
v2.1.280以降、ゲートウェイ経由でのみ出る疑う場所ゲートウェイが足したsystemメッセージ最初の一手プロキシを外して再現を比べる
/clear 後は消えるがまた出る疑う場所履歴ではなく、毎ターンの追加処理最初の一手リクエスト本文の role 並びを記録する
この記事を共有:XはてブLinkedIn