news 2026/9/2 12:07:24

Mac菜单栏实时显示LLM Token消耗:基于SwiftBar的轻量监控方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Mac菜单栏实时显示LLM Token消耗:基于SwiftBar的轻量监控方案

在开始做 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 还是 nub

macOS 菜单栏扩展的形态,通常有三种:

形态特点适合场景
panel点击后展开一个信息面板,显示完整统计需要看到详细数据、趋势、项目列表
pill常态显示一段文本,像一个胶囊标签只关心今日 token、费用这几个关键数字
nub很小的圆点或图标,几乎不占空间只需要一个状态信号,例如正常、异常、超额

从工程角度看,这三种形态不是割裂的。菜单栏第一行显示的是什么形态,由你的脚本输出内容决定:如果你只输出一个圆点,就是 nub;输出一行“Token 12.3K”,就是 pill;下拉菜单里的各项信息,则可以理解为一个 panel。

SwiftBar 的插件机制很适合实现这三种形态。插件脚本最核心的规则是:

  • 第一行输出会显示在菜单栏上;
  • 第二行开始的内容属于下拉菜单;
  • ---表示菜单分隔线;
  • 支持hrefbashrefresh等参数实现点击动作。

因此,你完全可以先写一个返回“药丸”样式的脚本,让它常态显示今日 token 数;再在菜单里补充一个完整的面板视图,展示按项目拆分的明细。

选择 SwiftBar 而不是从零写一个原生 App,原因很简单:

  1. 开发成本低:一段 Python 脚本即可完成,不需要 Xcode 编译;
  2. 迭代快:改完脚本立刻刷新,不用重新打包;
  3. 门槛低:团队里的同学都能维护;
  4. 生态成熟:SwiftBar 本身就是开源项目,社区有大量现成插件可参考。

瓶颈在于 Python 脚本的执行频率。建议设置 5 到 30 秒刷新一次,既能保持数据新鲜,又不会带来明显的性能问题。

4. 环境准备与前置条件

开始写代码之前,需要准备好这些环境。

4.1 操作系统与运行时

  • macOS 12 及以上版本(新版 macOS 对菜单栏应用的权限管理更严格);
  • Python 3.9 以上版本,macOS 自带python3,但版本可能不是最新,建议用brew install python安装;
  • SwiftBar 最新版本。

如果你还没有安装 SwiftBar,可以使用 Homebrew:

brew 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, mode=0o700, exist_ok=True) def log_usage(model, prompt_tokens, completion_tokens, project="default", extra=None): """ 记录一次 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", encoding="utf-8") as f: f.write(json.dumps(record, ensure_ascii=False) + "\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_key=os.environ["OPENAI_API_KEY"]) response = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "你是一个技术助手"}, {"role": "user", "content": "介绍一下 Python 的 with 语句"} ] ) usage = response.usage log_usage( model=response.model, prompt_tokens=usage.prompt_tokens, completion_tokens=usage.completion_tokens, project="tech-blog-demo" )

这里需要注意一个问题:log_usage必须在 API 调用成功后调用。如果调用抛异常,不要记录 usage,因为请求可能没有成功消耗 token,或者你无法拿到准确的 usage 数据。

如果你使用的是其他语言,比如 Java 或 Node.js,思路完全一致:解析 LLM API 的响应对象,取出prompt_tokenscompletion_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", encoding="utf-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("刷新 | refresh=true") # 支持打开记录目录 print("打开记录目录 | bash='open' param1='~/.llm_usage' terminal=false") 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.py

SwiftBar 会自动识别文件名里的.30s.,每 30 秒运行一次脚本。如果你的插件目录已经存在其他脚本,SwiftBar 也可以同时运行多个插件,互不干扰。

改完脚本后,在 SwiftBar 菜单里选择“刷新插件”即可看到效果。

7. 运行验证与效果检查

写完了脚本,怎么判断整套链路是否正确?

建议先手动运行一次插件脚本,检查输出格式:

python3 ~/.swiftbar/plugins/llm-usage.30s.py

如果此时还没有任何 usage 记录,你会看到类似输出:

⚡ 0 --- 今日总 Token: 0 今日输入: 0 今日输出: 0 --- 累计总 Token: 0 累计输入: 0 累计输出: 0 --- 刷新 | refresh=true 打开记录目录 | bash='open' param1='~/.llm_usage' terminal=false

这说明脚本本身能正常执行。接下来,模拟一次调用记录:

mkdir -p ~/.llm_usage echo '{"timestamp":"2025-01-01T10:00:00+0800","model":"gpt-4o-mini","prompt_tokens":100,"completion_tokens":200,"project":"demo"}' >> ~/.llm_usage/usage.jsonl

再次运行插件脚本,第一行应该变成:

⚡ 300

因为今日总共多了 300 个 token。如果你在 SwiftBar 菜单栏中看不到任何变化,先检查:

  1. SwiftBar 是否已经运行;
  2. 脚本是否放在插件目录;
  3. 脚本是否有执行权限;
  4. SwiftBar 设置里的插件目录是否指向了正确位置;
  5. 文件名的.30s.后缀是否拼写正确。

如果脚本在终端可以运行,但菜单栏不刷新,可以手动点击 SwiftBar 菜单选择“刷新所有插件”,或者直接重启 SwiftBar。

8. 常见问题与排查方法

问题现象可能原因排查方式解决方案
菜单栏不显示任何插件SwiftBar 插件目录配置错误打开 SwiftBar 设置,查看插件目录路径把脚本放到正确的插件目录并重载
脚本手动运行正常,菜单栏不刷新执行权限缺失或文件名后缀错误ls -l查看权限chmod +x脚本,并确认文件名包含.30s.
菜单栏一直显示 0usage.jsonl 路径不对或没有记录查看脚本中USAGE_FILE路径确认应用写入了~/.llm_usage/usage.jsonl
JSON 解析报错多进程同时写入导致半行数据检查 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)} | color=red") elif today_total > 50000: print(f"⚡ {format_number(today_total)} | color=orange") else: print(f"⚡ {format_number(today_total)} | color=green")

这样,菜单栏药丸会随着消耗量变化颜色,一眼就能看出今天是否“烧得快”。

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 记录,然后看着菜单栏上的数字变化。等这个过程让你觉得“它已经长在手边”了,再考虑往里面加功能。毕竟,一个工具只有当它足够轻、足够顺手地出现在你眼前,才真正称得上有效。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/2 12:07:16

让产品自己说话:从认知负荷到前端工程的自解释性设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/2 12:02:43

STM32F407双电机FOC驱动方案:基于FreeRTOS的实时控制与霍尔混合传感实现

简介:本资源是面向嵌入式电机控制工程师与高校电赛/毕业设计学生的STM32F4系列双电机FOC实战项目,聚焦磁场定向控制在高性能PMSM驱动中的落地实现,解决多电机同步协调、霍尔位置反馈闭环及实时性保障等核心工程难题。压缩包含1191个文件&…

作者头像 李华
网站建设 2026/9/2 11:59:53

按键精灵写暗影格斗三刷福包脚本:原理、实战与风险

1. 为什么会有“暗影格斗三刷福包”这种需求 玩过《暗影格斗三》这类偏重收集和赛季活动的玩家应该都有体会:游戏里的“福包”并不总是直接发到背包里,很多时候需要你手动点进活动页、领取奖励、关闭弹窗,甚至每隔一段时间重新进入一次。日常…

作者头像 李华
网站建设 2026/9/2 11:57:28

基于STM32与FreeRTOS的六自由度机械臂实时控制系统设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华