10分

推論バックエンドを一日で載せ替え、その請求書を十三日かけて払った

コードが一つのサーバーの方言に縛られていました。互換シグネチャを先に立てて呼び出し元100か所超を一日で移し4,500行を消しましたが、その互換層が残した痕跡を片づけるのにさらに十三日かかりました。

  • vLLM
  • LiteLLM
  • マイグレーション
  • リファクタリング
Mac mini上のOpenMakeが専用ネットワーク経由でDGX SparkのvLLMへ推論を送る構成図
いまの構成はこの移行の結果です。アプリケーションと推論のあいだの境界がOpenAI互換の規格になって初めて、その先を入れ替えられるようになりました。

SHIPPED / EVIDENCE

今回リリースしたもの

ローカル推論のバックエンドを、Ollama独自のHTTP APIからLiteLLMプロキシの背後にあるvLLMへ移しました。OpenAI互換の規格だけを話す新しいモジュールを、従来のクライアントと同じシグネチャのまま立て、呼び出し元を一度に移してから旧実装15ファイルを削除しました。

新しいモジュール
1,682行
削除した実装
15ファイル4,500行
一括で移した呼び出し元
100か所超
痕跡の清算まで
13日

01

コードが一つのサーバーの方言に縛られていた

それまでローカル推論はOllama独自のHTTP APIを通していました。行区切りJSONのストリーミング、複数の鍵を回して使う鍵プール、そしてチャットと埋め込みとWeb検索がそれぞれ別の専用経路です。この形がクライアントのコードの奥まで入り込んでいました。

移り先はLiteLLMプロキシの背後のvLLMで、こちらはOpenAI互換の規格だけを話します。問題は宛先を変えるだけの話ではありませんでした。ストリーミングの形式が違い、使用量を数える単位が違い、片方にしかない機能があります。

しかもこのコードを呼ぶ場所が100か所を超えていました。全部を直しながら同時に規格まで変えると、何が壊れたのか分からなくなります。

02

互換シグネチャを先に立てた

そこで最初の段階は移行ではなく準備でした。OpenAI SDKの上に薄いラッパーとして新しいモジュールを作りつつ、外から見えるシグネチャは既存のクライアントとまったく同じに合わせました。呼ぶ側から見れば、名前が違うだけで使い方は同じものです。

型定義685行には、あえて古い名前をそのまま残しました。名前を整えることとバックエンドを入れ替えることを同じコミットでやると、あとで問題が起きたときにどちらが原因か切り分けられません。互換を先に、整理は後回しにしました。

  • ストリームのパーサーはOpenAIのデルタ形式と推論フィールドを扱えるよう書き直しました
  • 思考の強度の設定はオプトインとし、既定では無効にしました
  • 使用量の追跡を時間基準からトークン基準へ変えました。同じ名前の上限が別の意味を持つ変更なので、別立てで明示しました
  • ツールを複数ターンにまたいで呼ぶループは、OpenAIのツール仕様に合わせて作り直しました

03

バックエンドを変えると消える機能があった

旧バックエンドにはWeb検索の専用経路がありましたが、vLLMにはありません。規格を標準に合わせると、標準に無い機能も一緒に消えます。

無くすわけにはいかないので、別の層へ移す必要がありました。アダプターを一つ置いて、Web検索の呼び出しをMCPのツール側へ委任するようにしました。推論サーバーが提供していた機能が、ツール層の責務へ移ったことになります。

結果としてこちらのほうが正しい置き場所でもありました。検索は推論エンジンの仕事ではなくツールの仕事であり、あとで検索の提供元を変えるときも推論バックエンドに触れずに済むようになりました。

04

100か所を一度に移し、4,500行を消した

二段階目は機械的な作業でした。importのパスを一括で置き換え、設定の参照名を変えるcodemodをリポジトリ全体に走らせました。シグネチャを合わせておいたおかげで、呼ぶ側はパスを更新するだけでそのままコンパイルできました。

