dsh-connect-minimaxcode 0.2.2 → 0.3.0

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/lib/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import "@earendil-works/pi-ai";
2
- import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
2
+ import { LlmAdapter } from "@deepseek-ai/dsh-llm";
3
3
  import { Context } from "@deepseek-ai/cordis";
4
4
  //#region src/auth.d.ts
5
5
  /** File the desktop app writes its runtime auth context into. */
@@ -140,7 +140,10 @@ declare const MINIMAXCODE_PROVIDER_ID = "minimaxcode";
140
140
  * models the desktop app is known to ship.
141
141
  *
142
142
  * Deliberately small and conservative: a fallback entry overstates nothing we
143
- * could not confirm.
143
+ * could not confirm. Last verified against the live catalog on 2026-10-19,
144
+ * when upstream retired `MiniMax-M2.7` and `MiniMax-M2.7-highspeed` and
145
+ * promoted the model previously served anonymously (internally "space bunny")
146
+ * as `MiniMax-M3.1-Flash`.
144
147
  */
145
148
  declare const FALLBACK_MODELS: readonly CatalogModel[];
146
149
  /** Where the currently served list came from. */
@@ -194,9 +197,260 @@ declare class MinimaxCatalog {
194
197
  get identity(): string | undefined;
195
198
  }
196
199
  //#endregion
