dsh-connect-minimaxcode 0.2.3 → 0.4.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. */
@@ -197,9 +197,336 @@ declare class MinimaxCatalog {
197
197
  get identity(): string | undefined;
198
198
  }
199
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
+ * One grant of credits.
313
+ *
314
+ * Credits are granted in lots, and each lot carries its own expiry: the
315
+ * desktop app grants credits daily, and every lot falls due about a month
316
+ * later on a day of its own. Credits in a lot are lost when that day arrives.
317
+ */
318
+ interface CreditLot {
319
+ /** How much this lot was granted. */
320
+ granted: number;
321
+ /** How much of it is left. */
322
+ remaining: number;
323
+ /** How much of it has been used. */
324
+ consumed: number;
325
+ /** When the lot was granted, in milliseconds since the epoch. */
326
+ grantedAtMs: number;
327
+ /** When the lot expires, in milliseconds since the epoch. */
328
+ expireAtMs: number;
329
+ /** The upstream's kind of lot, when it says. */
330
+ creditType?: number;
331
+ }
332
+ /** What the lots say about credits that are about to be lost. */
333
+ interface ExpirySummary {
334
+ /** Credits in lots that expire within {@link EXPIRING_WINDOW_DAYS} days. */
335
+ expiring: number;
336
+ /** How many lots that is. */
337
+ expiringLots: number;
338
+ /** Credits in lots that expire within {@link EXPIRING_HORIZON_DAYS} days. */
339
+ expiringSoon: number;
340
+ /** When the next lot with credits left expires. */
341
+ nextExpiryAtMs?: number;
342
+ /** The next lots with credits left, soonest first. */
343
+ upcoming: {
344
+ amount: number;
345
+ expireAtMs: number;
346
+ }[];
347
+ /** Credits left across every lot. */
348
+ totalRemaining: number;
349
+ }
350
+ /**
351
+ * Keeps the current balance and the change between reads.
352
+ *
353
+ * The first read has nothing to compare against, so it reports no movement
354
+ * rather than pretending the whole balance was spent.
355
+ */
356
+ declare class MinimaxCredits {
357
+ #private;
358
+ /** The balance as last read. */
359
+ get snapshot(): CreditSnapshot | undefined;
360
+ /** Why the last read failed, when one did. */
361
+ get error(): string | undefined;
362
+ /** The credit lots as last read. */
363
+ get lots(): readonly CreditLot[];
364
+ /** What the lots say about expiry, when the ledger could be read. */
365
+ get expiry(): ExpirySummary | undefined;
366
+ /** When the ledger was last read. */
367
+ get ledgerAtMs(): number | undefined;
368
+ /** Why the ledger could not be read, when it could not. */
369
+ get ledgerError(): string | undefined;
370
+ /**
371
+ * Read the balance and record how much was consumed since the last read.
372
+ *
373
+ * The credit ledger is read alongside it, but a ledger that cannot be read
374
+ * never hides the balance: the two come from different endpoints and only
375
+ * the balance is essential.
376
+ *
377
+ * @returns Whether the balance changed.
378
+ */
379
+ refresh(credential: OAuthCredential, region: MinimaxRegion, fetchImpl?: typeof fetch, now?: number): Promise<boolean>;
380
+ /**
381
+ * Read the credit ledger and work out what is about to expire.
382
+ *
383
+ * A failure here is remembered but never thrown: the balance is what the
384
+ * user needs most, and a ledger that is temporarily unavailable must not
385
+ * take it down with it.
386
+ */
387
+ refreshLedger(credential: OAuthCredential, region: MinimaxRegion, fetchImpl?: typeof fetch, now?: number): Promise<void>;
388
+ }
389
+ //#endregion
390
+ //#region src/usage.d.ts
391
+ /**
392
+ * What a conversation actually cost.
393
+ *
394
+ * Every response from the gateway carries its own usage counters, so the
395
+ * consumption of a session can be measured exactly rather than estimated from
396
+ * the transcript. The upstream sends four numbers, and the distinction
397
+ * between them matters: `input_tokens` counts only the tokens that missed the
398
+ * prompt cache, so a naive sum understates a long conversation badly.
399
+ *
400
+ * The conversion to money uses MiniMax's published domestic price list
401
+ * (元/百万 tokens). It is an equivalent amount, not a charge: the credits the
402
+ * desktop app shows are a separate resource, and MiniMax publishes no
403
+ * numeric exchange rate between the two.
404
+ *
405
+ * @module dsh-connect-minimaxcode/usage
406
+ */
407
+ /** Token counts of one request, as the gateway reported them. */
408
+ interface ModelUsage {
409
+ /** Tokens sent that missed the prompt cache. */
410
+ inputTokens: number;
411
+ /** Tokens produced. */
412
+ outputTokens: number;
413
+ /** Tokens served from the prompt cache. */
414
+ cacheReadTokens: number;
415
+ /** Tokens written into the prompt cache. */
416
+ cacheWriteTokens: number;
417
+ /** Tokens spent reasoning, when the model reports them separately. */
418
+ reasoningTokens: number;
419
+ }
420
+ /** An empty usage record. */
421
+ declare const EMPTY_USAGE: ModelUsage;
422
+ /** Prices in 元 per million tokens, from MiniMax's domestic price list. */
423
+ interface ModelPrice {
424
+ input: number;
425
+ output: number;
426
+ cacheRead: number;
427
+ }
428
+ /** The published domestic prices of the models this plugin serves. */
429
+ declare const MODEL_PRICES: Readonly<Record<string, ModelPrice>>;
430
+ /**
431
+ * The price for one model at one input size.
432
+ *
433
+ * M3 is billed in two tiers: requests above 512k input tokens cost double.
434
+ * The tier is decided by the billed input, which is the uncached input plus
435
+ * the cache read.
436
+ */
437
+ declare function priceFor(model: string, billedInputTokens: number): ModelPrice | undefined;
438
+ /**
439
+ * The equivalent amount of one request, in 元.
440
+ *
441
+ * Returns `undefined` for a model without a published price, rather than
442
+ * pretending the request was free.
443
+ */
444
+ declare function estimateCny(model: string, usage: ModelUsage): number | undefined;
445
+ /** One recorded request. */
446
+ interface UsageRecord {
447
+ model: string;
448
+ usage: ModelUsage;
449
+ /** Equivalent amount in 元, when the model has a published price. */
450
+ cny?: number;
451
+ }
452
+ /**
453
+ * Accumulates what the sessions routed through this plugin have used.
454
+ *
455
+ * The meter is the plugin's own view: it counts the requests this host made,
456
+ * with the counters the gateway returned. It is not the account's total, and
457
+ * it is not the credit balance — the balance comes from the account API.
458
+ */
459
+ declare class UsageMeter {
460
+ #private;
461
+ /** Every recorded request, oldest first. */
462
+ get records(): readonly UsageRecord[];
463
+ /** The most recent request. */
464
+ get last(): UsageRecord | undefined;
465
+ /** Record one request's usage. */
466
+ record(model: string, usage: ModelUsage): UsageRecord;
467
+ /** Totals across every recorded request. */
468
+ totals(): ModelUsage;
469
+ /** Totals per model. */
470
+ byModel(): ReadonlyMap<string, ModelUsage>;
471
+ /** The equivalent amount of everything recorded, in 元. */
472
+ estimatedCny(): number;
473
+ /** Forget everything recorded. */
474
+ reset(): void;
475
+ }
476
+ //#endregion
200
477
  //#region src/status-route.d.ts
