nvicode 0.1.19 → 0.1.20

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/README.md CHANGED
@@ -17,6 +17,7 @@ What it gives you:
17
17
  - OpenClaw support
18
18
  - NVIDIA proxy mode with pacing and local usage tracking
19
19
  - OpenRouter direct mode for compatible models
20
+ - Dynamic NVIDIA model discovery for current Kimi, DeepSeek, GLM, and Qwen picks
20
21
 
21
22
  Supported environments:
22
23
  - macOS
@@ -116,6 +117,7 @@ nvicode launch codex "Explain this project"
116
117
 
117
118
  Behavior notes:
118
119
  - `nvicode select model` asks for provider, optional API key update, and model choice in one guided flow.
120
+ - For NVIDIA, model selection fetches the live NVIDIA catalog and highlights one current top pick each from Kimi, DeepSeek, GLM, and Qwen before falling back to curated defaults.
119
121
  - Claude Code uses direct OpenRouter mode for OpenRouter, and proxy mode for NVIDIA.
120
122
  - Codex currently uses the local `nvicode` proxy path.
121
123
  - `nvicode usage`, `activity`, and `dashboard` are currently focused on NVIDIA proxy sessions.
package/dist/cli.js CHANGED
@@ -448,11 +448,19 @@ const runDashboard = async () => {
448
448
  console.log("");
449
449
  await runActivity();
450
450
  };
