@juspay/neurolink 12.13.0 → 12.14.1
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 +3 -3
- package/dist/browser/neurolink.min.js +376 -376
- package/dist/cli/commands/proxy.d.ts +1 -0
- package/dist/cli/commands/proxy.js +52 -29
- package/dist/cli/proxy-clients/grok.d.ts +45 -0
- package/dist/cli/proxy-clients/grok.js +342 -0
- package/dist/cli/proxy-clients/registry.d.ts +2 -2
- package/dist/cli/proxy-clients/registry.js +4 -2
- package/dist/constants/proxyModels.d.ts +7 -1
- package/dist/constants/proxyModels.js +7 -1
- package/dist/proxy/bodyCaptureProcessing.d.ts +1 -1
- package/dist/proxy/bodyCaptureProcessing.js +10 -0
- package/dist/proxy/bodyCaptureWorker.d.ts +1 -1
- package/dist/proxy/clientAttribution.js +3 -0
- package/dist/proxy/logCleanupScheduler.js +4 -0
- package/dist/proxy/otelLogSink.d.ts +34 -0
- package/dist/proxy/otelLogSink.js +254 -0
- package/dist/proxy/proxyConfig.d.ts +4 -0
- package/dist/proxy/proxyConfig.js +13 -1
- package/dist/proxy/proxyLifecycle.js +31 -1
- package/dist/proxy/requestLogger.js +45 -10
- package/dist/proxy/workerLog.js +4 -0
- package/dist/types/proxy.d.ts +4 -0
- package/dist/types/proxyClient.d.ts +34 -1
- package/docs-site/static/search-index.json +1 -1
- package/package.json +1 -1
|
@@ -88,5 +88,6 @@ export declare const proxyStatusCommand: CommandModule<object, ProxyStatusArgs>;
|
|
|
88
88
|
export declare const proxyTelemetryCommand: CommandModule<object, ProxyTelemetryArgs>;
|
|
89
89
|
export declare const proxyGuardCommand: CommandModule<object, ProxyGuardArgs>;
|
|
90
90
|
export declare const proxySetupCommand: CommandModule;
|
|
91
|
+
export declare function buildProxyLaunchdPlist(port: number, host: string, envFile?: string, configFile?: string): string;
|
|
91
92
|
export declare const proxyInstallCommand: CommandModule;
|
|
92
93
|
export declare const proxyUninstallCommand: CommandModule;
|
|
@@ -1,23 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Implements commands for managing the Claude multi-account proxy:
|
|
5
|
-
* - neurolink proxy start — Start the proxy server
|
|
6
|
-
* - neurolink proxy status — Show proxy status (accounts, sessions, routing)
|
|
7
|
-
*
|
|
8
|
-
* The proxy creates a NeuroLink instance and builds a Hono app that registers
|
|
9
|
-
* Claude-compatible proxy routes. All requests flow through ctx.neurolink
|
|
10
|
-
* (generate/stream), with an optional ModelRouter for model remapping.
|
|
11
|
-
*/
|
|
1
|
+
import { initializeProxyOtelLogs, routeProxyConsoleToOtel, flushProxyOtelLogs, shutdownProxyOtelLogs, isProxyOtelOnly, withProxyOtelLogShutdown, } from "../../proxy/otelLogSink.js";
|
|
2
|
+
import { writeFileAtomic } from "../proxy-clients/snapshot.js";
|
|
12
3
|
import { spawn } from "node:child_process";
|
|
13
4
|
import { homedir } from "node:os";
|
|
14
|
-
import { dirname, join
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
15
6
|
import { stripVTControlCharacters } from "node:util";
|
|
16
7
|
import chalk from "chalk";
|
|
17
8
|
import ora from "ora";
|
|
18
9
|
import { buildProxyHealthResponse, createProxyReadinessState, markProxyDrainingForUpdate, markProxyReady, resumeProxyConnections, waitForProxyReadiness, } from "../../proxy/proxyHealth.js";
|
|
19
10
|
import { logger } from "../../utils/logger.js";
|
|
20
11
|
import { applyAllClients, restoreAllClients, } from "../proxy-clients/registry.js";
|
|
12
|
+
import { resolveProxyConfigPath } from "../../proxy/proxyConfig.js";
|
|
21
13
|
import { redactUrlsInText, sanitizeForLog, } from "../../utils/logSanitize.js";
|
|
22
14
|
import { withTimeout } from "../../utils/async/withTimeout.js";
|
|
23
15
|
import { formatUptime, isProcessRunning, StateFileManager, } from "../utils/serverUtils.js";
|
|
@@ -2397,6 +2389,8 @@ function registerProxyShutdownHandlers(params) {
|
|
|
2397
2389
|
}
|
|
2398
2390
|
try {
|
|
2399
2391
|
const { flushOpenTelemetry, shutdownOpenTelemetry } = await import("../../services/server/ai/observability/instrumentation.js");
|
|
2392
|
+
await flushProxyOtelLogs();
|
|
2393
|
+
await shutdownProxyOtelLogs();
|
|
2400
2394
|
await flushOpenTelemetry();
|
|
2401
2395
|
await shutdownOpenTelemetry();
|
|
2402
2396
|
}
|
|
@@ -2696,7 +2690,9 @@ async function startProxyRuntime(params) {
|
|
|
2696
2690
|
logger.always(` ${chalk.bold("Env File:")} ${chalk.cyan(params.loadedEnvFile)}`);
|
|
2697
2691
|
}
|
|
2698
2692
|
if (!isDev) {
|
|
2699
|
-
for (const result of await applyAllClients(url
|
|
2693
|
+
for (const result of await applyAllClients(url, {
|
|
2694
|
+
configPath: params.configPath,
|
|
2695
|
+
})) {
|
|
2700
2696
|
if (result.error) {
|
|
2701
2697
|
// Visible, not debug-level. A client whose config could not be written
|
|
2702
2698
|
// will keep talking to its own upstream, which looks like the proxy
|
|
@@ -2755,6 +2751,9 @@ async function startProxyRuntime(params) {
|
|
|
2755
2751
|
* serving-process exits.
|
|
2756
2752
|
*/
|
|
2757
2753
|
async function runLaunchdProxySupervisor(argv, spinner) {
|
|
2754
|
+
await loadProxyStartEnv(argv, spinner);
|
|
2755
|
+
initializeProxyOtelLogs("supervisor");
|
|
2756
|
+
routeProxyConsoleToOtel();
|
|
2758
2757
|
await ensureProxyStartAllowed(spinner);
|
|
2759
2758
|
const entryScript = process.argv[1];
|
|
2760
2759
|
if (!entryScript) {
|
|
@@ -2868,6 +2867,8 @@ async function runLaunchdProxySupervisor(argv, spinner) {
|
|
|
2868
2867
|
updaterSupervisor.stop();
|
|
2869
2868
|
await rollingServer.close();
|
|
2870
2869
|
await flushProxyLifecycleEvents().catch((error) => logger.warn(String(error)));
|
|
2870
|
+
await flushProxyOtelLogs().catch(() => undefined);
|
|
2871
|
+
await shutdownProxyOtelLogs().catch(() => undefined);
|
|
2871
2872
|
const supervisorState = loadProxySupervisorState();
|
|
2872
2873
|
if (supervisorState?.pid === process.pid) {
|
|
2873
2874
|
clearProxySupervisorState();
|
|
@@ -2948,14 +2949,14 @@ async function startProxyCommandHandler(argv) {
|
|
|
2948
2949
|
env: baseEnv,
|
|
2949
2950
|
});
|
|
2950
2951
|
const loadedEnvFile = await loadProxyStartEnv(argv, spinner);
|
|
2952
|
+
initializeProxyOtelLogs("worker");
|
|
2953
|
+
routeProxyConsoleToOtel();
|
|
2951
2954
|
// Reuse upstream TCP connections (longer keep-alive + bounded pool) instead
|
|
2952
2955
|
// of opening a new flow per request — cuts outbound flow churn through host
|
|
2953
2956
|
// content-filters. Runs once, after env load so it can be tuned via env.
|
|
2954
2957
|
configureProxyKeepAliveDispatcher();
|
|
2955
2958
|
const { neurolink, logsDir } = await createProxyNeurolinkRuntime(devPaths?.logsDir);
|
|
2956
|
-
const configPath = argv.config
|
|
2957
|
-
? resolve(argv.config)
|
|
2958
|
-
: join(homedir(), ".neurolink", "proxy-config.yaml");
|
|
2959
|
+
const configPath = resolveProxyConfigPath(argv.config);
|
|
2959
2960
|
const runtimeConfigStore = await ProxyRuntimeConfigStore.create({
|
|
2960
2961
|
configPath,
|
|
2961
2962
|
configRequired: Boolean(argv.config),
|
|
@@ -3012,6 +3013,7 @@ async function startProxyCommandHandler(argv) {
|
|
|
3012
3013
|
passthrough,
|
|
3013
3014
|
logsDir,
|
|
3014
3015
|
runtimeConfigStore,
|
|
3016
|
+
configPath,
|
|
3015
3017
|
});
|
|
3016
3018
|
}
|
|
3017
3019
|
catch (error) {
|
|
@@ -3590,7 +3592,9 @@ export const proxyGuardCommand = {
|
|
|
3590
3592
|
default: true,
|
|
3591
3593
|
});
|
|
3592
3594
|
},
|
|
3593
|
-
handler: async (argv) => {
|
|
3595
|
+
handler: withProxyOtelLogShutdown(async (argv) => {
|
|
3596
|
+
initializeProxyOtelLogs("updater");
|
|
3597
|
+
routeProxyConsoleToOtel();
|
|
3594
3598
|
const host = argv.host ?? "127.0.0.1";
|
|
3595
3599
|
const port = argv.port ?? 55669;
|
|
3596
3600
|
const parentPid = Number(argv.parentPid);
|
|
@@ -3914,6 +3918,8 @@ export const proxyGuardCommand = {
|
|
|
3914
3918
|
logger.always(`[updater] update successful: now running ${result.latestVersion}`);
|
|
3915
3919
|
persistUpdaterState("record successful update", () => recordSuccessfulUpdate(result.latestVersion));
|
|
3916
3920
|
// The replacement proxy starts a worker running the new version.
|
|
3921
|
+
await flushProxyOtelLogs().catch(() => undefined);
|
|
3922
|
+
await shutdownProxyOtelLogs().catch(() => undefined);
|
|
3917
3923
|
process.exit(0);
|
|
3918
3924
|
}
|
|
3919
3925
|
else {
|
|
@@ -4081,7 +4087,7 @@ export const proxyGuardCommand = {
|
|
|
4081
4087
|
if (cleared && !argv.quiet) {
|
|
4082
4088
|
logger.always(`[proxy] fail-open guard removed stale ${expectedBaseUrl} from Claude settings`);
|
|
4083
4089
|
}
|
|
4084
|
-
},
|
|
4090
|
+
}),
|
|
4085
4091
|
};
|
|
4086
4092
|
// =============================================================================
|
|
4087
4093
|
// PROXY SETUP COMMAND
|
|
@@ -4174,7 +4180,9 @@ export const proxySetupCommand = {
|
|
|
4174
4180
|
const nextStep = stepNum + 1;
|
|
4175
4181
|
console.info(chalk.blue(`\nStep ${nextStep}:`) + " Configuring Claude Code...");
|
|
4176
4182
|
const url = `http://127.0.0.1:${port}`;
|
|
4177
|
-
for (const result of await applyAllClients(url
|
|
4183
|
+
for (const result of await applyAllClients(url, {
|
|
4184
|
+
configPath: resolveProxyConfigPath(argv.config),
|
|
4185
|
+
})) {
|
|
4178
4186
|
if (result.error) {
|
|
4179
4187
|
console.info(chalk.yellow(` ⚠ Could not auto-configure ${result.displayName}: ${result.error.message}`));
|
|
4180
4188
|
// Claude Code is the one client whose manual fallback is a single
|
|
@@ -4253,7 +4261,7 @@ function buildLaunchdPath() {
|
|
|
4253
4261
|
}
|
|
4254
4262
|
return [...segments].join(":");
|
|
4255
4263
|
}
|
|
4256
|
-
function
|
|
4264
|
+
export function buildProxyLaunchdPlist(port, host, envFile, configFile) {
|
|
4257
4265
|
// The plist invokes the trampoline script (a tiny shell wrapper at
|
|
4258
4266
|
// ~/.neurolink/bin/neurolink-proxy) which re-resolves the real
|
|
4259
4267
|
// `neurolink` binary via PATH on every launch. This way, launchd
|
|
@@ -4269,6 +4277,20 @@ function buildPlist(port, host, envFile, configFile) {
|
|
|
4269
4277
|
<string>--config</string>
|
|
4270
4278
|
<string>${escapeXml(configFile)}</string>`
|
|
4271
4279
|
: "";
|
|
4280
|
+
const otelEnvironment = isProxyOtelOnly()
|
|
4281
|
+
? [
|
|
4282
|
+
"NEUROLINK_PROXY_LOG_SINK",
|
|
4283
|
+
"OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
4284
|
+
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
|
|
4285
|
+
"OTEL_EXPORTER_OTLP_HEADERS",
|
|
4286
|
+
"OTEL_EXPORTER_OTLP_LOGS_HEADERS",
|
|
4287
|
+
"OTEL_SERVICE_NAME",
|
|
4288
|
+
"NEUROLINK_PROXY_SESSION_SECRET",
|
|
4289
|
+
]
|
|
4290
|
+
.filter((name) => process.env[name] !== undefined)
|
|
4291
|
+
.map((name) => ` <key>${name}</key>\n <string>${escapeXml(process.env[name])}</string>`)
|
|
4292
|
+
.join("\n")
|
|
4293
|
+
: "";
|
|
4272
4294
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
4273
4295
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
4274
4296
|
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -4307,10 +4329,10 @@ ${configArgs}
|
|
|
4307
4329
|
<integer>45</integer>
|
|
4308
4330
|
|
|
4309
4331
|
<key>StandardOutPath</key>
|
|
4310
|
-
<string>${join(homedir(), ".neurolink", "logs", "proxy-launchd-stdout.log")}</string>
|
|
4332
|
+
<string>${isProxyOtelOnly() ? "/dev/null" : join(homedir(), ".neurolink", "logs", "proxy-launchd-stdout.log")}</string>
|
|
4311
4333
|
|
|
4312
4334
|
<key>StandardErrorPath</key>
|
|
4313
|
-
<string>${join(homedir(), ".neurolink", "logs", "proxy-launchd-stderr.log")}</string>
|
|
4335
|
+
<string>${isProxyOtelOnly() ? "/dev/null" : join(homedir(), ".neurolink", "logs", "proxy-launchd-stderr.log")}</string>
|
|
4314
4336
|
|
|
4315
4337
|
<key>EnvironmentVariables</key>
|
|
4316
4338
|
<dict>
|
|
@@ -4318,6 +4340,7 @@ ${configArgs}
|
|
|
4318
4340
|
<string>${buildLaunchdPath()}</string>
|
|
4319
4341
|
<key>HOME</key>
|
|
4320
4342
|
<string>${homedir()}</string>
|
|
4343
|
+
${otelEnvironment}
|
|
4321
4344
|
</dict>
|
|
4322
4345
|
</dict>
|
|
4323
4346
|
</plist>`;
|
|
@@ -4358,15 +4381,13 @@ export const proxyInstallCommand = {
|
|
|
4358
4381
|
console.info(chalk.yellow("On Linux, use systemd. On Windows, use Task Scheduler."));
|
|
4359
4382
|
process.exit(1);
|
|
4360
4383
|
}
|
|
4361
|
-
const {
|
|
4384
|
+
const { mkdirSync, existsSync, chmodSync } = await import("fs");
|
|
4362
4385
|
const envResolution = resolveProxyEnvFile({
|
|
4363
4386
|
explicitEnvFile: argv.envFile,
|
|
4364
4387
|
});
|
|
4365
4388
|
const envFile = envResolution.path;
|
|
4366
4389
|
const explicitConfig = argv.config;
|
|
4367
|
-
const configPath = explicitConfig
|
|
4368
|
-
? resolve(explicitConfig)
|
|
4369
|
-
: join(homedir(), ".neurolink", "proxy-config.yaml");
|
|
4390
|
+
const configPath = resolveProxyConfigPath(explicitConfig);
|
|
4370
4391
|
if (explicitConfig && !existsSync(configPath)) {
|
|
4371
4392
|
console.info(chalk.red(`Proxy config file not found: ${configPath}`));
|
|
4372
4393
|
process.exit(1);
|
|
@@ -4376,8 +4397,9 @@ export const proxyInstallCommand = {
|
|
|
4376
4397
|
console.info(chalk.red(`Proxy env file not found: ${envFile}`));
|
|
4377
4398
|
process.exit(1);
|
|
4378
4399
|
}
|
|
4400
|
+
await loadProxyEnvFile({ explicitEnvFile: envFile });
|
|
4379
4401
|
const logsDir = join(homedir(), ".neurolink", "logs");
|
|
4380
|
-
if (!existsSync(logsDir)) {
|
|
4402
|
+
if (!isProxyOtelOnly() && !existsSync(logsDir)) {
|
|
4381
4403
|
mkdirSync(logsDir, { recursive: true });
|
|
4382
4404
|
}
|
|
4383
4405
|
if (!existsSync(PLIST_DIR)) {
|
|
@@ -4401,8 +4423,9 @@ export const proxyInstallCommand = {
|
|
|
4401
4423
|
process.exit(1);
|
|
4402
4424
|
}
|
|
4403
4425
|
console.info(chalk.green(`✓ Trampoline validated (resolves to neurolink v${trampolineVersion})`));
|
|
4404
|
-
const plist =
|
|
4405
|
-
|
|
4426
|
+
const plist = buildProxyLaunchdPlist(port, host, envFile, configFile);
|
|
4427
|
+
await writeFileAtomic(PLIST_PATH, plist, 0o600);
|
|
4428
|
+
chmodSync(PLIST_PATH, 0o600);
|
|
4406
4429
|
console.info(chalk.green(`✓ Plist written to ${PLIST_PATH}`));
|
|
4407
4430
|
if (envFile) {
|
|
4408
4431
|
console.info(chalk.green(`✓ Proxy env file: ${envFile}`));
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grok Build client configurator.
|
|
3
|
+
*
|
|
4
|
+
* Grok Build (`grok`, xAI's terminal agent) is a Codex-shaped TOML client
|
|
5
|
+
* that speaks three wire formats. Built-in `grok-4.6` / `grok-4.5` stay on
|
|
6
|
+
* xAI (`cli-chat-proxy.grok.com`, Responses API, 500k window). This writer
|
|
7
|
+
* does not remap those. It adds the proxy's advertised catalog as extra
|
|
8
|
+
* picker entries so Grok can send Claude traffic through `/v1/messages`
|
|
9
|
+
* (passthrough) and Gemini/OpenAI traffic through `/v1/chat/completions`
|
|
10
|
+
* (translation), each with a `context_window` that Grok's own compaction
|
|
11
|
+
* will honour — the proxy does not truncate.
|
|
12
|
+
*
|
|
13
|
+
* Snapshot lives in `~/.neurolink/`, following Codex, never inside
|
|
14
|
+
* `config.toml`. Grok's TOML parser is not a closed schema, but putting
|
|
15
|
+
* bookkeeping keys in the user's file is how OpenCode was bricked, and new
|
|
16
|
+
* writers do not rely on tolerance.
|
|
17
|
+
*
|
|
18
|
+
* Adaptive thinking: Grok's global `default_reasoning_effort = "xhigh"`
|
|
19
|
+
* becomes Anthropic `thinking.type = "adaptive"`. Haiku 4.5 rejects that
|
|
20
|
+
* with 400. Only Claude Opus/Sonnet 4.6 and 5.x keep reasoning enabled.
|
|
21
|
+
*/
|
|
22
|
+
import type { CliGrokProxyModelSpec, CliProxyClientApplyOptions, CliProxyClientConfigurator, ModelMapping } from "../../types/index.js";
|
|
23
|
+
declare function getGrokConfigDir(): string;
|
|
24
|
+
declare function getGrokConfigPath(): string;
|
|
25
|
+
declare function getGrokSnapshotPath(): string;
|
|
26
|
+
declare function classifyGrokProxyModel(id: string, mapping?: ModelMapping): CliGrokProxyModelSpec;
|
|
27
|
+
declare function loadRoutedModelIds(configPath?: string): Promise<string[]>;
|
|
28
|
+
declare function catalogModelIds(configPath?: string): Promise<string[]>;
|
|
29
|
+
declare function buildGrokManagedBlock(baseUrl: string, configPath?: string): Promise<string>;
|
|
30
|
+
export declare function setGrokProxySettings(baseUrl: string, options?: CliProxyClientApplyOptions): Promise<boolean>;
|
|
31
|
+
export declare function clearGrokProxySettings(expectedBaseUrl?: string): Promise<boolean>;
|
|
32
|
+
export declare const grokConfigurator: CliProxyClientConfigurator;
|
|
33
|
+
/** Test-only export (CLAUDE.md rule 15 determinism exception). See openCode.ts. */
|
|
34
|
+
export declare const __grokTestHooks: {
|
|
35
|
+
getGrokConfigDir: typeof getGrokConfigDir;
|
|
36
|
+
getGrokConfigPath: typeof getGrokConfigPath;
|
|
37
|
+
getGrokSnapshotPath: typeof getGrokSnapshotPath;
|
|
38
|
+
setGrokProxySettings: typeof setGrokProxySettings;
|
|
39
|
+
clearGrokProxySettings: typeof clearGrokProxySettings;
|
|
40
|
+
classifyGrokProxyModel: typeof classifyGrokProxyModel;
|
|
41
|
+
loadRoutedModelIds: typeof loadRoutedModelIds;
|
|
42
|
+
catalogModelIds: typeof catalogModelIds;
|
|
43
|
+
buildGrokManagedBlock: typeof buildGrokManagedBlock;
|
|
44
|
+
};
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grok Build client configurator.
|
|
3
|
+
*
|
|
4
|
+
* Grok Build (`grok`, xAI's terminal agent) is a Codex-shaped TOML client
|
|
5
|
+
* that speaks three wire formats. Built-in `grok-4.6` / `grok-4.5` stay on
|
|
6
|
+
* xAI (`cli-chat-proxy.grok.com`, Responses API, 500k window). This writer
|
|
7
|
+
* does not remap those. It adds the proxy's advertised catalog as extra
|
|
8
|
+
* picker entries so Grok can send Claude traffic through `/v1/messages`
|
|
9
|
+
* (passthrough) and Gemini/OpenAI traffic through `/v1/chat/completions`
|
|
10
|
+
* (translation), each with a `context_window` that Grok's own compaction
|
|
11
|
+
* will honour — the proxy does not truncate.
|
|
12
|
+
*
|
|
13
|
+
* Snapshot lives in `~/.neurolink/`, following Codex, never inside
|
|
14
|
+
* `config.toml`. Grok's TOML parser is not a closed schema, but putting
|
|
15
|
+
* bookkeeping keys in the user's file is how OpenCode was bricked, and new
|
|
16
|
+
* writers do not rely on tolerance.
|
|
17
|
+
*
|
|
18
|
+
* Adaptive thinking: Grok's global `default_reasoning_effort = "xhigh"`
|
|
19
|
+
* becomes Anthropic `thinking.type = "adaptive"`. Haiku 4.5 rejects that
|
|
20
|
+
* with 400. Only Claude Opus/Sonnet 4.6 and 5.x keep reasoning enabled.
|
|
21
|
+
*/
|
|
22
|
+
import { readFileSync } from "fs";
|
|
23
|
+
import { homedir } from "os";
|
|
24
|
+
import { join } from "path";
|
|
25
|
+
import { logger } from "../../utils/logger.js";
|
|
26
|
+
import { getContextWindowSize } from "../../constants/contextWindows.js";
|
|
27
|
+
import { DEFAULT_PROXY_MODEL_IDS } from "../../constants/proxyModels.js";
|
|
28
|
+
import { defaultProxyConfigPath, parseProxyConfigString, } from "../../proxy/proxyConfig.js";
|
|
29
|
+
import { isUsableSnapshot, shouldCaptureSnapshot, writeFileAtomic, } from "./snapshot.js";
|
|
30
|
+
const GROK_BLOCK_BEGIN = "# >>> neurolink-proxy (managed) >>>";
|
|
31
|
+
const GROK_BLOCK_END = "# <<< neurolink-proxy (managed) <<<";
|
|
32
|
+
const PLACEHOLDER_KEY = "neurolink-proxy";
|
|
33
|
+
const ANTHROPIC_VERSION = "2023-06-01";
|
|
34
|
+
function getGrokConfigDir() {
|
|
35
|
+
const env = process.env.GROK_HOME;
|
|
36
|
+
return env !== undefined && env.trim().length > 0
|
|
37
|
+
? env.trim()
|
|
38
|
+
: join(homedir(), ".grok");
|
|
39
|
+
}
|
|
40
|
+
function getGrokConfigPath() {
|
|
41
|
+
return join(getGrokConfigDir(), "config.toml");
|
|
42
|
+
}
|
|
43
|
+
function getGrokSnapshotPath() {
|
|
44
|
+
return join(homedir(), ".neurolink", "grok-proxy-snapshot.json");
|
|
45
|
+
}
|
|
46
|
+
function escapeRegExp(value) {
|
|
47
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
48
|
+
}
|
|
49
|
+
function tomlKey(id) {
|
|
50
|
+
return /^[A-Za-z0-9_-]+$/.test(id) ? id : JSON.stringify(id);
|
|
51
|
+
}
|
|
52
|
+
function displayNameFor(id) {
|
|
53
|
+
const base = id.replace(/-\d{8}$/, "");
|
|
54
|
+
const pretty = base
|
|
55
|
+
.split("-")
|
|
56
|
+
.map((part) => /^\d/.test(part) ? part : part.charAt(0).toUpperCase() + part.slice(1))
|
|
57
|
+
.join(" ")
|
|
58
|
+
.replace(/(\d) (\d)/g, "$1.$2");
|
|
59
|
+
return `${pretty} (NeuroLink)`;
|
|
60
|
+
}
|
|
61
|
+
function providerForModelId(id) {
|
|
62
|
+
if (id.startsWith("claude-")) {
|
|
63
|
+
return "anthropic";
|
|
64
|
+
}
|
|
65
|
+
if (id.startsWith("gemini-")) {
|
|
66
|
+
return "vertex";
|
|
67
|
+
}
|
|
68
|
+
return "openai";
|
|
69
|
+
}
|
|
70
|
+
function providerFromMapping(provider, fallbackId) {
|
|
71
|
+
const normalized = provider.trim().toLowerCase();
|
|
72
|
+
if (normalized === "anthropic" || normalized === "claude") {
|
|
73
|
+
return "anthropic";
|
|
74
|
+
}
|
|
75
|
+
if (normalized === "openai") {
|
|
76
|
+
return "openai";
|
|
77
|
+
}
|
|
78
|
+
if (normalized === "vertex" ||
|
|
79
|
+
normalized === "google" ||
|
|
80
|
+
normalized === "google-ai" ||
|
|
81
|
+
normalized === "gemini") {
|
|
82
|
+
return "vertex";
|
|
83
|
+
}
|
|
84
|
+
return providerForModelId(fallbackId);
|
|
85
|
+
}
|
|
86
|
+
function isMissingFileError(error) {
|
|
87
|
+
if (!error || typeof error !== "object" || !("code" in error)) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
return error.code === "ENOENT";
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Anthropic `thinking.type = "adaptive"` is what Grok emits for `xhigh`.
|
|
94
|
+
* Measured: Haiku 4.5 returns 400 "adaptive thinking is not supported".
|
|
95
|
+
* Opus/Sonnet 4.6 and the 5-series accept it.
|
|
96
|
+
*/
|
|
97
|
+
function supportsAdaptiveThinking(id) {
|
|
98
|
+
return (/^claude-(opus|sonnet)-4-6$/.test(id) || /^claude-(opus|sonnet)-5/.test(id));
|
|
99
|
+
}
|
|
100
|
+
function classifyGrokProxyModel(id, mapping) {
|
|
101
|
+
const classifyId = mapping && mapping.to.trim().length > 0 ? mapping.to.trim() : id;
|
|
102
|
+
const provider = mapping
|
|
103
|
+
? providerFromMapping(mapping.provider, classifyId)
|
|
104
|
+
: providerForModelId(id);
|
|
105
|
+
const windowProvider = mapping && mapping.provider.trim().length > 0
|
|
106
|
+
? mapping.provider.trim()
|
|
107
|
+
: provider;
|
|
108
|
+
const apiBackend = provider === "anthropic" ? "messages" : "chat_completions";
|
|
109
|
+
const contextWindow = getContextWindowSize(windowProvider, classifyId);
|
|
110
|
+
return {
|
|
111
|
+
id,
|
|
112
|
+
name: displayNameFor(id),
|
|
113
|
+
apiBackend,
|
|
114
|
+
contextWindow,
|
|
115
|
+
maxCompletionTokens: contextWindow >= 1_000_000 ? 16_384 : 8_192,
|
|
116
|
+
supportsReasoningEffort: supportsAdaptiveThinking(classifyId),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
async function loadRoutedMappings(configPath) {
|
|
120
|
+
const resolvedPath = configPath ?? defaultProxyConfigPath();
|
|
121
|
+
try {
|
|
122
|
+
const config = await parseProxyConfigString(readFileSync(resolvedPath, "utf8"));
|
|
123
|
+
return (config.routing?.modelMappings ?? []).filter((mapping) => mapping.from.trim().length > 0);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function loadRoutedModelIds(configPath) {
|
|
130
|
+
return (await loadRoutedMappings(configPath)).map((mapping) => mapping.from.trim());
|
|
131
|
+
}
|
|
132
|
+
async function catalogGrokSpecs(configPath) {
|
|
133
|
+
const seen = new Set();
|
|
134
|
+
const specs = [];
|
|
135
|
+
const routed = await loadRoutedMappings(configPath);
|
|
136
|
+
const routedByFrom = new Map(routed.map((mapping) => [mapping.from.trim(), mapping]));
|
|
137
|
+
for (const id of [
|
|
138
|
+
...DEFAULT_PROXY_MODEL_IDS,
|
|
139
|
+
...routed.map((mapping) => mapping.from.trim()),
|
|
140
|
+
]) {
|
|
141
|
+
if (id.startsWith("grok-") || seen.has(id)) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
seen.add(id);
|
|
145
|
+
specs.push(classifyGrokProxyModel(id, routedByFrom.get(id)));
|
|
146
|
+
}
|
|
147
|
+
return specs;
|
|
148
|
+
}
|
|
149
|
+
async function catalogModelIds(configPath) {
|
|
150
|
+
return (await catalogGrokSpecs(configPath)).map((spec) => spec.id);
|
|
151
|
+
}
|
|
152
|
+
function buildGrokModelBlock(spec, baseUrl) {
|
|
153
|
+
const lines = [
|
|
154
|
+
`[model.${tomlKey(spec.id)}]`,
|
|
155
|
+
`model = ${JSON.stringify(spec.id)}`,
|
|
156
|
+
`name = ${JSON.stringify(spec.name)}`,
|
|
157
|
+
`base_url = ${JSON.stringify(baseUrl)}`,
|
|
158
|
+
`api_backend = ${JSON.stringify(spec.apiBackend)}`,
|
|
159
|
+
`context_window = ${spec.contextWindow}`,
|
|
160
|
+
`auto_compact_threshold_percent = 80`,
|
|
161
|
+
`max_completion_tokens = ${spec.maxCompletionTokens}`,
|
|
162
|
+
`supports_backend_search = false`,
|
|
163
|
+
];
|
|
164
|
+
if (!spec.supportsReasoningEffort) {
|
|
165
|
+
lines.push("supports_reasoning_effort = false");
|
|
166
|
+
}
|
|
167
|
+
if (spec.apiBackend === "messages") {
|
|
168
|
+
lines.push(`extra_headers = { "x-api-key" = ${JSON.stringify(PLACEHOLDER_KEY)}, "anthropic-version" = ${JSON.stringify(ANTHROPIC_VERSION)} }`);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
lines.push(`api_key = ${JSON.stringify(PLACEHOLDER_KEY)}`);
|
|
172
|
+
}
|
|
173
|
+
return lines.join("\n");
|
|
174
|
+
}
|
|
175
|
+
async function buildGrokManagedBlock(baseUrl, configPath) {
|
|
176
|
+
const specs = await catalogGrokSpecs(configPath);
|
|
177
|
+
const body = specs
|
|
178
|
+
.map((spec) => buildGrokModelBlock(spec, baseUrl))
|
|
179
|
+
.join("\n\n");
|
|
180
|
+
return [
|
|
181
|
+
GROK_BLOCK_BEGIN,
|
|
182
|
+
"# Proxy catalog. Built-in grok-4.6 / grok-4.5 stay on xAI.",
|
|
183
|
+
"# context_window is Grok's compaction limit and must be <= upstream.",
|
|
184
|
+
"",
|
|
185
|
+
body,
|
|
186
|
+
GROK_BLOCK_END,
|
|
187
|
+
"",
|
|
188
|
+
].join("\n");
|
|
189
|
+
}
|
|
190
|
+
function stripGrokManagedBlock(text) {
|
|
191
|
+
const blockRe = new RegExp(`\\n?${escapeRegExp(GROK_BLOCK_BEGIN)}[\\s\\S]*?${escapeRegExp(GROK_BLOCK_END)}\\n?`, "g");
|
|
192
|
+
return text.replace(blockRe, "\n");
|
|
193
|
+
}
|
|
194
|
+
function extractManagedBaseUrl(text) {
|
|
195
|
+
const match = text.match(new RegExp(`${escapeRegExp(GROK_BLOCK_BEGIN)}[\\s\\S]*?base_url\\s*=\\s*"([^"]*)"`));
|
|
196
|
+
return match?.[1];
|
|
197
|
+
}
|
|
198
|
+
async function readGrokSnapshot() {
|
|
199
|
+
const fs = await import("fs");
|
|
200
|
+
let parsed;
|
|
201
|
+
try {
|
|
202
|
+
parsed = JSON.parse(fs.readFileSync(getGrokSnapshotPath(), "utf8"));
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
if (!isUsableSnapshot(parsed, "originalExisted")) {
|
|
208
|
+
logger.debug("[proxy] Grok: ignoring a malformed snapshot rather than treating it as empty");
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
const record = parsed;
|
|
212
|
+
const originalExisted = record.originalExisted;
|
|
213
|
+
const writtenBaseUrl = record.writtenBaseUrl;
|
|
214
|
+
if (typeof originalExisted !== "boolean") {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
if (typeof writtenBaseUrl !== "string") {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
return { originalExisted, writtenBaseUrl };
|
|
221
|
+
}
|
|
222
|
+
export async function setGrokProxySettings(baseUrl, options) {
|
|
223
|
+
const fs = await import("fs");
|
|
224
|
+
try {
|
|
225
|
+
fs.accessSync(getGrokConfigDir());
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
let original;
|
|
231
|
+
try {
|
|
232
|
+
original = fs.readFileSync(getGrokConfigPath(), "utf8");
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
if (!isMissingFileError(error)) {
|
|
236
|
+
logger.warn("[proxy] Grok: unable to read config.toml; leaving it untouched");
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
original = null;
|
|
240
|
+
}
|
|
241
|
+
const existingSnapshot = await readGrokSnapshot();
|
|
242
|
+
if (existingSnapshot === null && fs.existsSync(getGrokSnapshotPath())) {
|
|
243
|
+
logger.warn("[proxy] Grok: snapshot file is unreadable; leaving config.toml untouched rather than overwriting with no way back");
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
const currentBlock = original
|
|
247
|
+
? original.includes(GROK_BLOCK_BEGIN)
|
|
248
|
+
? original.slice(original.indexOf(GROK_BLOCK_BEGIN), original.indexOf(GROK_BLOCK_END) === -1
|
|
249
|
+
? original.length
|
|
250
|
+
: original.indexOf(GROK_BLOCK_END) + GROK_BLOCK_END.length)
|
|
251
|
+
: undefined
|
|
252
|
+
: undefined;
|
|
253
|
+
if (existingSnapshot === null ||
|
|
254
|
+
shouldCaptureSnapshot({
|
|
255
|
+
hasSnapshot: existingSnapshot !== null,
|
|
256
|
+
written: existingSnapshot?.writtenBaseUrl,
|
|
257
|
+
current: extractManagedBaseUrl(original ?? "") ?? currentBlock,
|
|
258
|
+
})) {
|
|
259
|
+
fs.mkdirSync(join(homedir(), ".neurolink"), { recursive: true });
|
|
260
|
+
await writeFileAtomic(getGrokSnapshotPath(), JSON.stringify({
|
|
261
|
+
originalExisted: existingSnapshot?.originalExisted ?? original !== null,
|
|
262
|
+
writtenBaseUrl: baseUrl,
|
|
263
|
+
}, null, 2), 0o600);
|
|
264
|
+
}
|
|
265
|
+
const withoutBlock = original ? stripGrokManagedBlock(original) : "";
|
|
266
|
+
const trimmed = withoutBlock.replace(/\s*$/, "\n");
|
|
267
|
+
const next = `${trimmed}\n${await buildGrokManagedBlock(baseUrl, options?.configPath)}`;
|
|
268
|
+
await writeFileAtomic(getGrokConfigPath(), next.startsWith("\n") && original === null
|
|
269
|
+
? next.replace(/^\n+/, "")
|
|
270
|
+
: next, original === null ? 0o600 : undefined);
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
export async function clearGrokProxySettings(expectedBaseUrl) {
|
|
274
|
+
const fs = await import("fs");
|
|
275
|
+
let current;
|
|
276
|
+
try {
|
|
277
|
+
current = fs.readFileSync(getGrokConfigPath(), "utf8");
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
if (!current.includes(GROK_BLOCK_BEGIN)) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
const configuredUrl = extractManagedBaseUrl(current);
|
|
286
|
+
if (expectedBaseUrl &&
|
|
287
|
+
configuredUrl !== undefined &&
|
|
288
|
+
configuredUrl !== expectedBaseUrl) {
|
|
289
|
+
logger.debug("[proxy] Grok clear: base URL is not the one we wrote, leaving it intact");
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
const snapshot = await readGrokSnapshot();
|
|
293
|
+
if (snapshot === null) {
|
|
294
|
+
logger.warn("[proxy] Grok clear: no usable snapshot, leaving config.toml untouched rather than stripping a block we cannot prove we own");
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
const remainder = stripGrokManagedBlock(current).replace(/\s*$/, "\n");
|
|
298
|
+
if (!snapshot.originalExisted && remainder.trim().length === 0) {
|
|
299
|
+
fs.rmSync(getGrokConfigPath(), { force: true });
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
await writeFileAtomic(getGrokConfigPath(), remainder);
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
fs.rmSync(getGrokSnapshotPath(), { force: true });
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
// next apply overwrites
|
|
309
|
+
}
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
export const grokConfigurator = {
|
|
313
|
+
id: "grok",
|
|
314
|
+
displayName: "Grok Build",
|
|
315
|
+
detect: async () => {
|
|
316
|
+
const fs = await import("fs");
|
|
317
|
+
try {
|
|
318
|
+
fs.accessSync(getGrokConfigDir());
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
// Grok appends `/messages` or `/chat/completions` to `base_url`, so it
|
|
326
|
+
// takes the `/v1` door rather than the proxy root.
|
|
327
|
+
apply: (proxyBaseUrl, options) => setGrokProxySettings(`${proxyBaseUrl}/v1`, options),
|
|
328
|
+
restore: (proxyBaseUrl) => clearGrokProxySettings(`${proxyBaseUrl}/v1`),
|
|
329
|
+
};
|
|
330
|
+
/** Test-only export (CLAUDE.md rule 15 determinism exception). See openCode.ts. */
|
|
331
|
+
export const __grokTestHooks = {
|
|
332
|
+
getGrokConfigDir,
|
|
333
|
+
getGrokConfigPath,
|
|
334
|
+
getGrokSnapshotPath,
|
|
335
|
+
setGrokProxySettings,
|
|
336
|
+
clearGrokProxySettings,
|
|
337
|
+
classifyGrokProxyModel,
|
|
338
|
+
loadRoutedModelIds,
|
|
339
|
+
catalogModelIds,
|
|
340
|
+
buildGrokManagedBlock,
|
|
341
|
+
};
|
|
342
|
+
//# sourceMappingURL=grok.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CliProxyClientApplyResult, CliProxyClientConfigurator, CliProxyClientRestoreResult } from "../../types/index.js";
|
|
1
|
+
import type { CliProxyClientApplyOptions, CliProxyClientApplyResult, CliProxyClientConfigurator, CliProxyClientRestoreResult } from "../../types/index.js";
|
|
2
2
|
/**
|
|
3
3
|
* Every CLI the proxy auto-configures, in apply order.
|
|
4
4
|
*
|
|
@@ -14,6 +14,6 @@ export declare const PROXY_CLIENT_CONFIGURATORS: readonly CliProxyClientConfigur
|
|
|
14
14
|
* how loudly to report — the daemon-start path logs failures at debug level
|
|
15
15
|
* while the setup wizard prints a visible warning.
|
|
16
16
|
*/
|
|
17
|
-
export declare function applyAllClients(proxyBaseUrl: string): Promise<CliProxyClientApplyResult[]>;
|
|
17
|
+
export declare function applyAllClients(proxyBaseUrl: string, options?: CliProxyClientApplyOptions): Promise<CliProxyClientApplyResult[]>;
|
|
18
18
|
/** Restore every client's previous configuration. See applyAllClients. */
|
|
19
19
|
export declare function restoreAllClients(proxyBaseUrl: string): Promise<CliProxyClientRestoreResult[]>;
|
|
@@ -5,6 +5,7 @@ import { codexConfigurator } from "./codex.js";
|
|
|
5
5
|
import { qwenCodeConfigurator } from "./qwenCode.js";
|
|
6
6
|
import { copilotConfigurator } from "./copilot.js";
|
|
7
7
|
import { geminiConfigurator } from "./gemini.js";
|
|
8
|
+
import { grokConfigurator } from "./grok.js";
|
|
8
9
|
/**
|
|
9
10
|
* Every CLI the proxy auto-configures, in apply order.
|
|
10
11
|
*
|
|
@@ -18,6 +19,7 @@ export const PROXY_CLIENT_CONFIGURATORS = [
|
|
|
18
19
|
qwenCodeConfigurator,
|
|
19
20
|
copilotConfigurator,
|
|
20
21
|
geminiConfigurator,
|
|
22
|
+
grokConfigurator,
|
|
21
23
|
];
|
|
22
24
|
/**
|
|
23
25
|
* Point every detected client at the proxy.
|
|
@@ -27,12 +29,12 @@ export const PROXY_CLIENT_CONFIGURATORS = [
|
|
|
27
29
|
* how loudly to report — the daemon-start path logs failures at debug level
|
|
28
30
|
* while the setup wizard prints a visible warning.
|
|
29
31
|
*/
|
|
30
|
-
export async function applyAllClients(proxyBaseUrl) {
|
|
32
|
+
export async function applyAllClients(proxyBaseUrl, options) {
|
|
31
33
|
const results = [];
|
|
32
34
|
for (const client of PROXY_CLIENT_CONFIGURATORS) {
|
|
33
35
|
try {
|
|
34
36
|
const applied = (await client.detect())
|
|
35
|
-
? await client.apply(proxyBaseUrl)
|
|
37
|
+
? await client.apply(proxyBaseUrl, options)
|
|
36
38
|
: false;
|
|
37
39
|
// Only ask for a note when something was actually written: a note on a
|
|
38
40
|
// client that was skipped would read as an instruction to act on a
|