@jacobbd/relay-ai 0.4.5 → 0.4.7

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
@@ -186,6 +186,7 @@ relay-ai ui
186
186
  Opens a browser-based dashboard on a random local port. From the UI you can:
187
187
 
188
188
  - **Launch any supported tool** — app cards for Claude Code CLI, Codex CLI, Gemini CLI, Antigravity CLI, Antigravity App, Antigravity IDE, Claude Code Desktop, and the ChatGPT Desktop app (Codex mode). Select a provider and model in the card, then click **Launch** — a native terminal opens with the selection pre-wired. No second picker in the terminal.
189
+ - **Keep Claude Code's Anthropic login** — on the Claude Code CLI card, check **Keep my Anthropic login and add Relay models** to keep your normal Claude models while adding the selected Relay model and compatible favorites. This option is not shown for Claude Desktop.
189
190
  - **Manage General Favorites** — the sidebar shows your saved favorite models with a slot indicator (Slots used X/20). Favorites launch through all supported agents.
190
191
  - **Manage Antigravity Favorites** — separate favorites panel for Antigravity sessions.
191
192
  - **Manage providers** — add providers from templates, delete providers, and refresh model lists inline, all without leaving the browser.
@@ -201,6 +202,18 @@ relay-ai claude
201
202
 
202
203
  First run: pick a provider from your registry (or complete the inline setup wizard). If you've added OpenCode Zen/Go, those appear alongside registry providers like Groq, Nvidia, or DeepSeek.
203
204
 
205
+ After you choose a compatible Relay model, the wizard asks whether you want to keep your normal Claude models available too. Choose **Yes** to use the Relay model alongside your existing Anthropic login, or **No** for the existing Relay-only launch.
206
+
207
+ To skip that question, use `--http-proxy`. A selected model does not need to be a favorite:
208
+
209
+ ```bash
210
+ relay-ai claude --http-proxy --provider moonshot --model kimi-k3
211
+ ```
212
+
213
+ Using `--http-proxy` without a provider/model keeps native Claude as the starting model and adds any compatible saved favorites. Relay models cannot be inserted into Claude Code's built-in model picker, so the terminal prints the exact `/model relay:<provider>:<model>` commands you can use to switch. The temporary local connection is password-protected, uses a per-session certificate, and closes automatically when Claude Code exits. Existing non-local network or corporate proxy settings are rejected with a clear error because proxy chaining is not supported yet.
214
+
215
+ > **Compatibility:** This mixed Anthropic + Relay mode currently supports Claude Code connected directly to Anthropic. It does not preserve Google Vertex AI configuration. Vertex users should continue using their normal Claude Code launch or Relay-only mode.
216
+
204
217
  #### Favorite models and mid-session switching
205
218
 
206
219
  Save the models you bounce between:
@@ -213,6 +226,8 @@ Add up to 20 favorites from Zen, Go, or any OpenCode-configured provider. When y
213
226
 
214
227
  No favorites? Launch works like before: single model, no switch menu. `--dry-run` ignores saved favorites so you can preview a single-model launch.
215
228
 
229
+ That built-in favorites catalog applies to **Relay-only mode** (choose **No** when asked whether to keep your normal Claude models). In mixed Anthropic + Relay mode, compatible favorites are available through the printed `/model relay:<provider>:<model>` commands instead; Claude Code cannot add them to its built-in picker.
230
+
216
231
  #### `relay-ai claude` options
217
232
 
218
233
  | Flag | Description |
@@ -220,6 +235,7 @@ No favorites? Launch works like before: single model, no switch menu. `--dry-run
220
235
  | `--dry-run` | Run the full wizard but preview the launch command instead of executing |
221
236
  | `--setup` | Reminder to use `relay-ai providers` for provider setup |
222
237
  | `--trace` | Write debug logs to `~/.relay-ai/logs/` and show errors on exit |
238
+ | `--http-proxy` | Keep the normal Anthropic login/models and add the selected model plus compatible favorites |
223
239
  | `--help` | Show command help |
224
240
  | `--version` | Show version |
225
241
 
@@ -227,6 +243,7 @@ No favorites? Launch works like before: single model, no switch menu. `--dry-run
227
243
  relay-ai claude --dry-run
228
244
  relay-ai claude --setup
229
245
  relay-ai claude --trace
