获取 API 密钥

无审查 API 错误码与排障手册

报错不可怕,可怕的是不知道该重试还是该改代码。这份手册按状态码逐个拆:触发条件、判断依据、修法。错误体统一是 JSON,形如 error 对象里带 code 和 message,所以排障第一步永远是读 body,而不是只看状态码。后面附了带指数退避的重试代码、客户端限速代码,和一份能逐项打勾的清单。

更新于

要点

  • 只有 429 和 503 值得自动重试,其余状态码重试只会白白浪费请求。
  • 402 no_credit 是余额用完或试用过期,403 content_blocked 是内容被拦,二者都不是网络问题。
  • 重试要带指数退避加随机抖动,并设最大次数。
  • 每把 key 每分钟 300 次请求,批量任务要在客户端先限速。

错误体格式与读取顺序

所有失败响应都是同一个结构:

{"error":{"code":"...","message":"..."}}

读取顺序固定三步:

  1. HTTP 状态码,决定大类;
  2. error.code,决定具体原因,程序里用它做分支;
  3. error.message,给人看,写进日志,不要用它做字符串匹配。

先想清楚一个分界:能靠重试解决的(429、503,以及网络超时)和必须改动才能解决的(其余)。把两类混在一起,是线上事故最常见的根源,比如对 402 不停重试,结果每秒打几十个无效请求。

日志里建议固定记录四个字段:时间、状态码、error.code、本次请求的 prompt_tokens 估值。出问题时,一眼就能看出是某一类错误突然增多,还是整体失败。再配一条告警规则:402 只要出现一次就立刻通知,因为它意味着服务已经对用户不可用;429 则看比例,偶发无需处理,持续出现说明并发设计有问题。

状态码速查

状态码error.code含义重试?
400—请求不合法,如 prompt 加 max_tokens 超过 100k否
401—key 无效或缺失否
402no_credit余额用尽或试用过期否
403content_blocked内容被拦截否
404—端点不存在否
429—触发限流是,退避
503upstream_busy服务暂时繁忙是,几秒后

表中“—”表示没有需要你特别处理的固定 code 字符串,按状态码分支即可。

4xx:改请求,别重试

400 请求不合法

  • 成因一:JSON 格式错误,常见于手拼字符串时引号没转义。
  • 成因二:prompt 加 max_tokens 超过 100,000。长对话最容易触发。
  • 成因三:请求体超过 8 MB。
  • 修法:用 JSON 库序列化;发送前估算 token,超限就裁剪历史或调小 max_tokens;见长上下文实战。

401 密钥问题

  • Header 缺失,或缺了 Bearer 前缀。
  • 重新生成过 key,旧 key 立刻失效,而某台机器还在用旧值。
  • 环境变量没带进容器或定时任务。

402 no_credit

余额用完,或 7 天试用已过期。去账户页充值预付费余额即可恢复。建议你自己的服务监控余额,别等用户报错才发现。

403 content_blocked

内容被拦截。合法的成人向内容、虚构和争议话题不会被拒绝,但涉及未成年人的性内容一律拦截,包括小说和角色扮演。遇到 403,应该检查输入和历史里是否有这类内容,而不是换措辞重试。

404 端点不存在

只有两个端点:POST /v1/chat/completions 和 GET /v1/models。路径漏了 /v1、多了斜杠、拼错,或者请求了不支持的 embeddings、图片等接口,都会 404。

400 里还有一类容易误判:长对话的 token 随轮次线性增长,白天测试一切正常,跑到几十轮后突然开始 400。这不是接口不稳定,而是上下文到顶了。解决办法是给历史设上限,超过阈值就丢最旧的轮次,或者把旧内容压缩成摘要。别等报错再处理,在发请求前就估算好。

401 的排查有个小技巧:把 key 的前后各四位打印到日志里,不要打印完整内容,和账户页里显示的比对。这样可以确认进程里读到的到底是不是你以为的那把,尤其是容器和定时任务环境里,环境变量缺失或读到旧值是最常见的原因。

429 与 503:该重试的两类

429 限流

每把 key 每分钟 300 次请求。批量任务、多实例共用一把 key、重试风暴,都会触发。修法有两层:客户端先限速(见后面的代码),再对 429 做退避重试。

503 upstream_busy

服务暂时繁忙,几秒后重试即可。别立刻连发,也别在一秒内重试十次,那只会让情况更糟。

网络超时

长文本生成耗时较长,timeout 不要设得太短,示例用 120 秒。超时后重试要注意:上一次请求可能已经在服务端执行,重复调用可能重复消耗额度,所以对生成类请求,超时重试次数要少,最好配合流式输出缩短单次等待。

import os
import random
import time
import requests

