@juspay/neurolink 12.3.0 → 12.4.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 +5 -5
- package/dist/cli/proxy-clients/gemini.d.ts +51 -0
- package/dist/cli/proxy-clients/gemini.js +278 -0
- package/dist/cli/proxy-clients/openCode.d.ts +41 -0
- package/dist/cli/proxy-clients/openCode.js +212 -27
- package/dist/cli/proxy-clients/registry.js +2 -0
- package/dist/cli/proxy-clients/snapshot.d.ts +13 -0
- package/dist/cli/proxy-clients/snapshot.js +18 -0
- package/dist/constants/proxyModels.d.ts +22 -0
- package/dist/constants/proxyModels.js +36 -0
- package/dist/core/baseProvider.d.ts +1 -1
- package/dist/core/baseProvider.js +2 -2
- package/dist/evaluation/scorers/rule/formatScorer.js +1 -1
- package/dist/mcp/mcpRegistryClient.js +1 -1
- package/dist/middleware/utils/guardrailsUtils.js +1 -1
- package/dist/neurolink.js +2 -2
- package/dist/proxy/proxyTranslationEngine.js +3 -22
- package/dist/rag/ChunkerFactory.js +1 -1
- package/dist/server/websocket/WebSocketHandler.js +3 -3
- package/dist/types/proxyClient.d.ts +33 -0
- package/dist/workflow/config.d.ts +1 -1
- package/dist/workflow/config.js +1 -1
- package/dist/workflow/core/workflowRegistry.js +1 -1
- package/dist/workflow/core/workflowRunner.js +1 -1
- package/dist/workflow/utils/workflowMetrics.d.ts +1 -1
- package/dist/workflow/utils/workflowMetrics.js +1 -1
- package/package.json +2 -1
|
@@ -3,11 +3,44 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Moved verbatim out of `proxy.ts` so that adding a CLI means adding a file
|
|
5
5
|
* here rather than editing a 5,000-line command module in seven places.
|
|
6
|
+
*
|
|
7
|
+
* Two defects made every config this writer produced unusable, and both are
|
|
8
|
+
* fixed here. They are recorded because each was invisible to the tests that
|
|
9
|
+
* were supposed to cover this file.
|
|
10
|
+
*
|
|
11
|
+
* 1. The snapshot lived in `opencode.json` itself, under two `__proxy_*` keys
|
|
12
|
+
* at the top level. OpenCode validates its config against a closed schema
|
|
13
|
+
* and rejects unknown top-level keys outright:
|
|
14
|
+
*
|
|
15
|
+
* Error: Configuration is invalid at ~/.config/opencode/opencode.json
|
|
16
|
+
* ↳ Unrecognized keys: "__proxy_original_neurolink", "__proxy_written_neurolink"
|
|
17
|
+
*
|
|
18
|
+
* Every `opencode` invocation failed at startup — not just proxied ones —
|
|
19
|
+
* so auto-configuration bricked the CLI it was meant to onboard. The
|
|
20
|
+
* snapshot now lives beside Codex's, in `~/.neurolink/`, which is what
|
|
21
|
+
* `codex.ts` has always done. Claude Code and Qwen embed a snapshot the
|
|
22
|
+
* same way and survive it only because their schemas ignore unknown keys;
|
|
23
|
+
* that is tolerance, not permission, and new writers should not rely on it.
|
|
24
|
+
*
|
|
25
|
+
* 2. `models` was written as `{}`. OpenCode resolves `--model provider/id`
|
|
26
|
+
* against that map and never calls `/v1/models`, so an empty map meant
|
|
27
|
+
* every id was unknown:
|
|
28
|
+
*
|
|
29
|
+
* ProviderModelNotFoundError: providerID "neurolink", suggestions: []
|
|
30
|
+
*
|
|
31
|
+
* Fixing only the keys exposed this one immediately underneath.
|
|
32
|
+
*
|
|
33
|
+
* Configs written by the previous version are repaired in place: both apply()
|
|
34
|
+
* and restore() adopt a legacy in-file snapshot before deleting the keys, so
|
|
35
|
+
* an existing broken config heals on the next `proxy start` without losing the
|
|
36
|
+
* user's original provider block.
|
|
6
37
|
*/
|
|
38
|
+
import { createHash } from "crypto";
|
|
7
39
|
import { homedir } from "os";
|
|
8
40
|
import { join } from "path";
|
|
9
41
|
import { logger } from "../../utils/logger.js";
|
|
10
|
-
import {
|
|
42
|
+
import { DEFAULT_PROXY_MODEL_IDS } from "../../constants/proxyModels.js";
|
|
43
|
+
import { cloneForSnapshot, isProxyOwnedValue, isUsableSnapshot, shouldCaptureSnapshot, writeFileAtomic, } from "./snapshot.js";
|
|
11
44
|
function getOpenCodeConfigDir() {
|
|
12
45
|
// OpenCode resolves this with the unmodified `xdg-basedir` package —
|
|
13
46
|
// `XDG_CONFIG_HOME || ~/.config` — on every platform, macOS included. There
|
|
@@ -21,19 +54,130 @@ function getOpenCodeConfigPath() {
|
|
|
21
54
|
return join(getOpenCodeConfigDir(), "opencode.json");
|
|
22
55
|
}
|
|
23
56
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
57
|
+
* Where the snapshot of the user's pre-existing `provider.neurolink` lives.
|
|
58
|
+
*
|
|
59
|
+
* Outside `opencode.json`, for the reason in the file header. Persisting it on
|
|
60
|
+
* disk (rather than in process memory) means restoration still works when the
|
|
61
|
+
* proxy crashes or shutdown runs in a different process — the property the
|
|
62
|
+
* in-file version was reaching for.
|
|
63
|
+
*/
|
|
64
|
+
function getOpenCodeSnapshotPath() {
|
|
65
|
+
// Scoped to the config directory, not just HOME. `getOpenCodeConfigPath()`
|
|
66
|
+
// resolves through XDG_CONFIG_HOME, so two XDG roots under one HOME are two
|
|
67
|
+
// independent OpenCode installs — and a single shared snapshot file made the
|
|
68
|
+
// second apply() overwrite the first's saved original. Clearing the first
|
|
69
|
+
// root then restored the second root's block onto it, or deleted a real
|
|
70
|
+
// provider entry outright. Measured before this fix: root A came back
|
|
71
|
+
// holding root B's block.
|
|
72
|
+
const slug = createHash("sha256")
|
|
73
|
+
.update(getOpenCodeConfigDir())
|
|
74
|
+
.digest("hex")
|
|
75
|
+
.slice(0, 12);
|
|
76
|
+
return join(homedir(), ".neurolink", `opencode-proxy-snapshot-${slug}.json`);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The unscoped path used before snapshots were scoped per config directory.
|
|
28
80
|
*
|
|
29
|
-
*
|
|
81
|
+
* Read-only, and only as a fallback: a real user has exactly one config dir, so
|
|
82
|
+
* adopting their existing snapshot is correct. Writes always go to the scoped
|
|
83
|
+
* path, so the ambiguity cannot be reintroduced.
|
|
30
84
|
*/
|
|
31
|
-
|
|
85
|
+
function getLegacyOpenCodeSnapshotPath() {
|
|
86
|
+
return join(homedir(), ".neurolink", "opencode-proxy-snapshot.json");
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Top-level keys written by the pre-fix version of this writer. Present only
|
|
90
|
+
* in configs it already corrupted; removed on sight.
|
|
91
|
+
*/
|
|
92
|
+
const LEGACY_ORIGINAL_KEY = "__proxy_original_neurolink";
|
|
93
|
+
const LEGACY_WRITTEN_KEY = "__proxy_written_neurolink";
|
|
32
94
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
95
|
+
* Remove the legacy in-file snapshot keys.
|
|
96
|
+
*
|
|
97
|
+
* @returns the legacy snapshot if one was found, so the caller can migrate it
|
|
98
|
+
* to the external file rather than discard the user's original provider block.
|
|
35
99
|
*/
|
|
36
|
-
|
|
100
|
+
function takeLegacySnapshot(config) {
|
|
101
|
+
const hasOriginal = LEGACY_ORIGINAL_KEY in config;
|
|
102
|
+
const removed = hasOriginal || LEGACY_WRITTEN_KEY in config;
|
|
103
|
+
if (!removed) {
|
|
104
|
+
return { removed: false, snapshot: null };
|
|
105
|
+
}
|
|
106
|
+
// Both keys go, always — leaving either behind keeps OpenCode unstartable.
|
|
107
|
+
// But only a record that actually carries `original` may be restored from.
|
|
108
|
+
// A file with just the written key proves the proxy wrote something; it does
|
|
109
|
+
// NOT prove the user had no provider block, and treating it as `original:
|
|
110
|
+
// null` made restore delete a real one.
|
|
111
|
+
const snapshot = hasOriginal
|
|
112
|
+
? {
|
|
113
|
+
original: config[LEGACY_ORIGINAL_KEY],
|
|
114
|
+
written: config[LEGACY_WRITTEN_KEY],
|
|
115
|
+
}
|
|
116
|
+
: null;
|
|
117
|
+
delete config[LEGACY_ORIGINAL_KEY];
|
|
118
|
+
delete config[LEGACY_WRITTEN_KEY];
|
|
119
|
+
logger.debug("[proxy] OpenCode: migrated in-file snapshot keys out of opencode.json");
|
|
120
|
+
return { removed, snapshot };
|
|
121
|
+
}
|
|
122
|
+
async function readSnapshotFile(filePath) {
|
|
123
|
+
const fs = await import("fs");
|
|
124
|
+
let parsed;
|
|
125
|
+
try {
|
|
126
|
+
parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
// A snapshot missing `original` is malformed, not a record of "the user had
|
|
132
|
+
// no provider block". Restore distinguishes those by deleting in the second
|
|
133
|
+
// case, so returning `{}` here would destroy a real provider.neurolink.
|
|
134
|
+
if (!isUsableSnapshot(parsed, "original")) {
|
|
135
|
+
logger.debug("[proxy] OpenCode: ignoring a malformed snapshot rather than treating it as empty");
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
return parsed;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Resolve the snapshot for the ACTIVE config directory, most specific first.
|
|
142
|
+
*
|
|
143
|
+
* The unscoped file is shared by every XDG root on the machine, so it must
|
|
144
|
+
* never outrank a record that belongs to this config in particular. Preferring
|
|
145
|
+
* it let one root adopt another root's `original` and restore the wrong
|
|
146
|
+
* provider block.
|
|
147
|
+
*/
|
|
148
|
+
async function resolveOpenCodeSnapshot(inFileLegacy) {
|
|
149
|
+
const scoped = await readSnapshotFile(getOpenCodeSnapshotPath());
|
|
150
|
+
if (scoped !== null) {
|
|
151
|
+
return { snapshot: scoped, source: "scoped" };
|
|
152
|
+
}
|
|
153
|
+
if (inFileLegacy !== null) {
|
|
154
|
+
return { snapshot: inFileLegacy, source: "in-file" };
|
|
155
|
+
}
|
|
156
|
+
const unscoped = await readSnapshotFile(getLegacyOpenCodeSnapshotPath());
|
|
157
|
+
return unscoped === null
|
|
158
|
+
? { snapshot: null, source: null }
|
|
159
|
+
: { snapshot: unscoped, source: "unscoped" };
|
|
160
|
+
}
|
|
161
|
+
async function writeOpenCodeSnapshot(snap) {
|
|
162
|
+
const fs = await import("fs");
|
|
163
|
+
fs.mkdirSync(join(homedir(), ".neurolink"), { recursive: true });
|
|
164
|
+
// 0o600: the snapshot holds whatever the user's own provider block held,
|
|
165
|
+
// which for a custom endpoint includes its API key.
|
|
166
|
+
await writeFileAtomic(getOpenCodeSnapshotPath(), JSON.stringify(snap, null, 2), 0o600);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The models map written into `provider.neurolink`.
|
|
170
|
+
*
|
|
171
|
+
* OpenCode needs every selectable id present here; see DEFAULT_PROXY_MODEL_IDS
|
|
172
|
+
* for why an empty map is fatal rather than merely unhelpful.
|
|
173
|
+
*/
|
|
174
|
+
function buildModelsMap() {
|
|
175
|
+
const models = {};
|
|
176
|
+
for (const id of DEFAULT_PROXY_MODEL_IDS) {
|
|
177
|
+
models[id] = { name: id };
|
|
178
|
+
}
|
|
179
|
+
return models;
|
|
180
|
+
}
|
|
37
181
|
export async function setOpenCodeProxySettings(baseUrl, proxyKey) {
|
|
38
182
|
const fs = await import("fs");
|
|
39
183
|
const configDir = getOpenCodeConfigDir();
|
|
@@ -54,34 +198,43 @@ export async function setOpenCodeProxySettings(baseUrl, proxyKey) {
|
|
|
54
198
|
// file missing/invalid — create fresh config object
|
|
55
199
|
config = { provider: {} };
|
|
56
200
|
}
|
|
201
|
+
// Repair a config written by the pre-fix version before doing anything else.
|
|
202
|
+
// The legacy snapshot is the only record of the user's original block, so it
|
|
203
|
+
// is adopted rather than dropped when no external snapshot exists yet.
|
|
204
|
+
const { snapshot: legacySnapshot } = takeLegacySnapshot(config);
|
|
57
205
|
const provider = (config.provider ?? {});
|
|
58
206
|
// Persist a snapshot of the user's pre-existing provider.neurolink. Repeat
|
|
59
207
|
// apply() calls must not overwrite it with the proxy's own block — but a
|
|
60
208
|
// block the user wrote while the proxy was gone must replace it. See
|
|
61
209
|
// shouldCaptureSnapshot.
|
|
62
210
|
const currentBlock = "neurolink" in provider ? provider.neurolink : undefined;
|
|
211
|
+
let { snapshot } = await resolveOpenCodeSnapshot(legacySnapshot);
|
|
63
212
|
if (shouldCaptureSnapshot({
|
|
64
|
-
hasSnapshot:
|
|
65
|
-
written:
|
|
213
|
+
hasSnapshot: snapshot !== null,
|
|
214
|
+
written: snapshot?.written,
|
|
66
215
|
current: currentBlock,
|
|
67
216
|
})) {
|
|
68
|
-
|
|
69
|
-
currentBlock === undefined ? null : cloneForSnapshot(currentBlock)
|
|
217
|
+
snapshot = {
|
|
218
|
+
original: currentBlock === undefined ? null : cloneForSnapshot(currentBlock),
|
|
219
|
+
};
|
|
70
220
|
}
|
|
71
221
|
const block = {
|
|
72
222
|
id: "neurolink",
|
|
73
223
|
name: "NeuroLink Proxy",
|
|
74
224
|
npm: "@ai-sdk/openai-compatible",
|
|
75
225
|
env: [],
|
|
76
|
-
models:
|
|
226
|
+
models: buildModelsMap(),
|
|
77
227
|
options: {
|
|
78
228
|
baseURL: baseUrl,
|
|
79
229
|
apiKey: proxyKey || "neurolink-proxy",
|
|
80
230
|
},
|
|
81
231
|
};
|
|
82
232
|
provider.neurolink = block;
|
|
83
|
-
config[OPENCODE_WRITTEN_KEY] = cloneForSnapshot(block);
|
|
84
233
|
config.provider = provider;
|
|
234
|
+
await writeOpenCodeSnapshot({
|
|
235
|
+
original: snapshot?.original ?? null,
|
|
236
|
+
written: cloneForSnapshot(block),
|
|
237
|
+
});
|
|
85
238
|
await writeFileAtomic(getOpenCodeConfigPath(), JSON.stringify(config, null, 2));
|
|
86
239
|
return true;
|
|
87
240
|
}
|
|
@@ -94,8 +247,18 @@ export async function clearOpenCodeProxySettings(expectedBaseUrl) {
|
|
|
94
247
|
catch {
|
|
95
248
|
return false;
|
|
96
249
|
}
|
|
250
|
+
// Always strip legacy keys, even on a path that returns false below: leaving
|
|
251
|
+
// them behind keeps OpenCode unusable, which is the whole defect.
|
|
252
|
+
const { removed: legacyStripped, snapshot: legacySnapshot } = takeLegacySnapshot(config);
|
|
253
|
+
const flushLegacy = async () => {
|
|
254
|
+
if (legacyStripped) {
|
|
255
|
+
config.provider = config.provider ?? {};
|
|
256
|
+
await writeFileAtomic(getOpenCodeConfigPath(), JSON.stringify(config, null, 2));
|
|
257
|
+
}
|
|
258
|
+
};
|
|
97
259
|
const provider = config.provider;
|
|
98
260
|
if (!provider || !("neurolink" in provider)) {
|
|
261
|
+
await flushLegacy();
|
|
99
262
|
return false;
|
|
100
263
|
}
|
|
101
264
|
// Check if our proxy URL matches before removing
|
|
@@ -105,6 +268,7 @@ export async function clearOpenCodeProxySettings(expectedBaseUrl) {
|
|
|
105
268
|
if (options && typeof options.baseURL === "string") {
|
|
106
269
|
if (options.baseURL !== expectedBaseUrl) {
|
|
107
270
|
// User configured a different URL; do not clobber
|
|
271
|
+
await flushLegacy();
|
|
108
272
|
return false;
|
|
109
273
|
}
|
|
110
274
|
}
|
|
@@ -112,41 +276,61 @@ export async function clearOpenCodeProxySettings(expectedBaseUrl) {
|
|
|
112
276
|
const hadNeurolink = "neurolink" in provider;
|
|
113
277
|
// Restore from the snapshot persisted at first set(), regardless of process
|
|
114
278
|
// identity. Only delete provider.neurolink when the snapshot says the user
|
|
115
|
-
// explicitly had no entry before — never on
|
|
116
|
-
//
|
|
117
|
-
|
|
279
|
+
// explicitly had no entry before — never on a missing snapshot, since that
|
|
280
|
+
// would mean the snapshot was lost and we cannot prove the entry is ours.
|
|
281
|
+
const { snapshot, source: snapshotSource } = await resolveOpenCodeSnapshot(legacySnapshot);
|
|
282
|
+
if (snapshot !== null) {
|
|
118
283
|
// Only restore what we can prove is ours. The base-URL check above lets
|
|
119
284
|
// through a block still pointing at the proxy that the user has edited
|
|
120
285
|
// beside the URL; reverting that discards a deliberate change.
|
|
121
286
|
if (isProxyOwnedValue({
|
|
122
|
-
written:
|
|
287
|
+
written: snapshot.written,
|
|
123
288
|
current: existing,
|
|
124
289
|
})) {
|
|
125
|
-
|
|
126
|
-
if (snapshot === null) {
|
|
290
|
+
if (snapshot.original === null || snapshot.original === undefined) {
|
|
127
291
|
// User had no provider.neurolink before the proxy started — safe to remove.
|
|
128
292
|
delete provider.neurolink;
|
|
129
293
|
}
|
|
130
294
|
else {
|
|
131
|
-
provider.neurolink = snapshot;
|
|
295
|
+
provider.neurolink = snapshot.original;
|
|
132
296
|
}
|
|
133
297
|
}
|
|
134
298
|
else {
|
|
135
299
|
logger.debug("[proxy] OpenCode clear: provider.neurolink was edited after the proxy wrote it, leaving it intact");
|
|
136
300
|
}
|
|
137
|
-
|
|
138
|
-
|
|
301
|
+
// Deletion is deferred until after the config write below. Removing the
|
|
302
|
+
// recovery data first means a failed write leaves opencode.json still
|
|
303
|
+
// pointing at the proxy with nothing left to restore from — the one
|
|
304
|
+
// ordering that turns a recoverable error into permanent loss.
|
|
139
305
|
}
|
|
140
306
|
else {
|
|
141
307
|
// No snapshot present — refuse to delete to avoid destroying a config
|
|
142
308
|
// the proxy may not own (e.g. a user wrote their own `neurolink` block
|
|
143
|
-
// before the snapshot
|
|
144
|
-
//
|
|
309
|
+
// before the snapshot existed, or this is being cleared from a process
|
|
310
|
+
// that never ran set()).
|
|
145
311
|
logger.debug("[proxy] OpenCode clear: no original-provider snapshot found, leaving provider.neurolink intact");
|
|
312
|
+
await flushLegacy();
|
|
146
313
|
return false;
|
|
147
314
|
}
|
|
148
315
|
config.provider = provider;
|
|
149
316
|
await writeFileAtomic(getOpenCodeConfigPath(), JSON.stringify(config, null, 2));
|
|
317
|
+
// Only now, and only the store we actually consumed. The unscoped file is
|
|
318
|
+
// shared by every XDG root on this machine: deleting it because *this* root
|
|
319
|
+
// restored from its own scoped snapshot would take away another root's only
|
|
320
|
+
// record of its original provider block.
|
|
321
|
+
try {
|
|
322
|
+
if (snapshotSource === "scoped") {
|
|
323
|
+
fs.rmSync(getOpenCodeSnapshotPath(), { force: true });
|
|
324
|
+
}
|
|
325
|
+
else if (snapshotSource === "unscoped") {
|
|
326
|
+
fs.rmSync(getLegacyOpenCodeSnapshotPath(), { force: true });
|
|
327
|
+
}
|
|
328
|
+
// "in-file" needs no deletion: takeLegacySnapshot already removed the keys
|
|
329
|
+
// and the config write above persisted their absence.
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
// A snapshot we cannot delete is harmless: the next apply() overwrites it.
|
|
333
|
+
}
|
|
150
334
|
return hadNeurolink;
|
|
151
335
|
}
|
|
152
336
|
/**
|
|
@@ -159,6 +343,7 @@ export async function clearOpenCodeProxySettings(expectedBaseUrl) {
|
|
|
159
343
|
export const __openCodeTestHooks = {
|
|
160
344
|
getOpenCodeConfigDir,
|
|
161
345
|
getOpenCodeConfigPath,
|
|
346
|
+
getOpenCodeSnapshotPath,
|
|
162
347
|
setOpenCodeProxySettings,
|
|
163
348
|
clearOpenCodeProxySettings,
|
|
164
349
|
};
|
|
@@ -4,6 +4,7 @@ import { openCodeConfigurator } from "./openCode.js";
|
|
|
4
4
|
import { codexConfigurator } from "./codex.js";
|
|
5
5
|
import { qwenCodeConfigurator } from "./qwenCode.js";
|
|
6
6
|
import { copilotConfigurator } from "./copilot.js";
|
|
7
|
+
import { geminiConfigurator } from "./gemini.js";
|
|
7
8
|
/**
|
|
8
9
|
* Every CLI the proxy auto-configures, in apply order.
|
|
9
10
|
*
|
|
@@ -16,6 +17,7 @@ export const PROXY_CLIENT_CONFIGURATORS = [
|
|
|
16
17
|
codexConfigurator,
|
|
17
18
|
qwenCodeConfigurator,
|
|
18
19
|
copilotConfigurator,
|
|
20
|
+
geminiConfigurator,
|
|
19
21
|
];
|
|
20
22
|
/**
|
|
21
23
|
* Point every detected client at the proxy.
|
|
@@ -33,6 +33,19 @@ export declare function shouldCaptureSnapshot(args: {
|
|
|
33
33
|
written: unknown;
|
|
34
34
|
current: unknown;
|
|
35
35
|
}): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Whether a decoded snapshot file is structurally usable.
|
|
38
|
+
*
|
|
39
|
+
* A snapshot on disk is not necessarily one we wrote: it can be truncated by a
|
|
40
|
+
* full disk, hand-edited, or left over from another version. `JSON.parse` is
|
|
41
|
+
* happy with `{}`, `[]`, `null` and `"text"`, and every one of those then reads
|
|
42
|
+
* as "a snapshot whose recorded original is absent" — which restore paths treat
|
|
43
|
+
* as "the user had nothing here", and act on by deleting the user's real
|
|
44
|
+
* config. Requiring the discriminating key present makes a malformed file fall
|
|
45
|
+
* through to the caller's no-snapshot branch, which refuses to destroy
|
|
46
|
+
* anything, instead of impersonating an empty one.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isUsableSnapshot<K extends string>(value: unknown, requiredKey: K): value is Record<K, unknown>;
|
|
36
49
|
/** Deep copy through JSON, so a snapshot cannot alias the object it describes. */
|
|
37
50
|
export declare function cloneForSnapshot<T>(value: T): T;
|
|
38
51
|
/**
|
|
@@ -74,6 +74,24 @@ export function shouldCaptureSnapshot(args) {
|
|
|
74
74
|
}
|
|
75
75
|
return !valuesMatch(args.current, args.written);
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Whether a decoded snapshot file is structurally usable.
|
|
79
|
+
*
|
|
80
|
+
* A snapshot on disk is not necessarily one we wrote: it can be truncated by a
|
|
81
|
+
* full disk, hand-edited, or left over from another version. `JSON.parse` is
|
|
82
|
+
* happy with `{}`, `[]`, `null` and `"text"`, and every one of those then reads
|
|
83
|
+
* as "a snapshot whose recorded original is absent" — which restore paths treat
|
|
84
|
+
* as "the user had nothing here", and act on by deleting the user's real
|
|
85
|
+
* config. Requiring the discriminating key present makes a malformed file fall
|
|
86
|
+
* through to the caller's no-snapshot branch, which refuses to destroy
|
|
87
|
+
* anything, instead of impersonating an empty one.
|
|
88
|
+
*/
|
|
89
|
+
export function isUsableSnapshot(value, requiredKey) {
|
|
90
|
+
return (typeof value === "object" &&
|
|
91
|
+
value !== null &&
|
|
92
|
+
!Array.isArray(value) &&
|
|
93
|
+
Object.prototype.hasOwnProperty.call(value, requiredKey));
|
|
94
|
+
}
|
|
77
95
|
/** Deep copy through JSON, so a snapshot cannot alias the object it describes. */
|
|
78
96
|
export function cloneForSnapshot(value) {
|
|
79
97
|
return JSON.parse(JSON.stringify(value));
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model IDs the proxy advertises when no routing config narrows the list.
|
|
3
|
+
*
|
|
4
|
+
* Two consumers need the same answer and must not drift apart:
|
|
5
|
+
*
|
|
6
|
+
* - `proxyTranslationEngine` serves them from `GET /v1/models`.
|
|
7
|
+
* - The OpenCode client configurator writes them into `provider.neurolink.
|
|
8
|
+
* models`, because OpenCode resolves a `--model` against that map alone.
|
|
9
|
+
* It does not call `/v1/models`, so an empty map means every model id is
|
|
10
|
+
* unknown and `opencode run` fails with `ProviderModelNotFoundError`
|
|
11
|
+
* before a request is ever made.
|
|
12
|
+
*
|
|
13
|
+
* Format matches the IDs used throughout `src/lib/models/` and
|
|
14
|
+
* `src/lib/constants/` (e.g. `claude-3-5-haiku-20241022`, not
|
|
15
|
+
* `claude-haiku-3.5-20241022`).
|
|
16
|
+
*
|
|
17
|
+
* This is the no-router default. A proxy configured with explicit
|
|
18
|
+
* `routing.model-mappings` serves those instead, and a config written from
|
|
19
|
+
* this list will not mention them — a limitation worth knowing, but strictly
|
|
20
|
+
* better than the empty map it replaces.
|
|
21
|
+
*/
|
|
22
|
+
export declare const DEFAULT_PROXY_MODEL_IDS: readonly string[];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model IDs the proxy advertises when no routing config narrows the list.
|
|
3
|
+
*
|
|
4
|
+
* Two consumers need the same answer and must not drift apart:
|
|
5
|
+
*
|
|
6
|
+
* - `proxyTranslationEngine` serves them from `GET /v1/models`.
|
|
7
|
+
* - The OpenCode client configurator writes them into `provider.neurolink.
|
|
8
|
+
* models`, because OpenCode resolves a `--model` against that map alone.
|
|
9
|
+
* It does not call `/v1/models`, so an empty map means every model id is
|
|
10
|
+
* unknown and `opencode run` fails with `ProviderModelNotFoundError`
|
|
11
|
+
* before a request is ever made.
|
|
12
|
+
*
|
|
13
|
+
* Format matches the IDs used throughout `src/lib/models/` and
|
|
14
|
+
* `src/lib/constants/` (e.g. `claude-3-5-haiku-20241022`, not
|
|
15
|
+
* `claude-haiku-3.5-20241022`).
|
|
16
|
+
*
|
|
17
|
+
* This is the no-router default. A proxy configured with explicit
|
|
18
|
+
* `routing.model-mappings` serves those instead, and a config written from
|
|
19
|
+
* this list will not mention them — a limitation worth knowing, but strictly
|
|
20
|
+
* better than the empty map it replaces.
|
|
21
|
+
*/
|
|
22
|
+
export const DEFAULT_PROXY_MODEL_IDS = [
|
|
23
|
+
// Claude 4-series (current generation, hyphen-suffix family)
|
|
24
|
+
"claude-opus-4-6",
|
|
25
|
+
"claude-sonnet-4-6",
|
|
26
|
+
"claude-haiku-4-5",
|
|
27
|
+
// Claude 4 dated variant
|
|
28
|
+
"claude-sonnet-4-20250514",
|
|
29
|
+
// Claude 3.5-series (canonical Anthropic form: claude-3-5-{variant}-{date})
|
|
30
|
+
"claude-3-5-sonnet-20241022",
|
|
31
|
+
"claude-3-5-haiku-20241022",
|
|
32
|
+
// OpenAI / Google for translated-fallback users
|
|
33
|
+
"gpt-4o",
|
|
34
|
+
"gemini-2.5-pro",
|
|
35
|
+
"gemini-2.5-flash",
|
|
36
|
+
];
|
|
@@ -341,7 +341,7 @@ export declare abstract class BaseProvider implements AIProvider {
|
|
|
341
341
|
/**
|
|
342
342
|
* Get AI SDK model with middleware applied
|
|
343
343
|
* This method wraps the base model with any configured middleware
|
|
344
|
-
* TODO(#
|
|
344
|
+
* TODO(#1576): Implement global level middlewares that can be used
|
|
345
345
|
*/
|
|
346
346
|
protected getAISDKModelWithMiddleware(options?: TextGenerationOptions | StreamOptions): Promise<LanguageModel>;
|
|
347
347
|
/**
|
|
@@ -102,7 +102,7 @@ export class BaseProvider {
|
|
|
102
102
|
modelName;
|
|
103
103
|
providerName;
|
|
104
104
|
defaultTimeout = 30000; // 30 seconds
|
|
105
|
-
middlewareOptions; // TODO(#
|
|
105
|
+
middlewareOptions; // TODO(#1576): Implement global level middlewares that can be used
|
|
106
106
|
// Tools are conditionally included based on centralized configuration
|
|
107
107
|
directTools = shouldDisableBuiltinTools()
|
|
108
108
|
? {}
|
|
@@ -1584,7 +1584,7 @@ export class BaseProvider {
|
|
|
1584
1584
|
/**
|
|
1585
1585
|
* Get AI SDK model with middleware applied
|
|
1586
1586
|
* This method wraps the base model with any configured middleware
|
|
1587
|
-
* TODO(#
|
|
1587
|
+
* TODO(#1576): Implement global level middlewares that can be used
|
|
1588
1588
|
*/
|
|
1589
1589
|
async getAISDKModelWithMiddleware(options = {}) {
|
|
1590
1590
|
// Get the base model
|
|
@@ -396,7 +396,7 @@ export class FormatScorer extends BaseRuleScorer {
|
|
|
396
396
|
// First check if it's valid JSON
|
|
397
397
|
try {
|
|
398
398
|
JSON.parse(text);
|
|
399
|
-
// TODO(#
|
|
399
|
+
// TODO(#1576): Implement full JSON Schema validation
|
|
400
400
|
// For now, just check it's valid JSON
|
|
401
401
|
return { passed: true, score: 1.0 };
|
|
402
402
|
}
|
|
@@ -318,7 +318,7 @@ export function applyContentFiltering(text, badWordsConfig, context = "unknown")
|
|
|
318
318
|
logger.debug(`[ContentFiltering:${context}] Applying regex pattern filtering with ${badWordsConfig.regexPatterns.length} patterns using replacement: "${replacementText}".`);
|
|
319
319
|
for (const pattern of badWordsConfig.regexPatterns) {
|
|
320
320
|
try {
|
|
321
|
-
// TODO(#
|
|
321
|
+
// TODO(#1576): Add blocking for overly complex or long patterns
|
|
322
322
|
if (pattern.length > 1000) {
|
|
323
323
|
logger.warn(`[ContentFiltering:${context}] Regex pattern exceeds max length (1000 chars): "${pattern.substring(0, 50)}..."`);
|
|
324
324
|
}
|
package/dist/neurolink.js
CHANGED
|
@@ -9283,7 +9283,7 @@ Current user's request: ${currentInput}`;
|
|
|
9283
9283
|
// ========================================
|
|
9284
9284
|
// ENHANCED: Tool Event Emission API
|
|
9285
9285
|
// ========================================
|
|
9286
|
-
// TODO(#
|
|
9286
|
+
// TODO(#1576): Add ToolExecutionEvent utility methods in future version
|
|
9287
9287
|
// Will provide structured event format for consistent tool event processing
|
|
9288
9288
|
/**
|
|
9289
9289
|
* Emit tool start event with execution tracking
|
|
@@ -9406,7 +9406,7 @@ Current user's request: ${currentInput}`;
|
|
|
9406
9406
|
clearCurrentStreamExecutions() {
|
|
9407
9407
|
this.currentStreamToolExecutions = [];
|
|
9408
9408
|
}
|
|
9409
|
-
// TODO(#
|
|
9409
|
+
// TODO(#1576): Add getToolExecutionEvents() method in future version
|
|
9410
9410
|
// Will return properly formatted ToolExecutionEvent objects for structured event processing
|
|
9411
9411
|
// ========================================
|
|
9412
9412
|
// Tool Registration API
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { ClaudeStreamSerializer, generateToolUseId, serializeClaudeResponse, } from "./claudeFormat.js";
|
|
15
15
|
import { buildGeminiResponse, createGeminiSerializerAdapter, } from "./geminiFormat.js";
|
|
16
16
|
import { generateOpenAIToolCallId, OpenAIStreamSerializer, serializeOpenAIResponse, } from "./openaiFormat.js";
|
|
17
|
+
import { DEFAULT_PROXY_MODEL_IDS } from "../constants/proxyModels.js";
|
|
17
18
|
import { logRequest } from "./requestLogger.js";
|
|
18
19
|
import { recordAttempt, recordAttemptError, recordFinalError, recordFinalSuccess, } from "./usageStats.js";
|
|
19
20
|
import { ErrorCategory, ErrorSeverity } from "../constants/enums.js";
|
|
@@ -677,7 +678,7 @@ export function buildModelsListResponse(modelRouter) {
|
|
|
677
678
|
}
|
|
678
679
|
// Always include a default entry if nothing else is configured
|
|
679
680
|
if (models.length === 0) {
|
|
680
|
-
for (const id of
|
|
681
|
+
for (const id of DEFAULT_PROXY_MODEL_IDS) {
|
|
681
682
|
models.push({
|
|
682
683
|
id,
|
|
683
684
|
object: "model",
|
|
@@ -691,26 +692,6 @@ export function buildModelsListResponse(modelRouter) {
|
|
|
691
692
|
data: models,
|
|
692
693
|
};
|
|
693
694
|
}
|
|
694
|
-
/**
|
|
695
|
-
* Canonical default model IDs surfaced when no router is configured. Format
|
|
696
|
-
* matches the IDs used throughout `src/lib/models/` and `src/lib/constants/`
|
|
697
|
-
* (e.g. `claude-3-5-haiku-20241022`, not `claude-haiku-3.5-20241022`).
|
|
698
|
-
*/
|
|
699
|
-
const DEFAULT_MODEL_IDS = [
|
|
700
|
-
// Claude 4-series (current generation, hyphen-suffix family)
|
|
701
|
-
"claude-opus-4-6",
|
|
702
|
-
"claude-sonnet-4-6",
|
|
703
|
-
"claude-haiku-4-5",
|
|
704
|
-
// Claude 4 dated variant
|
|
705
|
-
"claude-sonnet-4-20250514",
|
|
706
|
-
// Claude 3.5-series (canonical Anthropic form: claude-3-5-{variant}-{date})
|
|
707
|
-
"claude-3-5-sonnet-20241022",
|
|
708
|
-
"claude-3-5-haiku-20241022",
|
|
709
|
-
// OpenAI / Google for translated-fallback users
|
|
710
|
-
"gpt-4o",
|
|
711
|
-
"gemini-2.5-pro",
|
|
712
|
-
"gemini-2.5-flash",
|
|
713
|
-
];
|
|
714
695
|
/**
|
|
715
696
|
* Build an Anthropic-shaped `/v1/models` list response.
|
|
716
697
|
*
|
|
@@ -736,7 +717,7 @@ export function buildAnthropicModelsListResponse(modelRouter) {
|
|
|
736
717
|
}
|
|
737
718
|
}
|
|
738
719
|
if (ids.length === 0) {
|
|
739
|
-
ids.push(...
|
|
720
|
+
ids.push(...DEFAULT_PROXY_MODEL_IDS);
|
|
740
721
|
}
|
|
741
722
|
// Deduplicate while preserving order — multiple router sources can publish
|
|
742
723
|
// the same id (e.g. both an explicit mapping and a passthrough entry).
|
|
@@ -177,7 +177,7 @@ export class ChunkerFactory extends BaseFactory {
|
|
|
177
177
|
}, DEFAULT_CHUNKER_METADATA.latex);
|
|
178
178
|
// Register semantic chunker (placeholder - uses recursive as fallback)
|
|
179
179
|
this.registerChunker("semantic", async (config) => {
|
|
180
|
-
// TODO(#
|
|
180
|
+
// TODO(#1576): Implement dedicated SemanticChunker with LLM support
|
|
181
181
|
// For now, fall back to RecursiveChunker with semantic defaults
|
|
182
182
|
const { RecursiveChunker } = await import("./chunkers/RecursiveChunker.js");
|
|
183
183
|
return new RecursiveChunker(config);
|
|
@@ -334,17 +334,17 @@ export function createAgentWebSocketHandler(_neurolink) {
|
|
|
334
334
|
// Register message routes
|
|
335
335
|
router.route("generate", async (connection, payload) => {
|
|
336
336
|
const { prompt, options: _options } = payload;
|
|
337
|
-
// TODO(#
|
|
337
|
+
// TODO(#1576): Implement generate using neurolink
|
|
338
338
|
return { type: "response", data: `Received: ${prompt}` };
|
|
339
339
|
});
|
|
340
340
|
router.route("stream", async (connection, payload) => {
|
|
341
341
|
const { prompt, options: _options } = payload;
|
|
342
|
-
// TODO(#
|
|
342
|
+
// TODO(#1576): Implement streaming using neurolink
|
|
343
343
|
return { type: "stream_start", data: { prompt } };
|
|
344
344
|
});
|
|
345
345
|
router.route("tool_call", async (connection, payload) => {
|
|
346
346
|
const { toolName, args: _args } = payload;
|
|
347
|
-
// TODO(#
|
|
347
|
+
// TODO(#1576): Implement tool call using neurolink
|
|
348
348
|
return { type: "tool_result", data: { toolName, result: null } };
|
|
349
349
|
});
|
|
350
350
|
return {
|