200
+ //#region src/credits.d.ts
201
+ /** The desktop app's own OAuth client id. */
202
+ declare const MINIMAX_OAUTH_CLIENT_ID = "mcode-public";
203
+ /** Membership endpoint: carries the credit summary. */
204
+ declare const MEMBERSHIP_PATH = "/matrix/api/v1/commerce/get_membership_info";
205
+ /** The OAuth credential the account endpoints accept. */
206
+ interface OAuthCredential {
207
+ /** The `mmoat_` bearer token. */
208
+ token: string;
209
+ /** The refresh token, when the app stored one. */
210
+ refreshToken?: string;
211
+ /** Expiry in milliseconds since the epoch, when known. */
212
+ expiresAtMs?: number;
213
+ /** The client the token was issued to. */
214
+ clientId: string;
215
+ /** Scopes the token carries. */
216
+ scopes: string[];
217
+ /** Audience the token was issued for. */
218
+ audience: string;
219
+ /** The app's login generation; preserved verbatim on write-back. */
220
+ generation?: number;
221
+ }
222
+ /** Why the account data may be unavailable. */
223
+ type OAuthState = {
224
+ state: 'signed-in';
225
+ credential: OAuthCredential;
226
+ } | {
227
+ state: 'expired';
228
+ credential: OAuthCredential;
229
+ } | {
230
+ state: 'signed-out';
231
+ reason: 'app-missing' | 'not-signed-in' | 'unreadable' | 'malformed';
232
+ };
233
+ /**
234
+ * Parse the app's credential document.
235
+ *
236
+ * The record key is generated per install, so the record is found by value
237
+ * rather than by a hard-coded name.
238
+ */
239
+ declare function parseOAuthDocument(text: string, now?: number): OAuthState;
240
+ /**
241
+ * Read the desktop app's OAuth credential.
242
+ *
243
+ * Every failure is a state, not an exception: an app that is not installed, or
244
+ * not signed in, is a normal condition the UI renders rather than an error the
245
+ * host logs on every request.
246
+ */
247
+ declare function readOAuthCredential(region: MinimaxRegion, env?: NodeJS.ProcessEnv, now?: number): Promise<OAuthState>;
248
+ /**
249
+ * Exchange the stored refresh token for a fresh access token.
250
+ *
251
+ * The authorization server rotates the refresh token, so the new pair is
252
+ * written back into the app's own file, in the app's own format, with an
253
+ * atomic replace. `generation` is left untouched on purpose: the app uses it
254
+ * to decide whether a stored credential still belongs to its own login, and
255
+ * raising it would make the app believe it had been signed in elsewhere.
256
+ */
257
+ declare function refreshOAuthCredential(region: MinimaxRegion, credential: OAuthCredential, env?: NodeJS.ProcessEnv, now?: number, fetchImpl?: typeof fetch): Promise<OAuthState>;
258
+ /**
259
+ * Write a rotated credential back into the app's store.
260
+ *
261
+ * The document is rewritten as a whole so the app's own reader keeps seeing
262
+ * the shape it wrote, and the replace is atomic so a crash mid-write cannot
263
+ * leave a truncated credential file behind.
264
+ */
265
+ declare function writeOAuthCredential(region: MinimaxRegion, credential: OAuthCredential, env?: NodeJS.ProcessEnv): Promise<void>;
266
+ /**
267
+ * Read the credential, refreshing it when it is close to expiry.
268
+ *
269
+ * A refresh is only attempted for a credential the app already stored, and
270
+ * only when it is actually stale: a fresh token is left alone, so the plugin
271
+ * never competes with the app for the same grant.
272
+ */
273
+ declare function ensureOAuthCredential(region: MinimaxRegion, env?: NodeJS.ProcessEnv, now?: number, fetchImpl?: typeof fetch): Promise<OAuthState>;
274
+ /** The credit balance the desktop app shows. */
275
+ interface CreditSummary {
276
+ /** Remaining credits, free plus purchased. */
277
+ total: number;
278
+ /** Remaining purchased credits. */
279
+ purchased: number;
280
+ /** Remaining free (granted) credits. */
281
+ free: number;
282
+ }
283
+ /**
284
+ * Read the credit summary out of a membership response.
285
+ *
286
+ * The app's settings page prefers the summary and falls back to the flat
287
+ * balance fields, so the same fallback chain is used here.
288
+ */
289
+ declare function parseCreditSummary(document: unknown): CreditSummary | undefined;
290
+ /**
291
+ * Fetch the current credit balance.
292
+ *
293
+ * @returns The balance, or `undefined` when the account has no credit summary.
294
+ */
295
+ declare function fetchCreditSummary(credential: OAuthCredential, region: MinimaxRegion, fetchImpl?: typeof fetch): Promise<CreditSummary | undefined>;
296
+ /** The balance plus what changed since the previous read. */
297
+ interface CreditSnapshot {
298
+ summary: CreditSummary;
299
+ /** Milliseconds since the epoch, when the balance was read. */
300
+ fetchedAtMs: number;
301
+ /**
302
+ * Credits consumed since the previous read.
303
+ *
304
+ * The upstream balance is a live counter, so the difference between two
305
+ * reads is the credits actually spent in between. It can be negative when
306
+ * credits are granted (the app grants credits daily), and it counts every
307
+ * client of the account, not just this one.
308
+ */
309
+ consumed: number;
310
+ }
311
+ /**
312
+ * Keeps the current balance and the change between reads.
313
+ *
314
+ * The first read has nothing to compare against, so it reports no movement
315
+ * rather than pretending the whole balance was spent.
316
+ */
317
+ declare class MinimaxCredits {
318
+ #private;
319
+ /** The balance as last read. */
320
+ get snapshot(): CreditSnapshot | undefined;
321
+ /** Why the last read failed, when one did. */
322
+ get error(): string | undefined;
323
+ /**
324
+ * Read the balance and record how much was consumed since the last read.
325
+ *
326
+ * @returns Whether the balance changed.
327
+ */
328
+ refresh(credential: OAuthCredential, region: MinimaxRegion, fetchImpl?: typeof fetch, now?: number): Promise<boolean>;
329
+ }
330
+ //#endregion
331
+ //#region src/usage.d.ts
332
+ /**
333
+ * What a conversation actually cost.
334
+ *
335
+ * Every response from the gateway carries its own usage counters, so the
336
+ * consumption of a session can be measured exactly rather than estimated from
337
+ * the transcript. The upstream sends four numbers, and the distinction
338
+ * between them matters: `input_tokens` counts only the tokens that missed the
339
+ * prompt cache, so a naive sum understates a long conversation badly.
340
+ *
341
+ * The conversion to money uses MiniMax's published domestic price list
342
+ * (元/百万 tokens). It is an equivalent amount, not a charge: the credits the
343
+ * desktop app shows are a separate resource, and MiniMax publishes no
344
+ * numeric exchange rate between the two.
345
+ *
346
+ * @module dsh-connect-minimaxcode/usage
347
+ */
348
+ /** Token counts of one request, as the gateway reported them. */
349
+ interface ModelUsage {
350
+ /** Tokens sent that missed the prompt cache. */
351
+ inputTokens: number;
352
+ /** Tokens produced. */
353
+ outputTokens: number;
354
+ /** Tokens served from the prompt cache. */
355
+ cacheReadTokens: number;
356
+ /** Tokens written into the prompt cache. */
357
+ cacheWriteTokens: number;
358
+ /** Tokens spent reasoning, when the model reports them separately. */
359
+ reasoningTokens: number;
360
+ }
361
+ /** An empty usage record. */
362
+ declare const EMPTY_USAGE: ModelUsage;
363
+ /** Prices in 元 per million tokens, from MiniMax's domestic price list. */
364
+ interface ModelPrice {
365
+ input: number;
366
+ output: number;
367
+ cacheRead: number;
368
+ }
369
+ /** The published domestic prices of the models this plugin serves. */
370
+ declare const MODEL_PRICES: Readonly<Record<string, ModelPrice>>;
371
+ /**
372
+ * The price for one model at one input size.
373
+ *
374
+ * M3 is billed in two tiers: requests above 512k input tokens cost double.
375
+ * The tier is decided by the billed input, which is the uncached input plus
376
+ * the cache read.
377
+ */
378
+ declare function priceFor(model: string, billedInputTokens: number): ModelPrice | undefined;
379
+ /**
380
+ * The equivalent amount of one request, in 元.
381
+ *
382
+ * Returns `undefined` for a model without a published price, rather than
383
+ * pretending the request was free.
384
+ */
385
+ declare function estimateCny(model: string, usage: ModelUsage): number | undefined;
386
+ /** One recorded request. */
387
+ interface UsageRecord {
388
+ model: string;
389
+ usage: ModelUsage;
390
+ /** Equivalent amount in 元, when the model has a published price. */
391
+ cny?: number;
392
+ }
393
+ /**
394
+ * Accumulates what the sessions routed through this plugin have used.
395
+ *
396
+ * The meter is the plugin's own view: it counts the requests this host made,
397
+ * with the counters the gateway returned. It is not the account's total, and
398
+ * it is not the credit balance — the balance comes from the account API.
399
+ */
400
+ declare class UsageMeter {
401
+ #private;
402
+ /** Every recorded request, oldest first. */
403
+ get records(): readonly UsageRecord[];
404
+ /** The most recent request. */
405
+ get last(): UsageRecord | undefined;
406
+ /** Record one request's usage. */
407
+ record(model: string, usage: ModelUsage): UsageRecord;
408
+ /** Totals across every recorded request. */
409
+ totals(): ModelUsage;
410
+ /** Totals per model. */
411
+ byModel(): ReadonlyMap<string, ModelUsage>;
412
+ /** The equivalent amount of everything recorded, in 元. */
413
+ estimatedCny(): number;
414
+ /** Forget everything recorded. */
415
+ reset(): void;
416
+ }
417
+ //#endregion
197
418
  //#region src/status-route.d.ts