246
+ relay-ai claude --http-proxy --provider moonshot --model kimi-k3
230
247
  ```
231
248
 
232
249
  Claude Code flags and session IDs pass through unchanged:
@@ -491,6 +508,8 @@ When you launch, relay-ai builds a clean child environment:
491
508
 
492
509
  When Claude Code exits (normal exit, Ctrl+C, terminal close), your shell is unchanged. No cleanup step. No restore needed.
493
510
 
511
+ In `--http-proxy` mode, relay-ai does not replace your Anthropic credential. It removes stale third-party endpoint/cloud overrides from the child process, sets a password-protected loopback proxy plus a temporary CA bundle, and forwards native Anthropic requests—including their original auth and body—unchanged. Only the selected Relay model and compatible favorites are allowed to use registry provider credentials.
512
+
494
513
  **Caveat: Claude Code persists the model.** relay-ai doesn't edit `~/.claude/settings.json`, but Claude Code saves the model you launched with (via `--model` and `ANTHROPIC_MODEL`). A later bare `claude` launch may still show that model, e.g. `anthropic-opencode-go__deepseek-v4-flash` from a prior relay-ai session. To get back to a first-party default, run `claude --model sonnet` (or your preferred Claude model), or remove the `"model"` key from `~/.claude/settings.json`. If you used the favorites switch menu, Claude Code may also cache the gateway catalog at `~/.claude/cache/gateway-models.json`. Delete that file if `/model` shows stale entries from a dead proxy.
495
514
 
496
515
  ### Model compatibility
@@ -570,7 +589,9 @@ The deprecated `OPENCODE_STARTER_HOME` env var still works as a fallback for `RE
570
589
 
571
590
  ## Contributing
572
591
 
573
- Private beta right now. Issues and PRs welcome on GitHub.
592
+ Private beta right now. Issues and focused PRs are welcome on GitHub.
593
+
594
+ Please read [CONTRIBUTING.md](CONTRIBUTING.md) before starting. Submit one feature or fix per PR, and discuss large or security-sensitive changes with a maintainer first.
574
595
 
575
596
  ## Support
576
597
 
@@ -11,7 +11,7 @@ import { join } from "path";
11
11
  // package.json
