夕方にデプロイしたばかりの定型処理から、「このモデルは現在利用できません」というエラーが返ってきたことがありました。リトライしても結果は同じで、私は同じ呼び出しを十回ほど繰り返し、ログだけを無駄に太らせてしまいました。
あとから気づいたのは、リトライは「同じ条件をもう一度試す」操作だということです。拒否されたのがモデル ID そのものなら、待っても条件は変わりません。 待つのではなく、指定を切り替える設計が要るのだと思います。
ここからは、そのエラーを 529 や 429 から切り離して扱う方法と、リクエスト単位でモデルを切り替えるフォールバックの実装をお伝えします。あわせて、以前の私が思い込みで書いて後から直した箇所も正直に残しておきます。
このエラーは何を意味しているのか
529 Overloaded や 429 Rate Limit は、サーバーの混雑や自分の使いすぎが理由です。しばらく待てば通るので、SDK の自動再試行に任せるのが筋になります。
一方、このエラーは「いま指定したモデル ID を、この経路では受け付けられない」という拒否です。待てば直ることもありますが、直らないこともあります。両者を同じ網で扱うと、混雑の最中にフォールバック先へ負荷を流し込んだり、逆に恒久的な拒否を延々とリトライしたりしてしまいます。
なお、エラー文面は経路(Anthropic 直、クラウド事業者経由)や時期によって変わります。この記事の判別ロジックは、文面の完全一致に頼らず、ステータスコードと SDK の例外クラスを軸にしています。ご自身の環境で実際に返ってきた応答を、一度ログで確かめていただければ幸いです。
出やすい場面を五つに分けて見る
私が見てきた範囲では、拒否の背景はおおむね次の五つに分かれます。発生割合の数字は取っていないので、頻度の順位づけはしません。
| 場面 | 典型的な状況 | 対処の向き |
|---|---|---|
| 新モデルの公開直後 | 新しい ID を指定したが、経路やリージョンによってまだ届いていない | 前世代のモデルへ切り替える |
| 廃止済み・廃止間近の ID | 古い ID を固定で書き続けている | 廃止日を確認し、後継へ移す。移行前の保険としてフォールバックを持つ |
| クラウド事業者経由 | Bedrock や Vertex AI でリージョンごとに提供モデルが違う | リージョンごとに候補リストを持つ |
| ID の書き間違い・別名の混在 | 日付付き ID と別名を取り違えている | 起動時に ID の存在を一度確かめる |
| 権限・プランの制約 | アカウントに許可されていないモデルを指定している | 切り替えでは直らない。通知して人が見る |
表の最後の行がいちばん大事です。権限の拒否はフォールバックで隠してはいけません。切り替えて動いてしまうと、本来は気づくべき設定の不一致が見えなくなるからです。
モデル ID のフォールバック表を持つ
最初に作るのは、用途ごとの「優先モデル」と「落ちたときの代役」の表です。
type ModelTier = "frontier" | "balanced" | "fast";
interface ModelFallbackChain {
primary: string;
fallbacks: string[];
}
// ID は実在するものだけを書く。書き換え前に公式のモデル一覧で確認してください
const FALLBACK_CHAINS: Record<ModelTier, ModelFallbackChain> = {
frontier: {
primary: "claude-opus-4-6",
fallbacks: ["claude-sonnet-4-6"],
},
balanced: {
primary: "claude-sonnet-4-6",
fallbacks: ["claude-sonnet-4-5", "claude-haiku-4-5-20251001"],
},
fast: {
primary: "claude-haiku-4-5-20251001",
fallbacks: ["claude-sonnet-4-6"],
},
};以前の版では、存在するかどうかを確かめないまま claude-haiku-4 のような ID を代役の欄に並べていました。実在しない ID は、フォールバックの途中で必ず失敗します。代役の列こそ、いちばん慎重に書くべき場所でした。
拒否の種類を例外クラスで見分ける
判別の軸は、メッセージの部分一致ではなく、SDK が投げる例外の種類にします。文面は変わっても、ステータスの意味は比較的変わりにくいからです。
import Anthropic from "@anthropic-ai/sdk";
type Verdict = "fallback" | "alert" | "rethrow";
// 404: モデルが見つからない → 切り替える
// 403: 権限がない → 切り替えず、人に知らせる
// 429 / 529 / その他: SDK の再試行に任せ、フォールバックしない
function classify(err: unknown): Verdict {
if (err instanceof Anthropic.NotFoundError) return "fallback";
if (err instanceof Anthropic.PermissionDeniedError) return "alert";
if (err instanceof Anthropic.BadRequestError) {
// 400 はパラメータ不備でも返る。モデル起因と読める文面のときだけ切り替える
const msg = (err.message ?? "").toLowerCase();
if (msg.includes("unavailable") || msg.includes("not available")) return "fallback";
}
return "rethrow";
}
async function invokeWithFallback(
client: Anthropic,
tier: ModelTier,
buildRequest: (modelId: string) => Anthropic.Messages.MessageCreateParamsNonStreaming
): Promise<Anthropic.Messages.Message> {
const chain = FALLBACK_CHAINS[tier];
const candidates = [chain.primary, ...chain.fallbacks];
let lastError: unknown;
for (const modelId of candidates) {
try {
return await client.messages.create(buildRequest(modelId));
} catch (err) {
const verdict = classify(err);
if (verdict === "fallback") {
console.warn(`model ${modelId} rejected, trying next`);
lastError = err;
continue;
}
if (verdict === "alert") {
await notifyOwner(`permission denied for ${modelId}`); // 通知先は各自の環境で
}
throw err; // 切り替え対象でないものは、握りつぶさずに上へ返す
}
}
throw lastError;
}ここで 429 や 529 を意図的にフォールバックの外に置いているのは、混雑中に代役へ同じ負荷を移しても状況が良くならないからです。再試行は SDK の max_retries に任せ、切り替えは「モデルそのものが受け付けられない」ときだけにします。
モデルごとに違う機能を、表で持つ
もうひとつ、フォールバックで踏みやすいのが機能の食い違いです。優先モデルでは通っていた拡張思考などのパラメータが、代役では受け付けられないことがあります。この場合に返るのは「モデルが使えない」ではなく、パラメータ不備の 400 です。
そこで、モデルごとに「使える機能」を表にして、リクエストを組み立てる側で出し分けます。
interface ModelCapabilities {
extendedThinking: boolean;
}
// 値は例です。対応状況は公式ドキュメントで確かめ、実際の値に書き換えてください
const CAPABILITY_MATRIX: Record<string, ModelCapabilities> = {
"claude-opus-4-6": { extendedThinking: true },
"claude-sonnet-4-6": { extendedThinking: true },
"claude-sonnet-4-5": { extendedThinking: true },
"claude-haiku-4-5-20251001": { extendedThinking: false },
};
function buildRequestForModel(
modelId: string,
prompt: string,
tools: Anthropic.Messages.Tool[]
): Anthropic.Messages.MessageCreateParamsNonStreaming {
const caps = CAPABILITY_MATRIX[modelId] ?? { extendedThinking: false };
return {
model: modelId,
// 拡張思考を使うときは budget_tokens より大きい max_tokens が必要
max_tokens: caps.extendedThinking ? 20000 : 4096,
messages: [{ role: "user", content: prompt }],
tools,
...(caps.extendedThinking
? { thinking: { type: "enabled" as const, budget_tokens: 16384 } }
: {}),
};
}以前の私のコードは、max_tokens: 4096 のまま budget_tokens: 16384 を渡していました。拡張思考の予算は max_tokens より小さくなければならないので、これはフォールバック以前に、最初の呼び出しから失敗する書き方です。手元の確認では通っていた「つもり」だったのは、思考を切った経路だけを試していたからでした。
表に載っていない ID には、機能を足さない既定値を当てています。分からないときは、機能を減らして確実に返すほうへ倒す、という線引きです。
切り替わった事実を記録する
フォールバックは、うまく動くほど気づきにくくなります。優先モデルが落ちたまま代役で動き続けていても、画面上は成功に見えるからです。
interface ModelInvocationLog {
timestamp: string;
intendedModel: string;
actualModel: string;
fallbackDepth: number; // 0 = 優先モデルで成功
taskType: string;
outcome: "success" | "all_failed";
}このログを週に一度だけ集計し、fallbackDepth が 1 以上の割合を眺めます。見直しの目安になる割合は、扱う処理の重要度で変わるので、最初の数週間の実測を見てから自分で決めるのが無理のない進め方です。私も数字を先に決めず、まず記録を貯めるところから始めました。
開発中にこの経路を再現する
本番でしか出ないエラーは、テストが後回しになりがちです。存在しない ID(たとえば claude-opus-99-test)を model に渡すと、モデルが見つからない旨の 404 が返ってくるはずです。実際に返った応答を一度ログに残し、classify の単体テストのデータに使うと、フォールバックの全段が正しく次へ進むかを CI で確かめられます。
本番でモデルが落ちた瞬間に「ここは試していなかった」と気づくのは、胃が痛くなる経験でした。その夜から、チェーンの全段が失敗した場合の動き(throw lastError に至る経路)も、テストに含めるようにしています。
まず一つだけ、今日やること
次の一行を、いまのコードに足してみてください。優先モデルで呼び出したあとに、実際に使われたモデル ID を、レスポンスの model フィールドからログへ残す、というだけです。フォールバックを作る前に、どのくらい切り替わっているのかが見えるようになります。
そのうえで、404 と 403 を切り分ける classify を入れていただければ、権限の問題を代役で隠してしまう失敗は避けられます。