图像生成为什么需要独立的 Gateway 抽象:参数、重试与幂等设计

0 阅读10分钟

图像生成为什么需要独立的 Gateway 抽象:参数、重试与幂等设计

图01

开篇:一个 generateImage(),可能不是一次供应商请求

调用方看到一次 generateImage(),供应商侧却未必只收到一次请求。AI SDK 的官方文档明确说明:当 n 超过模型单次生成上限时,SDK 会自动拆成多次并行调用;API 还提供 maxRetries,当前参考页给出的默认值是 2。一个看似原子的 SDK 调用,已经可能展开成多次有独立费用和失败状态的供应商 Attempt。

图像生成还多了尺寸、宽高比、参考图、mask、Seed、质量、风格、输出格式和二进制产物。若统一层只做字段重命名,它会在最危险的位置制造“看似兼容”:参数被忽略、重试重复计费、fallback 输出不符合用户约束,取消按钮也可能只停止等待而没有停止供应商执行。

AI SDK 的 generateImage 展示了统一入口的价值,也明确保留 providerOptions、size/aspectRatio 互斥能力、seed、mask、abortSignal 和多调用拆分。官方文档一个生产级自有网关应延续这个原则:统一共同语义,同时把一次用户意图展开后的每个 Attempt 和 Artifact 暴露为可审计对象。

核心结论

图02

  1. 用户 Job、供应商 Attempt 和图片 Artifact 是三个不同实体,幂等键也应分层。
  2. Canonical request 只应包含跨模型语义稳定的字段,特殊能力放入 provider options。
  3. 路由前必须做 capability negotiation,不能把不支持参数静默丢弃。
  4. 自动重试必须区分“确定未执行”“执行状态未知”和“已完成响应丢失”。
  5. Cost Ledger 记录每次供应商 Attempt,而不是只记录最终展示给用户的图。

一、三层对象模型

图03 图04

这条链路不是一对一:一个 User Image Job 可以展开成多个 Provider Attempt;每个 Attempt 又可以产生一个或多个 Artifact。审核、转码和缩略图会继续派生新 Artifact,最终只有被选中的对象进入交付集合。

Image Job

用户意图的稳定对象:prompt、编辑输入、期望数量、能力要求、预算、保留策略、租户和业务幂等键。

Provider Attempt

一次实际供应商请求:模型、映射后参数、请求 ID、开始/结束、状态、错误、计费、fallback 原因。

Artifact

任何二进制产物:供应商输出、原图、审核图、缩略图、水印图和最终交付图。每个 Artifact 有哈希、MIME、尺寸、存储位置、来源 Attempt、派生父对象和删除状态。

将三者混成一张 generations 表,会导致重试后无法解释哪个账单对应哪个图片。

网关至少要为每次调用返回一份内部执行回执:

{
  "job_id": "img_job_01",
  "attempts": [
    {
      "attempt_id": "att_01",
      "trigger": "initial",
      "provider": "provider-a",
      "credential_type": "byok",
      "status": "unknown_after_send",
      "provider_request_id": "req_xxx",
      "estimated_cost_usd": 0.04
    },
    {
      "attempt_id": "att_02",
      "trigger": "manual_retry_after_reconciliation",
      "provider": "provider-b",
      "credential_type": "system",
      "status": "succeeded",
      "artifact_ids": ["art_01"]
    }
  ],
  "delivery_artifact_ids": ["art_02"]
}

这份回执不是为了把内部实现暴露给终端用户,而是让客服、账单、删除任务和事故复盘能够回答:发送了几次、谁实际执行、用了谁的凭证、哪些图片被保存或交付。

二、Canonical Request

export type ImageJobRequest = {
  userRequestId: string;
  prompt: string;
  count: number;
  operation: 'generate' | 'edit' | 'variation';
  inputArtifacts?: string[];
  maskArtifact?: string;
  output: ({
    aspectRatio: string;
    width?: never;
    height?: never;
  } | {
    aspectRatio?: never;
    width: number;
    height: number;
  }) & {
    format?: 'png' | 'jpeg' | 'webp';
    transparent?: boolean;
  };
  reproducibility?: {
    seed?: number;
    strict?: boolean;
  };
  policy: {
    maxCostUsd: number;
    allowedModels?: string[];
    artifactRetention: 'ephemeral' | 'standard' | 'custom';
    inferenceDataPolicy: 'required_zdr' | 'disallow_training' | 'standard';
  };
  providerOptions?: Record<string, unknown>;
};

