ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Claude Code 实时费用监控:基于 DeepSeek 日志的分时计价状态栏实现

Claude Code 实时费用监控:基于 DeepSeek 日志的分时计价状态栏实现 月初打开 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-chat和deepseek-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, keylambda 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, encodingutf-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, keylambda 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 时间而错峰窗口是北京时间那么在 UTC8 的窗口边界上判断结果会整体偏移 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-chat和deepseek-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_write和cache_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 显示本会话费用还是今日费用状态栏空间有限显示什么信息需要取舍。我做了两个 scopesession和today。session统计当前 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_tokens和output_tokens都对但总数就是少。后来才想起cache_creation_input_tokens这种字段没进累计逻辑。Claude Code 的日志里缓存相关字段可能很长一段不出现在某条消息里只有在上下文快填满时才突然写入一批缓存。如果你只统计了普通输入输出前面大部分会话看着都对但某几个关键节点会把费用拉高一截。我在脚本里对这四个字段做了空值兜底全部default 0并且在测试脚本时专门构造了一条带大缓存字段的日志验证总费用会随之跳涨。这也提醒我任何和账单对不上的排查第一步都是确认四类 token 全部统计进来了。5.4 ccswitch 切换模型后价格串用我用 ccswitch 在deepseek-chat和deepseek-reasoner之间切换模型一开始脚本里的价格表只有一套日志里如果出现deepseek-reasoner的条目脚本会找不到对应的价格 key直接 fallback 到默认模型价格。结果就是推理模型的实际费用被低估了。修复方式是给calc_cost加上显式的 model 匹配匹配不到时不要沉默 fallback而是输出一条告警比如unknown-model。这样一旦发现价格表缺失你能立刻在状态栏里看到异常而不是被一个偏低的数字误导。另外ccswitch 切换模型后最好手动跑一次脚本确认当前日志里message.model字段已经变了因为有些情况下模型名写在别的位置比如环境变量里需要额外处理。最后分享一点我的实际体会这套状态栏计费脚本跑了一阵子之后我最大的收获不是省下了多少钱而是对 token 有了真正的体感。以前我完全不觉得让 Claude Code 反复读一个大仓库有什么问题现在状态栏上那个数字每刷新一次都在提醒我哦刚才那次目录扫描花了 3 分钱那几次大文件的重复读取花了 1 毛钱。这种即时反馈改变了我分配任务的方式我开始主动拆大文件、合并小任务、把批量任务挪到低谷时段再跑。这套脚本的完整版本大概两百行不到核心就是读日志、算价格、显示在状态栏。如果你也想做建议按这个顺序来先跑通日志解析和费用计算再用配置方式挂上状态栏最后再引入峰谷分时和模型价格矩阵。每一步都能独立验证出问题也好定位。踩坑的部分我都写在上面了照着绕开就行。
返回列表