dsh-plugin-subscription-usage 0.0.0-stage → 0.2.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/LICENSE +22 -0
- package/README.md +50 -2
- package/cordis.patch.yml +5 -0
- package/icon.svg +9 -0
- package/lib/auth/claude-code-creds.d.ts +23 -0
- package/lib/auth/claude-code-creds.js +172 -0
- package/lib/auth/device-flow.d.ts +55 -0
- package/lib/auth/device-flow.js +177 -0
- package/lib/auth/jwt.d.ts +10 -0
- package/lib/auth/jwt.js +25 -0
- package/lib/auth/oauth-flow.d.ts +91 -0
- package/lib/auth/oauth-flow.js +227 -0
- package/lib/auth/pkce.d.ts +31 -0
- package/lib/auth/pkce.js +35 -0
- package/lib/auth/rpc.d.ts +233 -0
- package/lib/auth/rpc.js +501 -0
- package/lib/auth/store.d.ts +198 -0
- package/lib/auth/store.js +473 -0
- package/lib/client/Footer.d.ts +3 -0
- package/lib/client/Footer.js +73 -0
- package/lib/client/Gauge.d.ts +28 -0
- package/lib/client/Gauge.js +139 -0
- package/lib/client/ImageGallery.d.ts +54 -0
- package/lib/client/ImageGallery.js +112 -0
- package/lib/client/ImageGenerateToolview.d.ts +48 -0
- package/lib/client/ImageGenerateToolview.js +130 -0
- package/lib/client/Notice.d.ts +15 -0
- package/lib/client/Notice.js +93 -0
- package/lib/client/ProviderCard.d.ts +35 -0
- package/lib/client/ProviderCard.js +240 -0
- package/lib/client/SpeedSelect.d.ts +72 -0
- package/lib/client/SpeedSelect.js +214 -0
- package/lib/client/Usage.d.ts +7 -0
- package/lib/client/Usage.js +83 -0
- package/lib/client/VideoGenerateToolview.d.ts +42 -0
- package/lib/client/VideoGenerateToolview.js +172 -0
- package/lib/client/context.d.ts +16 -0
- package/lib/client/context.js +11 -0
- package/lib/client/fast-command.d.ts +2 -0
- package/lib/client/fast-command.js +4 -0
- package/lib/client/hooks.d.ts +2 -0
- package/lib/client/hooks.js +10 -0
- package/lib/client/index.d.ts +3 -0
- package/lib/client/index.js +113 -0
- package/lib/client/locales.d.ts +139 -0
- package/lib/client/locales.js +267 -0
- package/lib/client/meter.d.ts +91 -0
- package/lib/client/meter.js +196 -0
- package/lib/client/prefs.d.ts +51 -0
- package/lib/client/prefs.js +71 -0
- package/lib/client/rpc.d.ts +161 -0
- package/lib/client/rpc.js +31 -0
- package/lib/client/store.d.ts +67 -0
- package/lib/client/store.js +129 -0
- package/lib/client/styles.d.ts +7 -0
- package/lib/client/styles.js +176 -0
- package/lib/client.js +3933 -0
- package/lib/client.js.map +1 -0
- package/lib/compat.d.ts +55 -0
- package/lib/compat.js +20 -0
- package/lib/http.d.ts +134 -0
- package/lib/http.js +431 -0
- package/lib/index.d.ts +121 -0
- package/lib/index.js +13399 -0
- package/lib/json-text.d.ts +2 -0
- package/lib/json-text.js +4 -0
- package/lib/model-defaults.d.ts +23 -0
- package/lib/model-defaults.js +238 -0
- package/lib/model-speeds.d.ts +10 -0
- package/lib/model-speeds.js +48 -0
- package/lib/models-directory.d.ts +17 -0
- package/lib/models-directory.js +25 -0
- package/lib/provider-settings.d.ts +55 -0
- package/lib/provider-settings.js +215 -0
- package/lib/providers/account-preferences.d.ts +46 -0
- package/lib/providers/account-preferences.js +188 -0
- package/lib/providers/accounts.d.ts +113 -0
- package/lib/providers/accounts.js +142 -0
- package/lib/providers/antigravity-oauth-client.d.ts +7 -0
- package/lib/providers/antigravity-oauth-client.js +7 -0
- package/lib/providers/antigravity.d.ts +119 -0
- package/lib/providers/antigravity.js +552 -0
- package/lib/providers/catalog-refresh.d.ts +4 -0
- package/lib/providers/catalog-refresh.js +30 -0
- package/lib/providers/catalog-store.d.ts +47 -0
- package/lib/providers/catalog-store.js +283 -0
- package/lib/providers/claude-cli-version.d.ts +15 -0
- package/lib/providers/claude-cli-version.js +24 -0
- package/lib/providers/claude.d.ts +212 -0
- package/lib/providers/claude.js +726 -0
- package/lib/providers/codex-client-version.d.ts +8 -0
- package/lib/providers/codex-client-version.js +17 -0
- package/lib/providers/codex-search.d.ts +29 -0
- package/lib/providers/codex-search.js +198 -0
- package/lib/providers/codex.d.ts +253 -0
- package/lib/providers/codex.js +930 -0
- package/lib/providers/common.d.ts +415 -0
- package/lib/providers/common.js +679 -0
- package/lib/providers/copilot.d.ts +365 -0
- package/lib/providers/copilot.js +956 -0
- package/lib/providers/grok.d.ts +214 -0
- package/lib/providers/grok.js +750 -0
- package/lib/providers/image-pool.d.ts +30 -0
- package/lib/providers/image-pool.js +89 -0
- package/lib/providers/npm-cli-version.d.ts +56 -0
- package/lib/providers/npm-cli-version.js +98 -0
- package/lib/providers/pool-family.d.ts +56 -0
- package/lib/providers/pool-family.js +45 -0
- package/lib/providers/pool-health.d.ts +91 -0
- package/lib/providers/pool-health.js +164 -0
- package/lib/providers/pool-usage.d.ts +106 -0
- package/lib/providers/pool-usage.js +302 -0
- package/lib/providers/pool.d.ts +115 -0
- package/lib/providers/pool.js +461 -0
- package/lib/providers/rate-limit.d.ts +192 -0
- package/lib/providers/rate-limit.js +338 -0
- package/lib/providers/replay.d.ts +4 -0
- package/lib/providers/replay.js +37 -0
- package/lib/providers/reset-redemption.d.ts +20 -0
- package/lib/providers/reset-redemption.js +70 -0
- package/lib/tools/image-generate.d.ts +128 -0
- package/lib/tools/image-generate.js +438 -0
- package/lib/tools/registration.d.ts +16 -0
- package/lib/tools/registration.js +25 -0
- package/lib/tools/usage.d.ts +62 -0
- package/lib/tools/usage.js +89 -0
- package/lib/tools/video-generate.d.ts +89 -0
- package/lib/tools/video-generate.js +257 -0
- package/lib/tools/x-search.d.ts +58 -0
- package/lib/tools/x-search.js +197 -0
- package/lib/translate/anthropic.d.ts +180 -0
- package/lib/translate/anthropic.js +534 -0
- package/lib/translate/antigravity-images.d.ts +11 -0
- package/lib/translate/antigravity-images.js +62 -0
- package/lib/translate/antigravity-schema.d.ts +4 -0
- package/lib/translate/antigravity-schema.js +145 -0
- package/lib/translate/antigravity-thinking.d.ts +5 -0
- package/lib/translate/antigravity-thinking.js +44 -0
- package/lib/translate/antigravity.d.ts +120 -0
- package/lib/translate/antigravity.js +412 -0
- package/lib/translate/chat-completions.d.ts +120 -0
- package/lib/translate/chat-completions.js +382 -0
- package/lib/translate/resolved.d.ts +98 -0
- package/lib/translate/resolved.js +186 -0
- package/lib/translate/responses.d.ts +192 -0
- package/lib/translate/responses.js +443 -0
- package/lib/translate/sse.d.ts +21 -0
- package/lib/translate/sse.js +62 -0
- package/lib/usage/headers.d.ts +21 -0
- package/lib/usage/headers.js +64 -0
- package/lib/usage/pace.d.ts +53 -0
- package/lib/usage/pace.js +48 -0
- package/lib/usage/service.d.ts +64 -0
- package/lib/usage/service.js +104 -0
- package/lib/usage-providers/common.d.ts +23 -0
- package/lib/usage-providers/common.js +28 -0
- package/lib/usage-providers/index.d.ts +42 -0
- package/lib/usage-providers/index.js +71 -0
- package/lib/usage-providers/keys.d.ts +11 -0
- package/lib/usage-providers/keys.js +52 -0
- package/lib/usage-providers/kimi.d.ts +4 -0
- package/lib/usage-providers/kimi.js +52 -0
- package/lib/usage-providers/ollama-cloud.d.ts +4 -0
- package/lib/usage-providers/ollama-cloud.js +42 -0
- package/lib/usage-providers/zai.d.ts +5 -0
- package/lib/usage-providers/zai.js +53 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +115 -4
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal SSE byte-stream parser (~30 lines of framing): reassembles chunks,
|
|
3
|
+
* splits CRLF/LF lines, joins multi-`data:` payloads, skips comments and
|
|
4
|
+
* non-data fields, and dispatches an event only on its blank-line terminator.
|
|
5
|
+
* An unterminated tail at EOF is truncation and is dropped, matching the
|
|
6
|
+
* spec-strict framing the harness's own adapters use.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Decode an SSE byte stream into events.
|
|
10
|
+
* @param stream - raw response bytes; reads may split anywhere, including mid-UTF-8 sequence.
|
|
11
|
+
* @param onActivity - called on every received chunk and comment line; drives the idle watchdog.
|
|
12
|
+
* @returns events in arrival order.
|
|
13
|
+
*/
|
|
14
|
+
export async function* parseSse(stream, onActivity) {
|
|
15
|
+
const reader = stream.getReader();
|
|
16
|
+
const decoder = new TextDecoder();
|
|
17
|
+
let pending = '';
|
|
18
|
+
let dataLines = [];
|
|
19
|
+
let eventName;
|
|
20
|
+
try {
|
|
21
|
+
while (true) {
|
|
22
|
+
const { done, value } = await reader.read();
|
|
23
|
+
if (done)
|
|
24
|
+
return;
|
|
25
|
+
onActivity?.();
|
|
26
|
+
pending += decoder.decode(value, { stream: true });
|
|
27
|
+
let newline = pending.indexOf('\n');
|
|
28
|
+
while (newline >= 0) {
|
|
29
|
+
let line = pending.slice(0, newline);
|
|
30
|
+
pending = pending.slice(newline + 1);
|
|
31
|
+
newline = pending.indexOf('\n');
|
|
32
|
+
if (line.endsWith('\r'))
|
|
33
|
+
line = line.slice(0, -1);
|
|
34
|
+
if (line.length === 0) {
|
|
35
|
+
if (dataLines.length > 0) {
|
|
36
|
+
yield { data: dataLines.join('\n'), ...eventName === undefined ? {} : { event: eventName } };
|
|
37
|
+
}
|
|
38
|
+
dataLines = [];
|
|
39
|
+
eventName = undefined;
|
|
40
|
+
}
|
|
41
|
+
else if (line.startsWith(':')) {
|
|
42
|
+
onActivity?.();
|
|
43
|
+
}
|
|
44
|
+
else if (line.startsWith('data:')) {
|
|
45
|
+
dataLines.push(line.slice(5).replace(/^ /, ''));
|
|
46
|
+
}
|
|
47
|
+
else if (line.startsWith('event:')) {
|
|
48
|
+
eventName = line.slice(6).replace(/^ /, '');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
// A terminal SSE frame can precede HTTP EOF; returning or decoding failure
|
|
55
|
+
// must close the remaining body rather than leave the connection reading.
|
|
56
|
+
try {
|
|
57
|
+
await reader.cancel();
|
|
58
|
+
}
|
|
59
|
+
catch { /* preserve the original stream outcome */ }
|
|
60
|
+
reader.releaseLock();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan-usage windows carried on ordinary model responses. Reading them after
|
|
3
|
+
* every reply keeps meters current without calling the usage endpoints.
|
|
4
|
+
*
|
|
5
|
+
* Shapes observed on 2026-11-08 (docs/features/FEAT-001, ASM-002):
|
|
6
|
+
* - Codex: `x-codex-{primary,secondary}-used-percent` (0..100),
|
|
7
|
+
* `-window-minutes`, `-reset-at` (epoch seconds). Sent on 2xx and 429.
|
|
8
|
+
* - Claude: `anthropic-ratelimit-unified-{5h,7d}-utilization` (0..1),
|
|
9
|
+
* `-reset` (epoch seconds).
|
|
10
|
+
* Only these fields are read; nothing else from the response is kept.
|
|
11
|
+
*/
|
|
12
|
+
import type { UsageWindow } from '../providers/common.js';
|
|
13
|
+
/** Providers whose replies carry usage headers. */
|
|
14
|
+
export type LiveUsageProvider = 'codex' | 'claude';
|
|
15
|
+
/**
|
|
16
|
+
* Parse the usage windows a reply carries.
|
|
17
|
+
* @param provider - the provider that answered.
|
|
18
|
+
* @param headers - the reply headers.
|
|
19
|
+
* @returns the windows in session-then-weekly order; empty when the reply carries none.
|
|
20
|
+
*/
|
|
21
|
+
export declare function usageWindowsFromHeaders(provider: LiveUsageProvider, headers: Headers): UsageWindow[];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const MINUTE_MS = 60_000;
|
|
2
|
+
const FIVE_HOURS_MS = 5 * 60 * MINUTE_MS;
|
|
3
|
+
const SEVEN_DAYS_MS = 7 * 24 * 60 * MINUTE_MS;
|
|
4
|
+
/** Read a finite number header, or undefined. */
|
|
5
|
+
function numberHeader(headers, name) {
|
|
6
|
+
const raw = headers.get(name);
|
|
7
|
+
if (raw === null || raw.trim() === '')
|
|
8
|
+
return undefined;
|
|
9
|
+
const value = Number(raw);
|
|
10
|
+
return Number.isFinite(value) ? value : undefined;
|
|
11
|
+
}
|
|
12
|
+
/** Epoch-seconds header to epoch milliseconds. */
|
|
13
|
+
function epochHeader(headers, name) {
|
|
14
|
+
const seconds = numberHeader(headers, name);
|
|
15
|
+
return seconds === undefined || seconds <= 0 ? undefined : seconds * 1000;
|
|
16
|
+
}
|
|
17
|
+
/** Kind from a window length, matching the endpoint parsers' session/weekly split. */
|
|
18
|
+
function kindOf(durationMs, fallback) {
|
|
19
|
+
if (durationMs === undefined)
|
|
20
|
+
return fallback;
|
|
21
|
+
if (durationMs <= 24 * 60 * MINUTE_MS)
|
|
22
|
+
return 'session';
|
|
23
|
+
if (durationMs <= 8 * 24 * 60 * MINUTE_MS)
|
|
24
|
+
return 'weekly';
|
|
25
|
+
return 'other';
|
|
26
|
+
}
|
|
27
|
+
function codexWindow(headers, slot) {
|
|
28
|
+
const usedPercent = numberHeader(headers, `x-codex-${slot}-used-percent`);
|
|
29
|
+
if (usedPercent === undefined)
|
|
30
|
+
return undefined;
|
|
31
|
+
const minutes = numberHeader(headers, `x-codex-${slot}-window-minutes`);
|
|
32
|
+
const durationMs = minutes !== undefined && minutes > 0 ? minutes * MINUTE_MS : undefined;
|
|
33
|
+
const resetsAt = epochHeader(headers, `x-codex-${slot}-reset-at`);
|
|
34
|
+
return {
|
|
35
|
+
kind: kindOf(durationMs, slot === 'primary' ? 'session' : 'weekly'),
|
|
36
|
+
usedPercent,
|
|
37
|
+
...durationMs === undefined ? {} : { durationMs },
|
|
38
|
+
...resetsAt === undefined ? {} : { resetsAt },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function claudeWindow(headers, slot) {
|
|
42
|
+
const utilization = numberHeader(headers, `anthropic-ratelimit-unified-${slot}-utilization`);
|
|
43
|
+
if (utilization === undefined)
|
|
44
|
+
return undefined;
|
|
45
|
+
const resetsAt = epochHeader(headers, `anthropic-ratelimit-unified-${slot}-reset`);
|
|
46
|
+
return {
|
|
47
|
+
kind: slot === '5h' ? 'session' : 'weekly',
|
|
48
|
+
usedPercent: utilization * 100,
|
|
49
|
+
durationMs: slot === '5h' ? FIVE_HOURS_MS : SEVEN_DAYS_MS,
|
|
50
|
+
...resetsAt === undefined ? {} : { resetsAt },
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Parse the usage windows a reply carries.
|
|
55
|
+
* @param provider - the provider that answered.
|
|
56
|
+
* @param headers - the reply headers.
|
|
57
|
+
* @returns the windows in session-then-weekly order; empty when the reply carries none.
|
|
58
|
+
*/
|
|
59
|
+
export function usageWindowsFromHeaders(provider, headers) {
|
|
60
|
+
const windows = provider === 'codex'
|
|
61
|
+
? [codexWindow(headers, 'primary'), codexWindow(headers, 'secondary')]
|
|
62
|
+
: [claudeWindow(headers, '5h'), claudeWindow(headers, '7d')];
|
|
63
|
+
return windows.filter(window => window !== undefined);
|
|
64
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pace of one usage window: how much quota is used against how much of the
|
|
3
|
+
* window has elapsed, and whether the current rate empties the window before
|
|
4
|
+
* it resets. Pure, so the Host and the browser bundle share one definition.
|
|
5
|
+
*/
|
|
6
|
+
import type { UsageWindow } from '../providers/common.js';
|
|
7
|
+
/** Below this share of elapsed window a burst says little about the whole window. */
|
|
8
|
+
export declare const MIN_FORECAST_ELAPSED = 0.2;
|
|
9
|
+
/** Below this usage the projection is noise. */
|
|
10
|
+
export declare const MIN_FORECAST_USED_PERCENT = 3;
|
|
11
|
+
/** Projected use above this percent turns a window `tight`. */
|
|
12
|
+
export declare const TIGHT_PROJECTED_PERCENT = 85;
|
|
13
|
+
/** Pace of one window. `calm` and `tight` may carry a projection; `over` always does. */
|
|
14
|
+
export type Pace = {
|
|
15
|
+
state: 'unknown';
|
|
16
|
+
} | {
|
|
17
|
+
state: 'out';
|
|
18
|
+
usedPercent: number;
|
|
19
|
+
} | {
|
|
20
|
+
state: 'calm';
|
|
21
|
+
usedPercent: number;
|
|
22
|
+
elapsed?: number;
|
|
23
|
+
projectedPercent?: number;
|
|
24
|
+
} | {
|
|
25
|
+
state: 'tight';
|
|
26
|
+
usedPercent: number;
|
|
27
|
+
elapsed: number;
|
|
28
|
+
projectedPercent: number;
|
|
29
|
+
} | {
|
|
30
|
+
state: 'over';
|
|
31
|
+
usedPercent: number;
|
|
32
|
+
elapsed: number;
|
|
33
|
+
projectedPercent: number;
|
|
34
|
+
emptyAt: number;
|
|
35
|
+
};
|
|
36
|
+
/** The states a meter colors by. */
|
|
37
|
+
export type PaceState = Pace['state'];
|
|
38
|
+
/**
|
|
39
|
+
* Compute the pace of one window at `now`.
|
|
40
|
+
* @param window - the provider-reported window.
|
|
41
|
+
* @param now - epoch milliseconds.
|
|
42
|
+
* @returns the pace; `elapsed` is the share of the window passed, 0 to 1.
|
|
43
|
+
*/
|
|
44
|
+
export declare function pace(window: UsageWindow, now: number): Pace;
|
|
45
|
+
/** Whether quota reads as remaining or as consumed. Display only; routing reads `usedPercent`. */
|
|
46
|
+
export type QuotaDisplay = 'left' | 'used';
|
|
47
|
+
/**
|
|
48
|
+
* The percent a meter shows.
|
|
49
|
+
* @param window - the window to show.
|
|
50
|
+
* @param display - left or used.
|
|
51
|
+
* @returns an integer percent, or undefined when the provider reported none.
|
|
52
|
+
*/
|
|
53
|
+
export declare function displayPercent(window: UsageWindow, display: QuotaDisplay): number | undefined;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Below this share of elapsed window a burst says little about the whole window. */
|
|
2
|
+
export const MIN_FORECAST_ELAPSED = 0.2;
|
|
3
|
+
/** Below this usage the projection is noise. */
|
|
4
|
+
export const MIN_FORECAST_USED_PERCENT = 3;
|
|
5
|
+
/** Projected use above this percent turns a window `tight`. */
|
|
6
|
+
export const TIGHT_PROJECTED_PERCENT = 85;
|
|
7
|
+
/**
|
|
8
|
+
* Compute the pace of one window at `now`.
|
|
9
|
+
* @param window - the provider-reported window.
|
|
10
|
+
* @param now - epoch milliseconds.
|
|
11
|
+
* @returns the pace; `elapsed` is the share of the window passed, 0 to 1.
|
|
12
|
+
*/
|
|
13
|
+
export function pace(window, now) {
|
|
14
|
+
const used = window.usedPercent;
|
|
15
|
+
if (!Number.isFinite(used))
|
|
16
|
+
return { state: 'unknown' };
|
|
17
|
+
const usedPercent = Math.max(0, Math.min(100, used));
|
|
18
|
+
if (usedPercent >= 100)
|
|
19
|
+
return { state: 'out', usedPercent };
|
|
20
|
+
const { durationMs, resetsAt } = window;
|
|
21
|
+
if (durationMs === undefined || resetsAt === undefined || !(durationMs > 0) || !Number.isFinite(resetsAt)) {
|
|
22
|
+
return { state: 'calm', usedPercent };
|
|
23
|
+
}
|
|
24
|
+
const elapsed = Math.min(1, Math.max(0, 1 - (resetsAt - now) / durationMs));
|
|
25
|
+
if (elapsed < MIN_FORECAST_ELAPSED || usedPercent < MIN_FORECAST_USED_PERCENT) {
|
|
26
|
+
return { state: 'calm', usedPercent, elapsed };
|
|
27
|
+
}
|
|
28
|
+
const projectedPercent = usedPercent / elapsed;
|
|
29
|
+
if (projectedPercent > 100) {
|
|
30
|
+
const ratePerMs = usedPercent / (elapsed * durationMs);
|
|
31
|
+
return { state: 'over', usedPercent, elapsed, projectedPercent, emptyAt: now + (100 - usedPercent) / ratePerMs };
|
|
32
|
+
}
|
|
33
|
+
if (projectedPercent > TIGHT_PROJECTED_PERCENT)
|
|
34
|
+
return { state: 'tight', usedPercent, elapsed, projectedPercent };
|
|
35
|
+
return { state: 'calm', usedPercent, elapsed, projectedPercent };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The percent a meter shows.
|
|
39
|
+
* @param window - the window to show.
|
|
40
|
+
* @param display - left or used.
|
|
41
|
+
* @returns an integer percent, or undefined when the provider reported none.
|
|
42
|
+
*/
|
|
43
|
+
export function displayPercent(window, display) {
|
|
44
|
+
if (!Number.isFinite(window.usedPercent))
|
|
45
|
+
return undefined;
|
|
46
|
+
const used = Math.round(Math.max(0, Math.min(100, window.usedPercent)));
|
|
47
|
+
return display === 'left' ? 100 - used : used;
|
|
48
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display-side usage reads with a refresh policy per reason, live ingestion
|
|
3
|
+
* from model replies, and the account that served each session.
|
|
4
|
+
*
|
|
5
|
+
* Routing and display share one {@link PoolUsageTracker}, so its negative
|
|
6
|
+
* cache guards the provider usage endpoints for both.
|
|
7
|
+
*/
|
|
8
|
+
import type { UsageProviderId } from '../usage-providers/index.js';
|
|
9
|
+
import type { ProviderUsage } from '../providers/common.js';
|
|
10
|
+
import type { PoolUsageTracker } from '../providers/pool-usage.js';
|
|
11
|
+
/** Why the Client is reading usage; each reason has its own staleness bound. */
|
|
12
|
+
export type UsageReadReason = 'poll' | 'open' | 'manual';
|
|
13
|
+
/** Opening the popover refetches only snapshots older than this. */
|
|
14
|
+
export declare const OPEN_REFRESH_AFTER_MS = 60000;
|
|
15
|
+
/** A manual refresh of one account fetches at most once in this period. */
|
|
16
|
+
export declare const MANUAL_REFRESH_COOLDOWN_MS = 30000;
|
|
17
|
+
/** A usage snapshot as the Client sees it. */
|
|
18
|
+
export type DisplayUsage = ProviderUsage & {
|
|
19
|
+
/** `live` when the latest data came from a model reply rather than the usage endpoint. */
|
|
20
|
+
source?: 'live' | 'endpoint';
|
|
21
|
+
/** Epoch ms before which another manual refresh returns the cached snapshot. */
|
|
22
|
+
manualReadyAt?: number;
|
|
23
|
+
};
|
|
24
|
+
/** The account that served a request, and when. */
|
|
25
|
+
export interface ServedAccount {
|
|
26
|
+
account: string;
|
|
27
|
+
at: number;
|
|
28
|
+
}
|
|
29
|
+
export interface UsageServiceOptions {
|
|
30
|
+
tracker: PoolUsageTracker;
|
|
31
|
+
/** Whether a provider has a usage endpoint at all. */
|
|
32
|
+
hasEndpoint: (provider: UsageProviderId) => boolean;
|
|
33
|
+
/** Runs before a manual refresh fetches, so a stale routing cooldown does not outlive the user's retry. */
|
|
34
|
+
onManualRefresh?: (provider: UsageProviderId, account: string) => void;
|
|
35
|
+
now?: () => number;
|
|
36
|
+
}
|
|
37
|
+
/** Usage reads and live ingestion for the Client surfaces. */
|
|
38
|
+
export declare class UsageService {
|
|
39
|
+
private readonly options;
|
|
40
|
+
private readonly lastManual;
|
|
41
|
+
private readonly live;
|
|
42
|
+
private readonly servedBySession;
|
|
43
|
+
private readonly servedByProvider;
|
|
44
|
+
private readonly now;
|
|
45
|
+
/** Bumped whenever a snapshot or serving account changes, so the Client knows to re-read. */
|
|
46
|
+
revision: number;
|
|
47
|
+
constructor(options: UsageServiceOptions);
|
|
48
|
+
/**
|
|
49
|
+
* Read one account's usage under the policy for `reason`.
|
|
50
|
+
* A failure cooldown is never bypassed (the tracker answers from its negative cache).
|
|
51
|
+
*/
|
|
52
|
+
read(provider: UsageProviderId, account: string, reason: UsageReadReason): Promise<DisplayUsage>;
|
|
53
|
+
/**
|
|
54
|
+
* Record one model reply: its usage headers and the account that served it.
|
|
55
|
+
* @returns whether usage changed.
|
|
56
|
+
*/
|
|
57
|
+
observe(provider: UsageProviderId, account: string, sessionId: string | undefined, headers: Headers): boolean;
|
|
58
|
+
/** The account that last served `provider`: in this session when known, else anywhere. */
|
|
59
|
+
servedAccount(provider: UsageProviderId, sessionId?: string): ServedAccount | undefined;
|
|
60
|
+
/** Forget an account after logout. */
|
|
61
|
+
forget(provider: UsageProviderId, account?: string): void;
|
|
62
|
+
private ageOf;
|
|
63
|
+
private decorate;
|
|
64
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { usageWindowsFromHeaders } from './headers.js';
|
|
2
|
+
/** Opening the popover refetches only snapshots older than this. */
|
|
3
|
+
export const OPEN_REFRESH_AFTER_MS = 60_000;
|
|
4
|
+
/** A manual refresh of one account fetches at most once in this period. */
|
|
5
|
+
export const MANUAL_REFRESH_COOLDOWN_MS = 30_000;
|
|
6
|
+
/** Usage reads and live ingestion for the Client surfaces. */
|
|
7
|
+
export class UsageService {
|
|
8
|
+
options;
|
|
9
|
+
lastManual = new Map();
|
|
10
|
+
live = new Set();
|
|
11
|
+
servedBySession = new Map();
|
|
12
|
+
servedByProvider = new Map();
|
|
13
|
+
now;
|
|
14
|
+
/** Bumped whenever a snapshot or serving account changes, so the Client knows to re-read. */
|
|
15
|
+
revision = 0;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.options = options;
|
|
18
|
+
this.now = options.now ?? Date.now;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Read one account's usage under the policy for `reason`.
|
|
22
|
+
* A failure cooldown is never bypassed (the tracker answers from its negative cache).
|
|
23
|
+
*/
|
|
24
|
+
async read(provider, account, reason) {
|
|
25
|
+
if (!this.options.hasEndpoint(provider))
|
|
26
|
+
return { supported: false };
|
|
27
|
+
const key = `${provider}/${account}`;
|
|
28
|
+
const age = this.ageOf(provider, account);
|
|
29
|
+
let force = false;
|
|
30
|
+
if (reason === 'open')
|
|
31
|
+
force = age === undefined || age > OPEN_REFRESH_AFTER_MS;
|
|
32
|
+
if (reason === 'manual') {
|
|
33
|
+
const last = this.lastManual.get(key);
|
|
34
|
+
if (last === undefined || this.now() - last >= MANUAL_REFRESH_COOLDOWN_MS) {
|
|
35
|
+
force = true;
|
|
36
|
+
this.lastManual.set(key, this.now());
|
|
37
|
+
this.options.onManualRefresh?.(provider, account);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const before = this.options.tracker.fetchedAt(provider, account);
|
|
41
|
+
const snapshot = await this.options.tracker.displaySnapshotFor(provider, account, force);
|
|
42
|
+
const after = this.options.tracker.fetchedAt(provider, account);
|
|
43
|
+
if (after !== before) {
|
|
44
|
+
this.live.delete(key);
|
|
45
|
+
this.revision++;
|
|
46
|
+
}
|
|
47
|
+
return this.decorate(provider, account, snapshot);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Record one model reply: its usage headers and the account that served it.
|
|
51
|
+
* @returns whether usage changed.
|
|
52
|
+
*/
|
|
53
|
+
observe(provider, account, sessionId, headers) {
|
|
54
|
+
const at = this.now();
|
|
55
|
+
const served = { account, at };
|
|
56
|
+
this.servedByProvider.set(provider, served);
|
|
57
|
+
if (sessionId !== undefined) {
|
|
58
|
+
const bySession = this.servedBySession.get(sessionId) ?? new Map();
|
|
59
|
+
bySession.set(provider, served);
|
|
60
|
+
this.servedBySession.set(sessionId, bySession);
|
|
61
|
+
}
|
|
62
|
+
this.revision++;
|
|
63
|
+
if (provider !== 'codex' && provider !== 'claude')
|
|
64
|
+
return false;
|
|
65
|
+
const windows = usageWindowsFromHeaders(provider, headers);
|
|
66
|
+
if (this.options.tracker.ingest(provider, account, windows, at) === undefined)
|
|
67
|
+
return false;
|
|
68
|
+
this.live.add(`${provider}/${account}`);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
/** The account that last served `provider`: in this session when known, else anywhere. */
|
|
72
|
+
servedAccount(provider, sessionId) {
|
|
73
|
+
return (sessionId === undefined ? undefined : this.servedBySession.get(sessionId)?.get(provider))
|
|
74
|
+
?? this.servedByProvider.get(provider);
|
|
75
|
+
}
|
|
76
|
+
/** Forget an account after logout. */
|
|
77
|
+
forget(provider, account) {
|
|
78
|
+
for (const key of [...this.live])
|
|
79
|
+
if (key === `${provider}/${account}` || (account === undefined && key.startsWith(`${provider}/`)))
|
|
80
|
+
this.live.delete(key);
|
|
81
|
+
const served = this.servedByProvider.get(provider);
|
|
82
|
+
if (served !== undefined && (account === undefined || served.account === account))
|
|
83
|
+
this.servedByProvider.delete(provider);
|
|
84
|
+
for (const bySession of this.servedBySession.values()) {
|
|
85
|
+
const entry = bySession.get(provider);
|
|
86
|
+
if (entry !== undefined && (account === undefined || entry.account === account))
|
|
87
|
+
bySession.delete(provider);
|
|
88
|
+
}
|
|
89
|
+
this.revision++;
|
|
90
|
+
}
|
|
91
|
+
ageOf(provider, account) {
|
|
92
|
+
const at = this.options.tracker.fetchedAt(provider, account);
|
|
93
|
+
return at === undefined || !Number.isFinite(at) ? undefined : this.now() - at;
|
|
94
|
+
}
|
|
95
|
+
decorate(provider, account, snapshot) {
|
|
96
|
+
const key = `${provider}/${account}`;
|
|
97
|
+
const last = this.lastManual.get(key);
|
|
98
|
+
return {
|
|
99
|
+
...snapshot,
|
|
100
|
+
source: this.live.has(key) ? 'live' : 'endpoint',
|
|
101
|
+
...last === undefined ? {} : { manualReadyAt: last + MANUAL_REFRESH_COOLDOWN_MS },
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Helpers shared by the API key usage providers. */
|
|
2
|
+
import type { FetchFn } from '../providers/common.js';
|
|
3
|
+
import type { ProviderUsage } from '../providers/common.js';
|
|
4
|
+
export declare const isRecord: (value: unknown) => value is Record<string, unknown>;
|
|
5
|
+
export declare const finite: (value: unknown) => number | undefined;
|
|
6
|
+
/** An epoch in seconds or milliseconds, or an ISO date string, as epoch milliseconds. */
|
|
7
|
+
export declare function timestamp(value: unknown): number | undefined;
|
|
8
|
+
export declare const clampPercent: (value: number) => number;
|
|
9
|
+
/** One key provider: how to read usage with an API key. */
|
|
10
|
+
export interface KeyProvider {
|
|
11
|
+
id: KeyProviderId;
|
|
12
|
+
name: string;
|
|
13
|
+
/** @throws with a message that never contains the key. */
|
|
14
|
+
fetchUsage(key: string, fetchFn: FetchFn, signal: AbortSignal): Promise<ProviderUsage>;
|
|
15
|
+
}
|
|
16
|
+
/** Ids of the providers that read usage with a pasted API key. */
|
|
17
|
+
export declare const KEY_PROVIDER_IDS: readonly ["zai", "ollama-cloud", "kimi"];
|
|
18
|
+
export type KeyProviderId = (typeof KEY_PROVIDER_IDS)[number];
|
|
19
|
+
export declare const isKeyProviderId: (value: unknown) => value is KeyProviderId;
|
|
20
|
+
export declare class KeyRejectedError extends Error {
|
|
21
|
+
}
|
|
22
|
+
/** GET or POST one JSON endpoint with a bearer key. */
|
|
23
|
+
export declare function bearerJson(name: string, url: string, key: string, fetchFn: FetchFn, signal: AbortSignal, method?: 'GET' | 'POST'): Promise<unknown>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
2
|
+
export const finite = (value) => typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
3
|
+
/** An epoch in seconds or milliseconds, or an ISO date string, as epoch milliseconds. */
|
|
4
|
+
export function timestamp(value) {
|
|
5
|
+
const number = finite(value);
|
|
6
|
+
if (number !== undefined)
|
|
7
|
+
return number < 1_000_000_000_000 ? number * 1000 : number;
|
|
8
|
+
if (typeof value === 'string') {
|
|
9
|
+
const parsed = Date.parse(value);
|
|
10
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
export const clampPercent = (value) => Math.min(100, Math.max(0, value));
|
|
15
|
+
/** Ids of the providers that read usage with a pasted API key. */
|
|
16
|
+
export const KEY_PROVIDER_IDS = ['zai', 'ollama-cloud', 'kimi'];
|
|
17
|
+
export const isKeyProviderId = (value) => typeof value === 'string' && KEY_PROVIDER_IDS.includes(value);
|
|
18
|
+
export class KeyRejectedError extends Error {
|
|
19
|
+
}
|
|
20
|
+
/** GET or POST one JSON endpoint with a bearer key. */
|
|
21
|
+
export async function bearerJson(name, url, key, fetchFn, signal, method = 'GET') {
|
|
22
|
+
const response = await fetchFn(url, { method, headers: { authorization: `Bearer ${key}`, accept: 'application/json' }, signal });
|
|
23
|
+
if (response.status === 401 || response.status === 403)
|
|
24
|
+
throw new KeyRejectedError(`${name} rejected the key (HTTP ${response.status})`);
|
|
25
|
+
if (!response.ok)
|
|
26
|
+
throw new Error(`${name} returned HTTP ${response.status}`);
|
|
27
|
+
return response.json();
|
|
28
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage providers read with a pasted API key. They are separate from the
|
|
3
|
+
* OAuth `ProviderId` set, which drives login, model routing and the pool.
|
|
4
|
+
*/
|
|
5
|
+
import type { FetchFn, ProviderUsage } from '../providers/common.js';
|
|
6
|
+
import { isKeyProviderId, KEY_PROVIDER_IDS } from './common.js';
|
|
7
|
+
import type { KeyProviderId } from './common.js';
|
|
8
|
+
import { KeyStore } from './keys.js';
|
|
9
|
+
import type { ProviderId } from '../auth/store.js';
|
|
10
|
+
export { isKeyProviderId, KEY_PROVIDER_IDS };
|
|
11
|
+
/** Anything the Usage page can read: an OAuth provider or a key provider. */
|
|
12
|
+
export type UsageProviderId = ProviderId | KeyProviderId;
|
|
13
|
+
export type { KeyProviderId };
|
|
14
|
+
/** The only account a key provider has. */
|
|
15
|
+
export declare const KEY_ACCOUNT = "key";
|
|
16
|
+
/** One key provider as `status` reports it. */
|
|
17
|
+
export interface KeyProviderView {
|
|
18
|
+
id: KeyProviderId;
|
|
19
|
+
name: string;
|
|
20
|
+
configured: boolean;
|
|
21
|
+
/** Last four characters of the key, for recognition. */
|
|
22
|
+
tail?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare class KeyUsage {
|
|
25
|
+
private readonly store;
|
|
26
|
+
private readonly fetchFn;
|
|
27
|
+
private readonly onChanged;
|
|
28
|
+
/** Providers with a stored key; kept in memory so the tracker can look up a fetcher synchronously. */
|
|
29
|
+
private readonly configured;
|
|
30
|
+
constructor(store: KeyStore, fetchFn: FetchFn, onChanged: (id: KeyProviderId) => void);
|
|
31
|
+
/** Load the stored keys once at startup. */
|
|
32
|
+
load(): Promise<void>;
|
|
33
|
+
has(id: KeyProviderId): boolean;
|
|
34
|
+
list(): Promise<KeyProviderView[]>;
|
|
35
|
+
/** Read usage with the stored key; the tracker polls this. */
|
|
36
|
+
fetchStored(id: KeyProviderId): Promise<ProviderUsage>;
|
|
37
|
+
/** Check the key with one usage read, then store it. A bad key stores nothing. */
|
|
38
|
+
set(id: KeyProviderId, key: string): Promise<void>;
|
|
39
|
+
remove(id: KeyProviderId): Promise<void>;
|
|
40
|
+
private fetch;
|
|
41
|
+
}
|
|
42
|
+
export { KeyStore };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { isKeyProviderId, KEY_PROVIDER_IDS } from './common.js';
|
|
2
|
+
import { KeyStore } from './keys.js';
|
|
3
|
+
import { kimi } from './kimi.js';
|
|
4
|
+
import { ollamaCloud } from './ollama-cloud.js';
|
|
5
|
+
import { zai } from './zai.js';
|
|
6
|
+
export { isKeyProviderId, KEY_PROVIDER_IDS };
|
|
7
|
+
const PROVIDERS = { zai, 'ollama-cloud': ollamaCloud, kimi };
|
|
8
|
+
/** The only account a key provider has. */
|
|
9
|
+
export const KEY_ACCOUNT = 'key';
|
|
10
|
+
const KEY_TIMEOUT_MS = 20_000;
|
|
11
|
+
export class KeyUsage {
|
|
12
|
+
store;
|
|
13
|
+
fetchFn;
|
|
14
|
+
onChanged;
|
|
15
|
+
/** Providers with a stored key; kept in memory so the tracker can look up a fetcher synchronously. */
|
|
16
|
+
configured = new Set();
|
|
17
|
+
constructor(store, fetchFn, onChanged) {
|
|
18
|
+
this.store = store;
|
|
19
|
+
this.fetchFn = fetchFn;
|
|
20
|
+
this.onChanged = onChanged;
|
|
21
|
+
}
|
|
22
|
+
/** Load the stored keys once at startup. */
|
|
23
|
+
async load() {
|
|
24
|
+
this.configured.clear();
|
|
25
|
+
for (const id of Object.keys(await this.store.read()))
|
|
26
|
+
if (isKeyProviderId(id))
|
|
27
|
+
this.configured.add(id);
|
|
28
|
+
}
|
|
29
|
+
has(id) {
|
|
30
|
+
return this.configured.has(id);
|
|
31
|
+
}
|
|
32
|
+
async list() {
|
|
33
|
+
const keys = await this.store.read();
|
|
34
|
+
return KEY_PROVIDER_IDS.map(id => {
|
|
35
|
+
const key = keys[id];
|
|
36
|
+
return { id, name: PROVIDERS[id].name, configured: key !== undefined, ...key === undefined ? {} : { tail: key.slice(-4) } };
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** Read usage with the stored key; the tracker polls this. */
|
|
40
|
+
async fetchStored(id) {
|
|
41
|
+
const key = (await this.store.read())[id];
|
|
42
|
+
if (key === undefined)
|
|
43
|
+
throw new Error('No key is stored');
|
|
44
|
+
return this.fetch(id, key);
|
|
45
|
+
}
|
|
46
|
+
/** Check the key with one usage read, then store it. A bad key stores nothing. */
|
|
47
|
+
async set(id, key) {
|
|
48
|
+
const trimmed = key.trim();
|
|
49
|
+
if (trimmed === '')
|
|
50
|
+
throw new Error('The key is empty');
|
|
51
|
+
await this.fetch(id, trimmed);
|
|
52
|
+
await this.store.set(id, trimmed);
|
|
53
|
+
this.configured.add(id);
|
|
54
|
+
this.onChanged(id);
|
|
55
|
+
}
|
|
56
|
+
async remove(id) {
|
|
57
|
+
await this.store.remove(id);
|
|
58
|
+
this.configured.delete(id);
|
|
59
|
+
this.onChanged(id);
|
|
60
|
+
}
|
|
61
|
+
async fetch(id, key) {
|
|
62
|
+
try {
|
|
63
|
+
return await PROVIDERS[id].fetchUsage(key, this.fetchFn, AbortSignal.timeout(KEY_TIMEOUT_MS));
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
67
|
+
throw new Error(message.split(key).join('…'));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export { KeyStore };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { KeyProviderId } from './common.js';
|
|
2
|
+
export type KeyMap = Partial<Record<KeyProviderId, string>>;
|
|
3
|
+
export declare const defaultKeyPath: () => string;
|
|
4
|
+
export declare class KeyStore {
|
|
5
|
+
private readonly path;
|
|
6
|
+
constructor(path?: string);
|
|
7
|
+
read(): Promise<KeyMap>;
|
|
8
|
+
set(id: KeyProviderId, key: string): Promise<void>;
|
|
9
|
+
remove(id: KeyProviderId): Promise<void>;
|
|
10
|
+
private write;
|
|
11
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** The API key store at `~/.dsh/plugins/subscriptions/usage-keys.json`, owner-only. */
|
|
2
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
5
|
+
import { isKeyProviderId, isRecord } from './common.js';
|
|
6
|
+
export const defaultKeyPath = () => dshHomePath('plugins', 'subscriptions', 'usage-keys.json');
|
|
7
|
+
export class KeyStore {
|
|
8
|
+
path;
|
|
9
|
+
constructor(path = defaultKeyPath()) {
|
|
10
|
+
this.path = path;
|
|
11
|
+
}
|
|
12
|
+
async read() {
|
|
13
|
+
let text;
|
|
14
|
+
try {
|
|
15
|
+
text = await readFile(this.path, 'utf8');
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
|
|
19
|
+
return {};
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
const parsed = JSON.parse(text);
|
|
23
|
+
const keys = {};
|
|
24
|
+
if (isRecord(parsed)) {
|
|
25
|
+
for (const [id, value] of Object.entries(parsed)) {
|
|
26
|
+
if (isKeyProviderId(id) && typeof value === 'string' && value !== '')
|
|
27
|
+
keys[id] = value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return keys;
|
|
31
|
+
}
|
|
32
|
+
async set(id, key) {
|
|
33
|
+
await this.write({ ...await this.read(), [id]: key });
|
|
34
|
+
}
|
|
35
|
+
async remove(id) {
|
|
36
|
+
const { [id]: _removed, ...rest } = await this.read();
|
|
37
|
+
await this.write(rest);
|
|
38
|
+
}
|
|
39
|
+
async write(keys) {
|
|
40
|
+
await mkdir(dirname(this.path), { recursive: true });
|
|
41
|
+
const tmp = `${this.path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
42
|
+
try {
|
|
43
|
+
await writeFile(tmp, JSON.stringify(keys, null, 2), { mode: 0o600 });
|
|
44
|
+
await chmod(tmp, 0o600);
|
|
45
|
+
await rename(tmp, this.path);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
await rm(tmp, { force: true });
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|