メインコンテンツへ移動
AIチュートリアル

Underclassで堅牢なAIサブスクリプションプールを構築する

Underclassの実行方法、OAuthデバイスフローによるサブスクリプションアカウントの追加、OpenCodeの接続、ルーティングとクールダウンの設定、モデルの確認、Cargo・Nix・NixOSを使ったプロキシの安全な運用方法を学びます。

Underclassで堅牢なAIサブスクリプションプールを構築する

Underclassの概要

Underclassは、複数のChatGPT/CodexおよびGitHub Copilotのサブスクリプションを、OpenAI互換の単一エンドポイントに統合するローカルプロキシです。クライアントは使い慣れた/v1/*ルートで通信し、Underclassが利用可能なサブスクリプションを選択してクォータの状態を追跡し、適切な上流サービスへリクエストを転送します。

各サブスクリプションに個別のクォータがある場合に便利です。いずれかのアカウントが上限に達するたびに手動で切り替える代わりに、アカウントを共有プールに追加できます。クォータを使い切ったアカウントはクールダウン状態になり、クォータ期間がリセットされると自動的にローテーションへ復帰します。

Underclassは、アカウントの状態、セッションの親和性、プロンプトキャッシュの有効性を維持しながら、十分な容量を持つ1つのプロバイダーのように動作することを目的としています。

主な機能

  • OpenAI互換API: クライアントは/v1/responses、/v1/chat/completions、/v1/modelsを利用できます。
  • 複数のバックエンド: プールにはChatGPT/Codexアカウント、GitHub Copilotアカウント、またはその両方を追加できます。
  • スティッキーセッション: prompt_cache_keyまたはpromptCacheKeyを含むリクエストは、可能な限り同じサブスクリプションに割り当てられ続けます。
  • 自動クールダウン: クォータに関するレスポンスを受けると、アカウントは再試行可能な期間が終了するまでローテーションから外れます。
  • 高速な飽和レスポンス: モデルを利用できるすべてのアカウントがクールダウン中の場合、Underclassは無期限にキューへ入れる代わりに、最も早く利用可能になるRetry-After値を含む429を返します。
  • 処理中リクエスト数の最少選択: 利用可能なCodexおよびCopilotアカウントをフラットなプールで競合させ、各バックエンドのモデルカタログで絞り込みます。
  • 状態の永続化: アカウント、モデルカタログ、認証情報、スティッキーバインディングは再起動後も保持されます。
  • 管理用Web UI: アカウントの追加と管理、モデルカタログの編集、直近200件のリクエストの確認ができます。
  • 構造化された可観測性: すべてのレスポンスにx-request-idが付与され、各リクエストを処理したアカウントのラベルがログに記録されます。

Underclassのインストールと起動

方法1: Rustプロジェクトから実行する

リポジトリから、APIクライアントと管理用UIのシークレットを別々に生成し、サーバーを起動します。

UNDERCLASS_PROXY_KEY="$(openssl rand -hex 32)" \
UNDERCLASS_UI_TOKEN="$(openssl rand -hex 32)" \
cargo run -- serve

デフォルトのリスナーはhttp://127.0.0.1:8080で、WebインターフェースはルートURLから利用できます。

http://127.0.0.1:8080/

生成された2つの値は、パスワードマネージャーまたは実行時シークレットファイルに保存してください。プロキシキーは/v1/*へのリクエストを認証し、UIトークンは管理操作を認証します。Underclassがこれらの値を診断情報に書き込むことはありません。

方法2: Nixで実行する

このリポジトリはNix flakeでもあります。インストールせずにプロキシを実行します。

nix run github:ghuntley/underclass -- serve

Nixプロファイルにパッケージをインストールするには、次を実行します。

nix profile install github:ghuntley/underclass

開発時は、提供されているシェルに入り、テストを実行します。

nix develop --no-pure-eval
cargo test

devenv開発シェルでは--no-pure-evalオプションが必要です。nix runやnix profile installでは必要ありません。

プールへのアカウント追加

  1. http://127.0.0.1:8080/を開きます。
  2. UNDERCLASS_UI_TOKENに設定した値を貼り付けます。
  3. アカウントを追加を選択します。
  4. ChatGPT / CodexまたはGitHub Copilotのいずれかを選択します。
  5. 表示されたデバイス認証URLを開き、デバイスコードを入力します。
  6. オンボーディングが完了するまで待ちます。Underclassはメールアドレスまたはユーザー名をアカウントのラベルとして使用します。
  7. プールに参加させるすべてのサブスクリプションについて、この手順を繰り返します。

UIでは、アカウントの有効化、無効化、削除、再ログインも行えます。無効化したアカウントは設定に残りますが、リクエストの選択対象にはなりません。

OpenCodeへの接続

Underclassには、OpenCodeにUnderclassプロバイダーを追加するconnectコマンドが含まれています。

cargo run -- connect

デフォルトでは、このコマンドはグローバルなOpenCode設定を~/.config/opencode/以下で更新します。プロバイダーブロックをopencode.jsonまたはopencode.jsoncに統合し、認証情報をauth.jsonに書き込み、バックアップを作成します。この操作は冪等なので、安全に再実行できます。

プールから公開されているモデルを指定してOpenCodeを起動します。

opencode --provider underclass --model underclass/gpt-5.5

プロジェクト固有の設定に書き込むには、--projectを追加します。

cargo run -- connect --project

ファイルを書き込まずに統合後の設定を確認するには、次を実行します。

cargo run -- connect --dry-run

不要になった生成済みの連携設定を削除します。

cargo run -- connect --remove

このコマンドでは、生成されるプロバイダー設定をカスタマイズする必要がある場合、--base-url、--api-key、--model、--no-default-modelも使用できます。

OpenAI互換APIの利用

互換性のあるクライアントであれば、ベースURLをUnderclassのリスナーに設定し、プロキシキーをBearerトークンとして指定することでプロキシを利用できます。

利用可能なモデルの一覧表示

curl http://127.0.0.1:8080/v1/models \
  -H "Authorization: Bearer $UNDERCLASS_PROXY_KEY"

modelsエンドポイントは、統合された制限情報を持つ統合カタログを返します。カタログには、文書化されたCodexモデルファミリーと、Copilotの最新モデル一覧が初期登録されています。カタログはデータとして保存されるため、プロキシコードを変更せずにUIまたは管理APIから編集できます。

チャット補完を送信する

curl http://127.0.0.1:8080/v1/chat/completions \
  -H "Authorization: Bearer $UNDERCLASS_PROXY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-5.5",
    "messages": [
      {"role": "user", "content": "Explain sticky session routing."}
    ]
  }'

プロキシはPOST /v1/responsesにも対応しています。どちらのリクエストルートも、選択されたアカウントを通じて結果をストリーミングします。ストリーミング開始後にアップストリームで障害が発生した場合、部分的に完了したリクエストを黙って再送せず、障害をそのまま返します。

ルーティングとフェイルオーバーを理解する

スティッキーセッションの動作

リクエストにprompt_cache_keyまたはpromptCacheKeyが含まれている場合、Underclassはその値を1つのサブスクリプションに紐付けます。OpenCodeは、setCacheKey: trueで設定するとセッションIDを送信できます。紐付けは24時間有効で、プロキシを再起動しても維持されます。

割り当てられたアカウントがクールダウンを開始すると、Underclassは同じバックエンド上の別のアカウントへセッションを優先的に再紐付けします。この設計により、アップストリームのプロンプトキャッシュの利点を維持しながら、クォータ超過からの復旧も可能になります。

クォータと認証の処理

関連する429や使用量制限のレスポンスを含むクォータレスポンスは、アカウントをクールダウン状態に移行します。Underclassは、利用可能な場合はアップストリームのRetry-After値を使用し、提供されない場合は設定済みのバックエンド固有のクールダウンを使用します。クールダウンが終了すると、クールダウン中のアカウントは自動的に再び利用可能になります。

アップストリームが401を返した場合、Underclassはトークンを1回更新して、1回だけ再試行します。それでも認証に失敗した場合、アカウントには新しいログインが必要です。

プールの飽和

要求されたモデルに利用可能なアカウントがない場合、Underclassはリクエストをキューで保持しません。直ちに429を返し、Retry-Afterを通じて既知の最も早いリセット時刻を通知します。

プロキシを設定する

永続的な設定は任意で~/.config/underclass/config.tomlに配置できます。

bind = "127.0.0.1:8080"
proxy_key = "replace-with-a-client-secret"
ui_token = "replace-with-an-admin-secret"
codex_cooldown_secs = 1800
copilot_cooldown_secs = 1800

アップストリームサービスが再試行間隔を提供しない場合、各バックエンドのデフォルトのクールダウンは1,800秒です。

環境変数は対応する設定値を上書きします。

  • UNDERCLASS_BIND
  • UNDERCLASS_PROXY_KEY
  • UNDERCLASS_UI_TOKEN

モックサービスに対するテストでは、UNDERCLASS_CODEX_UPSTREAMとUNDERCLASS_COPILOT_UPSTREAMを使用します。デフォルトでは実際のアップストリームエンドポイントを指します。

Underclassは、認証情報、スティッキーな紐付け、発行済みキー、モデルカタログを~/.local/share/underclass/pool.dbに保存します。このデータベースを削除すると、ローカル状態が完全にリセットされます。

高度な運用のヒント

ログを確認してリクエストを関連付ける

処理されたすべてのレスポンスにはx-request-idが含まれます。ログは構造化されており、--log-format jsonまたは--log-format prettyでJSON形式または見やすい形式を選択できます。フィルタリングの制御にはRUST_LOGを使用します。

preflightをUnderclassの前段に配置した場合、x-request-idとして指定された有効なUUIDv4は、レスポンス、ルーティングログ、再試行ログ、リクエスト履歴で保持されます。無効な識別子、重複した識別子、nil識別子、欠落した識別子、v4以外の識別子は置き換えられます。この識別子は関連付けのみに使用され、認可または検査の証明にはなりません。

モデルカタログを慎重に管理する

ルーティングの可否とOpenCodeのモデルブロックは、保存されたカタログに従います。Web UIまたは管理カタログAPIを使用して、バックエンドのモデルマッピングを確認または編集します。不明なモデル識別子はCodexにそのまま渡されるため、新しく導入されたCodexモデルもプロキシのリリースなしで利用できます。

デフォルトではサービスを非公開にする

Underclassはデフォルトでlocalhost上で待ち受けます。リモートアクセスが意図され、保護されている場合を除き、このデフォルトを維持してください。プロキシはアクセストークン、リフレッシュトークン、認可ヘッダー、プロンプト本文をログに記録しませんが、アカウントラベルはログとUIに表示されます。

  • pool.dbや生成された*.bakファイルをコミットしないでください。
  • UIトークンはクライアントプロキシキーとは別に保護してください。
  • OpenCodeが生成するauth.jsonを非公開にしてください。Underclassはこのファイルを0600の権限で書き込みます。
  • localhostの外部にサービスを公開する前に、安全なトンネルなど、意図的な保護手段を使用してください。

NixOSでUnderclassを実行する

このflakeはNixOSモジュールをエクスポートします。サービス設定では、Underclassを有効化し、ローカルにバインドし、実行時環境ファイルからシークレットを読み込み、バックエンドのクールダウンを設定できます。

{
  inputs.underclass.url = "github:ghuntley/underclass";

  outputs = { nixpkgs, underclass, ... }: {
    nixosConfigurations.my-host = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        underclass.nixosModules.default
        {
          services.underclass = {
            enable = true;
            bindAddress = "127.0.0.1:8080";
            environmentFile = "/run/secrets/underclass.env";
            settings = {
              codex_cooldown_secs = 1800;
              copilot_cooldown_secs = 1800;
            };
          };
        }
      ];
    };
  };
}

ランタイム環境ファイルには、シークレットを Nix ストアに配置せずに保存できます。

UNDERCLASS_PROXY_KEY=sk-underclass-replace-me
UNDERCLASS_UI_TOKEN=replace-me

このモジュールは動的ユーザーを使用し、永続データを /var/lib/underclass に保存し、デフォルトでは localhost にバインドして、ファイアウォールを閉じたままにします。localhost の外部に意図的にバインドする場合にのみ、services.underclass.openFirewall を有効にしてください。

プロジェクトの開発とテスト

Cargo でビルドし、テストスイートを実行します。

cargo build
cargo test

このプロジェクトでは、ユニットテスト、Hegel のプロパティテスト、エンドツーエンドテストを組み合わせています。プールとヘルスチェックのコアは同期的に動作し、注入されたクロックを使用します。一方、非同期処理はエッジ部分に限定されています。新しいプロバイダーは provider::Backend トレイトを実装し、アプリケーションに登録します。

アーキテクチャの背景については、プロジェクトのアーキテクチャ決定記録を確認してください。ルーティングモデルについては ADR 0010で説明し、プロキシ間のリクエスト相関については ADR 0012で扱っています。

まとめ

Underclass は、複数の ChatGPT/Codex および GitHub Copilot のサブスクリプションを、ローカルで動作する OpenAI 互換サービスにまとめます。スティッキールーティング、自動的なクールダウンからの復旧、モデルを考慮したアカウント選択、飽和時のフェイルファスト動作により、予測可能なクライアント動作を維持しながら、アカウントを手動で切り替える手間を減らします。サーバーを起動し、Web UI からアカウントを登録して、underclass connect を実行し、OpenCode またはその他の互換クライアントを生成されたエンドポイントに接続してください。