0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LiteLLMのUI追加モデルとconfig.yamlは同名で併存する — 消えないDB行と分岐するトラフィックをソースと実測で追う

0
Last updated at Posted at 2026-09-12

前回、LiteLLMのUIで追加したモデルが401を返す事故を書いた。原因はUI追加経路では os.environ/ 参照が解決されないことで、解決の過程でDBのモデル行をYAMLへ移設した。そのとき行った後始末の1つに「DBに残った同名の行を削除する」があり、そのときは脇道として片付けたのだが、放置したら何が起きたかを掘っていない。

本稿の問いはこれ1つだ: config.yamlとUI追加 (DB) の同名モデルは、プロキシ内部でどう併存するのか。 答えから言うと、両者は「同じ名前の設定」ではなく別々のidを持つ2つのdeploymentとして同一のmodel_groupに並び、デフォルトのルーティングが両方へリクエストを振り分ける。config.yaml側を削除してもDB行は生き続け、DB行を消すまでモデルは消えない。

この記事を読み終えた時、手元に残るもの:

  1. 同名併存が「設定の上書き」でなく「deploymentの追加」になる仕組み (同期diffがid基準であることのソース該当箇所)
  2. 同名併存が実際にトラフィックを分岐することの実測 (api_baseの異なる2deploymentへの振り分け)
  3. yaml側を消してもDB行が生き残ること、そして消し方がUI削除しかないことの実測

環境と計測方法

  • LiteLLM Proxy 1.83.10 (公式Docker image main-stable。稼働環境と同一versionの /app/litellm ソースを実読)
  • postgres + STORE_MODEL_IN_DB: True (UIで追加したモデルはDBに保存される)
  • 実挙動は隔離した検証用スタックで測った: 本番と同構成のプロキシに、APIの向き先 (api_base) だけ違う2つのモック上流 (/a/b) を用意し、どちらのdeploymentにリクエストが届いたかを上流側のログで数える。api_keyの値はどの出力にも出さない (本文中のSQLも値を出さない列設計にしてある)

検証はすべて廃棄可能なコンテナで行い、稼働中のゲートウェイには書き込んでいない。

前提: モデル一覧には2つの源泉がある

litellm proxyのモデル定義は、2つの経路からrouterに載る。前回の記事の構図をIDの視点で引き直す。

経路A: config.yaml (起動時読み込み)

起動時、config.get("model_list") がそのまま litellm.Router(model_list=...) へ渡る。このとき os.environ/ 参照は解決される。ここまでは前回書いた通り。

今回追加で押さえるのはidの由来だ。config.yaml側のモデルに明示的な model_info.id を書いていない場合、idは _generate_model_id(model_group, litellm_params) で作られる — model_nameとlitellm_params全体から作る決定的なハッシュだ。同じ内容からは常に同じidが生成される。

経路B: DB行 (UI追加)

UIの「Add model」(POST /model/new) で追加したモデルは暗号化されてDBに保存され、反映時は復号だけを行う経路でrouterへupsertされる。こちらのidはDB行のmodel_id (UUID) で、get_model_info_with_iddb_model: true のフラグと一緒に付ける。

つまり同名のモデルを両方の経路で定義すると、config側は「paramsのハッシュ」、DB側は「行のUUID」という異なるidを持つ2つの定義になる。

同期のdiffはid基準 — 同名では削除されない

「DBに残った同名行を削除した」とき、なぜ削除が必要だったのか。idの視点で読むと見える。

プロキシは起動時やモデル追加・削除のたびに add_deployment を呼び、DBの現在値とrouterの状態を同期する。この中の _delete_deployment (proxy_server.py) がやっていることを要約すると:

  1. 合集を作る — DB行のid全部と、config.yamlのモデルid全部を1つのリストにまとめる (config側は os.environ/ を解決してからidを生成する)
  2. routerと比較する — routerが今持っているdeploymentのidのうち、合集に存在しないものだけを delete_deployment(id=...) で削除する

比較の単位が一貫して model_info.id であり、model_nameは削除判定に一切関与しない。だから「config.yamlに同名を書いたからDBの古い定義は上書きされる」ことは起きない。idが違う2つは、同期のどのタイミングでもそれぞれ「まだ存在するもの」として扱われ、両方ともrouterに残る。

上書きに見える操作が1つだけある。Router.upsert_deployment はidが同一のdeploymentを見つけたら置換する。だがこれはidが同じ場合 (DB行の再保存など) の話で、config側 (ハッシュid) とDB側 (UUID) が同じidになることはない。

実測1: 同名併存は「2行のモデル一覧」として見える

隔離スタックで確かめる。config.yamlに echo-dupe (上流はモックの /a) を定義して起動し、UI経路で同名 echo-dupe (上流 /b) を追加した直後の /model/info がこれだ。

