# token-tracker **Repository Path**: float_dreams/token-tracker ## Basic Information - **Project Name**: token-tracker - **Description**: 可以追踪token用量的代理工具 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 4 - **Forks**: 0 - **Created**: 2026-07-23 - **Last Updated**: 2026-11-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Token Tracker > 本地运行的 AI API Token 用量监控工具 —— 用一个本地中转入口把已购的多个 API 套餐聚到一起(各家 coding plan、中转站等),按模型自动选套餐并统计 input / output / 缓存 Token、换算成费用,内置可视化仪表板;每种套餐的购买 / 续费周期与开销也一起管起来。 [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Python](https://img.shields.io/badge/python-3.9%2B-3776AB.svg)](https://www.python.org/) [![Version](https://img.shields.io/badge/version-v4.1-brightgreen.svg)](#) [![Platform](https://img.shields.io/badge/platform-Windows-0078D4.svg)](#) **数据全部留在本机**:不上传任何数据,中转入口默认只监听 `127.0.0.1`。 中转需要用套餐 Key 替换客户端凭证,因此套餐 Key 会以明文存于本机数据库(详见[安全与隐私](#安全与隐私))。 --- ## 目录 - [核心特性](#核心特性) - [支持的客户端与协议](#支持的客户端与协议) - [快速开始(exe 版)](#快速开始exe-版) - [从源码运行](#从源码运行) - [使用方法](#使用方法) - [API 中转(一个入口用多个套餐)](#api-中转一个入口用多个套餐) - [代理模块(TT-RP v1.0-alpha)](#5-代理模块接入外部-exe) - [仪表板说明](#仪表板说明) - [API 端点](#api-端点) - [数据存储](#数据存储) - [工作原理](#工作原理) - [项目结构](#项目结构) - [常见问题](#常见问题) - [安全与隐私](#安全与隐私) - [开发与测试](#开发与测试) - [许可证](#许可证) --- ## 核心特性 | 能力 | 说明 | |------|------| | 🔄 **本地中转入口** | 一个端口对接所有已购套餐,只转发已知 API 路径;按请求里的 `模型id@上游名` 自动选套餐、注入对应 Key(见[工作原理](#工作原理)) | | 📊 **Token 统计** | 自动统计 input / output / 缓存 Token,流式(SSE)与非流式响应均支持 | | 💰 **费用换算** | 按模型设置单价(元 / 百万 Token),支持**按时间版本定价**与**峰谷定价** | | 🏷️ **模型管理** | 配了相同展示名的模型 id **按别名聚成一行**(与「模型用量」同口径),没配的在「待配置」区;详情页里有展示名 / 谷峰谷定价 / 各厂商用量 / 删除 | | 🤖 **多客户端** | Kimi Code、OpenCode、Codex、火山引擎等 OpenAI 兼容 API 客户端开箱可用 | | 🔀 **API 中转** | 把多个已购套餐(智谱 / Kimi / DeepSeek…)聚合成**一个本地入口**,按请求里的 `模型id@上游名` 自动选套餐、注入对应 Key,用量按套餐分别统计 | | 📈 **可视化仪表板** | 左侧导航多页面布局(用量统计 / 模型用量 / API 中转 + 设置项),浅色 / 深色主题 | | 🖼️ **模型头像** | 给每个模型传一张图标(自动压到 128px),没传就显示主色首字母徽标 | | 📅 **每日使用热力图** | GitHub 贡献图风格热力图,按自然年展示,可前后切换年份 | | 🔍 **多维筛选** | 按日 / 周 / 月粒度查看趋势,可指定模型与逐小时区间 | | ⚡ **实时更新** | 页面数据由 WebSocket **实时推送**(后端数据一变就刷新);连接断开时自动回退 5 秒轮询,恢复后回到推送模式 | | 🧾 **套餐开销管理** | 每个套餐一张卡片,可记每一次**购买 / 续费**的时间段与价格;总价、有效 / 已过期、最近到期一目了然 | | 🌐 **代理模块** | 把本地中转**代理出去**:托管外部代理 exe(frp / cloudflared / 自研隧道),用 WebSocket 控制通道 + 心跳判定它是否正常(见[代理模块](#5-代理模块接入外部-exe)) | | 💾 **SQLite 存储** | 用量、价格、套餐配置与购买周期统一入库;WAL 模式,多线程安全 | ## 支持的客户端与协议 - **Kimi Code (Moonshot AI)** — 含 `/coding/v1/*` 路径 - **火山引擎(Volcengine)** — 含 `/api/coding/v3` 路径 - **OpenCode / Codex / ChatGPT** — OpenAI Chat Completions 协议 - **其他 OpenAI Chat Completions 兼容客户端** ## 快速开始(exe 版) 无需安装 Python,单文件直接运行。 ### 目录结构 ``` token-tracker/ └── web/ ├── token-tracker.exe # 主程序,双击运行(自包含,无需其他文件;仓库内位于 web/) ├── start.bat # 一键启动:优先跑根目录 exe,没有则用源码启动 ├── build-exe.bat # 一键打包:产物直落 web/ 根目录 ├── requirements.txt # Python 运行依赖清单 ├── code/ # 源码:backend/ 后端 + frontend/ 前端 + build/ 打包配置 └── data/ # 数据目录(首次运行自动创建,位于 exe 同级) ├── token_usage.db # SQLite 数据库(用量数据 + 套餐配置与购买周期 + 模型价格 + 模型头像) └── 2api/ # 代理资源(添加的代理 exe 自动拷一份进来托管) ``` > 仓库里源码与构建脚本统一放在 `web/code/`(`backend/` 后端、`frontend/` 前端、`build/` 打包配置);README 等说明文档留在仓库根目录,不进 `web/`。打包后的 exe 是单文件,不依赖它们。 ### 使用步骤 1. 双击 `token-tracker.exe` 启动(程序会在后台运行,不显示控制台窗口) 2. 在浏览器打开仪表板(默认 ) 3. 进入「左侧导航 → API 中转」,点「添加套餐」把已购买的套餐接进来(名称 / API 地址 / Key / 模型映射) 4. 点「启动中转」,并把你的 AI 客户端指向这一个地址(见[配置你的 AI 客户端](#2-配置你的-ai-客户端)) 5. 请求完成后回到仪表板查看 Token 用量、费用,以及各套餐的购买 / 续费周期 > exe 版本是**窗口化构建**(无控制台),因此看不到运行日志。需要日志时请[从源码运行](#从源码运行)并加 `--verbose`。 ### 命令行参数 ```bash token-tracker.exe --dashboard-port 9001 --data-dir ./my-data ``` | 参数 | 说明 | 默认值 | |------|------|--------| | `--host` | 绑定主机(**API 中转与仪表板共用**) | `127.0.0.1` | | `--dashboard-port` | 仪表板端口 | `8764` | | `--data-dir` | 数据存储目录 | 见下方说明 | | `--verbose`, `-v` | 输出 DEBUG 级日志(仅控制台) | `false` | > **数据目录**:默认为**程序同级**的 `data/`——以 exe 运行即 `exe 同级/data`,从源码运行即 `web/data`(与启动时的工作目录无关)。也可在仪表板「左侧导航 → 数据目录」中在线修改(会自动创建目录并重载数据,但**不会迁移已有数据**)。 > > ⚠️ `--host 0.0.0.0` 会同时把 API 中转和仪表板暴露到局域网,请仅在明确需要时使用。 --- ## 从源码运行 ### 环境要求 - Python **3.9+** - 依赖:`aiohttp`、`aiohttp-cors`(见 `web/requirements.txt`) ### 安装与启动 ```bash git clone https://gitee.com/float_dreams/token-tracker.git cd token-tracker/web pip install -r requirements.txt python code\backend\main.py ``` > 源码在 `web/code/backend/`,前端(`index.html` / `assets/`)在 `web/code/frontend/`,打包脚本在 `web/code/build/`。数据目录锚定在源码所在位置(源码运行固定为 `web/data`),从哪个目录启动都一样。 启动后控制台会打印仪表板地址(默认 );按 `Ctrl+C` 停止,运行中的 API 中转会一并停止。 > 也可以直接运行 `web/start.bat`:优先启动根目录已打包的 `token-tracker.exe`;找不到 exe 时自动创建虚拟环境、装好依赖并从源码启动。 ### 打包为 exe 双击(或在终端执行)`web/build-exe.bat` 即可一键打包,其等价手动命令: ```bash cd web pip install -r requirements.txt -r code/build/requirements.txt pyinstaller --noconfirm --clean --distpath . --workpath tmp\build code\build/token-tracker.spec ``` 产物为 `web/token-tracker.exe`(单文件、自包含,直接覆盖根目录入口副本)。打包前请确认: - 已安装 PyInstaller 及 `requirements.txt` 中的全部依赖 - **已关闭正在运行的 `token-tracker.exe`**——文件被占用会导致覆盖失败 --- ## 使用方法 ### 1. 配置 API 中转(套餐 + 上游 + 购买周期) 打开仪表板,进入「左侧导航 → API 中转」: 1. 中转端口**写死为 8888**(`http://127.0.0.1:8888`),界面上没有端口设置项,直接往下走 2. 点「添加套餐」,先只填套餐名(套餐本身 = 一张卡片 = 一份钱 + 一组统计) 3. 点套餐卡片进入**二级界面**,在「上游」里点「添加上游」:名称 / Base URL / API Key / 模型映射 —— 一个套餐可以挂**多条上游**(同一份订阅的两个入口,如 `kimi` 与 `kimiwork`), 各自地址、Key、模型独立,卡片上算一份钱、用量合计(细节见 [API 中转](#api-中转一个入口用多个套餐)) 4. 同一页的「购买 / 续费周期」里把每一次购买或续费的**时间段与价格**记进去 (续费多次就添多条:卡片上的「价格合计」是各周期之和,「有效期至」取最晚的到期日) 5. 点「启动中转」,把客户端指向这一个地址(见下一节) > 从老版本升级上来的用户:原来「代理管理」里的每条代理都会**自动平移成一个同名套餐**, > 套餐下自动生成一条**同名上游**(地址一起带过来,Key / 模型映射留空等你补); > 历史用量归在该上游名下,也就是这张卡片上。 ### 2. 配置你的 AI 客户端 **方法一:修改 API Base URL(推荐)** 把客户端里的 API base URL 指向中转入口(**不要带 `/v1`**,网关对外只有 `/chat/completions` 一个端点): ``` http://127.0.0.1:8888 ``` **方法二:设置 HTTP 代理** ```bash # Linux / macOS export HTTP_PROXY=http://127.0.0.1:8888 export HTTPS_PROXY=http://127.0.0.1:8888 ``` > 一个入口就能用上所有已购套餐:模型名用「模型id@上游名」,网关按它选套餐并注入该套餐的 Key。 > 详见下方 [API 中转](#api-中转一个入口用多个套餐)。 ### 3. 模型管理(含定价) 「左侧导航 → 模型管理」**配了展示名的模型按别名归并成组**(多个模型 id 配同一个名字 = 一行, 与「模型用量」页同口径;组内各 id 以小徽标列出),没配的在下方「**待配置**」区按模型 id 单行。 列表每行直接展示**谷时价格**摘要(输入 / 输出 / 缓存,元/百万 Token),峰谷价格与全部操作 在行尾「**详情**」按钮的二级页面里。 - **待配置的模型同样有入口**:点「配置」进详情页,输入框留空保存即完成一次「**原名配置**」 (把该模型标记为已配置,展示名与模型 id 一致) **详情页**(一个别名组一页)包含四块: - **模型名**:组内每个模型 id 一行,各自设置 / 修改展示名(只改显示,真实模型 id 不变,悬浮仍回显真实 id)。 输入框留空保存即恢复原名;展示名以**裸模型名为键**,对所有提供商生效,同时是本列表与「模型用量」页的归并口径。 把组内某个 id 改成新名字,它会从当前组拆出去单成一行;改回同名则重新合并 - **定价**:按模型 id 分块(价格按**裸模型 id** 生效,组内各 id 独立配置),每块含谷时输入 / 输出 / 缓存 + 峰谷开关、峰时价格与时间段;「修改价格」展开编辑表单,保存时有两种方式: - **覆盖**:以当前填写的价格替换该模型全部历史版本(历史花费按新价重算) - **追加**:保留历史版本,从当前时间起应用新价格(历史记录按当时价格计费) - **峰谷定价**:可开启峰时价格并配置多个时间段,每个时间段可选生效的星期,支持跨夜时间段(如 `22:00-06:00`);峰时未填写的字段回退为谷时价格 - **用量**:组内全部模型的 Token / 请求 / 花费汇总,以及各**厂商(provider)× 模型 id** 的消耗明细 - **删除**:组内每个模型 id 一个删除按钮;删除该模型并**清除其全部关联数据**(不可撤销), 包括全部请求记录、日 / 小时 / 模型 / 会话各维度聚合统计、仅使用过该模型的会话、价格配置与展示名 > **定价跟随模型管理**:一个模型被删除后,它的价格配置也会跟着消失 —— 打开「模型管理」时会自动清掉 > 「该模型在任何用量表里都查不到」的残留价格与展示名(清理结果会弹提示列出被清的模型)。 > 界面上无法为「没有任何用量」的模型新建价格,所以这种残留只可能是删除模型后留下的。 > 判断口径取**宽口径**:只要模型还出现在任何用量表(含 `daily_summary` / `hourly_summary`)里,价格就保留。 **计价口径**: ``` 费用 = (非缓存输入 × 输入单价 + 输出 × 输出单价 + 缓存命中 × 缓存单价) / 1,000,000 ``` 其中「非缓存输入 = 输入 Token − 缓存 Token」。因此仪表板概览里的「输入 Token」显示的是**扣除缓存后的真实输入量**,与 API 返回的原始 `input_tokens` 不同。 > - 未设置价格的模型不参与费用统计,花费列与总花费显示 `-` > - 某个模型首次产生请求时会自动生成一条全零的初始价格(不可删除);想修改初始价格请用「覆盖」 ### 4. 数据目录 「左侧导航 → 数据目录」中可: - 查看当前数据目录路径与目录状态(存在 / 不存在) - 查看目录内的数据文件与体积,「打开目录」直接在文件管理器中定位 - 在线修改数据目录路径,保存后立即切换(**数据不会自动迁移**,新数据写入新目录) 目录里的子目录由程序托管,并**跟着数据目录一起走**:`2api/` 存代理资源 (在「代理模块」里添加的代理 exe 会自动拷一份进来,源文件保留原地)。 模型头像不再落盘文件,直接以 base64 存进数据库(`model_avatars` 表)。 --- ### 5. 代理模块(接入外部 exe) 本程序只监听本机。要让**局域网里的其它机器**或**公网**用上 API 中转,中间需要一个代理, 代理能力由**外部 exe** 提供。在「左侧导航 → 代理模块」里: 1. 点「添加模块」,点「代理 exe 路径」右侧的按钮用**系统文件选择器**选中 exe (例如 `D:/tools/frpc.exe`,不支持手动输入);**注入端口**会从 9000 起自动取号 (全局唯一,监听端口由宿主经 `TT_RP_PORT` 注入,模块在它上面开放地址); 按需填**启动参数**(原样传给 exe,模块自行解析) —— 保存时会自动把这份 exe 复制到「数据目录 / `2api/`」统一托管(源文件保留原地, 界面上显示、程序实际启动的都是归档副本;换数据目录时副本跟着走,存量模块在启动时自动收编)。 副本按 exe 原名保存:同名同内容直接复用、同名不同内容**原地覆盖**(没有被别的模块占用时), 删除模块时也会把没人再用的副本一并清掉 —— 不会越攒越多,也不会出现 `xx-1.exe` / `xx-2.exe` 这种「加一次多一个」的命名 2. 按需勾选「随程序启动」,保存后点卡片上的「启动」——本程序会拉起这个 exe, 并通过 **WebSocket 控制通道**跟它握手 模块**只监听 `127.0.0.1:TT_RP_PORT`**,不对外开放;它把请求转给**自己上报的真实上游** (`hello.upstream`,宿主据此展示与落库)。要指向别的服务时用模块自己的启动参数 (参考实现支持 `--target`)。 卡片上的状态灯就是心跳结果:**正常**(心跳往返正常,显示延迟)/ **心跳异常**(进程还在但连续超时)/ **控制通道断开**(没连回来或连接断了)/ **异常退出**(进程挂了,可配失败自动重启)。 点「日志」能同时看到 exe 输出与本程序记录的状态事件。 模块必须遵守 **TT-RP v1.0-alpha 协议**(契约全文见 [`proxy-schema.md`](proxy-schema.md): 注入端口 / 真实上游上报 / 系统提示词改写 / 日志只打启动与报错), 仓库里附了一个零依赖的参考实现 `reverse_proxy_demo.py`(可用 PyInstaller 打成 exe 直接接入)。 ## API 中转(一个入口用多个套餐) 如果你同时买了多个套餐(各家 coding plan、中转站等),**API 中转**把它们收进**一个本地入口**: 客户端只配一个 Base URL,网关按请求里的 `model` 自动选套餐、注入该套餐的 Key,用量同样按套餐统计。 本来每个上游要开一个本地代理端口,现在只需要这一个入口 —— 所以「代理管理」已经整体并进来了: 老库里的每条代理都会自动变成一个同名套餐,从前的用量也就自然落在同一张卡片上。 在「左侧导航 → API 中转」中配置: 1. 中转端口**写死为 8888** —— 对外入口恒为 `http://127.0.0.1:8888`,界面上没有端口设置项 2. 点「添加套餐」:套餐本身只有 **名称**(卡片标题 / 用量与费用的统计单位)与 **状态**(启用 / 停用); 真正的上游在下一步里加 3. 点套餐卡片进入**二级界面**,一共三块: **上游** —— 点「添加上游」(或某条上游的「编辑」)**进入三级编辑页**,在那里填一条上游: 「一个地址 + 一把 Key + 一组模型」 - **名称** — 这条上游在用量里的「提供商」标签(如 `kimi` / `kimiwork`),**全局唯一** - **Base URL** — 该上游的**完整地址(含版本段)**,如 `https://open.bigmodel.cn/api/coding/paas/v4` 或 `https://api.moonshot.cn/v1`;网关只在这后面接 `/chat/completions`,所以别把版本段漏掉 - **API Key** — 该上游自己的 Key(只存在本机数据库,界面只显示掩码 `sk-z…1234`;留空保存表示不修改,点「清除」可显式清空) - **模型映射** — 每行一条,只填 **modelId**;对外模型名由系统自动拼成 `modelId@上游名`(见下方「模型映射规则」)。 每条映射可以单独**停用 / 启用**:停用的映射不路由(客户端请求旧对外名按「模型不存在」处理), 名下历史用量继续按旧展示名独立统计 —— 上游真实模型没变、只是换了展示名时, **停用旧映射 + 新增一条新展示名**即可替代(新旧两条统计线分开,见下方「模型映射规则」) - **启用** — 临时停用某条上游(它的模型会返回 503) - **依赖代理** — 勾上表示这条上游是**经由代理模块**访问的(见 [代理模块](#代理模块tt-rp-v10-alpha))。 勾上后「Base URL」变成下拉框里选一个**代理模块**(地址由模块现算,不用手填): 绑定关系记的是**模块名**,与上游名、套餐名(厂商)都无关 —— 模块改了注入端口会自动跟上,**删除模块也不会把绑定改回默认**(界面上照旧显示 「代理 · 模块名」,同名模块再加回来就自动接上)。此时用量记录里的「路径」取 **代理模块上报的真实上游**(`hello.upstream`,含厂商版本段)拼上请求路径, 而不是网关自己拼出的地址;用量仍按**上游名**归档,换代理不影响数据汇总。 不勾则照旧记完整上游地址。 三级编辑页顶部是「‹ 返回套餐详情」,保存 / 取消都会回到二级界面。 **购买 / 续费周期** —— 逐条记录每一次购买 / 续费: - **开始日期 / 到期日期**(到期留空 = 不限,不会判过期) - **价格**(元)与**备注**(如「首发」「续费」「升级」) - 卡片与列表顶部的「套餐总价 / 有效 / 已过期 / 最近到期」都由此汇总而来; **已过期的套餐也会算进总价**,只是单独标上「过期」并压暗整张卡片 - 套餐状态还能在**套餐设置**(详情页标题行右侧的「编辑」)里**手动单选**「自动 / 有效 / 过期」: 自动就按最晚的到期日判定,手动则按你选的来(比如中途退订、或还没到期就不想用了) **套餐设置** —— 改名称 / 状态,以及删除套餐:入口就在详情页**标题那一行**右侧的「编辑 / 删除套餐」, 不再单独占卡片体最下方的一块 4. 点「启动中转」 然后把客户端指向这一个地址: ``` Base URL : http://127.0.0.1:8888 ← 不带 /v1 API Key : 任意(客户端填的 Key 会被替换为套餐自己的 Key) Model : modelId@上游名 ← 对外模型名,如「glm-4.6@kimiwork」 ``` ### 模型映射规则 - 映射里只填 **modelId**(如 `glm-4.6`);网关对外暴露的名字是 **`modelId@上游名`** (上游「kimiwork」下的 `glm-4.6` → 客户端请求 `glm-4.6@kimiwork`),界面上也是这么显示的 - **对外名跟着上游名走,不跟套餐名走**:上游名跨套餐唯一,所以对外名也天然唯一; 改套餐名不影响任何客户端请求名 - 因此**不同套餐 / 上游可以各自提供同一个模型**:都配 `glm-4.6` 不会冲突, 客户端按 `@上游名` 后缀选其中一份(中转不再提供 `/v1/models` 列表,名字要客户端自己填) - 不写上游模型名时,默认**去掉「厂商/」前缀**(`zhipu/glm-4.6` → 上游收到 `glm-4.6`); 需要别的名字就显式写 `zhipu/glm-4.6 = glm-4.6-air`(右边那栏是「上游模型名」) - **落库只留裸 model-id**:上游模型名写成带前缀的形式(`cli/x1 = cline-pass/x1`)时, 转发给上游的仍是完整的 `cline-pass/x1`(映射照旧、路由照旧),但用量表里只记 `x1` —— 一个上游换个写法不会让同一个模型在「模型用量」里裂成两条 - **同一个上游内不能有重复的 modelId**(对外名相同),保存时直接报冲突 - **同一个上游内,一个真实模型名最多一条「启用」映射**(2026-10-26):直接添加时撞上 重复的真实名会被拦下并提示**先停用之前那条**(前端添加 / 改名 / 重新启用时本地先查一遍, 保存时后端再校验一次,双保险);把旧映射停用后再新增即可替代它 - **展示名原地改名 = 新统计实体,历史不动**(2026-10-28 起,取代 2026-10-26 的「改名 = 历史用量跟着挪」口径): 上游一个模型 id 换绑到别的模型、或展示名直接改名时,旧展示名名下的**全部历史数据**(请求明细 traces 与 日 / 小时 / 模型 / 会话×模型 各维聚合)和模型头像都**原样保留**,新展示名作为全新统计实体从零开始进 「模型管理」「模型用量」;**删除映射同样只删配置、不删已记录的用量** —— 有数据的历史永远不被改写或清除 - 停用的映射不进路由表、也不参与直连流量的展示名归并(`display_model_of` 只认启用映射), 但继续出现在映射列表与「模型用量」卡片里(带「已停用」标记,随时可再启用) - 请求里的模型没有任何上游提供时返回 **404**,响应里带 `available_models` 方便排查 (**已停用**的映射同样不在 `available_models` 里) - 套餐 / 上游被**停用**时它的模型返回 **503**(而不是悄悄切到别的套餐) ### 行为说明 - **只放行 `/chat/completions` 一个端点**,客户端 Base URL 填 `http://host:port`(**不带 `/v1`**), 不做 Anthropic / Responses 协议转换;其它路径(`/v1/chat/completions`、`/v1/models`、`/v1/embeddings`、文件 / 用量类…)一律 **404** - **上游地址 = Base URL + `/chat/completions`**:Base URL 请写成**完整地址(含版本段)**, 客户端路径**原样追加**,网关自己不拼任何版本段,所以 `.../paas/v4` + `/chat/completions` → `.../paas/v4/chat/completions`, `https://api.moonshot.cn/v1` + `/chat/completions` → `https://api.moonshot.cn/v1/chat/completions` - **中转设置只有端口一个**:对外路径固定为 `/chat/completions`,不提供自定义路径前缀 - 请求体里的 `model` 会被改写为该套餐的上游模型名后再转发;改写后 `Content-Length` 会重算,不会把上游卡住 - 用量记在**上游名下**(模型名记上游真实模型名的**裸 id**,带前缀的写法会被剥掉), 套餐卡片上显示的是它名下所有上游的**合计**; 因此「模型用量」「会话明细」天然按上游(厂商)分开,二级界面里还能看到每个上游各自的消耗 - **一个套餐可以有多条上游**:同一份订阅的两个入口(`kimi` + `kimiwork`)放进同一个套餐, 卡片上算一份钱、用量合计,明细里仍能看到两个厂商各自的消耗 - **删除套餐不会删除已记录的用量**(元数据改动不该抹掉历史);改写**上游名**才是改用量标签, 改套餐名不会动历史(上游名是独立的「厂商名」),但会**重贴对外模型名的套餐名前缀** - 套餐以**卡片**形式排列(一行两张),点卡片进入二级界面:**套餐设置**(编辑 / 删除套餐)在标题行右侧, 卡片体里依次是**上游** → **购买 / 续费周期**; 上游的编辑在**三级编辑页**里做(点「添加上游」/「编辑」进入,保存或取消回到二级界面) - 卡片上直接给出**上游数 / 模型数 / 周期数 / 价格合计 / 有效期至 / 用量**,并按「全部 / 有效 / 过期」单选筛选 - 每条上游都有「测试」按钮:通过本机中转发一条最小对话请求(`max_tokens:1`),用来确认地址与 Key 是否正确 --- ## 仪表板说明 页面是「左侧导航 + 内容区」的多页面布局:**没有设置弹窗**,设置项(模型管理 / 数据目录) 就是左侧导航里的普通页面。导航栏吸顶且高度跟着视口走,所以滚动时始终占满整列;内容多高页面就多高,整页自然滚动。 左侧导航共 6 项,顺次排成一列(不分组): **用量统计** · **模型用量** · **API 中转** · **代理模块** · **模型管理** · **数据目录** 当前页会记在浏览器本地,并同步到地址栏 hash(如 `#stats`、`#pricing`),刷新或直接粘链接都能回到同一页; 打开某个模型的明细时 hash 会变成 `#models/<模型名>`,深链接可以直接打开那张明细。 **模型用量** / **API 中转** 两项目标右侧会显示数量徽标;窗口变窄(≤900px)时导航自动折成横向一排。 ### 页面区域 - **用量统计**:总 Token、输入 / 输出 / 缓存 Token、总花费、总请求数(含会话数)、使用模型数 (**与「模型用量」页同口径**:同一个模型被多个厂商提供只算一种,配了展示名的按展示名归并);下面依次是 「每日使用热力图」、「数据趋势」与「会话明细」三张带标题的卡片(一张卡一件事,不需要在页面里长距离滚动找) - **每日使用热力图**(在用量统计页):GitHub 贡献图风格,一列一周、一行一天,按当日 Token 总量分位着色;右上角切换年份,无数据日期为空白灰格;悬浮查看日期与用量,底部显示该年总用量与有使用的天数 - **数据趋势**(在用量统计页):日 / 周 / 月粒度图表,可按模型筛选;日粒度支持指定小时区间,周 / 月粒度支持「开始~结束日期」范围筛选;「用量 / 性能」切换按钮可切到性能视图 —— 左轴输出速度(TPS)、右轴首Tok延迟(TTFT)双轴趋势图(无样本时段断线);图表上方展示区间汇总 —— Token 占比、首Tok延迟 / 输出速度(带同色圆点,无调用显示 0)、金额放最后 - **会话明细**(在用量统计页最下方):按会话分组的用量与花费,分页展示,悬停可查看该会话下各模型调用明细 - **模型用量**:一个模型一张卡片(按 Token 数降序,**一行两张**),卡片左侧是**模型头像** (没传图时用该模型主色的首字母),右侧是**模型名 / 用量 / 花费 / 请求数**与输入·输出·缓存分布条; **点头像即可上传 / 更换头像**(悬浮头像右上角会出现移除按钮), **点卡片进入二级界面**看**各个厂商(provider)在这个模型下的消耗与花费** (各厂商的「花费」与卡片上的花费同色),左上角「返回模型列表」回一级。详见下方「模型用量怎么汇总」 - **API 中转**:顶部是启停按钮 + **写死的端点**(`http://host:8888`)与**套餐总价、有效、过期、最近到期**的汇总,按「全部 / 有效 / 过期」单选筛选; 下面是**套餐卡片**(一行两张,卡片上给价格合计 / 有效期至 / 周期数 / 模型数 / 用量)。 **点卡片进入二级界面**:标题那一行右侧就是**套餐设置**的入口(「编辑」改名称 / 状态,「删除套餐」直接删), 卡片体里依次是**上游**列表(每条带测试 / 编辑 / 删除)与**购买 / 续费周期**(逐条填时间段与价格,可增删改)。 **上游的编辑在三级编辑页**(点「添加上游」或某条的「编辑」进入):名称 / Base URL / API Key / 模型映射 / 启用, 保存或取消回到二级界面 ### 模型用量怎么汇总 「模型用量」页按**模型 id** 汇总,而不是按「厂商 / 模型」拆开——同一个模型无论走哪个套餐, 都并到同一张卡片里: - 分组键 = **该模型的展示名(别称);没配别称时用真实模型名** - 所以把几个模型都设成同一个展示名,它们会归并成一张卡片(例如同一个模型被不同上游报成 `glm-4.6` 与 `glm-4.6-air`,都改名成 `GLM-4.6` 即可合到一起) - 卡片标题右侧的「N 个来源」= 参与这张卡的 (厂商, 模型) 组合数;鼠标悬浮卡片名可看到组内所有真实模型名 - **后端存储与统计口径**:`traces` / `model_summary` 里是真实模型名 + provider,但模型名只保留 **裸 id**(`厂商/model-id` → `model-id`;上游若配了 `cli/x1 = cline-pass/x1` 这种带前缀的名字也一并剥掉, 见上文「模型映射规则」)。启动时会做一次幂等折叠,把老版本存下的带前缀的行合并到裸名下。 别称只存在 `model_aliases`,只影响这里的展示与归并;顶部的趋势图、模型筛选、会话明细也都还是按真实名走 - 花费同样是把组内各来源的花费相加(价格按**裸模型名**生效,同一个模型在各来源用同一份单价) - 排序按 Token 总量降序,但不显示名次;**点卡片进入二级界面**,即可看到各厂商的 输入 / 输出 / 缓存 / Token / 花费 (二级界面同样给出该模型的 用量 / 花费 / 请求数 与分布条;别称归并了多个真实模型时,还会列出参与归并的模型 id) ### 模型头像 给模型配个图标(例如各家厂商的 logo),一眼就能认出是哪家的模型: - **上传**:点卡片左侧的圆形头像(或二级界面标题上的头像)→ 选一张本地图片。 图片会在浏览器里**居中裁成方形并压到 128px 见方**再上传,所以随便丢一张大图也没问题 - **更换 / 移除**:鼠标悬浮头像后,右上角会出现一个「×」,点它即可移除(有二次确认) - **没传图时**:显示该模型主色的**首字母**圆形徽标(主色与卡片左侧高亮条、分布条同色系) - 头像按**卡片的分组键**(展示名;没配别称时是真实模型名)存取,所以「同一张卡」的多个厂商共用一张头像; 中转映射**原地改展示名**时头像会尽量跟着挪到新名下(新名已有头像、或旧展示名还被别的启用映射用着时不挪, 那种情况需要重新上传一次) - 头像以 base64 存在数据库表 `model_avatars` 里(不写文件副本),接口只回索引 (哪些 key 有图 + 版本),图片本身走 `/api/avatar?key=...`, 所以页面 5 秒刷新不会重复下载图片 ### 顶部工具栏 | 按钮 | 作用 | |------|------| | 状态指示 | 显示 API 中转的运行状态(「API 中转运行中 / 未启动」) | | 刷新数据 | 立即刷新页面数据 | | 主题切换 | 在浅色 / 深色主题间切换(首次加载跟随系统偏好,并记忆上次选择) | | 关闭 | 停止 API 中转并退出程序(有二次确认) | --- ## API 端点 仪表板同时提供完整的 JSON API(已开启 CORS,可被外部页面直接嵌入调用)。 ### 数据查询 | 端点 | 说明 | |------|------| | `GET /api/stats` | 总体统计信息 | | `GET /api/models` | 模型使用统计(`model` 始终是真实模型名,设置展示名时额外返回 `display_name`) | | `GET /api/sessions` | 会话列表 | | `GET /api/session/{id}` | 单个会话详情 | | `GET /api/daily?days=30&model=xxx&provider=xxx` | 每日使用统计(支持模型 / 提供商筛选) | | `GET /api/daily-with-cost?days=30&model=xxx&provider=xxx` | 每日统计(含服务端预计算费用) | | `GET /api/hourly?date=YYYY-MM-DD&model=xxx&provider=xxx` | 逐小时使用统计 | | `GET /api/hourly-with-cost?date=YYYY-MM-DD&model=xxx&provider=xxx` | 逐小时统计(含费用) | ### API 中转 | 端点 | 说明 | |------|------| | `GET /api/gateway` | 中转状态(`running` / `port` / `base_url`,端口写死 8888、无任何设置项)、套餐列表(Key 以掩码返回)与各套餐用量 | | `POST /api/gateway/packages` | 新增 / 修改单个套餐(含上游),请求体 `{"name","upstreams":[{"id"?,"name","base_url","api_key"?,"enabled","models":[{"model","upstream"}]}],"enabled","status","old_name"?}`;`models[].model` 填**裸 modelId**(对外名由服务端拼成 `modelId@上游名`);`upstreams` **省略** = 不动上游,空数组 = 清空 | | `POST /api/gateway/packages/delete/{name}` | 删除套餐及其上游 / 模型 / 周期(**不删除**已记录的用量) | | `POST /api/gateway/packages/test/{name}` | 探测该套餐的某条上游(可选 `{"upstream_id":N}`,省略 = 第一条),返回 `{ok,status,auth_ok,url,upstream,detail}` | | `POST /api/gateway/periods` | 新增 / 修改一条**购买 / 续费周期**:`{"package":"...","id":?,"start_at":"YYYY-MM-DD","end_at":"YYYY-MM-DD","price":199.5,"note":"续费"}`(`end_at` 空 = 不限;日期格式错误、到期早于开始都会被拒) | | `POST /api/gateway/periods/delete/{id}` | 删除一条周期记录(套餐本身保留) | | `POST /api/gateway/start` | 启动中转 | | `POST /api/gateway/stop` | 停止中转 | > 每条上游的 `api_key` **省略**表示保留已存 Key(浏览器永远拿不到真实 Key);传空字符串表示清空。 > 传 `old_name` 表示重命名套餐,其上游 / 模型 / 周期会一并改名(用量标签是上游名,不受影响)。 > 同一个 `model` 只能属于一个套餐,冲突时返回 `400 {"error":"模型「x」已被套餐「y」使用"}`; > 上游名同样全局唯一,冲突时返回 `400 {"error":"上游名「x」已被套餐「y」使用"}`。 中转对外提供的 API(客户端使用,不是管理接口): | 端点 | 说明 | |------|------| | `POST {base_url}/chat/completions` | 唯一对外端点:按 `model` 路由到对应套餐并转发到「Base URL + `/chat/completions`」 | > 其它路径(含 `/v1/chat/completions`、`/v1/models`)一律 **404** —— 中转只放行 `/chat/completions`。 ### 模型价格与展示名 | 端点 | 说明 | |------|------| | `GET /api/model-settings` | 返回各模型最新价格 `prices` 与展示名 `aliases` | | `POST /api/model-settings` | 单模型变更,`action` 取值见下表 | `POST /api/model-settings` 的请求体: | action | 请求体 | 说明 | |--------|--------|------| | `overwrite` | `{"action":"overwrite","model":"...","price":{...}}` | 以当前值替换该模型全部历史价格版本 | | `append` | `{"action":"append","model":"...","price":{...}}` | 保留历史版本,从当前时间起应用新价 | | `rename` | `{"action":"rename","model":"...","display_name":"...","provider":"..."}` | 设置展示名(仅前端展示,空值则清除);响应返回最新 `aliases` | | `delete` | `{"action":"delete","model":"...","provider":"..."}` | 删除模型及其全部数据;响应返回 `{"success":true,"traces":n,"prices":n,"sessions_removed":n}` | | `prune_prices` | `{"action":"prune_prices"}` | 清理残留定价:删掉在任何用量表里都查不到的模型的 `prices` / `model_aliases`;响应返回 `{"success":true,"prices":n,"aliases":n,"models":[...]}`(「模型管理」页打开时自动调用) | > `price` 支持的字段:`input` / `output` / `cache`(谷时价),以及可选的 `peak_enabled`、`peak_periods`、`peak_input` / `peak_output` / `peak_cache`。 > `provider` 可选:`delete` 传入时只删除该提供商下的数据,`rename` 传入时不改变展示名的跨提供商生效规则。 ### 模型头像 | 端点 | 说明 | |------|------| | `GET /api/avatars` | 返回 `{"avatars": {"<展示名>": "<更新时间>"}}` —— 只回索引,很小,页面每次刷新都会拉一次 | | `GET /api/avatar?key=<展示名>` | 返回该头像的原始图片字节(带 `Cache-Control`);不存在返回 404 | | `POST /api/avatars` | `{"action":"set","key":"<展示名>","image":"data:image/png;base64,..."}` 上传 / 覆盖;`{"action":"delete","key":"..."}` 移除 | > 图片由前端先居中裁成方形并压到 128px 见方再上传(PNG data URL),后端再校验一次类型 > (只收 png / jpeg / webp / gif)与体积(≤ 512KB 原图,约 700K base64 字符),超限直接拒。 > 因为是要塞进数据库的小图,**不要**改成把原图直接存进去。 ### 数据目录与系统 | 端点 | 说明 | |------|------| | `GET /api/data-dir` | 数据目录信息(路径 / 是否存在 / 文件列表) | | `POST /api/data-dir` | 修改数据目录(请求体 `{"path": "..."}`,自动建目录并重载数据) | | `POST /api/data-dir/open` | 在系统文件管理器中打开数据目录 | | `POST /api/shutdown` | 停止 API 中转并退出应用 | ### 代理模块(TT-RP v1.0-alpha) | 端点 | 说明 | |------|------| | `GET /api/reverse-proxies` | 模块列表 + 运行时状态(状态机 / 心跳延迟 / 最后心跳 / 退出码 / 注入端口)+ 标准说明 | | `POST /api/reverse-proxies` | 新增 / 修改模块(`{id?, name, exe_path, args?, port, enabled?, autostart?, restart_on_failure?}`;`port` 注入端口全局唯一) | | `POST /api/reverse-proxies/start/{id}` | 启动模块(拉起 exe,等它接入控制通道) | | `POST /api/reverse-proxies/stop/{id}` | 停止模块(请求退出 → terminate → kill,每步都有界) | | `POST /api/reverse-proxies/restart/{id}` | 重启模块 | | `POST /api/reverse-proxies/delete/{id}` | 先停进程再删配置 | | `GET /api/reverse-proxies/log/{id}?lines=200` | exe 输出 + 状态事件 | | `POST /api/reverse-proxies/open/{id}` | 在文件管理器里定位代理 exe | | `POST /api/reverse-proxies/pick-exe` | 弹出**系统文件选择器**选一个 exe(返回完整路径;取消时 `path` 为 null) | | `GET /ws/reverse-proxy?id=&token=` | **控制通道**(WebSocket):模块启动后连回来握手 + 心跳,令牌不对返回 403 | --- ## 数据存储 所有数据保存在数据目录下的 `token_usage.db`(SQLite,WAL 模式,多线程访问安全,统计汇总在写入时实时聚合)。 ### 数据库表 | 表名 | 说明 | |------|------| | `traces` | 每条请求的原始记录(时间 / 会话 / 提供商 / 模型 / token 等;模型列只存**裸 id**) | | `daily_summary` | 按 (日期, provider, model) 聚合的日统计 | | `hourly_summary` | 按 (小时, provider, model) 聚合的小时统计 | | `model_summary` | 按 (provider, model) 聚合的总统计 | | `sessions` | 会话级统计(按首请 messages 数组锚点聚类;同一锚点的续传 / 重放合并为一行) | | `session_models` | 会话 × 提供商 × 模型的聚合明细 | | `sessions_key` | 会话锚点(首请第 min(20, len) 层 messages 数组链哈希)→ 会话 ID 映射 | | `prices` | 模型定价(按时间分版本的单价,含峰谷字段) | | `model_aliases` | 模型展示名(仅影响仪表板展示,真实模型名不变) | | `gateway_periods` | 套餐的购买 / 续费周期(起止日期 + 价格 + 备注;套餐级价格与有效期由它汇总) | | `gateway_packages` | API 中转套餐(名称 / 是否启用 / 状态:按有效期自动或手动有效·过期) | | `gateway_upstreams` | 套餐下的**上游**(名称 / Base URL / API Key / 是否启用);一个套餐可有多条,全部地址与 Key 都住这里 | | `gateway_models` | 展示名映射:`(套餐, 上游id, display_model)` → 上游真实模型名 + 该条映射的 `enabled` 停用开关(停用的不路由、不参与直连展示名归并);对外模型名 `modelId@上游名` 由服务端现算(不落库)。改展示名 / 换绑真实模型**不改写历史**:旧展示名的存量用量原样保留,新名从零开始统计(详见「模型映射规则」) | | `model_avatars` | 模型头像(键 = 「模型用量」卡片的分组键,存 mime + base64 小图) | | `reverse_proxies` | 代理模块(exe 路径 / 参数 / 自启与失败重启开关 / 控制通道握手令牌) | > 费用金额(`money` 列)在**写入时**按当时生效的价格版本预计算并存储,因此查看历史数据不依赖扫描 `traces`;修改价格后(覆盖 / 追加)会自动重算全部聚合。 ### 版本与迁移 - v1.1 起使用 SQLite 作为默认存储后端;当前版本 **v4.1** - 历史版本(v1.0 及更早)使用 JSON 文件:`token_usage.json` / `traces.jsonl` / `proxies.json` - 旧版数据库(v1 布局,如 `traces.target`)**无法原地升级**,需删除或改名 `token_usage.db` 后重新启动 - 已废弃的 `meta` / `session_meta` 等旧表会在启动时自动清理 --- ## 工作原理 1. **本地中转入口** — Token Tracker 作为本地代理服务器运行,在配置的端口上监听 2. **路径白名单** — **API 中转只放行 `/chat/completions` 一个端点**(客户端 Base URL 不带 `/v1`),其余路径一律 404; 3. **请求转发** — 转发到目标 API,过滤 hop-by-hop 头并强制 `Accept-Encoding: identity`(保证 SSE / JSON 可直接解析);请求体**不限制大小**(长对话、多模态、大型 tools schema 均可通过) 4. **响应分析** — 解析响应中的 token 使用信息: - **非流式**:直接解析响应体的 `usage` - **流式(SSE)**:按帧重组事件流并提取最终 `usage`(跨 TCP 分块也能正确处理) - 超时策略:流式请求**不设超时**(长思考 / 慢生成不会被中断);非流式请求总超时 600 秒、读取超时 300 秒 5. **会话识别** — 按请求的完整 `messages` 数组聚类: - **锚点**:每个会话在 `sessions_key` 里只存一行 —— **首请** messages 数组前 `min(20, len)` 条的链式哈希; - **匹配**:新请求计算自身第 1..20 层前缀链哈希,从最深往浅与库中锚点最长匹配 —— 命中即同会话 (追加式续传 / 逐字节重放),全 miss 才铸新会话(`session_<日>_<锚点hash8>`)并写入自己的锚点; - **数组逐字节全同才合并**:模板开头(system + 注入上下文)撞不到任何行,因此不再需要「首条 user 消息指纹」和「冷启动切分」; - **侧请求自成会话**:harness 的「生成会话标题」调用按同一套数组指纹自成一条会话(不再特判)。 6. **数据落库** — 只有请求体中**带 `model` 的调用**才算推理调用并记录(其余非模型类调用不落库,因此不会出现 `provider/unknown` 这类假模型)。模型名只留**裸 id**(`厂商/model-id` → `model-id`) 7. **费用计算** — 按请求发生时生效的价格版本(含峰谷时段)实时换算 8. **可视化** — 通过内置 Web 仪表板查看统计图表与明细 ### API 中转的转发流程 转发时按请求里的模型**选套餐**: 1. 客户端把请求发到中转端口(如 `http://127.0.0.1:8888/chat/completions`) 2. 从请求体取 `model`(**保留**「厂商/」前缀,不做剥前缀),在 `gateway_models` 里查它属于哪个套餐 3. 未命中 → 直接返回 404(附 `available_models`);套餐停用 → 503;**两种情况都不落库** 4. 命中 → 用该套餐的 Base URL(**完整地址,含版本段**)**原样**接上客户端的 `/chat/completions`,并把客户端带来的 `Authorization` / `x-api-key` **替换成该套餐自己的 Key** 5. 请求体的 `model` 改写为该套餐的上游模型名后转发(改写后重算 `Content-Length`) 6. 响应处理与转发引擎**共用一套代码**(`RelayServer`:流式 / 非流式路由、SSE 重组、超时策略、 落库唯一入口),用量记在**上游名**作为 provider 的名下,模型名记上游模型的**裸 id**(`cline-pass/x1` → `x1`) --- ## 项目结构 ``` token-tracker/ ├── README.md # 本文件 ├── LICENSE # MIT 许可证 ├── proxy-schema.md # 代理模块协议(TT-RP v1.0-alpha)规范全文 ├── .gitignore ├── web/ # 程序本体:可执行入口 + 运行数据 │ ├── token-tracker.exe # 打包好的主程序(双击运行,data/ 在同级) │ ├── start.bat # 一键启动:优先跑根目录 exe,没有则用源码起 backend │ ├── build-exe.bat # 一键打包:产物直落 web/ 根目录(中间产物进 tmp/) │ ├── requirements.txt # Python 运行依赖唯一清单(backend 所需) │ ├── code/ │ │ ├── backend/ # 后端源码(Python 依赖见 web/requirements.txt) │ │ │ ├── main.py # 入口:命令行参数、日志、GatewayManager 装配、DashboardServer 与全部 API 路由 │ │ │ ├── proxy.py # 转发引擎 RelayServer(路径白名单、流式 / 非流式响应、落库唯一出口) │ │ │ ├── gateway.py # API 中转:ApiGateway(单端口按 model 路由到多个套餐)与 GatewayManager(配置持久化 / 生命周期) │ │ │ ├── sse.py # SSE 事件流重组器(跨分块解析 usage 帧) │ │ │ ├── usage.py # usage 归一化:模型提取、token 字段归一、会话指纹哈希 │ │ │ ├── storage.py # TokenStorage:SQLite 读写、聚合、费用计算、模型删除与迁移 │ │ │ ├── storage_sql.py # 纯 SQL 常量(建表 / 查询语句) │ │ │ ├── reverseproxy.py # 代理模块托管:进程生命周期 + WebSocket 控制通道 + 心跳判活 │ │ │ ├── reverse_proxy_demo.py # TT-RP v1.0-alpha 参考实现(零依赖,可打包成 exe) │ │ │ └── smoke_test.py # 冒烟测试(转发全链路 / 网关路由 / 网关 HTTP API / 代理模块 / 仪表板 WS 五段) │ │ ├── frontend/ # 仪表板前端 │ │ │ ├── index.html # 单文件仪表板(Vue 3 + Element Plus,CDN 引入) │ │ │ └── assets/ # 静态资源(favicon / 标题图) │ │ └── build/ # 打包程序 │ │ ├── token-tracker.spec # PyInstaller 打包配置(入口 code/backend/main.py) │ │ └── requirements.txt # 仅 PyInstaller 等打包工具(运行依赖在 web/) │ ├── data/ # 运行数据目录(首次运行自动创建,已 gitignore) │ └── tmp/ # 打包中间产物 / 临时文件(已 gitignore) └── tmp/ # 临时文件:DOM 快照 / 调试日志等(已 gitignore) ``` > **web 版按标准 Python 软件结构组织**:`web/` 根目录只放 `start.bat` / `build-exe.bat` / `requirements.txt` / exe 与 `code/`、`data/`、`tmp/`,代码全部进 `code/`(`backend/` 后端、`frontend/` 前端、`build/` 打包);README 等说明文档统一放仓库根目录,不进 `web/`。数据目录固定为 `web/data`,与启动时的工作目录无关。 --- ## 常见问题 **Q:仪表板页面空白 / 图表不出来?** 仪表板前端通过 CDN(unpkg)加载 Vue 3 与 Element Plus。**首次打开需要联网**;若网络受限或 CDN 不可达,页面会停留在空白状态。此时可把 5 个前端文件下载到本地并内联进 `index.html`(数据 API 本身始终是本地的)。 **Q:客户端调用返回 404?** 中转**只放行 `/chat/completions`**,客户端 Base URL 填 `http://host:port`(不带 `/v1`)。 如果客户端请求的是别的路径(`/v1/chat/completions`、`/v1/models`、`/v1/embeddings`、`/v1/responses`、文件 / 用量类接口…), 一律返回 404 —— 这是设计如此,中转不做协议转换,也不提供模型列表。 **Q:API 中转报「没有套餐提供模型 xxx」?** 中转按请求体里的 `model` **精确匹配**「对外模型名」,也就是 `modelId@上游名`。请确认:① 客户端填的模型名与中转页里显示的一致(**含 `@上游名` 后缀**,如 `glm-4.6@kimiwork`);② 该套餐 / 上游处于「启用」状态。响应里的 `available_models` 列出了当前所有可用模型名。 **Q:API 中转返回 401 / 403?** 说明上游拒绝了请求。点该套餐行的「测试」按钮看具体状态:`auth_ok=false` 即 Key 不对。也请确认 **Base URL 写的是完整地址(含版本段)**,如 `.../paas/v4` 或 `https://api.moonshot.cn/v1` —— 网关只在这后面接 `/chat/completions`;客户端自己的 Base URL 不要再带 `/v1`(带了会 404)。 **Q:两个套餐想配同一个模型名?** 直接配就行。映射里填的是**裸 modelId**,对外名由服务端自动带上 `@上游名` 后缀(`modelId@上游名`),所以两个套餐都配 `glm-4.6` 不会冲突 —— 客户端用 `glm-4.6@上游A` / `glm-4.6@上游B` 分别走它们。 只有**同一个上游内**重复的 modelId 会被拒绝(对外名相同 = 路由不唯一)。 **Q:模型用量里为什么一张卡片挂着好几个厂商?想把两个模型合成一张卡怎么办?** 「模型用量」按模型 id 汇总,所以同一个模型被多个套餐调用时天然并成一张卡,点开就是各厂商明细。 如果两个**不同**的模型 id 想合成一张卡,到「左侧导航 → 模型管理」把它们的展示名改成同一个即可(后端仍按真实名存储)。 **Q:仪表板里没有数据 / Token 全是 0?** 按顺序排查: 1. API 中转是否已「启动」(顶部状态指示) 2. 客户端的 base URL 是否指向了中转端口(不是仪表板端口) 3. 请求体里是否带 `model` 字段——不带 model 的调用不会落库;走中转时模型名还必须与某个套餐的对外模型名匹配 4. 上游是否返回了 `usage`:部分服务商需要显式开启(如流式请求的 `stream_options.include_usage`)才回传用量,此时请求数会记录、Token 列保持 0 **Q:会话数比预期多?** 会话按请求的完整 `messages` 数组聚类:只有数组逐字节相同(续传 / 重放)才合并。同一会话若改写了历史消息(编辑开场、切换 system 上下文),会被识别为新会话;生成标题的侧请求也会各自成一条会话。 **Q:总花费显示 `-`?** 该模型尚未配置价格。到「左侧导航 → 模型管理」打开该模型的「详情」填入单价后即可显示。 **Q:端口被占用启动失败?** 用 `--dashboard-port` 换仪表板端口;**API 中转端口写死为 `8888`**,改不了 —— 被占用时请先释放该端口。 **Q:怎么导出/备份数据?** 直接复制数据目录下的 `token_usage.db` 即可(建议先停止程序,或使用 SQLite 在线备份,避免 WAL 未落盘)。 **Q:能监控多台机器的用量吗?** 可以。用 `--host 0.0.0.0` 或指定网卡地址启动,让其他机器的客户端指向本机 IP;注意这会同时把仪表板暴露到局域网。 --- ## 安全与隐私 - **不记录客户端凭证**:落库时请求头中的 `Authorization`、`Cookie`、`X-Api-Key` 等敏感字段会被过滤,不写入数据库 - **API 中转的套餐 Key 会落库**:中转需要用它替换客户端凭证,因此套餐 Key 以明文存于本机 `token_usage.db`(数据库 API 只返回掩码 `sk-z…1234`,浏览器永远拿不到完整 Key)。请把数据目录放在可信位置 - **日志不落盘**:运行日志只输出到控制台,不会在数据目录留下任何日志文件 - **数据留在本机**:所有数据仅存于本地 SQLite,不上传任何服务器 - **上传的模型头像是本地文件**:只写进本机 `token_usage.db` 的 `model_avatars` 表(浏览器里已压缩),不会外传 - **默认只监听本机**:API 中转与仪表板默认绑定 `127.0.0.1` - 仪表板 API 已开启 CORS(反射请求 Origin),便于嵌入外部页面——因为只监听本机,这是安全的;若改为对外监听请自行评估风险 ## 开发与测试 所有命令都在 `web/` 目录下执行: ```bash cd web # 冒烟测试(五段:转发全链路 / 网关路由 / 网关 HTTP API / 代理模块 / 仪表板 WS) python code\backend\smoke_test.py # 带详细日志启动(DEBUG,仅控制台) python code\backend\main.py --verbose ``` `smoke_test.py` 需要 `aiohttp`,会自行拉起 mock 上游与本地监听端口(18901–18963), **分五段**验证:① 响应形态路由(声明流式 / 未声明却流式 / 忽略流式 / 误标 SSE / 非 JSON)、会话聚类、 聚合与费用;② 网关的 `model` → 套餐路由、模型改写、Key 注入、只放行 `/chat/completions`(其余 404)、404 / 503 分支; ③ `/api/gateway` 的套餐增删改、Key 掩码、写死的端口、改名重贴模型名前缀、同一个 modelId 被两个套餐提供; ④ 代理模块(拉起外部 exe → WebSocket 握手 → 心跳往返 → 真的转发 → 令牌错 403 → 心跳超时判 `unhealthy` 但不杀进程 → 停止 / 删除);⑤ 仪表板 `/ws` 推送通道。 ## 许可证 [MIT License](LICENSE)