open-agents-ai 0.103.36 → 0.103.38

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 +116 -32
  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
@@ -12607,30 +12607,74 @@ async function handleCmd(cmd) {
12607
12607
  dlog('expose: auth key configured (' + exposeAuthKey.length + ' chars)');
12608
12608
  }
12609
12609
 
12610
- // Query Ollama for available models
12610
+ // Passthrough mode: forward from a remote /endpoint (Chutes, Groq, etc.)
12611
+ var isPassthrough = args.passthrough === 'true';
12612
+ var endpointAuth = args.endpoint_auth || '';
12613
+ if (isPassthrough) {
12614
+ dlog('expose: PASSTHROUGH mode \u2014 forwarding from upstream endpoint');
12615
+ }
12616
+
12617
+ // Query models from the backend
12611
12618
  const ollamaUrl = args.ollama_url || process.env.OLLAMA_URL || 'http://localhost:11434';
12612
12619
  let models = [];
12613
- try {
12614
- const tagsResp = await fetch(ollamaUrl + '/api/tags');
12615
- if (tagsResp.ok) {
12616
- const tagsData = await tagsResp.json();
12617
- models = (tagsData.models || []).map(m => ({
12618
- name: m.name,
12619
- size: m.size || 0,
12620
- parameterSize: m.details?.parameter_size || '',
12621
- family: m.details?.family || '',
12622
- quantization: m.details?.quantization_level || '',
12623
- }));
12620
+
12621
+ if (isPassthrough) {
12622
+ // Passthrough: query /v1/models from the upstream endpoint (OpenAI-compatible)
12623
+ try {
12624
+ var modelsHeaders = { 'Content-Type': 'application/json' };
12625
+ if (endpointAuth) modelsHeaders['Authorization'] = 'Bearer ' + endpointAuth;
12626
+ var modelsUrl = ollamaUrl.replace(/\\/+$/, '') + '/v1/models';
12627
+ dlog('expose: passthrough querying models from ' + modelsUrl);
12628
+ const modelsResp = await fetch(modelsUrl, { headers: modelsHeaders });
12629
+ if (modelsResp.ok) {
12630
+ const modelsData = await modelsResp.json();
12631
+ var modelList = modelsData.data || modelsData.models || [];
12632
+ models = modelList.map(function(m) {
12633
+ return {
12634
+ name: m.id || m.name || 'unknown',
12635
+ size: 0,
12636
+ parameterSize: '',
12637
+ family: m.owned_by || '',
12638
+ quantization: '',
12639
+ };
12640
+ });
12641
+ } else {
12642
+ var errText = '';
12643
+ try { errText = await modelsResp.text(); } catch {}
12644
+ writeResp(id, { ok: false, output: 'Upstream /v1/models returned ' + modelsResp.status + ': ' + errText.slice(0, 200) });
12645
+ return;
12646
+ }
12647
+ } catch (e) {
12648
+ writeResp(id, { ok: false, output: 'Cannot reach upstream endpoint at ' + ollamaUrl + ': ' + e.message });
12649
+ return;
12650
+ }
12651
+ } else {
12652
+ // Local Ollama: query /api/tags
12653
+ try {
12654
+ const tagsResp = await fetch(ollamaUrl + '/api/tags');
12655
+ if (tagsResp.ok) {
12656
+ const tagsData = await tagsResp.json();
12657
+ models = (tagsData.models || []).map(function(m) {
12658
+ return {
12659
+ name: m.name,
12660
+ size: m.size || 0,
12661
+ parameterSize: m.details ? m.details.parameter_size || '' : '',
12662
+ family: m.details ? m.details.family || '' : '',
12663
+ quantization: m.details ? m.details.quantization_level || '' : '',
12664
+ };
12665
+ });
12666
+ }
12667
+ } catch (e) {
12668
+ writeResp(id, { ok: false, output: 'Cannot reach Ollama at ' + ollamaUrl + ': ' + e.message });
12669
+ return;
12624
12670
  }
12625
- } catch (e) {
12626
- writeResp(id, { ok: false, output: 'Cannot reach Ollama at ' + ollamaUrl + ': ' + e.message });
12627
- return;
12628
12671
  }
12629
12672
 
12630
12673
  if (models.length === 0) {
12631
- writeResp(id, { ok: false, output: 'No models found on Ollama. Pull a model first.' });
12674
+ writeResp(id, { ok: false, output: isPassthrough ? 'No models found on upstream endpoint.' : 'No models found on Ollama. Pull a model first.' });
12632
12675
  return;
12633
12676
  }
12677
+ dlog('expose: found ' + models.length + ' models' + (isPassthrough ? ' (passthrough)' : ''));
12634
12678
 
12635
12679
  // Fetch market rates from OpenRouter (free, no auth)
12636
12680
  let marketRates = {};
@@ -12782,10 +12826,12 @@ async function handleCmd(cmd) {
12782
12826
  if (parsedReq.temperature !== undefined) chatBody.temperature = parsedReq.temperature;
12783
12827
  if (parsedReq.max_tokens) chatBody.max_tokens = parsedReq.max_tokens;
12784
12828
 
12785
- dlog('expose: calling /v1/chat/completions with ' + chatBody.messages.length + ' messages, tools=' + (chatBody.tools ? chatBody.tools.length : 0));
12829
+ var chatHeaders = { 'Content-Type': 'application/json' };
12830
+ if (isPassthrough && endpointAuth) chatHeaders['Authorization'] = 'Bearer ' + endpointAuth;
12831
+ dlog('expose: calling /v1/chat/completions with ' + chatBody.messages.length + ' messages, tools=' + (chatBody.tools ? chatBody.tools.length : 0) + (isPassthrough ? ' (passthrough)' : ''));
12786
12832
  genResp = await fetch(ollamaUrl + '/v1/chat/completions', {
12787
12833
  method: 'POST',
12788
- headers: { 'Content-Type': 'application/json' },
12834
+ headers: chatHeaders,
12789
12835
  body: JSON.stringify(chatBody),
12790
12836
  });
12791
12837
  genData = await genResp.json();
@@ -12816,8 +12862,36 @@ async function handleCmd(cmd) {
12816
12862
  usage: { input_tokens: inputTokens, output_tokens: outputTokens },
12817
12863
  pricing: entry.pricing,
12818
12864
  };
12865
+ } else if (isPassthrough) {
12866
+ // Passthrough flat prompt \u2014 convert to /v1/chat/completions (upstream has no /api/generate)
12867
+ var flatPrompt = parsedReq && parsedReq.prompt ? parsedReq.prompt : (prompt || 'Hello');
12868
+ var ptHeaders = { 'Content-Type': 'application/json' };
12869
+ if (endpointAuth) ptHeaders['Authorization'] = 'Bearer ' + endpointAuth;
12870
+ genResp = await fetch(ollamaUrl + '/v1/chat/completions', {
12871
+ method: 'POST',
12872
+ headers: ptHeaders,
12873
+ body: JSON.stringify({
12874
+ model: model.name,
12875
+ messages: [{ role: 'user', content: flatPrompt }],
12876
+ stream: false,
12877
+ }),
12878
+ });
12879
+ genData = await genResp.json();
12880
+ var ptChoices = genData.choices || [];
12881
+ var ptMsg = (ptChoices[0] || {}).message || {};
12882
+ output = (ptMsg.content || '').replace(/<think>[\\s\\S]*?<\\/think>/g, '').trim();
12883
+ inputTokens = (genData.usage || {}).prompt_tokens || 0;
12884
+ outputTokens = (genData.usage || {}).completion_tokens || 0;
12885
+
12886
+ responsePayload = {
12887
+ model: model.name,
12888
+ response: output,
12889
+ choices: ptChoices,
12890
+ usage: { input_tokens: inputTokens, output_tokens: outputTokens },
12891
+ pricing: entry.pricing,
12892
+ };
12819
12893
  } else {
12820
- // Flat prompt \u2014 use /api/generate (original behavior)
12894
+ // Flat prompt \u2014 use /api/generate (local Ollama)
12821
12895
  var flatPrompt = parsedReq && parsedReq.prompt ? parsedReq.prompt : (prompt || 'Hello');
12822
12896
  genResp = await fetch(ollamaUrl + '/api/generate', {
12823
12897
  method: 'POST',
@@ -26371,10 +26445,15 @@ function renderSlashHelp() {
26371
26445
  ["/nexus", "Show nexus P2P network status (or connect/disconnect/rooms)"],
26372
26446
  ["/nexus connect", "Connect to the agent mesh network"],
26373
26447
  ["/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"],
26448
+ ["/expose <backend>", "Expose local inference via libp2p (default)"],
26449
+ ["/expose <backend> --tunnel", "Expose via cloudflared tunnel"],
26450
+ ["/expose passthrough", "Forward configured /endpoint through libp2p P2P"],
26451
+ ["/expose forward --loadbalance", "Passthrough with distributed rate-limit budget"],
26452
+ ["/expose config", "Interactive expose configuration menu"],
26453
+ ["/expose status", "Show gateway stats (requests, models, budget)"],
26454
+ ["/expose stop", "Stop all expose gateways"],
26455
+ ["/expose stop --tunnel", "Stop tunnel gateway only"],
26456
+ ["/expose stop --libp2p", "Stop libp2p gateway only"],
26378
26457
  ["/p2p start", "Join the P2P agent mesh network"],
26379
26458
  ["/p2p status", "Show mesh peers and rooms"],
26380
26459
  ["/p2p stop", "Leave the mesh network"],
@@ -28353,6 +28432,7 @@ ${this.formatConnectionInfo()}`);
28353
28432
  _margin;
28354
28433
  _passthrough = false;
28355
28434
  _loadbalance = false;
28435
+ _endpointAuth;
28356
28436
  _pollTimer = null;
28357
28437
  _stats = {
28358
28438
  status: "standby",
@@ -28397,6 +28477,7 @@ ${this.formatConnectionInfo()}`);
28397
28477
  this._margin = options.margin ?? 0.5;
28398
28478
  this._passthrough = options.passthrough ?? false;
28399
28479
  this._loadbalance = options.loadbalance ?? false;
28480
+ this._endpointAuth = options.endpointAuth;
28400
28481
  this._onInfo = options.onInfo;
28401
28482
  this._onError = options.onError;
28402
28483
  if (options.authKey === void 0 || options.authKey === "") {
@@ -28416,21 +28497,23 @@ ${this.formatConnectionInfo()}`);
28416
28497
  throw new Error(`Nexus daemon connect failed: ${connectResult.error}`);
28417
28498
  }
28418
28499
  await new Promise((r) => setTimeout(r, 500));
28419
- this._onInfo?.("Registering inference capabilities...");
28420
- let exposeResult = await this._nexusTool.execute({
28500
+ this._onInfo?.(this._passthrough ? `Registering passthrough capabilities (\u2192 ${this._targetUrl})...` : "Registering inference capabilities...");
28501
+ const exposeArgs = {
28421
28502
  action: "expose",
28422
28503
  ollama_url: this._targetUrl,
28423
28504
  margin: String(this._margin),
28424
28505
  auth_key: this._authKey
28425
- });
28506
+ };
28507
+ if (this._passthrough) {
28508
+ exposeArgs.passthrough = "true";
28509
+ if (this._endpointAuth) {
28510
+ exposeArgs.endpoint_auth = this._endpointAuth;
28511
+ }
28512
+ }
28513
+ let exposeResult = await this._nexusTool.execute(exposeArgs);
28426
28514
  if (!exposeResult.success && exposeResult.error?.includes("not running")) {
28427
28515
  await new Promise((r) => setTimeout(r, 1500));
28428
- exposeResult = await this._nexusTool.execute({
28429
- action: "expose",
28430
- ollama_url: this._targetUrl,
28431
- margin: String(this._margin),
28432
- auth_key: this._authKey
28433
- });
28516
+ exposeResult = await this._nexusTool.execute(exposeArgs);
28434
28517
  }
28435
28518
  if (!exposeResult.success) {
28436
28519
  throw new Error(`Expose failed: ${exposeResult.error}`);
@@ -46912,6 +46995,7 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
46912
46995
  stateDir: join50(repoRoot, ".oa"),
46913
46996
  passthrough: passthrough ?? false,
46914
46997
  loadbalance: loadbalance ?? false,
46998
+ endpointAuth: passthrough ? config.apiKey : void 0,
46915
46999
  onInfo: (msg) => {
46916
47000
  exposeSpinnerMsg = msg;
46917
47001
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.103.36",
3
+ "version": "0.103.38",
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",