@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.
- 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.d.ts +33 -0
- package/dist/lib/providers/googleVertex/client.js +110 -11
- 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.d.ts +33 -0
- package/dist/providers/googleVertex/client.js +110 -11
- 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 +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
|
-
//
|
|
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
|
});
|
|
@@ -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
|