新增三部分:此前猜的五个码名与官方对照(六错五,作为教训留档)、官方表 13 条的 分类一览(全是基础设施与计费,**没有任何内容审核/侵权码**)、逐条抄自文档的映射表。 两处标注清楚的判断:400 的两义用 error.param 的有无区分(推断,附三条依据与取错 代价的不对称分析);Retry-After 暂不接(附可算的理由——Java 侧退避基线已长于服务商 通常给的值)。 另记一条厂商改判:视频过载已从 429 改判为 503,我们两者都归可重试故不受影响, 并说明测试在哪里钉住这个「无影响」。 mkdocs build --strict 通过。
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# OpenAI gpt-image-2 图片生成:接口事实与映射
|
||||
|
||||
> 取证日期:2026-09-21 来源:用户提供的两份官方文档(`doc_temp/gpt生图.md`、
|
||||
> `doc_temp/gpt图像编辑.md`)
|
||||
> 取证日期:2026-09-21 来源:用户提供的三份官方文档(`doc_temp/gpt生图.md`、
|
||||
> `doc_temp/gpt图像编辑.md`、`doc_temp/gpt错误码.md`)
|
||||
> 实现位置:`patbond-ai` 的 `app/providers/openai/`(`client.py` 传输 / `errors.py` 错误映射 /
|
||||
> `models.py` 厂商 DTO / `image.py` 图片业务 / `capabilities.py` 参数限制 /
|
||||
> `tasks.py` 同步→异步桥)
|
||||
@@ -145,41 +145,108 @@ edge limits」——**没给具体数值**,故这一条无法在本地校验
|
||||
可退避重试)。服务商说成功却没给图,那不是成功——把它当成功会让上层拿到一个没有
|
||||
产物的 SUCCEEDED 去空指针。
|
||||
|
||||
## 6. 错误映射 ⚠️ 未经文档证实
|
||||
## 6. 错误映射
|
||||
|
||||
> **两份文档都没有错误码一节。** 这与火山那边给了完整错误码表的情况不同。
|
||||
> 取证补充(2026-09-21):收到官方 Error codes 页(`doc_temp/gpt错误码.md`)。
|
||||
> 本节此前是按 HTTP 状态码推断的版本,现已按文档实际码名重写。
|
||||
|
||||
因此 `errors.py` 的映射建立在 OpenAI 通用错误约定之上(错误体形如
|
||||
`{"error": {message, type, code, param}}`),并**以 HTTP 状态码为主、
|
||||
`type`/`code` 为辅**。选这个策略是因为状态码语义由 HTTP 规范保证,而 `type`/`code`
|
||||
的取值集没有文档支撑——凭记忆列一张表只会得到一张看起来权威、实则可能错漏的映射。
|
||||
### 先记一笔教训:此前猜的五个码名,一个都不存在
|
||||
|
||||
| 我猜的 | 官方文档里 |
|
||||
| --- | --- |
|
||||
| `rate_limit_exceeded` | ❌ 不存在。真实的是 type `rate_limit_error` + code `slow_down` |
|
||||
| `moderation_blocked` | ❌ 不存在 |
|
||||
| `content_policy_violation` | ❌ 不存在 |
|
||||
| `invalid_api_key` | ❌ 不存在(401 只给人类可读消息,无 code) |
|
||||
| `image_generation_user_error` | ❌ 不存在 |
|
||||
| `insufficient_quota` | ✅ 存在,但它是**宽 `type`** 而非 `code` |
|
||||
|
||||
六个里错五个。这就是「凭记忆列错误码表」的实际命中率——那张表当时看起来是权威的。
|
||||
故当时的 ⚠️ 标注不是客套:**没有文档就要明写没有文档**。
|
||||
|
||||
### ⚠️ 官方错误码表里没有任何业务层失败
|
||||
|
||||
13 条全是**基础设施与计费**:
|
||||
|
||||
| 类别 | 条目 |
|
||||
| --- | --- |
|
||||
| 鉴权(401) | 无效认证 / 密钥不正确 / 不属于任何组织 / IP 不在白名单 |
|
||||
| 地域(403) | 国家、地区或领地不受支持 |
|
||||
| 参数(400) | `service_tier` 不被项目允许 |
|
||||
| 限流(429) | 请求过快 / `slow_down`(涨得太快) |
|
||||
| 钱(429) | 余额耗尽 / 组织消费限额 / 项目消费限额 / 组织用量限额 |
|
||||
| 服务端(500 / 503) | 内部错误 / 模型过载 |
|
||||
|
||||
**没有内容审核、没有版权/侵权、没有「这个提示词不能生成」。** 但图片接口明确有
|
||||
`moderation` 参数(`low`/`auto`),说明审核确实存在——只是它的错误形态未被文档化。
|
||||
|
||||
### 映射表(键全部抄自文档)
|
||||
|
||||
| 依据 | `retry` | 用户可见 |
|
||||
| --- | --- | --- |
|
||||
| `code`/`type` = `insufficient_quota` | `QUOTA` | 配额耗尽 |
|
||||
| `code`/`type` = `rate_limit_exceeded` | `BACKOFF` | 限流 |
|
||||
| `code` = `moderation_blocked` / `content_policy_violation` | `NEVER` | 内容被拒 |
|
||||
| `code` = `invalid_api_key` | `NEVER` | 服务端错误 |
|
||||
| 网络层失败(连不上、超时) | `BACKOFF` | 上游不可用 |
|
||||
| HTTP 429(且拿不到 type/code) | `BACKOFF` | **按限流处理** |
|
||||
| `credit_balance_exhausted` | `QUOTA` | 配额耗尽 |
|
||||
| `organization_spend_limit_exceeded` | `QUOTA` | 配额耗尽 |
|
||||
| `project_spend_limit_exceeded` | `QUOTA` | 配额耗尽 |
|
||||
| `organization_usage_limit_exceeded` | `QUOTA` | 配额耗尽 |
|
||||
| `insufficient_quota`(宽 type) | `QUOTA` | 配额耗尽 |
|
||||
| `slow_down` / `rate_limit_error` | `BACKOFF` | 限流 |
|
||||
| `server_is_overloaded` / `service_unavailable_error` | `BACKOFF` | 上游不可用 |
|
||||
| HTTP 429(无 code 且 type 不认识) | `BACKOFF` | 限流 |
|
||||
| HTTP 400 **带** `error.param` | `NEVER` | 服务端错误 |
|
||||
| HTTP 400 **不带** `error.param` | `NEVER` | **内容被拒**(推断,见下) |
|
||||
| HTTP 401 / 403 | `NEVER` | 服务端错误 |
|
||||
| HTTP 400 | `NEVER` | **服务端错误**,不猜成「内容被拒」 |
|
||||
| 其余 4xx | `NEVER` | 服务端错误 |
|
||||
| 网络层失败(连不上、超时) | `BACKOFF` | 上游不可用 |
|
||||
| 5xx 及一切未知 | `BACKOFF` | 上游不可用 |
|
||||
|
||||
两处取舍值得记下来:
|
||||
`Retry.QUOTA` 这一档的存在理由正是文档那句原话:**「Retrying billing, spend, or
|
||||
quota errors won't restore API access」**——重试不会恢复访问,要人去充值或改限额。
|
||||
Worker 不该把重试预算花在这上面,应直接终态失败并让用户看到出口。
|
||||
|
||||
- **429 拿不到 type/code 时按限流而非配额耗尽。** 429 在 OpenAI 上两义兼有;猜成
|
||||
配额耗尽会把一次本可重试的限流判死,代价更大。
|
||||
- **400 归「服务端错误」而不是「内容被拒」。** OpenAI 用 400 同时表示参数问题与
|
||||
内容被拒;拿不到 type/code 时无法区分,而错报「内容被拒」会让用户去改一个其实
|
||||
没问题的提示词。
|
||||
**两个字段查同一张表。** 文档把某个标识列在哪个字段下,不保证实际响应里它一定出现
|
||||
在那个字段(`insufficient_quota` 被描述为「broader `error.type`」,但很容易出现在
|
||||
`code` 里)。查错字段的代价是把配额耗尽降级成限流、白烧一轮重试预算;而两边都查
|
||||
不引入任何**新**词汇。
|
||||
|
||||
未知情形一律偏向**可退避重试**:把一次可恢复的抖动判成用户可见的失败是更糟的
|
||||
错误,而重试预算本身有上限(`max_attempts`)。
|
||||
### 唯一的推断:400 的两义如何区分
|
||||
|
||||
**待补**:拿到官方错误码文档、或在真实调用中观察到实际取值后,校正 `errors.py`
|
||||
并回改本节。在那之前,`errors.py` 顶部那段 ⚠️ 说明不要删。
|
||||
因为没有内容审核码,**一次侵权或违规拒绝很可能以一个普通 400 到达**,只带一句
|
||||
人类可读的消息。若把这种 400 一律归为「服务端错误」,用户会看到「服务出错了」
|
||||
然后反复重试同一个注定被拒的提示词。
|
||||
|
||||
用 `error.param` 的有无做结构化区分,三条依据:
|
||||
|
||||
1. 文档在 `service_tier` 那条明确写了「as an `invalid_request_error` with
|
||||
`error.param` set to `service_tier`」——**参数类错误会带 `param`**。
|
||||
2. 官方表没有任何内容审核码,故内容拒绝只能以通用 400 到达,且它不指向任何具体
|
||||
参数,`param` 应为空。
|
||||
3. 我方在调用前已用 `capabilities.py` 校验过尺寸、画质、格式,`model`/`n` 由程序
|
||||
生成——**能活着到达服务商并被 400 拒掉的,参数嫌疑本来就不大**。
|
||||
|
||||
取错的代价不对称,这也是选这个方向的理由:把内容拒绝报成「服务出错」会让用户反复
|
||||
重试一个注定被拒的提示词;反过来把一个参数 bug 报成「内容可能不被支持」只是让用户
|
||||
多试一次,而排查者看到的 `provider_code` 与日志仍然准确。走这条分支时会记一条
|
||||
**WARNING**,那是日后校正它的唯一素材来源。
|
||||
|
||||
**待补**:拿到真实的审核错误形态(或官方补出业务码)后,把这里换成确定的判定。
|
||||
|
||||
### 一处厂商改判,对我们无影响
|
||||
|
||||
文档提醒:**视频过载已从 429 改判为 503**,且 Python SDK 分别抛 `RateLimitError`
|
||||
与 `InternalServerError`,只捕一类会漏。我们两者都归可重试,故这次改判不影响本
|
||||
服务——`tests/providers/openai/test_errors.py` 里有一条测试专门钉住这个「无影响」,
|
||||
哪天有人把 5xx 改成不可重试,它会红。
|
||||
|
||||
### 未接的一项:`Retry-After`
|
||||
|
||||
文档在 429 与 503 处反复要求遵守 `Retry-After` 响应头。本服务目前**不读它**。
|
||||
|
||||
不读的理由是可算的:Java 侧 Worker 的退避阶梯基线 30 秒、指数增长、上限 10 分钟
|
||||
(`CreationProperties.retryBackoffFor`),**在绝大多数情形下已长于服务商给的
|
||||
`Retry-After`**——也就是我们已经比要求更保守,忽略它只会让重试偏慢,不会加剧限流。
|
||||
|
||||
要接起来是一次跨语言契约变更:领域 `Failure` 加字段 → `openapi-internal.yaml` 小
|
||||
版本 → Java 侧 DTO 与 Worker 调度都要改。收益只是延迟优化,故排在存储决策之后。
|
||||
|
||||
## 7. 同步接口的两笔代价
|
||||
|
||||
@@ -232,7 +299,7 @@ edge limits」——**没给具体数值**,故这一条无法在本地校验
|
||||
| 项 | 状态 |
|
||||
| --- | --- |
|
||||
| HTTP 传输与鉴权(`client.py`) | ✅ 离线测试(MockTransport) |
|
||||
| 错误分类(`errors.py`) | ✅ 有测试,但**映射本身未经文档证实**(第 6 节) |
|
||||
| 错误分类(`errors.py`) | ✅ 按官方 Error codes 页逐条落地;仅 400 的两义区分是推断(第 6 节) |
|
||||
| 参数预校验(`capabilities.py`) | ✅ 四条算术规则 + 画质/格式 |
|
||||
| 同步→异步桥(`tasks.py`) | ✅ 含容量上限、TTL、死锁回归 |
|
||||
| 图片业务(`image.py`) | ✅ 文生图 + 图生图(含参考图下载) |
|
||||
|
||||
Reference in New Issue
Block a user