open-agents-ai 0.84.0 → 0.85.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/dist/index.js CHANGED
@@ -11551,6 +11551,207 @@ async function handleCmd(cmd) {
11551
11551
  break;
11552
11552
  }
11553
11553
 
11554
+ // \u2500\u2500 Metered Inference Exposure \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
11555
+ case 'expose': {
11556
+ // Query Ollama for available models
11557
+ const ollamaUrl = args.ollama_url || process.env.OLLAMA_URL || 'http://localhost:11434';
11558
+ let models = [];
11559
+ try {
11560
+ const tagsResp = await fetch(ollamaUrl + '/api/tags');
11561
+ if (tagsResp.ok) {
11562
+ const tagsData = await tagsResp.json();
11563
+ models = (tagsData.models || []).map(m => ({
11564
+ name: m.name,
11565
+ size: m.size || 0,
11566
+ parameterSize: m.details?.parameter_size || '',
11567
+ family: m.details?.family || '',
11568
+ quantization: m.details?.quantization_level || '',
11569
+ }));
11570
+ }
11571
+ } catch (e) {
11572
+ writeResp(id, { ok: false, output: 'Cannot reach Ollama at ' + ollamaUrl + ': ' + e.message });
11573
+ return;
11574
+ }
11575
+
11576
+ if (models.length === 0) {
11577
+ writeResp(id, { ok: false, output: 'No models found on Ollama. Pull a model first.' });
11578
+ return;
11579
+ }
11580
+
11581
+ // Fetch market rates from OpenRouter (free, no auth)
11582
+ let marketRates = {};
11583
+ try {
11584
+ const orResp = await fetch('https://openrouter.ai/api/v1/models');
11585
+ if (orResp.ok) {
11586
+ const orData = await orResp.json();
11587
+ for (const m of (orData.data || [])) {
11588
+ if (m.pricing) {
11589
+ marketRates[m.id] = {
11590
+ input: parseFloat(m.pricing.prompt || '0') * 1_000_000,
11591
+ output: parseFloat(m.pricing.completion || '0') * 1_000_000,
11592
+ };
11593
+ }
11594
+ }
11595
+ }
11596
+ } catch { /* offline \u2014 use zero rates */ }
11597
+
11598
+ // Build pricing menu \u2014 match local models to market rates
11599
+ const margin = parseFloat(args.margin || '0.5'); // default 50% of market rate
11600
+ const pricingMenu = [];
11601
+
11602
+ for (const model of models) {
11603
+ // Try to match to OpenRouter model ID
11604
+ const baseName = model.name.replace(/:latest$/, '').toLowerCase();
11605
+ let matched = null;
11606
+ for (const [orId, rates] of Object.entries(marketRates)) {
11607
+ const orBase = orId.toLowerCase();
11608
+ if (orBase.includes(baseName) || baseName.includes(orBase.split('/').pop())) {
11609
+ matched = { orId, ...rates };
11610
+ break;
11611
+ }
11612
+ }
11613
+
11614
+ const entry = {
11615
+ model: model.name,
11616
+ parameterSize: model.parameterSize,
11617
+ family: model.family,
11618
+ quantization: model.quantization,
11619
+ pricing: {
11620
+ input_per_1m_tokens: matched ? +(matched.input * margin).toFixed(4) : 0,
11621
+ output_per_1m_tokens: matched ? +(matched.output * margin).toFixed(4) : 0,
11622
+ currency: 'USD',
11623
+ source: matched ? 'openrouter:' + matched.orId : 'self-hosted:free',
11624
+ margin: margin,
11625
+ },
11626
+ };
11627
+ pricingMenu.push(entry);
11628
+
11629
+ // Register as nexus capability
11630
+ const capName = 'inference:' + model.name.replace(/[^a-zA-Z0-9._-]/g, '_');
11631
+ if (typeof nexus.registerCapability === 'function') {
11632
+ nexus.registerCapability(capName, async (request, stream) => {
11633
+ const logEntry = {
11634
+ ts: Date.now(),
11635
+ from: request.from || 'unknown',
11636
+ capability: capName,
11637
+ model: model.name,
11638
+ requestId: request.requestId,
11639
+ };
11640
+ const logFile = join(invocationsDir, Date.now() + '-' + capName + '.json');
11641
+ try { writeFileSync(logFile, JSON.stringify(logEntry, null, 2)); } catch {}
11642
+
11643
+ // Collect input
11644
+ let prompt = '';
11645
+ stream.onData((msg) => {
11646
+ if (msg.type === 'invoke.chunk') {
11647
+ prompt += (typeof msg.data === 'string' ? msg.data : JSON.stringify(msg.data));
11648
+ }
11649
+ });
11650
+
11651
+ // Accept
11652
+ await stream.write({
11653
+ type: 'invoke.accept', version: 1,
11654
+ requestId: request.requestId, accepted: true,
11655
+ });
11656
+
11657
+ // Forward to Ollama
11658
+ try {
11659
+ const genResp = await fetch(ollamaUrl + '/api/generate', {
11660
+ method: 'POST',
11661
+ headers: { 'Content-Type': 'application/json' },
11662
+ body: JSON.stringify({
11663
+ model: model.name,
11664
+ prompt: prompt || 'Hello',
11665
+ stream: false,
11666
+ }),
11667
+ });
11668
+ const genData = await genResp.json();
11669
+ const output = genData.response || '';
11670
+ const inputTokens = genData.prompt_eval_count || 0;
11671
+ const outputTokens = genData.eval_count || 0;
11672
+
11673
+ // Stream result back
11674
+ await stream.write({
11675
+ type: 'invoke.event', version: 1,
11676
+ requestId: request.requestId, seq: 0,
11677
+ event: 'result',
11678
+ data: JSON.stringify({
11679
+ model: model.name,
11680
+ response: output,
11681
+ usage: { input_tokens: inputTokens, output_tokens: outputTokens },
11682
+ pricing: entry.pricing,
11683
+ }),
11684
+ });
11685
+
11686
+ await stream.write({
11687
+ type: 'invoke.done', version: 1,
11688
+ requestId: request.requestId,
11689
+ usage: {
11690
+ inputBytes: prompt.length,
11691
+ outputBytes: output.length,
11692
+ tokens: inputTokens + outputTokens,
11693
+ },
11694
+ });
11695
+ } catch (e) {
11696
+ await stream.write({
11697
+ type: 'invoke.event', version: 1,
11698
+ requestId: request.requestId, seq: 0,
11699
+ event: 'error', data: 'Inference failed: ' + e.message,
11700
+ });
11701
+ await stream.write({
11702
+ type: 'invoke.done', version: 1,
11703
+ requestId: request.requestId,
11704
+ usage: { inputBytes: 0, outputBytes: 0 },
11705
+ });
11706
+ }
11707
+ stream.close();
11708
+ });
11709
+ }
11710
+ }
11711
+
11712
+ // Write pricing menu to file
11713
+ const pricingFile = join(nexusDir, 'pricing.json');
11714
+ writeFileSync(pricingFile, JSON.stringify({ updated: new Date().toISOString(), models: pricingMenu }, null, 2));
11715
+ writeStatus({ exposedModels: pricingMenu.length });
11716
+
11717
+ const lines = ['Exposed ' + pricingMenu.length + ' model(s) as nexus capabilities:'];
11718
+ for (const p of pricingMenu) {
11719
+ const cost = p.pricing.input_per_1m_tokens === 0
11720
+ ? 'FREE (self-hosted)'
11721
+ : '$' + p.pricing.input_per_1m_tokens + '/$' + p.pricing.output_per_1m_tokens + ' per 1M tokens';
11722
+ lines.push(' inference:' + p.model + ' \u2014 ' + cost);
11723
+ }
11724
+ lines.push('');
11725
+ lines.push('Pricing menu saved to ' + pricingFile);
11726
+ lines.push('Market rates: ' + Object.keys(marketRates).length + ' models from OpenRouter');
11727
+ writeResp(id, { ok: true, output: lines.join('\\n') });
11728
+ break;
11729
+ }
11730
+
11731
+ case 'pricing_menu': {
11732
+ const pricingFile = join(nexusDir, 'pricing.json');
11733
+ if (!existsSync(pricingFile)) {
11734
+ writeResp(id, { ok: false, output: 'No pricing menu. Run expose first.' });
11735
+ return;
11736
+ }
11737
+ try {
11738
+ const menu = JSON.parse(readFileSync(pricingFile, 'utf8'));
11739
+ const lines = ['Inference Pricing Menu (updated: ' + menu.updated + ')'];
11740
+ lines.push('');
11741
+ for (const m of (menu.models || [])) {
11742
+ const cost = m.pricing.input_per_1m_tokens === 0
11743
+ ? 'FREE'
11744
+ : '$' + m.pricing.input_per_1m_tokens + ' in / $' + m.pricing.output_per_1m_tokens + ' out per 1M tokens';
11745
+ lines.push(' ' + m.model + ' (' + m.parameterSize + ', ' + m.quantization + ')');
11746
+ lines.push(' ' + cost + ' [' + m.pricing.source + ']');
11747
+ }
11748
+ writeResp(id, { ok: true, output: lines.join('\\n') });
11749
+ } catch (e) {
11750
+ writeResp(id, { ok: false, output: 'Failed to read pricing menu: ' + e.message });
11751
+ }
11752
+ break;
11753
+ }
11754
+
11554
11755
  case 'ping': {
11555
11756
  writeResp(id, { ok: true, output: 'pong' });
11556
11757
  break;
@@ -11659,7 +11860,7 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
11659
11860
  ];
