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.
Files changed (3) hide show
  1. package/README.md +89 -4
  2. package/dist/index.js +243 -44
  3. 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** — `/p2p` mesh networking with secret-safe variable placeholders (`{{OA_VAR_*}}`), trust tiers (LOCAL/TEE/VERIFIED/PUBLIC), WebSocket peer mesh, and inference routing with automatic secret redaction/injection
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 Ollama models as metered inference capabilities with OpenRouter-based pricing |
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 stop` | Stop the expose gateway |
960
- | `/expose status` | Show expose usage stats |
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>", "Share local inference via cloudflared tunnel"],
26375
- ["/expose <backend> --key", "Share with auto-generated auth key"],
26376
- ["/expose status", "Show gateway stats (requests, models, errors)"],
26377
- ["/expose stop", "Stop the expose gateway"],
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.modelUsage.size > 0) {
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 s.modelUsage) {
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
- if (user.models.size > 0) {
28281
- const modelParts = [];
28282
- for (const [m, mm] of user.models) {
28283
- modelParts.push(`${m} (${mm.requests} reqs, ${fmtTokens(mm.tokensIn + mm.tokensOut)} tokens)`);
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
- lines.push(` ${c2.green("\u25CF")} Expose gateway active ${c2.dim("(libp2p)")}`);
28579
- lines.push(` ${c2.cyan("Backend".padEnd(12))} ${this._kind} (${this._targetUrl})`);
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
- lines.push(` ${c2.bold("Expose Gateway Stats")} ${c2.dim("(libp2p)")}`);
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.modelUsage.size > 0) {
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 s.modelUsage) {
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
- if (user.models.size > 0) {
28647
- const modelParts = [];
28648
- for (const [m, mm] of user.models) {
28649
- modelParts.push(`${m} (${mm.requests} reqs, ${fmtTokens(mm.tokensIn + mm.tokensOut)} tokens)`);
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 Expose local Ollama via libp2p (default)");
33529
- renderInfo(" /expose ollama --tunnel Expose via cloudflared tunnel");
33530
- renderInfo(" /expose ollama --libp2p Expose via libp2p (explicit)");
33531
- renderInfo(" /expose vllm Expose local vLLM (8000)");
33532
- renderInfo(" /expose llvm Expose local LLVM (8080)");
33533
- renderInfo(" /expose host:port Expose custom address");
33534
- renderInfo(" /expose stop Stop all gateways");
33535
- renderInfo(" /expose stop --tunnel Stop tunnel only");
33536
- renderInfo(" /expose stop --libp2p Stop libp2p only");
33537
- renderInfo(" /expose status Show usage stats");
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 reqStr = this._expose.totalRequests > 0 ? ` ${this._expose.totalRequests}req` : "";
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 (knownKinds.includes(kindOrUrl)) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.103.35",
3
+ "version": "0.103.37",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",