公司动态

OpenAI Python SDK 429 后又断流?按 Retry-After 与 finish_reason 分层

📅 2026/8/7 6:56:11
OpenAI Python SDK 429 后又断流?按 Retry-After 与 finish_reason 分层
OpenAI Python SDK 429 后又断流按 Retry-After 与 finish_reason 分层Python 服务调用 OpenAI API 或 OpenAI-compatible endpoint 时常见一条让日志很难解释的故障链第一次请求返回429SDK 等待后自动重试第二次已经拿到 HTTP200和部分流式文本却在结束前断开。此时只看最后一个异常会误以为整次请求只发生了一次只看 HTTP200又会把半段文本当成完整结果。本文锁定Python 3.11.15 openai2.53.0 Chat Completions SSE。先给可执行配置和成功信号再解释重试边界。实测使用只监听127.0.0.1的夹具没有请求线上模型也没有使用真实 API Key。先按这组环境和配置复现1. 固定 Python 与 SDK 版本openai 2.53.0要求 Python 3.10 或更高。先建立隔离环境python3.11-mvenv /tmp/openai-429-streamsource/tmp/openai-429-stream/bin/activate python-mpipinstallopenai2.53.0python-cimport openai; print(openai.__version__)本次机器的本地 PyPI 镜像只同步到2.48.0所以上面的 PyPI 安装没有作为实测证据。实际执行改用官方v2.53.0对应 commitgh repo clone openai/openai-python /tmp/openai-python ----depth1git-C/tmp/openai-python checkout 5e36cd326fa2ebe00260386e7a27fe1c8c02d4fd python-mpipinstall/tmp/openai-python python-cimport openai; print(openai.__version__)成功信号必须精确输出2.53.0。如果仍是旧版本先不要拿新版本源码结论解释旧客户端行为。2. 配置位置与最小客户端配置入口就在OpenAI(...)构造函数。真实 Key 应由环境变量或密钥管理服务注入不要写进源码、日志或截图importosfromopenaiimportOpenAI clientOpenAI(api_keyos.environ[OPENAI_API_KEY],base_urlhttps://your-endpoint.example/v1,max_retries2,timeout30.0,)这里的max_retries2表示一次初始请求失败后符合条件时最多再试两次。官方文档同时说明临时 429 可能携带Retry-After官方 SDK 会在可重试场景中遵守这个值。不要再在外层无条件套一个“重试三次”的循环否则业务层和 SDK 会叠加请求。3. 最小流式请求与完整成功信号streamclient.chat.completions.create(modelyour-model-id,messages[{role:user,content:reply with OK}],streamTrue,)parts[]finish_reasonNoneforchunkinstream:ifnotchunk.choices:continuechoicechunk.choices[0]ifchoice.delta.content:parts.append(choice.delta.content)ifchoice.finish_reasonisnotNone:finish_reasonchoice.finish_reasoniffinish_reason!stop:raiseRuntimeError(stream incomplete: no finish_reasonstop)text.join(parts)本文的最小成功信号不是“收到第一个字”也不是“HTTP 状态是 200”而是流已完整消费、没有传输异常并且最后观察到finish_reasonstop。失败路径至少包括429已耗尽重试、连接在部分文本后中断以及流结束但没有可接受的完成原因。429 发生在响应体之前先让 SDK 处理一次OpenAI 官方错误文档把429映射为RateLimitError。但 429 不是单一原因临时速率限制可以重试配额、账单或其他需要人工处理的错误不能因为多等几秒就自动恢复。判断入口应是错误类型、错误体、Retry-After、x-request-id和限流剩余/重置头而不是“看到 429 就循环”。在openai-python v2.53.0中默认重试列表包含 429客户端会解析Retry-After对有限且有效的服务端等待值优先使用该时间。官方文档还提醒失败请求本身也会消耗每分钟限额所以快速连续重发只会加剧拥塞。本地夹具的 429 分支固定为第一次返回429 Retry-After: 0.20第二次返回 200。实际输出如下RATE_ATTEMPTS2 RATE_ELAPSED_MS313 RATE_RETRY_AFTER_HONORED1 RATE_FINAL_HTTP200 RATE_FINAL_REQUEST_IDreq_rate_2 RATE_FINAL_TEXTRATE_OK RATE_CLIENT_ID_REUSED1这证明在本文版本与夹具下SDK 没有立即撞第二次请求而是完成一次等待后重试。313ms不是 OpenAI 或任何网关的固定退避时间它只证明实际总耗时超过了夹具给出的 200ms 最小等待。HTTP 200 后断流不是另一条 429 重试自动重试的关键边界在“错误发生在哪个阶段”。429 是服务器在可用响应体之前明确拒绝请求流中断则可能发生在客户端已经收到响应头、HTTP 200 和若干 SSE 分片之后。夹具的断流分支先返回一个合法文本分片PARTIAL声明一个更长的Content-Length然后提前关闭连接。客户端设置仍然是max_retries2实际输出为STREAM_BREAK_REQUESTS1 STREAM_BREAK_REQUEST_IDreq_stream_break_1 STREAM_BREAK_LAST_TEXTPARTIAL STREAM_BREAK_FINISH_REASONmissing STREAM_BREAK_ERRORRemoteProtocolError最重要的不是异常类名字而是三个组合信号请求计数仍为 1、已经产生部分文本、没有finish_reasonstop。在这个阶段SDK 没有把整个流自动重放。不同 HTTP 客户端或代理可能给出其他传输异常因此线上排查应记录异常类型但不要把RemoteProtocolError当成唯一可能。request_id 要分清服务端和调用方x-request-id是服务端响应头中的请求标识。普通成功对象可以通过顶层_request_id读取流式对象可以从底层响应头保留它。第三方兼容服务不一定提供该头缺失时应写missing_request_id不能自己造一个值冒充服务端 ID。X-Client-Request-Id则是调用方主动发送的标识官方文档明确它不会自动添加。它适合把“业务操作”和“单次 HTTP 请求”联系起来尤其是在超时或断流后拿不到服务端 ID 时。建议同时保留两层字段operation_id业务侧一次操作 client_request_id调用方显式生成的单次请求 ID server_request_id响应头 x-request-id可能缺失 attempt该操作内的实际 HTTP 次数不要把完整请求头写进日志这里只需要标识、时间、方法、脱敏主机/路径、状态和完成信号。恢复前先决定“部分文本是否已经生效”断流后最危险的动作是无条件重放。若你已经把部分文本推送给用户、触发工具、写数据库、发消息或扣费重放可能制造重复副作用。更稳妥的边界是流式文本先进入本次请求的临时缓冲区。只有完整消费并出现可接受的finish_reason才提交最终结果。中断时记录最后文本长度、最后完成原因、客户端与服务端 request ID。只有确认业务动作可重放时才发起一个新的显式请求。总预算同时限制次数与总耗时并把 SDK 内建重试算进去。夹具的显式恢复分支是一个新的请求结果为STREAM_RECOVERY_REQUESTS1 STREAM_RECOVERY_REQUEST_IDreq_stream_recover_1 STREAM_RECOVERY_TEXTRECOVERED STREAM_RECOVERY_FINISH_REASONstop STREAM_RECOVERY_ERRORnone它只能证明恢复代码路径能够识别完整结束不能证明任何线上服务一定会恢复也不能证明原请求没有产生副作用。用这张表决定下一步观察结果当前层第一动作是否直接重试429 有效Retry-After请求被限流让 SDK 处理符合条件的一次等待记录实际次数不再额外套无界循环429 配额/账单/需人工处理错误业务或账户条件停止快速重发按错误体处理否HTTP 200 部分文本 传输异常响应体读取阶段保留最后文本、request ID、异常和 attempt先查副作用与幂等流结束但finish_reason缺失完成契约不满足把结果标为 incomplete不提交缓冲区受控决定完整消费 finish_reasonstop成功提交缓冲结果并记录成功样本不需要与常见旧排障题的差别这不是另一篇 502/503 通用重试教程。5xx 题重点是默认重试次数、max_retries0和 timeout本文重点是Retry-After被 SDK 处理后为什么 HTTP 200 后的流体中断仍只发生一次请求以及如何用finish_reason保护部分输出。这也不是 Dify Workflow 的 429/Timeout 题。Dify 文章要核对/v1/workflows/run、首包/总耗时和工作流事件本文只讨论官方 Python SDK 的 Chat Completions 流、SDK 重试所有权和完成契约。本地实测说明完整探针只绑定127.0.0.1HTTP 客户端设置trust_envFalse以避免系统代理改写回环请求。合成 Key 只在内存中传入没有输出到日志。最终摘要为SUMMARYpass rate_attempts2 retry_after_honored1 break_requests1 break_finishmissing recovery_finishstop ONLINE_PROVIDER_REQUESTNO总结OpenAI Python SDK 遇到 429 后又流式中断时先按阶段拆开429 发生在可用响应体之前符合条件时由 SDK 按Retry-After有限重试HTTP 200 后已经收到部分 SSE 再断开则要按响应体不完整处理不能假设max_retries会自动重放。把实际请求次数、x-request-id、调用方 request ID、最后文本和finish_reason放在同一条记录里才能避免重复请求也避免把半段答案写成成功。参考资料OpenAI API Overview: https://developers.openai.com/api/reference/overviewOpenAI Error Codes: https://developers.openai.com/api/docs/guides/error-codesOpenAI Rate Limits: https://developers.openai.com/api/docs/guides/rate-limitsopenai-python v2.53.0: https://github.com/openai/openai-python/releases/tag/v2.53.0