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 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: Settings → Local bridge → paste the token, switch on, Save. The card reads
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 # interactive
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 → Settings → Local bridge and turn the switch on. It is printed only this once.\n');
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 → Settings → Local bridge)`);
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`);
@@ -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 API key is removed, never sent to the bridge. */
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
- /** Puts `env` into the config dir's `settings.json`, keeping the rest; a warning when it cannot. */
27
- export declare function writeSettings(dir: string, env: Record<string, string>): string | null;
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 local model can take minutes before its first token.
100
- API_TIMEOUT_MS: '600000',
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 API key is removed, never sent to the bridge. */
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(['ANTHROPIC_API_KEY', ...Object.keys(ours)]);
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
- /** Puts `env` into the config dir's `settings.json`, keeping the rest; a warning when it cannot. */
120
- export function writeSettings(dir, env) {
121
- const file = join(dir, 'settings.json');
122
- mkdirSync(dir, { recursive: true, mode: 0o700 });
123
- let settings = {};
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
- const parsed = JSON.parse(readFileSync(file, 'utf8'));
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 (e) {
131
- if (e.code !== 'ENOENT')
132
- return `could not read ${file}; left it as it is`;
159
+ catch {
160
+ return [];
133
161
  }
134
- settings.env = { ...(isObj(settings.env) ? settings.env : {}), ...env };
135
- writeFileSync(file, `${JSON.stringify(settings, null, 2)}\n`);
136
- return null;
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 → Settings → Local bridge, connect to ` +
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
- if (dir) {
216
- const warning = writeSettings(dir, ours);
217
- if (warning)
218
- say(warning);
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
- const child = spawn(claude, o.args, { stdio: 'inherit', env });
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 → Settings → Local bridge, paste the bridge token ' +
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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rebellm-bridge",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "Use the model in your RebeLLM browser tab from the command line, OpenAI/Anthropic clients and Claude Code",
5
5
  "keywords": [
6
6
  "rebellm",