dsh-plugin-subscription-oauth 0.8.3 → 0.8.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.
@@ -32,6 +32,9 @@ export declare const ANTIGRAVITY_AUTHORIZE_URL = "https://accounts.google.com/o/
32
32
  export declare const ANTIGRAVITY_TOKEN_URL = "https://oauth2.googleapis.com/token";
33
33
  /** Google's profile endpoint, used for the account label. */
34
34
  export declare const ANTIGRAVITY_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
35
+ /** Minimum beat between Antigravity dispatch attempts, so a refused account is not re-hit
36
+ * at once and a sweep across the pool does not become a burst. */
37
+ export declare const ANTIGRAVITY_PACING_MS = 2000;
35
38
  /** Default Cloud Code Assist origin. */
36
39
  export declare const ANTIGRAVITY_BASE_URL = "https://daily-cloudcode-pa.googleapis.com";
37
40
  /** Production origin, tried when the daily one is unavailable. */
@@ -34,6 +34,34 @@ export const ANTIGRAVITY_AUTHORIZE_URL = 'https://accounts.google.com/o/oauth2/v
34
34
  export const ANTIGRAVITY_TOKEN_URL = 'https://oauth2.googleapis.com/token';
35
35
  /** Google's profile endpoint, used for the account label. */
36
36
  export const ANTIGRAVITY_USERINFO_URL = 'https://www.googleapis.com/oauth2/v2/userinfo';
37
+ /** Minimum beat between Antigravity dispatch attempts, so a refused account is not re-hit
38
+ * at once and a sweep across the pool does not become a burst. */
39
+ export const ANTIGRAVITY_PACING_MS = 2_000;
40
+ /** The last moment an Antigravity dispatch started; the next waits out the beat. */
41
+ let lastAntigravityDispatchAt = 0;
42
+ /**
43
+ * Wait out the Antigravity pacing beat before a dispatch attempt.
44
+ *
45
+ * A request right after a refused one is what turns a transient Google safety check into
46
+ * a sustained block; a short, provider-scoped pause between attempts is the discipline
47
+ * that keeps the pool reachable. Aborts when the caller's signal fires.
48
+ *
49
+ * @param signal - the caller's abort signal.
50
+ */
51
+ async function paceAntigravityAttempt(signal) {
52
+ const wait = lastAntigravityDispatchAt + ANTIGRAVITY_PACING_MS - Date.now();
53
+ lastAntigravityDispatchAt = Date.now();
54
+ if (wait <= 0 || signal.aborted)
55
+ return;
56
+ await new Promise((resolve) => {
57
+ const onAbort = () => { clearTimeout(timer); resolve(); };
58
+ const timer = setTimeout(() => {
59
+ signal.removeEventListener('abort', onAbort);
60
+ resolve();
61
+ }, wait);
62
+ signal.addEventListener('abort', onAbort, { once: true });
63
+ });
64
+ }
37
65
  /** Default Cloud Code Assist origin. */
38
66
  export const ANTIGRAVITY_BASE_URL = 'https://daily-cloudcode-pa.googleapis.com';
39
67
  /** Production origin, tried when the daily one is unavailable. */
