nvicode 0.1.15 → 0.1.17
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 +65 -26
- package/dist/cli.js +175 -17
- package/dist/proxy.js +267 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# NviCode
|
|
1
|
+
# NviCode
|
|
2
2
|
|
|
3
3
|
[](https://github.com/dineshpotla/nvicode/actions/workflows/ci.yml)
|
|
4
4
|
[](https://www.npmjs.com/package/nvicode)
|
|
@@ -6,9 +6,17 @@
|
|
|
6
6
|
[](https://nodejs.org/)
|
|
7
7
|
[](./LICENSE)
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
29
|
+
Install `nvicode`:
|
|
22
30
|
|
|
23
31
|
```sh
|
|
24
32
|
npm install -g nvicode
|
|
25
33
|
```
|
|
26
34
|
|
|
27
|
-
|
|
35
|
+
Choose provider, key, and model:
|
|
28
36
|
|
|
29
37
|
```sh
|
|
30
38
|
nvicode select model
|
|
31
39
|
```
|
|
32
40
|
|
|
33
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|

|
|
62
77
|
|
|
63
|
-
### Launch
|
|
78
|
+
### Launch through your selected provider
|
|
64
79
|
|
|
65
80
|

|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
-
|
|
83
|
-
-
|
|
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
|
|
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
|
|
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
|
|
22
|
-
nvicode models
|
|
23
|
-
nvicode auth
|
|
24
|
-
nvicode config
|
|
25
|
-
nvicode usage
|
|
26
|
-
nvicode activity
|
|
27
|
-
nvicode dashboard
|
|
28
|
-
nvicode launch claude [...]
|
|
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
|
|
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,
|
|
@@ -565,6 +588,70 @@ const ensurePersistentClaudeRouting = async () => {
|
|
|
565
588
|
await writeExecutableTextFile(wrapperPath, wrapperContents);
|
|
566
589
|
return "installed";
|
|
567
590
|
};
|
|
591
|
+
const findExistingCodexNativeInDirectory = async (directory) => {
|
|
592
|
+
for (const name of getCodexNativeNames()) {
|
|
593
|
+
const candidate = path.join(directory, name);
|
|
594
|
+
if (await isExecutable(candidate)) {
|
|
595
|
+
return candidate;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return null;
|
|
599
|
+
};
|
|
600
|
+
const resolvePersistentCodexCommand = async () => {
|
|
601
|
+
for (const name of getCodexCommandNames()) {
|
|
602
|
+
const found = await findExecutableInPath(name);
|
|
603
|
+
if (found) {
|
|
604
|
+
return found;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return null;
|
|
608
|
+
};
|
|
609
|
+
const getCodexWrapperInstallPaths = async (codexCommandPath) => {
|
|
610
|
+
const directory = path.dirname(codexCommandPath);
|
|
611
|
+
const existingNative = await findExistingCodexNativeInDirectory(directory);
|
|
612
|
+
if (existingNative) {
|
|
613
|
+
return { wrapperPath: codexCommandPath, nativePath: existingNative };
|
|
614
|
+
}
|
|
615
|
+
if (isWindows && path.extname(codexCommandPath).toLowerCase() === ".exe") {
|
|
616
|
+
return {
|
|
617
|
+
wrapperPath: path.join(directory, "codex.cmd"),
|
|
618
|
+
nativePath: path.join(directory, "codex-native.exe"),
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
const extension = path.extname(codexCommandPath);
|
|
622
|
+
return {
|
|
623
|
+
wrapperPath: codexCommandPath,
|
|
624
|
+
nativePath: path.join(directory, `codex-native${extension}`),
|
|
625
|
+
};
|
|
626
|
+
};
|
|
627
|
+
const ensurePersistentCodexRouting = async () => {
|
|
628
|
+
const codexCommandPath = await resolvePersistentCodexCommand();
|
|
629
|
+
if (!codexCommandPath) {
|
|
630
|
+
return "skipped";
|
|
631
|
+
}
|
|
632
|
+
if (await isManagedClaudeWrapper(codexCommandPath)) {
|
|
633
|
+
const currentWrapper = await readIfExists(codexCommandPath);
|
|
634
|
+
const wrapperContents = renderCodexWrapper();
|
|
635
|
+
if (currentWrapper === wrapperContents) {
|
|
636
|
+
return "already";
|
|
637
|
+
}
|
|
638
|
+
await writeExecutableTextFile(codexCommandPath, wrapperContents);
|
|
639
|
+
return "updated";
|
|
640
|
+
}
|
|
641
|
+
const { wrapperPath, nativePath } = await getCodexWrapperInstallPaths(codexCommandPath);
|
|
642
|
+
const wrapperContents = renderCodexWrapper();
|
|
643
|
+
if (!(await pathExists(nativePath))) {
|
|
644
|
+
await fs.rename(codexCommandPath, nativePath);
|
|
645
|
+
}
|
|
646
|
+
else if (codexCommandPath !== wrapperPath && await pathExists(wrapperPath)) {
|
|
647
|
+
await fs.rm(wrapperPath, { force: true });
|
|
648
|
+
}
|
|
649
|
+
else if (codexCommandPath === wrapperPath) {
|
|
650
|
+
await fs.rm(wrapperPath, { force: true });
|
|
651
|
+
}
|
|
652
|
+
await writeExecutableTextFile(wrapperPath, wrapperContents);
|
|
653
|
+
return "installed";
|
|
654
|
+
};
|
|
568
655
|
const resolveClaudeBinary = async () => {
|
|
569
656
|
for (const name of getClaudeNativeNames()) {
|
|
570
657
|
const nativeInPath = await findExecutableInPath(name);
|
|
@@ -635,6 +722,39 @@ const resolveOpenClawBinary = async () => {
|
|
|
635
722
|
}
|
|
636
723
|
throw new Error("Unable to locate OpenClaw binary. Install OpenClaw first with `npm install -g openclaw@latest`.");
|
|
637
724
|
};
|
|
725
|
+
const resolveCodexBinary = async () => {
|
|
726
|
+
for (const name of getCodexNativeNames()) {
|
|
727
|
+
const nativeInPath = await findExecutableInPath(name);
|
|
728
|
+
if (nativeInPath) {
|
|
729
|
+
return nativeInPath;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
const homeBinCandidates = isWindows
|
|
733
|
+
? [
|
|
734
|
+
path.join(os.homedir(), ".local", "bin", "codex-native.exe"),
|
|
735
|
+
path.join(os.homedir(), ".local", "bin", "codex-native"),
|
|
736
|
+
path.join(os.homedir(), ".local", "bin", "codex.exe"),
|
|
737
|
+
path.join(os.homedir(), ".local", "bin", "codex.cmd"),
|
|
738
|
+
path.join(os.homedir(), ".local", "bin", "codex.bat"),
|
|
739
|
+
path.join(os.homedir(), ".local", "bin", "codex"),
|
|
740
|
+
]
|
|
741
|
+
: [
|
|
742
|
+
path.join(os.homedir(), ".local", "bin", "codex-native"),
|
|
743
|
+
path.join(os.homedir(), ".local", "bin", "codex"),
|
|
744
|
+
];
|
|
745
|
+
for (const candidate of homeBinCandidates) {
|
|
746
|
+
if (await isExecutable(candidate)) {
|
|
747
|
+
return candidate;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
for (const name of getCodexCommandNames()) {
|
|
751
|
+
const codexInPath = await findExecutableInPath(name);
|
|
752
|
+
if (codexInPath && !(await isManagedClaudeWrapper(codexInPath))) {
|
|
753
|
+
return codexInPath;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
throw new Error("Unable to locate Codex binary. Install Codex first with `npm install -g @openai/codex`.");
|
|
757
|
+
};
|
|
638
758
|
const findExecutableInPath = async (name) => {
|
|
639
759
|
const pathEntries = (process.env.PATH || "").split(path.delimiter);
|
|
640
760
|
for (const entry of pathEntries) {
|
|
@@ -714,8 +834,6 @@ const ensureOpenClawConfigured = async (openclawBinary, config, activeApiKey) =>
|
|
|
714
834
|
"advanced",
|
|
715
835
|
"--auth-choice",
|
|
716
836
|
"custom-api-key",
|
|
717
|
-
"--secret-input-mode",
|
|
718
|
-
"ref",
|
|
719
837
|
"--custom-provider-id",
|
|
720
838
|
providerId,
|
|
721
839
|
"--custom-compatibility",
|
|
@@ -763,7 +881,7 @@ const runLaunchOpenClaw = async (args) => {
|
|
|
763
881
|
const openclawBinary = await resolveOpenClawBinary();
|
|
764
882
|
const { env, updated } = await ensureOpenClawConfigured(openclawBinary, config, activeApiKey);
|
|
765
883
|
if (updated) {
|
|
766
|
-
console.error("nvicode updated the default OpenClaw config. Restart the OpenClaw gateway to apply it: `openclaw gateway restart
|
|
884
|
+
console.error("nvicode updated the default OpenClaw config. Restart the OpenClaw gateway to apply it: `openclaw gateway restart` (or `openclaw gateway run` for a foreground test).");
|
|
767
885
|
}
|
|
768
886
|
const child = spawnClaudeProcess(openclawBinary, args, env);
|
|
769
887
|
await new Promise((resolve, reject) => {
|
|
@@ -778,6 +896,45 @@ const runLaunchOpenClaw = async (args) => {
|
|
|
778
896
|
child.on("error", reject);
|
|
779
897
|
});
|
|
780
898
|
};
|
|
899
|
+
const runLaunchCodex = async (args) => {
|
|
900
|
+
const config = await ensureConfigured();
|
|
901
|
+
const routingStatus = await ensurePersistentCodexRouting().catch(() => "skipped");
|
|
902
|
+
const activeModel = getActiveModel(config);
|
|
903
|
+
const codexBinary = await resolveCodexBinary();
|
|
904
|
+
await ensureProxyRunning(config);
|
|
905
|
+
const proxyBaseUrl = `http://127.0.0.1:${config.proxyPort}/v1`;
|
|
906
|
+
const env = {
|
|
907
|
+
...process.env,
|
|
908
|
+
NVICODE_PROXY_TOKEN: config.proxyToken,
|
|
909
|
+
};
|
|
910
|
+
const hasModelFlag = args.includes("--model") || args.includes("-m");
|
|
911
|
+
const codexArgs = [
|
|
912
|
+
"-c", `model_provider="nvicode"`,
|
|
913
|
+
"-c", `model_providers.nvicode.name="nvicode proxy"`,
|
|
914
|
+
"-c", `model_providers.nvicode.base_url="${proxyBaseUrl}"`,
|
|
915
|
+
"-c", `model_providers.nvicode.env_key="NVICODE_PROXY_TOKEN"`,
|
|
916
|
+
"-c", `model_providers.nvicode.wire_api="responses"`,
|
|
917
|
+
"-c", `model_providers.nvicode.supports_websockets=false`,
|
|
918
|
+
...(hasModelFlag ? [] : ["--model", activeModel]),
|
|
919
|
+
...args,
|
|
920
|
+
];
|
|
921
|
+
if (process.stdout.isTTY &&
|
|
922
|
+
(routingStatus === "installed" || routingStatus === "updated")) {
|
|
923
|
+
console.error("nvicode installed persistent `codex` routing. Future plain `codex` launches will use the selected nvicode provider and model.");
|
|
924
|
+
}
|
|
925
|
+
const child = spawnClaudeProcess(codexBinary, codexArgs, env);
|
|
926
|
+
await new Promise((resolve, reject) => {
|
|
927
|
+
child.on("exit", (code, signal) => {
|
|
928
|
+
if (signal) {
|
|
929
|
+
reject(new Error(`Codex exited with signal ${signal}`));
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
process.exitCode = code ?? 0;
|
|
933
|
+
resolve();
|
|
934
|
+
});
|
|
935
|
+
child.on("error", reject);
|
|
936
|
+
});
|
|
937
|
+
};
|
|
781
938
|
const runLaunchClaude = async (args) => {
|
|
782
939
|
const config = await ensureConfigured();
|
|
783
940
|
const routingStatus = await ensurePersistentClaudeRouting().catch(() => "skipped");
|
|
@@ -832,15 +989,12 @@ const runLaunchClaude = async (args) => {
|
|
|
832
989
|
};
|
|
833
990
|
const runServe = async () => {
|
|
834
991
|
const config = await ensureConfigured();
|
|
835
|
-
if (config.provider !== "nvidia") {
|
|
836
|
-
throw new Error("`nvicode serve` is only available for the NVIDIA provider.");
|
|
837
|
-
}
|
|
838
992
|
const server = createProxyServer(config);
|
|
839
993
|
await new Promise((resolve, reject) => {
|
|
840
994
|
server.once("error", reject);
|
|
841
995
|
server.listen(config.proxyPort, "127.0.0.1", () => resolve());
|
|
842
996
|
});
|
|
843
|
-
console.error(`nvicode proxy listening on http://127.0.0.1:${config.proxyPort} using ${config.
|
|
997
|
+
console.error(`nvicode proxy listening on http://127.0.0.1:${config.proxyPort} using ${getActiveModel(config)} (${config.provider})`);
|
|
844
998
|
const shutdown = () => {
|
|
845
999
|
server.close(() => process.exit(0));
|
|
846
1000
|
};
|
|
@@ -897,7 +1051,11 @@ const main = async () => {
|
|
|
897
1051
|
await runLaunchOpenClaw(rest.slice(1));
|
|
898
1052
|
return;
|
|
899
1053
|
}
|
|
900
|
-
|
|
1054
|
+
if (rest[0] === "codex") {
|
|
1055
|
+
await runLaunchCodex(rest.slice(1));
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
throw new Error("Supported launch targets are `claude`, `openclaw`, and `codex`.");
|
|
901
1059
|
}
|
|
902
1060
|
throw new Error(`Unknown command: ${command}`);
|
|
903
1061
|
};
|
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.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.17",
|
|
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",
|