ESP32/docs/zhipu-coding-plan-usage-api.md
2026-06-23 20:38:23 +08:00

21 KiB

智谱 GLM Coding Plan 用量查询对接文档

文档目的:为后续开发者(或 AI 助手)提供智谱 GLM Coding Plan 用量查询的完整对接方案,基于 cc-switch 源码逆向 + 官方文档查证,内容经过用户实测验证(用户在 cc-switch 里能看到自己的套餐用量)。

权威来源:以 farion1231/cc-switch 源码实现为准。该实现用户已验证可用。

本地参考源码:D:\AI Code\ESP-32\espcc-switch\(已克隆,可直接查阅)


一、背景

ESP32-S3-RLCD-4.2 小屏需要显示智谱 GLM Coding Plan 的:

  • 5 小时限额已用百分比 + 重置时间
  • 周限额已用百分比 + 重置时间(新套餐才有)
  • token 消耗统计
  • 套餐层级

智谱官方没有公开的用量查询 API 文档,但通过逆向 cc-switch 找到了实际可用的工作端点。

二、关键结论(一句话总结)

问题 答案
用什么端点查? GET /api/monitor/usage/quota/limit
怎么鉴权? 纯 API Key,放 Authorization 头,不加 Bearer 前缀
需要浏览器 Cookie 吗? 不需要(跟之前社区工具 coding-plan-monitor 不一样)
ESP-32 能直接用吗? 能,纯 HTTP GET,esp_http_client 可 1:1 复现
怎么区分 5h vs 周? 看 limits[].unit 字段:3=5h, 6=周
怎么判断老/新套餐? limits[] 里 TOKENS_LIMIT 条目数量(1=老无周, 2=新有周)
能拿到绝对 token 数吗? 不能,quota/limit 只给百分比。绝对 token 要调 model-usage 端点

三、核心 API

端点

国内:GET https://open.bigmodel.cn/api/monitor/usage/quota/limit
国际:GET https://api.z.ai/api/monitor/usage/quota/limit

域名选择:

  • 用户的 API Key 配在 open.bigmodel.cn → 用国内
  • 用户的 API Key 配在 api.z.ai → 用国际
  • 两条路径(path)完全一致,只是域名不同

域名选择参考代码(来自 cc-switch coding_plan.rs:302-308):

fn zhipu_quota_base(base_url: &str) -> &str {
    if base_url.contains("bigmodel.cn") {
        "https://open.bigmodel.cn"
    } else {
        "https://api.z.ai"
    }
}

请求

GET /api/monitor/usage/quota/limit HTTP/1.1
Host: open.bigmodel.cn
Authorization: <你的智谱 API_KEY>
Content-Type: application/json
Accept-Language: en-US,en

关键点:

  • 方法:GET
  • 鉴权:Authorization 头直接放 API Key 原值,不要加 Bearer 前缀
    • cc-switch 在 coding_plan.rs:319 显式注释:// 注意:智谱不加 Bearer 前缀
    • 这跟 OpenAI、Anthropic 等标准 Bearer 鉴权不同,是个例外
  • Content-Type: application/json
  • Accept-Language: en-US,en(cc-switch 加了这个,建议照抄)
  • 无请求体
  • 无查询参数
  • 建议超时:15 秒
  • 不需要浏览器 Cookie
  • 不需要伪装 User-Agent

返回 JSON 结构

{
  "success": true,
  "msg": "操作成功",
  "data": {
    "level": "Max",
    "limits": [
      {
        "type": "TOKENS_LIMIT",
        "unit": 3,
        "number": 5,
        "percentage": 32.5,
        "nextResetTime": 1773734366338
      },
      {
        "type": "TOKENS_LIMIT",
        "unit": 6,
        "number": 7,
        "percentage": 45.0,
        "nextResetTime": 1774663282997
      }
    ]
  }
}

字段说明

字段路径 类型 含义
success bool 业务级成功标记,false 时读 msg
msg string 错误文案(success=false 时有用)
data.level string 套餐层级(如 "Max" / "Pro" / "Lite")
data.limits[] array 限额窗口数组
data.limits[].type string "TOKENS_LIMIT"(我们要的) / "TIME_LIMIT"(使用时长,跳过)
data.limits[].unit int 窗口类型,核心字段:3 = 5 小时,6 = 每周
data.limits[].number int 数量(5h 时为 5,周时为 7)
data.limits[].percentage float 已用百分比 0-100
data.limits[].nextResetTime int(ms) 重置毫秒时间戳;0% 用量时可能缺失

