← 全部文章

即插即用的 OpenAI 兼容 API——两行代码完成迁移

2026-08-20 · 2 分钟阅读 · api, migration

unbleep 是一个 OpenAI 兼容 API,在实践中这意味着迁移只需两行代码,且不引入任何新依赖。你可以保留官方 SDK、你的重试逻辑、流式循环、token 计量和错误处理器。变化的只有请求发往哪里,以及用哪个密钥做认证。这篇文章先讲这次替换,再讲四个差异大到不了解就会出问题的地方。

两行代码迁移到 OpenAI 兼容 API

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..."

同样这一对变量覆盖了构建在该 SDK 之上的大部分生态——LangChain、LlamaIndex、Instructor、Vercel AI SDK,以及任何暴露了 base URL 设置的工具。密钥带有前缀,这样一旦泄露,密钥扫描器能一眼认出:ub_live_ 和 ub_test_ 都从同一份预付费额度中、按同样的每 token 单价计费。测试密钥是一个独立的、可撤销的凭证,上限更低——每分钟 15 次请求,而不是账户的 60 次——它不是免费档。两种密钥都请只放在服务端。

GET /v1/models 可用,所以那些通过枚举模型来填充下拉菜单的工具不需要特殊处理。

选择模型

三个档位。带日期的 id——unbleep-250811、unbleep-high-250811、unbleep-mini-250811——作为别名可以被接受,但目前它们解析到的构建与不带日期的 id 相同,响应中报告回来的也是不带日期的 id。把它们当作向前兼容的写法,而不是可复现性保证;如果一项评估必须可重复,请记录输出,而不是模型 id。

| 模型 | 上下文 | 输入 / 输出价格(每 1M) | 说明 | | --- | --- | --- | --- | | unbleep | 256K | $3.00 / $3.00 | 默认。推理档位。 | | unbleep-high | 1M* | $5.00 / $5.00 | 最大型的任务。推理档位。 | | unbleep-mini | 32K | $1.00 / $1.00 | 便宜且快。直接作答,无推理轨迹。 |

*请求体上限为 2,000,000 字节——约 500k 个 token——因此单次调用实际上填不满 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}
  }'

无论你是否主动要求,你都会拿到 token 计数:unbleep 总是向后端请求 usage 并将其转发,只有显式的 stream_options: {"include_usage": false} 才会在输出时把它去掉。无论哪种情况,流都会以一个最终 chunk 结束,这个 chunk 的 choices 数组为空——除非你选择退出,否则它携带填充好的 usage 对象——这就是为什么下面的循环在访问 choices 之前要先检查它。

reasoning_content 字段

这是 schema 中唯一一处真正的新增。unbleep 和 unbleep-high 在作答之前会先思考,这条思维链通过 reasoning_content 返回——它是 message(非流式)或 delta(流式)上与 content 平级的字段。上游后端在该叫 reasoning 还是 reasoning_content 上意见不一;本 API 统一规范为 reasoning_content,所以你只需处理一种形态。

因为它不属于 OpenAI schema,所以 SDK 的类型存根里没有它。响应模型允许额外字段,所以运行时的属性访问是可行的——但请用 getattr 来读取,这样没有推理轨迹的 mini 响应就不会抛出异常:

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)}")

三个实际后果:

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 内容回填到后续轮次中。它是诊断输出,不是对话历史,重放它会降低下一次响应的质量。

错误与两个真正的坑

错误使用 OpenAI 的信封格式——{"error": {"type", "code", "message"}}——所以你现有的 except 块继续有效。状态码的映射符合预期:401 密钥无效,422 被 policy: strict 拦截,429 触发速率限制,5xx 可重试的上游错误。

你大概从未处理过的状态码是 402,额度用尽。账户是预付费的,所以没有超额,也没有账单——请求会直接停止,直到你充值为止。OpenAI 用 429 来表示配额耗尽,这意味着按部就班的迁移路径会让你的退避逻辑无限重试 402。请把它当作终止性错误处理,并为它设置告警。

第二个坑:不返回 system_fingerprint。 它标识的是服务后端,所以与其他厂商字段一起被剥离了。如果你的缓存或可复现性检查以它为键,你需要自己的版本标记:带日期的模型 id 是当前构建的别名,不是冻结的快照,所以它们不会告诉你后端何时发生了变化。

速率限制以响应头的形式随每个响应返回——x-ratelimit-limit-requests、x-ratelimit-remaining-requests、x-ratelimit-reset-requests——这样批处理作业可以主动控制请求节奏,而不是撞上上限才发现它。还有第二个上限是这些响应头没有描述的:每个账户最多 8 个在途请求,第 9 个会返回 429,错误码为 too_many_concurrent_requests。一个流式调用会一直占用它的槽位直到流结束,所以请把你自己的 worker 池上限设为 8。

你指向的是什么

有必要说明白:这个端点背后的模型是 abliterated 的,也就是说它们的拒答行为已经在权重层面被移除了。这正是重点——它是一个面向安全研究、红队测试和评估的开发者 API,在这些场景中拒答就是测量误差。这也意味着,通常用来拦截糟糕提示词的护栏并不存在,所以请让一个人为输出负责,并阅读可接受使用政策。合法使用是你的责任。

获取 API 密钥——迁移真的只需要两行。