dsh-connect-minimaxcode 0.5.7 → 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,15 @@ 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
+
311
341
  ### 0.5.7 (2026-11-10)
312
342
 
313
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.
package/README.md CHANGED
@@ -319,6 +319,27 @@ Authorization: Bearer <API Key>
319
319
  - **没有订阅就没有窗口,且这不算错误**:上游对未订阅 M Plan 的账户明确回 `2062 no active token plan subscription`,插件会如实标为「无订阅」而不是「读取失败」—— 积分包本身不带滚动窗口
320
320
  - **接口异常时其余显示不受影响**:限额读不到,胶囊与卡片仍显示余额、签到与本机用量
321
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
+
322
343
  ### 关于续期
323
344
 
324
345
  OAuth 令牌有效期约 1 小时,由客户端自动续期。插件**只在自己的令牌已过期时**才用 App 已存的 refresh token 续一次,并把轮换后的新令牌按客户端**原格式原子写回**同一个文件(`generation` 保持不变),因此桌面 App 的登录态不受影响。插件不会发起登录、不会登出、也不会调用任何付费/订阅类接口。
@@ -344,6 +365,15 @@ OAuth 令牌有效期约 1 小时,由客户端自动续期。插件**只在自
344
365
 
345
366
  ## 更新记录
346
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
+
347
377
  ### 0.5.7(2026-11-10)
348
378
 
349
379
  - **限额窗口改走官方文档接口**:原先读的是 `/backend/account/token_plan/remains_percent`,这条路径现在对任何账户都只回一个空的 `success`(连 `model_remains` 字段都没有),于是限额一栏对所有人都显示不出来,却看不出原因。改用开放平台官方文档(M Plan 用量说明页)给出的 `GET /v1/token_plan/remains`。新接口要求**开放平台 API 密钥**而非 OAuth 令牌,密钥取自 `token_plan_credit` 的响应并缓存复用。
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.8",
6
6
  "author": "JeffTsuiCUG",
7
7
  "contributors": [
8
8
  {