四、字段→业务含义映射

屏幕显示 字段路径 备注
5 小时限额已用% limits[type=TOKENS_LIMIT && unit=3].percentage 0-100
5 小时重置时间 limits[type=TOKENS_LIMIT && unit=3].nextResetTime ms 时间戳
周限额已用% limits[type=TOKENS_LIMIT && unit=6].percentage 新套餐才有,老套餐查不到
周限额重置时间 limits[type=TOKENS_LIMIT && unit=6].nextResetTime 同上
套餐层级 data.level 字符串

时间戳转换规则(用于显示)

根据之前 UI 设计讨论的结果,重置时间的显示策略:

  • 距离当前时间 ≤ 24 小时 → 显示 HH:MM(如 18:42)
  • 距离当前时间 > 24 小时 → 显示 MM-DD(如 06-28)

参考实现(formatResetTime):

function formatResetTime(resetMs) {
  const diff = resetMs - Date.now();
  const absDiff = Math.abs(diff);
  const date = new Date(resetMs);
  const pad = n => String(n).padStart(2, '0');
  if (absDiff <= 24 * 3600 * 1000) {
    return `${pad(date.getHours())}:${pad(date.getMinutes())}`;
  } else {
    return `${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
  }
}

ESP-32 C 实现可用 <time.h> 的 localtime_r + difftime。


五、⚠️ 三个必避的坑

坑 1:必须用 unit 字段区分 5h 和周,不能按重置时间排序

问题:cc-switch 在 issue #3036 踩过——每周周期末尾,周窗口会比 5 小时窗口更早重置,按 nextResetTime 升序排序必然把"5h"和"周"标反。

正确做法:只看 unit 字段:

  • unit == 3 → 5 小时窗口
  • unit == 6 → 周窗口

只有当 unit 缺失或未识别时,才回落到启发式(无 reset 优先归 5h,其余按 reset 升序填空槽)。

源码引用:cc-switch/src-tauri/src/services/coding_plan.rs:226-234 有显式注释说明这个坑。

坑 2:老套餐 vs 新套餐的判定

判定方式不是某个标志位,而是 limits[] 数组里 type==TOKENS_LIMIT 条目的数量:

  • 老套餐(2026-02-12 前订阅):只返回 1 条 TOKENS_LIMIT(只有 5h,无周)
  • 新套餐:返回 2 条 TOKENS_LIMIT(5h + 周)

当前用户:用的是老套餐(无周限额),所以查出来只会看到 1 条 TOKENS_LIMIT,没有 unit=6 的条目。

代码处理建议:用两个可空 slot(five_hour / weekly),有就填、没有就留空。屏幕显示时:

  • 5h 没查到 → 显示 "—"
  • 周限额没查到 → 显示 "无限制" 或 "—" 或 "OLD PLAN"

源码参考:cc-switch/src-tauri/src/services/coding_plan.rs:279-291 实现了天然降级逻辑。

坑 3:智谱接口只给百分比,不给绝对 token 数

问题:cc-switch 在 coding_plan.rs:282-289 明确:used_value_usd / max_value_usd 都是 None。智谱接口只返回 percentage,不返回绝对 token 数。

影响:之前 UI 设计里说的"token 消耗按 M/B 显示"不能从 quota/limit 接口拿到。

解决方案:要拿绝对 token 数,得调另一个端点:

GET /api/monitor/usage/model-usage?startTime=...&endTime=...
  • 参数:startTime / endTime URL 编码后的 "yyyy-MM-dd HH:mm:ss" 格式
  • 官方插件 zai-coding-plugins 的默认窗口:昨天当前小时整点 → 今天当前小时末秒
  • cc-switch 没用这个接口,所以返回字段结构需要实测确认(官方插件只是 JSON.stringify 原样打印,没固定字段名)

建议:

  1. 先做百分比显示(已用 X% + 重置时间),这是 quota/limit 能直接拿到的
  2. 绝对 token 数等 model-usage 端点字段确认后再加,或者干脆本地累加(每次调 LLM 时把 response.usage.total_tokens 累计起来,自己统计今日/本周消耗)

六、ESP-32 实现参考(伪代码)

#include "esp_http_client.h"
#include "cJSON.h"

// 1. 构造请求
esp_http_client_config_t config = {
    .url = "https://open.bigmodel.cn/api/monitor/usage/quota/limit",
    .method = HTTP_METHOD_GET,
    .timeout_ms = 15000,
    .cert_pem = bigmodel_root_ca,   // 需要 HTTPS 根证书
    .user_agent = "esp32-rlcd",     // 随便写,没限制
};

esp_http_client_handle_t client = esp_http_client_init(&config);

// 关键:Authorization 不加 Bearer 前缀!
char auth_header[256];
snprintf(auth_header, sizeof(auth_header), "%s", api_key);
esp_http_client_set_header(client, "Authorization", auth_header);
esp_http_client_set_header(client, "Content-Type", "application/json");
esp_http_client_set_header(client, "Accept-Language", "en-US,en");

// 2. 发请求
esp_err_t err = esp_http_client_perform(client);
if (err != ESP_OK) { /* 错误处理 */ }

int status = esp_http_client_get_status_code(client);
if (status == 401 || status == 403) {
    // 凭据失效
} else if (status != 200) {
    // 其他错误
}

// 3. 读响应体
char buffer[4096];
int len = esp_http_client_read(client, buffer, sizeof(buffer) - 1);
buffer[len] = '\0';

// 4. 解析 JSON
cJSON *root = cJSON_Parse(buffer);
cJSON *data = cJSON_GetObjectItem(root, "data");
cJSON *level = cJSON_GetObjectItem(data, "level");
const char *plan_level = level->valuestring;   // "Max"

cJSON *limits = cJSON_GetObjectItem(data, "limits");
cJSON *limit;
cJSON_ArrayForEach(limit, limits) {
    cJSON *type = cJSON_GetObjectItem(limit, "type");
    if (!cJSON_IsString(type) || strcmp(type->valuestring, "TOKENS_LIMIT") != 0) {
        continue;   // 跳过 TIME_LIMIT
    }

    cJSON *unit = cJSON_GetObjectItem(limit, "unit");
    cJSON *pct = cJSON_GetObjectItem(limit, "percentage");
    cJSON *reset = cJSON_GetObjectItem(limit, "nextResetTime");

    switch (unit->valueint) {
        case 3:   // 5 小时窗口
            five_hour_pct = pct->valuedouble;
            five_hour_reset_ms = reset ? reset->valuedouble : 0;
            break;
        case 6:   // 周窗口(新套餐才有)
            weekly_pct = pct->valuedouble;
            weekly_reset_ms = reset ? reset->valuedouble : 0;
            break;
    }
}

cJSON_Delete(root);
esp_http_client_cleanup(client);

// 5. 转换时间戳为显示字符串(24h 内显示 HH:MM, 否则 MM-DD)
//    使用 <time.h> 的 localtime_r + difftime

七、鉴权验证(写代码前先做)

写代码前,先用 curl 验证 API Key 能不能直接调通。这是最省时间的步骤——避免代码写完才发现鉴权走不通。

curl -X GET \
  -H "Authorization: <你的智谱 API_KEY>" \
  -H "Content-Type: application/json" \
  -H "Accept-Language: en-US,en" \
  https://open.bigmodel.cn/api/monitor/usage/quota/limit

预期响应(老套餐):

{
  "success": true,
  "msg": "操作成功",
  "data": {
    "level": "Max",
    "limits": [
      {
        "type": "TOKENS_LIMIT",
        "unit": 3,
        "number": 5,
        "percentage": 32.5,
        "nextResetTime": 1773734366338
      }
    ]
  }
}

判定:

  • ✅ 返回 200 + 有 data.limits → API Key 路线可行,ESP-32 可以直接复现
  • ❌ 返回 401/403 → API Key 无效或权限不够
  • ❌ 返回空 body 或非 JSON → 可能被反爬,但根据 cc-switch 经验这种情况不应该发生(cc-switch 用纯 API Key 跑通了)

八、源码引用清单

权威实现(cc-switch,用户已验证):

文件 行 作用
src-tauri/src/services/coding_plan.rs 302-324 query_zhipu 端点+请求头构造
src-tauri/src/services/coding_plan.rs 319 "// 注意:智谱不加 Bearer 前缀"
src-tauri/src/services/coding_plan.rs 218-224 classify_zhipu_window 按 unit 分类
src-tauri/src/services/coding_plan.rs 226-234 unit 字段坑的注释(issue #3036)
src-tauri/src/services/coding_plan.rs 238-292 parse_zhipu_token_tiers 解析逻辑
src-tauri/src/services/coding_plan.rs 279-291 老/新套餐天然降级(可空 slot)
src-tauri/src/services/coding_plan.rs 282-289 只读 percentage,绝对值为 None
src-tauri/src/services/subscription.rs 301, 308 TIER_FIVE_HOUR / TIER_WEEKLY_LIMIT 常量
src-tauri/src/services/coding_plan.rs 1224-1240 测试:老套餐单 tier 降级到 5h
src-tauri/src/services/coding_plan.rs 1316-1333 测试:unit 字段覆盖 reset 排序

本地路径:D:\AI Code\ESP-32\espcc-switch\(已克隆)

辅助参考(官方插件):

  • zai-coding-plugins/plugins/glm-plan-usage/skills/usage-query-skill/scripts/query-usage.mjs — 调用了 3 个端点(quota/limit + model-usage + tool-usage),quota/limit 字段处理跟 cc-switch 一致

辅助参考(社区工具,鉴权方式不同,不要照抄):

  • JinHanAI/coding-plan-monitor/src/providers/zhipu.ts — 走浏览器 Cookie,不支持 API Key 直连

九、官方文档(背景参考)


十、对接 Checklist(给 codex 用的执行清单)

  • Step 1:用 curl 验证 API Key 能调通(参考第七节)
  • Step 2:在 ESP-32 项目里加 HTTPS 根证书(open.bigmodel.cn 的根 CA)
  • Step 3:实现 zhipu_query_quota() 函数,封装 HTTP GET + JSON 解析
  • Step 4:实现 unit 字段分类(3=5h, 6=周),不要按时间排序
  • Step 5:实现老/新套餐降级(周限额查不到时显示"无限制")
  • Step 6:实现时间戳→显示字符串转换(24h 内 HH:MM,否则 MM-DD)
  • Step 7:绝对 token 数(如果需要)→ 调 model-usage 端点实测字段,或本地累加每次 LLM 调用的 usage.total_tokens
  • Step 8:把查询结果通过 BLE/USB 推送到板子(参考现有 metrics_* 模块的推送方式)

附:接口对比速查表

项目 端点 鉴权 用户验证 推荐用
cc-switch /api/monitor/usage/quota/limit 纯 API Key(无 Bearer) ✅ 用户已验证 ✅ 本文档基准
zai-coding-plugins(官方) 同上 + model-usage + tool-usage API Key(无 Bearer) ✅ 官方插件 辅助参考
coding-plan-monitor(社区) 同上 浏览器 Cookie ⚠️ 仅浏览器 ❌ 不照抄

十一、token 使用量获取方案

核心结论:quota/limit 端点只返回百分比,不返回绝对 token 数。要拿"今日用了多少 token",有三种方案,各有取舍。

三种方案对比

方案 原理 优点 缺点 推荐度
A. 调 model-usage 端点 查询时间窗口内的 token 消耗明细 服务端权威数据,无需本地存储 ⚠️ 字段结构官方未公开,需实测 ⭐⭐⭐
B. 本地累加(推荐) 每次调 LLM 时累计响应里的 usage.totalTokens 字段确定(智谱对话补全 API 标准),实现简单 只统计"经过本代理的"调用,跨设备/跨工具会漏 ⭐⭐⭐⭐⭐
C. 只显示百分比 直接用 quota/limit 的 percentage 零开发量,已有数据 没有绝对值,信息量少 ⭐⭐

方案 A:model-usage 端点(需实测字段)

端点:

GET https://open.bigmodel.cn/api/monitor/usage/model-usage?startTime=...&endTime=...

请求(跟 quota/limit 同款鉴权):

GET /api/monitor/usage/model-usage?startTime=2026-06-23%2000%3A00%3A00&endTime=2026-06-23%2023%3A59%3A59 HTTP/1.1
Host: open.bigmodel.cn
Authorization: <API_KEY>     ← 不加 Bearer
Content-Type: application/json
Accept-Language: en-US,en

参数:

  • startTime / endTime:URL 编码后的 "yyyy-MM-dd HH:mm:ss" 格式
  • 官方插件默认窗口:昨天当前小时整点 → 今天当前小时末秒

返回 JSON 结构(⚠️ 推断,需实测):

{
  "success": true,
  "code": 200,
  "msg": "ok",
  "data": {
    "usageList": [          // 字段名推断, 实测可能不同
      {
        "modelName": "glm-4.6",
        "totalTokens": 123456,
        "promptTokens": 100000,       // 智谱用驼峰命名(不是 prompt_tokens)
        "completionTokens": 23456
      }
    ]
  }
}

⚠️ 字段未公开的证据:

  • 官方插件 query-usage.mjs:167 调 model-usage 时没传 postProcessor,line 147 直接 JSON.stringify(outputData) 原样打印给 LLM
  • 官方 agent 文档 usage-query-agent.md 也没描述字段结构
  • 说明官方故意不固定字段文档(可能经常变),让 LLM 运行时自适应解读

实测步骤(必做):

curl -X GET \
  -H "Authorization: <你的 API_KEY>" \
  -H "Content-Type: application/json" \
  -H "Accept-Language: en-US,en" \
  "https://open.bigmodel.cn/api/monitor/usage/model-usage?startTime=$(date -d 'yesterday' +%Y-%m-%d)%2000%3A00%3A00&endTime=$(date +%Y-%m-%d)%2023%3A59%3A59"

拿到真实 JSON 后,根据实际字段名写解析代码。

方案 B:本地累加(推荐 ⭐)

原理:智谱对话补全 API 的每次响应都带 usage 字段,本地累加即可。

智谱对话补全响应里的 usage 字段(官方文档确认):

{
  "choices": [ ... ],
  "usage": {
    "promptTokens": 123,        // 输入 token(驼峰命名!)
    "completionTokens": 456,    // 输出 token
    "totalTokens": 579          // 总计
  }
}

⚠️ 命名坑:智谱用驼峰(promptTokens),不是 OpenAI 的下划线(prompt_tokens)。解析时别搞混。

实现方式(cc-switch 就是这么做的):

  • cc-switch 在 src-tauri/src/proxy/usage/parser.rs:17-22 定义了 TokenUsage 结构
  • 每次代理 LLM 请求时,从响应里解析 usage,写入本地 SQLite(usage_stats.rs)
  • 查询时 get_usage_summary 聚合统计

ESP-32 上的简化实现:

  • 如果电脑端有代理服务(像 cc-switch 那样),在代理层累加 token,定期推给板子
  • 如果板子自己调 LLM,在板子上累加(但 ESP-32 存储有限,建议只存今日/本周两个计数器)

为什么推荐:

  1. 字段确定(智谱对话补全 API 标准,有官方文档)
  2. 实现简单(就是累加)
  3. 不依赖未公开的 model-usage 端点
  4. cc-switch 验证过这条路可行

方案 C:只显示百分比

直接用 quota/limit 的 TOKENS_LIMIT.percentage,显示"5H 已用 32%"。

适合 MVP 阶段——零开发量,数据现成。后续再加方案 A 或 B 补绝对值。

给 codex 的建议

  1. 第一阶段:用方案 C(百分比),先把 quota/limit 跑通
  2. 第二阶段:如果电脑端有代理服务,用方案 B(本地累加)——字段确定、实现简单
  3. 第三阶段:如果需要服务端权威数据,用方案 A——但必须先 curl 实测字段,不要按本文档的推断结构写代码

十二、cc-switch 的 token 统计实现(方案 B 参考)

cc-switch 不调 model-usage 端点,而是本地 SQLite 累加。这是方案 B 的最佳参考。

核心文件

文件 作用
src-tauri/src/proxy/usage/parser.rs 从 LLM 响应解析 TokenUsage(支持 Claude/OpenAI 格式)
src-tauri/src/services/usage_stats.rs SQLite 存储 + 聚合查询(get_usage_summary)
src-tauri/src/services/session_usage_codex.rs Codex 专用 session 用量

TokenUsage 结构(parser.rs:17-22)

pub struct TokenUsage {
    pub input_tokens: u32,
    pub output_tokens: u32,
    pub cache_read_tokens: u32,
    pub cache_creation_tokens: u32,
}

解析入口

  • from_claude_response(parser.rs:66)— 解析 Claude API 响应的 usage.input_tokens / output_tokens
  • 也支持 OpenAI 兼容格式(prompt_tokens / completion_tokens)

统计查询

get_usage_summary(usage_stats.rs:505-664)支持按时间范围聚合,返回各模型的 token 总量。

本地路径:D:\AI Code\ESP-32\espcc-switch\src-tauri\src\proxy\usage\parser.rs


Sources(token 使用量部分)