PAYATHON 2026

Azure OpenAI Realtime API 用量与计费数据不一致

支付阿杰

结论

response.done.usage 是这次模型响应的 token 用量明细,但不能直接拿它与 Azure Cost Management 中某一天、某个部署的 meter quantity 做逐请求等值比较。两者的统计层级、入账时间和聚合方式都不同。

不过,你给出的差异已经明显超出正常的入账延迟或小幅修正范围:

  • API 返回了 cached input,但两个 cached meter 都是 0;
  • audio input 从 110 变成了 640;
  • 四个主 meter 的数值都系统性偏高。

如果已经排除了同一资源、部署和订阅中的其他流量,那么问题更可能出在 Azure 的计量归类、导出单位或服务端计费遥测上。“隐藏 token”“音频分帧开销”或 Whisper 转写都无法合理解释这组差异。建议先核对 Azure Cost Details/Usage Details 的原始记录,再提交 Azure 支持工单。

还有一点很容易被忽略:cached tokens 是 input tokens 的子集,不能重复相加。

这组数据应按下面的方式理解:

text input total       = 119
cached text input      = 64
uncached text input    = 119 - 64 = 55

audio input total      = 110
cached audio input     = 64
uncached audio input   = 110 - 64 = 46

不能计算成 119 + 64 和 110 + 64。

response.done 与 meter 的合理映射

如果 Azure 没有针对这个模型版本发布特殊的计量规则,合理的映射关系应当是:

response.done 字段预期计费类别本次值
input_token_details.text_tokens - cached_tokens_details.text_tokensnon-cached text input55
cached_tokens_details.text_tokenscached text input64
input_token_details.audio_tokens - cached_tokens_details.audio_tokensnon-cached audio input46
cached_tokens_details.audio_tokenscached audio input64
output_token_details.text_tokenstext output117
output_token_details.audio_tokensaudio output420

不同 SDK 可能使用 PascalCase 属性名,但字段语义相同。

如果 Azure 的 non-cached meter 只记录未缓存部分,应该比较的数值是 55 和 46。如果当前计费实现暂时把缓存部分也计入普通 input meter,数值最多也只会接近 119 和 110。无论按哪种方式解释,都推导不出 357 和 640。

TotalTokenCount 之间的关系是:

229 + 537 = 766
119 + 110 = 229
117 + 420 = 537

可见,API 返回的内部分类本身是自洽的。

Whisper 转写不应这样并入 Realtime meter

启用以下配置:

AudioTranscriptionOptions = new RealtimeAudioTranscriptionOptions
{
    Model = "whisper-1",
}

会建立一条独立的输入音频转写路径。转写结果主要用于向客户端提供文本,并不等于将转写文本再次作为主 Realtime 模型的 text output。

因此,不能默认作出以下判断:

  • Whisper 处理的输入音频会再次计入 gpt rt aud mn in;
  • Whisper 生成的转写文本会计入 gpt rt txt mn out;
  • 转写用量会出现在主响应的 response.done.usage 中。

如果转写需要单独收费,原则上应该记入相应的语音转写计费项,而不是作为 Realtime 模型的输入或输出 token。具体 meter 名称和收费方式可能随区域、API 版本和预览状态变化,应以该订阅的 Cost Details 原始记录及当期 Azure 定价说明为准。

事件中的以下字段:

"usage": {
  "type": "duration",
  "seconds": 0
}

也不能证明这次转写一定免费。seconds: 0 可能来自服务端未填充、SDK 映射问题,或者该用量字段尚未完整实现。无论是哪种情况,它都解释不了 Realtime meter 中多出的数百个 token。

可以进行一次对照实验:完全移除 AudioTranscriptionOptions,保持输入文件、模型、voice 和其他参数不变。如果 Realtime meter 仍有相同的异常增量,基本可以排除 Whisper 转写的影响。

不应把差额归因于隐藏 chain-of-thought

默认 instructions、协议上下文和服务端控制信息可能参与模型执行,但这不代表 Azure 会向客户收取一批没有在 usage 中披露的隐藏 token。

具体来说:

  • 不应假定 internal chain-of-thought 会作为 text output 计费;
  • PCM/WebSocket 分帧开销不属于模型的 audio token;
  • 每 100 ms 调用一次 SendInputAudioAsync,不会仅仅因为分片较多就产生数倍 token;
  • VAD 控制事件和失败的空 buffer commit,不应自动产生一次完整的模型用量。

如果服务确实会对某类额外 token 收费,Azure 应提供明确的计费口径,或者在账单明细中体现这部分用量。在没有公开规则的情况下,不能用“隐藏 token”解释账单差异。

为什么 Cost Management 不能直接用于单会话核对

Azure Cost Management 展示的通常是经过计量管道处理后的聚合数据,并非请求级日志。核对时至少要考虑以下几项因素:

  1. 统计时间不等于会话时间

    Cost Management 中的 UsageDateTime、入账时间和实际请求时间可能不一致。对于跨 UTC 日期的请求,记录尤其容易落到相邻日期。

  2. 记录会按 meter 聚合

    门户图表可能按资源、meter、日期或其他维度合并数据,因此不能证明某个数量只来自一个 WebSocket session。

  3. 计量单位可能经过缩放

    meter 名称中的 1M Tokens 表示计价单位。导出字段可能记录 token、千 token,或者百万 token 的小数。只看门户格式化后的显示值,无法确定原始 token 数。

  4. 缓存分类可能延迟或出错

    API 已经明确返回 cached token,但账单记录没有进入 cached meter。这至少说明 API telemetry 与 billing telemetry 的分类没有对齐。

  5. 预览模型或新模型可能存在计量问题

    某个具体模型版本是否存在已知问题,需要 Azure 支持结合资源后台的计量记录确认,无法仅凭客户端事件判断。

