跳到主要内容
mcpd
无守护进程
源码

成本核算

一个成本数字,只有在你说得出它是从哪来的时候才值得拥有。这一页讲的是从一个 token 计数到屏幕上一个数字之间的每一步。

只在摄取时定一次价

当一个 span 带着模型 id 和 token 计数到达时,mcpd 会在一份价目表里查这个模型, 算出这次调用花了多少钱,并把结果存在这个 span 上。读取时不会重算任何东西。

这是这一页上那个要紧的决定。价格是会变的。如果成本是在查询时、用那天恰好加载 着的那份价目表推出来的,那么下个月的一次降价就会悄悄改写上个月的花费,而你 报给某个人的那个数字,等他去核对时就不在那里了。在摄取时定价,把这个数字冻结 在这次调用发生时真实生效的费率上。每一份存下的成本还记录了产出它的那份价目表 的 id,所以一个数字可以回溯到它背后的费率。

一个本身已经带着成本的 span,会照它到达的样子原封不动地留下。如果你的供应商 给了你一个真实的数字,而你把它记了下来,mcpd 会保留你的,不会覆盖着定价。 is_estimated 标志把两者分开:凡是 mcpd 从价目表推出来的都标记为 估算,因为它确实是。

费率按每百万 token 计

价目表里的每一个费率都是每百万 token 多少个货币单位,各家供应商也都是这样 公布的。一个从定价页上抄下来的费率,可以原样写进价目表,没有一个凌晨三点会 除错的除法。一个计费项的算法是 tokens × rate ÷ 1,000,000。

成本记在步骤上,不记在运行上

每一次模型调用带着自己的数字。一次运行的合计是它那些 span 的和,而每当其中 一个 span 到达时,mcpd 都从这些 span 重新算一遍这个和,而不是给一个计数器加 数。span 会乱序到达、会被重试、会被就地更新,而一个累加的合计在这三种情况下 都会漂移。一个和它下面那些行对不上的头条数字,会毁掉这一页上其他每一个数字的 可信度。

Trace b3d9e71a Fault
0ms 2.1s 4.2s 6.3s 8.4s
  1. agent support_agent fail
  2. model chat
  3. tool returns_policy
  4. model chat
  5. tool returns_policy
  6. model chat
  7. tool returns_policy

The model called one tool three times and never answered. That is visible in four seconds here, and invisible in a log file.

上面那三次模型调用的定价是 $0.00015、$0.00018 和 $0.00021。表头里那次运行的 合计就是它们的和,别的什么也没有:那些工具调用、那次记忆查找和那道护栏没有 消耗 token,所以它们不带成本,也不贡献成本。

计费项

一次模型调用不是一个 token 数字。供应商会按不同的费率计好几个计费项,而一份 价目表为其中每一个都给出一个费率。

费率适用于价目表里没有它时
input 普通的输入 token。 必填。
output 普通的补全 token。 必填。
cache_read 从提示词缓存里读出来的 token。 按 input 计费。
cache_write 写进提示词缓存的 token。 按 input 计费。
cache_write_5m 写进五分钟缓存,用于供应商按 TTL 分别定价的场合。 退回到 cache_write,再退回到 input。
cache_write_1h 写进一小时缓存。 退回到 cache_write,再退回到 input。
audio_input 音频输入 token。 按 input 计费。
audio_output 音频输出 token。 按 output 计费。
reasoning 隐藏的推理 token,用于供应商分开计费的场合。 没有退路。它们作为输出合计的一部分按 output 计费,几家主要 供应商就是这么计的,除非供应商同时上报了一个单独的 text 计数。见下文。

一个缓存 token 只计一次费

把账单算高的经典办法,是把一个缓存 token 计两次费:一次按缓存费率,另一次 作为它本来就在里面的那个上报输入合计的一部分。所以当供应商上报了一份明细时, 以那份明细为准。mcpd 按各自的费率给每个计费项定价,只把余下的部分按普通输入 费率计费。

