@juspay/neurolink 12.3.0 → 12.4.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.
@@ -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 { cloneForSnapshot, isProxyOwnedValue, shouldCaptureSnapshot, writeFileAtomic, } from "./snapshot.js";
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
- * Key under which we persist the snapshot of the user's pre-existing
25
- * `provider.neurolink` config inside `opencode.json` itself. Persisting (rather
26
- * than relying on in-process state) means restoration still works even if the
27
- * proxy crashes or shutdown handlers run in a different process.
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
- * Mirrors the Claude pattern (`__proxy_original_env` inside Claude's settings).
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
- const OPENCODE_ORIGINAL_KEY = "__proxy_original_neurolink";
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
- * What this writer last wrote into provider.neurolink. Lets apply() tell its
34
- * own block from one the user substituted while the proxy was not running.
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
- const OPENCODE_WRITTEN_KEY = "__proxy_written_neurolink";
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: OPENCODE_ORIGINAL_KEY in config,
65
- written: config[OPENCODE_WRITTEN_KEY],
213
+ hasSnapshot: snapshot !== null,
214
+ written: snapshot?.written,
66
215
  current: currentBlock,
67
216
  })) {
68
- config[OPENCODE_ORIGINAL_KEY] =
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 an "undefined" snapshot, since
116
- // that would mean the snapshot was lost and we cannot prove the entry is ours.
117
- if (OPENCODE_ORIGINAL_KEY in config) {
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: config[OPENCODE_WRITTEN_KEY],
287
+ written: snapshot.written,
123
288
  current: existing,
124
289
  })) {
125
- const snapshot = config[OPENCODE_ORIGINAL_KEY];
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
- delete config[OPENCODE_ORIGINAL_KEY];
138
- delete config[OPENCODE_WRITTEN_KEY];
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 key was introduced, or this is being cleared from
144
- // a process that never ran set()).
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
+ ];
@@ -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 DEFAULT_MODEL_IDS) {
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(...DEFAULT_MODEL_IDS);
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).
@@ -47,6 +47,39 @@ export type CliProxyClientRestoreResult = {
47
47
  restored: boolean;
48
48
  error?: Error;
49
49
  };
50
+ /**
51
+ * Snapshot of the user's pre-existing OpenCode `provider.neurolink`.
52
+ *
53
+ * Persisted to `~/.neurolink/opencode-proxy-snapshot.json`, never inside
54
+ * `opencode.json` — OpenCode validates against a closed schema and rejects
55
+ * unknown top-level keys, so an in-file snapshot made the CLI unstartable.
56
+ */
57
+ export type CliOpenCodeSnapshot = {
58
+ /** The user's provider.neurolink before the proxy first touched it. */
59
+ original: unknown;
60
+ /** What the writer last wrote, so apply() can recognise its own block. */
61
+ written?: unknown;
62
+ };
63
+ /**
64
+ * Snapshot of the user's pre-existing Gemini CLI `~/.gemini/.env`.
65
+ *
66
+ * The whole file is kept rather than the managed keys alone: restoring must
67
+ * reproduce the user's comments, ordering and unrelated variables exactly.
68
+ */
69
+ export type CliGeminiSnapshot = {
70
+ /** The whole prior `.env`, or null when the user had no such file. */
71
+ originalEnv: string | null;
72
+ /**
73
+ * What the writer last wrote for each managed variable. Compared against the
74
+ * file on disk to detect a snapshot that has gone stale — one left behind by
75
+ * a restore whose cleanup failed, or overtaken by a user edit. Reusing such a
76
+ * record would make the next restore replay outdated values.
77
+ */
78
+ written?: {
79
+ baseUrl: string;
80
+ apiKey: string;
81
+ };
82
+ };
50
83
  /**
51
84
  * Raw contents of a Qwen Code `settings.json`. Deliberately open-ended: the
52
85
  * configurator rewrites only `security.auth` and must round-trip every other
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "12.3.0",
3
+ "version": "12.4.0",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
6
6
  "author": {