@juspay/neurolink 11.11.7 → 11.13.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +6 -2
  2. package/dist/browser/neurolink.min.js +396 -396
  3. package/dist/cli/commands/proxy.js +42 -0
  4. package/dist/cli/commands/proxyAnalyze.js +10 -1
  5. package/dist/cli/proxy-clients/claudeCode.js +42 -10
  6. package/dist/cli/proxy-clients/openCode.js +37 -15
  7. package/dist/cli/proxy-clients/qwenCode.js +33 -9
  8. package/dist/cli/proxy-clients/registry.js +10 -2
  9. package/dist/cli/proxy-clients/snapshot.d.ts +52 -0
  10. package/dist/cli/proxy-clients/snapshot.js +98 -0
  11. package/dist/lib/providers/googleAiStudio/client.js +6 -3
  12. package/dist/lib/providers/googleVertex/client.d.ts +33 -0
  13. package/dist/lib/providers/googleVertex/client.js +110 -11
  14. package/dist/lib/proxy/codexUsage.d.ts +68 -0
  15. package/dist/lib/proxy/codexUsage.js +247 -0
  16. package/dist/lib/proxy/proxyAnalysis.js +87 -3
  17. package/dist/lib/proxy/proxyFetch.d.ts +1 -0
  18. package/dist/lib/proxy/proxyFetch.js +29 -0
  19. package/dist/lib/proxy/proxyTracer.d.ts +13 -2
  20. package/dist/lib/proxy/proxyTracer.js +29 -7
  21. package/dist/lib/proxy/proxyTranslationEngine.js +22 -5
  22. package/dist/lib/server/routes/codexProxyRoutes.js +29 -1
  23. package/dist/lib/server/routes/openaiProxyRoutes.js +5 -0
  24. package/dist/lib/types/proxy.d.ts +65 -0
  25. package/dist/lib/utils/pricing.d.ts +9 -0
  26. package/dist/lib/utils/pricing.js +136 -1
  27. package/dist/providers/googleAiStudio/client.js +6 -3
  28. package/dist/providers/googleVertex/client.d.ts +33 -0
  29. package/dist/providers/googleVertex/client.js +110 -11
  30. package/dist/proxy/codexUsage.d.ts +68 -0
  31. package/dist/proxy/codexUsage.js +246 -0
  32. package/dist/proxy/proxyAnalysis.js +87 -3
  33. package/dist/proxy/proxyFetch.d.ts +1 -0
  34. package/dist/proxy/proxyFetch.js +29 -0
  35. package/dist/proxy/proxyTracer.d.ts +13 -2
  36. package/dist/proxy/proxyTracer.js +29 -7
  37. package/dist/proxy/proxyTranslationEngine.js +22 -5
  38. package/dist/server/routes/codexProxyRoutes.js +29 -1
  39. package/dist/server/routes/openaiProxyRoutes.js +5 -0
  40. package/dist/types/proxy.d.ts +65 -0
  41. package/dist/utils/pricing.d.ts +9 -0
  42. package/dist/utils/pricing.js +136 -1
  43. package/package.json +2 -1
@@ -3993,6 +3993,46 @@ export const proxyInstallCommand = {
3993
3993
  console.info(chalk.gray(` Remove: neurolink proxy uninstall`));
3994
3994
  },
3995
3995
  };
