Skip to content

feat(gateway): 图/视频调用经进程内 Gateway 做重试、Fallback 与观测 - #331

Open
xiaocheny214 wants to merge 24 commits into
1024XEngineer:mainfrom
xiaocheny214:feat/llm-gateway
Open

feat(gateway): 图/视频调用经进程内 Gateway 做重试、Fallback 与观测#331
xiaocheny214 wants to merge 24 commits into
1024XEngineer:mainfrom
xiaocheny214:feat/llm-gateway

Conversation

@xiaocheny214

@xiaocheny214 xiaocheny214 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Summary

  • 把图生图和 i2v 的重试、同家族 Fallback、熔断和结构化 trace 从 sufy.py 抽到 windup_framework.gateway。业务仍只调 ImageProvider.gen_image / VideoProvider.i2v
  • 521/522/523/525 同型号只再打一次、不换型号;520/524 不重试。视频有 job_id 后不再 POST 第二单,仅 failed/cancelled 允许换型号新开。
  • 任务失败原因带上 request_id,便于从日志捞 attempt 链。积分扣费仍走现有 QUOTA_*

Fixes #330

Test plan

  • backend/ 下 gateway + sufy + custom_action + executor 等相关测试 167 passed
  • 图像 522:同一型号恰好 2 次 POST,不打 fallback 型号
  • 图像 429:primary 最多 3 次后走 fallback
  • 图像 520:只打 1 次
  • 视频 submit 拿到 job_id 后,轮询 429/失败不再开第二单;上游 failed/cancelled 才换型号
  • 非法 video_model 仍 4xx
  • 失败任务的 error 含 request_id=

xiaocheny214 and others added 11 commits August 17, 2026 09:34
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
并发过期不再 KeyError;跳过开路型号会记 fallback_used;视频分阶段耗时写入 trace;提交未拿到 job_id 时除 429 外不再换型号。

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented Aug 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
windup Ignored Ignored Preview Aug 20, 2026 10:11am

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review conclusion

整体分层与提交后不重复建单的主路径一致,但视频错误状态仍有两处会扩大重试或污染后续路由:轮询 52x 被 adapter 与 Gateway 双重重试,以及本次请求明确禁止 fallback 时仍提前打开型号熔断。建议修正这两个状态转换后再合入。

验证:完整检查了固定 diff 4103106...19fce6a 的 gateway、Sufy adapter、executor/API 接入与相关测试;git diff --check 通过。当前运行环境缺少 uv 和项目 Python 依赖,未能执行 pytest。

View job run

Comment thread backend/packages/framework/src/windup_framework/gateway/video.py Outdated
Comment thread backend/packages/framework/src/windup_framework/providers/sufy.py
@johnnyzhang-eng

Copy link
Copy Markdown
Contributor

我的 #324 也改 sufy.py——抽了个 ChatCompletionsFace 让出图和判官共用建连和重试,和你这版重叠。你把重试整个移到 Gateway 之后那层就多余了,所以不管哪边先合,我都按你的 AdapterResult 收,不另起一套。

@xiaocheny214

Copy link
Copy Markdown
Contributor Author

我的 #324 也改 sufy.py——抽了个 ChatCompletionsFace 让出图和判官共用建连和重试,和你这版重叠。你把重试整个移到 Gateway 之后那层就多余了,所以不管哪边先合,我都按你的 AdapterResult 收,不另起一套。

现按照我的来做吧。咱们把重试的逻辑统一放在gateway做,这样方便后续的链路追踪、熔断、限流等逻辑的引入,以及更换模型路由。这样看起来,重试逻辑也不会太过散乱,你觉得呢?
@johnnyzhang-eng

@johnnyzhang-eng

Copy link
Copy Markdown
Contributor

同意,重试归 Gateway。#324 没有另起一套:它改的是 sufy.py 里已有那处,把措辞从「图像服务」改成通用、让判官复用同一条建连和退避,你这版落地后删的是同一处,不会留两套。另外 #331 自 08-17 是冲突态没再动过而 #324 已绿,所以我按互不阻塞处理,谁先合我跟谁改。

429 换 key 不换模型,522 跳过该入口剩余 key;Chat 走同一套路由。
AttemptTrace 只保留热字段,排障详情进 AttemptDetail。
xyh202131
xyh202131 previously approved these changes Aug 19, 2026
xyh202131
xyh202131 previously approved these changes Aug 19, 2026

@xyh202131 xyh202131 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

批准:重试、Fallback、熔断与 trace 集中到 Gateway,且视频拿到 job_id 后不重复下单,避免重复扣费;请求链可追踪,错误分类覆盖完整。

