dsh-subscriptions 0.1.5 → 0.2.2
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 +212 -154
- 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 +73 -2
- package/lib/auth/rpc.js +124 -23
- 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 +13 -0
- package/lib/client/OllamaCard.js +94 -0
- package/lib/client/OllamaSettingsManager.d.ts +24 -0
- package/lib/client/OllamaSettingsManager.js +329 -0
- package/lib/client/ProviderAccountManager.d.ts +1 -1
- package/lib/client/ProviderAccountManager.js +164 -49
- package/lib/client/ProviderModelEditor.js +109 -26
- package/lib/client/SpeedSelect.d.ts +62 -14
- package/lib/client/SpeedSelect.js +257 -46
- package/lib/client/SubscriptionUsageBadge.d.ts +5 -11
- package/lib/client/SubscriptionUsageBadge.js +301 -112
- package/lib/client/SubscriptionsSection.d.ts +2 -0
- package/lib/client/SubscriptionsSection.js +259 -85
- package/lib/client/UsageBadgeDisplaySetting.js +35 -12
- package/lib/client/UsageMeter.d.ts +1 -1
- package/lib/client/UsageMeter.js +69 -27
- 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 +78 -0
- package/lib/client/locales.js +121 -34
- package/lib/client/ollama-draft.d.ts +229 -0
- package/lib/client/ollama-draft.js +255 -0
- 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.d.ts +7 -0
- package/lib/client/usage-pace.js +43 -10
- package/lib/client.js +1647 -266
- package/lib/client.js.map +1 -1
- package/lib/http.js +1 -1
- package/lib/index.d.ts +28 -3
- package/lib/index.js +9329 -8217
- package/lib/model-defaults.js +4 -2
- package/lib/provider-settings.d.ts +14 -1
- package/lib/provider-settings.js +64 -20
- 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 +23 -3
- package/lib/providers/claude.js +167 -92
- 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.d.ts +14 -0
- package/lib/providers/common.js +118 -38
- 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.d.ts +9 -2
- package/lib/providers/external-usage-controller.js +30 -7
- package/lib/providers/external-usage.d.ts +8 -0
- package/lib/providers/external-usage.js +140 -20
- 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.d.ts +90 -0
- package/lib/providers/ollama-settings.js +213 -0
- package/lib/providers/ollama-web.d.ts +46 -0
- package/lib/providers/ollama-web.js +231 -0
- package/lib/providers/ollama.d.ts +107 -0
- package/lib/providers/ollama.js +608 -0
- 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 +141 -50
- 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 +26 -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,6 +1,6 @@
|
|
|
1
1
|
import type { ProviderUsage } from './common.js';
|
|
2
2
|
/** Usage-only sources whose model routes are supplied by the DSH base install. */
|
|
3
|
-
export type ExternalUsageSource = 'opencode-go' | 'kimi-code' | 'minimax' | 'minimax-cn';
|
|
3
|
+
export type ExternalUsageSource = 'opencode-go' | 'kimi-code' | 'minimax' | 'minimax-cn' | 'ollama-cloud';
|
|
4
4
|
export declare const EXTERNAL_USAGE_SOURCES: readonly ExternalUsageSource[];
|
|
5
5
|
export interface ExternalUsageStatus {
|
|
6
6
|
configured: boolean;
|
|
@@ -8,11 +8,18 @@ export interface ExternalUsageStatus {
|
|
|
8
8
|
type ResolveCredential = (name: string) => Promise<{
|
|
9
9
|
value: string;
|
|
10
10
|
} | undefined>;
|
|
11
|
+
/**
|
|
12
|
+
* A setting the controller reads per call. The function form keeps the Ollama
|
|
13
|
+
* reference and base URL live: the Settings card can change them after the
|
|
14
|
+
* controller was constructed.
|
|
15
|
+
*/
|
|
16
|
+
type LiveValue = string | (() => string);
|
|
11
17
|
export declare class ExternalUsageController {
|
|
12
18
|
private readonly resolveCredential;
|
|
13
19
|
private readonly http;
|
|
14
20
|
private readonly refs;
|
|
15
|
-
|
|
21
|
+
private readonly ollamaBaseURL;
|
|
22
|
+
constructor(resolveCredential: ResolveCredential, http?: typeof fetch, refs?: Partial<Record<ExternalUsageSource, LiveValue>>, ollamaBaseURL?: LiveValue);
|
|
16
23
|
status(): Promise<Record<ExternalUsageSource, ExternalUsageStatus>>;
|
|
17
24
|
usage(source: ExternalUsageSource, signal?: AbortSignal): Promise<ProviderUsage>;
|
|
18
25
|
}
|
|
@@ -1,35 +1,58 @@
|
|
|
1
1
|
import { fetchMiniMaxUsage } from './minimax-usage.js';
|
|
2
|
-
import { fetchKimiCodeUsage, fetchOpenCodeGoUsage } from './external-usage.js';
|
|
3
|
-
|
|
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
|
+
];
|
|
11
|
+
function valueOf(value) {
|
|
12
|
+
return typeof value === 'function' ? value() : value;
|
|
13
|
+
}
|
|
4
14
|
const DEFAULT_REFS = {
|
|
5
15
|
'opencode-go': 'OPENCODE_GO_API_KEY',
|
|
6
16
|
'kimi-code': 'KIMI_CODING_API_KEY',
|
|
7
|
-
|
|
17
|
+
minimax: 'MINIMAX_API_KEY',
|
|
8
18
|
'minimax-cn': 'MINIMAX_CN_API_KEY',
|
|
19
|
+
'ollama-cloud': DEFAULT_OLLAMA_API_KEY_REF,
|
|
9
20
|
};
|
|
10
21
|
export class ExternalUsageController {
|
|
11
22
|
resolveCredential;
|
|
12
23
|
http;
|
|
13
24
|
refs;
|
|
14
|
-
|
|
25
|
+
ollamaBaseURL;
|
|
26
|
+
constructor(resolveCredential, http = fetch, refs = {}, ollamaBaseURL = DEFAULT_OLLAMA_BASE_URL) {
|
|
15
27
|
this.resolveCredential = resolveCredential;
|
|
16
28
|
this.http = http;
|
|
17
29
|
this.refs = { ...DEFAULT_REFS, ...refs };
|
|
30
|
+
this.ollamaBaseURL = ollamaBaseURL;
|
|
18
31
|
}
|
|
19
32
|
async status() {
|
|
20
33
|
const values = await Promise.all(EXTERNAL_USAGE_SOURCES.map(async (source) => {
|
|
21
|
-
const value = await this.resolveCredential(this.refs[source]);
|
|
34
|
+
const value = await this.resolveCredential(valueOf(this.refs[source]));
|
|
22
35
|
return typeof value?.value === 'string' && value.value.length > 0;
|
|
23
36
|
}));
|
|
24
|
-
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
|
+
]));
|
|
25
41
|
}
|
|
26
42
|
async usage(source, signal) {
|
|
27
|
-
|
|
43
|
+
// Snapshot the endpoint facts before the first await: a save that
|
|
44
|
+
// switches the ref or host mid-flight must not send the old
|
|
45
|
+
// reference's key to the new host.
|
|
46
|
+
const ref = valueOf(this.refs[source]);
|
|
47
|
+
const ollamaBase = valueOf(this.ollamaBaseURL);
|
|
48
|
+
const credential = await this.resolveCredential(ref);
|
|
28
49
|
if (credential?.value === undefined || credential.value.length === 0) {
|
|
29
50
|
throw new Error(`${source} API key is not configured`);
|
|
30
51
|
}
|
|
31
52
|
if (source === 'minimax' || source === 'minimax-cn')
|
|
32
53
|
return fetchMiniMaxUsage(credential.value, source === 'minimax-cn' ? 'cn' : 'global', this.http, signal);
|
|
54
|
+
if (source === 'ollama-cloud')
|
|
55
|
+
return fetchOllamaUsage(credential.value, ollamaBase, this.http, signal);
|
|
33
56
|
return source === 'opencode-go'
|
|
34
57
|
? fetchOpenCodeGoUsage(credential.value, this.http, signal)
|
|
35
58
|
: fetchKimiCodeUsage(credential.value, this.http, signal);
|
|
@@ -5,4 +5,12 @@ type HttpFetch = typeof fetch;
|
|
|
5
5
|
export declare function fetchOpenCodeGoUsage(apiKey: string, http?: HttpFetch, signal?: AbortSignal): Promise<ProviderUsage>;
|
|
6
6
|
/** Read official CLI count windows and retain compatibility with older ratio pools. */
|
|
7
7
|
export declare function fetchKimiCodeUsage(apiKey: string, http?: HttpFetch, signal?: AbortSignal): Promise<ProviderUsage>;
|
|
8
|
+
/**
|
|
9
|
+
* Read Ollama Cloud quota from `GET {base}/balance`. The Cloud has no
|
|
10
|
+
* `limits` object: quota is the included allowance against its remaining
|
|
11
|
+
* balance (`included.allowance_usd`, `included.balance_usd`) over the
|
|
12
|
+
* reported period. A 404 means a self-hosted endpoint without the
|
|
13
|
+
* surface: not an error, the card renders an unsupported note instead.
|
|
14
|
+
*/
|
|
15
|
+
export declare function fetchOllamaUsage(apiKey: string, baseURL: string, http?: HttpFetch, signal?: AbortSignal): Promise<ProviderUsage>;
|
|
8
16
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Usage readers for subscriptions whose model adapters are built into DSH. */
|
|
2
|
-
import {
|
|
2
|
+
import { attributionHeaders } from '@deepseek-ai/dsh-llm';
|
|
3
|
+
import { parseProviderJson, readBoundedJson } from './common.js';
|
|
3
4
|
function numeric(value) {
|
|
4
5
|
const n = typeof value === 'string' && value.trim() !== '' ? Number(value) : value;
|
|
5
6
|
return typeof n === 'number' && Number.isFinite(n) ? n : undefined;
|
|
@@ -23,7 +24,10 @@ function record(value) {
|
|
|
23
24
|
: undefined;
|
|
24
25
|
}
|
|
25
26
|
function percent(value) {
|
|
26
|
-
return typeof value === 'number' &&
|
|
27
|
+
return typeof value === 'number' &&
|
|
28
|
+
Number.isFinite(value) &&
|
|
29
|
+
value >= 0 &&
|
|
30
|
+
value <= 100
|
|
27
31
|
? value
|
|
28
32
|
: undefined;
|
|
29
33
|
}
|
|
@@ -59,14 +63,24 @@ export async function fetchOpenCodeGoUsage(apiKey, http = fetch, signal) {
|
|
|
59
63
|
if (usedPercent === undefined)
|
|
60
64
|
continue;
|
|
61
65
|
const resetsAt = resetTime(value?.resetsAt);
|
|
66
|
+
// Monthly has no interval fields. Both key providers' monthly pools
|
|
67
|
+
// read the same display assumption: a 30-day span back from the
|
|
68
|
+
// reset, clamped to now. Only the marker may ride it — pace never.
|
|
69
|
+
const monthlyDisplay = field === 'monthly' && resetsAt !== undefined
|
|
70
|
+
? { displayStartsAt: Math.min(resetsAt - 30 * 86_400_000, Date.now()) }
|
|
71
|
+
: {};
|
|
62
72
|
windows.push({
|
|
63
73
|
kind,
|
|
64
74
|
...(scope === undefined ? {} : { scope }),
|
|
65
75
|
usedPercent,
|
|
66
76
|
// Go's rolling counter is first-use anchored, not per-request sliding.
|
|
67
|
-
...(field === 'rolling'
|
|
68
|
-
|
|
77
|
+
...(field === 'rolling'
|
|
78
|
+
? { fixedWindow: true, windowDurationMs: 18_000_000 }
|
|
79
|
+
: field === 'weekly'
|
|
80
|
+
? { fixedWindow: true, windowDurationMs: 604_800_000 }
|
|
81
|
+
: {}),
|
|
69
82
|
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
83
|
+
...monthlyDisplay,
|
|
70
84
|
});
|
|
71
85
|
}
|
|
72
86
|
if (windows.length === 0)
|
|
@@ -79,40 +93,80 @@ export async function fetchKimiCodeUsage(apiKey, http = fetch, signal) {
|
|
|
79
93
|
if (body === undefined)
|
|
80
94
|
throw new Error('Kimi Code usage response is not an object');
|
|
81
95
|
const modern = [];
|
|
96
|
+
// Only the top-level `usage` object is the summary the official CLI labels
|
|
97
|
+
// "Weekly limit"; an anonymous `limits[]` entry is "Limit #n" there.
|
|
82
98
|
const rows = [
|
|
83
|
-
...(record(body.usage)
|
|
84
|
-
|
|
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
|
+
: []),
|
|
85
105
|
];
|
|
86
|
-
for (const raw of rows) {
|
|
106
|
+
for (const { raw, summary } of rows) {
|
|
87
107
|
const item = record(raw);
|
|
88
108
|
if (!item)
|
|
89
109
|
continue;
|
|
90
110
|
const detail = record(item.detail) ?? item;
|
|
91
111
|
const limit = numeric(detail.limit);
|
|
92
|
-
const used = numeric(detail.used) ??
|
|
93
|
-
|
|
112
|
+
const used = numeric(detail.used) ??
|
|
113
|
+
(limit !== undefined && numeric(detail.remaining) !== undefined
|
|
114
|
+
? limit - numeric(detail.remaining)
|
|
115
|
+
: undefined);
|
|
94
116
|
if (limit === undefined || limit <= 0 || used === undefined)
|
|
95
117
|
continue;
|
|
96
|
-
const usedPercent = percent(used / limit * 100);
|
|
118
|
+
const usedPercent = percent((used / limit) * 100);
|
|
97
119
|
if (usedPercent === undefined)
|
|
98
120
|
continue;
|
|
99
121
|
const window = record(item.window) ?? item;
|
|
100
122
|
const duration = numeric(window.duration ?? item.duration ?? detail.duration);
|
|
101
123
|
const unit = String(window.timeUnit ?? item.timeUnit ?? detail.timeUnit ?? '').toUpperCase();
|
|
102
|
-
const multiplier = unit.includes('MINUTE')
|
|
103
|
-
|
|
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;
|
|
104
133
|
const durationMs = duration === undefined || duration <= 0 || multiplier === undefined
|
|
105
|
-
? undefined
|
|
106
|
-
|
|
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';
|
|
107
145
|
const resetsAt = kimiReset(detail);
|
|
108
146
|
const scope = detail.name ?? detail.title ?? item.name ?? item.title;
|
|
109
|
-
|
|
147
|
+
// A vendor-declared span anchors the display marker at
|
|
148
|
+
// min(reset − span, now); pace stays unverified (rolling semantics
|
|
149
|
+
// are not established), so the assumed instant rides displayStartsAt.
|
|
150
|
+
// The top-level summary without a usable period (absent, zero, or no
|
|
151
|
+
// unit) is read as the weekly limit (above), so its marker assumes seven
|
|
152
|
+
// days. The official CLI
|
|
153
|
+
// gives only the label, not a span; the span rests on a live account
|
|
154
|
+
// whose summary reset matched `usages.limit_7d` to within a second.
|
|
155
|
+
// An anonymous `limits[]` entry gets no such assumption. Display only.
|
|
156
|
+
const markerSpan = durationMs ?? (summary && kind === 'weekly' ? 604_800_000 : undefined);
|
|
157
|
+
const displayStart = resetsAt !== undefined && markerSpan !== undefined
|
|
158
|
+
? { displayStartsAt: Math.min(resetsAt - markerSpan, Date.now()) }
|
|
159
|
+
: {};
|
|
160
|
+
modern.push({
|
|
161
|
+
kind,
|
|
162
|
+
usedPercent,
|
|
110
163
|
...(typeof scope === 'string' ? { scope } : {}),
|
|
111
164
|
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
112
165
|
...(durationMs === undefined ? {} : { windowDurationMs: durationMs }),
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
...(
|
|
166
|
+
// A whole seven-day bucket reset, not a rolling span, only where the
|
|
167
|
+
// endpoint actually said 7 DAY.
|
|
168
|
+
...(durationMs === 604_800_000 ? { fixedWindow: true } : {}),
|
|
169
|
+
...displayStart,
|
|
116
170
|
});
|
|
117
171
|
}
|
|
118
172
|
if (modern.length)
|
|
@@ -128,16 +182,82 @@ export async function fetchKimiCodeUsage(apiKey, http = fetch, signal) {
|
|
|
128
182
|
]) {
|
|
129
183
|
const value = record(usages[field]);
|
|
130
184
|
const ratio = value?.used_ratio ?? value?.usedRatio;
|
|
131
|
-
if (typeof ratio !== 'number' ||
|
|
185
|
+
if (typeof ratio !== 'number' ||
|
|
186
|
+
!Number.isFinite(ratio) ||
|
|
187
|
+
ratio < 0 ||
|
|
188
|
+
ratio > 1)
|
|
132
189
|
continue;
|
|
133
190
|
const resetsAt = resetTime(value?.reset_time ?? value?.resetAt);
|
|
191
|
+
// The monthly total resets on the subscription's cycle, not the
|
|
192
|
+
// calendar month; the display marker assumes ~30 days back. It only
|
|
193
|
+
// moves the marker — pace never rides it.
|
|
194
|
+
const monthlyDisplay = field === 'limit_month_total' && resetsAt !== undefined
|
|
195
|
+
? { displayStartsAt: Math.min(resetsAt - 30 * 86_400_000, Date.now()) }
|
|
196
|
+
: {};
|
|
134
197
|
windows.push({
|
|
135
198
|
kind,
|
|
136
199
|
...(scope === undefined ? {} : { scope }),
|
|
137
200
|
usedPercent: ratio * 100,
|
|
138
|
-
...(field === 'limit_7d'
|
|
201
|
+
...(field === 'limit_7d'
|
|
202
|
+
? { fixedWindow: true, windowDurationMs: 604_800_000 }
|
|
203
|
+
: {}),
|
|
139
204
|
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
205
|
+
...monthlyDisplay,
|
|
140
206
|
});
|
|
141
207
|
}
|
|
142
208
|
return { supported: true, windows, plan: 'Kimi Code' };
|
|
143
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Read Ollama Cloud quota from `GET {base}/balance`. The Cloud has no
|
|
212
|
+
* `limits` object: quota is the included allowance against its remaining
|
|
213
|
+
* balance (`included.allowance_usd`, `included.balance_usd`) over the
|
|
214
|
+
* reported period. A 404 means a self-hosted endpoint without the
|
|
215
|
+
* surface: not an error, the card renders an unsupported note instead.
|
|
216
|
+
*/
|
|
217
|
+
export async function fetchOllamaUsage(apiKey, baseURL, http = fetch, signal) {
|
|
218
|
+
const combined = signal === undefined
|
|
219
|
+
? AbortSignal.timeout(15_000)
|
|
220
|
+
: AbortSignal.any([AbortSignal.timeout(15_000), signal]);
|
|
221
|
+
const response = await http(`${baseURL.replace(/\/+$/, '')}/balance`, {
|
|
222
|
+
headers: {
|
|
223
|
+
accept: 'application/json',
|
|
224
|
+
authorization: `Bearer ${apiKey}`,
|
|
225
|
+
...attributionHeaders(),
|
|
226
|
+
},
|
|
227
|
+
redirect: 'error',
|
|
228
|
+
signal: combined,
|
|
229
|
+
});
|
|
230
|
+
if (signal?.aborted)
|
|
231
|
+
throw signal.reason;
|
|
232
|
+
if (response.status === 404)
|
|
233
|
+
return { supported: false };
|
|
234
|
+
if (!response.ok)
|
|
235
|
+
throw new Error(`Usage endpoint returned HTTP ${response.status}`);
|
|
236
|
+
const body = record(await readBoundedJson(response, 'usage', 1 * 1024 * 1024));
|
|
237
|
+
const included = record(body?.included);
|
|
238
|
+
const allowance = numeric(included?.allowance_usd);
|
|
239
|
+
const balance = numeric(included?.balance_usd);
|
|
240
|
+
if (included === undefined ||
|
|
241
|
+
allowance === undefined ||
|
|
242
|
+
allowance <= 0 ||
|
|
243
|
+
balance === undefined) {
|
|
244
|
+
throw new Error('Ollama balance response has no usable included allowance');
|
|
245
|
+
}
|
|
246
|
+
const period = record(included.period);
|
|
247
|
+
const resetsAt = period === undefined ? undefined : resetTime(period.until);
|
|
248
|
+
const startsAt = period === undefined ? undefined : resetTime(period.from);
|
|
249
|
+
return {
|
|
250
|
+
supported: true,
|
|
251
|
+
windows: [
|
|
252
|
+
{
|
|
253
|
+
kind: 'other',
|
|
254
|
+
scope: 'Credits',
|
|
255
|
+
// An overdrawn balance stays visible instead of dropping the window.
|
|
256
|
+
usedPercent: ((allowance - balance) / allowance) * 100,
|
|
257
|
+
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
258
|
+
...(startsAt === undefined ? {} : { startsAt }),
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
plan: 'Ollama Cloud',
|
|
262
|
+
};
|
|
263
|
+
}
|