字段必须分三种:

  • Required semantic:供应商不支持就不能路由;
  • Preferred semantic:可降级,但要返回 warning;
  • Provider-specific:只对指定 adapter 有效。

例如 transparent=true 若是产品承诺,应标 required;如果只是偏好,可 fallback 到后处理抠图,但这会改变成本与质量,必须写入 plan。

三、Capability Registry

{
  "model": "provider/model-version",
  "operations": ["generate", "edit"],
  "sizes": {"mode": "aspect_ratio", "values": ["1:1", "16:9", "9:16"]},
  "supports_seed": false,
  "supports_mask": "unknown",
  "output_delivery": "inline_binary",
  "inference_data_policy": {
    "gateway_zdr_eligible": "unknown",
    "byok_contract_status": "unknown"
  },
  "pricing": {
    "type": "per_image",
    "amount": "PRICE_FROM_VERIFIED_SOURCE",
    "source": "MODEL_PAGE_OR_MODELS_API",
    "as_of": "QUERY_TIME"
  },
  "registry_version": "2026-08-31"
}

示例字段只是网关内部格式;具体 capability 必须从官方模型文档确认。unknownfalse 要分开:未知不能被自动当成不支持或支持。Vercel 在 2026 年 4 月新增团队级与请求级 ZDR,可用 zeroDataRetention: true 让 Gateway 只选择符合要求的 provider,并在响应 metadata 中留下过滤轨迹;这意味着应用不必继续手写每个系统凭证 route 的 ZDR 表。

但 ZDR 仍不能被压成模型布尔值。团队/请求策略、Gateway 当前 provider 资格、BYOK 合同和应用自己的 Artifact 保留策略是不同层。特别是 BYOK 协议由用户持有,Gateway 无法自动替你证明其保留条款。Capability Registry 应记录可验证的策略输入与执行回执,而不是复制一个静态 supports_zdr=true

路由步骤:

  1. 校验请求;
  2. 根据 required capabilities 过滤;
  3. 根据 inference data policy、Gateway 执行回执、BYOK 合同、区域和供应商 allowlist 过滤;
  4. 估算成本与延迟;
  5. 按质量/成本/可用性排序;
  6. 生成参数映射计划;
  7. 返回 warnings;
  8. 创建 Attempt 并发送。

四、参数映射不能静默

CanonicalProvider AProvider B处理
aspectRatio=16:9aspect_ratiosize=1536x864可映射,记录实际尺寸
seed=42支持不支持strict 时拒绝;非 strict 警告
count=4单次最多 1单次最多 4A 拆 4 次 Attempt
format=webpPNG onlyWebPA 生成后转码,新 Artifact
edit+mask支持不支持不能 fallback 到 B

AI SDK 文档指出 n 可能被自动拆成多次请求,maxImagesPerCall 还能改变拆分方式,正说明“一个 SDK 调用”与“一个供应商请求”不同。成本和幂等必须按 Attempt 记录,并固定 SDK 与 adapter 版本,否则升级依赖也可能改变 Attempt 数量。

五、幂等分三层

Business Idempotency

用户重复点击或客户端重试,不应创建第二个 Job:

tenant + operation + user_request_id → image_job_id

Attempt Idempotency

若供应商支持原生 idempotency key,使用 image_job_id + attempt_index。若不支持,网关只能避免自己重复发送,无法保证网络未知状态下供应商没有执行。

Artifact Deduplication

对返回二进制计算内容哈希,避免同一响应被多次保存;但不同生成结果即使 prompt 相同也不应按请求哈希去重,因为随机性是产品行为。

六、重试状态机

CREATED
→ SENT
→ ACKNOWLEDGED
→ SUCCEEDED
→ ARTIFACT_STORED

失败可分:

  • PRE_SEND_FAILURE:确定未到供应商,可安全重试;
  • REJECTED:参数/政策错误,不应原样重试;
  • PROVIDER_FAILED:供应商明确失败,按策略 fallback;
  • UNKNOWN_AFTER_SEND:已发送但无结果,可能计费和生成;
  • RESPONSE_RECEIVED_STORE_FAILED:已有图片,不应重新生成,应重试存储。

UNKNOWN_AFTER_SEND 是关键。自动重试前应:查询供应商状态(若有)、等待宽限期、检查 provider request ID、核对成本预算,并在产品允许时要求用户确认。

