Mac菜单栏实时显示LLM Token消耗:基于SwiftBar的轻量监控方案 📅 发布时间:2026/9/2 12:07:44 👁 浏览次数: 在开始做 AI 应用之后你迟早会面对一个问题这个月到底花了多少 LLM 费用查官方控制台页面加载慢数据延迟一两天而且只能看到账号级汇总看不到哪个项目、哪个用户在烧钱。于是很多开发者习惯用 Excel 记账或者靠月底账单到了再肉疼。这显然不是好的工程体验。这篇文章要讲的是在 Mac 菜单栏里直接显示 LLM usage 的一种实现方式通过一个轻量扩展以 panel面板、pill药丸、nub凸起三种形态呈现今日 token 消耗、累计调用量以及你关心的成本估算。我给出的核心判断是这个需求表面上是写一个菜单栏小工具真正重要的其实是“使用量数据的采集链路”。UI 只是最后 10% 的工作数据怎么统一记录、按时间聚合、跨模型汇总才是决定你这个工具好不好用的关键。读完这篇文章你会得到一套不依赖某个特定云厂商、可以自己扩展的 LLM 使用量监控方案先在应用层记录每次调用的 token 消耗再用 SwiftBar Python 脚本把它变成一个漂亮的 Mac 菜单栏扩展。全文围绕可落地的代码展开你照做就能在本地跑通。1. 为什么要在 Mac 菜单栏显示 LLM 使用量很多开发者第一次接触 LLM最关心的是“这个模型能不能完成我的任务”很少会第一时间关注 token 消耗。但一旦应用进入测试期、身边同事开始大量试用问题就会迅速暴露一个联调环境一天跑出几千次请求token 成本悄悄累积某个测试用户反复触发长上下文重试费用比核心业务还高上线后没有监控告警直到云厂商账单出来才发现异常调用。这时候如果能在开发机上常驻一个菜单栏小程序实时看到“今天消耗了多少 token、最近 1 小时是否出现调用尖峰”很多问题就能提前被发现。还有一个更现实的场景团队内部共用一个模型 API Key或者一个人同时维护多个项目。你想知道这个月预算还剩多少每个项目用了多少。官方后台给你的往往是总量不拆项目、不拆用户。你在自己的应用层做一次 usage 记录才能得到真正可解释的数据。所以这篇文章关注的不只是 Mac 菜单栏 UI而是完整的“LLM usage 观测链路”。菜单栏扩展只是它的呈现终端核心是数据采集和聚合逻辑。从技术选型上说macOS 菜单栏扩展的生态已经很成熟不需要为一个小需求去写一个完整 SwiftUI App。SwiftBar 这类工具允许你用一段 Python 脚本快速实现菜单栏插件支持定时刷新、下拉菜单、点击动作足够覆盖绝大多数场景。2. LLM Usage 是什么从 token 到费用要做出一个能真实反映成本的菜单栏扩展首先要理解 LLM usage 的数据结构。以最常用的 OpenAI 接口为例一次普通对话请求的响应中通常带有usage字段{ model: gpt-4o-mini, usage: { prompt_tokens: 128, completion_tokens: 256, total_tokens: 384 } }其中prompt_tokens输入给模型的 token 数量包含系统提示词、历史对话、工具定义等completion_tokens模型生成的 token 数量total_tokens两者之和。Anthropic 的 API 响应也类似只是字段名可能不同。你在自己应用中记录 usage 时不需要写死某个厂商的字段只需要把“输入 token 数”和“输出 token 数”两个数字提取出来统一存到本地。Token 数量直观但费用不能只看 token。不同模型的价格差异很大同一个模型的输入和输出价格也不同。所以一个完整的用量统计还应该包含模型名称调用发生的时间请求所属的项目或业务线输入 token 和输出 token可选本次调用的估算成本。这里有一个很容易被忽略的点从云厂商的 Usage API 拉数据通常只能拿到账号维度的汇总拿不到应用内部的字段。而自己记录则天然拥有“调用请求的全貌”。如果你想做多模型、多项目、多用户维度的分析本地计量是更合理的选择。因此本文的方案不依赖某个厂商的专用 Usage API而是在应用层加一个 usage 日志钩子。每次调用 LLM 成功后把响应中的 usage 写进本地文件。菜单栏扩展再定期读取这个文件按天、按模型、按项目聚合展示。事实上这个思路和许多 LLM 应用框架的埋点设计是相通的。你在框架层统一记录 token 消耗后续做成本核算、限额控制、异常告警时都能基于同一份数据。3. 方案选型panel、pill 还是 nubmacOS 菜单栏扩展的形态通常有三种形态特点适合场景panel点击后展开一个信息面板显示完整统计需要看到详细数据、趋势、项目列表pill常态显示一段文本像一个胶囊标签只关心今日 token、费用这几个关键数字nub很小的圆点或图标几乎不占空间只需要一个状态信号例如正常、异常、超额从工程角度看这三种形态不是割裂的。菜单栏第一行显示的是什么形态由你的脚本输出内容决定如果你只输出一个圆点就是 nub输出一行“Token 12.3K”就是 pill下拉菜单里的各项信息则可以理解为一个 panel。SwiftBar 的插件机制很适合实现这三种形态。插件脚本最核心的规则是第一行输出会显示在菜单栏上第二行开始的内容属于下拉菜单---表示菜单分隔线支持href、bash、refresh等参数实现点击动作。因此你完全可以先写一个返回“药丸”样式的脚本让它常态显示今日 token 数再在菜单里补充一个完整的面板视图展示按项目拆分的明细。选择 SwiftBar 而不是从零写一个原生 App原因很简单开发成本低一段 Python 脚本即可完成不需要 Xcode 编译迭代快改完脚本立刻刷新不用重新打包门槛低团队里的同学都能维护生态成熟SwiftBar 本身就是开源项目社区有大量现成插件可参考。瓶颈在于 Python 脚本的执行频率。建议设置 5 到 30 秒刷新一次既能保持数据新鲜又不会带来明显的性能问题。4. 环境准备与前置条件开始写代码之前需要准备好这些环境。4.1 操作系统与运行时macOS 12 及以上版本新版 macOS 对菜单栏应用的权限管理更严格Python 3.9 以上版本macOS 自带python3但版本可能不是最新建议用brew install python安装SwiftBar 最新版本。如果你还没有安装 SwiftBar可以使用 Homebrewbrew install --cask swiftbar安装完成后打开 SwiftBar第一次启动会提示设置插件目录。通常默认目录是~/.swiftbar/plugins/SwiftBar 会自动扫描这个目录下可执行脚本并按照文件名中的刷新频率参数运行。4.2 插件文件命名规则SwiftBar 通过文件名后缀决定刷新频率。例如llm-usage.5s.py # 每 5 秒执行一次 llm-usage.30s.py # 每 30 秒执行一次 llm-usage.1m.py # 每 1 分钟执行一次建议大家使用.30s.py或.1m.py没必要用 5 秒。菜单栏扩展追求的是“看一眼就知道状态”不是实时仪表盘。刷新太频繁反而会增加磁盘 IO 和脚本调度开销。4.3 文件目录规划建议单独创建一个数据目录mkdir -p ~/.llm_usage chmod 700 ~/.llm_usage这个目录用于存放 usage 记录文件。权限设置为700避免其他本地用户读取到你记录的数据。你还需要确保插件脚本有执行权限chmod x ~/.swiftbar/plugins/llm-usage.30s.py如果没有执行权限SwiftBar 会拒绝运行这个插件。4.4 关于 API Key 的安全提醒这套方案本质上不在菜单栏里保存任何密钥。你的应用代码读取 API Key 时应该通过环境变量或系统的钥匙串来管理而不是硬编码在 Python 文件里。你可以写一个.env文件但千万别把sk-开头的密钥提交到 Git 仓库。如果团队有多个成员可以约定每人维护自己本地的环境变量避免密钥互相传到代码库。5. 在应用层采集并记录 LLM usage这是整个方案里最关键的一步把你的 LLM 调用日志统一记录到本地。5.1 设计统一的 usage 记录模块为了让多个项目共用同一套统计我们把记录逻辑放在一个独立模块usage_logger.py里。它支持向本地 JSONL 文件追加一条 usage 记录。#!/usr/bin/env python3 # 文件usage_logger.py import json import os import time USAGE_DIR os.path.expanduser(~/.llm_usage) USAGE_FILE os.path.join(USAGE_DIR, usage.jsonl) def ensure_dir(): os.makedirs(USAGE_DIR, mode0o700, exist_okTrue) def log_usage(model, prompt_tokens, completion_tokens, projectdefault, extraNone): 记录一次 LLM 调用的 token 消耗。 :param model: 模型名称例如 gpt-4o-mini :param prompt_tokens: 输入 token 数 :param completion_tokens: 输出 token 数 :param project: 项目标识方便按项目聚合 :param extra: 额外自定义字段例如用户ID、请求ID ensure_dir() record { timestamp: time.strftime(%Y-%m-%dT%H:%M:%S%z), model: model, prompt_tokens: prompt_tokens, completion_tokens: completion_tokens, project: project, } if extra: record[extra] extra with open(USAGE_FILE, a, encodingutf-8) as f: f.write(json.dumps(record, ensure_asciiFalse) \n)这段代码的逻辑很简单把一次调用的元信息序列化成 JSON然后以追加模式写入 JSONL 文件。JSONL 的优点是写入成本低、方便按行解析也方便后续用grep等工具排查问题。5.2 在调用 LLM 后接入记录逻辑假设你已经在项目中通过 OpenAI Python SDK 调用模型接入方式如下# 文件your_app.py import os from openai import OpenAI from usage_logger import log_usage client OpenAI(api_keyos.environ[OPENAI_API_KEY]) response client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: 你是一个技术助手}, {role: user, content: 介绍一下 Python 的 with 语句} ] ) usage response.usage log_usage( modelresponse.model, prompt_tokensusage.prompt_tokens, completion_tokensusage.completion_tokens, projecttech-blog-demo )这里需要注意一个问题log_usage必须在 API 调用成功后调用。如果调用抛异常不要记录 usage因为请求可能没有成功消耗 token或者你无法拿到准确的 usage 数据。如果你使用的是其他语言比如 Java 或 Node.js思路完全一致解析 LLM API 的响应对象取出prompt_tokens和completion_tokens写入同一个 JSONL 文件。建议把写入操作封装成一个小工具库方便接入。5.3 为什么不直接用官方 Usage API很多云厂商有 Usage API理论上可以直接从后台拉取。但在实际项目中它有明显的局限数据延迟高往往不是实时的不能按你自定义的项目维度拆分多模型、多厂商统一统计比较麻烦某些账号权限不足无法访问使用量接口。自己记录 usage相当于在你和模型之间加了一层可观测性拦截器。它不关心模型来自 OpenAI、Anthropic 还是自建服务只要你能拿到 usage 字段就能统一统计。当然你也可以在后续成熟后同时采集官方 Usage API 做交叉验证。但第一版先用本地计量最简单也最可控。6. 编写 SwiftBar 菜单栏扩展显示 pill 样式现在进入菜单栏扩展部分。我们会在 SwiftBar 插件目录下创建一个 Python 脚本读取~/.llm_usage/usage.jsonl聚合成今日和累计数据并以 pill 样式输出到菜单栏。6.1 完整插件脚本#!/usr/bin/env python3 # 文件~/.swiftbar/plugins/llm-usage.30s.py import json import os from datetime import date USAGE_FILE os.path.expanduser(~/.llm_usage/usage.jsonl) def load_records(): if not os.path.exists(USAGE_FILE): return [] records [] with open(USAGE_FILE, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue try: records.append(json.loads(line)) except json.JSONDecodeError: continue return records def aggregate(records): today_prefix date.today().isoformat() total_prompt 0 total_completion 0 today_prompt 0 today_completion 0 for r in records: p r.get(prompt_tokens, 0) c r.get(completion_tokens, 0) total_prompt p total_completion c ts r.get(timestamp, ) if ts.startswith(today_prefix): today_prompt p today_completion c return total_prompt, total_completion, today_prompt, today_completion def format_number(n): if n 1000: return f{n / 1000:.1f}K return str(n) def main(): total_p, total_c, today_p, today_c aggregate(load_records()) today_total today_p today_c total_total total_p total_c # 第一行pill 样式显示今日总 token print(f⚡ {format_number(today_total)}) # 分隔线 print(---) # 菜单 panel今日详情 print(f今日总 Token: {today_total}) print(f今日输入: {today_p}) print(f今日输出: {today_c}) print(---) # 菜单 panel累计详情 print(f累计总 Token: {total_total}) print(f累计输入: {total_p}) print(f累计输出: {total_c}) print(---) # 支持点击刷新菜单栏 print(刷新 | refreshtrue) # 支持打开记录目录 print(打开记录目录 | bashopen param1~/.llm_usage terminalfalse) if __name__ __main__: main()6.2 脚本的关键逻辑load_records()逐行解析 JSONL忽略损坏的行aggregate()一次遍历完成今日与累计统计format_number()把 123456 格式化成 “123.5K”让菜单栏更紧凑第一行print(⚡ 12.3K)就是菜单栏上显示的 pill 样式下拉菜单里的“今日输入/输出”“累计输入/输出”就构成了一个最简单的 panel。如果你想显示成 nub只需要把第一行改成一个表示状态的小圆点比如print(●)如果你想显示具体的费用估算可以在aggregate里引入模型单价表。这里不做展开因为不同模型价格变化频繁而且在自建模型或内部计费环境下单价往往不是官方价。6.3 执行权限与刷新配置脚本保存后需要给它执行权限chmod x ~/.swiftbar/plugins/llm-usage.30s.pySwiftBar 会自动识别文件名里的.30s.每 30 秒运行一次脚本。如果你的插件目录已经存在其他脚本SwiftBar 也可以同时运行多个插件互不干扰。改完脚本后在 SwiftBar 菜单里选择“刷新插件”即可看到效果。7. 运行验证与效果检查写完了脚本怎么判断整套链路是否正确建议先手动运行一次插件脚本检查输出格式python3 ~/.swiftbar/plugins/llm-usage.30s.py如果此时还没有任何 usage 记录你会看到类似输出⚡ 0 --- 今日总 Token: 0 今日输入: 0 今日输出: 0 --- 累计总 Token: 0 累计输入: 0 累计输出: 0 --- 刷新 | refreshtrue 打开记录目录 | bashopen param1~/.llm_usage terminalfalse这说明脚本本身能正常执行。接下来模拟一次调用记录mkdir -p ~/.llm_usage echo {timestamp:2025-01-01T10:00:000800,model:gpt-4o-mini,prompt_tokens:100,completion_tokens:200,project:demo} ~/.llm_usage/usage.jsonl再次运行插件脚本第一行应该变成⚡ 300因为今日总共多了 300 个 token。如果你在 SwiftBar 菜单栏中看不到任何变化先检查SwiftBar 是否已经运行脚本是否放在插件目录脚本是否有执行权限SwiftBar 设置里的插件目录是否指向了正确位置文件名的.30s.后缀是否拼写正确。如果脚本在终端可以运行但菜单栏不刷新可以手动点击 SwiftBar 菜单选择“刷新所有插件”或者直接重启 SwiftBar。8. 常见问题与排查方法问题现象可能原因排查方式解决方案菜单栏不显示任何插件SwiftBar 插件目录配置错误打开 SwiftBar 设置查看插件目录路径把脚本放到正确的插件目录并重载脚本手动运行正常菜单栏不刷新执行权限缺失或文件名后缀错误ls -l查看权限chmod x脚本并确认文件名包含.30s.菜单栏一直显示 0usage.jsonl 路径不对或没有记录查看脚本中USAGE_FILE路径确认应用写入了~/.llm_usage/usage.jsonlJSON 解析报错多进程同时写入导致半行数据检查 usage.jsonl 末尾是否有空行或异常行记录时使用追加模式避免并发写坏文件token 数据显示不全部分调用没有记录 usage检查应用代码是否在异常分支遗漏日志统一封装调用入口确保成功响应均记录菜单栏刷新卡顿脚本执行时间过长查看文件行数是否过大定期归档旧数据或改用 SQLite 存储显示 emoji 乱码终端或菜单栏字体不支持更换普通字符或系统默认 emoji改用Token文本或简单的几何符号切换 macOS 版本后脚本失效Python 路径变化which python3查看路径脚本首行改成#!/usr/bin/env python3这里重点说一个容易被忽视的坑多进程写入 JSONL 文件时如果两个请求同时写入可能出现写了一半的数据导致菜单栏脚本解析失败。避免这个问题最简单的办法是把log_usage放在进程内统一调用或者使用 Python 的flock文件锁。绝大多数场景下只要写入频率不高JSONL 足够可靠。如果你所在团队每天有几十万次调用建议升级到 SQLite 存储。SQLite 对并发写入更友好聚合查询也更方便。9. 最佳实践与工程建议9.1 数据采集统一收口不要在业务代码里到处手动调用log_usage。更推荐的做法是封装一个统一的 LLM 客户端或中间件。所有调用都走同一个入口在这个入口内部自动记录 usage。这样后续增加统计维度、调整字段、切换存储方式都只改一处。9.2 记录元数据但不要记录敏感内容usage 日志只记录 token 数量、模型名、项目名和时间不要记录 prompt 原文和 completion 内容。这不仅是隐私问题也是磁盘和性能问题。菜单栏扩展展示的是统计信息不需要原始文本。如果你确实需要排查某个请求的输入输出应该把原始日志放到独立的、有权限控制的日志系统里。9.3 设置金额估算时单独维护价格表如果你需要直接看到“今日费用”可以单独维护一个价格表文件例如MODEL_PRICES { gpt-4o-mini: {input: 0.00015, output: 0.0006}, gpt-4o: {input: 0.005, output: 0.015}, }这里的价格单位通常是“每 1K token 的美金”。计算费用时用输入 token 和输出 token 分别乘以对应单价。价格表需要定期更新并且不要写死在菜单栏脚本里否则每次调价都要改插件。9.4 为菜单栏扩展加上颜色告警你可以根据今日消耗阈值在脚本第一行输出颜色信息。SwiftBar 支持用 ANSI 颜色或直接返回带color参数的行。例如if today_total 100000: print(f⚡ {format_number(today_total)} | colorred) elif today_total 50000: print(f⚡ {format_number(today_total)} | colororange) else: print(f⚡ {format_number(today_total)} | colorgreen)这样菜单栏药丸会随着消耗量变化颜色一眼就能看出今天是否“烧得快”。9.5 数据备份与归档usage.jsonl会随着时间增长。建议按月归档一次保留最近几个月的数据即可。你可以在~/.llm_usage/下按日期组织~/.llm_usage/ usage.jsonl archive/ 2025-01.jsonl 2025-02.jsonl归档脚本简单写一个 Python 或 Shell 脚本即可目的是避免菜单栏扩展每次扫描过大文件导致卡顿。9.6 从脚本到原生 App 的演进时机当你的统计维度越来越复杂比如要展示折线图、按用户筛选、设置预算提醒SwiftBar 脚本会变得臃肿。这时候可以考虑升级为原生 SwiftUI 菜单栏 App。SwiftUI 的MenuBarExtra组件可以很方便地创建菜单栏应用数据处理也能用 Swift 的 SQLite 库。但从今天这套 Python 方案迁移过去核心的数据模型和采集链路是可以直接复用的。所以不要一开始就追求“做一个漂亮的原生 App”。先用脚本把数据链路跑通等需求真实出现了再决定是否重写 UI。9.7 安全与权限最小化脚本只读 usage 记录文件不要赋予它读取 API Key 的权限usage 记录文件权限设置为700不要在菜单栏显示原始日志内容不要把你自己的 API Key 写进任何示例代码如果需要在团队内分发这套方案只发代码模板不发个人密钥。总结与后续学习方向这篇文章梳理了一个完整的 LLM usage 监控闭环在应用层统一采集 token 消耗写入本地 JSONL 文件再通过 SwiftBar 菜单栏脚本以 pill 形态实时展示。相比直接查云厂商后台这套方案具备多项目维度、跨模型统一、实时可见的好处。从一个最小可用的菜单栏扩展开始你可以继续深入的方向还有很多接入 Anthropic 或其他模型厂商的 usage 字段、计算并展示估算费用、按项目做到预算限额、接入企业微信或钉钉告警或者干脆把脚本重构成原生 SwiftUI 菜单栏 App。背后的数据模型和日志钩子思想在哪个阶段都用得上。我建议你先花 20 分钟按本文代码跑通一次最小流程写一条模拟 usage 记录然后看着菜单栏上的数字变化。等这个过程让你觉得“它已经长在手边”了再考虑往里面加功能。毕竟一个工具只有当它足够轻、足够顺手地出现在你眼前才真正称得上有效。