@flop-labs/cli 0.4.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +84 -0
- package/README.md +22 -1
- package/dist/cli.js +1308 -419
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -402,12 +402,12 @@ var init_state = __esm({
|
|
|
402
402
|
return computeChecksum(snapshotData);
|
|
403
403
|
}
|
|
404
404
|
/** Transition to a new lifecycle state. */
|
|
405
|
-
transitionTo(state,
|
|
405
|
+
transitionTo(state, reason3) {
|
|
406
406
|
this.data.lifecycle.state = state;
|
|
407
407
|
this.data.lifecycle.stateChangedAt = Date.now();
|
|
408
408
|
this.data.lifecycle.lastActiveAt = Date.now();
|
|
409
|
-
if (
|
|
410
|
-
this.data.lifecycle.terminationReason =
|
|
409
|
+
if (reason3) {
|
|
410
|
+
this.data.lifecycle.terminationReason = reason3;
|
|
411
411
|
}
|
|
412
412
|
this.data.checksum = this.computeChecksumInternal();
|
|
413
413
|
}
|
|
@@ -724,14 +724,14 @@ var init_task_runner = __esm({
|
|
|
724
724
|
}
|
|
725
725
|
}
|
|
726
726
|
withTimeout(promise, timeoutMs) {
|
|
727
|
-
return new Promise((
|
|
727
|
+
return new Promise((resolve11, reject) => {
|
|
728
728
|
const timer = setTimeout(
|
|
729
729
|
() => reject(new AgentError("T002" /* TASK_TIMEOUT */, `Task timed out after ${timeoutMs}ms`, true)),
|
|
730
730
|
timeoutMs
|
|
731
731
|
);
|
|
732
732
|
promise.then((result) => {
|
|
733
733
|
clearTimeout(timer);
|
|
734
|
-
|
|
734
|
+
resolve11(result);
|
|
735
735
|
}).catch((err) => {
|
|
736
736
|
clearTimeout(timer);
|
|
737
737
|
reject(err);
|
|
@@ -739,7 +739,7 @@ var init_task_runner = __esm({
|
|
|
739
739
|
});
|
|
740
740
|
}
|
|
741
741
|
sleep(ms) {
|
|
742
|
-
return new Promise((
|
|
742
|
+
return new Promise((resolve11) => setTimeout(resolve11, ms));
|
|
743
743
|
}
|
|
744
744
|
};
|
|
745
745
|
}
|
|
@@ -829,10 +829,10 @@ var init_worker = __esm({
|
|
|
829
829
|
await this.processLoop(options.api, options.signer);
|
|
830
830
|
}
|
|
831
831
|
/** Request graceful shutdown. */
|
|
832
|
-
async stop(
|
|
832
|
+
async stop(reason3) {
|
|
833
833
|
this.stopRequested = true;
|
|
834
|
-
this.state.transitionTo("draining",
|
|
835
|
-
this.logger.info("Shutdown requested", { reason:
|
|
834
|
+
this.state.transitionTo("draining", reason3);
|
|
835
|
+
this.logger.info("Shutdown requested", { reason: reason3 });
|
|
836
836
|
}
|
|
837
837
|
/** Main processing loop. */
|
|
838
838
|
async processLoop(api, signer) {
|
|
@@ -904,7 +904,7 @@ var init_worker = __esm({
|
|
|
904
904
|
return this.identity;
|
|
905
905
|
}
|
|
906
906
|
sleep(ms) {
|
|
907
|
-
return new Promise((
|
|
907
|
+
return new Promise((resolve11) => setTimeout(resolve11, ms));
|
|
908
908
|
}
|
|
909
909
|
};
|
|
910
910
|
}
|
|
@@ -912,10 +912,10 @@ var init_worker = __esm({
|
|
|
912
912
|
|
|
913
913
|
// ../agent/src/chain-apis.ts
|
|
914
914
|
async function submitTx(tx, signer) {
|
|
915
|
-
return new Promise((
|
|
915
|
+
return new Promise((resolve11, reject) => {
|
|
916
916
|
tx.signAndSend(signer, ({ status, dispatchError }) => {
|
|
917
917
|
if (dispatchError) reject(new Error(`Tx failed: ${dispatchError.toString()}`));
|
|
918
|
-
else if (status.isInBlock || status.isFinalized)
|
|
918
|
+
else if (status.isInBlock || status.isFinalized) resolve11(status.asFinalized.toString());
|
|
919
919
|
}).catch(reject);
|
|
920
920
|
});
|
|
921
921
|
}
|
|
@@ -1219,7 +1219,7 @@ async function waitForTaskCompletion(api, taskId, timeoutMs = DEFAULT_TIMEOUT, p
|
|
|
1219
1219
|
return receipt;
|
|
1220
1220
|
}
|
|
1221
1221
|
}
|
|
1222
|
-
await new Promise((
|
|
1222
|
+
await new Promise((resolve11) => setTimeout(resolve11, pollIntervalMs));
|
|
1223
1223
|
}
|
|
1224
1224
|
throw new AgentError(
|
|
1225
1225
|
"T002" /* TASK_TIMEOUT */,
|
|
@@ -1657,18 +1657,18 @@ var init_fee_estimation = __esm({
|
|
|
1657
1657
|
const outOfBudget = candidates.filter((m) => !affordable.has(m.hash)).map((m) => ({ model: m, fee: feeMap.get(m.hash) }));
|
|
1658
1658
|
const selectedModel = selected ?? candidates[0];
|
|
1659
1659
|
const estimatedFee = feeMap.get(selectedModel.hash);
|
|
1660
|
-
let
|
|
1660
|
+
let reason3;
|
|
1661
1661
|
if (selected) {
|
|
1662
|
-
|
|
1662
|
+
reason3 = preferBestValue ? `Best value model within ${maxBudget} FLOP budget` : `Highest quality model within ${maxBudget} FLOP budget`;
|
|
1663
1663
|
} else {
|
|
1664
|
-
|
|
1664
|
+
reason3 = `Best available model (cheapest) \u2014 all options exceed ${maxBudget} FLOP budget`;
|
|
1665
1665
|
}
|
|
1666
1666
|
return {
|
|
1667
1667
|
selectedModel,
|
|
1668
1668
|
estimatedFee,
|
|
1669
1669
|
alternatives,
|
|
1670
1670
|
outOfBudget,
|
|
1671
|
-
reason:
|
|
1671
|
+
reason: reason3
|
|
1672
1672
|
};
|
|
1673
1673
|
}
|
|
1674
1674
|
/**
|
|
@@ -1819,69 +1819,13 @@ import { Keyring as Keyring3 } from "@polkadot/keyring";
|
|
|
1819
1819
|
import { cryptoWaitReady as cryptoWaitReady3, randomAsHex } from "@polkadot/util-crypto";
|
|
1820
1820
|
import {
|
|
1821
1821
|
FLOP_NETWORK_PROFILES,
|
|
1822
|
-
DEFAULT_FAUCET_AMOUNT as SDK_DEFAULT_FAUCET_AMOUNT
|
|
1822
|
+
DEFAULT_FAUCET_AMOUNT as SDK_DEFAULT_FAUCET_AMOUNT,
|
|
1823
|
+
fundFromFaucetDetailed,
|
|
1824
|
+
FaucetUnconfirmedError
|
|
1823
1825
|
} from "@flop-labs/sdk";
|
|
1824
1826
|
function isFaucetAvailable(network) {
|
|
1825
1827
|
return FAUCET_ENDPOINTS[network] !== null;
|
|
1826
1828
|
}
|
|
1827
|
-
async function requestFaucetFunding(address, network, timeout = 3e4, referral) {
|
|
1828
|
-
const endpoint = FAUCET_ENDPOINTS[network];
|
|
1829
|
-
if (!endpoint) {
|
|
1830
|
-
return {
|
|
1831
|
-
success: false,
|
|
1832
|
-
error: `No faucet available for network: ${network}`
|
|
1833
|
-
};
|
|
1834
|
-
}
|
|
1835
|
-
try {
|
|
1836
|
-
const response = await fetch(`${endpoint}/fund`, {
|
|
1837
|
-
method: "POST",
|
|
1838
|
-
headers: {
|
|
1839
|
-
"Content-Type": "application/json"
|
|
1840
|
-
},
|
|
1841
|
-
body: JSON.stringify({
|
|
1842
|
-
address,
|
|
1843
|
-
amount: DEFAULT_FAUCET_AMOUNT,
|
|
1844
|
-
...referral === void 0 ? {} : { ref: referral, source: "sdk" }
|
|
1845
|
-
}),
|
|
1846
|
-
signal: AbortSignal.timeout(timeout)
|
|
1847
|
-
});
|
|
1848
|
-
if (!response.ok) {
|
|
1849
|
-
const errorText = await response.text();
|
|
1850
|
-
return {
|
|
1851
|
-
success: false,
|
|
1852
|
-
error: `Faucet request failed: ${response.status} - ${errorText}`
|
|
1853
|
-
};
|
|
1854
|
-
}
|
|
1855
|
-
const data = await response.json();
|
|
1856
|
-
return {
|
|
1857
|
-
success: true,
|
|
1858
|
-
amount: data.amount ?? DEFAULT_FAUCET_AMOUNT,
|
|
1859
|
-
txHash: data.txHash
|
|
1860
|
-
};
|
|
1861
|
-
} catch (error) {
|
|
1862
|
-
return {
|
|
1863
|
-
success: false,
|
|
1864
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1865
|
-
};
|
|
1866
|
-
}
|
|
1867
|
-
}
|
|
1868
|
-
async function waitForBalance(api, address, minBalance, timeoutMs = 6e4) {
|
|
1869
|
-
const deadline = Date.now() + timeoutMs;
|
|
1870
|
-
const pollInterval = 2e3;
|
|
1871
|
-
while (Date.now() < deadline) {
|
|
1872
|
-
try {
|
|
1873
|
-
const accountInfo = await api.query.system.account(address);
|
|
1874
|
-
const data = accountInfo.data;
|
|
1875
|
-
const freeBalance = BigInt(data.free.toString());
|
|
1876
|
-
if (freeBalance >= minBalance) {
|
|
1877
|
-
return true;
|
|
1878
|
-
}
|
|
1879
|
-
} catch {
|
|
1880
|
-
}
|
|
1881
|
-
await new Promise((resolve10) => setTimeout(resolve10, pollInterval));
|
|
1882
|
-
}
|
|
1883
|
-
return false;
|
|
1884
|
-
}
|
|
1885
1829
|
async function getBalance(api, address) {
|
|
1886
1830
|
const accountInfo = await api.query.system.account(address);
|
|
1887
1831
|
const data = accountInfo.data;
|
|
@@ -2001,9 +1945,9 @@ var init_quickstart = __esm({
|
|
|
2001
1945
|
/**
|
|
2002
1946
|
* Stop the agent worker gracefully.
|
|
2003
1947
|
*/
|
|
2004
|
-
async stop(
|
|
2005
|
-
await this.worker.stop(
|
|
2006
|
-
this.logger.info("QuickStart agent stopped", { reason:
|
|
1948
|
+
async stop(reason3) {
|
|
1949
|
+
await this.worker.stop(reason3);
|
|
1950
|
+
this.logger.info("QuickStart agent stopped", { reason: reason3 });
|
|
2007
1951
|
}
|
|
2008
1952
|
/**
|
|
2009
1953
|
* Shutdown agent and disconnect from network.
|
|
@@ -2376,6 +2320,10 @@ var init_quickstart = __esm({
|
|
|
2376
2320
|
* @returns QuickStart agent result with agent wrapper and metadata
|
|
2377
2321
|
*/
|
|
2378
2322
|
static async create(config = {}) {
|
|
2323
|
+
const shouldFaucet = config.faucet ?? (config.network ?? "devnet") === "devnet";
|
|
2324
|
+
if (shouldFaucet && isFaucetAvailable(config.network ?? "devnet") && !config.keypair && !config.signerUri?.trim()) {
|
|
2325
|
+
throw new Error("Faucet funding requires a caller-owned signer or keypair");
|
|
2326
|
+
}
|
|
2379
2327
|
await cryptoWaitReady3();
|
|
2380
2328
|
const network = config.network ?? "devnet";
|
|
2381
2329
|
const endpoint = config.rpcEndpoint ?? NETWORK_ENDPOINTS[network];
|
|
@@ -2383,15 +2331,13 @@ var init_quickstart = __esm({
|
|
|
2383
2331
|
const keypair = resolveOrCreateKeypair(config);
|
|
2384
2332
|
const api = await connectToNetwork(endpoint, timeout);
|
|
2385
2333
|
let initialBalance = await getBalance(api, keypair.address);
|
|
2386
|
-
const shouldFaucet = config.faucet ?? network === "devnet";
|
|
2387
2334
|
if (shouldFaucet && isFaucetAvailable(network) && initialBalance < DEFAULT_FAUCET_AMOUNT) {
|
|
2388
|
-
const
|
|
2389
|
-
if (
|
|
2390
|
-
const
|
|
2391
|
-
const
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
}
|
|
2335
|
+
const funding = await fundFromFaucetDetailed(api, keypair.address, { network, timeout, referral: config.referral });
|
|
2336
|
+
if (funding.status !== "refused") {
|
|
2337
|
+
const error = new FaucetUnconfirmedError(funding);
|
|
2338
|
+
const cleanup = await Promise.allSettled([api.disconnect()]);
|
|
2339
|
+
error.cleanupErrors = cleanup.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
2340
|
+
throw error;
|
|
2395
2341
|
}
|
|
2396
2342
|
}
|
|
2397
2343
|
const agentId = generateAgentId();
|
|
@@ -2456,7 +2402,7 @@ var init_quickstart = __esm({
|
|
|
2456
2402
|
* ```ts
|
|
2457
2403
|
* const result = await FlopAgent.inference('Write a Python function to sort a list', {
|
|
2458
2404
|
* network: 'devnet',
|
|
2459
|
-
*
|
|
2405
|
+
* signerUri: process.env.FLOP_SIGNER_URI!, // Pre-funded, caller-owned signer
|
|
2460
2406
|
* });
|
|
2461
2407
|
* console.log(result.output);
|
|
2462
2408
|
* ```
|
|
@@ -4941,9 +4887,9 @@ var init_src = __esm({
|
|
|
4941
4887
|
});
|
|
4942
4888
|
|
|
4943
4889
|
// src/cli.ts
|
|
4944
|
-
import { Command as
|
|
4890
|
+
import { Command as Command51, Option as Option8 } from "commander";
|
|
4945
4891
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
4946
|
-
import { join as
|
|
4892
|
+
import { join as join12 } from "node:path";
|
|
4947
4893
|
|
|
4948
4894
|
// src/output.ts
|
|
4949
4895
|
var activeOutputMode = "human";
|
|
@@ -5512,8 +5458,8 @@ function sessionListCmd() {
|
|
|
5512
5458
|
import { Command as Command10, Option } from "commander";
|
|
5513
5459
|
function parseSessionMaxFlop(value) {
|
|
5514
5460
|
if (!/^\d+(?:\.\d{1,18})?$/.test(value)) throw new Error("--max-flop must be a positive decimal with at most 18 fractional digits");
|
|
5515
|
-
const [whole,
|
|
5516
|
-
const raw = BigInt(whole) * 10n ** 18n + BigInt(
|
|
5461
|
+
const [whole, fraction2 = ""] = value.split(".");
|
|
5462
|
+
const raw = BigInt(whole) * 10n ** 18n + BigInt(fraction2.padEnd(18, "0"));
|
|
5517
5463
|
if (raw <= 0n || raw >= 1n << 128n) throw new Error("--max-flop must be positive and fit the chain balance type");
|
|
5518
5464
|
return raw;
|
|
5519
5465
|
}
|
|
@@ -5528,6 +5474,7 @@ function sessionRunCmd() {
|
|
|
5528
5474
|
return new Command10("run").description("Run verified inference directly on a named miner and settle its channel").requiredOption("--miner <ss58>", "Serving miner account").requiredOption("--model <slug>", "Model slug").requiredOption("--prompt <text>", "Prompt").requiredOption("--max-flop <N>", "Maximum escrow in FLOP").option("--max-tokens <N>", "Maximum completion tokens (SDK default: 256)").addOption(new Option("--via <path>", "Open path").choices(["auto", "offer", "force"]).default("auto")).option("--endpoint <URL>", "Reachable miner session endpoint", "http://127.0.0.1:8645").option("--auth-token-env <VAR>", "Environment variable holding the miner bearer (legacy/broker auth)").option("--new-wallet", "Save a fresh owner-only devnet wallet and fund it from the devnet faucet").option("--json", "Output as JSON").action(async (opts, command) => {
|
|
5529
5475
|
let client;
|
|
5530
5476
|
let walletPath;
|
|
5477
|
+
let funding;
|
|
5531
5478
|
try {
|
|
5532
5479
|
const maxFlop = parseSessionMaxFlop(opts.maxFlop);
|
|
5533
5480
|
const maxTokens = opts.maxTokens === void 0 ? void 0 : parseSessionMaxTokens(opts.maxTokens);
|
|
@@ -5564,9 +5511,9 @@ function sessionRunCmd() {
|
|
|
5564
5511
|
client = await sdk.FlopClient.createWithKeypair(resolved.rpc, pair);
|
|
5565
5512
|
const faucet = await sdk.getFaucetChainInfo({ endpoint: resolved.profile.faucet });
|
|
5566
5513
|
if (faucet.genesisHash.toLowerCase() !== client.api.genesisHash.toHex().toLowerCase()) throw new Error("faucet/RPC genesis mismatch");
|
|
5567
|
-
|
|
5568
|
-
if (!
|
|
5569
|
-
if (!await sdk.
|
|
5514
|
+
funding = await sdk.requestFaucetFunding(pair.address, { network: "devnet", endpoint: resolved.profile.faucet, maxRetries: 1 });
|
|
5515
|
+
if (!funding.success) throw new Error(funding.error ?? "devnet faucet funding failed");
|
|
5516
|
+
if (!await sdk.waitForAvailableBalance(client.api, pair.address, Number(opts.maxFlop))) {
|
|
5570
5517
|
throw new Error("faucet balance did not arrive before the funding deadline");
|
|
5571
5518
|
}
|
|
5572
5519
|
} else {
|
|
@@ -5598,11 +5545,18 @@ function sessionRunCmd() {
|
|
|
5598
5545
|
}, getJson(command));
|
|
5599
5546
|
} catch (error) {
|
|
5600
5547
|
const channelId = error && typeof error === "object" && "channelId" in error && error.channelId instanceof Uint8Array ? `0x${Buffer.from(error.channelId).toString("hex")}` : null;
|
|
5548
|
+
const fundingUnknown = funding && (funding.outcome === "unknown" || funding.success);
|
|
5601
5549
|
printError({
|
|
5602
5550
|
code: "SESSION_FAILED",
|
|
5603
5551
|
message: error instanceof Error ? error.message : String(error),
|
|
5604
5552
|
channel: channelId,
|
|
5605
|
-
recovery: channelId ? "inspect finalized channel and terminal events before retrying" : "no channel reported",
|
|
5553
|
+
recovery: channelId ? "inspect finalized channel and terminal events before retrying" : fundingUnknown ? "funding is unconfirmed; reconcile the original request before requesting again" : "no channel reported",
|
|
5554
|
+
...funding ? { funding: {
|
|
5555
|
+
outcome: fundingUnknown ? "unknown" : funding.outcome,
|
|
5556
|
+
...funding.txHash ? { txHash: funding.txHash } : {},
|
|
5557
|
+
...funding.amount !== void 0 ? { amount: funding.amount } : {},
|
|
5558
|
+
...funding.status !== void 0 ? { status: funding.status } : {}
|
|
5559
|
+
} } : {},
|
|
5606
5560
|
...walletPath ? { wallet: walletPath } : {}
|
|
5607
5561
|
}, "json");
|
|
5608
5562
|
process.exitCode = 1;
|
|
@@ -6127,12 +6081,12 @@ function pathCheck(id, target, required = true) {
|
|
|
6127
6081
|
};
|
|
6128
6082
|
}
|
|
6129
6083
|
async function probeHttpUrl(id, url, required = true, timeoutMs = 3e3) {
|
|
6130
|
-
return new Promise((
|
|
6084
|
+
return new Promise((resolve11) => {
|
|
6131
6085
|
let parsed;
|
|
6132
6086
|
try {
|
|
6133
6087
|
parsed = new URL(url);
|
|
6134
6088
|
} catch {
|
|
6135
|
-
|
|
6089
|
+
resolve11({
|
|
6136
6090
|
id,
|
|
6137
6091
|
status: "fail",
|
|
6138
6092
|
required,
|
|
@@ -6145,7 +6099,7 @@ async function probeHttpUrl(id, url, required = true, timeoutMs = 3e3) {
|
|
|
6145
6099
|
const request3 = transport.request(parsed, { method: "GET", timeout: timeoutMs }, (response) => {
|
|
6146
6100
|
response.resume();
|
|
6147
6101
|
const reachable = (response.statusCode ?? 500) < 500;
|
|
6148
|
-
|
|
6102
|
+
resolve11({
|
|
6149
6103
|
id,
|
|
6150
6104
|
status: reachable ? "pass" : required ? "fail" : "warn",
|
|
6151
6105
|
required,
|
|
@@ -6155,7 +6109,7 @@ async function probeHttpUrl(id, url, required = true, timeoutMs = 3e3) {
|
|
|
6155
6109
|
});
|
|
6156
6110
|
request3.on("timeout", () => request3.destroy(new Error("timeout")));
|
|
6157
6111
|
request3.on("error", (error) => {
|
|
6158
|
-
|
|
6112
|
+
resolve11({
|
|
6159
6113
|
id,
|
|
6160
6114
|
status: required ? "fail" : "warn",
|
|
6161
6115
|
required,
|
|
@@ -6199,7 +6153,7 @@ function unwrapStorage(raw) {
|
|
|
6199
6153
|
|
|
6200
6154
|
// src/commands/launch.ts
|
|
6201
6155
|
function checkGateway(url) {
|
|
6202
|
-
return new Promise((
|
|
6156
|
+
return new Promise((resolve11) => {
|
|
6203
6157
|
try {
|
|
6204
6158
|
const parsedUrl = new URL(url);
|
|
6205
6159
|
const req = http2.request(
|
|
@@ -6211,17 +6165,17 @@ function checkGateway(url) {
|
|
|
6211
6165
|
timeout: 1e3
|
|
6212
6166
|
},
|
|
6213
6167
|
(res) => {
|
|
6214
|
-
|
|
6168
|
+
resolve11(res.statusCode === 200);
|
|
6215
6169
|
}
|
|
6216
6170
|
);
|
|
6217
|
-
req.on("error", () =>
|
|
6171
|
+
req.on("error", () => resolve11(false));
|
|
6218
6172
|
req.on("timeout", () => {
|
|
6219
6173
|
req.destroy();
|
|
6220
|
-
|
|
6174
|
+
resolve11(false);
|
|
6221
6175
|
});
|
|
6222
6176
|
req.end();
|
|
6223
6177
|
} catch {
|
|
6224
|
-
|
|
6178
|
+
resolve11(false);
|
|
6225
6179
|
}
|
|
6226
6180
|
});
|
|
6227
6181
|
}
|
|
@@ -6529,7 +6483,7 @@ async function withCredentialLock(path5, action) {
|
|
|
6529
6483
|
continue;
|
|
6530
6484
|
}
|
|
6531
6485
|
if (Date.now() >= deadline) throw new Error(`FLOP credential lock is busy at ${lock}`);
|
|
6532
|
-
await new Promise((
|
|
6486
|
+
await new Promise((resolve11) => setTimeout(resolve11, 100));
|
|
6533
6487
|
}
|
|
6534
6488
|
}
|
|
6535
6489
|
try {
|
|
@@ -6597,17 +6551,35 @@ function walletEnrollment(options) {
|
|
|
6597
6551
|
const pair = await enrollmentWallet(options.walletPath ?? devnetWalletPath());
|
|
6598
6552
|
const minBalance = options.minBalance ?? DEFAULT_MIN_BALANCE;
|
|
6599
6553
|
if (await options.balance(pair.address) < minBalance) {
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6554
|
+
let funded;
|
|
6555
|
+
try {
|
|
6556
|
+
funded = await fetch(`${options.faucet}/fund`, {
|
|
6557
|
+
method: "POST",
|
|
6558
|
+
headers: { "content-type": "application/json" },
|
|
6559
|
+
body: JSON.stringify({ address: pair.address }),
|
|
6560
|
+
signal: AbortSignal.timeout(6e4)
|
|
6561
|
+
});
|
|
6562
|
+
} catch {
|
|
6563
|
+
throw new Error("devnet faucet outcome unknown; reconcile the original request for the saved wallet before requesting again");
|
|
6564
|
+
}
|
|
6565
|
+
if (!funded.ok) {
|
|
6566
|
+
if (funded.status >= 500 || funded.status === 408) throw new Error("devnet faucet outcome unknown; reconcile the original request for the saved wallet before requesting again");
|
|
6567
|
+
throw await failure(funded, "devnet faucet");
|
|
6568
|
+
}
|
|
6569
|
+
const response = await funded.json().catch(() => null);
|
|
6570
|
+
const hint = typeof response?.txHash === "string" && /^0x[0-9a-f]{64}$/i.test(response.txHash) ? ` (transaction ${response.txHash})` : "";
|
|
6607
6571
|
const deadline = Date.now() + (options.fundTimeoutMs ?? 12e4);
|
|
6608
|
-
|
|
6609
|
-
|
|
6610
|
-
|
|
6572
|
+
const unconfirmed = () => new Error(`devnet balance did not reach the required floor${hint}; the faucet request is unconfirmed, reconcile it before requesting again`);
|
|
6573
|
+
while (true) {
|
|
6574
|
+
let available;
|
|
6575
|
+
try {
|
|
6576
|
+
available = await options.balance(pair.address);
|
|
6577
|
+
} catch {
|
|
6578
|
+
throw unconfirmed();
|
|
6579
|
+
}
|
|
6580
|
+
if (available >= minBalance) break;
|
|
6581
|
+
if (Date.now() >= deadline) throw unconfirmed();
|
|
6582
|
+
await new Promise((resolve11) => setTimeout(resolve11, options.pollMs ?? 3e3));
|
|
6611
6583
|
}
|
|
6612
6584
|
}
|
|
6613
6585
|
const challenge = await fetch(
|
|
@@ -6882,8 +6854,8 @@ import http3 from "http";
|
|
|
6882
6854
|
|
|
6883
6855
|
// src/proxy/openai.ts
|
|
6884
6856
|
var GFLOPS_PER_TOKEN = 10;
|
|
6885
|
-
function estimateTokens2(
|
|
6886
|
-
return Math.max(1, Math.ceil((
|
|
6857
|
+
function estimateTokens2(text4) {
|
|
6858
|
+
return Math.max(1, Math.ceil((text4?.length ?? 0) / 4));
|
|
6887
6859
|
}
|
|
6888
6860
|
function chatMessagesToPrompt(messages) {
|
|
6889
6861
|
return messages.map((m) => `${m.role}: ${m.content}`).join("\n");
|
|
@@ -6944,8 +6916,8 @@ function modelsListResponse(models) {
|
|
|
6944
6916
|
function streamMeta(outcome) {
|
|
6945
6917
|
return { id: outcome.taskId, model: outcome.model, created: Math.floor(Date.now() / 1e3) };
|
|
6946
6918
|
}
|
|
6947
|
-
function chunkText(
|
|
6948
|
-
return
|
|
6919
|
+
function chunkText(text4) {
|
|
6920
|
+
return text4 ? text4.match(/\s+|\S+/g) ?? [] : [];
|
|
6949
6921
|
}
|
|
6950
6922
|
function buildChatChunk(meta, delta, finishReason) {
|
|
6951
6923
|
return {
|
|
@@ -6956,13 +6928,13 @@ function buildChatChunk(meta, delta, finishReason) {
|
|
|
6956
6928
|
choices: [{ index: 0, delta, finish_reason: finishReason }]
|
|
6957
6929
|
};
|
|
6958
6930
|
}
|
|
6959
|
-
function buildTextChunk(meta,
|
|
6931
|
+
function buildTextChunk(meta, text4, finishReason) {
|
|
6960
6932
|
return {
|
|
6961
6933
|
id: meta.id,
|
|
6962
6934
|
object: "text_completion",
|
|
6963
6935
|
created: meta.created,
|
|
6964
6936
|
model: meta.model,
|
|
6965
|
-
choices: [{ index: 0, text:
|
|
6937
|
+
choices: [{ index: 0, text: text4, finish_reason: finishReason }]
|
|
6966
6938
|
};
|
|
6967
6939
|
}
|
|
6968
6940
|
function buildUsageChunk(meta, outcome, promptTokens) {
|
|
@@ -7014,14 +6986,14 @@ var CORS_HEADERS = {
|
|
|
7014
6986
|
"Access-Control-Allow-Headers": "Authorization, Content-Type"
|
|
7015
6987
|
};
|
|
7016
6988
|
function readBody(req) {
|
|
7017
|
-
return new Promise((
|
|
6989
|
+
return new Promise((resolve11) => {
|
|
7018
6990
|
let data = "";
|
|
7019
6991
|
req.on("data", (c) => data += c);
|
|
7020
6992
|
req.on("end", () => {
|
|
7021
6993
|
try {
|
|
7022
|
-
|
|
6994
|
+
resolve11(data ? JSON.parse(data) : {});
|
|
7023
6995
|
} catch {
|
|
7024
|
-
|
|
6996
|
+
resolve11({});
|
|
7025
6997
|
}
|
|
7026
6998
|
});
|
|
7027
6999
|
});
|
|
@@ -7175,8 +7147,8 @@ function createLocalEmbedder(model) {
|
|
|
7175
7147
|
extractor = await transformers.pipeline("feature-extraction", model);
|
|
7176
7148
|
}
|
|
7177
7149
|
const out = [];
|
|
7178
|
-
for (const
|
|
7179
|
-
const t = await extractor(
|
|
7150
|
+
for (const text4 of inputs) {
|
|
7151
|
+
const t = await extractor(text4, { pooling: "mean", normalize: true });
|
|
7180
7152
|
out.push(Array.from(t.data));
|
|
7181
7153
|
}
|
|
7182
7154
|
return out;
|
|
@@ -7258,7 +7230,7 @@ async function startProxyServer(o) {
|
|
|
7258
7230
|
embeddingModel: o.embeddingModel,
|
|
7259
7231
|
embed: createLocalEmbedder(o.embeddingModel)
|
|
7260
7232
|
});
|
|
7261
|
-
await new Promise((
|
|
7233
|
+
await new Promise((resolve11) => server.listen(o.port, o.host, () => resolve11()));
|
|
7262
7234
|
const addr = server.address();
|
|
7263
7235
|
const url = `http://${o.host}:${addr.port}`;
|
|
7264
7236
|
const models = await listModels();
|
|
@@ -7395,9 +7367,9 @@ function launchExternal2(command, args, envOverrides) {
|
|
|
7395
7367
|
function runHermesConfig(launcher, commands) {
|
|
7396
7368
|
return commands.reduce(
|
|
7397
7369
|
(p, args) => p.then(
|
|
7398
|
-
() => new Promise((
|
|
7370
|
+
() => new Promise((resolve11, reject) => {
|
|
7399
7371
|
const c = spawnLauncher(launcher, args, { stdio: "inherit" });
|
|
7400
|
-
c.on("exit", (code) => code === 0 ?
|
|
7372
|
+
c.on("exit", (code) => code === 0 ? resolve11() : reject(new Error(`hermes ${args.join(" ")} exited ${code}`)));
|
|
7401
7373
|
c.on("error", reject);
|
|
7402
7374
|
})
|
|
7403
7375
|
),
|
|
@@ -7624,14 +7596,18 @@ function daWithdrawCmd() {
|
|
|
7624
7596
|
|
|
7625
7597
|
// src/commands/model-upload.ts
|
|
7626
7598
|
import { Command as Command30, Option as Option2 } from "commander";
|
|
7627
|
-
import {
|
|
7628
|
-
import {
|
|
7599
|
+
import { createWriteStream } from "node:fs";
|
|
7600
|
+
import { copyFile, lstat as lstat7, mkdir as mkdir11, mkdtemp as mkdtemp2, rm as rm4, writeFile as writeFile9 } from "node:fs/promises";
|
|
7601
|
+
import { pipeline as pipeline2 } from "node:stream/promises";
|
|
7602
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
7629
7603
|
import { basename as basename2, join as join6, resolve as resolve8 } from "node:path";
|
|
7630
7604
|
|
|
7631
7605
|
// src/model.ts
|
|
7632
7606
|
import { resolve as resolve5, relative, sep } from "node:path";
|
|
7633
|
-
import { mkdir as mkdir7, readFile as readFile4, readdir, lstat as lstat3, writeFile as writeFile7 } from "node:fs/promises";
|
|
7607
|
+
import { mkdir as mkdir7, mkdtemp, readFile as readFile4, readdir, lstat as lstat3, rm as rm2, writeFile as writeFile7 } from "node:fs/promises";
|
|
7634
7608
|
import { execFile as execFile3, spawn as spawn4 } from "node:child_process";
|
|
7609
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
7610
|
+
import { tmpdir } from "node:os";
|
|
7635
7611
|
import { pipeline } from "node:stream/promises";
|
|
7636
7612
|
import { promisify as promisify3 } from "node:util";
|
|
7637
7613
|
function modelToOutput(model) {
|
|
@@ -7678,6 +7654,26 @@ function parseModelSource(raw) {
|
|
|
7678
7654
|
`unsupported model source "${raw}"; use hf:<repo>, modelscope:<repo>, https://..., or da:<commitment>`
|
|
7679
7655
|
);
|
|
7680
7656
|
}
|
|
7657
|
+
function parseRevision(raw) {
|
|
7658
|
+
if (raw !== void 0 && !/^[0-9a-f]{40}$/.test(raw)) throw new Error("--revision must be an immutable 40-hex commit");
|
|
7659
|
+
return raw;
|
|
7660
|
+
}
|
|
7661
|
+
async function downloadPinnedHuggingFace(repo, dir, expectedRoot, opts) {
|
|
7662
|
+
const sdk = await import("@flop-labs/sdk");
|
|
7663
|
+
const hub = { revision: opts.revision, token: opts.token };
|
|
7664
|
+
const preflight = await sdk.preflightHuggingFace(repo, hub);
|
|
7665
|
+
if (sdk.normalizeRoot(preflight.contentRoot) !== sdk.normalizeRoot(expectedRoot)) {
|
|
7666
|
+
const found = `${repo} at ${preflight.revision} has content_root 0x${sdk.normalizeRoot(preflight.contentRoot)}, not the registered root ${expectedRoot}`;
|
|
7667
|
+
if (!opts.revision) {
|
|
7668
|
+
throw new Error(
|
|
7669
|
+
`${found}; nothing was downloaded. A repo whose main moved needs the registered --revision (the onboarding identity's sourceRevision), and a verity-v1 model's root cannot be checked from hub metadata, so it downloads only at an explicit --revision and is measured after transfer`
|
|
7670
|
+
);
|
|
7671
|
+
}
|
|
7672
|
+
process.stderr.write(`${found}; a verity-v1 root is checkable only after download, so the bytes are measured once fetched
|
|
7673
|
+
`);
|
|
7674
|
+
}
|
|
7675
|
+
return sdk.fetchHuggingFaceToDir(repo, dir, { ...hub, revision: preflight.revision, files: preflight.files });
|
|
7676
|
+
}
|
|
7681
7677
|
function daHttpSource(endpoint) {
|
|
7682
7678
|
if (!endpoint) return void 0;
|
|
7683
7679
|
const base = endpoint.replace(/\/+$/, "");
|
|
@@ -7877,31 +7873,82 @@ async function submitDaBlob(executable, rpc, blobFile, publisherKey) {
|
|
|
7877
7873
|
}
|
|
7878
7874
|
return parseCustodyResult(stdout2);
|
|
7879
7875
|
}
|
|
7876
|
+
async function daRetrieveChunk(executable, rpc, commitment, j) {
|
|
7877
|
+
const dir = await mkdtemp(resolve5(tmpdir(), "flop-da-chunk-"));
|
|
7878
|
+
const output = resolve5(dir, "chunk.bin");
|
|
7879
|
+
try {
|
|
7880
|
+
let stdout2;
|
|
7881
|
+
try {
|
|
7882
|
+
({ stdout: stdout2 } = await promisify3(execFile3)(executable, [
|
|
7883
|
+
"retrieve",
|
|
7884
|
+
"--chain-rpc",
|
|
7885
|
+
rpc,
|
|
7886
|
+
"--commitment",
|
|
7887
|
+
commitment,
|
|
7888
|
+
"--output",
|
|
7889
|
+
output,
|
|
7890
|
+
"--chunk",
|
|
7891
|
+
String(j),
|
|
7892
|
+
"--json"
|
|
7893
|
+
], { maxBuffer: 1024 * 1024 }));
|
|
7894
|
+
} catch (error) {
|
|
7895
|
+
const stderr = String(error.stderr ?? "");
|
|
7896
|
+
if (/unexpected argument '--chunk'/.test(stderr)) {
|
|
7897
|
+
throw new Error("this da-validator has no `retrieve --chunk`; install one built from flop-core with it to spot-check the DA image");
|
|
7898
|
+
}
|
|
7899
|
+
throw new Error(`da-validator retrieve --chunk ${j} failed: ${stderr.trim() || error.message}`);
|
|
7900
|
+
}
|
|
7901
|
+
const line = lastJsonLine(stdout2);
|
|
7902
|
+
if (!line) throw new Error("da-validator retrieve --chunk returned no JSON receipt");
|
|
7903
|
+
const receipt = JSON.parse(line);
|
|
7904
|
+
const bytes4 = new Uint8Array(await readFile4(output));
|
|
7905
|
+
if (receipt.chunk !== j || receipt.length !== bytes4.length || receipt.sha256 !== createHash2("sha256").update(bytes4).digest("hex")) {
|
|
7906
|
+
throw new Error(`da-validator chunk ${j} receipt does not match the retrieved bytes`);
|
|
7907
|
+
}
|
|
7908
|
+
return { chunk: j, offset: Number(receipt.offset), sizeBytes: Number(receipt.size_bytes), chunkSize: Number(receipt.chunk_size), bytes: bytes4 };
|
|
7909
|
+
} finally {
|
|
7910
|
+
await rm2(dir, { recursive: true, force: true });
|
|
7911
|
+
}
|
|
7912
|
+
}
|
|
7913
|
+
async function readHuggingFaceRange(repo, revision, path5, start, end, size, token) {
|
|
7914
|
+
const url = `https://huggingface.co/${repo}/resolve/${revision}/${path5.split("/").map(encodeURIComponent).join("/")}`;
|
|
7915
|
+
const response = await fetch(url, {
|
|
7916
|
+
headers: { Range: `bytes=${start}-${end}`, ...token ? { Authorization: `Bearer ${token}` } : {} },
|
|
7917
|
+
signal: AbortSignal.timeout(12e4)
|
|
7918
|
+
});
|
|
7919
|
+
if (response.status !== 206 || response.headers.get("content-range") !== `bytes ${start}-${end}/${size}`) {
|
|
7920
|
+
await response.body?.cancel();
|
|
7921
|
+
throw new Error(`Hugging Face did not honor byte range ${start}-${end} of ${path5} at ${revision} (HTTP ${response.status})`);
|
|
7922
|
+
}
|
|
7923
|
+
const bytes4 = new Uint8Array(await response.arrayBuffer());
|
|
7924
|
+
if (bytes4.length !== end - start + 1) throw new Error(`Hugging Face returned ${bytes4.length} bytes for range ${start}-${end} of ${path5}`);
|
|
7925
|
+
return bytes4;
|
|
7926
|
+
}
|
|
7880
7927
|
|
|
7881
7928
|
// src/commands/model-onboard.ts
|
|
7882
7929
|
import { Command as Command29 } from "commander";
|
|
7883
|
-
import { createHash as
|
|
7930
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
7884
7931
|
import { execFile as execFile4 } from "node:child_process";
|
|
7885
7932
|
import { access, lstat as lstat6, mkdir as mkdir10, readFile as readFile7, writeFile as writeFile8 } from "node:fs/promises";
|
|
7886
7933
|
import { basename, isAbsolute, join as join5, resolve as resolve7 } from "node:path";
|
|
7887
7934
|
import { promisify as promisify4 } from "node:util";
|
|
7888
|
-
import { computeChannel as
|
|
7935
|
+
import { computeChannel as computeChannel5 } from "@flop-labs/sdk";
|
|
7889
7936
|
|
|
7890
7937
|
// src/model-onboard-state.ts
|
|
7891
|
-
import { createHash as
|
|
7938
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
7892
7939
|
import { createReadStream } from "node:fs";
|
|
7893
|
-
import { chmod as chmod3, lstat as lstat4, mkdir as mkdir8, open, readFile as readFile5, rename as rename6, rm as
|
|
7940
|
+
import { chmod as chmod3, lstat as lstat4, mkdir as mkdir8, open, readFile as readFile5, rename as rename6, rm as rm3 } from "node:fs/promises";
|
|
7894
7941
|
import { homedir as homedir3, hostname as hostname2 } from "node:os";
|
|
7895
7942
|
import { join as join3, resolve as resolve6 } from "node:path";
|
|
7896
7943
|
import { collectModelFiles } from "@flop-labs/sdk";
|
|
7897
|
-
var digest = (value) =>
|
|
7944
|
+
var digest = (value) => createHash3("sha256").update(value).digest("hex");
|
|
7898
7945
|
function onboardingDirectory(binding, override) {
|
|
7899
7946
|
return override ? resolve6(override) : join3(process.env.XDG_STATE_HOME ?? join3(homedir3(), ".local/state"), "flop/model-onboarding", digest(JSON.stringify(binding)));
|
|
7900
7947
|
}
|
|
7901
7948
|
async function hashFile(path5) {
|
|
7902
7949
|
const stat2 = await lstat4(path5);
|
|
7903
7950
|
if (!stat2.isFile() || stat2.isSymbolicLink()) throw new Error("onboarding artifact must be a regular file");
|
|
7904
|
-
const hash2 =
|
|
7951
|
+
const hash2 = createHash3("sha256");
|
|
7905
7952
|
for await (const chunk of createReadStream(path5)) hash2.update(chunk);
|
|
7906
7953
|
return hash2.digest("hex");
|
|
7907
7954
|
}
|
|
@@ -7925,7 +7972,7 @@ async function openOnboardingState(dir, binding) {
|
|
|
7925
7972
|
const statePath = join3(dir, "state.json");
|
|
7926
7973
|
const close = async () => {
|
|
7927
7974
|
await lock.close();
|
|
7928
|
-
await
|
|
7975
|
+
await rm3(lockPath);
|
|
7929
7976
|
};
|
|
7930
7977
|
try {
|
|
7931
7978
|
await lock.writeFile(JSON.stringify({ pid: process.pid, host: hostname2() }));
|
|
@@ -8066,10 +8113,10 @@ import { computeChannel as computeChannel2, attestation } from "@flop-labs/sdk";
|
|
|
8066
8113
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
8067
8114
|
import { chmod as chmod4, lstat as lstat5, mkdir as mkdir9, open as open2, readFile as readFile6, rename as rename7 } from "node:fs/promises";
|
|
8068
8115
|
import { join as join4 } from "node:path";
|
|
8069
|
-
var bytes = (
|
|
8070
|
-
if (!/^0x(?:[0-9a-f]{2})*$/.test(
|
|
8116
|
+
var bytes = (hex5) => {
|
|
8117
|
+
if (!/^0x(?:[0-9a-f]{2})*$/.test(hex5))
|
|
8071
8118
|
throw new Error("invalid private replay bytes");
|
|
8072
|
-
return new Uint8Array(Buffer.from(
|
|
8119
|
+
return new Uint8Array(Buffer.from(hex5.slice(2), "hex"));
|
|
8073
8120
|
};
|
|
8074
8121
|
var hex = (v) => `0x${Buffer.from(v).toString("hex")}`;
|
|
8075
8122
|
var privateJson = (value) => JSON.stringify(
|
|
@@ -8406,8 +8453,8 @@ async function checkPaidAdmission(api, endpoint, identity, token, completion, pa
|
|
|
8406
8453
|
await anonymous.body?.cancel();
|
|
8407
8454
|
const info = await computeChannel3.fetchMinerSessionInfo(endpoint, { Authorization: `Bearer ${token}` }, AbortSignal.timeout(3e4));
|
|
8408
8455
|
const actual = info.modelIdentity;
|
|
8409
|
-
const
|
|
8410
|
-
if (!actual || actual.registryModel !== identity.registryModel || actual.registryVersion !== identity.registryVersion || actual.backendModelName !== identity.backendModelName ||
|
|
8456
|
+
const hex5 = (v) => `0x${Buffer.from(v).toString("hex")}`;
|
|
8457
|
+
if (!actual || actual.registryModel !== identity.registryModel || actual.registryVersion !== identity.registryVersion || actual.backendModelName !== identity.backendModelName || hex5(actual.modelHash) !== identity.modelHash || hex5(actual.measuredRoot) !== identity.measuredRoot || hex5(actual.decodePolicyHash) !== identity.decodePolicyHash || actual.precision !== identity.precision) throw new Error("paid admission authenticated model identity differs from prepared checkpoint");
|
|
8411
8458
|
if (!info.isReady || !info.calibration?.ready) throw new Error("paid admission miner is not calibrated and serving");
|
|
8412
8459
|
const head = await api.rpc.chain.getFinalizedHead(), view = await api.at(head);
|
|
8413
8460
|
const registration = await view.query.minerStaking.miners(info.minerAddress);
|
|
@@ -8416,7 +8463,7 @@ async function checkPaidAdmission(api, endpoint, identity, token, completion, pa
|
|
|
8416
8463
|
if (key.length !== 32) throw new Error("paid admission requires the runtime AccountId32 encoding");
|
|
8417
8464
|
const result = await api.rpc.state.call(
|
|
8418
8465
|
"TrustRootApi_accepted_calibration_cap",
|
|
8419
|
-
|
|
8466
|
+
hex5(Uint8Array.from([128, ...key])),
|
|
8420
8467
|
head
|
|
8421
8468
|
);
|
|
8422
8469
|
const value = result.toU8a(true);
|
|
@@ -8432,7 +8479,7 @@ async function checkPaidAdmission(api, endpoint, identity, token, completion, pa
|
|
|
8432
8479
|
hash: (await api.rpc.chain.getBlockHash((BigInt(anchor.receipt.number) + 1n).toString())).toHex()
|
|
8433
8480
|
};
|
|
8434
8481
|
const session = await rpc.session(anchor, seed, finalized);
|
|
8435
|
-
if (!payer || session.agent !==
|
|
8482
|
+
if (!payer || session.agent !== hex5(api.createType("AccountId", payer).toU8a()) || session.miner !== hex5(key) || session.model.modelHash !== identity.modelHash || session.model.measuredRoot !== identity.measuredRoot || session.model.precision !== identity.precision || session.model.decodePolicyHash !== identity.decodePolicyHash) throw new Error("historical paid completion differs from onboarding model/payer/miner");
|
|
8436
8483
|
historicalCompletion = await verifyStoredReplay(api, completion);
|
|
8437
8484
|
}
|
|
8438
8485
|
return {
|
|
@@ -8444,6 +8491,215 @@ async function checkPaidAdmission(api, endpoint, identity, token, completion, pa
|
|
|
8444
8491
|
};
|
|
8445
8492
|
}
|
|
8446
8493
|
|
|
8494
|
+
// src/throughput-ceiling.ts
|
|
8495
|
+
import { computeChannel as computeChannel4 } from "@flop-labs/sdk";
|
|
8496
|
+
function parseMinTokensPerSecond(raw) {
|
|
8497
|
+
const text4 = raw.trim();
|
|
8498
|
+
if (!/^\d+(?:\.\d{1,6})?$/.test(text4) || !/[1-9]/.test(text4)) {
|
|
8499
|
+
throw new Error("--min-tokens-per-second must be a positive decimal with at most 6 fractional digits");
|
|
8500
|
+
}
|
|
8501
|
+
return text4;
|
|
8502
|
+
}
|
|
8503
|
+
function fraction(minimum) {
|
|
8504
|
+
const [whole, frac = ""] = minimum.split(".");
|
|
8505
|
+
return [BigInt(whole + frac), 10n ** BigInt(frac.length)];
|
|
8506
|
+
}
|
|
8507
|
+
var FLOPS_PER_GN = 1000000000n;
|
|
8508
|
+
function perTokenWork(checkpoint) {
|
|
8509
|
+
const hybrid = computeChannel4.isDenseGdnArch(checkpoint.arch);
|
|
8510
|
+
return {
|
|
8511
|
+
flopsPerToken: hybrid ? checkpoint.gnPerCompletionToken * FLOPS_PER_GN : 2n * checkpoint.activeParams,
|
|
8512
|
+
gnPerToken: checkpoint.gnPerCompletionToken
|
|
8513
|
+
};
|
|
8514
|
+
}
|
|
8515
|
+
function centiTokensPerSecond(capGnPerBlock, blockTimeMs, flopsPerToken) {
|
|
8516
|
+
if (flopsPerToken <= 0n || blockTimeMs <= 0n) throw new RangeError("flopsPerToken and blockTimeMs must be positive");
|
|
8517
|
+
return capGnPerBlock * FLOPS_PER_GN * 1000n * 100n / (blockTimeMs * flopsPerToken);
|
|
8518
|
+
}
|
|
8519
|
+
function exactGn(flops) {
|
|
8520
|
+
const frac = (flops % FLOPS_PER_GN).toString().padStart(9, "0").replace(/0+$/, "");
|
|
8521
|
+
return `${flops / FLOPS_PER_GN}${frac ? `.${frac}` : ""}`;
|
|
8522
|
+
}
|
|
8523
|
+
function decimal(centi) {
|
|
8524
|
+
return `${centi / 100n}.${(centi % 100n).toString().padStart(2, "0")}`;
|
|
8525
|
+
}
|
|
8526
|
+
function unknown(work, minimum, reason3) {
|
|
8527
|
+
return {
|
|
8528
|
+
status: "unknown",
|
|
8529
|
+
tokensPerSecond: `unknown (${reason3})`,
|
|
8530
|
+
gnPerToken: work ? exactGn(work.flopsPerToken) : null,
|
|
8531
|
+
flopsPerToken: work?.flopsPerToken.toString() ?? null,
|
|
8532
|
+
capGnPerBlock: null,
|
|
8533
|
+
capSource: null,
|
|
8534
|
+
miner: null,
|
|
8535
|
+
bestTokensPerSecond: null,
|
|
8536
|
+
bestCapGnPerBlock: null,
|
|
8537
|
+
blockTimeMs: null,
|
|
8538
|
+
calibratedMiners: null,
|
|
8539
|
+
finalizedBlock: null,
|
|
8540
|
+
minTokensPerSecond: minimum === void 0 ? null : Number(minimum),
|
|
8541
|
+
meetsMinimum: minimum === void 0 ? null : false,
|
|
8542
|
+
reason: reason3
|
|
8543
|
+
};
|
|
8544
|
+
}
|
|
8545
|
+
function throughputCeiling(work, chain, minimum) {
|
|
8546
|
+
if (typeof work === "string") return unknown(null, minimum, work);
|
|
8547
|
+
if (work.flopsPerToken <= 0n) return unknown(work, minimum, "metering reports no work per completion token");
|
|
8548
|
+
if (chain === null) return unknown(work, minimum, "no chain connection");
|
|
8549
|
+
if (typeof chain === "string") return unknown(work, minimum, chain);
|
|
8550
|
+
if (chain.capGnPerBlock === null) {
|
|
8551
|
+
return unknown(work, minimum, chain.miner === null ? "no miner holds an accepted calibration cap at finality" : `miner ${chain.miner} holds no accepted calibration cap at finality`);
|
|
8552
|
+
}
|
|
8553
|
+
const flops = work.flopsPerToken;
|
|
8554
|
+
const centi = centiTokensPerSecond(chain.capGnPerBlock, chain.blockTimeMs, flops);
|
|
8555
|
+
let meets = null;
|
|
8556
|
+
if (minimum !== void 0) {
|
|
8557
|
+
const [num, den] = fraction(minimum);
|
|
8558
|
+
meets = chain.capGnPerBlock * FLOPS_PER_GN * 1000n * den >= num * chain.blockTimeMs * flops;
|
|
8559
|
+
}
|
|
8560
|
+
return {
|
|
8561
|
+
status: "known",
|
|
8562
|
+
tokensPerSecond: decimal(centi),
|
|
8563
|
+
gnPerToken: exactGn(flops),
|
|
8564
|
+
flopsPerToken: flops.toString(),
|
|
8565
|
+
capGnPerBlock: chain.capGnPerBlock.toString(),
|
|
8566
|
+
capSource: chain.capSource,
|
|
8567
|
+
miner: chain.miner,
|
|
8568
|
+
bestTokensPerSecond: chain.bestCapGnPerBlock === null ? null : decimal(centiTokensPerSecond(chain.bestCapGnPerBlock, chain.blockTimeMs, flops)),
|
|
8569
|
+
bestCapGnPerBlock: chain.bestCapGnPerBlock?.toString() ?? null,
|
|
8570
|
+
blockTimeMs: chain.blockTimeMs.toString(),
|
|
8571
|
+
calibratedMiners: chain.calibratedMiners,
|
|
8572
|
+
finalizedBlock: chain.finalizedBlock.toString(),
|
|
8573
|
+
minTokensPerSecond: minimum === void 0 ? null : Number(minimum),
|
|
8574
|
+
meetsMinimum: meets
|
|
8575
|
+
};
|
|
8576
|
+
}
|
|
8577
|
+
async function readChainCapInputs(api, miner) {
|
|
8578
|
+
const block = api.consts?.babe?.expectedBlockTime;
|
|
8579
|
+
const lease = api.consts?.hwCalibration?.calibrationLeaseBlocks;
|
|
8580
|
+
if (block == null) throw new Error("runtime constant babe.expectedBlockTime is missing");
|
|
8581
|
+
if (lease == null) throw new Error("runtime constant hwCalibration.calibrationLeaseBlocks is missing");
|
|
8582
|
+
const head = await api.rpc.chain.getFinalizedHead();
|
|
8583
|
+
const [view, header] = await Promise.all([api.at(head), api.rpc.chain.getHeader(head)]);
|
|
8584
|
+
const caps = view.query?.hwCalibration?.benchmarkBurstCaps;
|
|
8585
|
+
if (!caps?.entries) throw new Error("runtime has no hwCalibration.benchmarkBurstCaps");
|
|
8586
|
+
const now = BigInt(header.number.toString());
|
|
8587
|
+
const leaseBlocks = BigInt(lease.toString());
|
|
8588
|
+
const live = (raw) => {
|
|
8589
|
+
if (!raw?.isSome) return null;
|
|
8590
|
+
const cap = raw.unwrap();
|
|
8591
|
+
const acceptedAt = BigInt((cap.acceptedAt ?? cap.accepted_at).toString());
|
|
8592
|
+
const value = BigInt((cap.capGflopsPerSec ?? cap.cap_gflops_per_sec).toString());
|
|
8593
|
+
return now - acceptedAt >= leaseBlocks || value <= 0n ? null : value;
|
|
8594
|
+
};
|
|
8595
|
+
const entries = await caps.entries();
|
|
8596
|
+
const values = entries.map(([, raw]) => live(raw)).filter((value) => value !== null).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
8597
|
+
const mid = Math.floor(values.length / 2);
|
|
8598
|
+
const median = values.length === 0 ? null : values.length % 2 === 1 ? values[mid] : (values[mid - 1] + values[mid]) / 2n;
|
|
8599
|
+
const capGnPerBlock = miner === void 0 ? median : live(await caps(miner));
|
|
8600
|
+
return {
|
|
8601
|
+
capGnPerBlock,
|
|
8602
|
+
capSource: miner === void 0 ? "chain-median" : "miner",
|
|
8603
|
+
miner: miner ?? null,
|
|
8604
|
+
bestCapGnPerBlock: values[values.length - 1] ?? null,
|
|
8605
|
+
blockTimeMs: BigInt(block.toString()),
|
|
8606
|
+
calibratedMiners: values.length,
|
|
8607
|
+
finalizedBlock: now
|
|
8608
|
+
};
|
|
8609
|
+
}
|
|
8610
|
+
var CEILING_READ_TIMEOUT_MS = 15e3;
|
|
8611
|
+
async function chainCapInputsOrReason(api, miner, timeoutMs = CEILING_READ_TIMEOUT_MS) {
|
|
8612
|
+
let timer;
|
|
8613
|
+
try {
|
|
8614
|
+
return await Promise.race([
|
|
8615
|
+
readChainCapInputs(api, miner),
|
|
8616
|
+
new Promise((_, reject) => {
|
|
8617
|
+
timer = setTimeout(() => reject(new Error(`timed out after ${timeoutMs} ms`)), timeoutMs);
|
|
8618
|
+
})
|
|
8619
|
+
]);
|
|
8620
|
+
} catch (error) {
|
|
8621
|
+
return `chain read failed: ${error.message}`;
|
|
8622
|
+
} finally {
|
|
8623
|
+
clearTimeout(timer);
|
|
8624
|
+
}
|
|
8625
|
+
}
|
|
8626
|
+
function ceilingWarning(ceiling) {
|
|
8627
|
+
if (ceiling.status === "unknown") return `warning: paid throughput ceiling ${ceiling.tokensPerSecond}
|
|
8628
|
+
`;
|
|
8629
|
+
const at = ceiling.capSource === "miner" ? `miner ${ceiling.miner}'s cap` : "the median live cap";
|
|
8630
|
+
const detail = `${ceiling.gnPerToken} G_n/token at ${at} ${ceiling.capGnPerBlock} G_n/block; best of ${ceiling.calibratedMiners} calibrated miner(s) ${ceiling.bestTokensPerSecond ?? "n/a"} tok/s (not gated)`;
|
|
8631
|
+
const tail = "per channel, completion tokens only; prompt and attention G_n lower it";
|
|
8632
|
+
if (ceiling.meetsMinimum === false) {
|
|
8633
|
+
return `warning: paid throughput ceiling ${ceiling.tokensPerSecond} tok/s (${detail}) is below --min-tokens-per-second ${ceiling.minTokensPerSecond}; ${tail}
|
|
8634
|
+
`;
|
|
8635
|
+
}
|
|
8636
|
+
return `paid throughput ceiling: ${ceiling.tokensPerSecond} tok/s (${detail}); ${tail}
|
|
8637
|
+
`;
|
|
8638
|
+
}
|
|
8639
|
+
function enforceCeiling(ceiling) {
|
|
8640
|
+
if (ceiling.meetsMinimum !== false) return;
|
|
8641
|
+
throw new Error(
|
|
8642
|
+
ceiling.status === "unknown" ? `paid throughput ceiling is ${ceiling.tokensPerSecond}; --min-tokens-per-second ${ceiling.minTokensPerSecond} cannot be shown to hold` : `paid throughput ceiling ${ceiling.tokensPerSecond} tok/s is below --min-tokens-per-second ${ceiling.minTokensPerSecond}`
|
|
8643
|
+
);
|
|
8644
|
+
}
|
|
8645
|
+
function snakeCeiling(ceiling) {
|
|
8646
|
+
return Object.fromEntries(Object.entries(ceiling).map(([k, v]) => [k.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`), v]));
|
|
8647
|
+
}
|
|
8648
|
+
|
|
8649
|
+
// src/model-da-spot-check.ts
|
|
8650
|
+
import { createHash as createHash4, randomInt } from "node:crypto";
|
|
8651
|
+
var SPOT_CHECK_CHUNKS = 2;
|
|
8652
|
+
var BLOCK_SIZE = 4096;
|
|
8653
|
+
function randomChunks(n, k) {
|
|
8654
|
+
const picked = /* @__PURE__ */ new Set();
|
|
8655
|
+
while (picked.size < Math.min(n, k)) picked.add(randomInt(n));
|
|
8656
|
+
return [...picked].sort((a, b) => a - b);
|
|
8657
|
+
}
|
|
8658
|
+
async function spotCheckImage(files, imageSize, io) {
|
|
8659
|
+
const ordered = [...files].sort((a, b) => Buffer.from(a.path).compare(Buffer.from(b.path)));
|
|
8660
|
+
let cursor = 0;
|
|
8661
|
+
const layout = ordered.map((file) => {
|
|
8662
|
+
const start = cursor;
|
|
8663
|
+
cursor += Math.ceil(file.size / BLOCK_SIZE) * BLOCK_SIZE;
|
|
8664
|
+
return { file, start, dataEnd: start + file.size, end: cursor };
|
|
8665
|
+
});
|
|
8666
|
+
if (cursor !== imageSize) throw new Error(`file table packs to ${cursor} bytes, not the bound image's ${imageSize}`);
|
|
8667
|
+
const n = Math.max(1, Math.ceil(imageSize / io.chunkSize));
|
|
8668
|
+
const chunks = (io.pick ?? randomChunks)(n, SPOT_CHECK_CHUNKS);
|
|
8669
|
+
for (const j of chunks) {
|
|
8670
|
+
const chunkStart = j * io.chunkSize;
|
|
8671
|
+
const chunkEnd = Math.min(chunkStart + io.chunkSize, imageSize);
|
|
8672
|
+
const got = await io.readChunk(j);
|
|
8673
|
+
const expectedChunkSize = n === 1 ? imageSize : io.chunkSize;
|
|
8674
|
+
if (got.chunkSize !== expectedChunkSize || got.sizeBytes !== imageSize || got.offset !== chunkStart || got.bytes.length !== chunkEnd - chunkStart) {
|
|
8675
|
+
throw new Error(`DA chunk ${j} layout differs from the ${io.chunkSize}-byte chunking of a ${imageSize}-byte image`);
|
|
8676
|
+
}
|
|
8677
|
+
const at = (offset) => offset - chunkStart;
|
|
8678
|
+
for (const { file, start, dataEnd, end } of layout) {
|
|
8679
|
+
if (end <= chunkStart || start >= chunkEnd) continue;
|
|
8680
|
+
const from = Math.max(start, chunkStart), to = Math.min(dataEnd, chunkEnd);
|
|
8681
|
+
if (from < to) {
|
|
8682
|
+
const slice = got.bytes.subarray(at(from), at(to));
|
|
8683
|
+
if (start >= chunkStart && dataEnd <= chunkEnd) {
|
|
8684
|
+
if (createHash4("sha256").update(slice).digest("hex") !== file.sha256.toLowerCase().replace(/^0x/, "")) {
|
|
8685
|
+
throw new Error(`DA spot check: chunk ${j} of the bound image does not hold ${file.path} (sha256 differs)`);
|
|
8686
|
+
}
|
|
8687
|
+
} else {
|
|
8688
|
+
const hub = await io.readHubRange(file.path, from - start, to - start - 1, file.size);
|
|
8689
|
+
if (!Buffer.from(hub).equals(Buffer.from(slice))) {
|
|
8690
|
+
throw new Error(`DA spot check: chunk ${j} of the bound image differs from ${file.path} at the hub revision`);
|
|
8691
|
+
}
|
|
8692
|
+
}
|
|
8693
|
+
}
|
|
8694
|
+
const padFrom = Math.max(dataEnd, chunkStart), padTo = Math.min(end, chunkEnd);
|
|
8695
|
+
if (padFrom < padTo && got.bytes.subarray(at(padFrom), at(padTo)).some((b) => b !== 0)) {
|
|
8696
|
+
throw new Error(`DA spot check: chunk ${j} of the bound image has nonzero padding after ${file.path}`);
|
|
8697
|
+
}
|
|
8698
|
+
}
|
|
8699
|
+
}
|
|
8700
|
+
return { chunks, of: n };
|
|
8701
|
+
}
|
|
8702
|
+
|
|
8447
8703
|
// src/commands/model-onboard.ts
|
|
8448
8704
|
var run = promisify4(execFile4);
|
|
8449
8705
|
var PRECISIONS = ["FP32", "FP16", "BF16", "INT8", "INT4", "Mixed"];
|
|
@@ -8455,7 +8711,7 @@ async function requireHybridApproval(api, arch) {
|
|
|
8455
8711
|
async function requireHybridApprovalAt(view, arch) {
|
|
8456
8712
|
const query = view.query.meteringVerifier?.hybridProfiles;
|
|
8457
8713
|
if (!query) throw new Error("runtime has no hybrid proof profile approval; deploy the reviewed runtime first");
|
|
8458
|
-
const hash2 = `0x${Buffer.from(
|
|
8714
|
+
const hash2 = `0x${Buffer.from(computeChannel5.meteringArchHash(arch)).toString("hex")}`;
|
|
8459
8715
|
const value = await query(hash2);
|
|
8460
8716
|
if (!value || value.isNone) throw new Error(`hybrid proof profile ${hash2} is not approved`);
|
|
8461
8717
|
const profile = value.unwrap();
|
|
@@ -8522,7 +8778,7 @@ function resolveParamsBillion(checkpoint, override) {
|
|
|
8522
8778
|
throw new Error(`--params-billion must be a positive integer (got ${override})`);
|
|
8523
8779
|
}
|
|
8524
8780
|
if (override === registered) return override;
|
|
8525
|
-
if (
|
|
8781
|
+
if (computeChannel5.isDenseGdnArch(checkpoint.arch)) throw new Error("--params-billion must match the complete checkpoint census");
|
|
8526
8782
|
if (Math.abs(override - derived) > 0.05 * derived) {
|
|
8527
8783
|
throw new Error(
|
|
8528
8784
|
`--params-billion ${override} disagrees with config.json's ${derived.toFixed(2)}B parameters by more than 5%`
|
|
@@ -8552,9 +8808,9 @@ function onboardTeePlatforms(soft, raw) {
|
|
|
8552
8808
|
return normalizeTeePlatforms(raw);
|
|
8553
8809
|
}
|
|
8554
8810
|
function normalizeHash(raw, name) {
|
|
8555
|
-
const
|
|
8556
|
-
if (!/^[0-9a-f]{64}$/.test(
|
|
8557
|
-
return `0x${
|
|
8811
|
+
const hex5 = raw.trim().replace(/^0x/i, "").toLowerCase();
|
|
8812
|
+
if (!/^[0-9a-f]{64}$/.test(hex5)) throw new Error(`${name} must be 32-byte hex`);
|
|
8813
|
+
return `0x${hex5}`;
|
|
8558
8814
|
}
|
|
8559
8815
|
function identityFromPack(registryModel2, registryVersion, backendModelName, precision, decodePolicyHash, contentRoot, checkpoint, sourceRevision) {
|
|
8560
8816
|
const measuredRoot = normalizeHash(contentRoot, "modelpack content_root");
|
|
@@ -8566,7 +8822,7 @@ function identityFromPack(registryModel2, registryVersion, backendModelName, pre
|
|
|
8566
8822
|
decodePolicyHash: normalizeHash(decodePolicyHash, "decode policy hash"),
|
|
8567
8823
|
measuredRoot,
|
|
8568
8824
|
backendModelName,
|
|
8569
|
-
archHash: `0x${Buffer.from(
|
|
8825
|
+
archHash: `0x${Buffer.from(computeChannel5.meteringArchHash(checkpoint.arch)).toString("hex")}`,
|
|
8570
8826
|
quantization: catalogQuantization(checkpoint),
|
|
8571
8827
|
gnPerPromptToken: checkpoint.gnPerPromptToken.toString(),
|
|
8572
8828
|
gnPerCompletionToken: checkpoint.gnPerCompletionToken.toString(),
|
|
@@ -8719,7 +8975,7 @@ function boundCensus(stdout2, configHash) {
|
|
|
8719
8975
|
return { total: BigInt(result.total_params), sourceFiles: result.source_files };
|
|
8720
8976
|
}
|
|
8721
8977
|
async function validateCapture(endpoint, identity, checkpoint, modelDir) {
|
|
8722
|
-
const hybrid =
|
|
8978
|
+
const hybrid = computeChannel5.isDenseGdnArch(checkpoint.arch);
|
|
8723
8979
|
const script = join5(repoRoot(), "infra/miner/vllm-toploc", hybrid ? "validate_hybrid_capture.py" : "validate_capture.py");
|
|
8724
8980
|
try {
|
|
8725
8981
|
await run("uv", ["run", "--no-project", "python", script], {
|
|
@@ -8747,8 +9003,8 @@ function optionValue(value) {
|
|
|
8747
9003
|
}
|
|
8748
9004
|
function codecText(value) {
|
|
8749
9005
|
if (value?.toUtf8) return value.toUtf8();
|
|
8750
|
-
const
|
|
8751
|
-
return /^0x(?:[0-9a-f]{2})+$/i.test(
|
|
9006
|
+
const text4 = value?.toString?.() ?? String(value ?? "");
|
|
9007
|
+
return /^0x(?:[0-9a-f]{2})+$/i.test(text4) ? Buffer.from(text4.slice(2), "hex").toString("utf8") : text4;
|
|
8752
9008
|
}
|
|
8753
9009
|
function codecHash(value) {
|
|
8754
9010
|
return normalizeHash(value?.toHex?.() ?? value?.toString?.() ?? String(value), "on-chain hash");
|
|
@@ -8797,7 +9053,7 @@ async function requireApprovalEvidence(api, modelHash, precision, signer, imageS
|
|
|
8797
9053
|
if (!Array.from(members).some((member) => sameAccount(member, signer))) throw new Error("approval signer is not an active registrar");
|
|
8798
9054
|
if (sameAccount(model.publisher, signer)) throw new Error("registrar cannot approve its own published model");
|
|
8799
9055
|
}
|
|
8800
|
-
const requiresHybrid =
|
|
9056
|
+
const requiresHybrid = computeChannel5.isDenseGdnArch(arch);
|
|
8801
9057
|
if (!root) {
|
|
8802
9058
|
const requirements = optionValue(await registry.admissionRequirements(modelHash));
|
|
8803
9059
|
if (!requirements) throw new Error("approval requires model admission requirements");
|
|
@@ -8811,7 +9067,7 @@ async function requireApprovalEvidence(api, modelHash, precision, signer, imageS
|
|
|
8811
9067
|
}
|
|
8812
9068
|
const archHash = optionValue(rawArch);
|
|
8813
9069
|
if (!archHash) throw new Error("approval requires a model architecture hash");
|
|
8814
|
-
const expectedArchHash = `0x${Buffer.from(
|
|
9070
|
+
const expectedArchHash = `0x${Buffer.from(computeChannel5.meteringArchHash(arch)).toString("hex")}`;
|
|
8815
9071
|
if (codecHash(archHash) !== expectedArchHash) throw new Error("approval architecture hash differs from the checkpoint");
|
|
8816
9072
|
const policies = optionValue(rawPolicies);
|
|
8817
9073
|
if (!policies || policies.length === 0) throw new Error("approval requires a nonempty decode-policy allowlist");
|
|
@@ -8829,6 +9085,7 @@ async function requireApprovalEvidence(api, modelHash, precision, signer, imageS
|
|
|
8829
9085
|
const leaseEnd = metadata.leaseEnd ?? metadata.lease_end;
|
|
8830
9086
|
const header = await api.rpc.chain.getHeader(head);
|
|
8831
9087
|
if (leaseEnd === null || leaseEnd === void 0 || BigInt(leaseEnd) <= BigInt(header.number.toString())) throw new Error("approval requires a live DA lease");
|
|
9088
|
+
let boundTable = null;
|
|
8832
9089
|
if (files) {
|
|
8833
9090
|
const sdk = await import("@flop-labs/sdk");
|
|
8834
9091
|
const { blake2AsHex: blake2AsHex2 } = await import("@polkadot/util-crypto");
|
|
@@ -8837,7 +9094,15 @@ async function requireApprovalEvidence(api, modelHash, precision, signer, imageS
|
|
|
8837
9094
|
throw new Error("approval file table does not hash to model_hash");
|
|
8838
9095
|
}
|
|
8839
9096
|
const tableCommitment = normalizeHash(blake2AsHex2(table2, 256), "file table commitment");
|
|
8840
|
-
|
|
9097
|
+
boundTable = tableCommitment;
|
|
9098
|
+
if (!registry.modelMirrors) throw new Error("cannot read ModelMirrors at finality: the runtime has no modelRegistry.modelMirrors");
|
|
9099
|
+
let rawMirrors;
|
|
9100
|
+
try {
|
|
9101
|
+
rawMirrors = await registry.modelMirrors(modelHash);
|
|
9102
|
+
} catch (error) {
|
|
9103
|
+
throw new Error(`cannot read ModelMirrors at finality: ${error.message}`);
|
|
9104
|
+
}
|
|
9105
|
+
const mirrors = rawMirrors?.isSome ? sdk.decodeModelSources(rawMirrors.unwrap().toJSON()) : [];
|
|
8841
9106
|
if (!mirrors.some((m) => m.kind === "da" && normalizeHash(String(m.commitment), "DA mirror commitment") === tableCommitment)) {
|
|
8842
9107
|
throw new Error("approval requires the DA mirror to name the model's file table");
|
|
8843
9108
|
}
|
|
@@ -8858,7 +9123,7 @@ async function requireApprovalEvidence(api, modelHash, precision, signer, imageS
|
|
|
8858
9123
|
}
|
|
8859
9124
|
const status = String(model.status);
|
|
8860
9125
|
if (status !== "Pending" && status !== "Verified") throw new Error(`model cannot be approved in ${status} status`);
|
|
8861
|
-
return { approved: status === "Verified" };
|
|
9126
|
+
return { approved: status === "Verified", weights: ref.commitment, table: boundTable };
|
|
8862
9127
|
}
|
|
8863
9128
|
function decodeDataRef(value) {
|
|
8864
9129
|
const ref = optionValue(value);
|
|
@@ -8923,12 +9188,19 @@ function missingMirrors(registered, desired) {
|
|
|
8923
9188
|
return !registered.some((actual) => sourceMatches(actual, comparable));
|
|
8924
9189
|
});
|
|
8925
9190
|
}
|
|
8926
|
-
async function verifiedHuggingFaceMirror(repo,
|
|
9191
|
+
async function verifiedHuggingFaceMirror(repo, contentRoot, hub) {
|
|
8927
9192
|
if (!/^[^/\s]+\/[^/\s]+$/.test(repo)) throw new Error(`--hf-repo must be <owner>/<model> (got ${repo})`);
|
|
8928
|
-
const
|
|
9193
|
+
const bare = (hex5) => hex5.toLowerCase().replace(/^0x/, "");
|
|
9194
|
+
const table2 = await hub.preflight(repo);
|
|
9195
|
+
if (bare(table2.contentRoot) !== bare(contentRoot)) {
|
|
9196
|
+
throw new Error(
|
|
9197
|
+
`Hugging Face ${repo} at ${table2.revision} has content_root 0x${bare(table2.contentRoot)}, not the measured root ${contentRoot}; nothing was downloaded. Pass the --revision the local checkpoint came from, onboard from hf:${repo} so the registered files are the repo's, or omit --hf-repo`
|
|
9198
|
+
);
|
|
9199
|
+
}
|
|
9200
|
+
const result = await hub.verify({ kind: "huggingface", repo }, contentRoot, table2.revision);
|
|
8929
9201
|
if (!result.ok) {
|
|
8930
9202
|
throw new Error(
|
|
8931
|
-
`Hugging Face ${repo} does not reproduce the measured root ${
|
|
9203
|
+
`Hugging Face ${repo} does not reproduce the measured root ${contentRoot} (${result.reason ?? "no match"}); onboard from hf:${repo} so the registered files are the repo's, or omit --hf-repo`
|
|
8932
9204
|
);
|
|
8933
9205
|
}
|
|
8934
9206
|
return { mirror: { kind: "huggingface", repo }, revision: result.revision ?? null };
|
|
@@ -8972,7 +9244,14 @@ function modelOnboardCmd() {
|
|
|
8972
9244
|
).option("--registry-model <name>", "Registry model name").option("--registry-version <version>", "Registry version", "1").option("--backend-model-name <name>", "Exact model name returned by the serving backend").option("--da-validator <path>", "da-validator executable", "da-validator").option(
|
|
8973
9245
|
"--hf-repo <owner/model>",
|
|
8974
9246
|
"Hugging Face repo a local --source came from; registered as a mirror only if it reproduces the measured root"
|
|
8975
|
-
).option(
|
|
9247
|
+
).option(
|
|
9248
|
+
"--revision <sha>",
|
|
9249
|
+
"Immutable Hugging Face commit (of --source, or of --hf-repo for a local source); first resolution is retained when omitted"
|
|
9250
|
+
).option("--state-dir <path>", "Private resumable workspace (defaults to XDG_STATE_HOME/flop/model-onboarding)").option("--registrar-keystore <path>", "Independent registrar keystore for registration pins and approval").option("--registrar-password-env <name>", "Environment variable containing the registrar keystore password").option("--approve", "Approve after rechecking all admission evidence at one finalized head").option("--paid-admission", "Check paid devnet prerequisites after registration; fail closed at an unavailable independent audit gate").option("--session-endpoint <url>", "Authenticated provisioned miner session endpoint for paid admission").option("--token <token>", "Hugging Face bearer token (prefer HF_TOKEN)").option(
|
|
9251
|
+
"--min-tokens-per-second <n>",
|
|
9252
|
+
"Fail before any chain write unless the paid completion-token ceiling per channel at the median live calibration cap is at least n; an unknown ceiling fails",
|
|
9253
|
+
parseMinTokensPerSecond
|
|
9254
|
+
).option("--miner <address>", "Compute the ceiling at this miner's accepted calibration cap instead of the median live one; the best is reported, never gated").option("--json", "Output the identity block as JSON").action(async (opts, command) => {
|
|
8976
9255
|
let client;
|
|
8977
9256
|
let registrar;
|
|
8978
9257
|
let workDir;
|
|
@@ -8989,15 +9268,12 @@ function modelOnboardCmd() {
|
|
|
8989
9268
|
const local = await exists(opts.source);
|
|
8990
9269
|
if (local && opts.download) throw new Error("--download is for a Hugging Face source; a local source is always packed");
|
|
8991
9270
|
const metadataOnly = !local && !opts.download;
|
|
8992
|
-
if (metadataOnly && opts.approve) {
|
|
8993
|
-
throw new Error("--approve needs the model's DA backup (packed image and file table); rerun with --download to upload it in the same run");
|
|
8994
|
-
}
|
|
8995
9271
|
const binding = {
|
|
8996
9272
|
source: opts.source,
|
|
8997
9273
|
revision: opts.revision ?? null,
|
|
8998
9274
|
network,
|
|
8999
|
-
rpcHash:
|
|
9000
|
-
captureEndpointHash:
|
|
9275
|
+
rpcHash: createHash5("sha256").update(getRpc(command)).digest("hex"),
|
|
9276
|
+
captureEndpointHash: createHash5("sha256").update(opts.captureEndpoint).digest("hex"),
|
|
9001
9277
|
registryModel: opts.registryModel ?? null,
|
|
9002
9278
|
registryVersion: opts.registryVersion,
|
|
9003
9279
|
backendModelName: opts.backendModelName ?? null,
|
|
@@ -9008,7 +9284,7 @@ function modelOnboardCmd() {
|
|
|
9008
9284
|
hfRepo: opts.hfRepo ?? null,
|
|
9009
9285
|
packContract: metadataOnly ? "content-v1-metadata" : "modelpack-v1-strict",
|
|
9010
9286
|
paidAdmission: opts.paidAdmission === true,
|
|
9011
|
-
sessionEndpointHash: opts.sessionEndpoint ?
|
|
9287
|
+
sessionEndpointHash: opts.sessionEndpoint ? createHash5("sha256").update(opts.sessionEndpoint).digest("hex") : null
|
|
9012
9288
|
};
|
|
9013
9289
|
workDir = onboardingDirectory(binding, opts.stateDir);
|
|
9014
9290
|
job = await openOnboardingState(workDir, binding);
|
|
@@ -9022,17 +9298,32 @@ function modelOnboardCmd() {
|
|
|
9022
9298
|
preflight = await sdk2.preflightHuggingFace(hubRepo, { revision: job.state.sourceRevision ?? opts.revision, token: opts.token ?? process.env.HF_TOKEN });
|
|
9023
9299
|
if (!preflight.configJson) throw new Error("checkpoint has no root config.json; a workload-specific ingestion contract is required");
|
|
9024
9300
|
job.state.sourceRevision = preflight.revision;
|
|
9025
|
-
job.state.configHash =
|
|
9301
|
+
job.state.configHash = createHash5("sha256").update(preflight.configJson).digest("hex");
|
|
9026
9302
|
job.state.phase = "preflight";
|
|
9027
9303
|
await job.save();
|
|
9028
9304
|
process.stderr.write(`content_root ${preflight.contentRoot}: ${preflight.files.length} files, image ${preflight.imageSize} bytes, ${preflight.daChunks} DA chunk(s) at ${preflight.revision}
|
|
9029
9305
|
`);
|
|
9030
|
-
const candidate =
|
|
9031
|
-
if (
|
|
9306
|
+
const candidate = computeChannel5.parseMeteringConfig(preflight.configJson, preflight.hfQuantConfigJson);
|
|
9307
|
+
if (computeChannel5.isDenseGdnArch(candidate.arch)) {
|
|
9032
9308
|
client = await createClient(getRpc(command), getSigner(command));
|
|
9033
9309
|
await requireHybridApproval(client.api, candidate.arch);
|
|
9034
9310
|
}
|
|
9035
|
-
} else if (opts.revision) throw new Error("--revision is only for a Hugging Face source");
|
|
9311
|
+
} else if (opts.revision && !opts.hfRepo) throw new Error("--revision is only for a Hugging Face source or a local source's --hf-repo");
|
|
9312
|
+
const checkCeiling = async (load) => {
|
|
9313
|
+
let work;
|
|
9314
|
+
try {
|
|
9315
|
+
work = perTokenWork(computeChannel5.parseMeteringConfig(...await load()));
|
|
9316
|
+
} catch (error) {
|
|
9317
|
+
work = `no metering arch: ${error.message}`;
|
|
9318
|
+
}
|
|
9319
|
+
client ??= await createClient(getRpc(command), getSigner(command));
|
|
9320
|
+
const result = throughputCeiling(work, await chainCapInputsOrReason(client.api, opts.miner), opts.minTokensPerSecond);
|
|
9321
|
+
process.stderr.write(ceilingWarning(result));
|
|
9322
|
+
enforceCeiling(result);
|
|
9323
|
+
return result;
|
|
9324
|
+
};
|
|
9325
|
+
let ceiling;
|
|
9326
|
+
if (preflight) ceiling = await checkCeiling(async () => [preflight.configJson, preflight.hfQuantConfigJson ?? null]);
|
|
9036
9327
|
let source;
|
|
9037
9328
|
if (metadataOnly) {
|
|
9038
9329
|
const modelDir = join5(workDir, "metadata");
|
|
@@ -9050,14 +9341,15 @@ function modelOnboardCmd() {
|
|
|
9050
9341
|
} else {
|
|
9051
9342
|
source = await materializeSource(opts.source, sourceDir, opts.token, job.state.sourceRevision, join5(workDir, "download.json"), preflight?.files);
|
|
9052
9343
|
}
|
|
9344
|
+
ceiling ??= await checkCeiling(async () => [await readFile7(join5(source.modelDir, "config.json")), await readHfQuantConfig(source.modelDir)]);
|
|
9053
9345
|
let checkpoint;
|
|
9054
9346
|
let parameterFiles;
|
|
9055
9347
|
try {
|
|
9056
9348
|
const raw = await readFile7(join5(source.modelDir, "config.json"), "utf8");
|
|
9057
|
-
if (job.state.configHash &&
|
|
9058
|
-
checkpoint =
|
|
9349
|
+
if (job.state.configHash && createHash5("sha256").update(raw).digest("hex") !== job.state.configHash) throw new Error("downloaded config differs from preflight");
|
|
9350
|
+
checkpoint = computeChannel5.parseMeteringConfig(Buffer.from(raw), await readHfQuantConfig(source.modelDir));
|
|
9059
9351
|
if (checkpoint.totalParams === null) {
|
|
9060
|
-
const configHash =
|
|
9352
|
+
const configHash = createHash5("sha256").update(raw).digest("hex");
|
|
9061
9353
|
const census = metadataOnly ? await censusHeaders(workDir, preflight, await (await import("@flop-labs/sdk")).readSafetensorsHeaders(
|
|
9062
9354
|
hubRepo,
|
|
9063
9355
|
preflight.revision,
|
|
@@ -9104,11 +9396,11 @@ function modelOnboardCmd() {
|
|
|
9104
9396
|
let { mirror, revision } = source;
|
|
9105
9397
|
if (opts.hfRepo) {
|
|
9106
9398
|
if (mirror) throw new Error("--hf-repo is for a local --source; a Hugging Face --source registers its own mirror");
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
(hf, root) => sdk.verifyMirror(hf, root, {
|
|
9111
|
-
));
|
|
9399
|
+
const token = opts.token ?? process.env.HF_TOKEN;
|
|
9400
|
+
({ mirror, revision } = await verifiedHuggingFaceMirror(opts.hfRepo, measured.contentRoot, {
|
|
9401
|
+
preflight: (repo) => sdk.preflightHuggingFace(repo, { revision: opts.revision, token }),
|
|
9402
|
+
verify: (hf, root, at) => sdk.verifyMirror(hf, root, { revision: at, token })
|
|
9403
|
+
}));
|
|
9112
9404
|
}
|
|
9113
9405
|
const identity = identityFromPack(
|
|
9114
9406
|
opts.registryModel ?? source.registryModel,
|
|
@@ -9138,10 +9430,15 @@ function modelOnboardCmd() {
|
|
|
9138
9430
|
if (job.state.chain && JSON.stringify(job.state.chain) !== JSON.stringify(chain)) throw new Error("onboarding chain/publisher resume conflict");
|
|
9139
9431
|
job.state.chain = chain;
|
|
9140
9432
|
await job.save();
|
|
9141
|
-
if (
|
|
9433
|
+
if (computeChannel5.isDenseGdnArch(checkpoint.arch)) await requireHybridApproval(client.api, checkpoint.arch);
|
|
9142
9434
|
const submit = (tx) => submitOnboardingTx(client.api, tx, client.signer, job);
|
|
9143
9435
|
let state = await readState(client.api, sdk, identity.modelHash, precision);
|
|
9144
|
-
|
|
9436
|
+
if (metadataOnly && opts.approve && (!state.registered || !state.weightsRef)) {
|
|
9437
|
+
throw new Error(
|
|
9438
|
+
`--approve on a metadata run needs the bound DA backup of ${identity.modelHash}; register without --approve, run \`${modelBackupHandoff(identity.modelHash, precision, preflight?.revision)}\` on a box with room for the checkpoint, bind it with \`flop model upload --commitment\` and \`flop model mirrors set --append --source da:<table_ref>\`, then rerun with --approve (or rerun with --download --approve from a fresh --state-dir)`
|
|
9439
|
+
);
|
|
9440
|
+
}
|
|
9441
|
+
const checkedParams = computeChannel5.isDenseGdnArch(checkpoint.arch) ? paramsBillion : void 0;
|
|
9145
9442
|
planOnboard(state, identity, precision, teePlatforms, checkedParams);
|
|
9146
9443
|
await validateCapture(opts.captureEndpoint, identity, checkpoint, source.modelDir);
|
|
9147
9444
|
job.state.phase = "capture-verified";
|
|
@@ -9152,21 +9449,46 @@ function modelOnboardCmd() {
|
|
|
9152
9449
|
throw new Error("pending registration parameter count differs from the checkpoint census; original transaction retained");
|
|
9153
9450
|
}
|
|
9154
9451
|
}
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9452
|
+
let daSpotCheck;
|
|
9453
|
+
const approvalEvidence = async (api, signer, root) => {
|
|
9454
|
+
const evidence = await requireApprovalEvidence(
|
|
9455
|
+
api,
|
|
9456
|
+
identity.modelHash,
|
|
9457
|
+
precision,
|
|
9458
|
+
signer,
|
|
9459
|
+
measured.imageSize,
|
|
9460
|
+
checkpoint.arch,
|
|
9461
|
+
root,
|
|
9462
|
+
measured.files
|
|
9463
|
+
);
|
|
9464
|
+
if (!metadataOnly || evidence.approved) return evidence;
|
|
9465
|
+
const token = opts.token ?? process.env.HF_TOKEN;
|
|
9466
|
+
daSpotCheck = await spotCheckImage(measured.files, measured.imageSize, {
|
|
9467
|
+
chunkSize: sdk.DA_CHUNK_SIZE,
|
|
9468
|
+
readChunk: (j) => daRetrieveChunk(opts.daValidator, getRpc(command), evidence.weights, j),
|
|
9469
|
+
readHubRange: (path5, start, end, size) => readHuggingFaceRange(hubRepo, preflight.revision, path5, start, end, size, token)
|
|
9470
|
+
});
|
|
9471
|
+
const again = await requireApprovalEvidence(
|
|
9472
|
+
api,
|
|
9162
9473
|
identity.modelHash,
|
|
9163
9474
|
precision,
|
|
9164
9475
|
signer,
|
|
9165
9476
|
measured.imageSize,
|
|
9166
9477
|
checkpoint.arch,
|
|
9167
|
-
|
|
9478
|
+
root,
|
|
9168
9479
|
measured.files
|
|
9169
9480
|
);
|
|
9481
|
+
if (again.weights !== evidence.weights || again.table !== evidence.table) {
|
|
9482
|
+
throw new Error("the bound DA image or file table changed during the spot check; nothing was submitted");
|
|
9483
|
+
}
|
|
9484
|
+
return again;
|
|
9485
|
+
};
|
|
9486
|
+
await recoverOnboardingTx(client.api, job, async (original) => {
|
|
9487
|
+
if (!containsApproval(original.method)) return;
|
|
9488
|
+
if (!opts.approve) throw new Error("pending approval requires --approve before rebroadcast; signed transaction retained");
|
|
9489
|
+
const signer = original.signer.toString();
|
|
9490
|
+
const mode = await registryAuthorityMode(client.api, signer, network);
|
|
9491
|
+
await approvalEvidence(client.api, signer, mode === "root");
|
|
9170
9492
|
});
|
|
9171
9493
|
state = await readState(client.api, sdk, identity.modelHash, precision);
|
|
9172
9494
|
const plan = planOnboard(state, identity, precision, teePlatforms, checkedParams);
|
|
@@ -9298,7 +9620,7 @@ function modelOnboardCmd() {
|
|
|
9298
9620
|
const view = await authority.api.at(await authority.api.rpc.chain.getFinalizedHead());
|
|
9299
9621
|
const raw = await view.query.modelRegistry.admissionRequirements(identity.modelHash);
|
|
9300
9622
|
const requirements = optionValue(raw);
|
|
9301
|
-
const expectedHybrid =
|
|
9623
|
+
const expectedHybrid = computeChannel5.isDenseGdnArch(checkpoint.arch);
|
|
9302
9624
|
if (requirements) {
|
|
9303
9625
|
const metadata = requirements.toJSON?.() ?? requirements;
|
|
9304
9626
|
if (BigInt(metadata.weightsSizeBytes ?? metadata.weights_size_bytes) !== BigInt(measured.imageSize) || (metadata.requiresHybridProfile ?? metadata.requires_hybrid_profile) !== expectedHybrid) {
|
|
@@ -9313,16 +9635,7 @@ function modelOnboardCmd() {
|
|
|
9313
9635
|
}
|
|
9314
9636
|
}
|
|
9315
9637
|
if (opts.approve) {
|
|
9316
|
-
const evidence = await
|
|
9317
|
-
authority.api,
|
|
9318
|
-
identity.modelHash,
|
|
9319
|
-
precision,
|
|
9320
|
-
authority.signer.address,
|
|
9321
|
-
measured.imageSize,
|
|
9322
|
-
checkpoint.arch,
|
|
9323
|
-
mode === "root",
|
|
9324
|
-
measured.files
|
|
9325
|
-
);
|
|
9638
|
+
const evidence = await approvalEvidence(authority.api, authority.signer.address, mode === "root");
|
|
9326
9639
|
if (!evidence.approved) {
|
|
9327
9640
|
const call = authority.api.tx.modelRegistry.approveModel(identity.modelHash, authority.signer.address);
|
|
9328
9641
|
await submitOnboardingTx(authority.api, registryAuthorityCall(authority.api, call, mode), authority.signer, job);
|
|
@@ -9365,7 +9678,9 @@ then bind the DataRef it prints. Rerunning this command with --download packs th
|
|
|
9365
9678
|
...identity,
|
|
9366
9679
|
registered: state.registered,
|
|
9367
9680
|
approved: state.approved === true,
|
|
9368
|
-
|
|
9681
|
+
throughputCeiling: ceiling,
|
|
9682
|
+
...metadataOnly ? { contentRoot: measured.contentRoot, imageSize: measured.imageSize, daBackup: state.weightsRef ? "present" : "pending" } : {},
|
|
9683
|
+
...daSpotCheck ? { daSpotCheck } : {}
|
|
9369
9684
|
},
|
|
9370
9685
|
getJson(command)
|
|
9371
9686
|
);
|
|
@@ -9384,7 +9699,10 @@ then bind the DataRef it prints. Rerunning this command with --download packs th
|
|
|
9384
9699
|
// src/commands/model-upload.ts
|
|
9385
9700
|
var PRECISIONS2 = ["FP32", "FP16", "BF16", "INT8", "INT4", "Mixed"];
|
|
9386
9701
|
function modelUploadCmd() {
|
|
9387
|
-
return new Command30("upload").description("Push verified weights to validator DA, register the lease, and bind the model").requiredOption("--model-hash <hex>", "Registered model hash").option("--path <path>", "Local weight file or directory (packed and streamed from disk)").option("--source <source>", "Remote source: hf:<repo>, modelscope:<repo>, https://..., da:<commitment>").
|
|
9702
|
+
return new Command30("upload").description("Push verified weights to validator DA, register the lease, and bind the model").requiredOption("--model-hash <hex>", "Registered model hash").option("--path <path>", "Local weight file or directory (packed and streamed from disk)").option("--source <source>", "Remote source: hf:<repo>, modelscope:<repo>, https://..., da:<commitment>").option(
|
|
9703
|
+
"--revision <sha>",
|
|
9704
|
+
"Immutable 40-hex commit for an hf: --source; when omitted, main is used only if its sha256 table reproduces the registered content-v1 root"
|
|
9705
|
+
).addOption(new Option2("--precision <precision>", "Measured-root precision").choices(PRECISIONS2).default("FP16")).option("--da-endpoint <url>", "DA blob gateway for a da:<commitment> source").option("--da-validator <path>", "da-validator executable", "da-validator").option("--token <token>", "HF/ModelScope bearer token (prefer HF_TOKEN)").option("--commitment <hex>", "Existing DA commitment (commitment-only recovery mode)").option("--size <bytes>", "Existing DA blob size (required with --commitment)").option("--json", "Output as JSON").action(async (opts, command) => {
|
|
9388
9706
|
let client;
|
|
9389
9707
|
let tempDir;
|
|
9390
9708
|
try {
|
|
@@ -9394,6 +9712,9 @@ function modelUploadCmd() {
|
|
|
9394
9712
|
}
|
|
9395
9713
|
if (opts.commitment && !opts.size) throw new Error("--size is required with --commitment");
|
|
9396
9714
|
if (opts.size && !opts.commitment) throw new Error("--commitment is required with --size");
|
|
9715
|
+
const revision = parseRevision(opts.revision);
|
|
9716
|
+
const remote = opts.source ? parseModelSource(opts.source) : void 0;
|
|
9717
|
+
if (revision && remote?.kind !== "huggingface") throw new Error("--revision is only for an hf: --source");
|
|
9397
9718
|
const sdk = await import("@flop-labs/sdk");
|
|
9398
9719
|
const rpc = getRpc(command);
|
|
9399
9720
|
client = await createClient(rpc, getSigner(command));
|
|
@@ -9406,8 +9727,10 @@ function modelUploadCmd() {
|
|
|
9406
9727
|
let imagePath;
|
|
9407
9728
|
let custody;
|
|
9408
9729
|
if (!commitment) {
|
|
9409
|
-
tempDir = await
|
|
9730
|
+
tempDir = await mkdtemp2(join6(tmpdir2(), "flop-model-upload-"));
|
|
9410
9731
|
imagePath = join6(tempDir, "weights.img");
|
|
9732
|
+
const expected = await sdk.readMeasuredRoot(client.api, opts.modelHash, opts.precision);
|
|
9733
|
+
if (!expected) throw new Error(`no measured root for ${opts.modelHash} at ${opts.precision}`);
|
|
9411
9734
|
if (opts.path) {
|
|
9412
9735
|
let modelDir = resolve8(opts.path);
|
|
9413
9736
|
const stat2 = await lstat7(modelDir);
|
|
@@ -9418,8 +9741,18 @@ function modelUploadCmd() {
|
|
|
9418
9741
|
await copyFile(resolve8(opts.path), join6(modelDir, basename2(opts.path)));
|
|
9419
9742
|
}
|
|
9420
9743
|
measured = await packModel(modelDir, imagePath, join6(tempDir, "manifest.json"));
|
|
9744
|
+
} else if (remote.kind === "huggingface" && !remote.path) {
|
|
9745
|
+
const modelDir = join6(tempDir, "model");
|
|
9746
|
+
await mkdir11(modelDir);
|
|
9747
|
+
await downloadPinnedHuggingFace(remote.repo, modelDir, expected, {
|
|
9748
|
+
revision,
|
|
9749
|
+
token: opts.token ?? process.env.HF_TOKEN
|
|
9750
|
+
});
|
|
9751
|
+
const roots = await sdk.measureModelDir(modelDir);
|
|
9752
|
+
measured = { content_root: roots.contentRoot, verity_root: roots.verityRoot };
|
|
9753
|
+
await pipeline2(sdk.packedImageStream(modelDir, roots.files), createWriteStream(imagePath));
|
|
9421
9754
|
} else {
|
|
9422
|
-
const files = (await sdk.fetchWeights(
|
|
9755
|
+
const files = (await sdk.fetchWeights(remote, {
|
|
9423
9756
|
token: opts.token ?? process.env.HF_TOKEN,
|
|
9424
9757
|
daSource: daHttpSource(opts.daEndpoint)
|
|
9425
9758
|
})).files;
|
|
@@ -9427,8 +9760,6 @@ function modelUploadCmd() {
|
|
|
9427
9760
|
measured = { content_root: roots.contentRoot, verity_root: roots.verityRoot };
|
|
9428
9761
|
await writeFile9(imagePath, sdk.buildImage(files));
|
|
9429
9762
|
}
|
|
9430
|
-
const expected = await sdk.readMeasuredRoot(client.api, opts.modelHash, opts.precision);
|
|
9431
|
-
if (!expected) throw new Error(`no measured root for ${opts.modelHash} at ${opts.precision}`);
|
|
9432
9763
|
const scheme = sdk.rootScheme({ contentRoot: measured.content_root, verityRoot: measured.verity_root }, expected);
|
|
9433
9764
|
if (!scheme) {
|
|
9434
9765
|
throw new Error(`source content_root ${measured.content_root} and verity_root ${measured.verity_root} != registered ${expected}`);
|
|
@@ -9480,7 +9811,7 @@ function modelUploadCmd() {
|
|
|
9480
9811
|
next: "rerun the same command to resume the DA push; to abandon a registered blob, run the printed `flop da withdraw` recovery"
|
|
9481
9812
|
});
|
|
9482
9813
|
} finally {
|
|
9483
|
-
if (tempDir) await
|
|
9814
|
+
if (tempDir) await rm4(tempDir, { recursive: true, force: true });
|
|
9484
9815
|
await client?.disconnect();
|
|
9485
9816
|
}
|
|
9486
9817
|
});
|
|
@@ -9525,12 +9856,187 @@ function modelAuditCmd() {
|
|
|
9525
9856
|
});
|
|
9526
9857
|
}
|
|
9527
9858
|
|
|
9528
|
-
// src/commands/model-
|
|
9859
|
+
// src/commands/model-mirrors.ts
|
|
9529
9860
|
import { Command as Command32 } from "commander";
|
|
9861
|
+
var MIRROR_URI_MAX_BYTES = 512;
|
|
9862
|
+
var MAX_MODEL_MIRRORS = 8;
|
|
9863
|
+
var REPO = /^[^/\s]+\/[^/\s]+$/;
|
|
9864
|
+
function boundedUri(value, what) {
|
|
9865
|
+
if (Buffer.byteLength(value, "utf8") > MIRROR_URI_MAX_BYTES) {
|
|
9866
|
+
throw new Error(`${what} exceeds the registry's ${MIRROR_URI_MAX_BYTES}-byte bound`);
|
|
9867
|
+
}
|
|
9868
|
+
return value;
|
|
9869
|
+
}
|
|
9870
|
+
function parseMirrorSource(raw) {
|
|
9871
|
+
if (raw.startsWith("https://")) {
|
|
9872
|
+
let url;
|
|
9873
|
+
try {
|
|
9874
|
+
url = new URL(raw);
|
|
9875
|
+
} catch {
|
|
9876
|
+
throw new Error(`invalid https mirror URL: ${raw}`);
|
|
9877
|
+
}
|
|
9878
|
+
if (url.protocol !== "https:" || !url.hostname) throw new Error(`invalid https mirror URL: ${raw}`);
|
|
9879
|
+
if (url.username || url.password) throw new Error("an https mirror URL must not carry credentials");
|
|
9880
|
+
return { kind: "https", url: boundedUri(raw, "https mirror URL") };
|
|
9881
|
+
}
|
|
9882
|
+
for (const [prefix, kind] of [["hf:", "huggingface"], ["huggingface:", "huggingface"], ["modelscope:", "modelscope"]]) {
|
|
9883
|
+
if (raw.startsWith(prefix)) {
|
|
9884
|
+
const repo = raw.slice(prefix.length);
|
|
9885
|
+
if (!REPO.test(repo)) throw new Error(`${kind} mirror must be ${prefix}<owner>/<model> (got ${raw})`);
|
|
9886
|
+
return { kind, repo: boundedUri(repo, `${kind} repo id`) };
|
|
9887
|
+
}
|
|
9888
|
+
}
|
|
9889
|
+
if (raw.startsWith("da:")) return { kind: "da", commitment: normalizeHash(raw.slice(3), "DA mirror commitment") };
|
|
9890
|
+
throw new Error(`unsupported mirror "${raw}"; use https://..., hf:<owner>/<model>, modelscope:<owner>/<model> or da:<commitment>`);
|
|
9891
|
+
}
|
|
9892
|
+
function mirrorKey(entry) {
|
|
9893
|
+
if (entry.kind === "https") return `https:${entry.url}`;
|
|
9894
|
+
if (entry.kind === "da") return `da:${entry.commitment}@${entry.providerId ?? "?"}`;
|
|
9895
|
+
return `${entry.kind}:${entry.repo}`;
|
|
9896
|
+
}
|
|
9897
|
+
function chainModelSource(entry) {
|
|
9898
|
+
switch (entry.kind) {
|
|
9899
|
+
case "https":
|
|
9900
|
+
return { Https: entry.url };
|
|
9901
|
+
case "huggingface":
|
|
9902
|
+
return { HuggingFace: entry.repo };
|
|
9903
|
+
case "modelscope":
|
|
9904
|
+
return { ModelScope: entry.repo };
|
|
9905
|
+
case "da":
|
|
9906
|
+
if (entry.providerId === void 0 || entry.retention === void 0) {
|
|
9907
|
+
throw new Error(`DA mirror ${entry.commitment} has no resolved provider or retention`);
|
|
9908
|
+
}
|
|
9909
|
+
return { DaLayer: { commitment: entry.commitment, provider_id: entry.providerId, retention: entry.retention } };
|
|
9910
|
+
}
|
|
9911
|
+
}
|
|
9912
|
+
function decodeRegisteredMirror(json) {
|
|
9913
|
+
const text4 = (hex5) => {
|
|
9914
|
+
const value = String(hex5);
|
|
9915
|
+
return /^0x([0-9a-f]{2})*$/i.test(value) ? Buffer.from(value.slice(2), "hex").toString("utf8") : value;
|
|
9916
|
+
};
|
|
9917
|
+
if (json?.https !== void 0) return { kind: "https", url: text4(json.https) };
|
|
9918
|
+
if (json?.huggingFace !== void 0) return { kind: "huggingface", repo: text4(json.huggingFace) };
|
|
9919
|
+
if (json?.modelScope !== void 0) return { kind: "modelscope", repo: text4(json.modelScope) };
|
|
9920
|
+
if (json?.daLayer !== void 0) {
|
|
9921
|
+
const ref = json.daLayer;
|
|
9922
|
+
return {
|
|
9923
|
+
kind: "da",
|
|
9924
|
+
commitment: normalizeHash(String(ref.commitment), "registered DA mirror commitment"),
|
|
9925
|
+
providerId: Number(ref.providerId ?? ref.provider_id),
|
|
9926
|
+
retention: String(ref.retention)
|
|
9927
|
+
};
|
|
9928
|
+
}
|
|
9929
|
+
throw new Error(`registered mirror has an unknown shape: ${JSON.stringify(json)}`);
|
|
9930
|
+
}
|
|
9931
|
+
function planMirrorList(requested, opts) {
|
|
9932
|
+
if (opts.clear) {
|
|
9933
|
+
if (requested.length > 0 || opts.append) throw new Error("--clear takes no --source and no --append");
|
|
9934
|
+
return { sources: [], added: [] };
|
|
9935
|
+
}
|
|
9936
|
+
if (requested.length === 0) throw new Error("pass at least one --source, or --clear to remove every mirror");
|
|
9937
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9938
|
+
for (const entry of requested) {
|
|
9939
|
+
const key = mirrorKey(entry);
|
|
9940
|
+
if (seen.has(key)) throw new Error(`duplicate mirror ${key}`);
|
|
9941
|
+
seen.add(key);
|
|
9942
|
+
}
|
|
9943
|
+
const registered = opts.append ? opts.registered ?? [] : [];
|
|
9944
|
+
const kept = new Set(registered.map(mirrorKey));
|
|
9945
|
+
const added = requested.filter((entry) => !kept.has(mirrorKey(entry)));
|
|
9946
|
+
const sources = [...registered, ...added];
|
|
9947
|
+
if (sources.length > MAX_MODEL_MIRRORS) {
|
|
9948
|
+
throw new Error(`${sources.length} mirrors exceed the registry's limit of ${MAX_MODEL_MIRRORS}`);
|
|
9949
|
+
}
|
|
9950
|
+
return { sources, added };
|
|
9951
|
+
}
|
|
9952
|
+
function display(entry) {
|
|
9953
|
+
if (entry.kind === "da") return { kind: "da", commitment: entry.commitment, provider_id: entry.providerId, retention: entry.retention };
|
|
9954
|
+
return entry.kind === "https" ? { kind: "https", url: entry.url } : { kind: entry.kind, repo: entry.repo };
|
|
9955
|
+
}
|
|
9956
|
+
var collect = (value, previous) => [...previous, value];
|
|
9957
|
+
function modelMirrorsSetCmd() {
|
|
9958
|
+
return new Command32("set").description(
|
|
9959
|
+
"Replace a registered model's mirror hint list (modelRegistry.setModelMirrors). Only the model's publisher may sign it: the call is signed-origin and checks publisher == signer, so neither a registrar nor Root (sudo) can set mirrors. Mirrors are hints: a miner serves the model only if the bytes it fetches reproduce the registered root"
|
|
9960
|
+
).requiredOption("--model-hash <hex>", "Registered model hash").option(
|
|
9961
|
+
"--source <mirror>",
|
|
9962
|
+
`Mirror, repeatable (at most ${MAX_MODEL_MIRRORS} in the final list): https://<url>, hf:<owner>/<model>, modelscope:<owner>/<model>, or da:<commitment> of a registered Leased DA blob (its provider is read from chain)`,
|
|
9963
|
+
collect,
|
|
9964
|
+
[]
|
|
9965
|
+
).option(
|
|
9966
|
+
"--append",
|
|
9967
|
+
"Keep the finalized registered mirrors verbatim and add the new ones after them (a DA entry counts as present only at the same commitment and provider). The list is read at finality and then replaced, so a concurrent mirror change by the same publisher between the two is overwritten"
|
|
9968
|
+
).option("--clear", "Remove every mirror (an empty list)").option("--dry-run", "Validate and print the encoded call without signing or submitting it").option("--json", "Output as JSON").action(async (opts, command) => {
|
|
9969
|
+
let client;
|
|
9970
|
+
try {
|
|
9971
|
+
const modelHash = normalizeHash(opts.modelHash, "--model-hash");
|
|
9972
|
+
const requested = opts.source.map(parseMirrorSource);
|
|
9973
|
+
planMirrorList(requested, { clear: opts.clear, append: opts.append, registered: [] });
|
|
9974
|
+
client = await createClient(getRpc(command), getSigner(command));
|
|
9975
|
+
const api = client.api;
|
|
9976
|
+
const head = await api.rpc.chain.getFinalizedHead();
|
|
9977
|
+
const [view, header] = await Promise.all([api.at(head), api.rpc.chain.getHeader(head)]);
|
|
9978
|
+
const rawModel = await view.query.modelRegistry.models(modelHash);
|
|
9979
|
+
if (!rawModel?.isSome) throw new Error(`model not registered at finality: ${modelHash}`);
|
|
9980
|
+
const publisher = rawModel.unwrap().publisher;
|
|
9981
|
+
if (!sameAccount(publisher, client.signer.address)) {
|
|
9982
|
+
throw new Error(`only the model publisher ${publisher.toString()} may set its mirrors; the signer is ${client.signer.address}`);
|
|
9983
|
+
}
|
|
9984
|
+
const finalized = { query: view.query, rpc: { chain: { getHeader: async () => header } } };
|
|
9985
|
+
for (const entry of requested) {
|
|
9986
|
+
if (entry.kind !== "da") continue;
|
|
9987
|
+
const blob = await view.query.daRegistry.blobs(entry.commitment);
|
|
9988
|
+
if (!blob?.isSome) throw new Error(`DA mirror ${entry.commitment} is not a registered DA blob at finality`);
|
|
9989
|
+
const size = blob.unwrap().toJSON().sizeBytes;
|
|
9990
|
+
const ref = await existingLeasedRef(finalized, entry.commitment, Number(size));
|
|
9991
|
+
if (!ref) throw new Error(`DA mirror ${entry.commitment} is not a registered DA blob at finality`);
|
|
9992
|
+
entry.providerId = ref.providerId;
|
|
9993
|
+
entry.retention = ref.retention;
|
|
9994
|
+
}
|
|
9995
|
+
const raw = await view.query.modelRegistry.modelMirrors(modelHash);
|
|
9996
|
+
const registered = raw?.isSome ? raw.unwrap().toJSON().map(decodeRegisteredMirror) : [];
|
|
9997
|
+
const { sources, added } = planMirrorList(requested, { clear: opts.clear, append: opts.append, registered });
|
|
9998
|
+
const result = {
|
|
9999
|
+
model_hash: modelHash,
|
|
10000
|
+
publisher: publisher.toString(),
|
|
10001
|
+
registered: registered.map(display),
|
|
10002
|
+
sources: sources.map(display),
|
|
10003
|
+
added: added.map(display),
|
|
10004
|
+
finalized_block: header.number.toString()
|
|
10005
|
+
};
|
|
10006
|
+
if (opts.append && added.length === 0) {
|
|
10007
|
+
print({ ...result, unchanged: true }, getJson(command));
|
|
10008
|
+
return;
|
|
10009
|
+
}
|
|
10010
|
+
const tx = api.tx.modelRegistry.setModelMirrors(modelHash, sources.map(chainModelSource));
|
|
10011
|
+
if (opts.dryRun) {
|
|
10012
|
+
print({ ...result, dry_run: true, call: tx.method.toHex(), next: "rerun without --dry-run to sign and submit" }, getJson(command));
|
|
10013
|
+
return;
|
|
10014
|
+
}
|
|
10015
|
+
const sdk = await import("@flop-labs/sdk");
|
|
10016
|
+
const submitted = await sdk.watchTransaction(tx, client.signer, { api });
|
|
10017
|
+
print({ ...result, tx_hash: (submitted.txHash ?? tx.hash).toHex() }, getJson(command));
|
|
10018
|
+
} catch (error) {
|
|
10019
|
+
commandError(error, { next: "fix the reported mirror or signer and rerun; after a submit timeout, rerun the same command with --dry-run and compare its `registered` (the finalized list) with `sources` before resubmitting" });
|
|
10020
|
+
} finally {
|
|
10021
|
+
await client?.disconnect();
|
|
10022
|
+
}
|
|
10023
|
+
});
|
|
10024
|
+
}
|
|
10025
|
+
function modelMirrorsCmd() {
|
|
10026
|
+
return new Command32("mirrors").description("Model mirror hints (publisher-signed)").addCommand(modelMirrorsSetCmd());
|
|
10027
|
+
}
|
|
10028
|
+
|
|
10029
|
+
// src/commands/model-preflight.ts
|
|
10030
|
+
import { Command as Command33 } from "commander";
|
|
9530
10031
|
function modelPreflightCmd() {
|
|
9531
|
-
return new
|
|
10032
|
+
return new Command33("preflight").description(
|
|
9532
10033
|
"Print a Hugging Face checkpoint's content_root (content-v1 model_hash), packed image size, DA chunk count and stored tensor elements from hub metadata and Range-read safetensors headers; no weights are downloaded"
|
|
9533
|
-
).requiredOption("--source <hf-id>", "Hugging Face id (owner/model or hf:owner/model)").option("--revision <sha>", "Immutable 40-hex commit; main is resolved to one when omitted").option("--token <token>", "Hugging Face bearer token (prefer HF_TOKEN)").option(
|
|
10034
|
+
).requiredOption("--source <hf-id>", "Hugging Face id (owner/model or hf:owner/model)").option("--revision <sha>", "Immutable 40-hex commit; main is resolved to one when omitted").option("--token <token>", "Hugging Face bearer token (prefer HF_TOKEN)").option(
|
|
10035
|
+
"--min-tokens-per-second <n>",
|
|
10036
|
+
"Fail unless the paid completion-token ceiling per channel at the median live calibration cap (or --miner's) is at least n; an unknown ceiling fails. The chain is read only with an explicit --rpc",
|
|
10037
|
+
parseMinTokensPerSecond
|
|
10038
|
+
).option("--miner <address>", "Compute the ceiling at this miner's accepted calibration cap instead of the median live one; the best is reported, never gated").option("--json", "Output as JSON").action(async (opts, command) => {
|
|
10039
|
+
let client;
|
|
9534
10040
|
try {
|
|
9535
10041
|
if (opts.revision && !/^[0-9a-f]{40}$/.test(opts.revision)) throw new Error("--revision must be an immutable 40-hex commit");
|
|
9536
10042
|
const repo = opts.source.startsWith("hf:") ? opts.source.slice(3) : opts.source;
|
|
@@ -9538,6 +10044,25 @@ function modelPreflightCmd() {
|
|
|
9538
10044
|
const hub = { revision: opts.revision, token: opts.token ?? process.env.HF_TOKEN };
|
|
9539
10045
|
const preflight = await sdk.preflightHuggingFace(repo, hub);
|
|
9540
10046
|
const headers = await sdk.readSafetensorsHeaders(repo, preflight.revision, preflight.files, hub);
|
|
10047
|
+
let work = "no root config.json";
|
|
10048
|
+
if (preflight.configJson) {
|
|
10049
|
+
try {
|
|
10050
|
+
work = perTokenWork(sdk.computeChannel.parseMeteringConfig(preflight.configJson, preflight.hfQuantConfigJson));
|
|
10051
|
+
} catch (error) {
|
|
10052
|
+
work = `no metering arch: ${error.message}`;
|
|
10053
|
+
}
|
|
10054
|
+
}
|
|
10055
|
+
let chain = null;
|
|
10056
|
+
if (resolveNetwork(command).rpcOverridden) {
|
|
10057
|
+
try {
|
|
10058
|
+
client = await createReadOnlyClient(getRpc(command));
|
|
10059
|
+
chain = await chainCapInputsOrReason(client.api, opts.miner);
|
|
10060
|
+
} catch (error) {
|
|
10061
|
+
chain = `chain connection failed: ${error.message}`;
|
|
10062
|
+
}
|
|
10063
|
+
}
|
|
10064
|
+
const ceiling = throughputCeiling(work, chain, opts.minTokensPerSecond);
|
|
10065
|
+
process.stderr.write(ceilingWarning(ceiling));
|
|
9541
10066
|
print({
|
|
9542
10067
|
repo,
|
|
9543
10068
|
revision: preflight.revision,
|
|
@@ -9548,18 +10073,22 @@ function modelPreflightCmd() {
|
|
|
9548
10073
|
fetched_metadata_files: preflight.metadataFiles.length,
|
|
9549
10074
|
safetensors_shards: headers.length,
|
|
9550
10075
|
// Not a parameter count for packed sub-byte weights, and scale tensors are included.
|
|
9551
|
-
stored_tensor_elements: sdk.storedTensorElements(headers).toString()
|
|
10076
|
+
stored_tensor_elements: sdk.storedTensorElements(headers).toString(),
|
|
10077
|
+
throughput_ceiling: snakeCeiling(ceiling)
|
|
9552
10078
|
}, getJson(command));
|
|
10079
|
+
enforceCeiling(ceiling);
|
|
9553
10080
|
} catch (error) {
|
|
9554
10081
|
commandError(error);
|
|
10082
|
+
} finally {
|
|
10083
|
+
await client?.disconnect();
|
|
9555
10084
|
}
|
|
9556
10085
|
});
|
|
9557
10086
|
}
|
|
9558
10087
|
|
|
9559
10088
|
// src/commands/model-backup.ts
|
|
9560
|
-
import { Command as
|
|
9561
|
-
import { createHash as
|
|
9562
|
-
import { mkdir as mkdir12, rm as
|
|
10089
|
+
import { Command as Command34, Option as Option3 } from "commander";
|
|
10090
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
10091
|
+
import { mkdir as mkdir12, rm as rm5, stat, writeFile as writeFile10 } from "node:fs/promises";
|
|
9563
10092
|
import { dirname as dirname6, join as join8, resolve as resolve9 } from "node:path";
|
|
9564
10093
|
var PRECISIONS3 = ["FP32", "FP16", "BF16", "INT8", "INT4", "Mixed"];
|
|
9565
10094
|
async function finalizedLeasedRef2(api, commitment, size) {
|
|
@@ -9578,7 +10107,7 @@ async function awaitFinalizedLeasedRef(api, commitment, size) {
|
|
|
9578
10107
|
}
|
|
9579
10108
|
function downloadStateDir(binding, override) {
|
|
9580
10109
|
if (override) return resolve9(override);
|
|
9581
|
-
const key =
|
|
10110
|
+
const key = createHash6("sha256").update(JSON.stringify(binding)).digest("hex");
|
|
9582
10111
|
return join8(dirname6(binding.modelDir), `.flop-backup-state-${key.slice(0, 16)}`);
|
|
9583
10112
|
}
|
|
9584
10113
|
async function assertSameFilesystem(stateDir, modelDir) {
|
|
@@ -9597,7 +10126,7 @@ function registeredHubRepo(mirrors) {
|
|
|
9597
10126
|
return repos[0];
|
|
9598
10127
|
}
|
|
9599
10128
|
function modelBackupCmd() {
|
|
9600
|
-
return new
|
|
10129
|
+
return new Command34("backup").description(
|
|
9601
10130
|
"Stream a registered content-v1 checkpoint from Hugging Face into validator DA under this box's DA key, without ever writing the packed image out, and print the DataRefs its publisher then binds"
|
|
9602
10131
|
).argument("<model_hash>", "Registered model hash (0x-hex)").requiredOption("--model-dir <path>", "Directory the checkpoint streams into (needs room for it)").option("--source <hf-id>", "Hugging Face id (owner/model or hf:owner/model); the model's registered mirror when omitted").option("--revision <sha>", "Immutable 40-hex commit; main is resolved to one when omitted").addOption(new Option3("--precision <precision>", "Measured-root precision").choices(PRECISIONS3).default("FP16")).option("--da-validator <path>", "da-validator executable", "da-validator").option("--token <token>", "Hugging Face bearer token (prefer HF_TOKEN)").option("--state-dir <path>", "Download resume receipts, on the checkpoint's filesystem but outside its tree (defaults to beside --model-dir)").option("--dry-run", "Print the plan and the DA deposit it would lock, and stop before any transfer").option("--json", "Output as JSON").action(async (modelHash, opts, command) => {
|
|
9603
10132
|
let client;
|
|
@@ -9713,7 +10242,7 @@ function modelBackupCmd() {
|
|
|
9713
10242
|
table_register_tx: table2.registerTx,
|
|
9714
10243
|
// The binding is the model publisher's own transaction: this box holds no
|
|
9715
10244
|
// model-registry authority, and `set_model_weights_ref` takes any Leased ref from it.
|
|
9716
|
-
next: `the model publisher binds the backup with \`flop model upload --model-hash ${modelHash} --commitment ${weights.ref.commitment} --size ${imageSize}\`, then advertises the file table
|
|
10245
|
+
next: `the model publisher binds the backup with \`flop model upload --model-hash ${modelHash} --commitment ${weights.ref.commitment} --size ${imageSize}\`, then advertises the file table with \`flop model mirrors set --model-hash ${modelHash} --append --source da:${table2.ref.commitment}\``,
|
|
9717
10246
|
recovery: `flop da withdraw --commitment ${weights.ref.commitment} (and ${table2.ref.commitment}) refunds both deposits while nothing pins them`
|
|
9718
10247
|
}, getJson(command));
|
|
9719
10248
|
} catch (error) {
|
|
@@ -9721,16 +10250,16 @@ function modelBackupCmd() {
|
|
|
9721
10250
|
next: "rerun the same command to resume the download and the DA push; to abandon a registered blob, run the printed `flop da withdraw` recovery"
|
|
9722
10251
|
});
|
|
9723
10252
|
} finally {
|
|
9724
|
-
if (workDir) await
|
|
10253
|
+
if (workDir) await rm5(workDir, { recursive: true, force: true });
|
|
9725
10254
|
await client?.disconnect();
|
|
9726
10255
|
}
|
|
9727
10256
|
});
|
|
9728
10257
|
}
|
|
9729
10258
|
|
|
9730
10259
|
// src/commands/model-list.ts
|
|
9731
|
-
import { Command as
|
|
10260
|
+
import { Command as Command35 } from "commander";
|
|
9732
10261
|
function modelListCmd() {
|
|
9733
|
-
return new
|
|
10262
|
+
return new Command35("list").description("List models registered on the selected FLOP chain").option("--name <text>", "Filter model names").option("--limit <n>", "Maximum rows returned", "100").option("--json", "Output as JSON").action(async (opts, command) => {
|
|
9734
10263
|
let client;
|
|
9735
10264
|
try {
|
|
9736
10265
|
const sdk = await import("@flop-labs/sdk");
|
|
@@ -9760,9 +10289,9 @@ function modelListCmd() {
|
|
|
9760
10289
|
}
|
|
9761
10290
|
|
|
9762
10291
|
// src/commands/model-get.ts
|
|
9763
|
-
import { Command as
|
|
10292
|
+
import { Command as Command36 } from "commander";
|
|
9764
10293
|
function modelGetCmd() {
|
|
9765
|
-
return new
|
|
10294
|
+
return new Command36("get").description("Get a registered model by hash or exact name").argument("<identifier>", "Model hash or name").option("--precision <precision>", "Measured-root precision", "FP16").option("--json", "Output as JSON").action(async (identifier, opts, command) => {
|
|
9766
10295
|
let client;
|
|
9767
10296
|
try {
|
|
9768
10297
|
const sdk = await import("@flop-labs/sdk");
|
|
@@ -9793,12 +10322,25 @@ function modelGetCmd() {
|
|
|
9793
10322
|
}
|
|
9794
10323
|
|
|
9795
10324
|
// src/commands/model-fetch.ts
|
|
9796
|
-
import { Command as
|
|
10325
|
+
import { Command as Command37, Option as Option4 } from "commander";
|
|
10326
|
+
import { mkdir as mkdir13, mkdtemp as mkdtemp3, readdir as readdir2, rename as rename8, rm as rm6, rmdir } from "node:fs/promises";
|
|
10327
|
+
import { basename as basename3, dirname as dirname7, join as join9, resolve as resolve10 } from "node:path";
|
|
9797
10328
|
var PRECISIONS4 = ["FP32", "FP16", "BF16", "INT8", "INT4", "Mixed"];
|
|
10329
|
+
async function requireEmptyOrAbsent(dir) {
|
|
10330
|
+
try {
|
|
10331
|
+
if ((await readdir2(dir)).length > 0) throw new Error(`--output-dir ${dir} is not empty; a Hugging Face checkpoint is written only into an empty or new directory`);
|
|
10332
|
+
} catch (error) {
|
|
10333
|
+
if (error.code !== "ENOENT") throw error;
|
|
10334
|
+
}
|
|
10335
|
+
}
|
|
9798
10336
|
function modelFetchCmd() {
|
|
9799
|
-
return new
|
|
10337
|
+
return new Command37("fetch").description("Fetch model weights and accept only a registered dm-verity root").argument("<identifier>", "Registered model hash or exact name").addOption(new Option4("--precision <precision>", "Weights precision").choices(PRECISIONS4).default("FP16")).option("--source <source...>", "Override mirrors: hf:<repo>, modelscope:<repo>, https://..., da:<commitment>").option(
|
|
10338
|
+
"--revision <sha>",
|
|
10339
|
+
"Immutable 40-hex Hugging Face commit for hf: mirrors; when omitted, main is used only if its sha256 table reproduces the registered content-v1 root"
|
|
10340
|
+
).option("--da-endpoint <url>", "DA blob gateway exposing GET /blob/<commitment>").option("--output-dir <path>", "Destination directory", "./flop-model").option("--token <token>", "HF/ModelScope bearer token (prefer HF_TOKEN)").option("--json", "Output as JSON").action(async (identifier, opts, command) => {
|
|
9800
10341
|
let client;
|
|
9801
10342
|
try {
|
|
10343
|
+
const revision = parseRevision(opts.revision);
|
|
9802
10344
|
const sdk = await import("@flop-labs/sdk");
|
|
9803
10345
|
client = await createClient(getRpc(command));
|
|
9804
10346
|
const registry = new sdk.OnChainModelRegistry(client.api);
|
|
@@ -9810,30 +10352,56 @@ function modelFetchCmd() {
|
|
|
9810
10352
|
}
|
|
9811
10353
|
const sources = opts.source?.map(parseModelSource) ?? await sdk.readModelMirrors(client.api, model.hash);
|
|
9812
10354
|
if (sources.length === 0) throw new Error(`no mirrors registered for model: ${model.hash}`);
|
|
10355
|
+
const hub = sources.slice(0, 16).some((s) => s.kind === "huggingface");
|
|
10356
|
+
if (revision && !hub) throw new Error("--revision is only for a Hugging Face source");
|
|
10357
|
+
const outputDir = resolve10(opts.outputDir);
|
|
10358
|
+
const token = opts.token ?? process.env.HF_TOKEN;
|
|
9813
10359
|
const attempts = [];
|
|
9814
10360
|
for (const source of sources.slice(0, 16)) {
|
|
10361
|
+
let staged;
|
|
9815
10362
|
try {
|
|
9816
|
-
|
|
9817
|
-
|
|
9818
|
-
|
|
9819
|
-
|
|
9820
|
-
|
|
10363
|
+
let result;
|
|
10364
|
+
let write;
|
|
10365
|
+
let at;
|
|
10366
|
+
if (source.kind === "huggingface" && !source.path) {
|
|
10367
|
+
await requireEmptyOrAbsent(outputDir);
|
|
10368
|
+
await mkdir13(dirname7(outputDir), { recursive: true });
|
|
10369
|
+
const dir = await mkdtemp3(join9(dirname7(outputDir), `.${basename3(outputDir)}.fetch-`));
|
|
10370
|
+
staged = dir;
|
|
10371
|
+
const download = await downloadPinnedHuggingFace(source.repo, dir, expected, { revision, token });
|
|
10372
|
+
at = download.revision;
|
|
10373
|
+
result = await sdk.verifyModelDir(dir, expected);
|
|
10374
|
+
write = async () => {
|
|
10375
|
+
await rmdir(outputDir).catch((error) => {
|
|
10376
|
+
if (error.code !== "ENOENT") throw error;
|
|
10377
|
+
});
|
|
10378
|
+
await rename8(dir, outputDir);
|
|
10379
|
+
staged = void 0;
|
|
10380
|
+
return download.paths.map((p) => join9(outputDir, ...p.split("/")));
|
|
10381
|
+
};
|
|
10382
|
+
} else {
|
|
10383
|
+
const fetched = await sdk.fetchWeights(source, { token, daSource: daHttpSource(opts.daEndpoint) }, expected);
|
|
10384
|
+
result = sdk.verifyModelWeights(fetched.files, expected);
|
|
10385
|
+
write = () => writeModelFiles(opts.outputDir, fetched.files);
|
|
10386
|
+
}
|
|
9821
10387
|
const verified = result.scheme === "content-v1" ? result.computedContentRoot : result.computedVerityRoot;
|
|
9822
10388
|
attempts.push({
|
|
9823
10389
|
source,
|
|
10390
|
+
...at ? { revision: at } : {},
|
|
9824
10391
|
ok: result.ok,
|
|
9825
10392
|
scheme: result.scheme,
|
|
9826
10393
|
computed_content_root: result.computedContentRoot,
|
|
9827
10394
|
computed_verity_root: result.computedVerityRoot
|
|
9828
10395
|
});
|
|
9829
10396
|
if (!result.ok) continue;
|
|
9830
|
-
const files = await
|
|
10397
|
+
const files = await write();
|
|
9831
10398
|
print({
|
|
9832
10399
|
model_hash: model.hash,
|
|
9833
10400
|
precision: opts.precision,
|
|
9834
10401
|
verified_root: verified,
|
|
9835
10402
|
scheme: result.scheme,
|
|
9836
10403
|
source,
|
|
10404
|
+
...at ? { revision: at } : {},
|
|
9837
10405
|
output_dir: opts.outputDir,
|
|
9838
10406
|
file_count: files.length,
|
|
9839
10407
|
files: files.slice(0, 20),
|
|
@@ -9843,6 +10411,8 @@ function modelFetchCmd() {
|
|
|
9843
10411
|
return;
|
|
9844
10412
|
} catch (error) {
|
|
9845
10413
|
attempts.push({ source, ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
10414
|
+
} finally {
|
|
10415
|
+
if (staged) await rm6(staged, { recursive: true, force: true });
|
|
9846
10416
|
}
|
|
9847
10417
|
}
|
|
9848
10418
|
throw new Error(`no mirror reproduced registered root ${expected}: ${JSON.stringify(attempts)}`);
|
|
@@ -9859,7 +10429,7 @@ function modelFetchCmd() {
|
|
|
9859
10429
|
|
|
9860
10430
|
// src/commands/agent.ts
|
|
9861
10431
|
import path2 from "node:path";
|
|
9862
|
-
import { Command as
|
|
10432
|
+
import { Command as Command38, Option as Option5 } from "commander";
|
|
9863
10433
|
import { getFlopNetworkProfile as getFlopNetworkProfile3 } from "@flop-labs/sdk";
|
|
9864
10434
|
var DEFAULT_MCP_URL = "https://mcp.flop.network/mcp";
|
|
9865
10435
|
var DEVNET_MCP_URL = "https://mcp.flop.network/mcp";
|
|
@@ -9947,7 +10517,7 @@ function buildAgentInitPlan(client, mode, mcpUrl2 = DEFAULT_MCP_URL) {
|
|
|
9947
10517
|
};
|
|
9948
10518
|
}
|
|
9949
10519
|
function agentInitCmd() {
|
|
9950
|
-
return new
|
|
10520
|
+
return new Command38("init").description("Print a safe agent onboarding plan and MCP configuration").addOption(new Option5("--client <client>", "Agent client").choices([
|
|
9951
10521
|
"claude-code",
|
|
9952
10522
|
"codex",
|
|
9953
10523
|
"cursor",
|
|
@@ -9965,7 +10535,7 @@ function agentInitCmd() {
|
|
|
9965
10535
|
});
|
|
9966
10536
|
}
|
|
9967
10537
|
function agentDoctorCmd() {
|
|
9968
|
-
return new
|
|
10538
|
+
return new Command38("doctor").description("Check agent MCP, gateway, and optional chain connectivity").addOption(new Option5("--mode <mode>", "MCP mode").choices(["remote", "local"]).default("remote")).option("--mcp-url <url>", "MCP URL").option("--gateway <url>", "Optional OpenAI-compatible gateway URL").option("--repo <path>", "flop-core checkout for local mode", process.cwd()).option("--check-rpc", "Also require the configured chain RPC").option("--json", "Output as JSON").action(async (opts, command) => {
|
|
9969
10539
|
try {
|
|
9970
10540
|
const checks = [executableCheck("node")];
|
|
9971
10541
|
if (opts.mode === "remote") {
|
|
@@ -9992,7 +10562,7 @@ function agentDoctorCmd() {
|
|
|
9992
10562
|
});
|
|
9993
10563
|
}
|
|
9994
10564
|
function agentCredentialCmd() {
|
|
9995
|
-
return new
|
|
10565
|
+
return new Command38("credential").description("Provision or reuse a devnet gateway key for agent clients (wallet-signed by default)").option("--print-key", "Print only the bearer key for a local client auth helper").option("--tailnet", "Enroll through the private tailnet lane (approved Tailscale users only)").option("--json", "Output non-secret credential metadata as JSON").action(async (opts, command) => {
|
|
9996
10566
|
try {
|
|
9997
10567
|
if (getNetwork(command) !== "devnet") {
|
|
9998
10568
|
throw new Error("agent credential requires --network devnet");
|
|
@@ -10025,20 +10595,20 @@ function agentCredentialCmd() {
|
|
|
10025
10595
|
});
|
|
10026
10596
|
}
|
|
10027
10597
|
function agentCmd() {
|
|
10028
|
-
const command = new
|
|
10598
|
+
const command = new Command38("agent").description("Onboard and operate FLOP-backed agents");
|
|
10029
10599
|
command.addCommand(agentInitCmd());
|
|
10030
10600
|
command.addCommand(agentDoctorCmd());
|
|
10031
10601
|
command.addCommand(agentCredentialCmd());
|
|
10032
10602
|
command.addCommand(launchCmd());
|
|
10033
10603
|
command.addCommand(proxyCmd());
|
|
10034
10604
|
command.addCommand(hermesCmd());
|
|
10035
|
-
const keys = new
|
|
10605
|
+
const keys = new Command38("keys").description("Manage agent keys");
|
|
10036
10606
|
keys.addCommand(keysGenerateCmd());
|
|
10037
10607
|
command.addCommand(keys);
|
|
10038
|
-
const wallet = new
|
|
10608
|
+
const wallet = new Command38("wallet").description("Manage agent wallets");
|
|
10039
10609
|
wallet.addCommand(walletAgentCreateCmd());
|
|
10040
10610
|
command.addCommand(wallet);
|
|
10041
|
-
const session = new
|
|
10611
|
+
const session = new Command38("session").description("Manage agent session keys");
|
|
10042
10612
|
session.addCommand(sessionCreateCmd());
|
|
10043
10613
|
session.addCommand(sessionInfoCmd());
|
|
10044
10614
|
session.addCommand(sessionRevokeCmd());
|
|
@@ -10050,7 +10620,7 @@ function agentCmd() {
|
|
|
10050
10620
|
// src/commands/miner.ts
|
|
10051
10621
|
import { readFileSync } from "node:fs";
|
|
10052
10622
|
import path3 from "node:path";
|
|
10053
|
-
import { Command as
|
|
10623
|
+
import { Command as Command40, Option as Option6 } from "commander";
|
|
10054
10624
|
import { formatFlop as formatFlop2 } from "@flop-labs/sdk";
|
|
10055
10625
|
|
|
10056
10626
|
// src/gpu-inventory-remark.ts
|
|
@@ -10127,6 +10697,85 @@ function gpusFromDetection(devices) {
|
|
|
10127
10697
|
return aggregate(devices.map((device) => ({ name: device.deviceName.trim(), memoryMib: device.memoryTotal })));
|
|
10128
10698
|
}
|
|
10129
10699
|
|
|
10700
|
+
// src/commands/miner-capacity-rate.ts
|
|
10701
|
+
import { Command as Command39 } from "commander";
|
|
10702
|
+
var U64_MAX = (1n << 64n) - 1n;
|
|
10703
|
+
function parseReservationRate(value) {
|
|
10704
|
+
if (!/^\d+$/.test(value.trim())) throw new Error("rate must be a positive whole number of G_n/s");
|
|
10705
|
+
const rate = BigInt(value.trim());
|
|
10706
|
+
if (rate <= 0n) throw new Error("rate must be positive; use --whole-cap to restore the default");
|
|
10707
|
+
if (rate > U64_MAX) throw new Error("rate exceeds u64");
|
|
10708
|
+
return rate;
|
|
10709
|
+
}
|
|
10710
|
+
function capacityRateCall(api, rate) {
|
|
10711
|
+
const call = api.tx.computeChannel?.setCapacityReservationRate;
|
|
10712
|
+
if (typeof call !== "function") {
|
|
10713
|
+
throw new Error(`runtime spec ${api.runtimeVersion?.specVersion?.toString() ?? "?"} has no computeChannel.setCapacityReservationRate`);
|
|
10714
|
+
}
|
|
10715
|
+
return call(rate);
|
|
10716
|
+
}
|
|
10717
|
+
var text = (value) => value === void 0 || value === null ? null : value.toString();
|
|
10718
|
+
function showCmd() {
|
|
10719
|
+
return new Command39("show").description("Show the miner's reservation rate and free calibrated capacity at the finalized block").argument("[address]", "Miner SS58 address; defaults to the configured signer").option("--json", "Output as JSON").action(async (address, _opts, command) => {
|
|
10720
|
+
let client;
|
|
10721
|
+
try {
|
|
10722
|
+
const rpc = getRpc(command);
|
|
10723
|
+
client = address ? await createReadOnlyClient(rpc) : await createClient(rpc, getSigner(command));
|
|
10724
|
+
const miner = address ?? client.address;
|
|
10725
|
+
const { computeChannel: computeChannel9 } = await import("@flop-labs/sdk");
|
|
10726
|
+
const state = await computeChannel9.readMinerCapacityState(client.api, miner);
|
|
10727
|
+
if (!state) {
|
|
10728
|
+
print({ address: miner, rpc, supported: false }, getJson(command));
|
|
10729
|
+
return;
|
|
10730
|
+
}
|
|
10731
|
+
const capacity = computeChannel9.minerCapacity(state);
|
|
10732
|
+
print({
|
|
10733
|
+
address: miner,
|
|
10734
|
+
rpc,
|
|
10735
|
+
supported: true,
|
|
10736
|
+
block: text(capacity.block),
|
|
10737
|
+
rate_set: text(state.rate),
|
|
10738
|
+
cap_gflops_per_sec: text(capacity.cap),
|
|
10739
|
+
reserved_gn_per_sec: text(capacity.reserved),
|
|
10740
|
+
free_gn_per_sec: text(capacity.free),
|
|
10741
|
+
next_open_reserves: text(capacity.rate),
|
|
10742
|
+
next_open_fits: capacity.fits,
|
|
10743
|
+
...capacity.reason ? { reason: capacity.reason } : {},
|
|
10744
|
+
live_reservations: state.reservations.filter((r) => r.lapsesAt > state.block).length
|
|
10745
|
+
}, getJson(command));
|
|
10746
|
+
} catch (error) {
|
|
10747
|
+
commandError(error);
|
|
10748
|
+
} finally {
|
|
10749
|
+
await client?.disconnect();
|
|
10750
|
+
}
|
|
10751
|
+
});
|
|
10752
|
+
}
|
|
10753
|
+
function setCmd() {
|
|
10754
|
+
return new Command39("set").description("Set the G_n/s each new channel reserves from the signer's cap (miner call set_capacity_reservation_rate)").argument("[rate]", "Rate in G_n/s, a positive integer").option("--whole-cap", "Clear the rate: each new channel reserves the whole current cap").option("--dry-run", "Print the encoded call without signing").option("--json", "Output as JSON").action(async (value, opts, command) => {
|
|
10755
|
+
let client;
|
|
10756
|
+
try {
|
|
10757
|
+
if (value === void 0 === !opts.wholeCap) throw new Error("pass a rate or --whole-cap, not both");
|
|
10758
|
+
const rate = opts.wholeCap ? null : parseReservationRate(value);
|
|
10759
|
+
const rpc = getRpc(command);
|
|
10760
|
+
client = opts.dryRun ? await createReadOnlyClient(rpc) : await createClient(rpc, getSigner(command));
|
|
10761
|
+
const call = capacityRateCall(client.api, rate);
|
|
10762
|
+
if (opts.dryRun) {
|
|
10763
|
+
print({ action: "capacity_rate_call", rate_gn_per_sec: text(rate), call_data: call.method.toHex() }, getJson(command));
|
|
10764
|
+
return;
|
|
10765
|
+
}
|
|
10766
|
+
const txHash = await submitExtrinsic(client.api, client.signer, call);
|
|
10767
|
+
print({ action: "capacity_rate_set", address: client.address, rate_gn_per_sec: text(rate), tx_hash: txHash }, getJson(command));
|
|
10768
|
+
} catch (error) {
|
|
10769
|
+
commandError(error);
|
|
10770
|
+
} finally {
|
|
10771
|
+
await client?.disconnect();
|
|
10772
|
+
}
|
|
10773
|
+
});
|
|
10774
|
+
}
|
|
10775
|
+
function minerCapacityRateCmd() {
|
|
10776
|
+
return new Command39("capacity-rate").description("Show or set the capacity each channel opened against this miner reserves").addCommand(showCmd()).addCommand(setCmd());
|
|
10777
|
+
}
|
|
10778
|
+
|
|
10130
10779
|
// src/commands/miner-controller-info.ts
|
|
10131
10780
|
async function probeMinerSessionInfo(url, token, required = true) {
|
|
10132
10781
|
try {
|
|
@@ -10163,6 +10812,159 @@ async function probeMinerSessionInfo(url, token, required = true) {
|
|
|
10163
10812
|
}
|
|
10164
10813
|
}
|
|
10165
10814
|
|
|
10815
|
+
// src/commands/miner-running.ts
|
|
10816
|
+
import { decodeAddress } from "@polkadot/util-crypto";
|
|
10817
|
+
import { computeChannel as computeChannel6 } from "@flop-labs/sdk";
|
|
10818
|
+
var hex2 = (bytes4) => `0x${Buffer.from(bytes4).toString("hex")}`;
|
|
10819
|
+
var reason = (error) => error instanceof Error ? error.message : String(error);
|
|
10820
|
+
async function readLocalIdentity(sessionUrl, token, fetchImpl = fetch) {
|
|
10821
|
+
const base = sessionUrl.replace(/\/session\/info\/?$/, "");
|
|
10822
|
+
try {
|
|
10823
|
+
const info = await computeChannel6.fetchMinerSessionInfo(
|
|
10824
|
+
base,
|
|
10825
|
+
token ? { Authorization: `Bearer ${token}` } : {},
|
|
10826
|
+
AbortSignal.timeout(2e4),
|
|
10827
|
+
fetchImpl
|
|
10828
|
+
);
|
|
10829
|
+
if (!info.modelIdentity) {
|
|
10830
|
+
return { check: {
|
|
10831
|
+
id: "session:identity",
|
|
10832
|
+
status: "fail",
|
|
10833
|
+
required: true,
|
|
10834
|
+
detail: "worker reports no model identity",
|
|
10835
|
+
next: "check the worker logs for the model gate"
|
|
10836
|
+
} };
|
|
10837
|
+
}
|
|
10838
|
+
return { info, check: {
|
|
10839
|
+
id: "session:identity",
|
|
10840
|
+
status: "pass",
|
|
10841
|
+
required: true,
|
|
10842
|
+
detail: `${info.minerAddress} serves ${info.modelIdentity.registryModel} (${hex2(info.modelIdentity.modelHash)}), session key ${hex2(info.enclaveKey)}`
|
|
10843
|
+
} };
|
|
10844
|
+
} catch {
|
|
10845
|
+
return { check: {
|
|
10846
|
+
id: "session:identity",
|
|
10847
|
+
status: "fail",
|
|
10848
|
+
required: true,
|
|
10849
|
+
detail: "authenticated session info unavailable",
|
|
10850
|
+
next: "check --session-url and SESSION_AUTH_TOKEN"
|
|
10851
|
+
} };
|
|
10852
|
+
}
|
|
10853
|
+
}
|
|
10854
|
+
async function probeMinerChain(api, address) {
|
|
10855
|
+
try {
|
|
10856
|
+
const account = decodeAddress(address);
|
|
10857
|
+
const head = await api.rpc.chain.getFinalizedHead();
|
|
10858
|
+
const at = await api.at(head);
|
|
10859
|
+
const registration = await at.query.minerStaking.miners(account);
|
|
10860
|
+
const active = registration?.isSome ? registration.unwrap().isActive : void 0;
|
|
10861
|
+
const block = (await api.rpc.chain.getHeader(head)).number.toString();
|
|
10862
|
+
if (!(active?.isTrue ?? active === true)) {
|
|
10863
|
+
return {
|
|
10864
|
+
id: "chain:miner",
|
|
10865
|
+
status: "fail",
|
|
10866
|
+
required: true,
|
|
10867
|
+
detail: `${address} is not registered and active at finalized #${block}`,
|
|
10868
|
+
next: "flop miner register; flop miner status"
|
|
10869
|
+
};
|
|
10870
|
+
}
|
|
10871
|
+
const cap = (await api.rpc.state.call(
|
|
10872
|
+
"TrustRootApi_accepted_calibration_cap",
|
|
10873
|
+
hex2(Uint8Array.from([128, ...account])),
|
|
10874
|
+
head
|
|
10875
|
+
)).toU8a(true);
|
|
10876
|
+
if (cap.length === 1 && cap[0] === 0) {
|
|
10877
|
+
return {
|
|
10878
|
+
id: "chain:miner",
|
|
10879
|
+
status: "fail",
|
|
10880
|
+
required: true,
|
|
10881
|
+
detail: `${address} has no accepted calibration at finalized #${block}`,
|
|
10882
|
+
next: "check the worker's calibration logs"
|
|
10883
|
+
};
|
|
10884
|
+
}
|
|
10885
|
+
if (cap.length !== 9 || cap[0] !== 1) throw new Error("TrustRootApi_accepted_calibration_cap did not return an Option<u64>");
|
|
10886
|
+
const gflops = new DataView(cap.buffer, cap.byteOffset + 1, 8).getBigUint64(0, true);
|
|
10887
|
+
if (gflops === 0n) {
|
|
10888
|
+
return {
|
|
10889
|
+
id: "chain:miner",
|
|
10890
|
+
status: "fail",
|
|
10891
|
+
required: true,
|
|
10892
|
+
detail: `${address} has a zero calibration cap at finalized #${block}`,
|
|
10893
|
+
next: "check the worker's calibration logs"
|
|
10894
|
+
};
|
|
10895
|
+
}
|
|
10896
|
+
return {
|
|
10897
|
+
id: "chain:miner",
|
|
10898
|
+
status: "pass",
|
|
10899
|
+
required: true,
|
|
10900
|
+
detail: `registered, active, accepted calibration cap ${gflops} GFLOP/s at finalized #${block} (capacity bond not checked)`
|
|
10901
|
+
};
|
|
10902
|
+
} catch (error) {
|
|
10903
|
+
return { id: "chain:miner", status: "fail", required: true, detail: reason(error), next: "check --address and --rpc" };
|
|
10904
|
+
}
|
|
10905
|
+
}
|
|
10906
|
+
async function probePublicEndpoint(api, endpoint, address, local, fetchImpl = fetch) {
|
|
10907
|
+
let url;
|
|
10908
|
+
try {
|
|
10909
|
+
url = new URL(endpoint);
|
|
10910
|
+
} catch {
|
|
10911
|
+
}
|
|
10912
|
+
if (!url || url.protocol !== "https:" || url.pathname !== "/" || url.search || url.hash || url.username || url.password) {
|
|
10913
|
+
return [{
|
|
10914
|
+
id: "endpoint",
|
|
10915
|
+
status: "fail",
|
|
10916
|
+
required: true,
|
|
10917
|
+
detail: `${endpoint} is not a bare https://host[:port] endpoint`,
|
|
10918
|
+
next: "pass the endpoint the worker advertises"
|
|
10919
|
+
}];
|
|
10920
|
+
}
|
|
10921
|
+
const base = url.origin;
|
|
10922
|
+
const checks = [];
|
|
10923
|
+
try {
|
|
10924
|
+
const anonymous = await fetchImpl(`${base}/session/info`, { redirect: "error", signal: AbortSignal.timeout(1e4) });
|
|
10925
|
+
await anonymous.body?.cancel();
|
|
10926
|
+
checks.push(anonymous.status === 401 ? { id: "endpoint:auth", status: "pass", required: true, detail: `${base}/session/info refuses anonymous requests (401)` } : {
|
|
10927
|
+
id: "endpoint:auth",
|
|
10928
|
+
status: "fail",
|
|
10929
|
+
required: true,
|
|
10930
|
+
detail: anonymous.status === 200 ? `${base}/session/info answers without authentication` : `${base}/session/info answered ${anonymous.status}`,
|
|
10931
|
+
next: "route the public name to the worker; it must require the session token"
|
|
10932
|
+
});
|
|
10933
|
+
} catch (error) {
|
|
10934
|
+
checks.push({
|
|
10935
|
+
id: "endpoint:auth",
|
|
10936
|
+
status: "fail",
|
|
10937
|
+
required: true,
|
|
10938
|
+
detail: `${base} unreachable: ${reason(error)}`,
|
|
10939
|
+
next: "check DNS, TLS and the firewall from outside this host"
|
|
10940
|
+
});
|
|
10941
|
+
}
|
|
10942
|
+
try {
|
|
10943
|
+
const proven = await computeChannel6.fetchProvenSessionInfo(base, {
|
|
10944
|
+
genesisHash: api.genesisHash.toU8a(),
|
|
10945
|
+
account: decodeAddress(address),
|
|
10946
|
+
minerAddress: address,
|
|
10947
|
+
enclaveKey: local.enclaveKey,
|
|
10948
|
+
modelIdentity: local.modelIdentity
|
|
10949
|
+
}, fetchImpl, AbortSignal.timeout(1e4));
|
|
10950
|
+
checks.push({
|
|
10951
|
+
id: "endpoint:info-proof",
|
|
10952
|
+
status: "pass",
|
|
10953
|
+
required: true,
|
|
10954
|
+
detail: `fresh ready proof from the local session key for the local model (image ${proven.image?.version ?? "?"} ${proven.image?.revision ?? "?"})`
|
|
10955
|
+
});
|
|
10956
|
+
} catch (error) {
|
|
10957
|
+
checks.push({
|
|
10958
|
+
id: "endpoint:info-proof",
|
|
10959
|
+
status: "fail",
|
|
10960
|
+
required: true,
|
|
10961
|
+
detail: reason(error),
|
|
10962
|
+
next: "the public name must reach this worker, ready, on this chain; a probe from this host can differ from an outside one (hairpin NAT)"
|
|
10963
|
+
});
|
|
10964
|
+
}
|
|
10965
|
+
return checks;
|
|
10966
|
+
}
|
|
10967
|
+
|
|
10166
10968
|
// src/commands/miner.ts
|
|
10167
10969
|
function collectGpu(value, previous) {
|
|
10168
10970
|
return [...previous, value];
|
|
@@ -10268,7 +11070,7 @@ function buildMinerInitPlan(options) {
|
|
|
10268
11070
|
};
|
|
10269
11071
|
}
|
|
10270
11072
|
function minerInitCmd() {
|
|
10271
|
-
return new
|
|
11073
|
+
return new Command40("init").description("Print a phased miner onboarding plan").addOption(new Option6("--mode <mode>", "Miner mode").choices(["sim", "tdx", "cloud"]).default("sim")).option("--instance <number>", "Local guest instance number", "0").option("--provider <name>", "Cloud provider", "runpod").option(
|
|
10272
11074
|
"--gpu <SKU[:COUNT]>",
|
|
10273
11075
|
"Expected GPU inventory; repeat for mixed-SKU hosts",
|
|
10274
11076
|
collectGpu,
|
|
@@ -10311,10 +11113,20 @@ async function gpuCheck(mode) {
|
|
|
10311
11113
|
}
|
|
10312
11114
|
}
|
|
10313
11115
|
function minerDoctorCmd() {
|
|
10314
|
-
return new
|
|
11116
|
+
return new Command40("doctor").description("Check miner runtime, hardware, chain, and serving prerequisites").addOption(new Option6("--mode <mode>", "Miner mode").choices(["sim", "tdx", "cloud"]).default("sim")).option("--repo <path>", "flop-core checkout", process.cwd()).option("--session-url <url>", "Optional miner session info URL").option("--offline", "Skip chain RPC check").option("--running", "Check a running miner instead of setup prerequisites (needs --address or --session-url)").option("--address <ss58>", "Miner account: registered, active and calibrated at finality").option("--endpoint <url>", "Public https endpoint: refuses anonymous info and answers a fresh info-proof (needs --session-url)").addHelpText("after", [
|
|
11117
|
+
"",
|
|
11118
|
+
"--running skips the docker/just/checkout/GPU checks, so it runs from a release pack or another host.",
|
|
11119
|
+
"The session token is read from SESSION_AUTH_TOKEN, never a flag. --endpoint compares the public",
|
|
11120
|
+
"proof with the worker's own authenticated identity; the gateway listing itself is not visible here.",
|
|
11121
|
+
"From the pack's CLI image use --network host to reach the worker on 127.0.0.1."
|
|
11122
|
+
].join("\n")).option("--json", "Output as JSON").action(async (opts, command) => {
|
|
10315
11123
|
try {
|
|
11124
|
+
if (opts.running && !opts.address && !opts.sessionUrl) throw new Error("--running needs --address or --session-url");
|
|
11125
|
+
if (opts.endpoint && !opts.sessionUrl) throw new Error("--endpoint needs --session-url: the public proof is checked against the worker's own identity");
|
|
11126
|
+
if (opts.offline && (opts.address || opts.endpoint || opts.running)) throw new Error("--offline cannot check chain state; drop --offline");
|
|
10316
11127
|
const checks = [];
|
|
10317
|
-
if (opts.
|
|
11128
|
+
if (opts.running) {
|
|
11129
|
+
} else if (opts.mode === "cloud") {
|
|
10318
11130
|
checks.push(executableCheck("mise"), executableCheck("uv"));
|
|
10319
11131
|
checks.push(pathCheck(
|
|
10320
11132
|
"gpu-providers",
|
|
@@ -10332,14 +11144,34 @@ function minerDoctorCmd() {
|
|
|
10332
11144
|
checks.push(pathCheck("device:kvm", "/dev/kvm"));
|
|
10333
11145
|
}
|
|
10334
11146
|
}
|
|
11147
|
+
if (opts.sessionUrl) checks.push(await probeMinerSessionInfo(opts.sessionUrl, process.env.SESSION_AUTH_TOKEN));
|
|
11148
|
+
const local = opts.sessionUrl && (opts.running || opts.endpoint) ? await readLocalIdentity(opts.sessionUrl, process.env.SESSION_AUTH_TOKEN) : void 0;
|
|
11149
|
+
if (local) checks.push(local.check);
|
|
11150
|
+
const address = opts.address ?? local?.info?.minerAddress;
|
|
11151
|
+
if (opts.address && local?.info && local.info.minerAddress !== opts.address) {
|
|
11152
|
+
checks.push({
|
|
11153
|
+
id: "session:account",
|
|
11154
|
+
status: "fail",
|
|
11155
|
+
required: true,
|
|
11156
|
+
detail: `worker signs as ${local.info.minerAddress}, not ${opts.address}`,
|
|
11157
|
+
next: "check the miner key in the pack configuration"
|
|
11158
|
+
});
|
|
11159
|
+
}
|
|
10335
11160
|
if (!opts.offline) {
|
|
10336
11161
|
const rpc = getRpc(command);
|
|
10337
11162
|
const client = await createClient(rpc);
|
|
10338
|
-
|
|
10339
|
-
|
|
11163
|
+
try {
|
|
11164
|
+
const rpcCheck = await probeRpc(client.api, rpc);
|
|
11165
|
+
checks.push(rpcCheck);
|
|
11166
|
+
if (rpcCheck.status === "pass" && address) checks.push(await probeMinerChain(client.api, address));
|
|
11167
|
+
if (rpcCheck.status === "pass" && opts.endpoint && address && local?.info?.modelIdentity) {
|
|
11168
|
+
checks.push(...await probePublicEndpoint(client.api, opts.endpoint, address, local.info));
|
|
11169
|
+
}
|
|
11170
|
+
} finally {
|
|
11171
|
+
await client.disconnect();
|
|
11172
|
+
}
|
|
10340
11173
|
}
|
|
10341
|
-
|
|
10342
|
-
const report = finalizeDoctor("miner", opts.mode, checks);
|
|
11174
|
+
const report = finalizeDoctor("miner", opts.running ? "running" : opts.mode, checks);
|
|
10343
11175
|
printDoctor(report, getJson(command));
|
|
10344
11176
|
if (!report.ready) process.exitCode = 1;
|
|
10345
11177
|
} catch (error) {
|
|
@@ -10348,7 +11180,7 @@ function minerDoctorCmd() {
|
|
|
10348
11180
|
});
|
|
10349
11181
|
}
|
|
10350
11182
|
function minerStatusCmd() {
|
|
10351
|
-
return new
|
|
11183
|
+
return new Command40("status").description("Show miner registration, calibration, and optional session status").argument("[address]", "Miner SS58 address; defaults to the configured signer").option("--session-url <url>", "Optional miner session info URL").option(
|
|
10352
11184
|
"--expect-gpu <SKU[:COUNT]>",
|
|
10353
11185
|
"Expected inventory; repeat to check attested state",
|
|
10354
11186
|
collectGpu,
|
|
@@ -10398,7 +11230,7 @@ function minerStatusCmd() {
|
|
|
10398
11230
|
});
|
|
10399
11231
|
}
|
|
10400
11232
|
function minerSkusCmd() {
|
|
10401
|
-
return new
|
|
11233
|
+
return new Command40("skus").description("List GPU SKUs with active governance calibration ceilings").option("--json", "Output as JSON").action(async (_opts, command) => {
|
|
10402
11234
|
let client;
|
|
10403
11235
|
try {
|
|
10404
11236
|
const rpc = getRpc(command);
|
|
@@ -10422,7 +11254,7 @@ function minerSkusCmd() {
|
|
|
10422
11254
|
});
|
|
10423
11255
|
}
|
|
10424
11256
|
function minerRegisterCmd() {
|
|
10425
|
-
return new
|
|
11257
|
+
return new Command40("register").description("Register the signer as a miner and lock the runtime-defined self-stake").option("--commission <percent>", "Commission percent (0-20)", "10").option("--json", "Output as JSON").action(async (opts, command) => {
|
|
10426
11258
|
let client;
|
|
10427
11259
|
try {
|
|
10428
11260
|
const commission = Number(opts.commission);
|
|
@@ -10449,7 +11281,7 @@ function minerRegisterCmd() {
|
|
|
10449
11281
|
});
|
|
10450
11282
|
}
|
|
10451
11283
|
function minerDeclareGpuCmd() {
|
|
10452
|
-
return new
|
|
11284
|
+
return new Command40("declare-gpu").description("Publish the signer's self-reported GPU inventory as a flop.gpu-inventory.v1 remark (display-only)").option("--nvidia-smi <path>", "Output of `nvidia-smi --query-gpu=name,memory.total --format=csv,noheader,nounits` (- for stdin)").option("--detect", "Run nvidia-smi here (on the GPU host)").option("--retract", "Publish an empty inventory, withdrawing an earlier declaration").option("--dry-run", "Print the remark without signing").option("--json", "Output as JSON").action(async (opts, command) => {
|
|
10453
11285
|
let client;
|
|
10454
11286
|
try {
|
|
10455
11287
|
if ([opts.nvidiaSmi, opts.detect, opts.retract].filter(Boolean).length !== 1) {
|
|
@@ -10485,20 +11317,21 @@ function minerDeclareGpuCmd() {
|
|
|
10485
11317
|
});
|
|
10486
11318
|
}
|
|
10487
11319
|
function minerCmd() {
|
|
10488
|
-
const command = new
|
|
11320
|
+
const command = new Command40("miner").description("Onboard and operate inference miners");
|
|
10489
11321
|
command.addCommand(minerInitCmd());
|
|
10490
11322
|
command.addCommand(minerDoctorCmd());
|
|
10491
11323
|
command.addCommand(minerStatusCmd());
|
|
10492
11324
|
command.addCommand(minerSkusCmd());
|
|
10493
11325
|
command.addCommand(minerRegisterCmd());
|
|
10494
11326
|
command.addCommand(minerDeclareGpuCmd());
|
|
11327
|
+
command.addCommand(minerCapacityRateCmd());
|
|
10495
11328
|
command.addCommand(stakeInfoCmd());
|
|
10496
11329
|
return command;
|
|
10497
11330
|
}
|
|
10498
11331
|
|
|
10499
11332
|
// src/commands/validator.ts
|
|
10500
11333
|
import path4 from "node:path";
|
|
10501
|
-
import { Command as
|
|
11334
|
+
import { Command as Command41, Option as Option7 } from "commander";
|
|
10502
11335
|
import { formatFlop as formatFlop3 } from "@flop-labs/sdk";
|
|
10503
11336
|
function buildValidatorInitPlan(mode, minimumStake) {
|
|
10504
11337
|
const bringUp = mode === "local" ? "just run-node" : "infra/vultr/create-vm.sh <args>; infra/vultr/deploy-core.sh --vm-ip <TAILSCALE-IP>";
|
|
@@ -10548,7 +11381,7 @@ function buildValidatorInitPlan(mode, minimumStake) {
|
|
|
10548
11381
|
};
|
|
10549
11382
|
}
|
|
10550
11383
|
function validatorInitCmd() {
|
|
10551
|
-
return new
|
|
11384
|
+
return new Command41("init").description("Print a validator onboarding plan with the live minimum stake").addOption(new Option7("--mode <mode>", "Validator deployment mode").choices(["local", "vultr"]).default("local")).option("--offline", "Do not query the configured chain").option("--json", "Output as JSON").action(async (opts, command) => {
|
|
10552
11385
|
let client;
|
|
10553
11386
|
try {
|
|
10554
11387
|
let minimumStake;
|
|
@@ -10567,7 +11400,7 @@ function validatorInitCmd() {
|
|
|
10567
11400
|
});
|
|
10568
11401
|
}
|
|
10569
11402
|
function validatorDoctorCmd() {
|
|
10570
|
-
return new
|
|
11403
|
+
return new Command41("doctor").description("Check validator node and on-chain eligibility prerequisites").addOption(new Option7("--mode <mode>", "Validator deployment mode").choices(["local", "vultr"]).default("local")).option("--repo <path>", "flop-core checkout", process.cwd()).option("--offline", "Skip chain and account checks").option("--json", "Output as JSON").action(async (opts, command) => {
|
|
10571
11404
|
let client;
|
|
10572
11405
|
try {
|
|
10573
11406
|
const checks = [
|
|
@@ -10638,7 +11471,7 @@ function validatorDoctorCmd() {
|
|
|
10638
11471
|
});
|
|
10639
11472
|
}
|
|
10640
11473
|
function validatorStatusCmd() {
|
|
10641
|
-
return new
|
|
11474
|
+
return new Command41("status").description("Show validator stake, jail, performance, and verified-work state").argument("[address]", "Validator SS58 address; defaults to the configured signer").option("--json", "Output as JSON").action(async (address, opts, command) => {
|
|
10642
11475
|
let client;
|
|
10643
11476
|
try {
|
|
10644
11477
|
const rpc = getRpc(command);
|
|
@@ -10683,7 +11516,7 @@ function validatorStatusCmd() {
|
|
|
10683
11516
|
});
|
|
10684
11517
|
}
|
|
10685
11518
|
function validatorRegisterCmd() {
|
|
10686
|
-
return new
|
|
11519
|
+
return new Command41("register").description("Register the signer as a validator, bonding the effective minimum").option("--json", "Output as JSON").action(async (opts, command) => {
|
|
10687
11520
|
let client;
|
|
10688
11521
|
try {
|
|
10689
11522
|
const rpc = getRpc(command);
|
|
@@ -10713,7 +11546,7 @@ function validatorRegisterCmd() {
|
|
|
10713
11546
|
});
|
|
10714
11547
|
}
|
|
10715
11548
|
function validatorCommissionCmd() {
|
|
10716
|
-
return new
|
|
11549
|
+
return new Command41("commission").description("Set validator commission").argument("<percent>", "Commission percent (0-100)").option("--json", "Output as JSON").action(async (percent, opts, command) => {
|
|
10717
11550
|
let client;
|
|
10718
11551
|
try {
|
|
10719
11552
|
const value = Number(percent);
|
|
@@ -10733,7 +11566,7 @@ function validatorCommissionCmd() {
|
|
|
10733
11566
|
});
|
|
10734
11567
|
}
|
|
10735
11568
|
function validatorLifecycleCmd(name, description, sdkMethod) {
|
|
10736
|
-
return new
|
|
11569
|
+
return new Command41(name).description(description).option("--json", "Output as JSON").action(async (opts, command) => {
|
|
10737
11570
|
let client;
|
|
10738
11571
|
try {
|
|
10739
11572
|
const rpc = getRpc(command);
|
|
@@ -10749,7 +11582,7 @@ function validatorLifecycleCmd(name, description, sdkMethod) {
|
|
|
10749
11582
|
});
|
|
10750
11583
|
}
|
|
10751
11584
|
function validatorCmd() {
|
|
10752
|
-
const command = new
|
|
11585
|
+
const command = new Command41("validator").description("Onboard and operate validator nodes");
|
|
10753
11586
|
command.addCommand(validatorInitCmd());
|
|
10754
11587
|
command.addCommand(validatorDoctorCmd());
|
|
10755
11588
|
command.addCommand(validatorStatusCmd());
|
|
@@ -10762,7 +11595,7 @@ function validatorCmd() {
|
|
|
10762
11595
|
}
|
|
10763
11596
|
|
|
10764
11597
|
// src/commands/network-status.ts
|
|
10765
|
-
import { Command as
|
|
11598
|
+
import { Command as Command42 } from "commander";
|
|
10766
11599
|
|
|
10767
11600
|
// src/network.ts
|
|
10768
11601
|
async function probeRpcChain(network, rpc) {
|
|
@@ -10796,7 +11629,7 @@ function networkStatusCmd(probeRpc2 = probeRpcChain, probeFaucet = async (endpoi
|
|
|
10796
11629
|
return { ...identity, ready: null, readinessError: error instanceof Error ? error.message : String(error) };
|
|
10797
11630
|
}
|
|
10798
11631
|
}) {
|
|
10799
|
-
return new
|
|
11632
|
+
return new Command42("status").description("Show resolved network endpoints and live chain identity").option("--offline", "Resolve configured endpoints without connecting").option("--json", "Output as JSON").action(async (opts, command) => {
|
|
10800
11633
|
const resolved = resolveNetwork(command);
|
|
10801
11634
|
let rpcInfo = null;
|
|
10802
11635
|
let rpcError;
|
|
@@ -10850,9 +11683,9 @@ function networkStatusCmd(probeRpc2 = probeRpcChain, probeFaucet = async (endpoi
|
|
|
10850
11683
|
}
|
|
10851
11684
|
|
|
10852
11685
|
// src/commands/faucet-request.ts
|
|
10853
|
-
import { Command as
|
|
11686
|
+
import { Command as Command43 } from "commander";
|
|
10854
11687
|
function faucetRequestCmd(probeRpc2 = probeRpcChain) {
|
|
10855
|
-
return new
|
|
11688
|
+
return new Command43("request").description("Request dev/test FLOP after verifying faucet and RPC chain identity").argument("<address>", "SS58 address to fund").option("--amount <flop>", "Requested FLOP amount; server policy may cap it").option("--endpoint <url>", "Explicit faucet endpoint override").option("--json", "Output as JSON").action(async (address, opts, command) => {
|
|
10856
11689
|
try {
|
|
10857
11690
|
const resolved = resolveNetwork(command);
|
|
10858
11691
|
const endpoint = opts.endpoint ?? resolved.profile.faucet;
|
|
@@ -10879,6 +11712,25 @@ function faucetRequestCmd(probeRpc2 = probeRpcChain) {
|
|
|
10879
11712
|
amount,
|
|
10880
11713
|
maxRetries: 1
|
|
10881
11714
|
});
|
|
11715
|
+
if (!result.success && result.outcome === "unknown") {
|
|
11716
|
+
printError({
|
|
11717
|
+
code: "FAUCET_OUTCOME_UNKNOWN",
|
|
11718
|
+
message: `${result.error ?? "Faucet outcome unknown"}; reconcile the original request before requesting again`,
|
|
11719
|
+
retryable: false,
|
|
11720
|
+
next: "reconcile the original request before requesting again",
|
|
11721
|
+
network: resolved.network,
|
|
11722
|
+
address,
|
|
11723
|
+
faucet: endpoint,
|
|
11724
|
+
genesis_hash: rpcChain.genesisHash,
|
|
11725
|
+
...result.status !== void 0 ? { status: result.status } : {},
|
|
11726
|
+
...result.reason ? { reason: result.reason } : {},
|
|
11727
|
+
...result.retryAfterSeconds !== void 0 ? { retry_after_seconds: result.retryAfterSeconds } : {},
|
|
11728
|
+
...result.txHash ? { tx_hash: result.txHash } : {},
|
|
11729
|
+
...result.amount !== void 0 ? { amount: result.amount } : {}
|
|
11730
|
+
}, getJson(command));
|
|
11731
|
+
process.exitCode = 1;
|
|
11732
|
+
return;
|
|
11733
|
+
}
|
|
10882
11734
|
if (!result.success) {
|
|
10883
11735
|
commandError(new Error(result.error ?? "Faucet request failed"), {
|
|
10884
11736
|
code: result.status === 429 ? "RATE_LIMITED" : "FAUCET_REQUEST_FAILED",
|
|
@@ -10893,7 +11745,9 @@ function faucetRequestCmd(probeRpc2 = probeRpcChain) {
|
|
|
10893
11745
|
amount: result.amount,
|
|
10894
11746
|
tx_hash: result.txHash,
|
|
10895
11747
|
faucet: endpoint,
|
|
10896
|
-
genesis_hash: rpcChain.genesisHash
|
|
11748
|
+
genesis_hash: rpcChain.genesisHash,
|
|
11749
|
+
status: "unconfirmed",
|
|
11750
|
+
recovery: "reconcile the original request before requesting again"
|
|
10897
11751
|
}, getJson(command));
|
|
10898
11752
|
} catch (error) {
|
|
10899
11753
|
commandError(error, {
|
|
@@ -10905,16 +11759,16 @@ function faucetRequestCmd(probeRpc2 = probeRpcChain) {
|
|
|
10905
11759
|
}
|
|
10906
11760
|
|
|
10907
11761
|
// src/commands/chat.ts
|
|
10908
|
-
import { Command as
|
|
11762
|
+
import { Command as Command44 } from "commander";
|
|
10909
11763
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
10910
11764
|
import { homedir as homedir4 } from "node:os";
|
|
10911
|
-
import { join as
|
|
11765
|
+
import { join as join10 } from "node:path";
|
|
10912
11766
|
import { createInterface } from "node:readline/promises";
|
|
10913
11767
|
import { stdin, stdout } from "node:process";
|
|
10914
11768
|
|
|
10915
11769
|
// src/conversations.ts
|
|
10916
|
-
import { mkdir as
|
|
10917
|
-
import { dirname as
|
|
11770
|
+
import { mkdir as mkdir14, readFile as readFile8, rename as rename9, writeFile as writeFile11 } from "node:fs/promises";
|
|
11771
|
+
import { dirname as dirname8 } from "node:path";
|
|
10918
11772
|
var MAX_CONVERSATIONS = 100;
|
|
10919
11773
|
var MAX_CONVERSATION_MESSAGES = 100;
|
|
10920
11774
|
var MAX_MESSAGE_CHARS = 16384;
|
|
@@ -10933,11 +11787,11 @@ var ConversationStore = class {
|
|
|
10933
11787
|
}
|
|
10934
11788
|
}
|
|
10935
11789
|
async write(data) {
|
|
10936
|
-
await
|
|
11790
|
+
await mkdir14(dirname8(this.path), { recursive: true, mode: 448 });
|
|
10937
11791
|
const temp = `${this.path}.${process.pid}.tmp`;
|
|
10938
11792
|
await writeFile11(temp, `${JSON.stringify(data, null, 2)}
|
|
10939
11793
|
`, { mode: 384 });
|
|
10940
|
-
await
|
|
11794
|
+
await rename9(temp, this.path);
|
|
10941
11795
|
}
|
|
10942
11796
|
async list() {
|
|
10943
11797
|
return Object.values((await this.read()).conversations).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
@@ -10972,7 +11826,7 @@ function conversationPrompt(messages) {
|
|
|
10972
11826
|
|
|
10973
11827
|
// src/commands/chat.ts
|
|
10974
11828
|
function chatCmd() {
|
|
10975
|
-
return new
|
|
11829
|
+
return new Command44("chat").description("Start or resume a persistent FLOP chat, optionally through a bounded session").argument("[prompt]", "One-shot prompt; omit for interactive chat").option("--conversation <id>", "Resume a conversation ID").option("--model <id>", "32-byte model hash or exact registered name from flop model list").option("--session-owner <address>", "Use --signer as this owner's session delegate").option("--max-flop <n>", "Per-turn FLOP cap", "5").option("--tee-requirements <hex>", "Registered HTLC appraisal commitments (100-byte SCALE hex)", process.env.FLOP_HTLC_TEE_REQUIREMENTS).option("--skip-model-check", "Allow an unregistered model (local development only)").option("--history", "List stored conversations without connecting").option("--delete <id>", "Delete a stored conversation without connecting").option("--store <path>", "Conversation store path", join10(homedir4(), ".flop", "conversations.json")).option("--json", "Output as JSON").action(async (prompt, opts, command) => {
|
|
10976
11830
|
const store = new ConversationStore(opts.store);
|
|
10977
11831
|
let agent;
|
|
10978
11832
|
try {
|
|
@@ -11097,7 +11951,7 @@ function chatCmd() {
|
|
|
11097
11951
|
}
|
|
11098
11952
|
|
|
11099
11953
|
// src/commands/tx-build.ts
|
|
11100
|
-
import { Command as
|
|
11954
|
+
import { Command as Command45 } from "commander";
|
|
11101
11955
|
import { writeFile as writeFile12 } from "node:fs/promises";
|
|
11102
11956
|
import { execFile as execFile5 } from "node:child_process";
|
|
11103
11957
|
import { promisify as promisify5 } from "node:util";
|
|
@@ -11164,7 +12018,7 @@ function assertEnvelopeChain(api, envelope) {
|
|
|
11164
12018
|
|
|
11165
12019
|
// src/commands/tx-build.ts
|
|
11166
12020
|
function txBuildCmd() {
|
|
11167
|
-
return new
|
|
12021
|
+
return new Command45("build").description("Build an immortal unsigned transaction envelope for air-gapped signing").requiredOption("--address <address>", "Signing account").requiredOption("--call <hex>", "SCALE call hex").option("--out <path>", "Write envelope JSON").option("--qr", "Render the signing payload using installed qrencode").option("--json", "Output as JSON").action(async (opts, command) => {
|
|
11168
12022
|
let client;
|
|
11169
12023
|
try {
|
|
11170
12024
|
const resolved = resolveNetwork(command);
|
|
@@ -11209,10 +12063,10 @@ function txBuildCmd() {
|
|
|
11209
12063
|
}
|
|
11210
12064
|
|
|
11211
12065
|
// src/commands/tx-broadcast.ts
|
|
11212
|
-
import { Command as
|
|
12066
|
+
import { Command as Command46 } from "commander";
|
|
11213
12067
|
import { readFile as readFile9 } from "node:fs/promises";
|
|
11214
12068
|
function txBroadcastCmd() {
|
|
11215
|
-
return new
|
|
12069
|
+
return new Command46("broadcast").description("Import an offline signature, reject chain drift, and broadcast").argument("<envelope>", "Unsigned envelope JSON").requiredOption("--signature <hex>", "SCALE MultiSignature from the offline signer").option("--allow-nonce-drift", "Broadcast even if account nonce has changed").option("--json", "Output as JSON").action(async (path5, opts, command) => {
|
|
11216
12070
|
let client;
|
|
11217
12071
|
try {
|
|
11218
12072
|
if (!/^0x[0-9a-f]+$/i.test(opts.signature)) throw new Error("--signature must be 0x hex");
|
|
@@ -11248,7 +12102,7 @@ function txBroadcastCmd() {
|
|
|
11248
12102
|
}
|
|
11249
12103
|
|
|
11250
12104
|
// src/commands/multisig.ts
|
|
11251
|
-
import { Command as
|
|
12105
|
+
import { Command as Command47 } from "commander";
|
|
11252
12106
|
import { createKeyMulti, encodeAddress, sortAddresses } from "@polkadot/util-crypto";
|
|
11253
12107
|
import { blake2AsHex } from "@polkadot/util-crypto";
|
|
11254
12108
|
function otherSignatories(signer, values) {
|
|
@@ -11269,8 +12123,8 @@ function timepoint(opts) {
|
|
|
11269
12123
|
return { height: parsePositiveInt(opts.height, "--height"), index };
|
|
11270
12124
|
}
|
|
11271
12125
|
function multisigCmd() {
|
|
11272
|
-
const command = new
|
|
11273
|
-
command.addCommand(new
|
|
12126
|
+
const command = new Command47("multisig").description("Participate in Substrate multisig transactions");
|
|
12127
|
+
command.addCommand(new Command47("address").description("Derive the deterministic multisig address").requiredOption("--threshold <n>", "Approval threshold").requiredOption("--signatory <address...>", "All signatory addresses").option("--json", "Output as JSON").action(async (opts, cmd) => {
|
|
11274
12128
|
try {
|
|
11275
12129
|
const threshold = parsePositiveInt(opts.threshold, "--threshold");
|
|
11276
12130
|
const sorted = sortAddresses(opts.signatory, 42);
|
|
@@ -11285,7 +12139,7 @@ function multisigCmd() {
|
|
|
11285
12139
|
commandError(error);
|
|
11286
12140
|
}
|
|
11287
12141
|
}));
|
|
11288
|
-
command.addCommand(new
|
|
12142
|
+
command.addCommand(new Command47("propose").description("Create or execute a multisig call").requiredOption("--threshold <n>", "Approval threshold").requiredOption("--other <address...>", "Other signatories (exclude configured signer)").requiredOption("--call <hex>", "SCALE call hex").option("--max-ref-time <n>", "Maximum call refTime").option("--max-proof-size <n>", "Maximum call proofSize").option("--json", "Output as JSON").action(async (opts, cmd) => {
|
|
11289
12143
|
let client;
|
|
11290
12144
|
try {
|
|
11291
12145
|
client = await createClient(getRpc(cmd), getSigner(cmd));
|
|
@@ -11308,7 +12162,7 @@ function multisigCmd() {
|
|
|
11308
12162
|
await client?.disconnect();
|
|
11309
12163
|
}
|
|
11310
12164
|
}));
|
|
11311
|
-
command.addCommand(new
|
|
12165
|
+
command.addCommand(new Command47("approve").description("Approve a pending multisig call hash").requiredOption("--threshold <n>", "Approval threshold").requiredOption("--other <address...>", "Other signatories").requiredOption("--call-hash <hex>", "Pending call hash").requiredOption("--height <n>", "Timepoint block height").requiredOption("--index <n>", "Timepoint extrinsic index").option("--max-ref-time <n>", "Maximum call refTime").option("--max-proof-size <n>", "Maximum call proofSize").option("--json", "Output as JSON").action(async (opts, cmd) => {
|
|
11312
12166
|
let client;
|
|
11313
12167
|
try {
|
|
11314
12168
|
client = await createClient(getRpc(cmd), getSigner(cmd));
|
|
@@ -11327,7 +12181,7 @@ function multisigCmd() {
|
|
|
11327
12181
|
await client?.disconnect();
|
|
11328
12182
|
}
|
|
11329
12183
|
}));
|
|
11330
|
-
command.addCommand(new
|
|
12184
|
+
command.addCommand(new Command47("cancel").description("Cancel a pending multisig as its depositor").requiredOption("--threshold <n>", "Approval threshold").requiredOption("--other <address...>", "Other signatories").requiredOption("--call-hash <hex>", "Pending call hash").requiredOption("--height <n>", "Timepoint block height").requiredOption("--index <n>", "Timepoint extrinsic index").option("--json", "Output as JSON").action(async (opts, cmd) => {
|
|
11331
12185
|
let client;
|
|
11332
12186
|
try {
|
|
11333
12187
|
client = await createClient(getRpc(cmd), getSigner(cmd));
|
|
@@ -11344,7 +12198,7 @@ function multisigCmd() {
|
|
|
11344
12198
|
await client?.disconnect();
|
|
11345
12199
|
}
|
|
11346
12200
|
}));
|
|
11347
|
-
command.addCommand(new
|
|
12201
|
+
command.addCommand(new Command47("list").alias("status").description("List pending calls for a multisig account").requiredOption("--account <address>", "Multisig account").option("--json", "Output as JSON").action(async (opts, cmd) => {
|
|
11348
12202
|
let client;
|
|
11349
12203
|
try {
|
|
11350
12204
|
client = await createClient(getRpc(cmd));
|
|
@@ -11364,10 +12218,10 @@ function multisigCmd() {
|
|
|
11364
12218
|
}
|
|
11365
12219
|
|
|
11366
12220
|
// src/commands/channel.ts
|
|
11367
|
-
import { Command as
|
|
12221
|
+
import { Command as Command48 } from "commander";
|
|
11368
12222
|
function channelCmd() {
|
|
11369
|
-
const command = new
|
|
11370
|
-
command.addCommand(new
|
|
12223
|
+
const command = new Command48("channel").description("Inspect and finalize compute channels");
|
|
12224
|
+
command.addCommand(new Command48("list").description("List on-chain compute channels").option("--agent <address>", "Filter by paying agent").option("--miner <address>", "Filter by serving miner").option("--include-closed", "Include terminal channels").option("--json", "Output as JSON").action(async (opts, cmd) => {
|
|
11371
12225
|
let client;
|
|
11372
12226
|
try {
|
|
11373
12227
|
client = await createClient(getRpc(cmd));
|
|
@@ -11395,7 +12249,7 @@ function channelCmd() {
|
|
|
11395
12249
|
await client?.disconnect();
|
|
11396
12250
|
}
|
|
11397
12251
|
}));
|
|
11398
|
-
command.addCommand(new
|
|
12252
|
+
command.addCommand(new Command48("finalize").description("Finalize a force-settled channel after its challenge window").argument("<channel-id>", "0x channel ID").option("--json", "Output as JSON").action(async (channelId, _opts, cmd) => {
|
|
11399
12253
|
let client;
|
|
11400
12254
|
try {
|
|
11401
12255
|
client = await createClient(getRpc(cmd), getSigner(cmd));
|
|
@@ -11437,7 +12291,7 @@ function timestamp(value, label) {
|
|
|
11437
12291
|
if (!Number.isFinite(parsed)) throw new Error(`${label} must be an ISO-8601 timestamp`);
|
|
11438
12292
|
return parsed;
|
|
11439
12293
|
}
|
|
11440
|
-
function
|
|
12294
|
+
function decimal2(value, label) {
|
|
11441
12295
|
if (!DECIMAL.test(value)) throw new Error(`${label} must be a non-negative decimal string`);
|
|
11442
12296
|
}
|
|
11443
12297
|
function positiveInt(value, label) {
|
|
@@ -11465,6 +12319,12 @@ function parseCanonicalCatalog(value, options = {}) {
|
|
|
11465
12319
|
if (!HASH.test(record.decode_policy_hash)) throw new Error(`${label}.decode_policy_hash must be lowercase 32-byte hex`);
|
|
11466
12320
|
if (!Number.isSafeInteger(record.precision) || record.precision < 0 || record.precision > 5) throw new Error(`${label}.precision is invalid`);
|
|
11467
12321
|
if (!QUANTIZATIONS.has(record.quantization)) throw new Error(`${label}.quantization is invalid`);
|
|
12322
|
+
if (record.precision_detail !== void 0) {
|
|
12323
|
+
if (!record.precision_detail || typeof record.precision_detail !== "object" || Array.isArray(record.precision_detail)) throw new Error(`${label}.precision_detail is invalid`);
|
|
12324
|
+
for (const field2 of ["activations", "kv_cache"]) {
|
|
12325
|
+
if (record.precision_detail[field2] !== void 0 && !QUANTIZATIONS.has(record.precision_detail[field2])) throw new Error(`${label}.precision_detail.${field2} is invalid`);
|
|
12326
|
+
}
|
|
12327
|
+
}
|
|
11468
12328
|
if (!Array.isArray(record.input_modalities) || record.input_modalities.length < 1 || record.input_modalities.some((v) => !INPUT_MODALITIES.has(v))) throw new Error(`${label}.input_modalities is invalid`);
|
|
11469
12329
|
if (!Array.isArray(record.output_modalities) || record.output_modalities.length < 1 || record.output_modalities.some((v) => !OUTPUT_MODALITIES.has(v))) throw new Error(`${label}.output_modalities is invalid`);
|
|
11470
12330
|
if (!Array.isArray(record.supported_sampling_parameters) || record.supported_sampling_parameters.some((v) => !SAMPLING.has(v))) throw new Error(`${label}.supported_sampling_parameters is invalid`);
|
|
@@ -11495,9 +12355,9 @@ function parseCanonicalCatalog(value, options = {}) {
|
|
|
11495
12355
|
if (!price.version || !price.miner_address || !price.flop_usd_source) throw new Error(`${priceLabel} provenance is incomplete`);
|
|
11496
12356
|
if (pricedMiners.has(price.miner_address)) throw new Error(`${priceLabel} duplicates a miner price`);
|
|
11497
12357
|
pricedMiners.add(price.miner_address);
|
|
11498
|
-
for (const field2 of ["flop_per_gn", "gn_per_prompt_token", "gn_per_completion_token", "flop_usd"])
|
|
11499
|
-
if (price.request_usd !== void 0)
|
|
11500
|
-
if (price.image_usd !== void 0)
|
|
12358
|
+
for (const field2 of ["flop_per_gn", "gn_per_prompt_token", "gn_per_completion_token", "flop_usd"]) decimal2(price[field2], `${priceLabel}.${field2}`);
|
|
12359
|
+
if (price.request_usd !== void 0) decimal2(price.request_usd, `${priceLabel}.request_usd`);
|
|
12360
|
+
if (price.image_usd !== void 0) decimal2(price.image_usd, `${priceLabel}.image_usd`);
|
|
11501
12361
|
timestamp(price.ratified_at, `${priceLabel}.ratified_at`);
|
|
11502
12362
|
if (timestamp(price.effective_at, `${priceLabel}.effective_at`) >= timestamp(price.expires_at, `${priceLabel}.expires_at`)) throw new Error(`${priceLabel} has an empty validity window`);
|
|
11503
12363
|
if (price.rounding?.mode !== "ceil" || !Number.isSafeInteger(price.rounding.decimal_places) || price.rounding.decimal_places < 0 || price.rounding.decimal_places > 18) throw new Error(`${priceLabel}.rounding is invalid`);
|
|
@@ -11511,7 +12371,7 @@ function parseCanonicalCatalog(value, options = {}) {
|
|
|
11511
12371
|
const timed = Number.isSafeInteger(override.utc_start) && Number.isSafeInteger(override.utc_end) && Number(override.utc_start) >= 0 && Number(override.utc_start) <= 2359 && Number(override.utc_end) >= 0 && Number(override.utc_end) <= 2359;
|
|
11512
12372
|
if (longContext === timed) throw new Error(`${overrideLabel} must have exactly one supported condition kind`);
|
|
11513
12373
|
for (const field2 of ["prompt", "completion", "request", "image", "input_cache_read", "internal_reasoning"]) {
|
|
11514
|
-
if (override[field2] !== void 0)
|
|
12374
|
+
if (override[field2] !== void 0) decimal2(String(override[field2]), `${overrideLabel}.${field2}`);
|
|
11515
12375
|
}
|
|
11516
12376
|
}
|
|
11517
12377
|
}
|
|
@@ -11522,8 +12382,8 @@ function parseCanonicalCatalog(value, options = {}) {
|
|
|
11522
12382
|
});
|
|
11523
12383
|
}
|
|
11524
12384
|
function parts(value) {
|
|
11525
|
-
const [whole,
|
|
11526
|
-
return { value: BigInt(whole +
|
|
12385
|
+
const [whole, fraction2 = ""] = value.split(".");
|
|
12386
|
+
return { value: BigInt(whole + fraction2), scale: fraction2.length };
|
|
11527
12387
|
}
|
|
11528
12388
|
function multiply(values, places) {
|
|
11529
12389
|
const product = values.map(parts).reduce((acc, item) => ({ value: acc.value * item.value, scale: acc.scale + item.scale }), { value: 1n, scale: 0 });
|
|
@@ -11590,13 +12450,20 @@ function buildLiveCatalog(records, sessions, now = Date.now(), policy) {
|
|
|
11590
12450
|
for (const field2 of ["prompt", "completion", "request", "image", "input_cache_read", "internal_reasoning"]) if (compareDecimal(candidate[field2], current[field2]) > 0) next[field2] = candidate[field2];
|
|
11591
12451
|
return next;
|
|
11592
12452
|
}, void 0);
|
|
12453
|
+
const ready = is_ready && capacity_tpm > 0;
|
|
11593
12454
|
return {
|
|
11594
12455
|
record,
|
|
11595
|
-
is_ready:
|
|
11596
|
-
capacity_tpm:
|
|
12456
|
+
is_ready: ready,
|
|
12457
|
+
capacity_tpm: ready ? capacity_tpm : null,
|
|
12458
|
+
// Report the oldest contributing observation and earliest evidence expiry.
|
|
12459
|
+
// These describe evidence freshness, not a guarantee of continued availability.
|
|
12460
|
+
...ready ? {
|
|
12461
|
+
capacity_observed_at: priced.reduce((oldest, item) => timestamp(item.session.capacity_observed_at, "capacity_observed_at") < timestamp(oldest, "capacity_observed_at") ? item.session.capacity_observed_at : oldest, priced[0].session.capacity_observed_at),
|
|
12462
|
+
capacity_expires_at: priced.reduce((earliest, item) => timestamp(item.session.capacity_expires_at, "capacity_expires_at") < timestamp(earliest, "capacity_expires_at") ? item.session.capacity_expires_at : earliest, priced[0].session.capacity_expires_at)
|
|
12463
|
+
} : {},
|
|
11597
12464
|
datacenters: is_ready ? [...new Set(priced.map((item) => item.session.datacenter.country_code))].sort().map((country_code) => ({ country_code })) : [],
|
|
11598
12465
|
...is_ready && conservative ? { pricing: conservative } : {},
|
|
11599
|
-
price_sources: is_ready ? priced.map(({ session, price }) => ({ miner_address: session.miner_address, version: price.version, source: price.flop_usd_source, effective_at: price.effective_at, expires_at: price.expires_at })).sort((a, b) => a.miner_address.localeCompare(b.miner_address)) : [],
|
|
12466
|
+
price_sources: is_ready ? priced.map(({ session, price }) => ({ miner_address: session.miner_address, version: price.version, source: price.flop_usd_source, effective_at: price.effective_at, expires_at: price.expires_at, ratified_at: price.ratified_at, flop_per_gn: price.flop_per_gn, gn_per_prompt_token: price.gn_per_prompt_token, gn_per_completion_token: price.gn_per_completion_token, flop_usd: price.flop_usd, rounding: price.rounding })).sort((a, b) => a.miner_address.localeCompare(b.miner_address)) : [],
|
|
11600
12467
|
admitted_sessions: is_ready ? priced.map((item) => item.session.session_id).sort() : []
|
|
11601
12468
|
};
|
|
11602
12469
|
});
|
|
@@ -11841,12 +12708,12 @@ function generateGatewayConfig(input) {
|
|
|
11841
12708
|
}
|
|
11842
12709
|
|
|
11843
12710
|
// src/commands/gateway-allowlist.ts
|
|
11844
|
-
import { Command as
|
|
12711
|
+
import { Command as Command49 } from "commander";
|
|
11845
12712
|
var PRECISIONS5 = ["FP32", "FP16", "BF16", "INT8", "INT4", "Mixed"];
|
|
11846
12713
|
function field(value, camel, snake) {
|
|
11847
12714
|
return value?.[camel] ?? value?.[snake];
|
|
11848
12715
|
}
|
|
11849
|
-
function
|
|
12716
|
+
function text2(value, label) {
|
|
11850
12717
|
const json = value?.toJSON?.() ?? value;
|
|
11851
12718
|
if (typeof json === "string") {
|
|
11852
12719
|
return json.startsWith("0x") ? Buffer.from(json.slice(2), "hex").toString("utf8") : json;
|
|
@@ -11854,7 +12721,7 @@ function text(value, label) {
|
|
|
11854
12721
|
if (Array.isArray(json)) return Buffer.from(json).toString("utf8");
|
|
11855
12722
|
throw new Error(`on-chain ${label} is invalid`);
|
|
11856
12723
|
}
|
|
11857
|
-
function
|
|
12724
|
+
function hex3(value, label) {
|
|
11858
12725
|
const rendered = value?.toHex?.() ?? value?.toJSON?.() ?? value;
|
|
11859
12726
|
if (typeof rendered !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(rendered)) {
|
|
11860
12727
|
throw new Error(`on-chain ${label} is not a 32-byte hash`);
|
|
@@ -11874,8 +12741,8 @@ async function fetchMinerSessionInfo(endpoint, authToken, signal) {
|
|
|
11874
12741
|
if (anonymous.status !== 401) throw new Error("miner /session/info does not enforce bearer authentication");
|
|
11875
12742
|
const response = await fetch(infoUrl, { redirect: "error", headers: { Authorization: `Bearer ${authToken}` }, signal });
|
|
11876
12743
|
if (!response.ok) throw new Error(`miner /session/info returned HTTP ${response.status}`);
|
|
11877
|
-
const { computeChannel:
|
|
11878
|
-
return JSON.parse(await
|
|
12744
|
+
const { computeChannel: computeChannel9 } = await import("@flop-labs/sdk");
|
|
12745
|
+
return JSON.parse(await computeChannel9.readMinerResponse(response, 16 * 1024));
|
|
11879
12746
|
}
|
|
11880
12747
|
async function readChainSnapshot(api, miner, pinnedHead) {
|
|
11881
12748
|
const identity = miner.modelIdentity;
|
|
@@ -11907,16 +12774,16 @@ async function readChainSnapshot(api, miner, pinnedHead) {
|
|
|
11907
12774
|
`model ${identity.modelHash} is ${status}, not Verified; refusing to generate a gateway catalog`
|
|
11908
12775
|
);
|
|
11909
12776
|
}
|
|
11910
|
-
const measuredRoot = rootRaw?.isSome ?
|
|
11911
|
-
const allowedDecodePolicyHashes = policiesRaw?.isSome ? Array.from(policiesRaw.unwrap(), (item) =>
|
|
12777
|
+
const measuredRoot = rootRaw?.isSome ? hex3(rootRaw.unwrap(), "measuredRoot") : null;
|
|
12778
|
+
const allowedDecodePolicyHashes = policiesRaw?.isSome ? Array.from(policiesRaw.unwrap(), (item) => hex3(item, "allowedDecodePolicies")) : [];
|
|
11912
12779
|
const lease = BigInt(at.consts.hwCalibration.calibrationLeaseBlocks.toString());
|
|
11913
12780
|
const calibration = calibrationRaw?.isSome ? calibrationRaw.unwrap() : null;
|
|
11914
12781
|
const acceptedAt = calibration ? BigInt(field(calibration, "acceptedAt", "accepted_at").toString()) : 0n;
|
|
11915
12782
|
return {
|
|
11916
12783
|
model: {
|
|
11917
|
-
modelHash:
|
|
11918
|
-
registryModel:
|
|
11919
|
-
registryVersion:
|
|
12784
|
+
modelHash: hex3(field(modelJson, "modelHash", "model_hash"), "modelHash"),
|
|
12785
|
+
registryModel: text2(field(modelJson, "modelName", "model_name"), "modelName"),
|
|
12786
|
+
registryVersion: text2(field(modelJson, "version", "version"), "version")
|
|
11920
12787
|
},
|
|
11921
12788
|
measuredRoot,
|
|
11922
12789
|
allowedDecodePolicyHashes,
|
|
@@ -11930,7 +12797,7 @@ async function readChainSnapshot(api, miner, pinnedHead) {
|
|
|
11930
12797
|
};
|
|
11931
12798
|
}
|
|
11932
12799
|
function gatewayAllowlistCmd() {
|
|
11933
|
-
return new
|
|
12800
|
+
return new Command49("allowlist").description("Generate gateway allowlist and model catalog from a live miner and chain").requiredOption("--miner <url>", "Authenticated miner base URL").requiredOption("--auth-token-env <name>", "Environment variable containing the miner bearer token").requiredOption("--attestation <path>", "Operator/provider deployment, catalog, and price evidence JSON").option("--json", "Output as JSON").action(async (opts, command) => {
|
|
11934
12801
|
let client;
|
|
11935
12802
|
try {
|
|
11936
12803
|
const authToken = process.env[opts.authTokenEnv];
|
|
@@ -11956,13 +12823,13 @@ function gatewayAllowlistCmd() {
|
|
|
11956
12823
|
}
|
|
11957
12824
|
|
|
11958
12825
|
// src/commands/gateway-renew.ts
|
|
11959
|
-
import { open as open3, readFile as readFile11, rename as
|
|
11960
|
-
import { createHash as
|
|
12826
|
+
import { open as open3, readFile as readFile11, rename as rename10, rm as rm7, writeFile as writeFile13 } from "node:fs/promises";
|
|
12827
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
11961
12828
|
import { request as request2 } from "node:http";
|
|
11962
12829
|
import { setTimeout as delay } from "node:timers/promises";
|
|
11963
12830
|
import { Keyring as Keyring5 } from "@polkadot/keyring";
|
|
11964
|
-
import { decodeAddress as
|
|
11965
|
-
import { computeChannel as
|
|
12831
|
+
import { decodeAddress as decodeAddress3, randomAsU8a } from "@polkadot/util-crypto";
|
|
12832
|
+
import { computeChannel as computeChannel8 } from "@flop-labs/sdk";
|
|
11966
12833
|
|
|
11967
12834
|
// ../gateway-core/src/gateway-evidence.ts
|
|
11968
12835
|
var GATEWAY_EVIDENCE_VERSION = "flop.gateway-evidence.v1";
|
|
@@ -11982,7 +12849,7 @@ function instant(value, label) {
|
|
|
11982
12849
|
if (!Number.isFinite(parsed)) throw new Error(`${label} must be an ISO-8601 timestamp`);
|
|
11983
12850
|
return parsed;
|
|
11984
12851
|
}
|
|
11985
|
-
function
|
|
12852
|
+
function text3(value, label) {
|
|
11986
12853
|
if (typeof value !== "string" || !value) throw new Error(`${label} is required`);
|
|
11987
12854
|
}
|
|
11988
12855
|
function parseGatewayEvidence(value) {
|
|
@@ -11994,15 +12861,15 @@ function parseGatewayEvidence(value) {
|
|
|
11994
12861
|
}
|
|
11995
12862
|
for (const [index, entry] of evidence.deployments.entries()) {
|
|
11996
12863
|
const label = `deployments[${index}]`;
|
|
11997
|
-
|
|
11998
|
-
|
|
12864
|
+
text3(entry?.miner_address, `${label}.miner_address`);
|
|
12865
|
+
text3(entry.endpoint, `${label}.endpoint`);
|
|
11999
12866
|
if (!HASH3.test(entry.enclave_key)) throw new Error(`${label}.enclave_key is invalid`);
|
|
12000
12867
|
const identity = entry.model_identity;
|
|
12001
12868
|
if (!identity || IDENTITY_FIELDS.some((field2) => field2 === "precision" ? !Number.isSafeInteger(identity.precision) : typeof identity[field2] !== "string" || !identity[field2])) {
|
|
12002
12869
|
throw new Error(`${label}.model_identity is incomplete`);
|
|
12003
12870
|
}
|
|
12004
|
-
|
|
12005
|
-
|
|
12871
|
+
text3(entry.source, `${label}.source`);
|
|
12872
|
+
text3(entry.observer, `${label}.observer`);
|
|
12006
12873
|
if (!/^[A-Z]{2}$/.test(entry.country_code)) throw new Error(`${label}.country_code is invalid`);
|
|
12007
12874
|
if (instant(entry.observed_at, `${label}.observed_at`) >= instant(entry.expires_at, `${label}.expires_at`)) {
|
|
12008
12875
|
throw new Error(`${label} has an empty validity window`);
|
|
@@ -12010,10 +12877,10 @@ function parseGatewayEvidence(value) {
|
|
|
12010
12877
|
}
|
|
12011
12878
|
for (const [index, entry] of evidence.conformance.entries()) {
|
|
12012
12879
|
const label = `conformance[${index}]`;
|
|
12013
|
-
|
|
12014
|
-
|
|
12015
|
-
|
|
12016
|
-
|
|
12880
|
+
text3(entry?.openrouter_slug, `${label}.openrouter_slug`);
|
|
12881
|
+
text3(entry.feature, `${label}.feature`);
|
|
12882
|
+
text3(entry.registry_version, `${label}.registry_version`);
|
|
12883
|
+
text3(entry.observer, `${label}.observer`);
|
|
12017
12884
|
if (!HASH3.test(entry.model_hash) || !HASH3.test(entry.decode_policy_hash)) throw new Error(`${label} identity hashes are invalid`);
|
|
12018
12885
|
if (entry.passed !== true) throw new Error(`${label} must record a pass`);
|
|
12019
12886
|
if (instant(entry.tested_at, `${label}.tested_at`) >= instant(entry.expires_at, `${label}.expires_at`)) {
|
|
@@ -12054,11 +12921,11 @@ function renewConformance(record, evidence, now = Date.now()) {
|
|
|
12054
12921
|
}
|
|
12055
12922
|
|
|
12056
12923
|
// ../gateway-core/src/tools-qualification.ts
|
|
12057
|
-
import { createHash as
|
|
12924
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
12058
12925
|
import { isDeepStrictEqual } from "node:util";
|
|
12059
12926
|
var OWNER_TOOLS_QUALIFICATION = "flop.owner-tools-qualification.v1";
|
|
12060
12927
|
function qualificationHash(value) {
|
|
12061
|
-
return
|
|
12928
|
+
return createHash7("sha256").update(JSON.stringify(value)).digest("hex");
|
|
12062
12929
|
}
|
|
12063
12930
|
function toolsQualificationEnvelope(model, version, scope) {
|
|
12064
12931
|
return {
|
|
@@ -12156,12 +13023,13 @@ function qualificationSignedToolCalls(frames) {
|
|
|
12156
13023
|
|
|
12157
13024
|
// ../gateway-core/src/miner-admissions.ts
|
|
12158
13025
|
import { closeSync, fstatSync, openSync, readSync, statSync } from "node:fs";
|
|
12159
|
-
import { createHash as
|
|
13026
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
12160
13027
|
var MAX_ADMITTED_MINERS = 256;
|
|
13028
|
+
var ADMISSION_CONCURRENCY = 4;
|
|
12161
13029
|
var MAX_FILE_BYTES = 512 * 1024;
|
|
12162
13030
|
var HASH4 = /^0x[0-9a-f]{64}$/;
|
|
12163
13031
|
var minerSlot = (miner) => `${miner.minerAddress}:${miner.modelIdentity.requestedSlug}`;
|
|
12164
|
-
var staticMinerSha256 = (miner) =>
|
|
13032
|
+
var staticMinerSha256 = (miner) => createHash8("sha256").update(JSON.stringify(miner)).digest("hex");
|
|
12165
13033
|
var sameModel = (a, b) => Object.keys(a.modelIdentity).every((key) => a.modelIdentity[key] === b.modelIdentity[key]);
|
|
12166
13034
|
var MinerRouting = class {
|
|
12167
13035
|
constructor(statics, path5) {
|
|
@@ -12274,16 +13142,21 @@ var MinerAdmissions = class {
|
|
|
12274
13142
|
const file = JSON.parse(buffer.subarray(0, size).toString("utf8"));
|
|
12275
13143
|
if (file.schema !== "flop-miner-admissions/v1" || !Number.isFinite(Date.parse(file.generatedAt)) || !Array.isArray(file.entries) || file.entries.length > MAX_ADMITTED_MINERS) throw new Error("invalid admissions snapshot");
|
|
12276
13144
|
const slots = /* @__PURE__ */ new Set();
|
|
12277
|
-
const entries = file.entries.
|
|
13145
|
+
const entries = file.entries.flatMap((entry) => {
|
|
12278
13146
|
const id = entry.modelIdentity;
|
|
12279
13147
|
const url = new URL(entry.endpoint);
|
|
12280
13148
|
const accepted = Date.parse(entry.adAcceptedAt), expiry = Date.parse(entry.admissionExpiresAt);
|
|
12281
13149
|
if (url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash || typeof entry.minerAddress !== "string" || !entry.minerAddress || !HASH4.test(entry.enclaveKey) || !id || ![id.modelHash, id.decodePolicyHash, id.measuredRoot].every((h) => HASH4.test(h)) || ![id.requestedSlug, id.registryModel, id.registryVersion, id.backendModelName].every((s) => typeof s === "string" && s.length > 0) || !Number.isInteger(id.precision) || id.precision < 0 || id.precision > 5 || !/^[1-9]\d*$/.test(entry.calibrationCapGflopsPerSec) || !Number.isFinite(accepted) || !Number.isFinite(expiry) || accepted > now || expiry <= accepted || expiry > accepted + 36e5 || entry.observation?.countryCode !== null && !/^[A-Z]{2}$/.test(entry.observation?.countryCode) || entry.observation?.regional !== false || entry.observation.observedAt !== entry.adAcceptedAt) throw new Error("invalid admission entry");
|
|
12282
|
-
|
|
13150
|
+
try {
|
|
13151
|
+
requireWorkerMinimum(entry.worker?.version, entry.worker?.revision);
|
|
13152
|
+
} catch (error) {
|
|
13153
|
+
if (error instanceof WorkerUpgradeRequired) return [];
|
|
13154
|
+
throw error;
|
|
13155
|
+
}
|
|
12283
13156
|
const slot = minerSlot(entry);
|
|
12284
13157
|
if (slots.has(slot)) throw new Error("duplicate admission slot");
|
|
12285
13158
|
slots.add(slot);
|
|
12286
|
-
return {
|
|
13159
|
+
return [{
|
|
12287
13160
|
endpoint: entry.endpoint,
|
|
12288
13161
|
authToken: "",
|
|
12289
13162
|
channelSigned: true,
|
|
@@ -12298,7 +13171,7 @@ var MinerAdmissions = class {
|
|
|
12298
13171
|
observedAt: entry.adAcceptedAt,
|
|
12299
13172
|
expiresAt: entry.admissionExpiresAt
|
|
12300
13173
|
}
|
|
12301
|
-
};
|
|
13174
|
+
}];
|
|
12302
13175
|
});
|
|
12303
13176
|
this.entries = entries;
|
|
12304
13177
|
} catch {
|
|
@@ -12309,6 +13182,17 @@ var MinerAdmissions = class {
|
|
|
12309
13182
|
return this.entries.filter((entry) => Date.parse(entry.admissionExpiresAt) > now);
|
|
12310
13183
|
}
|
|
12311
13184
|
};
|
|
13185
|
+
async function mapAdmissions(items, run2, limit = ADMISSION_CONCURRENCY) {
|
|
13186
|
+
const results = new Array(items.length);
|
|
13187
|
+
let next = 0;
|
|
13188
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
13189
|
+
while (next < items.length) {
|
|
13190
|
+
const index = next++;
|
|
13191
|
+
results[index] = await run2(items[index]);
|
|
13192
|
+
}
|
|
13193
|
+
}));
|
|
13194
|
+
return results;
|
|
13195
|
+
}
|
|
12312
13196
|
|
|
12313
13197
|
// ../gateway-core/src/miner-peer.ts
|
|
12314
13198
|
import { BlockList, isIP } from "node:net";
|
|
@@ -12375,16 +13259,16 @@ async function resolveDnsAddresses(host, v4 = resolve42, v6 = resolve62) {
|
|
|
12375
13259
|
}
|
|
12376
13260
|
}
|
|
12377
13261
|
var systemResolve = resolveDnsAddresses;
|
|
12378
|
-
async function resolvePublic(host,
|
|
13262
|
+
async function resolvePublic(host, resolve11 = systemResolve) {
|
|
12379
13263
|
const family = isIP(host);
|
|
12380
|
-
const addresses = family ? [{ address: host, family }] : await
|
|
13264
|
+
const addresses = family ? [{ address: host, family }] : await resolve11(host);
|
|
12381
13265
|
if (addresses.length === 0 || !addresses.every((a) => isPublicAddress(a.address))) {
|
|
12382
13266
|
throw new EndpointRefusal(`${host} does not resolve to public addresses only`);
|
|
12383
13267
|
}
|
|
12384
13268
|
return addresses[0];
|
|
12385
13269
|
}
|
|
12386
13270
|
function minerPeerFetch(endpoint, dynamic, before = () => {
|
|
12387
|
-
},
|
|
13271
|
+
}, resolve11 = systemResolve, send2 = request) {
|
|
12388
13272
|
return async (input, init = {}) => {
|
|
12389
13273
|
before();
|
|
12390
13274
|
const url = new URL(String(input));
|
|
@@ -12395,7 +13279,7 @@ function minerPeerFetch(endpoint, dynamic, before = () => {
|
|
|
12395
13279
|
const pinned = await new Promise((accept, reject) => {
|
|
12396
13280
|
const abort = () => reject(signal.reason);
|
|
12397
13281
|
signal.addEventListener("abort", abort, { once: true });
|
|
12398
|
-
resolvePublic(url.hostname.replace(/^\[|\]$/g, ""),
|
|
13282
|
+
resolvePublic(url.hostname.replace(/^\[|\]$/g, ""), resolve11).then(accept, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
12399
13283
|
if (signal.aborted) abort();
|
|
12400
13284
|
});
|
|
12401
13285
|
before();
|
|
@@ -12431,11 +13315,11 @@ function minerPeerFetch(endpoint, dynamic, before = () => {
|
|
|
12431
13315
|
}
|
|
12432
13316
|
|
|
12433
13317
|
// src/commands/gateway-renew.ts
|
|
12434
|
-
import { Command as
|
|
13318
|
+
import { Command as Command50 } from "commander";
|
|
12435
13319
|
|
|
12436
13320
|
// src/commands/gateway-tools-qualification.ts
|
|
12437
|
-
import { decodeAddress } from "@polkadot/util-crypto";
|
|
12438
|
-
import { computeChannel as
|
|
13321
|
+
import { decodeAddress as decodeAddress2 } from "@polkadot/util-crypto";
|
|
13322
|
+
import { computeChannel as computeChannel7 } from "@flop-labs/sdk";
|
|
12439
13323
|
function require2(value, message) {
|
|
12440
13324
|
if (!value) throw new Error(message);
|
|
12441
13325
|
}
|
|
@@ -12464,36 +13348,36 @@ async function verifyOwnedToolsQualification(value, record, terminal, now = Date
|
|
|
12464
13348
|
require2(wire, "qualification wire policy is unsupported");
|
|
12465
13349
|
const envelope = toolsQualificationEnvelope(record.openrouter_slug, wire, q);
|
|
12466
13350
|
assertQualificationEnvelope(envelope, q);
|
|
12467
|
-
const input =
|
|
12468
|
-
const receipt =
|
|
13351
|
+
const input = computeChannel7.encodeChatTurnEnvelope(envelope);
|
|
13352
|
+
const receipt = computeChannel7.decodeTurnReceipt(evidence.receipt);
|
|
12469
13353
|
require2(
|
|
12470
13354
|
receipt.modelIdentity && receipt.modelIdentity.requestedSlug === record.openrouter_slug && receipt.modelIdentity.registryModel === record.registry_model && receipt.modelIdentity.registryVersion === record.registry_version && u8aToHex(receipt.modelIdentity.modelHash) === record.model_hash && receipt.modelIdentity.precision === record.precision && u8aToHex(receipt.modelIdentity.decodePolicyHash) === record.decode_policy_hash && receipt.modelIdentity.backendModelName === record.backend_model_name && receipt.decodePolicyHash && u8aToHex(receipt.decodePolicyHash) === record.decode_policy_hash,
|
|
12471
13355
|
"qualification receipt model identity differs"
|
|
12472
13356
|
);
|
|
12473
|
-
require2(equal(
|
|
13357
|
+
require2(equal(computeChannel7.blake2_256(input), receipt.hIn), "qualification purpose/envelope preimage differs");
|
|
12474
13358
|
require2(
|
|
12475
13359
|
receipt.promptTokens !== void 0 && receipt.completionTokens !== void 0 && receipt.finishReason,
|
|
12476
13360
|
"qualification authoritative provider usage is missing"
|
|
12477
13361
|
);
|
|
12478
|
-
require2(equal(
|
|
13362
|
+
require2(equal(computeChannel7.turnEvidenceHash(
|
|
12479
13363
|
receipt.output,
|
|
12480
13364
|
receipt.promptTokens,
|
|
12481
13365
|
receipt.completionTokens,
|
|
12482
13366
|
receipt.finishReason,
|
|
12483
13367
|
receipt.modelIdentity
|
|
12484
13368
|
), receipt.hOut), "qualification output evidence hash differs");
|
|
12485
|
-
|
|
13369
|
+
computeChannel7.validateRenderedChatReceipt(input, receipt);
|
|
12486
13370
|
require2(Array.isArray(evidence.frames) && evidence.frames.length > 0, "qualification signed frames are missing");
|
|
12487
13371
|
const frames = evidence.frames.map((frame) => ({ kind: frame.kind, data: bytes2(frame.data) }));
|
|
12488
|
-
|
|
13372
|
+
computeChannel7.reconstructSignedTurnOutput(frames, receipt);
|
|
12489
13373
|
assertQualificationToolCalls(qualificationSignedToolCalls(frames), receipt.finishReason);
|
|
12490
13374
|
require2(
|
|
12491
13375
|
Array.isArray(evidence.prefix) && evidence.prefix.length === receipt.turnIndex + 1 && evidence.prefix.length <= 1024,
|
|
12492
13376
|
"qualification signed prefix is incomplete"
|
|
12493
13377
|
);
|
|
12494
|
-
const accumulator = new
|
|
13378
|
+
const accumulator = new computeChannel7.MerkleAccumulator();
|
|
12495
13379
|
let aggregate2 = 0n;
|
|
12496
|
-
const selectedLeaf =
|
|
13380
|
+
const selectedLeaf = computeChannel7.reconstructLeafVersioned(
|
|
12497
13381
|
receipt.leafVersion,
|
|
12498
13382
|
bytes2(evidence.channel_id),
|
|
12499
13383
|
receipt.turnIndex,
|
|
@@ -12511,7 +13395,7 @@ async function verifyOwnedToolsQualification(value, record, terminal, now = Date
|
|
|
12511
13395
|
const t = row, gn = BigInt(t.gN), timing = [BigInt(t.minerRecvMs), BigInt(t.minerDoneMs), BigInt(t.latencyMs)];
|
|
12512
13396
|
require2(t.turnIndex === index && gn > 0n && gn < 1n << 64n && t.decodePolicyHash === record.decode_policy_hash, "qualification prefix sequence/work differs");
|
|
12513
13397
|
require2(
|
|
12514
|
-
|
|
13398
|
+
computeChannel7.verifyEnclaveLeaf(
|
|
12515
13399
|
bytes2(evidence.enclave_key),
|
|
12516
13400
|
t.leafVersion,
|
|
12517
13401
|
bytes2(evidence.channel_id),
|
|
@@ -12527,7 +13411,7 @@ async function verifyOwnedToolsQualification(value, record, terminal, now = Date
|
|
|
12527
13411
|
),
|
|
12528
13412
|
"qualification prefix signature is invalid"
|
|
12529
13413
|
);
|
|
12530
|
-
const leaf =
|
|
13414
|
+
const leaf = computeChannel7.reconstructLeafVersioned(
|
|
12531
13415
|
t.leafVersion,
|
|
12532
13416
|
bytes2(evidence.channel_id),
|
|
12533
13417
|
index,
|
|
@@ -12546,7 +13430,7 @@ async function verifyOwnedToolsQualification(value, record, terminal, now = Date
|
|
|
12546
13430
|
}
|
|
12547
13431
|
}
|
|
12548
13432
|
const ack = evidence.ack;
|
|
12549
|
-
require2(ack && ack.turnIndex === receipt.turnIndex && ack.leafHash === u8aToHex(selectedLeaf) &&
|
|
13433
|
+
require2(ack && ack.turnIndex === receipt.turnIndex && ack.leafHash === u8aToHex(selectedLeaf) && computeChannel7.verifyTurnAck(
|
|
12550
13434
|
bytes2(evidence.agent_key),
|
|
12551
13435
|
bytes2(evidence.channel_id),
|
|
12552
13436
|
receipt.turnIndex,
|
|
@@ -12556,7 +13440,7 @@ async function verifyOwnedToolsQualification(value, record, terminal, now = Date
|
|
|
12556
13440
|
bytes2(ack.agentSig)
|
|
12557
13441
|
), "qualification turn ACK is invalid");
|
|
12558
13442
|
const checkpoint = evidence.checkpoint;
|
|
12559
|
-
require2(equal(accumulator.root(), receipt.runningRoot) && u8aToHex(accumulator.root()) === checkpoint.finalRoot && aggregate2.toString() === checkpoint.aggregateGn &&
|
|
13443
|
+
require2(equal(accumulator.root(), receipt.runningRoot) && u8aToHex(accumulator.root()) === checkpoint.finalRoot && aggregate2.toString() === checkpoint.aggregateGn && computeChannel7.verifyReceipt(
|
|
12560
13444
|
bytes2(evidence.agent_key),
|
|
12561
13445
|
bytes2(evidence.channel_id),
|
|
12562
13446
|
bytes2(checkpoint.finalRoot),
|
|
@@ -12627,10 +13511,10 @@ async function readQualificationTerminal(api, eventId, channelId) {
|
|
|
12627
13511
|
aggregateGn: data[3].toString(),
|
|
12628
13512
|
enclaveKey: channel.enclaveKey.toHex(),
|
|
12629
13513
|
agentKey: channel.agentKey.toHex(),
|
|
12630
|
-
minerAccountId: u8aToHex(
|
|
13514
|
+
minerAccountId: u8aToHex(decodeAddress2(channel.miner.toString())),
|
|
12631
13515
|
modelHash: channel.modelHash.toHex(),
|
|
12632
13516
|
measuredRoot: channel.measuredRoot.toHex(),
|
|
12633
|
-
precision:
|
|
13517
|
+
precision: computeChannel7.modelPrecisionCode(channel.precision.toString()),
|
|
12634
13518
|
timestampMs: Number(stamp.toString()),
|
|
12635
13519
|
grossRaw: gross.toString(),
|
|
12636
13520
|
debitRaw: debit.toString()
|
|
@@ -12695,7 +13579,7 @@ function tailnetNodeRef(source) {
|
|
|
12695
13579
|
return match ? { host: match[1], egress: match[2], asn: match[3] } : null;
|
|
12696
13580
|
}
|
|
12697
13581
|
function readTailscaleStatus(socketPath) {
|
|
12698
|
-
return new Promise((
|
|
13582
|
+
return new Promise((resolve11, reject) => {
|
|
12699
13583
|
const req = request2({
|
|
12700
13584
|
socketPath,
|
|
12701
13585
|
path: "/localapi/v0/status",
|
|
@@ -12710,7 +13594,7 @@ function readTailscaleStatus(socketPath) {
|
|
|
12710
13594
|
res.on("end", () => {
|
|
12711
13595
|
if (res.statusCode !== 200) return reject(new Error(`tailscaled status returned HTTP ${res.statusCode}`));
|
|
12712
13596
|
try {
|
|
12713
|
-
|
|
13597
|
+
resolve11(JSON.parse(body));
|
|
12714
13598
|
} catch {
|
|
12715
13599
|
reject(new Error("tailscaled status is not JSON"));
|
|
12716
13600
|
}
|
|
@@ -12780,7 +13664,7 @@ function conformanceWindows(record) {
|
|
|
12780
13664
|
}
|
|
12781
13665
|
var ProbeSignerUnderfunded = class extends Error {
|
|
12782
13666
|
};
|
|
12783
|
-
function
|
|
13667
|
+
function reason2(error) {
|
|
12784
13668
|
return error instanceof Error ? error.message : String(error);
|
|
12785
13669
|
}
|
|
12786
13670
|
async function renewGatewayEvidence(miners, records, observe) {
|
|
@@ -12789,7 +13673,7 @@ async function renewGatewayEvidence(miners, records, observe) {
|
|
|
12789
13673
|
const withdrawn = [];
|
|
12790
13674
|
const observed = [];
|
|
12791
13675
|
let underfunded;
|
|
12792
|
-
|
|
13676
|
+
const observations = await mapAdmissions(miners, async (miner) => {
|
|
12793
13677
|
const subject = `${miner.minerAddress}:${miner.modelIdentity.requestedSlug}`;
|
|
12794
13678
|
try {
|
|
12795
13679
|
const declared = miner.deployment;
|
|
@@ -12801,8 +13685,7 @@ async function renewGatewayEvidence(miners, records, observe) {
|
|
|
12801
13685
|
if (miner.admissionExpiresAt) {
|
|
12802
13686
|
await observe.miner(miner);
|
|
12803
13687
|
if (Date.parse(miner.admissionExpiresAt) <= observe.now()) throw new Error("miner admission expired");
|
|
12804
|
-
|
|
12805
|
-
continue;
|
|
13688
|
+
return { miner };
|
|
12806
13689
|
}
|
|
12807
13690
|
if (node) {
|
|
12808
13691
|
session = await observe.miner(miner);
|
|
@@ -12812,7 +13695,7 @@ async function renewGatewayEvidence(miners, records, observe) {
|
|
|
12812
13695
|
session = await observe.miner(miner);
|
|
12813
13696
|
}
|
|
12814
13697
|
const observedAt = observe.now();
|
|
12815
|
-
|
|
13698
|
+
const deployment = {
|
|
12816
13699
|
miner_address: miner.minerAddress,
|
|
12817
13700
|
endpoint: miner.endpoint,
|
|
12818
13701
|
enclave_key: miner.enclaveKey,
|
|
@@ -12822,11 +13705,16 @@ async function renewGatewayEvidence(miners, records, observe) {
|
|
|
12822
13705
|
observed_at: new Date(observedAt).toISOString(),
|
|
12823
13706
|
expires_at: new Date(observedAt + ttl).toISOString(),
|
|
12824
13707
|
observer: `${provider}; ${session}`
|
|
12825
|
-
}
|
|
12826
|
-
|
|
13708
|
+
};
|
|
13709
|
+
return { miner, deployment };
|
|
12827
13710
|
} catch (error) {
|
|
12828
|
-
|
|
13711
|
+
return { failure: { subject, reason: `deployment not renewed: ${reason2(error)}` } };
|
|
12829
13712
|
}
|
|
13713
|
+
});
|
|
13714
|
+
for (const { miner, deployment, failure: failure2 } of observations) {
|
|
13715
|
+
if (failure2) withdrawn.push(failure2);
|
|
13716
|
+
if (miner) observed.push(miner);
|
|
13717
|
+
if (deployment) deployments.push(deployment);
|
|
12830
13718
|
}
|
|
12831
13719
|
for (const record of records) {
|
|
12832
13720
|
for (const [feature, ttl] of conformanceWindows(record)) {
|
|
@@ -12865,7 +13753,7 @@ async function renewGatewayEvidence(miners, records, observe) {
|
|
|
12865
13753
|
break;
|
|
12866
13754
|
} catch (error) {
|
|
12867
13755
|
if (error instanceof ProbeSignerUnderfunded) underfunded = error.message;
|
|
12868
|
-
else failures.push(`${miner.minerAddress}: ${
|
|
13756
|
+
else failures.push(`${miner.minerAddress}: ${reason2(error)}`);
|
|
12869
13757
|
}
|
|
12870
13758
|
}
|
|
12871
13759
|
if (underfunded) withdrawn.push({ subject, reason: `probe not run: ${underfunded}` });
|
|
@@ -12885,8 +13773,8 @@ async function renewGatewayEvidence(miners, records, observe) {
|
|
|
12885
13773
|
}
|
|
12886
13774
|
function planckToFlop(planck) {
|
|
12887
13775
|
const scale = 10n ** BigInt(FLOP_DECIMALS);
|
|
12888
|
-
const
|
|
12889
|
-
return
|
|
13776
|
+
const fraction2 = (planck % scale).toString().padStart(FLOP_DECIMALS, "0").replace(/0+$/, "");
|
|
13777
|
+
return fraction2 ? `${planck / scale}.${fraction2}` : `${planck / scale}`;
|
|
12890
13778
|
}
|
|
12891
13779
|
async function assertProbeSignerFunded(api, address, escrow) {
|
|
12892
13780
|
const head = await api.rpc.chain.getFinalizedHead();
|
|
@@ -12926,10 +13814,10 @@ async function writeEvidenceFile(path5, evidence, appendQualification = false) {
|
|
|
12926
13814
|
parseGatewayEvidence(evidence);
|
|
12927
13815
|
await writeFile13(temporary, `${JSON.stringify(evidence, null, 2)}
|
|
12928
13816
|
`, { mode: 420 });
|
|
12929
|
-
await
|
|
13817
|
+
await rename10(temporary, path5);
|
|
12930
13818
|
} finally {
|
|
12931
13819
|
await lock.close();
|
|
12932
|
-
await
|
|
13820
|
+
await rm7(`${path5}.lock`);
|
|
12933
13821
|
}
|
|
12934
13822
|
}
|
|
12935
13823
|
async function finishRenewal(report, out, output) {
|
|
@@ -12978,7 +13866,7 @@ function assertToolsConformance(calls, finishReason) {
|
|
|
12978
13866
|
}
|
|
12979
13867
|
return `signed ${PROBE_TOOL.function.name} tool call`;
|
|
12980
13868
|
}
|
|
12981
|
-
function
|
|
13869
|
+
function hex4(bytes4) {
|
|
12982
13870
|
return `0x${Buffer.from(bytes4).toString("hex")}`;
|
|
12983
13871
|
}
|
|
12984
13872
|
function bytes3(value) {
|
|
@@ -13004,7 +13892,7 @@ async function waitForSettlementAllowance(api, channelId, aggregateGn, deadline)
|
|
|
13004
13892
|
const snapshot = await query.computeChannel.channelCalibrationSnapshots.at(head, channelId);
|
|
13005
13893
|
const current = (await api.rpc.state.call(
|
|
13006
13894
|
"TrustRootApi_accepted_calibration_cap",
|
|
13007
|
-
|
|
13895
|
+
hex4(Uint8Array.from([128, ...decodeAddress3(opened.miner.toString())])),
|
|
13008
13896
|
head
|
|
13009
13897
|
)).toU8a(true);
|
|
13010
13898
|
if (!snapshot?.isSome || current.length !== 9 || current[0] !== 1) throw new Error("settlement calibration is unavailable");
|
|
@@ -13053,7 +13941,7 @@ async function reconcileProbeClose(api, channelId, aggregateGn, deadline, submit
|
|
|
13053
13941
|
const events = [...await bounded(at.query.system.events())];
|
|
13054
13942
|
for (const row of events) {
|
|
13055
13943
|
const event = row.event;
|
|
13056
|
-
if (event.section !== "computeChannel" || !["Settled", "TimedOutRefunded"].includes(event.method) || event.data[0].toHex() !==
|
|
13944
|
+
if (event.section !== "computeChannel" || !["Settled", "TimedOutRefunded"].includes(event.method) || event.data[0].toHex() !== hex4(channelId) || !row.phase.isApplyExtrinsic) continue;
|
|
13057
13945
|
const index = row.phase.asApplyExtrinsic.toNumber();
|
|
13058
13946
|
if (!events.some((other) => other.phase.isApplyExtrinsic && other.phase.asApplyExtrinsic.toNumber() === index && other.event.section === "system" && other.event.method === "ExtrinsicSuccess")) continue;
|
|
13059
13947
|
if (event.method === "TimedOutRefunded") return `refunded at finalized ${blockHash.toHex()}`;
|
|
@@ -13081,7 +13969,7 @@ async function settleProbe(api, channelId, aggregateGn, deadline, allowance2, su
|
|
|
13081
13969
|
await forceClose();
|
|
13082
13970
|
} catch {
|
|
13083
13971
|
}
|
|
13084
|
-
return `cooperative settlement not submitted; force close attempted (${
|
|
13972
|
+
return `cooperative settlement not submitted; force close attempted (${reason2(error)})`;
|
|
13085
13973
|
}
|
|
13086
13974
|
return reconcileProbeClose(api, channelId, aggregateGn, deadline, submit);
|
|
13087
13975
|
}
|
|
@@ -13091,7 +13979,7 @@ async function renewalMinerInfo(api, miner, fetchImpl = fetch) {
|
|
|
13091
13979
|
const registered = await at.query.minerStaking.miners(miner.minerAddress);
|
|
13092
13980
|
const active = registered?.isSome && registered.unwrap().isActive;
|
|
13093
13981
|
if (!(active?.isTrue ?? active === true)) throw new Error("miner is not registered and active at finality");
|
|
13094
|
-
const encoded = await api.rpc.state.call("TrustRootApi_accepted_calibration_cap", `0x80${
|
|
13982
|
+
const encoded = await api.rpc.state.call("TrustRootApi_accepted_calibration_cap", `0x80${hex4(decodeAddress3(miner.minerAddress)).slice(2)}`, head);
|
|
13095
13983
|
const cap = encoded.toU8a(true);
|
|
13096
13984
|
if (cap.length !== 9 || cap[0] !== 1 || new DataView(cap.buffer, cap.byteOffset + 1, 8).getBigUint64(0, true) <= 0n) throw new Error("miner has no finalized calibrated capacity");
|
|
13097
13985
|
const identity = {
|
|
@@ -13105,13 +13993,13 @@ async function renewalMinerInfo(api, miner, fetchImpl = fetch) {
|
|
|
13105
13993
|
await anonymous.body?.cancel();
|
|
13106
13994
|
if (anonymous.status !== 401) throw new Error("miner /session/info does not enforce authentication");
|
|
13107
13995
|
}
|
|
13108
|
-
const info = miner.channelSigned ? await
|
|
13996
|
+
const info = miner.channelSigned ? await computeChannel8.fetchProvenSessionInfo(miner.endpoint, {
|
|
13109
13997
|
genesisHash: api.genesisHash.toU8a(),
|
|
13110
|
-
account:
|
|
13998
|
+
account: decodeAddress3(miner.minerAddress),
|
|
13111
13999
|
minerAddress: miner.minerAddress,
|
|
13112
14000
|
enclaveKey: bytes3(miner.enclaveKey),
|
|
13113
14001
|
modelIdentity: identity
|
|
13114
|
-
}, fetchImpl, AbortSignal.timeout(3e4)) : await
|
|
14002
|
+
}, fetchImpl, AbortSignal.timeout(3e4)) : await computeChannel8.fetchMinerSessionInfo(
|
|
13115
14003
|
miner.endpoint,
|
|
13116
14004
|
{ Authorization: `Bearer ${miner.authToken}` },
|
|
13117
14005
|
AbortSignal.timeout(3e4),
|
|
@@ -13121,13 +14009,13 @@ async function renewalMinerInfo(api, miner, fetchImpl = fetch) {
|
|
|
13121
14009
|
const wire = {
|
|
13122
14010
|
...info,
|
|
13123
14011
|
is_ready: info.isReady,
|
|
13124
|
-
enclaveKey:
|
|
13125
|
-
modelHash:
|
|
14012
|
+
enclaveKey: hex4(info.enclaveKey),
|
|
14013
|
+
modelHash: hex4(info.modelHash),
|
|
13126
14014
|
modelIdentity: {
|
|
13127
14015
|
...info.modelIdentity,
|
|
13128
|
-
modelHash:
|
|
13129
|
-
decodePolicyHash:
|
|
13130
|
-
measuredRoot:
|
|
14016
|
+
modelHash: hex4(info.modelIdentity.modelHash),
|
|
14017
|
+
decodePolicyHash: hex4(info.modelIdentity.decodePolicyHash),
|
|
14018
|
+
measuredRoot: hex4(info.modelIdentity.measuredRoot)
|
|
13131
14019
|
}
|
|
13132
14020
|
};
|
|
13133
14021
|
const snapshot = await readChainSnapshot(api, wire, head.toHex());
|
|
@@ -13156,7 +14044,7 @@ async function probeToolsConformance(input) {
|
|
|
13156
14044
|
let requestHeaders;
|
|
13157
14045
|
const pinned = miner.modelIdentity;
|
|
13158
14046
|
const advertised = info.modelIdentity;
|
|
13159
|
-
if (info.minerAddress !== miner.minerAddress ||
|
|
14047
|
+
if (info.minerAddress !== miner.minerAddress || hex4(info.enclaveKey) !== miner.enclaveKey.toLowerCase() || !advertised || advertised.requestedSlug !== pinned.requestedSlug || advertised.registryModel !== pinned.registryModel || advertised.registryVersion !== pinned.registryVersion || hex4(advertised.modelHash) !== pinned.modelHash || advertised.precision !== pinned.precision || hex4(advertised.decodePolicyHash) !== pinned.decodePolicyHash || hex4(advertised.measuredRoot) !== pinned.measuredRoot || advertised.backendModelName !== pinned.backendModelName) {
|
|
13160
14048
|
throw new Error("miner no longer advertises its allowlisted identity");
|
|
13161
14049
|
}
|
|
13162
14050
|
let version;
|
|
@@ -13173,7 +14061,7 @@ async function probeToolsConformance(input) {
|
|
|
13173
14061
|
const maxSettlementTurns = Number(api.consts.computeChannel.maxSettlementTurns.toString());
|
|
13174
14062
|
const frames = [];
|
|
13175
14063
|
let sequence = 0;
|
|
13176
|
-
const transport = new
|
|
14064
|
+
const transport = new computeChannel8.HttpSessionTransport(miner.endpoint, PROBE_TIMEOUT_MS, {
|
|
13177
14065
|
headers,
|
|
13178
14066
|
fetchImpl,
|
|
13179
14067
|
requestHeaders: (method, path5, body) => requestHeaders?.(method, path5, body) ?? {},
|
|
@@ -13182,10 +14070,10 @@ async function probeToolsConformance(input) {
|
|
|
13182
14070
|
if (frame.type === "chunk") frames.push({ kind: frame.kind, data: bytes3(frame.data) });
|
|
13183
14071
|
}
|
|
13184
14072
|
});
|
|
13185
|
-
const agentKey =
|
|
14073
|
+
const agentKey = computeChannel8.SessionKey.generate();
|
|
13186
14074
|
const identity = { ...pinned, modelHash: bytes3(pinned.modelHash), decodePolicyHash: bytes3(pinned.decodePolicyHash), measuredRoot: bytes3(pinned.measuredRoot) };
|
|
13187
14075
|
input.assertAdmission?.();
|
|
13188
|
-
const session = await
|
|
14076
|
+
const session = await computeChannel8.ChannelSession.open({
|
|
13189
14077
|
chain,
|
|
13190
14078
|
transport,
|
|
13191
14079
|
enclaveKey: info.enclaveKey,
|
|
@@ -13205,13 +14093,13 @@ async function probeToolsConformance(input) {
|
|
|
13205
14093
|
maxDuration: BigInt(miner.maxDurationBlocks),
|
|
13206
14094
|
reattestEvery: 1,
|
|
13207
14095
|
escrow: input.escrow,
|
|
13208
|
-
nonce: BigInt(
|
|
14096
|
+
nonce: BigInt(hex4(randomAsU8a(8)))
|
|
13209
14097
|
});
|
|
13210
14098
|
transport.bind(session.channelId);
|
|
13211
14099
|
let verdict;
|
|
13212
14100
|
let receipt;
|
|
13213
14101
|
try {
|
|
13214
|
-
if (miner.channelSigned) requestHeaders = await
|
|
14102
|
+
if (miner.channelSigned) requestHeaders = await computeChannel8.channelSessionAuth(
|
|
13215
14103
|
miner.endpoint,
|
|
13216
14104
|
session.channelId,
|
|
13217
14105
|
agentKey,
|
|
@@ -13219,7 +14107,7 @@ async function probeToolsConformance(input) {
|
|
|
13219
14107
|
fetchImpl,
|
|
13220
14108
|
AbortSignal.timeout(3e4)
|
|
13221
14109
|
);
|
|
13222
|
-
await
|
|
14110
|
+
await computeChannel8.notifyMinerOpen(miner.endpoint, session.channelId, agentKey.publicKey, headers, AbortSignal.timeout(3e4), requestHeaders, fetchImpl);
|
|
13223
14111
|
receipt = await session.chatTurn({
|
|
13224
14112
|
version,
|
|
13225
14113
|
model: record.openrouter_slug,
|
|
@@ -13228,7 +14116,7 @@ async function probeToolsConformance(input) {
|
|
|
13228
14116
|
tool_choice: "auto",
|
|
13229
14117
|
max_tokens: 192
|
|
13230
14118
|
}, (candidate) => {
|
|
13231
|
-
|
|
14119
|
+
computeChannel8.reconstructSignedTurnOutput(frames, candidate);
|
|
13232
14120
|
verdict = assertToolsConformance(signedToolCalls(frames), candidate.finishReason);
|
|
13233
14121
|
});
|
|
13234
14122
|
} catch (error) {
|
|
@@ -13245,7 +14133,7 @@ async function probeToolsConformance(input) {
|
|
|
13245
14133
|
deadline,
|
|
13246
14134
|
() => waitForSettlementAllowance(api, session.channelId, receipt.gN, deadline),
|
|
13247
14135
|
() => session.closeCooperative(async (claim) => {
|
|
13248
|
-
const wire = await
|
|
14136
|
+
const wire = await computeChannel8.notifyMinerClose(miner.endpoint, claim, headers, AbortSignal.timeout(3e4), requestHeaders, fetchImpl);
|
|
13249
14137
|
if (!wire) throw new Error("miner did not report a settlement");
|
|
13250
14138
|
return {
|
|
13251
14139
|
channelId: bytes3(wire.channelId),
|
|
@@ -13258,7 +14146,7 @@ async function probeToolsConformance(input) {
|
|
|
13258
14146
|
}),
|
|
13259
14147
|
() => session.close()
|
|
13260
14148
|
);
|
|
13261
|
-
return `${verdict} in channel ${
|
|
14149
|
+
return `${verdict} in channel ${hex4(session.channelId)}; ${settlement}`;
|
|
13262
14150
|
}
|
|
13263
14151
|
function envJson(name) {
|
|
13264
14152
|
const value = process.env[name];
|
|
@@ -13275,7 +14163,7 @@ function envSecret(name) {
|
|
|
13275
14163
|
return value;
|
|
13276
14164
|
}
|
|
13277
14165
|
function gatewayRenewCmd() {
|
|
13278
|
-
return new
|
|
14166
|
+
return new Command50("renew").description("Re-observe allowlisted miners, re-run feature conformance, and write the gateway evidence file").option("--allowlist-env <name>", "Direct probe mode: environment variable holding GATEWAY_SESSION_ALLOWLIST (never printed)").requiredOption("--catalog-env <name>", "Environment variable holding GATEWAY_MODEL_CATALOG").option("--catalog-pricing <mode>", "The gateway's GATEWAY_CATALOG_PRICING: operator, or chain when the catalog omits prices", "operator").option("--provider-key-env <name>", "Direct probe mode: environment variable holding the GPU.ai API key").option("--tailscale-socket <path>", "tailscaled LocalAPI socket for tailnet-hosted miners", "/var/run/tailscale/tailscaled.sock").option("--probe-signer-env <name>", "Direct probe mode: environment variable holding the dedicated funded probe signer URI").option("--probe-escrow <flop>", "Direct probe mode: FLOP escrowed for each conformance probe channel").option("--qualification-receipt <path>", "Receipt-only mode: settled owner tools-qualification evidence; never runs another probe").option("--qualification-receipt-sha256 <hash>", "SHA-256 of the immutable owner qualification receipt").requiredOption("--out <path>", "Evidence file the gateway reads as GATEWAY_EVIDENCE_FILE").option("--json", "Output as JSON").action(async (opts, command) => {
|
|
13279
14167
|
let client;
|
|
13280
14168
|
try {
|
|
13281
14169
|
if (opts.catalogPricing !== "operator" && opts.catalogPricing !== "chain") {
|
|
@@ -13286,7 +14174,7 @@ function gatewayRenewCmd() {
|
|
|
13286
14174
|
throw new Error("receipt-only mode requires the receipt/hash and cannot run direct probes");
|
|
13287
14175
|
}
|
|
13288
14176
|
const raw = await readFile11(opts.qualificationReceipt);
|
|
13289
|
-
if (raw.length > 8e6 ||
|
|
14177
|
+
if (raw.length > 8e6 || createHash9("sha256").update(raw).digest("hex") !== opts.qualificationReceiptSha256) throw new Error("qualification receipt bytes differ");
|
|
13290
14178
|
const value = JSON.parse(raw.toString("utf8"));
|
|
13291
14179
|
const records2 = parseCanonicalCatalog(envJson(opts.catalogEnv), { chainPriced: opts.catalogPricing === "chain" });
|
|
13292
14180
|
const record = records2.find((item) => item.openrouter_slug === value.model);
|
|
@@ -13296,7 +14184,7 @@ function gatewayRenewCmd() {
|
|
|
13296
14184
|
const evidence = parseGatewayEvidence(JSON.parse(await readFile11(opts.out, "utf8")));
|
|
13297
14185
|
const renewed = renewConformance(record, evidence, tested);
|
|
13298
14186
|
client = await createClient(getRpc(command));
|
|
13299
|
-
await
|
|
14187
|
+
await computeChannel8.ready();
|
|
13300
14188
|
const pass = await verifyOwnedToolsQualification(value, renewed, (id, channel) => readQualificationTerminal(client.api, id, channel));
|
|
13301
14189
|
await writeEvidenceFile(opts.out, { schema_version: GATEWAY_EVIDENCE_VERSION, generated_at: (/* @__PURE__ */ new Date()).toISOString(), deployments: [], conformance: [pass] }, true);
|
|
13302
14190
|
print({ out: opts.out, conformance: [pass], receipt_only: true, paid_probe_count: 0 }, getJson(command));
|
|
@@ -13319,9 +14207,9 @@ function gatewayRenewCmd() {
|
|
|
13319
14207
|
const escrow = flopToPlanck(opts.probeEscrow);
|
|
13320
14208
|
client = await createClient(getRpc(command));
|
|
13321
14209
|
const api = client.api;
|
|
13322
|
-
await
|
|
14210
|
+
await computeChannel8.ready();
|
|
13323
14211
|
const signer = new Keyring5({ type: "sr25519" }).addFromUri(signerUri);
|
|
13324
|
-
const chain = new
|
|
14212
|
+
const chain = new computeChannel8.ComputeChannelChainClient(new computeChannel8.FlopChainSubmitter(api, signer, true));
|
|
13325
14213
|
const assertAdmission = (miner) => {
|
|
13326
14214
|
if (!miner.admissionExpiresAt) return;
|
|
13327
14215
|
const dynamic = admissions?.read() ?? [];
|
|
@@ -13337,13 +14225,13 @@ function gatewayRenewCmd() {
|
|
|
13337
14225
|
const info = {
|
|
13338
14226
|
...proven,
|
|
13339
14227
|
is_ready: proven.isReady,
|
|
13340
|
-
enclaveKey:
|
|
13341
|
-
modelHash:
|
|
14228
|
+
enclaveKey: hex4(proven.enclaveKey),
|
|
14229
|
+
modelHash: hex4(proven.modelHash),
|
|
13342
14230
|
modelIdentity: {
|
|
13343
14231
|
...proven.modelIdentity,
|
|
13344
|
-
modelHash:
|
|
13345
|
-
decodePolicyHash:
|
|
13346
|
-
measuredRoot:
|
|
14232
|
+
modelHash: hex4(proven.modelIdentity.modelHash),
|
|
14233
|
+
decodePolicyHash: hex4(proven.modelIdentity.decodePolicyHash),
|
|
14234
|
+
measuredRoot: hex4(proven.modelIdentity.measuredRoot)
|
|
13347
14235
|
}
|
|
13348
14236
|
};
|
|
13349
14237
|
const pinned = miner.modelIdentity;
|
|
@@ -13369,9 +14257,9 @@ function gatewayRenewCmd() {
|
|
|
13369
14257
|
}
|
|
13370
14258
|
|
|
13371
14259
|
// src/release-check.ts
|
|
13372
|
-
import { mkdir as
|
|
14260
|
+
import { mkdir as mkdir15, readFile as readFile12, rename as rename11, writeFile as writeFile14 } from "node:fs/promises";
|
|
13373
14261
|
import { homedir as homedir5 } from "node:os";
|
|
13374
|
-
import { dirname as
|
|
14262
|
+
import { dirname as dirname9, join as join11 } from "node:path";
|
|
13375
14263
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
13376
14264
|
var READ_ONLY = /* @__PURE__ */ new Set([
|
|
13377
14265
|
"health",
|
|
@@ -13415,7 +14303,7 @@ async function checkRelease(options) {
|
|
|
13415
14303
|
if (options.offline || (options.testing ?? (process.env.NODE_ENV === "test" || !!process.env.VITEST))) return;
|
|
13416
14304
|
const warn = options.warn ?? ((message) => console.error(message));
|
|
13417
14305
|
const now = options.now ?? Date.now();
|
|
13418
|
-
const cacheFile = options.cacheFile ??
|
|
14306
|
+
const cacheFile = options.cacheFile ?? join11(process.env.XDG_CACHE_HOME || join11(homedir5(), ".cache"), "flop", "releases.json");
|
|
13419
14307
|
let releases;
|
|
13420
14308
|
try {
|
|
13421
14309
|
const cached = JSON.parse(await readFile12(cacheFile, "utf8"));
|
|
@@ -13430,10 +14318,10 @@ async function checkRelease(options) {
|
|
|
13430
14318
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
13431
14319
|
releases = parseOperatorReleases(await response.json());
|
|
13432
14320
|
try {
|
|
13433
|
-
await
|
|
14321
|
+
await mkdir15(dirname9(cacheFile), { recursive: true });
|
|
13434
14322
|
const temporary = `${cacheFile}.${process.pid}.tmp`;
|
|
13435
14323
|
await writeFile14(temporary, JSON.stringify({ fetchedAt: now, releases }), { mode: 384 });
|
|
13436
|
-
await
|
|
14324
|
+
await rename11(temporary, cacheFile);
|
|
13437
14325
|
} catch {
|
|
13438
14326
|
warn("warning: could not cache operator releases");
|
|
13439
14327
|
}
|
|
@@ -13457,10 +14345,10 @@ async function checkRelease(options) {
|
|
|
13457
14345
|
}
|
|
13458
14346
|
|
|
13459
14347
|
// src/cli.ts
|
|
13460
|
-
var program = new
|
|
14348
|
+
var program = new Command51();
|
|
13461
14349
|
var outputMode = outputModeFromArgv(process.argv.slice(2));
|
|
13462
14350
|
setOutputMode(outputMode);
|
|
13463
|
-
program.name("flop").description("FLOP Network CLI").version(operatorVersion(JSON.parse(readFileSync2(
|
|
14351
|
+
program.name("flop").description("FLOP Network CLI").version(operatorVersion(JSON.parse(readFileSync2(join12(packageRoot, "package.json"), "utf8")).version)).option("--offline", "Skip the operator release check").addOption(new Option8("--network <network>", "Network profile").choices(["local", "devnet", "testnet", "mainnet"]).default("local")).option("--rpc <url>", "RPC endpoint override").option("--signer <uri>", "Signer URI (local/dev only; required outside local unless --keystore)").option("--keystore <path>", "Encrypted Polkadot JSON keystore").option("--password-env <name>", "Environment variable containing the keystore password").addOption(new Option8("--output <mode>", "Output mode").choices(["human", "json", "agent"]).default("human")).option("--json", "Output as JSON");
|
|
13464
14352
|
program.addCommand(healthCmd());
|
|
13465
14353
|
program.addCommand(balanceCmd());
|
|
13466
14354
|
program.addCommand(gpuCmd());
|
|
@@ -13472,13 +14360,13 @@ program.addCommand(chatCmd());
|
|
|
13472
14360
|
program.addCommand(agentCmd());
|
|
13473
14361
|
program.addCommand(minerCmd());
|
|
13474
14362
|
program.addCommand(validatorCmd());
|
|
13475
|
-
var networkCmd = new
|
|
14363
|
+
var networkCmd = new Command51("network").description("Resolve and inspect network profiles");
|
|
13476
14364
|
networkCmd.addCommand(networkStatusCmd());
|
|
13477
14365
|
program.addCommand(networkCmd);
|
|
13478
|
-
var faucetCmd = new
|
|
14366
|
+
var faucetCmd = new Command51("faucet").description("Request dev/test FLOP");
|
|
13479
14367
|
faucetCmd.addCommand(faucetRequestCmd());
|
|
13480
14368
|
program.addCommand(faucetCmd);
|
|
13481
|
-
var sessionCmd = new
|
|
14369
|
+
var sessionCmd = new Command51("session").description("Manage session keys");
|
|
13482
14370
|
sessionCmd.addCommand(sessionRunCmd());
|
|
13483
14371
|
sessionCmd.addCommand(sessionListCmd());
|
|
13484
14372
|
sessionCmd.addCommand(sessionCreateCmd());
|
|
@@ -13486,7 +14374,7 @@ sessionCmd.addCommand(sessionInfoCmd());
|
|
|
13486
14374
|
sessionCmd.addCommand(sessionRevokeCmd());
|
|
13487
14375
|
sessionCmd.addCommand(sessionLimitsCmd());
|
|
13488
14376
|
program.addCommand(sessionCmd);
|
|
13489
|
-
var keysCmd = new
|
|
14377
|
+
var keysCmd = new Command51("keys").description("Manage keys");
|
|
13490
14378
|
keysCmd.addCommand(keysGenerateCmd());
|
|
13491
14379
|
keysCmd.addCommand(keysInspectCmd());
|
|
13492
14380
|
keysCmd.addCommand(keysHardwareCmd());
|
|
@@ -13494,35 +14382,36 @@ keysCmd.addCommand(keysImportCmd());
|
|
|
13494
14382
|
keysCmd.addCommand(keysExportCmd());
|
|
13495
14383
|
keysCmd.addCommand(keysClaudeProfileCmd());
|
|
13496
14384
|
program.addCommand(keysCmd);
|
|
13497
|
-
var walletCmd = new
|
|
13498
|
-
var agentWalletCmd = new
|
|
14385
|
+
var walletCmd = new Command51("wallet").description("Manage wallets");
|
|
14386
|
+
var agentWalletCmd = new Command51("agent").description("Agent wallet operations");
|
|
13499
14387
|
agentWalletCmd.addCommand(walletAgentCreateCmd());
|
|
13500
14388
|
walletCmd.addCommand(agentWalletCmd);
|
|
13501
14389
|
program.addCommand(walletCmd);
|
|
13502
|
-
var txCmd = new
|
|
14390
|
+
var txCmd = new Command51("tx").description("Build, sign, and broadcast transactions");
|
|
13503
14391
|
txCmd.addCommand(txBuildCmd());
|
|
13504
14392
|
txCmd.addCommand(txBroadcastCmd());
|
|
13505
14393
|
program.addCommand(txCmd);
|
|
13506
14394
|
program.addCommand(multisigCmd());
|
|
13507
14395
|
program.addCommand(channelCmd());
|
|
13508
|
-
var gatewayCmd = new
|
|
14396
|
+
var gatewayCmd = new Command51("gateway").description("Generate verified gateway configuration");
|
|
13509
14397
|
gatewayCmd.addCommand(gatewayAllowlistCmd());
|
|
13510
14398
|
gatewayCmd.addCommand(gatewayRenewCmd());
|
|
13511
14399
|
program.addCommand(gatewayCmd);
|
|
13512
|
-
var x402Cmd = new
|
|
14400
|
+
var x402Cmd = new Command51("x402").description("x402 payment gateway operations");
|
|
13513
14401
|
x402Cmd.addCommand(x402SettleCmd());
|
|
13514
14402
|
program.addCommand(x402Cmd);
|
|
13515
|
-
var daCmd = new
|
|
14403
|
+
var daCmd = new Command51("da").description("Sovereign DA registry (leases, deposits)");
|
|
13516
14404
|
daCmd.addCommand(daDepositCmd());
|
|
13517
14405
|
daCmd.addCommand(daRegisterCmd());
|
|
13518
14406
|
daCmd.addCommand(daTopupCmd());
|
|
13519
14407
|
daCmd.addCommand(daWithdrawCmd());
|
|
13520
14408
|
program.addCommand(daCmd);
|
|
13521
|
-
var modelCmd = new
|
|
14409
|
+
var modelCmd = new Command51("model").description("Model registry operations");
|
|
13522
14410
|
modelCmd.addCommand(modelListCmd());
|
|
13523
14411
|
modelCmd.addCommand(modelGetCmd());
|
|
13524
14412
|
modelCmd.addCommand(modelFetchCmd());
|
|
13525
14413
|
modelCmd.addCommand(modelUploadCmd());
|
|
14414
|
+
modelCmd.addCommand(modelMirrorsCmd());
|
|
13526
14415
|
modelCmd.addCommand(modelPreflightCmd());
|
|
13527
14416
|
modelCmd.addCommand(modelOnboardCmd());
|
|
13528
14417
|
modelCmd.addCommand(modelBackupCmd());
|
|
@@ -13532,7 +14421,7 @@ program.hook("preAction", async (_root, action) => {
|
|
|
13532
14421
|
const path5 = [];
|
|
13533
14422
|
for (let command = action; command && command !== program; command = command.parent) path5.unshift(command.name());
|
|
13534
14423
|
await checkRelease({
|
|
13535
|
-
version: operatorVersion(JSON.parse(readFileSync2(
|
|
14424
|
+
version: operatorVersion(JSON.parse(readFileSync2(join12(packageRoot, "package.json"), "utf8")).version),
|
|
13536
14425
|
readOnly: isReadOnlyCommand(path5),
|
|
13537
14426
|
offline: program.opts().offline,
|
|
13538
14427
|
image: !!process.env.FLOP_IMAGE_VERSION
|