症状: エラーが一度も出ていない
生成AIモデルの利用状況を棚卸ししていたところ、ある処理が呼んでいるモデルが3ヶ月半前に提供終了していたことが分かりました。
コードは変えていません。エラーも警告も一度も出ていません。処理自体は毎回正常に完了していました。
真因: プロバイダが古いモデル名を黙って後継へリダイレクトしていた
提供終了したモデル名でリクエストを送ると、プロバイダ側がそれを後継モデルへ内部的にリダイレクトしていました。API呼び出し側から見ると、古いモデル名を指定したリクエストが正常なレスポンスを返し続けます。
問題は課金です。実際に処理していたのは後継モデルで、課金もその後継モデルの単価で発生していました。 呼び出し側は古いモデル名のつもりでコストを見積もっていたので、単価がいつの間にか変わっていることに気付きようがありませんでした。
エラーになる方がまだ良心的です。エラーなら気付けます。「動き続けている」ことが、この件を隠していました。
なぜ機械的に検知できないのか
モデルの提供終了予定日を返すAPIは、どのプロバイダにも存在しません。実際にモデル一覧を返すAPIを呼んでみても、返ってくるのはID・作成日・表示名・トークン上限程度で、提供終了予定日という項目自体がありません。
この情報は公式ドキュメントのページにしか載っていません。 しかも各社バラバラな形式です。
最初に作ったもの: 人手更新の台帳
最初の対策は、使用中のモデルとその提供終了予定日を記録した台帳でした。
model_id provider repo retirement_date last_verified source_url
last_verified(最後にこの情報を確認した日)が閾値より古くなっていたら警告する、というだけの仕組みです。これは失敗でした。 「人がドキュメントを見て台帳を更新する」という運用は、結局のところ最初に見つかった問題(人が思い出したときにしか確認しない)を、形を変えて再現しているだけでした。台帳という仕組みを作ったこと自体に満足して、更新が滞ることは防げていませんでした。
作り直したもの: ドキュメントを直接パースする
台帳を人手更新から自動取得へ作り替えました。実行のたびに各プロバイダの公式ドキュメントを取得し、その場でパースして得た値を判定に使います。台帳ファイルは「commitされた基準値」に格下げし、実行時の判断材料にはしません。
プロバイダごとに掲載形式が違うため、パーサも別々に持っています。1つ具体的に見せます。
def parse_deprecation_table(body: str, model: str):
"""廃止表: | shutdown date | `model` | `replacement` |
2列目(Model列)を完全一致で見る。推奨移行先の列は見ない。
"""
for line in body.split("\n"):
if not line.lstrip().startswith("|"):
continue
cells = [c.strip() for c in line.strip().strip("|").split("|")]
if len(cells) < 2:
continue
ids = [i.strip() for i in re.findall(r"`([^`]+)`", cells[1])]
if model not in ids:
continue
retire = parse_date(cells[0])
if retire is None:
return "unverified", None, f"提供終了日欄を解釈できません: '{cells[0]}'"
return "ok", retire.isoformat(), "廃止表に掲載あり"
return "ok", "NONE", "廃止表に掲載なし"
完全一致にこだわっているのには理由があります。 廃止表には似た名前のモデルが並びます。あるモデル名が別のモデル名の部分文字列になっているケースがあり、部分一致で探すと、廃止されていない方まで誤って「廃止対象」と判定してしまいます。逆に、廃止表の3列目(推奨移行先)まで検索対象に含めると、今度は現行モデルが移行先として載っているせいで、廃止されていないモデルまで拾ってしまいます。どの列を、どういう一致条件で見るかを1つ間違えるだけで、静かに間違った結論を出す作りになります。
もう1社、掲載形式がさらに違うプロバイダもありました。廃止の一覧表を持たず、個別の告知ページに情報が散っています。そこで全ドキュメントを連結した索引ファイルを取得し、パスに「retirement」を含むセクションだけを対象にしました。ここでも同じ罠があり、廃止ページの本文には移行先として現行モデルの名前が何度も登場します。単純な文字列出現で判定すると、現行モデルまで「廃止された」と誤検知します。実際に踏んだので、「廃止スラッグの箇条書き」と「移行表の左列」という、構造的に限定された位置だけを見るようにしました。
失敗したときに黙って成功させない
このパーサ群には、もう1つ大事な設計があります。取得や解析に失敗したら、それを「提供終了予定なし」として扱わず、はっきり「確認できなかった」という状態を返します。
status, new_retire, note = parser(body, model)
live[model] = {"status": status, "retire": new_retire, "note": note}
if status != "ok":
unverified.append(f"{model}({provider}): {note} / {url}")
continue
ここを「取得に失敗したら前回の値をそのまま使う」という作りにすると、パーサがドキュメントの構造変化で静かに壊れた瞬間から、自動化される前の状態(誰も見ていない)へ黙って戻ります。 そして自動化した安心感だけが残ります。
失敗を明示的な状態として扱っておけば、この「確認できなかった」という状態自体を、台帳の鮮度検査(自動確認が壊れて何日経ったか)に引き継げます。自動化が壊れたことが、自動化されたままの見た目で放置される、という一番怖い状態を避けるための設計です。
45日という数字の根拠
台帳の鮮度検査は残してあります。理由は、パーサが壊れて自動取得ができなくなったモデルについては、依然として人が確認するしかないからです。その閾値をなぜ45日にしたか、という点も書いておきます。ラウンドナンバーではありません。
プロバイダが提供終了を予告する期間には最短の実績があります。今回参照した範囲では、最短の予告期間は60日でした。
確認の間隔を60日より長くしてしまうと、「予告が出てから提供終了するまで」の期間が、2回の確認の間にすっぽり収まってしまう可能性があります。そうなると、予告を一度も見ないまま提供終了を迎えます。
確認の間隔を45日にすれば、どんなに短い予告期間であっても、その間に必ず1回は確認のタイミングが挟まります。「見に行く頻度」を、プロバイダが保証している最短の猶予期間より短くする、というのが根拠です。
最後の砦: モデル一覧APIへの実在確認
ドキュメントのパースとは別に、プロバイダがモデル一覧を返すAPIを提供している場合は、そのモデルIDが今も一覧に実在するかを機械的に確認しています。
これが実質的に最も強い検知です。ドキュメントの記載がどうであれ、モデルIDが一覧から消えていれば、それは提供終了の動かぬ証拠だからです。パーサがドキュメントの構造変化に対応できていない間も、この確認だけは生き続けます。APIキーが無い環境ではスキップする設計にしてあり、ドキュメントのパースと合わせて二重の検知経路にしています。
まとめ
- 提供終了したモデルへのリクエストが、プロバイダ側で後継モデルへ黙ってリダイレクトされることがあります。エラーにならないので気付けません
- リダイレクト先の単価で課金されるため、コストの見積もりが実態とずれます
- モデルの提供終了予定日を機械取得できるAPIは無く、情報は公式ドキュメントにしか載りません。プロバイダごとに掲載形式が違うため、パーサも別々に持つ必要がありました
- 名前の似たモデルを部分一致で探すと、廃止されていないモデルを誤って拾います。どの列を、どういう一致条件で見るかを厳密にしてください
- 取得や解析に失敗したときは「変化なし」ではなく「確認できなかった」という状態を明示的に返してください。そうしないと自動化が壊れたこと自体に誰も気付けません
- 確認間隔は、プロバイダが保証する最短の予告期間より短く設定してください
- ドキュメントのパースだけに頼らず、可能なら実在確認(モデル一覧APIとの突合)を別レイヤーとして持ってください。ドキュメントより実態に忠実です

