错误
Half Off Dev 返回的 HTTP 状态码,Anthropic、OpenAI、Gemini 三种格式的错误体,流内错误,以及可直接复制的重试代码。
token 再便宜,失败处理做不好也会白白浪费。Half Off Dev 的错误规则很简单:使用标准 HTTP 状态码,错误体采用你所调用接口的格式,所以 Anthropic、OpenAI 和 Google 的 SDK 抛出的异常和直连官方时一样。大致来说:4xx 表示要改请求(或者充值),429 和 5xx 是暂时性问题,值得重试。
状态码一览#
| 状态码 | 名称 | 常见原因 | 是否重试 |
|---|---|---|---|
400 | Bad Request | 请求体不是合法 JSON;模型拒绝了某个参数,例如 max_tokens 超出上限;x-halfoff-group 未知;模型的内容过滤拒绝了请求 | 否,先改请求 |
401 | Unauthenticated | 密钥缺失、输错、已吊销或已过期 | 否,检查密钥 |
402 | Payment Required | 余额为 $0,或密钥达到消费上限 | 充值或调高上限后再试 |
403 | Forbidden | 模型不在密钥的白名单中 | 否 |
404 | Not Found | 模型 id 不存在、该模型不支持这种接口格式、端点不存在或 HTTP 方法不对 | 否 |
413 | Payload Too Large | 提示词超出模型上限 | 否,缩短内容 |
422 | Unprocessable | 模型拒绝了请求内容 | 否 |
429 | Too Many Requests | 密钥的频率或并发限制,或模型容量繁忙 | 是,按 retry-after 等待 |
500 | Internal Error | Half Off Dev 内部意外错误 | 是,退避重试 |
502 | Bad Gateway | Half Off Dev 尝试过的所有渠道都失败了 | 是,退避重试 |
其他 5xx 按 502 处理。没有到达模型就被拒绝的请求一律不扣费:非法 JSON 或未知分组导致的 400、401、402、403、未知模型或端点导致的 404,以及密钥限制导致的 429。
错误体格式#
同一个 401 在三种格式下分别是:
{"type": "error", "error": {"type": "authentication_error", "message": "Invalid or revoked API key"}}各状态码对应的 type 与 status#
| 状态码 | Anthropic error.type | OpenAI error.type 与 error.code | Gemini error.status |
|---|---|---|---|
400 | invalid_request_error | invalid_request_error | INVALID_ARGUMENT |
401 | authentication_error | authentication_error | UNAUTHENTICATED |
402 | billing_error | insufficient_quota | RESOURCE_EXHAUSTED |
403 | permission_error | permission_denied | PERMISSION_DENIED |
404 | not_found_error,端点不存在时为 not_found | not_found_error,端点不存在时为 not_found | NOT_FOUND |
413、422 | invalid_request_error | invalid_request_error | INVALID_ARGUMENT |
429 | rate_limit_error | rate_limit_exceeded | RESOURCE_EXHAUSTED |
500 | api_error | api_error | INTERNAL |
502、503 | api_error | api_error | UNAVAILABLE |
504 | api_error | api_error | DEADLINE_EXCEEDED |
Gemini 错误体里的 error.code 始终等于 HTTP 状态码,而 402 和 429 都对应 RESOURCE_EXHAUSTED,所以请根据数字判断,不要依赖字符串。即使是在读取请求体之前就产生的错误(比如路径不存在),也会使用与所调用路径相匹配的格式。
可能看到的错误消息#
| 状态码 | message |
|---|---|
400 | Body must be JSON |
400 | Unknown group 'xyz' |
400 | The model provider's content filter refused this request. Rewording it or using another model may help. |
400、413、422 | 模型自己给出的原因,例如 prompt is too long |
401 | Missing API key、Invalid or revoked API key、This API key has expired |
402 | Insufficient balance. Top up at the dashboard. |
402 | This API key has reached its spend limit. |
403 | This API key is not allowed to use 'gpt-6.1-sol'. |
404 | Model 'gpt-9' is not available. See GET /v1/models. |
404 | Unknown endpoint /v1/embeddings、No route for GET /v1/messages |
429 | This API key has reached its rate or concurrency limit. |
429 | All upstream capacity for this model is busy. Retry shortly. |
502 | The model provider returned an error. Please retry. |
所有错误响应都带 x-halfoff-request-id 头,包括没到达模型的请求(密钥错误、余额为零、模型不存在)。联系支持时请附上它。
流内错误#
流一旦开始,200 状态码就已经发出,之后的问题只能以流中事件的形式出现。首个 token 之前的失败会在后台换渠道重试,因此通常的情况是:流要么成功,要么在开始前就以普通 HTTP 错误失败。
如果模型在流的中途 3 分钟没有任何输出,Half Off Dev 会按该流的格式发送错误事件并关闭它:
event: error
data: {"type": "error", "error": {"type": "api_error", "message": "Upstream stopped responding mid-stream"}}渠道在流中途报告的错误会被替换成同格式的通用事件。容量繁忙会变成 Anthropic 的 overloaded_error、OpenAI 的 rate_limit_exceeded 或 Gemini 的 RESOURCE_EXHAUSTED,消息为 Upstream capacity for this model is busy. Retry shortly.;其他情况一律变成 api_error(Gemini 为 UNAVAILABLE),消息为 The model provider returned an error. Please retry.。Responses API 收到的是 error 事件,例如 {"type": "error", "code": "server_error", "message": "…"}。请从头重新发起请求;出错前已经流出的内容照常计费。
重试策略#
Half Off Dev 在返回错误之前已经把请求转到其他渠道重试过。剩下的情况,交给你这边的重试:
- 重试:
429、500、502、503、504和网络错误。 - 等待:有
retry-after就照它等;没有就指数退避,1、2、4、8 秒……最长 30 秒,再加最多 1 秒的随机抖动,避免多个进程同时重试。 - 放弃:大约 5 次之后停止,把错误抛出来。
- 不要原样重发:
400、401、403、404、413、422。402只有充值后再试才有意义。 - 留意超时:在你这边超时的请求可能已经完成并扣费。参阅重试与幂等。
官方 Anthropic 和 OpenAI SDK 已经会对 429 和 5xx 自动退避重试两次,可在客户端上设置 max_retries(Python)或 maxRetries(Node.js)调整。直接用 HTTP 调用时,可以用下面这样的辅助函数:
import os
import random
import time
import requests
RETRY_ON = {429, 500, 502, 503, 504}
def wait_time(attempt, retry_after=None):
if retry_after:
return float(retry_after)
return min(30, 2 ** (attempt - 1)) + random.random()
def call_with_retries(url, payload, attempts=5):
headers = {"Authorization": f"Bearer {os.environ['HALFOFF_API_KEY']}"}
for attempt in range(1, attempts + 1):
try:
resp = requests.post(url, json=payload, headers=headers, timeout=300)
except (requests.ConnectionError, requests.Timeout):
if attempt == attempts:
raise
time.sleep(wait_time(attempt))
continue
if resp.status_code in RETRY_ON and attempt < attempts:
time.sleep(wait_time(attempt, resp.headers.get("retry-after")))
continue
if not resp.ok: # 不该重试的 4xx,或最后一次仍然失败
raise RuntimeError(f"{resp.status_code}: {resp.text[:500]}")
return resp.json()
answer = call_with_retries(
"https://halfoff.dev/v1/chat/completions",
{
"model": "gpt-6.1-sol",
"messages": [{"role": "user", "content": "回形针有哪三种意想不到的用法?"}],
},
)
print(answer["choices"][0]["message"]["content"])无论哪种格式,error.message 都是写给人看的。记录日志时,把它和状态码、x-halfoff-request-id 头一起保存。