451
- const waitForHealthyProxy = async (port) => {
451
+ const PROXY_PROTOCOL_VERSION = 2;
452
+ const isExpectedProxyHealth = (config, health) => health.ok === true &&
453
+ health.proxyProtocolVersion === PROXY_PROTOCOL_VERSION &&
454
+ health.provider === config.provider &&
455
+ health.model === getActiveModel(config) &&
456
+ health.port === config.proxyPort &&
457
+ health.thinking === config.thinking &&
458
+ health.maxRequestsPerMinute === config.maxRequestsPerMinute;
459
+ const waitForHealthyProxy = async (config) => {
452
460
  for (let attempt = 0; attempt < 50; attempt += 1) {
453
461
  try {
454
- const response = await fetch(`http://127.0.0.1:${port}/health`);
455
- if (response.ok) {
462
+ const response = await fetch(`http://127.0.0.1:${config.proxyPort}/health`);
463
+ if (response.ok && isExpectedProxyHealth(config, await response.json())) {
456
464
  return true;
457
465
  }
458
466
  }
@@ -463,11 +471,35 @@ const waitForHealthyProxy = async (port) => {
463
471
  }
464
472
  return false;
465
473
  };
474
+ const stopExistingProxy = async () => {
475
+ const paths = getNvicodePaths();
476
+ const rawPid = await readIfExists(paths.pidFile);
477
+ const pid = Number(rawPid?.trim());
478
+ if (!Number.isInteger(pid) || pid <= 0) {
479
+ return;
480
+ }
481
+ try {
482
+ process.kill(pid, "SIGTERM");
483
+ }
484
+ catch {
485
+ return;
486
+ }
487
+ for (let attempt = 0; attempt < 20; attempt += 1) {
488
+ try {
489
+ process.kill(pid, 0);
490
+ await new Promise((resolve) => setTimeout(resolve, 100));
491
+ }
492
+ catch {
493
+ break;
494
+ }
495
+ }
496
+ };
466
497
  const ensureProxyRunning = async (config) => {
467
- if (await waitForHealthyProxy(config.proxyPort)) {
498
+ if (await waitForHealthyProxy(config)) {
468
499
  return;
469
500
  }
470
501
  const paths = getNvicodePaths();
502
+ await stopExistingProxy();
471
503
  await fs.mkdir(paths.stateDir, { recursive: true });
472
504
  const logFd = openSync(paths.logFile, "a");
473
505
  const child = spawn(process.execPath, [__filename, "serve"], {
@@ -480,7 +512,7 @@ const ensureProxyRunning = async (config) => {
480
512
  });
481
513
  child.unref();
482
514
  await fs.writeFile(paths.pidFile, `${child.pid}\n`);
483
- if (!(await waitForHealthyProxy(config.proxyPort))) {
515
+ if (!(await waitForHealthyProxy(config))) {
484
516
  throw new Error(`nvicode proxy failed to start. See ${paths.logFile}`);
485
517
  }
486
518
  };
@@ -536,6 +568,30 @@ const resolvePersistentClaudeCommand = async () => {
536
568
  }
537
569
  return null;
538
570
  };
571
+ const resolveLatestClaudeManagedVersion = async () => {
572
+ const versionsDir = path.join(os.homedir(), ".local", "share", "claude", "versions");
573
+ try {
574
+ const entries = await fs.readdir(versionsDir);
575
+ const sortedEntries = entries.sort((left, right) => left.localeCompare(right, undefined, {
576
+ numeric: true,
577
+ sensitivity: "base",
578
+ }));
579
+ for (let index = sortedEntries.length - 1; index >= 0; index -= 1) {
580
+ const entry = sortedEntries[index];
581
+ if (!entry) {
582
+ continue;
583
+ }
584
+ const resolved = await resolveClaudeVersionEntry(path.join(versionsDir, entry));
585
+ if (resolved) {
586
+ return resolved;
587
+ }
588
+ }
589
+ }
590
+ catch {
591
+ // continue with other install layouts
592
+ }
593
+ return null;
594
+ };
539
595
  const getWrapperInstallPaths = async (claudeCommandPath) => {
540
596
  const directory = path.dirname(claudeCommandPath);
541
597
  const existingNative = await findExistingClaudeNativeInDirectory(directory);
@@ -656,6 +712,10 @@ const ensurePersistentCodexRouting = async () => {
656
712
  return "installed";
657
713
  };
658
714
  const resolveClaudeBinary = async () => {
715
+ const latestManagedVersion = await resolveLatestClaudeManagedVersion();
716
+ if (latestManagedVersion) {
717
+ return latestManagedVersion;
718
+ }
659
719
  for (const name of getClaudeNativeNames()) {
660
720
  const nativeInPath = await findExecutableInPath(name);
661
721
  if (nativeInPath) {
@@ -674,27 +734,10 @@ const resolveClaudeBinary = async () => {
674
734
  path.join(os.homedir(), ".local", "bin", "claude"),
675
735
  ];
676
736
  for (const candidate of homeBinCandidates) {
677
- if (await isExecutable(candidate)) {
737
+ if ((await isExecutable(candidate)) && !(await isManagedClaudeWrapper(candidate))) {
678
738
  return candidate;
679
739
  }
680
740
  }
681
- const versionsDir = path.join(os.homedir(), ".local", "share", "claude", "versions");
682
- try {
683
- const entries = await fs.readdir(versionsDir);
684
- const latest = entries.sort((left, right) => left.localeCompare(right, undefined, {
685
- numeric: true,
686
- sensitivity: "base",
687
- })).at(-1);
688
- if (latest) {
689
- const resolved = await resolveClaudeVersionEntry(path.join(versionsDir, latest));
690
- if (resolved) {
691
- return resolved;
692
- }
693
- }
694
- }
695
- catch {
696
- // continue
697
- }
698
741
  for (const name of getClaudeCommandNames()) {
699
742
  const claudeInPath = await findExecutableInPath(name);
700
743
  if (claudeInPath && !(await isManagedClaudeWrapper(claudeInPath))) {
package/dist/config.js CHANGED
@@ -7,6 +7,12 @@ const DEFAULT_PROVIDER = "nvidia";
7
7
  const DEFAULT_NVIDIA_MODEL = "moonshotai/kimi-k2.5";
8
8
  const DEFAULT_OPENROUTER_MODEL = "anthropic/claude-sonnet-4.6";
9
9
  const DEFAULT_MAX_REQUESTS_PER_MINUTE = 40;
10
+ const NVIDIA_MODEL_ALIASES = {
11
+ "deepseek/deepseek-v4-pro": "deepseek-ai/deepseek-v4-flash",
12
+ "deepseek-ai/deepseek-v4-pro": "deepseek-ai/deepseek-v4-flash",
13
+ "deepseek-ai/deepseek-v3.2": "deepseek-ai/deepseek-v4-flash",
14
+ };
15
+ const normalizeNvidiaModel = (model) => NVIDIA_MODEL_ALIASES[model] || model;
10
16
  const getEnvNumber = (name) => {
11
17
  const raw = process.env[name];
12
18
  if (!raw) {
@@ -61,7 +67,7 @@ const withDefaults = (config) => {
61
67
  return {
62
68
  provider: config.provider === "openrouter" ? "openrouter" : DEFAULT_PROVIDER,
63
69
  nvidiaApiKey: config.nvidiaApiKey?.trim() || legacyApiKey,
64
- nvidiaModel: config.nvidiaModel?.trim() || legacyModel,
70
+ nvidiaModel: normalizeNvidiaModel(config.nvidiaModel?.trim() || legacyModel),
65
71
  openrouterApiKey: config.openrouterApiKey?.trim() || "",
66
72
  openrouterModel: config.openrouterModel?.trim() || DEFAULT_OPENROUTER_MODEL,
67
73
  proxyPort: Number.isInteger(config.proxyPort) && config.proxyPort > 0
package/dist/models.js CHANGED
@@ -15,9 +15,9 @@ export const NVIDIA_CURATED_MODELS = [
15
15
  description: "General purpose reasoning model with code capability.",
16
16
  },
17
17
  {
18
- id: "deepseek-ai/deepseek-v3.2",
19
- label: "DeepSeek V3.2",
20
- description: "General coding and reasoning model.",
18
+ id: "deepseek-ai/deepseek-v4-flash",
19
+ label: "DeepSeek V4 Flash",
20
+ description: "Responsive DeepSeek V4-family model.",
21
21
  },
22
22
  {
23
23
  id: "mistralai/codestral-22b-instruct-v0.1",
@@ -30,6 +30,36 @@ export const NVIDIA_CURATED_MODELS = [
30
30
  description: "Smaller coding-focused Qwen model.",
31
31
  },
32
32
  ];
33
+ const NVIDIA_MODEL_FAMILIES = [
34
+ {
35
+ name: "Kimi",
36
+ labelPrefix: "Kimi",
37
+ description: "Latest available Kimi model from NVIDIA.",
38
+ match: /^moonshotai\/kimi/i,
39
+ prefer: [/k2\.5/i, /thinking/i, /instruct/i],
40
+ },
41
+ {
42
+ name: "DeepSeek",
43
+ labelPrefix: "DeepSeek",
44
+ description: "Latest available DeepSeek model from NVIDIA.",
45
+ match: /^deepseek-ai\/deepseek/i,
46
+ prefer: [/v4-flash/i, /v4-pro/i, /v4/i, /v3\.2/i, /coder/i],
47
+ },
48
+ {
49
+ name: "GLM",
50
+ labelPrefix: "GLM",
51
+ description: "Latest available GLM model from NVIDIA.",
52
+ match: /^z-ai\/glm/i,
53
+ prefer: [/5\.1/i, /5/i, /4\.7/i],
54
+ },
55
+ {
56
+ name: "Qwen",
57
+ labelPrefix: "Qwen",
58
+ description: "Latest available Qwen coding model from NVIDIA.",
59
+ match: /^qwen\/qwen/i,
60
+ prefer: [/qwen3-coder/i, /qwen3\.5/i, /qwen3-next/i, /qwen3/i, /qwen2\.5-coder/i],
61
+ },
62
+ ];
33
63
  export const OPENROUTER_CURATED_MODELS = [
34
64
  {
35
65
  id: "qwen/qwen3.6-plus-preview:free",
@@ -71,14 +101,87 @@ export const fetchAvailableModelIds = async (apiKey) => {
71
101
  }
72
102
  return ids;
73
103
  };
104
+ const formatModelNameToken = (part) => {
105
+ const normalized = part.toLowerCase();
106
+ const brandNames = {
107
+ deepseek: "DeepSeek",
108
+ glm: "GLM",
109
+ kimi: "Kimi",
110
+ qwen: "Qwen",
111
+ };
112
+ if (brandNames[normalized]) {
113
+ return brandNames[normalized];
114
+ }
115
+ if (/^[vk]\d/i.test(part) || /^\d+b$/i.test(part) || /^a\d+b$/i.test(part)) {
116
+ return part.toUpperCase();
117
+ }
118
+ return part.charAt(0).toUpperCase() + part.slice(1);
119
+ };
120
+ const titleCaseModelPart = (value) => value
121
+ .split(/[-_]/)
122
+ .filter(Boolean)
123
+ .map(formatModelNameToken)
124
+ .join(" ");
125
+ const formatDynamicLabel = (family, id) => {
126
+ const modelName = id.split("/").at(-1) || id;
127
+ return `${family.labelPrefix}: ${titleCaseModelPart(modelName)}`;
128
+ };
129
+ const getVersionScore = (id) => {
130
+ const versionNumbers = [...id.matchAll(/\d+(?:\.\d+)?/g)]
131
+ .map((match) => Number(match[0]))
132
+ .filter((value) => Number.isFinite(value));
133
+ if (versionNumbers.length === 0) {
134
+ return 0;
135
+ }
136
+ return Math.max(...versionNumbers);
137
+ };
138
+ const scoreFamilyModel = (family, id) => {
139
+ let score = getVersionScore(id);
140
+ family.prefer.forEach((pattern, index) => {
141
+ if (pattern.test(id)) {
142
+ score += (family.prefer.length - index) * 1000;
143
+ }
144
+ });
145
+ if (/preview|beta|experimental/i.test(id)) {
146
+ score -= 10;
147
+ }
148
+ return score;
149
+ };
150
+ const pickFamilyModel = (family, ids) => {
151
+ const candidates = ids.filter((id) => family.match.test(id));
152
+ if (candidates.length === 0) {
153
+ return null;
154
+ }
155
+ const [best] = candidates.sort((left, right) => {
156
+ const scoreDelta = scoreFamilyModel(family, right) - scoreFamilyModel(family, left);
157
+ return scoreDelta || right.localeCompare(left, undefined, { numeric: true });
158
+ });
159
+ if (!best) {
160
+ return null;
161
+ }
162
+ return {
163
+ id: best,
164
+ label: formatDynamicLabel(family, best),
165
+ description: family.description,
166
+ };
167
+ };
168
+ const getDynamicNvidiaModels = (available) => {
169
+ const ids = [...available];
170
+ const picked = NVIDIA_MODEL_FAMILIES
171
+ .map((family) => pickFamilyModel(family, ids))
172
+ .filter((model) => Boolean(model));
173
+ const seen = new Set(picked.map((model) => model.id));
174
+ const fallback = NVIDIA_CURATED_MODELS.filter((model) => available.has(model.id) && !seen.has(model.id));
175
+ return [...picked, ...fallback];
176
+ };
74
177
  export const getRecommendedModels = async (provider, apiKey) => {
75
178
  if (provider === "openrouter") {
76
179
  return OPENROUTER_CURATED_MODELS;
77
180
  }
78
181
  try {
79
182
  const available = await fetchAvailableModelIds(apiKey);
80
- const curated = NVIDIA_CURATED_MODELS.filter((model) => available.has(model.id));
81
- return curated.length > 0 ? curated : NVIDIA_CURATED_MODELS;
183
+ const dynamic = getDynamicNvidiaModels(available);
184
+ return dynamic.length > 0 ? dynamic : NVIDIA_CURATED_MODELS;
82
185
  }
83
186
  catch {
84
187
  return NVIDIA_CURATED_MODELS;
package/dist/proxy.js CHANGED
@@ -6,12 +6,33 @@ const NVIDIA_URL = "https://integrate.api.nvidia.com/v1/chat/completions";
6
6
  const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions";
7
7
  const DEFAULT_RETRY_DELAY_MS = 2_000;
8
8
  const MAX_NVIDIA_RETRIES = 3;
9
+ const UPSTREAM_TIMEOUT_MS = 60_000;
10
+ const PROXY_PROTOCOL_VERSION = 2;
9
11
  const sleep = async (ms) => {
10
12
  if (ms <= 0) {
11
13
  return;
12
14
  }
13
15
  await new Promise((resolve) => setTimeout(resolve, ms));
14
16
  };
17
+ const fetchWithTimeout = async (input, init) => {
18
+ const controller = new AbortController();
19
+ const timeout = setTimeout(() => controller.abort(), UPSTREAM_TIMEOUT_MS);
20
+ try {
21
+ return await fetch(input, {
22
+ ...init,
23
+ signal: controller.signal,
24
+ });
25
+ }
26
+ catch (error) {
27
+ if (error.name === "AbortError") {
28
+ throw new Error(`Upstream API timed out after ${UPSTREAM_TIMEOUT_MS / 1000}s`);
29
+ }
30
+ throw error;
31
+ }
32
+ finally {
33
+ clearTimeout(timeout);
34
+ }
35
+ };
15
36
  const parseRetryAfterMs = (value) => {
16
37
  if (!value) {
17
38
  return null;
@@ -415,14 +436,14 @@ const callNvidia = async (config, scheduleRequest, payload) => {
415
436
  if (toolChoice) {
416
437
  requestBody.tool_choice = toolChoice;
417
438
  }
418
- if (config.thinking) {
439
+ if (config.provider === "nvidia") {
419
440
  requestBody.chat_template_kwargs = {
420
- thinking: true,
441
+ thinking: config.thinking,
421
442
  };
422
443
  }
423
444
  const invoke = async () => {
424
445
  for (let attempt = 0; attempt <= MAX_NVIDIA_RETRIES; attempt += 1) {
425
- const response = await fetch(NVIDIA_URL, {
446
+ const response = await fetchWithTimeout(NVIDIA_URL, {
426
447
  method: "POST",
427
448
  headers: {
428
449
  Authorization: `Bearer ${config.nvidiaApiKey}`,
@@ -654,7 +675,7 @@ const callChatCompletions = async (config, scheduleRequest, body) => {
654
675
  const apiKey = getActiveApiKey(config);
655
676
  const invoke = async () => {
656
677
  for (let attempt = 0; attempt <= MAX_NVIDIA_RETRIES; attempt += 1) {
657
- const resp = await fetch(upstreamUrl, {
678
+ const resp = await fetchWithTimeout(upstreamUrl, {
658
679
  method: "POST",
659
680
  headers: {
660
681
  Authorization: `Bearer ${apiKey}`,
@@ -691,7 +712,9 @@ export const createProxyServer = (config) => {
691
712
  if (url.pathname === "/health") {
692
713
  sendJson(response, 200, {
693
714
  ok: true,
694
- model: config.nvidiaModel,
715
+ proxyProtocolVersion: PROXY_PROTOCOL_VERSION,
716
+ provider: config.provider,
717
+ model: getActiveModel(config),
695
718
  port: config.proxyPort,
696
719
  thinking: config.thinking,
697
720
  maxRequestsPerMinute: config.maxRequestsPerMinute,
@@ -872,6 +895,11 @@ export const createProxyServer = (config) => {
872
895
  max_tokens: payload.max_output_tokens ?? 16_384,
873
896
  stream: false,
874
897
  };
898
+ if (config.provider === "nvidia") {
899
+ chatBody.chat_template_kwargs = {
900
+ thinking: config.thinking,
901
+ };
902
+ }
875
903
  if (typeof payload.temperature === "number") {
876
904
  chatBody.temperature = payload.temperature;
877
905
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nvicode",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "description": "Route Claude Code, Codex CLI, and OpenClaw through NVIDIA or OpenRouter with one setup.",
5
5
  "author": "Dinesh Potla",
6
6
  "keywords": [