11660
11861
  NexusTool = class {
11661
11862
  name = "nexus";
11662
- description = "Decentralized agent-to-agent communication via open-agents-nexus v1.5.0. Spawns a background Node.js process with real network sockets for libp2p + NATS P2P mesh. Simple flow: connect \u2192 join room \u2192 send messages. Auto-installs/updates nexus if needed. v1.5.0: register_capability (serve invocations), block_peer/unblock_peer (trust policy), metering_status (usage tracking), room_members (live member tracking). Also supports direct peer invoke (streaming inference), IPFS content storage, direct messages, peer discovery, x402 micropayments, and inference proofs.";
11863
+ description = "Decentralized agent-to-agent communication via open-agents-nexus v1.5.0. Spawns a background Node.js process with real network sockets for libp2p + NATS P2P mesh. Simple flow: connect \u2192 join room \u2192 send messages. Auto-installs/updates nexus if needed. EXPOSE: expose action queries Ollama models, fetches OpenRouter market rates, registers metered inference capabilities so peers can invoke your models. pricing_menu shows rates. v1.5.0: register_capability, block_peer/unblock_peer, metering_status, room_members. Also supports direct peer invoke, IPFS storage, DMs, discovery, x402, inference proofs.";
11663
11864
  parameters = {
11664
11865
  type: "object",
11665
11866
  properties: {
@@ -11689,7 +11890,9 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
11689
11890
  "block_peer",
11690
11891
  "unblock_peer",
11691
11892
  "metering_status",
11692
- "room_members"
11893
+ "room_members",
11894
+ "expose",
11895
+ "pricing_menu"
11693
11896
  ],
11694
11897
  description: "The nexus action to perform"
11695
11898
  },
@@ -11736,6 +11939,14 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
11736
11939
  cid: {
11737
11940
  type: "string",
11738
11941
  description: "Content ID for retrieve_content"
11942
+ },
11943
+ ollama_url: {
11944
+ type: "string",
11945
+ description: "Ollama API URL for expose (default: http://localhost:11434)"
11946
+ },
11947
+ margin: {
11948
+ type: "string",
11949
+ description: "Price margin multiplier for expose (0.5 = 50% of market rate, 0 = free). Default: 0.5"
11739
11950
  }
11740
11951
  },