一次 claude-sonnet-4 调用上报了一百万个输入 token,其中九十万个 是从缓存里读的,对着内置的价目表定价是这样:

  • 900,000 次缓存读取,每百万 $0.30,即 $0.27。
  • 余下的 100,000 个输入 token,每百万 $3.00,即 $0.30。
  • 输入成本 $0.57,而不是单看上报合计会算出来的 $3.00。

当供应商给出一个明确的 text 计数时,就直接把这个计数当作余下部分。 当它没有给时,余下部分是上报合计减去每一个计费项字段。

这留下了一处值得点名的空档。一个价目表没有费率的计费项,只会作为余下部分的 一部分被计费,于是当供应商同时发来了一个 text 计数时,它根本 不会被计费。只要你的价目表没给推理 token 自己的费率,它们就落在这里,而声明 一个费率就能解决。图像 token 则永远落在这里,因为价目表没有图像费率可给, 所以在一个多模态很重的工作负载上,请把这个数字读作偏低,而不是精确。

一个未知模型是没有成本,不是成本为零

如果价目表没有这个模型的费率,mcpd 存下这个 span 时根本不带成本字段。它不会 存一个零。

零是一项主张:这次调用是免费的。缺席是一句承认:我们不知道这次调用花了多少。 两者导向不同的决定,而一个把后者报成前者的工具,是在账单上撒谎。同一条规则 贯穿读取路径:一次运行的那些 span 合计不出任何东西时,返回的是不带成本数字, 而不是成本为零,所以一个空白的成本列意味着未定价,绝不意味着 免费。

实际后果值得记牢:一个不在价目表里的模型,也不在任何合计里。它的花费不会出现 在 /v1/api/analytics/cost 里,它的那些运行会被任何一个为正的 min_cost 过滤条件排除掉。一个看着太低的周合计,通常是某个模型 id 没能解析出来,而不是这一周清闲。

解析一个模型 id

span 上的那个 id 是你的客户端发来的样子,而客户端会给它加装饰。一个网关会加上 它的厂商前缀,一个云会加上它的区域前缀,一个供应商会钉上一个发布日期。价目表 是按裸模型名做键的,所以查找会依次试四件事,在第一个命中处停下。

步骤例子
id 原样,做大小写折叠 GPT-4o 找到 gpt-4o
最后一个斜杠之后的部分 openai/gpt-4o 找到 gpt-4o
一个云 id 里点号后面的尾部 us.anthropic.claude-sonnet-4 找到 claude-sonnet-4
去掉尾部日期之后的 id,带前缀和不带前缀都试 openai/gpt-4o-2024-08-06 找到 gpt-4o

-YYYY-MM-DD 和 -YYYYMMDD 两种后缀都认得。四步全都 失败是一个正常的结果,不是一个错误:它意味着没有成本,如上所述。

随附的那份价目表

mcpd 带着一份小小的内置价目表,好让 docker run 在完全没有配置的 情况下也产出真实的数字。它叫 builtin,以 USD 计价,它产出的每一份 成本都记下这个名字。

模型输入输出缓存读取缓存写入,5 分钟缓存写入,1 小时
claude-opus-415.0075.001.5018.7530.00
claude-opus-4-115.0075.001.5018.7530.00
claude-opus-4-55.0025.000.506.2510.00
claude-sonnet-43.0015.000.303.756.00
claude-sonnet-4-53.0015.000.303.756.00
claude-haiku-4-51.005.000.101.252.00
claude-3-5-haiku0.804.000.081.001.60
gpt-4o2.5010.001.25按输入按输入
gpt-4o-mini0.150.600.075按输入按输入
gpt-4.12.008.000.50按输入按输入
gpt-4.1-mini0.401.600.10按输入按输入
o32.008.000.50按输入按输入
o4-mini1.104.400.275按输入按输入
gemini-2.5-pro1.2510.000.31按输入按输入
gemini-2.5-flash0.302.500.075按输入按输入
text-embedding-3-small0.020.00按输入按输入按输入
text-embedding-3-large0.130.00按输入按输入按输入

