Claude Code 实时费用监控:基于 DeepSeek 日志的分时计价状态栏实现
2026/9/19 0:12:43 网站建设 项目流程

月初打开 DeepSeek 开放平台的消费明细,我盯着那行总额愣了好一会儿。上周末我让 Claude Code 跑了一个"顺手就能做"的批量重构任务,从晚上十点挂到凌晨两点,账单上的数字够我买两周菜。问题不在这次任务本身,而在于整个过程中我完全不知道它在烧钱——Claude Code 的体验太顺滑了,顺滑到你根本意识不到每一次工具调用、每一次上下文读取都在产生费用。

于是我做了一个小项目:给 Claude Code 的状态栏加上实时花费显示,并且针对 DeepSeek 的峰谷分时计价做了适配。低谷时段跑的 token 自动按折扣价估算,高峰时段则显示原价,这样我一眼就能看到当前这个会话到底花了多少钱,以及如果前面这堆活放到凌晨跑能省多少。这篇文章就是这次实战的完整记录,包含数据怎么拿、费用怎么算、状态栏怎么接,以及我踩过的那些坑。如果你也在用 Claude Code 接 DeepSeek 控制成本,这份代码可以直接抄。

1. 账单焦虑:为什么我决定在状态栏常驻一个花钱速度表

1.1 在终端里跑 agent,钱是怎么悄悄溜走的

Claude Code 这类终端 AI 编程工具和普通的 Chat 界面有个本质区别:它会自己决定下一步做什么。处理一个中型仓库的改动,它可能先扫描目录结构,再读几个关键文件,调用编辑工具改完代码,再跑一遍测试验证结果。每一步在界面上可能就是一行日志,但背后是几千甚至上万 token 的进出。

最容易被低估的是缓存命中和上下文重读。同一个会话里,Claude Code 会在多次工具调用之间反复带上之前的对话历史和文件内容;如果你给的任务涉及多个文件,它可能在每个步骤都重新读取一次大文件。这些 token 的量级往往比最终的"回答"大一个数量级,而大多数人在界面里只看得到模型输出的那几行字。

我用过一阵子没接计费的状态栏,说白了就是"闭眼坐出租车,到站才知道多少钱"。这种模式在任务量小的时候感受不到问题,一旦你开始让 agent 跑批量任务、夜间无人值守、或者一口气开多个会话,月底账单会给你一个措手不及的惊喜。

1.2 DeepSeek 的峰谷计价,值不值得专门为它做一套逻辑

DeepSeek 开放平台有错峰优惠机制,低谷时段的价格和高峰时段差距明显。这里先说一个原则:具体优惠时段和折扣系数以官方计价页面为准,不要拿任何第三方文章里的数字当圣旨。我们做这套脚本的目的就是把这些动态价格变成可配置的参数,官方一调价,改一行配置就能跟上。

我当时算了一笔账:假设一个批量任务每月消耗 500 万输入 token 和 50 万输出 token,按公开的示例价格粗算,高峰期跑和低谷期跑的费用差得不是一星半点。如果你本身就在深夜维护任务,或者愿意把大批量、低交互的任务挪到凌晨跑,一套"分时计价"逻辑能省下的钱远超写脚本的投入。

这个算账过程让我意识到,单纯显示总费用是不够的。我得知道"此刻这个 token 是多少钱一毛",才能判断"现在跑这个任务到底划不划算"。所以我决定把峰谷计价也做进状态栏脚本里,让一个数字同时回答两个问题:花了多少,以及按什么时候的价格在花。

2. 计费数据的准确来源:从 Claude Code 日志里捞 token 消耗

2.1 为什么我选择日志解析,而不是搭本地代理劫持请求

给 Claude Code 做计费,第一个要解决的问题是:数据从哪来。

方案 A 是在本地搭一个代理层,把 Claude Code 发出的 API 请求转发给 DeepSeek,在中间把响应体里的 usage 字段摘出来。这个方案数据最精确,还能实时拿到每次调用后的 token 消耗。但它的问题也很明显:需要额外维护一个代理进程,处理环境变量、端口、证书,Claude Code 升级后可能还要重新配置。对于只想要一个"状态栏数字"的人来说,这个成本有点重了。