198
419
  /** Route this plugin mounts, matching its settings card. */
199
420
  declare const MINIMAX_STATUS_PATH = "/plugins/dsh-connect-minimaxcode/status";
421
+ /** The account's credit balance, as the desktop app shows it. */
422
+ interface CreditStatus {
423
+ /** Whether the balance could be read. */
424
+ state: 'ok' | 'unavailable' | 'expired' | 'signed-out';
425
+ /** Remaining credits, free plus purchased. */
426
+ total?: number;
427
+ /** Remaining purchased credits. */
428
+ purchased?: number;
429
+ /** Remaining free (granted) credits. */
430
+ free?: number;
431
+ /** Credits consumed between the two last reads. */
432
+ consumed?: number;
433
+ /** When the balance was read, in milliseconds since the epoch. */
434
+ updatedAtMs?: number;
435
+ /** Why the balance could not be read. */
436
+ error?: string;
437
+ }
438
+ /** What the plugin's own requests have used. */
439
+ interface UsageStatus {
440
+ inputTokens: number;
441
+ outputTokens: number;
442
+ cacheReadTokens: number;
443
+ cacheWriteTokens: number;
444
+ reasoningTokens: number;
445
+ /** Equivalent amount of everything this host spent, in 元. */
446
+ estimatedCny: number;
447
+ /** The equivalent amount of the last request, in 元. */
448
+ lastCny?: number;
449
+ /** The model of the last request. */
450
+ lastModel?: string;
451
+ /** How many requests were measured. */
452
+ requests: number;
453
+ }
200
454
  /** What the card renders. */
