fleetmesh-node 0.2.0__py3-none-any.whl
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.
- fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
- fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
- fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
- fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
- fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
- fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
- node/RELEASE +1 -0
- node/__init__.py +35 -0
- node/app/__init__.py +49 -0
- node/app/commands.py +265 -0
- node/app/config.py +131 -0
- node/app/control.py +670 -0
- node/app/core.py +867 -0
- node/app/plugins.py +76 -0
- node/app/router.py +83 -0
- node/app/socket.py +431 -0
- node/app/systemd.py +36 -0
- node/app/views.py +204 -0
- node/channels/__init__.py +378 -0
- node/channels/bus.py +331 -0
- node/channels/journal.py +46 -0
- node/commands/__init__.py +140 -0
- node/commands/adapter.py +210 -0
- node/commands/base.py +278 -0
- node/commands/cluster_cmds.py +183 -0
- node/commands/compute_cmds.py +401 -0
- node/commands/core.py +474 -0
- node/commands/gossip_cmds.py +106 -0
- node/commands/nsite_cmds.py +141 -0
- node/commands/peering.py +402 -0
- node/commands/plugin_cmds.py +135 -0
- node/commands/sidecar_cmds.py +120 -0
- node/commands/sync.py +388 -0
- node/commands/transport_cmds.py +451 -0
- node/commands/video_cmds.py +99 -0
- node/compartment/__init__.py +124 -0
- node/compartment/base.py +868 -0
- node/compartment/command.py +458 -0
- node/compartment/context.py +163 -0
- node/compartment/every.py +108 -0
- node/compartment/handler.py +131 -0
- node/compartment/lifecycle.py +460 -0
- node/compartment/lint.py +388 -0
- node/compartment/need.py +158 -0
- node/compartment/problem.py +521 -0
- node/compartment/refusals.py +120 -0
- node/compartment/registry.py +790 -0
- node/compartment/schema.py +290 -0
- node/compartment/sidecar.py +499 -0
- node/compartment/stores.py +662 -0
- node/compartment/wire.py +225 -0
- node/compartment/worker.py +61 -0
- node/config/__init__.py +43 -0
- node/config/base.py +116 -0
- node/config/change.py +441 -0
- node/config/changestore.py +201 -0
- node/config/compartments.py +1144 -0
- node/config/node.py +318 -0
- node/config/parameter.py +563 -0
- node/config/protocols.py +274 -0
- node/config/records.py +171 -0
- node/examples/__init__.py +1 -0
- node/examples/demo.py +226 -0
- node/examples/follower.py +46 -0
- node/interfaces.py +235 -0
- node/keystore/__init__.py +27 -0
- node/keystore/core.py +103 -0
- node/keystore/enclave.py +688 -0
- node/meshnode.py +111 -0
- node/metrics/__init__.py +396 -0
- node/metrics/adapters.py +102 -0
- node/metrics/face.py +110 -0
- node/planes/__init__.py +21 -0
- node/planes/identity/__init__.py +20 -0
- node/planes/identity/announce/__init__.py +9 -0
- node/planes/identity/announce/compartment.py +447 -0
- node/planes/identity/announce/types.py +26 -0
- node/planes/identity/discovery/__init__.py +9 -0
- node/planes/identity/discovery/compartment.py +522 -0
- node/planes/identity/discovery/types.py +13 -0
- node/planes/identity/linked/__init__.py +1 -0
- node/planes/identity/linked/compartment.py +270 -0
- node/planes/identity/linked/types.py +25 -0
- node/planes/identity/presence/__init__.py +7 -0
- node/planes/identity/presence/compartment.py +409 -0
- node/planes/identity/presence/types.py +14 -0
- node/planes/operations/__init__.py +13 -0
- node/planes/operations/scheduler/__init__.py +9 -0
- node/planes/operations/scheduler/compartment.py +353 -0
- node/planes/operations/scheduler/types.py +21 -0
- node/planes/operations/updates/__init__.py +17 -0
- node/planes/operations/updates/core.py +310 -0
- node/planes/operations/updates/store.py +79 -0
- node/planes/operations/updates/types.py +20 -0
- node/planes/peering/__init__.py +28 -0
- node/planes/peering/budget/__init__.py +43 -0
- node/planes/peering/budget/chain.py +43 -0
- node/planes/peering/budget/core.py +537 -0
- node/planes/peering/budget/evaluation.py +80 -0
- node/planes/peering/budget/meter.py +100 -0
- node/planes/peering/budget/receipts.py +439 -0
- node/planes/peering/budget/shape.py +101 -0
- node/planes/peering/budget/tick.py +110 -0
- node/planes/peering/budget/types.py +67 -0
- node/planes/peering/budget/windows.py +346 -0
- node/planes/peering/capacity/__init__.py +7 -0
- node/planes/peering/capacity/compartment.py +342 -0
- node/planes/peering/capacity/types.py +24 -0
- node/planes/peering/peering/__init__.py +25 -0
- node/planes/peering/peering/core.py +464 -0
- node/planes/peering/peering/envelope.py +142 -0
- node/planes/peering/peering/handshake.py +198 -0
- node/planes/peering/peering/shape.py +80 -0
- node/planes/peering/peering/types.py +39 -0
- node/planes/peering/reporter/__init__.py +42 -0
- node/planes/peering/reporter/constants.py +29 -0
- node/planes/peering/reporter/core.py +299 -0
- node/planes/peering/reporter/finding.py +74 -0
- node/planes/peering/reporter/findings.py +171 -0
- node/planes/peering/reporter/publish.py +285 -0
- node/planes/peering/reporter/reading.py +193 -0
- node/planes/peering/reporter/shape.py +86 -0
- node/planes/peering/reporter/types.py +50 -0
- node/planes/peering/strain/__init__.py +9 -0
- node/planes/peering/strain/compartment.py +204 -0
- node/planes/peering/strain/types.py +21 -0
- node/planes/peering/trust/__init__.py +9 -0
- node/planes/peering/trust/compartment.py +239 -0
- node/planes/peering/trust/types.py +30 -0
- node/planes/services/__init__.py +33 -0
- node/planes/services/compute/__init__.py +160 -0
- node/planes/services/compute/common.py +134 -0
- node/planes/services/compute/compartment.py +681 -0
- node/planes/services/compute/executors/__init__.py +121 -0
- node/planes/services/compute/executors/agent.py +840 -0
- node/planes/services/compute/executors/bacalhau.py +605 -0
- node/planes/services/compute/executors/base.py +127 -0
- node/planes/services/compute/executors/harness.py +439 -0
- node/planes/services/compute/executors/mcp.py +454 -0
- node/planes/services/compute/executors/media.py +898 -0
- node/planes/services/compute/executors/nondet.py +283 -0
- node/planes/services/compute/executors/providers.py +499 -0
- node/planes/services/compute/executors/registry.py +140 -0
- node/planes/services/compute/executors/wasm.py +244 -0
- node/planes/services/compute/executors/zk.py +604 -0
- node/planes/services/compute/jobs.py +227 -0
- node/planes/services/compute/load.py +180 -0
- node/planes/services/compute/market.py +433 -0
- node/planes/services/compute/sandbox.py +282 -0
- node/planes/services/compute/types.py +84 -0
- node/planes/services/compute_wire/__init__.py +58 -0
- node/planes/services/compute_wire/compartment.py +256 -0
- node/planes/services/compute_wire/messages.py +320 -0
- node/planes/services/compute_wire/sealing.py +147 -0
- node/planes/services/compute_wire/session.py +572 -0
- node/planes/services/compute_wire/types.py +49 -0
- node/planes/services/compute_wire/verify.py +331 -0
- node/planes/services/dvm/__init__.py +24 -0
- node/planes/services/dvm/gateway.py +526 -0
- node/planes/services/dvm/protocol.py +35 -0
- node/planes/services/dvm/translate.py +15 -0
- node/planes/services/dvm/types.py +25 -0
- node/planes/services/gateway/__init__.py +40 -0
- node/planes/services/gateway/settle.py +279 -0
- node/planes/services/gateway/translate.py +644 -0
- node/planes/services/gossip/__init__.py +22 -0
- node/planes/services/gossip/core.py +929 -0
- node/planes/services/gossip/envelope.py +154 -0
- node/planes/services/gossip/mesh.py +127 -0
- node/planes/services/gossip/types.py +50 -0
- node/planes/services/nsite/__init__.py +7 -0
- node/planes/services/nsite/client.py +62 -0
- node/planes/services/nsite/compartment.py +752 -0
- node/planes/services/nsite/types.py +44 -0
- node/planes/services/paidfetch/__init__.py +7 -0
- node/planes/services/paidfetch/compartment.py +862 -0
- node/planes/services/paidfetch/types.py +99 -0
- node/planes/services/settle/__init__.py +27 -0
- node/planes/services/settle/compartment.py +384 -0
- node/planes/services/settle/engine.py +456 -0
- node/planes/services/settle/gateways.py +541 -0
- node/planes/services/settle/payment.py +512 -0
- node/planes/services/settle/store.py +74 -0
- node/planes/services/settle/types.py +58 -0
- node/planes/services/sidecars/__init__.py +8 -0
- node/planes/services/sidecars/compartment.py +461 -0
- node/planes/services/sidecars/manifest.py +304 -0
- node/planes/services/sidecars/manifests/10-grasp.json +37 -0
- node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
- node/planes/services/sidecars/manifests/30-magpie.json +28 -0
- node/planes/services/sidecars/types.py +43 -0
- node/planes/services/video/__init__.py +106 -0
- node/planes/services/video/barter.py +407 -0
- node/planes/services/video/cli.py +158 -0
- node/planes/services/video/compartment.py +932 -0
- node/planes/services/video/ingest.py +524 -0
- node/planes/services/video/ledger.py +705 -0
- node/planes/services/video/packager.py +847 -0
- node/planes/services/video/player.py +456 -0
- node/planes/services/video/timed_segments.py +124 -0
- node/planes/services/video/transit.py +478 -0
- node/planes/store/__init__.py +24 -0
- node/planes/store/blob_replication/__init__.py +7 -0
- node/planes/store/blob_replication/compartment.py +496 -0
- node/planes/store/blob_replication/types.py +35 -0
- node/planes/store/blob_wire/__init__.py +48 -0
- node/planes/store/blob_wire/client.py +57 -0
- node/planes/store/blob_wire/core.py +359 -0
- node/planes/store/blob_wire/intake.py +74 -0
- node/planes/store/blob_wire/messages.py +85 -0
- node/planes/store/projected/__init__.py +7 -0
- node/planes/store/projected/compartment.py +258 -0
- node/planes/store/projected/types.py +29 -0
- node/planes/store/retention/__init__.py +7 -0
- node/planes/store/retention/compartment.py +214 -0
- node/planes/store/retention/types.py +12 -0
- node/planes/store/sync/__init__.py +35 -0
- node/planes/store/sync/core.py +311 -0
- node/planes/store/sync/envelope.py +106 -0
- node/planes/store/sync/handover.py +206 -0
- node/planes/store/sync/initiator.py +128 -0
- node/planes/store/sync/responder.py +114 -0
- node/planes/store/sync/rules.py +179 -0
- node/planes/store/sync/shape.py +80 -0
- node/planes/store/sync/types.py +51 -0
- node/planes/transport/__init__.py +23 -0
- node/planes/transport/bridge/__init__.py +13 -0
- node/planes/transport/bridge/compartment.py +785 -0
- node/planes/transport/bridge/ratelimit.py +44 -0
- node/planes/transport/bridge/types.py +43 -0
- node/planes/transport/lan/__init__.py +9 -0
- node/planes/transport/lan/compartment.py +446 -0
- node/planes/transport/lan/types.py +25 -0
- node/planes/transport/link/__init__.py +7 -0
- node/planes/transport/link/compartment.py +257 -0
- node/planes/transport/link/types.py +27 -0
- node/planes/transport/mediation/__init__.py +45 -0
- node/planes/transport/mediation/compartment.py +238 -0
- node/planes/transport/mediation/leaf.py +107 -0
- node/planes/transport/mediation/messages.py +302 -0
- node/planes/transport/mediation/service.py +233 -0
- node/planes/transport/mediation/store.py +37 -0
- node/planes/transport/mediation/types.py +20 -0
- node/planes/transport/p2p/__init__.py +7 -0
- node/planes/transport/p2p/compartment.py +349 -0
- node/planes/transport/p2p/types.py +22 -0
- node/protocols/__init__.py +277 -0
- node/protocols/activitypub/__init__.py +56 -0
- node/protocols/activitypub/actor.py +230 -0
- node/protocols/activitypub/federation.py +490 -0
- node/protocols/activitypub/media.py +145 -0
- node/protocols/activitypub/models.py +286 -0
- node/protocols/activitypub/package.py +271 -0
- node/protocols/activitypub/store.py +564 -0
- node/protocols/arcade/__init__.py +26 -0
- node/protocols/atproto/__init__.py +13 -0
- node/protocols/atproto/firehose.py +126 -0
- node/protocols/atproto/metering.py +165 -0
- node/protocols/atproto/package.py +155 -0
- node/protocols/atproto/repo.py +250 -0
- node/protocols/atproto/xrpc.py +285 -0
- node/protocols/base.py +109 -0
- node/protocols/circles/__init__.py +29 -0
- node/protocols/dm/__init__.py +50 -0
- node/protocols/grasp/__init__.py +22 -0
- node/protocols/isotime.py +15 -0
- node/protocols/nostr/__init__.py +51 -0
- node/protocols/nostr/blobstore.py +383 -0
- node/protocols/nostr/blossom.py +627 -0
- node/protocols/nostr/client.py +377 -0
- node/protocols/nostr/descriptor.py +211 -0
- node/protocols/nostr/eventstore.py +304 -0
- node/protocols/nostr/package.py +182 -0
- node/protocols/nostr/peers.py +109 -0
- node/protocols/nostr/relayserver.py +656 -0
- node/protocols/p2panda/__init__.py +22 -0
- node/protocols/p2panda/identity.py +19 -0
- node/protocols/p2panda/package.py +236 -0
- node/protocols/p2panda/rung.py +152 -0
- node/protocols/p2panda/store.py +273 -0
- node/protocols/rhio/__init__.py +33 -0
- node/protocols/rhio/package.py +275 -0
- node/protocols/rhio/rung.py +136 -0
- node/protocols/rhio/store.py +1423 -0
- node/protocols/ssb/muxrpc.py +256 -0
- node/protocols/ssb/package.py +267 -0
- node/protocols/textile/__init__.py +111 -0
- node/protocols/textile/transducers.py +259 -0
- node/release.py +119 -0
- node/storage/__init__.py +88 -0
- node/storage/accounts.py +305 -0
- node/storage/adapter.py +161 -0
- node/storage/base.py +385 -0
- node/storage/dialect.py +209 -0
- node/storage/grants.py +317 -0
- node/storage/locator.py +150 -0
- node/storage/postgres.py +294 -0
- node/storage/projected.py +953 -0
- node/storage/sharding.py +448 -0
- node/storage/standing.py +568 -0
- node/testnet/__init__.py +9 -0
- node/testnet/cluster.py +731 -0
- node/testnet/fips_daemon.py +230 -0
- node/transport/__init__.py +94 -0
- node/transport/const.py +110 -0
- node/transport/didcomm_inbound.py +251 -0
- node/transport/dnstxt.py +169 -0
- node/transport/fips_rung/__init__.py +67 -0
- node/transport/fips_rung/core.py +666 -0
- node/transport/fips_rung/listener.py +96 -0
- node/transport/fips_rung/types.py +12 -0
- node/transport/host.py +334 -0
- node/transport/iroh_rung/__init__.py +29 -0
- node/transport/iroh_rung/core.py +477 -0
- node/transport/iroh_rung/quic.py +942 -0
- node/transport/iroh_rung/types.py +22 -0
- node/transport/ladder.py +145 -0
- node/transport/lineproto.py +49 -0
- node/transport/mdns.py +303 -0
- node/transport/netfetch.py +283 -0
- node/transport/relay_rung/__init__.py +23 -0
- node/transport/relay_rung/cooldown.py +87 -0
- node/transport/relay_rung/core.py +138 -0
- node/transport/relay_rung/inbound.py +161 -0
- node/transport/relay_rung/ledger.py +104 -0
- node/transport/relay_rung/listen.py +121 -0
- node/transport/relay_rung/outbound.py +181 -0
- node/transport/relay_rung/shape.py +71 -0
- node/transport/relay_rung/types.py +58 -0
- node/transport/relay_rung/wire.py +63 -0
- node/transport/router.py +761 -0
- node/transport/rung.py +276 -0
- node/transport/ssb_rung.py +166 -0
- node/transport/ws.py +502 -0
- sidecar/fleetmesh_announce.py +338 -0
- sidecar/fleetmesh_p2p.py +373 -0
- sidecar/fleetmesh_rhio.py +352 -0
- sidecar/fleetmesh_ssb.py +603 -0
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
"""The Compute subsystem: node-side lifecycle, offer publication and job handling.
|
|
2
|
+
|
|
3
|
+
Owns its wire half: `compute/wire/` (ComputeWire, ComputeSession, the
|
|
4
|
+
compute/1.0 and /1.1 messages) is constructed here, and wire imports
|
|
5
|
+
executors/jobs from this package - one direction, no cycle.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from typing import Any, Callable, Mapping, Optional
|
|
11
|
+
|
|
12
|
+
from fleetmesh_core.crypto import crypto
|
|
13
|
+
from fleetmesh_core.wire.interfaces import NostrEvent
|
|
14
|
+
from node import channels as ch_mod, compartment as compartment_mod, metrics as mx_mod
|
|
15
|
+
from node.compartment import stores as st
|
|
16
|
+
from node.compartment import Command, Need
|
|
17
|
+
from node.config.parameter import Parameter
|
|
18
|
+
from node.config import ComputeConfig
|
|
19
|
+
from .common import ComputeError, ASK_KIND
|
|
20
|
+
from ..compute_wire import ComputeWire
|
|
21
|
+
from ..compute_wire.messages import PIURI as COMPUTE_PIURI, PIURI_V11 as COMPUTE_PIURI_V11
|
|
22
|
+
from .market import (
|
|
23
|
+
build_ask,
|
|
24
|
+
build_offer,
|
|
25
|
+
match_ask_to_offers,
|
|
26
|
+
match_offer_to_asks,
|
|
27
|
+
parse_ask,
|
|
28
|
+
parse_offer,
|
|
29
|
+
validate_offer,
|
|
30
|
+
)
|
|
31
|
+
from .executors import ExecutorRegistry
|
|
32
|
+
from .jobs import build_receipt, run_job
|
|
33
|
+
from .load import ComputeLoadMonitor
|
|
34
|
+
|
|
35
|
+
# -------------------------------------------------------------------------
|
|
36
|
+
# Compute Compartment
|
|
37
|
+
# -------------------------------------------------------------------------
|
|
38
|
+
class Compute(compartment_mod.Compartment):
|
|
39
|
+
optional: bool = True
|
|
40
|
+
enabled_by: Optional[str] = 'compute'
|
|
41
|
+
"""The compute/1.0 compartment of meshnode."""
|
|
42
|
+
|
|
43
|
+
name = 'compute'
|
|
44
|
+
piuris: tuple[str, ...] = (COMPUTE_PIURI, COMPUTE_PIURI_V11)
|
|
45
|
+
reads = ('compute', 'executors', 'relays')
|
|
46
|
+
config_cls = ComputeConfig
|
|
47
|
+
NEEDS = (
|
|
48
|
+
Need('ks', 'ks'),
|
|
49
|
+
Need('blobs', 'stores.blobs'),
|
|
50
|
+
)
|
|
51
|
+
COMMANDS = (
|
|
52
|
+
Command(
|
|
53
|
+
key='show',
|
|
54
|
+
name='Compute Status',
|
|
55
|
+
description='display active compute offers, asks, and receipts',
|
|
56
|
+
tags=('compute', 'status'),
|
|
57
|
+
permission='compute/show',
|
|
58
|
+
run='state',
|
|
59
|
+
reply='dict',
|
|
60
|
+
),
|
|
61
|
+
Command(
|
|
62
|
+
key='offer',
|
|
63
|
+
name='Publish Offer',
|
|
64
|
+
description='publish a kind 11803 compute offer',
|
|
65
|
+
tags=('compute', 'offer'),
|
|
66
|
+
parameters=(
|
|
67
|
+
Parameter(key='engine', name='engine', description='execution engine', type=str, default='', required=True),
|
|
68
|
+
Parameter(key='msat_per_mfuel', name='msat_per_mfuel', description='price in msat per megabytes fuel', type=object, default=0, required=True),
|
|
69
|
+
),
|
|
70
|
+
permission='compute/offer',
|
|
71
|
+
run='_cmd_offer',
|
|
72
|
+
reply='dict',
|
|
73
|
+
),
|
|
74
|
+
Command(
|
|
75
|
+
key='ask',
|
|
76
|
+
name='Publish Ask',
|
|
77
|
+
description='publish a kind 30810 confidential compute ask',
|
|
78
|
+
tags=('compute', 'ask'),
|
|
79
|
+
parameters=(
|
|
80
|
+
Parameter(key='id', name='id', description='ask id', type=str, default='', required=True),
|
|
81
|
+
Parameter(key='job', name='job', description='job payload', type=dict, default=dict, required=True),
|
|
82
|
+
Parameter(key='bid', name='bid', description='bid amount', type=object, default=0, required=True),
|
|
83
|
+
Parameter(key='executor', name='executor', description='executor type', type=str, default='wasm', required=False),
|
|
84
|
+
Parameter(key='verify', name='verify', description='verification mode', type=str, default='none', required=False),
|
|
85
|
+
),
|
|
86
|
+
permission='compute/ask',
|
|
87
|
+
run='_cmd_ask',
|
|
88
|
+
reply='dict',
|
|
89
|
+
),
|
|
90
|
+
Command(
|
|
91
|
+
key='run',
|
|
92
|
+
name='Run Job',
|
|
93
|
+
description='execute a job payload and return signed kind 30811 receipt',
|
|
94
|
+
tags=('compute', 'run'),
|
|
95
|
+
parameters=(
|
|
96
|
+
Parameter(key='job', name='job', description='job payload', type=dict, default=dict, required=True),
|
|
97
|
+
Parameter(key='buyer', name='buyer', description='buyer public key', type=str, default='', required=True),
|
|
98
|
+
Parameter(key='preimage', name='preimage', description='payment preimage', type=str, default=None, required=False),
|
|
99
|
+
Parameter(key='paid_msat', name='paid_msat', description='paid amount in millisatoshis', type=int, default=None, required=False),
|
|
100
|
+
Parameter(key='payment_hash', name='payment_hash', description='payment hash', type=str, default=None, required=False),
|
|
101
|
+
),
|
|
102
|
+
permission='compute/run',
|
|
103
|
+
run='_cmd_run',
|
|
104
|
+
reply='dict',
|
|
105
|
+
),
|
|
106
|
+
Command(
|
|
107
|
+
key='submit',
|
|
108
|
+
name='Submit or Reflect Job',
|
|
109
|
+
description='execute a job locally when CPU headroom permits or reflect to a less-loaded mesh peer',
|
|
110
|
+
tags=('compute', 'submit', 'reflection'),
|
|
111
|
+
parameters=(
|
|
112
|
+
Parameter(key='job', name='job', description='job payload', type=dict, default=dict, required=True),
|
|
113
|
+
Parameter(key='buyer', name='buyer', description='buyer public key', type=str, default='', required=True),
|
|
114
|
+
Parameter(key='bid', name='bid', description='maximum bid in msat per mfuel', type=int, default=0, required=False),
|
|
115
|
+
Parameter(key='force_reflect', name='force_reflect', description='force reflection to a remote peer if available', type=bool, default=False, required=False),
|
|
116
|
+
),
|
|
117
|
+
permission='compute/submit',
|
|
118
|
+
run='_cmd_submit',
|
|
119
|
+
reply='dict',
|
|
120
|
+
),
|
|
121
|
+
Command(
|
|
122
|
+
key='load',
|
|
123
|
+
name='Compute Load Snapshot',
|
|
124
|
+
description='display live host CPU utilization, active worker slots, and reflection status',
|
|
125
|
+
tags=('compute', 'load', 'metrics'),
|
|
126
|
+
permission='compute/load',
|
|
127
|
+
run='_cmd_load',
|
|
128
|
+
reply='dict',
|
|
129
|
+
),
|
|
130
|
+
Command(
|
|
131
|
+
key='receipts',
|
|
132
|
+
name='Job Receipts',
|
|
133
|
+
description='list compute job receipts held',
|
|
134
|
+
tags=('compute', 'receipts'),
|
|
135
|
+
permission='compute/receipts',
|
|
136
|
+
run='receipts_cmd',
|
|
137
|
+
reply='list',
|
|
138
|
+
),
|
|
139
|
+
Command(
|
|
140
|
+
key='book',
|
|
141
|
+
name='Order Book',
|
|
142
|
+
description='display active order book offers and asks',
|
|
143
|
+
tags=('compute', 'book'),
|
|
144
|
+
permission='compute/book',
|
|
145
|
+
run='book_cmd',
|
|
146
|
+
reply='dict',
|
|
147
|
+
),
|
|
148
|
+
Command(
|
|
149
|
+
key='match',
|
|
150
|
+
name='Match Orders',
|
|
151
|
+
description='find matching counterparties in order book',
|
|
152
|
+
tags=('compute', 'match'),
|
|
153
|
+
parameters=(
|
|
154
|
+
Parameter(key='ask', name='ask', description='ask id or event', type=object, default=None, required=False),
|
|
155
|
+
Parameter(key='offer', name='offer', description='offer id or event', type=object, default=None, required=False),
|
|
156
|
+
),
|
|
157
|
+
permission='compute/match',
|
|
158
|
+
run='match_cmd',
|
|
159
|
+
reply='list',
|
|
160
|
+
),
|
|
161
|
+
Command(
|
|
162
|
+
key='index_offer',
|
|
163
|
+
name='Index Offer',
|
|
164
|
+
description='index an external kind 11803 offer',
|
|
165
|
+
tags=('compute', 'offer'),
|
|
166
|
+
parameters=(
|
|
167
|
+
Parameter(key='offer', name='offer', description='kind 11803 offer to index', type=object, default=dict, required=True),
|
|
168
|
+
),
|
|
169
|
+
permission='compute/index_offer',
|
|
170
|
+
run='index_offer_cmd',
|
|
171
|
+
reply='bool',
|
|
172
|
+
),
|
|
173
|
+
Command(
|
|
174
|
+
key='index_ask',
|
|
175
|
+
name='Index Ask',
|
|
176
|
+
description='index an external kind 30810 ask',
|
|
177
|
+
tags=('compute', 'ask'),
|
|
178
|
+
parameters=(
|
|
179
|
+
Parameter(key='ask', name='ask', description='kind 30810 ask to index', type=object, default=dict, required=True),
|
|
180
|
+
),
|
|
181
|
+
permission='compute/index_ask',
|
|
182
|
+
run='index_ask_cmd',
|
|
183
|
+
reply='bool',
|
|
184
|
+
),
|
|
185
|
+
Command(
|
|
186
|
+
key='models',
|
|
187
|
+
name='Compute Models',
|
|
188
|
+
description='list available AI/LLM models and inference features',
|
|
189
|
+
tags=('compute', 'models'),
|
|
190
|
+
permission='compute/models',
|
|
191
|
+
run='_cmd_models',
|
|
192
|
+
reply='dict',
|
|
193
|
+
),
|
|
194
|
+
Command(
|
|
195
|
+
key='stream',
|
|
196
|
+
name='Stream Inference',
|
|
197
|
+
description='run a streaming AI/LLM inference query and return token deltas',
|
|
198
|
+
tags=('compute', 'stream'),
|
|
199
|
+
parameters=(
|
|
200
|
+
Parameter(key='prompt', name='prompt', description='prompt text', type=str, default='', required=True),
|
|
201
|
+
Parameter(key='model', name='model', description='model identifier', type=str, default=None, required=False),
|
|
202
|
+
),
|
|
203
|
+
permission='compute/stream',
|
|
204
|
+
run='_cmd_stream',
|
|
205
|
+
reply='dict',
|
|
206
|
+
),
|
|
207
|
+
Command(
|
|
208
|
+
key='link_provider',
|
|
209
|
+
name='Link AI Provider',
|
|
210
|
+
description='link an external or local AI/LLM inference provider to this meshnode',
|
|
211
|
+
tags=('compute', 'models'),
|
|
212
|
+
parameters=(
|
|
213
|
+
Parameter(key='provider_id', name='provider_id', description='provider preset id (openrouter, claude, openai, gemini, copilot, routstr, payperquery, ollama, vllm)', type=str, default='openrouter', required=True),
|
|
214
|
+
Parameter(key='endpoint', name='endpoint', description='custom base endpoint URL', type=str, default=None, required=False),
|
|
215
|
+
Parameter(key='default_model', name='default_model', description='default model ID', type=str, default=None, required=False),
|
|
216
|
+
Parameter(key='api_key', name='api_key', description='operator API key or bearer secret', type=str, default=None, required=False),
|
|
217
|
+
Parameter(key='cashu_token', name='cashu_token', description='operator Cashu token for Routstr/PPQ', type=str, default=None, required=False),
|
|
218
|
+
Parameter(key='rate_msat_per_token', name='rate_msat_per_token', description='msat rate per token', type=int, default=None, required=False),
|
|
219
|
+
Parameter(key='enabled', name='enabled', description='whether provider is active', type=bool, default=True, required=False),
|
|
220
|
+
Parameter(key='set_default', name='set_default', description='set as primary provider', type=bool, default=True, required=False),
|
|
221
|
+
),
|
|
222
|
+
permission='compute/link_provider',
|
|
223
|
+
run='_cmd_link_provider',
|
|
224
|
+
reply='dict',
|
|
225
|
+
),
|
|
226
|
+
)
|
|
227
|
+
STORES = (
|
|
228
|
+
st.Store(
|
|
229
|
+
key='offers',
|
|
230
|
+
description='offers published by this node, newest last',
|
|
231
|
+
bound=1024,
|
|
232
|
+
kind=st.RING,
|
|
233
|
+
),
|
|
234
|
+
st.Store(
|
|
235
|
+
key='asks',
|
|
236
|
+
description='active asks placed by this node, by ask_id',
|
|
237
|
+
bound=1024,
|
|
238
|
+
evict=st.LRU,
|
|
239
|
+
kind=st.TABLE,
|
|
240
|
+
keyed_by='ask',
|
|
241
|
+
),
|
|
242
|
+
st.Store(
|
|
243
|
+
key='order_book_offers',
|
|
244
|
+
description='indexed offers from mesh peers, by offer_id',
|
|
245
|
+
bound=4096,
|
|
246
|
+
evict=st.LRU,
|
|
247
|
+
kind=st.TABLE,
|
|
248
|
+
keyed_by='offer',
|
|
249
|
+
),
|
|
250
|
+
st.Store(
|
|
251
|
+
key='order_book_asks',
|
|
252
|
+
description='indexed asks from mesh peers, by ask_id',
|
|
253
|
+
bound=4096,
|
|
254
|
+
evict=st.LRU,
|
|
255
|
+
kind=st.TABLE,
|
|
256
|
+
keyed_by='ask',
|
|
257
|
+
),
|
|
258
|
+
st.Store(
|
|
259
|
+
key='receipts_held',
|
|
260
|
+
description='job completion receipts held by this node, newest last',
|
|
261
|
+
bound=1024,
|
|
262
|
+
kind=st.RING,
|
|
263
|
+
),
|
|
264
|
+
)
|
|
265
|
+
CHANNELS = (
|
|
266
|
+
ch_mod.Channel('job_executed', 'A compute task execution completed and receipt signed',
|
|
267
|
+
level=ch_mod.NOTE, say='compute job {job_id} executed for buyer {buyer}',
|
|
268
|
+
tags=('compute', 'execution')),
|
|
269
|
+
ch_mod.Channel('job_reflected', 'A compute task was reflected to a less-loaded mesh peer',
|
|
270
|
+
level=ch_mod.INFO, say='compute job {job_id} reflected to peer {peer} ({reason})',
|
|
271
|
+
tags=('compute', 'reflection')),
|
|
272
|
+
ch_mod.Channel('offer_published', 'A compute engine capability offer was published',
|
|
273
|
+
level=ch_mod.NOTE, say='compute offer published for engine {engine}',
|
|
274
|
+
tags=('compute', 'market')),
|
|
275
|
+
ch_mod.Channel('ask_published', 'A confidential compute ask was published to the mesh',
|
|
276
|
+
level=ch_mod.NOTE, say='compute ask {ask_id} published bid {bid}',
|
|
277
|
+
tags=('compute', 'market')),
|
|
278
|
+
)
|
|
279
|
+
METRICS = (
|
|
280
|
+
mx_mod.Metric(name='compute_jobs_executed', description='Compute task executions completed.',
|
|
281
|
+
kind=mx_mod.COUNTER, counts='job_executed', tags=('compute', 'execution')),
|
|
282
|
+
mx_mod.Metric(name='compute_jobs_reflected', description='Compute jobs reflected to less-loaded mesh peers.',
|
|
283
|
+
kind=mx_mod.COUNTER, counts='job_reflected', tags=('compute', 'reflection')),
|
|
284
|
+
mx_mod.Metric(name='compute_offers_published', description='Compute engine offers published.',
|
|
285
|
+
kind=mx_mod.COUNTER, counts='offer_published', tags=('compute', 'market')),
|
|
286
|
+
mx_mod.Metric(name='compute_asks_published', description='Compute asks published to mesh.',
|
|
287
|
+
kind=mx_mod.COUNTER, counts='ask_published', tags=('compute', 'market')),
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
def __init__(self, node: Any = None, registry: Optional[ExecutorRegistry] = None,
|
|
291
|
+
ks: Optional[Any] = None, blobs: Optional[Any] = None,
|
|
292
|
+
clock: Optional[Callable[[], int]] = None,
|
|
293
|
+
load_monitor: Optional[ComputeLoadMonitor] = None,
|
|
294
|
+
remote_dispatcher: Optional[Callable[[Mapping[str, Any], Mapping[str, Any]], Mapping[str, Any]]] = None) -> None:
|
|
295
|
+
super().__init__(node, clock)
|
|
296
|
+
# What this compartment takes from the node, named: the keystore that
|
|
297
|
+
# signs offers/asks/receipts and the blob store a job reads from.
|
|
298
|
+
# Suites that pass a fake node and nothing else still get them off it.
|
|
299
|
+
self.ks: Optional[Any] = ks if ks is not None else getattr(node, 'ks', None)
|
|
300
|
+
self.blobs: Optional[Any] = blobs if blobs is not None else getattr(node, 'blobs', None)
|
|
301
|
+
self.offers: st.Ring[NostrEvent]
|
|
302
|
+
self.asks: st.Table[str, NostrEvent]
|
|
303
|
+
self.order_book_offers: st.Table[str, NostrEvent]
|
|
304
|
+
self.order_book_asks: st.Table[str, NostrEvent]
|
|
305
|
+
self.receipts_held: st.Ring[Mapping[str, Any]]
|
|
306
|
+
self.jobs_executed: int = 0
|
|
307
|
+
self.load_monitor: ComputeLoadMonitor = load_monitor or ComputeLoadMonitor()
|
|
308
|
+
self.remote_dispatcher: Optional[Callable[[Mapping[str, Any], Mapping[str, Any]], Mapping[str, Any]]] = remote_dispatcher
|
|
309
|
+
# Operators may enable fixture executors through cfg['compute']
|
|
310
|
+
# ['mock_mode'] (for demos on a box with no LLM backend); tests pass
|
|
311
|
+
# a registry directly. Neither happens by default.
|
|
312
|
+
if registry is None:
|
|
313
|
+
node_dir = getattr(node, 'dir', None)
|
|
314
|
+
registry = ExecutorRegistry(blob_store=self.blobs,
|
|
315
|
+
mock_mode=self.settings().mock_mode,
|
|
316
|
+
media_cache_dir=os.path.join(node_dir, 'media-cache') if node_dir else None)
|
|
317
|
+
self.registry: ExecutorRegistry = registry
|
|
318
|
+
self.compute_wire = getattr(node, 'compute_wire', None) or ComputeWire(node=node, ks=self.ks)
|
|
319
|
+
|
|
320
|
+
def handle(self, inbound: Any) -> Optional[tuple[Any, ...]]:
|
|
321
|
+
return self.compute_wire.handle(inbound)
|
|
322
|
+
|
|
323
|
+
def index_offer(self, offer: NostrEvent) -> bool:
|
|
324
|
+
probs = validate_offer(offer)
|
|
325
|
+
if probs:
|
|
326
|
+
raise ComputeError(f"invalid offer event: {', '.join(probs)}")
|
|
327
|
+
self.order_book_offers[offer['id']] = offer
|
|
328
|
+
return True
|
|
329
|
+
|
|
330
|
+
def index_ask(self, ask: NostrEvent) -> bool:
|
|
331
|
+
if ask.get('kind') != ASK_KIND:
|
|
332
|
+
raise ComputeError("not a kind 30810 ask")
|
|
333
|
+
ask_id = parse_ask(ask)['ask_id']
|
|
334
|
+
self.order_book_asks[ask_id or ask['id']] = ask
|
|
335
|
+
return True
|
|
336
|
+
|
|
337
|
+
def find_offers_for_ask(
|
|
338
|
+
self,
|
|
339
|
+
ask: NostrEvent | Mapping[str, Any] | str,
|
|
340
|
+
trust_weights: Optional[Mapping[str, float]] = None,
|
|
341
|
+
) -> list[Mapping[str, Any]]:
|
|
342
|
+
ask_ev = self.order_book_asks.get(ask) if isinstance(ask, str) else ask
|
|
343
|
+
if not ask_ev:
|
|
344
|
+
if isinstance(ask, str) and ask in self.asks:
|
|
345
|
+
ask_ev = self.asks[ask]
|
|
346
|
+
else:
|
|
347
|
+
raise ComputeError(f"ask not found: {ask}")
|
|
348
|
+
return match_ask_to_offers(ask_ev, list(self.order_book_offers.values()), now=self.clock(), trust_weights=trust_weights)
|
|
349
|
+
|
|
350
|
+
def find_asks_for_offer(
|
|
351
|
+
self,
|
|
352
|
+
offer: NostrEvent | Mapping[str, Any] | str,
|
|
353
|
+
) -> list[Mapping[str, Any]]:
|
|
354
|
+
off_ev = self.order_book_offers.get(offer) if isinstance(offer, str) else offer
|
|
355
|
+
if not off_ev:
|
|
356
|
+
if isinstance(offer, str):
|
|
357
|
+
for o in self.offers:
|
|
358
|
+
if o.get('id') == offer:
|
|
359
|
+
off_ev = o
|
|
360
|
+
break
|
|
361
|
+
if not off_ev:
|
|
362
|
+
raise ComputeError(f"offer not found: {offer}")
|
|
363
|
+
return match_offer_to_asks(off_ev, list(self.order_book_asks.values()), now=self.clock())
|
|
364
|
+
|
|
365
|
+
def state(self) -> Mapping[str, Any]:
|
|
366
|
+
snap = self.load_monitor.snapshot()
|
|
367
|
+
return {
|
|
368
|
+
'offers_published': len(self.offers),
|
|
369
|
+
'active_asks': len(self.asks),
|
|
370
|
+
'indexed_offers': len(self.order_book_offers),
|
|
371
|
+
'indexed_asks': len(self.order_book_asks),
|
|
372
|
+
'receipts_held': len(self.receipts_held),
|
|
373
|
+
'jobs_executed': self.jobs_executed,
|
|
374
|
+
'jobs_reflected': snap['jobs_reflected'],
|
|
375
|
+
'total_fuel_used': snap['total_fuel_used'],
|
|
376
|
+
'cpu_utilization_ratio': snap['cpu_utilization_ratio'],
|
|
377
|
+
'cpu_load_pct': snap['cpu_load_pct'],
|
|
378
|
+
'active_jobs': snap['active_jobs'],
|
|
379
|
+
'max_concurrent_jobs': snap['max_concurrent_jobs'],
|
|
380
|
+
'free_slots': snap['free_slots'],
|
|
381
|
+
'available_engines': self.registry.runnable_engines(),
|
|
382
|
+
'registered_engines': list(self.registry.executors.keys()),
|
|
383
|
+
'active_sessions': len(self.compute_wire.sessions),
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
def trouble(self) -> list[str]:
|
|
387
|
+
out: list[str] = []
|
|
388
|
+
recent_errs = self.refusals.within(self.TROUBLE_WINDOW, self.clock(), kind='compute')
|
|
389
|
+
if recent_errs:
|
|
390
|
+
out.append('%d compute failure(s) in the last 30 minutes' % len(recent_errs))
|
|
391
|
+
return out
|
|
392
|
+
|
|
393
|
+
def receipts_cmd(self) -> list[Mapping[str, Any]]:
|
|
394
|
+
return list(self.receipts_held)
|
|
395
|
+
|
|
396
|
+
def book_cmd(self) -> Mapping[str, Any]:
|
|
397
|
+
return {
|
|
398
|
+
'offers_count': len(self.order_book_offers),
|
|
399
|
+
'asks_count': len(self.order_book_asks),
|
|
400
|
+
'offers': [parse_offer(o) for o in self.order_book_offers.values()],
|
|
401
|
+
'asks': [parse_ask(a) for a in self.order_book_asks.values()],
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
def index_offer_cmd(self, args: Mapping[str, Any]) -> bool:
|
|
405
|
+
return self.index_offer(args['offer'])
|
|
406
|
+
|
|
407
|
+
def index_ask_cmd(self, args: Mapping[str, Any]) -> bool:
|
|
408
|
+
return self.index_ask(args['ask'])
|
|
409
|
+
|
|
410
|
+
def match_cmd(self, args: Mapping[str, Any]) -> list[Any]:
|
|
411
|
+
if args.get('ask') is not None:
|
|
412
|
+
return self.find_offers_for_ask(args['ask'])
|
|
413
|
+
if args.get('offer') is not None:
|
|
414
|
+
return self.find_asks_for_offer(args['offer'])
|
|
415
|
+
raise ComputeError("match requires 'ask' or 'offer' argument")
|
|
416
|
+
|
|
417
|
+
def _require_keystore(self) -> Any:
|
|
418
|
+
"""The node keystore, or a ComputeError when this subsystem is detached."""
|
|
419
|
+
if self.ks is None:
|
|
420
|
+
raise ComputeError('no node keystore available')
|
|
421
|
+
return self.ks
|
|
422
|
+
|
|
423
|
+
def _cmd_load(self) -> Mapping[str, Any]:
|
|
424
|
+
return self.load_monitor.snapshot()
|
|
425
|
+
|
|
426
|
+
def _cmd_offer(self, args: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
427
|
+
ks = self._require_keystore()
|
|
428
|
+
engine = args['engine']
|
|
429
|
+
msat = str(args['msat_per_mfuel'])
|
|
430
|
+
runtimes = None
|
|
431
|
+
executor_obj = self.registry.get(engine)
|
|
432
|
+
if executor_obj and executor_obj.wasi_profile_digest:
|
|
433
|
+
runtimes = [[engine, '1.0', executor_obj.wasi_profile_digest]]
|
|
434
|
+
limits = self.load_monitor.offer_limits()
|
|
435
|
+
now_ts = self.clock()
|
|
436
|
+
ev = build_offer(
|
|
437
|
+
ks,
|
|
438
|
+
executors=[[engine, 'msat_per_mfuel', msat]],
|
|
439
|
+
limits=limits,
|
|
440
|
+
runtimes=runtimes,
|
|
441
|
+
now=now_ts,
|
|
442
|
+
expiration=now_ts + 86400,
|
|
443
|
+
)
|
|
444
|
+
self.offers.append(ev)
|
|
445
|
+
self.order_book_offers[ev['id']] = ev
|
|
446
|
+
self.say('offer_published', engine=engine)
|
|
447
|
+
return {'id': ev['id'], 'kind': ev['kind'], 'tags': ev['tags']}
|
|
448
|
+
|
|
449
|
+
def _cmd_ask(self, args: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
450
|
+
ks = self._require_keystore()
|
|
451
|
+
ask_id = args['id']
|
|
452
|
+
job = args['job']
|
|
453
|
+
bid = int(args['bid'])
|
|
454
|
+
now_ts = self.clock()
|
|
455
|
+
ev = build_ask(
|
|
456
|
+
buyer_sk=ks.node_sk,
|
|
457
|
+
ask_id=ask_id,
|
|
458
|
+
job_payload=job,
|
|
459
|
+
executor=args.get('executor', str(job.get('executor', 'wasm'))),
|
|
460
|
+
bid_currency='msat_per_mfuel',
|
|
461
|
+
bid_price=bid,
|
|
462
|
+
deadline=now_ts + 3600,
|
|
463
|
+
fuel_max=int(job.get('fuel_max', 2500000000)),
|
|
464
|
+
verify_mode=args.get('verify', 'none'),
|
|
465
|
+
now=now_ts,
|
|
466
|
+
expiration=now_ts + 7200,
|
|
467
|
+
)
|
|
468
|
+
self.asks[ask_id] = ev
|
|
469
|
+
self.order_book_asks[ask_id] = ev
|
|
470
|
+
self.say('ask_published', ask_id=str(ask_id)[:16], bid=bid)
|
|
471
|
+
return {'id': ev['id'], 'kind': ev['kind'], 'commit': ev['tags'][1][1]}
|
|
472
|
+
|
|
473
|
+
def _cmd_run(self, args: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
474
|
+
ks = self._require_keystore()
|
|
475
|
+
job = dict(args['job'])
|
|
476
|
+
if 'blob_store' not in job and self.blobs is not None:
|
|
477
|
+
job['blob_store'] = self.blobs
|
|
478
|
+
buyer_pk = args['buyer']
|
|
479
|
+
with self.load_monitor.job_slot():
|
|
480
|
+
try:
|
|
481
|
+
res = run_job(job, registry=self.registry)
|
|
482
|
+
except Exception as e:
|
|
483
|
+
self.refusals.refuse('compute', 'execution failed', detail={'error': str(e)})
|
|
484
|
+
raise ComputeError('job execution failed: %s' % (e,))
|
|
485
|
+
|
|
486
|
+
self.load_monitor.record_fuel(int(res.get('fuel_used') or 0))
|
|
487
|
+
paid_msat = int(job.get('paid_msat') or args.get('paid_msat') or 0)
|
|
488
|
+
payment_hash = job.get('payment_hash') or args.get('payment_hash') or ('0' * 64)
|
|
489
|
+
|
|
490
|
+
blinding = crypto.sha256(os.urandom(32))
|
|
491
|
+
receipt_payload = {
|
|
492
|
+
'ask': job.get('ask', '0' * 64),
|
|
493
|
+
'module': (job.get('module') or {}).get('sha256', '0' * 64),
|
|
494
|
+
'input': (job.get('input') or {}).get('sha256', '0' * 64),
|
|
495
|
+
'result': res['result_hash'],
|
|
496
|
+
'result_cid': res.get('result_cid'),
|
|
497
|
+
'fuel_used': res['fuel_used'],
|
|
498
|
+
'paid_msat': paid_msat,
|
|
499
|
+
'payment_hash': payment_hash,
|
|
500
|
+
'executor': {
|
|
501
|
+
'engine': res.get('engine', 'wasm'),
|
|
502
|
+
'wasi_profile_digest': res.get('wasi_profile_digest', '0' * 64),
|
|
503
|
+
},
|
|
504
|
+
'verify': job.get('verify', {'mode': 'none'}),
|
|
505
|
+
'blinding': blinding,
|
|
506
|
+
}
|
|
507
|
+
if args.get('preimage'):
|
|
508
|
+
receipt_payload['preimage'] = args['preimage']
|
|
509
|
+
|
|
510
|
+
ev = build_receipt(ks.node_sk, buyer_pk, receipt_payload, blinding)
|
|
511
|
+
self.receipts_held.append({'receipt': ev, 'payload': receipt_payload, 'buyer': str(buyer_pk)})
|
|
512
|
+
self.jobs_executed += 1
|
|
513
|
+
self.say(
|
|
514
|
+
'job_executed',
|
|
515
|
+
job_id=str(job.get('ask', 'job'))[:16],
|
|
516
|
+
buyer=str(buyer_pk),
|
|
517
|
+
fuel=int(res['fuel_used']),
|
|
518
|
+
)
|
|
519
|
+
return {
|
|
520
|
+
'result_hash': res['result_hash'],
|
|
521
|
+
'result_cid': res.get('result_cid'),
|
|
522
|
+
'raw_output': res.get('raw_output'),
|
|
523
|
+
'counters': res.get('counters', {}),
|
|
524
|
+
'receipt_id': ev['id'],
|
|
525
|
+
'receipt': ev,
|
|
526
|
+
'fuel_used': res['fuel_used'],
|
|
527
|
+
'paid_msat': paid_msat,
|
|
528
|
+
'payment_hash': payment_hash,
|
|
529
|
+
'worker_pubkey': ks.node_pk,
|
|
530
|
+
'reflected': False,
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
def submit_or_reflect(
|
|
534
|
+
self,
|
|
535
|
+
job: Mapping[str, Any],
|
|
536
|
+
buyer_pk: str,
|
|
537
|
+
*,
|
|
538
|
+
bid_msat: int = 0,
|
|
539
|
+
force_reflect: bool = False,
|
|
540
|
+
remote_dispatcher: Optional[Callable[[Mapping[str, Any], Mapping[str, Any]], Mapping[str, Any]]] = None,
|
|
541
|
+
) -> Mapping[str, Any]:
|
|
542
|
+
"""Execute a job locally if CPU/slot headroom permits, or reflect it to the least-loaded mesh peer."""
|
|
543
|
+
ks = self._require_keystore()
|
|
544
|
+
should_reflect, reason = self.load_monitor.should_reflect_job()
|
|
545
|
+
if force_reflect:
|
|
546
|
+
should_reflect, reason = True, 'forced_reflection'
|
|
547
|
+
|
|
548
|
+
if should_reflect:
|
|
549
|
+
ask_id = str(job.get('ask') or crypto.sha256(os.urandom(32)))
|
|
550
|
+
exec_engine = str(job.get('executor') or job.get('engine') or 'wasm')
|
|
551
|
+
v_obj = job.get('verify')
|
|
552
|
+
v_mode = str(v_obj.get('mode', 'delivery')) if isinstance(v_obj, Mapping) else 'delivery'
|
|
553
|
+
if v_mode not in ('none', 'delivery', 'sample', 'dual', 'proof', 'schema-bounds', 'tee-attestation', 'quasi-deterministic'):
|
|
554
|
+
v_mode = 'delivery'
|
|
555
|
+
job_with_ask = dict(job, ask=ask_id)
|
|
556
|
+
self._cmd_ask({
|
|
557
|
+
'id': ask_id,
|
|
558
|
+
'job': job_with_ask,
|
|
559
|
+
'bid': bid_msat,
|
|
560
|
+
'executor': exec_engine,
|
|
561
|
+
'verify': v_mode,
|
|
562
|
+
})
|
|
563
|
+
candidates = [
|
|
564
|
+
m for m in self.find_offers_for_ask(ask_id)
|
|
565
|
+
if m.get('seller_pubkey') != ks.node_pk
|
|
566
|
+
]
|
|
567
|
+
if candidates:
|
|
568
|
+
best = candidates[0]
|
|
569
|
+
peer_pk = str(best['seller_pubkey'])
|
|
570
|
+
self.load_monitor.record_reflection()
|
|
571
|
+
self.say('job_reflected', job_id=ask_id[:16], peer=peer_pk[:16], reason=reason)
|
|
572
|
+
if hasattr(self.compute_wire, 'open_buyer_session'):
|
|
573
|
+
self.compute_wire.open_buyer_session(ask_id, peer_pk, job_with_ask)
|
|
574
|
+
dispatcher = remote_dispatcher or self.remote_dispatcher
|
|
575
|
+
if callable(dispatcher):
|
|
576
|
+
remote_res = dict(dispatcher(best, job_with_ask))
|
|
577
|
+
remote_res.setdefault('reflected', True)
|
|
578
|
+
remote_res.setdefault('reflect_reason', reason)
|
|
579
|
+
remote_res.setdefault('worker_pubkey', peer_pk)
|
|
580
|
+
remote_res.setdefault('offer_id', best['offer_id'])
|
|
581
|
+
remote_res.setdefault('ask_id', ask_id)
|
|
582
|
+
return remote_res
|
|
583
|
+
return {
|
|
584
|
+
'reflected': True,
|
|
585
|
+
'reflect_reason': reason,
|
|
586
|
+
'ask_id': ask_id,
|
|
587
|
+
'offer_id': best['offer_id'],
|
|
588
|
+
'worker_pubkey': peer_pk,
|
|
589
|
+
'candidate': best,
|
|
590
|
+
'candidates': candidates,
|
|
591
|
+
}
|
|
592
|
+
self.asks.pop(ask_id, None)
|
|
593
|
+
self.order_book_asks.pop(ask_id, None)
|
|
594
|
+
|
|
595
|
+
res = dict(self._cmd_run({'job': job, 'buyer': buyer_pk}))
|
|
596
|
+
res['reflected'] = False
|
|
597
|
+
res['reflect_reason'] = reason
|
|
598
|
+
return res
|
|
599
|
+
|
|
600
|
+
def _cmd_submit(self, args: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
601
|
+
return self.submit_or_reflect(
|
|
602
|
+
args['job'],
|
|
603
|
+
args['buyer'],
|
|
604
|
+
bid_msat=int(args.get('bid') or 0),
|
|
605
|
+
force_reflect=bool(args.get('force_reflect', False)),
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
def _cmd_models(self) -> Mapping[str, Any]:
|
|
609
|
+
llm_exec = self.registry.get('llm')
|
|
610
|
+
router = getattr(self.registry, 'provider_router', None)
|
|
611
|
+
active_prov = router.get_provider() if router is not None else None
|
|
612
|
+
default_m = (
|
|
613
|
+
active_prov.default_model
|
|
614
|
+
if active_prov is not None
|
|
615
|
+
else getattr(llm_exec, 'default_model', 'meta-llama/Llama-3.3-70B-Instruct')
|
|
616
|
+
)
|
|
617
|
+
endpoint = (
|
|
618
|
+
active_prov.endpoint
|
|
619
|
+
if active_prov is not None
|
|
620
|
+
else getattr(llm_exec, 'endpoint', 'http://127.0.0.1:8000/v1')
|
|
621
|
+
)
|
|
622
|
+
avail: list[str] = [
|
|
623
|
+
default_m,
|
|
624
|
+
'deepseek-ai/DeepSeek-R1',
|
|
625
|
+
'meta-llama/Llama-3.1-8B-Instruct',
|
|
626
|
+
]
|
|
627
|
+
if router is not None:
|
|
628
|
+
for p in router.list_public_providers():
|
|
629
|
+
for m in p.get('models', []):
|
|
630
|
+
if m not in avail:
|
|
631
|
+
avail.append(m)
|
|
632
|
+
return {
|
|
633
|
+
'default_model': default_m,
|
|
634
|
+
'endpoint': endpoint,
|
|
635
|
+
'default_provider': router.default_provider if router is not None else 'vllm',
|
|
636
|
+
'providers': router.list_public_providers() if router is not None else [],
|
|
637
|
+
'available_models': avail,
|
|
638
|
+
'features': ['streaming', 'tool-calling', 'grammar-json', 'schema-bounds', 'multi-provider'],
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
def _cmd_link_provider(self, args: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
642
|
+
from .executors.providers import LinkedProviderConfig
|
|
643
|
+
cfg = LinkedProviderConfig.from_preset(
|
|
644
|
+
str(args.get('provider_id') or 'openrouter'),
|
|
645
|
+
endpoint=args.get('endpoint'),
|
|
646
|
+
default_model=args.get('default_model'),
|
|
647
|
+
api_key=args.get('api_key'),
|
|
648
|
+
cashu_token=args.get('cashu_token'),
|
|
649
|
+
rate_msat_per_token=args.get('rate_msat_per_token'),
|
|
650
|
+
enabled=bool(args.get('enabled', True)),
|
|
651
|
+
)
|
|
652
|
+
self.registry.configure_llm_provider(cfg, set_default=bool(args.get('set_default', True)))
|
|
653
|
+
return cfg.to_public_dict()
|
|
654
|
+
|
|
655
|
+
def _cmd_stream(self, args: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
656
|
+
prompt = args['prompt']
|
|
657
|
+
model = args.get('model')
|
|
658
|
+
job = {
|
|
659
|
+
'executor': 'llm',
|
|
660
|
+
'prompt': prompt,
|
|
661
|
+
'stream': True,
|
|
662
|
+
}
|
|
663
|
+
if model:
|
|
664
|
+
job['model'] = model
|
|
665
|
+
with self.load_monitor.job_slot():
|
|
666
|
+
try:
|
|
667
|
+
res = run_job(job, registry=self.registry)
|
|
668
|
+
except Exception as e:
|
|
669
|
+
self.refusals.refuse('compute', 'stream execution failed', detail={'error': str(e)})
|
|
670
|
+
raise ComputeError('stream execution failed: %s' % (e,))
|
|
671
|
+
self.load_monitor.record_fuel(int(res.get('fuel_used') or 0))
|
|
672
|
+
raw = res['raw_output']
|
|
673
|
+
out_text = raw.decode('utf-8', errors='replace') if isinstance(raw, bytes) else str(raw)
|
|
674
|
+
return {
|
|
675
|
+
'model': model or getattr(self.registry.get('llm'), 'default_model', 'meta-llama/Llama-3.3-70B-Instruct'),
|
|
676
|
+
'output': out_text,
|
|
677
|
+
'result_hash': res['result_hash'],
|
|
678
|
+
'fuel_used': res['fuel_used'],
|
|
679
|
+
'counters': res.get('counters', {}),
|
|
680
|
+
'verification_mode': res.get('verification_mode', 'delivery'),
|
|
681
|
+
}
|