echo-dupe | id: 278739dd1678e3f1... | db_model: False | api_base: .../a
echo-dupe | id: e6027d1a-036d-4cbc-... | db_model: True  | api_base: .../b

同じ model_name の行が2つ並ぶ。idの形式も違えば出自のフラグ (db_model) も違う。UIのモデル一覧ではこの2行が同じ名前で並んで見えるので、「設定が二重に見える」状態そのものだ。

実測2: トラフィックは2つのdeploymentに分岐する

見た目だけなら「重複行の表示バグ」で済む。そうではないことを、上流で数える。

追加前 (config側だけ) に echo-dupe へ6リクエスト送ると、6件とも /a に届く — deploymentが1つなので分岐のしようがない。UI追加後に10リクエスト送ると:

/a に 3件
/b に 7件

10走で3:7なのはランダム振り分けの揺らぎで、比率に意味はない。意味があるのは両方に届いたことだ。同名のmodel_groupにdeploymentが2つあると、デフォルトのルーティング戦略 (simple-shuffle) はグループ内のdeploymentから選んでリクエストを投げる。つまり同名併存は「死んだ古い定義が残る」のではなく、生きている第2の経路に毎リクエストが振り分けられる

ここが実運用で効く。片方のdeploymentだけが壊れている場合 (前回の401のように、DB側だけkeyがリテラルのまま等)、同じモデル名へのリクエストが安定して失敗するのではなく、一部だけ失敗する。10回中7回成功する断続的な401は、恒常的な401より原因の切り分けが難しい。「同じkeyなのに一部だけ落ちる」観測の読み方は前回書いたが、同名併存はそれとは別の、プロキシ内部で作られる断続失敗の機構だ。

実測3: config.yaml側を消してもDB行は生き残る

次に、yaml側を消すケース。config.yamlから echo-dupe を削除してプロキシを再起動した。

other-model | db_model: False | api_base: .../b   # yamlに残した別モデル
echo-dupe   | db_model: True  | api_base: .../b   # DB行だけが生き残った

再起動後も echo-dupe/model/info にいて、リクエストは4件とも /b (DB行の上流) に届いた。同期diffの合集からyaml側のidが消えても、DB行のidは合集に残っているため、対応するdeploymentは削除されない。「YAMLに書いていないのにモデルが消えない」のはこの機構で、YAML側の一覧を見て運用している人には状態が見えにくい。

反対方向の削除は即座に効く。UIの削除 (POST /model/delete にidを指定) でDB行を消すと、再同期で合集からidが外れ、deploymentが削除され、echo-dupe へのリクエストは400 (モデル不在) になった。同名併存を解消する手段は、DB行の削除しかない。

確認と後始末のクエリ

併存に気づくかどうかが実害を決める。DBを直接見るときは、値を出さない列設計で。

select model_name, model_id, (litellm_params ? 'api_key') as has_api_key
from "LiteLLM_ProxyModelTable";

model_name の重複があれば、config.yamlの model_list と突き合わせて同名がないか確認する。前回記事の os.environ/% 検出 (api_key のリテラル残存チェック) と併せると、「解決漏れ」と「併存」の両方を1クエリずつで拾える。

運用方針としては前回と同じ結論になる: モデル定義はconfig.yamlに一元化し、UIからの追加はしない。一元化していてもUIで1度でも追加するとDB行は独立に生きるので、併存の監視はDB側で行う (yaml側の一覧だけ見ていると気づけない)。

限界

  • 読んで実測したのは1.83.10 (2026-09時点のmain-stable)。_delete_deployment / upsert_deployment / _generate_model_id の構造が将来のversionで変わる可能性がある
  • ルーティングの実測はデフォルトの simple-shuffle のみ。least-busy 等を明示した場合の振る分け方は別の挙動になりうる (本稿では「同名があると分岐する」ことだけを主張する)
  • 実測はモック上流2面の隔離スタックによる。実プロバイダーでの障害時の挙動 (retryが反対側のdeploymentに切り替わるか等) は踏んでいない
  • DB保存時の暗号化・鍵管理の詳細は前回同様スコープ外

2源泉の構造 (config.yaml = 起動時・解決つき・決定的id / DB = 追加時・復号のみ・行id) を1回押さえると、「UIで直したのに反映されない」「yamlから消したのに消えない」「一部のリクエストだけ失敗する」という別々の症状が同じ土台で読めるようになる。

モデルを追加したら実タスクで使い倒して選定に使う手順は、LLMはベンチマークで選ばない — 自業務の実タスクで60分比較する手順にまとめてある。前回の401記事 (LiteLLMのUIで追加したモデルだけ401になる) と合わせて読むと、UI追加経路の落とし穴が一望できる。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?