@@ -430,30 +458,22 @@ export class AntigravityAdapter extends LlmAdapter {
430
458
  const diag = [`pool=${accounts.join(',') || '(none)'}`];
431
459
  const tried = new Set();
432
460
  let token = await this.options.tokens.resolve();
433
- // Move an account known to be spent (from a past failure) to the back, so a fresh
434
- // request starts on one that has allowance. A lone account still serves.
435
- const order = [...accounts.filter((a) => a !== token.account && !this.isSpent(a)), token.account,
436
- ...accounts.filter((a) => a !== token.account && this.isSpent(a))];
461
+ // Stay on the default account until it refuses. opencodex keeps account affinity —
462
+ // a request stays on one account and only moves after a 429 — because switching
463
+ // accounts on every request is what reads to Google as two accounts in play at once
464
+ // and trips the verify gate. Reordering here caused exactly that.
465
+ const order = [token.account, ...accounts.filter((a) => a !== token.account)];
437
466
  for (const accountKey of order) {
438
467
  if (tried.has(accountKey))
439
468
  continue;
440
469
  tried.add(accountKey);
441
470
  token = await this.options.tokens.resolve(accountKey);
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
- }
471
+ // Pacing: a connect attempt right after a refused one is what turns a transient
472
+ // Google safety check into a sustained block, so the streamer waits out a short
473
+ // beat before each retry. opencodex keeps this discipline with a request pacer;
474
+ // this is the same idea narrowed to one provider.
475
+ await paceAntigravityAttempt(watchdog.signal);
476
+ let response = await this.request(options, model, token.account, token.accessToken, watchdog.signal);
457
477
  if (response.status === 401 || response.status === 403) {
458
478
  // The stored token was accepted locally but refused on the wire; force one
459
479
  // refresh. A 403 that survives the refresh is Google's "verify your account"
@@ -15,6 +15,12 @@ import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm';
15
15
  import type { SessionBase } from '../auth/store.js';
16
16
  import type { TokenManager } from '../auth/tokens.js';
17
17
  import type { UsageWindow } from '../protocol.js';
18
+ /**
19
+ * Minimum beat between dispatch attempts on one route, so a refused account is not
20
+ * re-hit at once and a sweep across the pool does not become a burst. opencodex keeps
21
+ * this discipline with a request pacer; this is the same idea narrowed to one plugin.
22
+ */
23
+ export declare const DISPATCH_PACING_MS = 2000;
18
24
  /**
19
25
  * Build the cross-account hooks one route needs to draw on its whole account pool.
20
26
  *
@@ -15,6 +15,35 @@ import { EMPTY_RESPONSE_CODE, LlmError } from '@deepseek-ai/dsh-llm';
15
15
  import { allowanceScheduler } from '../auth/allowance-scheduler.js';
16
16
  import { streamResponses } from '../translate/responses.js';
17
17
  import { httpLlmError, idleWatchdog, mapFetchFailure } from './common.js';
18
+ /**
19
+ * Minimum beat between dispatch attempts on one route, so a refused account is not
20
+ * re-hit at once and a sweep across the pool does not become a burst. opencodex keeps
21
+ * this discipline with a request pacer; this is the same idea narrowed to one plugin.
22
+ */
23
+ export const DISPATCH_PACING_MS = 2_000;
24
+ /** Per-route last-dispatch instants, so each route paces its own attempts. */
25
+ const lastDispatchAt = new Map();
26
+ /**
27
+ * Wait out a route's pacing beat before a dispatch attempt.
28
+ *
29
+ * @param route - the route key pacing its own attempts.
30
+ * @param signal - the caller's abort signal.
31
+ */
32
+ async function paceDispatch(route, signal) {
33
+ const last = lastDispatchAt.get(route) ?? 0;
34
+ const wait = last + DISPATCH_PACING_MS - Date.now();
35
+ lastDispatchAt.set(route, Date.now());
36
+ if (wait <= 0 || signal.aborted)
37
+ return;
38
+ await new Promise((resolve) => {
39
+ const onAbort = () => { clearTimeout(timer); resolve(); };
40
+ const timer = setTimeout(() => {
41
+ signal.removeEventListener('abort', onAbort);
42
+ resolve();
43
+ }, wait);
44
+ signal.addEventListener('abort', onAbort, { once: true });
45
+ });
46
+ }
18
47
  /**
19
48
  * Build the cross-account hooks one route needs to draw on its whole account pool.
20
49
  *
@@ -101,28 +130,17 @@ export async function* streamProviderResponses(options, input) {
101
130
  if (models.length === 0) {
102
131
  throw new LlmError(`${label} has no enabled model to dispatch to`, EMPTY_RESPONSE_CODE);
103
132
  }
104
- // When the provider meters per account and an account's allowance can run out,
105
- // every account is a candidate: a spent one falls through to the next, so one
106
- // model id draws on the whole pool's allowance. Accounts are tried in order;
107
- // the first that serves the request wins.
133
+ // Stay on the default account until it refuses. opencodex keeps account affinity — a
134
+ // request stays on one account and only moves after a 429 — because switching accounts
135
+ // on every request is what reads to a provider as several accounts in play at once and
136
+ // trips the safety gate. Reordering candidates here caused exactly that.
108
137
  let accounts = input.accounts === undefined
109
138
  ? undefined
110
139
  : [...await input.accounts()];
111
- // Send work straight to an account that has allowance: one already known to be
112
- // spent until a future reset is moved to the back, so a fresh request does not
113
- // fail against it once to find out. This is a preference, not a skip — a lone
114
- // account still serves, and the spent-retry below catches a stale record.
115
- if (accounts !== undefined && accounts.length > 1 && input.isAccountSpent !== undefined) {
116
- const fresh = [];
117
- const tired = [];
118
- for (const account of accounts) {
119
- if (await input.isAccountSpent(account))
120
- tired.push(account);
121
- else
122
- fresh.push(account);
123
- }
124
- if (tired.length > 0 && fresh.length > 0) {
125
- accounts = [...fresh, ...tired];
140
+ if (accounts !== undefined && accounts.length > 1) {
141
+ const defaultAccount = await tokens.defaultAccount();
142
+ if (defaultAccount !== undefined) {
143
+ accounts = [defaultAccount, ...accounts.filter((a) => a !== defaultAccount)];
126
144
  }
127
145
  }
128
146
  const tryAccounts = accounts === undefined || accounts.length === 0
@@ -130,6 +148,10 @@ export async function* streamProviderResponses(options, input) {
130
148
  : accounts;
131
149
  for (const accountKey of tryAccounts) {
132
150
  let token = await tokens.resolve(accountKey);
151
+ // Pace each dispatch: a request right after a refused one is what turns a
152
+ // transient safety check into a sustained block, so the streamer waits out a
153
+ // short beat before each attempt (the same discipline opencodex keeps).
154
+ await paceDispatch(label, watchdog.signal);
133
155
  // Track which accounts a spent-allowance already moved past, so a later model
134
156
  // in the chain does not re-try an account known to be exhausted.
135
157
  const spent = new Set();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-subscription-oauth",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
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",