← 記事一覧

そのまま差し替えられる OpenAI 互換 API — 移行は2行で完了

2026-08-20 · 2 分で読めます · api, migration

unbleep は OpenAI 互換 API です。実務上これが意味するのは、移行は2行で済み、新しい依存関係も増えないということです。公式 SDK も、リトライロジックも、ストリーミングのループも、トークンの集計もエラーハンドラーもそのまま使えます。変わるのは、リクエストの送り先と、それを認証するキーだけです。この記事では差し替えの手順と、知らないと何かが壊れる程度には異なる4つの点を扱います。

OpenAI 互換 API への移行を2行で

python
import os

from openai import OpenAI

client = OpenAI(
    base_url="https://unbleep.ai/v1",
    api_key=os.environ["UNBLEEP_API_KEY"],
)

resp = client.chat.completions.create(
    model="unbleep",
    messages=[{"role": "user", "content": "Summarise this incident report."}],
)
print(resp.choices[0].message.content)

コードにまったく触れたくないなら、SDK は両方の値を環境変数から読むので、設定の変更だけで十分です。

bash
export OPENAI_BASE_URL="https://unbleep.ai/v1"
export OPENAI_API_KEY="ub_live_9f2c..."

この同じ2つの値で、SDK の上に構築されたエコシステムの大半(LangChain、LlamaIndex、Instructor、Vercel AI SDK など、ベース URL の設定を公開しているものすべて)がカバーされます。キーにはプレフィックスが付いているので、漏洩してもシークレットスキャナーがすぐに気づけます。ub_live_ と ub_test_ はどちらも同じプリペイドクレジットに対して、同じトークン単価で課金されます。テストキーは、上限が低い(アカウントの毎分60リクエストに対して毎分15リクエスト)別個の失効可能な認証情報であって、無料枠ではありません。どちらもサーバー側で保管してください。

GET /v1/models も動作するので、ドロップダウンを埋めるためにモデルを列挙するツールに特別な対応は不要です。

モデルの選択

ティアは3つです。日付付きの id(unbleep-250811、unbleep-high-250811、unbleep-mini-250811)はエイリアスとして受け付けられますが、現時点では日付なしの id と同じビルドに解決され、レスポンスには日付なしの id が返ってきます。これらは前方互換のための表記であって、再現性の保証ではないと考えてください。評価を再現可能にする必要があるなら、モデル id ではなく出力を記録してください。

| モデル | コンテキスト | 料金(入力 / 出力、100万トークンあたり) | 備考 | | --- | --- | --- | --- | | unbleep | 256K | $3.00 / $3.00 | デフォルト。推論ティア。 | | unbleep-high | 1M* | $5.00 / $5.00 | 最大規模のジョブ向け。推論ティア。 | | unbleep-mini | 32K | $1.00 / $1.00 | 安価で高速。推論トレースなしで直接回答。 |

*リクエストボディの上限は 2,000,000 バイト(およそ 500k トークン)なので、1回の呼び出しで 1M のウィンドウを実際に埋めることはできません。それを超えるものは 413 payload_too_large として返ってきます。

unbleep-mini の 32K という上限は、128K コンテキストのモデルから移行してきた人がよく引っかかるところです。以前は収まっていたプロンプトが拒否されるようになります。コストでルーティングするなら、長さでもルーティングしてください。

python
def pick_model(prompt_chars: int) -> str:
    """~4 chars/token is a deliberate under-estimate; leave room for the completion."""
    est_tokens = prompt_chars // 4
    if est_tokens < 24_000:
        return "unbleep-mini"
    return "unbleep" if est_tokens < 200_000 else "unbleep-high"

ストリーミング

stream=True を指定すると、標準的な Server-Sent Events が返ってきます。各イベントは delta を含む chat.completion.chunk で、ストリームはリテラルの data: [DONE] で終了します。既存のループはそのまま動きます。

bash
curl -N https://unbleep.ai/v1/chat/completions \
  -H "Authorization: Bearer $UNBLEEP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "unbleep",
    "messages": [{"role": "user", "content": "Explain the residual stream."}],
    "stream": true,
    "stream_options": {"include_usage": true}
  }'

トークン数は、要求してもしなくても返ってきます。unbleep は常にバックエンドに usage を要求してそのまま中継し、明示的に stream_options: {"include_usage": false} を指定した場合にだけ、送出時にそれを取り除きます。いずれにせよストリームは、choices 配列が空の最終チャンク1つで終わります(オプトアウトしていない限り、そこに値の入った usage オブジェクトが載っています)。下のループが choices に触れる前にチェックしているのはそのためです。

