open-agents-ai 0.103.35 → 0.103.37
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 +89 -4
- package/dist/index.js +243 -44
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,7 +72,8 @@ The agent uses tools autonomously in a loop — reading errors, fixing code, and
|
|
|
72
72
|
- **Task templates** — specialized system prompts and tool recommendations for code, document, analysis, plan tasks
|
|
73
73
|
- **Inference capability scoring** — canirun.ai-style hardware assessment at first launch: memory/compute/speed scores, per-model compatibility matrix, recommended model selection
|
|
74
74
|
- **Auto-install everything** — first-run wizard auto-installs Ollama, curl, Python3, python3-venv with platform-aware package managers (apt, dnf, yum, pacman, apk, zypper, brew)
|
|
75
|
-
- **P2P inference network** — `/
|
|
75
|
+
- **P2P inference network** — `/expose` local models or forward any `/endpoint` (Chutes, Groq, OpenRouter, etc.) through the libp2p P2P mesh. Passthrough mode (`/expose passthrough`) relays upstream API requests; `--loadbalance` distributes rate-limited token budgets across peers. `/expose config` provides an arrow-key menu for all settings. Gateway stats show budget remaining from `x-ratelimit-*` headers. Background daemon persists across OA restarts
|
|
76
|
+
- **P2P mesh networking** — `/p2p` with secret-safe variable placeholders (`{{OA_VAR_*}}`), trust tiers (LOCAL/TEE/VERIFIED/PUBLIC), WebSocket peer mesh, and inference routing with automatic secret redaction/injection
|
|
76
77
|
- **Secret vault** — `/secrets` manages API keys and credentials with AES-256-GCM encrypted persistence; secrets are automatically redacted before sending to untrusted inference peers and re-injected on response
|
|
77
78
|
- **Auto-expanding context** — detects RAM/VRAM and creates an optimized model variant on first run
|
|
78
79
|
- **Mid-task steering** — type while the agent works to add context without interrupting
|
|
@@ -277,7 +278,7 @@ On startup and `/model` switch, Open Agents detects your RAM/VRAM and creates an
|
|
|
277
278
|
| `aiwg_workflow` | Execute AIWG commands and workflows |
|
|
278
279
|
| **Nexus P2P & x402 Payments** | |
|
|
279
280
|
| `nexus` | Decentralized agent networking — connect, rooms, DMs, peer discovery, invoke capabilities, metering, trust/blocking, IPFS storage |
|
|
280
|
-
| `nexus:expose` | Expose local
|
|
281
|
+
| `nexus:expose` | Expose local models or forward upstream endpoints as metered inference capabilities with pricing, passthrough, and load balancing |
|
|
281
282
|
| `nexus:wallet_create` | Generate secp256k1/EVM wallet (Base mainnet USDC) with AES-256-GCM encryption + x402-wallet.key |
|
|
282
283
|
| `nexus:spend` | Sign EIP-3009 USDC TransferWithAuthorization — budget-checked, gasless for payer |
|
|
283
284
|
| `nexus:remote_infer` | Route inference to a remote peer's model — auto-discovers peers, budget-checks, invokes, returns result |
|
|
@@ -956,8 +957,12 @@ The TUI features an animated multilingual phrase carousel, live metrics bar with
|
|
|
956
957
|
| `/expose ollama` | Expose local inference via libp2p (default) |
|
|
957
958
|
| `/expose ollama --tunnel` | Expose via cloudflared tunnel |
|
|
958
959
|
| `/expose ollama --full` | Allow full Ollama API access (pull/delete) |
|
|
959
|
-
| `/expose
|
|
960
|
-
| `/expose
|
|
960
|
+
| `/expose passthrough` | Forward configured `/endpoint` through libp2p P2P |
|
|
961
|
+
| `/expose forward --loadbalance` | Passthrough with distributed rate-limit budget |
|
|
962
|
+
| `/expose config` | Interactive expose configuration menu (arrow-key nav) |
|
|
963
|
+
| `/expose stop` | Stop all expose gateways |
|
|
964
|
+
| `/expose stop --libp2p` | Stop libp2p gateway only |
|
|
965
|
+
| `/expose status` | Show expose usage stats + budget |
|
|
961
966
|
| **Metrics & Updates** | |
|
|
962
967
|
| `/cost` | Show token cost breakdown for the current session |
|
|
963
968
|
| `/score` | Show inference capability scorecard (memory, compute, speed, model compatibility) |
|
|
@@ -1526,6 +1531,86 @@ Expose your local Ollama models to the decentralized nexus network, or consume a
|
|
|
1526
1531
|
|
|
1527
1532
|
Transport: DHT + mDNS + NATS relay + circuit relay. Auth key is auto-generated and required for all requests. System metrics (CPU/GPU/memory) are available to consumers via the `system_metrics` capability. Without `--full`, destructive Ollama API endpoints (`/api/pull`, `/api/delete`, `/api/create`) are blocked.
|
|
1528
1533
|
|
|
1534
|
+
#### Passthrough & Forward Mode
|
|
1535
|
+
|
|
1536
|
+
Forward any configured `/endpoint` (Chutes, Groq, OpenRouter, Together, vLLM, etc.) through the libp2p P2P network. Your node becomes a relay — peers connect to you via libp2p and you forward their requests to your upstream API:
|
|
1537
|
+
|
|
1538
|
+
```bash
|
|
1539
|
+
# Set your upstream endpoint first
|
|
1540
|
+
/endpoint https://llm.chutes.ai --auth cpk_your_key_here
|
|
1541
|
+
|
|
1542
|
+
# Expose it through P2P — peers discover and invoke via libp2p
|
|
1543
|
+
/expose passthrough
|
|
1544
|
+
# or equivalently:
|
|
1545
|
+
/expose forward
|
|
1546
|
+
|
|
1547
|
+
# With load balancing: distributes daily token budget across peers
|
|
1548
|
+
/expose passthrough --loadbalance
|
|
1549
|
+
```
|
|
1550
|
+
|
|
1551
|
+
**How it works:**
|
|
1552
|
+
- Your node registers inference capabilities on the P2P mesh using your upstream endpoint's models
|
|
1553
|
+
- Remote peers discover and invoke these capabilities via libp2p streams (DHT/mDNS/NATS)
|
|
1554
|
+
- Requests are forwarded to your upstream API, responses streamed back to the peer
|
|
1555
|
+
- The libp2p daemon persists in the background — it survives OA restarts and remains discoverable even when the TUI is closed
|
|
1556
|
+
- When you reopen OA, it reconnects to the existing daemon and resumes stats tracking
|
|
1557
|
+
|
|
1558
|
+
**Rate limit distribution (`--loadbalance`):**
|
|
1559
|
+
- Captures `x-ratelimit-remaining-tokens` and `x-ratelimit-limit-tokens` headers from upstream API responses
|
|
1560
|
+
- Displays remaining token budget in the gateway stats under "Budget"
|
|
1561
|
+
- Distributes the total daily token budget across connected peers proportionally
|
|
1562
|
+
- Prevents any single peer from exhausting the shared budget
|
|
1563
|
+
|
|
1564
|
+
#### Budget & Rate Limit Monitoring
|
|
1565
|
+
|
|
1566
|
+
When exposing an upstream endpoint that returns rate-limit headers (most cloud providers do), the gateway stats automatically track your remaining budget:
|
|
1567
|
+
|
|
1568
|
+
```
|
|
1569
|
+
Expose Gateway Stats (libp2p passthrough)
|
|
1570
|
+
Status active
|
|
1571
|
+
Transport libp2p (passthrough)
|
|
1572
|
+
Peer ID 12D3KooWSzC75QX...
|
|
1573
|
+
Uptime 2h 15m
|
|
1574
|
+
Total requests 847
|
|
1575
|
+
Tokens in 125.4K
|
|
1576
|
+
Tokens out 892.1K
|
|
1577
|
+
Budget 1.2M/10M (12% left)
|
|
1578
|
+
|
|
1579
|
+
Models
|
|
1580
|
+
qwen3.5-4b 412 reqs in:52.3K out:401.2K
|
|
1581
|
+
qwen3.5-9b 435 reqs in:73.1K out:490.9K
|
|
1582
|
+
|
|
1583
|
+
Active Peers (3)
|
|
1584
|
+
12D3KooWSwaCi1Jg...
|
|
1585
|
+
Session: 1h 45m Last seen: now Requests: 523
|
|
1586
|
+
Tokens: in:82.1K out:612.4K
|
|
1587
|
+
· qwen3.5-4b 312req 401.2Ktok
|
|
1588
|
+
· qwen3.5-9b 211req 293.3Ktok
|
|
1589
|
+
12D3KooWKnCgxx7D...
|
|
1590
|
+
Session: 45m Last seen: 2m ago Requests: 324
|
|
1591
|
+
Tokens: in:43.3K out:279.7K
|
|
1592
|
+
· qwen3.5-9b 224req 197.6Ktok
|
|
1593
|
+
```
|
|
1594
|
+
|
|
1595
|
+
Internal capabilities (`system_metrics`, `__list_capabilities`) are hidden from all displays — both the full stats view and the compact status bar one-liner.
|
|
1596
|
+
|
|
1597
|
+
#### `/expose config` — Interactive Configuration
|
|
1598
|
+
|
|
1599
|
+
Arrow-key navigable menu for all expose settings:
|
|
1600
|
+
|
|
1601
|
+
```bash
|
|
1602
|
+
/expose config
|
|
1603
|
+
```
|
|
1604
|
+
|
|
1605
|
+
Shows options to:
|
|
1606
|
+
- View current stats
|
|
1607
|
+
- Stop all gateways
|
|
1608
|
+
- Start Ollama (libp2p or tunnel)
|
|
1609
|
+
- Start passthrough (with or without load balancing)
|
|
1610
|
+
- Start vLLM
|
|
1611
|
+
|
|
1612
|
+
Uses the same arrow-key navigation pattern as `/model` and `/endpoint` selection.
|
|
1613
|
+
|
|
1529
1614
|
### Endpoint Cascade Failover
|
|
1530
1615
|
|
|
1531
1616
|
When you've used multiple endpoints, the agent automatically builds a failover cascade. If the primary endpoint fails with transient errors (502, connection refused, timeout), it transparently switches to the next endpoint that has the same model — then periodically probes the primary to return when it recovers:
|
package/dist/index.js
CHANGED
|
@@ -26371,10 +26371,15 @@ function renderSlashHelp() {
|
|
|
26371
26371
|
["/nexus", "Show nexus P2P network status (or connect/disconnect/rooms)"],
|
|
26372
26372
|
["/nexus connect", "Connect to the agent mesh network"],
|
|
26373
26373
|
["/nexus wallet", "Show wallet address and x402 payment status"],
|
|
26374
|
-
["/expose <backend>", "
|
|
26375
|
-
["/expose <backend> --
|
|
26376
|
-
["/expose
|
|
26377
|
-
["/expose
|
|
26374
|
+
["/expose <backend>", "Expose local inference via libp2p (default)"],
|
|
26375
|
+
["/expose <backend> --tunnel", "Expose via cloudflared tunnel"],
|
|
26376
|
+
["/expose passthrough", "Forward configured /endpoint through libp2p P2P"],
|
|
26377
|
+
["/expose forward --loadbalance", "Passthrough with distributed rate-limit budget"],
|
|
26378
|
+
["/expose config", "Interactive expose configuration menu"],
|
|
26379
|
+
["/expose status", "Show gateway stats (requests, models, budget)"],
|
|
26380
|
+
["/expose stop", "Stop all expose gateways"],
|
|
26381
|
+
["/expose stop --tunnel", "Stop tunnel gateway only"],
|
|
26382
|
+
["/expose stop --libp2p", "Stop libp2p gateway only"],
|
|
26378
26383
|
["/p2p start", "Join the P2P agent mesh network"],
|
|
26379
26384
|
["/p2p status", "Show mesh peers and rooms"],
|
|
26380
26385
|
["/p2p stop", "Leave the mesh network"],
|
|
@@ -27527,6 +27532,33 @@ function isProcessAlive(pid) {
|
|
|
27527
27532
|
return false;
|
|
27528
27533
|
}
|
|
27529
27534
|
}
|
|
27535
|
+
function parseRateLimitHeaders(headers) {
|
|
27536
|
+
const get = (name) => {
|
|
27537
|
+
const v = headers[name] ?? headers[name.toLowerCase()];
|
|
27538
|
+
return typeof v === "string" ? v : Array.isArray(v) ? v[0] : void 0;
|
|
27539
|
+
};
|
|
27540
|
+
const remaining = parseInt(get("x-ratelimit-remaining-tokens") ?? get("x-ratelimit-remaining") ?? "", 10);
|
|
27541
|
+
const total = parseInt(get("x-ratelimit-limit-tokens") ?? get("x-ratelimit-limit") ?? "", 10);
|
|
27542
|
+
const resetRaw = get("x-ratelimit-reset-tokens") ?? get("x-ratelimit-reset") ?? "";
|
|
27543
|
+
let resetAt = 0;
|
|
27544
|
+
if (resetRaw) {
|
|
27545
|
+
const n = Number(resetRaw);
|
|
27546
|
+
if (!isNaN(n)) {
|
|
27547
|
+
resetAt = n < 1e12 ? n * 1e3 : n;
|
|
27548
|
+
} else {
|
|
27549
|
+
const d = new Date(resetRaw).getTime();
|
|
27550
|
+
if (!isNaN(d))
|
|
27551
|
+
resetAt = d;
|
|
27552
|
+
}
|
|
27553
|
+
}
|
|
27554
|
+
if (isNaN(remaining) && isNaN(total))
|
|
27555
|
+
return null;
|
|
27556
|
+
return {
|
|
27557
|
+
remaining: isNaN(remaining) ? 0 : remaining,
|
|
27558
|
+
total: isNaN(total) ? 0 : total,
|
|
27559
|
+
resetAt
|
|
27560
|
+
};
|
|
27561
|
+
}
|
|
27530
27562
|
async function collectSystemMetricsAsync() {
|
|
27531
27563
|
const [l1, l5, l15] = loadavg();
|
|
27532
27564
|
const cores = cpus().length;
|
|
@@ -27602,7 +27634,7 @@ function removeP2PExposeState(stateDir) {
|
|
|
27602
27634
|
} catch {
|
|
27603
27635
|
}
|
|
27604
27636
|
}
|
|
27605
|
-
var HOP_BY_HOP_HEADERS, CF_HEADERS_PREFIX, DEFAULT_TARGETS, STATE_FILE_NAME, ExposeGateway, P2P_STATE_FILE_NAME, ExposeP2PGateway;
|
|
27637
|
+
var HOP_BY_HOP_HEADERS, CF_HEADERS_PREFIX, INTERNAL_CAPABILITIES, DEFAULT_TARGETS, STATE_FILE_NAME, ExposeGateway, P2P_STATE_FILE_NAME, ExposeP2PGateway;
|
|
27606
27638
|
var init_expose = __esm({
|
|
27607
27639
|
"packages/cli/dist/tui/expose.js"() {
|
|
27608
27640
|
"use strict";
|
|
@@ -27618,6 +27650,7 @@ var init_expose = __esm({
|
|
|
27618
27650
|
"upgrade"
|
|
27619
27651
|
]);
|
|
27620
27652
|
CF_HEADERS_PREFIX = ["cf-", "cdn-"];
|
|
27653
|
+
INTERNAL_CAPABILITIES = /* @__PURE__ */ new Set(["system_metrics", "__list_capabilities"]);
|
|
27621
27654
|
DEFAULT_TARGETS = {
|
|
27622
27655
|
ollama: "http://127.0.0.1:11434",
|
|
27623
27656
|
vllm: "http://127.0.0.1:8000",
|
|
@@ -27648,7 +27681,10 @@ var init_expose = __esm({
|
|
|
27648
27681
|
totalTokensOut: 0,
|
|
27649
27682
|
startedAt: Date.now(),
|
|
27650
27683
|
modelUsage: /* @__PURE__ */ new Map(),
|
|
27651
|
-
users: /* @__PURE__ */ new Map()
|
|
27684
|
+
users: /* @__PURE__ */ new Map(),
|
|
27685
|
+
budgetTokensRemaining: 0,
|
|
27686
|
+
budgetTokensTotal: 0,
|
|
27687
|
+
budgetResetAt: 0
|
|
27652
27688
|
};
|
|
27653
27689
|
get tunnelUrl() {
|
|
27654
27690
|
return this._tunnelUrl;
|
|
@@ -27995,6 +28031,13 @@ var init_expose = __esm({
|
|
|
27995
28031
|
headers: forwardHeaders
|
|
27996
28032
|
}, (proxyRes) => {
|
|
27997
28033
|
cleanupHeartbeat();
|
|
28034
|
+
const rlHeaders = proxyRes.headers;
|
|
28035
|
+
const rl = parseRateLimitHeaders(rlHeaders);
|
|
28036
|
+
if (rl && (rl.total > 0 || rl.remaining > 0)) {
|
|
28037
|
+
this._stats.budgetTokensRemaining = rl.remaining;
|
|
28038
|
+
this._stats.budgetTokensTotal = rl.total;
|
|
28039
|
+
this._stats.budgetResetAt = rl.resetAt;
|
|
28040
|
+
}
|
|
27998
28041
|
proxyRes.on("error", () => {
|
|
27999
28042
|
finalizeRequest();
|
|
28000
28043
|
});
|
|
@@ -28250,10 +28293,16 @@ ${this.formatConnectionInfo()}`);
|
|
|
28250
28293
|
lines.push(` ${c2.cyan("Errors".padEnd(18))} ${s.errors}`);
|
|
28251
28294
|
lines.push(` ${c2.cyan("Tokens in".padEnd(18))} ${fmtTokens(s.totalTokensIn)}`);
|
|
28252
28295
|
lines.push(` ${c2.cyan("Tokens out".padEnd(18))} ${fmtTokens(s.totalTokensOut)}`);
|
|
28253
|
-
if (s.
|
|
28296
|
+
if (s.budgetTokensTotal > 0) {
|
|
28297
|
+
const pct = Math.round(s.budgetTokensRemaining / s.budgetTokensTotal * 100);
|
|
28298
|
+
const budgetColor = pct > 50 ? c2.green : pct > 20 ? c2.yellow : c2.red;
|
|
28299
|
+
lines.push(` ${c2.cyan("Budget".padEnd(18))} ${budgetColor(fmtTokens(s.budgetTokensRemaining))}${c2.dim("/")}${fmtTokens(s.budgetTokensTotal)} ${c2.dim(`(${pct}% left)`)}`);
|
|
28300
|
+
}
|
|
28301
|
+
const visibleModels = Array.from(s.modelUsage.entries()).filter(([model]) => !INTERNAL_CAPABILITIES.has(model));
|
|
28302
|
+
if (visibleModels.length > 0) {
|
|
28254
28303
|
lines.push("");
|
|
28255
28304
|
lines.push(` ${c2.bold("Models")}`);
|
|
28256
|
-
for (const [model, count] of
|
|
28305
|
+
for (const [model, count] of visibleModels) {
|
|
28257
28306
|
let mIn = 0, mOut = 0;
|
|
28258
28307
|
for (const user of s.users.values()) {
|
|
28259
28308
|
const mm = user.models.get(model);
|
|
@@ -28277,17 +28326,23 @@ ${this.formatConnectionInfo()}`);
|
|
|
28277
28326
|
lines.push(` ${c2.bold(user.ip)}${activeTag}`);
|
|
28278
28327
|
lines.push(` ${c2.dim("Session:")} ${ageStr} ${c2.dim("Last seen:")} ${lastStr} ${c2.dim("Requests:")} ${user.requests}`);
|
|
28279
28328
|
lines.push(` ${c2.dim("Tokens:")} in:${fmtTokens(user.tokensIn)} out:${fmtTokens(user.tokensOut)} ${c2.dim("Total:")} ${fmtTokens(user.tokensIn + user.tokensOut)}`);
|
|
28280
|
-
|
|
28281
|
-
|
|
28282
|
-
for (const [m, mm] of
|
|
28283
|
-
|
|
28329
|
+
const userModels = Array.from(user.models.entries()).filter(([m]) => !INTERNAL_CAPABILITIES.has(m));
|
|
28330
|
+
if (userModels.length > 0) {
|
|
28331
|
+
for (const [m, mm] of userModels) {
|
|
28332
|
+
lines.push(` ${c2.dim("\xB7")} ${c2.cyan(m)} ${c2.dim(`${mm.requests}req ${fmtTokens(mm.tokensIn + mm.tokensOut)}tok`)}`);
|
|
28284
28333
|
}
|
|
28285
|
-
lines.push(` ${c2.dim("Models:")} ${modelParts.join(", ")}`);
|
|
28286
28334
|
}
|
|
28287
28335
|
}
|
|
28288
28336
|
}
|
|
28289
28337
|
return lines.join("\n");
|
|
28290
28338
|
}
|
|
28339
|
+
/** Update budget from rate limit headers */
|
|
28340
|
+
updateBudget(remaining, total, resetAt) {
|
|
28341
|
+
this._stats.budgetTokensRemaining = remaining;
|
|
28342
|
+
this._stats.budgetTokensTotal = total;
|
|
28343
|
+
this._stats.budgetResetAt = resetAt;
|
|
28344
|
+
this.emitStats();
|
|
28345
|
+
}
|
|
28291
28346
|
};
|
|
28292
28347
|
P2P_STATE_FILE_NAME = "expose-p2p-state.json";
|
|
28293
28348
|
ExposeP2PGateway = class _ExposeP2PGateway extends EventEmitter3 {
|
|
@@ -28301,6 +28356,8 @@ ${this.formatConnectionInfo()}`);
|
|
|
28301
28356
|
_exposedModels = 0;
|
|
28302
28357
|
_stateDir;
|
|
28303
28358
|
_margin;
|
|
28359
|
+
_passthrough = false;
|
|
28360
|
+
_loadbalance = false;
|
|
28304
28361
|
_pollTimer = null;
|
|
28305
28362
|
_stats = {
|
|
28306
28363
|
status: "standby",
|
|
@@ -28311,7 +28368,10 @@ ${this.formatConnectionInfo()}`);
|
|
|
28311
28368
|
totalTokensOut: 0,
|
|
28312
28369
|
startedAt: Date.now(),
|
|
28313
28370
|
modelUsage: /* @__PURE__ */ new Map(),
|
|
28314
|
-
users: /* @__PURE__ */ new Map()
|
|
28371
|
+
users: /* @__PURE__ */ new Map(),
|
|
28372
|
+
budgetTokensRemaining: 0,
|
|
28373
|
+
budgetTokensTotal: 0,
|
|
28374
|
+
budgetResetAt: 0
|
|
28315
28375
|
};
|
|
28316
28376
|
get peerId() {
|
|
28317
28377
|
return this._peerId;
|
|
@@ -28340,6 +28400,8 @@ ${this.formatConnectionInfo()}`);
|
|
|
28340
28400
|
this._targetUrl = options.targetUrl ?? DEFAULT_TARGETS[options.kind];
|
|
28341
28401
|
this._stateDir = options.stateDir ?? null;
|
|
28342
28402
|
this._margin = options.margin ?? 0.5;
|
|
28403
|
+
this._passthrough = options.passthrough ?? false;
|
|
28404
|
+
this._loadbalance = options.loadbalance ?? false;
|
|
28343
28405
|
this._onInfo = options.onInfo;
|
|
28344
28406
|
this._onError = options.onError;
|
|
28345
28407
|
if (options.authKey === void 0 || options.authKey === "") {
|
|
@@ -28406,6 +28468,8 @@ ${this.formatConnectionInfo()}`);
|
|
|
28406
28468
|
kind: this._kind,
|
|
28407
28469
|
targetUrl: this._targetUrl,
|
|
28408
28470
|
margin: this._margin,
|
|
28471
|
+
passthrough: this._passthrough || void 0,
|
|
28472
|
+
loadbalance: this._loadbalance || void 0,
|
|
28409
28473
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
28410
28474
|
});
|
|
28411
28475
|
}
|
|
@@ -28456,6 +28520,8 @@ ${this.formatConnectionInfo()}`);
|
|
|
28456
28520
|
authKey: state.authKey,
|
|
28457
28521
|
stateDir,
|
|
28458
28522
|
margin: state.margin,
|
|
28523
|
+
passthrough: state.passthrough,
|
|
28524
|
+
loadbalance: state.loadbalance,
|
|
28459
28525
|
...options
|
|
28460
28526
|
}, nexusTool);
|
|
28461
28527
|
gateway._peerId = state.peerId;
|
|
@@ -28575,8 +28641,9 @@ ${this.formatConnectionInfo()}`);
|
|
|
28575
28641
|
formatConnectionInfo() {
|
|
28576
28642
|
const endpointCmd = `/endpoint ${this._peerId ?? "<peer-id>"} --auth ${this._authKey}`;
|
|
28577
28643
|
const lines = [];
|
|
28578
|
-
|
|
28579
|
-
lines.push(` ${c2.
|
|
28644
|
+
const modeLabel = this._passthrough ? "libp2p passthrough" : "libp2p";
|
|
28645
|
+
lines.push(` ${c2.green("\u25CF")} Expose gateway active ${c2.dim(`(${modeLabel})`)}`);
|
|
28646
|
+
lines.push(` ${c2.cyan("Backend".padEnd(12))} ${this._passthrough ? "passthrough" : this._kind} (${this._targetUrl})`);
|
|
28580
28647
|
lines.push(` ${c2.cyan("Transport".padEnd(12))} ${c2.bold("libp2p")} (DHT + mDNS + NATS relay)`);
|
|
28581
28648
|
lines.push(` ${c2.cyan("Peer ID".padEnd(12))} ${c2.bold(this._peerId ?? "connecting...")}`);
|
|
28582
28649
|
lines.push(` ${c2.cyan("Auth Key".padEnd(12))} ${c2.bold(this._authKey)}`);
|
|
@@ -28607,9 +28674,10 @@ ${this.formatConnectionInfo()}`);
|
|
|
28607
28674
|
const lines = [];
|
|
28608
28675
|
const uptimeSec = Math.floor((Date.now() - s.startedAt) / 1e3);
|
|
28609
28676
|
const uptimeStr = uptimeSec < 60 ? `${uptimeSec}s` : uptimeSec < 3600 ? `${Math.floor(uptimeSec / 60)}m ${uptimeSec % 60}s` : `${Math.floor(uptimeSec / 3600)}h ${Math.floor(uptimeSec % 3600 / 60)}m`;
|
|
28610
|
-
|
|
28677
|
+
const modeTag = this._passthrough ? c2.dim("(libp2p passthrough)") : c2.dim("(libp2p)");
|
|
28678
|
+
lines.push(` ${c2.bold("Expose Gateway Stats")} ${modeTag}`);
|
|
28611
28679
|
lines.push(` ${c2.cyan("Status".padEnd(18))} ${s.status === "active" ? c2.green("active") : s.status === "error" ? c2.red("error") : c2.yellow("standby")}`);
|
|
28612
|
-
lines.push(` ${c2.cyan("Transport".padEnd(18))} libp2p`);
|
|
28680
|
+
lines.push(` ${c2.cyan("Transport".padEnd(18))} libp2p${this._passthrough ? " (passthrough)" : ""}`);
|
|
28613
28681
|
lines.push(` ${c2.cyan("Peer ID".padEnd(18))} ${this._peerId ?? "n/a"}`);
|
|
28614
28682
|
lines.push(` ${c2.cyan("Uptime".padEnd(18))} ${uptimeStr}`);
|
|
28615
28683
|
lines.push(` ${c2.cyan("Total requests".padEnd(18))} ${s.totalRequests}`);
|
|
@@ -28617,10 +28685,16 @@ ${this.formatConnectionInfo()}`);
|
|
|
28617
28685
|
lines.push(` ${c2.cyan("Errors".padEnd(18))} ${s.errors}`);
|
|
28618
28686
|
lines.push(` ${c2.cyan("Tokens in".padEnd(18))} ${fmtTokens(s.totalTokensIn)}`);
|
|
28619
28687
|
lines.push(` ${c2.cyan("Tokens out".padEnd(18))} ${fmtTokens(s.totalTokensOut)}`);
|
|
28620
|
-
if (s.
|
|
28688
|
+
if (s.budgetTokensTotal > 0) {
|
|
28689
|
+
const pct = Math.round(s.budgetTokensRemaining / s.budgetTokensTotal * 100);
|
|
28690
|
+
const budgetColor = pct > 50 ? c2.green : pct > 20 ? c2.yellow : c2.red;
|
|
28691
|
+
lines.push(` ${c2.cyan("Budget".padEnd(18))} ${budgetColor(fmtTokens(s.budgetTokensRemaining))}${c2.dim("/")}${fmtTokens(s.budgetTokensTotal)} ${c2.dim(`(${pct}% left)`)}`);
|
|
28692
|
+
}
|
|
28693
|
+
const visibleModels = Array.from(s.modelUsage.entries()).filter(([model]) => !INTERNAL_CAPABILITIES.has(model));
|
|
28694
|
+
if (visibleModels.length > 0) {
|
|
28621
28695
|
lines.push("");
|
|
28622
28696
|
lines.push(` ${c2.bold("Models")}`);
|
|
28623
|
-
for (const [model, count] of
|
|
28697
|
+
for (const [model, count] of visibleModels) {
|
|
28624
28698
|
let mIn = 0, mOut = 0;
|
|
28625
28699
|
for (const user of s.users.values()) {
|
|
28626
28700
|
const mm = user.models.get(model);
|
|
@@ -28643,17 +28717,29 @@ ${this.formatConnectionInfo()}`);
|
|
|
28643
28717
|
lines.push(` ${c2.bold(user.ip)}`);
|
|
28644
28718
|
lines.push(` ${c2.dim("Session:")} ${ageStr} ${c2.dim("Last seen:")} ${lastStr} ${c2.dim("Requests:")} ${user.requests}`);
|
|
28645
28719
|
lines.push(` ${c2.dim("Tokens:")} in:${fmtTokens(user.tokensIn)} out:${fmtTokens(user.tokensOut)}`);
|
|
28646
|
-
|
|
28647
|
-
|
|
28648
|
-
for (const [m, mm] of
|
|
28649
|
-
|
|
28720
|
+
const userModels = Array.from(user.models.entries()).filter(([m]) => !INTERNAL_CAPABILITIES.has(m));
|
|
28721
|
+
if (userModels.length > 0) {
|
|
28722
|
+
for (const [m, mm] of userModels) {
|
|
28723
|
+
lines.push(` ${c2.dim("\xB7")} ${c2.cyan(m)} ${c2.dim(`${mm.requests}req ${fmtTokens(mm.tokensIn + mm.tokensOut)}tok`)}`);
|
|
28650
28724
|
}
|
|
28651
|
-
lines.push(` ${c2.dim("Models:")} ${modelParts.join(", ")}`);
|
|
28652
28725
|
}
|
|
28653
28726
|
}
|
|
28654
28727
|
}
|
|
28655
28728
|
return lines.join("\n");
|
|
28656
28729
|
}
|
|
28730
|
+
/** Update budget from rate limit headers */
|
|
28731
|
+
updateBudget(remaining, total, resetAt) {
|
|
28732
|
+
this._stats.budgetTokensRemaining = remaining;
|
|
28733
|
+
this._stats.budgetTokensTotal = total;
|
|
28734
|
+
this._stats.budgetResetAt = resetAt;
|
|
28735
|
+
this.emitStats();
|
|
28736
|
+
}
|
|
28737
|
+
get isPassthrough() {
|
|
28738
|
+
return this._passthrough;
|
|
28739
|
+
}
|
|
28740
|
+
get isLoadbalance() {
|
|
28741
|
+
return this._loadbalance;
|
|
28742
|
+
}
|
|
28657
28743
|
};
|
|
28658
28744
|
}
|
|
28659
28745
|
});
|
|
@@ -33519,25 +33605,44 @@ async function handleSlashCommand(input, ctx) {
|
|
|
33519
33605
|
}
|
|
33520
33606
|
return "handled";
|
|
33521
33607
|
}
|
|
33608
|
+
if (arg === "config") {
|
|
33609
|
+
await handleExposeConfig(ctx);
|
|
33610
|
+
return "handled";
|
|
33611
|
+
}
|
|
33612
|
+
if (arg?.startsWith("passthrough") || arg?.startsWith("forward")) {
|
|
33613
|
+
const ptParts = (arg ?? "").split(/\s+/);
|
|
33614
|
+
const ptLoadbalance = ptParts.includes("--loadbalance");
|
|
33615
|
+
try {
|
|
33616
|
+
const tunnelUrl = await ctx.exposeStart("passthrough", void 0, "libp2p", false, true, ptLoadbalance);
|
|
33617
|
+
if (tunnelUrl) {
|
|
33618
|
+
const info = ctx.getExposeInfo?.();
|
|
33619
|
+
if (info) {
|
|
33620
|
+
process.stdout.write("\n" + info.stats + "\n\n");
|
|
33621
|
+
}
|
|
33622
|
+
} else {
|
|
33623
|
+
renderError("Failed to start passthrough expose gateway.");
|
|
33624
|
+
}
|
|
33625
|
+
} catch (err) {
|
|
33626
|
+
renderError(`Expose passthrough error: ${err instanceof Error ? err.message : String(err)}`);
|
|
33627
|
+
}
|
|
33628
|
+
return "handled";
|
|
33629
|
+
}
|
|
33522
33630
|
if (!arg) {
|
|
33523
33631
|
const info = ctx.getExposeInfo?.();
|
|
33524
33632
|
if (info && ctx.isExposeActive?.()) {
|
|
33525
33633
|
process.stdout.write("\n" + info.stats + "\n\n");
|
|
33526
33634
|
} else {
|
|
33527
33635
|
renderInfo("Usage: /expose <backend> [options]");
|
|
33528
|
-
renderInfo(" /expose ollama
|
|
33529
|
-
renderInfo(" /expose ollama --tunnel
|
|
33530
|
-
renderInfo(" /expose
|
|
33531
|
-
renderInfo(" /expose
|
|
33532
|
-
renderInfo(" /expose
|
|
33533
|
-
renderInfo(" /expose
|
|
33534
|
-
renderInfo(" /expose stop
|
|
33535
|
-
renderInfo(" /expose
|
|
33536
|
-
renderInfo("
|
|
33537
|
-
renderInfo("
|
|
33538
|
-
renderInfo(" --key <key> Use specific auth key (tunnel mode)");
|
|
33539
|
-
renderInfo(" --key Auto-generate auth key (tunnel mode)");
|
|
33540
|
-
renderInfo(" --full Allow full Ollama API access (pull/delete/etc)");
|
|
33636
|
+
renderInfo(" /expose ollama Expose local Ollama via libp2p");
|
|
33637
|
+
renderInfo(" /expose ollama --tunnel Expose via cloudflared tunnel");
|
|
33638
|
+
renderInfo(" /expose vllm Expose local vLLM (8000)");
|
|
33639
|
+
renderInfo(" /expose passthrough --libp2p Forward /endpoint through libp2p");
|
|
33640
|
+
renderInfo(" /expose forward --loadbalance Forward with rate-limit distribution");
|
|
33641
|
+
renderInfo(" /expose config Configure expose settings");
|
|
33642
|
+
renderInfo(" /expose stop Stop all gateways");
|
|
33643
|
+
renderInfo(" /expose status Show usage stats");
|
|
33644
|
+
renderInfo(" --key <key> Use specific auth key (tunnel mode)");
|
|
33645
|
+
renderInfo(" --full Full Ollama API access");
|
|
33541
33646
|
}
|
|
33542
33647
|
return "handled";
|
|
33543
33648
|
}
|
|
@@ -34168,6 +34273,92 @@ function applyConfigChanges(ctx, changes) {
|
|
|
34168
34273
|
if (changes.commandsMode !== void 0)
|
|
34169
34274
|
ctx.setCommandsMode?.(changes.commandsMode);
|
|
34170
34275
|
}
|
|
34276
|
+
async function handleExposeConfig(ctx) {
|
|
34277
|
+
const info = ctx.getExposeInfo?.();
|
|
34278
|
+
const isActive = ctx.isExposeActive?.() ?? false;
|
|
34279
|
+
const items = [
|
|
34280
|
+
{ key: "hdr_status", label: "Status", kind: "header" },
|
|
34281
|
+
{ key: "status", label: isActive ? "Active" : "Inactive", detail: isActive ? "Gateway is running" : "No gateway active", kind: "info" }
|
|
34282
|
+
];
|
|
34283
|
+
if (isActive && info) {
|
|
34284
|
+
items.push({ key: "view_stats", label: "View Stats", detail: "Show current gateway statistics", kind: "action" });
|
|
34285
|
+
items.push({ key: "stop_all", label: "Stop All Gateways", detail: "Shut down all expose transports", kind: "action" });
|
|
34286
|
+
}
|
|
34287
|
+
items.push({ key: "hdr_start", label: "Start Gateway", kind: "header" });
|
|
34288
|
+
items.push({ key: "start_ollama", label: "Expose Ollama (libp2p)", detail: "Expose local Ollama via P2P network", kind: "action" });
|
|
34289
|
+
items.push({ key: "start_ollama_tunnel", label: "Expose Ollama (tunnel)", detail: "Expose via cloudflared public tunnel", kind: "action" });
|
|
34290
|
+
items.push({ key: "start_passthrough", label: "Passthrough /endpoint (libp2p)", detail: "Forward configured endpoint through P2P", kind: "action" });
|
|
34291
|
+
items.push({ key: "start_passthrough_lb", label: "Passthrough + Load Balance", detail: "Forward with distributed rate-limit budget", kind: "action" });
|
|
34292
|
+
items.push({ key: "start_vllm", label: "Expose vLLM (libp2p)", detail: "Expose local vLLM server (port 8000)", kind: "action" });
|
|
34293
|
+
const skipKeys = items.filter((e) => e.kind === "header" || e.kind === "info").map((e) => e.key);
|
|
34294
|
+
const result = await tuiSelect({
|
|
34295
|
+
items,
|
|
34296
|
+
title: "Expose Configuration",
|
|
34297
|
+
rl: ctx.rl,
|
|
34298
|
+
skipKeys
|
|
34299
|
+
});
|
|
34300
|
+
if (!result.confirmed || !result.key) {
|
|
34301
|
+
renderInfo("Expose config cancelled.");
|
|
34302
|
+
return;
|
|
34303
|
+
}
|
|
34304
|
+
switch (result.key) {
|
|
34305
|
+
case "view_stats": {
|
|
34306
|
+
const freshInfo = ctx.getExposeInfo?.();
|
|
34307
|
+
if (freshInfo)
|
|
34308
|
+
process.stdout.write("\n" + freshInfo.stats + "\n\n");
|
|
34309
|
+
break;
|
|
34310
|
+
}
|
|
34311
|
+
case "stop_all":
|
|
34312
|
+
await ctx.exposeStop?.();
|
|
34313
|
+
renderInfo("All expose gateways stopped.");
|
|
34314
|
+
break;
|
|
34315
|
+
case "start_ollama": {
|
|
34316
|
+
const url = await ctx.exposeStart?.("ollama", void 0, "libp2p");
|
|
34317
|
+
if (url) {
|
|
34318
|
+
const i = ctx.getExposeInfo?.();
|
|
34319
|
+
if (i)
|
|
34320
|
+
process.stdout.write("\n" + i.stats + "\n\n");
|
|
34321
|
+
}
|
|
34322
|
+
break;
|
|
34323
|
+
}
|
|
34324
|
+
case "start_ollama_tunnel": {
|
|
34325
|
+
const url = await ctx.exposeStart?.("ollama", "", "tunnel");
|
|
34326
|
+
if (url) {
|
|
34327
|
+
const i = ctx.getExposeInfo?.();
|
|
34328
|
+
if (i)
|
|
34329
|
+
process.stdout.write("\n" + i.stats + "\n\n");
|
|
34330
|
+
}
|
|
34331
|
+
break;
|
|
34332
|
+
}
|
|
34333
|
+
case "start_passthrough": {
|
|
34334
|
+
const url = await ctx.exposeStart?.("passthrough", void 0, "libp2p", false, true, false);
|
|
34335
|
+
if (url) {
|
|
34336
|
+
const i = ctx.getExposeInfo?.();
|
|
34337
|
+
if (i)
|
|
34338
|
+
process.stdout.write("\n" + i.stats + "\n\n");
|
|
34339
|
+
}
|
|
34340
|
+
break;
|
|
34341
|
+
}
|
|
34342
|
+
case "start_passthrough_lb": {
|
|
34343
|
+
const url = await ctx.exposeStart?.("passthrough", void 0, "libp2p", false, true, true);
|
|
34344
|
+
if (url) {
|
|
34345
|
+
const i = ctx.getExposeInfo?.();
|
|
34346
|
+
if (i)
|
|
34347
|
+
process.stdout.write("\n" + i.stats + "\n\n");
|
|
34348
|
+
}
|
|
34349
|
+
break;
|
|
34350
|
+
}
|
|
34351
|
+
case "start_vllm": {
|
|
34352
|
+
const url = await ctx.exposeStart?.("vllm", void 0, "libp2p");
|
|
34353
|
+
if (url) {
|
|
34354
|
+
const i = ctx.getExposeInfo?.();
|
|
34355
|
+
if (i)
|
|
34356
|
+
process.stdout.write("\n" + i.stats + "\n\n");
|
|
34357
|
+
}
|
|
34358
|
+
break;
|
|
34359
|
+
}
|
|
34360
|
+
}
|
|
34361
|
+
}
|
|
34171
34362
|
async function showModelPicker(ctx, local = false) {
|
|
34172
34363
|
try {
|
|
34173
34364
|
const models = await fetchModels(ctx.config.backendUrl, ctx.config.apiKey);
|
|
@@ -44237,14 +44428,16 @@ var init_status_bar = __esm({
|
|
|
44237
44428
|
}
|
|
44238
44429
|
}
|
|
44239
44430
|
if (this._expose) {
|
|
44431
|
+
const INTERNAL_CAPS = /* @__PURE__ */ new Set(["system_metrics", "__list_capabilities"]);
|
|
44240
44432
|
const statusEmoji = this._expose.status === "active" ? "\u{1F7E2}" : this._expose.status === "error" ? "\u{1F534}" : "\u{1F7E0}";
|
|
44241
|
-
const models = Array.from(this._expose.modelUsage.keys());
|
|
44433
|
+
const models = Array.from(this._expose.modelUsage.keys()).filter((m2) => !INTERNAL_CAPS.has(m2));
|
|
44242
44434
|
const modelParams = models.map((mdl) => {
|
|
44243
44435
|
const pm = mdl.match(/(\d+[bBmM])/);
|
|
44244
44436
|
return pm ? pm[1] : mdl.split(":")[0]?.split("/").pop() ?? mdl;
|
|
44245
44437
|
});
|
|
44246
44438
|
const modelStr = modelParams.join(",");
|
|
44247
|
-
const
|
|
44439
|
+
const visibleReqs = Array.from(this._expose.modelUsage.entries()).filter(([m2]) => !INTERNAL_CAPS.has(m2)).reduce((sum, [, n]) => sum + n, 0);
|
|
44440
|
+
const reqStr = visibleReqs > 0 ? ` ${visibleReqs}req` : "";
|
|
44248
44441
|
const connStr = this._expose.activeConnections > 0 ? ` ${this._expose.activeConnections}conn` : "";
|
|
44249
44442
|
const exposeExpanded = statusEmoji + pastel2(183, (modelStr ? " " + modelStr : "") + reqStr + connStr);
|
|
44250
44443
|
const exposeCompact = statusEmoji + (modelStr ? pastel2(183, " " + modelStr) : "");
|
|
@@ -46689,11 +46882,15 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
|
|
|
46689
46882
|
getCallUrl() {
|
|
46690
46883
|
return voiceSession?.tunnelUrl ?? null;
|
|
46691
46884
|
},
|
|
46692
|
-
async exposeStart(kindOrUrl, authKey, transport, fullAccess) {
|
|
46693
|
-
const knownKinds = ["ollama", "vllm", "llvm"];
|
|
46885
|
+
async exposeStart(kindOrUrl, authKey, transport, fullAccess, passthrough, loadbalance) {
|
|
46886
|
+
const knownKinds = ["ollama", "vllm", "llvm", "passthrough"];
|
|
46694
46887
|
let kind;
|
|
46695
46888
|
let targetUrl;
|
|
46696
|
-
if (
|
|
46889
|
+
if (kindOrUrl === "passthrough" || passthrough) {
|
|
46890
|
+
kind = "custom";
|
|
46891
|
+
targetUrl = config.backendUrl;
|
|
46892
|
+
passthrough = true;
|
|
46893
|
+
} else if (knownKinds.includes(kindOrUrl)) {
|
|
46697
46894
|
kind = kindOrUrl;
|
|
46698
46895
|
} else {
|
|
46699
46896
|
kind = "custom";
|
|
@@ -46706,7 +46903,7 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
|
|
|
46706
46903
|
p2pGateway = null;
|
|
46707
46904
|
}
|
|
46708
46905
|
const nexusTool = new NexusTool(repoRoot);
|
|
46709
|
-
let exposeSpinnerMsg = "Connecting to nexus P2P network...";
|
|
46906
|
+
let exposeSpinnerMsg = passthrough ? `Connecting to nexus P2P network (passthrough \u2192 ${targetUrl})...` : "Connecting to nexus P2P network...";
|
|
46710
46907
|
const exposeFrames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
46711
46908
|
let exposeFrame = 0;
|
|
46712
46909
|
const exposeSpinner = setInterval(() => {
|
|
@@ -46718,6 +46915,8 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
|
|
|
46718
46915
|
targetUrl,
|
|
46719
46916
|
authKey,
|
|
46720
46917
|
stateDir: join50(repoRoot, ".oa"),
|
|
46918
|
+
passthrough: passthrough ?? false,
|
|
46919
|
+
loadbalance: loadbalance ?? false,
|
|
46721
46920
|
onInfo: (msg) => {
|
|
46722
46921
|
exposeSpinnerMsg = msg;
|
|
46723
46922
|
},
|
package/package.json
CHANGED