ChatGPT プラン連携(Sign in with ChatGPT)実装計画
配信者が自分の ChatGPT Plus / Pro プランの利用枠で、会話AIを動かせるようにする。 OpenAI 公式の「Sign in with ChatGPT」(以下 SIWC)を使い、Codex CLI のクライアント ID を流用する非公式な方法は使わない。
(2026-11-03 時点の調査とコードに基づく。コードはまだ変更していない)
前提: OpenAI の承認が要る(Phase 0)
- AituberStudio はライセンス販売で Firebase のバックエンドを持つため、SIWC の「有料・リモートで提供するアプリ」に当たる。商用パートナーとして申請フォームを出し、client ID をもらうまで本番では有効にしない
- 申請時に OpenAI に確かめること(答えで Phase 1 の方式が変わる)
- デスクトップアプリ(配信 PC の Unity)の
127.0.0.1で受け取る PKCE ログインで、プラン利用(chatgpt.tokens.use.direct)を使えるか。Web(Admin)経由のログインしか認められないか - 24時間・予約の無人配信のような、人が操作しない連続利用が認められるか
- 利用上限の目安と、上限に達したときのエラーの形(HTTP ステータス、リセット時刻が返るか)
- 使えるモデル(GPT-5.6 Sol / Terra / Luna が使えるか)と、
reasoning.effortを送れるか - ログインボタンの表示など UI のガイドラインと、利用規約の条件
- デスクトップアプリ(配信 PC の Unity)の
- 承認前の開発は、OpenAI の応答を模したテスト(モック)で進める。オープンソース向けの登録方式(
dynamic_agent_client)を商用アプリの開発に使わない
決めたこと
| 項目 | 内容 | 理由 |
|---|---|---|
| 会話AIの種類 | LLMType.ChatGPTPlan を enum の末尾に足す |
送り方(Responses API・stream: true・store: false)と認証(OAuth トークン)が既存の OpenAI 系と違う。既存の LLMType.Response は store: true・previous_response_id・temperature・system ロールを使っており、SIWC の制限に合わない |
| プランで動かす範囲 | 会話・独り言・カンペ・練習(LLMController の会話経路)だけ |
SIWC が動くのは Responses API だけ。文字起こしは非対応と明記されている。Chat Completions・Assistants・モデレーションは記載が無い |
| 補助機能 | 要約・感情分析・話しかけ判定・住所検出・ゲーム判定・文字起こし・モデレーションは、これまでどおり OpenAI の API キー | 上と同じ。Admin で「この機能には OpenAI のキーが要る」と表示して、キーが無いまま補助機能だけ失敗している状態に気付けるようにする |
| トークンの置き場所 | 配信 PC の中だけ(SecurityUtils.Encrypt で端末に紐づけて暗号化)。Firestore には置かない |
ChatGPT のトークンを預かる範囲を最小にする。バックエンドと配信 PC の両方がリフレッシュすると、リフレッシュトークンの更新が衝突する |
| 上限・ログイン切れ | 既存のフェイルオーバー(#380 failoverLLM1/2)で、回数の閾値を待たずにすぐ切り替える |
無人配信で止まると無音になる |
| 解析用 LLM | analysisLLM の選択肢には出さない(Phase 1) |
画像の詳細解析はリクエストの形が別で、上限も早く使い切る |
データの流れ
- 選ぶ(Admin): キャラの会話の設定
#config-llm-providerで「ChatGPT プラン」を選び、モデルを選ぶ - 保存:
characterConfigs/{id}.llmModelConfig.currentLLM = "ChatGPTPlan"、新フィールドllmModelConfig.chatGptPlanModelId(例gpt-5.6-luna) - 読み出し(Unity):
CharacterConfigSyncManager→LLMController.ApplyRemoteConfig→currentLLM/chatGptPlanModelId - ログイン(配信 PC): Unity のログイン画面(
AuthUIController)の「ChatGPT と連携」→ 既定のブラウザで OpenAI のログイン →127.0.0.1で受け取る → コードをトークンに交換 → 付与されたスコープにchatgpt.tokens.use.directがあるか確かめる → 暗号化してpersistentDataPathに保存 - 送る:
ChatGptPlanAuth.GetAccessTokenAsync()(期限が近ければリフレッシュ)→POST https://api.openai.com/v1/responses(Authorization: Bearer <access token>)→ SSE を最後まで受けてから本文をつなぐ → 既存の応答処理へ - 状態を伝える:
ClientPresenceReporterの送信にchatGptPlan: { signedIn, limitedUntil, lastError }を足す → Admin の「配信の準備」の「会話AIの接続」に表示
変更するファイル
Unity(Assets/Scripts/)
| ファイル | 変更 |
|---|---|
LLMController.cs |
enum 末尾に ChatGPTPlan、endpoints に /v1/responses |
新規 AI/ChatGptPlan/ChatGptPlanAuth.cs |
PKCE・state / nonce の照合・127.0.0.1 の受け取り(TwitchOAuthManager の HttpListener を参考にする)・リフレッシュ(同時に1回だけ走らせる)・保存と連携解除・スコープの確認 |
新規 AI/ChatGptPlan/ChatGptPlanResponsesClient.cs |
リクエストの組み立て、SSE の解析、エラーの分類(認証切れ / 上限 / モデル不可 / その他)。テストしやすいよう static に分ける |
LLMcontroller.ApiKeys.cs |
ChatGPTPlan はキーファイルもクラウドのキーも読まない |
LLMcontroller.RequestTransport.cs |
SendRequest の「API キーが無ければ例外」を、認証情報があるかの判定(新しい HasCredentialFor(llm))に置き換える。CreateRequest で Bearer にトークンを付ける |
LLMcontroller.RequestBody.cs |
ChatGPTPlan のリクエスト(下の「リクエストの形」) |
LLMcontroller.ResponseParsing.cs |
SSE の response.output_text.delta をつなぐ。response.completed / response.failed を見る |
LLMcontroller.Monologue.cs |
SendMonologueRequest に ChatGPTPlan の分岐を足す(今は OpenAI 以外が endpoints[LLMType.OpenAI] と OpenAI キーへ落ちる作り) |
LLMcontroller.Practice.cs |
練習の送信に分岐を足す |
LLMcontroller.PromptSettings.cs |
max_output_tokens を送れないので、文字数の上限はプロンプトと受信後の切り詰めで守る |
LLMcontroller.ModelAccessors.cs |
HasApiKeyFor を HasCredentialFor に揃える。ProviderSupportsSituationVision(ChatGPTPlan) はモデルが画像を受け付けるとき true |
LLMcontroller.Retry.cs / LLMController.cs |
上限・認証切れのエラーは failoverThreshold を待たずに切り替える。上限での切り替え中は、primaryRecoveryCheckSeconds ではなくリセット時刻(分かれば)まで戻さない。フェイルオーバー先の判定もキーではなく HasCredentialFor で見る。2026-11-04 の再監査 R-02 から、使えないと分かった代わり(事前診断か実際の切り替えで 400・401・403・404。30分)は除く(全部そうならキーのある最初) |
LLMcontroller.RemoteConfig.cs / Cloud/DataModels/CloudDataModels.cs |
chatGptPlanModelId の適用と書き出し |
Core/PreflightDiagnostics.cs |
ログイン済みか、リフレッシュできるか、GET /v1/models に選んだモデルがあるか |
Cloud/AuthUIController.cs |
「ChatGPT と連携 / 連携を解除」ボタンと、連携の状態の表示 |
Cloud/ClientPresenceReporter.cs |
連携の状態を送る(トークンは送らない) |
Cloud/ApiEndpointConfig.cs |
SIWC の client ID を環境(staging / production)ごとに持つ。未設定なら「ChatGPT プランは使えません」 |
Functions(functions/src/)
| ファイル | 変更 |
|---|---|
types/index.ts |
LLMModelConfig.chatGptPlanModelId |
handlers/admin.ts(返事を試す、callLLM 付近) |
サーバーには ChatGPT のトークンが無いので、ChatGPTPlan は「配信 PC でしか試せません」と返す。テナントの OpenAI キーで代わりに試すかは画面で選ばせる |
handlers/broadcastHealth.ts(/client/presence) |
連携の状態を受け取って保存する |
services/developerInvokeService.ts |
GPT56_DEVELOPER_MODEL_BY_LLM_TYPE に無い ChatGPTPlan を受けたときの動きを確かめて、明示的に扱う |
Admin(docs/admin/)
| ファイル | 変更 |
|---|---|
index.html |
#config-llm-provider に「ChatGPT プラン」とモデル欄。#rc-provider-select(返事を試す)と #api-key-type(キー登録)には足さない |
modules/admin-broadcast-multimodal.js |
AdminLLMContract の会話AIの一覧に足す。解析用の一覧には足さない |
modules/admin-key-types.js |
requiredKeys: ChatGPTPlan は need: 'signin'(配信 PC でログイン)とし、補助機能に OpenAI のキーが要ることを並べて出す |
modules/admin-character-purpose.js |
会話AI → キー種別の対応に足す |
modules/admin-character-config.js |
選んだときだけモデル欄と注記を出す(OpenAICompatible の注記と同じ作り) |
modules/admin-readiness.js |
「会話AIの接続」に、配信 PC の連携状態(clientPresence)を出す |
modules/admin-response-check.js |
ChatGPTPlan を選んだキャラのときの説明 |
リクエストの形(SIWC の制限に合わせる)
{
"model": "<chatGptPlanModelId>",
"instructions": "<effectiveSystemMessage>",
"input": [{ "role": "user", "content": [
{ "type": "input_text", "text": "<prompt>" },
{ "type": "input_image", "image_url": "data:image/jpeg;base64,..." }
]}],
"reasoning": { "effort": "none" },
"stream": true,
"store": false
}
- 送らない:
temperature・max_output_tokens・previous_response_id・metadata・user・truncation・system ロールのメッセージ - 会話の履歴は今も
BuildPromptが毎回プロンプトに入れているので、previous_response_idが使えなくても困らない reasoning.effortを送れるかは Phase 0 で確かめる
条件の組み合わせと期待する動作
| # | 会話AI | 配信 PC の連携 | プランの枠 | 補助用の OpenAI キー | 期待する動作 |
|---|---|---|---|---|---|
| 1 | ChatGPTPlan | 済 | 残りあり | あり | 会話・独り言はプラン、補助機能はキー |
| 2 | ChatGPTPlan | 済 | 残りあり | なし | 会話はプラン。補助機能は今と同じく動かないので、配信の準備で「OpenAI のキーが要る機能」を要対応にする |
| 3 | ChatGPTPlan | 済 | 上限に達した | — | すぐ failoverLLM1 → failoverLLM2。リセット時刻までは戻さない。Admin に「上限のため ○○ で配信中」 |
| 4 | ChatGPTPlan | 未連携 / リフレッシュ失効 | — | — | プリフライトで警告、配信の準備で要対応。会話はフェイルオーバー。アプリの画面に再ログインの案内 |
| 5 | ChatGPTPlan | 済 | — | — | 選んだモデルがプランで使えない → プリフライトで警告し、フェイルオーバー |
| 6 | 別の AI(ChatGPTPlan はフェイルオーバー先) | 未連携 | — | — | フェイルオーバー時にスキップ(今の「キーが無ければスキップ」と同じ) |
| 7 | ChatGPTPlan・予約 / 24h(isAutoSwitchBroadcastMode) |
済 | — | — | 無人なので、フェイルオーバー先が未設定、またはそのキーが未登録なら、キャラ設定の保存時と配信の準備で警告する |
| 8 | ChatGPTPlan | — | — | — | Admin の「返事を試す」は、サーバーでは試せないと表示する |
| 9 | ChatGPTPlan・chatGptPlanModelId 無し(旧データ・未設定) |
済 | — | — | 既定のモデル(gpt-5.6-luna)で動かし、画面には既定であることを出す |
| 10 | ChatGPTPlan・古い Unity ビルド | — | — | — | 既存の TryParseLLMTypeWithWarning が警告して今の値のまま動く。Admin では clientPresence.appVersion を見て「配信アプリの更新が要る」と出す |
| 11 | 解析用 LLM | — | — | — | 選択肢に出さない |
| 12 | 同じ ChatGPT アカウントを複数の配信 PC で連携 | 済 | 共有 | — | 枠を共有する。PC ごとの連携状態を Admin に出す |
境界・例外:
- 保存済みと編集中の値: 配信の準備の判定は保存済みの値で行う。編集中に ChatGPTPlan を選んだら、保存前でも注記は出す
- 選択中キャラと配信するキャラ:
currentLLMはキャラごと、トークンは PC ごと。配信 PC で切り替えたキャラが ChatGPTPlan のときだけトークンを使う - 実行中の配信: 連携を解除したらすぐフェイルオーバーに入る。リフレッシュ中に送られた会話は、リフレッシュ完了を待つ(同時に1回だけ)
- 隠すことで動いたままにしない: 承認前の本番 Admin では選択肢を出さない。すでに
ChatGPTPlanが保存されたキャラは、選択肢が無くても値を表示し、配信の準備で状態を出す - キルスイッチ:
PlatformAiKillSwitchはSendRequestの先頭で効くので、そのまま効く
段階
| 段階 | 内容 | 終わりの条件 |
|---|---|---|
| Phase 0 | OpenAI への申請と、上の「確かめること」 | client ID と答えが揃う |
| Phase 1 | Unity: 認証・送信・SSE 解析・フェイルオーバー・プリフライト・ログイン画面(client ID 未設定なら無効) | EditMode テストが通る。staging の client ID で配信 PC から会話できる |
| Phase 2 | Admin / Functions: 選択肢・モデル欄・準備ページ・返事を試す・連携状態 | Jest とローカル Admin の確認が通る |
| Phase 3(任意) | 補助機能をプランへ寄せる、受信しながら読み上げを始める | 別に計画する |
Phase 1 の実装状況(2026-11-04・未コミット・Player 未配布)
実装の中身は 03_unity_app.md の同日節。計画から変えた点:
- ログインの入口:
AuthUIController(Firebase のログイン画面。ログイン後は隠れる)ではなく、ローカル管理画面(F11)の「ChatGPT と連携」「ChatGPT 連携解除」に置いた。既に Google のサインアウトなどアカウントの操作がある画面で、配信 PC でだけ開ける。 - OAuth の値: 公開仕様と
https://auth.openai.com/.well-known/openid-configurationで確かめた(認可/api/accounts/authorize・トークン/api/accounts/oauth/token・取り消し/api/accounts/oauth/revoke・受け取りhttp://127.0.0.1:<port>/auth/callback・scopeopenid profile email offline_access resource.invoke chatgpt.tokens.use.direct・resource=https://api.openai.com/v1)。商用の client ID はオープンソース向けのdynamic_agent_clientの登録を使わず、ApiEndpointConfigに設定した値をそのまま使う。 - モデル: プランで選べるモデルは
GET /v1/modelsのmodels[](visibility == "list")で決まり、GPT-5.6 とは限らない(公式の例はgpt-6.1-sol)。chatGptPlanModelIdが空なら一覧の先頭、指定があって一覧に無ければ送らない。 - 端末の状態報告(
ClientPresenceReporter→/client/presence)は、受け取る Functions と表示する Admin と一緒に Phase 2 で入れる。
確かめたこと: Unity 6000.2 の DLL で Assembly-CSharp と自前 asmdef を全ソースからコンパイルしてエラー 0(変更前もエラー 0)。ChatGptPlanProtocolTest 69 件を Mono で実行して全件成功。
未確認: Unity 2022.3.15f1 Editor でのコンパイルと EditMode テスト(ChatGptPlanLlmProviderTest は未実行)、実際の OpenAI での連携・会話・上限・取り消し(client ID が無い)、Windows の配信 PC での 127.0.0.1 の受け取り。
確認方法
- Unity EditMode テスト(新規
ChatGptPlanResponsesClientTest/ChatGptPlanAuthTest)- リクエストに
temperature・max_output_tokens・previous_response_id・system ロールが無く、stream: true・store: falseがある - SSE の解析(途中で切れた・
response.failed・空の本文) - エラーの分類(401 → 認証切れ、上限 → 即フェイルオーバー)
stateが違う受け取りを拒否する、リフレッシュで新しいリフレッシュトークンが無ければ古いものを残す
- リクエストに
- Jest: 返事を試すの
ChatGPTPlanの応答、/client/presenceの状態の保存、requiredKeysの判定 - ローカル Admin をブラウザで: 表の #1〜#5、#7、#9〜#11 の表示
- 配信 PC(staging、承認後): 連携 → 会話 → 連携解除 → フェイルオーバー → 再連携。上限到達は実際には起こしにくいので、モックで確かめ、実機では未確認と明記する
更新するドキュメント
01_admin_console / 02_backend_functions / 03_unity_app / 04_data_flow / 05_known_issues / 07_unity_complete_inventory / 08_backend_handlers_detail / 09_firestore_and_admin_details / 10_remaining_details / 13_data_schemas / 14_full_broadcast_scenario(docs/20260507最新実装/)
参考
- Sign in with ChatGPT Quickstart
- Request a client ID
- Preview limitations
- Integrating Sign in with ChatGPT in your Opensource App
- Using your ChatGPT plan in other apps and sites
Phase 2 の実装状況(2026-11-04・未コミット・未デプロイ)
実装の中身は 01_admin_console.md・02_backend_functions.md の同日節。計画から変えた点:
- 連携状態の報告:
ClientPresenceReporter(/client/presence)ではなく、BroadcastHealthReporter(/broadcast/health-snapshot)に載せた。配信の準備が既に読んでいるのはヘルス報告の端末一覧(health-devices)で、読み込み中のキャラ(rehearsal)と同じ並びで扱える。 - 代わりの会話AI(フェイルオーバー)の欄を足した: 調べたところ、フェイルオーバーの設定は Admin に欄が無く、配信アプリの初期値(オフ)のままだった。ChatGPT プランは上限・連携切れで止まるので、会話の設定に全ての会話AI向けの「代わりの会話AI」を足した(既存のキャラはオフのまま)。
- 返事を試す: Functions は変えていない。Admin がキャラの会話AIを ChatGPT プランと判断したら、OpenAI(テナントのキー)で代わりに試し、理由を出す。
- 本番での出し分け: 本番の Admin では選択肢を隠す(保存済みのキャラは表示する)。承認と本番の client ID が揃ったら
admin-broadcast-multimodal.jsのCHATGPT_PLAN_ENVIRONMENTSにproductionを足す。 - 「予約・24h のときだけ保存時に警告」は入れていない: 代わりの会話AIがオフのとき・使える代わりが無いときは、配信モードに関係なく会話の設定と配信の準備で知らせる形にした。
確かめたこと: Jest 全体(失敗は負荷で時間切れになる backupVerification・infraAssets だけで、どちらも単独で流すと通る。今回の変更で落ちた admin-copy-terms(「テナント」→「組織」)と admin-stage4-connect-voice(ソースの文字列)は直した)、新規 chatGptPlanAdmin.test.ts 27 件、functions の tsc --noEmit・変更ファイルの ESLint・Admin の ESLint(エラー 0)・?v= の検査、Unity の全ソースのコンパイル(Unity 6000.2 の DLL)。確認用画面(preview-admin-usability.py)をブラウザで開き、配信の準備の「会話AIの接続」(上限・未連携・古い配信アプリ)、会話の設定(本番相当で隠す・保存済みは表示・staging で選べる・注記・モデル欄・代わりの会話AIの注意と保存内容)、AIサービスのキーの「このキャラが使うキー」、返事を試すの説明を確認した。
未確認: staging へのデプロイ、実際の配信アプリからのヘルス報告(Player 未配布)、確認用画面での保存ボタンからの送信(確認用画面の API の作りで止まるため、保存内容は collectLLMConfig の結果で確認した)。
実装後の差分(2026-11-04、再監査 R-02。Player 未配布。Functions・Admin は main に入ると自動で出る): 既定の代わり(Claude・Gemini)は、配信アプリのモデル名が公開モデルに無く返事を作れなかったので、今の公開モデル(claude-haiku-5-5・gemini-3.5-flash-lite)と送信の形に直した。SwitchAwayFromChatGptPlan は使えないと分かった代わりを後回しにし、failoverReady は「キーがあり、使えないと分かっていない代わりがある」にした。事前診断に llm_failover「代わりの会話AI」(pass か warn)を足した。実装の後のレビューを受けて、返事を ChatGPT プランへ送っている途中に上限などになったときの扱いも直した。StateChanged から同期で動く SwitchAwayFromChatGptPlan がその場で切り替えても、その返事の TryFailoverGenerateResponse は失敗したのを ChatGPT プランとして扱い、切り替えた先を最初に試す(以前は切り替えた先を飛ばし、使えないと分かっている代わりにだけ送って返事が無くなることがあった)。全部が失敗したときの失敗の理由の AI 名も、この返事で送った AI(ChatGPT プラン)にした。中身は 03_unity_app.md の「2026-11-04 R-02」と NATIVE_TTS_FIX_A_PLAN_20261104.md 4章。