这里还要区分三类“超时”:客户端 abortSignal、SDK 自身重试策略,以及 Gateway/provider timeout。Vercel 当前 provider timeout 文档针对 BYOK,并以“开始响应前”的时限触发 failover;官方同时提醒部分 provider 不支持取消,超时请求仍可能计费。该文档的示例是流式文本,不能直接证明图像 provider 具有相同取消语义,但它足以证明一个通用边界:停止等待不等于远端没有执行。图像网关必须把 timeout 后状态标记为未知,除非 provider 给出确定终态。

七、Fallback 合同

图05

Vercel AI Gateway 已支持模型级 fallback,并会按 models 数组依次尝试。官方 Changelog 进一步说明,unsupported input、context limit 和 provider outage 等错误都可能触发 fallback。对文本可用的“先成功者返回”策略,放到图像任务上仍需应用层收紧:编辑能力、参考图数量、尺寸、透明背景、合规策略和结果许可不一致时,技术成功不等于产品兼容。

因此 Fallback 计划应在发送前生成:

{
  "primary": "model-a",
  "fallbacks": [
    {
      "model": "model-b",
      "allowed_on": ["provider_unavailable", "rate_limited"],
      "not_allowed_on": ["safety_rejection", "invalid_prompt"],
      "degradations": ["seed_not_supported", "max_resolution_lower"],
      "requires_user_consent": false
    }
  ]
}

安全拒绝不能自动换模型绕过。ZDR、训练限制或 BYOK 合同不满足时,也不能只因可用性进入 fallback。编辑任务、参考图和人脸一致性通常需要更严格的模型绑定。若使用 Gateway 原生 models fallback,应用应只把已经通过同一 Required Capability Contract 的模型放进数组,并从 provider metadata 回收实际 Attempt 链;否则应关闭自动模型 fallback,由自有状态机逐次发送。

八、核心数据表

image_jobs

  • id、tenant、request hash、status;
  • canonical request;
  • requested count;
  • budget、artifact retention policy、inference data policy;
  • final artifact IDs;
  • created/finished/cancelled。

provider_attempts

  • job ID、attempt index;
  • provider/model、credential type;
  • mapped request;
  • SDK/adapter version、provider request ID;
  • status/error;
  • started/ended;
  • reported cost/estimated cost;
  • retry/fallback reason、Gateway routing metadata。

artifacts

  • source attempt、parent artifact;
  • storage URI(内部引用,不给永久公开 URL);
  • SHA-256、MIME、尺寸、字节;
  • kind:raw/thumbnail/moderated/delivered;
  • artifact retention/deletion state。

moderation_results

  • input/output;
  • policy/model version;
  • labels/scores;
  • decision;
  • human review。

cost_ledger

  • attempt、charge type、amount、currency;
  • estimated/reported/reconciled;
  • provider invoice reference;
  • tenant/user/product attribution。

九、取消的真实含义

图06 图07

用户点击取消后:

  • 如果尚未发送,终止且不计生成成本;
  • 已发送但供应商不可取消,只能标记 cancel_requested,结果到达后删除或不交付;
  • 已生成并计费,取消不一定退款;
  • 派生存储和审核任务仍需停止;
  • 所有状态必须对用户透明。

不要把 UI 的“取消”写成“供应商已停止执行”,除非有确认。

十、可观测性

图08

每个 Job 至少监控:

  • queue delay;
  • provider latency;
  • artifact storage latency;
  • moderation latency;
  • delivery latency;
  • attempts/job;
  • unknown-after-send rate;
  • duplicate artifact rate;
  • cost/requested image 与 cost/delivered image;
  • fallback rate;
  • policy rejection;
  • deletion SLA。

“每张交付图片成本”比“API 单价”更接近业务现实。

风险与限制

网关能力目录会过期,供应商可能改变默认参数、ZDR 资格和审核政策。参数映射可能看似成功却产生质量退化。自建网关增加运维、合规和账单对账成本。统一抽象应保持可逃生:保存原始 provider metadata 和 adapter 版本,允许关键模型走原生路径。

本文没有运行真实图像请求,也没有验证某个 provider 对 abort、timeout、幂等键或查询任务状态的具体支持。UNKNOWN_AFTER_SEND 是保守的应用状态设计,不是对任一供应商内部实现的断言。

结论

图09

图像网关的正确抽象不是一个万能 generate(),而是 Job、Attempt 和 Artifact 的分层状态机。共同语义可以统一,特殊能力必须显式;SDK 拆分、重试和 Gateway fallback 都要展开成 Attempt;timeout 后必须认识未知执行状态;成本按 Attempt 对账。做到这些,统一入口才不会把供应商差异变成隐形故障。

参考资料