建议的核对步骤

1. 导出 Cost Details 原始数据

不要只查看 Cost Management 图表。应导出包含以下字段的明细:

Date
UsageDateTime
ResourceId
ResourceLocation
MeterId
MeterName
MeterCategory
MeterSubCategory
ProductName
ServiceName
Quantity
UnitOfMeasure
EffectivePrice
CostInBillingCurrency
AdditionalInfo
Tags

重点检查以下内容:

  • Quantity 的原始小数值;
  • UnitOfMeasure;
  • 相邻的 UTC 日期;
  • 是否存在名称相似但 MeterId 不同的记录;
  • 筛选条件是否精确到了具体 deployment,而不只是 Azure OpenAI resource;
  • cached meter 是否在次日或更晚补记。

如果 Quantity 以百万 token 为单位,例如:

0.000420 × 1,000,000 = 420 tokens

需要先换算单位,再与 API 返回值比较。

2. 计算扣除缓存后的预期计费值

var usage = responseDone.Response.Usage;

long textInputTotal = usage.InputTokenDetails.TextTokenCount;
long audioInputTotal = usage.InputTokenDetails.AudioTokenCount;

long cachedText =
    usage.InputTokenDetails.CachedTokenDetails.TextTokenCount;
long cachedAudio =
    usage.InputTokenDetails.CachedTokenDetails.AudioTokenCount;

long uncachedText = textInputTotal - cachedText;
long uncachedAudio = audioInputTotal - cachedAudio;

Console.WriteLine($"Text input total:      {textInputTotal}");
Console.WriteLine($"Text input uncached:   {uncachedText}");
Console.WriteLine($"Text input cached:     {cachedText}");
Console.WriteLine($"Audio input total:     {audioInputTotal}");
Console.WriteLine($"Audio input uncached:  {uncachedAudio}");
Console.WriteLine($"Audio input cached:    {cachedAudio}");
Console.WriteLine(
    $"Text output:           {usage.OutputTokenDetails.TextTokenCount}");
Console.WriteLine(
    $"Audio output:          {usage.OutputTokenDetails.AudioTokenCount}");

还要防止服务端或 SDK 返回不完整的字段:

if (cachedText > textInputTotal || cachedAudio > audioInputTotal)
{
    throw new InvalidOperationException(
        "Cached input token count exceeds total input token count.");
}

3. 进行四组隔离实验

每组实验使用新的部署,或者在足够长的独立计量窗口内执行:

  • 文本输入、文本输出;
  • 音频输入、文本输出;
  • 音频输入、音频输出,并关闭 transcription;
  • 音频输入、音频输出,同时启用 whisper-1 transcription。

这四组实验可以分别确认:

  • audio meter 是否只在音频模式下异常;
  • transcription 是否会产生额外的计费记录;
  • cached meter 是否一直为 0;
  • 异常是否仅发生在 gpt-realtime-mini-2025-12-15。

每次只运行一个 session,等 Cost Details 完整入账后再开始下一组。只按“自然日”隔离并不稳妥,最好使用独立 deployment、resource,或者不同的 tag/订阅边界。

4. 保存服务端原始事件和请求标识

除了 response.done,还应保存:

  • response ID;
  • conversation/session ID;
  • Azure 返回的 request ID 或 trace ID;
  • WebSocket 建连和关闭时间,并统一记录为 UTC;
  • deployment name、model name 和 API version;
  • 原始 response.done JSON;
  • Cost Details 中的 MeterId、Quantity 和 UsageDateTime。

不要只保存 SDK 反序列化后的属性。支持团队可能需要检查原始 JSON 字段是否被 SDK 错误映射。

提交 Azure 支持工单时应明确指出

可以将问题整理为下面这组可验证的事实:

API usage:
- Text input total: 119
- Cached text input: 64
- Expected uncached text input: 55
- Audio input total: 110
- Cached audio input: 64
- Expected uncached audio input: 46
- Text output: 117
- Audio output: 420

Billing records:
- Non-cached text input: 357
- Cached text input: 0
- Non-cached audio input: 640
- Cached audio input: 0
- Text output: 181
- Audio output: 631

请支持团队确认:

  • 每个 MeterId 分别对应哪个 usage 字段;
  • input meter 记录的是 total input,还是 total - cached;
  • cached tokens 是否因为该区域或模型的计量配置,被计入普通 meter;
  • transcription 是否产生了独立计费记录;
  • response.done 与后台计费遥测之间是否存在已知差异;
  • 能否根据 request/session 时间和资源后台日志,重新计算该会话的用量。

注意事项

response.done.usage 不是根据音频时长得出的估算值,而是服务端为该响应提供的 token usage。它适合用于应用侧监控和成本预估,最终账单则以 Azure Cost Management 为准。两者可能因为聚合方式和入账时间出现暂时差异,但不应长期存在这种数量级和分类上的全面冲突。

目前这组数据无法合理完成“对账”。应先确认 Cost Details 的单位、UTC 时间窗口和资源筛选条件。如果原始明细仍然是 640、631、357、181,且 cached meter 仍为 0,就应将其作为疑似 Azure billing telemetry/meter mapping 问题处理,而不是在客户端代码中增加补偿系数。

备注:内容仅供参考。