3996
+ /**
3997
+ * Put every auto-configured CLI back the way it was, using the URL recorded in
3998
+ * the proxy's own state file.
3999
+ *
4000
+ * `proxy uninstall` is the one moment where "the proxy is going away for good"
4001
+ * is unambiguous, and it is the only place this can be done for a
4002
+ * launchd-managed service: the shutdown path restores on SIGINT only, and both
4003
+ * `launchctl unload` and this command stop the supervisor with SIGTERM. Without
4004
+ * this, uninstalling left all five clients pointing at a socket that no longer
4005
+ * answers, with nothing to say why.
4006
+ *
4007
+ * Must run before `clearProxyState()` — the state file is what tells us which
4008
+ * URL the clients were given, and the restore helpers deliberately refuse to
4009
+ * touch a URL they did not write.
4010
+ */
4011
+ async function restoreClientsOnUninstall() {
4012
+ let state = null;
4013
+ try {
4014
+ state = loadProxyState();
4015
+ }
4016
+ catch {
4017
+ // An unreadable state file is not a reason to abort an uninstall.
4018
+ }
4019
+ if (!state?.port) {
4020
+ // Nothing recorded, so we cannot prove which URL the configs carry.
4021
+ return;
4022
+ }
4023
+ // 0.0.0.0 is a bind address, never a reachable one; clients were handed
4024
+ // localhost, so that is what restore has to match on.
4025
+ const host = state.host === "0.0.0.0" ? "localhost" : (state.host ?? "localhost");
4026
+ const results = await restoreAllClients(`http://${host}:${state.port}`);
4027
+ for (const result of results) {
4028
+ if (result.restored) {
4029
+ console.info(chalk.green(`\u2713 Restored ${result.displayName} settings`));
4030
+ }
4031
+ else if (result.error) {
4032
+ logger.debug(`[proxy] ${result.id} restore failed during uninstall: ${result.error.message}`);
4033
+ }
4034
+ }
4035
+ }
3996
4036
  export const proxyUninstallCommand = {
3997
4037
  command: "uninstall",
3998
4038
  describe: "Remove proxy background service",
@@ -4008,6 +4048,7 @@ export const proxyUninstallCommand = {
4008
4048
  console.info(chalk.red("A proxy supervisor is still running and could not be stopped; leaving its state intact. Stop it manually and retry."));
4009
4049
  process.exit(1);
4010
4050
  }
4051
+ await restoreClientsOnUninstall();
4011
4052
  clearProxyState();
4012
4053
  clearProxySupervisorState();
4013
4054
  console.info(chalk.yellow("No proxy service installed."));
@@ -4029,6 +4070,7 @@ export const proxyUninstallCommand = {
4029
4070
  process.exit(1);
4030
4071
  }
4031
4072
  unlinkSync(PLIST_PATH);
4073
+ await restoreClientsOnUninstall();
4032
4074
  clearProxyState();
4033
4075
  clearProxySupervisorState();
4034
4076
  console.info(chalk.green(`✓ Plist removed from ${PLIST_PATH}`));
@@ -81,7 +81,16 @@ function printAnalysis(report) {
81
81
  logger.always(chalk.bold(" Cache"));
82
82
  if (report.coverage.cacheUsage) {
83
83
  logger.always(` Usage records: ${report.cache.requestsWithUsage}, cache-read requests: ${report.cache.requestsWithCacheRead}, hit rate: ${report.cache.requestHitRate === null ? "-" : `${(report.cache.requestHitRate * 100).toFixed(1)}%`}`);
84
- logger.always(` Tokens: ${report.cache.cacheReadTokens} read, ${report.cache.cacheCreationTokens} created, ${report.cache.inputTokens} input`);
84
+ logger.always(` Tokens: ${report.cache.cacheReadTokens} read, ${report.cache.cacheCreationTokens} created, ${report.cache.inputTokens} input, ${report.cache.outputTokens} output`);
85
+ logger.always(report.cache.requestsPriced > 0
86
+ ? ` Estimated cost: $${report.cache.estimatedCostUsd.toFixed(4)} across ${report.cache.requestsPriced} priced request(s)`
87
+ : " Estimated cost: unavailable (no request carried a priceable model)");
88
+ if (report.cache.requestsPricedByPrefix > 0) {
89
+ logger.always(chalk.yellow(` ⚠ ${report.cache.requestsPricedByPrefix} request(s) priced by name-prefix fallback, not an exact rate: ${report.cache.modelsPricedByPrefix.join(", ")}`));
90
+ }
91
+ if (report.cache.requestsUnpriced > 0) {
92
+ logger.always(chalk.yellow(` ⚠ ${report.cache.requestsUnpriced} request(s) had usage but no pricing row: ${report.cache.unpricedModels.join(", ")}`));
93
+ }
85
94
  }
86
95
  else {
87
96
  logger.always(chalk.yellow(" Cache usage: unavailable"));
@@ -8,6 +8,7 @@
8
8
  import { homedir } from "os";
9
9
  import { join } from "path";
10
10
  import { logger } from "../../lib/utils/logger.js";
11
+ import { isProxyOwnedValue, shouldCaptureSnapshot } from "./snapshot.js";
11
12
  /**
12
13
  * Resolved per call rather than at module load so `detect()` and `apply()`
13
14
  * agree when HOME changes — under test, and on the `--dev` isolation path.
@@ -20,6 +21,13 @@ function getClaudeSettingsPath() {
20
21
  }
21
22
  /** Keys we manage in Claude Code's settings.env */
22
23
  const PROXY_MANAGED_KEYS = ["ANTHROPIC_BASE_URL", "ENABLE_TOOL_SEARCH"];
24
+ /** The user's values for those keys, as they were before the proxy first ran. */
25
+ const CLAUDE_ORIGINAL_KEY = "__proxy_original_env";
26
+ /**
27
+ * The values this writer last wrote. Lets apply() tell its own values from ones
28
+ * the user substituted while the proxy was not running.
29
+ */
30
+ const CLAUDE_WRITTEN_KEY = "__proxy_written_env";
23
31
  export async function setClaudeProxySettings(baseUrl) {
24
32
  const fs = await import("fs");
25
33
  let settings = {};
@@ -31,18 +39,30 @@ export async function setClaudeProxySettings(baseUrl) {
31
39
  }
32
40
  const env = (settings.env ?? {});
33
41
  // Preserve original values so clearClaudeProxySettings can restore them.
34
- // Only snapshot once subsequent calls should not overwrite the snapshot.
35
- const originals = (settings
36
- .__proxy_original_env ?? {});
42
+ // A repeat apply() must not overwrite the snapshot with the proxy's own
43
+ // values but a value the user set while the proxy was gone must replace
44
+ // it. See shouldCaptureSnapshot.
45
+ const originals = (settings[CLAUDE_ORIGINAL_KEY] ?? {});
46
+ const written = (settings[CLAUDE_WRITTEN_KEY] ??
47
+ {});
37
48
  for (const key of PROXY_MANAGED_KEYS) {
38
- if (!(key in originals)) {
39
- originals[key] = key in env ? env[key] : null;
49
+ const current = key in env ? env[key] : undefined;
50
+ if (shouldCaptureSnapshot({
51
+ hasSnapshot: key in originals,
52
+ written: written[key],
53
+ current,
54
+ })) {
55
+ originals[key] = current ?? null;
40
56
  }
41
57
  }
42
- settings.__proxy_original_env = originals;
58
+ settings[CLAUDE_ORIGINAL_KEY] = originals;
43
59
  env.ANTHROPIC_BASE_URL = baseUrl;
44
60
  env.ENABLE_TOOL_SEARCH = "true";
45
61
  settings.env = env;
62
+ settings[CLAUDE_WRITTEN_KEY] = {
63
+ ANTHROPIC_BASE_URL: baseUrl,
64
+ ENABLE_TOOL_SEARCH: "true",
65
+ };
46
66
  fs.writeFileSync(getClaudeSettingsPath(), JSON.stringify(settings, null, 2));
47
67
  }
48
68
  export async function clearClaudeProxySettings(expectedBaseUrl) {
@@ -64,7 +84,7 @@ export async function clearClaudeProxySettings(expectedBaseUrl) {
64
84
  // User switched to a different proxy URL; do not clobber.
65
85
  return false;
66
86
  }
67
- if (!("__proxy_original_env" in settings)) {
87
+ if (!(CLAUDE_ORIGINAL_KEY in settings)) {
68
88
  // No snapshot means we cannot prove these keys are ours. Without this
69
89
  // guard the loop below reads every managed key as "did not exist before"
70
90
  // and deletes it — wiping a real user value. Leaving a stale proxy URL
@@ -76,9 +96,20 @@ export async function clearClaudeProxySettings(expectedBaseUrl) {
76
96
  const hadBaseUrl = typeof env.ANTHROPIC_BASE_URL === "string";
77
97
  const hadToolSearch = env.ENABLE_TOOL_SEARCH === "true";
78
98
  // Restore original values if they were saved, otherwise delete the keys
79
- const originals = (settings
80
- .__proxy_original_env ?? {});
99
+ const originals = (settings[CLAUDE_ORIGINAL_KEY] ?? {});
100
+ // Only restore keys we can prove are ours. A user who kept the proxy's base
101
+ // URL but changed one of the other managed values made a deliberate edit;
102
+ // reverting it to the snapshot — or deleting it, when the snapshot says the
103
+ // key was absent — would silently undo them.
104
+ const writtenValues = (settings[CLAUDE_WRITTEN_KEY] ?? {});
81
105
  for (const key of PROXY_MANAGED_KEYS) {
106
+ if (!isProxyOwnedValue({
107
+ written: writtenValues[key],
108
+ current: key in env ? env[key] : undefined,
109
+ })) {
110
+ logger.debug(`[proxy] Claude clear: ${key} was edited after the proxy wrote it, leaving it intact`);
111
+ continue;
112
+ }
82
113
  const original = originals[key];
83
114
  if (original !== undefined && original !== null) {
84
115
  // Restore the value that existed before the proxy was started
@@ -89,7 +120,8 @@ export async function clearClaudeProxySettings(expectedBaseUrl) {
89
120
  delete env[key];
90
121
  }
91
122
  }
92
- delete settings.__proxy_original_env;
123
+ delete settings[CLAUDE_ORIGINAL_KEY];
124
+ delete settings[CLAUDE_WRITTEN_KEY];
93
125
  if (Object.keys(env).length === 0) {
94
126
  delete settings.env;
95
127
  }
@@ -7,6 +7,7 @@
7
7
  import { homedir } from "os";
8
8
  import { join } from "path";
9
9
  import { logger } from "../../lib/utils/logger.js";
10
+ import { cloneForSnapshot, isProxyOwnedValue, shouldCaptureSnapshot, } from "./snapshot.js";
10
11
  function getOpenCodeConfigDir() {
11
12
  // OpenCode resolves this with the unmodified `xdg-basedir` package —
12
13
  // `XDG_CONFIG_HOME || ~/.config` — on every platform, macOS included. There
@@ -28,6 +29,11 @@ function getOpenCodeConfigPath() {
28
29
  * Mirrors the Claude pattern (`__proxy_original_env` inside Claude's settings).
29
30
  */
30
31
  const OPENCODE_ORIGINAL_KEY = "__proxy_original_neurolink";
32
+ /**
33
+ * What this writer last wrote into provider.neurolink. Lets apply() tell its
34
+ * own block from one the user substituted while the proxy was not running.
35
+ */
36
+ const OPENCODE_WRITTEN_KEY = "__proxy_written_neurolink";
31
37
  export async function setOpenCodeProxySettings(baseUrl, proxyKey) {
32
38
  const fs = await import("fs");
33
39
  const configDir = getOpenCodeConfigDir();
@@ -49,18 +55,20 @@ export async function setOpenCodeProxySettings(baseUrl, proxyKey) {
49
55
  config = { provider: {} };
50
56
  }
51
57
  const provider = (config.provider ?? {});
52
- // Persist a snapshot of the user's pre-existing provider.neurolink — but
53
- // only the first time we touch the file. Subsequent set() calls must NOT
54
- // overwrite the snapshot (otherwise after the proxy writes its own block,
55
- // the next set() would store the proxy's block as the "original" and
56
- // permanently lose the user's real config on the next clear()).
57
- if (!(OPENCODE_ORIGINAL_KEY in config)) {
58
+ // Persist a snapshot of the user's pre-existing provider.neurolink. Repeat
59
+ // apply() calls must not overwrite it with the proxy's own block but a
60
+ // block the user wrote while the proxy was gone must replace it. See
61
+ // shouldCaptureSnapshot.
62
+ const currentBlock = "neurolink" in provider ? provider.neurolink : undefined;
63
+ if (shouldCaptureSnapshot({
64
+ hasSnapshot: OPENCODE_ORIGINAL_KEY in config,
65
+ written: config[OPENCODE_WRITTEN_KEY],
66
+ current: currentBlock,
67
+ })) {
58
68
  config[OPENCODE_ORIGINAL_KEY] =
59
- "neurolink" in provider
60
- ? JSON.parse(JSON.stringify(provider.neurolink))
61
- : null;
69
+ currentBlock === undefined ? null : cloneForSnapshot(currentBlock);
62
70
  }
63
- provider.neurolink = {
71
+ const block = {
64
72
  id: "neurolink",
65
73
  name: "NeuroLink Proxy",
66
74
  npm: "@ai-sdk/openai-compatible",
@@ -71,6 +79,8 @@ export async function setOpenCodeProxySettings(baseUrl, proxyKey) {
71
79
  apiKey: proxyKey || "neurolink-proxy",
72
80
  },
73
81
  };
82
+ provider.neurolink = block;
83
+ config[OPENCODE_WRITTEN_KEY] = cloneForSnapshot(block);
74
84
  config.provider = provider;
75
85
  fs.writeFileSync(getOpenCodeConfigPath(), JSON.stringify(config, null, 2));
76
86
  return true;
@@ -105,15 +115,27 @@ export async function clearOpenCodeProxySettings(expectedBaseUrl) {
105
115
  // explicitly had no entry before — never on an "undefined" snapshot, since
106
116
  // that would mean the snapshot was lost and we cannot prove the entry is ours.
107
117
  if (OPENCODE_ORIGINAL_KEY in config) {
108
- const snapshot = config[OPENCODE_ORIGINAL_KEY];
109
- if (snapshot === null) {
110
- // User had no provider.neurolink before the proxy started — safe to remove.
111
- delete provider.neurolink;
118
+ // Only restore what we can prove is ours. The base-URL check above lets
119
+ // through a block still pointing at the proxy that the user has edited
120
+ // beside the URL; reverting that discards a deliberate change.
121
+ if (isProxyOwnedValue({
122
+ written: config[OPENCODE_WRITTEN_KEY],
123
+ current: existing,
124
+ })) {
125
+ const snapshot = config[OPENCODE_ORIGINAL_KEY];
126
+ if (snapshot === null) {
127
+ // User had no provider.neurolink before the proxy started — safe to remove.
128
+ delete provider.neurolink;
129
+ }
130
+ else {
131
+ provider.neurolink = snapshot;
132
+ }
112
133
  }
113
134
  else {
114
- provider.neurolink = snapshot;
135
+ logger.debug("[proxy] OpenCode clear: provider.neurolink was edited after the proxy wrote it, leaving it intact");
115
136
  }
116
137
  delete config[OPENCODE_ORIGINAL_KEY];
138
+ delete config[OPENCODE_WRITTEN_KEY];
117
139
  }
118
140
  else {
119
141
  // No snapshot present — refuse to delete to avoid destroying a config
@@ -20,6 +20,7 @@
20
20
  import { homedir } from "os";
21
21
  import { join } from "path";
22
22
  import { logger } from "../../lib/utils/logger.js";
23
+ import { cloneForSnapshot, isProxyOwnedValue, shouldCaptureSnapshot, } from "./snapshot.js";
23
24
  /**
24
25
  * Resolved per call rather than at module load so `detect()` and `apply()`
25
26
  * agree when HOME changes — under test, and on the `--dev` isolation path.
@@ -37,6 +38,12 @@ function getQwenSettingsPath() {
37
38
  * the same approach the Claude and OpenCode writers take.
38
39
  */
39
40
  const QWEN_ORIGINAL_KEY = "__proxy_original_qwen_auth";
41
+ /**
42
+ * The auth block this writer last wrote. Lets apply() tell its own block from
43
+ * one the user substituted while the proxy was not running — which for Qwen
44
+ * carries a real API key.
45
+ */
46
+ const QWEN_WRITTEN_KEY = "__proxy_written_qwen_auth";
40
47
  function readQwenSettings(fs) {
41
48
  try {
42
49
  const parsed = JSON.parse(fs.readFileSync(getQwenSettingsPath(), "utf8"));
@@ -61,18 +68,25 @@ export async function setQwenProxySettings(baseUrl, proxyKey) {
61
68
  const settings = readQwenSettings(fs) ?? {};
62
69
  const security = (settings.security ?? {});
63
70
  const auth = (security.auth ?? {});
64
- // Snapshot only on first touch. A second apply() must not overwrite the
65
- // snapshot with our own block, which would lose the user's real config on
66
- // the next restore.
67
- if (!(QWEN_ORIGINAL_KEY in settings)) {
71
+ // A repeat apply() must not overwrite the snapshot with our own block, which
72
+ // would lose the user's real config on the next restore — but a block the
73
+ // user wrote while the proxy was gone must replace it. See
74
+ // shouldCaptureSnapshot.
75
+ const currentAuth = "auth" in security ? security.auth : undefined;
76
+ if (shouldCaptureSnapshot({
77
+ hasSnapshot: QWEN_ORIGINAL_KEY in settings,
78
+ written: settings[QWEN_WRITTEN_KEY],
79
+ current: currentAuth,
80
+ })) {
68
81
  settings[QWEN_ORIGINAL_KEY] =
69
- "auth" in security ? JSON.parse(JSON.stringify(auth)) : null;
82
+ currentAuth === undefined ? null : cloneForSnapshot(currentAuth);
70
83
  }
71
84
  auth.selectedType = "openai";
72
85
  auth.baseUrl = baseUrl;
73
86
  auth.apiKey = proxyKey || "neurolink-proxy";
74
87
  security.auth = auth;
75
88
  settings.security = security;
89
+ settings[QWEN_WRITTEN_KEY] = cloneForSnapshot(auth);
76
90
  fs.writeFileSync(getQwenSettingsPath(), JSON.stringify(settings, null, 2));
77
91
  return true;
78
92
  }
@@ -100,14 +114,24 @@ export async function clearQwenProxySettings(expectedBaseUrl) {
100
114
  logger.debug("[proxy] Qwen clear: no original-auth snapshot found, leaving security.auth intact");
101
115
  return false;
102
116
  }
103
- const snapshot = settings[QWEN_ORIGINAL_KEY];
104
- if (snapshot === null) {
105
- delete security.auth;
117
+ // Only restore what we can prove is ours. The base-URL check above lets
118
+ // through a block still pointing at the proxy whose key the user rotated
119
+ // beside it — overwriting that would destroy a live credential. Our
120
+ // bookkeeping keys go either way, so a stale sentinel cannot outlive us.
121
+ if (isProxyOwnedValue({ written: settings[QWEN_WRITTEN_KEY], current: auth })) {
122
+ const snapshot = settings[QWEN_ORIGINAL_KEY];
123
+ if (snapshot === null) {
124
+ delete security.auth;
125
+ }
126
+ else {
127
+ security.auth = snapshot;
128
+ }
106
129
  }
107
130
  else {
108
- security.auth = snapshot;
131
+ logger.debug("[proxy] Qwen clear: security.auth was edited after the proxy wrote it, leaving it intact");
109
132
  }
110
133
  delete settings[QWEN_ORIGINAL_KEY];
134
+ delete settings[QWEN_WRITTEN_KEY];
111
135
  settings.security = security;
112
136
  fs.writeFileSync(getQwenSettingsPath(), JSON.stringify(settings, null, 2));
113
137
  return true;
@@ -1,3 +1,4 @@
1
+ import { logger } from "../../lib/utils/logger.js";
1
2
  import { claudeCodeConfigurator } from "./claudeCode.js";
2
3
  import { openCodeConfigurator } from "./openCode.js";
3
4
  import { codexConfigurator } from "./codex.js";
@@ -34,11 +35,16 @@ export async function applyAllClients(proxyBaseUrl) {
34
35
  results.push({ id: client.id, displayName: client.displayName, applied });
35
36
  }
36
37
  catch (error) {
38
+ const wrapped = error instanceof Error ? error : new Error(String(error));
39
+ // The result already carries the id, but a caller is free to ignore it.
40
+ // Naming the client here means a path-resolution failure (a HOME that
41
+ // moved, a directory that vanished mid-run) is never fully silent.
42
+ logger.debug(`[proxy] ${client.id} apply failed: ${wrapped.message}`);
37
43
  results.push({
38
44
  id: client.id,
39
45
  displayName: client.displayName,
40
46
  applied: false,
41
- error: error instanceof Error ? error : new Error(String(error)),
47
+ error: wrapped,
42
48
  });
43
49
  }
44
50
  }
@@ -56,11 +62,13 @@ export async function restoreAllClients(proxyBaseUrl) {
56
62
  });
57
63
  }
58
64
  catch (error) {
65
+ const wrapped = error instanceof Error ? error : new Error(String(error));
66
+ logger.debug(`[proxy] ${client.id} restore failed: ${wrapped.message}`);
59
67
  results.push({
60
68
  id: client.id,
61
69
  displayName: client.displayName,
62
70
  restored: false,
63
- error: error instanceof Error ? error : new Error(String(error)),
71
+ error: wrapped,
64
72
  });
65
73
  }
66
74
  }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Shared snapshot bookkeeping for the JSON-file configurators.
3
+ *
4
+ * Every writer that edits a user's config in place has to answer one question
5
+ * on each apply(): is the value sitting in the file right now the one *we* put
6
+ * there, or one the user put there?
7
+ *
8
+ * Snapshotting only on first touch is not enough. The sentinel is persisted in
9
+ * the user's file so a restore survives a crash, which means it also survives
10
+ * an unclean kill where no restore ever ran. If the user then edits the block
11
+ * by hand — reasonably, since the proxy is gone — a presence-only guard keeps
12
+ * the stale snapshot, apply() overwrites their edit, and the next restore
13
+ * writes the stale value back over it. For Qwen that value is a live API key.
14
+ *
15
+ * So each writer also records what it wrote. A current value that still matches
16
+ * the recorded write is ours and the snapshot stands; anything else is the
17
+ * user's and must be re-snapshotted before we overwrite it.
18
+ */
19
+ /**
20
+ * Whether apply() should record `current` as the user's original value.
21
+ *
22
+ * - No snapshot yet: record one. This is the first touch.
23
+ * - Snapshot present but no record of what we wrote: leave it alone. The file
24
+ * was written by a version that predates the write-record, so we cannot tell
25
+ * ours from the user's and the safe move is the old behaviour.
26
+ * - Snapshot present and the current value is exactly what we wrote: leave it
27
+ * alone. This is the repeat-apply case the first-touch guard exists for.
28
+ * - Snapshot present and the current value is *not* what we wrote: re-record.
29
+ * The user replaced it while the proxy was not running.
30
+ */
31
+ export declare function shouldCaptureSnapshot(args: {
32
+ hasSnapshot: boolean;
33
+ written: unknown;
34
+ current: unknown;
35
+ }): boolean;
36
+ /** Deep copy through JSON, so a snapshot cannot alias the object it describes. */
37
+ export declare function cloneForSnapshot<T>(value: T): T;
38
+ /**
39
+ * Whether the value currently in the file is the one this writer put there.
40
+ *
41
+ * Restore uses this as its licence to act. A base-URL check alone catches a
42
+ * user who repointed the client elsewhere, but not one who kept the proxy URL
43
+ * and changed something beside it — a rotated key, an added field. Reverting
44
+ * those to the snapshot destroys a deliberate edit; for Qwen it destroys a
45
+ * credential. No record of what we wrote (a config from before this existed)
46
+ * means we cannot prove ownership either way, so the caller keeps its previous
47
+ * behaviour rather than refusing every restore.
48
+ */
49
+ export declare function isProxyOwnedValue(args: {
50
+ written: unknown;
51
+ current: unknown;
52
+ }): boolean;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Shared snapshot bookkeeping for the JSON-file configurators.
3
+ *
4
+ * Every writer that edits a user's config in place has to answer one question
5
+ * on each apply(): is the value sitting in the file right now the one *we* put
6
+ * there, or one the user put there?
7
+ *
8
+ * Snapshotting only on first touch is not enough. The sentinel is persisted in
9
+ * the user's file so a restore survives a crash, which means it also survives
10
+ * an unclean kill where no restore ever ran. If the user then edits the block
11
+ * by hand — reasonably, since the proxy is gone — a presence-only guard keeps
12
+ * the stale snapshot, apply() overwrites their edit, and the next restore
13
+ * writes the stale value back over it. For Qwen that value is a live API key.
14
+ *
15
+ * So each writer also records what it wrote. A current value that still matches
16
+ * the recorded write is ours and the snapshot stands; anything else is the
17
+ * user's and must be re-snapshotted before we overwrite it.
18
+ */
19
+ /**
20
+ * Structural equality, insensitive to object key order.
21
+ *
22
+ * Comparing serialised JSON would be simpler but wrong: `JSON.stringify` is not
23
+ * canonical, so anything that rewrites the user's config — a formatter, an
24
+ * editor's "sort keys", another tool round-tripping the file — reorders keys and
25
+ * makes our own block look like the user's. The writer would then snapshot the
26
+ * proxy block as the "original" and restore it over the real config later.
27
+ *
28
+ * `undefined` is only ever equal to itself: absent is not the same as present.
29
+ */
30
+ function valuesMatch(current, written) {
31
+ if (current === undefined || written === undefined) {
32
+ return current === written;
33
+ }
34
+ if (current === null || written === null) {
35
+ return current === written;
36
+ }
37
+ if (typeof current !== "object" || typeof written !== "object") {
38
+ return current === written;
39
+ }
40
+ if (Array.isArray(current) || Array.isArray(written)) {
41
+ if (!Array.isArray(current) || !Array.isArray(written)) {
42
+ return false;
43
+ }
44
+ return (current.length === written.length &&
45
+ current.every((item, index) => valuesMatch(item, written[index])));
46
+ }
47
+ const a = current;
48
+ const b = written;
49
+ const aKeys = Object.keys(a);
50
+ if (aKeys.length !== Object.keys(b).length) {
51
+ return false;
52
+ }
53
+ return aKeys.every((key) => Object.prototype.hasOwnProperty.call(b, key) &&
54
+ valuesMatch(a[key], b[key]));
55
+ }
56
+ /**
57
+ * Whether apply() should record `current` as the user's original value.
58
+ *
59
+ * - No snapshot yet: record one. This is the first touch.
60
+ * - Snapshot present but no record of what we wrote: leave it alone. The file
61
+ * was written by a version that predates the write-record, so we cannot tell
62
+ * ours from the user's and the safe move is the old behaviour.
63
+ * - Snapshot present and the current value is exactly what we wrote: leave it
64
+ * alone. This is the repeat-apply case the first-touch guard exists for.
65
+ * - Snapshot present and the current value is *not* what we wrote: re-record.
66
+ * The user replaced it while the proxy was not running.
67
+ */
68
+ export function shouldCaptureSnapshot(args) {
69
+ if (!args.hasSnapshot) {
70
+ return true;
71
+ }
72
+ if (args.written === undefined) {
73
+ return false;
74
+ }
75
+ return !valuesMatch(args.current, args.written);
76
+ }
77
+ /** Deep copy through JSON, so a snapshot cannot alias the object it describes. */
78
+ export function cloneForSnapshot(value) {
79
+ return JSON.parse(JSON.stringify(value));
80
+ }
81
+ /**
82
+ * Whether the value currently in the file is the one this writer put there.
83
+ *
84
+ * Restore uses this as its licence to act. A base-URL check alone catches a
85
+ * user who repointed the client elsewhere, but not one who kept the proxy URL
86
+ * and changed something beside it — a rotated key, an added field. Reverting
87
+ * those to the snapshot destroys a deliberate edit; for Qwen it destroys a
88
+ * credential. No record of what we wrote (a config from before this existed)
89
+ * means we cannot prove ownership either way, so the caller keeps its previous
90
+ * behaviour rather than refusing every restore.
91
+ */
92
+ export function isProxyOwnedValue(args) {
93
+ if (args.written === undefined) {
94
+ return true;
95
+ }
96
+ return valuesMatch(args.current, args.written);
97
+ }
98
+ //# sourceMappingURL=snapshot.js.map
@@ -20,7 +20,7 @@ import { resolveToolExecutionRecords } from "../../core/toolExecutionRecorder.js
20
20
  import { buildGeminiResponseSchema, buildNativeConfig, computeMaxSteps, createContextGuard, buildUserPartsWithMultimodal, extractThoughtSignature, handleMaxStepsTermination, prependConversationMessages, } from "../googleNativeGemini3/index.js";
21
21
  import { createStreamChannel } from "../../core/streamChannel.js";
22
22
  import { toNativeToolDeclarations } from "../../core/nativeToolFormat.js";
23
- import { createProxyFetch } from "../../proxy/proxyFetch.js";
23
+ import { warnGoogleSdkIgnoresProxy } from "../../proxy/proxyFetch.js";
24
24
  // Google AI Live API types now imported from ../types/providerSpecific.js
25
25
  // Import proper types for multimodal message handling
26
26
  // Create Google GenAI client
@@ -38,16 +38,19 @@ async function createGoogleGenAIClient(apiKey, baseURL) {
38
38
  });
39
39
  }
40
40
  const Ctor = ctor;
41
- // Include httpOptions with proxy fetch for corporate network support.
41
+ // httpOptions carries the endpoint override and nothing else. It used to
42
+ // also pass a proxy fetch, which the SDK silently ignored — see
43
+ // warnGoogleSdkIgnoresProxy for why that is not fixable here.
44
+ //
42
45
  // baseUrl is only included when resolved — verified against
43
46
  // @google/genai's ApiClient (dist/node/index.cjs) that it falls back to
44
47
  // its own default whenever httpOptions.baseUrl is undefined, so omitting
45
48
  // the key and passing `baseUrl: undefined` behave identically; the key is
46
49
  // still omitted outright for a cleaner outbound config object.
50
+ warnGoogleSdkIgnoresProxy("GoogleAIStudio");
47
51
  return new Ctor({
48
52
  apiKey,
49
53
  httpOptions: {
50
- fetch: createProxyFetch(),
51
54
  ...(baseURL ? { baseUrl: baseURL } : {}),
52
55
  },
53
56
  });
@@ -54,6 +54,27 @@ export declare const resolveVertexLocation: (modelName: string | undefined, conf
54
54
  export declare class GoogleVertexProvider extends BaseProvider {
55
55
  private projectId;
56
56
  private location;
57
+ /**
58
+ * Vertex AI Express Mode credentials.
59
+ *
60
+ * Vertex supports two authentication modes. The long-standing one pairs a
61
+ * project and location with Application Default Credentials, which makes
62
+ * the SDK mint an OAuth token through google-auth-library before every
63
+ * request. Express Mode instead authenticates with an API key alone.
64
+ *
65
+ * Express is used only when an apiKey is supplied WITHOUT an explicit
66
+ * project or location, so existing ADC callers — including those already
67
+ * passing an apiKey alongside a project — keep exactly the behaviour they
68
+ * have today.
69
+ */
70
+ private expressApiKey?;
71
+ /**
72
+ * Optional endpoint override, mirroring AI Studio's
73
+ * `credentials.googleAiStudio.baseURL`: per-request credential first, then
74
+ * the environment, then unset so the SDK applies its own default. Blank
75
+ * values count as unset so an empty override cannot clobber that default.
76
+ */
77
+ private baseURL?;
57
78
  private registeredTools;
58
79
  private toolContext;
59
80
  private static modelConfigCache;
@@ -105,6 +126,18 @@ export declare class GoogleVertexProvider extends BaseProvider {
105
126
  * Create @google/genai client configured for Vertex AI
106
127
  */
107
128
  private createVertexGenAIClient;
129
+ /** Endpoint override: credential, then environment, then unset. */
130
+ private resolveBaseURL;
131
+ /**
132
+ * Express Mode key, if this provider should use it.
133
+ *
134
+ * Deliberately NOT read from GOOGLE_API_KEY: that variable is already set
135
+ * by callers who also configure a project, and treating it as an Express
136
+ * opt-in would silently switch their authentication mode. Express is opted
137
+ * into per request, or through GOOGLE_VERTEX_API_KEY which exists only for
138
+ * this purpose.
139
+ */
140
+ private resolveExpressApiKey;
108
141
  /**
109
142
  * Convert one AI-SDK tool into a Vertex Gemini function declaration.
110
143
  * Single source for the pre-loop snapshot AND the mid-turn discovery