個人で複数の AI API を使っていると、API キー管理・利用量の把握・権限分離がすぐ散らかります。
この記事では、OpenAI 互換の API ゲートウェイ(new-api)を個人運用し、Qiita OAuth ログインまで通したときの構成とハマりどころをまとめます。
利害関係の明示
文末で紹介する公開インスタンスは、筆者が細々と公益目的で運用している個人サイトです。商用 SLA はなく、予告なく仕様変更・停止する可能性があります。
この記事で得られること
- OpenAI 互換ゲートウェイを「個人向けに公開」する最小構成
- new-api を選んだ理由(LiteLLM 直運用との違い)
- Qiita の OAuth が標準と違う点と実装上の注意
- 公開時に最低限決めておくべき運用ルール
背景
やりたかったことは次の3つです。
- クライアント(SDK / Cursor 系ツール / 自作アプリ)から OpenAI 互換で叩ける
- 利用者ごとに API キーと利用量を分けられる
- 日本語 UI で、できれば Qiita アカウントでログインできる
「ただの reverse proxy」だと 2 と 3 が足りず、毎回自前実装が増えます。
そこで、ユーザー管理・トークン発行・モデルルーティング・管理画面が揃っている new-api をベースにしました。
なぜ new-api か
比較対象としてよく出るのは次あたりです。
| 方式 | 向いていること | 今回足りない点 |
|---|---|---|
| 各社 API を直接叩く | 最短 | キー分散、利用量が見えない |
| Nginx 等の素朴な中継 | 単純転送 | ユーザー/課金/トークン管理がない |
| LiteLLM Proxy | 多モデル統一、ルーティング | 今回欲しかった「利用者向け管理UI + OAuth」を別途組む必要 |
| new-api | OpenAI互換 + ユーザー/トークン/管理画面 | カスタム OAuth のプロバイダ差に注意 |
今回の目的は「研究用途の多段ルーティング」より、少人数が安全に使える公開入口だったので new-api を選びました。
全体構成
ざっくりこうです。
Browser / SDK
|
| HTTPS
v
Reverse Proxy (TLS終端)
|
v
new-api (OpenAI互換 API + Web UI)
|-- SQLite (ユーザー, token, OAuth設定, ログ)
|-- Upstream providers (OpenAI互換先など)
`-- Custom OAuth (Qiita)
ポイントだけ抜き出すと:
- API も UI も同じオリジンで提供
- DB はまずは SQLite(少人数なら十分)
- 認証は Qiita OAuth(カスタム OAuth プロバイダ)
- upstream の実キーはサーバー側に閉じ込め、利用者には new-api の token だけ渡す
公開前に決めた運用ルール
技術より先に、ここを決めないと事故ります。
- 対象: 個人の学習・検証用途
- 禁止: 再販、無制限の商用転用、攻撃・スパム用途
- 制限: レート制限 / 利用量上限を設ける
- 可用性: 個人運用。落ちたらその時
- データ: ログイン情報と利用ログは運用に必要な範囲のみ
「無料だから無制限」は、公開 API ではほぼ必ず壊れます。
制限を先に書いておく方が、結果的に信用されます。
Qiita OAuth 連携
new-api のカスタム OAuth に、Qiita を追加しました。
設定値
| 項目 | 値 |
|---|---|
| Provider slug | qiita |
| Authorization | https://qiita.com/api/v2/oauth/authorize |
| Token | https://qiita.com/api/v2/access_tokens |
| UserInfo | https://qiita.com/api/v2/authenticated_user |
| Scope | read_qiita |
| Callback | https://<your-domain>/oauth/qiita |
ユーザー情報マッピング例:
- user id:
permanent_id - username:
id - display name:
name - email:
email(取れる場合のみ)
最大のハマりどころ
ここが本記事の核心です。
多くの OAuth 実装は token endpoint に対して:
Content-Type: application/x-www-form-urlencoded- レスポンスの
access_tokenを読む
を前提にしています。
しかし Qiita は非標準です。
1. Token リクエストは JSON
Qiita は POST /api/v2/access_tokens に JSON を要求します。
{
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"code": "AUTHORIZATION_CODE"
}
grant_type や redirect_uri を form で送る一般的な形ではありません。
2. レスポンスのトークン名が token
Qiita の成功レスポンスはだいたい次の形です。
{
"client_id": "...",
"scopes": ["read_qiita"],
"token": "..."
}
標準的な access_token ではありません。
そのため、汎用 OAuth クライアントだと「token 交換は 200 なのに access_token が空」になりがちです。
実際、修正前は次の症状でした。
- Qiita 認可画面 → 成功
- ブラウザの callback → 成功
- アプリ側で
Failed to get token from Qiita - サーバログは
ExchangeToken failed: empty access token
3. 実装でやったこと
generic OAuth 実装側で、provider slug が qiita のときだけ分岐しました。
- リクエスト:
application/json - body:
client_id/client_secret/codeのみ - レスポンス:
tokenをaccess_token相当へマップ -
token_typeが無いのでBearerを補完 -
scopes配列は空白連結してscopeへ
擬似コード:
if slug == "qiita" {
// POST application/json
body := map[string]string{
"client_id": clientID,
"client_secret": clientSecret,
"code": code,
}
// response.token -> accessToken
// response.scopes -> strings.Join(..., " ")
// tokenType = "Bearer"
} else {
// 従来どおり x-www-form-urlencoded
}
セキュリティメモ
authorization code / client_secret / access token 全文はログに出さないこと。
デバッグするなら status code と「空トークンだった」程度に留めるのが安全です。
動作確認チェックリスト
- Qiita 側アプリケーションの callback URL が完全一致している
- new-api の
ServerAddress/FRONTEND_BASE_URLが公開 URL と一致 - カスタム OAuth が起動時に load されている
例:Loaded custom OAuth provider: Qiita (qiita) - 認可後、ユーザーが作成 or 既存ユーザーに bind される
- ログイン後に API トークンを発行できる
利用者側の使い方(最短)
ログイン後、管理画面で API キーを発行し、あとは OpenAI 互換で叩くだけです。
# モデル一覧
curl https://<your-domain>/v1/models \
-H "Authorization: Bearer sk-xxxx"
# Chat Completions 例
curl https://<your-domain>/v1/chat/completions \
-H "Authorization: Bearer sk-xxxx" \
-H "Content-Type: application/json" \
-d '{
"model": "your-model-name",
"messages": [
{"role":"user","content":"こんにちは"}
]
}'
OpenAI 公式 SDK を使う場合も、base_url を差し替えるだけです。
from openai import OpenAI
client = OpenAI(
api_key="sk-xxxx",
base_url="https://<your-domain>/v1",
)
res = client.chat.completions.create(
model="your-model-name",
messages=[{"role": "user", "content": "hello"}],
)
print(res.choices[0].message.content)
公開運用で気をつけたこと
1. upstream キーを絶対に出さない
利用者に配るのは new-api の token だけ。
upstream の本番キーはサーバー内に閉じます。
2. 制限は最初から入れる
- モデルごとの rate limit
- ユーザー/トークンごとの quota
- 異常な連続リクエストの遮断
後から締め付けるより、最初から狭い方が安全です。
3. バックアップとロールバック
SQLite 運用でも、最低限:
- デプロイ前に DB バックアップ
- 旧イメージを残す
- health check 成功を確認してから切替
をやっておくと、OAuth 修正のようなホットフィックスが楽です。
4. 日本語化
非英語圏の個人利用者向けなら、UI 言語を最初から絞る判断もありです。
運用画面の認知負荷が下がり、「まずはログインして token を発行する」までの離脱が減りました。
公開インスタンスについて
学習・検証用途の入口として、次のサイトを細々運用しています。
- URL: https://chenchen.openachieve.asia
- ログイン: Qiita OAuth 対応
- 位置づけ: 個人による公益寄りの OpenAI 互換入口
- 注意: ベストエフォート。商用利用や再販は想定していません
「便利なので使ってください」より、
同じものを自作したい人の参考例として見てもらえると嬉しいです。
まとめ
- 少人数向けに OpenAI 互換入口を出すなら、素朴な reverse proxy より ユーザー/トークン管理付きゲートウェイの方が運用が楽
- new-api は管理画面と互換 API が一体で、個人公益運用と相性が良い
-
Qiita OAuth は非標準(JSON token request +
tokenフィールド)なので、汎用 OAuth 実装はそのままでは落ちやすい - 公開するなら、技術より先に 制限・禁止事項・無保証 を明示した方が長い目で見て安全
今後やりたいこと
- 利用状況の可視化をもう少し分かりやすくする
- モデルごとの適正な rate limit 調整
- 障害時のステータスページ
- 同じ構成を再現するための、より短いセットアップ手順
同じ構成を試す人がいれば、Qiita OAuth 周りで詰まった点をコメントしてもらえると助かります。
この記事が、個人で OpenAI 互換ゲートウェイを安全に公開したい人の参考になれば幸いです。