201
478
  /** Route this plugin mounts, matching its settings card. */
202
479
  declare const MINIMAX_STATUS_PATH = "/plugins/dsh-connect-minimaxcode/status";
480
+ /** The account's credit balance, as the desktop app shows it. */
481
+ interface CreditStatus {
482
+ /** Whether the balance could be read. */
483
+ state: 'ok' | 'unavailable' | 'expired' | 'signed-out';
484
+ /** Remaining credits, free plus purchased. */
485
+ total?: number;
486
+ /** Remaining purchased credits. */
487
+ purchased?: number;
488
+ /** Remaining free (granted) credits. */
489
+ free?: number;
490
+ /** Credits consumed between the two last reads. */
491
+ consumed?: number;
492
+ /** When the balance was read, in milliseconds since the epoch. */
493
+ updatedAtMs?: number;
494
+ /** Why the balance could not be read. */
495
+ error?: string;
496
+ /** Credits that expire within the next week. */
497
+ expiring?: number;
498
+ /** How many grants that is. */
499
+ expiringLots?: number;
500
+ /** Credits that expire within the next month. */
501
+ expiringSoon?: number;
502
+ /** When the next grant with credits left expires. */
503
+ nextExpiryAtMs?: number;
504
+ /** The next grants with credits left, soonest first. */
505
+ upcoming?: {
506
+ amount: number;
507
+ expireAtMs: number;
508
+ }[];
509
+ /** When the ledger was read, in milliseconds since the epoch. */
510
+ ledgerAtMs?: number;
511
+ /** Why the ledger could not be read. */
512
+ ledgerError?: string;
513
+ }
514
+ /** What the plugin's own requests have used. */
515
+ interface UsageStatus {
516
+ inputTokens: number;
517
+ outputTokens: number;
518
+ cacheReadTokens: number;
519
+ cacheWriteTokens: number;
520
+ reasoningTokens: number;
521
+ /** Equivalent amount of everything this host spent, in 元. */
522
+ estimatedCny: number;
523
+ /** The equivalent amount of the last request, in 元. */
524
+ lastCny?: number;
525
+ /** The model of the last request. */
526
+ lastModel?: string;
527
+ /** How many requests were measured. */
528
+ requests: number;
529
+ }
203
530
  /** What the card renders. */
