拿到 doc_temp/gpt错误码.md 后核对,**此前猜的六个键里错了五个**: rate_limit_exceeded / moderation_blocked / content_policy_violation / invalid_api_key / image_generation_user_error 在官方文档里都不存在。真实的是 type rate_limit_error + code slow_down、type service_unavailable_error + code server_is_overloaded、四个 billing 码。唯一对的 insufficient_quota 也是宽 type 而非 code。这段历史留在 errors.py 顶部,作为凭记忆列错误码表代价的记录。 官方表的 13 条全是基础设施与计费,**没有任何内容审核/侵权码**——但图片接口确有 moderation 参数,说明审核存在、只是错误形态未文档化。直接后果:一次侵权或违规 拒绝很可能以普通 400 到达,只带一句人类可读消息。若一律归「服务端错误」,用户会 看到「服务出错了」并反复重试一个注定被拒的提示词。 故用 error.param 的有无做结构化区分(带 param = 参数错,不带 = 按内容被拒), 三条依据:文档的 service_tier 那条明确说参数错会带 param;内容拒绝不指向任何 具体参数;我方已在调用前校验过尺寸/画质/格式,能被 400 拒掉的参数嫌疑本就不大。 这一条是推断,走到时记 WARNING——那是日后校正它的唯一素材。client.py 相应补上 param 的解析。 两处顺带收获: - 无 code 的 429 按限流处理,现在是**文档支持的默认**(六种 429 里只有「Rate limit reached for requests」没有文档化的 code),不再是猜测。 - 文档提醒视频过载已从 429 改判为 503。我们两者都归可重试,故不受影响;加了一条 测试钉住这个「无影响」。 同一张表按 code 与 type 两个字段分别查:文档把标识列在哪个字段下,不保证响应里 它就在那个字段,查错的代价是把配额耗尽降级成限流、白烧一轮重试预算。 未接 Retry-After 并记录理由:Java 侧退避基线 30 秒、上限 10 分钟,已长于服务商 通常给的值,忽略只会偏慢、不会加剧限流;接它是一次跨语言契约变更。 测试 271 → 284。四道门禁逐条读退出码,全绿。
This commit is contained in:
@@ -357,11 +357,22 @@ class TestResponseMapping:
|
||||
|
||||
|
||||
class TestFailurePropagation:
|
||||
"""只验「厂商错误能原样穿过后台线程到达查询结果」,分类本身在 test_errors.py。
|
||||
|
||||
码名全部抄自官方 Error codes 页(`doc_temp/gpt错误码.md`)。
|
||||
"""
|
||||
|
||||
def test_vendor_rate_limit_becomes_a_retryable_failure(self):
|
||||
def handler(request):
|
||||
return httpx.Response(
|
||||
429,
|
||||
json={"error": {"message": "slow down", "type": "rate_limit_exceeded"}},
|
||||
json={
|
||||
"error": {
|
||||
"message": "Your request rate increased too quickly",
|
||||
"type": "rate_limit_error",
|
||||
"code": "slow_down",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
provider = provider_with(handler)
|
||||
@@ -371,11 +382,17 @@ class TestFailurePropagation:
|
||||
assert result.failure.retry is Retry.BACKOFF
|
||||
assert result.failure.user_facing is UserFacing.RATE_LIMITED
|
||||
|
||||
def test_insufficient_quota_is_not_worth_retrying(self):
|
||||
def test_an_exhausted_credit_balance_is_not_worth_retrying(self):
|
||||
def handler(request):
|
||||
return httpx.Response(
|
||||
429,
|
||||
json={"error": {"message": "no credit", "code": "insufficient_quota"}},
|
||||
json={
|
||||
"error": {
|
||||
"message": "Your organization has no prepaid credits remaining",
|
||||
"type": "insufficient_quota",
|
||||
"code": "credit_balance_exhausted",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
provider = provider_with(handler)
|
||||
@@ -384,11 +401,22 @@ class TestFailurePropagation:
|
||||
assert result.failure.retry is Retry.QUOTA
|
||||
assert result.failure.user_facing is UserFacing.QUOTA_EXHAUSTED
|
||||
|
||||
def test_content_policy_violation_is_terminal(self):
|
||||
def test_a_bare_400_is_surfaced_as_a_content_refusal(self):
|
||||
"""官方错误码表里**没有任何内容审核/侵权码**。
|
||||
|
||||
故一次「这个提示词不能生成」只能以通用 400 到达。不带 `error.param` 时按
|
||||
内容被拒处理——详见 `errors.py` 的 `_classify_bad_request`。
|
||||
"""
|
||||
|
||||
def handler(request):
|
||||
return httpx.Response(
|
||||
400,
|
||||
json={"error": {"message": "blocked", "code": "moderation_blocked"}},
|
||||
json={
|
||||
"error": {
|
||||
"message": "Your request was rejected",
|
||||
"type": "invalid_request_error",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
provider = provider_with(handler)
|
||||
|
||||
Reference in New Issue
Block a user