rebellm-bridge 0.1.0 → 0.1.2
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 +9 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +7 -0
- package/dist/launcher.d.ts +10 -3
- package/dist/launcher.js +49 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ SDKs: OpenAI with `base_url="http://127.0.0.1:7343/v1"`, Anthropic with
|
|
|
55
55
|
As Claude Code's model (its own config directory, your claude.ai login untouched):
|
|
56
56
|
|
|
57
57
|
```
|
|
58
|
-
rebellm-claude
|
|
58
|
+
npx rebellm-bridge claude # nothing to install; rebellm-claude after npm install -g
|
|
59
59
|
rebellm-claude -p "What is 2 + 3?"
|
|
60
60
|
```
|
|
61
61
|
|
|
@@ -65,6 +65,14 @@ and a long request timeout, and passes every other argument to `claude`. Options
|
|
|
65
65
|
Claude Code's first request is ~11k tokens of system prompt and tools, so it needs a tab
|
|
66
66
|
with a 32k context and patience on a slow GPU.
|
|
67
67
|
|
|
68
|
+
On a work machine Claude Code may be pinned to the company's endpoint or cloud provider.
|
|
69
|
+
The launcher blanks `ANTHROPIC_API_KEY`, `ANTHROPIC_CUSTOM_HEADERS` and `CLAUDE_CODE_USE_*`
|
|
70
|
+
from your shell and passes its own variables with `--settings`, which ranks above the
|
|
71
|
+
project's `.claude/settings.json` and your user settings. Managed settings (MDM,
|
|
72
|
+
`managed-settings.json`, the claude.ai admin console) rank above that and only your
|
|
73
|
+
organisation can change them: the launcher names the file when it finds one, and `/status`
|
|
74
|
+
inside `claude` lists the setting sources in force.
|
|
75
|
+
|
|
68
76
|
As a tool inside your normal Claude Code (Claude keeps its own model, can ask yours):
|
|
69
77
|
|
|
70
78
|
```
|
package/dist/cli.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { type BridgeServer } from './server.js';
|
|
|
5
5
|
import type { Health } from './tab.js';
|
|
6
6
|
export declare const VERSION: string;
|
|
7
7
|
export declare const TOKEN_ENV = "REBELLM_BRIDGE_TOKEN";
|
|
8
|
-
export declare const USAGE = "Usage: rebellm-bridge [options]\n\nLets Claude Code (MCP or rebellm-claude) and OpenAI-style clients use the model in your RebeLLM tab.\n\n --port <n> port for the tab and the HTTP API (default 7343)\n --host <addr> address to listen on (default 127.0.0.1; anything else exposes the model)\n --token <t> token the tab must present (default: REBELLM_BRIDGE_TOKEN, else ~/.rebellm-bridge/token)\n --wait <s> seconds a request waits for the tab and its model (default 120)\n --mcp serve MCP over stdio as well (Claude Code starts the bridge this way)\n --version print the version\n --help print this help";
|
|
8
|
+
export declare const USAGE = "Usage: rebellm-bridge [options]\n rebellm-bridge claude [launcher options] [claude arguments]\n\nLets Claude Code (MCP or rebellm-claude) and OpenAI-style clients use the model in your RebeLLM tab.\n\n claude ... run Claude Code on the tab's model, the same as rebellm-claude\n --port <n> port for the tab and the HTTP API (default 7343)\n --host <addr> address to listen on (default 127.0.0.1; anything else exposes the model)\n --token <t> token the tab must present (default: REBELLM_BRIDGE_TOKEN, else ~/.rebellm-bridge/token)\n --wait <s> seconds a request waits for the tab and its model (default 120)\n --mcp serve MCP over stdio as well (Claude Code starts the bridge this way)\n --version print the version\n --help print this help";
|
|
9
9
|
export interface CliOptions {
|
|
10
10
|
port: number;
|
|
11
11
|
host: string;
|
package/dist/cli.js
CHANGED
|
@@ -11,9 +11,11 @@ import { DEFAULT_HOST, DEFAULT_PORT, isLoopback, startServer } from './server.js
|
|
|
11
11
|
export const VERSION = createRequire(import.meta.url)('../package.json').version;
|
|
12
12
|
export const TOKEN_ENV = 'REBELLM_BRIDGE_TOKEN';
|
|
13
13
|
export const USAGE = `Usage: rebellm-bridge [options]
|
|
14
|
+
rebellm-bridge claude [launcher options] [claude arguments]
|
|
14
15
|
|
|
15
16
|
Lets Claude Code (MCP or rebellm-claude) and OpenAI-style clients use the model in your RebeLLM tab.
|
|
16
17
|
|
|
18
|
+
claude ... run Claude Code on the tab's model, the same as rebellm-claude
|
|
17
19
|
--port <n> port for the tab and the HTTP API (default ${DEFAULT_PORT})
|
|
18
20
|
--host <addr> address to listen on (default ${DEFAULT_HOST}; anything else exposes the model)
|
|
19
21
|
--token <t> token the tab must present (default: ${TOKEN_ENV}, else ~/.rebellm-bridge/token)
|
|
@@ -161,6 +163,11 @@ export async function run(o, io) {
|
|
|
161
163
|
}
|
|
162
164
|
/** The command line; resolves with the exit code once the bridge has stopped. */
|
|
163
165
|
export async function main(argv, io) {
|
|
166
|
+
if (argv[0] === 'claude') {
|
|
167
|
+
// Loaded here so the plain bridge never touches the launcher, and the two files can import each other.
|
|
168
|
+
const { launch } = await import('./launcher.js');
|
|
169
|
+
return launch(argv.slice(1), { stderr: io.stderr, env: io.env, ...(io.home ? { home: io.home } : {}) });
|
|
170
|
+
}
|
|
164
171
|
const o = parseCli(argv);
|
|
165
172
|
if ('error' in o) {
|
|
166
173
|
io.stderr.write(`rebellm-bridge: ${o.error}\n\n${USAGE}\n`);
|
package/dist/launcher.d.ts
CHANGED
|
@@ -19,16 +19,23 @@ export declare function parseLauncher(argv: string[]): LauncherOptions | {
|
|
|
19
19
|
export declare function findCommand(name: string, env: NodeJS.ProcessEnv, platform?: NodeJS.Platform): string | null;
|
|
20
20
|
/** What sends every model request of claude to the bridge. */
|
|
21
21
|
export declare function bridgeEnv(base: string, contextTokens?: number): Record<string, string>;
|
|
22
|
-
/** The parent's environment with the bridge's on top; a real
|
|
22
|
+
/** The parent's environment with the bridge's on top; a real key or provider never reaches claude. */
|
|
23
23
|
export declare function childEnv(parent: NodeJS.ProcessEnv, ours: Record<string, string>, platform?: NodeJS.Platform): NodeJS.ProcessEnv;
|
|
24
24
|
export declare const configDir: (home: string) => string;
|
|
25
25
|
export declare const logFile: (home: string) => string;
|
|
26
|
-
/**
|
|
27
|
-
export declare
|
|
26
|
+
/** Given to claude as `--settings`, which ranks above project, local and user settings. */
|
|
27
|
+
export declare const launchSettingsFile: (home: string) => string;
|
|
28
|
+
export declare function writeLaunchSettings(file: string, env: Record<string, string>): void;
|
|
29
|
+
/** Where an organisation's Claude Code settings live; they rank above everything the launcher can do. */
|
|
30
|
+
export declare function managedSettingsFile(platform: NodeJS.Platform, env: NodeJS.ProcessEnv): string;
|
|
31
|
+
/** The keys of a managed settings file that decide where claude sends requests; none without such a file. */
|
|
32
|
+
export declare function managedOverrides(file: string): string[];
|
|
28
33
|
export interface LaunchIo {
|
|
29
34
|
stderr: Writable;
|
|
30
35
|
env: NodeJS.ProcessEnv;
|
|
31
36
|
home?: string;
|
|
37
|
+
/** The managed settings file to look at, for tests. */
|
|
38
|
+
managedFile?: string;
|
|
32
39
|
/** How often to ask the bridge whether the tab is there. */
|
|
33
40
|
pollMs?: number;
|
|
34
41
|
platform?: NodeJS.Platform;
|
package/dist/launcher.js
CHANGED
|
@@ -87,9 +87,19 @@ export function findCommand(name, env, platform = process.platform) {
|
|
|
87
87
|
}
|
|
88
88
|
return null;
|
|
89
89
|
}
|
|
90
|
+
/** What Claude Code ranks above `ANTHROPIC_AUTH_TOKEN` and `ANTHROPIC_BASE_URL`, or would send along. */
|
|
91
|
+
const OUTRANKING = [
|
|
92
|
+
'ANTHROPIC_API_KEY',
|
|
93
|
+
'ANTHROPIC_CUSTOM_HEADERS',
|
|
94
|
+
'CLAUDE_CODE_USE_BEDROCK',
|
|
95
|
+
'CLAUDE_CODE_USE_VERTEX',
|
|
96
|
+
'CLAUDE_CODE_USE_FOUNDRY',
|
|
97
|
+
];
|
|
90
98
|
/** What sends every model request of claude to the bridge. */
|
|
91
99
|
export function bridgeEnv(base, contextTokens) {
|
|
92
100
|
return {
|
|
101
|
+
// Empty counts as unset; a value from a shell profile or a settings file would outrank the bridge.
|
|
102
|
+
...Object.fromEntries(OUTRANKING.map((k) => [k, ''])),
|
|
93
103
|
ANTHROPIC_BASE_URL: base,
|
|
94
104
|
// The bridge checks no key; a Bearer token needs no approval prompt in Claude Code.
|
|
95
105
|
ANTHROPIC_AUTH_TOKEN: 'rebellm-bridge-needs-no-key',
|
|
@@ -103,10 +113,10 @@ export function bridgeEnv(base, contextTokens) {
|
|
|
103
113
|
...(contextTokens ? { CLAUDE_CODE_MAX_CONTEXT_TOKENS: String(contextTokens) } : {}),
|
|
104
114
|
};
|
|
105
115
|
}
|
|
106
|
-
/** The parent's environment with the bridge's on top; a real
|
|
116
|
+
/** The parent's environment with the bridge's on top; a real key or provider never reaches claude. */
|
|
107
117
|
export function childEnv(parent, ours, platform = process.platform) {
|
|
108
118
|
const win = platform === 'win32';
|
|
109
|
-
const drop = new Set(
|
|
119
|
+
const drop = new Set(Object.keys(ours));
|
|
110
120
|
const env = {};
|
|
111
121
|
for (const [k, v] of Object.entries(parent))
|
|
112
122
|
if (!drop.has(win ? k.toUpperCase() : k))
|
|
@@ -115,25 +125,38 @@ export function childEnv(parent, ours, platform = process.platform) {
|
|
|
115
125
|
}
|
|
116
126
|
export const configDir = (home) => join(home, '.rebellm-bridge', 'claude');
|
|
117
127
|
export const logFile = (home) => join(home, '.rebellm-bridge', 'bridge.log');
|
|
128
|
+
/** Given to claude as `--settings`, which ranks above project, local and user settings. */
|
|
129
|
+
export const launchSettingsFile = (home) => join(home, '.rebellm-bridge', 'claude-settings.json');
|
|
118
130
|
const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
131
|
+
export function writeLaunchSettings(file, env) {
|
|
132
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
133
|
+
writeFileSync(file, `${JSON.stringify({ env }, null, 2)}\n`, { mode: 0o600 });
|
|
134
|
+
}
|
|
135
|
+
/** Where an organisation's Claude Code settings live; they rank above everything the launcher can do. */
|
|
136
|
+
export function managedSettingsFile(platform, env) {
|
|
137
|
+
if (platform === 'darwin')
|
|
138
|
+
return '/Library/Application Support/ClaudeCode/managed-settings.json';
|
|
139
|
+
if (platform === 'win32')
|
|
140
|
+
return join(env[envKey(env, 'PROGRAMFILES', true)] ?? 'C:\\Program Files', 'ClaudeCode', 'managed-settings.json');
|
|
141
|
+
return '/etc/claude-code/managed-settings.json';
|
|
142
|
+
}
|
|
143
|
+
const MANAGED_KEYS = ['apiKeyHelper', 'forceLoginMethod', 'forceLoginGatewayUrl'];
|
|
144
|
+
/** The keys of a managed settings file that decide where claude sends requests; none without such a file. */
|
|
145
|
+
export function managedOverrides(file) {
|
|
146
|
+
let settings;
|
|
124
147
|
try {
|
|
125
|
-
|
|
126
|
-
if (!isObj(parsed))
|
|
127
|
-
return `${file} is not a JSON object; left it as it is`;
|
|
128
|
-
settings = parsed;
|
|
148
|
+
settings = JSON.parse(readFileSync(file, 'utf8'));
|
|
129
149
|
}
|
|
130
|
-
catch
|
|
131
|
-
|
|
132
|
-
return `could not read ${file}; left it as it is`;
|
|
150
|
+
catch {
|
|
151
|
+
return [];
|
|
133
152
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
153
|
+
if (!isObj(settings))
|
|
154
|
+
return [];
|
|
155
|
+
const env = isObj(settings.env) ? Object.keys(settings.env) : [];
|
|
156
|
+
return [
|
|
157
|
+
...env.filter((k) => /^(ANTHROPIC_|CLAUDE_CODE_USE_)/.test(k)),
|
|
158
|
+
...MANAGED_KEYS.filter((k) => settings[k] !== undefined),
|
|
159
|
+
];
|
|
137
160
|
}
|
|
138
161
|
const sleep = (ms, signal) => new Promise((resolve) => {
|
|
139
162
|
const t = setTimeout(resolve, ms);
|
|
@@ -212,14 +235,17 @@ export async function launch(argv, io) {
|
|
|
212
235
|
say(`the RebeLLM tab is connected${model}`);
|
|
213
236
|
const dir = o.sharedConfig ? null : configDir(home);
|
|
214
237
|
const ours = bridgeEnv(base, health.contextTokens);
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
238
|
+
const settings = launchSettingsFile(home);
|
|
239
|
+
writeLaunchSettings(settings, ours);
|
|
240
|
+
const managed = io.managedFile ?? managedSettingsFile(platform, io.env);
|
|
241
|
+
const overrides = managedOverrides(managed);
|
|
242
|
+
if (overrides.length)
|
|
243
|
+
say(`your organisation's Claude Code settings in ${managed} set ${overrides.join(', ')}; they rank above ` +
|
|
244
|
+
`rebellm-claude, so claude may still go to your organisation's endpoint (/status in claude lists the sources)`);
|
|
220
245
|
say(`starting ${claude} with ${dir ? `the config in ${dir}` : 'your own Claude Code config'}`);
|
|
221
246
|
const env = childEnv(io.env, { ...ours, ...(dir ? { CLAUDE_CONFIG_DIR: dir } : {}) }, platform);
|
|
222
|
-
|
|
247
|
+
// Ours first: a --settings the user passes comes later and wins, as asked.
|
|
248
|
+
const child = spawn(claude, ['--settings', settings, ...o.args], { stdio: 'inherit', env });
|
|
223
249
|
return await new Promise((resolve) => {
|
|
224
250
|
child.on('error', (e) => {
|
|
225
251
|
say(`could not start ${claude}: ${e.message}`);
|