dsh-plugin-subscription-oauth 0.17.3 → 0.17.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.
@@ -254,6 +254,16 @@ export declare class AntigravityAdapter extends LlmAdapter {
254
254
  /** Translate an auth failure into the message a user can act on. */
255
255
  private refreshHint;
256
256
  /** Build and send one streaming request for one model. */
257
+ /**
258
+ * One dispatch attempt, retrying transient network faults before giving up on the account.
259
+ *
260
+ * A connect timeout or a dropped socket is a transport hiccup, not a refusal — the
261
+ * account is fine and the request never arrived, so it is worth a few backoff retries
262
+ * before the sweep moves on. An HTTP response (even a 429/403) is returned as-is: those
263
+ * drive the account-switch logic in {@link stream}, and must not be swallowed here.
264
+ * Non-network errors propagate untouched.
265
+ */
266
+ private requestWithRetry;
257
267
  private request;
258
268
  /** Resolve image blocks through the attachment service, when it is present. */
259
269
  private imageResolver;
@@ -62,6 +62,40 @@ async function paceAntigravityAttempt(signal) {
62
62
  signal.addEventListener('abort', onAbort, { once: true });
63
63
  });
64
64
  }
65
+ /** How many times a transient network fault is retried before the account is skipped. */
66
+ const NETWORK_RETRY_ATTEMPTS = 3;
67
+ /** Base delay between network retries; it doubles each attempt. */
68
+ const NETWORK_RETRY_BASE_MS = 800;
69
+ /**
70
+ * Whether an error from `fetch` is a transient network fault worth retrying.
71
+ *
72
+ * The Antigravity call is a single long-lived POST: a connect timeout, a reset, or a
73
+ * dropped socket is a transport hiccup, not a refusal — the account is fine, the request
74
+ * just never arrived. A thrown HTTP-shaped error or a non-network TypeError is not.
75
+ */
76
+ function isTransientNetworkFault(error) {
77
+ if (error instanceof LlmError)
78
+ return false;
79
+ const text = errorChain(error).toLowerCase();
80
+ return /fetch failed|connect timeout|connection reset|econnreset|etimedout|econnrefused|socket hang up|network error|timed out/.test(text);
81
+ }
82
+ /**
83
+ * Sleep for a backoff beat, honouring the caller's abort.
84
+ * @param ms - how long to wait.
85
+ * @param signal - the caller's abort signal.
86
+ */
87
+ function backoff(ms, signal) {
88
+ return new Promise((resolve) => {
89
+ if (signal.aborted)
90
+ return resolve();
91
+ const onAbort = () => { clearTimeout(timer); resolve(); };
92
+ const timer = setTimeout(() => {
93
+ signal.removeEventListener('abort', onAbort);
94
+ resolve();
95
+ }, ms);
96
+ signal.addEventListener('abort', onAbort, { once: true });
97
+ });
98
+ }
65
99
  /** Default Cloud Code Assist origin. */
66
100
  export const ANTIGRAVITY_BASE_URL = 'https://daily-cloudcode-pa.googleapis.com';
67
101
  /** Production origin, tried when the daily one is unavailable. */
