dsh-connect-minimaxcode 0.5.6 → 0.5.8

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
@@ -283,6 +283,27 @@ Two sets of data each cover one thing:
283
283
 
284
284
  > 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
285
 
286
+ ### Account usage profile
287
+
288
+ 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.
289
+
290
+ ```
291
+ GET https://www.minimaxi.com/backend/account/token_plan/usage_summary?token_type=5&heatmap_days=365&business=Agent&device_platform=desktop
292
+ Authorization: Bearer <OAuth 令牌>
293
+ ```
294
+
295
+ The card shows the parts worth acting on:
296
+
297
+ | Field | Meaning |
298
+ | --- | --- |
299
+ | `current_consecutive_days` | Days used in a row |
300
+ | `active_days` / `total_days` | Days with usage in the window / the window's length |
301
+ | `total_token_consumed` | Tokens consumed in the window, in the upstream's own wording (`826.14M`) |
302
+ | `usage_ranking_percent` | Where the account ranks by usage |
303
+ | `most_active_day` | The busiest day and what it consumed |
304
+
305
+ 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.
306
+
286
307
  ### About token refresh
287
308
 
288
309
  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 +329,23 @@ The OAuth token is valid for about an hour and is refreshed automatically by the
308
329
 
309
330
  ## Changelog
310
331
 
332
+ ### 0.5.8 (2026-11-10)
333
+
334
+ - **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.
335
+ 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.
336
+ - **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.
337
+ - **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.
338
+ - **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.
339
+ - 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.
340
+
341
+ ### 0.5.7 (2026-11-10)
342
+
343
+ - **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.
344
+ - **"No subscription" is no longer reported as a failed read**: upstream answers an account without an M Plan subscription with `2062 no active token plan subscription` — credit packages carry no rolling windows of their own. The plugin now reports that as what it is. The balance, the check-in and the model calls are untouched by it.
345
+ - The older path is kept as a **fallback**: an account whose open platform hands out no key still gets whatever the old path can give.
346
+ - Fixed a misfire that came with it: the quota endpoint is asked with the open platform's plan key, so a 401 there means the *key* is wrong, not that the person's credential was revoked. It used to feed the "was this credential revoked" check, which could sign someone out over a bad key. That check now reads only the account-domain endpoints (membership and credit ledger).
347
+ - Added `tests/quota-endpoint.spec.ts` (13 cases) pinning: how the plan key is obtained, the documented endpoint being asked with the key rather than the OAuth bearer, `2062` counted as "no subscription" rather than an error, the fallback to the older path when there is no key, and the key being fetched once. All 209 cases pass.
348
+
311
349
  ### 0.5.6 (2026-11-09)
312
350
 
313
351
  - **Fixed "every model request fails while credits and check-in still work"**: the two features read two different credential files. The model gateway only knew the plugin's own sign-in and the **legacy JWT** an older desktop app left behind (`local-runtime.auth.json`), while current installs store an OAuth grant instead (`auth/prod/<region>/mcode-public/auth.json`, an `mmoat_` token). Once the legacy JWT expired, the gateway answered every request with `401 auth failed` — and because credits read the OAuth file, the card showed a healthy balance beside a dead model. The gateway now tries, in order: the plugin's own sign-in → **the desktop app's OAuth grant** → the legacy JWT, taking the first that works.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  把本机已登录的 **MiniMax Code 桌面 App(国内版)** 的模型接入 **DeepSeek Harness(DSH)**,装好即用,无需配置 API Key。
4
4
 
