返回博客

Agent 生图生视频的钱算在哪条账上?怡途Claw 媒体生成用量 API 查询实战

2026年8月3日阅读约 39 分钟
怡途Claw 媒体生成用量 API 实战

让 Agent 生图生视频的钱算在哪条账上?本文用怡途Claw 的 /api/app/billing/media-usage 接口,把媒体生成用量的月度统计、明细查询、计费单位、透支扣费和幂等去重串成一条线,附 token 轨道与媒体轨道的对照。

让 Agent 生图生视频的钱,到底算在哪条账上?本文用怡途Claw 的 /api/app/billing/media-usage 这组接口,把媒体生成用量的月度统计、明细查询、计费单位、透支扣费和幂等去重串成一条线,附 token 轨道和媒体轨道的对照。

前面讲了 token 用量怎么查(301 那篇计费查询)、实例怎么启停(306)、登录怎么拿 token(307)、资料怎么改和注销(308)、支付怎么下单(309)。但这组 /api/app/billing/* 里还藏着一条容易被忽略的账:图片和视频生成的用量。它和 301 讲的 token 用量不是一回事——301 是"大模型对话烧了多少 token",按输入/输出 token 计;这篇是"Agent 生成图、生成视频烧了多少次/秒/张",按媒体单位计。两条轨道各自记账、各自查明细,但扣的是同一个余额。

为什么媒体用量要单独算

很多团队接进来才发现:让 Agent 调 Fal 生成一张图、调 Runway 生成一段视频,这笔成本跟大模型推理的 token 成本结构完全不同。

  • 大模型推理:按 input/output token 计费,单价通常是"元/千 token",几分钱到几毛钱一千。
  • 图片生成:多数按计费(unitType=images),少数图像模型按 token 计;一张图几毛到几块。
  • 视频生成:按videos)或按时长秒数seconds)计,一段视频成本可能是图片的几十倍。

如果混在 token 账里,你根本看不出"这个月那 800 块到底是聊天聊出来的,还是生成视频烧出来的"。所以怡途Claw 把媒体生成拆成独立的 media_usage_record 表,走 MediaUsageService 单独采集、单独扣费、单独查询。接口挂在 app/billing 下,和 301 那组 token 接口在同一个 controller(app-billing.controller.ts),但背后是两套 service。

实际接进来你会碰到这些问题

  • 我让 Agent 生成了一组图,这笔钱算在哪?跟 token 账是混在一起还是分开?
  • 图片按张算、视频按秒算、还有按"次"算的,到底看哪个字段?
  • 这个月生成了多少图、多少视频、花了多少钱,一个接口能拿到吗?
  • 想查某个实例上周三生成的视频各自多少钱、用的哪个模型,怎么筛?
  • 余额快见底了,生成还会成功吗?会不会把余额扣成负数?
  • 同一次生成被采集两遍怎么办?接口里有去重吗?
  • rawUsagemetadataproviderRef 这些字段存了啥,跟供应商对账时怎么看?

下面逐个拆。所有接口都要带 Authorization: Bearer <token>(307 讲过怎么拿)。

先对齐两个枚举

查之前得把两个枚举对上号,不然返回字段看不懂。

usageType —— 生成的媒体类型,二选一:

  • image 图片
  • video 视频

unitType —— 计费单位,五选一:

含义典型场景
tokens按 token极少数图像模型按 token 计
images按张图片生成(默认)
videos按段视频生成(默认)
seconds按秒视频按时长计
jobs按次/任务一次生成任务算一次

注意 usageTypeunitType 是两个维度:前者说"生成了啥",后者说"按啥单位算钱"。一条图片记录通常是 usageType=image, unitType=images;一段按时长计的视频是 usageType=video, unitType=seconds。代码里 unitType 默认值是 jobs,但定价解析时会按 usageType 兜底——图片默认 images、视频默认 videos,所以实际很少落到 jobs,除非供应商配置里明确写了。

月度总览:GET /api/app/billing/media-usage/stats

想知道"这个月媒体生成花了多少、环比上月涨没涨",调这个。它有个别名 media-stats,两个路径返回一模一样。

curl "https://www.yitukeji.cn/api/app/billing/media-usage/stats" \
  -H "Authorization: Bearer <你的token>"

返回:

{
  "success": true,
  "data": {
    "currentMonthCost": 12.6,
    "lastMonthCost": 3.2,
    "totalCost": 47.85,
    "currentMonthImageCount": 38,
    "currentMonthVideoCount": 2,
    "currentMonthRecordCount": 22
  }
}

几个要点:

  • 三个成本字段是三档对比:currentMonthCost 本月、lastMonthCost 上月、totalCost 历史。看环比直接拿前两个除一下。
  • currentMonthImageCount / currentMonthVideoCount 是本月生成的图片张数和视频段数。注意它是把每条记录的 outputUnits(没有就取 totalUnits)累加起来的——所以如果某条视频记录的 unitType=seconds,它累加的是秒数不是段数,这个数要结合 unitType 看,别想当然当成"段数"。
  • currentMonthRecordCount 是本月记录条数,不是图片张数。一次生成任务可能产生一条记录,也可能按文件拆成多条。记录条数和张数对不上是正常的。
  • "本月"是按自然月切的(每月 1 号 00:00 到月底),服务端用 new Date(年, 月, 1) 算的,跟你的时区设置有关。

明细列表:GET /api/app/billing/media-usage

总览觉得偏多了,就调明细定位到具体哪几条。支持分页和四个筛选维度。

curl "https://www.yitukeji.cn/api/app/billing/media-usage?page=1&pageSize=20&usageType=video&instanceId=inst_xxx&startDate=2026-07-01&endDate=2026-07-31" \
  -H "Authorization: Bearer <你的token>"

参数:

参数默认说明
page1页码
pageSize20每页条数
startDate-起始日期(含)
endDate-结束日期(含,自动补到当天 23:59:59.999)
instanceId-按实例筛
usageType-imagevideo

返回:

{
  "success": true,
  "data": {
    "data": [
      {
        "id": "rec_a1b2",
        "instanceId": "inst_xxx",
        "messageId": "msg_ddd",
        "sessionId": "sess_eee",
        "providerId": "prov_fal",
        "providerRef": "fal",
        "modelName": "flux-pro",
        "usageType": "image",
        "unitType": "images",
        "inputUnits": 0,
        "outputUnits": 4,
        "totalUnits": 4,
        "cost": 1.2,
        "currency": "CNY",
        "requestId": "req_abc",
        "rawUsage": { "prompt_tokens": 120, "total_tokens": 120 },
        "metadata": {
          "source": "manifest",
          "manifestPath": "/home/node/.openclaw/app-files/outbox/.media-usage/req_abc.json",
          "chargeStatus": "charged"
        },
        "createdAt": "2026-07-15T08:30:00.000Z",
        "updatedAt": "2026-07-15T08:30:00.000Z"
      }
    ],
    "pagination": {
      "page": 1,
      "pageSize": 20,
      "total": 22,
      "totalPages": 2,
      "hasNext": true,
      "hasPrev": false
    }
  }
}

几个要点:

  • endDate2026-07-31 时,服务端会把它补成 2026-07-31T23:59:59.999Z,所以"7 月 31 号当天生成的"是包含的,不会因为只传日期漏掉当天下午的记录。这是常见的"传日期当区间右端"的坑,这里帮你填了。
  • 结果按 createdAt 倒序,最新的在前。
  • usageType=video 只返回视频记录,配合 instanceId 能精准定位"某个实例生成的视频"。
  • pagination.hasNext / hasPrev 直接告诉你有没有下一页,不用自己算 page < totalPages

字段怎么读:从代码看每条记录存了啥

这条记录里的字段,基本就是排查计费争议的全部线索。对着 MediaUsageService 的代码逐个说:

  • usageType / unitType:生成类型和计费单位。对账时先确认 unitType 跟供应商的计价单位一致——供应商按秒收钱,你这里记成 videos 按段,账就对不上。
  • inputUnits / outputUnits / totalUnits:用量。图片通常 outputUnits=张数inputUnits=0;按 token 计的模型 inputUnits 才有值。
  • cost:这条记录扣的钱,单位元。currency 默认 CNY
  • providerRef / modelName:哪个供应商、哪个模型。providerRef 是供应商标识(如 falrunway),modelName 是模型 ID(如 flux-pro)。对账时拿这俩去供应商后台找对应订单。
  • requestId:本次生成的请求 ID。幂等去重的主键之一,也是跟供应商对账的关联键。
  • rawUsage:模型返回的原始用量数据原样存下来了(如 prompt_tokenscompletion_tokenstotal_tokens)。供应商说"我们记了你 150 token",你拿这个字段对。
  • metadata.source:这条记录的计费来源,两种:
    • manifest:从容器的 usage manifest 文件读出来的(最准)。
    • generated-files:没有 manifest,按生成的文件数兜底算的(粗略)。
  • metadata.chargeStatus:有没有真扣到钱,两种:
    • charged:扣了。
    • skipped_insufficient_balance:余额不够,记录照存但没扣钱。这个字段最关键,见下一节。

余额扣减与透支:为什么有记录却没扣钱

这是接媒体计费最容易困惑的点:明明 media-usage 里有一条记录、cost 也写了 1.2,但余额没变。

代码里的逻辑(persistMediaUsageRecord):

allowOverdraft = 1.0  // 允许透支 1 元
shouldCharge = (当前余额 - 本次成本) >= -1.0

也就是说:

  • 余额够,或者只透支 1 元以内 → 正常扣,metadata.chargeStatus = charged
  • 透支超过 1 元 → 记录照常写入数据库,但 metadata.chargeStatus = skipped_insufficient_balance不扣余额,只打一条 warn 日志。

这意味着:余额见底时,生成请求在模型供应商那边可能还是成功的(平台侧没拦),但平台这边没扣到钱,账上留一条 skipped 记录。所以对账时如果发现"生成了但余额没动",先查 metadata.chargeStatus,十有八九是 skipped_insufficient_balance。这种情况下要么补扣,要么接受这笔是"漏扣",别拿余额对不上来找 bug。

幂等去重:同一次生成不会算两遍

媒体用量不是调用接口时实时记的,是 Agent 生成完之后,平台扫描容器里的 usage manifest 文件异步采集的(captureForAssistantMessage)。扫描可能重跑,去重就靠这两个键:

  1. 先按 requestId 查重——manifest 里带 requestId 的,同 ID 只记一次。
  2. requestId 没有就按 sourceKey 查重——sourceKey 是记录来源的指纹:
    • manifest 来源:manifest:<文件路径>
    • 文件兜底来源:fallback:<消息ID>:<image|video>

所以哪怕扫描跑了两遍、容器里 manifest 文件还在,也不会重复记账。排查"这笔是不是算重了"时,看 requestIdsourceKey 有没有重复就行。

计费是怎么算出来的

最后补一笔成本计算逻辑(resolveMediaPricing + calculateTotalCost),方便你理解 cost 字段的来历:

  1. 找单价:读供应商配置里的 mediaPricing 树,先找模型级定价(mediaPricing.image.models.<modelId>),找不到退到 mediaPricing.image.default,再退到全局 default
  2. 单价字段优先级totalPricePerUnitpricePerUnitpricePerImagepricePerVideopricePerSecondpricePerJob,先命中谁用谁。
  3. 算钱
    • 有总价单价(totalPricePerUnit):cost = totalUnits × totalPricePerUnit
    • 没有总价单价但有分项单价:cost = inputPricePerUnit × inputUnits + outputPricePerUnit × outputUnits

所以同一张图,供应商配成"0.3 元/张"和配成"inputPrice 0、outputPrice 0.3/张",算出来一样,但走的分支不同。对账时如果数对不上,先看供应商配置用的是哪个单价字段。

串一条排查线

把上面串起来,遇到"媒体生成账对不上"时按这个顺序查:

  1. media-usage/stats 看本月总览,确认 currentMonthCost 量级对不对。
  2. 量级偏大就调 media-usage,按 instanceId + 日期段筛,定位是哪个实例哪几天烧的。
  3. 看每条的 unitType,确认计费单位跟供应商一致(别把秒当段)。
  4. metadata.chargeStatus,区分"扣了"和"skipped 没扣"——别把 skipped 的也当成已扣。
  5. providerRef + modelName + requestId + rawUsage 去供应商后台对原始订单。

这套和 301 那篇的 token 查询是平行的两条线:301 管"对话烧的 token",这篇管"生成烧的媒体"。两边的 cost 加起来,再减掉 309 那篇支付下单的充值,就是你账户余额的真实去向。