@juspay/neurolink 11.12.0 → 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 (41) 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.js +6 -3
  13. package/dist/lib/proxy/codexUsage.d.ts +68 -0
  14. package/dist/lib/proxy/codexUsage.js +247 -0
  15. package/dist/lib/proxy/proxyAnalysis.js +87 -3
  16. package/dist/lib/proxy/proxyFetch.d.ts +1 -0
  17. package/dist/lib/proxy/proxyFetch.js +29 -0
  18. package/dist/lib/proxy/proxyTracer.d.ts +13 -2
  19. package/dist/lib/proxy/proxyTracer.js +29 -7
  20. package/dist/lib/proxy/proxyTranslationEngine.js +22 -5
  21. package/dist/lib/server/routes/codexProxyRoutes.js +29 -1
  22. package/dist/lib/server/routes/openaiProxyRoutes.js +5 -0
  23. package/dist/lib/types/proxy.d.ts +65 -0
  24. package/dist/lib/utils/pricing.d.ts +9 -0
  25. package/dist/lib/utils/pricing.js +136 -1
  26. package/dist/providers/googleAiStudio/client.js +6 -3
  27. package/dist/providers/googleVertex/client.js +6 -3
  28. package/dist/proxy/codexUsage.d.ts +68 -0
  29. package/dist/proxy/codexUsage.js +246 -0
  30. package/dist/proxy/proxyAnalysis.js +87 -3
  31. package/dist/proxy/proxyFetch.d.ts +1 -0
  32. package/dist/proxy/proxyFetch.js +29 -0
  33. package/dist/proxy/proxyTracer.d.ts +13 -2
  34. package/dist/proxy/proxyTracer.js +29 -7
  35. package/dist/proxy/proxyTranslationEngine.js +22 -5
  36. package/dist/server/routes/codexProxyRoutes.js +29 -1
  37. package/dist/server/routes/openaiProxyRoutes.js +5 -0
  38. package/dist/types/proxy.d.ts +65 -0
  39. package/dist/utils/pricing.d.ts +9 -0
  40. package/dist/utils/pricing.js +136 -1
  41. package/package.json +1 -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
  });
@@ -11,7 +11,7 @@ import { DEFAULT_GEMINI_STREAM_TIMEOUT_MS, DEFAULT_MAX_STEPS, DEFAULT_TOOL_EXECU
11
11
  import { ModelConfigurationManager } from "../../core/modelConfiguration.js";
12
12
  import { isSchemaComplexityError } from "../../core/modules/structuredOutputPolicy.js";
13
13
  import { redactUrlForError, stringifyContentSafe, } from "../../utils/logSanitize.js";
14
- import { createProxyFetch } from "../../proxy/proxyFetch.js";
14
+ import { warnGoogleSdkIgnoresProxy } from "../../proxy/proxyFetch.js";
15
15
  import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../../types/index.js";
16
16
  import { classifyProviderError } from "../../utils/errorClassifier.js";
17
17
  import { ERROR_CODES, NeuroLinkError } from "../../utils/errorHandling.js";
@@ -843,6 +843,7 @@ export class GoogleVertexProvider extends BaseProvider {
843
843
  * Create @google/genai client configured for Vertex AI
844
844
  */
845
845
  async createVertexGenAIClient(regionOverride) {
846
+ warnGoogleSdkIgnoresProxy("GoogleVertex");
846
847
  const expressApiKey = this.resolveExpressApiKey();
847
848
  // Resolved only on the ADC path: getVertexProjectId() throws when no
848
849
  // project is configured, which an Express request legitimately has none of.
@@ -863,8 +864,10 @@ export class GoogleVertexProvider extends BaseProvider {
863
864
  const Ctor = ctor;
864
865
  const baseUrl = this.resolveBaseURL();
865
866
  const httpOptions = {
866
- // Proxy fetch for corporate network support.
867
- fetch: createProxyFetch(),
867
+ // The endpoint override and nothing else. This object used to also pass
868
+ // a proxy fetch, which the SDK silently ignored — see
869
+ // warnGoogleSdkIgnoresProxy for why that is not fixable here.
870
+ //
868
871
  // Only set when resolved: the SDK falls back to its own default
869
872
  // whenever httpOptions.baseUrl is undefined, so omitting the key and
870
873
  // passing undefined behave identically.