@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.
- package/CHANGELOG.md +6 -2
- package/dist/browser/neurolink.min.js +396 -396
- package/dist/cli/commands/proxy.js +42 -0
- package/dist/cli/commands/proxyAnalyze.js +10 -1
- package/dist/cli/proxy-clients/claudeCode.js +42 -10
- package/dist/cli/proxy-clients/openCode.js +37 -15
- package/dist/cli/proxy-clients/qwenCode.js +33 -9
- package/dist/cli/proxy-clients/registry.js +10 -2
- package/dist/cli/proxy-clients/snapshot.d.ts +52 -0
- package/dist/cli/proxy-clients/snapshot.js +98 -0
- package/dist/lib/providers/googleAiStudio/client.js +6 -3
- package/dist/lib/providers/googleVertex/client.js +6 -3
- package/dist/lib/proxy/codexUsage.d.ts +68 -0
- package/dist/lib/proxy/codexUsage.js +247 -0
- package/dist/lib/proxy/proxyAnalysis.js +87 -3
- package/dist/lib/proxy/proxyFetch.d.ts +1 -0
- package/dist/lib/proxy/proxyFetch.js +29 -0
- package/dist/lib/proxy/proxyTracer.d.ts +13 -2
- package/dist/lib/proxy/proxyTracer.js +29 -7
- package/dist/lib/proxy/proxyTranslationEngine.js +22 -5
- package/dist/lib/server/routes/codexProxyRoutes.js +29 -1
- package/dist/lib/server/routes/openaiProxyRoutes.js +5 -0
- package/dist/lib/types/proxy.d.ts +65 -0
- package/dist/lib/utils/pricing.d.ts +9 -0
- package/dist/lib/utils/pricing.js +136 -1
- package/dist/providers/googleAiStudio/client.js +6 -3
- package/dist/providers/googleVertex/client.js +6 -3
- package/dist/proxy/codexUsage.d.ts +68 -0
- package/dist/proxy/codexUsage.js +246 -0
- package/dist/proxy/proxyAnalysis.js +87 -3
- package/dist/proxy/proxyFetch.d.ts +1 -0
- package/dist/proxy/proxyFetch.js +29 -0
- package/dist/proxy/proxyTracer.d.ts +13 -2
- package/dist/proxy/proxyTracer.js +29 -7
- package/dist/proxy/proxyTranslationEngine.js +22 -5
- package/dist/server/routes/codexProxyRoutes.js +29 -1
- package/dist/server/routes/openaiProxyRoutes.js +5 -0
- package/dist/types/proxy.d.ts +65 -0
- package/dist/utils/pricing.d.ts +9 -0
- package/dist/utils/pricing.js +136 -1
- 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
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
|
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 (!(
|
|
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
|
-
|
|
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
|
|
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
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
if (
|
|
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
|
-
|
|
60
|
-
? JSON.parse(JSON.stringify(provider.neurolink))
|
|
61
|
-
: null;
|
|
69
|
+
currentBlock === undefined ? null : cloneForSnapshot(currentBlock);
|
|
62
70
|
}
|
|
63
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
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
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// the
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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 {
|
|
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
|
-
//
|
|
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 {
|
|
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
|
-
//
|
|
867
|
-
fetch
|
|
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.
|