Spaces:
Paused
Paused
icebear0828 Claude Opus 4.6 commited on
Commit ·
2996fd8
1
Parent(s): 60509b4
feat: display secondary rate limit window (weekly cap) in dashboard
Browse filesAdd secondary_rate_limit to CodexQuota type and map secondary_window
from Codex usage API. Dashboard now shows both primary (hourly) and
secondary (weekly) quota bars with independent progress and reset times.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- README.md +92 -28
- package-lock.json +392 -2
- package.json +1 -0
- shared/i18n/translations.ts +2 -0
- shared/types.d.ts +59 -0
- shared/types.js +1 -0
- src/auth/__tests__/account-pool.test.ts +0 -292
- src/auth/types.ts +12 -4
- src/proxy/__tests__/codex-api.test.ts +0 -201
- src/routes/accounts.ts +9 -0
- src/translation/__tests__/codex-event-extractor.test.ts +0 -182
- web/src/components/AccountCard.tsx +85 -28
- web/src/hooks/use-accounts.ts +10 -6
README.md
CHANGED
|
@@ -36,62 +36,124 @@
|
|
| 36 |
|
| 37 |
## 🚀 快速开始 (Quick Start)
|
| 38 |
|
| 39 |
-
|
| 40 |
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|------|--------|
|
| 45 |
-
| Windows | `Codex Proxy Setup x.x.x.exe` |
|
| 46 |
-
| macOS | `Codex Proxy-x.x.x.dmg` |
|
| 47 |
-
| Linux | `Codex Proxy-x.x.x.AppImage` |
|
| 48 |
|
| 49 |
-
|
| 50 |
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
```bash
|
| 54 |
git clone https://github.com/icebear0828/codex-proxy.git
|
| 55 |
cd codex-proxy
|
| 56 |
```
|
| 57 |
|
| 58 |
-
|
|
|
|
|
|
|
| 59 |
|
| 60 |
```bash
|
| 61 |
-
cp .env.example .env #
|
| 62 |
-
docker compose up -d
|
| 63 |
-
# 打开 http://localhost:8080 登录
|
| 64 |
```
|
| 65 |
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
-
|
| 71 |
|
| 72 |
```bash
|
| 73 |
-
npm install
|
| 74 |
-
cd web && npm install && cd ..
|
| 75 |
-
npm run dev # 开发模式(热重载)
|
| 76 |
-
# 或:npm run build && npm start # 生产模式
|
| 77 |
```
|
| 78 |
|
| 79 |
-
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
-
|
| 82 |
|
| 83 |
```bash
|
| 84 |
-
npm
|
| 85 |
-
|
| 86 |
-
npm run dev # 开发模式(热重载)
|
| 87 |
```
|
| 88 |
|
| 89 |
-
|
| 90 |
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
```bash
|
| 94 |
-
# 打开 http://localhost:8080 使用 ChatGPT 账号登录,然后:
|
| 95 |
curl http://localhost:8080/v1/chat/completions \
|
| 96 |
-H "Content-Type: application/json" \
|
| 97 |
-d '{
|
|
@@ -101,6 +163,8 @@ curl http://localhost:8080/v1/chat/completions \
|
|
| 101 |
}'
|
| 102 |
```
|
| 103 |
|
|
|
|
|
|
|
| 104 |
## 🌟 核心功能 (Features)
|
| 105 |
|
| 106 |
### 1. 🔌 全协议兼容 (Multi-Protocol API)
|
|
|
|
| 36 |
|
| 37 |
## 🚀 快速开始 (Quick Start)
|
| 38 |
|
| 39 |
+
> **前置条件**:你需要一个 ChatGPT 账号(免费账号即可)。如果还没有,先去 [chat.openai.com](https://chat.openai.com) 注册一个。
|
| 40 |
|
| 41 |
+
---
|
| 42 |
+
|
| 43 |
+
### 方式一:桌面应用(推荐新手,最简单)
|
| 44 |
+
|
| 45 |
+
像安装 QQ、微信一样,下载 → 安装 → 打开就能用。
|
| 46 |
+
|
| 47 |
+
**第 1 步:下载安装包**
|
| 48 |
+
|
| 49 |
+
打开 👉 [下载页面](https://github.com/icebear0828/codex-proxy/releases),找到最新版本,根据你的电脑系统下载对应文件:
|
| 50 |
+
|
| 51 |
+
| 你的电脑系统 | 下载哪个文件 | 怎么判断系统 |
|
| 52 |
+
|-------------|-------------|-------------|
|
| 53 |
+
| Windows | `Codex Proxy Setup x.x.x.exe` | 大部分人用的就是 Windows |
|
| 54 |
+
| macOS(苹果电脑) | `Codex Proxy-x.x.x.dmg` | 带苹果 logo 的笔记本/台式机 |
|
| 55 |
+
| Linux | `Codex Proxy-x.x.x.AppImage` | 你如果用 Linux 你肯定知道 |
|
| 56 |
+
|
| 57 |
+
**第 2 步:安装**
|
| 58 |
+
|
| 59 |
+
- **Windows**:双击下载的 `.exe` 文件,一路点"下一步"直到安装完成
|
| 60 |
+
- **macOS**:双击 `.dmg` 文件,把图标拖到 Applications 文件夹里
|
| 61 |
+
|
| 62 |
+
**第 3 步:打开并登录**
|
| 63 |
+
|
| 64 |
+
1. 打开刚装好的「Codex Proxy」应用
|
| 65 |
+
2. 点击页面上的登录按钮,用你的 ChatGPT 账号登录
|
| 66 |
+
3. 登录成功后,代理服务就已经在运行了
|
| 67 |
|
| 68 |
+
**第 4 步:验证是否成功**
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
+
打开浏览器,在地址栏输入 `http://localhost:8080`,如果能看到控制面板页面,说明一切正常。
|
| 71 |
|
| 72 |
+
> 桌面端默认只允许本机访问(`127.0.0.1:8080`),你电脑上的 AI 客户端可以直接连。
|
| 73 |
+
|
| 74 |
+
---
|
| 75 |
+
|
| 76 |
+
### 方式二:Docker 部署(推荐服务器 / 进阶用户)
|
| 77 |
+
|
| 78 |
+
Docker 就像一个"打包好的盒子",不需要你自己装各种依赖,一条命令就能跑起来。
|
| 79 |
+
|
| 80 |
+
**前置条件**:电脑上已安装 Docker。如果没有,先去 [docker.com](https://www.docker.com/products/docker-desktop/) 下载安装 Docker Desktop。
|
| 81 |
+
|
| 82 |
+
**第 1 步:下载项目代码**
|
| 83 |
+
|
| 84 |
+
打开终端(Windows 搜索"cmd"或"PowerShell";macOS 搜索"终端"),输入:
|
| 85 |
|
| 86 |
```bash
|
| 87 |
git clone https://github.com/icebear0828/codex-proxy.git
|
| 88 |
cd codex-proxy
|
| 89 |
```
|
| 90 |
|
| 91 |
+
> 如果提示 `git` 不是命令,需要先安装 Git:去 [git-scm.com](https://git-scm.com/) 下载安装。
|
| 92 |
+
|
| 93 |
+
**第 2 步:创建配置文件并启动**
|
| 94 |
|
| 95 |
```bash
|
| 96 |
+
cp .env.example .env # 复制一份配置模板
|
| 97 |
+
docker compose up -d # 启动服务(后台运行)
|
|
|
|
| 98 |
```
|
| 99 |
|
| 100 |
+
**第 3 步:登录**
|
| 101 |
+
|
| 102 |
+
打开浏览器,访问 `http://localhost:8080`,用 ChatGPT 账号登录。
|
| 103 |
+
|
| 104 |
+
> 你的账号数据会保存在项目的 `data/` 文件夹里,重启不会丢失。
|
| 105 |
+
>
|
| 106 |
+
> **其他 Docker 容器要连本服务?** 用你电脑的局域网 IP(如 `http://192.168.x.x:8080/v1`),不要用 `localhost`。
|
| 107 |
+
|
| 108 |
+
---
|
| 109 |
+
|
| 110 |
+
### 方式三:源码运行(开发者 / 想改代码的用户)
|
| 111 |
+
|
| 112 |
+
直接从源代码启动,适合想自己修改或调试的人。
|
| 113 |
|
| 114 |
+
**前置条件**:已安装 [Node.js](https://nodejs.org/) 18 或更高版本。安装时一路默认选项即可。
|
| 115 |
+
|
| 116 |
+
**第 1 步:下载项目代码**
|
| 117 |
+
|
| 118 |
+
```bash
|
| 119 |
+
git clone https://github.com/icebear0828/codex-proxy.git
|
| 120 |
+
cd codex-proxy
|
| 121 |
+
```
|
| 122 |
|
| 123 |
+
**第 2 步:安装依赖**
|
| 124 |
|
| 125 |
```bash
|
| 126 |
+
npm install # 安装后端依赖
|
| 127 |
+
cd web && npm install && cd .. # 安装前端依赖
|
|
|
|
|
|
|
| 128 |
```
|
| 129 |
|
| 130 |
+
> macOS / Linux 会自动下载 curl-impersonate(用于 Chrome TLS 伪装)。
|
| 131 |
+
> Windows 下不可用,会自动降级为系统 curl,建议搭配本地代理或改用 Docker。
|
| 132 |
+
|
| 133 |
+
**第 3 步:启动服务**
|
| 134 |
+
|
| 135 |
+
```bash
|
| 136 |
+
npm run dev # 开发模式(改了代码自动重启,适合调试)
|
| 137 |
+
```
|
| 138 |
|
| 139 |
+
或者,如果你要正式使用:
|
| 140 |
|
| 141 |
```bash
|
| 142 |
+
npm run build # 先编译
|
| 143 |
+
npm start # 再启动(性能更好)
|
|
|
|
| 144 |
```
|
| 145 |
|
| 146 |
+
**第 4 步:登录**
|
| 147 |
|
| 148 |
+
打开浏览器,访问 `http://localhost:8080`,用 ChatGPT 账号登录。
|
| 149 |
+
|
| 150 |
+
---
|
| 151 |
+
|
| 152 |
+
### ✅ 验证服务是否正常
|
| 153 |
+
|
| 154 |
+
登录成功后,打开终端,复制粘贴以下命令并回车:
|
| 155 |
|
| 156 |
```bash
|
|
|
|
| 157 |
curl http://localhost:8080/v1/chat/completions \
|
| 158 |
-H "Content-Type: application/json" \
|
| 159 |
-d '{
|
|
|
|
| 163 |
}'
|
| 164 |
```
|
| 165 |
|
| 166 |
+
如果看到一串 AI 回复的文字流出来,恭喜你,部署成功!接下来可以去 [客户端接入](#-客户端接入-client-setup) 章节,把它连到你常用的 AI 工具上。
|
| 167 |
+
|
| 168 |
## 🌟 核心功能 (Features)
|
| 169 |
|
| 170 |
### 1. 🔌 全协议兼容 (Multi-Protocol API)
|
package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
| 1 |
{
|
| 2 |
"name": "codex-proxy",
|
| 3 |
-
"version": "1.0.
|
| 4 |
"lockfileVersion": 3,
|
| 5 |
"requires": true,
|
| 6 |
"packages": {
|
| 7 |
"": {
|
| 8 |
"name": "codex-proxy",
|
| 9 |
-
"version": "1.0.
|
| 10 |
"hasInstallScript": true,
|
| 11 |
"dependencies": {
|
| 12 |
"@hono/node-server": "^1.0.0",
|
|
@@ -19,6 +19,7 @@
|
|
| 19 |
"@electron/asar": "^3.2.0",
|
| 20 |
"@types/js-yaml": "^4.0.0",
|
| 21 |
"@types/node": "^22.0.0",
|
|
|
|
| 22 |
"electron-to-chromium": "^1.5.302",
|
| 23 |
"js-beautify": "^1.15.0",
|
| 24 |
"tsx": "^4.0.0",
|
|
@@ -29,6 +30,80 @@
|
|
| 29 |
"koffi": "^2.15.1"
|
| 30 |
}
|
| 31 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
"node_modules/@electron/asar": {
|
| 33 |
"version": "3.4.1",
|
| 34 |
"resolved": "https://registry.npmmirror.com/@electron/asar/-/asar-3.4.1.tgz",
|
|
@@ -519,6 +594,37 @@
|
|
| 519 |
"node": ">=12"
|
| 520 |
}
|
| 521 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
"node_modules/@jridgewell/sourcemap-codec": {
|
| 523 |
"version": "1.5.5",
|
| 524 |
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
|
@@ -526,6 +632,17 @@
|
|
| 526 |
"dev": true,
|
| 527 |
"license": "MIT"
|
| 528 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 529 |
"node_modules/@one-ini/wasm": {
|
| 530 |
"version": "0.1.1",
|
| 531 |
"resolved": "https://registry.npmmirror.com/@one-ini/wasm/-/wasm-0.1.1.tgz",
|
|
@@ -936,6 +1053,40 @@
|
|
| 936 |
"undici-types": "~6.21.0"
|
| 937 |
}
|
| 938 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 939 |
"node_modules/@vitest/expect": {
|
| 940 |
"version": "3.2.4",
|
| 941 |
"resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-3.2.4.tgz",
|
|
@@ -1103,6 +1254,25 @@
|
|
| 1103 |
"node": ">=12"
|
| 1104 |
}
|
| 1105 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1106 |
"node_modules/balanced-match": {
|
| 1107 |
"version": "1.0.2",
|
| 1108 |
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
|
|
@@ -1486,6 +1656,16 @@
|
|
| 1486 |
"url": "https://github.com/sponsors/isaacs"
|
| 1487 |
}
|
| 1488 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1489 |
"node_modules/hono": {
|
| 1490 |
"version": "4.11.9",
|
| 1491 |
"resolved": "https://registry.npmmirror.com/hono/-/hono-4.11.9.tgz",
|
|
@@ -1495,6 +1675,13 @@
|
|
| 1495 |
"node": ">=16.9.0"
|
| 1496 |
}
|
| 1497 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1498 |
"node_modules/inflight": {
|
| 1499 |
"version": "1.0.6",
|
| 1500 |
"resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz",
|
|
@@ -1538,6 +1725,60 @@
|
|
| 1538 |
"dev": true,
|
| 1539 |
"license": "ISC"
|
| 1540 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1541 |
"node_modules/jackspeak": {
|
| 1542 |
"version": "3.4.3",
|
| 1543 |
"resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz",
|
|
@@ -1687,6 +1928,34 @@
|
|
| 1687 |
"@jridgewell/sourcemap-codec": "^1.5.5"
|
| 1688 |
}
|
| 1689 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1690 |
"node_modules/minimatch": {
|
| 1691 |
"version": "3.1.2",
|
| 1692 |
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
|
|
@@ -2131,6 +2400,127 @@
|
|
| 2131 |
"url": "https://github.com/sponsors/antfu"
|
| 2132 |
}
|
| 2133 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2134 |
"node_modules/tinybench": {
|
| 2135 |
"version": "2.9.0",
|
| 2136 |
"resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz",
|
|
|
|
| 1 |
{
|
| 2 |
"name": "codex-proxy",
|
| 3 |
+
"version": "1.0.28",
|
| 4 |
"lockfileVersion": 3,
|
| 5 |
"requires": true,
|
| 6 |
"packages": {
|
| 7 |
"": {
|
| 8 |
"name": "codex-proxy",
|
| 9 |
+
"version": "1.0.28",
|
| 10 |
"hasInstallScript": true,
|
| 11 |
"dependencies": {
|
| 12 |
"@hono/node-server": "^1.0.0",
|
|
|
|
| 19 |
"@electron/asar": "^3.2.0",
|
| 20 |
"@types/js-yaml": "^4.0.0",
|
| 21 |
"@types/node": "^22.0.0",
|
| 22 |
+
"@vitest/coverage-v8": "^3.2.4",
|
| 23 |
"electron-to-chromium": "^1.5.302",
|
| 24 |
"js-beautify": "^1.15.0",
|
| 25 |
"tsx": "^4.0.0",
|
|
|
|
| 30 |
"koffi": "^2.15.1"
|
| 31 |
}
|
| 32 |
},
|
| 33 |
+
"node_modules/@ampproject/remapping": {
|
| 34 |
+
"version": "2.3.0",
|
| 35 |
+
"resolved": "https://registry.npmmirror.com/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
| 36 |
+
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
| 37 |
+
"dev": true,
|
| 38 |
+
"license": "Apache-2.0",
|
| 39 |
+
"dependencies": {
|
| 40 |
+
"@jridgewell/gen-mapping": "^0.3.5",
|
| 41 |
+
"@jridgewell/trace-mapping": "^0.3.24"
|
| 42 |
+
},
|
| 43 |
+
"engines": {
|
| 44 |
+
"node": ">=6.0.0"
|
| 45 |
+
}
|
| 46 |
+
},
|
| 47 |
+
"node_modules/@babel/helper-string-parser": {
|
| 48 |
+
"version": "7.27.1",
|
| 49 |
+
"resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
| 50 |
+
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
| 51 |
+
"dev": true,
|
| 52 |
+
"license": "MIT",
|
| 53 |
+
"engines": {
|
| 54 |
+
"node": ">=6.9.0"
|
| 55 |
+
}
|
| 56 |
+
},
|
| 57 |
+
"node_modules/@babel/helper-validator-identifier": {
|
| 58 |
+
"version": "7.28.5",
|
| 59 |
+
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
| 60 |
+
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
| 61 |
+
"dev": true,
|
| 62 |
+
"license": "MIT",
|
| 63 |
+
"engines": {
|
| 64 |
+
"node": ">=6.9.0"
|
| 65 |
+
}
|
| 66 |
+
},
|
| 67 |
+
"node_modules/@babel/parser": {
|
| 68 |
+
"version": "7.29.0",
|
| 69 |
+
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.0.tgz",
|
| 70 |
+
"integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
|
| 71 |
+
"dev": true,
|
| 72 |
+
"license": "MIT",
|
| 73 |
+
"dependencies": {
|
| 74 |
+
"@babel/types": "^7.29.0"
|
| 75 |
+
},
|
| 76 |
+
"bin": {
|
| 77 |
+
"parser": "bin/babel-parser.js"
|
| 78 |
+
},
|
| 79 |
+
"engines": {
|
| 80 |
+
"node": ">=6.0.0"
|
| 81 |
+
}
|
| 82 |
+
},
|
| 83 |
+
"node_modules/@babel/types": {
|
| 84 |
+
"version": "7.29.0",
|
| 85 |
+
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz",
|
| 86 |
+
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
| 87 |
+
"dev": true,
|
| 88 |
+
"license": "MIT",
|
| 89 |
+
"dependencies": {
|
| 90 |
+
"@babel/helper-string-parser": "^7.27.1",
|
| 91 |
+
"@babel/helper-validator-identifier": "^7.28.5"
|
| 92 |
+
},
|
| 93 |
+
"engines": {
|
| 94 |
+
"node": ">=6.9.0"
|
| 95 |
+
}
|
| 96 |
+
},
|
| 97 |
+
"node_modules/@bcoe/v8-coverage": {
|
| 98 |
+
"version": "1.0.2",
|
| 99 |
+
"resolved": "https://registry.npmmirror.com/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
|
| 100 |
+
"integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
|
| 101 |
+
"dev": true,
|
| 102 |
+
"license": "MIT",
|
| 103 |
+
"engines": {
|
| 104 |
+
"node": ">=18"
|
| 105 |
+
}
|
| 106 |
+
},
|
| 107 |
"node_modules/@electron/asar": {
|
| 108 |
"version": "3.4.1",
|
| 109 |
"resolved": "https://registry.npmmirror.com/@electron/asar/-/asar-3.4.1.tgz",
|
|
|
|
| 594 |
"node": ">=12"
|
| 595 |
}
|
| 596 |
},
|
| 597 |
+
"node_modules/@istanbuljs/schema": {
|
| 598 |
+
"version": "0.1.3",
|
| 599 |
+
"resolved": "https://registry.npmmirror.com/@istanbuljs/schema/-/schema-0.1.3.tgz",
|
| 600 |
+
"integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
|
| 601 |
+
"dev": true,
|
| 602 |
+
"license": "MIT",
|
| 603 |
+
"engines": {
|
| 604 |
+
"node": ">=8"
|
| 605 |
+
}
|
| 606 |
+
},
|
| 607 |
+
"node_modules/@jridgewell/gen-mapping": {
|
| 608 |
+
"version": "0.3.13",
|
| 609 |
+
"resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
| 610 |
+
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
| 611 |
+
"dev": true,
|
| 612 |
+
"license": "MIT",
|
| 613 |
+
"dependencies": {
|
| 614 |
+
"@jridgewell/sourcemap-codec": "^1.5.0",
|
| 615 |
+
"@jridgewell/trace-mapping": "^0.3.24"
|
| 616 |
+
}
|
| 617 |
+
},
|
| 618 |
+
"node_modules/@jridgewell/resolve-uri": {
|
| 619 |
+
"version": "3.1.2",
|
| 620 |
+
"resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
| 621 |
+
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
| 622 |
+
"dev": true,
|
| 623 |
+
"license": "MIT",
|
| 624 |
+
"engines": {
|
| 625 |
+
"node": ">=6.0.0"
|
| 626 |
+
}
|
| 627 |
+
},
|
| 628 |
"node_modules/@jridgewell/sourcemap-codec": {
|
| 629 |
"version": "1.5.5",
|
| 630 |
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
|
|
|
| 632 |
"dev": true,
|
| 633 |
"license": "MIT"
|
| 634 |
},
|
| 635 |
+
"node_modules/@jridgewell/trace-mapping": {
|
| 636 |
+
"version": "0.3.31",
|
| 637 |
+
"resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
| 638 |
+
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
| 639 |
+
"dev": true,
|
| 640 |
+
"license": "MIT",
|
| 641 |
+
"dependencies": {
|
| 642 |
+
"@jridgewell/resolve-uri": "^3.1.0",
|
| 643 |
+
"@jridgewell/sourcemap-codec": "^1.4.14"
|
| 644 |
+
}
|
| 645 |
+
},
|
| 646 |
"node_modules/@one-ini/wasm": {
|
| 647 |
"version": "0.1.1",
|
| 648 |
"resolved": "https://registry.npmmirror.com/@one-ini/wasm/-/wasm-0.1.1.tgz",
|
|
|
|
| 1053 |
"undici-types": "~6.21.0"
|
| 1054 |
}
|
| 1055 |
},
|
| 1056 |
+
"node_modules/@vitest/coverage-v8": {
|
| 1057 |
+
"version": "3.2.4",
|
| 1058 |
+
"resolved": "https://registry.npmmirror.com/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz",
|
| 1059 |
+
"integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==",
|
| 1060 |
+
"dev": true,
|
| 1061 |
+
"license": "MIT",
|
| 1062 |
+
"dependencies": {
|
| 1063 |
+
"@ampproject/remapping": "^2.3.0",
|
| 1064 |
+
"@bcoe/v8-coverage": "^1.0.2",
|
| 1065 |
+
"ast-v8-to-istanbul": "^0.3.3",
|
| 1066 |
+
"debug": "^4.4.1",
|
| 1067 |
+
"istanbul-lib-coverage": "^3.2.2",
|
| 1068 |
+
"istanbul-lib-report": "^3.0.1",
|
| 1069 |
+
"istanbul-lib-source-maps": "^5.0.6",
|
| 1070 |
+
"istanbul-reports": "^3.1.7",
|
| 1071 |
+
"magic-string": "^0.30.17",
|
| 1072 |
+
"magicast": "^0.3.5",
|
| 1073 |
+
"std-env": "^3.9.0",
|
| 1074 |
+
"test-exclude": "^7.0.1",
|
| 1075 |
+
"tinyrainbow": "^2.0.0"
|
| 1076 |
+
},
|
| 1077 |
+
"funding": {
|
| 1078 |
+
"url": "https://opencollective.com/vitest"
|
| 1079 |
+
},
|
| 1080 |
+
"peerDependencies": {
|
| 1081 |
+
"@vitest/browser": "3.2.4",
|
| 1082 |
+
"vitest": "3.2.4"
|
| 1083 |
+
},
|
| 1084 |
+
"peerDependenciesMeta": {
|
| 1085 |
+
"@vitest/browser": {
|
| 1086 |
+
"optional": true
|
| 1087 |
+
}
|
| 1088 |
+
}
|
| 1089 |
+
},
|
| 1090 |
"node_modules/@vitest/expect": {
|
| 1091 |
"version": "3.2.4",
|
| 1092 |
"resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-3.2.4.tgz",
|
|
|
|
| 1254 |
"node": ">=12"
|
| 1255 |
}
|
| 1256 |
},
|
| 1257 |
+
"node_modules/ast-v8-to-istanbul": {
|
| 1258 |
+
"version": "0.3.12",
|
| 1259 |
+
"resolved": "https://registry.npmmirror.com/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz",
|
| 1260 |
+
"integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==",
|
| 1261 |
+
"dev": true,
|
| 1262 |
+
"license": "MIT",
|
| 1263 |
+
"dependencies": {
|
| 1264 |
+
"@jridgewell/trace-mapping": "^0.3.31",
|
| 1265 |
+
"estree-walker": "^3.0.3",
|
| 1266 |
+
"js-tokens": "^10.0.0"
|
| 1267 |
+
}
|
| 1268 |
+
},
|
| 1269 |
+
"node_modules/ast-v8-to-istanbul/node_modules/js-tokens": {
|
| 1270 |
+
"version": "10.0.0",
|
| 1271 |
+
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-10.0.0.tgz",
|
| 1272 |
+
"integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
|
| 1273 |
+
"dev": true,
|
| 1274 |
+
"license": "MIT"
|
| 1275 |
+
},
|
| 1276 |
"node_modules/balanced-match": {
|
| 1277 |
"version": "1.0.2",
|
| 1278 |
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
|
|
|
|
| 1656 |
"url": "https://github.com/sponsors/isaacs"
|
| 1657 |
}
|
| 1658 |
},
|
| 1659 |
+
"node_modules/has-flag": {
|
| 1660 |
+
"version": "4.0.0",
|
| 1661 |
+
"resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz",
|
| 1662 |
+
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
| 1663 |
+
"dev": true,
|
| 1664 |
+
"license": "MIT",
|
| 1665 |
+
"engines": {
|
| 1666 |
+
"node": ">=8"
|
| 1667 |
+
}
|
| 1668 |
+
},
|
| 1669 |
"node_modules/hono": {
|
| 1670 |
"version": "4.11.9",
|
| 1671 |
"resolved": "https://registry.npmmirror.com/hono/-/hono-4.11.9.tgz",
|
|
|
|
| 1675 |
"node": ">=16.9.0"
|
| 1676 |
}
|
| 1677 |
},
|
| 1678 |
+
"node_modules/html-escaper": {
|
| 1679 |
+
"version": "2.0.2",
|
| 1680 |
+
"resolved": "https://registry.npmmirror.com/html-escaper/-/html-escaper-2.0.2.tgz",
|
| 1681 |
+
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
| 1682 |
+
"dev": true,
|
| 1683 |
+
"license": "MIT"
|
| 1684 |
+
},
|
| 1685 |
"node_modules/inflight": {
|
| 1686 |
"version": "1.0.6",
|
| 1687 |
"resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz",
|
|
|
|
| 1725 |
"dev": true,
|
| 1726 |
"license": "ISC"
|
| 1727 |
},
|
| 1728 |
+
"node_modules/istanbul-lib-coverage": {
|
| 1729 |
+
"version": "3.2.2",
|
| 1730 |
+
"resolved": "https://registry.npmmirror.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
|
| 1731 |
+
"integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
|
| 1732 |
+
"dev": true,
|
| 1733 |
+
"license": "BSD-3-Clause",
|
| 1734 |
+
"engines": {
|
| 1735 |
+
"node": ">=8"
|
| 1736 |
+
}
|
| 1737 |
+
},
|
| 1738 |
+
"node_modules/istanbul-lib-report": {
|
| 1739 |
+
"version": "3.0.1",
|
| 1740 |
+
"resolved": "https://registry.npmmirror.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
|
| 1741 |
+
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
|
| 1742 |
+
"dev": true,
|
| 1743 |
+
"license": "BSD-3-Clause",
|
| 1744 |
+
"dependencies": {
|
| 1745 |
+
"istanbul-lib-coverage": "^3.0.0",
|
| 1746 |
+
"make-dir": "^4.0.0",
|
| 1747 |
+
"supports-color": "^7.1.0"
|
| 1748 |
+
},
|
| 1749 |
+
"engines": {
|
| 1750 |
+
"node": ">=10"
|
| 1751 |
+
}
|
| 1752 |
+
},
|
| 1753 |
+
"node_modules/istanbul-lib-source-maps": {
|
| 1754 |
+
"version": "5.0.6",
|
| 1755 |
+
"resolved": "https://registry.npmmirror.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
|
| 1756 |
+
"integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
|
| 1757 |
+
"dev": true,
|
| 1758 |
+
"license": "BSD-3-Clause",
|
| 1759 |
+
"dependencies": {
|
| 1760 |
+
"@jridgewell/trace-mapping": "^0.3.23",
|
| 1761 |
+
"debug": "^4.1.1",
|
| 1762 |
+
"istanbul-lib-coverage": "^3.0.0"
|
| 1763 |
+
},
|
| 1764 |
+
"engines": {
|
| 1765 |
+
"node": ">=10"
|
| 1766 |
+
}
|
| 1767 |
+
},
|
| 1768 |
+
"node_modules/istanbul-reports": {
|
| 1769 |
+
"version": "3.2.0",
|
| 1770 |
+
"resolved": "https://registry.npmmirror.com/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
|
| 1771 |
+
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
|
| 1772 |
+
"dev": true,
|
| 1773 |
+
"license": "BSD-3-Clause",
|
| 1774 |
+
"dependencies": {
|
| 1775 |
+
"html-escaper": "^2.0.0",
|
| 1776 |
+
"istanbul-lib-report": "^3.0.0"
|
| 1777 |
+
},
|
| 1778 |
+
"engines": {
|
| 1779 |
+
"node": ">=8"
|
| 1780 |
+
}
|
| 1781 |
+
},
|
| 1782 |
"node_modules/jackspeak": {
|
| 1783 |
"version": "3.4.3",
|
| 1784 |
"resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz",
|
|
|
|
| 1928 |
"@jridgewell/sourcemap-codec": "^1.5.5"
|
| 1929 |
}
|
| 1930 |
},
|
| 1931 |
+
"node_modules/magicast": {
|
| 1932 |
+
"version": "0.3.5",
|
| 1933 |
+
"resolved": "https://registry.npmmirror.com/magicast/-/magicast-0.3.5.tgz",
|
| 1934 |
+
"integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
|
| 1935 |
+
"dev": true,
|
| 1936 |
+
"license": "MIT",
|
| 1937 |
+
"dependencies": {
|
| 1938 |
+
"@babel/parser": "^7.25.4",
|
| 1939 |
+
"@babel/types": "^7.25.4",
|
| 1940 |
+
"source-map-js": "^1.2.0"
|
| 1941 |
+
}
|
| 1942 |
+
},
|
| 1943 |
+
"node_modules/make-dir": {
|
| 1944 |
+
"version": "4.0.0",
|
| 1945 |
+
"resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-4.0.0.tgz",
|
| 1946 |
+
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
|
| 1947 |
+
"dev": true,
|
| 1948 |
+
"license": "MIT",
|
| 1949 |
+
"dependencies": {
|
| 1950 |
+
"semver": "^7.5.3"
|
| 1951 |
+
},
|
| 1952 |
+
"engines": {
|
| 1953 |
+
"node": ">=10"
|
| 1954 |
+
},
|
| 1955 |
+
"funding": {
|
| 1956 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 1957 |
+
}
|
| 1958 |
+
},
|
| 1959 |
"node_modules/minimatch": {
|
| 1960 |
"version": "3.1.2",
|
| 1961 |
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
|
|
|
|
| 2400 |
"url": "https://github.com/sponsors/antfu"
|
| 2401 |
}
|
| 2402 |
},
|
| 2403 |
+
"node_modules/supports-color": {
|
| 2404 |
+
"version": "7.2.0",
|
| 2405 |
+
"resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz",
|
| 2406 |
+
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
| 2407 |
+
"dev": true,
|
| 2408 |
+
"license": "MIT",
|
| 2409 |
+
"dependencies": {
|
| 2410 |
+
"has-flag": "^4.0.0"
|
| 2411 |
+
},
|
| 2412 |
+
"engines": {
|
| 2413 |
+
"node": ">=8"
|
| 2414 |
+
}
|
| 2415 |
+
},
|
| 2416 |
+
"node_modules/test-exclude": {
|
| 2417 |
+
"version": "7.0.2",
|
| 2418 |
+
"resolved": "https://registry.npmmirror.com/test-exclude/-/test-exclude-7.0.2.tgz",
|
| 2419 |
+
"integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==",
|
| 2420 |
+
"dev": true,
|
| 2421 |
+
"license": "ISC",
|
| 2422 |
+
"dependencies": {
|
| 2423 |
+
"@istanbuljs/schema": "^0.1.2",
|
| 2424 |
+
"glob": "^10.4.1",
|
| 2425 |
+
"minimatch": "^10.2.2"
|
| 2426 |
+
},
|
| 2427 |
+
"engines": {
|
| 2428 |
+
"node": ">=18"
|
| 2429 |
+
}
|
| 2430 |
+
},
|
| 2431 |
+
"node_modules/test-exclude/node_modules/balanced-match": {
|
| 2432 |
+
"version": "4.0.4",
|
| 2433 |
+
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz",
|
| 2434 |
+
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
| 2435 |
+
"dev": true,
|
| 2436 |
+
"license": "MIT",
|
| 2437 |
+
"engines": {
|
| 2438 |
+
"node": "18 || 20 || >=22"
|
| 2439 |
+
}
|
| 2440 |
+
},
|
| 2441 |
+
"node_modules/test-exclude/node_modules/brace-expansion": {
|
| 2442 |
+
"version": "5.0.4",
|
| 2443 |
+
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.4.tgz",
|
| 2444 |
+
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
|
| 2445 |
+
"dev": true,
|
| 2446 |
+
"license": "MIT",
|
| 2447 |
+
"dependencies": {
|
| 2448 |
+
"balanced-match": "^4.0.2"
|
| 2449 |
+
},
|
| 2450 |
+
"engines": {
|
| 2451 |
+
"node": "18 || 20 || >=22"
|
| 2452 |
+
}
|
| 2453 |
+
},
|
| 2454 |
+
"node_modules/test-exclude/node_modules/glob": {
|
| 2455 |
+
"version": "10.4.5",
|
| 2456 |
+
"resolved": "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz",
|
| 2457 |
+
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
| 2458 |
+
"dev": true,
|
| 2459 |
+
"license": "ISC",
|
| 2460 |
+
"dependencies": {
|
| 2461 |
+
"foreground-child": "^3.1.0",
|
| 2462 |
+
"jackspeak": "^3.1.2",
|
| 2463 |
+
"minimatch": "^9.0.4",
|
| 2464 |
+
"minipass": "^7.1.2",
|
| 2465 |
+
"package-json-from-dist": "^1.0.0",
|
| 2466 |
+
"path-scurry": "^1.11.1"
|
| 2467 |
+
},
|
| 2468 |
+
"bin": {
|
| 2469 |
+
"glob": "dist/esm/bin.mjs"
|
| 2470 |
+
},
|
| 2471 |
+
"funding": {
|
| 2472 |
+
"url": "https://github.com/sponsors/isaacs"
|
| 2473 |
+
}
|
| 2474 |
+
},
|
| 2475 |
+
"node_modules/test-exclude/node_modules/glob/node_modules/balanced-match": {
|
| 2476 |
+
"version": "1.0.2",
|
| 2477 |
+
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
|
| 2478 |
+
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
| 2479 |
+
"dev": true,
|
| 2480 |
+
"license": "MIT"
|
| 2481 |
+
},
|
| 2482 |
+
"node_modules/test-exclude/node_modules/glob/node_modules/brace-expansion": {
|
| 2483 |
+
"version": "2.0.2",
|
| 2484 |
+
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
| 2485 |
+
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
| 2486 |
+
"dev": true,
|
| 2487 |
+
"license": "MIT",
|
| 2488 |
+
"dependencies": {
|
| 2489 |
+
"balanced-match": "^1.0.0"
|
| 2490 |
+
}
|
| 2491 |
+
},
|
| 2492 |
+
"node_modules/test-exclude/node_modules/glob/node_modules/minimatch": {
|
| 2493 |
+
"version": "9.0.9",
|
| 2494 |
+
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz",
|
| 2495 |
+
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
| 2496 |
+
"dev": true,
|
| 2497 |
+
"license": "ISC",
|
| 2498 |
+
"dependencies": {
|
| 2499 |
+
"brace-expansion": "^2.0.2"
|
| 2500 |
+
},
|
| 2501 |
+
"engines": {
|
| 2502 |
+
"node": ">=16 || 14 >=14.17"
|
| 2503 |
+
},
|
| 2504 |
+
"funding": {
|
| 2505 |
+
"url": "https://github.com/sponsors/isaacs"
|
| 2506 |
+
}
|
| 2507 |
+
},
|
| 2508 |
+
"node_modules/test-exclude/node_modules/minimatch": {
|
| 2509 |
+
"version": "10.2.4",
|
| 2510 |
+
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.4.tgz",
|
| 2511 |
+
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
|
| 2512 |
+
"dev": true,
|
| 2513 |
+
"license": "BlueOak-1.0.0",
|
| 2514 |
+
"dependencies": {
|
| 2515 |
+
"brace-expansion": "^5.0.2"
|
| 2516 |
+
},
|
| 2517 |
+
"engines": {
|
| 2518 |
+
"node": "18 || 20 || >=22"
|
| 2519 |
+
},
|
| 2520 |
+
"funding": {
|
| 2521 |
+
"url": "https://github.com/sponsors/isaacs"
|
| 2522 |
+
}
|
| 2523 |
+
},
|
| 2524 |
"node_modules/tinybench": {
|
| 2525 |
"version": "2.9.0",
|
| 2526 |
"resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz",
|
package.json
CHANGED
|
@@ -34,6 +34,7 @@
|
|
| 34 |
"@electron/asar": "^3.2.0",
|
| 35 |
"@types/js-yaml": "^4.0.0",
|
| 36 |
"@types/node": "^22.0.0",
|
|
|
|
| 37 |
"electron-to-chromium": "^1.5.302",
|
| 38 |
"js-beautify": "^1.15.0",
|
| 39 |
"tsx": "^4.0.0",
|
|
|
|
| 34 |
"@electron/asar": "^3.2.0",
|
| 35 |
"@types/js-yaml": "^4.0.0",
|
| 36 |
"@types/node": "^22.0.0",
|
| 37 |
+
"@vitest/coverage-v8": "^3.2.4",
|
| 38 |
"electron-to-chromium": "^1.5.302",
|
| 39 |
"js-beautify": "^1.15.0",
|
| 40 |
"tsx": "^4.0.0",
|
shared/i18n/translations.ts
CHANGED
|
@@ -24,6 +24,7 @@ export const translations = {
|
|
| 24 |
totalAll: "Total",
|
| 25 |
windowLabel: "window",
|
| 26 |
rateLimit: "Rate Limit",
|
|
|
|
| 27 |
limitReached: "Limit Reached",
|
| 28 |
used: "Used",
|
| 29 |
ok: "OK",
|
|
@@ -196,6 +197,7 @@ export const translations = {
|
|
| 196 |
totalAll: "\u7d2f\u8ba1",
|
| 197 |
windowLabel: "\u7a97\u53e3",
|
| 198 |
rateLimit: "\u901f\u7387\u9650\u5236",
|
|
|
|
| 199 |
limitReached: "\u5df2\u8fbe\u4e0a\u9650",
|
| 200 |
used: "\u5df2\u4f7f\u7528",
|
| 201 |
ok: "\u6b63\u5e38",
|
|
|
|
| 24 |
totalAll: "Total",
|
| 25 |
windowLabel: "window",
|
| 26 |
rateLimit: "Rate Limit",
|
| 27 |
+
secondaryRateLimit: "Weekly Limit",
|
| 28 |
limitReached: "Limit Reached",
|
| 29 |
used: "Used",
|
| 30 |
ok: "OK",
|
|
|
|
| 197 |
totalAll: "\u7d2f\u8ba1",
|
| 198 |
windowLabel: "\u7a97\u53e3",
|
| 199 |
rateLimit: "\u901f\u7387\u9650\u5236",
|
| 200 |
+
secondaryRateLimit: "\u5468\u9650\u5236",
|
| 201 |
limitReached: "\u5df2\u8fbe\u4e0a\u9650",
|
| 202 |
used: "\u5df2\u4f7f\u7528",
|
| 203 |
ok: "\u6b63\u5e38",
|
shared/types.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export interface AccountQuotaWindow {
|
| 2 |
+
used_percent?: number | null;
|
| 3 |
+
limit_reached?: boolean;
|
| 4 |
+
reset_at?: number | null;
|
| 5 |
+
limit_window_seconds?: number | null;
|
| 6 |
+
}
|
| 7 |
+
export interface AccountQuota {
|
| 8 |
+
rate_limit?: AccountQuotaWindow;
|
| 9 |
+
/** Secondary rate limit window (e.g. weekly cap). */
|
| 10 |
+
secondary_rate_limit?: AccountQuotaWindow | null;
|
| 11 |
+
}
|
| 12 |
+
export interface Account {
|
| 13 |
+
id: string;
|
| 14 |
+
email: string;
|
| 15 |
+
status: string;
|
| 16 |
+
planType?: string;
|
| 17 |
+
usage?: {
|
| 18 |
+
request_count?: number;
|
| 19 |
+
input_tokens?: number;
|
| 20 |
+
output_tokens?: number;
|
| 21 |
+
window_request_count?: number;
|
| 22 |
+
window_input_tokens?: number;
|
| 23 |
+
window_output_tokens?: number;
|
| 24 |
+
};
|
| 25 |
+
quota?: AccountQuota;
|
| 26 |
+
proxyId?: string;
|
| 27 |
+
proxyName?: string;
|
| 28 |
+
}
|
| 29 |
+
export interface ProxyHealthInfo {
|
| 30 |
+
exitIp: string | null;
|
| 31 |
+
latencyMs: number;
|
| 32 |
+
lastChecked: string;
|
| 33 |
+
error: string | null;
|
| 34 |
+
}
|
| 35 |
+
export interface ProxyEntry {
|
| 36 |
+
id: string;
|
| 37 |
+
name: string;
|
| 38 |
+
url: string;
|
| 39 |
+
status: "active" | "unreachable" | "disabled";
|
| 40 |
+
health: ProxyHealthInfo | null;
|
| 41 |
+
addedAt: string;
|
| 42 |
+
}
|
| 43 |
+
export interface ProxyAssignment {
|
| 44 |
+
accountId: string;
|
| 45 |
+
proxyId: string;
|
| 46 |
+
}
|
| 47 |
+
export type DiagnosticStatus = "pass" | "fail" | "skip";
|
| 48 |
+
export interface DiagnosticCheck {
|
| 49 |
+
name: string;
|
| 50 |
+
status: DiagnosticStatus;
|
| 51 |
+
latencyMs: number;
|
| 52 |
+
detail: string | null;
|
| 53 |
+
error: string | null;
|
| 54 |
+
}
|
| 55 |
+
export interface TestConnectionResult {
|
| 56 |
+
checks: DiagnosticCheck[];
|
| 57 |
+
overall: DiagnosticStatus;
|
| 58 |
+
timestamp: string;
|
| 59 |
+
}
|
shared/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
export {};
|
src/auth/__tests__/account-pool.test.ts
DELETED
|
@@ -1,292 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Tests for AccountPool core scheduling logic.
|
| 3 |
-
*
|
| 4 |
-
* Uses vi.mock to stub filesystem and JWT utilities so tests run
|
| 5 |
-
* without actual data files or valid JWT tokens.
|
| 6 |
-
*/
|
| 7 |
-
|
| 8 |
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
| 9 |
-
|
| 10 |
-
// Mock fs before importing AccountPool
|
| 11 |
-
vi.mock("fs", () => ({
|
| 12 |
-
readFileSync: vi.fn(() => { throw new Error("ENOENT"); }),
|
| 13 |
-
writeFileSync: vi.fn(),
|
| 14 |
-
renameSync: vi.fn(),
|
| 15 |
-
existsSync: vi.fn(() => false),
|
| 16 |
-
mkdirSync: vi.fn(),
|
| 17 |
-
}));
|
| 18 |
-
|
| 19 |
-
// Mock config
|
| 20 |
-
vi.mock("../../config.js", () => ({
|
| 21 |
-
getConfig: vi.fn(() => ({
|
| 22 |
-
auth: {
|
| 23 |
-
jwt_token: null,
|
| 24 |
-
rotation_strategy: "least_used",
|
| 25 |
-
rate_limit_backoff_seconds: 60,
|
| 26 |
-
},
|
| 27 |
-
server: {
|
| 28 |
-
proxy_api_key: null,
|
| 29 |
-
},
|
| 30 |
-
})),
|
| 31 |
-
}));
|
| 32 |
-
|
| 33 |
-
// Mock JWT utilities — all tokens are "valid"
|
| 34 |
-
vi.mock("../jwt-utils.js", () => ({
|
| 35 |
-
decodeJwtPayload: vi.fn(() => ({ exp: Math.floor(Date.now() / 1000) + 3600 })),
|
| 36 |
-
extractChatGptAccountId: vi.fn((token: string) => `acct-${token.slice(0, 8)}`),
|
| 37 |
-
extractUserProfile: vi.fn((token: string) => ({
|
| 38 |
-
email: `${token.slice(0, 4)}@test.com`,
|
| 39 |
-
chatgpt_plan_type: "free",
|
| 40 |
-
})),
|
| 41 |
-
isTokenExpired: vi.fn(() => false),
|
| 42 |
-
}));
|
| 43 |
-
|
| 44 |
-
// Mock jitter to return the exact value (no randomness in tests)
|
| 45 |
-
vi.mock("../../utils/jitter.js", () => ({
|
| 46 |
-
jitter: vi.fn((val: number) => val),
|
| 47 |
-
}));
|
| 48 |
-
|
| 49 |
-
import { AccountPool } from "../account-pool.js";
|
| 50 |
-
import { getConfig } from "../../config.js";
|
| 51 |
-
import { isTokenExpired } from "../jwt-utils.js";
|
| 52 |
-
|
| 53 |
-
describe("AccountPool", () => {
|
| 54 |
-
let pool: AccountPool;
|
| 55 |
-
|
| 56 |
-
beforeEach(() => {
|
| 57 |
-
// Reset mock implementations to defaults (clearAllMocks only clears call history)
|
| 58 |
-
vi.mocked(isTokenExpired).mockReturnValue(false);
|
| 59 |
-
vi.mocked(getConfig).mockReturnValue({
|
| 60 |
-
auth: {
|
| 61 |
-
jwt_token: null,
|
| 62 |
-
rotation_strategy: "least_used",
|
| 63 |
-
rate_limit_backoff_seconds: 60,
|
| 64 |
-
},
|
| 65 |
-
server: { proxy_api_key: null },
|
| 66 |
-
} as ReturnType<typeof getConfig>);
|
| 67 |
-
pool = new AccountPool();
|
| 68 |
-
});
|
| 69 |
-
|
| 70 |
-
afterEach(() => {
|
| 71 |
-
pool.destroy();
|
| 72 |
-
});
|
| 73 |
-
|
| 74 |
-
describe("addAccount + acquire", () => {
|
| 75 |
-
it("adds an account and acquires it", () => {
|
| 76 |
-
pool.addAccount("token-aaa");
|
| 77 |
-
|
| 78 |
-
const acquired = pool.acquire();
|
| 79 |
-
expect(acquired).not.toBeNull();
|
| 80 |
-
expect(acquired!.token).toBe("token-aaa");
|
| 81 |
-
});
|
| 82 |
-
|
| 83 |
-
it("deduplicates by accountId", () => {
|
| 84 |
-
const id1 = pool.addAccount("token-aaa");
|
| 85 |
-
const id2 = pool.addAccount("token-aaa"); // same prefix → same accountId
|
| 86 |
-
|
| 87 |
-
expect(id1).toBe(id2);
|
| 88 |
-
});
|
| 89 |
-
|
| 90 |
-
it("returns null when no accounts exist", () => {
|
| 91 |
-
expect(pool.acquire()).toBeNull();
|
| 92 |
-
});
|
| 93 |
-
});
|
| 94 |
-
|
| 95 |
-
describe("least_used rotation", () => {
|
| 96 |
-
it("selects the account with lowest request_count", () => {
|
| 97 |
-
pool.addAccount("token-aaa");
|
| 98 |
-
pool.addAccount("token-bbb");
|
| 99 |
-
|
| 100 |
-
// Use account A once
|
| 101 |
-
const first = pool.acquire()!;
|
| 102 |
-
pool.release(first.entryId, { input_tokens: 10, output_tokens: 5 });
|
| 103 |
-
|
| 104 |
-
// Next acquire should pick the other account (0 requests)
|
| 105 |
-
const second = pool.acquire()!;
|
| 106 |
-
expect(second.entryId).not.toBe(first.entryId);
|
| 107 |
-
});
|
| 108 |
-
});
|
| 109 |
-
|
| 110 |
-
describe("round_robin rotation", () => {
|
| 111 |
-
it("cycles through accounts in order", () => {
|
| 112 |
-
vi.mocked(getConfig).mockReturnValue({
|
| 113 |
-
auth: {
|
| 114 |
-
jwt_token: null,
|
| 115 |
-
rotation_strategy: "round_robin",
|
| 116 |
-
rate_limit_backoff_seconds: 60,
|
| 117 |
-
},
|
| 118 |
-
server: { proxy_api_key: null },
|
| 119 |
-
} as ReturnType<typeof getConfig>);
|
| 120 |
-
|
| 121 |
-
// Create a fresh pool with round_robin config
|
| 122 |
-
const rrPool = new AccountPool();
|
| 123 |
-
rrPool.addAccount("token-aaa");
|
| 124 |
-
rrPool.addAccount("token-bbb");
|
| 125 |
-
|
| 126 |
-
const a1 = rrPool.acquire()!;
|
| 127 |
-
rrPool.release(a1.entryId);
|
| 128 |
-
|
| 129 |
-
const a2 = rrPool.acquire()!;
|
| 130 |
-
rrPool.release(a2.entryId);
|
| 131 |
-
|
| 132 |
-
const a3 = rrPool.acquire()!;
|
| 133 |
-
rrPool.release(a3.entryId);
|
| 134 |
-
|
| 135 |
-
// a3 should wrap around to same as a1
|
| 136 |
-
expect(a3.entryId).toBe(a1.entryId);
|
| 137 |
-
expect(a1.entryId).not.toBe(a2.entryId);
|
| 138 |
-
|
| 139 |
-
rrPool.destroy();
|
| 140 |
-
});
|
| 141 |
-
});
|
| 142 |
-
|
| 143 |
-
describe("release", () => {
|
| 144 |
-
it("increments request_count and token usage", () => {
|
| 145 |
-
pool.addAccount("token-aaa");
|
| 146 |
-
|
| 147 |
-
const acquired = pool.acquire()!;
|
| 148 |
-
pool.release(acquired.entryId, { input_tokens: 100, output_tokens: 50 });
|
| 149 |
-
|
| 150 |
-
const accounts = pool.getAccounts();
|
| 151 |
-
expect(accounts[0].usage.request_count).toBe(1);
|
| 152 |
-
expect(accounts[0].usage.input_tokens).toBe(100);
|
| 153 |
-
expect(accounts[0].usage.output_tokens).toBe(50);
|
| 154 |
-
expect(accounts[0].usage.last_used).not.toBeNull();
|
| 155 |
-
});
|
| 156 |
-
|
| 157 |
-
it("unlocks account after release", () => {
|
| 158 |
-
pool.addAccount("token-aaa");
|
| 159 |
-
|
| 160 |
-
const a1 = pool.acquire()!;
|
| 161 |
-
// While locked, acquire returns null (only 1 account)
|
| 162 |
-
expect(pool.acquire()).toBeNull();
|
| 163 |
-
|
| 164 |
-
pool.release(a1.entryId);
|
| 165 |
-
// After release, can acquire again
|
| 166 |
-
expect(pool.acquire()).not.toBeNull();
|
| 167 |
-
});
|
| 168 |
-
});
|
| 169 |
-
|
| 170 |
-
describe("markRateLimited", () => {
|
| 171 |
-
it("marks account as rate_limited and skips it in acquire", () => {
|
| 172 |
-
pool.addAccount("token-aaa");
|
| 173 |
-
pool.addAccount("token-bbb");
|
| 174 |
-
|
| 175 |
-
const first = pool.acquire()!;
|
| 176 |
-
pool.markRateLimited(first.entryId);
|
| 177 |
-
|
| 178 |
-
// Pool summary should show 1 rate_limited
|
| 179 |
-
const summary = pool.getPoolSummary();
|
| 180 |
-
expect(summary.rate_limited).toBe(1);
|
| 181 |
-
expect(summary.active).toBe(1);
|
| 182 |
-
|
| 183 |
-
// Next acquire should skip the rate-limited account
|
| 184 |
-
const second = pool.acquire()!;
|
| 185 |
-
expect(second.entryId).not.toBe(first.entryId);
|
| 186 |
-
});
|
| 187 |
-
|
| 188 |
-
it("countRequest option increments usage on 429", () => {
|
| 189 |
-
pool.addAccount("token-aaa");
|
| 190 |
-
|
| 191 |
-
const acquired = pool.acquire()!;
|
| 192 |
-
pool.markRateLimited(acquired.entryId, { countRequest: true });
|
| 193 |
-
|
| 194 |
-
const accounts = pool.getAccounts();
|
| 195 |
-
expect(accounts[0].usage.request_count).toBe(1);
|
| 196 |
-
});
|
| 197 |
-
|
| 198 |
-
it("auto-recovers after rate_limit_until passes", () => {
|
| 199 |
-
pool.addAccount("token-aaa");
|
| 200 |
-
|
| 201 |
-
const acquired = pool.acquire()!;
|
| 202 |
-
// Set rate limit to already expired
|
| 203 |
-
pool.markRateLimited(acquired.entryId, { retryAfterSec: -1 });
|
| 204 |
-
|
| 205 |
-
// refreshStatus should detect the expired rate_limit_until
|
| 206 |
-
const summary = pool.getPoolSummary();
|
| 207 |
-
expect(summary.active).toBe(1);
|
| 208 |
-
expect(summary.rate_limited).toBe(0);
|
| 209 |
-
});
|
| 210 |
-
});
|
| 211 |
-
|
| 212 |
-
describe("stale lock auto-release", () => {
|
| 213 |
-
it("releases locks older than 5 minutes", () => {
|
| 214 |
-
pool.addAccount("token-aaa");
|
| 215 |
-
|
| 216 |
-
const acquired = pool.acquire()!;
|
| 217 |
-
|
| 218 |
-
// Manually backdate the lock by manipulating the acquireLocks map
|
| 219 |
-
// Access private field for testing — unavoidable for TTL tests
|
| 220 |
-
const locks = (pool as unknown as { acquireLocks: Map<string, number> }).acquireLocks;
|
| 221 |
-
locks.set(acquired.entryId, Date.now() - 6 * 60 * 1000); // 6 minutes ago
|
| 222 |
-
|
| 223 |
-
// Next acquire should auto-release the stale lock and return the same account
|
| 224 |
-
const reacquired = pool.acquire()!;
|
| 225 |
-
expect(reacquired).not.toBeNull();
|
| 226 |
-
expect(reacquired.entryId).toBe(acquired.entryId);
|
| 227 |
-
});
|
| 228 |
-
});
|
| 229 |
-
|
| 230 |
-
describe("expired tokens", () => {
|
| 231 |
-
it("skips expired accounts in acquire", () => {
|
| 232 |
-
vi.mocked(isTokenExpired).mockReturnValue(true);
|
| 233 |
-
pool.addAccount("token-expired");
|
| 234 |
-
|
| 235 |
-
expect(pool.acquire()).toBeNull();
|
| 236 |
-
expect(pool.getPoolSummary().expired).toBe(1);
|
| 237 |
-
});
|
| 238 |
-
});
|
| 239 |
-
|
| 240 |
-
describe("removeAccount", () => {
|
| 241 |
-
it("removes an account and clears its lock", () => {
|
| 242 |
-
pool.addAccount("token-aaa");
|
| 243 |
-
|
| 244 |
-
const acquired = pool.acquire()!;
|
| 245 |
-
pool.removeAccount(acquired.entryId);
|
| 246 |
-
|
| 247 |
-
expect(pool.getPoolSummary().total).toBe(0);
|
| 248 |
-
expect(pool.acquire()).toBeNull();
|
| 249 |
-
});
|
| 250 |
-
});
|
| 251 |
-
|
| 252 |
-
describe("resetUsage", () => {
|
| 253 |
-
it("resets counters to zero", () => {
|
| 254 |
-
pool.addAccount("token-aaa");
|
| 255 |
-
|
| 256 |
-
const acquired = pool.acquire()!;
|
| 257 |
-
pool.release(acquired.entryId, { input_tokens: 100, output_tokens: 50 });
|
| 258 |
-
pool.resetUsage(acquired.entryId);
|
| 259 |
-
|
| 260 |
-
const accounts = pool.getAccounts();
|
| 261 |
-
expect(accounts[0].usage.request_count).toBe(0);
|
| 262 |
-
expect(accounts[0].usage.input_tokens).toBe(0);
|
| 263 |
-
expect(accounts[0].usage.output_tokens).toBe(0);
|
| 264 |
-
});
|
| 265 |
-
});
|
| 266 |
-
|
| 267 |
-
describe("validateProxyApiKey", () => {
|
| 268 |
-
it("validates per-account proxy API key", () => {
|
| 269 |
-
pool.addAccount("token-aaa");
|
| 270 |
-
|
| 271 |
-
const accounts = pool.getAccounts();
|
| 272 |
-
// Each account gets a generated proxyApiKey — we can't predict it,
|
| 273 |
-
// but we can read it and validate
|
| 274 |
-
const entry = pool.getEntry(accounts[0].id)!;
|
| 275 |
-
expect(pool.validateProxyApiKey(entry.proxyApiKey)).toBe(true);
|
| 276 |
-
expect(pool.validateProxyApiKey("wrong-key")).toBe(false);
|
| 277 |
-
});
|
| 278 |
-
|
| 279 |
-
it("validates config-level proxy API key", () => {
|
| 280 |
-
vi.mocked(getConfig).mockReturnValue({
|
| 281 |
-
auth: {
|
| 282 |
-
jwt_token: null,
|
| 283 |
-
rotation_strategy: "least_used",
|
| 284 |
-
rate_limit_backoff_seconds: 60,
|
| 285 |
-
},
|
| 286 |
-
server: { proxy_api_key: "global-key-123" },
|
| 287 |
-
} as ReturnType<typeof getConfig>);
|
| 288 |
-
|
| 289 |
-
expect(pool.validateProxyApiKey("global-key-123")).toBe(true);
|
| 290 |
-
});
|
| 291 |
-
});
|
| 292 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/auth/types.ts
CHANGED
|
@@ -56,16 +56,24 @@ export interface AccountInfo {
|
|
| 56 |
quota?: CodexQuota;
|
| 57 |
}
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
/** Official Codex quota from /backend-api/codex/usage */
|
| 60 |
export interface CodexQuota {
|
| 61 |
plan_type: string;
|
| 62 |
-
rate_limit: {
|
| 63 |
allowed: boolean;
|
| 64 |
limit_reached: boolean;
|
| 65 |
-
used_percent: number | null;
|
| 66 |
-
reset_at: number | null;
|
| 67 |
-
limit_window_seconds: number | null;
|
| 68 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
code_review_rate_limit: {
|
| 70 |
allowed: boolean;
|
| 71 |
limit_reached: boolean;
|
|
|
|
| 56 |
quota?: CodexQuota;
|
| 57 |
}
|
| 58 |
|
| 59 |
+
/** A single rate limit window (primary or secondary). */
|
| 60 |
+
export interface CodexQuotaWindow {
|
| 61 |
+
used_percent: number | null;
|
| 62 |
+
reset_at: number | null;
|
| 63 |
+
limit_window_seconds: number | null;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
/** Official Codex quota from /backend-api/codex/usage */
|
| 67 |
export interface CodexQuota {
|
| 68 |
plan_type: string;
|
| 69 |
+
rate_limit: CodexQuotaWindow & {
|
| 70 |
allowed: boolean;
|
| 71 |
limit_reached: boolean;
|
|
|
|
|
|
|
|
|
|
| 72 |
};
|
| 73 |
+
/** Secondary rate limit window (e.g. weekly cap). Null when backend doesn't report one. */
|
| 74 |
+
secondary_rate_limit: CodexQuotaWindow & {
|
| 75 |
+
limit_reached: boolean;
|
| 76 |
+
} | null;
|
| 77 |
code_review_rate_limit: {
|
| 78 |
allowed: boolean;
|
| 79 |
limit_reached: boolean;
|
src/proxy/__tests__/codex-api.test.ts
DELETED
|
@@ -1,201 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Tests for CodexApi SSE parsing.
|
| 3 |
-
*
|
| 4 |
-
* parseStream() is the most fragile code path — it processes real-time
|
| 5 |
-
* byte streams from curl where chunks can split at any boundary.
|
| 6 |
-
*/
|
| 7 |
-
|
| 8 |
-
import { describe, it, expect } from "vitest";
|
| 9 |
-
import { CodexApi, type CodexSSEEvent } from "../codex-api.js";
|
| 10 |
-
|
| 11 |
-
/** Create a Response whose body emits the given string chunks sequentially. */
|
| 12 |
-
function mockResponse(...chunks: string[]): Response {
|
| 13 |
-
const encoder = new TextEncoder();
|
| 14 |
-
let idx = 0;
|
| 15 |
-
const stream = new ReadableStream<Uint8Array>({
|
| 16 |
-
pull(controller) {
|
| 17 |
-
if (idx < chunks.length) {
|
| 18 |
-
controller.enqueue(encoder.encode(chunks[idx]));
|
| 19 |
-
idx++;
|
| 20 |
-
} else {
|
| 21 |
-
controller.close();
|
| 22 |
-
}
|
| 23 |
-
},
|
| 24 |
-
});
|
| 25 |
-
return new Response(stream);
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
/** Collect all events from parseStream into an array. */
|
| 29 |
-
async function collectEvents(api: CodexApi, response: Response): Promise<CodexSSEEvent[]> {
|
| 30 |
-
const events: CodexSSEEvent[] = [];
|
| 31 |
-
for await (const evt of api.parseStream(response)) {
|
| 32 |
-
events.push(evt);
|
| 33 |
-
}
|
| 34 |
-
return events;
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
// CodexApi constructor requires a token — value is irrelevant for parsing tests
|
| 38 |
-
function createApi(): CodexApi {
|
| 39 |
-
return new CodexApi("test-token", null);
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
describe("CodexApi.parseStream", () => {
|
| 43 |
-
it("parses a complete SSE event in a single chunk", async () => {
|
| 44 |
-
const api = createApi();
|
| 45 |
-
const response = mockResponse(
|
| 46 |
-
'event: response.output_text.delta\ndata: {"delta":"Hello"}\n\n',
|
| 47 |
-
);
|
| 48 |
-
|
| 49 |
-
const events = await collectEvents(api, response);
|
| 50 |
-
expect(events).toHaveLength(1);
|
| 51 |
-
expect(events[0].event).toBe("response.output_text.delta");
|
| 52 |
-
expect(events[0].data).toEqual({ delta: "Hello" });
|
| 53 |
-
});
|
| 54 |
-
|
| 55 |
-
it("handles multiple events in a single chunk", async () => {
|
| 56 |
-
const api = createApi();
|
| 57 |
-
const response = mockResponse(
|
| 58 |
-
'event: response.created\ndata: {"response":{"id":"resp_1"}}\n\n' +
|
| 59 |
-
'event: response.output_text.delta\ndata: {"delta":"Hi"}\n\n' +
|
| 60 |
-
'event: response.completed\ndata: {"response":{"id":"resp_1","usage":{"input_tokens":10,"output_tokens":5}}}\n\n',
|
| 61 |
-
);
|
| 62 |
-
|
| 63 |
-
const events = await collectEvents(api, response);
|
| 64 |
-
expect(events).toHaveLength(3);
|
| 65 |
-
expect(events[0].event).toBe("response.created");
|
| 66 |
-
expect(events[1].event).toBe("response.output_text.delta");
|
| 67 |
-
expect(events[2].event).toBe("response.completed");
|
| 68 |
-
});
|
| 69 |
-
|
| 70 |
-
it("reassembles events split across chunk boundaries", async () => {
|
| 71 |
-
const api = createApi();
|
| 72 |
-
// Split in the middle of the JSON data
|
| 73 |
-
const response = mockResponse(
|
| 74 |
-
'event: response.output_text.delta\ndata: {"del',
|
| 75 |
-
'ta":"world"}\n\n',
|
| 76 |
-
);
|
| 77 |
-
|
| 78 |
-
const events = await collectEvents(api, response);
|
| 79 |
-
expect(events).toHaveLength(1);
|
| 80 |
-
expect(events[0].data).toEqual({ delta: "world" });
|
| 81 |
-
});
|
| 82 |
-
|
| 83 |
-
it("handles chunk split at \\n\\n boundary", async () => {
|
| 84 |
-
const api = createApi();
|
| 85 |
-
// First chunk ends with first \n, second starts with second \n
|
| 86 |
-
const response = mockResponse(
|
| 87 |
-
'event: response.output_text.delta\ndata: {"delta":"a"}\n',
|
| 88 |
-
'\nevent: response.output_text.delta\ndata: {"delta":"b"}\n\n',
|
| 89 |
-
);
|
| 90 |
-
|
| 91 |
-
const events = await collectEvents(api, response);
|
| 92 |
-
expect(events).toHaveLength(2);
|
| 93 |
-
expect(events[0].data).toEqual({ delta: "a" });
|
| 94 |
-
expect(events[1].data).toEqual({ delta: "b" });
|
| 95 |
-
});
|
| 96 |
-
|
| 97 |
-
it("handles many small single-character chunks", async () => {
|
| 98 |
-
const api = createApi();
|
| 99 |
-
const full = 'event: response.output_text.delta\ndata: {"delta":"x"}\n\n';
|
| 100 |
-
// Split into individual characters
|
| 101 |
-
const chunks = full.split("");
|
| 102 |
-
const response = mockResponse(...chunks);
|
| 103 |
-
|
| 104 |
-
const events = await collectEvents(api, response);
|
| 105 |
-
expect(events).toHaveLength(1);
|
| 106 |
-
expect(events[0].data).toEqual({ delta: "x" });
|
| 107 |
-
});
|
| 108 |
-
|
| 109 |
-
it("skips [DONE] marker without crashing", async () => {
|
| 110 |
-
const api = createApi();
|
| 111 |
-
const response = mockResponse(
|
| 112 |
-
'event: response.output_text.delta\ndata: {"delta":"hi"}\n\n' +
|
| 113 |
-
"data: [DONE]\n\n",
|
| 114 |
-
);
|
| 115 |
-
|
| 116 |
-
const events = await collectEvents(api, response);
|
| 117 |
-
expect(events).toHaveLength(1);
|
| 118 |
-
expect(events[0].data).toEqual({ delta: "hi" });
|
| 119 |
-
});
|
| 120 |
-
|
| 121 |
-
it("returns raw string when data is not valid JSON", async () => {
|
| 122 |
-
const api = createApi();
|
| 123 |
-
const response = mockResponse(
|
| 124 |
-
'event: response.output_text.delta\ndata: not-json-at-all\n\n',
|
| 125 |
-
);
|
| 126 |
-
|
| 127 |
-
const events = await collectEvents(api, response);
|
| 128 |
-
expect(events).toHaveLength(1);
|
| 129 |
-
expect(events[0].data).toBe("not-json-at-all");
|
| 130 |
-
});
|
| 131 |
-
|
| 132 |
-
it("handles malformed JSON (unclosed brace) gracefully", async () => {
|
| 133 |
-
const api = createApi();
|
| 134 |
-
const response = mockResponse(
|
| 135 |
-
'event: response.output_text.delta\ndata: {"delta":"unclosed\n\n',
|
| 136 |
-
);
|
| 137 |
-
|
| 138 |
-
const events = await collectEvents(api, response);
|
| 139 |
-
expect(events).toHaveLength(1);
|
| 140 |
-
// Should not throw — falls through to raw string
|
| 141 |
-
expect(typeof events[0].data).toBe("string");
|
| 142 |
-
});
|
| 143 |
-
|
| 144 |
-
it("skips empty blocks between events", async () => {
|
| 145 |
-
const api = createApi();
|
| 146 |
-
const response = mockResponse(
|
| 147 |
-
'event: response.output_text.delta\ndata: {"delta":"a"}\n\n' +
|
| 148 |
-
"\n\n" + // empty block
|
| 149 |
-
'event: response.output_text.delta\ndata: {"delta":"b"}\n\n',
|
| 150 |
-
);
|
| 151 |
-
|
| 152 |
-
const events = await collectEvents(api, response);
|
| 153 |
-
expect(events).toHaveLength(2);
|
| 154 |
-
});
|
| 155 |
-
|
| 156 |
-
it("processes remaining buffer after stream ends", async () => {
|
| 157 |
-
const api = createApi();
|
| 158 |
-
// No trailing \n\n — the event is only in the residual buffer
|
| 159 |
-
const response = mockResponse(
|
| 160 |
-
'event: response.output_text.delta\ndata: {"delta":"last"}',
|
| 161 |
-
);
|
| 162 |
-
|
| 163 |
-
const events = await collectEvents(api, response);
|
| 164 |
-
expect(events).toHaveLength(1);
|
| 165 |
-
expect(events[0].data).toEqual({ delta: "last" });
|
| 166 |
-
});
|
| 167 |
-
|
| 168 |
-
it("handles multi-line data fields", async () => {
|
| 169 |
-
const api = createApi();
|
| 170 |
-
const response = mockResponse(
|
| 171 |
-
'event: response.output_text.delta\ndata: {"delta":\n' +
|
| 172 |
-
'data: "multi-line"}\n\n',
|
| 173 |
-
);
|
| 174 |
-
|
| 175 |
-
const events = await collectEvents(api, response);
|
| 176 |
-
expect(events).toHaveLength(1);
|
| 177 |
-
// data lines are joined with \n: '{"delta":\n"multi-line"}'
|
| 178 |
-
expect(events[0].data).toEqual({ delta: "multi-line" });
|
| 179 |
-
});
|
| 180 |
-
|
| 181 |
-
it("returns null body error", async () => {
|
| 182 |
-
const api = createApi();
|
| 183 |
-
// Create a response with null body
|
| 184 |
-
const response = new Response(null);
|
| 185 |
-
|
| 186 |
-
await expect(async () => {
|
| 187 |
-
await collectEvents(api, response);
|
| 188 |
-
}).rejects.toThrow("Response body is null");
|
| 189 |
-
});
|
| 190 |
-
|
| 191 |
-
it("throws on buffer overflow (>10MB)", async () => {
|
| 192 |
-
const api = createApi();
|
| 193 |
-
// Create a chunk that exceeds the 10MB SSE buffer limit
|
| 194 |
-
const hugeData = "x".repeat(11 * 1024 * 1024);
|
| 195 |
-
const response = mockResponse(hugeData);
|
| 196 |
-
|
| 197 |
-
await expect(async () => {
|
| 198 |
-
await collectEvents(api, response);
|
| 199 |
-
}).rejects.toThrow("SSE buffer exceeded");
|
| 200 |
-
});
|
| 201 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/routes/accounts.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type { CookieJar } from "../proxy/cookie-jar.js";
|
|
| 26 |
import type { ProxyPool } from "../proxy/proxy-pool.js";
|
| 27 |
|
| 28 |
function toQuota(usage: CodexUsageResponse): CodexQuota {
|
|
|
|
| 29 |
return {
|
| 30 |
plan_type: usage.plan_type,
|
| 31 |
rate_limit: {
|
|
@@ -35,6 +36,14 @@ function toQuota(usage: CodexUsageResponse): CodexQuota {
|
|
| 35 |
reset_at: usage.rate_limit.primary_window?.reset_at ?? null,
|
| 36 |
limit_window_seconds: usage.rate_limit.primary_window?.limit_window_seconds ?? null,
|
| 37 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
code_review_rate_limit: usage.code_review_rate_limit
|
| 39 |
? {
|
| 40 |
allowed: usage.code_review_rate_limit.allowed,
|
|
|
|
| 26 |
import type { ProxyPool } from "../proxy/proxy-pool.js";
|
| 27 |
|
| 28 |
function toQuota(usage: CodexUsageResponse): CodexQuota {
|
| 29 |
+
const sw = usage.rate_limit.secondary_window;
|
| 30 |
return {
|
| 31 |
plan_type: usage.plan_type,
|
| 32 |
rate_limit: {
|
|
|
|
| 36 |
reset_at: usage.rate_limit.primary_window?.reset_at ?? null,
|
| 37 |
limit_window_seconds: usage.rate_limit.primary_window?.limit_window_seconds ?? null,
|
| 38 |
},
|
| 39 |
+
secondary_rate_limit: sw
|
| 40 |
+
? {
|
| 41 |
+
limit_reached: usage.rate_limit.limit_reached,
|
| 42 |
+
used_percent: sw.used_percent ?? null,
|
| 43 |
+
reset_at: sw.reset_at ?? null,
|
| 44 |
+
limit_window_seconds: sw.limit_window_seconds ?? null,
|
| 45 |
+
}
|
| 46 |
+
: null,
|
| 47 |
code_review_rate_limit: usage.code_review_rate_limit
|
| 48 |
? {
|
| 49 |
allowed: usage.code_review_rate_limit.allowed,
|
src/translation/__tests__/codex-event-extractor.test.ts
DELETED
|
@@ -1,182 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Tests for parseCodexEvent — the shared event type parser.
|
| 3 |
-
*
|
| 4 |
-
* Tests the discriminated union mapping from raw SSE events to typed events.
|
| 5 |
-
* This is the IR hub of the hub-and-spoke translation architecture.
|
| 6 |
-
*/
|
| 7 |
-
|
| 8 |
-
import { describe, it, expect } from "vitest";
|
| 9 |
-
import { parseCodexEvent } from "../../types/codex-events.js";
|
| 10 |
-
import type { CodexSSEEvent } from "../../proxy/codex-api.js";
|
| 11 |
-
|
| 12 |
-
describe("parseCodexEvent", () => {
|
| 13 |
-
it("parses response.created with id", () => {
|
| 14 |
-
const raw: CodexSSEEvent = {
|
| 15 |
-
event: "response.created",
|
| 16 |
-
data: { response: { id: "resp_abc123" } },
|
| 17 |
-
};
|
| 18 |
-
const typed = parseCodexEvent(raw);
|
| 19 |
-
expect(typed.type).toBe("response.created");
|
| 20 |
-
if (typed.type === "response.created") {
|
| 21 |
-
expect(typed.response.id).toBe("resp_abc123");
|
| 22 |
-
}
|
| 23 |
-
});
|
| 24 |
-
|
| 25 |
-
it("parses response.in_progress", () => {
|
| 26 |
-
const raw: CodexSSEEvent = {
|
| 27 |
-
event: "response.in_progress",
|
| 28 |
-
data: { response: { id: "resp_abc123" } },
|
| 29 |
-
};
|
| 30 |
-
const typed = parseCodexEvent(raw);
|
| 31 |
-
expect(typed.type).toBe("response.in_progress");
|
| 32 |
-
if (typed.type === "response.in_progress") {
|
| 33 |
-
expect(typed.response.id).toBe("resp_abc123");
|
| 34 |
-
}
|
| 35 |
-
});
|
| 36 |
-
|
| 37 |
-
it("parses response.output_text.delta", () => {
|
| 38 |
-
const raw: CodexSSEEvent = {
|
| 39 |
-
event: "response.output_text.delta",
|
| 40 |
-
data: { delta: "Hello, world!" },
|
| 41 |
-
};
|
| 42 |
-
const typed = parseCodexEvent(raw);
|
| 43 |
-
expect(typed.type).toBe("response.output_text.delta");
|
| 44 |
-
if (typed.type === "response.output_text.delta") {
|
| 45 |
-
expect(typed.delta).toBe("Hello, world!");
|
| 46 |
-
}
|
| 47 |
-
});
|
| 48 |
-
|
| 49 |
-
it("parses response.output_text.done", () => {
|
| 50 |
-
const raw: CodexSSEEvent = {
|
| 51 |
-
event: "response.output_text.done",
|
| 52 |
-
data: { text: "Complete response text" },
|
| 53 |
-
};
|
| 54 |
-
const typed = parseCodexEvent(raw);
|
| 55 |
-
expect(typed.type).toBe("response.output_text.done");
|
| 56 |
-
if (typed.type === "response.output_text.done") {
|
| 57 |
-
expect(typed.text).toBe("Complete response text");
|
| 58 |
-
}
|
| 59 |
-
});
|
| 60 |
-
|
| 61 |
-
it("parses response.completed with usage", () => {
|
| 62 |
-
const raw: CodexSSEEvent = {
|
| 63 |
-
event: "response.completed",
|
| 64 |
-
data: {
|
| 65 |
-
response: {
|
| 66 |
-
id: "resp_abc123",
|
| 67 |
-
usage: { input_tokens: 150, output_tokens: 42 },
|
| 68 |
-
},
|
| 69 |
-
},
|
| 70 |
-
};
|
| 71 |
-
const typed = parseCodexEvent(raw);
|
| 72 |
-
expect(typed.type).toBe("response.completed");
|
| 73 |
-
if (typed.type === "response.completed") {
|
| 74 |
-
expect(typed.response.id).toBe("resp_abc123");
|
| 75 |
-
expect(typed.response.usage).toEqual({
|
| 76 |
-
input_tokens: 150,
|
| 77 |
-
output_tokens: 42,
|
| 78 |
-
});
|
| 79 |
-
}
|
| 80 |
-
});
|
| 81 |
-
|
| 82 |
-
it("parses response.completed without usage", () => {
|
| 83 |
-
const raw: CodexSSEEvent = {
|
| 84 |
-
event: "response.completed",
|
| 85 |
-
data: { response: { id: "resp_abc123" } },
|
| 86 |
-
};
|
| 87 |
-
const typed = parseCodexEvent(raw);
|
| 88 |
-
expect(typed.type).toBe("response.completed");
|
| 89 |
-
if (typed.type === "response.completed") {
|
| 90 |
-
expect(typed.response.usage).toBeUndefined();
|
| 91 |
-
}
|
| 92 |
-
});
|
| 93 |
-
|
| 94 |
-
it("returns unknown for unrecognized event types", () => {
|
| 95 |
-
const raw: CodexSSEEvent = {
|
| 96 |
-
event: "response.some_future_event",
|
| 97 |
-
data: { foo: "bar" },
|
| 98 |
-
};
|
| 99 |
-
const typed = parseCodexEvent(raw);
|
| 100 |
-
expect(typed.type).toBe("unknown");
|
| 101 |
-
if (typed.type === "unknown") {
|
| 102 |
-
expect(typed.raw).toEqual({ foo: "bar" });
|
| 103 |
-
}
|
| 104 |
-
});
|
| 105 |
-
|
| 106 |
-
it("returns unknown when response.created has no response object", () => {
|
| 107 |
-
const raw: CodexSSEEvent = {
|
| 108 |
-
event: "response.created",
|
| 109 |
-
data: "not an object",
|
| 110 |
-
};
|
| 111 |
-
const typed = parseCodexEvent(raw);
|
| 112 |
-
expect(typed.type).toBe("unknown");
|
| 113 |
-
});
|
| 114 |
-
|
| 115 |
-
it("returns unknown when response.created data has no response field", () => {
|
| 116 |
-
const raw: CodexSSEEvent = {
|
| 117 |
-
event: "response.created",
|
| 118 |
-
data: { something_else: true },
|
| 119 |
-
};
|
| 120 |
-
const typed = parseCodexEvent(raw);
|
| 121 |
-
expect(typed.type).toBe("unknown");
|
| 122 |
-
});
|
| 123 |
-
|
| 124 |
-
it("returns unknown when delta is not a string", () => {
|
| 125 |
-
const raw: CodexSSEEvent = {
|
| 126 |
-
event: "response.output_text.delta",
|
| 127 |
-
data: { delta: 123 },
|
| 128 |
-
};
|
| 129 |
-
const typed = parseCodexEvent(raw);
|
| 130 |
-
expect(typed.type).toBe("unknown");
|
| 131 |
-
});
|
| 132 |
-
|
| 133 |
-
it("handles empty delta string", () => {
|
| 134 |
-
const raw: CodexSSEEvent = {
|
| 135 |
-
event: "response.output_text.delta",
|
| 136 |
-
data: { delta: "" },
|
| 137 |
-
};
|
| 138 |
-
const typed = parseCodexEvent(raw);
|
| 139 |
-
expect(typed.type).toBe("response.output_text.delta");
|
| 140 |
-
if (typed.type === "response.output_text.delta") {
|
| 141 |
-
expect(typed.delta).toBe("");
|
| 142 |
-
}
|
| 143 |
-
});
|
| 144 |
-
|
| 145 |
-
it("defaults usage token counts to 0 for non-numeric values", () => {
|
| 146 |
-
const raw: CodexSSEEvent = {
|
| 147 |
-
event: "response.completed",
|
| 148 |
-
data: {
|
| 149 |
-
response: {
|
| 150 |
-
id: "resp_1",
|
| 151 |
-
usage: { input_tokens: "not a number", output_tokens: null },
|
| 152 |
-
},
|
| 153 |
-
},
|
| 154 |
-
};
|
| 155 |
-
const typed = parseCodexEvent(raw);
|
| 156 |
-
expect(typed.type).toBe("response.completed");
|
| 157 |
-
if (typed.type === "response.completed") {
|
| 158 |
-
expect(typed.response.usage).toEqual({
|
| 159 |
-
input_tokens: 0,
|
| 160 |
-
output_tokens: 0,
|
| 161 |
-
});
|
| 162 |
-
}
|
| 163 |
-
});
|
| 164 |
-
|
| 165 |
-
it("handles null data", () => {
|
| 166 |
-
const raw: CodexSSEEvent = {
|
| 167 |
-
event: "response.created",
|
| 168 |
-
data: null,
|
| 169 |
-
};
|
| 170 |
-
const typed = parseCodexEvent(raw);
|
| 171 |
-
expect(typed.type).toBe("unknown");
|
| 172 |
-
});
|
| 173 |
-
|
| 174 |
-
it("handles array data", () => {
|
| 175 |
-
const raw: CodexSSEEvent = {
|
| 176 |
-
event: "response.output_text.delta",
|
| 177 |
-
data: [1, 2, 3],
|
| 178 |
-
};
|
| 179 |
-
const typed = parseCodexEvent(raw);
|
| 180 |
-
expect(typed.type).toBe("unknown");
|
| 181 |
-
});
|
| 182 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/AccountCard.tsx
CHANGED
|
@@ -66,7 +66,7 @@ export function AccountCard({ account, index, onDelete, proxies, onProxyChange }
|
|
| 66 |
if (err) alert(err);
|
| 67 |
}, [account.id, onDelete, t]);
|
| 68 |
|
| 69 |
-
// Quota
|
| 70 |
const q = account.quota;
|
| 71 |
const rl = q?.rate_limit;
|
| 72 |
const pct = rl?.used_percent != null ? Math.round(rl.used_percent) : null;
|
|
@@ -82,6 +82,23 @@ export function AccountCard({ account, index, onDelete, proxies, onProxyChange }
|
|
| 82 |
: "text-primary";
|
| 83 |
const resetAt = rl?.reset_at ? formatResetTime(rl.reset_at, lang === "zh") : null;
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
return (
|
| 86 |
<div class="bg-white dark:bg-card-dark border border-gray-200 dark:border-border-dark rounded-xl p-4 shadow-sm hover:shadow-md transition-all hover:border-primary/30 dark:hover:border-primary/50">
|
| 87 |
{/* Header */}
|
|
@@ -158,37 +175,77 @@ export function AccountCard({ account, index, onDelete, proxies, onProxyChange }
|
|
| 158 |
</div>
|
| 159 |
)}
|
| 160 |
|
| 161 |
-
{/* Quota
|
| 162 |
-
{rl && (
|
| 163 |
-
<div class="pt-3 mt-3 border-t border-slate-100 dark:border-border-dark">
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
<
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
</div>
|
| 182 |
)}
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
)}
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
)}
|
| 193 |
</div>
|
| 194 |
)}
|
|
|
|
| 66 |
if (err) alert(err);
|
| 67 |
}, [account.id, onDelete, t]);
|
| 68 |
|
| 69 |
+
// Quota — primary window
|
| 70 |
const q = account.quota;
|
| 71 |
const rl = q?.rate_limit;
|
| 72 |
const pct = rl?.used_percent != null ? Math.round(rl.used_percent) : null;
|
|
|
|
| 82 |
: "text-primary";
|
| 83 |
const resetAt = rl?.reset_at ? formatResetTime(rl.reset_at, lang === "zh") : null;
|
| 84 |
|
| 85 |
+
// Quota — secondary window (e.g. weekly)
|
| 86 |
+
const srl = q?.secondary_rate_limit;
|
| 87 |
+
const sPct = srl?.used_percent != null ? Math.round(srl.used_percent) : null;
|
| 88 |
+
const sBarColor =
|
| 89 |
+
sPct == null ? "bg-indigo-500" : sPct >= 90 ? "bg-red-500" : sPct >= 60 ? "bg-amber-500" : "bg-indigo-500";
|
| 90 |
+
const sPctColor =
|
| 91 |
+
sPct == null
|
| 92 |
+
? "text-indigo-500"
|
| 93 |
+
: sPct >= 90
|
| 94 |
+
? "text-red-500"
|
| 95 |
+
: sPct >= 60
|
| 96 |
+
? "text-amber-600 dark:text-amber-500"
|
| 97 |
+
: "text-indigo-500";
|
| 98 |
+
const sResetAt = srl?.reset_at ? formatResetTime(srl.reset_at, lang === "zh") : null;
|
| 99 |
+
const sWindowSec = srl?.limit_window_seconds;
|
| 100 |
+
const sWindowDur = sWindowSec ? formatWindowDuration(sWindowSec, lang === "zh") : null;
|
| 101 |
+
|
| 102 |
return (
|
| 103 |
<div class="bg-white dark:bg-card-dark border border-gray-200 dark:border-border-dark rounded-xl p-4 shadow-sm hover:shadow-md transition-all hover:border-primary/30 dark:hover:border-primary/50">
|
| 104 |
{/* Header */}
|
|
|
|
| 175 |
</div>
|
| 176 |
)}
|
| 177 |
|
| 178 |
+
{/* Quota bars */}
|
| 179 |
+
{(rl || srl) && (
|
| 180 |
+
<div class="pt-3 mt-3 border-t border-slate-100 dark:border-border-dark space-y-3">
|
| 181 |
+
{/* Primary window */}
|
| 182 |
+
{rl && (
|
| 183 |
+
<div>
|
| 184 |
+
<div class="flex justify-between text-[0.78rem] mb-1.5">
|
| 185 |
+
<span class="text-slate-500 dark:text-text-dim">
|
| 186 |
+
{t("rateLimit")}
|
| 187 |
+
{windowDur && (
|
| 188 |
+
<span class="ml-1 text-slate-400 dark:text-text-dim/70 text-[0.65rem]">({windowDur})</span>
|
| 189 |
+
)}
|
| 190 |
+
</span>
|
| 191 |
+
{rl.limit_reached ? (
|
| 192 |
+
<span class="px-2 py-0.5 rounded-full bg-red-100 dark:bg-red-900/20 text-red-600 dark:text-red-400 text-xs font-medium">
|
| 193 |
+
{t("limitReached")}
|
| 194 |
+
</span>
|
| 195 |
+
) : pct != null ? (
|
| 196 |
+
<span class={`font-medium ${pctColor}`}>
|
| 197 |
+
{pct}% {t("used")}
|
| 198 |
+
</span>
|
| 199 |
+
) : (
|
| 200 |
+
<span class="font-medium text-primary">{t("ok")}</span>
|
| 201 |
+
)}
|
| 202 |
+
</div>
|
| 203 |
+
{pct != null && (
|
| 204 |
+
<div class="w-full bg-slate-100 dark:bg-border-dark rounded-full h-2 overflow-hidden">
|
| 205 |
+
<div class={`${barColor} h-2 rounded-full transition-all`} style={{ width: `${pct}%` }} />
|
| 206 |
+
</div>
|
| 207 |
+
)}
|
| 208 |
+
{resetAt && (
|
| 209 |
+
<p class="text-xs text-slate-400 dark:text-text-dim mt-1">
|
| 210 |
+
{t("resetsAt")} {resetAt}
|
| 211 |
+
</p>
|
| 212 |
+
)}
|
| 213 |
</div>
|
| 214 |
)}
|
| 215 |
+
|
| 216 |
+
{/* Secondary window (e.g. weekly) */}
|
| 217 |
+
{srl && (
|
| 218 |
+
<div>
|
| 219 |
+
<div class="flex justify-between text-[0.78rem] mb-1.5">
|
| 220 |
+
<span class="text-slate-500 dark:text-text-dim">
|
| 221 |
+
{t("secondaryRateLimit")}
|
| 222 |
+
{sWindowDur && (
|
| 223 |
+
<span class="ml-1 text-slate-400 dark:text-text-dim/70 text-[0.65rem]">({sWindowDur})</span>
|
| 224 |
+
)}
|
| 225 |
</span>
|
| 226 |
+
{srl.limit_reached ? (
|
| 227 |
+
<span class="px-2 py-0.5 rounded-full bg-red-100 dark:bg-red-900/20 text-red-600 dark:text-red-400 text-xs font-medium">
|
| 228 |
+
{t("limitReached")}
|
| 229 |
+
</span>
|
| 230 |
+
) : sPct != null ? (
|
| 231 |
+
<span class={`font-medium ${sPctColor}`}>
|
| 232 |
+
{sPct}% {t("used")}
|
| 233 |
+
</span>
|
| 234 |
+
) : (
|
| 235 |
+
<span class="font-medium text-indigo-500">{t("ok")}</span>
|
| 236 |
+
)}
|
| 237 |
+
</div>
|
| 238 |
+
{sPct != null && (
|
| 239 |
+
<div class="w-full bg-slate-100 dark:bg-border-dark rounded-full h-2 overflow-hidden">
|
| 240 |
+
<div class={`${sBarColor} h-2 rounded-full transition-all`} style={{ width: `${sPct}%` }} />
|
| 241 |
+
</div>
|
| 242 |
)}
|
| 243 |
+
{sResetAt && (
|
| 244 |
+
<p class="text-xs text-slate-400 dark:text-text-dim mt-1">
|
| 245 |
+
{t("resetsAt")} {sResetAt}
|
| 246 |
+
</p>
|
| 247 |
+
)}
|
| 248 |
+
</div>
|
| 249 |
)}
|
| 250 |
</div>
|
| 251 |
)}
|
web/src/hooks/use-accounts.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
| 1 |
import { useState, useEffect, useCallback } from "preact/hooks";
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
export interface AccountQuota {
|
| 4 |
-
rate_limit?:
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
reset_at?: number | null;
|
| 8 |
-
limit_window_seconds?: number | null;
|
| 9 |
-
};
|
| 10 |
}
|
| 11 |
|
| 12 |
export interface Account {
|
|
|
|
| 1 |
import { useState, useEffect, useCallback } from "preact/hooks";
|
| 2 |
|
| 3 |
+
export interface AccountQuotaWindow {
|
| 4 |
+
used_percent?: number | null;
|
| 5 |
+
limit_reached?: boolean;
|
| 6 |
+
reset_at?: number | null;
|
| 7 |
+
limit_window_seconds?: number | null;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
export interface AccountQuota {
|
| 11 |
+
rate_limit?: AccountQuotaWindow;
|
| 12 |
+
/** Secondary rate limit window (e.g. weekly cap). */
|
| 13 |
+
secondary_rate_limit?: AccountQuotaWindow | null;
|
|
|
|
|
|
|
|
|
|
| 14 |
}
|
| 15 |
|
| 16 |
export interface Account {
|