11741
11952
  required: ["action"],
@@ -11830,6 +12041,12 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
11830
12041
  case "room_members":
11831
12042
  result = await this.sendDaemonCmd("room_members", args);
11832
12043
  break;
12044
+ case "expose":
12045
+ result = await this.sendDaemonCmd("expose", args, 3e4);
12046
+ break;
12047
+ case "pricing_menu":
12048
+ result = await this.sendDaemonCmd("pricing_menu", args);
12049
+ break;
11833
12050
  default:
11834
12051
  return { success: false, output: "", error: `Unknown nexus action: ${action}`, durationMs: Date.now() - start };
11835
12052
  }
@@ -11853,7 +12070,7 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
11853
12070
  return null;
11854
12071
  }
11855
12072
  }
11856
- async sendDaemonCmd(action, args) {
12073
+ async sendDaemonCmd(action, args, timeoutMs = 15e3) {
11857
12074
  const pid = this.getDaemonPid();
11858
12075
  if (!pid)
11859
12076
  throw new Error("Nexus daemon not running. Use action 'connect' first.");
@@ -11864,7 +12081,8 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
11864
12081
  await unlink(respFile).catch(() => {
11865
12082
  });
11866
12083
  await writeFile12(cmdFile, JSON.stringify({ id: cmdId, action, args }, null, 2));
11867
- for (let i = 0; i < 30; i++) {
12084
+ const polls = Math.ceil(timeoutMs / 500);
12085
+ for (let i = 0; i < polls; i++) {
11868
12086
  await new Promise((r) => setTimeout(r, 500));
11869
12087
  if (!existsSync21(respFile))
11870
12088
  continue;
@@ -11876,7 +12094,7 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
11876
12094
  } catch {
11877
12095
  }
11878
12096
  }
11879
- return "Daemon did not respond within 15s. It may still be processing.";
12097
+ return `Daemon did not respond within ${Math.round(timeoutMs / 1e3)}s. It may still be processing.`;
11880
12098
  }
11881
12099
  // =========================================================================
11882
12100
  // Actions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.84.0",
3
+ "version": "0.85.0",
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",
@@ -26,7 +26,7 @@ If a tool fails, try a different approach. If you're unsure, explore with your t
26
26
  - web_fetch: Fetch a web page and extract text content (for docs, MDN, w3schools.com, etc.)
27
27
  - memory_read: Read from persistent memory (learned patterns, solutions)
28
28
  - memory_write: Store a fact, pattern, or solution in persistent memory for future tasks
29
- - nexus: P2P agent networking (libp2p + NATS + IPFS). Connect to other agents, join rooms, send messages, discover peers, invoke capabilities, store content, manage wallet. Actions: connect, disconnect, status, join_room, leave_room, send_message, read_messages, discover_peers, list_rooms, send_dm, find_agent, invoke_capability, store_content, retrieve_content, wallet_status, wallet_create, inference_proof, register_capability, unregister_capability, list_capabilities, block_peer, unblock_peer, metering_status, room_members.
29
+ - nexus: P2P agent networking (libp2p + NATS + IPFS). Connect to other agents, join rooms, send messages, discover peers, invoke capabilities, store content, manage wallet. Key actions: connect, join_room, send_message, discover_peers, invoke_capability, expose (metered inference), pricing_menu, register_capability, block_peer, metering_status, room_members. Full list: connect, disconnect, status, join_room, leave_room, send_message, read_messages, discover_peers, list_rooms, send_dm, find_agent, invoke_capability, store_content, retrieve_content, wallet_status, wallet_create, inference_proof, register_capability, unregister_capability, list_capabilities, block_peer, unblock_peer, metering_status, room_members, expose, pricing_menu.
30
30
  - task_complete: Signal task completion with a summary
31
31
 
32
32
  ## Parallel Execution & Sub-Agents
@@ -251,12 +251,25 @@ Use invoke_capability for real work (inference, tool calls) — NOT room message
251
251
  ### v1.5.0: Room Members
252
252
  nexus(action='room_members', room_id='general') — live member list with capabilities
253
253
 
254
+ ### Metered Inference Exposure
255
+ nexus(action='expose') — expose ALL local Ollama models as nexus capabilities
256
+ nexus(action='expose', margin='0.5') — set pricing at 50% of market rate (default)
257
+ nexus(action='expose', margin='0') — expose for free (self-hosted, no cost)
258
+ nexus(action='expose', margin='1.0') — match market rate
259
+ nexus(action='pricing_menu') — show current pricing menu for exposed models
260
+
261
+ expose queries local Ollama for models, fetches live market rates from OpenRouter
262
+ (https://openrouter.ai/api/v1/models — free, no auth), registers each model as a
263
+ nexus capability (inference:{model_name}), and writes pricing to .oa/nexus/pricing.json.
264
+ Peers can invoke your models via invoke_capability and see metered usage.
265
+
254
266
  SECURITY: Wallet private keys are AES-256-GCM encrypted and NEVER accessible to you.
255
267
  All outbound messages are scanned for key material leaks.
256
268
 
257
269
  When the user asks about expanding capabilities or connecting with other agents, suggest
258
- enabling nexus networking. Use register_capability to serve invocations, inference_proof
259
- to benchmark, and room_members to discover who's online.
270
+ enabling nexus networking. Use expose to share your models with the network, pricing_menu
271
+ to check rates, register_capability to serve custom invocations, and room_members to
272
+ discover who's online.
260
273
 
261
274
  ## Temporal Agency — Scheduling, Reminders & Long-Horizon Tasks
262
275