URL = "https://api.wushenchaapi.com/v1/chat/completions"
HEADERS = {
    "Authorization": "Bearer " + os.environ["API_KEY"],
    "Content-Type": "application/json",
}
RETRYABLE = {429, 503}          # 只重试这两类
FATAL = {400, 401, 402, 403, 404}

class ApiError(Exception):
    def __init__(self, status, code, message):
        super().__init__(f"{status} {code}: {message}")
        self.status, self.code = status, code

def call(payload, max_retries=5, base=1.0, cap=30.0):
    for attempt in range(max_retries + 1):
        try:
            r = requests.post(URL, headers=HEADERS, json=payload, timeout=120)
        except (requests.ConnectionError, requests.Timeout):
            r = None                      # 网络层失败,按可重试处理
        if r is not None and r.ok:
            return r.json()
        if r is not None:
            try:
                err = r.json().get("error", {})
            except ValueError:
                err = {}
            if r.status_code in FATAL or r.status_code not in RETRYABLE:
                raise ApiError(r.status_code, err.get("code"), err.get("message"))
        if attempt == max_retries:
            raise ApiError(r.status_code if r is not None else 0, "retry_exhausted", "重试次数用尽")
        delay = min(cap, base * (2 ** attempt)) * random.uniform(0.5, 1.0)
        time.sleep(delay)

if __name__ == "__main__":
    out = call({"model": "uncensored", "max_tokens": 100,
                "messages": [{"role": "user", "content": "回复一个字:好"}]})
    print(out["choices"][0]["message"]["content"])

要点:退避公式是 min(cap, base × 2^n) × 随机系数,随机抖动避免多个客户端同时重试撞在一起;FATAL 集合里的状态码一次都不重试。

客户端限速与诊断请求

与其等 429 再退避,不如事先限速。下面的小工具保证一分钟内请求数低于设定值,多线程安全:

import threading
import time

class RateGate:
    """简单的客户端限速:保证一分钟内请求数不超过 limit。"""
    def __init__(self, limit=240):          # 留出余量,低于 300/分钟
        self.limit, self.stamps, self.lock = limit, [], threading.Lock()

    def wait(self):
        while True:
            with self.lock:
                now = time.time()
                self.stamps = [t for t in self.stamps if now - t < 60]
                if len(self.stamps) < self.limit:
                    self.stamps.append(now)
                    return
                sleep_for = 60 - (now - self.stamps[0])
            time.sleep(max(sleep_for, 0.05))

排查时,最干净的办法是脱离业务代码,用 curl 发一个最小请求,-i 能同时看到状态行和响应头:

curl -i https://api.wushenchaapi.com/v1/chat/completions \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"uncensored","max_tokens":20,"messages":[{"role":"user","content":"ping"}]}'

curl 能通而业务代码不通,问题在你的代码或环境(代理、环境变量、编码);curl 也不通,看 key、余额和网络。

限速值为什么设 240 而不是 300:多实例部署时,各实例各自限速,加起来仍可能超过总额;再加上重试会额外占用配额。留出两成余量,是比较稳妥的做法。如果你有多个实例,把每个实例的额度按总数平分,或者用一个共享的计数器统一调度。

排障清单

  1. 读 body 里的 error.code,不要只看状态码。
  2. 401:key 前缀、环境变量、是否重新生成过。
  3. 400:JSON 是否合法,prompt + max_tokens 是否超 100,000,请求体是否超 8 MB。
  4. 402:余额与试用有效期。
  5. 403:输入和历史里是否含被禁内容。
  6. 404:路径是否为 /v1/chat/completions 或 /v1/models。
  7. 429:是否多实例共用一把 key,是否做了客户端限速。
  8. 503:是否做了退避重试,间隔是否至少几秒。
  9. 超时:timeout 是否足够,是否可以改流式。
  10. 以上都排除:用 curl 最小请求复现。

刚接入的话,先看接入教程。更多参数在文档里。

清单用法:出问题时从上往下逐项排除,不要跳着看。大多数“诡异”故障,最终都落在前四项里。

补充一条经验:把每次排障结论回写到团队文档,下次同样的报错就能直接对号入座。

常见问题

返回 503 要等多久再重试?

几秒即可。建议第一次等一到两秒,之后按指数增加,并加随机抖动,设最大重试次数。

为什么我的请求一直 402?

余额已用完,或 7 天试用已过期。充值预付费余额后即可恢复,余额不会过期。

403 content_blocked 能通过改写绕过吗?

不要尝试。涉及未成年人的性内容在任何情况下都会被拦截,包括小说和角色扮演。正常的成人向内容不会触发这个错误。

429 是按账户还是按 key 限制?

限制是每把 key 每分钟 300 次请求。每个账户只有一把 key,所以多个服务共用时要共同限速。

只需填写表单即可获取密钥

创建账户,复制密钥,修改 Base URL。配置就是这么简单。