方案 B 是直接读 Claude Code 端侧落盘的会话日志。Claude Code 会把每次会话完整记录在本地 JSONL 文件里,每一条消息都带 usage 信息。脚本每次刷新时读一次最近的有更新的日志文件,把 token 字段解析出来累加就行。这个方案零侵入、不需要改任何网络链路,唯一的代价是"实时性"要依赖日志落盘的延迟。

我最后选了方案 B。原因是状态栏本来就不是精确到毫秒的仪表,它的刷新周期在秒级,而日志落盘延迟远小于这个时间,用户感知不到差别。与其维护一个随时可能被 Claude Code 版本更新搞坏的代理,不如读一个稳定的本地文件。

2.2 一个 JSON 行里藏着四类 token,每种单价不一样

打开~/.claude/projects/目录,你会看到按项目组织的 JSONL 文件。每个 JSONL 行可能代表一条用户消息、一条助手消息、一次工具调用等等。我们关心的助手消息长这样:

{ "type": "assistant", "timestamp": "2025-06-01T10:15:30Z", "message": { "model": "deepseek-chat", "usage": { "input_tokens": 1280, "output_tokens": 540, "cache_creation_input_tokens": 4200, "cache_read_input_tokens": 18500 } } }

这四个字段就是计费的"四笔账":

  • input_tokens:本次请求里没有命中缓存、也没有写入缓存的那部分输入 token。它按正常的输入单价算。
  • output_tokens:模型生成输出的 token 数。通常这是四类里单价最高的,尤其是 DeepSeek 的推理模型,输出里还可能包含思维链 token,数量往往会膨胀。
  • cache_creation_input_tokens:本次请求中写入缓存的输入 token。写入缓存的单价一般低于普通输入,但高于命中缓存。
  • cache_read_input_tokens:命中了之前缓存的那部分输入 token。这是最便宜的一类,也是长会话里成本差异的核心来源。

如果你的日志里还有system_tokens之类的字段,不用管它,Claude API 的标准口径里,计费只按上面四类走。拿到这四个数以后,分别乘以对应模型的分时单价,再除以一百万,就能得到一次请求的人民币费用。

有一点需要提醒:DeepSeek 的不同模型(deepseek-chatdeepseek-reasoner)价格不完全一样,日志里message.model字段会标明本次调用用了哪个模型。脚本要做的是按模型选择对应的价格表,而不是所有模型套一个价格。

3. 状态栏计费脚本实现:分时计价引擎与 statusline 接入

3.1 从日志读 token 的最小实现

我先写了一个只算累计费用的版本,验证"读日志 + 算钱"的链路是否通畅。核心逻辑不复杂:找到最近有写入的 JSONL 文件,逐行解析出usage,按当前模型价格累加。下面是去掉了很多边界处理后的骨架:

import json from pathlib import Path from datetime import datetime LOG_ROOT = Path.home() / ".claude" / "projects" def find_recent_session_file() -> Path | None: if not LOG_ROOT.exists(): return None candidates = list(LOG_ROOT.rglob("*.jsonl")) if not candidates: return None return max(candidates, key=lambda p: p.stat().st_mtime) def parse_usage(line: str) -> dict | None: try: obj = json.loads(line) except Exception: return None msg = obj.get("message") or {} if not isinstance(msg, dict): return None usage = msg.get("usage") or {} if not usage: return None return { "input": usage.get("input_tokens", 0), "output": usage.get("output_tokens", 0), "cache_read": usage.get("cache_read_input_tokens", 0), "cache_write": usage.get("cache_creation_input_tokens", 0), "model": msg.get("model") or "deepseek-chat", } def calc_cost(row: dict, price: dict) -> float: return ( row["input"] * price["input"] + row["output"] * price["output"] + row["cache_read"] * price["cache_read"] + row["cache_write"] * price["cache_write"] ) / 1_000_000 session_file = find_recent_session_file() if session_file is None: print("¥0.0000 | no session") else: total = 0.0 with session_file.open("r", encoding="utf-8") as f: for line in f: row = parse_usage(line) if row: price = PRICING.get(row["model"], PRICING["deepseek-chat"]) total += calc_cost(row, price) print(f"¥{total:.4f} | {session_file.name}")