单位是每百万 token 多少美元,取自这份价目表上一次对着一份第三方模型目录核对时 的状态,价目表把那个日期记作它的 as_of。Anthropic 的条目带着两个 缓存写入费率,因为守护进程会从一小时的费率退回到五分钟的那个,而一份漏掉较长 那个的价目表会不声不响地少计费。OpenAI 写入它的缓存不收费,所以那里的一次缓存 写入按输入费率计。gemini-2.5-flash 还带着一个 1.00 的音频输入 费率;其余每一个条目都按它的文本费率给音频定价。没有 claude-haiku-4:Anthropic 提供的 id 是 claude-haiku-4-5,而一个解析不到任何东西的 id 会被留作未定价, 不去猜。

这些是公布的标价,不是你的价格。如果你谈下了自己的费率,或者你跑的模型不在这张 表里,那么 mcpd 给你看的数字就是一份估算,也应当照估算来读。这份价目表故意做得 很短:一份长的会在更多地方过时,也会给人一种超出它应得的准确印象。

提供你自己的价目表,目前还没有命令行参数。价目表是服务端状态的一部分,今天替换 它的办法是把 mcpd 作为库嵌入,并把一份价目表传给 AppState::with_price_book。一份价目表由一个 id、一个 ISO 4217 货币和一张从模型 id 到费率的映射构成,它产出的每一个数字都盖上了它的 id, 这样两份价目表事后能分辨开。同一个项目里的价格应当共用一种货币;mcpd 把一次 运行的那些 span 按算术相加,并不在货币之间换算。

把钱读回来

GET /v1/api/analytics/cost 回答一个窗口内钱花到哪去了,不用把 任何东西导出到电子表格。

curl -s 'http://localhost:4317/v1/api/analytics/cost?from=-7d&group_by=model'
参数含义
from、to 窗口。RFC 3339 格式,或者相对时间的简写。默认是到现在为止的最近七天。
group_by 默认是 model。还可以是 provider、 entity_name、environment、 status、session_id、user_id、 service_name 或 name。
bucket 30s、5m、1h 或 1d。 不写它就只要合计;写上它就在合计之外再得到一条时间序列。
project 读哪个项目。默认是守护进程的默认项目。

钱花到哪去了

# 上一周的钱花在哪了,按模型分。
curl -s --get http://localhost:4317/v1/api/analytics/cost \
  --data-urlencode 'from=-7d' \
  --data-urlencode 'group_by=model'

# 同一周,做成按天的序列,好看出是哪一天变的。
curl -s --get http://localhost:4317/v1/api/analytics/cost \
  --data-urlencode 'from=-7d' \
  --data-urlencode 'group_by=model' \
  --data-urlencode 'bucket=1d'

分组按从大到小返回,最多一百行,而一条分桶的序列最多五千个点。

响应

{
  "from": "2026-08-20T09:00:00Z",
  "to": "2026-08-27T09:00:00Z",
  "currency": "USD",
  "total_cost": 12.418264,
  "groups": [
    {
      "key": "claude-sonnet-4",
      "count": 1841,
      "error_count": 12,
      "total_cost": 9.612055,
      "input_tokens": 2903118,
      "output_tokens": 214477,
      "latency": { "mean": 1840.2, "max": 24193.0 }
    },
    {
      "key": "gpt-4o-mini",
      "count": 6204,
      "error_count": 3,
      "total_cost": 2.806209,
      "input_tokens": 9114882,
      "output_tokens": 388310,
      "latency": { "mean": 612.4, "max": 8801.0 }
    }
  ]
}

count 上有一处微妙的地方。按 model、 provider 或 entity_name 分组读的是那些 span,所以 count 是模型调用的次数。按其他任何东西分组读的是运行投影,所以 count 是运行的次数。两者都有用;它们不是同一个数字,而拿其中一个 去和另一个比,是这里最容易犯的错。

这份响应里按分组给出的延迟只带一个均值和一个最大值。分位数来自 GET /v1/api/analytics/summary,它在这个窗口上算出它们。

接下来去哪

成本是运行列表里的一列,而运行列表正是一次排查真正开始的地方: 查找一次运行讲了 min_cost 和按花费排序。如果你的数字看着不对,是因为载荷在被定价 之前就被丢掉了,那就看 采样,是它决定了什么会被 记录下来。这一页上写到的每一个字段,都列在 数据模型里。