# 智谱 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`): ```rust fn zhipu_quota_base(base_url: &str) -> &str { if base_url.contains("bigmodel.cn") { "https://open.bigmodel.cn" } else { "https://api.z.ai" } } ``` ### 请求 ```http 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 结构 ```jsonc { "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`): ```javascript 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 实现可用 `` 的 `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 实现参考(伪代码) ```c #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) // 使用 的 localtime_r + difftime ``` --- ## 七、鉴权验证(写代码前先做) 写代码前,**先用 curl 验证 API Key 能不能直接调通**。这是最省时间的步骤——避免代码写完才发现鉴权走不通。 ```bash 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 ``` **预期响应**(老套餐): ```json { "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 直连 --- ## 九、官方文档(背景参考) - [智谱AI开放文档 - Coding Plan FAQ](https://docs.bigmodel.cn/cn/coding-plan/faq) — 5h + 周限额机制说明 - [智谱AI开放文档 - 套餐概览](https://docs.bigmodel.cn/cn/coding-plan/overview) — 各套餐限额参考表 - [智谱AI开放文档 - 用量查询插件](https://docs.bigmodel.cn/cn/coding-plan/extension/usage-query-plugin) — 官方插件介绍 - [智谱AI开放文档 - HTTP API 调用](https://docs.bigmodel.cn/cn/guide/develop/http/introduction) — 基础 HTTP 调用规范 - [cc-switch GitHub](https://github.com/farion1231/cc-switch) — 权威实现来源 - [cc-switch Issue #1857 - 用量统计与计费追踪](https://github.com/farion1231/cc-switch/issues/1857) - [cc-switch Issue #1588 - 智谱用量查询脚本示例](https://github.com/farion1231/cc-switch/issues/1588) --- ## 十、对接 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 同款鉴权): ```http 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: ← 不加 Bearer Content-Type: application/json Accept-Language: en-US,en ``` **参数**: - `startTime` / `endTime`:URL 编码后的 `"yyyy-MM-dd HH:mm:ss"` 格式 - 官方插件默认窗口:昨天当前小时整点 → 今天当前小时末秒 **返回 JSON 结构(⚠️ 推断,需实测)**: ```jsonc { "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 运行时自适应解读 **实测步骤**(必做): ```bash 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 字段**(官方文档确认): ```jsonc { "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) ```rust 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 使用量部分) - [智谱AI开放文档 - 核心参数(Token 定义)](https://docs.bigmodel.cn/cn/guide/start/concept-param) — promptTokens/completionTokens/totalTokens 定义 - [智谱AI开放文档 - 对话补全 API](https://docs.bigmodel.cn/api-reference/%E6%A8%A1%E5%9E%8B-api/%E5%AF%B9%E8%AF%9D%E8%A1%A5%E5%85%A8) — usage 字段在响应里的位置 - [智谱AI开放文档 - API 价格](https://open.bigmodel.cn/pricing) — token 计费说明 - [GitHub Issue zhayujie/CowAgent#2289](https://github.com/zhayujie/CowAgent/issues/2289) — 智谱驼峰命名 vs OpenAI 下划线命名的兼容性问题 - [cc-switch Discussion #1038 - 智谱用量查询脚本](https://github.com/farion1231/cc-switch/discussions/1038) — model-usage 端点调用示例 - 本地源码:`D:\esp\zai-coding-plugins\plugins\glm-plan-usage\skills\usage-query-skill\scripts\query-usage.mjs`(官方插件,证实 model-usage 字段未公开) - 本地源码:`D:\AI Code\ESP-32\espcc-switch\src-tauri\src\proxy\usage\parser.rs`(本地累加实现)