重试、超时与幂等
先把 SDK 的自动重试关掉
Section titled “先把 SDK 的自动重试关掉”OpenAI 与 Anthropic 的官方 SDK 默认会自动重试(通常 2 次)。在直连模型服务时这没什么,在网关后面则不然:
Halro 在把请求发往上游之前就把预算预留写入账本,并且这个预留是持久的。没有有效幂等键保护时,客户端的一次重试会创建新的预留与结算。SDK 帮你「悄悄重试两次」的结果,是账上出现三笔而不是一笔。
client = OpenAI(base_url="...", api_key=..., timeout=60.0, max_retries=0)const client = new OpenAI({ baseURL: "...", apiKey: ..., timeout: 60_000, maxRetries: 0 });关掉之后,重试策略由你自己写 —— 下面是判断依据。
什么时候重试是安全的
Section titled “什么时候重试是安全的”| 情况 | 能否重试 | 说明 |
|---|---|---|
| 429 每源限流 / Project 限流 | 可以 | 读 Retry-After(可能不存在,见下),退避后重试 |
| 429 Provider 并发上限 | 可以 | 有可解析的 Retry-After 时遵循它,否则应用退避 |
| 429 Deployment 并发上限 | 可以 | 不带 Retry-After,使用有上限的应用退避 |
503 provider_unavailable | 可以 | 当前没有健康的 Deployment |
503 all_candidates_suspended | 按原因处理 | 有窗口时退避;失效凭据无 Retry-After,联系管理员 |
503 accounting_capacity_exhausted | 可以 | 遵循 Retry-After 等待已准入工作结算 |
| 上游 5xx | 谨慎 | 可能已经到达 Provider,按歧义失败处理;不要假设网关已自动回退 |
403 token_guard_blocked | 不要立即重试 | 可能是成本上限或 temporary block;按错误详情调整请求或等待/管理员处置 |
403 model_not_allowed / 404 model_not_found | 不要 | 配置问题,找管理员 |
| 400 字段被拒 | 不要 | 请求本身不合法,改请求 |
| 结果不明确的失败 | 谨慎 | 见下一节 |
结果不明确的失败,网关不会替你重试
Section titled “结果不明确的失败,网关不会替你重试”如果一次上游调用的结果是歧义的 —— 例如连接在请求已发出但响应未回来时断开 —— Halro 不会重试它,也不会把费用退掉。这是有意的:无法确定上游是否已经产生了计费事件时,账目按发生过保守处理,而不是乐观地假设它没发生。
所以这类失败给你的是一个错误,而账上可能已经有一笔。你自己重发是允许的,但要清楚那会是第二笔独立的费用。
Chat 与 Embeddings:防重复调用,不重放答案
Section titled “Chat 与 Embeddings:防重复调用,不重放答案”v0.9.0 起,POST /v1/chat/completions(包括流式)和 POST /v1/embeddings 支持
Idempotency-Key。同一业务操作应保存并复用相同键、正文和 Run 归因;以下为首个请求示例,
重试时不要重新生成键:
IDEMPOTENCY_KEY="$(uuidgen)"curl https://halro.example.com/v1/chat/completions \ -H "Authorization: Bearer $HALRO_GATEWAY_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $IDEMPOTENCY_KEY" \ -d '{"model":"chat","max_tokens":256,"messages":[{"role":"user","content":"Hello"}]}'| 重复请求状态 | 返回 | 处理 |
|---|---|---|
| 相同键、不同请求正文 | 409 idempotency_conflict | 核对业务键,不能用换键掩盖冲突 |
| 仍在执行或结果不明确 | 409 idempotency_in_progress | 有界等待;不启动另一笔调用 |
| 已完成 | 409 idempotency_completed | 不会返回原答案;客户端需保存首次响应 |
| 无法安全检查或保存键 | 503,如 resource_store_unavailable | 修复存储后使用原键,不按“键可用”处理 |
流式重复请求在打开流之前作为 HTTP 错误拒绝。此机制保证键记录有效期内至多一次上游执行,
不保证响应可重放;记录不保存请求/响应正文,24 小时后所有状态都会过期,过期后复用可能产生新调用。
键按 Project 隔离,Chat 与 Embeddings 共用 inference_call 命名空间。键必须为 1–128 个可见 ASCII
字符且不含空白;格式错误返回 400 invalid_idempotency_key。未带键的请求维持原行为。
上游已被触达的失败会消耗键;能力、Token、预算或 Attempt 预留在上游调用前拒绝的请求会释放键。
客户端不能把所有失败都当作可安全换键重试。同步 Responses 和 Messages 不提供这套推理幂等契约。
需要稍后读取答案时,选择显式启用的 background: true Responses。
资源类端点:用 Idempotency-Key
Section titled “资源类端点:用 Idempotency-Key”创建型的资源端点接受 Idempotency-Key 请求头 —— 异步调用(POST /v1/async/invocations)、文件创建(POST /v1/files)、批处理创建(POST /v1/batches)。带上它,重复提交同一个键不会创建第二个资源。
curl https://halro.example.com/v1/files \ -H "Authorization: Bearer $HALRO_GATEWAY_KEY" \ -H "Halro-Route: batch-files" \ -H "Idempotency-Key: $(uuidgen)" \ -F purpose=batch -F file=@input.jsonl同步 Responses / Messages 不走这套;Chat / Embeddings 使用上面的防重复调用契约。 background: true 的 Responses 是例外:
Idempotency-Key 可选但强烈建议使用;相同 Key、正文和 X-Halro-Run-ID 归因返回同一记录,
其中任一项改变却复用 Key 都返回 409。
未受有效幂等键保护的同步调用,重发可能产生新的费用。
Retry-After 不是每次都有
Section titled “Retry-After 不是每次都有”只在能算出一个正数等待时长时才写入。每源限流与 Project 限流会带;Deployment 并发那条不设置;上游透传的 429 只在上游自己带了可解析的头时才有。读不到就用你自己的退避策略,不要假设它一定存在。
没有放之四海的建议值 —— 它取决于你的模型和 max_tokens。两个原则:
- 超时要长于你允许的最长生成时间,否则你会在上游正常工作时把连接掐掉,而那正好落进上一节说的「结果不明确」。
- 流式请求的超时应当针对单个事件间隔,而不是整个响应,否则长回答必然超时。
如何把 Project/Run 预算、客户端重试和 Halro Attempt 上限组合起来,见最佳实践案例库。