clauderipple 0.9.5 → 0.11.0
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/CHANGELOG.md +118 -0
- package/README.es.md +45 -0
- package/README.ja.md +17 -0
- package/README.ko.md +31 -0
- package/README.md +70 -0
- package/README.pt-BR.md +45 -0
- package/README.zh-CN.md +17 -0
- package/dist/app/assets/icon.icns +0 -0
- package/dist/app/assets/icon.ico +0 -0
- package/dist/app/assets/icon.png +0 -0
- package/dist/app/assets/trayDirectTemplate.png +0 -0
- package/dist/app/assets/trayDirectTemplate@2x.png +0 -0
- package/dist/app/assets/trayDownTemplate.png +0 -0
- package/dist/app/assets/trayDownTemplate@2x.png +0 -0
- package/dist/app/assets/trayTemplate.png +0 -0
- package/dist/app/assets/trayTemplate@2x.png +0 -0
- package/dist/app/assets/trayWarnTemplate.png +0 -0
- package/dist/app/assets/trayWarnTemplate@2x.png +0 -0
- package/dist/app/assets/trayWin.png +0 -0
- package/dist/app/assets/trayWin@2x.png +0 -0
- package/dist/app/assets/trayWinDirect.png +0 -0
- package/dist/app/assets/trayWinDirect@2x.png +0 -0
- package/dist/app/assets/trayWinDown.png +0 -0
- package/dist/app/assets/trayWinDown@2x.png +0 -0
- package/dist/app/assets/trayWinWarn.png +0 -0
- package/dist/app/assets/trayWinWarn@2x.png +0 -0
- package/dist/app/dist/claude-desktop.js +67 -0
- package/dist/app/dist/main.js +423 -110
- package/dist/app/dist/preload.js +21 -0
- package/dist/app/dist/protocol.js +121 -0
- package/dist/app/dist/status-model.js +87 -0
- package/dist/app/dist/window.js +133 -0
- package/dist/app/package.json +1 -1
- package/dist/cli/src/awake-setup.js +314 -0
- package/dist/cli/src/bundle.js +9 -5
- package/dist/cli/src/direct.js +159 -0
- package/dist/cli/src/doctor.js +205 -0
- package/dist/cli/src/hooks/agent-title.js +7 -4
- package/dist/cli/src/index.js +227 -31
- package/dist/cli/src/launchd.js +29 -20
- package/dist/cli/src/launcher.js +215 -0
- package/dist/cli/src/power.js +182 -0
- package/dist/cli/src/registration.js +157 -0
- package/dist/cli/src/remote.js +263 -0
- package/dist/cli/src/schtasks.js +50 -40
- package/dist/cli/src/settings.js +26 -1
- package/dist/cli/src/tray.js +5 -2
- package/dist/mod/hooks/i18n.ts +473 -227
- package/dist/mod/hooks/judge.ts +35 -17
- package/dist/mod/hooks/modes.ts +56 -1
- package/dist/mod/hooks/register.tsx +237 -32
- package/dist/mod/hooks/session.ts +102 -1
- package/dist/mod/hooks/settings.js +7 -3
- package/dist/mod/hooks/settings.ts +9 -4
- package/dist/mod/hooks/stats.ts +25 -1
- package/dist/mod/hooks/ui/agents.tsx +903 -79
- package/dist/mod/hooks/ui/alerts.tsx +20 -17
- package/dist/mod/hooks/ui/art.ts +33 -7
- package/dist/mod/hooks/ui/band.tsx +262 -81
- package/dist/mod/hooks/ui/flat.ts +265 -0
- package/dist/mod/hooks/ui/kit.tsx +70 -55
- package/dist/mod/hooks/ui/look.ts +129 -0
- package/dist/mod/hooks/ui/settings.tsx +71 -46
- package/dist/mod/hooks/ui/setup.tsx +15 -13
- package/dist/mod/hooks/ui/theme.ts +3 -0
- package/dist/mod/hooks/ui/tools.ts +124 -0
- package/dist/mod/hooks/ui/view.ts +120 -4
- package/dist/mod/hooks/workers.ts +33 -6
- package/dist/mod/tests/register.test.tsx +155 -11
- package/dist/mod/tests/ui.test.tsx +408 -27
- package/dist/mod/types/index.d.ts +66 -2
- package/dist/remote/package.json +28 -0
- package/dist/remote/src/away.js +85 -0
- package/dist/remote/src/bridge-client.js +336 -0
- package/dist/remote/src/composer-menu.js +11 -0
- package/dist/remote/src/config.js +102 -0
- package/dist/remote/src/desktop.js +1018 -0
- package/dist/remote/src/e2ee.js +126 -0
- package/dist/remote/src/e2ee.ts +126 -0
- package/dist/remote/src/eventlog.js +298 -0
- package/dist/remote/src/expo-push.js +127 -0
- package/dist/remote/src/folders.js +79 -0
- package/dist/remote/src/fsutil.js +39 -0
- package/dist/remote/src/handoff.js +38 -0
- package/dist/remote/src/host-service.js +168 -0
- package/dist/remote/src/images.js +215 -0
- package/dist/remote/src/local-control.js +138 -0
- package/dist/remote/src/main.js +264 -0
- package/dist/remote/src/permissions.js +124 -0
- package/dist/remote/src/power.js +76 -0
- package/dist/remote/src/processes.js +24 -0
- package/dist/remote/src/protocol.js +32 -0
- package/dist/remote/src/protocol.ts +284 -0
- package/dist/remote/src/push-content.js +32 -0
- package/dist/remote/src/push.js +134 -0
- package/dist/remote/src/receipts.js +111 -0
- package/dist/remote/src/relay.js +438 -0
- package/dist/remote/src/router.js +208 -0
- package/dist/remote/src/sdk-launcher.js +18 -0
- package/dist/remote/src/server.js +706 -0
- package/dist/remote/src/service.js +157 -0
- package/dist/remote/src/sessions.js +1800 -0
- package/dist/remote/src/sidebar-storage.js +211 -0
- package/dist/remote/src/sidebar.js +173 -0
- package/dist/remote/src/tool-details.js +205 -0
- package/dist/remote/src/tool-details.ts +153 -0
- package/dist/remote/src/worktree.js +99 -0
- package/dist/remote/web/app.ts +746 -0
- package/dist/remote/web/conn.ts +193 -0
- package/dist/remote/web/demo.ts +134 -0
- package/dist/remote/web/dom.ts +124 -0
- package/dist/remote/web/esc.ts +4 -0
- package/dist/remote/web/i18n.ts +374 -0
- package/dist/remote/web/icon.svg +1 -0
- package/dist/remote/web/index.html +24 -0
- package/dist/remote/web/manifest.webmanifest +10 -0
- package/dist/remote/web/mascot.ts +60 -0
- package/dist/remote/web/md.ts +78 -0
- package/dist/remote/web/notify.ts +55 -0
- package/dist/remote/web/pushstate.ts +27 -0
- package/dist/remote/web/secure.ts +109 -0
- package/dist/remote/web/sidebar.ts +106 -0
- package/dist/remote/web/style.css +311 -0
- package/dist/remote/web/sw.ts +48 -0
- package/dist/remote/web/timeline.ts +224 -0
- package/dist/remote/web/views.ts +323 -0
- package/dist/router/src/admin.js +331 -5
- package/dist/router/src/awake-win.js +590 -0
- package/dist/router/src/awake.js +432 -0
- package/dist/router/src/config.js +94 -0
- package/dist/router/src/health.js +86 -3
- package/dist/router/src/hold.js +107 -0
- package/dist/router/src/index.js +108 -6
- package/dist/router/src/ingress/server.js +26 -4
- package/dist/router/src/ingress/translate.js +6 -2
- package/dist/router/src/phone.js +82 -0
- package/dist/router/src/power.js +107 -0
- package/dist/router/src/providers/anthropic-usage.js +33 -1
- package/dist/router/src/providers/google/antigravity.js +73 -10
- package/dist/router/src/providers/google/index.js +191 -14
- package/dist/router/src/providers/google/prompt-refusal.js +29 -0
- package/dist/router/src/providers/google/thoughtSignatures.js +13 -0
- package/dist/router/src/providers/google/transport.js +1 -1
- package/dist/router/src/providers/google/usage.js +302 -0
- package/dist/router/src/providers/grok/index.js +18 -0
- package/dist/router/src/providers/grok/usage.js +77 -0
- package/dist/router/src/providers/openai/translate.js +13 -2
- package/dist/router/src/providers/opencode-usage.js +116 -0
- package/dist/router/src/providers/retry.js +4 -1
- package/dist/router/src/proxy.js +371 -261
- package/dist/router/src/remote-host.js +320 -0
- package/dist/router/src/remote-tunnel.js +320 -0
- package/dist/router/src/requestlog.js +48 -39
- package/dist/router/src/tether-win.js +211 -0
- package/dist/router/src/tether.js +712 -0
- package/dist/router/src/usage-history.js +227 -0
- package/dist/router/src/version.js +1 -1
- package/dist/ui/app.js +1083 -59
- package/dist/ui/i18n.js +670 -69
- package/dist/ui/index.html +33 -6
- package/dist/ui/style.css +177 -0
- package/docs/ARCHITECTURE.md +402 -14
- package/package.json +8 -2
|
@@ -31,6 +31,8 @@ export const GOOGLE_OAUTH = {
|
|
|
31
31
|
prodApi: "https://cloudcode-pa.googleapis.com",
|
|
32
32
|
/** Streaming and `onboardUser` use the daily host. Source: opencodex `src/oauth/google-antigravity.ts:24-25`. */
|
|
33
33
|
dailyApi: "https://daily-cloudcode-pa.googleapis.com",
|
|
34
|
+
/** The two Cloud Code Assist hosts. They offer different models and count quota separately. */
|
|
35
|
+
hosts: ["https://daily-cloudcode-pa.googleapis.com", "https://cloudcode-pa.googleapis.com"],
|
|
34
36
|
apiVersion: "v1internal",
|
|
35
37
|
/** Preferred loopback port; the callback path is fixed. Source: opencodex `src/oauth/google-antigravity.ts:34-35`. */
|
|
36
38
|
port: 51121,
|
|
@@ -91,8 +93,12 @@ export function resolveAntigravityThinkingLevel(effort) {
|
|
|
91
93
|
* `ANTIGRAVITY_EFFORT_WIRE_MAP` / `ANTIGRAVITY_SUFFIX_TIER_MODELS`.
|
|
92
94
|
*/
|
|
93
95
|
const SUFFIX_TIER_MODELS = {
|
|
96
|
+
// High rides on the medium tier: `-high` thinks with no budget (the catalogue's thinkingBudget -1,
|
|
97
|
+
// against 4000 for medium), and on 2026-10-17 the requests past 4000 output tokens took 64% of all
|
|
98
|
+
// request time. The owner chose speed over the deeper tier (2026-10-20). `-high` named outright
|
|
99
|
+
// still goes through as it is.
|
|
94
100
|
"gemini-3.8-flash": {
|
|
95
|
-
wire: { low: "gemini-3.8-flash-low", medium: "gemini-3.8-flash-medium", high: "gemini-3.8-flash-
|
|
101
|
+
wire: { low: "gemini-3.8-flash-low", medium: "gemini-3.8-flash-medium", high: "gemini-3.8-flash-medium" },
|
|
96
102
|
default: "medium",
|
|
97
103
|
},
|
|
98
104
|
};
|
|
@@ -221,14 +227,15 @@ function collapseWireId(wireId) {
|
|
|
221
227
|
return wireId;
|
|
222
228
|
}
|
|
223
229
|
/**
|
|
224
|
-
* Parse `POST {
|
|
225
|
-
* under `agentModelSorts[].groups[].modelIds`, with tiered Flash ids under
|
|
226
|
-
* describes each in `models[wireId]` (`maxTokens`). Source: opencodex
|
|
230
|
+
* Parse one host's `POST {host}/v1internal:fetchAvailableModels`. The body groups agent-callable
|
|
231
|
+
* models under `agentModelSorts[].groups[].modelIds`, with tiered Flash ids under
|
|
232
|
+
* `tieredModelIds.flash`, and describes each in `models[wireId]` (`maxTokens`). Source: opencodex
|
|
227
233
|
* `parseAntigravityAvailableModels` (src/providers/antigravity-models.ts:489-560).
|
|
228
234
|
*
|
|
229
235
|
* Returns null when the shape is unreadable, so the caller falls back instead of showing nothing.
|
|
236
|
+
* The wire ids are kept so per-model host routing can match an exact wire id as well as a base id.
|
|
230
237
|
*/
|
|
231
|
-
export function
|
|
238
|
+
export function parseHostCatalogue(payload, limit = 200) {
|
|
232
239
|
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
233
240
|
return null;
|
|
234
241
|
const body = payload;
|
|
@@ -240,10 +247,12 @@ export function parseAvailableModels(payload, limit = 200) {
|
|
|
240
247
|
// Collapse each wire id to its base id first, then take the context window and display name from
|
|
241
248
|
// the first wire id that carried that base. A collapsed id is what routing understands.
|
|
242
249
|
const collapsed = new Map();
|
|
250
|
+
const wireIds = [];
|
|
243
251
|
const add = (wireId) => {
|
|
244
252
|
if (typeof wireId !== "string" || !available.has(wireId))
|
|
245
253
|
return;
|
|
246
254
|
const id = collapseWireId(wireId);
|
|
255
|
+
wireIds.push(wireId);
|
|
247
256
|
if (!collapsed.has(id))
|
|
248
257
|
collapsed.set(id, wireId);
|
|
249
258
|
};
|
|
@@ -278,7 +287,25 @@ export function parseAvailableModels(payload, limit = 200) {
|
|
|
278
287
|
if (out.length >= limit)
|
|
279
288
|
break;
|
|
280
289
|
}
|
|
281
|
-
return out.length > 0 ? out : null;
|
|
290
|
+
return out.length > 0 ? { models: out, wireIds } : null;
|
|
291
|
+
}
|
|
292
|
+
/** The picker rows of one host's body, or null when the shape is unreadable. */
|
|
293
|
+
export function parseAvailableModels(payload, limit = 200) {
|
|
294
|
+
return parseHostCatalogue(payload, limit)?.models ?? null;
|
|
295
|
+
}
|
|
296
|
+
/** Build the per-model host map from each host's catalogue; a host with no catalogue contributes nothing. */
|
|
297
|
+
export function buildHostMap(cataloguesByHost) {
|
|
298
|
+
const map = new Map();
|
|
299
|
+
for (const [host, catalogue] of cataloguesByHost) {
|
|
300
|
+
const names = new Set([...catalogue.models.map((m) => m.id), ...catalogue.wireIds]);
|
|
301
|
+
for (const name of names) {
|
|
302
|
+
const hosts = map.get(name) ?? [];
|
|
303
|
+
if (!hosts.includes(host))
|
|
304
|
+
hosts.push(host);
|
|
305
|
+
map.set(name, hosts);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return map;
|
|
282
309
|
}
|
|
283
310
|
function extractProjectId(data) {
|
|
284
311
|
if (!data)
|
|
@@ -373,10 +400,10 @@ export async function fetchUserEmail(accessToken, fetchImpl = fetch, signal) {
|
|
|
373
400
|
return undefined;
|
|
374
401
|
}
|
|
375
402
|
}
|
|
376
|
-
/**
|
|
377
|
-
export async function
|
|
403
|
+
/** One host's catalogue, or null when the endpoint cannot be read. Never throws. */
|
|
404
|
+
export async function fetchHostCatalogue(accessToken, projectId, host, fetchImpl = fetch, signal) {
|
|
378
405
|
try {
|
|
379
|
-
const response = await fetchImpl(`${
|
|
406
|
+
const response = await fetchImpl(`${host}/${GOOGLE_OAUTH.apiVersion}:fetchAvailableModels`, {
|
|
380
407
|
method: "POST",
|
|
381
408
|
headers: { authorization: `Bearer ${accessToken}`, accept: "*/*", "content-type": "application/json", "user-agent": antigravityUserAgent() },
|
|
382
409
|
body: JSON.stringify({ project: projectId }),
|
|
@@ -384,9 +411,45 @@ export async function fetchAvailableModels(accessToken, projectId, fetchImpl = f
|
|
|
384
411
|
});
|
|
385
412
|
if (!response.ok)
|
|
386
413
|
return null;
|
|
387
|
-
return
|
|
414
|
+
return parseHostCatalogue(await response.json().catch(() => undefined));
|
|
388
415
|
}
|
|
389
416
|
catch {
|
|
390
417
|
return null;
|
|
391
418
|
}
|
|
392
419
|
}
|
|
420
|
+
/**
|
|
421
|
+
* The models a CCA account can reach, asked on every host that offers them (measured 2026-10-20:
|
|
422
|
+
* the prod host lists Gemini Pro models the daily host does not, and vice versa for some Flash
|
|
423
|
+
* tiers). Models are merged with the preferred host's row winning, and the returned host map is
|
|
424
|
+
* what routing uses to send a model to a host that actually offers it. Null when no host answered.
|
|
425
|
+
*/
|
|
426
|
+
export async function fetchAvailableModels(accessToken, projectId, opts = {}) {
|
|
427
|
+
const hosts = opts.hosts ?? GOOGLE_OAUTH.hosts;
|
|
428
|
+
const results = await Promise.all(hosts.map(async (host) => [host, await fetchHostCatalogue(accessToken, projectId, host, opts.fetchImpl ?? fetch, opts.signal)]));
|
|
429
|
+
const byHost = new Map();
|
|
430
|
+
for (const [host, catalogue] of results)
|
|
431
|
+
if (catalogue)
|
|
432
|
+
byHost.set(host, catalogue);
|
|
433
|
+
if (byHost.size === 0)
|
|
434
|
+
return null;
|
|
435
|
+
// Host order is preference order (daily first), so the first host to list a model keeps its row
|
|
436
|
+
// and a failing host simply contributes nothing to the merge.
|
|
437
|
+
const models = [];
|
|
438
|
+
const seen = new Set();
|
|
439
|
+
for (const catalogue of byHost.values()) {
|
|
440
|
+
for (const model of catalogue.models) {
|
|
441
|
+
if (seen.has(model.id))
|
|
442
|
+
continue;
|
|
443
|
+
seen.add(model.id);
|
|
444
|
+
models.push(model);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return { models, hosts: buildHostMap(byHost), byHost };
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* The host a model's request should go to: the first host in `hosts` that offers the resolved wire
|
|
451
|
+
* id or the base id, else `fallback`. An unknown model is not hidden — it goes to the default only.
|
|
452
|
+
*/
|
|
453
|
+
export function hostForModel(hosts, model, wire, fallback) {
|
|
454
|
+
return hosts.get(wire)?.[0] ?? hosts.get(model)?.[0] ?? fallback;
|
|
455
|
+
}
|
|
@@ -14,20 +14,50 @@
|
|
|
14
14
|
// vendor sent. The antigravity path picks the account the conversation is sticky to and moves to the
|
|
15
15
|
// next one only on a refusal that has not reached the client (§4 Credential pools).
|
|
16
16
|
import http from "node:http";
|
|
17
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
17
18
|
import { providerExhaustion } from "../../health.js";
|
|
18
19
|
import { CredentialPool, retryAfterMs } from "../../pool.js";
|
|
19
20
|
import { credentialHeaderValues, redactErrorText } from "../../redact.js";
|
|
20
21
|
import { SseParser } from "../chatgpt/sse.js";
|
|
21
22
|
import { fetchWithRetry } from "../retry.js";
|
|
22
23
|
import { estimateTokens, formatSse, GoogleStreamMapper, toGeminiRequest } from "./translate.js";
|
|
23
|
-
import { createTransport, googleBaseUrl, resolveApiKey } from "./transport.js";
|
|
24
|
+
import { antigravityBaseUrl, createTransport, googleBaseUrl, resolveApiKey } from "./transport.js";
|
|
24
25
|
import { ThoughtSignatureStore } from "./thoughtSignatures.js";
|
|
25
26
|
import { conversationKey, serverToolNames, toolNameRestoreMap } from "../chatgpt/translate.js";
|
|
26
27
|
import { GoogleAccountPool } from "./accounts.js";
|
|
27
|
-
import { fetchAvailableModels, staticAntigravityModels } from "./antigravity.js";
|
|
28
|
+
import { fetchAvailableModels, hostForModel, resolveAntigravityWireModel, staticAntigravityModels, GOOGLE_OAUTH } from "./antigravity.js";
|
|
29
|
+
import { GoogleUsage } from "./usage.js";
|
|
30
|
+
import { ANTIGRAVITY_PROMPT_REFUSAL_MESSAGE, isAntigravityPromptRefusal } from "./prompt-refusal.js";
|
|
28
31
|
const PING_MS = 15_000;
|
|
29
32
|
/** How long the CCA model list is reused before asking again. */
|
|
30
33
|
const MODELS_CACHE_MS = 60 * 60 * 1000;
|
|
34
|
+
/**
|
|
35
|
+
* How often a turn asks again when Google says it has no capacity for the model, and how long it
|
|
36
|
+
* waits first. A few slower asks rather than the ten-in-a-row ladder: each refusal took seconds to
|
|
37
|
+
* arrive (41–103 s for ten on 2026-10-20), and capacity does not come back in 250 ms. Mutable only so
|
|
38
|
+
* tests can shorten the waits.
|
|
39
|
+
*/
|
|
40
|
+
export const capacityRetry = { delaysMs: [3_000, 6_000] };
|
|
41
|
+
/**
|
|
42
|
+
* Cloud Code Assist saying the **model** has no capacity on its servers — not the account, not its
|
|
43
|
+
* quota. Measured 2026-10-20 on the daily host for `gemini-3.8-flash-high`: HTTP 503,
|
|
44
|
+
* `{"error":{"code":503,"status":"UNAVAILABLE","message":"No capacity available for model … on the
|
|
45
|
+
* server","details":[{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":
|
|
46
|
+
* "MODEL_CAPACITY_EXHAUSTED","domain":"cloudcode-pa.googleapis.com","metadata":{"model":…,
|
|
47
|
+
* "OVERLOADED_TOO_MANY_RETRIES_PER_REQUEST":"true",…}}]}}`. Recognised by the ErrorInfo reason alone;
|
|
48
|
+
* any other 503 keeps the ordinary transient handling.
|
|
49
|
+
*/
|
|
50
|
+
export function isModelCapacityExhausted(status, text) {
|
|
51
|
+
if (status !== 503)
|
|
52
|
+
return false;
|
|
53
|
+
try {
|
|
54
|
+
const details = JSON.parse(text)?.error?.details;
|
|
55
|
+
return Array.isArray(details) && details.some((d) => typeof d === "object" && d !== null && d.reason === "MODEL_CAPACITY_EXHAUSTED");
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
31
61
|
function anthropicError(status, type, message) {
|
|
32
62
|
return { status, body: JSON.stringify({ type: "error", error: { type, message } }) };
|
|
33
63
|
}
|
|
@@ -113,6 +143,8 @@ export class GoogleAdapter {
|
|
|
113
143
|
cfg;
|
|
114
144
|
log;
|
|
115
145
|
accounts;
|
|
146
|
+
/** Per-account quota, for the status bar's limits row (`GET /api/limits`). */
|
|
147
|
+
usage;
|
|
116
148
|
/** Cooldowns and conversation stickiness, shared with the proxy's other credential pools. */
|
|
117
149
|
pool;
|
|
118
150
|
lastInputByKey = new Map();
|
|
@@ -124,12 +156,57 @@ export class GoogleAdapter {
|
|
|
124
156
|
verifyUrls = new Map();
|
|
125
157
|
modelsCache = null;
|
|
126
158
|
modelsInFlight = null;
|
|
159
|
+
/** When the last listing attempt failed, so a turn does not retry an unreadable catalogue every time. */
|
|
160
|
+
modelsFailedAt = Number.NEGATIVE_INFINITY;
|
|
161
|
+
/** The fetch the adapter's side calls use (model listing, and the account store); injectable for tests. */
|
|
162
|
+
fetchImpl;
|
|
127
163
|
constructor(name, cfg, home, log, pool = new CredentialPool(), fetchImpl) {
|
|
128
164
|
this.name = name;
|
|
129
165
|
this.cfg = cfg;
|
|
130
166
|
this.log = log;
|
|
131
167
|
this.pool = pool;
|
|
168
|
+
this.fetchImpl = fetchImpl;
|
|
132
169
|
this.accounts = new GoogleAccountPool({ home, log, ...(fetchImpl ? { fetch: fetchImpl } : {}) });
|
|
170
|
+
// Quota: each Cloud Code Assist host counts its own, so the limits row reads every host a
|
|
171
|
+
// selected model lives on (resolved lazily from the cached model catalogue below).
|
|
172
|
+
this.usage = new GoogleUsage(this.accounts, {
|
|
173
|
+
baseUrl: antigravityBaseUrl(cfg.url),
|
|
174
|
+
hosts: () => this.quotaHosts(),
|
|
175
|
+
warn: (message) => log.warn(message),
|
|
176
|
+
...(fetchImpl ? { fetch: fetchImpl } : {}),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* The hosts whose quota the limits row must read: one per host that offers a model this provider
|
|
181
|
+
* routes (the configured model list, or the discovered catalogue when it lists none). A pinned
|
|
182
|
+
* `url` is the only host, because generation never leaves it.
|
|
183
|
+
*/
|
|
184
|
+
quotaHosts() {
|
|
185
|
+
if (this.cfg.url)
|
|
186
|
+
return [antigravityBaseUrl(this.cfg.url)];
|
|
187
|
+
const hosts = this.modelsCache?.hosts;
|
|
188
|
+
// Without a catalogue we cannot tell which host the models go to; read the default one only,
|
|
189
|
+
// rather than showing an unused host's numbers.
|
|
190
|
+
if (!hosts)
|
|
191
|
+
return [GOOGLE_OAUTH.dailyApi];
|
|
192
|
+
const models = this.cfg.models ?? [];
|
|
193
|
+
// No configured model list means the provider routes whatever the client asks for; both hosts
|
|
194
|
+
// are then in use in principle, so both quotas are read.
|
|
195
|
+
if (models.length === 0)
|
|
196
|
+
return [...GOOGLE_OAUTH.hosts];
|
|
197
|
+
const fallback = antigravityBaseUrl(this.cfg.url);
|
|
198
|
+
const used = [];
|
|
199
|
+
for (const entry of models) {
|
|
200
|
+
// The host the model is actually sent to, not every host that lists it: a shared model goes
|
|
201
|
+
// to the preferred (daily) host, so only that host's quota is spent by this provider. The
|
|
202
|
+
// default effort stands in for the configured model because the config carries no per-turn
|
|
203
|
+
// effort; its tier's host is the representative one.
|
|
204
|
+
const wire = resolveAntigravityWireModel(entry.id, this.cfg.defaultEffort).wire;
|
|
205
|
+
const host = hostForModel(hosts, entry.id, wire, fallback);
|
|
206
|
+
if (!used.includes(host))
|
|
207
|
+
used.push(host);
|
|
208
|
+
}
|
|
209
|
+
return used.length ? used : [GOOGLE_OAUTH.dailyApi];
|
|
133
210
|
}
|
|
134
211
|
describeAuth() {
|
|
135
212
|
const all = this.accounts.summaries();
|
|
@@ -161,6 +238,15 @@ export class GoogleAdapter {
|
|
|
161
238
|
};
|
|
162
239
|
});
|
|
163
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Each account's quota groups by owner id, for the status bar's limits row. Never a token; a
|
|
243
|
+
* failed lookup keeps that account's last good groups.
|
|
244
|
+
*/
|
|
245
|
+
usageSnapshot() {
|
|
246
|
+
// Knowing which hosts hold the selected models needs the catalogue; a cold cache would read
|
|
247
|
+
// daily only and show the wrong host's numbers. The listing is cached for an hour.
|
|
248
|
+
return this.ensureHostMap().then(() => this.usage.snapshot());
|
|
249
|
+
}
|
|
164
250
|
/**
|
|
165
251
|
* Ask Google again whether an account still needs its verification page (dashboard action). The
|
|
166
252
|
* mark otherwise clears only when the account next answers a turn, so a person who had just
|
|
@@ -173,7 +259,7 @@ export class GoogleAdapter {
|
|
|
173
259
|
return "unknown";
|
|
174
260
|
const model = this.cfg.models?.[0]?.id ?? staticAntigravityModels()[0].id;
|
|
175
261
|
const transport = createTransport("antigravity", this.cfg.url ? { url: this.cfg.url } : {});
|
|
176
|
-
const built = transport.build({ contents: [{ role: "user", parts: [{ text: "ping" }] }], generationConfig: { maxOutputTokens: 16 } }, { model, stream: false, antigravity: { accessToken: credential.accessToken, projectId: credential.projectId, conversationKey: `recheck:${ownerId}` } });
|
|
262
|
+
const built = transport.build({ contents: [{ role: "user", parts: [{ text: "ping" }] }], generationConfig: { maxOutputTokens: 16 } }, { model, stream: false, antigravity: { accessToken: credential.accessToken, projectId: credential.projectId, conversationKey: `recheck:${ownerId}` }, antigravityBaseUrl: this.hostFor(model, undefined) });
|
|
177
263
|
try {
|
|
178
264
|
const upstream = await fetch(built.url, { method: "POST", headers: built.headers, body: built.body, signal: AbortSignal.timeout(30_000) });
|
|
179
265
|
const text = await upstream.text().catch(() => "");
|
|
@@ -216,15 +302,53 @@ export class GoogleAdapter {
|
|
|
216
302
|
}
|
|
217
303
|
async fetchModelsOnce() {
|
|
218
304
|
const credential = await this.anyCredential();
|
|
305
|
+
// No account is not a failed listing: a sign-in must take effect at once, not an hour later.
|
|
219
306
|
if (credential instanceof Error)
|
|
220
307
|
return null;
|
|
221
|
-
|
|
222
|
-
|
|
308
|
+
// Both Cloud Code Assist hosts are asked; one host failing must not hide the other's models.
|
|
309
|
+
const hosts = this.cfg.url ? [antigravityBaseUrl(this.cfg.url)] : GOOGLE_OAUTH.hosts;
|
|
310
|
+
const catalogue = await fetchAvailableModels(credential.accessToken, credential.projectId, {
|
|
311
|
+
hosts,
|
|
312
|
+
...(this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}),
|
|
313
|
+
});
|
|
314
|
+
if (!catalogue) {
|
|
223
315
|
this.log.warn(`google ${this.name}: fetchAvailableModels returned nothing; falling back to the static list`);
|
|
316
|
+
// A cold host is not asked again on every turn while it is unreadable.
|
|
317
|
+
this.modelsFailedAt = Date.now();
|
|
224
318
|
return staticAntigravityModels();
|
|
225
319
|
}
|
|
226
|
-
this.modelsCache = { at: Date.now(), models };
|
|
227
|
-
|
|
320
|
+
this.modelsCache = { at: Date.now(), models: catalogue.models, hosts: catalogue.hosts };
|
|
321
|
+
if (catalogue.byHost.size < hosts.length) {
|
|
322
|
+
const answered = [...catalogue.byHost.keys()].map((h) => new URL(h).host);
|
|
323
|
+
const missing = hosts.filter((h) => !catalogue.byHost.has(h)).map((h) => new URL(h).host);
|
|
324
|
+
this.log.warn(`google ${this.name}: fetchAvailableModels answered on ${answered.join(", ")}; ${missing.join(", ")} failed, its models are hidden`);
|
|
325
|
+
}
|
|
326
|
+
return catalogue.models;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Make sure the host map is loaded, within the same hour-long cache as `fetchModels`. A listing
|
|
330
|
+
* that just failed is not retried on every turn; the default host is used meanwhile.
|
|
331
|
+
*/
|
|
332
|
+
async ensureHostMap() {
|
|
333
|
+
if (this.modelsCache || this.cfg.url)
|
|
334
|
+
return;
|
|
335
|
+
if (!this.accounts.signedIn())
|
|
336
|
+
return;
|
|
337
|
+
if (Date.now() - this.modelsFailedAt < MODELS_CACHE_MS)
|
|
338
|
+
return;
|
|
339
|
+
await this.fetchModels();
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* The host a turn for `model` must go to: the one that offers its resolved wire id, preferring
|
|
343
|
+
* daily when both do, else the provider's default. Unknown models and an unread catalogue fall
|
|
344
|
+
* back to the default, exactly as before the second host existed.
|
|
345
|
+
*/
|
|
346
|
+
hostFor(model, effort) {
|
|
347
|
+
const fallback = antigravityBaseUrl(this.cfg.url);
|
|
348
|
+
if (this.cfg.url || !this.modelsCache)
|
|
349
|
+
return fallback;
|
|
350
|
+
const wire = resolveAntigravityWireModel(model, effort).wire;
|
|
351
|
+
return hostForModel(this.modelsCache.hosts, model, wire, fallback);
|
|
228
352
|
}
|
|
229
353
|
/** For side calls (models, project refresh) with no conversation: a usable account, else any. */
|
|
230
354
|
async anyCredential() {
|
|
@@ -257,13 +381,23 @@ export class GoogleAdapter {
|
|
|
257
381
|
const caps = effortLevels ? { effortLevels } : {};
|
|
258
382
|
// A request that names no effort gets the provider's configured one, as the ChatGPT one does.
|
|
259
383
|
const useEffort = effort ?? this.cfg.defaultEffort;
|
|
384
|
+
// A model that only one CCA host offers must be sent there. The catalogue is the only source of
|
|
385
|
+
// that routing, so a cold cache (first turn after a restart) is filled before the turn is built.
|
|
386
|
+
// It is a no-op within the hour after that, and a failure silently keeps the default host.
|
|
387
|
+
if (this.cfg.auth === "antigravity")
|
|
388
|
+
await this.ensureHostMap();
|
|
389
|
+
const host = this.cfg.auth === "antigravity" ? this.hostFor(model, useEffort) : undefined;
|
|
390
|
+
// Thought signatures are per host: CCA runs its state separately on each, and replaying one
|
|
391
|
+
// host's signature to the other is refused (see thoughtSignatures.ts). The API-key path keeps
|
|
392
|
+
// the unscoped store.
|
|
393
|
+
const signatures = host ? this.thoughtSignatures.scope(host) : this.thoughtSignatures;
|
|
260
394
|
const upstreamRequest = toGeminiRequest(json, {
|
|
261
395
|
model,
|
|
262
396
|
...(useEffort ? { effort: useEffort } : {}),
|
|
263
397
|
caps,
|
|
264
398
|
...(this.cfg.identity === undefined ? {} : { identity: this.cfg.identity }),
|
|
265
399
|
...(this.cfg.instructionsAppend ? { instructionsAppend: this.cfg.instructionsAppend } : {}),
|
|
266
|
-
thoughtSignatures:
|
|
400
|
+
thoughtSignatures: signatures,
|
|
267
401
|
// The CCA path never adds the sentinel: the signature goes back on the exact functionCall the
|
|
268
402
|
// model signed (or nothing at all), rather than a fabricated validator bypass.
|
|
269
403
|
signatureSentinel: this.cfg.auth === "api-key",
|
|
@@ -275,7 +409,7 @@ export class GoogleAdapter {
|
|
|
275
409
|
const startInput = Math.max(estimateTokens(json), this.lastInputByKey.get(key) ?? 0);
|
|
276
410
|
const transport = createTransport(this.cfg.auth, { ...(this.cfg.apiKey ? { apiKey: this.cfg.apiKey } : {}), ...(this.cfg.url ? { url: this.cfg.url } : {}) });
|
|
277
411
|
if (this.cfg.auth === "antigravity") {
|
|
278
|
-
return this.handleAntigravity(res, json, upstreamRequest, transport, model, useEffort, wantStream, key, startInput);
|
|
412
|
+
return this.handleAntigravity(res, json, upstreamRequest, transport, model, useEffort, wantStream, key, startInput, host, signatures);
|
|
279
413
|
}
|
|
280
414
|
const apiKey = resolveApiKey(this.cfg.apiKey);
|
|
281
415
|
if (!apiKey) {
|
|
@@ -331,19 +465,23 @@ export class GoogleAdapter {
|
|
|
331
465
|
* and its failure handling mirror the ChatGPT pool (accounts.ts), because the cache cost of moving
|
|
332
466
|
* a conversation off its account is the same (§4).
|
|
333
467
|
*/
|
|
334
|
-
async handleAntigravity(res, json, upstreamRequest, transport, model, effort, wantStream, key, startInput) {
|
|
468
|
+
async handleAntigravity(res, json, upstreamRequest, transport, model, effort, wantStream, key, startInput, host, signatures) {
|
|
335
469
|
const controller = new AbortController();
|
|
336
470
|
const onClose = () => controller.abort();
|
|
337
471
|
res.on("close", onClose);
|
|
338
472
|
const sent = await this.sendToAnAccount({
|
|
339
473
|
conversation: key,
|
|
340
474
|
signal: controller.signal,
|
|
475
|
+
system: upstreamRequest.systemInstruction,
|
|
476
|
+
model,
|
|
477
|
+
host,
|
|
341
478
|
build: (credential) => {
|
|
342
479
|
const built = transport.build(upstreamRequest, {
|
|
343
480
|
model,
|
|
344
481
|
stream: wantStream,
|
|
345
482
|
...(effort ? { effort } : {}),
|
|
346
483
|
antigravity: { accessToken: credential.accessToken, projectId: credential.projectId, conversationKey: key },
|
|
484
|
+
antigravityBaseUrl: host,
|
|
347
485
|
});
|
|
348
486
|
return { built, secrets: [credential.accessToken, ...credentialHeaderValues(Object.entries(built.headers))] };
|
|
349
487
|
},
|
|
@@ -352,6 +490,17 @@ export class GoogleAdapter {
|
|
|
352
490
|
res.off("close", onClose);
|
|
353
491
|
if (sent.kind === "aborted")
|
|
354
492
|
return { status: 0, bytes: 0, note: "client closed" };
|
|
493
|
+
if (sent.kind === "prompt-refused") {
|
|
494
|
+
const err = anthropicError(400, "invalid_request_error", ANTIGRAVITY_PROMPT_REFUSAL_MESSAGE);
|
|
495
|
+
return { kind: "prompt-incompatible", status: 400, bytes: 0, errorBody: err.body,
|
|
496
|
+
note: "upstream 429: suspected Antigravity SDK prompt refusal with remaining quota", refusal: "prompt-incompatible" };
|
|
497
|
+
}
|
|
498
|
+
if (sent.kind === "no-capacity") {
|
|
499
|
+
// Overloaded, so Claude Code asks again on its own when no fallback takes the turn.
|
|
500
|
+
const err = anthropicError(529, "overloaded_error", `Google provider: ${vendorMessage(sent.text)} — Google is out of capacity for this model (MODEL_CAPACITY_EXHAUSTED), asked ${sent.asks} times. This is Google's server capacity, not your account or its quota; try again shortly, or give the route a fallback.`);
|
|
501
|
+
return { kind: "model-capacity", status: 529, bytes: 0, errorBody: err.body,
|
|
502
|
+
note: `upstream 503 MODEL_CAPACITY_EXHAUSTED after ${sent.asks} asks; account not rested` };
|
|
503
|
+
}
|
|
355
504
|
if (sent.kind === "unreachable") {
|
|
356
505
|
const err = anthropicError(502, "api_error", `Google provider unreachable: ${sent.error.message}`);
|
|
357
506
|
if (!res.headersSent)
|
|
@@ -389,13 +538,13 @@ export class GoogleAdapter {
|
|
|
389
538
|
...(sent.kind === "no-account" ? { upstreamCalled: false, refusal: "no-account" } : {}),
|
|
390
539
|
...(sent.kind === "refused" ? { exhaustion: providerExhaustion(sent.status, sent.text, sent.retryAfterSeconds === undefined ? undefined : sent.retryAfterSeconds * 1000) } : {}), };
|
|
391
540
|
}
|
|
392
|
-
return this.streamBack(res, sent.upstream, transport, json, model, wantStream, key, startInput, controller, onClose);
|
|
541
|
+
return this.streamBack(res, sent.upstream, transport, json, model, wantStream, key, startInput, controller, onClose, signatures);
|
|
393
542
|
}
|
|
394
543
|
/** Stream (or buffer) an already-ok upstream response back to the client as Anthropic SSE. */
|
|
395
|
-
async streamBack(res, upstream, transport, json, model, wantStream, key, startInput, controller, onClose) {
|
|
544
|
+
async streamBack(res, upstream, transport, json, model, wantStream, key, startInput, controller, onClose, signatures = this.thoughtSignatures) {
|
|
396
545
|
const mapper = new GoogleStreamMapper(model, startInput, toolNameRestoreMap(json), (info) => {
|
|
397
546
|
if (info.signature)
|
|
398
|
-
|
|
547
|
+
signatures.set(info.id, info.signature);
|
|
399
548
|
});
|
|
400
549
|
const parser = new SseParser();
|
|
401
550
|
const reader = upstream.body.getReader();
|
|
@@ -503,6 +652,9 @@ export class GoogleAdapter {
|
|
|
503
652
|
* - 401, or a 403 that reads as a credential refusal: one refresh of that account and a replay;
|
|
504
653
|
* refused again with a 401, the account is marked for sign-in.
|
|
505
654
|
* - 429 and 402: the account rests until its stated reset (or a default), and the next takes over.
|
|
655
|
+
* - A 503 `MODEL_CAPACITY_EXHAUSTED`: no rest and no other account — the model is out of capacity
|
|
656
|
+
* on Google's side, not this account. The same account is asked again after `capacityRetry`'s
|
|
657
|
+
* waits, then the turn ends unwritten so the proxy can try a route fallback (§5, 2026-10-20).
|
|
506
658
|
* - Any other 403, 5xx, or no connection: a short rest, and the next account.
|
|
507
659
|
* - Anything else is the request's fault; another account would refuse it the same way.
|
|
508
660
|
*
|
|
@@ -514,6 +666,7 @@ export class GoogleAdapter {
|
|
|
514
666
|
const refreshed = new Set();
|
|
515
667
|
let last = null;
|
|
516
668
|
let lastThrown = null;
|
|
669
|
+
let capacityAsks = 0;
|
|
517
670
|
for (;;) {
|
|
518
671
|
const credential = await this.pickAccount(spec.conversation, tried);
|
|
519
672
|
if (credential === "none")
|
|
@@ -536,7 +689,7 @@ export class GoogleAdapter {
|
|
|
536
689
|
headers: built.headers,
|
|
537
690
|
body: built.body,
|
|
538
691
|
signal: spec.signal,
|
|
539
|
-
}, { log: (line) => this.log.info(`google ${this.name}: ${line}`) });
|
|
692
|
+
}, { log: (line) => this.log.info(`google ${this.name}: ${line}`), stopOn: isModelCapacityExhausted });
|
|
540
693
|
}
|
|
541
694
|
catch (e) {
|
|
542
695
|
if (spec.signal.aborted)
|
|
@@ -557,6 +710,30 @@ export class GoogleAdapter {
|
|
|
557
710
|
const safeText = redactErrorText(text, secrets);
|
|
558
711
|
const status = upstream.status;
|
|
559
712
|
this.log.warn(`google ${this.name}: account ${credential.ownerId.slice(0, 8)} answered ${status}: ${safeText.slice(0, 400)}`);
|
|
713
|
+
// The model, not the account, is out of capacity: resting the account refused every other turn
|
|
714
|
+
// with "all accounts resting" for its ten seconds (10 times in 3 minutes, 2026-10-20), and
|
|
715
|
+
// another account would meet the same model on the same host. So nothing is charged to it.
|
|
716
|
+
if (isModelCapacityExhausted(status, text)) {
|
|
717
|
+
capacityAsks++;
|
|
718
|
+
const delay = capacityRetry.delaysMs[capacityAsks - 1];
|
|
719
|
+
if (delay === undefined)
|
|
720
|
+
return { kind: "no-capacity", text: safeText, asks: capacityAsks };
|
|
721
|
+
this.log.info(`google ${this.name}: ${spec.model} out of capacity (ask ${capacityAsks}); asking again in ${delay}ms without resting the account`);
|
|
722
|
+
try {
|
|
723
|
+
await sleep(delay, undefined, { signal: spec.signal });
|
|
724
|
+
}
|
|
725
|
+
catch {
|
|
726
|
+
return { kind: "aborted" };
|
|
727
|
+
}
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
// The generic 429 is also real exhaustion. Only fresh quota evidence for this
|
|
731
|
+
// account/model permits a request-specific refusal; otherwise keep ordinary
|
|
732
|
+
// cooldown and account rotation. The proxy can still try a route fallback.
|
|
733
|
+
if (isAntigravityPromptRefusal(spec.system, status, text, upstream.headers.get("retry-after")) &&
|
|
734
|
+
this.usage.hasRemainingQuota(credential, spec.model, spec.host)) {
|
|
735
|
+
return { kind: "prompt-refused" };
|
|
736
|
+
}
|
|
560
737
|
// Not a rest and not a refresh: the account works again the moment its person finishes
|
|
561
738
|
// Google's page, so it stays in rotation and the next turn finds out.
|
|
562
739
|
const verifyUrl = status === 403 ? googleValidationUrl(text) : undefined;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const SDK_PARAGRAPH = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
2
|
+
const GENERIC_EXHAUSTED = "Resource has been exhausted (e.g. check quota).";
|
|
3
|
+
export const ANTIGRAVITY_PROMPT_REFUSAL_MESSAGE = `Google Antigravity rejected this Claude Agent SDK system prompt (upstream 429: ${GENERIC_EXHAUSTED}). ` +
|
|
4
|
+
"This request appears incompatible with Antigravity; retrying or waiting on this account is unlikely to fix it. " +
|
|
5
|
+
"Choose another provider for this Claude Code request.";
|
|
6
|
+
/**
|
|
7
|
+
* Measured 2026-10-16: the same Gemini 3.8 Flash / Claude Opus 5.5 request got 429
|
|
8
|
+
* with this standalone SDK paragraph and 200 without it. This is response-based
|
|
9
|
+
* classification, never a prompt rewrite or a prediction before Google answers.
|
|
10
|
+
* Keep any explicit quota/reset details on the ordinary rate-limit path. This shape
|
|
11
|
+
* alone also occurs on real exhaustion; callers must require fresh quota evidence.
|
|
12
|
+
*/
|
|
13
|
+
export function isAntigravityPromptRefusal(system, status, text, retryAfter) {
|
|
14
|
+
if (status !== 429 || retryAfter !== null)
|
|
15
|
+
return false;
|
|
16
|
+
if (!system?.parts.some(part => part.text?.split(/\r?\n\s*\r?\n/).some(paragraph => paragraph.trim() === SDK_PARAGRAPH)))
|
|
17
|
+
return false;
|
|
18
|
+
try {
|
|
19
|
+
const error = JSON.parse(text)?.error;
|
|
20
|
+
if (!error || typeof error !== "object" || Array.isArray(error))
|
|
21
|
+
return false;
|
|
22
|
+
const e = error;
|
|
23
|
+
return e.code === 429 && e.status === "RESOURCE_EXHAUSTED" && e.message === GENERIC_EXHAUSTED &&
|
|
24
|
+
(e.details === undefined || (Array.isArray(e.details) && e.details.length === 0));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -43,5 +43,18 @@ export class ThoughtSignatureStore {
|
|
|
43
43
|
this.map.delete(oldest);
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* A view whose keys carry the Cloud Code Assist host the signature came from. Cloud Code Assist
|
|
48
|
+
* runs on two hosts with separate state, and replaying a signature minted on one to the other is
|
|
49
|
+
* what the backend refuses (TYPE_BYTES), so a conversation that moves between hosts must not
|
|
50
|
+
* find the other host's signature. The shared store keeps the LRU bound across both views.
|
|
51
|
+
*/
|
|
52
|
+
scope(host) {
|
|
53
|
+
const prefix = `${host}\u0000`;
|
|
54
|
+
return {
|
|
55
|
+
get: (id) => this.get(prefix + id),
|
|
56
|
+
set: (id, signature) => this.set(prefix + id, signature),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
46
59
|
get size() { return this.map.size; }
|
|
47
60
|
}
|
|
@@ -77,7 +77,7 @@ export function createTransport(mode, opts) {
|
|
|
77
77
|
request,
|
|
78
78
|
};
|
|
79
79
|
return {
|
|
80
|
-
url: `${base}/${GOOGLE_OAUTH.apiVersion}:${method}${query}`,
|
|
80
|
+
url: `${(o.antigravityBaseUrl ? o.antigravityBaseUrl.replace(/\/+$/, "") : base)}/${GOOGLE_OAUTH.apiVersion}:${method}${query}`,
|
|
81
81
|
headers: {
|
|
82
82
|
"content-type": "application/json",
|
|
83
83
|
accept: o.stream ? "text/event-stream" : "application/json",
|