dsh-connect-minimaxcode 0.5.7 → 0.5.9

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 CHANGED
@@ -10,19 +10,17 @@ Bring the models of the **MiniMax Code desktop app (China region)**, already sig
10
10
 
11
11
  ## Inspiration
12
12
 
13
- The idea for this plugin came from **[corrinehu/dsh-workbuddy-connect](https://github.com/corrinehu/dsh-workbuddy-connect)** (by Corrine Hu).
13
+ The community established the route this plugin follows: **a model already signed in on a desktop app can be moved into DSH with zero configuration** — no handling of accounts or passwords, no new authentication scheme; it just attaches a provider to the login state the machine already has and reuses DSH's built-in provider adapter. This plugin applies the same approach to MiniMax Code:
14
14
 
15
- That project was the first to show that **a model already signed in on a desktop app can be moved into DSH with zero configuration** — no handling of accounts or passwords, no new authentication scheme; it just attaches a provider to the login state the machine already has and reuses DSH's built-in provider adapter. This plugin applies the same approach to MiniMax Code:
16
-
17
- | | dsh-workbuddy-connect | This plugin |
15
+ | | Sibling plugins | This plugin |
18
16
  |---|---|---|
19
- | Data source | Local login state of the WorkBuddy desktop app | Local login state of the MiniMax Code desktop app |
17
+ | Data source | Local login state of their own desktop app | Local login state of the MiniMax Code desktop app |
20
18
  | Integration | Registers a DSH provider and exposes the already-signed-in models | Same |
21
19
  | Catalog source | Live model list from upstream | Same |
22
20
 
23
- Another implementation in the same direction is **[dingminhua/dsh-connect-workbuddy](https://github.com/dingminhua/dsh-connect-workbuddy)**; for **how the plugin is structured and how the provider is registered**, this plugin also follows the same author's [dsh-connect-trae](https://github.com/dingminhua/dsh-connect-trae) (whose connection core itself references corrinehu's implementation — see the acknowledgements in its README).
21
+ Several implementations exist in the same direction; for **how the plugin is structured and how the provider is registered**, this plugin follows **[dingminhua/dsh-connect-workbuddy](https://github.com/dingminhua/dsh-connect-workbuddy)** and the same author's [dsh-connect-trae](https://github.com/dingminhua/dsh-connect-trae).
24
22
 
25
- > The two differ in this way: **corrinehu was the original pioneer** — the "reuse the local login state" route was established by that project; **dingminhua's implementation follows the existing conventions of the DSH plugin structure more closely**, and this plugin's directory layout and `cordis.patch.yml` follow the latter. Both are credited at the end of this document.
23
+ > These projects are developed independently and are not affiliated with this one. This plugin's directory layout and `cordis.patch.yml` follow the existing conventions of the DSH plugin structure.
26
24
 
27
25
  ---
28
26
 
@@ -283,6 +281,27 @@ Two sets of data each cover one thing:
283
281
 
284
282
  > The official price list has no separate price for **cache writes** (`cache_creation_input_tokens`), so the plugin counts its tokens but **does not convert them to money**, to avoid quoting a number that has no public basis.
285
283
 
284
+ ### Account usage profile
285
+
286
+ The account's **usage history** comes from the open platform's usage summary, and it is a different thing from the token count the pill reports: the latter counts only the requests this host made.
287
+
288
+ ```
289
+ GET https://www.minimaxi.com/backend/account/token_plan/usage_summary?token_type=5&heatmap_days=365&business=Agent&device_platform=desktop
290
+ Authorization: Bearer <OAuth 令牌>
291
+ ```
292
+
293
+ The card shows the parts worth acting on:
294
+
295
+ | Field | Meaning |
296
+ | --- | --- |
297
+ | `current_consecutive_days` | Days used in a row |
298
+ | `active_days` / `total_days` | Days with usage in the window / the window's length |
299
+ | `total_token_consumed` | Tokens consumed in the window, in the upstream's own wording (`826.14M`) |
300
+ | `usage_ranking_percent` | Where the account ranks by usage |
301
+ | `most_active_day` | The busiest day and what it consumed |
302
+
303
+ This data **needs no M Plan subscription**: the endpoint is an account one, read with the OAuth bearer, unlike the limit windows, which belong to the subscription.
304
+
286
305
  ### About token refresh
287
306
 
288
307
  The OAuth token is valid for about an hour and is refreshed automatically by the client. The plugin **only refreshes it once, with the refresh token the app already stored, when its own token has expired**, and writes the rotated new tokens back to the same file atomically, in the client's **original format** (with `generation` unchanged), so the desktop app's login state is unaffected. The plugin never starts a login, never signs out, and never calls any paid or subscription endpoint.
@@ -308,6 +327,20 @@ The OAuth token is valid for about an hour and is refreshed automatically by the
308
327
 
309
328
  ## Changelog
310
329
 
330
+ ### 0.5.9 (2026-11-10)
331
+
332
+ - **README**: every reference to a repository that no longer exists (it now answers 404) is gone — the inspiration section, the sibling-plugin table and its section, and the acknowledgements. The practice it established and the sibling projects that followed it are still described, and every repository link left in the README was checked to be alive.
333
+ - **README**: the marketplace section now states the situation as it stands — all twelve listing pull requests are closed, the forks opened for them are cleaned up, those directories are not being worked on, and distribution runs through npm and this repository's releases. The two registries that still carry an entry (the official registry and dshbase) are still listed. The npm package ships the README, so this change needs a release to reach the package page.
334
+
335
+ ### 0.5.8 (2026-11-10)
336
+
337
+ - **Fixed "M3's reasoning could not be turned off"**: the plugin used to declare "reasoning cannot be disabled" for *every* reasoning model, on the grounds that the gateway refuses `thinking: {type: "disabled"}` with `400 ... requires adaptive thinking`. That refusal only holds for a `forced_on` model: the upstream catalog states `thinking_config.mode` per model, and `MiniMax-M3` is `switchable` (measured: the disabled form is accepted, 200) while `MiniMax-M3.1-Flash` is `forced_on` (measured: 400). Each model is now handled as upstream describes it: **M3's reasoning levels now include "off"**, and picking it really does stop the reasoning — fewer tokens, lower latency. M3.1-Flash is unchanged and the option still does not appear.
338
+ Upstream's `effort_options` (six levels for M3.1-Flash: default/low/medium/high/xhigh/max) are parsed and recorded but **not wired**: pi-ai's level mapping still dispatches the budget form, and reaching those six levels would switch the whole request shape, which this change does not do.
339
+ - **New "account usage profile"**: reads the open platform's `GET /backend/account/token_plan/usage_summary` (a constant this plugin had defined and never used). The card shows the consecutive days of use, the active days in the window, the tokens consumed in it, the usage ranking and the busiest day. It describes the *account*'s history, which is a different thing from the token count the pill reports for this host.
340
+ - **Says why there are no limit windows**: an account without an M Plan subscription gets `2062 no active token plan subscription` from upstream (already recognised in 0.5.7). The card now states that outright instead of leaving the reader to assume a failed read.
341
+ - **Endpoint survey**: every one of the twenty account-domain endpoints the desktop app calls each day was mapped — **nineteen of them are readable with the OAuth bearer and need no subscription at all**. What a subscription buys is not more endpoints but two things: the limit windows and a larger allowance.
342
+ - Added `tests/usage-profile.spec.ts` (16 cases) plus reasoning-mode cases in `tests/catalog.spec.ts` / `tests/adapter.spec.ts`. All 236 cases pass.
343
+
311
344
  ### 0.5.7 (2026-11-10)
312
345
 
313
346
  - **The quota readout now uses the open platform's documented endpoint**: the plugin read `/backend/account/token_plan/remains_percent`, and that path now answers every account with an empty `success` — it no longer even carries a `model_remains` field — so the readout showed nothing for anyone and said nothing about why. It now reads `GET /v1/token_plan/remains`, the endpoint the open platform's own M Plan documentation names. That endpoint asks for the **open platform's API key** rather than the OAuth bearer; the key comes from the `token_plan_credit` response and is remembered between reads.
@@ -395,30 +428,25 @@ For earlier versions, see [GitHub Releases](https://github.com/JeffTsuiCUG/dsh-c
395
428
 
396
429
  ## Marketplace listing and presentation
397
430
 
398
- The plugin ships an installable `dsh.bundle` manifest and is published to npm. The directory of the community plugin marketplace (`dshmarket`) is generated from the [Awesome DSH Plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) registry, and this plugin's listing has been submitted as PR [#8110](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/8110). A draft and the operating notes are kept in the repository: [`awesome-dsh-plugin-submission/`](awesome-dsh-plugin-submission/).
431
+ The plugin ships an installable `dsh.bundle` manifest and is published to npm — **npm is the installation path**:
432
+
433
+ ```bash
434
+ dsh plugin --profile desktop add dsh-connect-minimaxcode
435
+ ```
436
+
437
+ The directory of the community plugin marketplace (`dshmarket`) is generated from the [Awesome DSH Plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) registry, and this plugin's entry is in it ([#8110](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/8110) listed, [#8380](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/8380) refreshed), so it also shows up in the DSH plugin centre.
399
438
 
400
- **Other directories and marketplaces** (entries submitted or already listed, text refreshed to 0.5.1):
439
+ **Other directories**: entries were submitted to several awesome lists as well, but those projects see little maintenance; the entries and the forks opened for them have been cleaned up and are **no longer maintained**. Distribution runs through npm and this repository's Releases. The original draft and the submission notes are kept in the repository: [`awesome-dsh-plugin-submission/`](awesome-dsh-plugin-submission/), in case they are ever needed again.
401
440
 
402
441
  | Channel | State |
403
442
  |---|---|
404
- | [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) (the registry) | Listed; entry refresh PR [#8380](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/8380) |
405
- | [awesome-deepseek-harness](https://github.com/0xsline/awesome-deepseek-harness) | PR [#727](https://github.com/0xsline/awesome-deepseek-harness/pull/727) |
406
- | [Awesome-DeepSeek-Harness-Plugins](https://github.com/Zhiyuan-Fan/Awesome-DeepSeek-Harness-Plugins) | PR [#92](https://github.com/Zhiyuan-Fan/Awesome-DeepSeek-Harness-Plugins/pull/92) |
407
- | [dsh-suite](https://github.com/whyihaveyou/dsh-suite) | PR [#66](https://github.com/whyihaveyou/dsh-suite/pull/66) |
408
- | [awesome-dsh-plugins](https://github.com/kejixiaoliang/awesome-dsh-plugins) | PR [#143](https://github.com/kejixiaoliang/awesome-dsh-plugins/pull/143) |
409
- | [awesome-dsh-plugins](https://github.com/dshworks/awesome-dsh-plugins) | PR [#176](https://github.com/dshworks/awesome-dsh-plugins/pull/176) |
410
- | [awesome-deepseek-harness](https://github.com/awesome-deepseekharness/awesome-deepseek-harness) | PR [#177](https://github.com/awesome-deepseekharness/awesome-deepseek-harness/pull/177) |
411
- | [dsh-plugin-radar](https://github.com/AdamPlatin123/dsh-plugin-radar) | PR [#964](https://github.com/AdamPlatin123/dsh-plugin-radar/pull/964) |
412
- | [dsh-community-plugins](https://github.com/zhu1090093659/dsh-community-plugins) | PR [#46](https://github.com/zhu1090093659/dsh-community-plugins/pull/46) |
413
- | [dsh-plugin.org](https://github.com/dshplugin/dsh-plugin-hub) | Listed (automatic scan; the npm package shows up in the DSH plugin centre) |
414
- | [dshbase](https://github.com/ylwl1997/dshbase) | Listed |
415
- | [awesome-deepseek-harness-plugins](https://github.com/walkinglabs/awesome-deepseek-harness-plugins) | PR [#117](https://github.com/walkinglabs/awesome-deepseek-harness-plugins/pull/117) |
416
- | [awesome-deepseek-harness](https://github.com/Dominic789654/awesome-deepseek-harness) | PR [#663](https://github.com/Dominic789654/awesome-deepseek-harness/pull/663) |
417
- | [awesome-deepseek-harness-plugins](https://github.com/imsai-sh/awesome-deepseek-harness-plugins) | PR [#660](https://github.com/imsai-sh/awesome-deepseek-harness-plugins/pull/660) |
443
+ | [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) (the registry behind `dshmarket`) | Listed |
444
+ | [dshbase](https://github.com/ylwl1997/dshbase) | Listed (automatic scan) |
445
+ | Other awesome lists | No longer maintained; entries and forks cleaned up |
418
446
 
419
447
  Marketplace screenshots are declared, per the current convention, by the **`screenshots.json` in the plugin's own repository root** (the registry's `data/screenshots.json` is a fallback from the old convention, and no new keys are added there). This repository **does not ship screenshots yet**, so the file does not exist; once there are some, put `screenshots.json` in the repository root (an array of relative image paths) and the marketplace card will pick them up.
420
448
 
421
- > **Since 0.5.1 the readout lives in the composer tool row (beside the model picker) and is gated by the host's `modelSelection` projection**: the balance belongs to the provider the next request will actually use, so it sits where that selection is made — and it hides entirely, polling included, when another connector's model (workbuddy, for example) is selected. This follows `dsh-connect-trae`, whose `ComposerPointsGate` returns null for every provider but its own.
449
+ > **Since 0.5.1 the readout lives in the composer tool row (beside the model picker) and is gated by the host's `modelSelection` projection**: the balance belongs to the provider the next request will actually use, so it sits where that selection is made — and it hides entirely, polling included, when another connector's model (dsh-connect-workbuddy, for example) is selected. This follows `dsh-connect-trae`, whose `ComposerPointsGate` returns null for every provider but its own.
422
450
  >
423
451
  > The tool row is a shared slot and can in principle be crowded out (a sibling plugin lost a reading this way in `workbuddy` issue #62; trae's answer is a second seat). **This plugin registers the tool-row seat only**; the ambient dock is a proven fallback should crowding ever occur.
424
452
 
@@ -455,7 +483,6 @@ These plugins follow the same idea — **reuse the login state your local deskto
455
483
  |---|---|---|---|
456
484
  | **dsh-connect-trae** | Trae (China + international) | ✅ China region | ✅ Work / general credit overview |
457
485
  | **dsh-qoder-connect** | Qoder (China + international, PAT) | ✅ 100 Credits per day | ✅ Sidebar quota card + breakdown |
458
- | **dsh-workbuddy-connect** | WorkBuddy (China + international) | — | ✅ Remaining credits + breakdown |
459
486
 
460
487
  > Each plugin's check-in rules, quota basis and limitations are defined by its own documentation. The readings above come from the same machine and only represent the state at that time; they are not a promise.
461
488
 
@@ -475,16 +502,6 @@ Connects Qoder through a personal access token (PAT), with independent configura
475
502
  dsh plugin --profile desktop add dsh-qoder-connect
476
503
  ```
477
504
 
478
- ### [dsh-workbuddy-connect](https://github.com/corrinehu/dsh-workbuddy-connect)
479
-
480
- Connects the WorkBuddy desktop app; the China and international versions can coexist, each using its own account and its own credits. It shows the account information, token validity and remaining credits, and ships a CLI:
481
-
482
- ```bash
483
- dsh plugin --profile web exec dsh-workbuddy-connect status
484
- ```
485
-
486
- > This plugin's version must match your DSH core exactly (each core line uses a different version); see the version table in its README before installing.
487
-
488
505
  ## Contributors
489
506
 
490
507
  - **[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.
@@ -494,8 +511,7 @@ dsh plugin --profile web exec dsh-workbuddy-connect status
494
511
 
495
512
  ## Acknowledgements
496
513
 
497
- - **[corrinehu/dsh-workbuddy-connect](https://github.com/corrinehu/dsh-workbuddy-connect)** — **the inspiration for this plugin**. The overall idea of "reuse the desktop app's local login state and bring the already-signed-in model into DSH" was established first by that project, and this plugin follows the same approach; the read-only reuse of credentials was designed with it in mind.
498
- - **[dingminhua/dsh-connect-workbuddy](https://github.com/dingminhua/dsh-connect-workbuddy)** — another implementation in the same direction; this plugin's **plugin structure and provider registration** follow it (its connection core also references corrinehu's implementation — see that project's acknowledgements).
514
+ - **[dingminhua/dsh-connect-workbuddy](https://github.com/dingminhua/dsh-connect-workbuddy)** — another implementation in the same direction; this plugin's **plugin structure and provider registration** follow it.
499
515
  - [dingminhua/dsh-connect-trae](https://github.com/dingminhua/dsh-connect-trae) — the same author's DSH plugin, useful as a reference for the DSH plugin structure.
500
516
 
501
517
  > The projects listed under "Similar plugins" are the work of independent developers. They are not affiliated with or endorsed by this project, and each one follows its own license and disclaimer.
package/README.md CHANGED
@@ -10,19 +10,17 @@
10
10
 
11
11
  ## 灵感来源
12
12
 
13
- 本插件的思路来自 **[corrinehu/dsh-workbuddy-connect](https://github.com/corrinehu/dsh-workbuddy-connect)**(作者 Corrine Hu)。
13
+ 社区先走通了这样一条路:**桌面 App 已经登录的模型,可以零配置地搬进 DSH** —— 不去碰账号密码,不引入新的鉴权体系,只是把本机已有的登录态接上一个 provider,并复用 DSH 自带的 provider 适配器。本插件把同一套做法用在了 MiniMax Code 上:
14
14
 
15
- 它最早证明了这样一件事:**桌面 App 已经登录的模型,可以零配置地搬进 DSH**——不去碰账号密码,不引入新的鉴权体系,只是把本机已有的登录态接上一个 provider,并复用 DSH 自带的 provider 适配器。本插件把同一套做法用在了 MiniMax Code 上:
16
-
17
- | | dsh-workbuddy-connect | 本插件 |
15
+ | | 同类插件 | 本插件 |
18
16
  |---|---|---|
19
- | 数据来源 | WorkBuddy 桌面 App 的本地登录态 | MiniMax Code 桌面 App 的本地登录态 |
17
+ | 数据来源 | 各自桌面 App 的本地登录态 | MiniMax Code 桌面 App 的本地登录态 |
20
18
  | 接入方式 | 注册 DSH provider,暴露已登录模型 | 同左 |
21
19
  | 目录来源 | 上游实时模型列表 | 同左 |
22
20
 
23
- 同方向的另一实现是 **[dingminhua/dsh-connect-workbuddy](https://github.com/dingminhua/dsh-connect-workbuddy)**,本插件在**插件结构与 provider 注册的组织方式**上参考了同一作者的 [dsh-connect-trae](https://github.com/dingminhua/dsh-connect-trae)(该项目的连接内核本身也参照了 corrinehu 的实现,见其 README 致谢)。
21
+ 同方向已有多个实现,本插件在**插件结构与 provider 注册的组织方式**上参考了 **[dingminhua/dsh-connect-workbuddy](https://github.com/dingminhua/dsh-connect-workbuddy)** 与同一作者的 [dsh-connect-trae](https://github.com/dingminhua/dsh-connect-trae)。
24
22
 
25
- > 两者的价值可以这样区分:**corrinehu 是最早的开创者**,「复用本机登录态」的路线由它确立;**dingminhua 的实现更贴近 DSH 插件结构的既有约定**,本插件的目录布局与 `cordis.patch.yml` 写法沿用了后者。二者都已列入文末致谢。
23
+ > 同类项目各自独立开发,与本项目无隶属关系。本插件的目录布局与 `cordis.patch.yml` 写法沿用了 DSH 插件结构的既有约定。
26
24
 
27
25
  ---
28
26
 
@@ -319,6 +317,27 @@ Authorization: Bearer <API Key>
319
317
  - **没有订阅就没有窗口,且这不算错误**:上游对未订阅 M Plan 的账户明确回 `2062 no active token plan subscription`,插件会如实标为「无订阅」而不是「读取失败」—— 积分包本身不带滚动窗口
320
318
  - **接口异常时其余显示不受影响**:限额读不到,胶囊与卡片仍显示余额、签到与本机用量
321
319
  - 上游还发 `current_*_count` 计数字段(无计数制时为 `-1`),插件只读百分比与起止时刻
320
+ ### 账户用量画像
321
+
322
+ 账户的**用量历史**由开放平台的用量汇总接口给出,与「本机累计 token」是两回事 —— 后者只统计这台机器发起的请求:
323
+
324
+ ```
325
+ GET https://www.minimaxi.com/backend/account/token_plan/usage_summary?token_type=5&heatmap_days=365&business=Agent&device_platform=desktop
326
+ Authorization: Bearer <OAuth 令牌>
327
+ ```
328
+
329
+ 卡片显示的是其中有行动价值的几项:
330
+
331
+ | 字段 | 含义 |
332
+ | --- | --- |
333
+ | `current_consecutive_days` | 连续使用天数 |
334
+ | `active_days` / `total_days` | 统计窗口内有使用的天数 / 窗口长度 |
335
+ | `total_token_consumed` | 窗口内累计消耗(上游自己的写法,如 `826.14M`) |
336
+ | `usage_ranking_percent` | 用量排名分位 |
337
+ | `most_active_day` | 最活跃的一天及其消耗 |
338
+
339
+ 这一节读到的数据**不需要订阅 M Plan**:该接口属于账户域,用 OAuth 令牌即可读取,与限额窗口(属于订阅)不同。
340
+
322
341
  ### 关于续期
323
342
 
324
343
  OAuth 令牌有效期约 1 小时,由客户端自动续期。插件**只在自己的令牌已过期时**才用 App 已存的 refresh token 续一次,并把轮换后的新令牌按客户端**原格式原子写回**同一个文件(`generation` 保持不变),因此桌面 App 的登录态不受影响。插件不会发起登录、不会登出、也不会调用任何付费/订阅类接口。
@@ -344,6 +363,20 @@ OAuth 令牌有效期约 1 小时,由客户端自动续期。插件**只在自
344
363
 
345
364
  ## 更新记录
346
365
 
366
+ ### 0.5.9(2026-11-10)
367
+
368
+ - **README**:移除指向已不存在仓库的引用(该仓库现已 404),灵感来源、同类插件与致谢三处一并清理,保留的做法与仍有效的同类项目说明不变;README 中保留的每一个仓库链接都已逐一确认存活。
369
+ - **README**:市场收录章节改为如实描述现状 —— 十二个收录 PR 均已关闭、为收录开的分叉已清理、那些目录不再维护,分发以 npm 与本仓库 Releases 为准;仍收录本插件的两个注册表(官方注册表与 dshbase)照旧列出。npm 包内含 README,因此本次改动需要发版才能在包页生效。
370
+
371
+ ### 0.5.8(2026-11-10)
372
+
373
+ - **修复「M3 的推理关不掉」**:插件此前对**所有**推理模型都声明「不支持关闭推理」,理由是网关会以 `400 ... requires adaptive thinking` 拒绝 `thinking: {type: "disabled"}`。但该拒绝只对 `forced_on` 的模型成立 —— 上游模型目录逐模型声明 `thinking_config.mode`,`MiniMax-M3` 是 `switchable`(实测接受该形式,200),`MiniMax-M3.1-Flash` 才是 `forced_on`(实测 400)。现在按上游声明逐模型处理:**M3 的推理档位里出现了「关」**,选它就真的不发推理,既省 token 也更快;M3.1-Flash 的行为不变,选项仍不出现。
374
+ 上游的 `effort_options`(M3.1-Flash 有 default/low/medium/high/xhigh/max 六档)也一并解析存档,但**未接线**:pi-ai 的档位映射目前走 budget 形式,要用上这六档得切换整个请求形态,这一步不在本次改动里。
375
+ - **新增「账户用量画像」**:读取开放平台的 `GET /backend/account/token_plan/usage_summary`(该常量此前定义了却从未使用),卡片显示连续使用天数、统计窗口内的活跃天数、窗口内累计消耗、用量排名分位与最活跃的一天。它说的是**账户**的历史,与胶囊里「本机累计 token」是两回事。
376
+ - **说明「为什么没有限额窗口」**:账户未订阅 M Plan 时,上游回 `2062 no active token plan subscription`(0.5.7 已识别)。现在卡片直接写明「未订阅 M Plan(仅积分包),因此没有限额窗口」,而不是让人以为读取失败。
377
+ - **接口盘点**:把桌面 App 每天真实调用的 20 个账户域接口全部盘了一遍 —— 其中 **19 个用 OAuth 令牌即可读取,无需任何订阅**,只有「限额窗口」属于订阅本身。也就是说订阅 M Plan 换来的不是「更多接口」,而是**限额窗口**与**更高额度**这两样。
378
+ - 新增 `tests/usage-profile.spec.ts`(16 个用例)与 `tests/catalog.spec.ts` / `tests/adapter.spec.ts` 的推理模式用例。全量 236 个用例通过。
379
+
347
380
  ### 0.5.7(2026-11-10)
348
381
 
349
382
  - **限额窗口改走官方文档接口**:原先读的是 `/backend/account/token_plan/remains_percent`,这条路径现在对任何账户都只回一个空的 `success`(连 `model_remains` 字段都没有),于是限额一栏对所有人都显示不出来,却看不出原因。改用开放平台官方文档(M Plan 用量说明页)给出的 `GET /v1/token_plan/remains`。新接口要求**开放平台 API 密钥**而非 OAuth 令牌,密钥取自 `token_plan_credit` 的响应并缓存复用。
@@ -387,7 +420,7 @@ OAuth 令牌有效期约 1 小时,由客户端自动续期。插件**只在自
387
420
 
388
421
  ### 0.5.1(2026-10-20)
389
422
 
390
- - **余额读数跟随所选模型**:从输入框下方的 dock 移到**输入框工具行(模型选择器旁)**,并按宿主 `modelSelection` 投影门控 —— **只有选中 `minimaxcode` 模型时才显示**;切到其他反代插件(如 workbuddy)的模型即整体隐藏,且不轮询(学习 `dsh-connect-trae` 的做法)。
423
+ - **余额读数跟随所选模型**:从输入框下方的 dock 移到**输入框工具行(模型选择器旁)**,并按宿主 `modelSelection` 投影门控 —— **只有选中 `minimaxcode` 模型时才显示**;切到其他反代插件(如 dsh-connect-workbuddy)的模型即整体隐藏,且不轮询(学习 `dsh-connect-trae` 的做法)。
391
424
  - **`engines.dsh` 收窄为 `>=0.2.1-rc.2 <0.3.0-0`**:只声明实测过的档位,此前的 `>=0.1.7-rc.1` 覆盖过宽。
392
425
 
393
426
  ### 0.5.0(2026-10-20)
@@ -433,26 +466,21 @@ OAuth 令牌有效期约 1 小时,由客户端自动续期。插件**只在自
433
466
 
434
467
  ## 市场收录与展示
435
468
 
436
- 插件已包含可安装的 `dsh.bundle` manifest,并发布到 npm。社区插件市场(`dshmarket`)的目录由 [Awesome DSH Plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) 注册表生成,本插件的收录条目已提交为 PR [#8110](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/8110),仓库内保留了一份草稿与操作说明:[`awesome-dsh-plugin-submission/`](awesome-dsh-plugin-submission/)。
469
+ 插件已包含可安装的 `dsh.bundle` manifest,并发布到 npm —— **安装方式以 npm 为准**:
470
+
471
+ ```bash
472
+ dsh plugin --profile desktop add dsh-connect-minimaxcode
473
+ ```
474
+
475
+ 社区插件市场(`dshmarket`)的目录由 [Awesome DSH Plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) 注册表生成,本插件的条目已在该注册表中([#8110](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/8110) 收录、[#8380](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/8380) 更新),因此也会出现在 DSH 的插件中心里。
437
476
 
438
- **其它目录 / 市场的收录**(条目已提交或已上线,内容随版本更新到 0.5.1):
477
+ **其它目录**:此前也向若干 awesome 清单提交过收录条目,但那些项目的更新活跃度很低,条目与为此开的分叉都已清理完毕,**不再维护**。插件的分发以 npm 与本仓库的 Releases 为准。仓库内保留了一份当初的草稿与提交说明:[`awesome-dsh-plugin-submission/`](awesome-dsh-plugin-submission/),供日后需要时参考。
439
478
 
440
479
  | 渠道 | 状态 |
441
480
  |---|---|
442
- | [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)(官方注册表) | ✅ 已收录;条目更新 PR [#8380](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/8380) |
443
- | [awesome-deepseek-harness](https://github.com/0xsline/awesome-deepseek-harness) | PR [#727](https://github.com/0xsline/awesome-deepseek-harness/pull/727) |
444
- | [Awesome-DeepSeek-Harness-Plugins](https://github.com/Zhiyuan-Fan/Awesome-DeepSeek-Harness-Plugins) | PR [#92](https://github.com/Zhiyuan-Fan/Awesome-DeepSeek-Harness-Plugins/pull/92) |
445
- | [dsh-suite](https://github.com/whyihaveyou/dsh-suite) | PR [#66](https://github.com/whyihaveyou/dsh-suite/pull/66) |
446
- | [awesome-dsh-plugins](https://github.com/kejixiaoliang/awesome-dsh-plugins) | PR [#143](https://github.com/kejixiaoliang/awesome-dsh-plugins/pull/143) |
447
- | [awesome-dsh-plugins](https://github.com/dshworks/awesome-dsh-plugins) | PR [#176](https://github.com/dshworks/awesome-dsh-plugins/pull/176) |
448
- | [awesome-deepseek-harness](https://github.com/awesome-deepseekharness/awesome-deepseek-harness) | PR [#177](https://github.com/awesome-deepseekharness/awesome-deepseek-harness/pull/177) |
449
- | [dsh-plugin-radar](https://github.com/AdamPlatin123/dsh-plugin-radar) | PR [#964](https://github.com/AdamPlatin123/dsh-plugin-radar/pull/964) |
450
- | [dsh-community-plugins](https://github.com/zhu1090093659/dsh-community-plugins) | PR [#46](https://github.com/zhu1090093659/dsh-community-plugins/pull/46) |
451
- | [dsh-plugin.org](https://github.com/dshplugin/dsh-plugin-hub) | ✅ 已收录(自助扫描,会随目录更新出现在 DSH「插件中心」) |
452
- | [dshbase](https://github.com/ylwl1997/dshbase) | ✅ 已收录 |
453
- | [awesome-deepseek-harness-plugins](https://github.com/walkinglabs/awesome-deepseek-harness-plugins) | PR [#117](https://github.com/walkinglabs/awesome-deepseek-harness-plugins/pull/117) |
454
- | [awesome-deepseek-harness](https://github.com/Dominic789654/awesome-deepseek-harness) | PR [#663](https://github.com/Dominic789654/awesome-deepseek-harness/pull/663) |
455
- | [awesome-deepseek-harness-plugins](https://github.com/imsai-sh/awesome-deepseek-harness-plugins) | PR [#660](https://github.com/imsai-sh/awesome-deepseek-harness-plugins/pull/660) |
481
+ | [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)(官方注册表,`dshmarket` 目录来源) | ✅ 已收录 |
482
+ | [dshbase](https://github.com/ylwl1997/dshbase) | ✅ 已收录(自动扫描) |
483
+ | 其它 awesome 清单 | 已停止维护,条目与分叉已清理 |
456
484
 
457
485
  市场截图按当前约定由**插件自己仓库根目录的** `screenshots.json` 声明(注册表 `data/screenshots.json` 是旧约定下的回退,不再新增键)。本仓库目前**尚未提供截图**,所以还没有这个文件;补上截图后,在根目录放 `screenshots.json`(内容是图片相对路径的数组)即可被市场卡片取用。
458
486
 
@@ -493,7 +521,6 @@ pnpm build && git add lib
493
521
  |---|---|---|---|
494
522
  | **dsh-connect-trae** | Trae(国内版 + 国际版) | ✅ 国内版 | ✅ Work / 通用积分概览 |
495
523
  | **dsh-qoder-connect** | Qoder(国内版 + 国际版,PAT) | ✅ 每日 100 Credits | ✅ 侧栏额度小卡 + 明细 |
496
- | **dsh-workbuddy-connect** | WorkBuddy(国内版 + 国际版) | — | ✅ 剩余积分 + 明细 |
497
524
 
498
525
  > 各插件的签到规则、额度口径与限制以各自文档为准。下表为同一台机器上的实测读数,仅代表当时状态,非承诺。
499
526
 
@@ -513,16 +540,6 @@ dsh plugin --profile desktop add dsh-connect-trae
513
540
  dsh plugin --profile desktop add dsh-qoder-connect
514
541
  ```
515
542
 
516
- ### [dsh-workbuddy-connect](https://github.com/corrinehu/dsh-workbuddy-connect)
517
-
518
- 接入 WorkBuddy 桌面 App,国内版与国际版可并存,各自使用自己的账号与积分。提供账号信息、令牌有效期与剩余积分查看,并带 CLI:
519
-
520
- ```bash
521
- dsh plugin --profile web exec dsh-workbuddy-connect status
522
- ```
523
-
524
- > 该插件的版本需与你的 DSH 内核严格对应(不同内核线用不同版本),安装前请见其 README 的版本对应表。
525
-
526
543
  ## 贡献者
527
544
 
528
545
  - **[eghrhegpe](https://github.com/eghrhegpe)** —— 提出了插件页图标与中英文文案的建议([#1](https://github.com/JeffTsuiCUG/dsh-connect-minimaxcode/issues/1))。这条建议促成了 0.5.2:插件页有了自己的 SVG 图标,标题与简介有了中英两版,界面文案也开始跟随宿主语言。
@@ -532,8 +549,7 @@ dsh plugin --profile web exec dsh-workbuddy-connect status
532
549
 
533
550
  ## 致谢
534
551
 
535
- - **[corrinehu/dsh-workbuddy-connect](https://github.com/corrinehu/dsh-workbuddy-connect)** — **本插件的灵感来源**。「复用桌面 App 本机登录态、把已登录模型接进 DSH」的整体思路由该项目最早确立,本插件沿用同一做法;凭据只读复用的设计也参照于此。
536
- - **[dingminhua/dsh-connect-workbuddy](https://github.com/dingminhua/dsh-connect-workbuddy)** — 同方向的另一实现,本插件的**插件结构与 provider 注册组织方式**参考了它(其连接内核亦参照 corrinehu 的实现,见该项目致谢)。
552
+ - **[dingminhua/dsh-connect-workbuddy](https://github.com/dingminhua/dsh-connect-workbuddy)** — 同方向的另一实现,本插件的**插件结构与 provider 注册组织方式**参考了它。
537
553
  - [dingminhua/dsh-connect-trae](https://github.com/dingminhua/dsh-connect-trae) — 同作者的 DSH 插件,可作为 DSH 插件结构的参照。
538
554
 
539
555
  > 上方「同类插件」中的项目均为独立开发者的作品,与本项目无隶属或背书关系,各自遵循其自身的许可与免责声明。
package/lib/client.js CHANGED
@@ -68,6 +68,13 @@
68
68
  claimableToday: '今日可签到',
69
69
  usageWindow: '{duration}限额',
70
70
  resetIn: '{duration}后',
71
+ accountUsage: '账户用量',
72
+ usageStreak: '连续使用 {days} 天',
73
+ usageActiveDays: '近 {total} 天中有 {active} 天用过',
74
+ usageTotalTokens: '累计消耗 {count} tokens',
75
+ usageRanking: '用量排名前 {percent}%',
76
+ usageBestDay: '最活跃的一天 {date}',
77
+ planNoSubscription: '未订阅 M Plan(仅积分包),因此没有限额窗口',
71
78
  };
72
79
  const en = {
73
80
  title: 'MiniMax Code credits',
@@ -110,6 +117,13 @@
110
117
  claimableToday: 'Claimable today',
111
118
  usageWindow: '{duration} limit',
112
119
  resetIn: 'resets in {duration}',
120
+ accountUsage: 'Account usage',
121
+ usageStreak: '{days} days in a row',
122
+ usageActiveDays: 'Used on {active} of the last {total} days',
123
+ usageTotalTokens: '{count} tokens consumed',
124
+ usageRanking: 'Top {percent}% by usage',
125
+ usageBestDay: 'Busiest day {date}',
126
+ planNoSubscription: 'No M Plan subscription (credits only), so there are no usage windows',
113
127
  };
114
128
  const loader = globalThis.__ModuleLoader__;
115
129
  if (loader === undefined)
@@ -259,6 +273,49 @@
259
273
  return null;
260
274
  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
275
  }
276
+ /**
277
+ * The account's own usage history.
278
+ *
279
+ * This is not the token count of this host — the pill reports that
280
+ * separately — but the account's days, consumption and standing, which
281
+ * is what the upstream knows and nobody else does.
282
+ */
283
+ function usageProfileBlock(t, profile) {
284
+ if (profile === undefined || profile.state !== 'ok')
285
+ return null;
286
+ const rows = [];
287
+ if (profile.consecutiveDays !== undefined && profile.consecutiveDays > 0) {
288
+ rows.push(row(t('usageStreak', { days: profile.consecutiveDays }), ''));
289
+ }
290
+ if (profile.activeDays !== undefined) {
291
+ rows.push(row(t('usageActiveDays', { active: profile.activeDays, total: profile.totalDays ?? '' }), ''));
292
+ }
293
+ if (profile.totalTokenConsumed !== undefined) {
294
+ rows.push(row(t('usageTotalTokens', { count: profile.totalTokenConsumed }), ''));
295
+ }
296
+ if (profile.rankingPercent !== undefined) {
297
+ rows.push(row(t('usageRanking', { percent: profile.rankingPercent.toFixed(1) }), ''));
298
+ }
299
+ if (profile.mostActiveDay?.date !== undefined) {
300
+ rows.push(row(t('usageBestDay', { date: profile.mostActiveDay.date }), ''));
301
+ }
302
+ if (rows.length === 0)
303
+ return null;
304
+ return h('div', { style: expiryBoxStyle, 'data-minimax-usage-profile': true }, ...rows);
305
+ }
306
+ /**
307
+ * Why there are no usage windows, when there are none because the plan
308
+ * has none to give.
309
+ *
310
+ * The rolling windows belong to the subscription; an account holding
311
+ * only a credit package has none, and saying so is more useful than
312
+ * leaving the reader to wonder whether a read failed.
313
+ */
314
+ function planNotice(t, rateLimit) {
315
+ if (rateLimit?.reason !== 'no-subscription')
316
+ return null;
317
+ return h('div', { style: mutedStyle, 'data-minimax-plan-notice': true }, t('planNoSubscription'));
318
+ }
262
319
  /** The pill itself; the gate decides whether it is drawn at all. */
263
320
  function CreditPill(props) {
264
321
  const [status] = useStatus();
@@ -291,7 +348,7 @@
291
348
  const detail = available
292
349
  ? 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
293
350
  ? row(t('consumed'), credits(credits_.consumed, 3))
294
- : null, expiryBlock(t, credits_), rateLimitBlock(t, status?.rateLimit), usage !== undefined
351
+ : null, expiryBlock(t, credits_), rateLimitBlock(t, status?.rateLimit), planNotice(t, status?.rateLimit), usageProfileBlock(t, status?.usageProfile), usage !== undefined
295
352
  ? 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) })))
296
353
  : null, credits_.updatedAtMs !== undefined
297
354
  ? row(t('updated', { time: new Date(credits_.updatedAtMs).toLocaleTimeString() }), '')
@@ -432,11 +489,11 @@
432
489
  if (!available) {
433
490
  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));
434
491
  }
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
492
+ 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), planNotice(t, status?.rateLimit), signinBlock(t, signin, refresh), h('div', { style: sectionTitleStyle }, t('lots')), (credits_.upcoming ?? []).length === 0
436
493
  ? h('div', { style: mutedStyle }, t('noLots'))
437
494
  : 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
438
495
  ? 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)
439
- : null, loginBlock(t, login, refresh), credits_.updatedAtMs !== undefined
496
+ : null, usageProfileBlock(t, status?.usageProfile), loginBlock(t, login, refresh), credits_.updatedAtMs !== undefined
440
497
  ? h('div', { style: mutedStyle }, t('updated', { time: new Date(credits_.updatedAtMs).toLocaleTimeString() }))
441
498
  : null);
442
499
  }
package/lib/index.d.ts CHANGED
@@ -133,6 +133,28 @@ interface CatalogModel {
133
133
  inputModalities: string[];
134
134
  /** Whether the model accepts images at all. */
135
135
  attachment: boolean;
136
+ /**
137
+ * Whether reasoning can be turned off for this model.
138
+ *
139
+ * Upstream states it per model in `thinking_config.mode`: a `switchable`
140
+ * model accepts `thinking: {type: "disabled"}`, a `forced_on` model refuses
141
+ * it with `400 ... requires adaptive thinking` (both measured against the live
142
+ * gateway on 2026-11-10). Absent means upstream did not say, which is read as
143
+ * "reasoning is mandatory".
144
+ */
145
+ reasoningOptional?: boolean;
146
+ /**
147
+ * The reasoning effort levels upstream declares (`effort_options`).
148
+ *
149
+ * Recorded rather than acted on. pi-ai's own level mapping still dispatches
150
+ * the budget form (`thinking: {type: "enabled", budget_tokens}`) for these
151
+ * models, so naming the levels here changes no request; the names are kept
152
+ * so the information is not lost when it becomes usable. Reaching them on
153
+ * the wire would need pi-ai's `compat.forceAdaptiveThinking`, which switches
154
+ * the request to the adaptive form — a change of shape this plugin does not
155
+ * make on the strength of a catalog field alone.
156
+ */
157
+ effortLevels?: string[];
136
158
  }
137
159
  /** Provider id registered with DSH, distinct from the upstream one. */
138
160
  declare const MINIMAXCODE_PROVIDER_ID = "minimaxcode";
@@ -150,6 +172,11 @@ declare const MINIMAXCODE_PROVIDER_ID = "minimaxcode";
150
172
  * reports: 512k is the billing tier boundary, and the gateway itself was
151
173
  * measured serving 946,206 tokens before refusing ~1,050,000 with
152
174
  * `invalid params, context window exceeds limit (2013)`.
175
+ *
176
+ * The reasoning modes are the upstream ones measured on 2026-11-10: `M3` is
177
+ * `switchable` and accepts `thinking: {type: "disabled"}`, `M3.1-Flash` is
178
+ * `forced_on` and refuses it. Encoding them here keeps the fallback honest
179
+ * about what the gateway accepts even when no catalog could be fetched.
153
180
  */
154
181
  declare const FALLBACK_MODELS: readonly CatalogModel[];
155
182
  /** Where the currently served list came from. */
@@ -385,6 +412,36 @@ declare function parseRateLimitWindows(document: unknown): RateLimitWindows | un
385
412
  * @returns The windows, or `undefined` when the response carries none.
386
413
  */
387
414
  declare function fetchRateLimitWindows(credential: OAuthCredential, region: MinimaxRegion, fetchImpl?: typeof fetch): Promise<RateLimitWindows | undefined>;
415
+ /**
416
+ * The account's usage profile, as the upstream summarizes it.
417
+ *
418
+ * The desktop app asks for this once a day and shows it as a yearly heatmap
419
+ * with a few headline numbers. It is the only place the upstream says how the
420
+ * account actually uses its tokens, and none of it is visible elsewhere: the
421
+ * balance says how much is left, not how much was spent or how the account
422
+ * compares with other users.
423
+ */
424
+ interface UsageProfile {
425
+ /** Days the summary window covers. */
426
+ totalDays?: number;
427
+ /** Days in the window that saw any usage. */
428
+ activeDays?: number;
429
+ /** Consecutive days of use up to today. */
430
+ currentConsecutiveDays?: number;
431
+ /** Tokens consumed in the window, in the upstream's own wording ("826.14M"). */
432
+ totalTokenConsumed?: string;
433
+ /** Where the account sits among other users, in percent (10.3 = above 89.7% of users). */
434
+ usageRankingPercent?: number;
435
+ /** The busiest day in the window. */
436
+ mostActiveDay?: {
437
+ date?: string;
438
+ tokenCount?: string;
439
+ };
440
+ /** When the upstream last refreshed the summary. */
441
+ lastUpdateTime?: string;
442
+ /** Daily token usage, oldest first. */
443
+ dailyTokenUsage?: number[];
444
+ }
388
445
  /** What a quota read found. */
389
446
  interface RateLimitRead {
390
447
  /** The windows, when the plan meters any. */
@@ -440,14 +497,18 @@ declare class MinimaxCredits {
440
497
  get rateLimitAbsence(): 'no-subscription' | undefined;
441
498
  /** When the windows were last read. */
442
499
  get rateLimitAtMs(): number | undefined;
500
+ /** The account's usage profile as last read. */
501
+ get usageProfile(): UsageProfile | undefined;
502
+ /** Why the usage profile could not be read, when it could not. */
503
+ get usageProfileError(): string | undefined;
443
504
  /**
444
505
  * Read the balance and record how much was consumed since the last read.
445
506
  *
446
507
  * The credit ledger is read alongside it, but a ledger that cannot be read
447
508
  * never hides the balance: the two come from different endpoints and only
448
- * the balance is essential. The rate-limit windows ride along under the
449
- * same rule — they come from yet another endpoint, and none of the three
450
- * takes the others down with it.
509
+ * the balance is essential. The rate-limit windows and the usage profile
510
+ * ride along under the same rule — they come from yet other endpoints, and
511
+ * none of the four takes the others down with it.
451
512
  *
452
513
  * @returns Whether the balance changed.
453
514
  */
@@ -467,6 +528,13 @@ declare class MinimaxCredits {
467
528
  * window that cannot be read leaves the last one on display.
468
529
  */
469
530
  refreshRateLimit(credential: OAuthCredential, region: MinimaxRegion, fetchImpl?: typeof fetch, now?: number): Promise<void>;
531
+ /**
532
+ * Read the account's usage profile.
533
+ *
534
+ * Same rule as the ledger: a failure is remembered but never thrown, and a
535
+ * profile that cannot be read leaves the last one on display.
536
+ */
537
+ refreshUsageProfile(credential: OAuthCredential, region: MinimaxRegion, fetchImpl?: typeof fetch): Promise<void>;
470
538
  /**
471
539
  * Read the windows from whichever endpoint can answer.
472
540
  *
@@ -940,6 +1008,36 @@ interface RateLimitStatus {
940
1008
  /** Why the windows could not be read. */
941
1009
  error?: string;
942
1010
  }
1011
+ /**
1012
+ * The account's usage profile, as the upstream summarizes it.
1013
+ *
1014
+ * This is the account's own history — how long it has been used, how much it
1015
+ * has consumed, how it ranks — and it is a different thing from the usage
1016
+ * this host has made, which the card reports separately.
1017
+ */
1018
+ interface UsageProfileStatus {
1019
+ /** Whether the profile could be read. */
1020
+ state: 'ok' | 'unavailable';
1021
+ /** Days the summary window covers. */
1022
+ totalDays?: number;
1023
+ /** Days in the window that saw any usage. */
1024
+ activeDays?: number;
1025
+ /** Consecutive days of use up to today. */
1026
+ consecutiveDays?: number;
1027
+ /** Tokens consumed in the window, in the upstream's own wording. */
1028
+ totalTokenConsumed?: string;
1029
+ /** Where the account sits among other users, in percent. */
1030
+ rankingPercent?: number;
1031
+ /** The busiest day in the window. */
1032
+ mostActiveDay?: {
1033
+ date?: string;
1034
+ tokenCount?: string;
1035
+ };
1036
+ /** When the upstream last refreshed the summary. */
1037
+ updatedAt?: string;
1038
+ /** Why the profile could not be read. */
1039
+ error?: string;
1040
+ }
943
1041
  /** What the card renders. */
944
1042
  interface StatusDocument {
945
1043
  state: 'signed-in' | 'signed-out' | 'expired';
@@ -970,6 +1068,8 @@ interface StatusDocument {
970
1068
  signin?: SigninStatus;
971
1069
  /** How the plugin is signed in. */
972
1070
  login?: LoginStatus;
1071
+ /** The account's usage profile, when it could be read. */
1072
+ usageProfile?: UsageProfileStatus;
973
1073
  }
974
1074
  /** Inputs the document is derived from. */
975
1075
  interface StatusDeps {
@@ -998,6 +1098,8 @@ interface StatusDeps {
998
1098
  signin?: SigninStatus;
999
1099
  /** How the plugin is signed in. */
1000
1100
  login?: LoginStatus;
1101
+ /** The account's usage profile, when the account API could be reached. */
1102
+ usageProfile?: UsageProfileStatus;
1001
1103
  }
1002
1104
  /**
1003
1105
  * Build the card's status document.
package/lib/index.js CHANGED
@@ -243,6 +243,11 @@ const MINIMAXCODE_PROVIDER_ID = "minimaxcode";
243
243
  * reports: 512k is the billing tier boundary, and the gateway itself was
244
244
  * measured serving 946,206 tokens before refusing ~1,050,000 with
245
245
  * `invalid params, context window exceeds limit (2013)`.
246
+ *
247
+ * The reasoning modes are the upstream ones measured on 2026-11-10: `M3` is
248
+ * `switchable` and accepts `thinking: {type: "disabled"}`, `M3.1-Flash` is
249
+ * `forced_on` and refuses it. Encoding them here keeps the fallback honest
250
+ * about what the gateway accepts even when no catalog could be fetched.
246
251
  */
247
252
  const FALLBACK_MODELS = [{
248
253
  id: "MiniMax-M3",
@@ -256,7 +261,8 @@ const FALLBACK_MODELS = [{
256
261
  "image",
257
262
  "video"
258
263
  ],
259
- attachment: true
264
+ attachment: true,
265
+ reasoningOptional: true
260
266
  }, {
261
267
  id: "MiniMax-M3.1-Flash",
262
268
  name: "M3.1-Flash",
@@ -269,14 +275,20 @@ const FALLBACK_MODELS = [{
269
275
  "image",
270
276
  "video"
271
277
  ],
272
- attachment: true
278
+ attachment: true,
279
+ reasoningOptional: false
273
280
  }];
281
+ /** Whether two effort level lists name the same levels in the same order. */
282
+ function sameEffortLevels(a, b) {
283
+ if (a === void 0 || b === void 0) return a === b;
284
+ return a.length === b.length && a.every((value, index) => value === b[index]);
285
+ }
274
286
  /** Model with the same id, from a newer snapshot. */
275
287
  function sameCatalog(a, b) {
276
288
  if (a.length !== b.length) return false;
277
289
  return a.every((model, index) => {
278
290
  const other = b[index];
279
- return model.id === other.id && model.contextWindow === other.contextWindow && model.toolCall === other.toolCall && model.reasoning === other.reasoning && model.attachment === other.attachment;
291
+ return model.id === other.id && model.contextWindow === other.contextWindow && model.toolCall === other.toolCall && model.reasoning === other.reasoning && model.attachment === other.attachment && model.reasoningOptional === other.reasoningOptional && sameEffortLevels(model.effortLevels, other.effortLevels);
280
292
  });
281
293
  }
282
294
  /** Keep only models whose id the upstream keys them by. */
@@ -313,6 +325,37 @@ function readContextWindow(raw, limit) {
313
325
  }
314
326
  return typeof limit.context === "number" ? limit.context : 2e5;
315
327
  }
328
+ /**
329
+ * Read upstream's `thinking_config.mode`.
330
+ *
331
+ * `switchable` is the only mode whose reasoning can be turned off; every
332
+ * other value — including a missing block, which the gateway treats as
333
+ * mandatory reasoning — leaves the model's reasoning required.
334
+ *
335
+ * @param raw - The upstream model entry.
336
+ * @returns Whether reasoning is optional, or undefined when upstream said nothing.
337
+ */
338
+ function readReasoningOptional(raw) {
339
+ const mode = (raw.thinking_config ?? {}).mode;
340
+ if (mode === "switchable") return true;
341
+ if (mode === "forced_on") return false;
342
+ }
343
+ /**
344
+ * Read upstream's `effort_options`.
345
+ *
346
+ * The levels are recorded as upstream names them; nothing maps them onto a
347
+ * request yet, because pi-ai's level mapping dispatches the budget form for
348
+ * these models and the effort levels would need the adaptive form to travel.
349
+ *
350
+ * @param raw - The upstream model entry.
351
+ * @returns The declared levels, or undefined when upstream declares none.
352
+ */
353
+ function readEffortLevels(raw) {
354
+ const options = raw.effort_options;
355
+ if (!Array.isArray(options)) return void 0;
356
+ const names = options.filter((value) => typeof value === "string");
357
+ return names.length > 0 ? names : void 0;
358
+ }
316
359
  /** Read the fields the upstream sends, tolerating their absence. */
317
360
  function readModel(id, raw) {
318
361
  const limit = raw.limit ?? {};
@@ -327,7 +370,9 @@ function readModel(id, raw) {
327
370
  toolCall: raw.tool_call === true,
328
371
  reasoning: raw.reasoning === true,
329
372
  inputModalities: input,
330
- attachment: raw.attachment === true || input.includes("image")
373
+ attachment: raw.attachment === true || input.includes("image"),
374
+ reasoningOptional: readReasoningOptional(raw),
375
+ effortLevels: readEffortLevels(raw)
331
376
  };
332
377
  }
333
378
  /**
@@ -500,12 +545,16 @@ const NO_COST = {
500
545
  * anthropic client sends the resolved apiKey as `x-api-key`, and MiniMax
501
546
  * rejects that alone with 401 "token is required"; it accepts both headers, so
502
547
  * the bearer value is added alongside rather than replacing it.
503
- * - `thinkingLevelMap.off = null` marks "thinking off" as unsupported. Every
504
- * MiniMax model here requires reasoning, and pi-ai answers an explicit
505
- * `thinkingEnabled: false` with `thinking: {type: "disabled"}`, which the
506
- * gateway refuses with `400 ... requires adaptive thinking`. Marking the level
507
- * unsupported makes pi-ai omit the field instead, which the gateway accepts
508
- * (and, correctly, hides the option that cannot work).
548
+ * - `thinkingLevelMap.off` is how a model says "reasoning cannot be turned
549
+ * off". pi-ai answers an explicit `thinkingEnabled: false` with
550
+ * `thinking: {type: "disabled"}` whenever `off` is not `null`, and omits the
551
+ * field entirely when it is. Which of the two a model needs is upstream's
552
+ * call, not this plugin's: `thinking_config.mode` is `switchable` for a model
553
+ * that accepts the disabled form and `forced_on` for one that refuses it with
554
+ * `400 ... requires adaptive thinking` (both measured on 2026-11-10). So the
555
+ * restriction is declared only where it is real — pinning `off` to `null` for
556
+ * a switchable model would make its reasoning impossible to turn off, and
557
+ * leaving it unset for a forced_on model would make the gateway answer 400.
509
558
  *
510
559
  * @param model - Catalog entry to describe.
511
560
  * @param baseUrl - Region gateway base the anthropic client extends.
@@ -526,7 +575,7 @@ function toPiModel(model, baseUrl, authorization) {
526
575
  contextWindow: model.contextWindow,
527
576
  maxTokens: model.maxOutputTokens ?? 128e3,
528
577
  ...authorization === void 0 ? {} : { headers: { authorization } },
529
- ...model.reasoning ? { thinkingLevelMap: { off: null } } : {}
578
+ ...model.reasoning ? model.reasoningOptional === true ? {} : { thinkingLevelMap: { off: null } } : {}
530
579
  };
531
580
  }
532
581
  /**
@@ -717,7 +766,8 @@ function statusDocument(deps) {
717
766
  ...deps.rateLimit === void 0 ? {} : { rateLimit: deps.rateLimit },
718
767
  ...deps.usage === void 0 ? {} : { usage: deps.usage },
719
768
  ...deps.signin === void 0 ? {} : { signin: deps.signin },
720
- ...deps.login === void 0 ? {} : { login: deps.login }
769
+ ...deps.login === void 0 ? {} : { login: deps.login },
770
+ ...deps.usageProfile === void 0 ? {} : { usageProfile: deps.usageProfile }
721
771
  };
722
772
  if (auth.state === "expired") return {
723
773
  ...base,
@@ -793,6 +843,8 @@ const MINIMAX_OPEN_HOSTS = {
793
843
  const MEMBERSHIP_PATH = "/matrix/api/v1/commerce/get_membership_info";
794
844
  /** Credit ledger endpoint: per-grant amounts and expiry. */
795
845
  const CREDIT_DETAILS_PATH = "/minimax-cloud/api/v1/credit/details";
846
+ /** Per-model token usage summary (open platform). */
847
+ const USAGE_SUMMARY_PATH = "/backend/account/token_plan/usage_summary";
796
848
  /** Open-platform credit endpoint that carries the account's plan key. */
797
849
  const TOKEN_PLAN_CREDIT_PATH = "/backend/account/token_plan_credit";
798
850
  /**
@@ -1213,6 +1265,66 @@ async function fetchTokenPlanApiKey(credential, region, fetchImpl = fetch) {
1213
1265
  const key = (await response.json()).api_key;
1214
1266
  return typeof key === "string" && key.length > 0 ? key : void 0;
1215
1267
  }
1268
+ /**
1269
+ * Read a string field, ignoring anything that is not a string.
1270
+ *
1271
+ * The upstream's token counts carry their own unit ("826.14M"), so they are
1272
+ * kept as the wording it used rather than pulled apart into a number.
1273
+ */
1274
+ function toText(value) {
1275
+ return typeof value === "string" ? value : void 0;
1276
+ }
1277
+ /**
1278
+ * Read the usage profile out of a summary response.
1279
+ *
1280
+ * Every field is optional in practice: the upstream adds fields over time, and
1281
+ * an account with no history at all answers with a body that carries none of
1282
+ * them. A field that is missing — or that arrives in a shape this does not
1283
+ * recognise — is therefore reported as absent rather than as an error, so one
1284
+ * odd field never costs the whole profile. A body that is not an object at
1285
+ * all carries no profile and yields none.
1286
+ */
1287
+ function parseUsageProfile(document) {
1288
+ if (typeof document !== "object" || document === null || Array.isArray(document)) return void 0;
1289
+ const record = document;
1290
+ const day = record.most_active_day;
1291
+ const mostActiveDay = typeof day === "object" && day !== null ? day : void 0;
1292
+ const date = toText(mostActiveDay?.date);
1293
+ const tokenCount = toText(mostActiveDay?.token_count);
1294
+ const daily = Array.isArray(record.daily_token_usage) ? record.daily_token_usage.filter((entry) => typeof entry === "number" && Number.isFinite(entry)) : void 0;
1295
+ return {
1296
+ totalDays: toNumber$1(record.total_days),
1297
+ activeDays: toNumber$1(record.active_days),
1298
+ currentConsecutiveDays: toNumber$1(record.current_consecutive_days),
1299
+ totalTokenConsumed: toText(record.total_token_consumed),
1300
+ usageRankingPercent: toNumber$1(record.usage_ranking_percent),
1301
+ mostActiveDay: date === void 0 && tokenCount === void 0 ? void 0 : {
1302
+ date,
1303
+ tokenCount
1304
+ },
1305
+ lastUpdateTime: toText(record.last_update_time),
1306
+ dailyTokenUsage: daily
1307
+ };
1308
+ }
1309
+ /**
1310
+ * Fetch the account's usage profile.
1311
+ *
1312
+ * The endpoint lives on the open-platform host and takes the same OAuth
1313
+ * bearer the membership endpoint takes; the JWT the model gateway accepts
1314
+ * does not work here. The query is the one the desktop app sends, and it is
1315
+ * not trimmed down: `token_type` picks which token kind is counted and
1316
+ * `heatmap_days` sets how far back the daily series reaches.
1317
+ *
1318
+ * @returns The profile, or `undefined` when the response carries no object.
1319
+ */
1320
+ async function fetchUsageProfile(credential, region, fetchImpl = fetch) {
1321
+ const response = await fetchImpl(`${MINIMAX_OPEN_HOSTS[region]}${USAGE_SUMMARY_PATH}?token_type=5&heatmap_days=365&business=Agent&device_platform=desktop`, { headers: {
1322
+ accept: "application/json",
1323
+ authorization: `Bearer ${credential.token}`
1324
+ } });
1325
+ if (!response.ok) throw new Error(`usage summary request failed: HTTP ${response.status}`);
1326
+ return parseUsageProfile(await response.json());
1327
+ }
1216
1328
  /** The upstream's own code for "this account has no plan subscription". */
1217
1329
  const NO_SUBSCRIPTION_CODE = 2062;
1218
1330
  /** The upstream's own code for "the request carried no usable API key". */
@@ -1289,6 +1401,8 @@ var MinimaxCredits = class {
1289
1401
  #rateLimitError;
1290
1402
  #rateLimitAbsence;
1291
1403
  #rateLimitAtMs;
1404
+ #usageProfile;
1405
+ #usageProfileError;
1292
1406
  #apiKey;
1293
1407
  /** The balance as last read. */
1294
1408
  get snapshot() {
@@ -1330,14 +1444,22 @@ var MinimaxCredits = class {
1330
1444
  get rateLimitAtMs() {
1331
1445
  return this.#rateLimitAtMs;
1332
1446
  }
1447
+ /** The account's usage profile as last read. */
1448
+ get usageProfile() {
1449
+ return this.#usageProfile;
1450
+ }
1451
+ /** Why the usage profile could not be read, when it could not. */
1452
+ get usageProfileError() {
1453
+ return this.#usageProfileError;
1454
+ }
1333
1455
  /**
1334
1456
  * Read the balance and record how much was consumed since the last read.
1335
1457
  *
1336
1458
  * The credit ledger is read alongside it, but a ledger that cannot be read
1337
1459
  * never hides the balance: the two come from different endpoints and only
1338
- * the balance is essential. The rate-limit windows ride along under the
1339
- * same rule — they come from yet another endpoint, and none of the three
1340
- * takes the others down with it.
1460
+ * the balance is essential. The rate-limit windows and the usage profile
1461
+ * ride along under the same rule — they come from yet other endpoints, and
1462
+ * none of the four takes the others down with it.
1341
1463
  *
1342
1464
  * @returns Whether the balance changed.
1343
1465
  */
@@ -1361,6 +1483,7 @@ var MinimaxCredits = class {
1361
1483
  }
1362
1484
  await this.refreshLedger(credential, region, fetchImpl, now);
1363
1485
  await this.refreshRateLimit(credential, region, fetchImpl, now);
1486
+ await this.refreshUsageProfile(credential, region, fetchImpl);
1364
1487
  return changed;
1365
1488
  }
1366
1489
  /**
@@ -1399,6 +1522,21 @@ var MinimaxCredits = class {
1399
1522
  }
1400
1523
  }
1401
1524
  /**
1525
+ * Read the account's usage profile.
1526
+ *
1527
+ * Same rule as the ledger: a failure is remembered but never thrown, and a
1528
+ * profile that cannot be read leaves the last one on display.
1529
+ */
1530
+ async refreshUsageProfile(credential, region, fetchImpl = fetch) {
1531
+ try {
1532
+ const profile = await fetchUsageProfile(credential, region, fetchImpl);
1533
+ this.#usageProfile = profile;
1534
+ this.#usageProfileError = void 0;
1535
+ } catch (error) {
1536
+ this.#usageProfileError = error instanceof Error ? error.message : String(error);
1537
+ }
1538
+ }
1539
+ /**
1402
1540
  * Read the windows from whichever endpoint can answer.
1403
1541
  *
1404
1542
  * The documented endpoint needs the open platform's plan key, which the
@@ -2472,6 +2610,31 @@ function apply(ctx) {
2472
2610
  ...credits.ledgerError === void 0 ? {} : { ledgerError: credits.ledgerError }
2473
2611
  };
2474
2612
  };
2613
+ /**
2614
+ * The account's usage profile as the card reads it.
2615
+ *
2616
+ * The upstream's own summary of the account's history: how many days it has
2617
+ * been used in a row, how much it has consumed, and where it ranks. It is
2618
+ * about the account rather than about this host, which is what the pill's
2619
+ * own token count reports.
2620
+ */
2621
+ const buildUsageProfileStatus = () => {
2622
+ const profile = credits.usageProfile;
2623
+ if (profile === void 0) return credits.usageProfileError === void 0 ? void 0 : {
2624
+ state: "unavailable",
2625
+ error: credits.usageProfileError
2626
+ };
2627
+ return {
2628
+ state: "ok",
2629
+ totalDays: profile.totalDays,
2630
+ activeDays: profile.activeDays,
2631
+ consecutiveDays: profile.currentConsecutiveDays,
2632
+ totalTokenConsumed: profile.totalTokenConsumed,
2633
+ rankingPercent: profile.usageRankingPercent,
2634
+ mostActiveDay: profile.mostActiveDay,
2635
+ updatedAt: profile.lastUpdateTime
2636
+ };
2637
+ };
2475
2638
  /** The plan's rolling usage windows as the card reads them. */
2476
2639
  const buildRateLimitStatus = () => {
2477
2640
  if (creditState === "signed-out" || creditState === "expired") return { state: creditState };
@@ -2619,7 +2782,8 @@ function apply(ctx) {
2619
2782
  rateLimit: buildRateLimitStatus(),
2620
2783
  usage: buildUsageStatus(),
2621
2784
  signin: buildSigninStatus(),
2622
- login: buildLoginStatus()
2785
+ login: buildLoginStatus(),
2786
+ usageProfile: buildUsageProfileStatus()
2623
2787
  });
2624
2788
  };
2625
2789
  (async () => {
package/package.json CHANGED
@@ -2,7 +2,7 @@
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.7",
5
+ "version": "0.5.9",
6
6
  "author": "JeffTsuiCUG",
7
7
  "contributors": [
8
8
  {