@juspay/neurolink 12.12.16 → 12.14.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 +3 -3
- package/dist/browser/neurolink.min.js +381 -381
- package/dist/cli/commands/proxy.js +11 -9
- 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/core/baseProvider.d.ts +10 -0
- package/dist/core/baseProvider.js +21 -0
- package/dist/core/constants.d.ts +14 -1
- package/dist/core/constants.js +18 -1
- package/dist/core/loopEngine.js +150 -7
- package/dist/core/toolExecutionGuards.js +6 -1
- package/dist/providers/amazonBedrock/client.js +10 -0
- package/dist/providers/anthropic/client.d.ts +8 -0
- package/dist/providers/anthropic/client.js +132 -3
- package/dist/providers/anthropic/loopAdapter.js +352 -244
- package/dist/providers/googleAiStudio/client.js +12 -0
- package/dist/providers/googleVertex/client.js +19 -5
- package/dist/proxy/clientAttribution.js +3 -0
- package/dist/proxy/proxyConfig.d.ts +4 -0
- package/dist/proxy/proxyConfig.js +13 -1
- package/dist/types/generate.d.ts +22 -9
- package/dist/types/loopEngine.d.ts +97 -2
- package/dist/types/proxyClient.d.ts +34 -1
- package/dist/types/stream.d.ts +141 -5
- package/dist/utils/parameterValidation.d.ts +30 -0
- package/dist/utils/parameterValidation.js +101 -0
- package/dist/utils/timeout.js +25 -2
- package/package.json +2 -1
|
@@ -11,13 +11,14 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { spawn } from "node:child_process";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
|
-
import { dirname, join
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
15
|
import { stripVTControlCharacters } from "node:util";
|
|
16
16
|
import chalk from "chalk";
|
|
17
17
|
import ora from "ora";
|
|
18
18
|
import { buildProxyHealthResponse, createProxyReadinessState, markProxyDrainingForUpdate, markProxyReady, resumeProxyConnections, waitForProxyReadiness, } from "../../proxy/proxyHealth.js";
|
|
19
19
|
import { logger } from "../../utils/logger.js";
|
|
20
20
|
import { applyAllClients, restoreAllClients, } from "../proxy-clients/registry.js";
|
|
21
|
+
import { resolveProxyConfigPath } from "../../proxy/proxyConfig.js";
|
|
21
22
|
import { redactUrlsInText, sanitizeForLog, } from "../../utils/logSanitize.js";
|
|
22
23
|
import { withTimeout } from "../../utils/async/withTimeout.js";
|
|
23
24
|
import { formatUptime, isProcessRunning, StateFileManager, } from "../utils/serverUtils.js";
|
|
@@ -2696,7 +2697,9 @@ async function startProxyRuntime(params) {
|
|
|
2696
2697
|
logger.always(` ${chalk.bold("Env File:")} ${chalk.cyan(params.loadedEnvFile)}`);
|
|
2697
2698
|
}
|
|
2698
2699
|
if (!isDev) {
|
|
2699
|
-
for (const result of await applyAllClients(url
|
|
2700
|
+
for (const result of await applyAllClients(url, {
|
|
2701
|
+
configPath: params.configPath,
|
|
2702
|
+
})) {
|
|
2700
2703
|
if (result.error) {
|
|
2701
2704
|
// Visible, not debug-level. A client whose config could not be written
|
|
2702
2705
|
// will keep talking to its own upstream, which looks like the proxy
|
|
@@ -2953,9 +2956,7 @@ async function startProxyCommandHandler(argv) {
|
|
|
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) {
|
|
@@ -4174,7 +4176,9 @@ export const proxySetupCommand = {
|
|
|
4174
4176
|
const nextStep = stepNum + 1;
|
|
4175
4177
|
console.info(chalk.blue(`\nStep ${nextStep}:`) + " Configuring Claude Code...");
|
|
4176
4178
|
const url = `http://127.0.0.1:${port}`;
|
|
4177
|
-
for (const result of await applyAllClients(url
|
|
4179
|
+
for (const result of await applyAllClients(url, {
|
|
4180
|
+
configPath: resolveProxyConfigPath(argv.config),
|
|
4181
|
+
})) {
|
|
4178
4182
|
if (result.error) {
|
|
4179
4183
|
console.info(chalk.yellow(` ⚠ Could not auto-configure ${result.displayName}: ${result.error.message}`));
|
|
4180
4184
|
// Claude Code is the one client whose manual fallback is a single
|
|
@@ -4364,9 +4368,7 @@ export const proxyInstallCommand = {
|
|
|
4364
4368
|
});
|
|
4365
4369
|
const envFile = envResolution.path;
|
|
4366
4370
|
const explicitConfig = argv.config;
|
|
4367
|
-
const configPath = explicitConfig
|
|
4368
|
-
? resolve(explicitConfig)
|
|
4369
|
-
: join(homedir(), ".neurolink", "proxy-config.yaml");
|
|
4371
|
+
const configPath = resolveProxyConfigPath(explicitConfig);
|
|
4370
4372
|
if (explicitConfig && !existsSync(configPath)) {
|
|
4371
4373
|
console.info(chalk.red(`Proxy config file not found: ${configPath}`));
|
|
4372
4374
|
process.exit(1);
|
|
@@ -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
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Model IDs the proxy advertises when no routing config narrows the list.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Three consumers need the same answer and must not drift apart:
|
|
5
5
|
*
|
|
6
6
|
* - `proxyTranslationEngine` serves them from `GET /v1/models`.
|
|
7
7
|
* - The OpenCode client configurator writes them into `provider.neurolink.
|
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
* It does not call `/v1/models`, so an empty map means every model id is
|
|
10
10
|
* unknown and `opencode run` fails with `ProviderModelNotFoundError`
|
|
11
11
|
* before a request is ever made.
|
|
12
|
+
* - The Grok Build configurator writes the same ids (plus any
|
|
13
|
+
* `routing.model-mappings` in the active proxy config — default
|
|
14
|
+
* `~/.neurolink/proxy-config.yaml`, or the path passed to `--config`)
|
|
15
|
+
* into `~/.grok/config.toml` as `[model.<id>]` entries, with
|
|
16
|
+
* `api_backend` and `context_window` so Grok's compaction matches the
|
|
17
|
+
* upstream. Built-in grok-* ids are skipped.
|
|
12
18
|
*
|
|
13
19
|
* Format matches the IDs used throughout `src/lib/models/` and
|
|
14
20
|
* `src/lib/constants/` (e.g. `claude-3-5-haiku-20241022`, not
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Model IDs the proxy advertises when no routing config narrows the list.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Three consumers need the same answer and must not drift apart:
|
|
5
5
|
*
|
|
6
6
|
* - `proxyTranslationEngine` serves them from `GET /v1/models`.
|
|
7
7
|
* - The OpenCode client configurator writes them into `provider.neurolink.
|
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
* It does not call `/v1/models`, so an empty map means every model id is
|
|
10
10
|
* unknown and `opencode run` fails with `ProviderModelNotFoundError`
|
|
11
11
|
* before a request is ever made.
|
|
12
|
+
* - The Grok Build configurator writes the same ids (plus any
|
|
13
|
+
* `routing.model-mappings` in the active proxy config — default
|
|
14
|
+
* `~/.neurolink/proxy-config.yaml`, or the path passed to `--config`)
|
|
15
|
+
* into `~/.grok/config.toml` as `[model.<id>]` entries, with
|
|
16
|
+
* `api_backend` and `context_window` so Grok's compaction matches the
|
|
17
|
+
* upstream. Built-in grok-* ids are skipped.
|
|
12
18
|
*
|
|
13
19
|
* Format matches the IDs used throughout `src/lib/models/` and
|
|
14
20
|
* `src/lib/constants/` (e.g. `claude-3-5-haiku-20241022`, not
|
|
@@ -41,6 +41,16 @@ export declare abstract class BaseProvider implements AIProvider {
|
|
|
41
41
|
* @returns the current model's registered capability, or true when unknown
|
|
42
42
|
*/
|
|
43
43
|
supportsTools(): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Whether this provider implements the opt-in `executionControl` contract.
|
|
46
|
+
*
|
|
47
|
+
* Default false, and that default is load-bearing: a provider that has not
|
|
48
|
+
* implemented the contract must REJECT it, not ignore it. Silently dropping
|
|
49
|
+
* a caller-set execution policy is invisible until a long turn dies at a
|
|
50
|
+
* ceiling its owner believed had been removed. Overridden only where the
|
|
51
|
+
* control is genuinely honoured end to end.
|
|
52
|
+
*/
|
|
53
|
+
supportsExecutionControl(): boolean;
|
|
44
54
|
/**
|
|
45
55
|
* Apply the shared tool gate and optionally report registry-backed
|
|
46
56
|
* suppression at the request entry point.
|
|
@@ -46,6 +46,7 @@ import { TelemetryHandler } from "./modules/TelemetryHandler.js";
|
|
|
46
46
|
import { ToolsManager } from "./modules/ToolsManager.js";
|
|
47
47
|
import { Utilities } from "./modules/Utilities.js";
|
|
48
48
|
import { generateOnceNative } from "../utils/nativeSingleShot.js";
|
|
49
|
+
import { validateExecutionControl } from "../utils/parameterValidation.js";
|
|
49
50
|
import { extractTokenUsage } from "../utils/tokenUtils.js";
|
|
50
51
|
/**
|
|
51
52
|
* Read the consumer-facing lifecycle callbacks buried inside a request's
|
|
@@ -182,6 +183,18 @@ export class BaseProvider {
|
|
|
182
183
|
supportsTools() {
|
|
183
184
|
return modelSupports("functionCalling", this.providerName, this.modelName);
|
|
184
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Whether this provider implements the opt-in `executionControl` contract.
|
|
188
|
+
*
|
|
189
|
+
* Default false, and that default is load-bearing: a provider that has not
|
|
190
|
+
* implemented the contract must REJECT it, not ignore it. Silently dropping
|
|
191
|
+
* a caller-set execution policy is invisible until a long turn dies at a
|
|
192
|
+
* ceiling its owner believed had been removed. Overridden only where the
|
|
193
|
+
* control is genuinely honoured end to end.
|
|
194
|
+
*/
|
|
195
|
+
supportsExecutionControl() {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
185
198
|
/**
|
|
186
199
|
* Apply the shared tool gate and optionally report registry-backed
|
|
187
200
|
* suppression at the request entry point.
|
|
@@ -211,6 +224,14 @@ export class BaseProvider {
|
|
|
211
224
|
// maxTokens (getSafeMaxTokens consults the discovered output ceiling).
|
|
212
225
|
await this.ensureModelLimits();
|
|
213
226
|
let options = this.normalizeStreamOptions(optionsOrPrompt);
|
|
227
|
+
// Before anything else, and before a single byte leaves the process: an
|
|
228
|
+
// execution policy this provider cannot honour is an error, and a policy
|
|
229
|
+
// whose shape could be read two ways is an error. Both are silent bugs at
|
|
230
|
+
// the point they would otherwise matter.
|
|
231
|
+
validateExecutionControl(options.executionControl, this.providerName, this.supportsExecutionControl(), {
|
|
232
|
+
turnTimeoutMs: options.turnTimeoutMs,
|
|
233
|
+
toolTimeoutMs: options.toolTimeoutMs,
|
|
234
|
+
});
|
|
214
235
|
logger.info(`Starting stream`, {
|
|
215
236
|
provider: this.providerName,
|
|
216
237
|
hasTools: !options.disableTools && this.supportsTools(),
|
package/dist/core/constants.d.ts
CHANGED
|
@@ -27,9 +27,22 @@ export declare const DEFAULT_GEMINI_STREAM_TIMEOUT_MS = 300000;
|
|
|
27
27
|
* Default per-tool-execution timeout for native agentic loops. A tool that
|
|
28
28
|
* exceeds it fails with an error tool_result and costs one step — the turn
|
|
29
29
|
* continues instead of hanging on a wedged tool. Override per call with
|
|
30
|
-
* `toolTimeoutMs`.
|
|
30
|
+
* `toolTimeoutMs`, or remove the bound entirely with `toolTimeoutMs: null`.
|
|
31
31
|
*/
|
|
32
32
|
export declare const DEFAULT_TOOL_EXECUTION_TIMEOUT_MS = 300000;
|
|
33
|
+
/**
|
|
34
|
+
* Resolve a caller's `toolTimeoutMs` into the bound a loop should actually
|
|
35
|
+
* apply: a number of milliseconds, or `null` for no bound at all.
|
|
36
|
+
*
|
|
37
|
+
* The three-way distinction is the whole point, and `??` cannot express it:
|
|
38
|
+
* `undefined` means "no opinion, take the default", while `null` is a stated
|
|
39
|
+
* choice to run tools unbounded — the pre-existing behaviour of the loops that
|
|
40
|
+
* never had a per-tool timer, and the only way to say it, since a finite
|
|
41
|
+
* number is always a ceiling and `Infinity` silently desugars to `setTimeout`'s
|
|
42
|
+
* ~24.9-day cap. Every loop resolves it through here so `null` cannot come to
|
|
43
|
+
* mean "the default" on one provider and "unbounded" on another.
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveToolTimeoutMs(toolTimeoutMs: number | null | undefined): number | null;
|
|
33
46
|
/**
|
|
34
47
|
* Default wrap-up lead applied when `turnTimeoutMs` is set but
|
|
35
48
|
* `wrapupTimeLeadMs` is not: with less than this much turn time remaining,
|
package/dist/core/constants.js
CHANGED
|
@@ -122,9 +122,26 @@ export const DEFAULT_GEMINI_STREAM_TIMEOUT_MS = 300_000;
|
|
|
122
122
|
* Default per-tool-execution timeout for native agentic loops. A tool that
|
|
123
123
|
* exceeds it fails with an error tool_result and costs one step — the turn
|
|
124
124
|
* continues instead of hanging on a wedged tool. Override per call with
|
|
125
|
-
* `toolTimeoutMs`.
|
|
125
|
+
* `toolTimeoutMs`, or remove the bound entirely with `toolTimeoutMs: null`.
|
|
126
126
|
*/
|
|
127
127
|
export const DEFAULT_TOOL_EXECUTION_TIMEOUT_MS = 300_000;
|
|
128
|
+
/**
|
|
129
|
+
* Resolve a caller's `toolTimeoutMs` into the bound a loop should actually
|
|
130
|
+
* apply: a number of milliseconds, or `null` for no bound at all.
|
|
131
|
+
*
|
|
132
|
+
* The three-way distinction is the whole point, and `??` cannot express it:
|
|
133
|
+
* `undefined` means "no opinion, take the default", while `null` is a stated
|
|
134
|
+
* choice to run tools unbounded — the pre-existing behaviour of the loops that
|
|
135
|
+
* never had a per-tool timer, and the only way to say it, since a finite
|
|
136
|
+
* number is always a ceiling and `Infinity` silently desugars to `setTimeout`'s
|
|
137
|
+
* ~24.9-day cap. Every loop resolves it through here so `null` cannot come to
|
|
138
|
+
* mean "the default" on one provider and "unbounded" on another.
|
|
139
|
+
*/
|
|
140
|
+
export function resolveToolTimeoutMs(toolTimeoutMs) {
|
|
141
|
+
return toolTimeoutMs === null
|
|
142
|
+
? null
|
|
143
|
+
: (toolTimeoutMs ?? DEFAULT_TOOL_EXECUTION_TIMEOUT_MS);
|
|
144
|
+
}
|
|
128
145
|
/**
|
|
129
146
|
* Default wrap-up lead applied when `turnTimeoutMs` is set but
|
|
130
147
|
* `wrapupTimeLeadMs` is not: with less than this much turn time remaining,
|