路线选择前误用未赋值的 master,且不应再把型号传给 _get_generator。
xyh202131
xyh202131 previously approved these changes Aug 19, 2026

@xyh202131 xyh202131 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

批准:重试、Fallback、熔断与 trace 集中到 Gateway,视频拿到 job_id 后不重复下单;最新修正也去除了重复 generate 调用,避免重复请求与扣费。

Chat 工厂校验 AI_API_KEY / AI_CHAT_MODEL;图片失败解冻积分;stance 测试改打 generate_character_action。
@vercel

vercel Bot commented Aug 19, 2026

Copy link
Copy Markdown

Deployment failed for project windup with the following error:

Resource is limited - try again in 24 hours (more than 100, code: "api-deployments-free-per-day").

Learn More: https://vercel.com/huyan-s-projects1?upgradeToPro=build-rate-limit

xyh202131
xyh202131 previously approved these changes Aug 19, 2026

@xyh202131 xyh202131 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

批准:Gateway 统一重试、Fallback、熔断与 trace,视频已有 job_id 时避免重复下单;最新提交补齐 chat 配置校验及图片失败后的回滚与积分解冻。核心 CI 通过,Codecov patch 仍差 0.77 个百分点。

submit_image 一次一枪交给 Gateway;判官继续走带 429/52x 重试的 _post。
@johnnyzhang-eng

Copy link
Copy Markdown
Contributor

lint-and-test 红在 sufy.py 的解冲突残留:submit_image 取了 Gateway 版的单次 POST,但旧重试循环的循环体 521-548 行留在原地、for attempt 那行没了,所以 ruff 报 521 Unexpected indentation 和 572 Expected a statement。那段可以整段删掉,里面引用的 _POST_TRIES_CLOUDFLARE_UNREACHED_STATUS_MAX_RETRY_WAIT_retry_exhausted_messageresends 在本文件里已经都没有定义了,删完 520 行的 classify_http 直接接 549 行的 400/404 分支即可。重试归 Gateway 是 08-18 对齐过的,#324 已合,那边不会再留第二套。

合入 Gateway 时 _post 曾被丢掉;这些用例拦住再揉进出图路径。
codecov/patch 差在 chat.py 的电路开路与 LangChain 异常映射。
@minorcell

Copy link
Copy Markdown
Member

@fennoai

  1. 当前的设计思路是什么
  2. 为何改动这么大

@fennoai

fennoai Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

1. 当前设计思路

整体是在业务层与具体上游 Provider 之间增加一个进程内 Gateway 编排层,但保持业务调用接口不变:业务仍调用 gen_image() / i2v(),只是实际对象由原 Provider 换成 Gateway。

调用链大致是:

Executor / AI EngineImageGateway / VideoGateway / ChatGatewaySufy 等单次调用 Adapter → 上游 API

职责拆分如下:

  • Provider/Adapter 只负责协议细节:组装请求、提交任务、轮询、下载,并把一次调用统一转换为 AdapterResult;不再自行决定跨模型重试。
  • Gateway 负责可靠性编排:根据错误分类决定同路由重试、换 key、换 base URL、同家族模型 fallback 或直接失败。
  • Registry 保证兼容性:fallback 链只能使用已登记且请求协议相同的模型 family,避免“模型换了但请求结构不兼容”。
  • 视频使用有状态流程:区分 submit / follow / download;一旦拿到 job_id,超时、429 或网络错误不会再次 POST 新任务,只有上游明确返回 failed/cancelled 才允许换模型重新建单,以避免重复扣费。
  • 熔断按层级生效:可分别跳过 model、API key、base URL 或聚合器故障点,而不是把所有失败都视为模型失败。
  • 观测围绕一次业务请求串联:通过 request_id、attempt trace、热表/详情表记录候选路由、重试次数、错误、耗时、job_id、可能计费和估算成本;观测写入失败不会影响生成主流程。
  • 现有积分结算未迁入 Gateway:Gateway 记录上游调用和成本线索,业务的 QUOTA_* 扣费逻辑仍保持原样。

2. 为何改动这么大

这次并非单纯把几段 retry 代码从 sufy.py 搬到新文件,而是同时引入了一套完整的路由、状态机和观测基础设施。

