dsh-plugin-subscription-oauth 0.8.1 → 0.8.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.
|
@@ -17,9 +17,13 @@
|
|
|
17
17
|
export interface AllowanceState {
|
|
18
18
|
/** Epoch milliseconds at which the account's allowance resets, when known. */
|
|
19
19
|
resetsAt?: number;
|
|
20
|
+
/** Epoch milliseconds until which the account is cooling after a refused request. */
|
|
21
|
+
cooldownUntil?: number;
|
|
20
22
|
/** Epoch milliseconds at which this state was last learned. */
|
|
21
23
|
learnedAt: number;
|
|
22
24
|
}
|
|
25
|
+
/** Default cooldown after a refused request, so the provider's gate is not re-hit at once. */
|
|
26
|
+
export declare const DEFAULT_REFUSAL_COOLDOWN_MS: number;
|
|
23
27
|
/** In-memory, per-provider memory of account reset times. */
|
|
24
28
|
export declare class AllowanceScheduler {
|
|
25
29
|
private readonly byProvider;
|
|
@@ -33,6 +37,38 @@ export declare class AllowanceScheduler {
|
|
|
33
37
|
* @param resetsAt - epoch milliseconds of the allowance reset, when known.
|
|
34
38
|
*/
|
|
35
39
|
noteReset(provider: string, account: string, resetsAt?: number): void;
|
|
40
|
+
/**
|
|
41
|
+
* Cool one account after its request was refused, so the provider's gate is not
|
|
42
|
+
* re-hit at once.
|
|
43
|
+
*
|
|
44
|
+
* A 403 "verify your account" is Google's safety gate, not a dead credential; hitting
|
|
45
|
+
* it in a tight loop across every account is what turns a transient check into a
|
|
46
|
+
* sustained block. Cooling the refused account for a short beat (and trying another)
|
|
47
|
+
* is what keeps the pool reachable — the same discipline a careful client keeps.
|
|
48
|
+
*
|
|
49
|
+
* @param provider - the provider route id.
|
|
50
|
+
* @param account - the account key.
|
|
51
|
+
* @param until - epoch milliseconds to cool until; defaults to a short beat.
|
|
52
|
+
*/
|
|
53
|
+
coolDown(provider: string, account: string, until?: number): void;
|
|
54
|
+
/**
|
|
55
|
+
* Whether one account is cooling after a refused request and should be left alone.
|
|
56
|
+
*
|
|
57
|
+
* @param provider - the provider route id.
|
|
58
|
+
* @param account - the account key.
|
|
59
|
+
* @returns whether the account is mid-cooldown.
|
|
60
|
+
*/
|
|
61
|
+
isCooling(provider: string, account: string): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Forget one account's remembered state, when the account is removed.
|
|
64
|
+
*
|
|
65
|
+
* A signed-out account's allowance state is no longer meaningful — there is no
|
|
66
|
+
* account to schedule around — and a re-added account starts fresh.
|
|
67
|
+
*
|
|
68
|
+
* @param provider - the provider route id.
|
|
69
|
+
* @param account - the account key to forget.
|
|
70
|
+
*/
|
|
71
|
+
clear(provider: string, account: string): void;
|
|
36
72
|
/**
|
|
37
73
|
* Whether one account's allowance is known to be spent now.
|
|
38
74
|
*
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
*
|
|
14
14
|
* @module dsh-plugin-subscription-oauth/auth/allowance-scheduler
|
|
15
15
|
*/
|
|
16
|
+
/** Default cooldown after a refused request, so the provider's gate is not re-hit at once. */
|
|
17
|
+
export const DEFAULT_REFUSAL_COOLDOWN_MS = 2 * 60 * 1000;
|
|
16
18
|
/** In-memory, per-provider memory of account reset times. */
|
|
17
19
|
export class AllowanceScheduler {
|
|
18
20
|
byProvider = new Map();
|
|
@@ -33,7 +35,56 @@ export class AllowanceScheduler {
|
|
|
33
35
|
* @param resetsAt - epoch milliseconds of the allowance reset, when known.
|
|
34
36
|
*/
|
|
35
37
|
noteReset(provider, account, resetsAt) {
|
|
36
|
-
this.
|
|
38
|
+
const existing = this.byProvider.get(provider)?.get(account);
|
|
39
|
+
this.provider(provider).set(account, {
|
|
40
|
+
...existing?.cooldownUntil === undefined ? {} : { cooldownUntil: existing.cooldownUntil },
|
|
41
|
+
...resetsAt === undefined ? {} : { resetsAt },
|
|
42
|
+
learnedAt: Date.now(),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Cool one account after its request was refused, so the provider's gate is not
|
|
47
|
+
* re-hit at once.
|
|
48
|
+
*
|
|
49
|
+
* A 403 "verify your account" is Google's safety gate, not a dead credential; hitting
|
|
50
|
+
* it in a tight loop across every account is what turns a transient check into a
|
|
51
|
+
* sustained block. Cooling the refused account for a short beat (and trying another)
|
|
52
|
+
* is what keeps the pool reachable — the same discipline a careful client keeps.
|
|
53
|
+
*
|
|
54
|
+
* @param provider - the provider route id.
|
|
55
|
+
* @param account - the account key.
|
|
56
|
+
* @param until - epoch milliseconds to cool until; defaults to a short beat.
|
|
57
|
+
*/
|
|
58
|
+
coolDown(provider, account, until = Date.now() + DEFAULT_REFUSAL_COOLDOWN_MS) {
|
|
59
|
+
const existing = this.byProvider.get(provider)?.get(account);
|
|
60
|
+
this.provider(provider).set(account, {
|
|
61
|
+
...existing?.resetsAt === undefined ? {} : { resetsAt: existing.resetsAt },
|
|
62
|
+
cooldownUntil: until,
|
|
63
|
+
learnedAt: Date.now(),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Whether one account is cooling after a refused request and should be left alone.
|
|
68
|
+
*
|
|
69
|
+
* @param provider - the provider route id.
|
|
70
|
+
* @param account - the account key.
|
|
71
|
+
* @returns whether the account is mid-cooldown.
|
|
72
|
+
*/
|
|
73
|
+
isCooling(provider, account) {
|
|
74
|
+
const state = this.byProvider.get(provider)?.get(account);
|
|
75
|
+
return state?.cooldownUntil !== undefined && Date.now() < state.cooldownUntil;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Forget one account's remembered state, when the account is removed.
|
|
79
|
+
*
|
|
80
|
+
* A signed-out account's allowance state is no longer meaningful — there is no
|
|
81
|
+
* account to schedule around — and a re-added account starts fresh.
|
|
82
|
+
*
|
|
83
|
+
* @param provider - the provider route id.
|
|
84
|
+
* @param account - the account key to forget.
|
|
85
|
+
*/
|
|
86
|
+
clear(provider, account) {
|
|
87
|
+
this.byProvider.get(provider)?.delete(account);
|
|
37
88
|
}
|
|
38
89
|
/**
|
|
39
90
|
* Whether one account's allowance is known to be spent now.
|
|
@@ -50,6 +101,8 @@ export class AllowanceScheduler {
|
|
|
50
101
|
const state = this.byProvider.get(provider)?.get(account);
|
|
51
102
|
if (state === undefined)
|
|
52
103
|
return false;
|
|
104
|
+
if (state.cooldownUntil !== undefined && Date.now() < state.cooldownUntil)
|
|
105
|
+
return true;
|
|
53
106
|
if (state.resetsAt === undefined)
|
|
54
107
|
return false;
|
|
55
108
|
return Date.now() < state.resetsAt;
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { ACCOUNT_NOT_FOUND, BAD_REQUEST, MODEL_NOT_FOUND, REAUTH_REQUIRED, ProviderError } from '../errors.js';
|
|
17
17
|
import { entryMatches, resolveCatalog } from '../providers/catalog.js';
|
|
18
18
|
import { accountLabelOf, accountKeyOf } from './store.js';
|
|
19
|
+
import { allowanceScheduler } from './allowance-scheduler.js';
|
|
19
20
|
/** Human-readable message for any thrown value. */
|
|
20
21
|
function messageOf(error) {
|
|
21
22
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -239,8 +240,14 @@ export class ProviderController {
|
|
|
239
240
|
async logout(account) {
|
|
240
241
|
this.refused.delete(account ?? '');
|
|
241
242
|
const key = account ?? await this.options.store.defaultAccount();
|
|
242
|
-
if (key !== undefined)
|
|
243
|
+
if (key !== undefined) {
|
|
243
244
|
await this.options.store.remove(key);
|
|
245
|
+
// Drop every trace of the signed-out account: the remembered allowance state (so
|
|
246
|
+
// the scheduler stops treating it as live) and the cached usage figures (so the
|
|
247
|
+
// card and chip stop showing its quota).
|
|
248
|
+
allowanceScheduler.clear(this.options.spec.id, key);
|
|
249
|
+
this.usageByAccount.delete(key);
|
|
250
|
+
}
|
|
244
251
|
return this.status();
|
|
245
252
|
}
|
|
246
253
|
/** Promote one account to default. */
|
|
@@ -425,6 +425,9 @@ export class AntigravityAdapter extends LlmAdapter {
|
|
|
425
425
|
const accounts = typeof this.options.tokens.accounts === 'function'
|
|
426
426
|
? await this.options.tokens.accounts()
|
|
427
427
|
: [];
|
|
428
|
+
// Surface the account pool and the 429 outcome to the caller, so a non-switch is
|
|
429
|
+
// diagnosable from the browser where the node stdout is hidden.
|
|
430
|
+
const diag = [`pool=${accounts.join(',') || '(none)'}`];
|
|
428
431
|
const tried = new Set();
|
|
429
432
|
let token = await this.options.tokens.resolve();
|
|
430
433
|
// Move an account known to be spent (from a past failure) to the back, so a fresh
|
|
@@ -436,33 +439,71 @@ export class AntigravityAdapter extends LlmAdapter {
|
|
|
436
439
|
continue;
|
|
437
440
|
tried.add(accountKey);
|
|
438
441
|
token = await this.options.tokens.resolve(accountKey);
|
|
439
|
-
|
|
442
|
+
// A transport failure (DNS, connect timeout) on one account may be that account's
|
|
443
|
+
// path, not the provider's; try the next account before giving up, the same way a
|
|
444
|
+
// spent allowance does. A lone account still surfaces the real error.
|
|
445
|
+
let response;
|
|
446
|
+
try {
|
|
447
|
+
response = await this.request(options, model, token.account, token.accessToken, watchdog.signal);
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
const transport = error instanceof TypeError || error.name === 'AbortError';
|
|
451
|
+
if (transport && order.length > 1 && !watchdog.timedOut) {
|
|
452
|
+
diag.push(`transport on ${token.account}, moving to next`);
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
throw error;
|
|
456
|
+
}
|
|
440
457
|
if (response.status === 401 || response.status === 403) {
|
|
441
|
-
// The stored token was accepted locally but refused on the wire; force
|
|
442
|
-
//
|
|
458
|
+
// The stored token was accepted locally but refused on the wire; force one
|
|
459
|
+
// refresh. A 403 that survives the refresh is Google's "verify your account"
|
|
460
|
+
// gate, not a dead credential — the account is fine, it just cannot serve now —
|
|
461
|
+
// so the call moves to the next account rather than failing. Only a 401 marks
|
|
462
|
+
// the credential as dead.
|
|
443
463
|
token = await this.options.tokens.resolve(token.account, true);
|
|
444
464
|
response = await this.request(options, model, token.account, token.accessToken, watchdog.signal);
|
|
445
|
-
if (response.status === 401
|
|
465
|
+
if (response.status === 401) {
|
|
446
466
|
this.options.onCredentialRejected?.(token.account);
|
|
447
467
|
}
|
|
448
468
|
}
|
|
449
|
-
// A spent allowance (402/429) moves to the next account, remembering when
|
|
469
|
+
// A spent allowance (402/429) moves to the next account, remembering when a spent
|
|
450
470
|
// one refills so later requests skip it until then.
|
|
451
471
|
if (!response.ok && (response.status === 402 || response.status === 429) && order.length > 1) {
|
|
472
|
+
diag.push(`${response.status} on ${token.account}, moving to next`);
|
|
452
473
|
this.noteSpent(token.account);
|
|
453
474
|
const resetAt = await this.resetAt(token.account);
|
|
454
475
|
allowanceScheduler.noteReset(ANTIGRAVITY_PROVIDER_ID, token.account, resetAt);
|
|
455
476
|
continue;
|
|
456
477
|
}
|
|
457
|
-
|
|
458
|
-
|
|
478
|
+
// A 403 "verify your account" is Google's safety gate, not a dead credential.
|
|
479
|
+
// Cool the account (so the gate is not re-hit at once) and move to the next —
|
|
480
|
+
// but only if one has not been cooled already this pass, else the loop would
|
|
481
|
+
// sweep every account and trip the gate on all of them at once.
|
|
482
|
+
if (!response.ok && response.status === 403 && order.length > 1) {
|
|
483
|
+
const alreadyCooled = [...tried].some((account) => allowanceScheduler.isCooling(ANTIGRAVITY_PROVIDER_ID, account));
|
|
484
|
+
if (!alreadyCooled) {
|
|
485
|
+
diag.push(`403 on ${token.account}, cooling and moving to next`);
|
|
486
|
+
allowanceScheduler.coolDown(ANTIGRAVITY_PROVIDER_ID, token.account);
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
diag.push(`403 on ${token.account}, another account already cooling; stopping the sweep`);
|
|
490
|
+
}
|
|
491
|
+
if (!response.ok) {
|
|
492
|
+
diag.push(`fail ${response.status} on ${token.account} (no switch)`);
|
|
493
|
+
const error = await httpLlmError(response, 'Antigravity API');
|
|
494
|
+
// The diagnostic rides the message so it surfaces where the user sees the
|
|
495
|
+
// failure, since the node half's stdout is unreachable in the desktop shell.
|
|
496
|
+
error.message = `${error.message} [${diag.join(' | ')}]`;
|
|
497
|
+
throw error;
|
|
498
|
+
}
|
|
459
499
|
if (response.body === null) {
|
|
460
500
|
throw new LlmError('Antigravity API returned no response body', EMPTY_RESPONSE_CODE);
|
|
461
501
|
}
|
|
462
502
|
yield* streamAntigravity(response.body, () => { watchdog.pulse(); });
|
|
463
503
|
return;
|
|
464
504
|
}
|
|
465
|
-
|
|
505
|
+
const exhausted = new LlmError(`Antigravity API: every account's allowance is spent [${diag.join(' | ')}]`, EMPTY_RESPONSE_CODE);
|
|
506
|
+
throw exhausted;
|
|
466
507
|
}
|
|
467
508
|
catch (error) {
|
|
468
509
|
throw mapFetchFailure('Antigravity API', this.refreshHint(error), watchdog, options.signal);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-subscription-oauth",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"description": "Use subscription accounts as DeepSeek Harness LLM providers — Kimi Code (OAuth device flow), Grok / X Premium (OIDC + PKCE), Google Antigravity (Google OAuth + Cloud Code Assist) and GLM / BigModel (ZCode's browser sign-in) — with per-model visibility, drag-to-reorder Auto fallback and plan quotas from the web Settings page",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "EricFL1998",
|