201
455
  interface StatusDocument {
202
456
  state: 'signed-in' | 'signed-out' | 'expired';
@@ -217,6 +471,10 @@ interface StatusDocument {
217
471
  }[];
218
472
  /** Whether a catalog refresh is in flight. */
219
473
  refreshing?: boolean;
474
+ /** The account's credit balance, when it could be read. */
475
+ credits?: CreditStatus;
476
+ /** What this host's requests have used. */
477
+ usage?: UsageStatus;
220
478
  }
221
479
  /** Inputs the document is derived from. */
222
480
  interface StatusDeps {
@@ -235,6 +493,10 @@ interface StatusDeps {
235
493
  contextWindow: number;
236
494
  }[];
237
495
  refreshing: boolean;
496
+ /** The account's credit balance, when the account API could be reached. */
497
+ credits?: CreditStatus;
498
+ /** What this host's requests have used. */
499
+ usage?: UsageStatus;
238
500
  }
239
501
  /**
240
502
  * Build the card's status document.
@@ -260,10 +522,23 @@ interface AdapterOptions {
260
522
  * model descriptors that carry it are built inside `getModels()`.
261
523
  */
262
524
  bearerToken: () => string | undefined;
525
+ /**
526
+ * Called with the usage the gateway reported for a finished request.
527
+ *
528
+ * The counters come from the upstream response, not from an estimate of the
529
+ * transcript, so what is reported is what was actually billed.
530
+ */
531
+ onUsage?: (model: string, usage: ModelUsage) => void;
263
532
  }
264
533
  /** What {@link createMinimaxCodeAdapter} hands back. */
265
534
  interface AdapterBundle {
266
- adapter: PiAiAdapter;
535
+ /**
536
+ * The adapter the host registers.
537
+ *
538
+ * It is the pi-ai adapter itself, or a wrapper around it that copies the
539
+ * usage counters out of the stream.
540
+ */
541
+ adapter: LlmAdapter;
267
542
  /** Drop cached profiles so the next pick rebuilds them. */
268
543
  invalidate: () => void;
269
544
  }
@@ -294,4 +569,4 @@ declare const inject: string[];
294
569
  */
295
570
  declare function apply(ctx: Context): void;
296
571
  //#endregion
297
- export { type AuthState, type CatalogModel, type CatalogSource, type Credential, FALLBACK_MODELS, MINIMAXCODE_PROVIDER_ID, MINIMAX_AUTH_FILENAME, MINIMAX_DATA_DIR, MINIMAX_GATEWAYS, MINIMAX_REGIONS, MINIMAX_STATUS_PATH, MINIMAX_STREAM_IDLE_TIMEOUT_MS, MinimaxCatalog, type MinimaxRegion, type StatusDocument, type TokenClaims, apply, authPath, bearerToken, catalogUrl, chatBaseUrl, createMinimaxCodeAdapter, dataDir, daysRemaining, decodeClaims, expiresInMs, expiryLevel, inject, name, parseAuthDocument, parseCatalog, readCredential, statusDocument };
572
+ export { type AuthState, type CatalogModel, type CatalogSource, type Credential, type CreditSummary, EMPTY_USAGE, FALLBACK_MODELS, MEMBERSHIP_PATH, MINIMAXCODE_PROVIDER_ID, MINIMAX_AUTH_FILENAME, MINIMAX_DATA_DIR, MINIMAX_GATEWAYS, MINIMAX_OAUTH_CLIENT_ID, MINIMAX_REGIONS, MINIMAX_STATUS_PATH, MINIMAX_STREAM_IDLE_TIMEOUT_MS, MODEL_PRICES, MinimaxCatalog, MinimaxCredits, type MinimaxRegion, type ModelUsage, type OAuthCredential, type OAuthState, type StatusDocument, type TokenClaims, UsageMeter, apply, authPath, bearerToken, catalogUrl, chatBaseUrl, createMinimaxCodeAdapter, dataDir, daysRemaining, decodeClaims, ensureOAuthCredential, estimateCny, expiresInMs, expiryLevel, fetchCreditSummary, inject, name, parseAuthDocument, parseCatalog, parseCreditSummary, parseOAuthDocument, priceFor, readCredential, readOAuthCredential, refreshOAuthCredential, statusDocument, writeOAuthCredential };