frutebox-node 0.1.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.
- frutebox/adminheader.py +59 -0
- frutebox/attest.py +43 -0
- frutebox/audiogen.py +364 -0
- frutebox/config.py +367 -0
- frutebox/deltasync.py +211 -0
- frutebox/economics.py +270 -0
- frutebox/engine_guard.py +718 -0
- frutebox/envelope.py +77 -0
- frutebox/envvars.py +167 -0
- frutebox/gpuprices.py +641 -0
- frutebox/gpuproviders.py +4489 -0
- frutebox/gui.py +270 -0
- frutebox/imagegen.py +152 -0
- frutebox/inference.py +624 -0
- frutebox/manifest.py +218 -0
- frutebox/mediaserving.py +603 -0
- frutebox/packaging.py +542 -0
- frutebox/pqctransport.py +660 -0
- frutebox/provider.py +1867 -0
- frutebox/providers.py +942 -0
- frutebox/registry/__init__.py +189 -0
- frutebox/registry/generate.py +311 -0
- frutebox/registry/generate_providers.py +120 -0
- frutebox/registry/models.yaml +1759 -0
- frutebox/registry/providers.yaml +518 -0
- frutebox/registry/registry.gen.json +2043 -0
- frutebox/registry_gen.py +3 -0
- frutebox/runpod_templates.py +143 -0
- frutebox/serving.py +691 -0
- frutebox/signedconfig.py +259 -0
- frutebox/signing.py +208 -0
- frutebox/thinkbudget.py +155 -0
- frutebox/videogen.py +118 -0
- frutebox_node-0.1.0.dist-info/METADATA +100 -0
- frutebox_node-0.1.0.dist-info/RECORD +36 -0
- frutebox_node-0.1.0.dist-info/WHEEL +4 -0
frutebox/economics.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""The reward engine — "mining, but for LLM inference".
|
|
2
|
+
|
|
3
|
+
Every settled request splits into four parts:
|
|
4
|
+
|
|
5
|
+
base_cost reimburses the provider's energy + basic maintenance
|
|
6
|
+
provider_margin the provider's small guaranteed profit
|
|
7
|
+
platform_fee the platform operator's (our) small fee
|
|
8
|
+
bonus a DECAYING bootstrap emission, like a Bitcoin block reward
|
|
9
|
+
|
|
10
|
+
The user only ever pays ``base_cost + provider_margin + platform_fee`` — which
|
|
11
|
+
is cheap and roughly flat. The ``bonus`` is paid to the provider *on top*, out
|
|
12
|
+
of a treasury/emission pool, and it halves every ``halving_epochs`` epochs. So
|
|
13
|
+
early contributors earn a lot, and over time earnings settle toward pure
|
|
14
|
+
cost-plus. That is what lets a company fund more hardware early, then coast on
|
|
15
|
+
sustainable margins later.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import time
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from datetime import datetime, timezone
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Dict, List, Optional
|
|
26
|
+
|
|
27
|
+
from .config import Config
|
|
28
|
+
from .envvars import env as envvar
|
|
29
|
+
|
|
30
|
+
#: Seeded per-region rates. Flat hourly values until real tariff data is loaded — see the file
|
|
31
|
+
#: itself for why a plausible-looking invented curve would be worse than an honest flat number.
|
|
32
|
+
_RATES_FILE = Path(envvar("RATES_FILE", str(Path(__file__).resolve().parent.parent / "data" / "electricity_rates.json")))
|
|
33
|
+
|
|
34
|
+
_RATES_CACHE: Dict[str, dict] = {}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _seed_rates() -> Dict[str, dict]:
|
|
38
|
+
global _RATES_CACHE
|
|
39
|
+
if _RATES_CACHE:
|
|
40
|
+
return _RATES_CACHE
|
|
41
|
+
try:
|
|
42
|
+
_RATES_CACHE = (json.loads(_RATES_FILE.read_text()) or {}).get("regions") or {}
|
|
43
|
+
except Exception:
|
|
44
|
+
# Never let a missing data file stop settlement — the defaults below are the same
|
|
45
|
+
# numbers the seed file carries.
|
|
46
|
+
_RATES_CACHE = {}
|
|
47
|
+
return _RATES_CACHE
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
#: Used only when the seed file is unreadable AND the region is unknown.
|
|
51
|
+
_DEFAULT_RATE = {"per_gb_eur": 0.01, "per_kwh_by_hour": [0.20] * 24}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def rates_for(region: str) -> dict:
|
|
55
|
+
"""Bandwidth + hourly electricity rates for a region.
|
|
56
|
+
|
|
57
|
+
Prefers live/admin-edited rates cached from the web app (`gpuprices.region_rates()`), then
|
|
58
|
+
the seeded file, then a flat default. Falls back region -> "default" so an unrecognised
|
|
59
|
+
region still prices rather than raising mid-settlement.
|
|
60
|
+
"""
|
|
61
|
+
key = (region or "default").strip().lower()
|
|
62
|
+
try: # local import: gpuprices imports config, not us
|
|
63
|
+
from . import gpuprices
|
|
64
|
+
live = gpuprices.region_rates() or {}
|
|
65
|
+
except Exception:
|
|
66
|
+
live = {}
|
|
67
|
+
for src in (live, _seed_rates()):
|
|
68
|
+
row = src.get(key) or src.get("default")
|
|
69
|
+
if isinstance(row, dict) and row.get("per_kwh_by_hour"):
|
|
70
|
+
return row
|
|
71
|
+
return _DEFAULT_RATE
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class JobMeter:
|
|
76
|
+
"""What one served request actually consumed (file 01-D).
|
|
77
|
+
|
|
78
|
+
Settlement used to price on `(tokens, cost_per_1k)`, which is not what serving costs
|
|
79
|
+
anyone: an operator pays for **bandwidth** and **electricity**. Tokens break in both
|
|
80
|
+
directions — a long cheap generation overpays the node, a short prompt against a huge
|
|
81
|
+
context underpays it — and no token price can express that electricity costs more at 19:00
|
|
82
|
+
than at 03:00.
|
|
83
|
+
|
|
84
|
+
`executor_class` decides who, if anyone, gets paid:
|
|
85
|
+
* ``account_node`` a community operator's machine -> internet + electricity + reward
|
|
86
|
+
* ``backup_infra`` rented capacity we pay a vendor -> **nothing** here; the vendor bill
|
|
87
|
+
is recorded as vendor spend, and the user price is the rented cost
|
|
88
|
+
plus the thin rented margin
|
|
89
|
+
* ``owned`` our own hardware -> no payout, cost side only
|
|
90
|
+
* ``local`` the user's own device -> settlement is never called at all
|
|
91
|
+
"""
|
|
92
|
+
bytes_in: int
|
|
93
|
+
bytes_out: int
|
|
94
|
+
t_dispatch: float
|
|
95
|
+
t_response: float
|
|
96
|
+
region: str = "default"
|
|
97
|
+
executor_class: str = "account_node"
|
|
98
|
+
#: 0 => not benchmarked; the tier wattage declaration is used instead.
|
|
99
|
+
node_power_w: float = 0.0
|
|
100
|
+
tier: str = "B"
|
|
101
|
+
#: §8A headroom input: what this job would have cost on the cheapest alternative, in EUR.
|
|
102
|
+
cheapest_alternative_eur: float = 0.0
|
|
103
|
+
tokens: int = 0
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def seconds(self) -> float:
|
|
107
|
+
# A clock that went backwards must not pay someone for negative time.
|
|
108
|
+
return max(0.0, float(self.t_response) - float(self.t_dispatch))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
EXECUTOR_CLASSES = ("account_node", "backup_infra", "owned", "local")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class Economics:
|
|
115
|
+
def __init__(self, cfg: Config, genesis: Optional[float] = None):
|
|
116
|
+
self.cfg = cfg
|
|
117
|
+
# genesis is injected (not time.time() in a default arg) so tests /
|
|
118
|
+
# resumes are deterministic.
|
|
119
|
+
self.genesis = genesis if genesis is not None else time.time()
|
|
120
|
+
self.totals: Dict[str, float] = {
|
|
121
|
+
"user_revenue": 0.0,
|
|
122
|
+
"provider_payout": 0.0,
|
|
123
|
+
"platform_fee": 0.0,
|
|
124
|
+
"bonus_emitted": 0.0,
|
|
125
|
+
"requests": 0,
|
|
126
|
+
"tokens": 0,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# --- epoch / reward schedule -----------------------------------------
|
|
130
|
+
def current_epoch(self) -> int:
|
|
131
|
+
return int((time.time() - self.genesis) // self.cfg.epoch_seconds)
|
|
132
|
+
|
|
133
|
+
def seconds_into_epoch(self) -> float:
|
|
134
|
+
return (time.time() - self.genesis) % self.cfg.epoch_seconds
|
|
135
|
+
|
|
136
|
+
def reward_multiplier(self, epoch: Optional[int] = None) -> float:
|
|
137
|
+
"""Bonus multiple of base cost at a given epoch (halving schedule)."""
|
|
138
|
+
if epoch is None:
|
|
139
|
+
epoch = self.current_epoch()
|
|
140
|
+
halvings = epoch // self.cfg.halving_epochs
|
|
141
|
+
return self.cfg.initial_bonus_rate * (0.5 ** halvings)
|
|
142
|
+
|
|
143
|
+
# --- settlement -------------------------------------------------------
|
|
144
|
+
def _tier_watts(self, tier: str) -> float:
|
|
145
|
+
return {
|
|
146
|
+
"C": self.cfg.tier_watts_c, "B": self.cfg.tier_watts_b,
|
|
147
|
+
"A": self.cfg.tier_watts_a, "S": self.cfg.tier_watts_s,
|
|
148
|
+
}.get((tier or "B").upper(), self.cfg.tier_watts_b)
|
|
149
|
+
|
|
150
|
+
def settle(self, job, cost_per_1k: Optional[float] = None) -> Dict[str, float]:
|
|
151
|
+
"""Price one served request on bytes + time-of-use electricity + headroom reward.
|
|
152
|
+
|
|
153
|
+
Accepts a `JobMeter` (the real path). The legacy `(tokens, cost_per_1k)` signature
|
|
154
|
+
still works as a thin compat shim so callers not yet migrated keep settling instead of
|
|
155
|
+
crashing — it estimates 4 bytes/token, which is an estimate and is flagged as one on
|
|
156
|
+
the result (`estimated: True`).
|
|
157
|
+
"""
|
|
158
|
+
if not isinstance(job, JobMeter):
|
|
159
|
+
tokens = int(job or 0)
|
|
160
|
+
rate = self.cfg.default_cost_per_1k if cost_per_1k is None else float(cost_per_1k)
|
|
161
|
+
# 4 bytes/token is the rough average for UTF-8 English; good enough to keep an
|
|
162
|
+
# unmigrated caller settling, never good enough to call measured.
|
|
163
|
+
approx = tokens * 4
|
|
164
|
+
job = JobMeter(bytes_in=approx // 2, bytes_out=approx - approx // 2,
|
|
165
|
+
t_dispatch=0.0, t_response=0.0, tokens=tokens,
|
|
166
|
+
cheapest_alternative_eur=(tokens / 1000.0) * rate)
|
|
167
|
+
return self._settle_meter(job, estimated=True)
|
|
168
|
+
return self._settle_meter(job, estimated=False)
|
|
169
|
+
|
|
170
|
+
def _settle_meter(self, job: JobMeter, *, estimated: bool) -> Dict[str, float]:
|
|
171
|
+
if job.executor_class == "local":
|
|
172
|
+
# On-device inference costs the network nothing and pays nobody. Reaching here
|
|
173
|
+
# means a local job was routed through network settlement, which would invent
|
|
174
|
+
# both a cost and a payout out of nothing — fail loudly instead.
|
|
175
|
+
raise ValueError("settle() must never be called for a local (on-device) job")
|
|
176
|
+
if job.executor_class not in EXECUTOR_CLASSES:
|
|
177
|
+
raise ValueError(f"unknown executor_class {job.executor_class!r}")
|
|
178
|
+
|
|
179
|
+
rates = rates_for(job.region)
|
|
180
|
+
per_gb = float(rates.get("per_gb_eur", _DEFAULT_RATE["per_gb_eur"]))
|
|
181
|
+
hourly = rates.get("per_kwh_by_hour") or _DEFAULT_RATE["per_kwh_by_hour"]
|
|
182
|
+
|
|
183
|
+
# Internet: what moved on the wire, both directions.
|
|
184
|
+
internet = (max(0, job.bytes_in) + max(0, job.bytes_out)) * per_gb / 1e9
|
|
185
|
+
|
|
186
|
+
# Electricity: seconds held x watts drawn x the tariff AT THE HOUR THE JOB STARTED.
|
|
187
|
+
hour = 0
|
|
188
|
+
if job.t_dispatch:
|
|
189
|
+
hour = datetime.fromtimestamp(job.t_dispatch, tz=timezone.utc).hour
|
|
190
|
+
per_kwh = float(hourly[hour % len(hourly)])
|
|
191
|
+
watts = job.node_power_w if job.node_power_w > 0 else self._tier_watts(job.tier)
|
|
192
|
+
electricity = (job.seconds / 3600.0) * (watts / 1000.0) * per_kwh
|
|
193
|
+
|
|
194
|
+
cost = internet + electricity
|
|
195
|
+
platform = cost * self.cfg.platform_rate
|
|
196
|
+
|
|
197
|
+
# Reward = §8A headroom: what we saved versus the cheapest alternative, after the
|
|
198
|
+
# operator's real costs and our fee. Never negative, and capped so a mispriced
|
|
199
|
+
# alternative cannot drain the treasury on a single job.
|
|
200
|
+
headroom = float(job.cheapest_alternative_eur) - cost - platform
|
|
201
|
+
reward = max(self.cfg.reward_floor_eur, min(max(0.0, headroom), self.cfg.reward_cap_eur))
|
|
202
|
+
|
|
203
|
+
if job.executor_class == "account_node":
|
|
204
|
+
payout = internet + electricity + reward
|
|
205
|
+
else:
|
|
206
|
+
# backup_infra: the vendor is paid on their own invoice, recorded as vendor spend.
|
|
207
|
+
# owned: our own hardware; the cost is ours and there is no counterparty to pay.
|
|
208
|
+
payout = 0.0
|
|
209
|
+
reward = 0.0
|
|
210
|
+
|
|
211
|
+
user_price = cost + platform + reward
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
"tokens": job.tokens,
|
|
215
|
+
"executor_class": job.executor_class,
|
|
216
|
+
"region": (job.region or "default"),
|
|
217
|
+
"seconds": round(job.seconds, 4),
|
|
218
|
+
"watts": round(watts, 2),
|
|
219
|
+
"power_measured": job.node_power_w > 0,
|
|
220
|
+
"per_kwh_eur": round(per_kwh, 5),
|
|
221
|
+
"per_gb_eur": per_gb,
|
|
222
|
+
"internet_cost": round(internet, 8),
|
|
223
|
+
"electricity_cost": round(electricity, 8),
|
|
224
|
+
"base_cost": round(cost, 8),
|
|
225
|
+
"platform_fee": round(platform, 8),
|
|
226
|
+
"reward": round(reward, 8),
|
|
227
|
+
"user_price": round(user_price, 8),
|
|
228
|
+
"provider_payout": round(payout, 8),
|
|
229
|
+
"estimated": estimated,
|
|
230
|
+
"epoch": self.current_epoch(),
|
|
231
|
+
# Kept so existing dashboards/records that read these keys keep working. The
|
|
232
|
+
# halving bonus is now expressed through the reward headroom, not a multiple of a
|
|
233
|
+
# synthetic base cost.
|
|
234
|
+
"provider_margin": 0.0,
|
|
235
|
+
"bonus": round(reward, 8),
|
|
236
|
+
"multiplier": round(self.reward_multiplier(), 4),
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
def record(self, s: Dict[str, float]) -> None:
|
|
240
|
+
self.totals["user_revenue"] += s["user_price"]
|
|
241
|
+
self.totals["provider_payout"] += s.get("provider_payout", 0.0)
|
|
242
|
+
self.totals["platform_fee"] += s["platform_fee"]
|
|
243
|
+
self.totals["bonus_emitted"] += s.get("bonus", 0.0)
|
|
244
|
+
self.totals["requests"] += 1
|
|
245
|
+
self.totals["tokens"] += s.get("tokens", 0)
|
|
246
|
+
|
|
247
|
+
# --- dashboard snapshot ----------------------------------------------
|
|
248
|
+
def curve(self) -> List[Dict[str, float]]:
|
|
249
|
+
n = self.cfg.halving_epochs * self.cfg.curve_epochs
|
|
250
|
+
return [
|
|
251
|
+
{"epoch": e, "multiplier": round(self.reward_multiplier(e), 4)}
|
|
252
|
+
for e in range(0, n + 1)
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
def snapshot(self) -> Dict:
|
|
256
|
+
return {
|
|
257
|
+
"epoch": self.current_epoch(),
|
|
258
|
+
"seconds_into_epoch": round(self.seconds_into_epoch(), 1),
|
|
259
|
+
"epoch_seconds": self.cfg.epoch_seconds,
|
|
260
|
+
"halving_epochs": self.cfg.halving_epochs,
|
|
261
|
+
"multiplier": round(self.reward_multiplier(), 4),
|
|
262
|
+
"rates": {
|
|
263
|
+
"margin": self.cfg.margin_rate,
|
|
264
|
+
"platform": self.cfg.platform_rate,
|
|
265
|
+
"initial_bonus": self.cfg.initial_bonus_rate,
|
|
266
|
+
},
|
|
267
|
+
"totals": {k: round(v, 4) if isinstance(v, float) else v
|
|
268
|
+
for k, v in self.totals.items()},
|
|
269
|
+
"curve": self.curve(),
|
|
270
|
+
}
|