固定 SHA 范围内共有 4094 行新增、334 行删除,其中:

  • 测试新增 1714 行,占新增代码约 42%;覆盖 429、52x、熔断、key/base URL/model fallback、视频幂等和 ledger 持久化等组合。
  • 生产代码新增 2380 行;仅 windup_framework/gateway/ 新模块合计约 1963 行。
  • PR 实际覆盖了 image、video、chat 三条链路,不只是摘要强调的图像和视频;chat.py 及对应测试本身就占了较大部分。
  • 增加了两张 Gateway attempt 数据表及持久化逻辑,同时需要在应用启动时注册 ORM 模型。
  • sufy.py 需要从“内部完成整套重试的 Provider”拆成“单次请求 Adapter + submit/follow/download 原语”,因此看起来既有新增也有较多重写。
  • Executor、模型配置、错误枚举、环境变量、质量判官和测试夹具都要接入统一上下文及 Gateway,因此改动扩散到 39 个文件。

结论

改动大的主要原因是一次 PR 同时完成了抽层、三类调用接入、多维路由、视频幂等、熔断、结构化观测与持久化,并非核心重试算法本身需要四千行。设计目标和分层总体是清晰的,但从评审粒度看确实偏大;如果希望降低风险和 review 成本,可以拆成:

  1. Gateway 公共类型、错误策略、路由与 trace 基础;
  2. 图像接入;
  3. 视频 submit/follow 状态机;
  4. Chat/质量判官接入;
  5. Ledger ORM 与持久化。

本次仅基于锁定提交 57aca58...180696d 做只读分析,未修改代码、未运行测试。

View job run

@minorcell

Copy link
Copy Markdown
Member

@xiaocheny214

如何测试的? 这类修改尽量在本地对着接口测试起来看看,或者写一些有质量的集成测试、e2e;对于大的改动我们要保持谨慎。

@minorcell

Copy link
Copy Markdown
Member

并起来看,这个其实是对架构对了比较大的改动;需要记录和评审的。

@fennoai

生成一个当前修改的系统业务架构图,可以使用 svg(svg 转 png,然后通过 gh 上传)

@fennoai

fennoai Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

架构图已生成

已按 PR #331 当前固定提交范围 57aca58...180696d 绘制系统业务架构图,覆盖:

  • 业务层:Orchestrator / ExecutorAI Engine / Quality Judge
  • Gateway 编排层:ImageGatewayVideoGatewayChatGatewayModelRegistry、路由候选、熔断、错误分类与策略
  • Provider Adapter 层:图像单次调用、视频 submit → follow → download、Chat normalized result
  • 上游调用:Video API、Image API、Chat API / Aggregator
  • 横切能力:request_idAttemptTrace、成本估算、Retry-After、ledger fail-open,以及仍由业务层负责的 QUOTA_*

架构图源文件和 PNG 已生成并提交到本地工作区:

  • docs/architecture/llm-gateway-architecture.svg
  • docs/architecture/llm-gateway-architecture.png
  • commit: 15083d9 docs: add llm gateway architecture diagram

发布状态

本运行环境尝试按 Fork PR 流程推送到 xiaocheny214/Windup,但 GitHub remote 无可用 push credentials,收到:fatal: could not read Username for 'https://github.com'。因此暂时无法生成 GitHub 附件 URL,也没有把未发布的 PNG 伪装成可访问链接。

拿到 Fork push 权限后,推送当前提交即可在 PR 中直接引用 PNG;SVG 同时保留为可编辑源文件。

View job run

@johnnyzhang-eng johnnyzhang-eng left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

重试从出图侧收到 Gateway 这条按 08-18 对齐的做法落地了,判官仍走 _post 自带的 429/52x 重试、出图一次一枪,两处没有留成两套。计费安全那条也在:policy 把 MAYBE_BILLED 直接判 FAIL,5xx 不会被重发。

@xiaocheny214

Copy link
Copy Markdown
Contributor Author

线上业务日志整理 + 测试对照

回应 @minorcell 的「如何测试」:这版 Gateway 除单元测试外,已经在 demo 环境跑过完整 出图 → 动作 i2v 链路(用户 3/4,入口 api.qnaigc.com,图像 gemini-2.5-flash-image,视频 kling-v2-5-turbo)。下面是 8/20 抽样,以及「合入前会红、合入后能扛」的对照。

业务形态固定:执行器按 num_images 连打 gen_image(预付 10 / 任务),再 i2v submit → poll(默认先睡 60s)→ download(预付 50)。Gateway 只管上游调用的分类 / 重试 / 不重复建单,积分仍走 QUOTA_*

抽样任务

任务 场景 Gateway 介入 结果
img-15 出图 ×2 无(主路 200) 成功,扣 10
act-16 idle i2v 第一次 POST 525,同路再 POST 一次 成功,只建一单,扣 50
act-11 / act-13 / act-20 / act-21 / act-24 / act-27 / act-29 动作 i2v 无(一次建单) 均成功
img-23 / img-25 / img-26 / img-30 出图 ×3 成功,每任务扣 10
img-22 出图 断连未进 Gateway(当时 adapter 把异常抛给执行器) 任务 FAILED,解冻 10

