halfoff.dev文档
获取密钥
API 参考

错误

Half Off Dev 返回的 HTTP 状态码,Anthropic、OpenAI、Gemini 三种格式的错误体,流内错误,以及可直接复制的重试代码。

token 再便宜,失败处理做不好也会白白浪费。Half Off Dev 的错误规则很简单:使用标准 HTTP 状态码,错误体采用你所调用接口的格式,所以 Anthropic、OpenAI 和 Google 的 SDK 抛出的异常和直连官方时一样。大致来说:4xx 表示要改请求(或者充值),429 和 5xx 是暂时性问题,值得重试。

状态码一览#

状态码名称常见原因是否重试
400Bad Request请求体不是合法 JSON;模型拒绝了某个参数,例如 max_tokens 超出上限;x-halfoff-group 未知;模型的内容过滤拒绝了请求否,先改请求
401Unauthenticated密钥缺失、输错、已吊销或已过期否,检查密钥
402Payment Required余额为 $0,或密钥达到消费上限充值或调高上限后再试
403Forbidden模型不在密钥的白名单中否
404Not Found模型 id 不存在、该模型不支持这种接口格式、端点不存在或 HTTP 方法不对否
413Payload Too Large提示词超出模型上限否,缩短内容
422Unprocessable模型拒绝了请求内容否
429Too Many Requests密钥的频率或并发限制,或模型容量繁忙是,按 retry-after 等待
500Internal ErrorHalf Off Dev 内部意外错误是,退避重试
502Bad GatewayHalf 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.typeOpenAI error.type 与 error.codeGemini error.status
400invalid_request_errorinvalid_request_errorINVALID_ARGUMENT
401authentication_errorauthentication_errorUNAUTHENTICATED
402billing_errorinsufficient_quotaRESOURCE_EXHAUSTED
403permission_errorpermission_deniedPERMISSION_DENIED
404not_found_error,端点不存在时为 not_foundnot_found_error,端点不存在时为 not_foundNOT_FOUND
413、422invalid_request_errorinvalid_request_errorINVALID_ARGUMENT
429rate_limit_errorrate_limit_exceededRESOURCE_EXHAUSTED
500api_errorapi_errorINTERNAL
502、503api_errorapi_errorUNAVAILABLE
504api_errorapi_errorDEADLINE_EXCEEDED

Gemini 错误体里的 error.code 始终等于 HTTP 状态码,而 402 和 429 都对应 RESOURCE_EXHAUSTED,所以请根据数字判断,不要依赖字符串。即使是在读取请求体之前就产生的错误(比如路径不存在),也会使用与所调用路径相匹配的格式。

可能看到的错误消息#

状态码message
400Body must be JSON
400Unknown group 'xyz'
400The model provider's content filter refused this request. Rewording it or using another model may help.
400、413、422模型自己给出的原因,例如 prompt is too long
401Missing API key、Invalid or revoked API key、This API key has expired
402Insufficient balance. Top up at the dashboard.
402This API key has reached its spend limit.
403This API key is not allowed to use 'gpt-6.1-sol'.
404Model 'gpt-9' is not available. See GET /v1/models.
404Unknown endpoint /v1/embeddings、No route for GET /v1/messages
429This API key has reached its rate or concurrency limit.
429All upstream capacity for this model is busy. Retry shortly.
502The 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 头一起保存。