nvicode 0.1.16 → 0.1.18

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
@@ -1,4 +1,4 @@
1
- # NviCode - Introducing one click Nvidia/OpenRouter keys to Claude Code. Free Claude code.
1
+ # NviCode
2
2
 
3
3
  [![CI](https://github.com/dineshpotla/nvicode/actions/workflows/ci.yml/badge.svg)](https://github.com/dineshpotla/nvicode/actions/workflows/ci.yml)
4
4
  [![npm version](https://img.shields.io/npm/v/nvicode)](https://www.npmjs.com/package/nvicode)
@@ -6,9 +6,17 @@
6
6
  [![node >=20](https://img.shields.io/badge/node-%3E%3D20-339933)](https://nodejs.org/)
7
7
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
8
8
 
9
- Internally, `nvicode` sits between Claude Code and the selected provider.
10
- For NVIDIA, it starts a local Anthropic-compatible proxy that translates Claude Code requests to NVIDIA chat completions.
11
- For OpenRouter, it points Claude Code directly at OpenRouter's compatible endpoint and applies the selected model configuration.
9
+ Route Claude Code, Codex CLI, and OpenClaw through NVIDIA or OpenRouter with one setup.
10
+
11
+ `nvicode` lets you choose a provider once, save the API key once, pick a model once, and then launch the coding tool you want against that same backend.
12
+
13
+ What it gives you:
14
+ - One guided setup flow for provider, key, and model
15
+ - Claude Code support
16
+ - Codex CLI support
17
+ - OpenClaw support
18
+ - NVIDIA proxy mode with pacing and local usage tracking
19
+ - OpenRouter direct mode for compatible models
12
20
 
13
21
  Supported environments:
14
22
  - macOS
@@ -18,37 +26,44 @@ Supported environments:
18
26
 
19
27
  ## Quickstart
20
28
 
21
- Install the published package:
29
+ Install `nvicode`:
22
30
 
23
31
  ```sh
24
32
  npm install -g nvicode
25
33
  ```
26
34
 
27
- Set up provider, key, and model:
35
+ Choose provider, key, and model:
28
36
 
29
37
  ```sh
30
38
  nvicode select model
31
39
  ```
32
40
 
33
- The setup flow asks for provider, API key, and model:
34
-
35
- - NVIDIA: get a free key from [NVIDIA Build API Keys](https://build.nvidia.com/settings/api-keys)
36
- - OpenRouter: use your OpenRouter API key
37
-
38
- Launch Claude Code through your selected provider:
41
+ Launch the tool you want:
39
42
 
40
43
  ```sh
41
44
  nvicode launch claude
45
+ nvicode launch codex
46
+ nvicode launch openclaw
42
47
  ```
43
48
 
44
- The first successful `nvicode launch claude` also installs persistent plain-`claude` routing.
45
- After that, restarting your terminal or PC and running:
49
+ Provider setup:
50
+
51
+ - NVIDIA: get a free key from [NVIDIA Build API Keys](https://build.nvidia.com/settings/api-keys)
52
+ - OpenRouter: use your OpenRouter API key
53
+
54
+ What happens after first launch:
55
+ - The first successful `nvicode launch claude` installs persistent plain `claude` routing.
56
+ - The first successful `nvicode launch codex` installs persistent plain `codex` routing.
57
+ - `nvicode launch openclaw` updates the default OpenClaw profile for the selected provider/model.
58
+
59
+ After that, plain:
46
60
 
47
61
  ```sh
48
62
  claude
63
+ codex
49
64
  ```
50
65
 
51
- will continue using your selected `nvicode` provider and model.
66
+ continues using your selected `nvicode` provider and model.
52
67
 
53
68
  ## Screenshots
54
69
 
@@ -60,15 +75,35 @@ will continue using your selected `nvicode` provider and model.
60
75
 
61
76
  ![nvicode select model](https://raw.githubusercontent.com/dineshpotla/nvicode/main/assets/screenshots/select-model.png)
62
77
 
63
- ### Launch Claude Code through your selected provider
78
+ ### Launch through your selected provider
64
79
 
65
80
  ![nvicode launch claude](https://raw.githubusercontent.com/dineshpotla/nvicode/main/assets/screenshots/launch.png)
66
81
 
82
+ ## How It Works
83
+
84
+ - Claude Code:
85
+ - NVIDIA uses a local Anthropic-compatible proxy on `127.0.0.1:8788`
86
+ - OpenRouter connects directly to `https://openrouter.ai/api`
87
+ - Codex CLI:
88
+ - uses the local `nvicode` proxy
89
+ - `nvicode` configures Codex to talk to that proxy through the Responses API
90
+ - OpenClaw:
91
+ - updates the default OpenClaw config for the selected provider/model
92
+ - restart the gateway after config changes:
93
+
94
+ ```sh
95
+ openclaw gateway restart
96
+ ```
97
+
67
98
  ## Commands
68
99
 
69
- Useful commands:
100
+ Common commands:
70
101
 
71
102
  ```sh
103
+ nvicode select model
104
+ nvicode launch claude
105
+ nvicode launch codex
106
+ nvicode launch openclaw
72
107
  nvicode dashboard
73
108
  nvicode usage
74
109
  nvicode activity
@@ -76,25 +111,29 @@ nvicode models
76
111
  nvicode config
77
112
  nvicode auth
78
113
  nvicode launch claude -p "Reply with exactly OK"
114
+ nvicode launch codex "Explain this project"
79
115
  ```
80
116
 
81
- Provider behavior:
82
- - NVIDIA: starts a local proxy on `127.0.0.1:8788`, points Claude Code at it with `ANTHROPIC_BASE_URL`, and forwards requests to NVIDIA `chat/completions`.
83
- - OpenRouter: points Claude Code directly at `https://openrouter.ai/api` using OpenRouter credentials and Anthropic-compatible model ids.
117
+ Behavior notes:
118
+ - `nvicode select model` asks for provider, optional API key update, and model choice in one guided flow.
119
+ - Claude Code uses direct OpenRouter mode for OpenRouter, and proxy mode for NVIDIA.
120
+ - Codex currently uses the local `nvicode` proxy path.
121
+ - `nvicode usage`, `activity`, and `dashboard` are currently focused on NVIDIA proxy sessions.
122
+ - OpenRouter does not currently produce the same local usage visibility as the NVIDIA proxy flow.
123
+ - NVIDIA requests are paced to `40 RPM` by default. Override with `NVICODE_MAX_RPM` if your account allows more.
84
124
 
85
125
  In an interactive terminal, `nvicode usage` refreshes live every 2 seconds. When piped or redirected, it prints a single snapshot.
86
126
 
87
- `nvicode select model` now asks for provider, optional API key update, and model choice in one guided flow.
88
- If no API key is saved for the active provider yet, `nvicode` prompts for one on first use.
89
- By default, the proxy paces upstream NVIDIA requests at `40 RPM`. Override that with `NVICODE_MAX_RPM` if your account has a different limit.
90
127
  The usage dashboard compares your local NVIDIA run cost against Claude Opus 4.6 at `$5 / MTok input` and `$25 / MTok output`, based on Anthropic pricing as of `2026-03-30`.
91
128
  If your NVIDIA endpoint is not free, override local cost estimates with `NVICODE_INPUT_USD_PER_MTOK` and `NVICODE_OUTPUT_USD_PER_MTOK`.
92
- Local `usage`, `activity`, and `dashboard` commands are available for NVIDIA proxy sessions. OpenRouter sessions use OpenRouter's direct connection path instead.
93
129
 
94
130
  ## Requirements
95
131
 
96
- - Claude Code must already be installed on the machine.
132
+ - Claude Code must already be installed to use `nvicode launch claude`.
133
+ - Codex must already be installed to use `nvicode launch codex`. Install with `npm install -g @openai/codex`.
134
+ - OpenClaw must already be installed to use `nvicode launch openclaw`. Install with `npm install -g openclaw@latest`.
97
135
  - Node.js 20 or newer is required to install `nvicode`.
136
+ - OpenClaw itself requires Node.js `>=22.14.0`.
98
137
  - On native Windows, Claude Code itself requires Git for Windows. See the [Claude Code setup docs](https://code.claude.com/docs/en/setup).
99
138
 
100
139
  ## Local Development
@@ -112,4 +151,4 @@ npm link
112
151
  - `thinking` is disabled by default because some NVIDIA reasoning models can consume the entire output budget and return no visible answer to Claude Code.
113
152
  - The proxy supports basic text, tool calls, tool results, and token count estimation.
114
153
  - The proxy includes upstream request pacing and retries on NVIDIA `429` responses.
115
- - Claude Code remains the frontend; the selected provider/model becomes the backend.
154
+ - Claude Code, Codex CLI, and OpenClaw remain the frontends; the selected provider/model becomes the backend.
package/dist/cli.js CHANGED
@@ -18,16 +18,17 @@ const usage = () => {
18
18
  console.log(`nvicode
19
19
 
20
20
  Commands:
21
- nvicode select model Guided provider, key, and model selection
22
- nvicode models Show recommended models for the active provider
23
- nvicode auth Save or update the API key for the active provider
24
- nvicode config Show current nvicode config
25
- nvicode usage Show token usage and cost comparison
26
- nvicode activity Show recent request activity
27
- nvicode dashboard Show usage summary and recent activity
28
- nvicode launch claude [...] Launch Claude Code through nvicode
21
+ nvicode select model Guided provider, key, and model selection
22
+ nvicode models Show recommended models for the active provider
23
+ nvicode auth Save or update the API key for the active provider
24
+ nvicode config Show current nvicode config
25
+ nvicode usage Show token usage and cost comparison
26
+ nvicode activity Show recent request activity
27
+ nvicode dashboard Show usage summary and recent activity
28
+ nvicode launch claude [...] Launch Claude Code through nvicode
29
29
  nvicode launch openclaw [...] Launch OpenClaw through nvicode
30
- nvicode serve Run the local proxy in the foreground
30
+ nvicode launch codex [...] Launch Codex through nvicode
31
+ nvicode serve Run the local proxy in the foreground
31
32
  `);
32
33
  };
33
34
  const isWindows = process.platform === "win32";
@@ -48,9 +49,15 @@ const getClaudeCommandNames = () => isWindows ? ["claude.exe", "claude.cmd", "cl
48
49
  const getOpenClawCommandNames = () => isWindows
49
50
  ? ["openclaw.exe", "openclaw.cmd", "openclaw.bat", "openclaw"]
50
51
  : ["openclaw"];
52
+ const getCodexCommandNames = () => isWindows
53
+ ? ["codex.exe", "codex.cmd", "codex.bat", "codex"]
54
+ : ["codex"];
51
55
  const getClaudeNativeNames = () => isWindows
52
56
  ? ["claude-native.exe", "claude-native.cmd", "claude-native.bat", "claude-native"]
53
57
  : ["claude-native"];
58
+ const getCodexNativeNames = () => isWindows
59
+ ? ["codex-native.exe", "codex-native.cmd", "codex-native.bat", "codex-native"]
60
+ : ["codex-native"];
54
61
  const pathExists = async (targetPath) => {
55
62
  try {
56
63
  await fs.access(targetPath, constants.F_OK);
@@ -88,6 +95,22 @@ const renderClaudeWrapper = () => {
88
95
  "",
89
96
  ].join("\n");
90
97
  };
98
+ const renderCodexWrapper = () => {
99
+ if (isWindows) {
100
+ return [
101
+ "@echo off",
102
+ `REM ${NVICODE_WRAPPER_MARKER}`,
103
+ `"${process.execPath}" "${__filename}" launch codex %*`,
104
+ "",
105
+ ].join("\r\n");
106
+ }
107
+ return [
108
+ "#!/bin/sh",
109
+ `# ${NVICODE_WRAPPER_MARKER}`,
110
+ `exec "${process.execPath}" "${__filename}" launch codex "$@"`,
111
+ "",
112
+ ].join("\n");
113
+ };
91
114
  const question = async (prompt) => {
92
115
  const rl = createInterface({
93
116
  input: process.stdin,
@@ -150,6 +173,13 @@ const promptApiKeyUpdate = async (config, provider) => {
150
173
  ? { openrouterApiKey: nextKey, nvidiaApiKey: config.nvidiaApiKey }
151
174
  : { nvidiaApiKey: nextKey, openrouterApiKey: config.openrouterApiKey };
152
175
  };
176
+ const promptRequiredApiKey = async (providerLabel) => {
177
+ const apiKey = await question(`${providerLabel} API key: `);
178
+ if (!apiKey) {
179
+ throw new Error(`${providerLabel} API key is required.`);
180
+ }
181
+ return apiKey;
182
+ };
153
183
  const ensureConfigured = async () => {
154
184
  let config = await loadConfig();
155
185
  let changed = false;
@@ -160,10 +190,8 @@ const ensureConfigured = async () => {
160
190
  if (!process.stdin.isTTY) {
161
191
  throw new Error(`Missing ${providerLabel} API key. Run \`nvicode auth\` first.`);
162
192
  }
163
- const apiKey = await question(`${providerLabel} API key: `);
164
- if (!apiKey) {
165
- throw new Error(`${providerLabel} API key is required.`);
166
- }
193
+ console.error(`Missing ${providerLabel} API key. Enter it now to continue.`);
194
+ const apiKey = await promptRequiredApiKey(providerLabel);
167
195
  config = {
168
196
  ...config,
169
197
  ...(config.provider === "openrouter"
@@ -171,6 +199,7 @@ const ensureConfigured = async () => {
171
199
  : { nvidiaApiKey: apiKey }),
172
200
  };
173
201
  changed = true;
202
+ console.error(`Saved ${providerLabel} API key. Continuing launch.`);
174
203
  }
175
204
  if (!activeModel) {
176
205
  const [first] = await getRecommendedModels(config.provider, getActiveApiKey(config));
@@ -191,16 +220,13 @@ const runAuth = async () => {
191
220
  const config = await loadConfig();
192
221
  const providerLabel = getProviderLabel(config.provider);
193
222
  const currentApiKey = getActiveApiKey(config);
194
- const apiKey = await question(currentApiKey
195
- ? `${providerLabel} API key (leave blank to keep current): `
196
- : `${providerLabel} API key: `);
223
+ const apiKey = currentApiKey
224
+ ? await question(`${providerLabel} API key (leave blank to keep current): `)
225
+ : await promptRequiredApiKey(providerLabel);
197
226
  if (!apiKey && currentApiKey) {
198
227
  console.log(`Kept existing ${providerLabel} API key.`);
199
228
  return;
200
229
  }
201
- if (!apiKey) {
202
- throw new Error(`${providerLabel} API key is required.`);
203
- }
204
230
  await saveConfig({
205
231
  ...config,
206
232
  ...(config.provider === "openrouter"
@@ -565,6 +591,70 @@ const ensurePersistentClaudeRouting = async () => {
565
591
  await writeExecutableTextFile(wrapperPath, wrapperContents);
566
592
  return "installed";
567
593
  };
594
+ const findExistingCodexNativeInDirectory = async (directory) => {
595
+ for (const name of getCodexNativeNames()) {
596
+ const candidate = path.join(directory, name);
597
+ if (await isExecutable(candidate)) {
598
+ return candidate;
599
+ }
600
+ }
601
+ return null;
602
+ };
603
+ const resolvePersistentCodexCommand = async () => {
604
+ for (const name of getCodexCommandNames()) {
605
+ const found = await findExecutableInPath(name);
606
+ if (found) {
607
+ return found;
608
+ }
609
+ }
610
+ return null;
611
+ };
612
+ const getCodexWrapperInstallPaths = async (codexCommandPath) => {
613
+ const directory = path.dirname(codexCommandPath);
614
+ const existingNative = await findExistingCodexNativeInDirectory(directory);
615
+ if (existingNative) {
616
+ return { wrapperPath: codexCommandPath, nativePath: existingNative };
617
+ }
618
+ if (isWindows && path.extname(codexCommandPath).toLowerCase() === ".exe") {
619
+ return {
620
+ wrapperPath: path.join(directory, "codex.cmd"),
621
+ nativePath: path.join(directory, "codex-native.exe"),
622
+ };
623
+ }
624
+ const extension = path.extname(codexCommandPath);
625
+ return {
626
+ wrapperPath: codexCommandPath,
627
+ nativePath: path.join(directory, `codex-native${extension}`),
628
+ };
629
+ };
630
+ const ensurePersistentCodexRouting = async () => {
631
+ const codexCommandPath = await resolvePersistentCodexCommand();
632
+ if (!codexCommandPath) {
633
+ return "skipped";
634
+ }
635
+ if (await isManagedClaudeWrapper(codexCommandPath)) {
636
+ const currentWrapper = await readIfExists(codexCommandPath);
637
+ const wrapperContents = renderCodexWrapper();
638
+ if (currentWrapper === wrapperContents) {
639
+ return "already";
640
+ }
641
+ await writeExecutableTextFile(codexCommandPath, wrapperContents);
642
+ return "updated";
643
+ }
644
+ const { wrapperPath, nativePath } = await getCodexWrapperInstallPaths(codexCommandPath);
645
+ const wrapperContents = renderCodexWrapper();
646
+ if (!(await pathExists(nativePath))) {
647
+ await fs.rename(codexCommandPath, nativePath);
648
+ }
649
+ else if (codexCommandPath !== wrapperPath && await pathExists(wrapperPath)) {
650
+ await fs.rm(wrapperPath, { force: true });
651
+ }
652
+ else if (codexCommandPath === wrapperPath) {
653
+ await fs.rm(wrapperPath, { force: true });
654
+ }
655
+ await writeExecutableTextFile(wrapperPath, wrapperContents);
656
+ return "installed";
657
+ };
568
658
  const resolveClaudeBinary = async () => {
569
659
  for (const name of getClaudeNativeNames()) {
570
660
  const nativeInPath = await findExecutableInPath(name);
@@ -635,6 +725,39 @@ const resolveOpenClawBinary = async () => {
635
725
  }
636
726
  throw new Error("Unable to locate OpenClaw binary. Install OpenClaw first with `npm install -g openclaw@latest`.");
637
727
  };
728
+ const resolveCodexBinary = async () => {
729
+ for (const name of getCodexNativeNames()) {
730
+ const nativeInPath = await findExecutableInPath(name);
731
+ if (nativeInPath) {
732
+ return nativeInPath;
733
+ }
734
+ }
735
+ const homeBinCandidates = isWindows
736
+ ? [
737
+ path.join(os.homedir(), ".local", "bin", "codex-native.exe"),
738
+ path.join(os.homedir(), ".local", "bin", "codex-native"),
739
+ path.join(os.homedir(), ".local", "bin", "codex.exe"),
740
+ path.join(os.homedir(), ".local", "bin", "codex.cmd"),
741
+ path.join(os.homedir(), ".local", "bin", "codex.bat"),
742
+ path.join(os.homedir(), ".local", "bin", "codex"),
743
+ ]
744
+ : [
745
+ path.join(os.homedir(), ".local", "bin", "codex-native"),
746
+ path.join(os.homedir(), ".local", "bin", "codex"),
747
+ ];
748
+ for (const candidate of homeBinCandidates) {
749
+ if (await isExecutable(candidate)) {
750
+ return candidate;
751
+ }
752
+ }
753
+ for (const name of getCodexCommandNames()) {
754
+ const codexInPath = await findExecutableInPath(name);
755
+ if (codexInPath && !(await isManagedClaudeWrapper(codexInPath))) {
756
+ return codexInPath;
757
+ }
758
+ }
759
+ throw new Error("Unable to locate Codex binary. Install Codex first with `npm install -g @openai/codex`.");
760
+ };
638
761
  const findExecutableInPath = async (name) => {
639
762
  const pathEntries = (process.env.PATH || "").split(path.delimiter);
640
763
  for (const entry of pathEntries) {
@@ -776,6 +899,45 @@ const runLaunchOpenClaw = async (args) => {
776
899
  child.on("error", reject);
777
900
  });
778
901
  };
902
+ const runLaunchCodex = async (args) => {
903
+ const config = await ensureConfigured();
904
+ const routingStatus = await ensurePersistentCodexRouting().catch(() => "skipped");
905
+ const activeModel = getActiveModel(config);
906
+ const codexBinary = await resolveCodexBinary();
907
+ await ensureProxyRunning(config);
908
+ const proxyBaseUrl = `http://127.0.0.1:${config.proxyPort}/v1`;
909
+ const env = {
910
+ ...process.env,
911
+ NVICODE_PROXY_TOKEN: config.proxyToken,
912
+ };
913
+ const hasModelFlag = args.includes("--model") || args.includes("-m");
914
+ const codexArgs = [
915
+ "-c", `model_provider="nvicode"`,
916
+ "-c", `model_providers.nvicode.name="nvicode proxy"`,
917
+ "-c", `model_providers.nvicode.base_url="${proxyBaseUrl}"`,
918
+ "-c", `model_providers.nvicode.env_key="NVICODE_PROXY_TOKEN"`,
919
+ "-c", `model_providers.nvicode.wire_api="responses"`,
920
+ "-c", `model_providers.nvicode.supports_websockets=false`,
921
+ ...(hasModelFlag ? [] : ["--model", activeModel]),
922
+ ...args,
923
+ ];
924
+ if (process.stdout.isTTY &&
925
+ (routingStatus === "installed" || routingStatus === "updated")) {
926
+ console.error("nvicode installed persistent `codex` routing. Future plain `codex` launches will use the selected nvicode provider and model.");
927
+ }
928
+ const child = spawnClaudeProcess(codexBinary, codexArgs, env);
929
+ await new Promise((resolve, reject) => {
930
+ child.on("exit", (code, signal) => {
931
+ if (signal) {
932
+ reject(new Error(`Codex exited with signal ${signal}`));
933
+ return;
934
+ }
935
+ process.exitCode = code ?? 0;
936
+ resolve();
937
+ });
938
+ child.on("error", reject);
939
+ });
940
+ };
779
941
  const runLaunchClaude = async (args) => {
780
942
  const config = await ensureConfigured();
781
943
  const routingStatus = await ensurePersistentClaudeRouting().catch(() => "skipped");
@@ -830,15 +992,12 @@ const runLaunchClaude = async (args) => {
830
992
  };
831
993
  const runServe = async () => {
832
994
  const config = await ensureConfigured();
833
- if (config.provider !== "nvidia") {
834
- throw new Error("`nvicode serve` is only available for the NVIDIA provider.");
835
- }
836
995
  const server = createProxyServer(config);
837
996
  await new Promise((resolve, reject) => {
838
997
  server.once("error", reject);
839
998
  server.listen(config.proxyPort, "127.0.0.1", () => resolve());
840
999
  });
841
- console.error(`nvicode proxy listening on http://127.0.0.1:${config.proxyPort} using ${config.nvidiaModel}`);
1000
+ console.error(`nvicode proxy listening on http://127.0.0.1:${config.proxyPort} using ${getActiveModel(config)} (${config.provider})`);
842
1001
  const shutdown = () => {
843
1002
  server.close(() => process.exit(0));
844
1003
  };
@@ -895,7 +1054,11 @@ const main = async () => {
895
1054
  await runLaunchOpenClaw(rest.slice(1));
896
1055
  return;
897
1056
  }
898
- throw new Error("Supported launch targets are `claude` and `openclaw`.");
1057
+ if (rest[0] === "codex") {
1058
+ await runLaunchCodex(rest.slice(1));
1059
+ return;
1060
+ }
1061
+ throw new Error("Supported launch targets are `claude`, `openclaw`, and `codex`.");
899
1062
  }
900
1063
  throw new Error(`Unknown command: ${command}`);
901
1064
  };
package/dist/proxy.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { createServer } from "node:http";
3
3
  import { appendUsageRecord, buildUsageRecord, getPricingSnapshot, } from "./usage.js";
4
+ import { getActiveApiKey, getActiveModel } from "./config.js";
4
5
  const NVIDIA_URL = "https://integrate.api.nvidia.com/v1/chat/completions";
6
+ const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions";
5
7
  const DEFAULT_RETRY_DELAY_MS = 2_000;
6
8
  const MAX_NVIDIA_RETRIES = 3;
7
9
  const sleep = async (ms) => {
@@ -448,6 +450,234 @@ const callNvidia = async (config, scheduleRequest, payload) => {
448
450
  upstream: await scheduleRequest(invoke),
449
451
  };
450
452
  };
453
+ const isInputMessage = (item) => "role" in item &&
454
+ (!("type" in item) || item.type === "message");
455
+ const translateResponsesInputToMessages = (input) => {
456
+ const messages = [];
457
+ let pendingToolCalls = [];
458
+ const flushPendingToolCalls = () => {
459
+ if (pendingToolCalls.length > 0) {
460
+ messages.push({
461
+ role: "assistant",
462
+ content: null,
463
+ tool_calls: pendingToolCalls,
464
+ });
465
+ pendingToolCalls = [];
466
+ }
467
+ };
468
+ for (const item of input) {
469
+ if (isInputMessage(item)) {
470
+ flushPendingToolCalls();
471
+ const role = item.role === "developer" ? "system" : item.role;
472
+ if (typeof item.content === "string") {
473
+ messages.push({ role, content: item.content });
474
+ }
475
+ else {
476
+ const parts = item.content.map((part) => {
477
+ if (part.type === "input_image" && part.image_url) {
478
+ return { type: "image_url", image_url: { url: part.image_url } };
479
+ }
480
+ return { type: "text", text: part.text || "" };
481
+ });
482
+ messages.push({ role, content: parts });
483
+ }
484
+ continue;
485
+ }
486
+ if (item.type === "function_call") {
487
+ pendingToolCalls.push({
488
+ id: item.call_id,
489
+ type: "function",
490
+ function: { name: item.name, arguments: item.arguments },
491
+ });
492
+ continue;
493
+ }
494
+ if (item.type === "function_call_output") {
495
+ flushPendingToolCalls();
496
+ messages.push({
497
+ role: "tool",
498
+ tool_call_id: item.call_id,
499
+ content: item.output,
500
+ });
501
+ }
502
+ }
503
+ flushPendingToolCalls();
504
+ return messages;
505
+ };
506
+ const translateResponsesTools = (tools) => {
507
+ if (!tools || tools.length === 0)
508
+ return undefined;
509
+ return tools
510
+ .filter((t) => t.type === "function")
511
+ .map((t) => ({
512
+ type: "function",
513
+ function: {
514
+ name: t.name,
515
+ description: t.description,
516
+ parameters: t.parameters ?? { type: "object", properties: {} },
517
+ ...(t.strict !== undefined ? { strict: t.strict } : {}),
518
+ },
519
+ }));
520
+ };
521
+ const buildResponsesApiResponse = (upstream, requestModel) => {
522
+ const choice = upstream.choices?.[0];
523
+ const output = [];
524
+ const messageContent = choice?.message?.content;
525
+ const text = typeof messageContent === "string"
526
+ ? messageContent
527
+ : Array.isArray(messageContent)
528
+ ? messageContent
529
+ .map((p) => (typeof p.text === "string" ? p.text : ""))
530
+ .filter(Boolean)
531
+ .join("\n")
532
+ : "";
533
+ if (text.length > 0 ||
534
+ (!choice?.message?.tool_calls?.length &&
535
+ !choice?.message?.reasoning?.trim())) {
536
+ const finalText = text.length > 0
537
+ ? text
538
+ : choice?.message?.reasoning?.trim() || "";
539
+ output.push({
540
+ type: "message",
541
+ id: `msg_${randomUUID()}`,
542
+ role: "assistant",
543
+ status: "completed",
544
+ content: [{ type: "output_text", text: finalText }],
545
+ });
546
+ }
547
+ for (const tc of choice?.message?.tool_calls ?? []) {
548
+ const name = tc.function?.name;
549
+ if (!name)
550
+ continue;
551
+ output.push({
552
+ type: "function_call",
553
+ id: `fc_${randomUUID()}`,
554
+ call_id: tc.id || `call_${randomUUID()}`,
555
+ name,
556
+ arguments: tc.function?.arguments || "{}",
557
+ status: "completed",
558
+ });
559
+ }
560
+ if (output.length === 0) {
561
+ output.push({
562
+ type: "message",
563
+ id: `msg_${randomUUID()}`,
564
+ role: "assistant",
565
+ status: "completed",
566
+ content: [{ type: "output_text", text: "" }],
567
+ });
568
+ }
569
+ return {
570
+ id: upstream.id || `resp_${randomUUID()}`,
571
+ object: "response",
572
+ created_at: Math.floor(Date.now() / 1000),
573
+ status: "completed",
574
+ model: requestModel,
575
+ output,
576
+ usage: {
577
+ input_tokens: upstream.usage?.prompt_tokens ?? 0,
578
+ output_tokens: upstream.usage?.completion_tokens ?? 0,
579
+ total_tokens: (upstream.usage?.prompt_tokens ?? 0) +
580
+ (upstream.usage?.completion_tokens ?? 0),
581
+ },
582
+ };
583
+ };
584
+ const writeResponsesSseEvent = (response, type, payload) => {
585
+ response.write(`data: ${JSON.stringify({ type, ...payload })}\n\n`);
586
+ };
587
+ const streamResponsesApiResponse = (response, apiResponse) => {
588
+ response.writeHead(200, {
589
+ "Content-Type": "text/event-stream",
590
+ "Cache-Control": "no-cache",
591
+ Connection: "keep-alive",
592
+ });
593
+ writeResponsesSseEvent(response, "response.created", {
594
+ response: { ...apiResponse, output: [], status: "in_progress" },
595
+ });
596
+ for (let i = 0; i < apiResponse.output.length; i++) {
597
+ const item = apiResponse.output[i];
598
+ if (item.type === "message") {
599
+ writeResponsesSseEvent(response, "response.output_item.added", {
600
+ output_index: i,
601
+ item: { ...item, content: [], status: "in_progress" },
602
+ });
603
+ for (let j = 0; j < item.content.length; j++) {
604
+ const part = item.content[j];
605
+ writeResponsesSseEvent(response, "response.content_part.added", {
606
+ output_index: i,
607
+ content_index: j,
608
+ part: { type: "output_text", text: "" },
609
+ });
610
+ for (const chunk of chunkText(part.text)) {
611
+ writeResponsesSseEvent(response, "response.output_text.delta", {
612
+ output_index: i,
613
+ content_index: j,
614
+ delta: chunk,
615
+ });
616
+ }
617
+ writeResponsesSseEvent(response, "response.output_text.done", {
618
+ output_index: i,
619
+ content_index: j,
620
+ text: part.text,
621
+ });
622
+ writeResponsesSseEvent(response, "response.content_part.done", {
623
+ output_index: i,
624
+ content_index: j,
625
+ part,
626
+ });
627
+ }
628
+ writeResponsesSseEvent(response, "response.output_item.done", {
629
+ output_index: i,
630
+ item: { ...item, status: "completed" },
631
+ });
632
+ }
633
+ else if (item.type === "function_call") {
634
+ writeResponsesSseEvent(response, "response.output_item.added", {
635
+ output_index: i,
636
+ item: { ...item, arguments: "", status: "in_progress" },
637
+ });
638
+ writeResponsesSseEvent(response, "response.function_call_arguments.delta", { output_index: i, delta: item.arguments });
639
+ writeResponsesSseEvent(response, "response.function_call_arguments.done", { output_index: i, arguments: item.arguments });
640
+ writeResponsesSseEvent(response, "response.output_item.done", {
641
+ output_index: i,
642
+ item: { ...item, status: "completed" },
643
+ });
644
+ }
645
+ }
646
+ writeResponsesSseEvent(response, "response.completed", {
647
+ response: apiResponse,
648
+ });
649
+ response.end();
650
+ };
651
+ const getUpstreamChatUrl = (config) => config.provider === "openrouter" ? OPENROUTER_URL : NVIDIA_URL;
652
+ const callChatCompletions = async (config, scheduleRequest, body) => {
653
+ const upstreamUrl = getUpstreamChatUrl(config);
654
+ const apiKey = getActiveApiKey(config);
655
+ const invoke = async () => {
656
+ for (let attempt = 0; attempt <= MAX_NVIDIA_RETRIES; attempt += 1) {
657
+ const resp = await fetch(upstreamUrl, {
658
+ method: "POST",
659
+ headers: {
660
+ Authorization: `Bearer ${apiKey}`,
661
+ Accept: "application/json",
662
+ "Content-Type": "application/json",
663
+ },
664
+ body: JSON.stringify(body),
665
+ });
666
+ const raw = await resp.text();
667
+ if (resp.ok)
668
+ return JSON.parse(raw);
669
+ if (resp.status === 429 && attempt < MAX_NVIDIA_RETRIES) {
670
+ const retryMs = parseRetryAfterMs(resp.headers.get("retry-after")) ||
671
+ DEFAULT_RETRY_DELAY_MS * 2 ** attempt;
672
+ await sleep(retryMs);
673
+ continue;
674
+ }
675
+ throw new Error(`Upstream API HTTP ${resp.status}: ${raw}`);
676
+ }
677
+ throw new Error("Upstream API retry loop exhausted unexpectedly.");
678
+ };
679
+ return scheduleRequest(invoke);
680
+ };
451
681
  export const createProxyServer = (config) => {
452
682
  const scheduleNvidiaRequest = createRequestScheduler(config.maxRequestsPerMinute);
453
683
  return createServer(async (request, response) => {
@@ -632,6 +862,43 @@ export const createProxyServer = (config) => {
632
862
  throw error;
633
863
  }
634
864
  }
865
+ if (request.method === "POST" && url.pathname === "/v1/responses") {
866
+ const rawBody = await readRequestBody(request);
867
+ const payload = JSON.parse(rawBody);
868
+ const targetModel = payload.model || getActiveModel(config);
869
+ const chatBody = {
870
+ model: targetModel,
871
+ messages: translateResponsesInputToMessages(payload.input),
872
+ max_tokens: payload.max_output_tokens ?? 16_384,
873
+ stream: false,
874
+ };
875
+ if (typeof payload.temperature === "number") {
876
+ chatBody.temperature = payload.temperature;
877
+ }
878
+ if (typeof payload.top_p === "number") {
879
+ chatBody.top_p = payload.top_p;
880
+ }
881
+ const tools = translateResponsesTools(payload.tools);
882
+ if (tools)
883
+ chatBody.tools = tools;
884
+ try {
885
+ const upstream = await callChatCompletions(config, scheduleNvidiaRequest, chatBody);
886
+ const apiResponse = buildResponsesApiResponse(upstream, targetModel);
887
+ if (payload.stream === false) {
888
+ sendJson(response, 200, apiResponse);
889
+ }
890
+ else {
891
+ streamResponsesApiResponse(response, apiResponse);
892
+ }
893
+ }
894
+ catch (err) {
895
+ const msg = err instanceof Error ? err.message : String(err);
896
+ sendJson(response, 502, {
897
+ error: { message: msg, type: "upstream_error", code: "upstream_error" },
898
+ });
899
+ }
900
+ return;
901
+ }
635
902
  sendAnthropicError(response, 404, "not_found_error", `Unsupported route: ${request.method || "GET"} ${url.pathname}`);
636
903
  }
637
904
  catch (error) {
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "nvicode",
3
- "version": "0.1.16",
4
- "description": "NviCode - Introducing one click Nvidia/OpenRouter keys to Claude Code. Free Claude code.",
3
+ "version": "0.1.18",
4
+ "description": "Route Claude Code, Codex CLI, and OpenClaw through NVIDIA or OpenRouter with one setup.",
5
5
  "author": "Dinesh Potla",
6
6
  "keywords": [
7
7
  "claude-code",
8
+ "codex",
9
+ "openclaw",
10
+ "openrouter",
8
11
  "nvidia",
9
12
  "kimi",
10
13
  "qwen",