そして旧ディレクトリを丸ごと削除しました。クライアント、鍵マネージャー、鍵プール、クールダウン、ストリームパーサー、コネクションプール、インターセプターまで15ファイル約4,500行です。廃止した実装に依存していたテストも一緒に整理しました。

ここでは削除が肝心です。新実装を入れて旧実装を残すと二つの経路が並存し、どちらが本物か知っている人が次第に減っていきます。互換シグネチャは移行のための橋であって、二つの実装を長く繋ぎ止めておくための仕掛けではありませんでした。

05

設定とラベル、そして意図的に残したもの

三段階目は人が見る部分でした。設定の例示ファイルから旧バックエンド専用の変数を大量に外し、新しい変数8個へ整理し直しました。複数の鍵、複数のモデル枠、クラスターノードの一覧といったものがこのとき消えました。プロキシのアドレス一つで足りるようになったからです。

ただし一つだけ意図的に残しました。この提供元を内部で識別する文字列が、データベースの制約とモデル識別子の接頭辞に埋め込まれていて、名前を変えるだけでもデータ移行が付いてくる状態だったのです。いますぐ必要な変更ではなかったため、後続の課題として書き残したうえで置いておきました。

残すと決めることと、忘れることは違います。これは十三日後に実際に片づきました。

06

同じ日の夕方、codemodが取りこぼしたもの

一括置換はパターンに合うものしか拾いません。その日の夕方に残りが表に出ました。

設定オブジェクト経由で読む形はcodemodが拾いましたが、環境変数を直接読む箇所は形が違って手つかずのまま残っていました。既定のモデル名がコードに埋め込まれていた箇所もあり、これを毎回環境変数を読み直す形へ変えて、再起動やテストでの上書きが即座に反映されるようにしました。

いちばん目立ったのはモデル一覧を確認するコードでした。旧バックエンド専用の経路を呼び、その応答形式を前提にし、失敗すると旧バックエンドのコマンドを案内していました。これをOpenAI標準の経路と応答形式へ変え、エラーメッセージもプロキシの設定と起動オプションを確認する案内へ直し、いまどのモデルが公開されているかを併せて表示して原因がすぐ分かるようにしました。

旧バックエンドにしか無かったクラウド区別の接尾辞を検証するロジックも、このとき無くなりました。新しいバックエンドにその区別はありません。

07

境界が標準になると、その先が自由になった

移行が終わったあとに起きたことが、この作業の値打ちを示しています。二日後にはモデルごとの起動スクリプトとサービスユニット、プロキシの別名設定が入りました。五日後にはローカルモデルのカタログができ、チャットモデル4つと埋め込みモデル1つを一覧として扱い、環境変数でその一覧を丸ごと差し替えられるようになりました。

その後の二週間で、実際に配信するモデルの構成は何度も変わりました。そのたびにアプリケーションのコードには触れていません。プロキシがモデル名でルーティングし、クライアントはアドレスを一つ知っていればよい構造なので、変わるものが設定ファイルの中で完結したからです。

最後に5月末、内部の提供元識別子を標準の名前へ変えて移行を締めました。三段階目であえて残しておいた、あの負債です。

08

互換層の請求書

この移行で互換シグネチャは確かに効きました。100か所を超える呼び出し元を一日で移せたのはそのおかげで、そうしなければバックエンドの入れ替えと呼び出し元の修正が絡まり、原因を切り分けられなくなっていたはずです。

同時に互換層は請求書を残します。古い名前が残る型定義、形が違って取りこぼした直接参照、データベースの制約に縛られて変えられなかった識別子が、すべてその請求書です。移行そのものは一日でしたが、請求書を払い切るには十三日かかりました。

差を生んだのは、払うと決めてあったことです。残すものごとに、なぜ残すのかといつ片づけるのかをコミットに書いておいたので、痕跡は忘れられる代わりに一つずつ消えていきました。互換層は橋として使うぶんには有用ですが、渡ったあとも置いておけば、それはただの二つ目の実装です。

根拠資料

根拠資料

Engineering Logへ戻る