@jacobbd/relay-ai 0.6.2 → 0.7.0

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
@@ -17,9 +17,9 @@
17
17
 
18
18
  📺 **Watch the Demos**
19
19
 
20
- | **Claude Code / Cowork / Desktop** | **Codex CLI & Desktop App** | **Gemini CLI** | **v0.4.1: UI & Antigravity** |
21
- |:---:|:---:|:---:|:---:|
22
- | [![Claude Demo](https://img.youtube.com/vi/IvsUPHLhX0o/mqdefault.jpg)](https://youtu.be/IvsUPHLhX0o) | [![Codex Demo](https://img.youtube.com/vi/42oiOB8IAu4/mqdefault.jpg)](https://youtu.be/42oiOB8IAu4) | [![Gemini Demo](https://img.youtube.com/vi/g7JKvqOHJl4/mqdefault.jpg)](https://www.youtube.com/watch?v=g7JKvqOHJl4) | [![UI & Antigravity Demo](https://img.youtube.com/vi/8vXJ0LfpdoY/mqdefault.jpg)](https://www.youtube.com/watch?v=8vXJ0LfpdoY) |
20
+ | **Claude Code / Cowork / Desktop** | **Codex CLI & Desktop App** | **Gemini CLI** | **v0.4.1: UI & Antigravity** | **Server Gateway** |
21
+ |:---:|:---:|:---:|:---:|:---:|
22
+ | [![Claude Demo](https://img.youtube.com/vi/IvsUPHLhX0o/mqdefault.jpg)](https://youtu.be/IvsUPHLhX0o) | [![Codex Demo](https://img.youtube.com/vi/42oiOB8IAu4/mqdefault.jpg)](https://youtu.be/42oiOB8IAu4) | [![Gemini Demo](https://img.youtube.com/vi/g7JKvqOHJl4/mqdefault.jpg)](https://www.youtube.com/watch?v=g7JKvqOHJl4) | [![UI & Antigravity Demo](https://img.youtube.com/vi/8vXJ0LfpdoY/mqdefault.jpg)](https://www.youtube.com/watch?v=8vXJ0LfpdoY) | [![Server Gateway Demo](https://img.youtube.com/vi/4CEw0MQUE9I/mqdefault.jpg)](https://www.youtube.com/watch?v=4CEw0MQUE9I) |
23
23
 
24
24
  **relay-ai** is an interactive CLI — and now a **visual launcher** — that connects AI coding tools to any provider and runs local API gateways on your machine. It supports **Claude Code**, **Claude Desktop (Cowork + Code)**, the **OpenAI Codex CLI**, the **ChatGPT desktop app in Codex mode (macOS + Windows)**, **Google Gemini CLI**, and experimental **Antigravity CLI / IDE** support.
25
25
 
@@ -53,6 +53,7 @@ Pick your backend:
53
53
  | `relay-ai antigravity-ide` | Launch Antigravity IDE with Relay models, macOS ([warning + guide](docs/ANTIGRAVITY.md)) |
54
54
  | `relay-ai providers auth <id>` | Authenticate an OAuth provider (GitHub Copilot, xAI, OpenAI) |
55
55
  | `relay-ai --ai` | Full agent reference for scripts and alef-agent ([guide](docs/AI-AGENTS.md)) |
56
+ | `@jacobbd/relay-ai/core` | Embed Relay AI in a Node app in-process — no CLI/UI/server ([guide](docs/CORE.md)) |
56
57
 
57
58
  ## Features
58
59
 
@@ -406,6 +407,31 @@ export ANTHROPIC_API_KEY="anything"
406
407
  unset CLAUDE_CODE_USE_VERTEX ANTHROPIC_VERTEX_PROJECT_ID CLOUD_ML_REGION
407
408
  ```
408
409
 
410
+ ## Embedded usage (`@jacobbd/relay-ai/core`)
411
+
412
+ Node.js applications can embed Relay AI in-process — no CLI, UI, or server — to list the configured model catalog and obtain ready [Vercel AI SDK](https://sdk.vercel.ai) `LanguageModel` instances:
413
+
414
+ ```ts
415
+ import { listRelayModels, createRelayModel, isRelayCoreError } from '@jacobbd/relay-ai/core';
416
+ import { streamText } from 'ai';
417
+
418
+ const models = listRelayModels(); // credential-free catalog
419
+ const model = await createRelayModel(models[0].routeId);
420
+ const result = await streamText({ model, prompt: 'Hello!' });
421
+ ```
422
+
423
+ **Route ids** are unconditionally scoped: `` `${providerId}::${modelId}` `` (split on the first `::`; model ids may contain `/` and `:`, e.g. `openrouter::vendor/model:free`). Because they are never bare, a route id persisted by your app stays valid even when a second provider later exposes the same model id.
424
+
425
+ **Ownership boundary:** Relay AI keeps sole ownership of provider registration, credentials, the OS keyring, and OAuth login/refresh. `createRelayModel()` resolves credentials (refreshing expiring OAuth tokens) at call time and returns only the SDK model — your app never receives or stores credential material. Re-authentication always happens through Relay (`relay-ai ui`), never through the consumer.
426
+
427
+ **No server required:** the Core API reads the same registry and config as the CLI (`~/.relay-ai`, overridable with `RELAY_AI_HOME`) and never starts a server, opens a browser, or writes to disk. `createRelayModel()` re-reads state on every call, so provider changes take effect without restarting your app. Errors are thrown as `RelayCoreError` with a machine-readable `code` (see `isRelayCoreError`) and never contain credential material.
428
+
429
+ **Schema compatibility:** Core supports registry schema v1. A registry written by a newer Relay version fails fast with `UNSUPPORTED_REGISTRY_VERSION` — upgrade relay-ai rather than downgrading the file.
430
+
431
+ **Capability detection:** `capabilities.tools` and `capabilities.vision` are always `'unknown'` today — the cached model catalog carries no tools/vision metadata, and Core deliberately never guesses from a model's name. Don't filter on `=== true` for these two fields; use `capabilities.reasoning` (`'none' | 'fixed' | 'adjustable' | 'unknown'`), which is derived from real provider metadata.
432
+
433
+ **Full guide:** prerequisites, the complete API and error-code reference, and troubleshooting — see **[docs/CORE.md](docs/CORE.md)**.
434
+
409
435
  ## Antigravity CLI, app, and IDE support
410
436
 
411
437
  Relay AI can launch the Antigravity CLI, standalone Antigravity app, and Antigravity IDE through a local Cloud Code gateway. This lets Antigravity's native model picker show Relay models from your configured providers.
@@ -455,8 +481,9 @@ Launch **Claude Desktop** (macOS or Windows) with registry providers:
455
481
  relay-ai claude-app
456
482
  ```
457
483
 
458
- This command automates the "Third-Party Inference" (Developer Mode) setup. It temporarily configures Claude Desktop to point at a local gateway, launches the app, and routes traffic to your chosen provider.
484
+ This command automates the "Third-Party Inference" (Developer Mode) setup. It temporarily configures Claude Desktop to point at a local gateway, launches the app, and exposes the selected model followed by your available saved favorites (up to 20 models total).
459
485
 
486
+ - **Selected + favorites:** The selected model is listed first; duplicates and unavailable favorites are skipped. Claude Desktop ultimately controls which discovered model it activates initially.
460
487
  - **Keep the terminal open:** The proxy runs in the foreground.
461
488
  - **Ctrl+C to restore:** When you're done, press `Ctrl+C` in the terminal to automatically restore Claude Desktop to its normal Anthropic cloud mode.
462
489
  - **Cleanup:** If the terminal crashes, run `relay-ai claude-app --restore`.
@@ -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.6.2",
14
+ version: "0.7.0",
15
15
  publishConfig: {
16
16
  access: "public"
17
17
  },
@@ -46,7 +46,7 @@ var package_default = {
46
46
  node: ">=18"
47
47
  },
48
48
  scripts: {
49
- build: "tsup && mkdir -p dist/ui/public && cp -r src/ui/public/. dist/ui/public/",
49
+ build: "tsup && tsup --config tsup.core.config.ts && node scripts/copy-ui-assets.mjs",
50
50
  dev: "tsup --watch",
51
51
  test: "vitest run",
52
52
  "test:watch": "vitest",
@@ -100,6 +100,14 @@ var package_default = {
100
100
  },
101
101
  overrides: {
102
102
  ws: "^8.21.0"
103
+ },
104
+ exports: {
105
+ "./core": {
106
+ types: "./dist/core/index.d.ts",
107
+ import: "./dist/core/index.js",
108
+ default: "./dist/core/index.js"
109
+ },
110
+ "./package.json": "./package.json"
103
111
  }
104
112
  };
105
113
 
@@ -139,6 +147,14 @@ var CONFLICTING_ENV_VARS = [
139
147
  "ANTHROPIC_DEFAULT_SONNET_MODEL",
140
148
  "ANTHROPIC_DEFAULT_HAIKU_MODEL"
141
149
  ];
150
+ var PARENT_SESSION_ENV_VARS = [
151
+ "CLAUDECODE",
152
+ "CLAUDE_CODE_CHILD_SESSION",
153
+ "CLAUDE_CODE_SESSION_ID",
154
+ "CLAUDE_CODE_HOST_SESSION_ID",
155
+ "CLAUDE_CODE_ENTRYPOINT",
156
+ "CLAUDE_PID"
157
+ ];
142
158
  var OPENCODE_CACHE_PATH = join(homedir(), ".cache", "opencode", "models.json");
143
159
  var MAX_MODEL_CATALOG = 20;
144
160
  var VERTEX_ANTHROPIC_NPM = "@ai-sdk/google-vertex/anthropic";
@@ -3045,6 +3061,9 @@ function buildChildEnv(baseUrl, model, apiKey, proxyPort, contextWindow, enableG
3045
3061
  for (const name of CONFLICTING_ENV_VARS) {
3046
3062
  delete env[name];
3047
3063
  }
3064
+ for (const name of PARENT_SESSION_ENV_VARS) {
3065
+ delete env[name];
3066
+ }
3048
3067
  env["ANTHROPIC_BASE_URL"] = proxyPort ? `http://127.0.0.1:${proxyPort}` : baseUrl;
3049
3068
  env["ANTHROPIC_API_KEY"] = apiKey;
3050
3069
  const bareModel = stripOneMContextSuffix(model);
@@ -3616,7 +3635,7 @@ function parseRegistry(raw) {
3616
3635
  if (typeof data.pricingCacheAt === "string") registry.pricingCacheAt = data.pricingCacheAt;
3617
3636
  return registry;
3618
3637
  }
3619
- function loadRegistry(path = getProvidersPath()) {
3638
+ function loadRegistry(path = getProvidersPath(), { persist = true } = {}) {
3620
3639
  if (!existsSync6(path)) {
3621
3640
  return { schemaVersion: REGISTRY_SCHEMA_VERSION, providers: [] };
3622
3641
  }
@@ -3627,7 +3646,7 @@ function loadRegistry(path = getProvidersPath()) {
3627
3646
  if (migrateOAuthOpenAiProvider(registry)) migrated = true;
3628
3647
  if (migrateOAuthXaiProvider(registry)) migrated = true;
3629
3648
  if (migrateAlibabaDashScopeChinaLabel(registry)) migrated = true;
3630
- if (migrated) {
3649
+ if (migrated && persist) {
3631
3650
  try {
3632
3651
  saveRegistry(registry, path);
3633
3652
  } catch {
@@ -4243,7 +4262,7 @@ function maskGatewayModelId(aliasId) {
4243
4262
  // src/server/models.ts
4244
4263
  var CREATED_AT_ISO = "2025-01-01T00:00:00Z";
4245
4264
  var CREATED_AT_UNIX = 1735689600;
4246
- function formatAnthropicModelEntry(id, displayName, contextWindow) {
4265
+ function formatAnthropicModelEntry(id, displayName, contextWindow, options) {
4247
4266
  const maxInput = resolveContextWindow(id, contextWindow);
4248
4267
  return {
4249
4268
  id,
@@ -4251,12 +4270,18 @@ function formatAnthropicModelEntry(id, displayName, contextWindow) {
4251
4270
  display_name: displayName,
4252
4271
  created_at: CREATED_AT_ISO,
4253
4272
  context_window: maxInput,
4254
- max_input_tokens: maxInput
4273
+ max_input_tokens: maxInput,
4274
+ ...options?.supportsOneM !== void 0 ? { supports_1m: options.supportsOneM } : {}
4255
4275
  };
4256
4276
  }
4257
4277
  function formatAnthropicModelList(entries) {
4258
4278
  return {
4259
- data: entries.map((entry) => formatAnthropicModelEntry(entry.id, entry.name, entry.contextWindow)),
4279
+ data: entries.map((entry) => formatAnthropicModelEntry(
4280
+ entry.id,
4281
+ entry.name,
4282
+ entry.contextWindow,
4283
+ { supportsOneM: entry.supportsOneM }
4284
+ )),
4260
4285
  has_more: false,
4261
4286
  first_id: entries[0]?.id ?? null,
4262
4287
  last_id: entries.at(-1)?.id ?? null
@@ -4282,22 +4307,28 @@ function openAiExposedId(model, collisions) {
4282
4307
  return collisions.has(model.id) ? `${gatewayProviderId(model)}/${model.id}` : model.id;
4283
4308
  }
4284
4309
  function exposedGatewayAliasId(model, opts) {
4285
- const alias = gatewayAliasId(model);
4286
- return opts?.maskGatewayIds ? maskGatewayModelId(alias) : alias;
4310
+ const singleOneM = usesSingleOneMEntry(model, opts);
4311
+ const alias = gatewayAliasId(singleOneM ? { ...model, id: stripOneMContextSuffix(model.id) } : model);
4312
+ const exposed = opts?.maskGatewayIds ? maskGatewayModelId(alias) : alias;
4313
+ return singleOneM ? `${stripOneMContextSuffix(exposed)}[1m]` : exposed;
4287
4314
  }
4288
4315
  function gatewayDisplayName(model, opts) {
4289
- if (!opts?.maskGatewayIds) return model.name;
4290
- return `${model.name} (${gatewayProviderLabel(model)})`;
4316
+ const name = opts?.maskGatewayIds ? `${model.name} (${gatewayProviderLabel(model)})` : model.name;
4317
+ return usesSingleOneMEntry(model, opts) && !/\b1m$/i.test(name) ? `${name} 1M` : name;
4291
4318
  }
4292
4319
  function formatGatewayAnthropicModels(models, opts) {
4293
4320
  return formatAnthropicModelList(
4294
4321
  models.map((model) => ({
4295
4322
  id: exposedGatewayAliasId(model, opts),
4296
4323
  name: gatewayDisplayName(model, opts),
4297
- contextWindow: model.contextWindow
4324
+ contextWindow: model.contextWindow,
4325
+ supportsOneM: usesSingleOneMEntry(model, opts) ? false : void 0
4298
4326
  }))
4299
4327
  );
4300
4328
  }
4329
+ function usesSingleOneMEntry(model, opts) {
4330
+ return opts?.longContextDisplay === "single-1m" && resolveContextWindow(stripOneMContextSuffix(model.id), model.contextWindow) >= 1e6;
4331
+ }
4301
4332
  function createGatewayModelCatalog(models, opts) {
4302
4333
  const byId = /* @__PURE__ */ new Map();
4303
4334
  const collisions = openAiIdCollisions(models);
@@ -4307,8 +4338,23 @@ function createGatewayModelCatalog(models, opts) {
4307
4338
  if (scopedId !== model.id) byId.set(scopedId, model);
4308
4339
  const alias = exposedGatewayAliasId(model, opts);
4309
4340
  if (alias !== model.id) byId.set(alias, model);
4341
+ const singleOneM = usesSingleOneMEntry(model, opts);
4342
+ if (singleOneM) {
4343
+ const bareModel = { ...model, id: stripOneMContextSuffix(model.id) };
4344
+ const rawBareAlias = gatewayAliasId(bareModel);
4345
+ const exposedBareAlias = opts?.maskGatewayIds ? maskGatewayModelId(rawBareAlias) : rawBareAlias;
4346
+ for (const compatibleId of [
4347
+ stripOneMContextSuffix(model.id),
4348
+ rawBareAlias,
4349
+ `${rawBareAlias}[1m]`,
4350
+ exposedBareAlias,
4351
+ `${exposedBareAlias}[1m]`
4352
+ ]) {
4353
+ byId.set(compatibleId, model);
4354
+ }
4355
+ }
4310
4356
  if (opts?.maskGatewayIds) {
4311
- const rawAlias = gatewayAliasId(model);
4357
+ const rawAlias = gatewayAliasId(singleOneM ? { ...model, id: stripOneMContextSuffix(model.id) } : model);
4312
4358
  if (rawAlias !== alias) byId.set(rawAlias, model);
4313
4359
  }
4314
4360
  }
@@ -10679,6 +10725,8 @@ function winLocalAppData2() {
10679
10725
  function winClaudeExeCandidates() {
10680
10726
  const local = winLocalAppData2();
10681
10727
  const bases = [
10728
+ // Squirrel install folder used by the Anthropic Claude desktop installer.
10729
+ join12(local, "AnthropicClaude"),
10682
10730
  join12(local, "Programs", "Claude"),
10683
10731
  join12(local, "Claude")
10684
10732
  ];
@@ -11063,4 +11111,4 @@ export {
11063
11111
  supportsClaudeTransparentMode,
11064
11112
  buildHttpProxyRoutes
11065
11113
  };
11066
- //# sourceMappingURL=chunk-I3I5LKZI.js.map
11114
+ //# sourceMappingURL=chunk-IYYLLN5T.js.map