rebellm-bridge 0.1.0 → 0.1.3
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 +13 -3
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +9 -2
- package/dist/launcher.d.ts +16 -3
- package/dist/launcher.js +61 -26
- package/dist/mcp.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ npm install -g rebellm-bridge # or run it once with: npx rebellm-bridge
|
|
|
21
21
|
|
|
22
22
|
1. `rebellm-bridge` — the first start prints a token once and keeps it in
|
|
23
23
|
`~/.rebellm-bridge/token` (`cat` it to see it again, delete it for a new one).
|
|
24
|
-
2. In RebeLLM:
|
|
24
|
+
2. In RebeLLM: Bridge (top of the page) → paste the token, Save, switch on. The sidebar reads
|
|
25
25
|
"Connected to 127.0.0.1:7343". The model must be loaded in that tab.
|
|
26
26
|
3. Check: `curl http://127.0.0.1:7343/health` → `{"tab":true,"state":"ready",...}`.
|
|
27
27
|
|
|
@@ -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
|
|
|
@@ -63,7 +63,17 @@ It reuses a running bridge or starts one, waits for the tab, sets the tab's cont
|
|
|
63
63
|
and a long request timeout, and passes every other argument to `claude`. Options:
|
|
64
64
|
`--claude <path>`, `--shared-config` (use your normal Claude Code config), `--port <n>`.
|
|
65
65
|
Claude Code's first request is ~11k tokens of system prompt and tools, so it needs a tab
|
|
66
|
-
with a 32k context and patience on a slow GPU
|
|
66
|
+
with a 32k context and patience on a slow GPU: the launcher raises Claude Code's request
|
|
67
|
+
and subagent timeouts to hours, so a subagent that shows nothing for twenty minutes is
|
|
68
|
+
prefilling, not stuck (the Bridge log in the tab shows it).
|
|
69
|
+
|
|
70
|
+
On a work machine Claude Code may be pinned to the company's endpoint or cloud provider.
|
|
71
|
+
The launcher blanks `ANTHROPIC_API_KEY`, `ANTHROPIC_CUSTOM_HEADERS` and `CLAUDE_CODE_USE_*`
|
|
72
|
+
from your shell and passes its own variables with `--settings`, which ranks above the
|
|
73
|
+
project's `.claude/settings.json` and your user settings. Managed settings (MDM,
|
|
74
|
+
`managed-settings.json`, the claude.ai admin console) rank above that and only your
|
|
75
|
+
organisation can change them: the launcher names the file when it finds one, and `/status`
|
|
76
|
+
inside `claude` lists the setting sources in force.
|
|
67
77
|
|
|
68
78
|
As a tool inside your normal Claude Code (Claude keeps its own model, can ask yours):
|
|
69
79
|
|
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)
|
|
@@ -115,7 +117,7 @@ export async function run(o, io) {
|
|
|
115
117
|
});
|
|
116
118
|
if (tok.created)
|
|
117
119
|
say(`\nNew bridge token (stored in ${tok.file}):\n\n ${tok.token}\n\n` +
|
|
118
|
-
'Paste it into RebeLLM →
|
|
120
|
+
'Paste it into RebeLLM → Bridge and turn the switch on. It is printed only this once.\n');
|
|
119
121
|
const warning = hostWarning(o.host);
|
|
120
122
|
if (warning)
|
|
121
123
|
log(warning);
|
|
@@ -140,7 +142,7 @@ export async function run(o, io) {
|
|
|
140
142
|
if (server) {
|
|
141
143
|
const where = `${urlHost(o.host)}:${server.port}`;
|
|
142
144
|
log(`${VERSION} listening on ${where}`);
|
|
143
|
-
say(` tab: ws://${where} (RebeLLM →
|
|
145
|
+
say(` tab: ws://${where} (RebeLLM → Bridge)`);
|
|
144
146
|
say(` OpenAI: http://${where}/v1`);
|
|
145
147
|
say(` Claude: http://${where} (Anthropic API; rebellm-claude runs Claude Code on it)`);
|
|
146
148
|
say(` token: ${tok.source === 'flag' ? 'from --token' : tok.source === 'env' ? `from ${TOKEN_ENV}` : `in ${tok.file}`}`);
|
|
@@ -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,29 @@ 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
|
+
/** Claude Code's extra model calls, minutes each on a slow model: the recap on return, prompt suggestions, thinking summaries. */
|
|
29
|
+
export declare const QUIET_SETTINGS: {
|
|
30
|
+
readonly awaySummaryEnabled: false;
|
|
31
|
+
readonly promptSuggestionEnabled: false;
|
|
32
|
+
readonly showThinkingSummaries: false;
|
|
33
|
+
};
|
|
34
|
+
export declare function writeLaunchSettings(file: string, env: Record<string, string>): void;
|
|
35
|
+
/** Where an organisation's Claude Code settings live; they rank above everything the launcher can do. */
|
|
36
|
+
export declare function managedSettingsFile(platform: NodeJS.Platform, env: NodeJS.ProcessEnv): string;
|
|
37
|
+
/** The keys of a managed settings file that decide where claude sends requests; none without such a file. */
|
|
38
|
+
export declare function managedOverrides(file: string): string[];
|
|
28
39
|
export interface LaunchIo {
|
|
29
40
|
stderr: Writable;
|
|
30
41
|
env: NodeJS.ProcessEnv;
|
|
31
42
|
home?: string;
|
|
43
|
+
/** The managed settings file to look at, for tests. */
|
|
44
|
+
managedFile?: string;
|
|
32
45
|
/** How often to ask the bridge whether the tab is there. */
|
|
33
46
|
pollMs?: number;
|
|
34
47
|
platform?: NodeJS.Platform;
|
package/dist/launcher.js
CHANGED
|
@@ -87,26 +87,39 @@ 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',
|
|
96
106
|
ANTHROPIC_MODEL: MODEL,
|
|
97
107
|
ANTHROPIC_SMALL_FAST_MODEL: MODEL,
|
|
98
108
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: MODEL,
|
|
99
|
-
// A
|
|
100
|
-
|
|
109
|
+
// A 32k prefill at 26 tok/s is 21 minutes and a long answer at 1.5 tok/s hours; the
|
|
110
|
+
// bridge's pings keep the idle watchdogs quiet, these keep the request and subagent alive.
|
|
111
|
+
API_TIMEOUT_MS: '21600000',
|
|
112
|
+
CLAUDE_ASYNC_AGENT_STALL_TIMEOUT_MS: '21600000',
|
|
113
|
+
CLAUDE_STREAM_IDLE_TIMEOUT_MS: '1800000',
|
|
101
114
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
|
|
102
115
|
// Claude Code assumes 200k for a model it does not know; it should compact within the tab's.
|
|
103
116
|
...(contextTokens ? { CLAUDE_CODE_MAX_CONTEXT_TOKENS: String(contextTokens) } : {}),
|
|
104
117
|
};
|
|
105
118
|
}
|
|
106
|
-
/** The parent's environment with the bridge's on top; a real
|
|
119
|
+
/** The parent's environment with the bridge's on top; a real key or provider never reaches claude. */
|
|
107
120
|
export function childEnv(parent, ours, platform = process.platform) {
|
|
108
121
|
const win = platform === 'win32';
|
|
109
|
-
const drop = new Set(
|
|
122
|
+
const drop = new Set(Object.keys(ours));
|
|
110
123
|
const env = {};
|
|
111
124
|
for (const [k, v] of Object.entries(parent))
|
|
112
125
|
if (!drop.has(win ? k.toUpperCase() : k))
|
|
@@ -115,25 +128,44 @@ export function childEnv(parent, ours, platform = process.platform) {
|
|
|
115
128
|
}
|
|
116
129
|
export const configDir = (home) => join(home, '.rebellm-bridge', 'claude');
|
|
117
130
|
export const logFile = (home) => join(home, '.rebellm-bridge', 'bridge.log');
|
|
131
|
+
/** Given to claude as `--settings`, which ranks above project, local and user settings. */
|
|
132
|
+
export const launchSettingsFile = (home) => join(home, '.rebellm-bridge', 'claude-settings.json');
|
|
118
133
|
const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
119
|
-
/**
|
|
120
|
-
export
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
134
|
+
/** Claude Code's extra model calls, minutes each on a slow model: the recap on return, prompt suggestions, thinking summaries. */
|
|
135
|
+
export const QUIET_SETTINGS = {
|
|
136
|
+
awaySummaryEnabled: false,
|
|
137
|
+
promptSuggestionEnabled: false,
|
|
138
|
+
showThinkingSummaries: false,
|
|
139
|
+
};
|
|
140
|
+
export function writeLaunchSettings(file, env) {
|
|
141
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
142
|
+
writeFileSync(file, `${JSON.stringify({ ...QUIET_SETTINGS, env }, null, 2)}\n`, { mode: 0o600 });
|
|
143
|
+
}
|
|
144
|
+
/** Where an organisation's Claude Code settings live; they rank above everything the launcher can do. */
|
|
145
|
+
export function managedSettingsFile(platform, env) {
|
|
146
|
+
if (platform === 'darwin')
|
|
147
|
+
return '/Library/Application Support/ClaudeCode/managed-settings.json';
|
|
148
|
+
if (platform === 'win32')
|
|
149
|
+
return join(env[envKey(env, 'PROGRAMFILES', true)] ?? 'C:\\Program Files', 'ClaudeCode', 'managed-settings.json');
|
|
150
|
+
return '/etc/claude-code/managed-settings.json';
|
|
151
|
+
}
|
|
152
|
+
const MANAGED_KEYS = ['apiKeyHelper', 'forceLoginMethod', 'forceLoginGatewayUrl'];
|
|
153
|
+
/** The keys of a managed settings file that decide where claude sends requests; none without such a file. */
|
|
154
|
+
export function managedOverrides(file) {
|
|
155
|
+
let settings;
|
|
124
156
|
try {
|
|
125
|
-
|
|
126
|
-
if (!isObj(parsed))
|
|
127
|
-
return `${file} is not a JSON object; left it as it is`;
|
|
128
|
-
settings = parsed;
|
|
157
|
+
settings = JSON.parse(readFileSync(file, 'utf8'));
|
|
129
158
|
}
|
|
130
|
-
catch
|
|
131
|
-
|
|
132
|
-
return `could not read ${file}; left it as it is`;
|
|
159
|
+
catch {
|
|
160
|
+
return [];
|
|
133
161
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
162
|
+
if (!isObj(settings))
|
|
163
|
+
return [];
|
|
164
|
+
const env = isObj(settings.env) ? Object.keys(settings.env) : [];
|
|
165
|
+
return [
|
|
166
|
+
...env.filter((k) => /^(ANTHROPIC_|CLAUDE_CODE_USE_)/.test(k)),
|
|
167
|
+
...MANAGED_KEYS.filter((k) => settings[k] !== undefined),
|
|
168
|
+
];
|
|
137
169
|
}
|
|
138
170
|
const sleep = (ms, signal) => new Promise((resolve) => {
|
|
139
171
|
const t = setTimeout(resolve, ms);
|
|
@@ -199,7 +231,7 @@ export async function launch(argv, io) {
|
|
|
199
231
|
}
|
|
200
232
|
}
|
|
201
233
|
if (!health?.tab) {
|
|
202
|
-
say(`waiting for the RebeLLM tab. In RebeLLM →
|
|
234
|
+
say(`waiting for the RebeLLM tab. In RebeLLM → Bridge, connect to ` +
|
|
203
235
|
`${base.replace('http:', 'ws:')} with ${token}\nCtrl+C quits.`);
|
|
204
236
|
while (!health?.tab) {
|
|
205
237
|
if (quit.signal.aborted)
|
|
@@ -212,14 +244,17 @@ export async function launch(argv, io) {
|
|
|
212
244
|
say(`the RebeLLM tab is connected${model}`);
|
|
213
245
|
const dir = o.sharedConfig ? null : configDir(home);
|
|
214
246
|
const ours = bridgeEnv(base, health.contextTokens);
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
247
|
+
const settings = launchSettingsFile(home);
|
|
248
|
+
writeLaunchSettings(settings, ours);
|
|
249
|
+
const managed = io.managedFile ?? managedSettingsFile(platform, io.env);
|
|
250
|
+
const overrides = managedOverrides(managed);
|
|
251
|
+
if (overrides.length)
|
|
252
|
+
say(`your organisation's Claude Code settings in ${managed} set ${overrides.join(', ')}; they rank above ` +
|
|
253
|
+
`rebellm-claude, so claude may still go to your organisation's endpoint (/status in claude lists the sources)`);
|
|
220
254
|
say(`starting ${claude} with ${dir ? `the config in ${dir}` : 'your own Claude Code config'}`);
|
|
221
255
|
const env = childEnv(io.env, { ...ours, ...(dir ? { CLAUDE_CONFIG_DIR: dir } : {}) }, platform);
|
|
222
|
-
|
|
256
|
+
// Ours first: a --settings the user passes comes later and wins, as asked.
|
|
257
|
+
const child = spawn(claude, ['--settings', settings, ...o.args], { stdio: 'inherit', env });
|
|
223
258
|
return await new Promise((resolve) => {
|
|
224
259
|
child.on('error', (e) => {
|
|
225
260
|
say(`could not start ${claude}: ${e.message}`);
|
package/dist/mcp.js
CHANGED
|
@@ -92,7 +92,7 @@ export function httpBackend(base) {
|
|
|
92
92
|
/** The `status` tool's text; never the token, which would end up in the client's context. */
|
|
93
93
|
export function describeHealth(h) {
|
|
94
94
|
if (!h.tab)
|
|
95
|
-
return ('No RebeLLM tab is connected. In RebeLLM →
|
|
95
|
+
return ('No RebeLLM tab is connected. In RebeLLM → Bridge, paste the bridge token ' +
|
|
96
96
|
'(printed at its first start, stored in ~/.rebellm-bridge/token) and turn the switch on.');
|
|
97
97
|
const tab = `A RebeLLM tab is connected${h.app ? ` (app ${h.app})` : ''}.`;
|
|
98
98
|
const model = h.model ? `The model ${h.model}` : 'The model';
|