@@ -539,7 +573,7 @@ export class AntigravityAdapter extends LlmAdapter {
539
573
  // beat before each retry. opencodex keeps this discipline with a request pacer;
540
574
  // this is the same idea narrowed to one provider.
541
575
  await paceAntigravityAttempt(watchdog.signal);
542
- let response = await this.request(options, model, token.account, token.accessToken, watchdog.signal);
576
+ let response = await this.requestWithRetry(options, model, token.account, token.accessToken, watchdog.signal);
543
577
  if (response.status === 401 || response.status === 403) {
544
578
  // The stored token was accepted locally but refused on the wire; force one
545
579
  // refresh. A 403 that survives the refresh is Google's "verify your account"
@@ -547,7 +581,7 @@ export class AntigravityAdapter extends LlmAdapter {
547
581
  // so the call moves to the next account rather than failing. Only a 401 marks
548
582
  // the credential as dead.
549
583
  token = await this.options.tokens.resolve(token.account, true);
550
- response = await this.request(options, model, token.account, token.accessToken, watchdog.signal);
584
+ response = await this.requestWithRetry(options, model, token.account, token.accessToken, watchdog.signal);
551
585
  if (response.status === 401) {
552
586
  this.options.onCredentialRejected?.(token.account);
553
587
  }
@@ -575,6 +609,13 @@ export class AntigravityAdapter extends LlmAdapter {
575
609
  diag.push(`403 on ${token.account}, another account already cooling; stopping the sweep`);
576
610
  }
577
611
  if (!response.ok) {
612
+ if (response.status === 400) {
613
+ // Google's 400 puts the offending field in error.details / fieldViolations, which
614
+ // the generic errorBodyMessage does not surface; log the whole body so the real
615
+ // cause is visible, since a bare "invalid argument" gives nothing to act on.
616
+ const body = await response.clone().text().catch(() => '');
617
+ this.options.onWarn?.(`antigravity: HTTP 400 body: ${body.slice(0, 600)}`);
618
+ }
578
619
  diag.push(`fail ${response.status} on ${token.account} (no switch)`);
579
620
  const error = await httpLlmError(response, 'Antigravity API');
580
621
  // The diagnostic rides the message so it surfaces where the user sees the
@@ -653,6 +694,33 @@ export class AntigravityAdapter extends LlmAdapter {
653
694
  return error;
654
695
  }
655
696
  /** Build and send one streaming request for one model. */
697
+ /**
698
+ * One dispatch attempt, retrying transient network faults before giving up on the account.
699
+ *
700
+ * A connect timeout or a dropped socket is a transport hiccup, not a refusal — the
701
+ * account is fine and the request never arrived, so it is worth a few backoff retries
702
+ * before the sweep moves on. An HTTP response (even a 429/403) is returned as-is: those
703
+ * drive the account-switch logic in {@link stream}, and must not be swallowed here.
704
+ * Non-network errors propagate untouched.
705
+ */
706
+ async requestWithRetry(options, model, account, accessToken, signal) {
707
+ let lastError;
708
+ for (let attempt = 0; attempt < NETWORK_RETRY_ATTEMPTS; attempt++) {
709
+ try {
710
+ return await this.request(options, model, account, accessToken, signal);
711
+ }
712
+ catch (error) {
713
+ if (!isTransientNetworkFault(error) || signal.aborted)
714
+ throw error;
715
+ lastError = error;
716
+ this.options.onWarn?.(`antigravity: transient network fault (${errorChain(error)}); retrying`);
717
+ if (attempt < NETWORK_RETRY_ATTEMPTS - 1) {
718
+ await backoff(NETWORK_RETRY_BASE_MS * 2 ** attempt, signal);
719
+ }
720
+ }
721
+ }
722
+ throw lastError;
723
+ }
656
724
  async request(options, model, account, accessToken, signal) {
657
725
  // Every call is scoped to the account's Cloud AI Companion project.
658
726
  const held = await this.options.tokens.session(account);
@@ -96,7 +96,13 @@ async function errorBodyMessage(response) {
96
96
  const record = parsed;
97
97
  const error = record.error;
98
98
  if (typeof error === 'object' && error !== null) {
99
- const message = error.message;
99
+ const errorRecord = error;
100
+ const message = errorRecord.message;
101
+ // Google puts the offending field in details[].fieldViolations, which the bare
102
+ // message omits; surface it so a 400 names what to fix instead of "invalid argument".
103
+ const detail = extractFieldViolations(errorRecord.details);
104
+ if (detail !== undefined)
105
+ return `${String(message ?? '').trim()} — ${detail}`;
100
106
  if (typeof message === 'string' && message.trim() !== '')
101
107
  return message.trim();
102
108
  }
@@ -115,6 +121,29 @@ async function errorBodyMessage(response) {
115
121
  return undefined;
116
122
  }
117
123
  }
124
+ /** The offending-field text inside a Google RPC error's `details` list, when present. */
125
+ function extractFieldViolations(details) {
126
+ if (!Array.isArray(details))
127
+ return undefined;
128
+ const out = [];
129
+ for (const entry of details) {
130
+ if (typeof entry !== 'object' || entry === null)
131
+ continue;
132
+ const violations = entry.fieldViolations;
133
+ if (!Array.isArray(violations))
134
+ continue;
135
+ for (const violation of violations) {
136
+ if (typeof violation !== 'object' || violation === null)
137
+ continue;
138
+ const field = violation.field;
139
+ const description = violation.description;
140
+ if (typeof description === 'string' && description.trim() !== '') {
141
+ out.push(typeof field === 'string' && field.trim() !== '' ? `${field}: ${description.trim()}` : description.trim());
142
+ }
143
+ }
144
+ }
145
+ return out.length === 0 ? undefined : out.join('; ');
146
+ }
118
147
  /**
119
148
  * Turn a non-2xx provider response into an LlmError carrying the status and
120
149
  * any rate-limit hint the headers disclosed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-subscription-oauth",
3
- "version": "0.17.3",
3
+ "version": "0.17.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",