204
531
  interface StatusDocument {
205
532
  state: 'signed-in' | 'signed-out' | 'expired';
@@ -220,6 +547,10 @@ interface StatusDocument {
220
547
  }[];
221
548
  /** Whether a catalog refresh is in flight. */
222
549
  refreshing?: boolean;
550
+ /** The account's credit balance, when it could be read. */
551
+ credits?: CreditStatus;
552
+ /** What this host's requests have used. */
553
+ usage?: UsageStatus;
223
554
  }
224
555
  /** Inputs the document is derived from. */
225
556
  interface StatusDeps {
@@ -238,6 +569,10 @@ interface StatusDeps {
238
569
  contextWindow: number;
239
570
  }[];
240
571
  refreshing: boolean;
572
+ /** The account's credit balance, when the account API could be reached. */
573
+ credits?: CreditStatus;
574
+ /** What this host's requests have used. */
575
+ usage?: UsageStatus;
241
576
  }
242
577
  /**
243
578
  * Build the card's status document.
@@ -263,10 +598,23 @@ interface AdapterOptions {
263
598
  * model descriptors that carry it are built inside `getModels()`.
264
599
  */
265
600
  bearerToken: () => string | undefined;
601
+ /**
602
+ * Called with the usage the gateway reported for a finished request.
603
+ *
604
+ * The counters come from the upstream response, not from an estimate of the
605
+ * transcript, so what is reported is what was actually billed.
606
+ */
607
+ onUsage?: (model: string, usage: ModelUsage) => void;
266
608
  }
267
609
  /** What {@link createMinimaxCodeAdapter} hands back. */
268
610
  interface AdapterBundle {
269
- adapter: PiAiAdapter;
611
+ /**
612
+ * The adapter the host registers.
613
+ *
614
+ * It is the pi-ai adapter itself, or a wrapper around it that copies the
615
+ * usage counters out of the stream.
616
+ */
617
+ adapter: LlmAdapter;
270
618
  /** Drop cached profiles so the next pick rebuilds them. */
271
619
  invalidate: () => void;
272
620
  }
@@ -297,4 +645,4 @@ declare const inject: string[];
297
645
  */
298
646
  declare function apply(ctx: Context): void;
299
647
  //#endregion
300
- 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 };
648
+ 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 };