dsh-subscriptions 0.1.3 → 0.1.5
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 +64 -0
- package/lib/auth/device-flow.js +6 -3
- package/lib/auth/oauth-flow.js +41 -8
- package/lib/auth/rpc.d.ts +3 -1
- package/lib/auth/rpc.js +10 -1
- package/lib/client/ProviderAccountManager.js +34 -41
- package/lib/client/ProviderModelEditor.js +5 -9
- package/lib/client/SpeedSelect.js +8 -7
- package/lib/client/SubscriptionUsageBadge.js +6 -5
- package/lib/client/SubscriptionsSection.d.ts +21 -73
- package/lib/client/SubscriptionsSection.js +130 -104
- package/lib/client/UsageBadgeDisplaySetting.js +4 -8
- package/lib/client/host-contract.d.ts +1 -1
- package/lib/client/host-contract.js +22 -0
- package/lib/client/locales.d.ts +58 -0
- package/lib/client/locales.js +58 -0
- package/lib/client/provider-settings-styles.d.ts +3 -0
- package/lib/client/provider-settings-styles.js +206 -0
- package/lib/client/reset-action.d.ts +14 -0
- package/lib/client/reset-action.js +15 -0
- package/lib/client/reset-credits-view.d.ts +53 -0
- package/lib/client/reset-credits-view.js +103 -0
- package/lib/client.js +937 -310
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +9 -0
- package/lib/index.js +6058 -5298
- package/lib/model-defaults.d.ts +4 -5
- package/lib/model-defaults.js +18 -14
- package/lib/providers/account-preferences.js +3 -2
- package/lib/providers/antigravity.d.ts +0 -2
- package/lib/providers/antigravity.js +5 -9
- package/lib/providers/catalog-store.d.ts +3 -2
- package/lib/providers/catalog-store.js +57 -26
- package/lib/providers/claude.d.ts +22 -3
- package/lib/providers/claude.js +118 -20
- package/lib/providers/codex-search.js +5 -2
- package/lib/providers/codex.d.ts +15 -2
- package/lib/providers/codex.js +77 -9
- package/lib/providers/common.d.ts +38 -0
- package/lib/providers/common.js +44 -18
- package/lib/providers/copilot.d.ts +2 -1
- package/lib/providers/copilot.js +13 -6
- package/lib/providers/cursor-adapter.d.ts +35 -0
- package/lib/providers/cursor-adapter.js +245 -4
- package/lib/providers/cursor-auth.js +11 -4
- package/lib/providers/cursor-usage.js +2 -1
- package/lib/providers/external-usage.js +2 -1
- package/lib/providers/grok.d.ts +2 -2
- package/lib/providers/grok.js +16 -11
- package/lib/providers/minimax-usage.js +2 -1
- package/lib/providers/pool-health.d.ts +2 -0
- package/lib/providers/pool-health.js +9 -0
- package/lib/providers/pool-usage.d.ts +13 -1
- package/lib/providers/pool-usage.js +39 -7
- package/lib/providers/pool.d.ts +7 -5
- package/lib/providers/pool.js +89 -12
- package/lib/providers/rate-limit.d.ts +0 -14
- package/lib/providers/rate-limit.js +0 -26
- package/lib/providers/replay.d.ts +4 -0
- package/lib/providers/replay.js +50 -0
- package/lib/providers/reset-redemption.d.ts +51 -0
- package/lib/providers/reset-redemption.js +102 -0
- package/lib/tools/image-generate.js +2 -1
- package/lib/tools/video-generate.d.ts +0 -5
- package/lib/tools/video-generate.js +25 -14
- package/lib/tools/x-search.js +2 -2
- package/lib/translate/anthropic.d.ts +21 -2
- package/lib/translate/anthropic.js +35 -11
- package/lib/translate/antigravity.js +13 -6
- package/lib/translate/chat-completions.js +1 -1
- package/lib/translate/resolved.d.ts +24 -0
- package/lib/translate/resolved.js +66 -3
- package/lib/translate/responses.js +8 -6
- package/lib/translate/sse.js +8 -0
- package/package.json +1 -1
|
@@ -17,23 +17,264 @@ export function projectCursorMessages(messages) {
|
|
|
17
17
|
}
|
|
18
18
|
: message);
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Vendor `LlmError` codes whose whole message is a local literal (no response
|
|
22
|
+
* header, trailer, body or end-stream text). Provider-derived failures can only
|
|
23
|
+
* carry a `classifyCursorError` code or `CURSOR_PROTOCOL`, never these, so the
|
|
24
|
+
* code is a sound discriminator for them.
|
|
25
|
+
*/
|
|
26
|
+
const LOCAL_VENDOR_CODES = new Set(['UNSUPPORTED_OPTION', 'UNSUPPORTED_CONTENT', 'TOOL_LIMIT']);
|
|
27
|
+
/**
|
|
28
|
+
* Plain-error messages the vendor throws as fixed literals (`vendor/cursor/
|
|
29
|
+
* index.js`: `waitForResponse`, the request write, the tool-continuation write,
|
|
30
|
+
* the stream `close` handler, the frame reader, the stall watchdog and the
|
|
31
|
+
* settings validation). None
|
|
32
|
+
* interpolates response text, so they are matched by exact message, the same way
|
|
33
|
+
* the auth messages are. A provider that echoed one of these sentences would
|
|
34
|
+
* only make the user read a local literal.
|
|
35
|
+
*/
|
|
36
|
+
const LOCAL_VENDOR_MESSAGES = new Set([
|
|
37
|
+
'Cursor HTTP response timeout',
|
|
38
|
+
'Cursor HTTP stream closed before response',
|
|
39
|
+
'Cursor agent bridge closed before accepting the request',
|
|
40
|
+
'Cursor tool continuation bridge closed before accepting the result',
|
|
41
|
+
'Cursor sent an unreadable compressed frame',
|
|
42
|
+
'Cursor stream idle timeout',
|
|
43
|
+
// `resolveCursorSettings`, run at the top of every stream.
|
|
44
|
+
'cursor-subscription: maxToolRounds must be an integer between 1 and 1000',
|
|
45
|
+
'cursor-subscription: retryCount must be an integer between 0 and 10',
|
|
46
|
+
'cursor-subscription: retryIntervalMs must be an integer between 0 and 300000',
|
|
47
|
+
'cursor-subscription: retryHttpStatusCodes must contain only HTTP status integers from 400 to 599',
|
|
48
|
+
'cursor-subscription: retryHttpStatusCodes must not contain duplicates',
|
|
49
|
+
'cursor-subscription: replayHistoryEachStep must be a boolean',
|
|
50
|
+
]);
|
|
51
|
+
/**
|
|
52
|
+
* The same, for the two literals that embed a number the vendor computes: the
|
|
53
|
+
* response status (`Number(headers[':status'])`, so digits or `NaN`) and the
|
|
54
|
+
* configured progress timeout.
|
|
55
|
+
*/
|
|
56
|
+
const LOCAL_VENDOR_PATTERNS = [
|
|
57
|
+
/^Cursor agent returned HTTP \d{3}$/,
|
|
58
|
+
/^Cursor stream progress timeout: no content for \d+ms$/,
|
|
59
|
+
];
|
|
60
|
+
/**
|
|
61
|
+
* Operating-system network error codes. An `Error` carrying one of these on
|
|
62
|
+
* itself, or on its `cause` (Node wraps a refused connection in
|
|
63
|
+
* `ERR_HTTP2_STREAM_CANCEL`), was produced by the local socket layer, so its
|
|
64
|
+
* message is local: `connect ECONNREFUSED 127.0.0.1:443`, `getaddrinfo ENOTFOUND
|
|
65
|
+
* host`. This is a property of the thrown object, which a provider cannot
|
|
66
|
+
* forge. It is deliberately not a test on the vendor's `TRANSPORT` code: the
|
|
67
|
+
* vendor derives that code from message text, so a provider's end-stream
|
|
68
|
+
* message or `grpc-message` containing "connection" or "socket" gets it too.
|
|
69
|
+
* Transport failures with no such code (TLS and certificate errors, HTTP/2
|
|
70
|
+
* protocol errors) are not recognised and stay replaced.
|
|
71
|
+
*/
|
|
72
|
+
const LOCAL_SYSTEM_ERRNO = new Set([
|
|
73
|
+
'ECONNREFUSED', 'ECONNRESET', 'ECONNABORTED', 'ENOTFOUND', 'EAI_AGAIN', 'ETIMEDOUT',
|
|
74
|
+
'ENETUNREACH', 'ENETDOWN', 'EHOSTUNREACH', 'EHOSTDOWN', 'EPIPE',
|
|
75
|
+
]);
|
|
76
|
+
function errorCode(value) {
|
|
77
|
+
return typeof value === 'object' && value !== null ? value.code : undefined;
|
|
78
|
+
}
|
|
79
|
+
function isLocalSystemError(error) {
|
|
80
|
+
if (!(error instanceof Error))
|
|
81
|
+
return false;
|
|
82
|
+
const own = errorCode(error);
|
|
83
|
+
const cause = errorCode(error.cause);
|
|
84
|
+
return (typeof own === 'string' && LOCAL_SYSTEM_ERRNO.has(own))
|
|
85
|
+
|| (typeof cause === 'string' && LOCAL_SYSTEM_ERRNO.has(cause));
|
|
86
|
+
}
|
|
87
|
+
function messageOf(error) {
|
|
88
|
+
return error instanceof Error ? error.message : String(error);
|
|
89
|
+
}
|
|
90
|
+
/** Bound on remembered local failure messages; they are short and few. */
|
|
91
|
+
const MAX_LOCAL_MESSAGES = 64;
|
|
92
|
+
/**
|
|
93
|
+
* Remembers the messages of failures raised by this machine rather than read
|
|
94
|
+
* from the provider: whatever the auth service threw, whatever the host
|
|
95
|
+
* attachment store threw while reading an image, and a socket-layer network
|
|
96
|
+
* error seen on the run. The vendor stream turns each of these into
|
|
97
|
+
* `finish/error` carrying exactly `error.message`, so a failure whose message
|
|
98
|
+
* equals a remembered one originated locally and may keep its text. The match
|
|
99
|
+
* is by exact text: a provider that echoed a remembered message would only
|
|
100
|
+
* display a local literal.
|
|
101
|
+
*/
|
|
102
|
+
class LocalFailureMessages {
|
|
103
|
+
messages = new Set();
|
|
104
|
+
/** Wrap `auth` so its failures are remembered and rethrown unchanged. */
|
|
105
|
+
wrap(auth) {
|
|
106
|
+
return {
|
|
107
|
+
accessToken: async (options) => {
|
|
108
|
+
try {
|
|
109
|
+
return await auth.accessToken(options);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
this.remember(messageOf(error));
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Wrap the host attachment store the vendor reads images through, so a read
|
|
120
|
+
* failure keeps its own message. The store is a local service; its errors
|
|
121
|
+
* carry no provider text. Anything that is not a store passes through.
|
|
122
|
+
*/
|
|
123
|
+
wrapAttachments(resolve) {
|
|
124
|
+
return () => {
|
|
125
|
+
const store = resolve();
|
|
126
|
+
const readImage = store?.readImage;
|
|
127
|
+
if (typeof readImage !== 'function')
|
|
128
|
+
return store;
|
|
129
|
+
return Object.create(store, {
|
|
130
|
+
readImage: {
|
|
131
|
+
value: async (...args) => {
|
|
132
|
+
try {
|
|
133
|
+
return await readImage.apply(store, args);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
this.remember(messageOf(error));
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Observe a vendor run's two failure channels, the response promise and the
|
|
146
|
+
* frame reader, and remember the message of an error the socket layer raised.
|
|
147
|
+
* The run's behavior is unchanged: the same error is rethrown.
|
|
148
|
+
*/
|
|
149
|
+
observeRun(run) {
|
|
150
|
+
if (typeof run !== 'object' || run === null)
|
|
151
|
+
return run;
|
|
152
|
+
const target = run;
|
|
153
|
+
const observe = (error) => {
|
|
154
|
+
if (isLocalSystemError(error))
|
|
155
|
+
this.remember(messageOf(error));
|
|
156
|
+
throw error;
|
|
157
|
+
};
|
|
158
|
+
const wait = target.waitForResponse;
|
|
159
|
+
if (typeof wait === 'function') {
|
|
160
|
+
target.waitForResponse = (...args) => Promise.resolve(wait.apply(target, args)).catch(observe);
|
|
161
|
+
}
|
|
162
|
+
const frames = target.frames;
|
|
163
|
+
const next = frames?.next;
|
|
164
|
+
if (frames !== undefined && typeof next === 'function') {
|
|
165
|
+
frames.next = (...args) => Promise.resolve(next.apply(frames, args)).catch(observe);
|
|
166
|
+
}
|
|
167
|
+
return run;
|
|
168
|
+
}
|
|
169
|
+
has(message) {
|
|
170
|
+
return this.messages.has(message);
|
|
171
|
+
}
|
|
172
|
+
remember(message) {
|
|
173
|
+
// An empty message displays nothing; remembering it would exempt every
|
|
174
|
+
// empty failure from the boundary for no benefit.
|
|
175
|
+
if (message === '')
|
|
176
|
+
return;
|
|
177
|
+
this.messages.delete(message);
|
|
178
|
+
this.messages.add(message);
|
|
179
|
+
if (this.messages.size > MAX_LOCAL_MESSAGES) {
|
|
180
|
+
const oldest = this.messages.values().next().value;
|
|
181
|
+
if (oldest !== undefined)
|
|
182
|
+
this.messages.delete(oldest);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function isVendorLocalMessage(message) {
|
|
187
|
+
return LOCAL_VENDOR_MESSAGES.has(message) || LOCAL_VENDOR_PATTERNS.some(pattern => pattern.test(message));
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Cursor adapter for the DSH host.
|
|
191
|
+
*
|
|
192
|
+
* Failure text contract: `finish/error` messages that originate from the
|
|
193
|
+
* vendor stream and can quote provider-controlled text (the response content
|
|
194
|
+
* type, trailers, end-stream errors, a frame-reader or other error whose message
|
|
195
|
+
* is not one of the literals below, and a transport-coded failure that is not a
|
|
196
|
+
* socket-layer error) are replaced by `<code> [provider response text omitted]`. Failures that are
|
|
197
|
+
* wholly local keep their own message and code:
|
|
198
|
+
*
|
|
199
|
+
* - anything the auth service threw (not signed in, sign-in expired, token
|
|
200
|
+
* refresh status), and a host attachment read failure;
|
|
201
|
+
* - the vendor's own request validation, by code (`UNSUPPORTED_OPTION`,
|
|
202
|
+
* `UNSUPPORTED_CONTENT`, `TOOL_LIMIT`);
|
|
203
|
+
* - the vendor's fixed-literal plain errors, by exact message: the HTTP status
|
|
204
|
+
* (`Cursor agent returned HTTP 429`), the response, idle and progress
|
|
205
|
+
* timeouts, a closed bridge or stream, an unreadable compressed frame, and
|
|
206
|
+
* an invalid Cursor setting;
|
|
207
|
+
* - a network error raised by the operating system (`ECONNREFUSED`,
|
|
208
|
+
* `ENOTFOUND`, ...), recognised from the thrown error's own code, not from
|
|
209
|
+
* the vendor's classification.
|
|
210
|
+
*
|
|
211
|
+
* Not a provider-origin class: a network error such as `ECONNREFUSED` is a
|
|
212
|
+
* local failure (no network, daemon down, DNS), and is kept. What is still
|
|
213
|
+
* replaced although it is probably local is a transport failure with no
|
|
214
|
+
* socket-layer errno: a TLS or certificate failure and an HTTP/2 protocol
|
|
215
|
+
* error. The vendor's `TRANSPORT` code cannot decide those, because
|
|
216
|
+
* `classifyCursorError` derives it from message text (`network`, `connection`,
|
|
217
|
+
* `socket`, `fetch`, `ECONN`, `http2`) and a provider's end-stream message or
|
|
218
|
+
* `grpc-message` can contain any of them; so the code is never trusted alone.
|
|
219
|
+
* Telling these apart would need a check on the thrown error object, as the
|
|
220
|
+
* errno check above does, extended to the TLS and HTTP/2 error codes. That is
|
|
221
|
+
* not done here and is not verified.
|
|
222
|
+
*/
|
|
20
223
|
export class CursorCompatAdapter extends CursorAdapter {
|
|
21
224
|
visibleModels;
|
|
225
|
+
localFailures;
|
|
22
226
|
constructor(options) {
|
|
23
|
-
|
|
227
|
+
const localFailures = new LocalFailureMessages();
|
|
228
|
+
super({
|
|
229
|
+
...options,
|
|
230
|
+
auth: localFailures.wrap(options.auth),
|
|
231
|
+
...(options.resolveAttachments === undefined
|
|
232
|
+
? {}
|
|
233
|
+
: { resolveAttachments: localFailures.wrapAttachments(options.resolveAttachments) }),
|
|
234
|
+
});
|
|
235
|
+
this.localFailures = localFailures;
|
|
24
236
|
this.visibleModels = options.visibleModels;
|
|
237
|
+
// The vendor assigns `createAgentRun` (the injected one, or its own
|
|
238
|
+
// `AgentRun` factory) in its constructor but does not type it as a member.
|
|
239
|
+
const vendor = this;
|
|
240
|
+
const createRun = vendor.createAgentRun;
|
|
241
|
+
vendor.createAgentRun = access => localFailures.observeRun(createRun(access));
|
|
25
242
|
}
|
|
26
243
|
async listModels(provider) {
|
|
27
244
|
const models = await super.listModels(provider);
|
|
28
245
|
const visible = this.visibleModels?.();
|
|
29
246
|
return visible === undefined ? models : models.filter(model => visible.includes(model.id));
|
|
30
247
|
}
|
|
31
|
-
stream(options) {
|
|
32
|
-
|
|
248
|
+
async *stream(options) {
|
|
249
|
+
for await (const chunk of super.stream({
|
|
33
250
|
...options,
|
|
34
251
|
// Existing DSH sessions may still hold this retired selection.
|
|
35
252
|
model: options.model === 'composer-2' ? 'composer-2.5' : options.model,
|
|
36
253
|
messages: projectCursorMessages(options.messages),
|
|
37
|
-
})
|
|
254
|
+
})) {
|
|
255
|
+
if (chunk.type === 'finish' && chunk.reason.kind === 'error'
|
|
256
|
+
&& !LOCAL_VENDOR_CODES.has(chunk.reason.failure.code)
|
|
257
|
+
&& !isVendorLocalMessage(chunk.reason.failure.message)
|
|
258
|
+
&& !this.localFailures.has(chunk.reason.failure.message)) {
|
|
259
|
+
// The vendor uses raw headers, trailers and end-stream errors for protocol
|
|
260
|
+
// checks and classification before this boundary. Keep its code and retry
|
|
261
|
+
// decisions; do not reclassify the replacement text. "Safe" here means no
|
|
262
|
+
// provider-controlled free text is displayed in failures, not that no
|
|
263
|
+
// secret can exist anywhere internally.
|
|
264
|
+
yield {
|
|
265
|
+
...chunk,
|
|
266
|
+
reason: {
|
|
267
|
+
...chunk.reason,
|
|
268
|
+
failure: {
|
|
269
|
+
...chunk.reason.failure,
|
|
270
|
+
message: `${chunk.reason.failure.code} [provider response text omitted]`,
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
yield chunk;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
38
279
|
}
|
|
39
280
|
}
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
/** Cursor OAuth session and browser-login lifecycle. Credentials stay in DSH. */
|
|
2
2
|
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
3
3
|
import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
4
|
+
import { parseProviderJson } from './common.js';
|
|
4
5
|
export const CURSOR_CREDENTIAL_REF = credentialRef('CURSOR_SUBSCRIPTION_OAUTH');
|
|
5
6
|
const API_ORIGIN = 'https://api2.cursor.sh';
|
|
6
7
|
const REFRESH_AHEAD_MS = 5 * 60_000;
|
|
7
|
-
function parseCredential(value) {
|
|
8
|
+
async function parseCredential(value) {
|
|
8
9
|
if (value === undefined || value.length === 0)
|
|
9
10
|
return undefined;
|
|
10
|
-
|
|
11
|
+
// A parse failure here is a local shape error, not a provider signal. That is a
|
|
12
|
+
// deliberate change: the vendor classifies a stream failure from the message
|
|
13
|
+
// text, and the raw JSON.parse message used to carry a body excerpt, so a
|
|
14
|
+
// corrupt credential whose body happened to contain a word like `quota` was
|
|
15
|
+
// classified as a rate limit. Sanitising the message removes that accident, so
|
|
16
|
+
// a malformed credential now fails as a general provider error instead.
|
|
17
|
+
const parsed = await parseProviderJson(value, 'Cursor stored credential');
|
|
11
18
|
if (parsed === null || typeof parsed !== 'object')
|
|
12
19
|
throw new Error('Cursor credential is malformed');
|
|
13
20
|
const item = parsed;
|
|
@@ -89,7 +96,7 @@ export class CursorAuth {
|
|
|
89
96
|
});
|
|
90
97
|
if (!response.ok)
|
|
91
98
|
throw new Error(`Cursor token refresh failed (HTTP ${response.status})`);
|
|
92
|
-
const body = await response
|
|
99
|
+
const body = await parseProviderJson(response, 'Cursor token refresh');
|
|
93
100
|
if (typeof body.accessToken !== 'string' || body.accessToken.length === 0) {
|
|
94
101
|
throw new Error('Cursor token refresh returned no access token');
|
|
95
102
|
}
|
|
@@ -136,7 +143,7 @@ export class CursorAuth {
|
|
|
136
143
|
}
|
|
137
144
|
if (!response.ok)
|
|
138
145
|
throw new Error(`Cursor login failed (HTTP ${response.status})`);
|
|
139
|
-
const body = await response
|
|
146
|
+
const body = await parseProviderJson(response, 'Cursor login poll');
|
|
140
147
|
signal.throwIfAborted();
|
|
141
148
|
if (typeof body.accessToken !== 'string' || body.accessToken.length === 0) {
|
|
142
149
|
throw new Error('Cursor login returned no access token');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Cursor individual-account quota projection from its dashboard endpoints. */
|
|
2
|
+
import { parseProviderJson } from './common.js';
|
|
2
3
|
const ORIGIN = 'https://cursor.com';
|
|
3
4
|
function record(value) {
|
|
4
5
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
@@ -44,7 +45,7 @@ async function dashboardJson(url, cookie, http, signal) {
|
|
|
44
45
|
});
|
|
45
46
|
if (!response.ok)
|
|
46
47
|
throw new Error(`Cursor usage endpoint returned HTTP ${response.status}`);
|
|
47
|
-
return response
|
|
48
|
+
return parseProviderJson(response, 'Cursor usage');
|
|
48
49
|
}
|
|
49
50
|
function legacyRequestWindow(body, resetsAt) {
|
|
50
51
|
const usage = record(body);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Usage readers for subscriptions whose model adapters are built into DSH. */
|
|
2
|
+
import { parseProviderJson } from './common.js';
|
|
2
3
|
function numeric(value) {
|
|
3
4
|
const n = typeof value === 'string' && value.trim() !== '' ? Number(value) : value;
|
|
4
5
|
return typeof n === 'number' && Number.isFinite(n) ? n : undefined;
|
|
@@ -39,7 +40,7 @@ async function getUsage(url, apiKey, http, signal) {
|
|
|
39
40
|
});
|
|
40
41
|
if (!response.ok)
|
|
41
42
|
throw new Error(`Usage endpoint returned HTTP ${response.status}`);
|
|
42
|
-
return response
|
|
43
|
+
return parseProviderJson(response, 'external usage');
|
|
43
44
|
}
|
|
44
45
|
/** Read the three account-wide OpenCode Go windows from its Go gateway. */
|
|
45
46
|
export async function fetchOpenCodeGoUsage(apiKey, http = fetch, signal) {
|
package/lib/providers/grok.d.ts
CHANGED
|
@@ -25,8 +25,8 @@ export declare const GROK_PREEMPT_MS: number;
|
|
|
25
25
|
* whose values are rollover durations (`6m0s`) present on every response, one
|
|
26
26
|
* per bucket — on a 429 the earliest of them is usually a bucket with room
|
|
27
27
|
* (`0s` for the request bucket while the token bucket is the one exhausted),
|
|
28
|
-
* which would burn the whole retry budget in seconds.
|
|
29
|
-
*
|
|
28
|
+
* which would burn the whole retry budget in seconds. The parsed body reset
|
|
29
|
+
* supplies the retry hint; diagnostics omit raw provider headers and body text.
|
|
30
30
|
*/
|
|
31
31
|
export declare const grokRateLimitReset: RateLimitResetReader;
|
|
32
32
|
/** Discovered OIDC endpoints for the xAI authorization server. */
|
package/lib/providers/grok.js
CHANGED
|
@@ -7,7 +7,7 @@ import { attributionHeaders, EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmErr
|
|
|
7
7
|
import { decodeJwtPayload } from '../auth/jwt.js';
|
|
8
8
|
import { resolveImages } from '../translate/resolved.js';
|
|
9
9
|
import { streamResponses, toResponsesInput, toResponsesTools } from '../translate/responses.js';
|
|
10
|
-
import { httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
10
|
+
import { httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, parseProviderJson, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
11
11
|
import { AccountTokenManager, DISCOVERY_TIMEOUT_MS, unionAccountCatalogs } from './accounts.js';
|
|
12
12
|
import { hostFetch } from '../http.js';
|
|
13
13
|
import { DEFAULT_RATE_LIMIT_WAIT, DEFAULT_RETRY, jsonBody, resetFromFields, subscriptionRetryPolicy, } from './rate-limit.js';
|
|
@@ -29,11 +29,16 @@ const GROK_RESET_FIELDS = ['retry_after', 'retry_after_seconds', 'resets_at', 'r
|
|
|
29
29
|
* whose values are rollover durations (`6m0s`) present on every response, one
|
|
30
30
|
* per bucket — on a 429 the earliest of them is usually a bucket with room
|
|
31
31
|
* (`0s` for the request bucket while the token bucket is the one exhausted),
|
|
32
|
-
* which would burn the whole retry budget in seconds.
|
|
33
|
-
*
|
|
32
|
+
* which would burn the whole retry budget in seconds. The parsed body reset
|
|
33
|
+
* supplies the retry hint; diagnostics omit raw provider headers and body text.
|
|
34
34
|
*/
|
|
35
35
|
export const grokRateLimitReset = (_response, body, now) => resetFromFields(jsonBody(body), GROK_RESET_FIELDS, now);
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* A discovered URL must be https on x.ai or a subdomain; anything else is a
|
|
38
|
+
* hostile document. The rejection names the field and never the URL: the URL
|
|
39
|
+
* comes from the provider's discovery document, which is exactly the text a
|
|
40
|
+
* hostile document would choose.
|
|
41
|
+
*/
|
|
37
42
|
function assertXaiEndpoint(url, field) {
|
|
38
43
|
let parsed;
|
|
39
44
|
try {
|
|
@@ -44,7 +49,7 @@ function assertXaiEndpoint(url, field) {
|
|
|
44
49
|
}
|
|
45
50
|
if (parsed.protocol !== 'https:'
|
|
46
51
|
|| (parsed.hostname !== 'x.ai' && !parsed.hostname.endsWith('.x.ai'))) {
|
|
47
|
-
throw new Error(`grok OIDC discovery returned a non-x.ai ${field}
|
|
52
|
+
throw new Error(`grok OIDC discovery returned a non-x.ai ${field}`);
|
|
48
53
|
}
|
|
49
54
|
return url;
|
|
50
55
|
}
|
|
@@ -59,7 +64,7 @@ export async function grokDiscovery() {
|
|
|
59
64
|
const response = await hostFetch(GROK_DISCOVERY_URL);
|
|
60
65
|
if (!response.ok)
|
|
61
66
|
throw await oauthEndpointError(response, 'grok OIDC discovery');
|
|
62
|
-
const document = await response
|
|
67
|
+
const document = await parseProviderJson(response, 'grok OIDC discovery');
|
|
63
68
|
if (typeof document.authorization_endpoint !== 'string' || typeof document.token_endpoint !== 'string') {
|
|
64
69
|
throw new Error('grok OIDC discovery document is missing endpoints');
|
|
65
70
|
}
|
|
@@ -191,7 +196,7 @@ export async function exchangeGrokCode(code, verifier, redirectUri, challenge) {
|
|
|
191
196
|
}
|
|
192
197
|
if (!response.ok)
|
|
193
198
|
throw await oauthEndpointError(response, 'grok');
|
|
194
|
-
return grokSession(await response
|
|
199
|
+
return grokSession(await parseProviderJson(response, 'grok token exchange'), discovery.tokenEndpoint);
|
|
195
200
|
}
|
|
196
201
|
/**
|
|
197
202
|
* Refresh a grok session (form-encoded grant).
|
|
@@ -210,7 +215,7 @@ export async function refreshGrok(session) {
|
|
|
210
215
|
});
|
|
211
216
|
if (!response.ok)
|
|
212
217
|
throw await oauthEndpointError(response, 'grok');
|
|
213
|
-
const next = grokSession(await response
|
|
218
|
+
const next = grokSession(await parseProviderJson(response, 'grok token refresh'), session.tokenEndpoint, session.refreshToken);
|
|
214
219
|
return {
|
|
215
220
|
...next,
|
|
216
221
|
...session.account === undefined ? {} : { account: session.account },
|
|
@@ -261,7 +266,7 @@ export async function fetchGrokUsage(session, fetchFn = hostFetch, signal) {
|
|
|
261
266
|
});
|
|
262
267
|
if (!response.ok)
|
|
263
268
|
throw await oauthEndpointError(response, 'grok billing');
|
|
264
|
-
const payload = await response
|
|
269
|
+
const payload = await parseProviderJson(response, 'grok billing');
|
|
265
270
|
const config = typeof payload.config === 'object' && payload.config !== null ? payload.config : {};
|
|
266
271
|
const windows = [];
|
|
267
272
|
if (config.currentPeriod || (typeof config.creditUsagePercent === 'number' && Number.isFinite(config.creditUsagePercent))) {
|
|
@@ -357,7 +362,7 @@ export async function fetchGrokCliCatalog(session, fetchFn = hostFetch, signal)
|
|
|
357
362
|
});
|
|
358
363
|
if (!response.ok)
|
|
359
364
|
throw await oauthEndpointError(response, 'grok CLI catalog');
|
|
360
|
-
const payload = await response
|
|
365
|
+
const payload = await parseProviderJson(response, 'grok CLI catalog');
|
|
361
366
|
if (!Array.isArray(payload.data))
|
|
362
367
|
throw new Error('grok CLI catalog returned no data array');
|
|
363
368
|
const catalog = new Map();
|
|
@@ -442,7 +447,7 @@ export async function fetchGrokModels(session, fetchFn = hostFetch, onWarn, prev
|
|
|
442
447
|
]);
|
|
443
448
|
if (!response.ok)
|
|
444
449
|
throw await oauthEndpointError(response, 'grok models');
|
|
445
|
-
const payload = await response
|
|
450
|
+
const payload = await parseProviderJson(response, 'grok models');
|
|
446
451
|
if (!Array.isArray(payload.data))
|
|
447
452
|
throw new Error('grok models endpoint returned no data array');
|
|
448
453
|
const seen = new Set();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseProviderJson } from './common.js';
|
|
1
2
|
/**
|
|
2
3
|
* Used percentage from the legacy counts, read as the official CLI does when no
|
|
3
4
|
* explicit percentage exists: the count is the quota remaining. Undefined
|
|
@@ -40,7 +41,7 @@ export async function fetchMiniMaxUsage(key, region, http = fetch, signal) {
|
|
|
40
41
|
});
|
|
41
42
|
if (!response.ok)
|
|
42
43
|
throw new Error(`MiniMax usage endpoint returned HTTP ${response.status}`);
|
|
43
|
-
const body = await response
|
|
44
|
+
const body = await parseProviderJson(response, 'MiniMax usage');
|
|
44
45
|
if ((body?.base_resp?.status_code !== undefined && body.base_resp.status_code !== 0) || !Array.isArray(body?.model_remains)) {
|
|
45
46
|
throw new Error('MiniMax usage response failed or has no quota records');
|
|
46
47
|
}
|
|
@@ -69,6 +69,8 @@ export declare class PoolHealthRegistry {
|
|
|
69
69
|
* `providerRetryAfterMs`.
|
|
70
70
|
*/
|
|
71
71
|
earliestRecovery(keys: ReadonlySet<string>, now?: number): number | undefined;
|
|
72
|
+
/** Active failure codes for the given keys; reading them does not change cooldowns. */
|
|
73
|
+
unavailableReasons(keys: ReadonlySet<string>, now?: number): string[];
|
|
72
74
|
/** Drop records of one provider, or of a single account when given (auth changes). */
|
|
73
75
|
clear(provider: ProviderId, account?: string): void;
|
|
74
76
|
}
|
|
@@ -139,6 +139,15 @@ export class PoolHealthRegistry {
|
|
|
139
139
|
}
|
|
140
140
|
return earliest;
|
|
141
141
|
}
|
|
142
|
+
/** Active failure codes for the given keys; reading them does not change cooldowns. */
|
|
143
|
+
unavailableReasons(keys, now = Date.now()) {
|
|
144
|
+
const reasons = [];
|
|
145
|
+
for (const [key, record] of this.records) {
|
|
146
|
+
if (keys.has(key) && record.unavailableUntil > now)
|
|
147
|
+
reasons.push(record.reason);
|
|
148
|
+
}
|
|
149
|
+
return reasons;
|
|
150
|
+
}
|
|
142
151
|
/** Drop records of one provider, or of a single account when given (auth changes). */
|
|
143
152
|
clear(provider, account) {
|
|
144
153
|
const prefix = account === undefined ? `${provider}/` : `${provider}/${account}/`;
|
|
@@ -39,6 +39,11 @@ export declare class PoolUsageTracker {
|
|
|
39
39
|
private readonly ttlMs;
|
|
40
40
|
private readonly entries;
|
|
41
41
|
private readonly inflight;
|
|
42
|
+
/** Bumped by {@link invalidate} so a fetch started before it cannot repopulate the cache. */
|
|
43
|
+
private readonly generation;
|
|
44
|
+
/** Monotonic per-key write order: only the newest settled fetch may cache. */
|
|
45
|
+
private nextSequence;
|
|
46
|
+
private readonly lastWritten;
|
|
42
47
|
constructor(fetcherFor: (provider: ProviderId, account: string) => (() => Promise<ProviderUsage>) | undefined, ttlMs?: number);
|
|
43
48
|
/**
|
|
44
49
|
* The quota view of one member. A cold cache awaits the first fetch; a
|
|
@@ -70,7 +75,14 @@ export declare class PoolUsageTracker {
|
|
|
70
75
|
* @returns `{ supported: false }` when the provider has no usage fetcher.
|
|
71
76
|
*/
|
|
72
77
|
snapshotFor(provider: ProviderId, account: string, force?: boolean): Promise<ProviderUsage>;
|
|
73
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Drop cached snapshots: one account, or a whole provider when `account` is
|
|
80
|
+
* omitted. A fetch already in flight is abandoned too. It was started against
|
|
81
|
+
* state the caller has just declared changed — a redeemed credit, a completed
|
|
82
|
+
* login — so joining it would return the pre-change numbers and cache them for
|
|
83
|
+
* the full freshness window. Its result is discarded on arrival by the
|
|
84
|
+
* generation check in {@link refresh}.
|
|
85
|
+
*/
|
|
74
86
|
invalidate(provider: ProviderId, account?: string): void;
|
|
75
87
|
/**
|
|
76
88
|
* Run (or join) the single in-flight fetch for one account key, caching
|
|
@@ -34,6 +34,11 @@ export class PoolUsageTracker {
|
|
|
34
34
|
ttlMs;
|
|
35
35
|
entries = new Map();
|
|
36
36
|
inflight = new Map();
|
|
37
|
+
/** Bumped by {@link invalidate} so a fetch started before it cannot repopulate the cache. */
|
|
38
|
+
generation = new Map();
|
|
39
|
+
/** Monotonic per-key write order: only the newest settled fetch may cache. */
|
|
40
|
+
nextSequence = 0;
|
|
41
|
+
lastWritten = new Map();
|
|
37
42
|
constructor(fetcherFor, ttlMs = USAGE_TTL_MS) {
|
|
38
43
|
this.fetcherFor = fetcherFor;
|
|
39
44
|
this.ttlMs = ttlMs;
|
|
@@ -126,15 +131,27 @@ export class PoolUsageTracker {
|
|
|
126
131
|
throw error;
|
|
127
132
|
}
|
|
128
133
|
}
|
|
129
|
-
/**
|
|
134
|
+
/**
|
|
135
|
+
* Drop cached snapshots: one account, or a whole provider when `account` is
|
|
136
|
+
* omitted. A fetch already in flight is abandoned too. It was started against
|
|
137
|
+
* state the caller has just declared changed — a redeemed credit, a completed
|
|
138
|
+
* login — so joining it would return the pre-change numbers and cache them for
|
|
139
|
+
* the full freshness window. Its result is discarded on arrival by the
|
|
140
|
+
* generation check in {@link refresh}.
|
|
141
|
+
*/
|
|
130
142
|
invalidate(provider, account) {
|
|
143
|
+
const drop = (key) => {
|
|
144
|
+
this.entries.delete(key);
|
|
145
|
+
this.inflight.delete(key);
|
|
146
|
+
this.generation.set(key, (this.generation.get(key) ?? 0) + 1);
|
|
147
|
+
};
|
|
131
148
|
if (account !== undefined) {
|
|
132
|
-
|
|
149
|
+
drop(`${provider}/${account}`);
|
|
133
150
|
return;
|
|
134
151
|
}
|
|
135
|
-
for (const key of [...this.entries.keys()]) {
|
|
152
|
+
for (const key of [...this.entries.keys(), ...this.inflight.keys()]) {
|
|
136
153
|
if (key.startsWith(`${provider}/`))
|
|
137
|
-
|
|
154
|
+
drop(key);
|
|
138
155
|
}
|
|
139
156
|
}
|
|
140
157
|
/**
|
|
@@ -155,13 +172,26 @@ export class PoolUsageTracker {
|
|
|
155
172
|
// consecutive failure, even though nothing newer ever replaced it.
|
|
156
173
|
const prior = this.entries.get(key);
|
|
157
174
|
const lastSnapshot = prior?.snapshot ?? prior?.lastSnapshot;
|
|
175
|
+
const generation = this.generation.get(key) ?? 0;
|
|
176
|
+
// A newer fetch may settle before this one does. The generation only
|
|
177
|
+
// catches an invalidate; the sequence catches a plain race, where this
|
|
178
|
+
// fetch fails late and would otherwise cache a cooldown over a fresher
|
|
179
|
+
// success that already landed.
|
|
180
|
+
const sequence = ++this.nextSequence;
|
|
158
181
|
pending = fetcher().then((value) => {
|
|
159
182
|
const at = Date.now();
|
|
160
183
|
const snapshot = { ...value, observedAt: at, stale: false };
|
|
161
|
-
|
|
184
|
+
// An invalidate() during the fetch means this result describes a world
|
|
185
|
+
// that has moved on; caching it would resurrect the old numbers.
|
|
186
|
+
if ((this.generation.get(key) ?? 0) === generation && (this.lastWritten.get(key) ?? 0) < sequence) {
|
|
187
|
+
this.lastWritten.set(key, sequence);
|
|
188
|
+
this.entries.set(key, { snapshot, at });
|
|
189
|
+
}
|
|
162
190
|
return snapshot;
|
|
163
191
|
}, (error) => {
|
|
164
|
-
if (!isMissingOrInvalidCredential(error))
|
|
192
|
+
if (!isMissingOrInvalidCredential(error) && (this.generation.get(key) ?? 0) === generation
|
|
193
|
+
&& (this.lastWritten.get(key) ?? 0) < sequence) {
|
|
194
|
+
this.lastWritten.set(key, sequence);
|
|
165
195
|
this.entries.set(key, {
|
|
166
196
|
error,
|
|
167
197
|
at: Date.now(),
|
|
@@ -171,7 +201,9 @@ export class PoolUsageTracker {
|
|
|
171
201
|
}
|
|
172
202
|
throw error;
|
|
173
203
|
}).finally(() => {
|
|
174
|
-
|
|
204
|
+
// Only clear our own slot: an invalidate may have replaced it.
|
|
205
|
+
if (this.inflight.get(key) === pending)
|
|
206
|
+
this.inflight.delete(key);
|
|
175
207
|
});
|
|
176
208
|
this.inflight.set(key, pending);
|
|
177
209
|
}
|
package/lib/providers/pool.d.ts
CHANGED
|
@@ -87,6 +87,12 @@ export declare class PoolAdapter extends LlmAdapter {
|
|
|
87
87
|
* metadata is account-specific, so every distinct member is resolved.
|
|
88
88
|
*/
|
|
89
89
|
resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
90
|
+
/**
|
|
91
|
+
* Fail over before output. A single non-quota failure keeps its identity
|
|
92
|
+
* and shared provider message, with scoped pool recovery or its own hint as fallback.
|
|
93
|
+
* Multiple failures carry a bounded summary and all attempts in an AggregateError cause.
|
|
94
|
+
* Pure quota/rate exhaustion retains RATE_LIMIT and this pool's earliest recovery hint.
|
|
95
|
+
*/
|
|
90
96
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
91
97
|
/**
|
|
92
98
|
* Order the candidates for one request. Health filters both strategies;
|
|
@@ -99,10 +105,6 @@ export declare class PoolAdapter extends LlmAdapter {
|
|
|
99
105
|
private select;
|
|
100
106
|
/** Pin the serving member to the session (with bounded memory). */
|
|
101
107
|
private remember;
|
|
102
|
-
/**
|
|
103
|
-
* The error for an exhausted pool, carrying the earliest recovery hint of
|
|
104
|
-
* THIS pool's members (the health registry is shared across pools, so the
|
|
105
|
-
* hint is scoped to the keys this pool can actually recover through).
|
|
106
|
-
*/
|
|
108
|
+
/** Report attempt failures or this pool's active cooldowns, never unrelated health records. */
|
|
107
109
|
private exhausted;
|
|
108
110
|
}
|