reasoning_content フィールド

これがスキーマに対する唯一の本物の追加です。unbleep と unbleep-high は回答の前に思考し、その思考の連鎖は reasoning_content として返されます。これはメッセージ(非ストリーミング)または delta(ストリーミング)上で content と並ぶフィールドです。上流のバックエンドは reasoning と呼ぶか reasoning_content と呼ぶかで一致していませんが、この API は reasoning_content に正規化するので、扱う形は常に1つだけです。

OpenAI のスキーマには含まれないため、SDK の型スタブにもありません。レスポンスのモデルは追加フィールドを許容するので、実行時には属性アクセスが動作します。ただし、トレースを持たない mini のレスポンスで例外が出ないよう、getattr で読み取ってください。

python
import sys

stream = client.chat.completions.create(
    model="unbleep",
    messages=[{"role": "user", "content": "Why did this detection rule misfire?"}],
    stream=True,
    stream_options={"include_usage": True},
)

usage = None
for chunk in stream:
    if not chunk.choices:          # final usage-only chunk
        usage = chunk.usage
        continue
    delta = chunk.choices[0].delta
    thought = getattr(delta, "reasoning_content", None)
    if thought:                    # trace to stderr, answer to stdout
        sys.stderr.write(thought)
    if delta.content:
        sys.stdout.write(delta.content)

if usage:
    details = usage.completion_tokens_details
    print(f"\nreasoning tokens: {getattr(details, 'reasoning_tokens', 0)}")

実務上の帰結は3つあります。

python
resp = client.chat.completions.create(
    model="unbleep",
    messages=[{"role": "user", "content": "phishing or benign?"}],
    max_tokens=4,
    extra_body={"thinking": False},   # spend the budget on the answer, not the trace
)

reasoning_content を後のターンに assistant のコンテンツとして戻してはいけません。これは診断用の出力であって会話履歴ではなく、再投入すると次のレスポンスの質が落ちます。

エラーと、本当に注意すべき2つの落とし穴

エラーは OpenAI のエンベロープ形式({"error": {"type", "code", "message"}})を使うので、既存の except ブロックはそのまま機能します。ステータスコードの対応は予想どおりです。401 はキーの不正、422 は policy: strict によるブロック、429 はレート制限、5xx はリトライ可能な上流のエラーです。

おそらく一度も処理したことがないステータスが、402、クレジット切れです。アカウントはプリペイド制なので、超過分の課金も請求書もありません。チャージするまでリクエストが単に止まるだけです。OpenAI はクォータの枯渇を 429 で通知するため、素朴に移行すると、バックオフロジックが 402 を永遠にリトライし続けることになります。これは回復不能なエラーとして扱い、アラートを出してください。

2つ目の落とし穴は、system_fingerprint が返されないことです。これはサービングバックエンドを識別するものなので、他のベンダー固有フィールドと一緒に取り除かれます。キャッシュや再現性チェックのキーにしているなら、独自のバージョンマーカーが必要になります。日付付きのモデル id は現行ビルドのエイリアスであって固定されたスナップショットではないので、バックエンドが変わったことを教えてはくれません。

レート制限はすべてのレスポンスにヘッダー(x-ratelimit-limit-requests、x-ratelimit-remaining-requests、x-ratelimit-reset-requests)として返ってくるので、バッチジョブは上限にぶつかって初めて知るのではなく、リクエストレートをあらかじめ調整できます。ヘッダーに現れないもう1つの上限があります。アカウントあたり同時に処理中のリクエストは最大8件で、9件目は 429 にコード too_many_concurrent_requests を伴って返ってきます。ストリーミング呼び出しはストリームが終わるまでスロットを占有するので、自前のワーカープールは8に制限してください。

接続先にあるもの

はっきり書いておきます。このエンドポイントの背後にあるモデルは abliterated されており、つまり拒否挙動が重みのレベルで取り除かれています。それが目的です。これはセキュリティ研究、レッドチーミング、評価といった、拒否が測定誤差になる領域を対象とした開発者向け API です。同時に、まずいプロンプトを止める通常のガードレールが存在しないということでもあるので、出力に対して責任を負う人間を置き、利用ポリシーを読んでください。合法的に使う責任はあなたにあります。

API キーを取得 — 移行は本当に2行です。