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.
Files changed (338) hide show
  1. fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
  2. fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
  3. fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
  4. fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
  5. fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
  6. fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
  7. node/RELEASE +1 -0
  8. node/__init__.py +35 -0
  9. node/app/__init__.py +49 -0
  10. node/app/commands.py +265 -0
  11. node/app/config.py +131 -0
  12. node/app/control.py +670 -0
  13. node/app/core.py +867 -0
  14. node/app/plugins.py +76 -0
  15. node/app/router.py +83 -0
  16. node/app/socket.py +431 -0
  17. node/app/systemd.py +36 -0
  18. node/app/views.py +204 -0
  19. node/channels/__init__.py +378 -0
  20. node/channels/bus.py +331 -0
  21. node/channels/journal.py +46 -0
  22. node/commands/__init__.py +140 -0
  23. node/commands/adapter.py +210 -0
  24. node/commands/base.py +278 -0
  25. node/commands/cluster_cmds.py +183 -0
  26. node/commands/compute_cmds.py +401 -0
  27. node/commands/core.py +474 -0
  28. node/commands/gossip_cmds.py +106 -0
  29. node/commands/nsite_cmds.py +141 -0
  30. node/commands/peering.py +402 -0
  31. node/commands/plugin_cmds.py +135 -0
  32. node/commands/sidecar_cmds.py +120 -0
  33. node/commands/sync.py +388 -0
  34. node/commands/transport_cmds.py +451 -0
  35. node/commands/video_cmds.py +99 -0
  36. node/compartment/__init__.py +124 -0
  37. node/compartment/base.py +868 -0
  38. node/compartment/command.py +458 -0
  39. node/compartment/context.py +163 -0
  40. node/compartment/every.py +108 -0
  41. node/compartment/handler.py +131 -0
  42. node/compartment/lifecycle.py +460 -0
  43. node/compartment/lint.py +388 -0
  44. node/compartment/need.py +158 -0
  45. node/compartment/problem.py +521 -0
  46. node/compartment/refusals.py +120 -0
  47. node/compartment/registry.py +790 -0
  48. node/compartment/schema.py +290 -0
  49. node/compartment/sidecar.py +499 -0
  50. node/compartment/stores.py +662 -0
  51. node/compartment/wire.py +225 -0
  52. node/compartment/worker.py +61 -0
  53. node/config/__init__.py +43 -0
  54. node/config/base.py +116 -0
  55. node/config/change.py +441 -0
  56. node/config/changestore.py +201 -0
  57. node/config/compartments.py +1144 -0
  58. node/config/node.py +318 -0
  59. node/config/parameter.py +563 -0
  60. node/config/protocols.py +274 -0
  61. node/config/records.py +171 -0
  62. node/examples/__init__.py +1 -0
  63. node/examples/demo.py +226 -0
  64. node/examples/follower.py +46 -0
  65. node/interfaces.py +235 -0
  66. node/keystore/__init__.py +27 -0
  67. node/keystore/core.py +103 -0
  68. node/keystore/enclave.py +688 -0
  69. node/meshnode.py +111 -0
  70. node/metrics/__init__.py +396 -0
  71. node/metrics/adapters.py +102 -0
  72. node/metrics/face.py +110 -0
  73. node/planes/__init__.py +21 -0
  74. node/planes/identity/__init__.py +20 -0
  75. node/planes/identity/announce/__init__.py +9 -0
  76. node/planes/identity/announce/compartment.py +447 -0
  77. node/planes/identity/announce/types.py +26 -0
  78. node/planes/identity/discovery/__init__.py +9 -0
  79. node/planes/identity/discovery/compartment.py +522 -0
  80. node/planes/identity/discovery/types.py +13 -0
  81. node/planes/identity/linked/__init__.py +1 -0
  82. node/planes/identity/linked/compartment.py +270 -0
  83. node/planes/identity/linked/types.py +25 -0
  84. node/planes/identity/presence/__init__.py +7 -0
  85. node/planes/identity/presence/compartment.py +409 -0
  86. node/planes/identity/presence/types.py +14 -0
  87. node/planes/operations/__init__.py +13 -0
  88. node/planes/operations/scheduler/__init__.py +9 -0
  89. node/planes/operations/scheduler/compartment.py +353 -0
  90. node/planes/operations/scheduler/types.py +21 -0
  91. node/planes/operations/updates/__init__.py +17 -0
  92. node/planes/operations/updates/core.py +310 -0
  93. node/planes/operations/updates/store.py +79 -0
  94. node/planes/operations/updates/types.py +20 -0
  95. node/planes/peering/__init__.py +28 -0
  96. node/planes/peering/budget/__init__.py +43 -0
  97. node/planes/peering/budget/chain.py +43 -0
  98. node/planes/peering/budget/core.py +537 -0
  99. node/planes/peering/budget/evaluation.py +80 -0
  100. node/planes/peering/budget/meter.py +100 -0
  101. node/planes/peering/budget/receipts.py +439 -0
  102. node/planes/peering/budget/shape.py +101 -0
  103. node/planes/peering/budget/tick.py +110 -0
  104. node/planes/peering/budget/types.py +67 -0
  105. node/planes/peering/budget/windows.py +346 -0
  106. node/planes/peering/capacity/__init__.py +7 -0
  107. node/planes/peering/capacity/compartment.py +342 -0
  108. node/planes/peering/capacity/types.py +24 -0
  109. node/planes/peering/peering/__init__.py +25 -0
  110. node/planes/peering/peering/core.py +464 -0
  111. node/planes/peering/peering/envelope.py +142 -0
  112. node/planes/peering/peering/handshake.py +198 -0
  113. node/planes/peering/peering/shape.py +80 -0
  114. node/planes/peering/peering/types.py +39 -0
  115. node/planes/peering/reporter/__init__.py +42 -0
  116. node/planes/peering/reporter/constants.py +29 -0
  117. node/planes/peering/reporter/core.py +299 -0
  118. node/planes/peering/reporter/finding.py +74 -0
  119. node/planes/peering/reporter/findings.py +171 -0
  120. node/planes/peering/reporter/publish.py +285 -0
  121. node/planes/peering/reporter/reading.py +193 -0
  122. node/planes/peering/reporter/shape.py +86 -0
  123. node/planes/peering/reporter/types.py +50 -0
  124. node/planes/peering/strain/__init__.py +9 -0
  125. node/planes/peering/strain/compartment.py +204 -0
  126. node/planes/peering/strain/types.py +21 -0
  127. node/planes/peering/trust/__init__.py +9 -0
  128. node/planes/peering/trust/compartment.py +239 -0
  129. node/planes/peering/trust/types.py +30 -0
  130. node/planes/services/__init__.py +33 -0
  131. node/planes/services/compute/__init__.py +160 -0
  132. node/planes/services/compute/common.py +134 -0
  133. node/planes/services/compute/compartment.py +681 -0
  134. node/planes/services/compute/executors/__init__.py +121 -0
  135. node/planes/services/compute/executors/agent.py +840 -0
  136. node/planes/services/compute/executors/bacalhau.py +605 -0
  137. node/planes/services/compute/executors/base.py +127 -0
  138. node/planes/services/compute/executors/harness.py +439 -0
  139. node/planes/services/compute/executors/mcp.py +454 -0
  140. node/planes/services/compute/executors/media.py +898 -0
  141. node/planes/services/compute/executors/nondet.py +283 -0
  142. node/planes/services/compute/executors/providers.py +499 -0
  143. node/planes/services/compute/executors/registry.py +140 -0
  144. node/planes/services/compute/executors/wasm.py +244 -0
  145. node/planes/services/compute/executors/zk.py +604 -0
  146. node/planes/services/compute/jobs.py +227 -0
  147. node/planes/services/compute/load.py +180 -0
  148. node/planes/services/compute/market.py +433 -0
  149. node/planes/services/compute/sandbox.py +282 -0
  150. node/planes/services/compute/types.py +84 -0
  151. node/planes/services/compute_wire/__init__.py +58 -0
  152. node/planes/services/compute_wire/compartment.py +256 -0
  153. node/planes/services/compute_wire/messages.py +320 -0
  154. node/planes/services/compute_wire/sealing.py +147 -0
  155. node/planes/services/compute_wire/session.py +572 -0
  156. node/planes/services/compute_wire/types.py +49 -0
  157. node/planes/services/compute_wire/verify.py +331 -0
  158. node/planes/services/dvm/__init__.py +24 -0
  159. node/planes/services/dvm/gateway.py +526 -0
  160. node/planes/services/dvm/protocol.py +35 -0
  161. node/planes/services/dvm/translate.py +15 -0
  162. node/planes/services/dvm/types.py +25 -0
  163. node/planes/services/gateway/__init__.py +40 -0
  164. node/planes/services/gateway/settle.py +279 -0
  165. node/planes/services/gateway/translate.py +644 -0
  166. node/planes/services/gossip/__init__.py +22 -0
  167. node/planes/services/gossip/core.py +929 -0
  168. node/planes/services/gossip/envelope.py +154 -0
  169. node/planes/services/gossip/mesh.py +127 -0
  170. node/planes/services/gossip/types.py +50 -0
  171. node/planes/services/nsite/__init__.py +7 -0
  172. node/planes/services/nsite/client.py +62 -0
  173. node/planes/services/nsite/compartment.py +752 -0
  174. node/planes/services/nsite/types.py +44 -0
  175. node/planes/services/paidfetch/__init__.py +7 -0
  176. node/planes/services/paidfetch/compartment.py +862 -0
  177. node/planes/services/paidfetch/types.py +99 -0
  178. node/planes/services/settle/__init__.py +27 -0
  179. node/planes/services/settle/compartment.py +384 -0
  180. node/planes/services/settle/engine.py +456 -0
  181. node/planes/services/settle/gateways.py +541 -0
  182. node/planes/services/settle/payment.py +512 -0
  183. node/planes/services/settle/store.py +74 -0
  184. node/planes/services/settle/types.py +58 -0
  185. node/planes/services/sidecars/__init__.py +8 -0
  186. node/planes/services/sidecars/compartment.py +461 -0
  187. node/planes/services/sidecars/manifest.py +304 -0
  188. node/planes/services/sidecars/manifests/10-grasp.json +37 -0
  189. node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
  190. node/planes/services/sidecars/manifests/30-magpie.json +28 -0
  191. node/planes/services/sidecars/types.py +43 -0
  192. node/planes/services/video/__init__.py +106 -0
  193. node/planes/services/video/barter.py +407 -0
  194. node/planes/services/video/cli.py +158 -0
  195. node/planes/services/video/compartment.py +932 -0
  196. node/planes/services/video/ingest.py +524 -0
  197. node/planes/services/video/ledger.py +705 -0
  198. node/planes/services/video/packager.py +847 -0
  199. node/planes/services/video/player.py +456 -0
  200. node/planes/services/video/timed_segments.py +124 -0
  201. node/planes/services/video/transit.py +478 -0
  202. node/planes/store/__init__.py +24 -0
  203. node/planes/store/blob_replication/__init__.py +7 -0
  204. node/planes/store/blob_replication/compartment.py +496 -0
  205. node/planes/store/blob_replication/types.py +35 -0
  206. node/planes/store/blob_wire/__init__.py +48 -0
  207. node/planes/store/blob_wire/client.py +57 -0
  208. node/planes/store/blob_wire/core.py +359 -0
  209. node/planes/store/blob_wire/intake.py +74 -0
  210. node/planes/store/blob_wire/messages.py +85 -0
  211. node/planes/store/projected/__init__.py +7 -0
  212. node/planes/store/projected/compartment.py +258 -0
  213. node/planes/store/projected/types.py +29 -0
  214. node/planes/store/retention/__init__.py +7 -0
  215. node/planes/store/retention/compartment.py +214 -0
  216. node/planes/store/retention/types.py +12 -0
  217. node/planes/store/sync/__init__.py +35 -0
  218. node/planes/store/sync/core.py +311 -0
  219. node/planes/store/sync/envelope.py +106 -0
  220. node/planes/store/sync/handover.py +206 -0
  221. node/planes/store/sync/initiator.py +128 -0
  222. node/planes/store/sync/responder.py +114 -0
  223. node/planes/store/sync/rules.py +179 -0
  224. node/planes/store/sync/shape.py +80 -0
  225. node/planes/store/sync/types.py +51 -0
  226. node/planes/transport/__init__.py +23 -0
  227. node/planes/transport/bridge/__init__.py +13 -0
  228. node/planes/transport/bridge/compartment.py +785 -0
  229. node/planes/transport/bridge/ratelimit.py +44 -0
  230. node/planes/transport/bridge/types.py +43 -0
  231. node/planes/transport/lan/__init__.py +9 -0
  232. node/planes/transport/lan/compartment.py +446 -0
  233. node/planes/transport/lan/types.py +25 -0
  234. node/planes/transport/link/__init__.py +7 -0
  235. node/planes/transport/link/compartment.py +257 -0
  236. node/planes/transport/link/types.py +27 -0
  237. node/planes/transport/mediation/__init__.py +45 -0
  238. node/planes/transport/mediation/compartment.py +238 -0
  239. node/planes/transport/mediation/leaf.py +107 -0
  240. node/planes/transport/mediation/messages.py +302 -0
  241. node/planes/transport/mediation/service.py +233 -0
  242. node/planes/transport/mediation/store.py +37 -0
  243. node/planes/transport/mediation/types.py +20 -0
  244. node/planes/transport/p2p/__init__.py +7 -0
  245. node/planes/transport/p2p/compartment.py +349 -0
  246. node/planes/transport/p2p/types.py +22 -0
  247. node/protocols/__init__.py +277 -0
  248. node/protocols/activitypub/__init__.py +56 -0
  249. node/protocols/activitypub/actor.py +230 -0
  250. node/protocols/activitypub/federation.py +490 -0
  251. node/protocols/activitypub/media.py +145 -0
  252. node/protocols/activitypub/models.py +286 -0
  253. node/protocols/activitypub/package.py +271 -0
  254. node/protocols/activitypub/store.py +564 -0
  255. node/protocols/arcade/__init__.py +26 -0
  256. node/protocols/atproto/__init__.py +13 -0
  257. node/protocols/atproto/firehose.py +126 -0
  258. node/protocols/atproto/metering.py +165 -0
  259. node/protocols/atproto/package.py +155 -0
  260. node/protocols/atproto/repo.py +250 -0
  261. node/protocols/atproto/xrpc.py +285 -0
  262. node/protocols/base.py +109 -0
  263. node/protocols/circles/__init__.py +29 -0
  264. node/protocols/dm/__init__.py +50 -0
  265. node/protocols/grasp/__init__.py +22 -0
  266. node/protocols/isotime.py +15 -0
  267. node/protocols/nostr/__init__.py +51 -0
  268. node/protocols/nostr/blobstore.py +383 -0
  269. node/protocols/nostr/blossom.py +627 -0
  270. node/protocols/nostr/client.py +377 -0
  271. node/protocols/nostr/descriptor.py +211 -0
  272. node/protocols/nostr/eventstore.py +304 -0
  273. node/protocols/nostr/package.py +182 -0
  274. node/protocols/nostr/peers.py +109 -0
  275. node/protocols/nostr/relayserver.py +656 -0
  276. node/protocols/p2panda/__init__.py +22 -0
  277. node/protocols/p2panda/identity.py +19 -0
  278. node/protocols/p2panda/package.py +236 -0
  279. node/protocols/p2panda/rung.py +152 -0
  280. node/protocols/p2panda/store.py +273 -0
  281. node/protocols/rhio/__init__.py +33 -0
  282. node/protocols/rhio/package.py +275 -0
  283. node/protocols/rhio/rung.py +136 -0
  284. node/protocols/rhio/store.py +1423 -0
  285. node/protocols/ssb/muxrpc.py +256 -0
  286. node/protocols/ssb/package.py +267 -0
  287. node/protocols/textile/__init__.py +111 -0
  288. node/protocols/textile/transducers.py +259 -0
  289. node/release.py +119 -0
  290. node/storage/__init__.py +88 -0
  291. node/storage/accounts.py +305 -0
  292. node/storage/adapter.py +161 -0
  293. node/storage/base.py +385 -0
  294. node/storage/dialect.py +209 -0
  295. node/storage/grants.py +317 -0
  296. node/storage/locator.py +150 -0
  297. node/storage/postgres.py +294 -0
  298. node/storage/projected.py +953 -0
  299. node/storage/sharding.py +448 -0
  300. node/storage/standing.py +568 -0
  301. node/testnet/__init__.py +9 -0
  302. node/testnet/cluster.py +731 -0
  303. node/testnet/fips_daemon.py +230 -0
  304. node/transport/__init__.py +94 -0
  305. node/transport/const.py +110 -0
  306. node/transport/didcomm_inbound.py +251 -0
  307. node/transport/dnstxt.py +169 -0
  308. node/transport/fips_rung/__init__.py +67 -0
  309. node/transport/fips_rung/core.py +666 -0
  310. node/transport/fips_rung/listener.py +96 -0
  311. node/transport/fips_rung/types.py +12 -0
  312. node/transport/host.py +334 -0
  313. node/transport/iroh_rung/__init__.py +29 -0
  314. node/transport/iroh_rung/core.py +477 -0
  315. node/transport/iroh_rung/quic.py +942 -0
  316. node/transport/iroh_rung/types.py +22 -0
  317. node/transport/ladder.py +145 -0
  318. node/transport/lineproto.py +49 -0
  319. node/transport/mdns.py +303 -0
  320. node/transport/netfetch.py +283 -0
  321. node/transport/relay_rung/__init__.py +23 -0
  322. node/transport/relay_rung/cooldown.py +87 -0
  323. node/transport/relay_rung/core.py +138 -0
  324. node/transport/relay_rung/inbound.py +161 -0
  325. node/transport/relay_rung/ledger.py +104 -0
  326. node/transport/relay_rung/listen.py +121 -0
  327. node/transport/relay_rung/outbound.py +181 -0
  328. node/transport/relay_rung/shape.py +71 -0
  329. node/transport/relay_rung/types.py +58 -0
  330. node/transport/relay_rung/wire.py +63 -0
  331. node/transport/router.py +761 -0
  332. node/transport/rung.py +276 -0
  333. node/transport/ssb_rung.py +166 -0
  334. node/transport/ws.py +502 -0
  335. sidecar/fleetmesh_announce.py +338 -0
  336. sidecar/fleetmesh_p2p.py +373 -0
  337. sidecar/fleetmesh_rhio.py +352 -0
  338. sidecar/fleetmesh_ssb.py +603 -0
