dsh-subscriptions 0.2.0 → 0.2.3
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.md +220 -180
- package/lib/auth/claude-code-creds.js +31 -9
- package/lib/auth/device-flow.js +16 -9
- package/lib/auth/oauth-flow.js +13 -10
- package/lib/auth/rpc.d.ts +8 -4
- package/lib/auth/rpc.js +89 -25
- package/lib/auth/store.js +80 -31
- package/lib/client/CursorCard.d.ts +1 -1
- package/lib/client/CursorCard.js +50 -16
- package/lib/client/ExternalUsageCards.d.ts +1 -1
- package/lib/client/ExternalUsageCards.js +37 -16
- package/lib/client/ImageGallery.d.ts +2 -2
- package/lib/client/ImageGallery.js +92 -33
- package/lib/client/ImageGenerateToolview.js +50 -14
- package/lib/client/OllamaCard.d.ts +1 -1
- package/lib/client/OllamaCard.js +26 -8
- package/lib/client/OllamaSettingsManager.js +113 -30
- package/lib/client/ProviderAccountManager.d.ts +1 -1
- package/lib/client/ProviderAccountManager.js +164 -49
- package/lib/client/ProviderModelEditor.js +137 -34
- package/lib/client/SpeedSelect.d.ts +62 -14
- package/lib/client/SpeedSelect.js +261 -49
- package/lib/client/SubscriptionUsageBadge.d.ts +4 -10
- package/lib/client/SubscriptionUsageBadge.js +300 -112
- package/lib/client/SubscriptionsSection.d.ts +2 -0
- package/lib/client/SubscriptionsSection.js +265 -85
- package/lib/client/UsageBadgeDisplaySetting.js +35 -12
- package/lib/client/UsageMeter.d.ts +1 -1
- package/lib/client/UsageMeter.js +62 -24
- package/lib/client/VideoGenerateToolview.js +69 -17
- package/lib/client/account-preferences.d.ts +8 -0
- package/lib/client/account-preferences.js +20 -5
- package/lib/client/host-contract.d.ts +11 -2
- package/lib/client/host-contract.js +20 -3
- package/lib/client/index.d.ts +5 -5
- package/lib/client/index.js +34 -12
- package/lib/client/locales.d.ts +22 -0
- package/lib/client/locales.js +73 -51
- package/lib/client/model-filter.d.ts +37 -0
- package/lib/client/model-filter.js +45 -0
- package/lib/client/ollama-draft.js +21 -12
- package/lib/client/reset-action.js +6 -4
- package/lib/client/reset-credits-view.d.ts +1 -1
- package/lib/client/reset-credits-view.js +66 -17
- package/lib/client/subscription-card-styles.js +94 -24
- package/lib/client/usage-color-preferences.js +4 -2
- package/lib/client/usage-pace.js +30 -12
- package/lib/client/usage-window-filter.d.ts +7 -0
- package/lib/client/usage-window-filter.js +12 -0
- package/lib/client.js +480 -136
- package/lib/client.js.map +1 -1
- package/lib/http.js +1 -1
- package/lib/index.d.ts +3 -3
- package/lib/index.js +167 -76
- package/lib/model-defaults.js +4 -2
- package/lib/provider-settings.d.ts +17 -0
- package/lib/provider-settings.js +79 -16
- package/lib/providers/account-preferences.js +51 -22
- package/lib/providers/accounts.js +15 -9
- package/lib/providers/antigravity-oauth-client.js +2 -2
- package/lib/providers/antigravity.js +141 -68
- package/lib/providers/catalog-store.js +120 -41
- package/lib/providers/claude-cli-version.js +3 -3
- package/lib/providers/claude.d.ts +9 -2
- package/lib/providers/claude.js +153 -90
- package/lib/providers/codex-client-version.js +3 -3
- package/lib/providers/codex-search.js +40 -13
- package/lib/providers/codex.js +196 -112
- package/lib/providers/common.js +71 -39
- package/lib/providers/copilot.js +143 -82
- package/lib/providers/cursor-adapter.js +42 -18
- package/lib/providers/cursor-auth.d.ts +1 -1
- package/lib/providers/cursor-auth.js +69 -24
- package/lib/providers/cursor-model-settings.js +14 -5
- package/lib/providers/cursor-usage.js +27 -13
- package/lib/providers/external-usage-controller.js +14 -5
- package/lib/providers/external-usage.js +59 -23
- package/lib/providers/grok.js +152 -89
- package/lib/providers/image-pool.js +29 -11
- package/lib/providers/minimax-usage.js +31 -11
- package/lib/providers/npm-cli-version.js +10 -3
- package/lib/providers/ollama-settings.js +24 -8
- package/lib/providers/ollama-web.js +29 -11
- package/lib/providers/ollama.js +129 -66
- package/lib/providers/pool-family.js +11 -3
- package/lib/providers/pool-health.js +28 -8
- package/lib/providers/pool-usage.js +16 -9
- package/lib/providers/pool.d.ts +8 -0
- package/lib/providers/pool.js +81 -42
- package/lib/providers/rate-limit.js +8 -2
- package/lib/providers/replay.js +40 -17
- package/lib/providers/reset-redemption.js +18 -6
- package/lib/tools/image-generate.js +123 -65
- package/lib/tools/registration.js +5 -2
- package/lib/tools/video-generate.js +51 -31
- package/lib/tools/x-search.js +50 -22
- package/lib/translate/anthropic.js +85 -37
- package/lib/translate/antigravity-schema.js +71 -28
- package/lib/translate/antigravity-thinking.js +22 -9
- package/lib/translate/antigravity.js +151 -52
- package/lib/translate/chat-completions.js +67 -23
- package/lib/translate/resolved.js +55 -21
- package/lib/translate/responses.js +69 -29
- package/lib/translate/sse.js +4 -1
- package/package.json +27 -4
|
@@ -18,11 +18,20 @@ async function parseCredential(value) {
|
|
|
18
18
|
if (parsed === null || typeof parsed !== 'object')
|
|
19
19
|
throw new Error('Cursor credential is malformed');
|
|
20
20
|
const item = parsed;
|
|
21
|
-
if (item.type !== 'oauth' ||
|
|
22
|
-
|
|
21
|
+
if (item.type !== 'oauth' ||
|
|
22
|
+
typeof item.access !== 'string' ||
|
|
23
|
+
item.access.length === 0 ||
|
|
24
|
+
typeof item.refresh !== 'string' ||
|
|
25
|
+
typeof item.expires !== 'number' ||
|
|
26
|
+
!Number.isFinite(item.expires)) {
|
|
23
27
|
throw new Error('Cursor credential is malformed');
|
|
24
28
|
}
|
|
25
|
-
return {
|
|
29
|
+
return {
|
|
30
|
+
type: 'oauth',
|
|
31
|
+
access: item.access,
|
|
32
|
+
refresh: item.refresh,
|
|
33
|
+
expires: item.expires,
|
|
34
|
+
};
|
|
26
35
|
}
|
|
27
36
|
function tokenExpiry(token, now) {
|
|
28
37
|
try {
|
|
@@ -34,15 +43,23 @@ function tokenExpiry(token, now) {
|
|
|
34
43
|
if (typeof exp === 'number' && Number.isFinite(exp))
|
|
35
44
|
return exp * 1000;
|
|
36
45
|
}
|
|
37
|
-
catch {
|
|
46
|
+
catch {
|
|
47
|
+
/* JWTs without exp still receive a bounded lifetime. */
|
|
48
|
+
}
|
|
38
49
|
return now + 24 * 60 * 60_000;
|
|
39
50
|
}
|
|
40
51
|
function abortableDelay(ms, signal) {
|
|
41
52
|
if (signal.aborted)
|
|
42
53
|
return Promise.reject(signal.reason);
|
|
43
54
|
return new Promise((resolve, reject) => {
|
|
44
|
-
const timer = setTimeout(() => {
|
|
45
|
-
|
|
55
|
+
const timer = setTimeout(() => {
|
|
56
|
+
signal.removeEventListener('abort', aborted);
|
|
57
|
+
resolve();
|
|
58
|
+
}, ms);
|
|
59
|
+
function aborted() {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
reject(signal.reason);
|
|
62
|
+
}
|
|
46
63
|
signal.addEventListener('abort', aborted, { once: true });
|
|
47
64
|
});
|
|
48
65
|
}
|
|
@@ -70,7 +87,7 @@ export class CursorAuth {
|
|
|
70
87
|
...(this.error === undefined ? {} : { error: this.error }),
|
|
71
88
|
};
|
|
72
89
|
}
|
|
73
|
-
async accessToken({ signal } = {}) {
|
|
90
|
+
async accessToken({ signal, } = {}) {
|
|
74
91
|
signal?.throwIfAborted();
|
|
75
92
|
const current = await this.read();
|
|
76
93
|
if (current === undefined)
|
|
@@ -82,27 +99,39 @@ export class CursorAuth {
|
|
|
82
99
|
if (this.refreshTask === undefined) {
|
|
83
100
|
const pending = this.refresh(current, signal);
|
|
84
101
|
this.refreshTask = pending;
|
|
85
|
-
void pending
|
|
86
|
-
|
|
102
|
+
void pending
|
|
103
|
+
.finally(() => {
|
|
104
|
+
if (this.refreshTask === pending)
|
|
105
|
+
this.refreshTask = undefined;
|
|
106
|
+
})
|
|
107
|
+
.catch(() => undefined);
|
|
87
108
|
}
|
|
88
109
|
return (await this.refreshTask).access;
|
|
89
110
|
}
|
|
90
111
|
async refresh(current, signal) {
|
|
91
112
|
const response = await this.http(`${API_ORIGIN}/auth/exchange_user_api_key`, {
|
|
92
|
-
method: 'POST',
|
|
93
|
-
|
|
113
|
+
method: 'POST',
|
|
114
|
+
redirect: 'error',
|
|
115
|
+
headers: {
|
|
116
|
+
authorization: `Bearer ${current.refresh}`,
|
|
117
|
+
accept: 'application/json',
|
|
118
|
+
'content-type': 'application/json',
|
|
119
|
+
},
|
|
94
120
|
body: '{}',
|
|
95
121
|
...(signal === undefined ? {} : { signal }),
|
|
96
122
|
});
|
|
97
123
|
if (!response.ok)
|
|
98
124
|
throw new Error(`Cursor token refresh failed (HTTP ${response.status})`);
|
|
99
|
-
const body = await parseProviderJson(response, 'Cursor token refresh');
|
|
125
|
+
const body = (await parseProviderJson(response, 'Cursor token refresh'));
|
|
100
126
|
if (typeof body.accessToken !== 'string' || body.accessToken.length === 0) {
|
|
101
127
|
throw new Error('Cursor token refresh returned no access token');
|
|
102
128
|
}
|
|
103
129
|
const next = {
|
|
104
|
-
type: 'oauth',
|
|
105
|
-
|
|
130
|
+
type: 'oauth',
|
|
131
|
+
access: body.accessToken,
|
|
132
|
+
refresh: typeof body.refreshToken === 'string' && body.refreshToken.length > 0
|
|
133
|
+
? body.refreshToken
|
|
134
|
+
: current.refresh,
|
|
106
135
|
expires: tokenExpiry(body.accessToken, this.now()),
|
|
107
136
|
};
|
|
108
137
|
const latest = await this.read();
|
|
@@ -121,13 +150,22 @@ export class CursorAuth {
|
|
|
121
150
|
const challenge = createHash('sha256').update(verifier).digest('base64url');
|
|
122
151
|
const uuid = randomUUID();
|
|
123
152
|
const url = new URL('https://cursor.com/loginDeepControl');
|
|
124
|
-
url.search = new URLSearchParams({
|
|
153
|
+
url.search = new URLSearchParams({
|
|
154
|
+
challenge,
|
|
155
|
+
uuid,
|
|
156
|
+
mode: 'login',
|
|
157
|
+
redirectTarget: 'cli',
|
|
158
|
+
}).toString();
|
|
125
159
|
const abort = new AbortController();
|
|
126
160
|
const promise = this.pollLogin(uuid, verifier, abort.signal)
|
|
127
|
-
.catch(error => {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
161
|
+
.catch((error) => {
|
|
162
|
+
if (!abort.signal.aborted)
|
|
163
|
+
this.error = error instanceof Error ? error.message : String(error);
|
|
164
|
+
})
|
|
165
|
+
.finally(() => {
|
|
166
|
+
if (this.loginTask?.abort === abort)
|
|
167
|
+
this.loginTask = undefined;
|
|
168
|
+
});
|
|
131
169
|
this.loginTask = { abort, promise };
|
|
132
170
|
return { authorizeUrl: url.href };
|
|
133
171
|
}
|
|
@@ -136,23 +174,30 @@ export class CursorAuth {
|
|
|
136
174
|
for (let attempt = 0; attempt < 150; attempt++) {
|
|
137
175
|
await abortableDelay(delay, signal);
|
|
138
176
|
const url = `${API_ORIGIN}/auth/poll?uuid=${encodeURIComponent(uuid)}&verifier=${encodeURIComponent(verifier)}`;
|
|
139
|
-
const response = await this.http(url, {
|
|
177
|
+
const response = await this.http(url, {
|
|
178
|
+
headers: { accept: 'application/json' },
|
|
179
|
+
redirect: 'error',
|
|
180
|
+
signal,
|
|
181
|
+
});
|
|
140
182
|
if (response.status === 404) {
|
|
141
183
|
delay = Math.min(Math.ceil(delay * 1.2), 10_000);
|
|
142
184
|
continue;
|
|
143
185
|
}
|
|
144
186
|
if (!response.ok)
|
|
145
187
|
throw new Error(`Cursor login failed (HTTP ${response.status})`);
|
|
146
|
-
const body = await parseProviderJson(response, 'Cursor login poll');
|
|
188
|
+
const body = (await parseProviderJson(response, 'Cursor login poll'));
|
|
147
189
|
signal.throwIfAborted();
|
|
148
|
-
if (typeof body.accessToken !== 'string' ||
|
|
190
|
+
if (typeof body.accessToken !== 'string' ||
|
|
191
|
+
body.accessToken.length === 0) {
|
|
149
192
|
throw new Error('Cursor login returned no access token');
|
|
150
193
|
}
|
|
151
|
-
if (typeof body.refreshToken !== 'string' ||
|
|
194
|
+
if (typeof body.refreshToken !== 'string' ||
|
|
195
|
+
body.refreshToken.length === 0) {
|
|
152
196
|
throw new Error('Cursor login returned no refresh token');
|
|
153
197
|
}
|
|
154
198
|
const value = {
|
|
155
|
-
type: 'oauth',
|
|
199
|
+
type: 'oauth',
|
|
200
|
+
access: body.accessToken,
|
|
156
201
|
refresh: body.refreshToken,
|
|
157
202
|
expires: tokenExpiry(body.accessToken, this.now()),
|
|
158
203
|
};
|
|
@@ -9,7 +9,8 @@ export function validateCursorModelSettings(input) {
|
|
|
9
9
|
const models = input.visibleModels;
|
|
10
10
|
if (models === undefined)
|
|
11
11
|
return {};
|
|
12
|
-
if (!Array.isArray(models) ||
|
|
12
|
+
if (!Array.isArray(models) ||
|
|
13
|
+
models.some((id) => typeof id !== 'string' || !id.trim())) {
|
|
13
14
|
throw new Error('visibleModels must be an array of model ids');
|
|
14
15
|
}
|
|
15
16
|
return { visibleModels: [...new Set(models)] };
|
|
@@ -26,18 +27,26 @@ export class CursorModelSettingsStore {
|
|
|
26
27
|
}
|
|
27
28
|
catch (error) {
|
|
28
29
|
if (error.code !== 'ENOENT')
|
|
29
|
-
throw new Error(`Cannot read Cursor model settings at ${path}`, {
|
|
30
|
+
throw new Error(`Cannot read Cursor model settings at ${path}`, {
|
|
31
|
+
cause: error,
|
|
32
|
+
});
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
|
-
get() {
|
|
33
|
-
|
|
35
|
+
get() {
|
|
36
|
+
return structuredClone(this.current);
|
|
37
|
+
}
|
|
38
|
+
visibleModels() {
|
|
39
|
+
return this.current.visibleModels;
|
|
40
|
+
}
|
|
34
41
|
set(input) {
|
|
35
42
|
const validated = validateCursorModelSettings(input);
|
|
36
43
|
const run = this.writes.then(async () => {
|
|
37
44
|
await mkdir(dirname(this.path), { recursive: true });
|
|
38
45
|
const temporary = `${this.path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
39
46
|
try {
|
|
40
|
-
await writeFile(temporary, JSON.stringify(validated, null, 2), {
|
|
47
|
+
await writeFile(temporary, JSON.stringify(validated, null, 2), {
|
|
48
|
+
mode: 0o600,
|
|
49
|
+
});
|
|
41
50
|
await rename(temporary, this.path);
|
|
42
51
|
}
|
|
43
52
|
finally {
|
|
@@ -52,17 +52,21 @@ function legacyRequestWindow(body, resetsAt) {
|
|
|
52
52
|
if (usage === undefined)
|
|
53
53
|
return undefined;
|
|
54
54
|
const gpt4 = record(usage['gpt-4']);
|
|
55
|
-
const candidate = gpt4 !== undefined && number(gpt4.numRequests) !== undefined
|
|
56
|
-
|
|
57
|
-
.
|
|
58
|
-
|
|
55
|
+
const candidate = gpt4 !== undefined && number(gpt4.numRequests) !== undefined
|
|
56
|
+
? gpt4
|
|
57
|
+
: Object.values(usage)
|
|
58
|
+
.map(record)
|
|
59
|
+
.filter((value) => value !== undefined)
|
|
60
|
+
.sort((a, b) => (number(b.maxRequestUsage) ?? 0) -
|
|
61
|
+
(number(a.maxRequestUsage) ?? 0))[0];
|
|
59
62
|
const used = number(candidate?.numRequests);
|
|
60
63
|
const limit = number(candidate?.maxRequestUsage);
|
|
61
64
|
if (used === undefined || limit === undefined || limit <= 0)
|
|
62
65
|
return undefined;
|
|
63
66
|
return {
|
|
64
|
-
kind: 'other',
|
|
65
|
-
|
|
67
|
+
kind: 'other',
|
|
68
|
+
scope: 'Included requests',
|
|
69
|
+
usedPercent: Math.max(0, Math.min(100, (used / limit) * 100)),
|
|
66
70
|
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
67
71
|
};
|
|
68
72
|
}
|
|
@@ -79,18 +83,26 @@ export async function fetchCursorUsage(accessToken, http = fetch, signal) {
|
|
|
79
83
|
dashboardJson(`${ORIGIN}/api/usage-summary`, cookie, http, signal),
|
|
80
84
|
dashboardJson(`${ORIGIN}/api/usage?user=${encodeURIComponent(userId)}`, cookie, http, signal),
|
|
81
85
|
]);
|
|
82
|
-
if (summaryResult.status === 'rejected' &&
|
|
83
|
-
|
|
86
|
+
if (summaryResult.status === 'rejected' &&
|
|
87
|
+
legacyResult.status === 'rejected') {
|
|
88
|
+
throw summaryResult.reason instanceof Error
|
|
89
|
+
? summaryResult.reason
|
|
90
|
+
: new Error('Cursor usage is unavailable');
|
|
84
91
|
}
|
|
85
|
-
const summary = summaryResult.status === 'fulfilled'
|
|
86
|
-
|
|
92
|
+
const summary = summaryResult.status === 'fulfilled'
|
|
93
|
+
? record(summaryResult.value)
|
|
94
|
+
: undefined;
|
|
95
|
+
const plan = record(record(summary?.individualUsage)?.plan) ??
|
|
96
|
+
record(record(summary?.teamUsage)?.plan);
|
|
87
97
|
const resetValue = summary?.billingCycleEnd;
|
|
88
98
|
const parsedReset = typeof resetValue === 'string' ? Date.parse(resetValue) : NaN;
|
|
89
99
|
const resetsAt = Number.isFinite(parsedReset) ? parsedReset : undefined;
|
|
90
100
|
const startValue = summary?.billingCycleStart;
|
|
91
101
|
const parsedStart = typeof startValue === 'string' ? Date.parse(startValue) : NaN;
|
|
92
|
-
const timing = {
|
|
93
|
-
...(
|
|
102
|
+
const timing = {
|
|
103
|
+
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
104
|
+
...(Number.isFinite(parsedStart) ? { startsAt: parsedStart } : {}),
|
|
105
|
+
};
|
|
94
106
|
const windows = [];
|
|
95
107
|
for (const [field, scope] of [
|
|
96
108
|
['totalPercentUsed', 'Included'],
|
|
@@ -112,6 +124,8 @@ export async function fetchCursorUsage(accessToken, http = fetch, signal) {
|
|
|
112
124
|
return {
|
|
113
125
|
supported: true,
|
|
114
126
|
windows,
|
|
115
|
-
...(typeof membershipType === 'string' && membershipType.length > 0
|
|
127
|
+
...(typeof membershipType === 'string' && membershipType.length > 0
|
|
128
|
+
? { plan: membershipType }
|
|
129
|
+
: {}),
|
|
116
130
|
};
|
|
117
131
|
}
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { fetchMiniMaxUsage } from './minimax-usage.js';
|
|
2
|
-
import { fetchKimiCodeUsage, fetchOllamaUsage, fetchOpenCodeGoUsage } from './external-usage.js';
|
|
3
|
-
import { DEFAULT_OLLAMA_API_KEY_REF, DEFAULT_OLLAMA_BASE_URL } from './ollama.js';
|
|
4
|
-
export const EXTERNAL_USAGE_SOURCES = [
|
|
2
|
+
import { fetchKimiCodeUsage, fetchOllamaUsage, fetchOpenCodeGoUsage, } from './external-usage.js';
|
|
3
|
+
import { DEFAULT_OLLAMA_API_KEY_REF, DEFAULT_OLLAMA_BASE_URL, } from './ollama.js';
|
|
4
|
+
export const EXTERNAL_USAGE_SOURCES = [
|
|
5
|
+
'opencode-go',
|
|
6
|
+
'kimi-code',
|
|
7
|
+
'minimax',
|
|
8
|
+
'minimax-cn',
|
|
9
|
+
'ollama-cloud',
|
|
10
|
+
];
|
|
5
11
|
function valueOf(value) {
|
|
6
12
|
return typeof value === 'function' ? value() : value;
|
|
7
13
|
}
|
|
8
14
|
const DEFAULT_REFS = {
|
|
9
15
|
'opencode-go': 'OPENCODE_GO_API_KEY',
|
|
10
16
|
'kimi-code': 'KIMI_CODING_API_KEY',
|
|
11
|
-
|
|
17
|
+
minimax: 'MINIMAX_API_KEY',
|
|
12
18
|
'minimax-cn': 'MINIMAX_CN_API_KEY',
|
|
13
19
|
'ollama-cloud': DEFAULT_OLLAMA_API_KEY_REF,
|
|
14
20
|
};
|
|
@@ -28,7 +34,10 @@ export class ExternalUsageController {
|
|
|
28
34
|
const value = await this.resolveCredential(valueOf(this.refs[source]));
|
|
29
35
|
return typeof value?.value === 'string' && value.value.length > 0;
|
|
30
36
|
}));
|
|
31
|
-
return Object.fromEntries(EXTERNAL_USAGE_SOURCES.map((source, i) => [
|
|
37
|
+
return Object.fromEntries(EXTERNAL_USAGE_SOURCES.map((source, i) => [
|
|
38
|
+
source,
|
|
39
|
+
{ configured: values[i] ?? false },
|
|
40
|
+
]));
|
|
32
41
|
}
|
|
33
42
|
async usage(source, signal) {
|
|
34
43
|
// Snapshot the endpoint facts before the first await: a save that
|
|
@@ -24,7 +24,10 @@ function record(value) {
|
|
|
24
24
|
: undefined;
|
|
25
25
|
}
|
|
26
26
|
function percent(value) {
|
|
27
|
-
return typeof value === 'number' &&
|
|
27
|
+
return typeof value === 'number' &&
|
|
28
|
+
Number.isFinite(value) &&
|
|
29
|
+
value >= 0 &&
|
|
30
|
+
value <= 100
|
|
28
31
|
? value
|
|
29
32
|
: undefined;
|
|
30
33
|
}
|
|
@@ -71,8 +74,11 @@ export async function fetchOpenCodeGoUsage(apiKey, http = fetch, signal) {
|
|
|
71
74
|
...(scope === undefined ? {} : { scope }),
|
|
72
75
|
usedPercent,
|
|
73
76
|
// Go's rolling counter is first-use anchored, not per-request sliding.
|
|
74
|
-
...(field === 'rolling'
|
|
75
|
-
|
|
77
|
+
...(field === 'rolling'
|
|
78
|
+
? { fixedWindow: true, windowDurationMs: 18_000_000 }
|
|
79
|
+
: field === 'weekly'
|
|
80
|
+
? { fixedWindow: true, windowDurationMs: 604_800_000 }
|
|
81
|
+
: {}),
|
|
76
82
|
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
77
83
|
...monthlyDisplay,
|
|
78
84
|
});
|
|
@@ -90,8 +96,12 @@ export async function fetchKimiCodeUsage(apiKey, http = fetch, signal) {
|
|
|
90
96
|
// Only the top-level `usage` object is the summary the official CLI labels
|
|
91
97
|
// "Weekly limit"; an anonymous `limits[]` entry is "Limit #n" there.
|
|
92
98
|
const rows = [
|
|
93
|
-
...(record(body.usage)
|
|
94
|
-
|
|
99
|
+
...(record(body.usage)
|
|
100
|
+
? [{ raw: { detail: body.usage }, summary: true }]
|
|
101
|
+
: []),
|
|
102
|
+
...(Array.isArray(body.limits)
|
|
103
|
+
? body.limits.map((raw) => ({ raw, summary: false }))
|
|
104
|
+
: []),
|
|
95
105
|
];
|
|
96
106
|
for (const { raw, summary } of rows) {
|
|
97
107
|
const item = record(raw);
|
|
@@ -99,25 +109,39 @@ export async function fetchKimiCodeUsage(apiKey, http = fetch, signal) {
|
|
|
99
109
|
continue;
|
|
100
110
|
const detail = record(item.detail) ?? item;
|
|
101
111
|
const limit = numeric(detail.limit);
|
|
102
|
-
const used = numeric(detail.used) ??
|
|
103
|
-
|
|
112
|
+
const used = numeric(detail.used) ??
|
|
113
|
+
(limit !== undefined && numeric(detail.remaining) !== undefined
|
|
114
|
+
? limit - numeric(detail.remaining)
|
|
115
|
+
: undefined);
|
|
104
116
|
if (limit === undefined || limit <= 0 || used === undefined)
|
|
105
117
|
continue;
|
|
106
|
-
const usedPercent = percent(used / limit * 100);
|
|
118
|
+
const usedPercent = percent((used / limit) * 100);
|
|
107
119
|
if (usedPercent === undefined)
|
|
108
120
|
continue;
|
|
109
121
|
const window = record(item.window) ?? item;
|
|
110
122
|
const duration = numeric(window.duration ?? item.duration ?? detail.duration);
|
|
111
123
|
const unit = String(window.timeUnit ?? item.timeUnit ?? detail.timeUnit ?? '').toUpperCase();
|
|
112
|
-
const multiplier = unit.includes('MINUTE')
|
|
113
|
-
|
|
124
|
+
const multiplier = unit.includes('MINUTE')
|
|
125
|
+
? 60_000
|
|
126
|
+
: unit.includes('HOUR')
|
|
127
|
+
? 3_600_000
|
|
128
|
+
: unit.includes('DAY')
|
|
129
|
+
? 86_400_000
|
|
130
|
+
: unit.includes('SECOND')
|
|
131
|
+
? 1000
|
|
132
|
+
: undefined;
|
|
114
133
|
const durationMs = duration === undefined || duration <= 0 || multiplier === undefined
|
|
115
|
-
? undefined
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
134
|
+
? undefined
|
|
135
|
+
: duration * multiplier;
|
|
136
|
+
const kind = durationMs === 18_000_000
|
|
137
|
+
? 'session'
|
|
138
|
+
: durationMs === 604_800_000
|
|
139
|
+
? 'weekly'
|
|
140
|
+
: // No period fields at all: the official CLI labels the top-level
|
|
141
|
+
// summary the weekly limit, so use that kind rather than 'other'.
|
|
142
|
+
durationMs === undefined && unit === ''
|
|
143
|
+
? 'weekly'
|
|
144
|
+
: 'other';
|
|
121
145
|
const resetsAt = kimiReset(detail);
|
|
122
146
|
const scope = detail.name ?? detail.title ?? item.name ?? item.title;
|
|
123
147
|
// A vendor-declared span anchors the display marker at
|
|
@@ -133,7 +157,9 @@ export async function fetchKimiCodeUsage(apiKey, http = fetch, signal) {
|
|
|
133
157
|
const displayStart = resetsAt !== undefined && markerSpan !== undefined
|
|
134
158
|
? { displayStartsAt: Math.min(resetsAt - markerSpan, Date.now()) }
|
|
135
159
|
: {};
|
|
136
|
-
modern.push({
|
|
160
|
+
modern.push({
|
|
161
|
+
kind,
|
|
162
|
+
usedPercent,
|
|
137
163
|
...(typeof scope === 'string' ? { scope } : {}),
|
|
138
164
|
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
139
165
|
...(durationMs === undefined ? {} : { windowDurationMs: durationMs }),
|
|
@@ -156,7 +182,10 @@ export async function fetchKimiCodeUsage(apiKey, http = fetch, signal) {
|
|
|
156
182
|
]) {
|
|
157
183
|
const value = record(usages[field]);
|
|
158
184
|
const ratio = value?.used_ratio ?? value?.usedRatio;
|
|
159
|
-
if (typeof ratio !== 'number' ||
|
|
185
|
+
if (typeof ratio !== 'number' ||
|
|
186
|
+
!Number.isFinite(ratio) ||
|
|
187
|
+
ratio < 0 ||
|
|
188
|
+
ratio > 1)
|
|
160
189
|
continue;
|
|
161
190
|
const resetsAt = resetTime(value?.reset_time ?? value?.resetAt);
|
|
162
191
|
// The monthly total resets on the subscription's cycle, not the
|
|
@@ -169,7 +198,9 @@ export async function fetchKimiCodeUsage(apiKey, http = fetch, signal) {
|
|
|
169
198
|
kind,
|
|
170
199
|
...(scope === undefined ? {} : { scope }),
|
|
171
200
|
usedPercent: ratio * 100,
|
|
172
|
-
...(field === 'limit_7d'
|
|
201
|
+
...(field === 'limit_7d'
|
|
202
|
+
? { fixedWindow: true, windowDurationMs: 604_800_000 }
|
|
203
|
+
: {}),
|
|
173
204
|
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
174
205
|
...monthlyDisplay,
|
|
175
206
|
});
|
|
@@ -206,7 +237,10 @@ export async function fetchOllamaUsage(apiKey, baseURL, http = fetch, signal) {
|
|
|
206
237
|
const included = record(body?.included);
|
|
207
238
|
const allowance = numeric(included?.allowance_usd);
|
|
208
239
|
const balance = numeric(included?.balance_usd);
|
|
209
|
-
if (included === undefined ||
|
|
240
|
+
if (included === undefined ||
|
|
241
|
+
allowance === undefined ||
|
|
242
|
+
allowance <= 0 ||
|
|
243
|
+
balance === undefined) {
|
|
210
244
|
throw new Error('Ollama balance response has no usable included allowance');
|
|
211
245
|
}
|
|
212
246
|
const period = record(included.period);
|
|
@@ -214,14 +248,16 @@ export async function fetchOllamaUsage(apiKey, baseURL, http = fetch, signal) {
|
|
|
214
248
|
const startsAt = period === undefined ? undefined : resetTime(period.from);
|
|
215
249
|
return {
|
|
216
250
|
supported: true,
|
|
217
|
-
windows: [
|
|
251
|
+
windows: [
|
|
252
|
+
{
|
|
218
253
|
kind: 'other',
|
|
219
254
|
scope: 'Credits',
|
|
220
255
|
// An overdrawn balance stays visible instead of dropping the window.
|
|
221
|
-
usedPercent: (allowance - balance) / allowance * 100,
|
|
256
|
+
usedPercent: ((allowance - balance) / allowance) * 100,
|
|
222
257
|
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
223
258
|
...(startsAt === undefined ? {} : { startsAt }),
|
|
224
|
-
}
|
|
259
|
+
},
|
|
260
|
+
],
|
|
225
261
|
plan: 'Ollama Cloud',
|
|
226
262
|
};
|
|
227
263
|
}
|