@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 +31 -4
- package/dist/{chunk-I3I5LKZI.js → chunk-IYYLLN5T.js} +62 -14
- package/dist/chunk-IYYLLN5T.js.map +1 -0
- package/dist/cli.js +320 -148
- package/dist/cli.js.map +1 -1
- package/dist/core/index.d.ts +91 -0
- package/dist/core/index.js +1854 -0
- package/dist/core/index.js.map +1 -0
- package/dist/{ui-command-GNCQS7F7.js → ui-command-3CWMSARO.js} +2 -2
- package/package.json +10 -2
- package/dist/chunk-I3I5LKZI.js.map +0 -1
- /package/dist/{ui-command-GNCQS7F7.js.map → ui-command-3CWMSARO.js.map} +0 -0
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
|
-
| [](https://youtu.be/IvsUPHLhX0o) | [](https://youtu.be/42oiOB8IAu4) | [](https://www.youtube.com/watch?v=g7JKvqOHJl4) | [](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
|
+
| [](https://youtu.be/IvsUPHLhX0o) | [](https://youtu.be/42oiOB8IAu4) | [](https://www.youtube.com/watch?v=g7JKvqOHJl4) | [](https://www.youtube.com/watch?v=8vXJ0LfpdoY) | [](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
|
|
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.
|
|
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 &&
|
|
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(
|
|
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
|
|
4286
|
-
|
|
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
|
-
|
|
4290
|
-
return
|
|
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-
|
|
11114
|
+
//# sourceMappingURL=chunk-IYYLLN5T.js.map
|