这段代码有几点很关键。一是用max(candidates, key=lambda p: p.stat().st_mtime)选文件,因为状态栏脚本运行时,当前正在写入的那个会话文件通常是 mtime 最新的,这样就不需要精确计算项目目录的哈希映射。二是parse_usage里对 JSON 解析失败要静默跳过,因为日志写入不是原子的,脚本完全可能在文件半写状态时读到不完整的行,抛一个异常会让整个状态栏报错。三是先用/ 1_000_000统一转换单位,避免后面价格配置里每个字段都带六个零。

3.2 峰谷时段判断:跨天与时区一个都不能错

DeepSeek 的错峰优惠有明确的时间窗口。我第一版脚本天真地用了一个简单比较,后来发现两个隐藏问题:一是跨天时段,二是时区。

跨天的问题很好理解。如果低谷时段是23:00 - 07:00,直接用start <= now < end判断,在凌晨 1 点就会判断失败,因为now已经不在start之后、end之前的区间里了。DeepSeek 的错峰窗口如果是从凌晨开始、到清晨结束,不跨天;但既然做的是可配置系统,就不能假设别人不会填一个跨天的窗口。

时区的问题更隐蔽。如果你的服务器或开发机用的是 UTC 时间,而错峰窗口是北京时间,那么在 UTC+8 的窗口边界上,判断结果会整体偏移 8 小时。脚本跑在本地时其实还好,因为本地时区通常就是业务时区;但只要你想让它稳定,就应该在脚本里固定用Asia/Shanghai判断峰谷,而不是依赖系统默认时区。

最终我用的判断函数是这样的:

from datetime import datetime, time as dtime from zoneinfo import ZoneInfo TZ = ZoneInfo("Asia/Shanghai") OFFPEAK_START = dtime(0, 30) # 以官方公告为准 OFFPEAK_END = dtime(8, 30) OFFPEAK_RATE = 0.5 # 低谷折扣系数,同样以官方为准 def peak_factor(now: datetime) -> float: t = now.astimezone(TZ).time() if OFFPEAK_START <= t < OFFPEAK_END: return OFFPEAK_RATE return 1.0

如果以后要支持跨天窗口,比如23:00 - 07:00,需要把判断改成处理start > end的情况。一个常见的写法是:如果start <= end,用区间判断;否则判断t >= start or t < end。这样一套逻辑就能覆盖所有窗口配置。

3.3 价格矩阵配置:把 deepseek-chat 和 deepseek-reasoner 分开

DeepSeek 目前最常用的两个模型是deepseek-chatdeepseek-reasoner。它们的价格策略不一样,推理模型的输出价格通常更高,因为思维链会占掉大量输出 token。我把价格放在脚本顶部的字典里,方便随时改:

PRICING = { "deepseek-chat": { "input": 2.0, # 元 / 百万 tokens,示例值 "output": 8.0, "cache_read": 0.5, "cache_write": 2.0, }, "deepseek-reasoner": { "input": 4.0, "output": 16.0, "cache_read": 1.0, "cache_write": 4.0, }, }

这些数字是我当时用的示例值,不是官方实时报价。你动手之前一定要打开 DeepSeek 官网的计价页面核对,把最新的价格填进去。这也解释了为什么我把价格矩阵做成纯配置项:官方调价是常态,写死在代码逻辑里每次都要翻代码,做成字典后改一行就行。

还有一个容易被忽略的点:cache_writecache_read的单价可能随时间调整,但不管怎么调,它们一定低于普通输入单价。如果你的价格表里发现缓存比普通输入还贵,那多半是版本记错了,回去重新查一下。

3.4 挂进 Claude Code 状态栏的两种方式

脚本本身能输出一行费用文本之后,剩下的问题是怎么让 Claude Code 把它放到状态栏。

Claude Code 支持自定义状态栏命令,实现方式大同小异:一种是直接在会话里输入/statusline,按提示把执行命令填进去;另一种是通过配置文件持久化设置。我现在用的是配置方式:

claude config set -g statusLine "python3 /path/to/claude_cost_statusline.py --scope session"

如果你在某个版本里发现这个配置键不生效,先跑一下claude config list看看当前版本的状态栏配置项到底叫什么,不同版本对这个键的命名可能有差异。我第一次配置时就因为照抄了旧教程,键名不对,状态栏迟迟没反应。

配置完成后,状态栏会按预设周期去执行那条命令,并把命令的标准输出显示在底部。这意味着脚本的执行速度直接关系到终端的手感——如果脚本跑 500 毫秒,你的状态栏刷新就会卡顿。所以脚本里要避免任何网络请求,只读本地文件,并且控制单次解析的量级。

这里给一个性能优化的小技巧:如果某个会话的 JSONL 已经累积到几十 MB,每次全量读取会很吃力。可以在脚本里记住上一次读取到的文件偏移量,之后每次用seek从偏移量继续读,只解析新增的部分。对于大多数日常项目,全量读也够用,但我建议大日志用户做增量改造,否则状态栏会越来越迟钝。

3.5 显示"本会话费用"还是"今日费用"

状态栏空间有限,显示什么信息需要取舍。我做了两个 scope:sessiontodaysession统计当前 JSONL 文件里的全部 usage,代表这个会话从开始到现在的总费用;today则过滤出当天时间戳的条目,代表今天的消耗。

对日常使用来说,session模式最直观,因为你可以盯着看一次任务从开始到结束花了多少钱。today模式适合那些开着多个会话同时干活的人,但它的统计口径依赖日志时间戳,如果日志里没有准确的时间字段,数值会偏小。我的建议是状态栏主用session,今天的总账留给 DeepSeek 开放平台去算,毕竟那个是最终账单。

4. 实测验证:一次重构任务的花费曲线与账单偏差

4.1 测试任务设计

脚本写完不能只看输出格式对不对,得用一个真实任务验证数值是否合理。我选了一个小仓库,里面有几个 Python 文件,任务描述很简单:"清理这些文件里没用的 import。"

按照我之前的使用习惯,这个任务会让 Claude Code 先列出目录结构,再依次打开每个文件分析 import 情况,最后用编辑工具逐个修改。整个过程中会有大量文件读取和上下文携带,是一个能代表典型 agent 工作负载的测试。

跑之前我先估算了一下预期费用:假设读入 12 万 token、输出 4 万 token,再加上缓存读写,按deepseek-chat价格算,总费用应该在几毛钱量级。这个估算不是为了精确预测,而是为了后续核对时不至于被离谱的误差带偏。

4.2 状态栏数字和开放平台账单的差距

任务跑完后,状态栏显示的费用和我预期的量级一致。把它和 DeepSeek 开放平台的账单明细对比,有三点发现:

  • 日志里统计的 token 总数与平台账单的 token 总数基本一致,偏差主要来自日志写入时的边界截断,通常不超过 1%。
  • 金额偏差主要来自价格参数。我脚本里配置的价格是"示例值",而平台账单按实时价格走,如果官方在任务执行期间调过价,误差就会体现出来。解决方式很简单:以平台账单为准,手动校准脚本里的价格矩阵。
  • 缓存相关的偏差最大。同一个会话里,Claude Code 可能多次读取相同内容,但第一次是cache_write,后面是cache_read,如果脚本漏算了某一类 token,累计费用会明显偏低。

我在实测中给状态栏加了一个"校准偏差"的说明:如果脚本金额和平台账单长期差太多,优先检查价格矩阵里的缓存价格,其次是检查是否最新写入的 JSONL 文件里有跨会话的日志混进来了。读完日志之后用wc -l看一下行数,再和平台账单的请求次数对比,就能快速定位问题。

5. 踩坑实录:状态栏不刷新、价格对不上、切换模型后串账

5.1 状态栏一直显示 ¥0.0000:当前会话定位失败

第一个让我抓狂的问题是:脚本在命令行手动执行能正常输出费用,但一挂到状态栏就永远显示¥0.0000。检查后发现,状态栏执行命令时的工作目录和我在终端里手动执行时不一样,导致find_recent_session_file()扫到的 JSONL 文件并不是当前会话。

这不是脚本逻辑问题,是环境差异问题。解决方案是别再依赖"当前目录"或"最近修改时间"这种隐含假设,而是在脚本里增加一个显式的--log-dir参数,或者从 Claude Code 给 statusline 命令注入的环境变量里拿项目路径。如果你不想改脚本,也可以在配置状态栏命令时写成cd /path/to/project && python3 ...,强制工作目录一致。印象里当时困扰了我一个晚上,最后看到状态栏终于跳出数字的时候,说实话成就感不亚于任务本身跑通。

5.2 峰谷边界判断漂移:时区不一致

有一次我无意中发现,状态栏上的金额在晚上 8 点半左右突然打了个折,但我配置的低谷开始时间是 0:30,怎么也想不通哪里出问题了。后来才意识到,脚本跑在一台 UTC 时区的开发机上,而我在价格配置里写的是北京时间。

这类问题特别隐蔽,因为脚本单独测试时你根本不会注意时区,只有到了某个边界时刻它的行为才会异常。我的建议是:峰谷判断里硬编码业务时区,不要用datetime.now()直接取系统时间。毕竟你要对齐的是 DeepSeek 官方计费的时间口径,而不是你机器上的时间。

5.3 缓存 read/write 口径混乱导致账单对不上

排查"脚本金额比平台账单低"的问题时,我反复确认过input_tokensoutput_tokens都对,但总数就是少。后来才想起cache_creation_input_tokens这种字段没进累计逻辑。

Claude Code 的日志里,缓存相关字段可能很长一段不出现在某条消息里,只有在上下文快填满时才突然写入一批缓存。如果你只统计了普通输入输出,前面大部分会话看着都对,但某几个关键节点会把费用拉高一截。我在脚本里对这四个字段做了空值兜底,全部default 0,并且在测试脚本时专门构造了一条带大缓存字段的日志,验证总费用会随之跳涨。这也提醒我:任何"和账单对不上"的排查,第一步都是确认四类 token 全部统计进来了。

5.4 ccswitch 切换模型后价格串用

我用 ccswitch 在deepseek-chatdeepseek-reasoner之间切换模型,一开始脚本里的价格表只有一套,日志里如果出现deepseek-reasoner的条目,脚本会找不到对应的价格 key,直接 fallback 到默认模型价格。结果就是推理模型的实际费用被低估了。

修复方式是给calc_cost加上显式的 model 匹配,匹配不到时不要沉默 fallback,而是输出一条告警,比如"unknown-model"。这样一旦发现价格表缺失,你能立刻在状态栏里看到异常,而不是被一个偏低的数字误导。另外,ccswitch 切换模型后,最好手动跑一次脚本确认当前日志里message.model字段已经变了,因为有些情况下模型名写在别的位置,比如环境变量里,需要额外处理。

最后分享一点我的实际体会

这套状态栏计费脚本跑了一阵子之后,我最大的收获不是省下了多少钱,而是对 token 有了真正的"体感"。以前我完全不觉得让 Claude Code 反复读一个大仓库有什么问题,现在状态栏上那个数字每刷新一次都在提醒我:哦,刚才那次目录扫描花了 3 分钱,那几次大文件的重复读取花了 1 毛钱。这种即时反馈改变了我分配任务的方式,我开始主动拆大文件、合并小任务、把批量任务挪到低谷时段再跑。

这套脚本的完整版本大概两百行不到,核心就是读日志、算价格、显示在状态栏。如果你也想做,建议按这个顺序来:先跑通日志解析和费用计算,再用配置方式挂上状态栏,最后再引入峰谷分时和模型价格矩阵。每一步都能独立验证,出问题也好定位。踩坑的部分我都写在上面了,照着绕开就行。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询