dsh-connect-minimaxcode 0.5.3 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +20 -0
- package/README.md +48 -0
- package/lib/client.js +69 -3
- package/lib/index.d.ts +91 -2
- package/lib/index.js +177 -5
- package/package.json +11 -3
package/README.en.md
CHANGED
|
@@ -79,6 +79,12 @@ dsh plugin --profile desktop add dsh-connect-minimaxcode
|
|
|
79
79
|
|
|
80
80
|
For the Web profile, replace `--profile desktop` with `--profile web`.
|
|
81
81
|
|
|
82
|
+
> **Installing a locally modified clone**: do not pass a directory path to `dsh plugin add` — pnpm installs it as a link and nests dependency symlinks inside the plugin's own `node_modules`; the host's plugin sandbox gets `EPERM` stat-ing those links and the plugin fails to activate (`failed to import`). Pack a tarball first, then install the tarball so dependencies resolve in the profile root `node_modules` like a registry install:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pnpm pack
|
|
86
|
+
dsh plugin --profile desktop add ./dsh-connect-minimaxcode-0.5.5.tgz
|
|
87
|
+
```
|
|
82
88
|
> The desktop profile is owned exclusively by the Electron app. If the `dsh` command is not on your PATH, use the carrier CLI bundled with the DSH app (the path depends on the install location, for example `<DSH install dir>\resources\runtime\cli\bin\dsh.cmd`).
|
|
83
89
|
|
|
84
90
|
After installing, the plugin hot-reloads, and the models then show up under the **MiniMax Code** group in the model picker.
|
|
@@ -297,10 +303,22 @@ The OAuth token is valid for about an hour and is refreshed automatically by the
|
|
|
297
303
|
- **No settings card is provided**. The plugin does not register a card on the DSH settings page; the interface it adds is a single pill in the composer tool row (**drawn only while this plugin's model is selected**) and a small card on the plugin page.
|
|
298
304
|
- **The upstream APIs are non-public** and may stop working when MiniMax updates its client. If the balance API breaks, the pill shows "credits unavailable", which does not affect model traffic.
|
|
299
305
|
- **Image input depends on the host's attachment service.** The catalog declares the `image` modality (`M3`, `M3.1-Flash`), but turning a durable attachment into request bytes is the host's job: without that service the plugin still loads and serves text, and only image requests are refused.
|
|
306
|
+
- **The context window is declared as the gateway's real ceiling (1,000,000), not the 512,000 in the catalog.** Upstream's `limit.context` is the *default* window and the billing tier boundary (input above 512k costs more); the largest entry of `context_window_options` is the ceiling the gateway enforces. Measured on 2026-10-25: 946,206 tokens were served normally, 1,050,000 were refused (`invalid params, context window exceeds limit (2013)`). Declaring 512,000 meant every turn of a long conversation was reported as `pi-ai detected context overflow` — while the model had answered perfectly well and the reply was thrown away.
|
|
300
307
|
- **The device code flow needs `account.minimax.cn` to be reachable**, and the browser that authorizes it has to be able to sign in to a MiniMax account. On some restricted networks the authorization page cannot be opened.
|
|
301
308
|
|
|
302
309
|
## Changelog
|
|
303
310
|
|
|
311
|
+
### 0.5.5 (2026-11-06)
|
|
312
|
+
|
|
313
|
+
- **New "rate-limit windows" readout**: reads the open platform's `GET /backend/account/token_plan/remains_percent` (found in the desktop app's own traffic, likewise undocumented) and shows the plan's **5-hour window** and **weekly window** with used percentages and a reset countdown. Window lengths are derived from the response's own edges, never hard-coded.
|
|
314
|
+
- The endpoint belongs to the account domain and **accepts only the OAuth token** (desktop app or plugin sign-in); a JWT answers `1016 invalid api key`. The read rides the credits poller with independent failure handling.
|
|
315
|
+
- Shown in the pill detail and the plugin page card; `tests/rate-limit.spec.ts` pins parsing, URL, failure handling and the polling wiring.
|
|
316
|
+
### 0.5.4 (2026-10-29)
|
|
317
|
+
|
|
318
|
+
- **Fixed long conversations being reported as context overflow**: upstream's `limit.context` (512,000) is the *default* window and the billing tier boundary, while the ceiling the gateway enforces is the 1,000,000 in `context_window_options`. Declaring 512,000 meant that once a conversation crossed that line, *every* turn was reported as `pi-ai detected context overflow` — even though the model answered normally and the reply was discarded. The window is now declared as the ceiling the gateway actually enforces.
|
|
319
|
+
- The built-in fallback roster (used when the catalog cannot be fetched) now declares 1,000,000 as well.
|
|
320
|
+
- Four new cases pin the window parsing: the largest of `context_window_options` wins, upstream's `limit.context` is the fallback, a malformed option list is ignored, and the fallback roster does not claim 512k.
|
|
321
|
+
|
|
304
322
|
### 0.5.3 (2026-10-25)
|
|
305
323
|
|
|
306
324
|
- **Fixed image input**: an upload used to fail every time — `pi-ai image input requires the durable attachment service` (`UNSUPPORTED_CONTENT`). The catalog always declared `image` among a model's input modalities, but the plugin built its `PiAiAdapter` without `resolveAttachments` or `resolveImageAccess`, so pi-ai had no way to turn a durable attachment reference into request bytes and refused the call before it reached the gateway. The plugin now reads the host's `attachments` service on demand and maps an attachment's host path into the current execution world with `fs.processPathFromHostPath`; a host that offers no attachment service gets `undefined` for both hooks, and text-only calls are unchanged.
|
|
@@ -455,6 +473,8 @@ dsh plugin --profile web exec dsh-workbuddy-connect status
|
|
|
455
473
|
## Contributors
|
|
456
474
|
|
|
457
475
|
- **[eghrhegpe](https://github.com/eghrhegpe)** — raised the request for a plugin-page icon and Chinese/English copy ([#1](https://github.com/JeffTsuiCUG/dsh-connect-minimaxcode/issues/1)). It became 0.5.2: the plugin page has its own SVG artwork, the title and the introduction come in both languages, and the interface follows the host's language.
|
|
476
|
+
- **[azsora](https://github.com/azsora)** — found the plan's rate-limit endpoint in the desktop app's own traffic and wired the 5-hour and weekly usage windows into the credit pill and the plugin page ([#4](https://github.com/JeffTsuiCUG/dsh-connect-minimaxcode/pull/4)): the window length is derived from the response's own edges rather than hard-coded. It became 0.5.5.
|
|
477
|
+
- **[qzz19970504](https://github.com/qzz19970504)** — found and fixed the wiring gap that made every image upload fail ([#3](https://github.com/JeffTsuiCUG/dsh-connect-minimaxcode/pull/3)): the catalog declared the `image` modality, but the adapter never connected the host's durable attachment service, so pi-ai refused image requests before they were sent. The fix became 0.5.3.
|
|
458
478
|
- Plugin author: **[JeffTsuiCUG](https://github.com/JeffTsuiCUG)**
|
|
459
479
|
|
|
460
480
|
## Acknowledgements
|
package/README.md
CHANGED
|
@@ -85,6 +85,12 @@ dsh plugin --profile desktop add dsh-connect-minimaxcode
|
|
|
85
85
|
|
|
86
86
|
Web profile 把 `--profile desktop` 换成 `--profile web`。
|
|
87
87
|
|
|
88
|
+
> **本地开发装自己改过的克隆**:不要把目录路径直接传给 `dsh plugin add`——pnpm 会以链接方式安装,把依赖的软链嵌进插件自己的 `node_modules`,宿主的插件沙箱 `stat` 这些链接会得到 `EPERM`,插件激活失败(`failed to import`)。先 `pnpm pack` 打成 tarball,再安装 tarball,依赖就会像 registry 安装一样解析在 profile 根 `node_modules`:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pnpm pack
|
|
92
|
+
dsh plugin --profile desktop add ./dsh-connect-minimaxcode-0.5.5.tgz
|
|
93
|
+
```
|
|
88
94
|
> 桌面版的 profile 由 Electron 应用独占,若 `dsh` 命令不在 PATH 中,请使用 DSH 应用自带的载体 CLI(路径随安装位置而变,例如 `<DSH安装目录>\resources\runtime\cli\bin\dsh.cmd`)。
|
|
89
95
|
|
|
90
96
|
安装后插件会热加载,模型随后出现在模型选择器的 **MiniMax Code** 分组中。
|
|
@@ -283,6 +289,32 @@ GET https://agent.minimax.cn/minimax-cloud/api/v1/credit/details?page_number=1&p
|
|
|
283
289
|
|
|
284
290
|
> 官方价目表没有单列**缓存写入**(`cache_creation_input_tokens`)的单价,因此插件只统计它的 token 数、**不折算金额**,以免给出一个没有公开依据的数字。
|
|
285
291
|
|
|
292
|
+
### 限额窗口(套餐的两个滚动窗口)
|
|
293
|
+
|
|
294
|
+
套餐用量按两个滚动窗口计量(当前套餐为 5 小时与 7 天),插件会显示各窗口的已用百分比与重置倒计时:
|
|
295
|
+
|
|
296
|
+
```
|
|
297
|
+
GET https://www.minimaxi.com/backend/account/token_plan/remains_percent?business=Agent&device_platform=desktop
|
|
298
|
+
Authorization: Bearer <OAuth 令牌>
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
```json
|
|
302
|
+
"model_remains": [{
|
|
303
|
+
"model_name": "general",
|
|
304
|
+
"start_time": 1793882204091, "end_time": 1793900204091, // 差值 = 5 小时
|
|
305
|
+
"current_interval_used_percent": "19%",
|
|
306
|
+
"weekly_start_time": 1793783898054, "weekly_end_time": 1794388698054, // 差值 = 7 天
|
|
307
|
+
"current_weekly_used_percent": "29%"
|
|
308
|
+
}]
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
几点实测结论:
|
|
312
|
+
|
|
313
|
+
- **窗口时长不写死**:从响应自身的起止字段推导(实测恰为 5 小时与 7 天),套餐条款变了显示会跟着变 —— 胶囊与详情行上的窗口名(`5h`、`7d`)也是这么来的,不是写死的字面量
|
|
314
|
+
- **无限量的窗口不报百分比**:上游用 `current_*_status` 标出没有额度的窗口(桌面 App 读作 `isUnlimited: 3 === status`),这类窗口不参与显示,避免出现「0%」这种没人能据此行动的读数
|
|
315
|
+
- **该接口在开放平台域**(`www.minimaxi.com`),只认 OAuth 令牌;JWT 打它返回 `1016 invalid api key`
|
|
316
|
+
- **接口异常时其余显示不受影响**:限额读不到,胶囊与卡片仍显示余额、签到与本机用量
|
|
317
|
+
- 上游还发 `current_*_count` 计数字段(无计数制时为 `-1`),插件只读百分比与起止时刻
|
|
286
318
|
### 关于续期
|
|
287
319
|
|
|
288
320
|
OAuth 令牌有效期约 1 小时,由客户端自动续期。插件**只在自己的令牌已过期时**才用 App 已存的 refresh token 续一次,并把轮换后的新令牌按客户端**原格式原子写回**同一个文件(`generation` 保持不变),因此桌面 App 的登录态不受影响。插件不会发起登录、不会登出、也不会调用任何付费/订阅类接口。
|
|
@@ -303,10 +335,24 @@ OAuth 令牌有效期约 1 小时,由客户端自动续期。插件**只在自
|
|
|
303
335
|
- **不提供设置界面卡片**。插件不在 DSH 设置页注册卡片;界面只在输入框工具行增加一枚胶囊(**仅选中本插件的模型时显示**),以及插件页面里的一张小卡片。
|
|
304
336
|
- **上游接口非公开**,MiniMax 客户端更新后可能失效。余额接口失效时胶囊会显示「积分不可用」,不会影响模型对话。
|
|
305
337
|
- **发图依赖宿主的附件服务**。模型目录声明了 `image` 模态(`M3`、`M3.1-Flash`),但把持久化附件读成请求字节是宿主的活儿:宿主没有附件服务时插件仍能加载并服务文本,只是带图的请求会被拒绝。
|
|
338
|
+
- **上下文窗口按网关的真实上限声明(1,000,000),不是目录里的 512,000**。上游 `limit.context` 是**默认档位与计费分界**(输入超过 512k 的部分按更高倍率计费),`context_window_options` 里最大的一档才是网关执行的硬上限。实测(2026-10-25):946,206 tokens 正常受理,1,050,000 被拒(`invalid params, context window exceeds limit (2013)`)。此前按 512,000 声明,长会话一旦过线就**每一轮**都被判成「上下文溢出」,模型其实答得好好的,回复却被丢掉(`pi-ai detected context overflow`)。
|
|
306
339
|
- **设备码流需要能访问 `account.minimax.cn`**,且授权时要在浏览器里能登录 MiniMax 账号。某些内网或受限网络下打不开授权页。
|
|
307
340
|
|
|
308
341
|
## 更新记录
|
|
309
342
|
|
|
343
|
+
### 0.5.5(2026-11-07)
|
|
344
|
+
|
|
345
|
+
- **新增「限额窗口」显示**:读取开放平台的 `GET /backend/account/token_plan/remains_percent`(该接口来自桌面 App 每天真实调用的日志,同样未在官方文档中公开),展示套餐的**5 小时窗口**与**每周窗口**的已用百分比与重置倒计时。窗口时长从响应自身的起止字段推导,不写死 5 小时或 7 天——套餐条款变过不止一次,显示应跟随服务端所说。
|
|
346
|
+
- 该接口属于账户域,**只认 OAuth 令牌**(桌面 App 登录态或插件自登录均可);JWT 打它返回 `1016 invalid api key`。读取挂在积分轮询上,独立容错:限额读不到不影响余额显示,反之亦然。
|
|
347
|
+
- 显示位置:胶囊详情与插件页卡片各两行;新增 `tests/rate-limit.spec.ts` 钉住解析、URL、容错与轮询接线。
|
|
348
|
+
- 窗口名跟随服务端:胶囊主标签与详情行上的窗口长度(`5h`、`7d`)从响应的起止字段推导,而不是写死 —— 解析层早已「不写死窗口长度」,显示层此前却写着「5h」,两处现在一致。
|
|
349
|
+
- 无限量窗口不报百分比:上游用 `current_*_status` 标出没有额度的窗口,这类窗口不参与显示,避免出现没人能据此行动的「0%」。
|
|
350
|
+
### 0.5.4(2026-10-29)
|
|
351
|
+
|
|
352
|
+
- **修复长会话被误判为「上下文溢出」**:上游 `limit.context`(512,000)是**默认档位与计费分界**,网关实际执行的上限是 `context_window_options` 里的 1,000,000。此前按 512,000 声明,会话一旦超过这条线,**每一轮**都会被判定为 `pi-ai detected context overflow` —— 而模型其实正常答了,回复却被丢弃。现在按网关的真实上限声明窗口。
|
|
353
|
+
- 兜底名录(离线时的内置猜测)同步改为 1,000,000。
|
|
354
|
+
- 新增 4 个用例钉住窗口解析:取 `context_window_options` 的最大值、上游没给就回落 `limit.context`、非法列表被忽略、兜底名录的窗口不是 512k。
|
|
355
|
+
|
|
310
356
|
### 0.5.3(2026-10-25)
|
|
311
357
|
|
|
312
358
|
- **修复图片输入**:此前发图必然失败 —— `本轮运行失败 pi-ai image input requires the durable attachment service`(`UNSUPPORTED_CONTENT`)。模型目录一直把 `image` 写进模型的输入模态,但插件构造 `PiAiAdapter` 时没有给 `resolveAttachments` 与 `resolveImageAccess` 两个钩子,pi-ai 因此无法把持久化的附件引用变成请求字节,在发往网关之前就拒绝了调用。现在插件按需读取宿主的 `attachments` 服务,并用 `fs.processPathFromHostPath` 把附件的宿主路径映射进当前执行世界;宿主没有提供附件服务时这两个钩子为 `undefined`,纯文本调用不受影响。
|
|
@@ -461,6 +507,8 @@ dsh plugin --profile web exec dsh-workbuddy-connect status
|
|
|
461
507
|
## 贡献者
|
|
462
508
|
|
|
463
509
|
- **[eghrhegpe](https://github.com/eghrhegpe)** —— 提出了插件页图标与中英文文案的建议([#1](https://github.com/JeffTsuiCUG/dsh-connect-minimaxcode/issues/1))。这条建议促成了 0.5.2:插件页有了自己的 SVG 图标,标题与简介有了中英两版,界面文案也开始跟随宿主语言。
|
|
510
|
+
- **[azsora](https://github.com/azsora)** —— 从桌面 App 的实际调用里找出了套餐的**限额窗口接口**,并把 5 小时与每周两个窗口的用量接进了积分胶囊与插件页卡片([#4](https://github.com/JeffTsuiCUG/dsh-connect-minimaxcode/pull/4)):窗口时长从响应自身的起止字段推导,不写死。该功能成为 0.5.5。
|
|
511
|
+
- **[qzz19970504](https://github.com/qzz19970504)** —— 定位并修复了「发图必然失败」的接线缺口([#3](https://github.com/JeffTsuiCUG/dsh-connect-minimaxcode/pull/3)):模型目录声明了 `image` 模态,但适配器没有把宿主的持久化附件服务接上,导致带图请求被 pi-ai 在发出前拒掉。该修复成为 0.5.3。
|
|
464
512
|
- 插件作者:**[JeffTsuiCUG](https://github.com/JeffTsuiCUG)**
|
|
465
513
|
|
|
466
514
|
## 致谢
|
package/lib/client.js
CHANGED
|
@@ -66,6 +66,8 @@
|
|
|
66
66
|
signinAuto: '每天自动签到',
|
|
67
67
|
signinClaim: '签到',
|
|
68
68
|
claimableToday: '今日可签到',
|
|
69
|
+
usageWindow: '{duration}限额',
|
|
70
|
+
resetIn: '{duration}后',
|
|
69
71
|
};
|
|
70
72
|
const en = {
|
|
71
73
|
title: 'MiniMax Code credits',
|
|
@@ -106,6 +108,8 @@
|
|
|
106
108
|
signinAuto: 'Claim daily on its own',
|
|
107
109
|
signinClaim: 'Claim',
|
|
108
110
|
claimableToday: 'Claimable today',
|
|
111
|
+
usageWindow: '{duration} limit',
|
|
112
|
+
resetIn: 'resets in {duration}',
|
|
109
113
|
};
|
|
110
114
|
const loader = globalThis.__ModuleLoader__;
|
|
111
115
|
if (loader === undefined)
|
|
@@ -208,6 +212,53 @@
|
|
|
208
212
|
? row(t('expiringSoon', { amount: credits(credits_.expiringSoon, 2) }), '')
|
|
209
213
|
: null, next !== undefined ? row(t('nextExpiry'), day(next)) : null);
|
|
210
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* A countdown to a window reset, coarse on purpose: minutes below an
|
|
217
|
+
* hour, hours and minutes below a day, days and hours beyond. It is
|
|
218
|
+
* computed at render time, so it is as fresh as the last status read.
|
|
219
|
+
*/
|
|
220
|
+
function countdown(resetAtMs) {
|
|
221
|
+
const minutes = Math.max(0, Math.ceil((resetAtMs - Date.now()) / 60000));
|
|
222
|
+
if (minutes < 60)
|
|
223
|
+
return `${minutes}m`;
|
|
224
|
+
if (minutes < 1440)
|
|
225
|
+
return `${Math.floor(minutes / 60)}h${minutes % 60}m`;
|
|
226
|
+
return `${Math.floor(minutes / 1440)}d${Math.floor((minutes % 1440) / 60)}h`;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The length of a window, as the server defines it.
|
|
230
|
+
*
|
|
231
|
+
* The window's own edges give the length, so the label follows the plan
|
|
232
|
+
* instead of the plan's terms today: five hours today reads "5h", and a
|
|
233
|
+
* plan that moved to a three-hour window reads "3h" without a code
|
|
234
|
+
* change. Only the word around it is translated.
|
|
235
|
+
*/
|
|
236
|
+
function windowLabel(window) {
|
|
237
|
+
const minutes = Math.max(1, Math.round((window.resetAtMs - window.startAtMs) / 60000));
|
|
238
|
+
if (minutes >= 1440)
|
|
239
|
+
return `${Math.round(minutes / 1440)}d`;
|
|
240
|
+
if (minutes >= 60) {
|
|
241
|
+
const hours = minutes / 60;
|
|
242
|
+
return Number.isInteger(hours) ? `${hours}h` : `${Math.floor(hours)}h${minutes % 60}m`;
|
|
243
|
+
}
|
|
244
|
+
return `${minutes}m`;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* The plan's two rolling usage windows, shared by the pill's detail and
|
|
248
|
+
* the page card.
|
|
249
|
+
*
|
|
250
|
+
* The percent sits on the right with the reset countdown in
|
|
251
|
+
* parentheses, and the window length is derived from the window itself
|
|
252
|
+
* rather than written into the label: the plan's terms have changed
|
|
253
|
+
* before, and the display should follow whatever the server says.
|
|
254
|
+
*/
|
|
255
|
+
function rateLimitBlock(t, rateLimit) {
|
|
256
|
+
if (rateLimit === undefined || rateLimit.state !== 'ok')
|
|
257
|
+
return null;
|
|
258
|
+
if (rateLimit.interval === undefined || rateLimit.weekly === undefined)
|
|
259
|
+
return null;
|
|
260
|
+
return h('div', { style: expiryBoxStyle, 'data-minimax-rate-limit': true }, row(t('usageWindow', { duration: windowLabel(rateLimit.interval) }), `${rateLimit.interval.usedPercent}%(${t('resetIn', { duration: countdown(rateLimit.interval.resetAtMs) })})`), row(t('usageWindow', { duration: windowLabel(rateLimit.weekly) }), `${rateLimit.weekly.usedPercent}%(${t('resetIn', { duration: countdown(rateLimit.weekly.resetAtMs) })})`));
|
|
261
|
+
}
|
|
211
262
|
/** The pill itself; the gate decides whether it is drawn at all. */
|
|
212
263
|
function CreditPill(props) {
|
|
213
264
|
const [status] = useStatus();
|
|
@@ -218,14 +269,29 @@
|
|
|
218
269
|
});
|
|
219
270
|
const credits_ = status?.credits;
|
|
220
271
|
const usage = status?.usage;
|
|
272
|
+
const rateLimit = status?.rateLimit;
|
|
221
273
|
const available = credits_?.state === 'ok' && credits_.total !== undefined;
|
|
222
|
-
const
|
|
274
|
+
const windows = rateLimit?.state === 'ok' && rateLimit.interval !== undefined && rateLimit.weekly !== undefined
|
|
275
|
+
? { interval: rateLimit.interval, weekly: rateLimit.weekly }
|
|
276
|
+
: undefined;
|
|
277
|
+
// A window that is used up means the plan cannot serve the next
|
|
278
|
+
// request right now, so the balance is the number worth seeing.
|
|
279
|
+
// Otherwise the pill leads with the short window — the one that gates
|
|
280
|
+
// the next request — named by the length the server gives it. No
|
|
281
|
+
// windows, the balance again: the pill never shows nothing just
|
|
282
|
+
// because one read failed.
|
|
283
|
+
const exhausted = windows !== undefined
|
|
284
|
+
&& (windows.interval.usedPercent >= 100 || windows.weekly.usedPercent >= 100);
|
|
285
|
+
const balance = available
|
|
223
286
|
? t('balance', { total: credits(credits_.total, 2) })
|
|
224
287
|
: t(credits_?.state === 'expired' ? 'expired' : credits_?.state === 'signed-out' ? 'signedOut' : 'unavailable');
|
|
288
|
+
const label = windows === undefined || exhausted
|
|
289
|
+
? balance
|
|
290
|
+
: `${windowLabel(windows.interval)} ${windows.interval.usedPercent}%(${t('resetIn', { duration: countdown(windows.interval.resetAtMs) })})`;
|
|
225
291
|
const detail = available
|
|
226
292
|
? h('div', { style: detailStyle, 'data-minimax-credits-detail': true }, row(t('free'), credits(credits_.free, 3)), row(t('purchased'), credits(credits_.purchased, 3)), credits_.consumed !== undefined && credits_.consumed > 0
|
|
227
293
|
? row(t('consumed'), credits(credits_.consumed, 3))
|
|
228
|
-
: null, expiryBlock(t, credits_), usage !== undefined
|
|
294
|
+
: null, expiryBlock(t, credits_), rateLimitBlock(t, status?.rateLimit), usage !== undefined
|
|
229
295
|
? h('div', { style: rowStyle }, h('span', null, t('tokens', { count: tokens(usage.inputTokens + usage.outputTokens + usage.cacheReadTokens) })), h('span', null, t('cost', { amount: money(usage.estimatedCny) })))
|
|
230
296
|
: null, credits_.updatedAtMs !== undefined
|
|
231
297
|
? row(t('updated', { time: new Date(credits_.updatedAtMs).toLocaleTimeString() }), '')
|
|
@@ -366,7 +432,7 @@
|
|
|
366
432
|
if (!available) {
|
|
367
433
|
return h('div', { style: cardStyle, 'data-minimax-credits-card': true }, h('div', { style: cardTitleStyle }, t('title')), h('div', { style: rowStyle }, h('span', null, t('unavailable')), h('span', null, credits_?.state === 'expired' ? t('expired') : '')), loginBlock(t, login, refresh));
|
|
368
434
|
}
|
|
369
|
-
return h('div', { style: cardStyle, 'data-minimax-credits-card': true }, h('div', { style: cardTitleStyle }, t('title')), h('div', { style: bigNumberStyle }, t('balance', { total: credits(credits_.total, 2) })), row(t('free'), credits(credits_.free, 3)), row(t('purchased'), credits(credits_.purchased, 3)), expiryBlock(t, credits_), signinBlock(t, signin, refresh), h('div', { style: sectionTitleStyle }, t('lots')), (credits_.upcoming ?? []).length === 0
|
|
435
|
+
return h('div', { style: cardStyle, 'data-minimax-credits-card': true }, h('div', { style: cardTitleStyle }, t('title')), h('div', { style: bigNumberStyle }, t('balance', { total: credits(credits_.total, 2) })), row(t('free'), credits(credits_.free, 3)), row(t('purchased'), credits(credits_.purchased, 3)), expiryBlock(t, credits_), rateLimitBlock(t, status?.rateLimit), signinBlock(t, signin, refresh), h('div', { style: sectionTitleStyle }, t('lots')), (credits_.upcoming ?? []).length === 0
|
|
370
436
|
? h('div', { style: mutedStyle }, t('noLots'))
|
|
371
437
|
: h('ul', { style: listStyle }, ...(credits_.upcoming ?? []).map((lot, index) => h('li', { key: `${lot.expireAtMs}-${index}`, style: rowStyle }, h('span', null, day(lot.expireAtMs)), h('span', null, credits(lot.amount, 2))))), usage !== undefined
|
|
372
438
|
? h('div', { style: usageBoxStyle }, row(t('tokens', { count: tokens(usage.inputTokens + usage.outputTokens + usage.cacheReadTokens) }), ''), row(t('cost', { amount: money(usage.estimatedCny) }), ''), usage.requests > 0 ? row(t('requests', { count: usage.requests }), usage.lastModel ?? '') : null)
|
package/lib/index.d.ts
CHANGED
|
@@ -145,6 +145,11 @@ declare const MINIMAXCODE_PROVIDER_ID = "minimaxcode";
|
|
|
145
145
|
* when upstream retired `MiniMax-M2.7` and `MiniMax-M2.7-highspeed` and
|
|
146
146
|
* promoted the model previously served anonymously (internally "space bunny")
|
|
147
147
|
* as `MiniMax-M3.1-Flash`.
|
|
148
|
+
*
|
|
149
|
+
* The window is 1,000,000 rather than the 512,000 the upstream `limit` block
|
|
150
|
+
* reports: 512k is the billing tier boundary, and the gateway itself was
|
|
151
|
+
* measured serving 946,206 tokens before refusing ~1,050,000 with
|
|
152
|
+
* `invalid params, context window exceeds limit (2013)`.
|
|
148
153
|
*/
|
|
149
154
|
declare const FALLBACK_MODELS: readonly CatalogModel[];
|
|
150
155
|
/** Where the currently served list came from. */
|
|
@@ -203,6 +208,8 @@ declare class MinimaxCatalog {
|
|
|
203
208
|
declare const MINIMAX_OAUTH_CLIENT_ID = "mcode-public";
|
|
204
209
|
/** Membership endpoint: carries the credit summary. */
|
|
205
210
|
declare const MEMBERSHIP_PATH = "/matrix/api/v1/commerce/get_membership_info";
|
|
211
|
+
/** Rate-limit window endpoint: the 5-hour and weekly usage the plan meters. */
|
|
212
|
+
declare const REMAINS_PERCENT_PATH = "/backend/account/token_plan/remains_percent";
|
|
206
213
|
/** The OAuth credential the account endpoints accept. */
|
|
207
214
|
interface OAuthCredential {
|
|
208
215
|
/** The `mmoat_` bearer token. */
|
|
@@ -331,6 +338,48 @@ interface CreditLot {
|
|
|
331
338
|
/** The upstream's kind of lot, when it says. */
|
|
332
339
|
creditType?: number;
|
|
333
340
|
}
|
|
341
|
+
/** One metered window: a span of time and how much of its allowance went. */
|
|
342
|
+
interface UsageWindow {
|
|
343
|
+
/** The share of the window's allowance already used, in percent. */
|
|
344
|
+
usedPercent: number;
|
|
345
|
+
/** When the window opened, in milliseconds since the epoch. */
|
|
346
|
+
startAtMs: number;
|
|
347
|
+
/** When the window closes and the allowance resets, in milliseconds since the epoch. */
|
|
348
|
+
resetAtMs: number;
|
|
349
|
+
}
|
|
350
|
+
/** The two rolling windows the plan meters usage in. */
|
|
351
|
+
interface RateLimitWindows {
|
|
352
|
+
/** The short window (five hours on the plans seen so far). */
|
|
353
|
+
interval: UsageWindow;
|
|
354
|
+
/** The weekly window (seven days on the plans seen so far). */
|
|
355
|
+
weekly: UsageWindow;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Read the two rate-limit windows out of a remains-percent response.
|
|
359
|
+
*
|
|
360
|
+
* The upstream answers with one entry per metered group; the general one is
|
|
361
|
+
* the only one the plan meters today, and the first entry is used when it is
|
|
362
|
+
* absent. The percents arrive as strings ("19%"), and the counts arrive as -1
|
|
363
|
+
* when the plan does not meter by count, so only the percents and the window
|
|
364
|
+
* edges are read. The window length is derived from the entry's own endpoints
|
|
365
|
+
* rather than assumed to be five hours or seven days — the plan's terms have
|
|
366
|
+
* changed before, and the display should follow whatever the server says.
|
|
367
|
+
*
|
|
368
|
+
* A window the server marks as unlimited (`current_*_status` of 3) carries no
|
|
369
|
+
* allowance to be a percentage of, so it produces no window at all: reporting
|
|
370
|
+
* "0%" of an unlimited quota would be a number nobody can act on.
|
|
371
|
+
*/
|
|
372
|
+
declare function parseRateLimitWindows(document: unknown): RateLimitWindows | undefined;
|
|
373
|
+
/**
|
|
374
|
+
* Fetch the plan's two rate-limit windows.
|
|
375
|
+
*
|
|
376
|
+
* The endpoint lives on the open-platform host, like the usage summary, and
|
|
377
|
+
* answers the same OAuth credential the membership endpoint takes. The JWT the
|
|
378
|
+
* model gateway accepts does not work here: this is an account endpoint.
|
|
379
|
+
*
|
|
380
|
+
* @returns The windows, or `undefined` when the response carries none.
|
|
381
|
+
*/
|
|
382
|
+
declare function fetchRateLimitWindows(credential: OAuthCredential, region: MinimaxRegion, fetchImpl?: typeof fetch): Promise<RateLimitWindows | undefined>;
|
|
334
383
|
/** What the lots say about credits that are about to be lost. */
|
|
335
384
|
interface ExpirySummary {
|
|
336
385
|
/** Credits in lots that expire within {@link EXPIRING_WINDOW_DAYS} days. */
|
|
@@ -369,12 +418,20 @@ declare class MinimaxCredits {
|
|
|
369
418
|
get ledgerAtMs(): number | undefined;
|
|
370
419
|
/** Why the ledger could not be read, when it could not. */
|
|
371
420
|
get ledgerError(): string | undefined;
|
|
421
|
+
/** The plan's rate-limit windows as last read. */
|
|
422
|
+
get rateLimit(): RateLimitWindows | undefined;
|
|
423
|
+
/** Why the windows could not be read, when they could not. */
|
|
424
|
+
get rateLimitError(): string | undefined;
|
|
425
|
+
/** When the windows were last read. */
|
|
426
|
+
get rateLimitAtMs(): number | undefined;
|
|
372
427
|
/**
|
|
373
428
|
* Read the balance and record how much was consumed since the last read.
|
|
374
429
|
*
|
|
375
430
|
* The credit ledger is read alongside it, but a ledger that cannot be read
|
|
376
431
|
* never hides the balance: the two come from different endpoints and only
|
|
377
|
-
* the balance is essential.
|
|
432
|
+
* the balance is essential. The rate-limit windows ride along under the
|
|
433
|
+
* same rule — they come from yet another endpoint, and none of the three
|
|
434
|
+
* takes the others down with it.
|
|
378
435
|
*
|
|
379
436
|
* @returns Whether the balance changed.
|
|
380
437
|
*/
|
|
@@ -387,6 +444,13 @@ declare class MinimaxCredits {
|
|
|
387
444
|
* take it down with it.
|
|
388
445
|
*/
|
|
389
446
|
refreshLedger(credential: OAuthCredential, region: MinimaxRegion, fetchImpl?: typeof fetch, now?: number): Promise<void>;
|
|
447
|
+
/**
|
|
448
|
+
* Read the plan's rate-limit windows.
|
|
449
|
+
*
|
|
450
|
+
* Same rule as the ledger: a failure is remembered but never thrown, and a
|
|
451
|
+
* window that cannot be read leaves the last one on display.
|
|
452
|
+
*/
|
|
453
|
+
refreshRateLimit(credential: OAuthCredential, region: MinimaxRegion, fetchImpl?: typeof fetch, now?: number): Promise<void>;
|
|
390
454
|
}
|
|
391
455
|
//#endregion
|
|
392
456
|
//#region src/usage.d.ts
|
|
@@ -826,6 +890,27 @@ interface UsageStatus {
|
|
|
826
890
|
/** How many requests were measured. */
|
|
827
891
|
requests: number;
|
|
828
892
|
}
|
|
893
|
+
/** The plan's two rolling usage windows, as the card reads them. */
|
|
894
|
+
interface RateLimitStatus {
|
|
895
|
+
/** Whether the windows could be read. */
|
|
896
|
+
state: 'ok' | 'unavailable' | 'expired' | 'signed-out';
|
|
897
|
+
/** The short window (five hours on the plans seen so far). */
|
|
898
|
+
interval?: {
|
|
899
|
+
usedPercent: number;
|
|
900
|
+
startAtMs: number;
|
|
901
|
+
resetAtMs: number;
|
|
902
|
+
};
|
|
903
|
+
/** The weekly window. */
|
|
904
|
+
weekly?: {
|
|
905
|
+
usedPercent: number;
|
|
906
|
+
startAtMs: number;
|
|
907
|
+
resetAtMs: number;
|
|
908
|
+
};
|
|
909
|
+
/** When the windows were read, in milliseconds since the epoch. */
|
|
910
|
+
updatedAtMs?: number;
|
|
911
|
+
/** Why the windows could not be read. */
|
|
912
|
+
error?: string;
|
|
913
|
+
}
|
|
829
914
|
/** What the card renders. */
|
|
830
915
|
interface StatusDocument {
|
|
831
916
|
state: 'signed-in' | 'signed-out' | 'expired';
|
|
@@ -848,6 +933,8 @@ interface StatusDocument {
|
|
|
848
933
|
refreshing?: boolean;
|
|
849
934
|
/** The account's credit balance, when it could be read. */
|
|
850
935
|
credits?: CreditStatus;
|
|
936
|
+
/** The plan's rolling usage windows, when they could be read. */
|
|
937
|
+
rateLimit?: RateLimitStatus;
|
|
851
938
|
/** What this host's requests have used. */
|
|
852
939
|
usage?: UsageStatus;
|
|
853
940
|
/** The daily check-in, when it could be read. */
|
|
@@ -874,6 +961,8 @@ interface StatusDeps {
|
|
|
874
961
|
refreshing: boolean;
|
|
875
962
|
/** The account's credit balance, when the account API could be reached. */
|
|
876
963
|
credits?: CreditStatus;
|
|
964
|
+
/** The plan's rolling usage windows, when the open platform could be reached. */
|
|
965
|
+
rateLimit?: RateLimitStatus;
|
|
877
966
|
/** What this host's requests have used. */
|
|
878
967
|
usage?: UsageStatus;
|
|
879
968
|
/** The daily check-in, when the account API could be reached. */
|
|
@@ -963,4 +1052,4 @@ declare const inject: string[];
|
|
|
963
1052
|
*/
|
|
964
1053
|
declare function apply(ctx: Context): void;
|
|
965
1054
|
//#endregion
|
|
966
|
-
export { type AuthState, type CatalogModel, type CatalogSource, type Credential, type CreditSummary, DEVICE_CODE_GRANT, DEVICE_CODE_PATH, type DeviceLoginState, EMPTY_USAGE, FALLBACK_MODELS, MEMBERSHIP_PATH, MINIMAXCODE_PROVIDER_ID, MINIMAX_ACTION_PATH, MINIMAX_AUTH_FILENAME, MINIMAX_DATA_DIR, MINIMAX_GATEWAYS, MINIMAX_OAUTH_CLIENT_ID, MINIMAX_REGIONS, MINIMAX_STATUS_PATH, MINIMAX_STREAM_IDLE_TIMEOUT_MS, MODEL_PRICES, MinimaxCatalog, MinimaxCredits, type MinimaxRegion, type ModelUsage, type OAuthCredential, type OAuthState, PLUGIN_CREDENTIAL_FILENAME, PLUGIN_STATE_FILENAME, type PluginCredential, type PluginState, SIGNIN_CLAIM_PATH, SIGNIN_CLAIM_RESULT, SIGNIN_CYCLE_DAYS, SIGNIN_DAY_STATUS, SIGNIN_PANEL_PATH, type SigninClaim, type SigninDay, type SigninPanel, type StatusDocument, type TokenClaims, UsageMeter, apply, authPath, bearerToken, catalogUrl, chatBaseUrl, claimSignin, createMinimaxCodeAdapter, dataDir, daysRemaining, decodeClaims, dshHome, ensureOAuthCredential, estimateCny, expiresInMs, expiryLevel, fetchCreditSummary, fetchSigninPanel, inject, localDay, makePkce, name, parseAuthDocument, parseCatalog, parseCreditSummary, parseOAuthDocument, parseSigninClaim, parseSigninPanel, pluginCredentialPath, pluginStatePath, pollDeviceLogin, priceFor, readCredential, readOAuthCredential, readPluginCredential, readPluginState, refreshOAuthCredential, refreshPluginCredential, startDeviceLogin, statusDocument, timezoneId, writeOAuthCredential, writePluginCredential, writePluginState };
|
|
1055
|
+
export { type AuthState, type CatalogModel, type CatalogSource, type Credential, type CreditSummary, DEVICE_CODE_GRANT, DEVICE_CODE_PATH, type DeviceLoginState, EMPTY_USAGE, FALLBACK_MODELS, MEMBERSHIP_PATH, MINIMAXCODE_PROVIDER_ID, MINIMAX_ACTION_PATH, MINIMAX_AUTH_FILENAME, MINIMAX_DATA_DIR, MINIMAX_GATEWAYS, MINIMAX_OAUTH_CLIENT_ID, MINIMAX_REGIONS, MINIMAX_STATUS_PATH, MINIMAX_STREAM_IDLE_TIMEOUT_MS, MODEL_PRICES, MinimaxCatalog, MinimaxCredits, type MinimaxRegion, type ModelUsage, type OAuthCredential, type OAuthState, PLUGIN_CREDENTIAL_FILENAME, PLUGIN_STATE_FILENAME, type PluginCredential, type PluginState, REMAINS_PERCENT_PATH, type RateLimitWindows, SIGNIN_CLAIM_PATH, SIGNIN_CLAIM_RESULT, SIGNIN_CYCLE_DAYS, SIGNIN_DAY_STATUS, SIGNIN_PANEL_PATH, type SigninClaim, type SigninDay, type SigninPanel, type StatusDocument, type TokenClaims, UsageMeter, apply, authPath, bearerToken, catalogUrl, chatBaseUrl, claimSignin, createMinimaxCodeAdapter, dataDir, daysRemaining, decodeClaims, dshHome, ensureOAuthCredential, estimateCny, expiresInMs, expiryLevel, fetchCreditSummary, fetchRateLimitWindows, fetchSigninPanel, inject, localDay, makePkce, name, parseAuthDocument, parseCatalog, parseCreditSummary, parseOAuthDocument, parseRateLimitWindows, parseSigninClaim, parseSigninPanel, pluginCredentialPath, pluginStatePath, pollDeviceLogin, priceFor, readCredential, readOAuthCredential, readPluginCredential, readPluginState, refreshOAuthCredential, refreshPluginCredential, startDeviceLogin, statusDocument, timezoneId, writeOAuthCredential, writePluginCredential, writePluginState };
|
package/lib/index.js
CHANGED
|
@@ -238,11 +238,16 @@ const MINIMAXCODE_PROVIDER_ID = "minimaxcode";
|
|
|
238
238
|
* when upstream retired `MiniMax-M2.7` and `MiniMax-M2.7-highspeed` and
|
|
239
239
|
* promoted the model previously served anonymously (internally "space bunny")
|
|
240
240
|
* as `MiniMax-M3.1-Flash`.
|
|
241
|
+
*
|
|
242
|
+
* The window is 1,000,000 rather than the 512,000 the upstream `limit` block
|
|
243
|
+
* reports: 512k is the billing tier boundary, and the gateway itself was
|
|
244
|
+
* measured serving 946,206 tokens before refusing ~1,050,000 with
|
|
245
|
+
* `invalid params, context window exceeds limit (2013)`.
|
|
241
246
|
*/
|
|
242
247
|
const FALLBACK_MODELS = [{
|
|
243
248
|
id: "MiniMax-M3",
|
|
244
249
|
name: "M3",
|
|
245
|
-
contextWindow:
|
|
250
|
+
contextWindow: 1e6,
|
|
246
251
|
maxOutputTokens: 128e3,
|
|
247
252
|
toolCall: true,
|
|
248
253
|
reasoning: true,
|
|
@@ -255,7 +260,7 @@ const FALLBACK_MODELS = [{
|
|
|
255
260
|
}, {
|
|
256
261
|
id: "MiniMax-M3.1-Flash",
|
|
257
262
|
name: "M3.1-Flash",
|
|
258
|
-
contextWindow:
|
|
263
|
+
contextWindow: 1e6,
|
|
259
264
|
maxOutputTokens: 128e3,
|
|
260
265
|
toolCall: true,
|
|
261
266
|
reasoning: true,
|
|
@@ -278,12 +283,42 @@ function sameCatalog(a, b) {
|
|
|
278
283
|
function usable(id, raw) {
|
|
279
284
|
return id.length > 0 && id.startsWith("MiniMax-") && typeof raw === "object" && raw !== null;
|
|
280
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* The window the gateway actually enforces.
|
|
288
|
+
*
|
|
289
|
+
* Upstream reports two different numbers, and they mean different things:
|
|
290
|
+
* `limit.context` is the *default* window — the billing tier boundary, above
|
|
291
|
+
* which input costs more — while `context_window_options` lists every window
|
|
292
|
+
* the gateway will accept, the largest being the hard ceiling.
|
|
293
|
+
*
|
|
294
|
+
* Measured against the live gateway on 2026-10-25: a prompt of 946,206 tokens
|
|
295
|
+
* was served normally, and one of ~1,050,000 was refused with
|
|
296
|
+
* `invalid params, context window exceeds limit (2013)`. The ceiling is
|
|
297
|
+
* therefore the largest option, not `limit.context`.
|
|
298
|
+
*
|
|
299
|
+
* Declaring the default instead of the ceiling is not a cosmetic difference:
|
|
300
|
+
* the adapter compares the gateway's own token count against this number, so a
|
|
301
|
+
* conversation that runs past 512k is reported as a context overflow on every
|
|
302
|
+
* turn even though the gateway answers it perfectly well.
|
|
303
|
+
*
|
|
304
|
+
* @param raw - The upstream model entry.
|
|
305
|
+
* @param limit - The upstream `limit` block, already read.
|
|
306
|
+
* @returns The window to declare, in tokens.
|
|
307
|
+
*/
|
|
308
|
+
function readContextWindow(raw, limit) {
|
|
309
|
+
const options = raw.context_window_options;
|
|
310
|
+
if (Array.isArray(options)) {
|
|
311
|
+
const sizes = options.filter((value) => typeof value === "number" && Number.isInteger(value) && value > 0);
|
|
312
|
+
if (sizes.length > 0) return Math.max(...sizes);
|
|
313
|
+
}
|
|
314
|
+
return typeof limit.context === "number" ? limit.context : 2e5;
|
|
315
|
+
}
|
|
281
316
|
/** Read the fields the upstream sends, tolerating their absence. */
|
|
282
317
|
function readModel(id, raw) {
|
|
283
318
|
const limit = raw.limit ?? {};
|
|
284
319
|
const modalities = raw.modalities ?? {};
|
|
285
320
|
const input = Array.isArray(modalities.input) ? modalities.input.filter((m) => typeof m === "string") : ["text"];
|
|
286
|
-
const contextWindow =
|
|
321
|
+
const contextWindow = readContextWindow(raw, limit);
|
|
287
322
|
return {
|
|
288
323
|
id,
|
|
289
324
|
name: typeof raw.name === "string" ? raw.name : id,
|
|
@@ -677,6 +712,7 @@ function statusDocument(deps) {
|
|
|
677
712
|
models: deps.models,
|
|
678
713
|
refreshing: deps.refreshing,
|
|
679
714
|
...deps.credits === void 0 ? {} : { credits: deps.credits },
|
|
715
|
+
...deps.rateLimit === void 0 ? {} : { rateLimit: deps.rateLimit },
|
|
680
716
|
...deps.usage === void 0 ? {} : { usage: deps.usage },
|
|
681
717
|
...deps.signin === void 0 ? {} : { signin: deps.signin },
|
|
682
718
|
...deps.login === void 0 ? {} : { login: deps.login }
|
|
@@ -745,10 +781,18 @@ const MINIMAX_ACCOUNT_HOSTS = {
|
|
|
745
781
|
io: "https://agent.minimax.io",
|
|
746
782
|
com: "https://agent.minimaxi.com"
|
|
747
783
|
};
|
|
784
|
+
/** Open-platform account API, which serves the per-model usage summary. */
|
|
785
|
+
const MINIMAX_OPEN_HOSTS = {
|
|
786
|
+
cn: "https://www.minimaxi.com",
|
|
787
|
+
io: "https://www.minimaxi.com",
|
|
788
|
+
com: "https://www.minimaxi.com"
|
|
789
|
+
};
|
|
748
790
|
/** Membership endpoint: carries the credit summary. */
|
|
749
791
|
const MEMBERSHIP_PATH = "/matrix/api/v1/commerce/get_membership_info";
|
|
750
792
|
/** Credit ledger endpoint: per-grant amounts and expiry. */
|
|
751
793
|
const CREDIT_DETAILS_PATH = "/minimax-cloud/api/v1/credit/details";
|
|
794
|
+
/** Rate-limit window endpoint: the 5-hour and weekly usage the plan meters. */
|
|
795
|
+
const REMAINS_PERCENT_PATH = "/backend/account/token_plan/remains_percent";
|
|
752
796
|
/** OAuth token endpoint. */
|
|
753
797
|
const TOKEN_PATH$1 = "/oauth2/token";
|
|
754
798
|
/** Absolute path of the app's credential file for a region. */
|
|
@@ -1053,6 +1097,84 @@ async function fetchCreditLots(credential, region, fetchImpl = fetch) {
|
|
|
1053
1097
|
return lots;
|
|
1054
1098
|
}
|
|
1055
1099
|
/**
|
|
1100
|
+
* Read the two rate-limit windows out of a remains-percent response.
|
|
1101
|
+
*
|
|
1102
|
+
* The upstream answers with one entry per metered group; the general one is
|
|
1103
|
+
* the only one the plan meters today, and the first entry is used when it is
|
|
1104
|
+
* absent. The percents arrive as strings ("19%"), and the counts arrive as -1
|
|
1105
|
+
* when the plan does not meter by count, so only the percents and the window
|
|
1106
|
+
* edges are read. The window length is derived from the entry's own endpoints
|
|
1107
|
+
* rather than assumed to be five hours or seven days — the plan's terms have
|
|
1108
|
+
* changed before, and the display should follow whatever the server says.
|
|
1109
|
+
*
|
|
1110
|
+
* A window the server marks as unlimited (`current_*_status` of 3) carries no
|
|
1111
|
+
* allowance to be a percentage of, so it produces no window at all: reporting
|
|
1112
|
+
* "0%" of an unlimited quota would be a number nobody can act on.
|
|
1113
|
+
*/
|
|
1114
|
+
function parseRateLimitWindows(document) {
|
|
1115
|
+
if (typeof document !== "object" || document === null) return void 0;
|
|
1116
|
+
const remains = document.model_remains;
|
|
1117
|
+
if (!Array.isArray(remains) || remains.length === 0) return void 0;
|
|
1118
|
+
const entry = remains.find((item) => typeof item === "object" && item !== null && item.model_name === "general") ?? remains[0];
|
|
1119
|
+
if (typeof entry !== "object" || entry === null) return void 0;
|
|
1120
|
+
const record = entry;
|
|
1121
|
+
const interval = readWindow(record.current_interval_used_percent, record.start_time, record.end_time, record.current_interval_status);
|
|
1122
|
+
const weekly = readWindow(record.current_weekly_used_percent, record.weekly_start_time, record.weekly_end_time, record.current_weekly_status);
|
|
1123
|
+
if (interval === void 0 || weekly === void 0) return void 0;
|
|
1124
|
+
return {
|
|
1125
|
+
interval,
|
|
1126
|
+
weekly
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* The status the upstream gives a window that is not limited at all.
|
|
1131
|
+
*
|
|
1132
|
+
* The desktop app reads it the same way: `isUnlimited: 3 === status`.
|
|
1133
|
+
*/
|
|
1134
|
+
const UNLIMITED_STATUS = 3;
|
|
1135
|
+
/** Assemble one window from its percent and edge fields; all must be present. */
|
|
1136
|
+
function readWindow(percent, startAtMs, resetAtMs, status) {
|
|
1137
|
+
if (status === UNLIMITED_STATUS) return void 0;
|
|
1138
|
+
const usedPercent = parsePercent(percent);
|
|
1139
|
+
const start = toNumber$1(startAtMs);
|
|
1140
|
+
const reset = toNumber$1(resetAtMs);
|
|
1141
|
+
if (usedPercent === void 0 || start === void 0 || reset === void 0) return void 0;
|
|
1142
|
+
return {
|
|
1143
|
+
usedPercent,
|
|
1144
|
+
startAtMs: start,
|
|
1145
|
+
resetAtMs: reset
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Read "19%" as 19, and a bare 19 as 19 as well.
|
|
1150
|
+
*
|
|
1151
|
+
* The desktop app parses both shapes, so both are accepted here: anything
|
|
1152
|
+
* unreadable stays unreadable rather than becoming a zero.
|
|
1153
|
+
*/
|
|
1154
|
+
function parsePercent(value) {
|
|
1155
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
|
|
1156
|
+
if (typeof value !== "string") return void 0;
|
|
1157
|
+
const parsed = Number(value.replace(/%\s*$/, ""));
|
|
1158
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Fetch the plan's two rate-limit windows.
|
|
1162
|
+
*
|
|
1163
|
+
* The endpoint lives on the open-platform host, like the usage summary, and
|
|
1164
|
+
* answers the same OAuth credential the membership endpoint takes. The JWT the
|
|
1165
|
+
* model gateway accepts does not work here: this is an account endpoint.
|
|
1166
|
+
*
|
|
1167
|
+
* @returns The windows, or `undefined` when the response carries none.
|
|
1168
|
+
*/
|
|
1169
|
+
async function fetchRateLimitWindows(credential, region, fetchImpl = fetch) {
|
|
1170
|
+
const response = await fetchImpl(`${MINIMAX_OPEN_HOSTS[region]}${REMAINS_PERCENT_PATH}?business=Agent&device_platform=desktop`, { headers: {
|
|
1171
|
+
accept: "application/json",
|
|
1172
|
+
authorization: `Bearer ${credential.token}`
|
|
1173
|
+
} });
|
|
1174
|
+
if (!response.ok) throw new Error(`rate-limit window request failed: HTTP ${response.status}`);
|
|
1175
|
+
return parseRateLimitWindows(await response.json());
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1056
1178
|
* Summarize what is about to expire.
|
|
1057
1179
|
*
|
|
1058
1180
|
* Only lots that still hold credits count: a lot that was granted and used up
|
|
@@ -1089,6 +1211,9 @@ var MinimaxCredits = class {
|
|
|
1089
1211
|
#expiry;
|
|
1090
1212
|
#ledgerAtMs;
|
|
1091
1213
|
#ledgerError;
|
|
1214
|
+
#rateLimit;
|
|
1215
|
+
#rateLimitError;
|
|
1216
|
+
#rateLimitAtMs;
|
|
1092
1217
|
/** The balance as last read. */
|
|
1093
1218
|
get snapshot() {
|
|
1094
1219
|
return this.#snapshot;
|
|
@@ -1113,12 +1238,26 @@ var MinimaxCredits = class {
|
|
|
1113
1238
|
get ledgerError() {
|
|
1114
1239
|
return this.#ledgerError;
|
|
1115
1240
|
}
|
|
1241
|
+
/** The plan's rate-limit windows as last read. */
|
|
1242
|
+
get rateLimit() {
|
|
1243
|
+
return this.#rateLimit;
|
|
1244
|
+
}
|
|
1245
|
+
/** Why the windows could not be read, when they could not. */
|
|
1246
|
+
get rateLimitError() {
|
|
1247
|
+
return this.#rateLimitError;
|
|
1248
|
+
}
|
|
1249
|
+
/** When the windows were last read. */
|
|
1250
|
+
get rateLimitAtMs() {
|
|
1251
|
+
return this.#rateLimitAtMs;
|
|
1252
|
+
}
|
|
1116
1253
|
/**
|
|
1117
1254
|
* Read the balance and record how much was consumed since the last read.
|
|
1118
1255
|
*
|
|
1119
1256
|
* The credit ledger is read alongside it, but a ledger that cannot be read
|
|
1120
1257
|
* never hides the balance: the two come from different endpoints and only
|
|
1121
|
-
* the balance is essential.
|
|
1258
|
+
* the balance is essential. The rate-limit windows ride along under the
|
|
1259
|
+
* same rule — they come from yet another endpoint, and none of the three
|
|
1260
|
+
* takes the others down with it.
|
|
1122
1261
|
*
|
|
1123
1262
|
* @returns Whether the balance changed.
|
|
1124
1263
|
*/
|
|
@@ -1141,6 +1280,7 @@ var MinimaxCredits = class {
|
|
|
1141
1280
|
this.#error = error instanceof Error ? error.message : String(error);
|
|
1142
1281
|
}
|
|
1143
1282
|
await this.refreshLedger(credential, region, fetchImpl, now);
|
|
1283
|
+
await this.refreshRateLimit(credential, region, fetchImpl, now);
|
|
1144
1284
|
return changed;
|
|
1145
1285
|
}
|
|
1146
1286
|
/**
|
|
@@ -1161,6 +1301,22 @@ var MinimaxCredits = class {
|
|
|
1161
1301
|
this.#ledgerError = error instanceof Error ? error.message : String(error);
|
|
1162
1302
|
}
|
|
1163
1303
|
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Read the plan's rate-limit windows.
|
|
1306
|
+
*
|
|
1307
|
+
* Same rule as the ledger: a failure is remembered but never thrown, and a
|
|
1308
|
+
* window that cannot be read leaves the last one on display.
|
|
1309
|
+
*/
|
|
1310
|
+
async refreshRateLimit(credential, region, fetchImpl = fetch, now = Date.now()) {
|
|
1311
|
+
try {
|
|
1312
|
+
const windows = await fetchRateLimitWindows(credential, region, fetchImpl);
|
|
1313
|
+
this.#rateLimit = windows;
|
|
1314
|
+
this.#rateLimitError = windows === void 0 ? "response carried no rate-limit windows" : void 0;
|
|
1315
|
+
this.#rateLimitAtMs = now;
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
this.#rateLimitError = error instanceof Error ? error.message : String(error);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1164
1320
|
};
|
|
1165
1321
|
//#endregion
|
|
1166
1322
|
//#region src/credentials.ts
|
|
@@ -2115,6 +2271,21 @@ function apply(ctx) {
|
|
|
2115
2271
|
...credits.ledgerError === void 0 ? {} : { ledgerError: credits.ledgerError }
|
|
2116
2272
|
};
|
|
2117
2273
|
};
|
|
2274
|
+
/** The plan's rolling usage windows as the card reads them. */
|
|
2275
|
+
const buildRateLimitStatus = () => {
|
|
2276
|
+
if (creditState === "signed-out" || creditState === "expired") return { state: creditState };
|
|
2277
|
+
const windows = credits.rateLimit;
|
|
2278
|
+
if (windows === void 0) return {
|
|
2279
|
+
state: "unavailable",
|
|
2280
|
+
...credits.rateLimitError === void 0 ? {} : { error: credits.rateLimitError }
|
|
2281
|
+
};
|
|
2282
|
+
return {
|
|
2283
|
+
state: "ok",
|
|
2284
|
+
interval: windows.interval,
|
|
2285
|
+
weekly: windows.weekly,
|
|
2286
|
+
...credits.rateLimitAtMs === void 0 ? {} : { updatedAtMs: credits.rateLimitAtMs }
|
|
2287
|
+
};
|
|
2288
|
+
};
|
|
2118
2289
|
/** What this host's own requests have used. */
|
|
2119
2290
|
const buildUsageStatus = () => {
|
|
2120
2291
|
const totals = meter.totals();
|
|
@@ -2217,6 +2388,7 @@ function apply(ctx) {
|
|
|
2217
2388
|
})),
|
|
2218
2389
|
refreshing,
|
|
2219
2390
|
credits: buildCreditStatus(),
|
|
2391
|
+
rateLimit: buildRateLimitStatus(),
|
|
2220
2392
|
usage: buildUsageStatus(),
|
|
2221
2393
|
signin: buildSigninStatus(),
|
|
2222
2394
|
login: buildLoginStatus()
|
|
@@ -2410,4 +2582,4 @@ async function readRequestBody(req) {
|
|
|
2410
2582
|
return text;
|
|
2411
2583
|
}
|
|
2412
2584
|
//#endregion
|
|
2413
|
-
export { DEVICE_CODE_GRANT, DEVICE_CODE_PATH, EMPTY_USAGE, FALLBACK_MODELS, MEMBERSHIP_PATH, MINIMAXCODE_PROVIDER_ID, MINIMAX_ACTION_PATH, MINIMAX_AUTH_FILENAME, MINIMAX_DATA_DIR, MINIMAX_GATEWAYS, MINIMAX_OAUTH_CLIENT_ID, MINIMAX_REGIONS, MINIMAX_STATUS_PATH, MINIMAX_STREAM_IDLE_TIMEOUT_MS, MODEL_PRICES, MinimaxCatalog, MinimaxCredits, PLUGIN_CREDENTIAL_FILENAME, PLUGIN_STATE_FILENAME, SIGNIN_CLAIM_PATH, SIGNIN_CLAIM_RESULT, SIGNIN_CYCLE_DAYS, SIGNIN_DAY_STATUS, SIGNIN_PANEL_PATH, UsageMeter, apply, authPath, bearerToken, catalogUrl, chatBaseUrl, claimSignin, createMinimaxCodeAdapter, dataDir, daysRemaining, decodeClaims, dshHome, ensureOAuthCredential, estimateCny, expiresInMs, expiryLevel, fetchCreditSummary, fetchSigninPanel, inject, localDay, makePkce, name, parseAuthDocument, parseCatalog, parseCreditSummary, parseOAuthDocument, parseSigninClaim, parseSigninPanel, pluginCredentialPath, pluginStatePath, pollDeviceLogin, priceFor, readCredential, readOAuthCredential, readPluginCredential, readPluginState, refreshOAuthCredential, refreshPluginCredential, startDeviceLogin, statusDocument, timezoneId, writeOAuthCredential, writePluginCredential, writePluginState };
|
|
2585
|
+
export { DEVICE_CODE_GRANT, DEVICE_CODE_PATH, EMPTY_USAGE, FALLBACK_MODELS, MEMBERSHIP_PATH, MINIMAXCODE_PROVIDER_ID, MINIMAX_ACTION_PATH, MINIMAX_AUTH_FILENAME, MINIMAX_DATA_DIR, MINIMAX_GATEWAYS, MINIMAX_OAUTH_CLIENT_ID, MINIMAX_REGIONS, MINIMAX_STATUS_PATH, MINIMAX_STREAM_IDLE_TIMEOUT_MS, MODEL_PRICES, MinimaxCatalog, MinimaxCredits, PLUGIN_CREDENTIAL_FILENAME, PLUGIN_STATE_FILENAME, REMAINS_PERCENT_PATH, SIGNIN_CLAIM_PATH, SIGNIN_CLAIM_RESULT, SIGNIN_CYCLE_DAYS, SIGNIN_DAY_STATUS, SIGNIN_PANEL_PATH, UsageMeter, apply, authPath, bearerToken, catalogUrl, chatBaseUrl, claimSignin, createMinimaxCodeAdapter, dataDir, daysRemaining, decodeClaims, dshHome, ensureOAuthCredential, estimateCny, expiresInMs, expiryLevel, fetchCreditSummary, fetchRateLimitWindows, fetchSigninPanel, inject, localDay, makePkce, name, parseAuthDocument, parseCatalog, parseCreditSummary, parseOAuthDocument, parseRateLimitWindows, parseSigninClaim, parseSigninPanel, pluginCredentialPath, pluginStatePath, pollDeviceLogin, priceFor, readCredential, readOAuthCredential, readPluginCredential, readPluginState, refreshOAuthCredential, refreshPluginCredential, startDeviceLogin, statusDocument, timezoneId, writeOAuthCredential, writePluginCredential, writePluginState };
|
package/package.json
CHANGED
|
@@ -2,16 +2,24 @@
|
|
|
2
2
|
"name": "dsh-connect-minimaxcode",
|
|
3
3
|
"displayName": "DSH Connect MiniMax Code",
|
|
4
4
|
"description": "把本机已登录的 MiniMax Code 桌面 App 模型接入 DeepSeek Harness:零配置、Anthropic Messages 兼容、动态模型目录,并显示积分余额与到期 — bring MiniMax Code desktop app models into DSH with a dynamic model catalog and a credit balance with expiry details.",
|
|
5
|
-
"version": "0.5.
|
|
5
|
+
"version": "0.5.5",
|
|
6
6
|
"author": "JeffTsuiCUG",
|
|
7
7
|
"contributors": [
|
|
8
8
|
{
|
|
9
9
|
"name": "JeffTsuiCUG",
|
|
10
10
|
"url": "https://github.com/JeffTsuiCUG"
|
|
11
11
|
},
|
|
12
|
+
{
|
|
13
|
+
"name": "eghrhegpe",
|
|
14
|
+
"url": "https://github.com/eghrhegpe"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "qzz19970504",
|
|
18
|
+
"url": "https://github.com/qzz19970504"
|
|
19
|
+
},
|
|
12
20
|
{
|
|
13
|
-
"name": "
|
|
14
|
-
"url": "https://github.com/
|
|
21
|
+
"name": "azsora",
|
|
22
|
+
"url": "https://github.com/azsora"
|
|
15
23
|
}
|
|
16
24
|
],
|
|
17
25
|
"license": "MIT",
|