12
12
  var package_default = {
13
13
  name: "@jacobbd/relay-ai",
14
- version: "0.4.5",
14
+ version: "0.4.7",
15
15
  publishConfig: {
16
16
  access: "public"
17
17
  },
@@ -77,6 +77,7 @@ var package_default = {
77
77
  ai: "^6.0.197",
78
78
  "gitlab-ai-provider": "^6.8.0",
79
79
  "ipaddr.js": "^2.4.0",
80
+ "node-forge": "^1.4.0",
80
81
  open: "^11.0.0",
81
82
  picocolors: "^1.1.1",
82
83
  "smol-toml": "^1.6.1",
@@ -86,6 +87,7 @@ var package_default = {
86
87
  },
87
88
  devDependencies: {
88
89
  "@types/node": "^22.0.0",
90
+ "@types/node-forge": "^1.3.14",
89
91
  "@types/ws": "^8.18.1",
90
92
  "@vitest/coverage-v8": "^2.1.9",
91
93
  tsup: "^8.0.0",
@@ -1677,6 +1679,7 @@ function loadPreferences() {
1677
1679
  lastGeminiModel: config.lastGeminiModel,
1678
1680
  lastAntigravityProvider: config.lastAntigravityProvider,
1679
1681
  lastAntigravityModel: config.lastAntigravityModel,
1682
+ lastClaudeTransparentMode: config.lastClaudeTransparentMode,
1680
1683
  recentModelsByProvider: config.recentModelsByProvider,
1681
1684
  favoriteModels: config.favoriteModels,
1682
1685
  antigravityCliFavoriteModels: config.antigravityCliFavoriteModels,
@@ -1697,6 +1700,7 @@ function savePreferences(prefs) {
1697
1700
  if (prefs.lastGeminiModel !== void 0) config.lastGeminiModel = prefs.lastGeminiModel;
1698
1701
  if (prefs.lastAntigravityProvider !== void 0) config.lastAntigravityProvider = prefs.lastAntigravityProvider;
1699
1702
  if (prefs.lastAntigravityModel !== void 0) config.lastAntigravityModel = prefs.lastAntigravityModel;
1703
+ if (prefs.lastClaudeTransparentMode !== void 0) config.lastClaudeTransparentMode = prefs.lastClaudeTransparentMode;
1700
1704
  if (prefs.recentModelsByProvider !== void 0) config.recentModelsByProvider = prefs.recentModelsByProvider;
1701
1705
  if (prefs.favoriteModels !== void 0) config.favoriteModels = prefs.favoriteModels;
1702
1706
  if (prefs.antigravityCliFavoriteModels !== void 0) config.antigravityCliFavoriteModels = prefs.antigravityCliFavoriteModels;
@@ -1909,7 +1913,7 @@ function getInstalledClaudeVersion() {
1909
1913
  return "2.1.183";
1910
1914
  }
1911
1915
  function buildClaudeArgs(model, extraArgs) {
1912
- return ["--model", model, ...extraArgs];
1916
+ return model ? ["--model", model, ...extraArgs] : [...extraArgs];
1913
1917
  }
1914
1918
  function launchClaude(env, model, extraArgs) {
1915
1919
  return new Promise((resolve) => {
@@ -1947,14 +1951,24 @@ function launchClaude(env, model, extraArgs) {
1947
1951
  const forward = (signal) => {
1948
1952
  child.kill(signal);
1949
1953
  };
1950
- process.once("SIGINT", () => forward("SIGINT"));
1951
- process.once("SIGTERM", () => forward("SIGTERM"));
1952
- child.on("exit", (code) => {
1954
+ const onSigint = () => forward("SIGINT");
1955
+ const onSigterm = () => forward("SIGTERM");
1956
+ const onSighup = () => forward("SIGHUP");
1957
+ const cleanup = () => {
1953
1958
  restore();
1959
+ process.off("SIGINT", onSigint);
1960
+ process.off("SIGTERM", onSigterm);
1961
+ process.off("SIGHUP", onSighup);
1962
+ };
1963
+ process.once("SIGINT", onSigint);
1964
+ process.once("SIGTERM", onSigterm);
1965
+ process.once("SIGHUP", onSighup);
1966
+ child.on("exit", (code) => {
1967
+ cleanup();
1954
1968
  resolve(code ?? 0);
1955
1969
  });
1956
1970
  child.on("error", (err) => {
1957
- restore();
1971
+ cleanup();
1958
1972
  resolve(1);
1959
1973
  });
1960
1974
  });
@@ -5814,6 +5828,54 @@ function startProxy(completionsUrl, modelId, debug = false, contextWindow, sdk,
5814
5828
  }], clientModelId, debug);
5815
5829
  }
5816
5830
 
5831
+ // src/catalog.ts
5832
+ function localModelToRoute(lp, model) {
5833
+ if (model.modelFormat === "anthropic" && !model.baseUrl) return null;
5834
+ if (model.modelFormat === "openai" && !isSdkMigratedNpm(model.npm) && !model.completionsUrl) return null;
5835
+ const upstreamUrl = model.modelFormat === "cloud-code" ? model.baseUrl ?? ANTIGRAVITY_BASE_URLS[0] : model.modelFormat === "anthropic" ? model.baseUrl : model.completionsUrl;
5836
+ return {
5837
+ aliasId: claudeCodeClientModelId(aliasModelId(model.id, lp.id), model.contextWindow),
5838
+ realModelId: model.upstreamModelId,
5839
+ displayName: `${model.name || model.id} (${lp.name})`,
5840
+ upstreamUrl: upstreamUrl ?? "",
5841
+ apiKey: lp.apiKey,
5842
+ modelFormat: model.modelFormat,
5843
+ contextWindow: model.contextWindow,
5844
+ npm: model.npm,
5845
+ baseURL: model.apiBaseUrl,
5846
+ providerId: lp.id,
5847
+ authType: lp.authType,
5848
+ oauthAccountId: lp.oauthAccountId,
5849
+ providerData: lp.providerData,
5850
+ headers: lp.headers,
5851
+ supportedParameters: model.supportedParameters,
5852
+ reasoning: model.reasoning,
5853
+ interleavedReasoningField: model.interleavedReasoningField,
5854
+ useResponsesLite: model.useResponsesLite,
5855
+ preferWebSockets: model.preferWebSockets
5856
+ };
5857
+ }
5858
+ function makeRouteResolver(localProviders) {
5859
+ return (providerId, modelId) => {
5860
+ const provider = localProviders?.find((lp) => lp.id === providerId);
5861
+ const model = provider?.models.find((m) => m.id === modelId);
5862
+ return provider && model ? localModelToRoute(provider, model) ?? void 0 : void 0;
5863
+ };
5864
+ }
5865
+ function buildCatalogRoutes(startingRoute, favorites, resolveRoute, max = MAX_MODEL_CATALOG) {
5866
+ const droppedFavorites = [];
5867
+ const tail = favorites.map((fav) => {
5868
+ const route = resolveRoute(fav.providerId, fav.modelId);
5869
+ if (!route) droppedFavorites.push(fav);
5870
+ return route;
5871
+ }).filter((route) => route !== void 0);
5872
+ const routes = [
5873
+ startingRoute,
5874
+ ...tail.filter((route) => route.aliasId !== startingRoute.aliasId)
5875
+ ].slice(0, max);
5876
+ return { routes, droppedFavorites };
5877
+ }
5878
+
5817
5879
  // src/data/model-incompatible.json
5818
5880
  var model_incompatible_default = {
5819
5881
  schema_version: "1",
@@ -8235,6 +8297,52 @@ function favoriteProviderDisplayName(provider) {
8235
8297
  return provider.name;
8236
8298
  }
8237
8299
 
8300
+ // src/http-proxy/routes.ts
8301
+ var HTTP_PROXY_MODEL_PREFIX = "relay:";
8302
+ function httpProxyModelId(providerId, modelId) {
8303
+ return `${HTTP_PROXY_MODEL_PREFIX}${providerId}:${modelId}`;
8304
+ }
8305
+ function supportsClaudeTransparentMode(model) {
8306
+ return model.modelFormat === "openai" && isSdkMigratedNpm(model.npm);
8307
+ }
8308
+ function buildHttpProxyRoutes(providers, favorites, selected, max = MAX_MODEL_CATALOG) {
8309
+ const routes = [];
8310
+ const unavailable = [];
8311
+ const unsupported = [];
8312
+ const seen = /* @__PURE__ */ new Set();
8313
+ const requested = selected ? [selected, ...favorites] : favorites;
8314
+ for (const item of requested) {
8315
+ const requestKey = `${item.providerId}\0${item.modelId}`;
8316
+ if (seen.has(requestKey)) continue;
8317
+ seen.add(requestKey);
8318
+ if (routes.length >= max) break;
8319
+ const provider = providers.find((candidate) => candidate.id === item.providerId);
8320
+ const model = provider?.models.find((candidate) => candidate.id === item.modelId);
8321
+ if (!provider || !model) {
8322
+ unavailable.push(item);
8323
+ continue;
8324
+ }
8325
+ if (!supportsClaudeTransparentMode(model)) {
8326
+ unsupported.push(item);
8327
+ continue;
8328
+ }
8329
+ const route = localModelToRoute(provider, model);
8330
+ if (!route || !route.apiKey.trim()) {
8331
+ unavailable.push(item);
8332
+ continue;
8333
+ }
8334
+ routes.push({
8335
+ ...route,
8336
+ aliasId: claudeCodeClientModelId(
8337
+ httpProxyModelId(provider.id, model.id),
8338
+ model.contextWindow
8339
+ ),
8340
+ displayName: `${model.name || model.id} (${provider.name})`
8341
+ });
8342
+ }
8343
+ return { routes, unavailable, unsupported };
8344
+ }
8345
+
8238
8346
  // src/opencode-serve.ts
8239
8347
  import { execSync as execSync2, spawn as spawn2 } from "child_process";
8240
8348
  import { existsSync as existsSync10 } from "fs";
@@ -10349,6 +10457,7 @@ async function launchOrRestartClaudeApp(prompt = "Restart Claude Desktop to appl
10349
10457
 
10350
10458
  export {
10351
10459
  BACKENDS,
10460
+ CONFLICTING_ENV_VARS,
10352
10461
  MAX_MODEL_CATALOG,
10353
10462
  VERTEX_ANTHROPIC_NPM,
10354
10463
  VERSION,
@@ -10358,7 +10467,6 @@ export {
10358
10467
  buildClaudeCodeBillingSystemLine,
10359
10468
  selectBetaFlags,
10360
10469
  injectClaudeIdentity,
10361
- isSdkMigratedNpm,
10362
10470
  maxToolsForNpm,
10363
10471
  createLanguageModel,
10364
10472
  getReasoningCapabilities,
@@ -10507,6 +10615,8 @@ export {
10507
10615
  aliasModelId,
10508
10616
  startProxyCatalog,
10509
10617
  startProxy,
10618
+ makeRouteResolver,
10619
+ buildCatalogRoutes,
10510
10620
  cachedModelToLocal,
10511
10621
  fetchProviderCatalog,
10512
10622
  providersForPicker,
@@ -10550,6 +10660,9 @@ export {
10550
10660
  findClaudeApp,
10551
10661
  isClaudeAppRunning,
10552
10662
  quitClaudeAppGracefully,
10553
- launchOrRestartClaudeApp
10663
+ launchOrRestartClaudeApp,
10664
+ httpProxyModelId,
10665
+ supportsClaudeTransparentMode,
10666
+ buildHttpProxyRoutes
10554
10667
  };
10555
- //# sourceMappingURL=chunk-UTPRIJFE.js.map
10668
+ //# sourceMappingURL=chunk-XQCHSU6L.js.map