视频正常拆段:建单约 4–8s(act-27 异常慢到 37s)、轮询约 60–138s、下载多数 2–26s(act-11 拉 1.45MB 用了 131s,属下载层,不是策略层)。


实锤:act-16 挡住了一次 525

这是本 PR 要证明的核心场景。合入前这类 525 会直接让动作任务失败(或更糟:有人在补丁里盲目再 POST,可能双开单)。

  1. POST /v1/videos525 SSL Handshake Failed with Origin Server
  2. Gateway 记一条 failed attempt:
    • error_type=unreachedhttp_status=525
    • job_id=nullmaybe_billed=false
    • edge_fingerprint=server=TencentEdgeOne(停在边缘,没到 Kling 源站)
  3. 策略:UNREACHED + 无 job_id + retry_count=0RETRY_SAME(不换模型、不换 key、不开第二入口)
  4. 第二次 POST /v1/videos → 200,拿到 qvideo-1382244847-1787214047229930378
  5. 成功 trace:retry_count=1resend_spent=1fallback_used=falsemaybe_billed=true
  6. 后处理出 32 帧,积分 只扣 50 一次(余额 480→430)

时间线:失败 attempt 10.0s + 成功单 87.8s ≈ 总 97860ms。没有第二张 job_id,没有双扣。


对照:现在挡住了什么

故障 合入前(散落在 sufy.py / 执行器) 本 PR Gateway 测试锚点 线上证据
HTTP 525 / 522 / 521 / 523,且无 job_id 任务失败,或补丁叠乘重发、可能换模型空转 同路由再打 1 次;仍失败才熔断该 base_url,有备用入口才切 URL;不换型号 test_522_is_unreachedtest_522_retries_once_then_opens_aggregatortest_submit_522_retries_once_does_not_open_second_job_on_fallback_modeltest_522_retries_same_model_once_and_does_not_fallback act-16 已救活
对端拆连接、无 HTTP 状态行RemoteProtocolError: Server disconnected without sending a response 异常冒出 adapter,无 trace,任务直接 FAILED 收成 UNREACHED / maybe_billed=false,走同上同路重试(b40a890 test_remote_protocol_error_is_unreachedtest_submit_image_maps_disconnect_to_unreachedtest_submit_video_maps_disconnect_to_unreached img-22 当时还没吃到这版,任务红了;部署后应变成「先 failed unreached,再同路重试」
520 / 524 / 已可能计费的 5xx 有补丁,但和协议循环缠在一起 禁止重发、禁止换模型 test_520_and_524_are_maybe_billedtest_520_never_retries 本批日志未出现(策略按「宁可不打第二枪」)
视频已有 job_id 后再 52x / 超时 存在再 POST 第二单的风险 只跟这一单(GET/下载),不新开 job test_job_id_blocks_fallback_on_unreachedtest_poll_timeout_fails_without_new_job 本批成功单都是一单 job_id
429 Provider 内退避 同 key 重试,耗尽后 切同 URL 下一把 key,不是换模型 test_429_retries_twice_then_fallback_keytest_submit_429_switches_key_on_same_base_url 本批未出现
连续两次入口不可达 同一 AI_BASE_URL 上换模型也救不了 base_url 熔断,切备用 URL(若配置了) test_submit_522_switches_base_url_route_before_model_fallback(图/视频各一条) 本批 525 第一次就救回来了,没走到熔断

本地对应命令(backend):

python -m pytest tests/test_gateway_classify.py tests/test_gateway_policy.py tests/test_gateway_image.py tests/test_gateway_video.py tests/test_sufy_video_download.py -q

这批日志里 Gateway 没有、也不该管的

  • 轮询默认先睡 60s:poll_count=1~2 是粒度,不是失败重试。
  • CDN 下载慢 / 不完整 body 再 GET(act-11 131s、act-16 同一 mp4 GET 两次):下载层,不重新建单。
  • 死帧 / 成色(act-27 死帧 9/32):产线质量闸,不是路由。
  • user_id 在 trace 上经常是 null:executor bind_call_context 还没带上,排障靠 task_id
  • 预付按任务(图 10 / 动作 50),和厂商侧「出图连打 2~3 次 chat」不是同一口径。

一句话: 这版 Gateway 已经在真实链路上把「边缘 525、没建到单」从任务失败收成同路一次重发;act-16 是成功样本。断连无状态行是同类入口故障,策略已对齐,等 b40a890 部署后再用 img-22 那种日志验收。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

图/视频调用需要把重试、Fallback 和计费边界从 adapter 里抽出来

4 participants