@eddyskywalker/dsh-chatgpt-subscription 0.2.2 → 0.2.4

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/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
2
  import * as LlmModule from "@deepseek-ai/dsh-llm";
3
- import { HarnessError, LlmAdapter, LlmError, ProviderRequestId, ReasoningEffortId, attributionHeaders, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
3
+ import { CallId, HarnessError, LlmAdapter, LlmError, ProviderRequestId, ReasoningEffortId, attributionHeaders, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
4
4
  import { WebError } from "@deepseek-ai/dsh-web";
5
5
  import { createUserMessage } from "@deepseek-ai/dsh-llm/message";
6
6
  import { defineTool } from "@deepseek-ai/dsh-tools";
@@ -14,6 +14,7 @@ import fs, { constants } from "node:fs";
14
14
  import fsPromises, { chmod, lstat, mkdir, open, rename, stat, unlink } from "node:fs/promises";
15
15
  import os, { homedir } from "node:os";
16
16
  import path, { dirname, join } from "node:path";
17
+ import { isDeepStrictEqual } from "node:util";
17
18
  import { URL as URL$1, URLSearchParams as URLSearchParams$1 } from "node:url";
18
19
  //#region src/compat.ts
19
20
  /**
@@ -3191,16 +3192,18 @@ const DEFAULT_ACCOUNT = "oauth";
3191
3192
  * `security` command-line tool. The payload is encrypted at rest by the
3192
3193
  * Keychain, so this store reports itself as encrypted like Windows DPAPI.
3193
3194
  */
3194
- var MacKeychainTokenStore = class {
3195
+ var MacKeychainCredentialStore = class {
3195
3196
  service;
3196
3197
  account;
3198
+ parse;
3197
3199
  storage = {
3198
3200
  kind: "macos-keychain",
3199
3201
  encrypted: true
3200
3202
  };
3201
- constructor(service = DEFAULT_SERVICE, account = DEFAULT_ACCOUNT) {
3203
+ constructor(service, account, parse) {
3202
3204
  this.service = service;
3203
3205
  this.account = account;
3206
+ this.parse = parse;
3204
3207
  if (process.platform !== "darwin") throw new Error("macOS Keychain storage requires macOS");
3205
3208
  }
3206
3209
  async load() {
@@ -3216,7 +3219,7 @@ var MacKeychainTokenStore = class {
3216
3219
  if (result.code !== 0) throw new Error("Keychain credential read failed");
3217
3220
  try {
3218
3221
  const payload = result.stdout.replace(/\r?\n$/, "");
3219
- return parseStoredCredentials(JSON.parse(payload));
3222
+ return this.parse(JSON.parse(payload));
3220
3223
  } catch {
3221
3224
  throw new Error("Keychain credential payload is invalid");
3222
3225
  }
@@ -3244,6 +3247,11 @@ var MacKeychainTokenStore = class {
3244
3247
  if (result.code !== 0 && result.code !== 44) throw new Error("Keychain credential deletion failed");
3245
3248
  }
3246
3249
  };
3250
+ var MacKeychainTokenStore = class extends MacKeychainCredentialStore {
3251
+ constructor(service = DEFAULT_SERVICE, account = DEFAULT_ACCOUNT) {
3252
+ super(service, account, parseStoredCredentials);
3253
+ }
3254
+ };
3247
3255
  function runSecurity(args) {
3248
3256
  return new Promise((resolve, reject) => {
3249
3257
  const child = spawn("security", args, {
@@ -3388,8 +3396,16 @@ $cipher = [Security.Cryptography.ProtectedData]::Protect($bytes, $null, [Securit
3388
3396
  $directory = [IO.Path]::GetDirectoryName($path)
3389
3397
  [IO.Directory]::CreateDirectory($directory) | Out-Null
3390
3398
  $temporary = $path + '.tmp-' + [Guid]::NewGuid().ToString('N')
3391
- [IO.File]::WriteAllBytes($temporary, $cipher)
3392
- if ([IO.File]::Exists($path)) { [IO.File]::Replace($temporary, $path, $null) } else { [IO.File]::Move($temporary, $path) }
3399
+ try {
3400
+ [IO.File]::WriteAllBytes($temporary, $cipher)
3401
+ if ([IO.File]::Exists($path)) {
3402
+ [IO.File]::Replace($temporary, $path, [System.Management.Automation.Language.NullString]::Value)
3403
+ } else {
3404
+ [IO.File]::Move($temporary, $path)
3405
+ }
3406
+ } finally {
3407
+ if ([IO.File]::Exists($temporary)) { [IO.File]::Delete($temporary) }
3408
+ }
3393
3409
  `;
3394
3410
  const UNPROTECT_SCRIPT = String.raw`
3395
3411
  $ErrorActionPreference = 'Stop'
@@ -3408,14 +3424,16 @@ if ([IO.File]::Exists($path)) { [IO.File]::Delete($path) }
3408
3424
  function defaultDpapiCredentialPath() {
3409
3425
  return join(process.env.DSH_HOME?.trim() || join(homedir(), ".dsh"), "storages", "dsh-chatgpt-subscription", "oauth.dpapi");
3410
3426
  }
3411
- var WindowsDpapiTokenStore = class {
3427
+ var WindowsDpapiCredentialStore = class {
3412
3428
  path;
3429
+ parse;
3413
3430
  storage = {
3414
3431
  kind: "windows-dpapi",
3415
3432
  encrypted: true
3416
3433
  };
3417
- constructor(path = defaultDpapiCredentialPath()) {
3434
+ constructor(path, parse) {
3418
3435
  this.path = path;
3436
+ this.parse = parse;
3419
3437
  if (process.platform !== "win32") throw new Error("Windows DPAPI storage requires Windows");
3420
3438
  if (dirname(path) === path) throw new Error("invalid DPAPI credential path");
3421
3439
  }
@@ -3424,7 +3442,7 @@ var WindowsDpapiTokenStore = class {
3424
3442
  if (result.code === 3) return null;
3425
3443
  if (result.code !== 0) throw new Error("DPAPI credential read failed");
3426
3444
  try {
3427
- return parseStoredCredentials(JSON.parse(result.stdout));
3445
+ return this.parse(JSON.parse(result.stdout));
3428
3446
  } catch {
3429
3447
  throw new Error("DPAPI credential payload is invalid");
3430
3448
  }
@@ -3436,6 +3454,11 @@ var WindowsDpapiTokenStore = class {
3436
3454
  if ((await runPowerShell(CLEAR_SCRIPT, this.path, "")).code !== 0) throw new Error("DPAPI credential deletion failed");
3437
3455
  }
3438
3456
  };
3457
+ var WindowsDpapiTokenStore = class extends WindowsDpapiCredentialStore {
3458
+ constructor(path = defaultDpapiCredentialPath()) {
3459
+ super(path, parseStoredCredentials);
3460
+ }
3461
+ };
3439
3462
  function runPowerShell(script, path, stdin) {
3440
3463
  return new Promise((resolve, reject) => {
3441
3464
  const child = spawn("powershell.exe", [
@@ -3856,6 +3879,96 @@ const MODELS = [
3856
3879
  }
3857
3880
  ];
3858
3881
  //#endregion
3882
+ //#region src/host/credential-store-secret-service.ts
3883
+ const UNAVAILABLE = "Linux encrypted credential storage requires secret-tool (libsecret) and an unlocked Secret Service keyring.";
3884
+ /** Secrets travel over stdin/stdout; command arguments contain only lookup attributes. */
3885
+ var SecretServiceCredentialStore = class {
3886
+ service;
3887
+ account;
3888
+ parse;
3889
+ constructor(service, account, parse) {
3890
+ this.service = service;
3891
+ this.account = account;
3892
+ this.parse = parse;
3893
+ }
3894
+ attributes() {
3895
+ return [
3896
+ "service",
3897
+ this.service,
3898
+ "account",
3899
+ this.account
3900
+ ];
3901
+ }
3902
+ async load() {
3903
+ const result = await runSecretTool(["lookup", ...this.attributes()]);
3904
+ if (result.code === 1 && !result.hasStderr && result.stdout === "") return null;
3905
+ if (result.code !== 0) throw new Error(UNAVAILABLE);
3906
+ try {
3907
+ return this.parse(JSON.parse(result.stdout));
3908
+ } catch {
3909
+ throw new Error("Secret Service credential payload is invalid");
3910
+ }
3911
+ }
3912
+ async save(value) {
3913
+ const payload = JSON.stringify(value);
3914
+ if (Buffer.byteLength(payload, "utf8") >= 8192) throw new Error("Secret Service credential payload is too large");
3915
+ if ((await runSecretTool([
3916
+ "store",
3917
+ "--label=DSH Antigravity OAuth",
3918
+ ...this.attributes()
3919
+ ], payload)).code !== 0) throw new Error(UNAVAILABLE);
3920
+ }
3921
+ async clear() {
3922
+ const result = await runSecretTool(["clear", ...this.attributes()]);
3923
+ if (result.code !== 0 && !(result.code === 1 && !result.hasStderr)) throw new Error(UNAVAILABLE);
3924
+ }
3925
+ };
3926
+ function runSecretTool(args, stdin = "") {
3927
+ return new Promise((resolve, reject) => {
3928
+ const child = spawn("secret-tool", args, {
3929
+ stdio: [
3930
+ "pipe",
3931
+ "pipe",
3932
+ "pipe"
3933
+ ],
3934
+ windowsHide: true
3935
+ });
3936
+ let stdout = "";
3937
+ let stderrLength = 0;
3938
+ let settled = false;
3939
+ const fail = () => {
3940
+ if (settled) return;
3941
+ settled = true;
3942
+ clearTimeout(timer);
3943
+ child.kill();
3944
+ reject(/* @__PURE__ */ new Error(UNAVAILABLE));
3945
+ };
3946
+ const timer = setTimeout(fail, 1e4);
3947
+ child.stdout.setEncoding("utf8");
3948
+ child.stdout.on("data", (chunk) => {
3949
+ stdout += chunk;
3950
+ if (stdout.length > 1 << 20) fail();
3951
+ });
3952
+ child.stderr.on("data", (chunk) => {
3953
+ stderrLength += chunk.length;
3954
+ if (stderrLength > 1 << 20) fail();
3955
+ });
3956
+ child.once("error", fail);
3957
+ child.stdin.once("error", fail);
3958
+ child.once("close", (code) => {
3959
+ if (settled) return;
3960
+ settled = true;
3961
+ clearTimeout(timer);
3962
+ resolve({
3963
+ code: code ?? 1,
3964
+ stdout,
3965
+ hasStderr: stderrLength > 0
3966
+ });
3967
+ });
3968
+ child.stdin.end(stdin);
3969
+ });
3970
+ }
3971
+ //#endregion
3859
3972
  //#region src/host/antigravity/token-store.ts
3860
3973
  const ANTIGRAVITY_PREFERENCES_NAMESPACE = "dsh-antigravity";
3861
3974
  function registerAntigravityPreferenceStore(settings, fallbackStore = new FileModelSettingsStore()) {
@@ -3917,36 +4030,110 @@ function credentialPath() {
3917
4030
  function modelSettingsPath() {
3918
4031
  return path.join(dshHomeDir(), "storages", "antigravity-models.json");
3919
4032
  }
4033
+ function parseAntigravityCredentials(value) {
4034
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("Antigravity credential payload is invalid");
4035
+ const record = value;
4036
+ const credentials = {};
4037
+ for (const key of [
4038
+ "access",
4039
+ "access_token",
4040
+ "refresh",
4041
+ "refresh_token",
4042
+ "email",
4043
+ "projectId"
4044
+ ]) {
4045
+ if (record[key] === void 0) continue;
4046
+ if (typeof record[key] !== "string") throw new Error("Antigravity credential payload is invalid");
4047
+ credentials[key] = record[key];
4048
+ }
4049
+ for (const key of ["expires", "expires_at"]) {
4050
+ if (record[key] === void 0) continue;
4051
+ if (typeof record[key] !== "number" || !Number.isFinite(record[key])) throw new Error("Antigravity credential expiry is invalid");
4052
+ credentials[key] = record[key];
4053
+ }
4054
+ if (!(credentials.access || credentials.access_token || credentials.refresh || credentials.refresh_token)) throw new Error("Antigravity credential tokens are missing");
4055
+ return credentials;
4056
+ }
4057
+ function credentialAccount(filePath) {
4058
+ return createHash("sha256").update(path.resolve(filePath)).digest("hex");
4059
+ }
4060
+ function createCredentialBackend(filePath) {
4061
+ if (process.platform === "win32") return new WindowsDpapiCredentialStore(`${filePath}.dpapi`, parseAntigravityCredentials);
4062
+ if (process.platform === "darwin") return new MacKeychainCredentialStore("dsh-antigravity", credentialAccount(filePath), parseAntigravityCredentials);
4063
+ if (process.platform === "linux") return new SecretServiceCredentialStore("dsh-antigravity", credentialAccount(filePath), parseAntigravityCredentials);
4064
+ throw new Error("Antigravity encrypted credential storage requires Windows, macOS, or Linux.");
4065
+ }
4066
+ const credentialOperations = /* @__PURE__ */ new Map();
4067
+ /** Keeps the public API; filePath identifies the legacy JSON that is migrated on first use. */
3920
4068
  var FileCredentialStore = class {
3921
4069
  filePath;
3922
- constructor(filePath = credentialPath()) {
4070
+ backend;
4071
+ constructor(filePath = credentialPath(), backend = createCredentialBackend(filePath)) {
3923
4072
  this.filePath = filePath;
4073
+ this.backend = backend;
3924
4074
  }
3925
4075
  path() {
3926
- return this.filePath;
4076
+ if (process.platform === "win32") return `${this.filePath}.dpapi`;
4077
+ return `${process.platform === "darwin" ? "Keychain" : "Secret Service"}: dsh-antigravity/${credentialAccount(this.filePath)}`;
4078
+ }
4079
+ serialize(operation) {
4080
+ const key = path.resolve(this.filePath);
4081
+ const result = (credentialOperations.get(key) || Promise.resolve()).then(operation);
4082
+ const settled = result.then(() => void 0, () => void 0);
4083
+ credentialOperations.set(key, settled);
4084
+ settled.then(() => {
4085
+ if (credentialOperations.get(key) === settled) credentialOperations.delete(key);
4086
+ });
4087
+ return result;
3927
4088
  }
3928
- async read() {
4089
+ async removeLegacy() {
3929
4090
  try {
3930
- const content = await fsPromises.readFile(this.filePath, "utf8");
3931
- const parsed = JSON.parse(content);
3932
- if (typeof parsed === "object" && parsed !== null && ("access_token" in parsed || "access" in parsed)) return parsed;
3933
- return null;
3934
- } catch {
3935
- return null;
4091
+ await fsPromises.unlink(this.filePath);
4092
+ } catch (error) {
4093
+ if (error.code !== "ENOENT") throw new Error("Antigravity legacy credential removal failed");
3936
4094
  }
3937
4095
  }
3938
- async write(credentials) {
3939
- await fsPromises.mkdir(path.dirname(this.filePath), { recursive: true });
3940
- const tmp = `${this.filePath}.tmp.${Date.now()}`;
3941
- await fsPromises.writeFile(tmp, JSON.stringify(credentials, null, 2), "utf8");
3942
- await fsPromises.rename(tmp, this.filePath);
4096
+ async saveVerified(credentials) {
4097
+ await this.backend.save(credentials);
4098
+ if (!isDeepStrictEqual(await this.backend.load(), credentials)) throw new Error("Antigravity encrypted credential verification failed");
4099
+ await this.removeLegacy();
4100
+ }
4101
+ read() {
4102
+ return this.serialize(async () => {
4103
+ const current = await this.backend.load();
4104
+ if (current !== null) {
4105
+ await this.removeLegacy();
4106
+ return current;
4107
+ }
4108
+ let legacy;
4109
+ try {
4110
+ const stats = await fsPromises.lstat(this.filePath);
4111
+ if (!stats.isFile() || stats.isSymbolicLink()) throw new Error("Invalid credential file");
4112
+ if (process.getuid && stats.uid !== process.getuid()) throw new Error("Invalid credential owner");
4113
+ if (process.platform !== "win32") await fsPromises.chmod(this.filePath, 384);
4114
+ legacy = await fsPromises.readFile(this.filePath, "utf8");
4115
+ } catch (error) {
4116
+ if (error.code === "ENOENT") return null;
4117
+ throw new Error("Antigravity legacy credential read failed");
4118
+ }
4119
+ let credentials;
4120
+ try {
4121
+ credentials = parseAntigravityCredentials(JSON.parse(legacy));
4122
+ } catch {
4123
+ throw new Error("Antigravity legacy credential payload is invalid");
4124
+ }
4125
+ await this.saveVerified(credentials);
4126
+ return credentials;
4127
+ });
3943
4128
  }
3944
- async delete() {
3945
- try {
3946
- await fsPromises.unlink(this.filePath);
3947
- } catch (err) {
3948
- if (err.code !== "ENOENT") throw err;
3949
- }
4129
+ write(credentials) {
4130
+ return this.serialize(() => this.saveVerified(parseAntigravityCredentials(credentials)));
4131
+ }
4132
+ delete() {
4133
+ return this.serialize(async () => {
4134
+ await this.removeLegacy();
4135
+ await this.backend.clear();
4136
+ });
3950
4137
  }
3951
4138
  };
3952
4139
  var FileModelSettingsStore = class {
@@ -4064,9 +4251,9 @@ function extractProjectId(data) {
4064
4251
  }
4065
4252
  }
4066
4253
  }
4067
- async function listCloudAICompanionProjects(token) {
4254
+ async function listCloudAICompanionProjects(token, fetchFn = fetch) {
4068
4255
  for (const endpoint of endpointCandidates()) try {
4069
- const response = await fetch(`${endpoint}/v1internal:listCloudAICompanionProjects`, {
4256
+ const response = await fetchFn(`${endpoint}/v1internal:listCloudAICompanionProjects`, {
4070
4257
  method: "POST",
4071
4258
  headers: antigravityHeaders(token),
4072
4259
  body: JSON.stringify({}),
@@ -4076,7 +4263,7 @@ async function listCloudAICompanionProjects(token) {
4076
4263
  return extractProjectId(await response.json());
4077
4264
  } catch {}
4078
4265
  }
4079
- async function loadCodeAssist(token) {
4266
+ async function loadCodeAssist(token, fetchFn = fetch) {
4080
4267
  const cached = projectCache.get(token);
4081
4268
  if (cached && cached.expiresAt > Date.now()) return cached.projectId;
4082
4269
  const body = JSON.stringify({ metadata: {
@@ -4085,7 +4272,7 @@ async function loadCodeAssist(token) {
4085
4272
  pluginType: "GEMINI"
4086
4273
  } });
4087
4274
  for (const endpoint of endpointCandidates()) try {
4088
- const response = await fetch(`${endpoint}/v1internal:loadCodeAssist`, {
4275
+ const response = await fetchFn(`${endpoint}/v1internal:loadCodeAssist`, {
4089
4276
  method: "POST",
4090
4277
  headers: antigravityHeaders(token),
4091
4278
  body,
@@ -4100,7 +4287,7 @@ async function loadCodeAssist(token) {
4100
4287
  });
4101
4288
  return project;
4102
4289
  }
4103
- const listProj = await listCloudAICompanionProjects(token);
4290
+ const listProj = await listCloudAICompanionProjects(token, fetchFn);
4104
4291
  if (listProj) {
4105
4292
  projectCache.set(token, {
4106
4293
  projectId: listProj,
@@ -4110,9 +4297,9 @@ async function loadCodeAssist(token) {
4110
4297
  }
4111
4298
  } catch {}
4112
4299
  }
4113
- async function postJson(path, token, body) {
4300
+ async function postJson(path, token, body, fetchFn = fetch) {
4114
4301
  for (const endpoint of endpointCandidates()) try {
4115
- const response = await fetch(`${endpoint}${path}`, {
4302
+ const response = await fetchFn(`${endpoint}${path}`, {
4116
4303
  method: "POST",
4117
4304
  headers: jsonHeaders(token),
4118
4305
  body: JSON.stringify(body)
@@ -4175,16 +4362,16 @@ function parseCatalogModels(data) {
4175
4362
  }
4176
4363
  return list;
4177
4364
  }
4178
- async function fetchAccountQuota(store = new FileCredentialStore(), modelSettings) {
4179
- const { token, projectId: credentialProjectId } = await ensureApiKey(store);
4365
+ async function fetchAccountQuota(store = new FileCredentialStore(), modelSettings, fetchFn = fetch) {
4366
+ const { token, projectId: credentialProjectId } = await ensureApiKey(store, fetchFn);
4180
4367
  const [assistResult, summaryResult] = await Promise.all([postJson("/v1internal:loadCodeAssist", token, { metadata: {
4181
4368
  ideType: "ANTIGRAVITY",
4182
4369
  platform: "PLATFORM_UNSPECIFIED",
4183
4370
  pluginType: "GEMINI"
4184
- } }).catch(() => null), postJson("/v1internal:retrieveUserQuotaSummary", token, {}).catch(() => null)]);
4371
+ } }, fetchFn).catch(() => null), postJson("/v1internal:retrieveUserQuotaSummary", token, {}, fetchFn).catch(() => null)]);
4185
4372
  const discoveredProject = assistResult ? extractProjectId(assistResult.data) : void 0;
4186
4373
  const projectId = credentialProjectId || discoveredProject || "antigravity-default";
4187
- const modelsData = (await postJson("/v1internal:fetchAvailableModels", token, { project: projectId }).catch(() => null))?.data;
4374
+ const modelsData = (await postJson("/v1internal:fetchAvailableModels", token, { project: projectId }, fetchFn).catch(() => null))?.data;
4188
4375
  const { groups, description } = summaryResult ? parseQuotaSummary(summaryResult.data) : { groups: [] };
4189
4376
  const catalogModels = modelsData ? parseCatalogModels(modelsData) : [];
4190
4377
  const assistData = assistResult?.data || {};
@@ -4282,9 +4469,9 @@ function openBrowser(url) {
4282
4469
  });
4283
4470
  } catch {}
4284
4471
  }
4285
- async function getUserEmail(token) {
4472
+ async function getUserEmail(token, fetchFn = fetch) {
4286
4473
  try {
4287
- const response = await fetch("https://www.googleapis.com/oauth2/v1/userinfo?alt=json", { headers: { Authorization: `Bearer ${token}` } });
4474
+ const response = await fetchFn("https://www.googleapis.com/oauth2/v1/userinfo?alt=json", { headers: { Authorization: `Bearer ${token}` } });
4288
4475
  if (!response.ok) return void 0;
4289
4476
  const data = await response.json();
4290
4477
  return typeof data.email === "string" ? data.email : void 0;
@@ -4363,8 +4550,8 @@ function startCallbackServer(expectedState) {
4363
4550
  });
4364
4551
  });
4365
4552
  }
4366
- async function exchangeOAuthCode(code, verifier, callbackUrl) {
4367
- const tokenResponse = await fetch(TOKEN_URL, {
4553
+ async function exchangeOAuthCode(code, verifier, callbackUrl, fetchFn = fetch) {
4554
+ const tokenResponse = await fetchFn(TOKEN_URL, {
4368
4555
  method: "POST",
4369
4556
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
4370
4557
  body: new URLSearchParams$1({
@@ -4382,7 +4569,7 @@ async function exchangeOAuthCode(code, verifier, callbackUrl) {
4382
4569
  const accessToken = typeof tokenData.access_token === "string" ? tokenData.access_token : "";
4383
4570
  const expiresIn = typeof tokenData.expires_in === "number" ? tokenData.expires_in : 3600;
4384
4571
  if (!refreshToken) throw new Error("No refresh token received. Re-run login and allow offline access.");
4385
- const [email, discoveredProject] = await Promise.all([getUserEmail(accessToken), loadCodeAssist(accessToken).catch(() => void 0)]);
4572
+ const [email, discoveredProject] = await Promise.all([getUserEmail(accessToken, fetchFn), loadCodeAssist(accessToken, fetchFn).catch(() => void 0)]);
4386
4573
  return {
4387
4574
  refresh: refreshToken,
4388
4575
  refresh_token: refreshToken,
@@ -4394,7 +4581,7 @@ async function exchangeOAuthCode(code, verifier, callbackUrl) {
4394
4581
  email
4395
4582
  };
4396
4583
  }
4397
- async function beginWebLogin(store) {
4584
+ async function beginWebLogin(store, fetchFn = fetch) {
4398
4585
  if (webLoginFlow.status === "pending") return { ...webLoginFlow };
4399
4586
  const { verifier, challenge } = generatePKCE();
4400
4587
  const state = base64Url(randomBytes(32));
@@ -4420,7 +4607,7 @@ async function beginWebLogin(store) {
4420
4607
  try {
4421
4608
  const { code, state: returnedState } = await waitForCode();
4422
4609
  if (returnedState !== state) throw new Error("OAuth state mismatch");
4423
- const credentials = await exchangeOAuthCode(code, verifier, callbackUrl);
4610
+ const credentials = await exchangeOAuthCode(code, verifier, callbackUrl, fetchFn);
4424
4611
  await store.write(credentials);
4425
4612
  webLoginFlow.status = "complete";
4426
4613
  webLoginFlow.email = credentials.email;
@@ -4438,10 +4625,10 @@ async function beginWebLogin(store) {
4438
4625
  function getWebLoginStatus() {
4439
4626
  return { ...webLoginFlow };
4440
4627
  }
4441
- async function refreshAntigravityToken(credentials) {
4628
+ async function refreshAntigravityToken(credentials, fetchFn = fetch) {
4442
4629
  const refreshToken = credentials.refresh || credentials.refresh_token;
4443
4630
  if (!refreshToken) throw new Error("Missing Antigravity refresh token.");
4444
- const response = await fetch(TOKEN_URL, {
4631
+ const response = await fetchFn(TOKEN_URL, {
4445
4632
  method: "POST",
4446
4633
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
4447
4634
  body: new URLSearchParams$1({
@@ -4466,12 +4653,12 @@ async function refreshAntigravityToken(credentials) {
4466
4653
  expires_at: Date.now() + expiresIn * 1e3 - 300 * 1e3
4467
4654
  };
4468
4655
  }
4469
- async function ensureApiKey(store) {
4656
+ async function ensureApiKey(store, fetchFn = fetch) {
4470
4657
  let credentials = await store.read();
4471
4658
  if (!credentials) throw new Error("Not logged into Antigravity. Please log in from Settings > Antigravity.");
4472
4659
  const expires = credentials.expires || credentials.expires_at || 0;
4473
4660
  if (!(credentials.access || credentials.access_token) || expires <= Date.now() + 6e4) {
4474
- credentials = await refreshAntigravityToken(credentials);
4661
+ credentials = await refreshAntigravityToken(credentials, fetchFn);
4475
4662
  await store.write(credentials);
4476
4663
  }
4477
4664
  return {
@@ -4479,7 +4666,7 @@ async function ensureApiKey(store) {
4479
4666
  projectId: credentials.projectId
4480
4667
  };
4481
4668
  }
4482
- async function loginAndSave(store, signal, onUrl) {
4669
+ async function loginAndSave(store, signal, onUrl, fetchFn = fetch) {
4483
4670
  const { verifier, challenge } = generatePKCE();
4484
4671
  const state = base64Url(randomBytes(32));
4485
4672
  const { server, waitForCode } = await startCallbackServer(state);
@@ -4501,7 +4688,7 @@ async function loginAndSave(store, signal, onUrl) {
4501
4688
  if (signal?.aborted) throw new Error("OAuth login aborted");
4502
4689
  const { code, state: returnedState } = await waitForCode();
4503
4690
  if (returnedState !== state) throw new Error("OAuth state mismatch");
4504
- const credentials = await exchangeOAuthCode(code, verifier, callbackUrl);
4691
+ const credentials = await exchangeOAuthCode(code, verifier, callbackUrl, fetchFn);
4505
4692
  await store.write(credentials);
4506
4693
  return credentials;
4507
4694
  } finally {
@@ -4578,15 +4765,27 @@ function toolResultText(blocks) {
4578
4765
  }
4579
4766
  function replayBlockFor(message, index) {
4580
4767
  const source = message.source;
4581
- if (!source || source.kind !== "model") return void 0;
4768
+ if (!source || source.kind !== "model" || source.provider !== "antigravity") return void 0;
4582
4769
  const state = source.replayState;
4583
4770
  if (!isRecord(state)) return void 0;
4771
+ if (Array.isArray(state.blocks)) return state.blocks[index];
4584
4772
  const resp = isRecord(state.response) ? state.response : void 0;
4585
4773
  if (resp) {
4586
4774
  if (Array.isArray(resp.outputItems)) return resp.outputItems[index];
4587
4775
  if (Array.isArray(resp.blocks)) return resp.blocks[index];
4588
4776
  }
4589
- if (Array.isArray(state.blocks)) return state.blocks[index];
4777
+ }
4778
+ function thoughtSignature(part) {
4779
+ return asString(part?.thoughtSignature) || asString(part?.thought_signature) || asString(part?.thinkingSignature) || asString(part?.textSignature);
4780
+ }
4781
+ function replayPart(part) {
4782
+ const copy = { ...part };
4783
+ const signature = thoughtSignature(part);
4784
+ delete copy.thought_signature;
4785
+ delete copy.thinkingSignature;
4786
+ if (signature) copy.thoughtSignature = signature;
4787
+ if (typeof copy.text === "string") copy.text = sanitizeText(copy.text);
4788
+ return copy;
4590
4789
  }
4591
4790
  function assistantParts(message, model, runtimeModel, toolNames) {
4592
4791
  const parts = [];
@@ -4595,30 +4794,39 @@ function assistantParts(message, model, runtimeModel, toolNames) {
4595
4794
  const block = message.content[index];
4596
4795
  if (!isRecord(block)) continue;
4597
4796
  const replay = replayBlockFor(message, index);
4598
- if (block.type === "text" && String(block.text || "").trim()) parts.push({ text: sanitizeText(String(block.text)) });
4599
- else if (block.type === "reasoning" && String(block.text || "").trim()) {
4600
- const sig = asString(replay?.thinkingSignature) || asString(replay?.thought_signature) || asString(block.thought_signature);
4601
- if (sig) parts.push({
4797
+ const originalParts = Array.isArray(replay?.parts) ? replay.parts.filter(isRecord) : [];
4798
+ if ((block.type === "text" || block.type === "reasoning") && originalParts.length > 0 && originalParts.every((part) => !part.functionCall) && originalParts.map((part) => asString(part.text) || "").join("") === sanitizeText(String(block.text || ""))) {
4799
+ parts.push(...originalParts.map(replayPart));
4800
+ continue;
4801
+ }
4802
+ if (block.type === "text" && String(block.text || "").trim()) {
4803
+ const sig = thoughtSignature(replay) || thoughtSignature(block);
4804
+ parts.push({
4805
+ text: sanitizeText(String(block.text)),
4806
+ ...sig ? { thoughtSignature: sig } : {}
4807
+ });
4808
+ } else if (block.type === "reasoning" && String(block.text || "").trim()) {
4809
+ const sig = thoughtSignature(replay) || thoughtSignature(block);
4810
+ parts.push({
4602
4811
  thought: true,
4603
4812
  text: sanitizeText(String(block.text)),
4604
- thought_signature: sig,
4605
- thoughtSignature: sig
4813
+ ...sig ? { thoughtSignature: sig } : {}
4606
4814
  });
4607
- else parts.push({ text: sanitizeText(String(block.text)) });
4608
4815
  } else if (block.type === "tool-call") {
4609
4816
  const toolId = String(block.id || "");
4610
4817
  const toolName = String(block.name || "");
4611
4818
  toolNames.set(toolId, toolName);
4612
- const sig = asString(replay?.thought_signature) || asString(replay?.thoughtSignature) || asString(block.thought_signature) || asString(block.thoughtSignature) || "skip_thought_signature_validator";
4819
+ const originalCall = originalParts.find((part) => isRecord(part.functionCall));
4820
+ const effectiveSignature = thoughtSignature(originalCall) || thoughtSignature(replay) || thoughtSignature(block) || (originalCall ? void 0 : "skip_thought_signature_validator");
4613
4821
  parts.push({
4614
4822
  functionCall: {
4615
4823
  name: toolName,
4616
4824
  args: parseArguments(block.arguments),
4617
4825
  ...toolCallIdNeeded(model.id, runtimeModel) ? { id: sanitizeToolCallId(toolId, toolName) } : {}
4618
4826
  },
4619
- thought_signature: sig,
4620
- thoughtSignature: sig
4827
+ ...effectiveSignature ? { thoughtSignature: effectiveSignature } : {}
4621
4828
  });
4829
+ parts.push(...originalParts.filter((part) => !part.functionCall).map(replayPart));
4622
4830
  }
4623
4831
  }
4624
4832
  return parts;
@@ -4722,6 +4930,8 @@ function buildRequest(options, model, projectId, runtimeModel, effort) {
4722
4930
  const isTiered = runtimeModel === "gemini-3.8-flash-tiered" || runtimeModel === "gemini-3.7-flash-tiered";
4723
4931
  const isSuffixed = /^gemini-.+(?:-(?:extra-)?low|-medium|-high|-xhigh)$/.test(runtimeModel);
4724
4932
  const isGemini25 = runtimeModel.startsWith("gemini-2.5-") || model.id.startsWith("gemini-2.5-");
4933
+ const isGemini3 = /^gemini-3[.-]/.test(runtimeModel) && !runtimeModel.includes("image");
4934
+ const isGeminiAgent = runtimeModel === "gemini-pro-agent" || runtimeModel === "gemini-3-flash-agent";
4725
4935
  if (isTiered) {
4726
4936
  const selected = (effort || "medium").toLowerCase();
4727
4937
  const isOff = selected === "off" || selected === "none";
@@ -4729,7 +4939,7 @@ function buildRequest(options, model, projectId, runtimeModel, effort) {
4729
4939
  thinkingLevel: isOff ? "MINIMAL" : selected === "high" || selected === "xhigh" ? "HIGH" : selected === "medium" ? "MEDIUM" : "LOW",
4730
4940
  includeThoughts: !isOff
4731
4941
  };
4732
- } else if (isSuffixed) {
4942
+ } else if (isSuffixed || isGemini3 || isGeminiAgent) {
4733
4943
  const selected = (effort || "medium").toLowerCase();
4734
4944
  generationConfig.thinkingConfig = { includeThoughts: !(selected === "off" || selected === "none") };
4735
4945
  } else if (isGemini25) {
@@ -4765,58 +4975,90 @@ function createStreamState() {
4765
4975
  replayBlocks: [],
4766
4976
  currentBlock: null,
4767
4977
  hasContent: false,
4768
- hasToolCall: false
4978
+ hasToolCall: false,
4979
+ usageMetadata: null,
4980
+ done: false,
4981
+ finished: false
4982
+ };
4983
+ }
4984
+ function closeCurrentBlock(state) {
4985
+ if (!state.currentBlock) return [];
4986
+ const { index, type, text } = state.currentBlock;
4987
+ const block = {
4988
+ type,
4989
+ text
4990
+ };
4991
+ state.blocks[index] = block;
4992
+ state.currentBlock = null;
4993
+ return [{
4994
+ type: "block-end",
4995
+ index,
4996
+ block
4997
+ }];
4998
+ }
4999
+ const USAGE_FIELDS = [
5000
+ "promptTokenCount",
5001
+ "cachedContentTokenCount",
5002
+ "candidatesTokenCount",
5003
+ "thoughtsTokenCount",
5004
+ "totalTokenCount"
5005
+ ];
5006
+ function collectUsage(value, state) {
5007
+ if (!isRecord(value)) return;
5008
+ for (const key of USAGE_FIELDS) {
5009
+ const count = value[key];
5010
+ if (typeof count !== "number" || !Number.isSafeInteger(count) || count < 0) continue;
5011
+ state.usageMetadata ??= {};
5012
+ state.usageMetadata[key] = count;
5013
+ }
5014
+ }
5015
+ function tokenUsage(u) {
5016
+ const prompt = u.promptTokenCount ?? 0;
5017
+ const cache = Math.min(prompt, u.cachedContentTokenCount ?? 0);
5018
+ const thoughts = u.thoughtsTokenCount ?? 0;
5019
+ const explicitOutput = (u.candidatesTokenCount ?? 0) + thoughts;
5020
+ const totalOutput = u.totalTokenCount !== void 0 && u.promptTokenCount !== void 0 ? Math.max(0, u.totalTokenCount - prompt) : 0;
5021
+ return {
5022
+ inputTokens: prompt - cache,
5023
+ outputTokens: Math.max(explicitOutput, totalOutput),
5024
+ ...cache > 0 ? { cacheReadTokens: cache } : {},
5025
+ ...u.thoughtsTokenCount !== void 0 ? { reasoningTokens: thoughts } : {}
4769
5026
  };
4770
5027
  }
4771
5028
  function processStreamLine(line, state) {
4772
- if (!line.startsWith("data:")) return [];
5029
+ if (state.finished || !line.startsWith("data:")) return [];
4773
5030
  const json = line.slice(5).trim();
4774
- if (!json || json === "[DONE]") return [];
5031
+ if (json === "[DONE]") {
5032
+ state.done = true;
5033
+ return closeStream(state);
5034
+ }
5035
+ if (!json) return [];
4775
5036
  const chunk = safeJsonParse(json);
4776
5037
  if (!isRecord(chunk)) return [];
4777
5038
  const responseData = isRecord(chunk.response) ? chunk.response : chunk;
4778
- const candidate = (Array.isArray(responseData.candidates) ? responseData.candidates : [])[0];
5039
+ const candidates = Array.isArray(responseData.candidates) ? responseData.candidates : [];
5040
+ const candidate = isRecord(candidates[0]) ? candidates[0] : void 0;
4779
5041
  const content = isRecord(candidate?.content) ? candidate.content : void 0;
4780
5042
  const parts = Array.isArray(content?.parts) ? content.parts : [];
4781
5043
  const out = [];
4782
- const closeCurrentBlock = () => {
4783
- if (!state.currentBlock) return;
4784
- const index = state.blocks.length - 1;
4785
- if (state.currentBlock.type === "text") out.push({
4786
- type: "block-end",
4787
- index,
4788
- block: {
4789
- type: "text",
4790
- text: state.currentBlock.text
4791
- }
4792
- });
4793
- else out.push({
4794
- type: "block-end",
4795
- index,
4796
- block: {
4797
- type: "reasoning",
4798
- text: state.currentBlock.text
4799
- }
4800
- });
4801
- state.currentBlock = null;
4802
- };
4803
5044
  for (const part of parts) {
4804
5045
  if (!isRecord(part)) continue;
4805
- if (part.text !== void 0 && typeof part.text === "string") {
5046
+ if (typeof part.text === "string" && part.text !== "") {
4806
5047
  const isThinking = Boolean(part.thought);
4807
5048
  const blockType = isThinking ? "reasoning" : "text";
4808
5049
  if (!state.currentBlock || state.currentBlock.type !== blockType) {
4809
- closeCurrentBlock();
5050
+ out.push(...closeCurrentBlock(state));
5051
+ const index = state.blocks.length;
4810
5052
  state.currentBlock = {
5053
+ index,
4811
5054
  type: blockType,
4812
5055
  text: ""
4813
5056
  };
4814
- const index = state.blocks.length;
4815
5057
  state.blocks.push({
4816
5058
  type: blockType,
4817
5059
  text: ""
4818
5060
  });
4819
- state.replayBlocks.push({ type: blockType });
5061
+ state.replayBlocks.push({ parts: [] });
4820
5062
  out.push({
4821
5063
  type: "block-start",
4822
5064
  index,
@@ -4826,45 +5068,55 @@ function processStreamLine(line, state) {
4826
5068
  const delta = sanitizeText(part.text);
4827
5069
  state.currentBlock.text += delta;
4828
5070
  state.hasContent = true;
4829
- if (isThinking && part.thoughtSignature) {
4830
- state.currentBlock.thinkingSignature = part.thoughtSignature;
4831
- state.replayBlocks[state.blocks.length - 1].thinkingSignature = part.thoughtSignature;
4832
- } else if (!isThinking && part.thoughtSignature) {
4833
- state.currentBlock.textSignature = part.thoughtSignature;
4834
- state.replayBlocks[state.blocks.length - 1].textSignature = part.thoughtSignature;
4835
- }
5071
+ state.replayBlocks[state.currentBlock.index].parts.push(replayPart(part));
4836
5072
  out.push({
4837
5073
  type: isThinking ? "reasoning-delta" : "text-delta",
4838
- index: state.blocks.length - 1,
5074
+ index: state.currentBlock.index,
4839
5075
  text: delta
4840
5076
  });
5077
+ } else if (!isRecord(part.functionCall) && thoughtSignature(part)) {
5078
+ if (state.replayBlocks.length === 0) {
5079
+ const type = part.thought ? "reasoning" : "text";
5080
+ state.blocks.push({
5081
+ type,
5082
+ text: ""
5083
+ });
5084
+ state.replayBlocks.push({ parts: [] });
5085
+ out.push({
5086
+ type: "block-start",
5087
+ index: 0,
5088
+ blockType: type
5089
+ });
5090
+ out.push({
5091
+ type: "block-end",
5092
+ index: 0,
5093
+ block: {
5094
+ type,
5095
+ text: ""
5096
+ }
5097
+ });
5098
+ }
5099
+ state.replayBlocks[state.replayBlocks.length - 1].parts.push(replayPart(part));
4841
5100
  }
4842
5101
  if (isRecord(part.functionCall)) {
4843
- closeCurrentBlock();
5102
+ out.push(...closeCurrentBlock(state));
4844
5103
  const fc = part.functionCall;
4845
5104
  const toolName = asString(fc.name) || "";
4846
5105
  const toolId = sanitizeToolCallId(asString(fc.id) || "", toolName);
4847
5106
  const argsText = JSON.stringify(isRecord(fc.args) ? fc.args : {});
4848
5107
  const index = state.blocks.length;
4849
- const sig = asString(part.thought_signature) || asString(part.thoughtSignature) || asString(part.thinkingSignature) || asString(fc.thought_signature) || asString(fc.thoughtSignature) || state.currentBlock?.thinkingSignature;
4850
5108
  const block = {
4851
5109
  type: "tool-call",
4852
- id: toolId,
5110
+ id: CallId(toolId),
4853
5111
  name: toolName,
4854
- arguments: argsText,
4855
- ...sig ? {
4856
- thought_signature: sig,
4857
- thoughtSignature: sig
4858
- } : {}
5112
+ arguments: argsText
4859
5113
  };
4860
5114
  state.blocks.push(block);
4861
- state.replayBlocks.push({
4862
- type: "tool-call",
4863
- ...sig ? {
4864
- thought_signature: sig,
4865
- thoughtSignature: sig
4866
- } : {}
4867
- });
5115
+ const sig = thoughtSignature(part) || thoughtSignature(fc);
5116
+ state.replayBlocks.push({ parts: [{
5117
+ ...replayPart(part),
5118
+ ...sig ? { thoughtSignature: sig } : {}
5119
+ }] });
4868
5120
  state.hasContent = true;
4869
5121
  state.hasToolCall = true;
4870
5122
  out.push({
@@ -4875,7 +5127,7 @@ function processStreamLine(line, state) {
4875
5127
  out.push({
4876
5128
  type: "tool-call-delta",
4877
5129
  index,
4878
- id: toolId,
5130
+ id: CallId(toolId),
4879
5131
  name: toolName,
4880
5132
  argumentsDelta: argsText
4881
5133
  });
@@ -4886,55 +5138,33 @@ function processStreamLine(line, state) {
4886
5138
  });
4887
5139
  }
4888
5140
  }
4889
- if (responseData.usageMetadata && isRecord(responseData.usageMetadata)) {
4890
- const u = responseData.usageMetadata;
4891
- const usage = {
4892
- inputTokens: Math.max(0, (u.promptTokenCount || 0) - (u.cachedContentTokenCount || 0)),
4893
- outputTokens: (u.candidatesTokenCount || 0) + (u.thoughtsTokenCount || 0),
4894
- ...u.cachedContentTokenCount ? { cacheReadTokens: u.cachedContentTokenCount } : {}
4895
- };
4896
- out.push({
4897
- type: "usage",
4898
- usage
4899
- });
4900
- }
4901
- const finishReason = candidate?.finishReason;
5141
+ collectUsage(chunk.usageMetadata, state);
5142
+ if (responseData !== chunk) collectUsage(responseData.usageMetadata, state);
5143
+ const finishReason = asString(candidate?.finishReason) || asString(responseData.finishReason);
4902
5144
  if (finishReason) {
4903
- closeCurrentBlock();
4904
- const reason = state.hasToolCall ? { kind: "tool-calls" } : finishReason === "MAX_TOKENS" ? { kind: "max-tokens" } : { kind: "stop" };
4905
- out.push({
4906
- type: "finish",
4907
- reason,
4908
- replayState: { response: {
4909
- outputItems: state.replayBlocks,
4910
- blocks: state.replayBlocks
4911
- } }
4912
- });
5145
+ state.finishReason = finishReason;
5146
+ out.push(...closeCurrentBlock(state));
4913
5147
  }
4914
5148
  return out;
4915
5149
  }
4916
5150
  function closeStream(state) {
4917
- const out = [];
4918
- if (state.currentBlock) {
4919
- const index = state.blocks.length - 1;
4920
- if (state.currentBlock.type === "text") out.push({
4921
- type: "block-end",
4922
- index,
4923
- block: {
4924
- type: "text",
4925
- text: state.currentBlock.text
4926
- }
4927
- });
4928
- else out.push({
4929
- type: "block-end",
4930
- index,
4931
- block: {
4932
- type: "reasoning",
4933
- text: state.currentBlock.text
4934
- }
4935
- });
4936
- state.currentBlock = null;
4937
- }
5151
+ if (state.finished) return [];
5152
+ if (!state.finishReason && !state.done) throw new LlmError("Antigravity stream ended before its terminal response", "PROVIDER_ERROR");
5153
+ state.finished = true;
5154
+ const out = closeCurrentBlock(state);
5155
+ if (state.usageMetadata) out.push({
5156
+ type: "usage",
5157
+ usage: tokenUsage(state.usageMetadata)
5158
+ });
5159
+ const reason = state.finishReason === "MAX_TOKENS" ? { kind: "max-tokens" } : state.hasToolCall ? { kind: "tool-calls" } : { kind: "stop" };
5160
+ out.push({
5161
+ type: "finish",
5162
+ reason,
5163
+ replayState: {
5164
+ response: { provider: PROVIDER_ID },
5165
+ blocks: state.replayBlocks
5166
+ }
5167
+ });
4938
5168
  return out;
4939
5169
  }
4940
5170
  //#endregion
@@ -4943,11 +5173,13 @@ var AntigravityAdapter = class extends LlmAdapter {
4943
5173
  store;
4944
5174
  modelSettings;
4945
5175
  preferences;
4946
- constructor(store = new FileCredentialStore(), modelSettings = new FileModelSettingsStore(), preferences) {
5176
+ options;
5177
+ constructor(store = new FileCredentialStore(), modelSettings = new FileModelSettingsStore(), preferences, options = {}) {
4947
5178
  super();
4948
5179
  this.store = store;
4949
5180
  this.modelSettings = modelSettings;
4950
5181
  this.preferences = preferences;
5182
+ this.options = options;
4951
5183
  }
4952
5184
  providerInfo(provider) {
4953
5185
  return {
@@ -5018,7 +5250,7 @@ var AntigravityAdapter = class extends LlmAdapter {
5018
5250
  contextWindow: 128e3,
5019
5251
  maxTokens: 65536
5020
5252
  };
5021
- const settings = await this.modelSettings.read();
5253
+ const settings = this.preferences ? this.preferences.status() : await this.modelSettings.read();
5022
5254
  const effectiveEffort = options.reasoningEffort || settings.defaultReasoningEffort || void 0;
5023
5255
  const effectiveOptions = effectiveEffort ? {
5024
5256
  ...options,
@@ -5027,7 +5259,8 @@ var AntigravityAdapter = class extends LlmAdapter {
5027
5259
  yield* wrapStreamWithWatchdog((watchdogSignal) => this.requestStream(effectiveOptions, model, watchdogSignal), options.signal, STREAM_IDLE_TIMEOUT_MS, STREAM_IDLE_TIMEOUT_CODE, "Antigravity");
5028
5260
  }
5029
5261
  async *requestStream(options, model, signal) {
5030
- const { token, projectId: defaultProj } = await ensureApiKey(this.store);
5262
+ const fetchFn = this.options.fetchFn ?? fetch;
5263
+ const { token, projectId: defaultProj } = await ensureApiKey(this.store, fetchFn);
5031
5264
  const projectId = defaultProj || "antigravity-default";
5032
5265
  const effort = String(options.reasoningEffort || "medium").toLowerCase();
5033
5266
  const routing = ROUTING[model.id];
@@ -5039,7 +5272,6 @@ var AntigravityAdapter = class extends LlmAdapter {
5039
5272
  for (const fc of routing.fallbackCandidates) if (!candidates.includes(fc)) candidates.push(fc);
5040
5273
  }
5041
5274
  let response;
5042
- candidates[0];
5043
5275
  for (const runtimeModel of candidates) {
5044
5276
  const body = JSON.stringify(buildRequest(options, model, projectId, runtimeModel, effort));
5045
5277
  const headers = {
@@ -5047,7 +5279,7 @@ var AntigravityAdapter = class extends LlmAdapter {
5047
5279
  ...model.id.startsWith("claude-") ? { "anthropic-beta": "interleaved-thinking-2025-05-14" } : {}
5048
5280
  };
5049
5281
  for (const endpoint of endpointCandidates()) try {
5050
- response = await fetch(`${endpoint}/v1internal:streamGenerateContent?alt=sse`, {
5282
+ response = await fetchFn(`${endpoint}/v1internal:streamGenerateContent?alt=sse`, {
5051
5283
  method: "POST",
5052
5284
  headers,
5053
5285
  body,
@@ -5083,8 +5315,10 @@ var AntigravityAdapter = class extends LlmAdapter {
5083
5315
  if (!trimmed) continue;
5084
5316
  const chunks = processStreamLine(trimmed, state);
5085
5317
  for (const chunk of chunks) yield chunk;
5318
+ if (state.finished) return;
5086
5319
  }
5087
5320
  }
5321
+ buffer += decoder.decode();
5088
5322
  if (buffer.trim()) {
5089
5323
  const chunks = processStreamLine(buffer.trim(), state);
5090
5324
  for (const chunk of chunks) yield chunk;
@@ -5151,7 +5385,7 @@ async function getAntigravityWebStatus(store, modelSettings, preferences) {
5151
5385
  defaultReasoningEffort: settings.defaultReasoningEffort || null
5152
5386
  };
5153
5387
  }
5154
- function registerAntigravityRoutes(ctx, store, modelSettings, preferences) {
5388
+ function registerAntigravityRoutes(ctx, store, modelSettings, preferences, fetchFn = fetch) {
5155
5389
  return ctx.webServer.register({
5156
5390
  kind: "prefix",
5157
5391
  path: "/antigravity/api",
@@ -5169,7 +5403,7 @@ function registerAntigravityRoutes(ctx, store, modelSettings, preferences) {
5169
5403
  if (request.method !== "POST") return sendMethodNotAllowed(response);
5170
5404
  return sendJson(response, 200, {
5171
5405
  ok: true,
5172
- value: await beginWebLogin(store)
5406
+ value: await beginWebLogin(store, fetchFn)
5173
5407
  });
5174
5408
  }
5175
5409
  if (path === "login/status") {
@@ -5181,7 +5415,7 @@ function registerAntigravityRoutes(ctx, store, modelSettings, preferences) {
5181
5415
  }
5182
5416
  if (path === "quota") {
5183
5417
  if (request.method !== "GET" && request.method !== "POST") return sendMethodNotAllowed(response);
5184
- const quota = await fetchAccountQuota(store, modelSettings);
5418
+ const quota = await fetchAccountQuota(store, modelSettings, fetchFn);
5185
5419
  return sendJson(response, 200, {
5186
5420
  ok: true,
5187
5421
  value: {
@@ -5254,21 +5488,15 @@ function apply(ctx) {
5254
5488
  const antigravityStore = new FileCredentialStore();
5255
5489
  const antigravityModelSettings = new FileModelSettingsStore();
5256
5490
  const antigravityPreferences = registerAntigravityPreferenceStore(ctx.settings, antigravityModelSettings);
5257
- const antigravityAdapter = new AntigravityAdapter(antigravityStore, antigravityModelSettings, antigravityPreferences);
5258
- ctx.effect(() => {
5259
- const disposeAntigravityAdapter = ctx.llm.registerAdapter([PROVIDER_ID], antigravityAdapter);
5260
- const disposeAntigravityRoutes = registerAntigravityRoutes(ctx, antigravityStore, antigravityModelSettings, antigravityPreferences);
5261
- return () => {
5262
- disposeAntigravityRoutes();
5263
- disposeAntigravityAdapter();
5264
- };
5265
- }, "dsh-antigravity: adapter, routes, and lifecycle");
5266
5491
  ctx.effect(() => {
5267
5492
  const proxyManager = new ProxyManager({
5268
5493
  getPreferences: () => preferences.status(),
5269
5494
  logger: ctx.logger
5270
5495
  });
5271
5496
  const proxyFetch = proxyManager.createFetch();
5497
+ const antigravityAdapter = new AntigravityAdapter(antigravityStore, antigravityModelSettings, antigravityPreferences, { fetchFn: proxyFetch });
5498
+ const disposeAntigravityAdapter = ctx.llm.registerAdapter([PROVIDER_ID], antigravityAdapter);
5499
+ const disposeAntigravityRoutes = registerAntigravityRoutes(ctx, antigravityStore, antigravityModelSettings, antigravityPreferences, proxyFetch);
5272
5500
  const oauth = new OAuthService(store, {
5273
5501
  fetchFn: proxyFetch,
5274
5502
  logger: ctx.logger
@@ -5312,6 +5540,8 @@ function apply(ctx) {
5312
5540
  disposeImageTool();
5313
5541
  disposeAdapter();
5314
5542
  disposeRoutes();
5543
+ disposeAntigravityRoutes();
5544
+ disposeAntigravityAdapter();
5315
5545
  oauth.dispose();
5316
5546
  proxyManager.dispose();
5317
5547
  };