5
5
  ![license](https://img.shields.io/badge/license-MIT-blue.svg)
6
- [![version](https://img.shields.io/npm/v/dsh-connect-minimaxcode?color=cb3837)](https://www.npmjs.com/package/dsh-connect-minimaxcode)
6
+ [![version](https://img.shields.io/npm/v/dsh-connect-minimaxcode?color=cb3837)](https://www.npmjs.com/package/dsh-connect-minimaxcode)
7
7
  [![CI](https://github.com/JeffTsuiCUG/dsh-connect-minimaxcode/actions/workflows/ci.yml/badge.svg)](https://github.com/JeffTsuiCUG/dsh-connect-minimaxcode/actions/workflows/ci.yml)
8
8
 
9
9
  [English](README.en.md) | 简体中文
@@ -291,13 +291,17 @@ GET https://agent.minimax.cn/minimax-cloud/api/v1/credit/details?page_number=1&p
291
291
 
292
292
  ### 限额窗口(套餐的两个滚动窗口)
293
293
 
294
- 套餐用量按两个滚动窗口计量(当前套餐为 5 小时与 7 天),插件会显示各窗口的已用百分比与重置倒计时:
294
+ 套餐用量按两个滚动窗口计量(当前套餐为 5 小时与 7 天),插件会显示各窗口的已用百分比与重置倒计时。
295
+
296
+ 读取走**开放平台官方文档给出的接口**(M Plan 用量说明页「如何查看 M Plan 用量 → 方式三:使用 API 接口查询」):
295
297
 
296
298
  ```
297
- GET https://www.minimaxi.com/backend/account/token_plan/remains_percent?business=Agent&device_platform=desktop
298
- Authorization: Bearer <OAuth 令牌>
299
+ GET https://www.minimaxi.com/v1/token_plan/remains
300
+ Authorization: Bearer <API Key>
299
301
  ```
300
302
 
303
+ 该接口要的是**开放平台的 API 密钥**,不是 OAuth 令牌。密钥由账户自己持有,取法与桌面 App 相同 —— 用 OAuth 令牌请求 `GET /backend/account/token_plan_credit?business=Agent&device_platform=desktop`,响应里的 `api_key` 就是它。
304
+
301
305
  ```json
302
306
  "model_remains": [{
303
307
  "model_name": "general",
@@ -312,9 +316,30 @@ Authorization: Bearer <OAuth 令牌>
312
316
 
313
317
  - **窗口时长不写死**:从响应自身的起止字段推导(实测恰为 5 小时与 7 天),套餐条款变了显示会跟着变 —— 胶囊与详情行上的窗口名(`5h`、`7d`)也是这么来的,不是写死的字面量
314
318
  - **无限量的窗口不报百分比**:上游用 `current_*_status` 标出没有额度的窗口(桌面 App 读作 `isUnlimited: 3 === status`),这类窗口不参与显示,避免出现「0%」这种没人能据此行动的读数
315
- - **该接口在开放平台域**(`www.minimaxi.com`),只认 OAuth 令牌;JWT 打它返回 `1016 invalid api key`
319
+ - **没有订阅就没有窗口,且这不算错误**:上游对未订阅 M Plan 的账户明确回 `2062 no active token plan subscription`,插件会如实标为「无订阅」而不是「读取失败」—— 积分包本身不带滚动窗口
316
320
  - **接口异常时其余显示不受影响**:限额读不到,胶囊与卡片仍显示余额、签到与本机用量
317
321
  - 上游还发 `current_*_count` 计数字段(无计数制时为 `-1`),插件只读百分比与起止时刻
322
+ ### 账户用量画像
323
+
324
+ 账户的**用量历史**由开放平台的用量汇总接口给出,与「本机累计 token」是两回事 —— 后者只统计这台机器发起的请求:
325
+
326
+ ```
327
+ GET https://www.minimaxi.com/backend/account/token_plan/usage_summary?token_type=5&heatmap_days=365&business=Agent&device_platform=desktop
328
+ Authorization: Bearer <OAuth 令牌>
329
+ ```
330
+
331
+ 卡片显示的是其中有行动价值的几项:
332
+
333
+ | 字段 | 含义 |
334
+ | --- | --- |
335
+ | `current_consecutive_days` | 连续使用天数 |
336
+ | `active_days` / `total_days` | 统计窗口内有使用的天数 / 窗口长度 |
337
+ | `total_token_consumed` | 窗口内累计消耗(上游自己的写法,如 `826.14M`) |
338
+ | `usage_ranking_percent` | 用量排名分位 |
339
+ | `most_active_day` | 最活跃的一天及其消耗 |
340
+
341
+ 这一节读到的数据**不需要订阅 M Plan**:该接口属于账户域,用 OAuth 令牌即可读取,与限额窗口(属于订阅)不同。
342
+
318
343
  ### 关于续期
319
344
 
320
345
  OAuth 令牌有效期约 1 小时,由客户端自动续期。插件**只在自己的令牌已过期时**才用 App 已存的 refresh token 续一次,并把轮换后的新令牌按客户端**原格式原子写回**同一个文件(`generation` 保持不变),因此桌面 App 的登录态不受影响。插件不会发起登录、不会登出、也不会调用任何付费/订阅类接口。
@@ -340,6 +365,23 @@ OAuth 令牌有效期约 1 小时,由客户端自动续期。插件**只在自
340
365
 
341
366
  ## 更新记录
342
367
 
368
+ ### 0.5.8(2026-11-10)
369
+
370
+ - **修复「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 的行为不变,选项仍不出现。
371
+ 上游的 `effort_options`(M3.1-Flash 有 default/low/medium/high/xhigh/max 六档)也一并解析存档,但**未接线**:pi-ai 的档位映射目前走 budget 形式,要用上这六档得切换整个请求形态,这一步不在本次改动里。
372
+ - **新增「账户用量画像」**:读取开放平台的 `GET /backend/account/token_plan/usage_summary`(该常量此前定义了却从未使用),卡片显示连续使用天数、统计窗口内的活跃天数、窗口内累计消耗、用量排名分位与最活跃的一天。它说的是**账户**的历史,与胶囊里「本机累计 token」是两回事。
373
+ - **说明「为什么没有限额窗口」**:账户未订阅 M Plan 时,上游回 `2062 no active token plan subscription`(0.5.7 已识别)。现在卡片直接写明「未订阅 M Plan(仅积分包),因此没有限额窗口」,而不是让人以为读取失败。
374
+ - **接口盘点**:把桌面 App 每天真实调用的 20 个账户域接口全部盘了一遍 —— 其中 **19 个用 OAuth 令牌即可读取,无需任何订阅**,只有「限额窗口」属于订阅本身。也就是说订阅 M Plan 换来的不是「更多接口」,而是**限额窗口**与**更高额度**这两样。
375
+ - 新增 `tests/usage-profile.spec.ts`(16 个用例)与 `tests/catalog.spec.ts` / `tests/adapter.spec.ts` 的推理模式用例。全量 236 个用例通过。
376
+
377
+ ### 0.5.7(2026-11-10)
378
+
379
+ - **限额窗口改走官方文档接口**:原先读的是 `/backend/account/token_plan/remains_percent`,这条路径现在对任何账户都只回一个空的 `success`(连 `model_remains` 字段都没有),于是限额一栏对所有人都显示不出来,却看不出原因。改用开放平台官方文档(M Plan 用量说明页)给出的 `GET /v1/token_plan/remains`。新接口要求**开放平台 API 密钥**而非 OAuth 令牌,密钥取自 `token_plan_credit` 的响应并缓存复用。
380
+ - **「没有订阅」不再被报成「读取失败」**:上游对未订阅 M Plan 的账户明确回 `2062 no active token plan subscription` —— 积分包本身不带 5 小时 / 周滚动窗口。插件现在把这种情况标为「无订阅」,余额、签到与模型调用不受任何影响。
381
+ - 旧路径保留为**兜底**:账户若拿不到开放平台密钥,仍走旧路径,不会因此彻底没有限额显示。
382
+ - 顺带修正一处误伤可能:限额接口是拿开放平台密钥问的,它回 401 说明的是密钥不对,**不是本人凭据被吊销**;此前它参与「凭据是否被吊销」的判断,可能把人误登出。现在该判断只看账户域接口(会员信息与积分流水)的 401。
383
+ - 新增 `tests/quota-endpoint.spec.ts`(13 个用例)钉住:计划密钥的取法、官方接口用密钥而非 OAuth 令牌请求、`2062` 归为「无订阅」而非错误、无密钥时回落旧路径、密钥只取一次。全量 209 个用例通过。
384
+
343
385
  ### 0.5.6(2026-11-09)
344
386
 
345
387
  - **修复「模型请求全部失败,但积分/签到正常」**:这是两条凭证路径读的不是同一个文件。模型网关此前只认插件自登录与桌面 App 留下的**旧版 JWT**(`local-runtime.auth.json`),而新版 App 早已改用 OAuth 存储(`auth/prod/<region>/mcode-public/auth.json`,`mmoat_` 令牌)。旧 JWT 过期后网关一律回 `401 auth failed`,而积分走的是 OAuth 文件,于是出现「积分能看、模型不通」。现在网关依次尝试:插件自登录 → **桌面 App 的 OAuth 授权** → 旧版 JWT,三者取第一个可用的。
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. */
@@ -208,7 +235,12 @@ declare class MinimaxCatalog {
208
235
  declare const MINIMAX_OAUTH_CLIENT_ID = "mcode-public";
209
236
  /** Membership endpoint: carries the credit summary. */
210
237
  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. */
238
+ /**
239
+ * Rate-limit window endpoint the plugin used first.
240
+ *
241
+ * Kept as a fallback for an account whose open platform hands out no plan
242
+ * key, since the documented path needs one.
243
+ */
212
244
  declare const REMAINS_PERCENT_PATH = "/backend/account/token_plan/remains_percent";
213
245
  /** The OAuth credential the account endpoints accept. */
214
246
  interface OAuthCredential {
@@ -371,7 +403,7 @@ interface RateLimitWindows {
371
403
  */
372
404
  declare function parseRateLimitWindows(document: unknown): RateLimitWindows | undefined;
373
405
  /**
374
- * Fetch the plan's two rate-limit windows.
406
+ * Fetch the plan's two rate-limit windows from the older open-platform path.
375
407
  *
376
408
  * The endpoint lives on the open-platform host, like the usage summary, and
377
409
  * answers the same OAuth credential the membership endpoint takes. The JWT the
@@ -380,6 +412,45 @@ declare function parseRateLimitWindows(document: unknown): RateLimitWindows | un
380
412
  * @returns The windows, or `undefined` when the response carries none.
381
413
  */
382
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
+ }
445
+ /** What a quota read found. */
446
+ interface RateLimitRead {
447
+ /** The windows, when the plan meters any. */
448
+ windows?: RateLimitWindows;
449
+ /** Why there are none, when the upstream said so plainly. */
450
+ absence?: 'no-subscription';
451
+ /** Why the read failed, when it did. */
452
+ error?: string;
453
+ }
383
454
  /** What the lots say about credits that are about to be lost. */
384
455
  interface ExpirySummary {
385
456
  /** Credits in lots that expire within {@link EXPIRING_WINDOW_DAYS} days. */
@@ -422,16 +493,22 @@ declare class MinimaxCredits {
422
493
  get rateLimit(): RateLimitWindows | undefined;
423
494
  /** Why the windows could not be read, when they could not. */
424
495
  get rateLimitError(): string | undefined;
496
+ /** Why the plan has no windows, when the upstream said so plainly. */
497
+ get rateLimitAbsence(): 'no-subscription' | undefined;
425
498
  /** When the windows were last read. */
426
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;
427
504
  /**
428
505
  * Read the balance and record how much was consumed since the last read.
429
506
  *
430
507
  * The credit ledger is read alongside it, but a ledger that cannot be read
431
508
  * never hides the balance: the two come from different endpoints and only
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.
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.
435
512
  *
436
513
  * @returns Whether the balance changed.
437
514
  */
@@ -451,6 +528,22 @@ declare class MinimaxCredits {
451
528
  * window that cannot be read leaves the last one on display.
452
529
  */
453
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>;
538
+ /**
539
+ * Read the windows from whichever endpoint can answer.
540
+ *
541
+ * The documented endpoint needs the open platform's plan key, which the
542
+ * same platform hands out; the key is remembered because it does not
543
+ * change from one read to the next. An account with no key at all still
544
+ * gets the older path's answer.
545
+ */
546
+ readWindows(credential: OAuthCredential, region: MinimaxRegion, fetchImpl?: typeof fetch): Promise<RateLimitRead>;
454
547
  }
455
548
  //#endregion
456
549
  //#region src/usage.d.ts
@@ -910,9 +1003,41 @@ interface RateLimitStatus {
910
1003
  };
911
1004
  /** When the windows were read, in milliseconds since the epoch. */
912
1005
  updatedAtMs?: number;
1006
+ /** Why the plan has no windows, when the upstream said so plainly. */
1007
+ reason?: 'no-subscription';
913
1008
  /** Why the windows could not be read. */
914
1009
  error?: string;
915
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
+ }
916
1041
  /** What the card renders. */
917
1042
  interface StatusDocument {
918
1043
  state: 'signed-in' | 'signed-out' | 'expired';
@@ -943,6 +1068,8 @@ interface StatusDocument {
943
1068
  signin?: SigninStatus;
944
1069
  /** How the plugin is signed in. */
945
1070
  login?: LoginStatus;
1071
+ /** The account's usage profile, when it could be read. */
1072
+ usageProfile?: UsageProfileStatus;
946
1073
  }
947
1074
  /** Inputs the document is derived from. */
948
1075
  interface StatusDeps {
@@ -971,6 +1098,8 @@ interface StatusDeps {
971
1098
  signin?: SigninStatus;
972
1099
  /** How the plugin is signed in. */
973
1100
  login?: LoginStatus;
1101
+ /** The account's usage profile, when the account API could be reached. */
1102
+ usageProfile?: UsageProfileStatus;
974
1103
  }
975
1104
  /**
976
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,7 +843,26 @@ 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";
796
- /** Rate-limit window endpoint: the 5-hour and weekly usage the plan meters. */
846
+ /** Per-model token usage summary (open platform). */
847
+ const USAGE_SUMMARY_PATH = "/backend/account/token_plan/usage_summary";
848
+ /** Open-platform credit endpoint that carries the account's plan key. */
849
+ const TOKEN_PLAN_CREDIT_PATH = "/backend/account/token_plan_credit";
850
+ /**
851
+ * Rate-limit window endpoint, as the open platform documents it.
852
+ *
853
+ * The M Plan FAQ names `GET /v1/token_plan/remains` as the way to read the
854
+ * quota over HTTP. It asks for an API secret key rather than the desktop
855
+ * app's OAuth bearer, and it is the path the upstream still maintains: the
856
+ * older path this plugin started with now answers 200 with an empty body
857
+ * even for accounts that do have windows.
858
+ */
859
+ const TOKEN_PLAN_REMAINS_PATH = "/v1/token_plan/remains";
860
+ /**
861
+ * Rate-limit window endpoint the plugin used first.
862
+ *
863
+ * Kept as a fallback for an account whose open platform hands out no plan
864
+ * key, since the documented path needs one.
865
+ */
797
866
  const REMAINS_PERCENT_PATH = "/backend/account/token_plan/remains_percent";
798
867
  /** OAuth token endpoint. */
799
868
  const TOKEN_PATH$1 = "/oauth2/token";
@@ -1160,7 +1229,7 @@ function parsePercent(value) {
1160
1229
  return Number.isFinite(parsed) ? parsed : void 0;
1161
1230
  }
1162
1231
  /**
1163
- * Fetch the plan's two rate-limit windows.
1232
+ * Fetch the plan's two rate-limit windows from the older open-platform path.
1164
1233
  *
1165
1234
  * The endpoint lives on the open-platform host, like the usage summary, and
1166
1235
  * answers the same OAuth credential the membership endpoint takes. The JWT the
@@ -1177,6 +1246,121 @@ async function fetchRateLimitWindows(credential, region, fetchImpl = fetch) {
1177
1246
  return parseRateLimitWindows(await response.json());
1178
1247
  }
1179
1248
  /**
1249
+ * Read the open platform's plan key.
1250
+ *
1251
+ * The documented quota endpoint wants an API secret key, and the open
1252
+ * platform is where the account's key comes from: the desktop app asks the
1253
+ * same endpoint with its OAuth bearer and gets the key back in the answer,
1254
+ * so the key is a property of the account rather than something the person
1255
+ * has to configure.
1256
+ *
1257
+ * @returns The key, or `undefined` when the account has none.
1258
+ */
1259
+ async function fetchTokenPlanApiKey(credential, region, fetchImpl = fetch) {
1260
+ const response = await fetchImpl(`${MINIMAX_OPEN_HOSTS[region]}${TOKEN_PLAN_CREDIT_PATH}?business=Agent&device_platform=desktop`, { headers: {
1261
+ accept: "application/json",
1262
+ authorization: `Bearer ${credential.token}`
1263
+ } });
1264
+ if (!response.ok) throw new Error(`token plan credit request failed: HTTP ${response.status}`);
1265
+ const key = (await response.json()).api_key;
1266
+ return typeof key === "string" && key.length > 0 ? key : void 0;
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
+ }
1328
+ /** The upstream's own code for "this account has no plan subscription". */
1329
+ const NO_SUBSCRIPTION_CODE = 2062;
1330
+ /** The upstream's own code for "the request carried no usable API key". */
1331
+ const MISSING_API_KEY_CODE = 1004;
1332
+ /** The upstream's status code inside a response body, when it carries one. */
1333
+ function readBaseRespCode(document) {
1334
+ const base = document.base_resp;
1335
+ if (typeof base !== "object" || base === null) return void 0;
1336
+ const code = base.status_code;
1337
+ return typeof code === "number" ? code : void 0;
1338
+ }
1339
+ /**
1340
+ * Read the windows from the documented quota endpoint.
1341
+ *
1342
+ * The upstream says plainly when there is nothing to report: `2062` means
1343
+ * the account has no plan subscription, and the quota does not exist for it
1344
+ * at all. That is a fact about the plan rather than a failure to read, so
1345
+ * it is reported as such and not as an error — the windows of a plan the
1346
+ * account does not hold are not missing data.
1347
+ */
1348
+ async function fetchTokenPlanWindows(apiKey, region, fetchImpl = fetch) {
1349
+ const response = await fetchImpl(`${MINIMAX_OPEN_HOSTS[region]}${TOKEN_PLAN_REMAINS_PATH}`, { headers: {
1350
+ accept: "application/json",
1351
+ authorization: `Bearer ${apiKey}`,
1352
+ "content-type": "application/json"
1353
+ } });
1354
+ if (!response.ok) throw new Error(`rate-limit window request failed: HTTP ${response.status}`);
1355
+ const document = await response.json();
1356
+ const code = readBaseRespCode(document);
1357
+ if (code === NO_SUBSCRIPTION_CODE) return { absence: "no-subscription" };
1358
+ if (code === MISSING_API_KEY_CODE) return { error: "the open platform did not accept the plan key" };
1359
+ const windows = parseRateLimitWindows(document);
1360
+ if (windows === void 0) return { error: "response carried no rate-limit windows" };
1361
+ return { windows };
1362
+ }
1363
+ /**
1180
1364
  * Summarize what is about to expire.
1181
1365
  *
1182
1366
  * Only lots that still hold credits count: a lot that was granted and used up
@@ -1215,7 +1399,11 @@ var MinimaxCredits = class {
1215
1399
  #ledgerError;
1216
1400
  #rateLimit;
1217
1401
  #rateLimitError;
1402
+ #rateLimitAbsence;
1218
1403
  #rateLimitAtMs;
1404
+ #usageProfile;
1405
+ #usageProfileError;
1406
+ #apiKey;
1219
1407
  /** The balance as last read. */
1220
1408
  get snapshot() {
1221
1409
  return this.#snapshot;
@@ -1248,18 +1436,30 @@ var MinimaxCredits = class {
1248
1436
  get rateLimitError() {
1249
1437
  return this.#rateLimitError;
1250
1438
  }
1439
+ /** Why the plan has no windows, when the upstream said so plainly. */
1440
+ get rateLimitAbsence() {
1441
+ return this.#rateLimitAbsence;
1442
+ }
1251
1443
  /** When the windows were last read. */
1252
1444
  get rateLimitAtMs() {
1253
1445
  return this.#rateLimitAtMs;
1254
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
+ }
1255
1455
  /**
1256
1456
  * Read the balance and record how much was consumed since the last read.
1257
1457
  *
1258
1458
  * The credit ledger is read alongside it, but a ledger that cannot be read
1259
1459
  * never hides the balance: the two come from different endpoints and only
1260
- * the balance is essential. The rate-limit windows ride along under the
1261
- * same rule — they come from yet another endpoint, and none of the three
1262
- * 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.
1263
1463
  *
1264
1464
  * @returns Whether the balance changed.
1265
1465
  */
@@ -1283,6 +1483,7 @@ var MinimaxCredits = class {
1283
1483
  }
1284
1484
  await this.refreshLedger(credential, region, fetchImpl, now);
1285
1485
  await this.refreshRateLimit(credential, region, fetchImpl, now);
1486
+ await this.refreshUsageProfile(credential, region, fetchImpl);
1286
1487
  return changed;
1287
1488
  }
1288
1489
  /**
@@ -1311,14 +1512,51 @@ var MinimaxCredits = class {
1311
1512
  */
1312
1513
  async refreshRateLimit(credential, region, fetchImpl = fetch, now = Date.now()) {
1313
1514
  try {
1314
- const windows = await fetchRateLimitWindows(credential, region, fetchImpl);
1315
- this.#rateLimit = windows;
1316
- this.#rateLimitError = windows === void 0 ? "response carried no rate-limit windows" : void 0;
1515
+ const read = await this.readWindows(credential, region, fetchImpl);
1516
+ if (read.error === void 0) this.#rateLimit = read.windows;
1517
+ this.#rateLimitAbsence = read.absence;
1518
+ this.#rateLimitError = read.error;
1317
1519
  this.#rateLimitAtMs = now;
1318
1520
  } catch (error) {
1319
1521
  this.#rateLimitError = error instanceof Error ? error.message : String(error);
1320
1522
  }
1321
1523
  }
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
+ /**
1540
+ * Read the windows from whichever endpoint can answer.
1541
+ *
1542
+ * The documented endpoint needs the open platform's plan key, which the
1543
+ * same platform hands out; the key is remembered because it does not
1544
+ * change from one read to the next. An account with no key at all still
1545
+ * gets the older path's answer.
1546
+ */
1547
+ async readWindows(credential, region, fetchImpl = fetch) {
1548
+ const apiKey = this.#apiKey ?? await fetchTokenPlanApiKey(credential, region, fetchImpl);
1549
+ this.#apiKey = apiKey;
1550
+ if (apiKey !== void 0) try {
1551
+ return await fetchTokenPlanWindows(apiKey, region, fetchImpl);
1552
+ } catch (error) {
1553
+ const legacy = await fetchRateLimitWindows(credential, region, fetchImpl);
1554
+ if (legacy !== void 0) return { windows: legacy };
1555
+ throw error;
1556
+ }
1557
+ const legacy = await fetchRateLimitWindows(credential, region, fetchImpl);
1558
+ return legacy === void 0 ? { error: "response carried no rate-limit windows" } : { windows: legacy };
1559
+ }
1322
1560
  };
1323
1561
  //#endregion
1324
1562
  //#region src/credentials.ts
@@ -2271,11 +2509,7 @@ function apply(ctx) {
2271
2509
  }
2272
2510
  await credits.refresh(state.credential, region, fetch, now);
2273
2511
  creditState = credits.snapshot === void 0 ? "unavailable" : "ok";
2274
- if ([
2275
- credits.error,
2276
- credits.ledgerError,
2277
- credits.rateLimitError
2278
- ].find((message) => isUnauthorized(message)) !== void 0) {
2512
+ if ([credits.error, credits.ledgerError].find((message) => isUnauthorized(message)) !== void 0) {
2279
2513
  await revokePluginCredential();
2280
2514
  return;
2281
2515
  }
@@ -2376,12 +2610,38 @@ function apply(ctx) {
2376
2610
  ...credits.ledgerError === void 0 ? {} : { ledgerError: credits.ledgerError }
2377
2611
  };
2378
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
+ };
2379
2638
  /** The plan's rolling usage windows as the card reads them. */
2380
2639
  const buildRateLimitStatus = () => {
2381
2640
  if (creditState === "signed-out" || creditState === "expired") return { state: creditState };
2382
2641
  const windows = credits.rateLimit;
2383
2642
  if (windows === void 0) return {
2384
2643
  state: "unavailable",
2644
+ ...credits.rateLimitAbsence === void 0 ? {} : { reason: credits.rateLimitAbsence },
2385
2645
  ...credits.rateLimitError === void 0 ? {} : { error: credits.rateLimitError }
2386
2646
  };
2387
2647
  return {
@@ -2522,7 +2782,8 @@ function apply(ctx) {
2522
2782
  rateLimit: buildRateLimitStatus(),
2523
2783
  usage: buildUsageStatus(),
2524
2784
  signin: buildSigninStatus(),
2525
- login: buildLoginStatus()
2785
+ login: buildLoginStatus(),
2786
+ usageProfile: buildUsageProfileStatus()
2526
2787
  });
2527
2788
  };
2528
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.6",
5
+ "version": "0.5.8",
6
6
  "author": "JeffTsuiCUG",
7
7
  "contributors": [
8
8
  {