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/manifest.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Node capability-classing, lean manifest issuance, and big-GPU-claim challenge
|
|
2
|
+
(§2) — in the REAL control plane (this Python service is what actually places
|
|
3
|
+
models on nodes; the web copy is a reference).
|
|
4
|
+
|
|
5
|
+
On join a node is benchmarked into a capability CLASS; the control plane issues a
|
|
6
|
+
LEAN manifest (2-5 models) matched to the hardware — chosen to fit with headroom,
|
|
7
|
+
span complementary task-types (so the box stays busy), and fill the region's demand
|
|
8
|
+
gaps. Nodes are untrusted, so a machine that CLAIMS a big GPU is challenged with a
|
|
9
|
+
real workload before it's trusted with big jobs; an inflated claim is downgraded to
|
|
10
|
+
the measured reality. Pure + deterministic.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Iterable
|
|
15
|
+
|
|
16
|
+
TIER_ORDER = ["C", "B", "A", "S"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _rank(t: str) -> int:
|
|
20
|
+
return TIER_ORDER.index(t)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Committed, commercial-safe roster (Appendix A1). Non-commercial models
|
|
24
|
+
# (MusicGen, FLUX.2 [dev]) are absent by construction.
|
|
25
|
+
MODEL_CATALOG: list[dict] = [
|
|
26
|
+
{"id": "llama-3.2-1b", "tier": "C", "tasks": ["triage", "draft"], "vram_gb": 0.9, "pinned": False},
|
|
27
|
+
{"id": "llama-3.2-3b", "tier": "C", "tasks": ["chat"], "vram_gb": 2.1, "pinned": False},
|
|
28
|
+
{"id": "qwen3-4b", "tier": "C", "tasks": ["chat", "code"], "vram_gb": 4.0, "pinned": False},
|
|
29
|
+
{"id": "phi-4-mini-3.8b", "tier": "C", "tasks": ["code"], "vram_gb": 2.5, "pinned": False},
|
|
30
|
+
{"id": "nomic-embed-text-1.5", "tier": "C", "tasks": ["embed"], "vram_gb": 0.3, "pinned": False},
|
|
31
|
+
{"id": "qwen3-8b", "tier": "B", "tasks": ["chat", "brain"], "vram_gb": 5.0, "pinned": False},
|
|
32
|
+
{"id": "devstral-small-24b", "tier": "B", "tasks": ["code"], "vram_gb": 16.0, "pinned": True},
|
|
33
|
+
{"id": "qwen3-embedding-4b", "tier": "B", "tasks": ["embed"], "vram_gb": 2.5, "pinned": False},
|
|
34
|
+
{"id": "qwen3-reranker-0.6b", "tier": "B", "tasks": ["rerank"], "vram_gb": 1.0, "pinned": False},
|
|
35
|
+
{"id": "whisper-large-v3-turbo", "tier": "B", "tasks": ["stt"], "vram_gb": 6.0, "pinned": False},
|
|
36
|
+
{"id": "sdxl-1.0", "tier": "B", "tasks": ["image"], "vram_gb": 8.0, "pinned": True},
|
|
37
|
+
{"id": "ace-step-1.5", "tier": "B", "tasks": ["music"], "vram_gb": 8.0, "pinned": True},
|
|
38
|
+
{"id": "kokoro-82m", "tier": "B", "tasks": ["tts"], "vram_gb": 1.0, "pinned": False},
|
|
39
|
+
{"id": "stable-audio-open-1.5", "tier": "B", "tasks": ["sfx"], "vram_gb": 12.0, "pinned": True},
|
|
40
|
+
{"id": "qwen-3.6-27b", "tier": "A", "tasks": ["brain", "code"], "vram_gb": 22.0, "pinned": True},
|
|
41
|
+
{"id": "qwen-image-20b", "tier": "A", "tasks": ["image"], "vram_gb": 13.0, "pinned": True},
|
|
42
|
+
{"id": "wan-2.2", "tier": "A", "tasks": ["video"], "vram_gb": 24.0, "pinned": True},
|
|
43
|
+
{"id": "qwen3-embedding-8b", "tier": "A", "tasks": ["embed"], "vram_gb": 4.7, "pinned": False},
|
|
44
|
+
{"id": "qwen3-vl", "tier": "A", "tasks": ["vision"], "vram_gb": 20.0, "pinned": True},
|
|
45
|
+
{"id": "yue-7b", "tier": "A", "tasks": ["song"], "vram_gb": 16.0, "pinned": True},
|
|
46
|
+
# 09-B1: xtts-v2 (Coqui CPML, NON-COMMERCIAL — a named landmine) was here until
|
|
47
|
+
# the registry CI gate caught it. Chatterbox (MIT, US) is the licence-clean TTS.
|
|
48
|
+
{"id": "chatterbox", "tier": "A", "tasks": ["tts"], "vram_gb": 6.0, "pinned": False},
|
|
49
|
+
{"id": "kimi-k2.7-code", "tier": "S", "tasks": ["code", "brain"], "vram_gb": 200.0, "pinned": True},
|
|
50
|
+
{"id": "kimi-k2.6", "tier": "S", "tasks": ["brain", "code"], "vram_gb": 200.0, "pinned": True},
|
|
51
|
+
{"id": "qwen3-235b-a22b", "tier": "S", "tasks": ["brain"], "vram_gb": 140.0, "pinned": True},
|
|
52
|
+
{"id": "ltx-2.3", "tier": "S", "tasks": ["video"], "vram_gb": 48.0, "pinned": True},
|
|
53
|
+
]
|
|
54
|
+
_BY_ID = {m["id"]: m for m in MODEL_CATALOG}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Real Ollama tags for the CPU-runnable (Tier C) catalog ids, so a laptop node can
|
|
58
|
+
# pull a concrete model by id/tier. Only these small models are Ollama-served here;
|
|
59
|
+
# larger/pinned models run via other engines (llama.cpp) and are absent by design.
|
|
60
|
+
OLLAMA_TAGS: dict[str, str] = {
|
|
61
|
+
"llama-3.2-1b": "llama3.2:1b",
|
|
62
|
+
"llama-3.2-3b": "llama3.2:3b",
|
|
63
|
+
"qwen3-4b": "qwen3:4b",
|
|
64
|
+
# The A40 smoke/backup lane's model (2026-08-30). Absent, the gateway's tag→id map could not
|
|
65
|
+
# resolve a node advertising "qwen3:8b" to the catalog's qwen3-8b, and dispatch answered
|
|
66
|
+
# "No machine available … available here: qwen3:8b" — the catalog-id/ollama-tag mismatch the
|
|
67
|
+
# runbook names as a usual culprit, firing again for want of one line.
|
|
68
|
+
"qwen3-8b": "qwen3:8b",
|
|
69
|
+
"phi-4-mini-3.8b": "phi4-mini:3.8b",
|
|
70
|
+
"nomic-embed-text-1.5": "nomic-embed-text",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _tags_from_registry() -> dict[str, str]:
|
|
75
|
+
"""28 §B — fill this map from the registry's PINNED `ollama_tag`s, without disturbing what is
|
|
76
|
+
already here.
|
|
77
|
+
|
|
78
|
+
THE DRIFT THIS CLOSES. The weights' location lived in four hand-maintained tables — the two in
|
|
79
|
+
`serving.py`, `MODEL_CATALOG` above, and this one — and in no registry. This table had 6 of the 12
|
|
80
|
+
enabled Ollama-runtime models, so the other 6 had no tag anywhere and `serving_spec` fell through
|
|
81
|
+
to an EMPTY command for them.
|
|
82
|
+
|
|
83
|
+
ADDITIVE ON PURPOSE, and this is a deliberate narrowing of the fix. The literal entries above WIN
|
|
84
|
+
over the registry where both exist, because this map is also read BACKWARDS — `gateway.py` builds
|
|
85
|
+
`{tag: id}` from it to resolve a node that advertises `qwen3:8b` to the catalog's `qwen3-8b`.
|
|
86
|
+
Changing an existing spelling would therefore change what an already-running node resolves to, and
|
|
87
|
+
a node that stops resolving reads to a user as "no machine available". Adding ids it did not have
|
|
88
|
+
cannot do that.
|
|
89
|
+
|
|
90
|
+
ONE KNOWN DISAGREEMENT, REPORTED AND NOT SILENTLY RESOLVED: this table says
|
|
91
|
+
`nomic-embed-text-1.5 -> "nomic-embed-text"` (unversioned, i.e. whatever `:latest` is) while the
|
|
92
|
+
registry pins `nomic-embed-text:v1.5`. Both pull the same weights today. `serving.py` uses the
|
|
93
|
+
pinned one; this map keeps the unversioned spelling so a node already advertising it still
|
|
94
|
+
resolves. Reconciling them means teaching the reverse lookup to accept both spellings, which is
|
|
95
|
+
gateway/routing work and a decision about node compatibility, not a registry edit.
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
from .registry_gen import REGISTRY
|
|
99
|
+
except Exception: # pragma: no cover - generated module missing
|
|
100
|
+
return {}
|
|
101
|
+
out: dict[str, str] = {}
|
|
102
|
+
for m in REGISTRY:
|
|
103
|
+
tag = (m.get("source") or {}).get("ollama_tag")
|
|
104
|
+
if tag and m["id"] not in OLLAMA_TAGS:
|
|
105
|
+
out[m["id"]] = tag
|
|
106
|
+
return out
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
OLLAMA_TAGS.update(_tags_from_registry())
|
|
110
|
+
|
|
111
|
+
# The default CPU chat model a laptop pulls when nothing else is specified. Matches
|
|
112
|
+
# inference.DEFAULT_MODEL (gemma3:4b) so the advertised model resolves correctly.
|
|
113
|
+
DEFAULT_OLLAMA_TAG = "gemma3:4b"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def ollama_tag_for_tier(tier: str) -> str:
|
|
117
|
+
"""The Ollama tag a node of this tier should pull for CPU chat.
|
|
118
|
+
|
|
119
|
+
Tier C (Pi / laptop / CPU-only) → the small default chat model (gemma3:4b);
|
|
120
|
+
higher tiers get a slightly larger CPU-runnable chat model. Used by the
|
|
121
|
+
installer / run-node wrapper to provision a concrete, CPU-runnable model. This
|
|
122
|
+
reads the roster above; it does not touch the classify_capability thresholds."""
|
|
123
|
+
if tier == "C":
|
|
124
|
+
return DEFAULT_OLLAMA_TAG
|
|
125
|
+
# B/A/S can also run CPU chat as a fallback; give them a marginally larger tag.
|
|
126
|
+
return OLLAMA_TAGS.get("llama-3.2-3b", DEFAULT_OLLAMA_TAG)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def classify_capability(vram_gb: float = 0.0) -> str:
|
|
130
|
+
"""Classify measured hardware into a placement tier (§2 thresholds)."""
|
|
131
|
+
if vram_gb >= 80:
|
|
132
|
+
return "S"
|
|
133
|
+
if vram_gb >= 24:
|
|
134
|
+
return "A"
|
|
135
|
+
if vram_gb >= 8:
|
|
136
|
+
return "B"
|
|
137
|
+
return "C" # Pi / laptop / old desktop / CPU-only
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def vision_models() -> list[str]:
|
|
141
|
+
"""Catalog ids that can actually LOOK AT an image.
|
|
142
|
+
|
|
143
|
+
WHY THIS EXISTS (bug 2026-10-13, and the mobile image-recognition report is the same bug).
|
|
144
|
+
`selection.select_model` picks "the cheapest TEXT model that clears the prompt's quality bar" —
|
|
145
|
+
its docstring says so — and it takes a query string. It has never had any way to know that the
|
|
146
|
+
turn carries an image, so a message with an attachment was routed to a text model, which could
|
|
147
|
+
not see it. The catalog has known which model does vision all along (`tasks: ["vision"]`); what
|
|
148
|
+
was missing was anything that ASKED.
|
|
149
|
+
|
|
150
|
+
Read from the catalog rather than hard-coded, so adding a VLM to the roster is enough.
|
|
151
|
+
"""
|
|
152
|
+
return [m["id"] for m in MODEL_CATALOG if "vision" in (m.get("tasks") or ())]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def models_for_tier(tier: str) -> list[dict]:
|
|
156
|
+
# FILE 09-B1/B5 — the registry licence gate runs HERE, where every serving set is
|
|
157
|
+
# computed: non-commercial/unverified models never reach a manifest, and a
|
|
158
|
+
# paid-commercial model (Stability, Lightricks) appears only after its
|
|
159
|
+
# STEVAI_LICENSE_<VENDOR>_AGREED env is set. Import inside the function so a broken
|
|
160
|
+
# registry module cannot take node registration down with it — it fails OPEN to the
|
|
161
|
+
# catalog, and the CI tests (not runtime luck) guarantee the catalog itself is clean.
|
|
162
|
+
try:
|
|
163
|
+
from frutebox.registry import serving_allowed
|
|
164
|
+
allowed = [m for m in MODEL_CATALOG if serving_allowed(m["id"])]
|
|
165
|
+
except Exception:
|
|
166
|
+
allowed = list(MODEL_CATALOG)
|
|
167
|
+
return [m for m in allowed if _rank(m["tier"]) <= _rank(tier)]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def build_manifest(
|
|
171
|
+
tier: str,
|
|
172
|
+
memory_budget_gb: float,
|
|
173
|
+
demand_gaps: Iterable[str] = (),
|
|
174
|
+
max_models: int = 5,
|
|
175
|
+
headroom: float = 0.8,
|
|
176
|
+
) -> list[str]:
|
|
177
|
+
"""A lean manifest: high-frequency small task-types + region demand gaps + a
|
|
178
|
+
brain if the budget allows, under a headroom fraction of the budget, capped in
|
|
179
|
+
size. Coverage comes from the mesh, not one node."""
|
|
180
|
+
budget = memory_budget_gb * headroom
|
|
181
|
+
candidates = sorted((m for m in models_for_tier(tier) if m["vram_gb"] <= budget), key=lambda m: m["vram_gb"])
|
|
182
|
+
chosen: list[dict] = []
|
|
183
|
+
used = 0.0
|
|
184
|
+
used_tasks: set[str] = set()
|
|
185
|
+
|
|
186
|
+
def try_add(m: dict | None) -> None:
|
|
187
|
+
nonlocal used
|
|
188
|
+
if not m or m in chosen or len(chosen) >= max_models or used + m["vram_gb"] > budget:
|
|
189
|
+
return
|
|
190
|
+
chosen.append(m)
|
|
191
|
+
used += m["vram_gb"]
|
|
192
|
+
used_tasks.update(m["tasks"])
|
|
193
|
+
|
|
194
|
+
def first_serving(task: str) -> dict | None:
|
|
195
|
+
return next((c for c in candidates if task in c["tasks"] and c not in chosen), None)
|
|
196
|
+
|
|
197
|
+
for task in ("triage", "embed", "chat"):
|
|
198
|
+
if task not in used_tasks:
|
|
199
|
+
try_add(first_serving(task))
|
|
200
|
+
for task in demand_gaps:
|
|
201
|
+
if task not in used_tasks:
|
|
202
|
+
try_add(first_serving(task))
|
|
203
|
+
if len(chosen) < max_models:
|
|
204
|
+
brain = next((c for c in reversed(candidates) if ("brain" in c["tasks"] or "code" in c["tasks"]) and c not in chosen and used + c["vram_gb"] <= budget), None)
|
|
205
|
+
try_add(brain)
|
|
206
|
+
return [m["id"] for m in chosen]
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def needs_capability_challenge(claimed_tier: str) -> bool:
|
|
210
|
+
"""Big-GPU claims (Tier A/S) must be proven with a real workload before trust."""
|
|
211
|
+
return claimed_tier in ("A", "S")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def verify_capability_claim(claimed_tier: str, measured_vram_gb: float) -> dict:
|
|
215
|
+
"""Trust a node at its MEASURED tier, never above an inflated claim."""
|
|
216
|
+
measured = classify_capability(measured_vram_gb)
|
|
217
|
+
ok = _rank(measured) >= _rank(claimed_tier)
|
|
218
|
+
return {"ok": ok, "trusted_tier": claimed_tier if ok else measured, "measured_tier": measured}
|