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_tokens | non-cached text input | 55 |
cached_tokens_details.text_tokens | cached text input | 64 |
input_token_details.audio_tokens - cached_tokens_details.audio_tokens | non-cached audio input | 46 |
cached_tokens_details.audio_tokens | cached audio input | 64 |
output_token_details.text_tokens | text output | 117 |
output_token_details.audio_tokens | audio output | 420 |
不同 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 展示的通常是经过计量管道处理后的聚合数据,并非请求级日志。核对时至少要考虑以下几项因素:
-
统计时间不等于会话时间
Cost Management 中的
UsageDateTime、入账时间和实际请求时间可能不一致。对于跨 UTC 日期的请求,记录尤其容易落到相邻日期。 -
记录会按 meter 聚合
门户图表可能按资源、meter、日期或其他维度合并数据,因此不能证明某个数量只来自一个 WebSocket session。
-
计量单位可能经过缩放
meter 名称中的
1M Tokens表示计价单位。导出字段可能记录 token、千 token,或者百万 token 的小数。只看门户格式化后的显示值,无法确定原始 token 数。 -
缓存分类可能延迟或出错
API 已经明确返回 cached token,但账单记录没有进入 cached meter。这至少说明 API telemetry 与 billing telemetry 的分类没有对齐。
-
预览模型或新模型可能存在计量问题
某个具体模型版本是否存在已知问题,需要 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-1transcription。
这四组实验可以分别确认:
- 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.doneJSON; - 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 问题处理,而不是在客户端代码中增加补偿系数。
备注:内容仅供参考。
