unbleep 是一个 OpenAI 兼容 API,在实践中这意味着迁移只需两行代码,且不引入任何新依赖。你可以保留官方 SDK、你的重试逻辑、流式循环、token 计量和错误处理器。变化的只有请求发往哪里,以及用哪个密钥做认证。这篇文章先讲这次替换,再讲四个差异大到不了解就会出问题的地方。
两行代码迁移到 OpenAI 兼容 API
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 会从环境变量中读取这两个值,所以改一下配置就够了:
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 上下文模型迁移过来的人栽跟头:以前放得下的提示词,现在会被拒绝。如果你按成本路由,也请同时按长度路由。
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] 结束。你现有的循环无需改动即可工作。
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 响应就不会抛出异常:
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)}")三个实际后果:
- 推理 token 就是输出 token。 它们计入
completion_tokens,按输出价格计费。usage.completion_tokens_details.reasoning_tokens告诉你账单中有多少是花在思考上的。 - 它们同样消耗
max_tokens。 在推理档位上,过紧的上限可能会被推理轨迹全部耗尽,只给你留下一个被截断的回答或空的content。请为两者都留出预算,或者关闭思考。 - 不需要时就关掉它。 对于简短或高频的调用,通过
extra_body发送"thinking": false。reasoning_effort也会透传。
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 密钥——迁移真的只需要两行。