@@ -0,0 +1,499 @@
1
+ """Operator-linked AI/LLM inference provider adapters for FleetMesh non-deterministic compute.
2
+
3
+ Supports linking a MeshNode to external and local inference providers:
4
+ - OpenRouter (`openrouter`)
5
+ - Anthropic Claude (`claude` / `anthropic`)
6
+ - OpenAI (`openai`)
7
+ - Google Gemini (`gemini`)
8
+ - GitHub Copilot (`copilot`)
9
+ - Routstr eCash Proxy (`routstr`)
10
+ - PayPerQuery / PPQ (`payperquery` / `ppq`)
11
+ - Local vLLM / Ollama (`vllm`, `ollama`, `custom`)
12
+
13
+ Security Invariant:
14
+ All upstream URLs and credentials (`api_key`, `cashu_token`) are strictly
15
+ operator-configured on the host node. Remote compute jobs may select an
16
+ advertised `provider` alias or `model` ID, never an arbitrary URL.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import dataclasses
21
+ import os
22
+ from typing import Any, Mapping, Optional, Sequence
23
+
24
+
25
+ @dataclasses.dataclass(frozen=True)
26
+ class ProviderPreset:
27
+ provider_id: str
28
+ label: str
29
+ endpoint: str
30
+ default_model: str
31
+ models: tuple[str, ...]
32
+ auth_scheme: str # 'none' | 'bearer' | 'anthropic' | 'cashu' | 'cashu_402'
33
+ wire_format: str # 'openai' | 'anthropic_messages'
34
+ env_key: str
35
+ rate_msat_per_token: int
36
+ extra_headers: Mapping[str, str] = dataclasses.field(default_factory=dict)
37
+
38
+
39
+ def _https(host: str, path: str = '') -> str:
40
+ return f'https://{host}{path}'
41
+
42
+
43
+ PROVIDER_PRESETS: dict[str, ProviderPreset] = {
44
+ 'openrouter': ProviderPreset(
45
+ provider_id='openrouter',
46
+ label='OpenRouter (Multi-Model Aggregator)',
47
+ endpoint=_https('openrouter.ai', '/api/v1'),
48
+ default_model='anthropic/claude-3.5-sonnet',
49
+ models=(
50
+ 'anthropic/claude-3.5-sonnet',
51
+ 'openai/gpt-4o-mini',
52
+ 'google/gemini-2.5-flash',
53
+ 'meta-llama/llama-3.3-70b-instruct',
54
+ 'deepseek/deepseek-r1',
55
+ ),
56
+ auth_scheme='bearer',
57
+ wire_format='openai',
58
+ env_key='OPENROUTER_API_KEY',
59
+ rate_msat_per_token=15,
60
+ extra_headers={
61
+ 'HTTP-Referer': _https('fleetmesh.org'),
62
+ 'X-Title': 'FleetMesh NonDet Compute',
63
+ },
64
+ ),
65
+ 'claude': ProviderPreset(
66
+ provider_id='claude',
67
+ label='Anthropic Claude',
68
+ endpoint=_https('api.anthropic.com', '/v1'),
69
+ default_model='claude-3-7-sonnet-latest',
70
+ models=(
71
+ 'claude-3-7-sonnet-latest',
72
+ 'claude-3-5-sonnet-latest',
73
+ 'claude-3-5-haiku-latest',
74
+ ),
75
+ auth_scheme='anthropic',
76
+ wire_format='anthropic_messages',
77
+ env_key='ANTHROPIC_API_KEY',
78
+ rate_msat_per_token=18,
79
+ ),
80
+ 'anthropic': ProviderPreset(
81
+ provider_id='anthropic',
82
+ label='Anthropic Claude',
83
+ endpoint=_https('api.anthropic.com', '/v1'),
84
+ default_model='claude-3-7-sonnet-latest',
85
+ models=(
86
+ 'claude-3-7-sonnet-latest',
87
+ 'claude-3-5-sonnet-latest',
88
+ 'claude-3-5-haiku-latest',
89
+ ),
90
+ auth_scheme='anthropic',
91
+ wire_format='anthropic_messages',
92
+ env_key='ANTHROPIC_API_KEY',
93
+ rate_msat_per_token=18,
94
+ ),
95
+ 'openai': ProviderPreset(
96
+ provider_id='openai',
97
+ label='OpenAI',
98
+ endpoint=_https('api.openai.com', '/v1'),
99
+ default_model='gpt-4o-mini',
100
+ models=('gpt-4o-mini', 'gpt-4o', 'o3-mini'),
101
+ auth_scheme='bearer',
102
+ wire_format='openai',
103
+ env_key='OPENAI_API_KEY',
104
+ rate_msat_per_token=12,
105
+ ),
106
+ 'gemini': ProviderPreset(
107
+ provider_id='gemini',
108
+ label='Google Gemini',
109
+ endpoint=_https('generativelanguage.googleapis.com', '/v1beta/openai'),
110
+ default_model='gemini-2.5-flash',
111
+ models=('gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-2.0-flash'),
112
+ auth_scheme='bearer',
113
+ wire_format='openai',
114
+ env_key='GEMINI_API_KEY',
115
+ rate_msat_per_token=10,
116
+ ),
117
+ 'copilot': ProviderPreset(
118
+ provider_id='copilot',
119
+ label='GitHub Copilot / GitHub Models',
120
+ endpoint=_https('models.inference.ai.azure.com'),
121
+ default_model='gpt-4o',
122
+ models=('gpt-4o', 'gpt-4o-mini', 'Llama-3.3-70B-Instruct'),
123
+ auth_scheme='bearer',
124
+ wire_format='openai',
125
+ env_key='GITHUB_TOKEN',
126
+ rate_msat_per_token=12,
127
+ ),
128
+ 'routstr': ProviderPreset(
129
+ provider_id='routstr',
130
+ label='Routstr (Decentralized Cashu LLM Router)',
131
+ endpoint=_https('api.routstr.com', '/v1'),
132
+ default_model='meta-llama/Llama-3.3-70B-Instruct',
133
+ models=(
134
+ 'meta-llama/Llama-3.3-70B-Instruct',
135
+ 'deepseek-ai/DeepSeek-R1',
136
+ 'anthropic/claude-3.5-sonnet',
137
+ ),
138
+ auth_scheme='cashu',
139
+ wire_format='openai',
140
+ env_key='ROUTSTR_CASHU_TOKEN',
141
+ rate_msat_per_token=12,
142
+ ),
143
+ 'payperquery': ProviderPreset(
144
+ provider_id='payperquery',
145
+ label='PayPerQuery / PPQ (HTTP 402 eCash Inference)',
146
+ endpoint=_https('api.ppq.ai', '/v1'),
147
+ default_model='meta-llama/Llama-3.3-70B-Instruct',
148
+ models=(
149
+ 'meta-llama/Llama-3.3-70B-Instruct',
150
+ 'gpt-4o-mini',
151
+ 'claude-3-5-sonnet',
152
+ ),
153
+ auth_scheme='cashu_402',
154
+ wire_format='openai',
155
+ env_key='PPQ_CASHU_TOKEN',
156
+ rate_msat_per_token=12,
157
+ ),
158
+ 'ppq': ProviderPreset(
159
+ provider_id='ppq',
160
+ label='PayPerQuery / PPQ (HTTP 402 eCash Inference)',
161
+ endpoint=_https('api.ppq.ai', '/v1'),
162
+ default_model='meta-llama/Llama-3.3-70B-Instruct',
163
+ models=(
164
+ 'meta-llama/Llama-3.3-70B-Instruct',
165
+ 'gpt-4o-mini',
166
+ 'claude-3-5-sonnet',
167
+ ),
168
+ auth_scheme='cashu_402',
169
+ wire_format='openai',
170
+ env_key='PPQ_CASHU_TOKEN',
171
+ rate_msat_per_token=12,
172
+ ),
173
+ 'ollama': ProviderPreset(
174
+ provider_id='ollama',
175
+ label='Ollama (Local Node)',
176
+ endpoint='http://127.0.0.1:11434/v1',
177
+ default_model='llama3.2',
178
+ models=('llama3.2', 'qwen2.5:14b', 'deepseek-r1:14b', 'mistral'),
179
+ auth_scheme='none',
180
+ wire_format='openai',
181
+ env_key='OLLAMA_API_KEY',
182
+ rate_msat_per_token=5,
183
+ ),
184
+ 'vllm': ProviderPreset(
185
+ provider_id='vllm',
186
+ label='vLLM (Local / Cluster GPU)',
187
+ endpoint='http://127.0.0.1:8000/v1',
188
+ default_model='meta-llama/Llama-3.3-70B-Instruct',
189
+ models=('meta-llama/Llama-3.3-70B-Instruct', 'deepseek-ai/DeepSeek-R1'),
190
+ auth_scheme='none',
191
+ wire_format='openai',
192
+ env_key='VLLM_API_KEY',
193
+ rate_msat_per_token=12,
194
+ ),
195
+ }
196
+
197
+
198
+ @dataclasses.dataclass
199
+ class LinkedProviderConfig:
200
+ """Operator-linked configuration for a single inference provider."""
201
+
202
+ provider_id: str
203
+ endpoint: str
204
+ default_model: str
205
+ models: tuple[str, ...] = ()
206
+ api_key: str = ''
207
+ cashu_token: str = ''
208
+ cashu_mint: str = ''
209
+ auth_scheme: str = 'bearer'
210
+ wire_format: str = 'openai'
211
+ rate_msat_per_token: int = 12
212
+ enabled: bool = True
213
+ label: str = ''
214
+ extra_headers: dict[str, str] = dataclasses.field(default_factory=dict)
215
+
216
+ @classmethod
217
+ def from_preset(
218
+ cls,
219
+ provider_id: str,
220
+ *,
221
+ endpoint: Optional[str] = None,
222
+ default_model: Optional[str] = None,
223
+ models: Optional[Sequence[str]] = None,
224
+ api_key: Optional[str] = None,
225
+ cashu_token: Optional[str] = None,
226
+ cashu_mint: Optional[str] = None,
227
+ rate_msat_per_token: Optional[int] = None,
228
+ enabled: bool = True,
229
+ extra_headers: Optional[Mapping[str, str]] = None,
230
+ ) -> 'LinkedProviderConfig':
231
+ pid = provider_id.strip().lower()
232
+ preset = PROVIDER_PRESETS.get(pid)
233
+ if preset is None:
234
+ resolved_ep = (endpoint or 'http://127.0.0.1:8000/v1').rstrip('/')
235
+ resolved_model = default_model or 'meta-llama/Llama-3.3-70B-Instruct'
236
+ resolved_models = tuple(models) if models else (resolved_model,)
237
+ return cls(
238
+ provider_id=pid,
239
+ label=pid.upper(),
240
+ endpoint=resolved_ep,
241
+ default_model=resolved_model,
242
+ models=resolved_models,
243
+ api_key=api_key or '',
244
+ cashu_token=cashu_token or '',
245
+ cashu_mint=cashu_mint or '',
246
+ auth_scheme='bearer' if api_key else ('cashu' if cashu_token else 'none'),
247
+ wire_format='openai',
248
+ rate_msat_per_token=int(rate_msat_per_token if rate_msat_per_token is not None else 12),
249
+ enabled=enabled,
250
+ extra_headers=dict(extra_headers or {}),
251
+ )
252
+
253
+ env_secret = os.environ.get(preset.env_key, '') if preset.env_key else ''
254
+ resolved_key = api_key if api_key is not None else (env_secret if preset.auth_scheme in ('bearer', 'anthropic') else '')
255
+ resolved_cashu = cashu_token if cashu_token is not None else (env_secret if preset.auth_scheme in ('cashu', 'cashu_402') else '')
256
+ resolved_ep = (endpoint or preset.endpoint).rstrip('/')
257
+ resolved_model = default_model or preset.default_model
258
+ resolved_models = tuple(models) if models else preset.models
259
+ if resolved_model not in resolved_models:
260
+ resolved_models = (resolved_model, *resolved_models)
261
+ merged_headers = dict(preset.extra_headers)
262
+ if extra_headers:
263
+ merged_headers.update(extra_headers)
264
+ return cls(
265
+ provider_id=preset.provider_id,
266
+ label=preset.label,
267
+ endpoint=resolved_ep,
268
+ default_model=resolved_model,
269
+ models=resolved_models,
270
+ api_key=resolved_key,
271
+ cashu_token=resolved_cashu,
272
+ cashu_mint=cashu_mint or '',
273
+ auth_scheme=preset.auth_scheme,
274
+ wire_format=preset.wire_format,
275
+ rate_msat_per_token=int(rate_msat_per_token if rate_msat_per_token is not None else preset.rate_msat_per_token),
276
+ enabled=enabled,
277
+ extra_headers=merged_headers,
278
+ )
279
+
280
+ def has_credentials(self) -> bool:
281
+ if self.auth_scheme == 'none':
282
+ return True
283
+ if self.auth_scheme in ('cashu', 'cashu_402'):
284
+ return bool(self.cashu_token or self.api_key or self.cashu_mint)
285
+ return bool(self.api_key)
286
+
287
+ def is_runnable(self) -> bool:
288
+ return bool(self.enabled and self.endpoint.startswith(('http://', 'https://')) and self.has_credentials())
289
+
290
+ def build_headers(self, per_query_cashu_token: Optional[str] = None) -> dict[str, str]:
291
+ headers: dict[str, str] = {
292
+ 'Content-Type': 'application/json',
293
+ 'User-Agent': 'fleetmesh-llm/1.1',
294
+ }
295
+ headers.update(self.extra_headers)
296
+ active_cashu = per_query_cashu_token or self.cashu_token
297
+ if self.auth_scheme == 'anthropic':
298
+ if self.api_key:
299
+ headers['x-api-key'] = self.api_key
300
+ headers.setdefault('anthropic-version', '2023-06-01')
301
+ elif self.auth_scheme in ('cashu', 'cashu_402'):
302
+ if active_cashu:
303
+ headers['X-Cashu'] = active_cashu
304
+ headers['Authorization'] = f'Bearer {active_cashu}'
305
+ elif self.api_key:
306
+ headers['Authorization'] = f'Bearer {self.api_key}'
307
+ elif self.auth_scheme == 'bearer' or self.api_key:
308
+ if self.api_key:
309
+ headers['Authorization'] = f'Bearer {self.api_key}'
310
+ return headers
311
+
312
+ def format_request(
313
+ self,
314
+ *,
315
+ messages: Sequence[Mapping[str, Any]],
316
+ model: Optional[str] = None,
317
+ max_tokens: int = 512,
318
+ temperature: float = 0.7,
319
+ top_p: float = 1.0,
320
+ seed: Optional[int] = None,
321
+ grammar: Optional[Mapping[str, Any]] = None,
322
+ stream: bool = False,
323
+ ) -> tuple[str, dict[str, Any]]:
324
+ target_model = model or self.default_model
325
+ if self.wire_format == 'anthropic_messages':
326
+ url = f"{self.endpoint}/messages" if not self.endpoint.endswith('/messages') else self.endpoint
327
+ system_parts: list[str] = []
328
+ chat_msgs: list[dict[str, str]] = []
329
+ for m in messages:
330
+ role = str(m.get('role', 'user'))
331
+ content = str(m.get('content', ''))
332
+ if role == 'system':
333
+ if content:
334
+ system_parts.append(content)
335
+ else:
336
+ chat_msgs.append({
337
+ 'role': 'assistant' if role == 'assistant' else 'user',
338
+ 'content': content or ' ',
339
+ })
340
+ if not chat_msgs:
341
+ chat_msgs = [{'role': 'user', 'content': 'Hello'}]
342
+ body: dict[str, Any] = {
343
+ 'model': target_model,
344
+ 'messages': chat_msgs,
345
+ 'max_tokens': max_tokens,
346
+ 'temperature': temperature,
347
+ 'stream': stream,
348
+ }
349
+ if system_parts:
350
+ body['system'] = '\n\n'.join(system_parts)
351
+ return url, body
352
+
353
+ url = f"{self.endpoint}/chat/completions" if not self.endpoint.endswith('/chat/completions') else self.endpoint
354
+ req_body: dict[str, Any] = {
355
+ 'model': target_model,
356
+ 'messages': [dict(m) for m in messages],
357
+ 'max_tokens': max_tokens,
358
+ 'temperature': temperature,
359
+ 'top_p': top_p,
360
+ 'stream': stream,
361
+ }
362
+ if seed is not None:
363
+ req_body['seed'] = int(seed)
364
+ if grammar:
365
+ g_type = grammar.get('type')
366
+ if g_type == 'json_schema' or 'json_schema' in grammar or grammar.get('response_format') == 'json_object':
367
+ req_body['response_format'] = {
368
+ 'type': 'json_object',
369
+ 'schema': grammar.get('definition') or grammar.get('json_schema') or grammar.get('schema_cid'),
370
+ }
371
+ elif g_type == 'regex' or 'guided_regex' in grammar:
372
+ req_body['guided_regex'] = grammar.get('definition') or grammar.get('guided_regex')
373
+ return url, req_body
374
+
375
+ def parse_response(
376
+ self,
377
+ resp_data: Mapping[str, Any],
378
+ default_prompt_tokens: int = 1,
379
+ ) -> tuple[str, int, int]:
380
+ if self.wire_format == 'anthropic_messages' or ('content' in resp_data and isinstance(resp_data.get('content'), list) and 'choices' not in resp_data):
381
+ blocks = resp_data.get('content') or []
382
+ text_parts: list[str] = []
383
+ for blk in blocks:
384
+ if isinstance(blk, Mapping) and blk.get('type') in ('text', None):
385
+ text_parts.append(str(blk.get('text') or ''))
386
+ raw_text = ''.join(text_parts)
387
+ usage = resp_data.get('usage') or {}
388
+ prompt_toks = int(usage.get('input_tokens', usage.get('prompt_tokens', default_prompt_tokens)))
389
+ completion_toks = int(usage.get('output_tokens', usage.get('completion_tokens', max(1, len(raw_text) // 4))))
390
+ return raw_text, prompt_toks, completion_toks
391
+
392
+ choices = resp_data.get('choices') or []
393
+ if not choices or not isinstance(choices[0], Mapping) or 'message' not in choices[0]:
394
+ raise ValueError("LLM backend returned no completion choices")
395
+ raw_text = str(choices[0]['message'].get('content') or '')
396
+ usage = resp_data.get('usage') or {}
397
+ prompt_toks = int(usage.get('prompt_tokens', default_prompt_tokens))
398
+ completion_toks = int(usage.get('completion_tokens', max(1, len(raw_text) // 4)))
399
+ return raw_text, prompt_toks, completion_toks
400
+
401
+ def to_public_dict(self) -> dict[str, Any]:
402
+ """Serialize provider metadata without exposing secret API keys or Cashu tokens."""
403
+ return {
404
+ 'provider_id': self.provider_id,
405
+ 'label': self.label or self.provider_id,
406
+ 'endpoint': self.endpoint,
407
+ 'default_model': self.default_model,
408
+ 'models': list(self.models),
409
+ 'auth_scheme': self.auth_scheme,
410
+ 'wire_format': self.wire_format,
411
+ 'rate_msat_per_token': self.rate_msat_per_token,
412
+ 'enabled': self.enabled,
413
+ 'has_credentials': self.has_credentials(),
414
+ 'is_runnable': self.is_runnable(),
415
+ 'cashu_mint': self.cashu_mint,
416
+ }
417
+
418
+
419
+ class OperatorProviderRouter:
420
+ """Registry of operator-linked LLM providers on a FleetMesh node."""
421
+
422
+ def __init__(
423
+ self,
424
+ providers: Optional[Sequence[LinkedProviderConfig]] = None,
425
+ default_provider: Optional[str] = None,
426
+ ) -> None:
427
+ self._providers: dict[str, LinkedProviderConfig] = {}
428
+ self.default_provider: Optional[str] = default_provider
429
+ if providers:
430
+ for p in providers:
431
+ self.register_provider(p)
432
+
433
+ def register_provider(self, config: LinkedProviderConfig, set_default: bool = False) -> None:
434
+ pid = config.provider_id.strip().lower()
435
+ self._providers[pid] = config
436
+ if set_default or self.default_provider is None:
437
+ self.default_provider = pid
438
+
439
+ def remove_provider(self, provider_id: str) -> bool:
440
+ pid = provider_id.strip().lower()
441
+ existed = pid in self._providers
442
+ self._providers.pop(pid, None)
443
+ if self.default_provider == pid:
444
+ self.default_provider = next(iter(self._providers.keys()), None)
445
+ return existed
446
+
447
+ def get_provider(self, provider_id: Optional[str] = None) -> Optional[LinkedProviderConfig]:
448
+ if provider_id:
449
+ pid = provider_id.strip().lower()
450
+ if pid in self._providers:
451
+ return self._providers[pid]
452
+ if pid == 'anthropic' and 'claude' in self._providers:
453
+ return self._providers['claude']
454
+ if pid == 'claude' and 'anthropic' in self._providers:
455
+ return self._providers['anthropic']
456
+ if pid == 'ppq' and 'payperquery' in self._providers:
457
+ return self._providers['payperquery']
458
+ if pid == 'payperquery' and 'ppq' in self._providers:
459
+ return self._providers['ppq']
460
+ if self.default_provider and self.default_provider in self._providers:
461
+ return self._providers[self.default_provider]
462
+ for p in self._providers.values():
463
+ if p.is_runnable():
464
+ return p
465
+ return next(iter(self._providers.values()), None)
466
+
467
+ def resolve_for_job(self, job_payload: Mapping[str, Any]) -> Optional[LinkedProviderConfig]:
468
+ req_provider = job_payload.get('provider') or (job_payload.get('input') or {}).get('provider') if isinstance(job_payload.get('input'), Mapping) else job_payload.get('provider')
469
+ if req_provider:
470
+ found = self.get_provider(str(req_provider))
471
+ if found and found.enabled:
472
+ return found
473
+ req_model = str(job_payload.get('model') or '')
474
+ if req_model:
475
+ for p in self._providers.values():
476
+ if p.enabled and req_model in p.models:
477
+ return p
478
+ return self.get_provider()
479
+
480
+ def runnable_providers(self) -> list[LinkedProviderConfig]:
481
+ return [p for p in self._providers.values() if p.is_runnable()]
482
+
483
+ def list_public_providers(self) -> list[dict[str, Any]]:
484
+ return [p.to_public_dict() for p in self._providers.values()]
485
+
486
+ @classmethod
487
+ def from_env(cls) -> 'OperatorProviderRouter':
488
+ """Auto-discover any operator-configured providers from environment variables."""
489
+ router = cls()
490
+ seen_Canonical: set[str] = set()
491
+ for pid, preset in PROVIDER_PRESETS.items():
492
+ if pid in ('anthropic', 'ppq'):
493
+ continue
494
+ val = os.environ.get(preset.env_key, '').strip()
495
+ if val and pid not in seen_Canonical:
496
+ seen_Canonical.add(pid)
497
+ cfg = LinkedProviderConfig.from_preset(pid)
498
+ router.register_provider(cfg)
499
+ return router
@@ -0,0 +1,140 @@
1
+ """ExecutorRegistry: which runtime answers which job kind, and the default set."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Mapping, Optional
5
+
6
+ from .base import ExecutorProtocol
7
+ from .media import FfmpegTranscodeExecutor
8
+ from .nondet import AgentExecutor, BacalhauExecutor, LlmExecutor, LlmAgentBackend
9
+ from .providers import PROVIDER_PRESETS, LinkedProviderConfig, OperatorProviderRouter
10
+ from .wasm import WasmExecutor
11
+ from .zk import ZkGroth16Executor, ZkProverExecutor, ZkVmExecutor
12
+
13
+ class ExecutorRegistry:
14
+ """Registry managing available compute runtime execution drivers."""
15
+
16
+ def __init__(self, blob_store: Optional[Any] = None, mock_mode: bool = False,
17
+ media_cache_dir: Optional[str] = None,
18
+ provider_router: Optional[OperatorProviderRouter] = None) -> None:
19
+ # blob_store: where WASM modules/inputs named by hash are looked up
20
+ # when the job itself does not carry one (e.g. a job received over
21
+ # the compute wire, which cannot carry a live store).
22
+ # mock_mode: build the LLM/Bacalhau/agent drivers as deterministic
23
+ # fixtures. Off by default so a node never ships fixture output as a
24
+ # paid result; tests and demos opt in explicitly.
25
+ self.blob_store: Optional[Any] = blob_store
26
+ self.mock_mode: bool = mock_mode
27
+ self.provider_router: OperatorProviderRouter = provider_router or OperatorProviderRouter()
28
+ wasm = WasmExecutor()
29
+ llm = LlmExecutor(mock_mode=mock_mode, provider_router=self.provider_router)
30
+ bacalhau = BacalhauExecutor(mock_mode=mock_mode)
31
+ agent_backend = LlmAgentBackend(provider_router=self.provider_router) if self.provider_router.runnable_providers() else None
32
+ agent = AgentExecutor(mock_mode=mock_mode, backend=agent_backend)
33
+ zkvm = ZkVmExecutor(mock_mode=mock_mode)
34
+ zk_prover = ZkProverExecutor(mock_mode=mock_mode)
35
+ ffmpeg = FfmpegTranscodeExecutor(mock_mode=mock_mode, cache_dir=media_cache_dir)
36
+ self.executors: dict[str, ExecutorProtocol] = {
37
+ 'wasmtime': wasm,
38
+ 'wasm': wasm,
39
+ 'wasmer': wasm,
40
+ 'zk-verify-groth16': ZkGroth16Executor(),
41
+ 'zkvm': zkvm,
42
+ 'zkvm-risc0': zkvm,
43
+ 'zkvm-sp1': zkvm,
44
+ 'zk-prove-groth16': zk_prover,
45
+ 'zk-prove-risc0': zk_prover,
46
+ 'llm': llm,
47
+ 'vllm': llm,
48
+ 'ollama': llm,
49
+ 'bacalhau': bacalhau,
50
+ 'container': bacalhau,
51
+ 'docker': bacalhau,
52
+ 'agent': agent,
53
+ 'react': agent,
54
+ 'fleetmesh-react-v1': agent,
55
+ 'ffmpeg': ffmpeg,
56
+ 'media-transcode': ffmpeg,
57
+ 'video-transcode': ffmpeg,
58
+ 'transcode': ffmpeg,
59
+ }
60
+
61
+ def configure_llm_provider(self, config: LinkedProviderConfig, set_default: bool = True) -> None:
62
+ """Link or update an external/local LLM provider for both LLM and Agent executors."""
63
+ self.provider_router.register_provider(config, set_default=set_default)
64
+ llm_ex = self.executors.get('llm')
65
+ if isinstance(llm_ex, LlmExecutor):
66
+ llm_ex.provider_router = self.provider_router
67
+ agent_ex = self.executors.get('agent')
68
+ if isinstance(agent_ex, AgentExecutor) and not self.mock_mode:
69
+ if self.provider_router.runnable_providers():
70
+ agent_ex.backend = LlmAgentBackend(
71
+ endpoint=config.endpoint,
72
+ model=config.default_model,
73
+ api_key=config.api_key,
74
+ cashu_token=config.cashu_token,
75
+ provider_id=config.provider_id,
76
+ provider_router=self.provider_router,
77
+ )
78
+
79
+ def register(self, executor: ExecutorProtocol, *aliases: str) -> None:
80
+ for name in (executor.engine_id, *aliases):
81
+ self.executors[name] = executor
82
+
83
+ def get(self, engine_id: str) -> Optional[ExecutorProtocol]:
84
+ return self.executors.get(engine_id)
85
+
86
+ def runnable_engines(self) -> list[str]:
87
+ """Engine ids that can actually complete a job on this node.
88
+
89
+ Queries is_live_runnable() polymorphically across all registered
90
+ executors. Executors reporting False (e.g. unconfigured backends outside
91
+ mock_mode, or WasmExecutor when wasmtime is not installed) are excluded
92
+ so Kind 11803 compute offers only advertise truly runnable capabilities.
93
+ """
94
+ out: list[str] = []
95
+ for eid, ex in self.executors.items():
96
+ if hasattr(ex, 'is_live_runnable'):
97
+ if not ex.is_live_runnable():
98
+ continue
99
+ out.append(eid)
100
+ return out
101
+
102
+ def select_for_job(self, job_payload: Mapping[str, Any]) -> ExecutorProtocol:
103
+ job_exec = job_payload.get('executor')
104
+ if job_exec and job_exec in self.executors:
105
+ return self.executors[job_exec]
106
+ if job_exec and str(job_exec).lower() in PROVIDER_PRESETS:
107
+ return self.executors['llm']
108
+
109
+ mod = job_payload.get('module') or {}
110
+ mod_type = mod.get('type') or mod.get('runtime')
111
+ if mod_type and mod_type in self.executors:
112
+ return self.executors[mod_type]
113
+
114
+ if job_payload.get('engine') in ('ffmpeg', 'media-transcode', 'video-transcode', 'transcode'):
115
+ return self.executors['ffmpeg']
116
+
117
+ if 'messages' in job_payload or 'model' in job_payload or 'prompt' in job_payload:
118
+ return self.executors['llm']
119
+
120
+ if 'storage_mounts' in job_payload or job_payload.get('engine') in ('docker', 'bacalhau'):
121
+ return self.executors['bacalhau']
122
+
123
+ if 'max_steps' in job_payload or 'tools' in job_payload or 'agent' in job_payload or 'task' in job_payload:
124
+ return self.executors['agent']
125
+
126
+ inp = job_payload.get('input', {})
127
+ if isinstance(inp, dict):
128
+ if 'source_url' in inp or 'source_path' in inp or ('profile' in inp and ('vod_id' in inp or 'duration_sec' in inp)):
129
+ return self.executors['ffmpeg']
130
+ if inp.get('action') == 'prove' or 'witness' in inp or 'private_inputs' in inp:
131
+ return self.executors.get('zk-prove-groth16', self.executors['zk-verify-groth16'])
132
+ if 'image_id' in inp or 'journal' in inp or str(job_payload.get('proof_system', '')).startswith(('risc0', 'sp1', 'zkvm')):
133
+ return self.executors.get('zkvm-risc0', self.executors['zk-verify-groth16'])
134
+ if 'proof' in inp or 'vk_sha256' in inp:
135
+ return self.executors['zk-verify-groth16']
136
+
137
+ return self.executors['wasmtime']
138
+
139
+
140
+ DEFAULT_REGISTRY: ExecutorRegistry = ExecutorRegistry()