@ornncompute/cli 1.0.25 → 1.0.35
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/README.md +82 -32
- package/package.json +1 -1
- package/src/admin-capacity-reservations.mjs +68 -43
- package/src/admin-enrollment.mjs +14 -13
- package/src/admin-node-lifecycle.mjs +10 -13
- package/src/admin-shell.mjs +146 -12
- package/src/catalog-shell.mjs +58 -45
- package/src/device-auth.mjs +1 -1
- package/src/evidence-commands.mjs +25 -11
- package/src/gpu-catalog.mjs +21 -0
- package/src/identity-file.mjs +28 -0
- package/src/kubernetes-commands.mjs +12 -6
- package/src/machine-reference.mjs +10 -2
- package/src/mount-commands.mjs +4 -2
- package/src/slurm-commands.mjs +11 -0
- package/src/storage-commands.mjs +27 -7
- package/src/vm-commands.mjs +42 -7
- package/vendor/capabilities/capabilities/admin-capacity-reservations.js +31 -61
- package/vendor/capabilities/capabilities/admin-enrollment.js +9 -9
- package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +6 -6
- package/vendor/capabilities/capabilities/admin.js +7 -7
- package/vendor/capabilities/capabilities/commerce-market.js +330 -0
- package/vendor/capabilities/capabilities/fleet.js +57 -21
- package/vendor/capabilities/capabilities/listings.js +137 -146
- package/vendor/capabilities/capabilities/market-shared.js +44 -0
- package/vendor/capabilities/capabilities/markets.js +449 -0
- package/vendor/capabilities/capabilities/networks.js +7 -0
- package/vendor/capabilities/capabilities/tokens.js +12 -9
- package/vendor/capabilities/capability.js +2 -1
- package/vendor/capabilities/cli-catalog.js +4 -2
- package/vendor/capabilities/cli-migrations.js +16 -43
- package/vendor/capabilities/cli.js +1 -1
- package/vendor/capabilities/spec-catalog.js +26 -3
- package/vendor/capabilities/supplier-directory.js +20 -0
- package/vendor/capabilities/capabilities/capacity-offers.js +0 -368
|
@@ -9,39 +9,21 @@ export const LEGACY_CLI_MIGRATIONS = [
|
|
|
9
9
|
{ legacy: ["catalog", "search"], replacement: "ornn capacity-offers list" },
|
|
10
10
|
{ legacy: ["gpu-types"], replacement: "ornn capacity-offers list" },
|
|
11
11
|
{ legacy: ["availability", "list"], replacement: "ornn capacity-offers list" },
|
|
12
|
-
{ legacy: ["availability", "forecast"], replacement: "ornn
|
|
12
|
+
{ legacy: ["availability", "forecast"], replacement: "ornn schedules list" },
|
|
13
13
|
{ legacy: ["availability", "show"], replacement: "ornn capacity-offers show" },
|
|
14
14
|
{ legacy: ["listing", "list"], replacement: "ornn capacity-offers list" },
|
|
15
|
-
{ legacy: ["listing", "forecast"], replacement: "ornn
|
|
15
|
+
{ legacy: ["listing", "forecast"], replacement: "ornn schedules list" },
|
|
16
16
|
{ legacy: ["listing", "show"], replacement: "ornn capacity-offers show" },
|
|
17
17
|
{ legacy: ["listings", "list"], replacement: "ornn capacity-offers list" },
|
|
18
|
-
{ legacy: ["listings", "forecast"], replacement: "ornn
|
|
18
|
+
{ legacy: ["listings", "forecast"], replacement: "ornn schedules list" },
|
|
19
19
|
{ legacy: ["listings", "show"], replacement: "ornn capacity-offers show" },
|
|
20
20
|
{ legacy: ["listings", "create"], replacement: "ornn admin capacity-offers create" },
|
|
21
|
-
{
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
|
|
27
|
-
replacement: "ornn capacity-offers list (use the Ornn web app for commercial workflows)",
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
legacy: ["bid", "list"],
|
|
31
|
-
replacement: "ornn capacity-offers list (use the Ornn web app for commercial workflows)",
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
legacy: ["bid", "show"],
|
|
35
|
-
replacement: "ornn capacity-offers list (use the Ornn web app for commercial workflows)",
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
legacy: ["bid", "update"],
|
|
39
|
-
replacement: "ornn capacity-offers list (use the Ornn web app for commercial workflows)",
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
legacy: ["bid", "withdraw"],
|
|
43
|
-
replacement: "ornn capacity-offers list (use the Ornn web app for commercial workflows)",
|
|
44
|
-
},
|
|
21
|
+
{ legacy: ["buy"], replacement: "ornn offers post" },
|
|
22
|
+
{ legacy: ["bid", "create"], replacement: "ornn spot bid" },
|
|
23
|
+
{ legacy: ["bid", "list"], replacement: "ornn spot orders" },
|
|
24
|
+
{ legacy: ["bid", "show"], replacement: "ornn spot orders" },
|
|
25
|
+
{ legacy: ["bid", "update"], replacement: "ornn spot bid" },
|
|
26
|
+
{ legacy: ["bid", "withdraw"], replacement: "ornn spot cancel" },
|
|
45
27
|
{
|
|
46
28
|
legacy: ["exchange", "create"],
|
|
47
29
|
replacement: "ornn capacity-offers list (use the Ornn web app for commercial workflows)",
|
|
@@ -86,14 +68,8 @@ export const LEGACY_CLI_MIGRATIONS = [
|
|
|
86
68
|
legacy: ["gpus", "checkout"],
|
|
87
69
|
replacement: "ornn capacity-offers list (use the Ornn web app for commercial workflows)",
|
|
88
70
|
},
|
|
89
|
-
{
|
|
90
|
-
|
|
91
|
-
replacement: "ornn capacity-offers list (use the Ornn web app for commercial workflows)",
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
legacy: ["reservations", "checkout"],
|
|
95
|
-
replacement: "ornn capacity-offers list (use the Ornn web app for commercial workflows)",
|
|
96
|
-
},
|
|
71
|
+
{ legacy: ["tenant", "reservations"], replacement: "ornn reservations list" },
|
|
72
|
+
{ legacy: ["reservations", "checkout"], replacement: "ornn reservations show" },
|
|
97
73
|
{
|
|
98
74
|
legacy: ["reservations", "operator"],
|
|
99
75
|
replacement: "ornn capacity-offers list (use the Ornn web app for commercial workflows)",
|
|
@@ -247,13 +223,10 @@ export const LEGACY_CLI_MIGRATIONS = [
|
|
|
247
223
|
legacy: ["billing"],
|
|
248
224
|
replacement: "ornn capacity-offers list (use the Ornn web app for commercial workflows)",
|
|
249
225
|
},
|
|
250
|
-
{
|
|
251
|
-
legacy: ["reservations", "commerce", "list"],
|
|
252
|
-
replacement: "ornn capacity-offers list (use the Ornn web app for commercial workflows)",
|
|
253
|
-
},
|
|
226
|
+
{ legacy: ["reservations", "commerce", "list"], replacement: "ornn reservations list" },
|
|
254
227
|
{ legacy: ["reservations", "commerce", "create"], replacement: "ornn admin reservations create" },
|
|
255
|
-
{ legacy: ["operator", "list"], replacement: "ornn admin
|
|
256
|
-
{ legacy: ["operators", "list"], replacement: "ornn admin
|
|
228
|
+
{ legacy: ["operator", "list"], replacement: "ornn admin suppliers list" },
|
|
229
|
+
{ legacy: ["operators", "list"], replacement: "ornn admin suppliers list" },
|
|
257
230
|
{ legacy: ["facilities", "list"], replacement: "ornn admin facilities list" },
|
|
258
231
|
{ legacy: ["tokens", "list"], replacement: "ornn admin enrollment-tokens list" },
|
|
259
232
|
{ legacy: ["tokens", "create"], replacement: "ornn admin enrollment-tokens create" },
|
|
@@ -270,8 +243,8 @@ export const LEGACY_CLI_MIGRATIONS = [
|
|
|
270
243
|
{ legacy: ["node", "terminate"], replacement: "ornn admin nodes terminate" },
|
|
271
244
|
{ legacy: ["node", "off-grid"], replacement: "ornn admin nodes suspend-management" },
|
|
272
245
|
{ legacy: ["node", "on-grid"], replacement: "ornn admin nodes resume-management" },
|
|
273
|
-
{ legacy: ["operator"], replacement: "ornn admin
|
|
274
|
-
{ legacy: ["operators"], replacement: "ornn admin
|
|
246
|
+
{ legacy: ["operator"], replacement: "ornn admin suppliers list" },
|
|
247
|
+
{ legacy: ["operators"], replacement: "ornn admin suppliers list" },
|
|
275
248
|
{ legacy: ["facilities"], replacement: "ornn admin facilities list" },
|
|
276
249
|
{ legacy: ["tokens"], replacement: "ornn admin enrollment-tokens list" },
|
|
277
250
|
{ legacy: ["users"], replacement: "ornn admin users search" },
|
|
@@ -3,5 +3,5 @@ export { catalog } from "./cli-catalog.js";
|
|
|
3
3
|
export { ADMIN_CLI_ROOTS } from "./capabilities/admin.js";
|
|
4
4
|
export { byCliCommand, byMcpName, forRole } from "./capability.js";
|
|
5
5
|
export { CLI_MIGRATION_GUIDE_URL, findLegacyCliMigration, LEGACY_CLI_MIGRATIONS, } from "./cli-migrations.js";
|
|
6
|
-
export { HARDWARE_CATALOG_PATH } from "./spec-catalog.js";
|
|
6
|
+
export { gpuDisplay, HARDWARE_CATALOG_PATH, parseHardwareCatalog } from "./spec-catalog.js";
|
|
7
7
|
export { SOFTWARE_CATALOG_PATH } from "./software-catalog.js";
|
|
@@ -66,6 +66,26 @@ export function parseHardwareCatalog(payload) {
|
|
|
66
66
|
export async function loadHardwareCatalog(fetchImpl) {
|
|
67
67
|
return parseHardwareCatalog(await fetchImpl(HARDWARE_CATALOG_PATH, { method: "GET" }));
|
|
68
68
|
}
|
|
69
|
+
/** What a GPU slug the catalog does not list renders as. */
|
|
70
|
+
export const UNKNOWN_GPU = "Unknown GPU";
|
|
71
|
+
/**
|
|
72
|
+
* The display name of a stored GPU slug. Every UI and server renderer maps
|
|
73
|
+
* slugs through the catalog's terms; nothing renders a slug. Empty while the
|
|
74
|
+
* catalog is still loading.
|
|
75
|
+
*/
|
|
76
|
+
export function gpuDisplay(terms, slug) {
|
|
77
|
+
if (!terms)
|
|
78
|
+
return "";
|
|
79
|
+
if (!slug)
|
|
80
|
+
return UNKNOWN_GPU;
|
|
81
|
+
return terms.find((term) => term.kind === "gpu" && term.canonical === slug)?.display ?? UNKNOWN_GPU;
|
|
82
|
+
}
|
|
83
|
+
/** GPU picker and filter options: slug values with display labels, in catalog order. */
|
|
84
|
+
export function gpuOptions(terms) {
|
|
85
|
+
return (terms ?? [])
|
|
86
|
+
.filter((term) => term.kind === "gpu")
|
|
87
|
+
.map((term) => ({ value: term.canonical, label: term.display }));
|
|
88
|
+
}
|
|
69
89
|
export function resolveCatalogTerm(terms, kind, query) {
|
|
70
90
|
if (looksLikeSpecDump(query) || !normalizeCatalogQuery(query) || !kind.trim()) {
|
|
71
91
|
return { canonical: null, display: null, hint: UNRESOLVED_CATALOG_HINT };
|
|
@@ -93,8 +113,10 @@ export function bindHardwareFields(terms, input, fields) {
|
|
|
93
113
|
continue;
|
|
94
114
|
}
|
|
95
115
|
const resolved = resolveCatalogTerm(terms, kind, value);
|
|
96
|
-
|
|
97
|
-
|
|
116
|
+
// GPUs are stored as slugs; other hardware keeps its display name.
|
|
117
|
+
const bound = kind === "gpu" ? resolved.canonical : resolved.display;
|
|
118
|
+
if (bound) {
|
|
119
|
+
payload[field] = bound;
|
|
98
120
|
continue;
|
|
99
121
|
}
|
|
100
122
|
rejected.push({ field, value });
|
|
@@ -104,7 +126,7 @@ export function bindHardwareFields(terms, input, fields) {
|
|
|
104
126
|
ok: false,
|
|
105
127
|
error: "unknown_catalog_term",
|
|
106
128
|
rejected,
|
|
107
|
-
hint: "Call listings_catalog_terms and retry with a display name from the dump. Do not ask to add spec-sheet text to the dictionary.",
|
|
129
|
+
hint: "Call listings_catalog_terms and retry with a slug, display name or alias from the dump. Do not ask to add spec-sheet text to the dictionary.",
|
|
108
130
|
};
|
|
109
131
|
}
|
|
110
132
|
return { ok: true, payload };
|
|
@@ -121,6 +143,7 @@ export function projectCatalogTerms(terms, kind) {
|
|
|
121
143
|
.filter((term) => (kind?.trim() ? term.kind.toLowerCase() === kind.toLowerCase() : true))
|
|
122
144
|
.map((term) => ({
|
|
123
145
|
kind: term.kind,
|
|
146
|
+
canonical: term.canonical,
|
|
124
147
|
display: term.display,
|
|
125
148
|
aliases: term.aliases ?? [],
|
|
126
149
|
})),
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** One supplier-filtered page, including server-side name search. */
|
|
2
|
+
export async function listSupplierOrganizationPage(fetch, input) {
|
|
3
|
+
const params = `role=supplier&limit=${input.limit}&offset=${input.offset}&sort=created_at&direction=asc` +
|
|
4
|
+
(input.q?.trim() ? `&q=${encodeURIComponent(input.q.trim())}` : "");
|
|
5
|
+
const page = await fetch(`/v1/organizations?${params}`, { method: "GET" });
|
|
6
|
+
if (!Array.isArray(page))
|
|
7
|
+
throw new Error("Invalid supplier directory response.");
|
|
8
|
+
return page;
|
|
9
|
+
}
|
|
10
|
+
/** Authorization uses offset pagination; never return a partial supplier catalog. */
|
|
11
|
+
export async function listSupplierOrganizations(fetch) {
|
|
12
|
+
const rows = [];
|
|
13
|
+
for (let offset = 0; offset < 50_000; offset += 1000) {
|
|
14
|
+
const page = await listSupplierOrganizationPage(fetch, { limit: 1000, offset });
|
|
15
|
+
rows.push(...page);
|
|
16
|
+
if (page.length < 1000)
|
|
17
|
+
return rows;
|
|
18
|
+
}
|
|
19
|
+
throw new Error("Supplier directory exceeded 50 pages; refusing a partial catalog.");
|
|
20
|
+
}
|
|
@@ -1,368 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { defineCapability, } from "../capability.js";
|
|
3
|
-
const CANONICAL_REFERENCE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
4
|
-
const BASE64URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
5
|
-
const EXACT_DECIMAL = /^(?:0|[1-9]\d*)(?:\.\d+)?$/;
|
|
6
|
-
const DEFAULT_LIMIT = 50;
|
|
7
|
-
export const capacityOffersListInput = z.object({
|
|
8
|
-
$positionals: z.array(z.string()).max(0).default([]),
|
|
9
|
-
cursor: z.string().min(1).optional(),
|
|
10
|
-
limit: z.coerce.number().int().min(1).max(500).optional(),
|
|
11
|
-
});
|
|
12
|
-
export const capacityOfferShowInput = z.object({
|
|
13
|
-
$positionals: z.array(z.string()).max(0).default([]),
|
|
14
|
-
offer: z.string().min(1).optional(),
|
|
15
|
-
});
|
|
16
|
-
function requireInteraction(context) {
|
|
17
|
-
const interaction = context.cli?.interaction;
|
|
18
|
-
if (!interaction)
|
|
19
|
-
throw new Error("Capacity offers require the Ornn CLI interaction runtime.");
|
|
20
|
-
return interaction;
|
|
21
|
-
}
|
|
22
|
-
function record(value) {
|
|
23
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
24
|
-
? value
|
|
25
|
-
: {};
|
|
26
|
-
}
|
|
27
|
-
function text(value) {
|
|
28
|
-
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
29
|
-
}
|
|
30
|
-
function integer(value) {
|
|
31
|
-
return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
|
|
32
|
-
}
|
|
33
|
-
function exactDecimal(value) {
|
|
34
|
-
const candidate = text(value);
|
|
35
|
-
return candidate && EXACT_DECIMAL.test(candidate) ? candidate : null;
|
|
36
|
-
}
|
|
37
|
-
function requireReference(value, field, interaction) {
|
|
38
|
-
if (!value)
|
|
39
|
-
interaction.usage(`--${field} is required.`, { code: "missing_input", field });
|
|
40
|
-
if (!CANONICAL_REFERENCE.test(value)) {
|
|
41
|
-
interaction.usage(`--${field} must be one canonical identifier.`, {
|
|
42
|
-
code: "invalid_reference",
|
|
43
|
-
field,
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
return value;
|
|
47
|
-
}
|
|
48
|
-
function offerFromWire(value, interaction) {
|
|
49
|
-
const item = record(value);
|
|
50
|
-
const ref = text(item.id ?? item.ref);
|
|
51
|
-
const gpuType = text(item.gpu_type ?? item.hardware);
|
|
52
|
-
const gpusPerMachine = integer(item.gpus_per_node ?? item.gpus_per_machine);
|
|
53
|
-
const machines = integer(item.available_node_count ?? item.node_count ?? item.machine_count);
|
|
54
|
-
const gpus = integer(item.available_gpu_count ?? item.gpu_count);
|
|
55
|
-
const facility = text(item.site_nickname ?? item.facility_name ?? item.facility);
|
|
56
|
-
const start = text(item.available_start_at ?? item.start_at ?? item.available_from);
|
|
57
|
-
const end = text(item.available_end_at ?? item.end_at ?? item.available_to);
|
|
58
|
-
const currency = text(item.currency)?.toUpperCase() ?? null;
|
|
59
|
-
const rawFixedValue = item.buy_now_price_per_gpu_hour_exact ?? item.ask_price_per_gpu_hour_exact;
|
|
60
|
-
const fixedValue = exactDecimal(rawFixedValue);
|
|
61
|
-
if (!ref ||
|
|
62
|
-
!CANONICAL_REFERENCE.test(ref) ||
|
|
63
|
-
!gpuType ||
|
|
64
|
-
gpusPerMachine === null ||
|
|
65
|
-
gpusPerMachine < 1 ||
|
|
66
|
-
machines === null ||
|
|
67
|
-
machines < 0 ||
|
|
68
|
-
gpus === null ||
|
|
69
|
-
gpus < 0 ||
|
|
70
|
-
!facility ||
|
|
71
|
-
!start ||
|
|
72
|
-
!end ||
|
|
73
|
-
!currency ||
|
|
74
|
-
!/^[A-Z]{3}$/.test(currency) ||
|
|
75
|
-
(rawFixedValue != null && !fixedValue)) {
|
|
76
|
-
interaction.error("The gateway returned an incomplete capacity offer.", {
|
|
77
|
-
code: "invalid_gateway_response",
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
return {
|
|
81
|
-
ref,
|
|
82
|
-
hardware: {
|
|
83
|
-
gpu_type: gpuType,
|
|
84
|
-
gpus_per_machine: gpusPerMachine,
|
|
85
|
-
cpu: text(item.cpu),
|
|
86
|
-
ram: text(item.ram),
|
|
87
|
-
storage: text(item.storage),
|
|
88
|
-
fabric: text(item.fabric_type ?? item.fabric),
|
|
89
|
-
internet: text(item.internet),
|
|
90
|
-
},
|
|
91
|
-
location: { facility },
|
|
92
|
-
availability: { machine_count: machines, gpu_count: gpus, start, end },
|
|
93
|
-
fixedPrice: fixedValue ? { value: fixedValue, currency, unit: "gpu_hour" } : null,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
function offerData(offer) {
|
|
97
|
-
return {
|
|
98
|
-
ref: offer.ref,
|
|
99
|
-
kind: "capacity_offer",
|
|
100
|
-
hardware: offer.hardware,
|
|
101
|
-
location: offer.location,
|
|
102
|
-
availability: offer.availability,
|
|
103
|
-
fixed_price: offer.fixedPrice,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
function query(parameters) {
|
|
107
|
-
const encoded = Object.entries(parameters)
|
|
108
|
-
.filter((entry) => entry[1] !== undefined)
|
|
109
|
-
.map(([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(String(value))}`)
|
|
110
|
-
.join("&");
|
|
111
|
-
return encoded ? `?${encoded}` : "";
|
|
112
|
-
}
|
|
113
|
-
function encodeOpaque(value) {
|
|
114
|
-
const input = encodeURIComponent(value);
|
|
115
|
-
let output = "";
|
|
116
|
-
for (let index = 0; index < input.length; index += 3) {
|
|
117
|
-
const first = input.charCodeAt(index);
|
|
118
|
-
const second = index + 1 < input.length ? input.charCodeAt(index + 1) : 0;
|
|
119
|
-
const third = index + 2 < input.length ? input.charCodeAt(index + 2) : 0;
|
|
120
|
-
output += BASE64URL[first >> 2];
|
|
121
|
-
output += BASE64URL[((first & 3) << 4) | (second >> 4)];
|
|
122
|
-
if (index + 1 < input.length)
|
|
123
|
-
output += BASE64URL[((second & 15) << 2) | (third >> 6)];
|
|
124
|
-
if (index + 2 < input.length)
|
|
125
|
-
output += BASE64URL[third & 63];
|
|
126
|
-
}
|
|
127
|
-
return output;
|
|
128
|
-
}
|
|
129
|
-
function decodeOpaque(value) {
|
|
130
|
-
if (!value || !/^[A-Za-z0-9_-]+$/.test(value))
|
|
131
|
-
throw new Error("invalid cursor encoding");
|
|
132
|
-
let output = "";
|
|
133
|
-
for (let index = 0; index < value.length; index += 4) {
|
|
134
|
-
const first = BASE64URL.indexOf(value[index]);
|
|
135
|
-
const second = BASE64URL.indexOf(value[index + 1]);
|
|
136
|
-
const third = index + 2 < value.length ? BASE64URL.indexOf(value[index + 2]) : 0;
|
|
137
|
-
const fourth = index + 3 < value.length ? BASE64URL.indexOf(value[index + 3]) : 0;
|
|
138
|
-
if (first < 0 || second < 0 || third < 0 || fourth < 0) {
|
|
139
|
-
throw new Error("invalid cursor encoding");
|
|
140
|
-
}
|
|
141
|
-
output += String.fromCharCode((first << 2) | (second >> 4));
|
|
142
|
-
if (index + 2 < value.length)
|
|
143
|
-
output += String.fromCharCode(((second & 15) << 4) | (third >> 2));
|
|
144
|
-
if (index + 3 < value.length)
|
|
145
|
-
output += String.fromCharCode(((third & 3) << 6) | fourth);
|
|
146
|
-
}
|
|
147
|
-
return decodeURIComponent(output);
|
|
148
|
-
}
|
|
149
|
-
function encodeCursor(cursor) {
|
|
150
|
-
return `ornn-capacity-v1.${encodeOpaque(JSON.stringify(cursor))}`;
|
|
151
|
-
}
|
|
152
|
-
function decodeCursor(raw, interaction) {
|
|
153
|
-
if (!raw)
|
|
154
|
-
return { source: "primary", cursor: null };
|
|
155
|
-
const prefix = "ornn-capacity-v1.";
|
|
156
|
-
try {
|
|
157
|
-
if (!raw.startsWith(prefix))
|
|
158
|
-
throw new Error("wrong cursor kind");
|
|
159
|
-
const decoded = JSON.parse(decodeOpaque(raw.slice(prefix.length)));
|
|
160
|
-
if (!decoded ||
|
|
161
|
-
(decoded.source !== "primary" && decoded.source !== "resale") ||
|
|
162
|
-
(decoded.cursor !== null && typeof decoded.cursor !== "string")) {
|
|
163
|
-
throw new Error("invalid cursor payload");
|
|
164
|
-
}
|
|
165
|
-
return decoded;
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
interaction.usage("--cursor is not a capacity-offer cursor returned by Ornn.", {
|
|
169
|
-
code: "invalid_cursor",
|
|
170
|
-
field: "cursor",
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
async function readPage(context, source, limit, cursor) {
|
|
175
|
-
const path = source === "primary"
|
|
176
|
-
? "/v1/workflows/commerce/listings"
|
|
177
|
-
: "/v1/workflows/commerce/resale-listings";
|
|
178
|
-
const payload = record(await context.fetch(`${path}${query({ limit, cursor: cursor ?? undefined })}`));
|
|
179
|
-
const rows = Array.isArray(payload.listings)
|
|
180
|
-
? payload.listings
|
|
181
|
-
: source === "primary" && Array.isArray(payload.items)
|
|
182
|
-
? payload.items
|
|
183
|
-
: [];
|
|
184
|
-
return { rows, next: text(payload.next_cursor) };
|
|
185
|
-
}
|
|
186
|
-
async function executeCapacityOffersList(context, input) {
|
|
187
|
-
const interaction = requireInteraction(context);
|
|
188
|
-
const limit = input.limit ?? DEFAULT_LIMIT;
|
|
189
|
-
const position = decodeCursor(input.cursor, interaction);
|
|
190
|
-
let page = await readPage(context, position.source, limit, position.cursor);
|
|
191
|
-
let source = position.source;
|
|
192
|
-
if (source === "primary" && page.rows.length === 0 && page.next === null) {
|
|
193
|
-
source = "resale";
|
|
194
|
-
page = await readPage(context, source, limit, null);
|
|
195
|
-
}
|
|
196
|
-
const offers = page.rows.map((item) => offerFromWire(item, interaction));
|
|
197
|
-
let nextCursor = page.next ? encodeCursor({ source, cursor: page.next }) : null;
|
|
198
|
-
if (source === "primary" && page.next === null && offers.length > 0) {
|
|
199
|
-
const resaleProbe = await readPage(context, "resale", 1, null);
|
|
200
|
-
if (resaleProbe.rows.length > 0)
|
|
201
|
-
nextCursor = encodeCursor({ source: "resale", cursor: null });
|
|
202
|
-
}
|
|
203
|
-
const next = nextCursor
|
|
204
|
-
? `ornn capacity-offers list --limit ${limit} --cursor ${nextCursor}`
|
|
205
|
-
: offers[0]
|
|
206
|
-
? `ornn capacity-offers show --offer ${offers[0].ref}`
|
|
207
|
-
: "ornn capacity-offers forecast";
|
|
208
|
-
return interaction.result({
|
|
209
|
-
data: {
|
|
210
|
-
capacity_offers: offers.map(offerData),
|
|
211
|
-
defaults: { limit },
|
|
212
|
-
},
|
|
213
|
-
outcome: { class: "ready", phase: offers.length ? "listed" : "empty" },
|
|
214
|
-
page: { limit, returned: offers.length, next_cursor: nextCursor },
|
|
215
|
-
presentation: {
|
|
216
|
-
lines: offers.length
|
|
217
|
-
? offers.map((offer) => {
|
|
218
|
-
const price = offer.fixedPrice
|
|
219
|
-
? `${offer.fixedPrice.value} ${offer.fixedPrice.currency}/GPU-hour`
|
|
220
|
-
: "price unavailable";
|
|
221
|
-
return `${offer.ref} ${offer.hardware.gpu_type} ${offer.location.facility} ${offer.availability.machine_count} machines ${price}`;
|
|
222
|
-
})
|
|
223
|
-
: ["No capacity offers are available."],
|
|
224
|
-
next,
|
|
225
|
-
},
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
async function executeCapacityOfferShow(context, input) {
|
|
229
|
-
const interaction = requireInteraction(context);
|
|
230
|
-
const ref = requireReference(input.offer, "offer", interaction);
|
|
231
|
-
const offer = offerFromWire(await context.fetch(`/v1/workflows/commerce/listings/${encodeURIComponent(ref)}`), interaction);
|
|
232
|
-
if (offer.ref !== ref) {
|
|
233
|
-
interaction.error("The gateway returned a different capacity offer.", {
|
|
234
|
-
code: "capacity_offer_correlation_changed",
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
const price = offer.fixedPrice
|
|
238
|
-
? `${offer.fixedPrice.value} ${offer.fixedPrice.currency} per GPU-hour`
|
|
239
|
-
: "Unavailable";
|
|
240
|
-
return interaction.result({
|
|
241
|
-
data: { capacity_offer: offerData(offer), defaults: {} },
|
|
242
|
-
outcome: { class: "ready", phase: "available" },
|
|
243
|
-
presentation: {
|
|
244
|
-
lines: [
|
|
245
|
-
`Capacity offer ${offer.ref}`,
|
|
246
|
-
`Hardware: ${offer.hardware.gpu_type}, ${offer.hardware.gpus_per_machine} GPUs per machine`,
|
|
247
|
-
`Location: ${offer.location.facility}`,
|
|
248
|
-
`Availability: ${offer.availability.machine_count} machines, ${offer.availability.start} to ${offer.availability.end}`,
|
|
249
|
-
`Price: ${price}`,
|
|
250
|
-
],
|
|
251
|
-
next: "ornn capacity-offers list",
|
|
252
|
-
},
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
export async function executeCapacityOfferForecast(context, input) {
|
|
256
|
-
const interaction = requireInteraction(context);
|
|
257
|
-
const limit = input.limit ?? DEFAULT_LIMIT;
|
|
258
|
-
const payload = record(await context.fetch(`/v1/workflows/commerce/listings/forecast${query({ limit, cursor: input.cursor })}`));
|
|
259
|
-
const rawListings = Array.isArray(payload.listings) ? payload.listings : [];
|
|
260
|
-
const offers = rawListings.map((item) => {
|
|
261
|
-
const row = record(item);
|
|
262
|
-
const offer = offerFromWire(row, interaction);
|
|
263
|
-
const availability = Array.isArray(row.availability)
|
|
264
|
-
? row.availability.map((bucket) => {
|
|
265
|
-
const interval = record(bucket);
|
|
266
|
-
const start = text(interval.start_at);
|
|
267
|
-
const end = text(interval.end_at);
|
|
268
|
-
const machines = integer(interval.available_nodes);
|
|
269
|
-
const gpus = integer(interval.available_gpus);
|
|
270
|
-
if (!start || !end || machines === null || gpus === null) {
|
|
271
|
-
interaction.error("The gateway returned an incomplete capacity forecast bucket.", {
|
|
272
|
-
code: "invalid_gateway_response",
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
return { start, end, machine_count: machines, gpu_count: gpus };
|
|
276
|
-
})
|
|
277
|
-
: [];
|
|
278
|
-
return { ...offerData(offer), availability };
|
|
279
|
-
});
|
|
280
|
-
const nextCursor = text(payload.next_cursor);
|
|
281
|
-
const next = nextCursor
|
|
282
|
-
? `ornn capacity-offers forecast --limit ${limit} --cursor ${nextCursor}`
|
|
283
|
-
: offers[0]
|
|
284
|
-
? `ornn capacity-offers show --offer ${offers[0].ref}`
|
|
285
|
-
: "ornn capacity-offers list";
|
|
286
|
-
return interaction.result({
|
|
287
|
-
data: {
|
|
288
|
-
generated_at: text(payload.generated_at),
|
|
289
|
-
bucket_hours: integer(payload.bucket_hours),
|
|
290
|
-
horizon_hours: integer(payload.horizon_hours),
|
|
291
|
-
capacity_offers: offers,
|
|
292
|
-
defaults: { limit },
|
|
293
|
-
},
|
|
294
|
-
outcome: { class: "ready", phase: offers.length ? "forecast" : "empty" },
|
|
295
|
-
page: { limit, returned: offers.length, next_cursor: nextCursor },
|
|
296
|
-
presentation: {
|
|
297
|
-
lines: offers.length
|
|
298
|
-
? offers.map((offer) => `${offer.ref} ${offer.hardware.gpu_type} ${offer.location.facility} ${offer.availability.length} forecast buckets`)
|
|
299
|
-
: ["No forecast capacity offers are available."],
|
|
300
|
-
next,
|
|
301
|
-
},
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
export const capacityOfferForecastVnext = {
|
|
305
|
-
arguments: "semantic",
|
|
306
|
-
audience: "customer",
|
|
307
|
-
category: "resource",
|
|
308
|
-
outcome: "Forecast owner-provided capacity availability in bounded pages.",
|
|
309
|
-
human: "ornn capacity-offers forecast [--limit <1-500>] [--cursor <opaque>]",
|
|
310
|
-
automation: "ornn capacity-offers forecast --limit 50 --format json",
|
|
311
|
-
acceptedReferences: ["opaque page cursor"],
|
|
312
|
-
related: ["capacity-offers list"],
|
|
313
|
-
order: 21,
|
|
314
|
-
};
|
|
315
|
-
export const capacityOffersListCapability = defineCapability({
|
|
316
|
-
id: "listings.capacity_list",
|
|
317
|
-
domain: "listings",
|
|
318
|
-
roles: ["user"],
|
|
319
|
-
description: "List primary and resale capacity offers through one customer-safe shape.",
|
|
320
|
-
mutation: "read",
|
|
321
|
-
input: capacityOffersListInput,
|
|
322
|
-
cli: {
|
|
323
|
-
command: ["capacity-offers", "list"],
|
|
324
|
-
vnext: {
|
|
325
|
-
arguments: "semantic",
|
|
326
|
-
audience: "customer",
|
|
327
|
-
category: "resource",
|
|
328
|
-
outcome: "List ordinary capacity offers with bounded opaque pagination and no supplier provenance.",
|
|
329
|
-
human: "ornn capacity-offers list [--limit <1-500>] [--cursor <opaque>]",
|
|
330
|
-
automation: "ornn capacity-offers list --limit 50 --format json",
|
|
331
|
-
acceptedReferences: ["opaque page cursor"],
|
|
332
|
-
related: ["capacity-offers show", "capacity-offers forecast"],
|
|
333
|
-
order: 20,
|
|
334
|
-
},
|
|
335
|
-
},
|
|
336
|
-
mcp: false,
|
|
337
|
-
slack: false,
|
|
338
|
-
execute: executeCapacityOffersList,
|
|
339
|
-
});
|
|
340
|
-
export const capacityOfferShowCapability = defineCapability({
|
|
341
|
-
id: "listings.capacity_show",
|
|
342
|
-
domain: "listings",
|
|
343
|
-
roles: ["user"],
|
|
344
|
-
description: "Show one exact primary or resale capacity offer without supplier provenance.",
|
|
345
|
-
mutation: "read",
|
|
346
|
-
input: capacityOfferShowInput,
|
|
347
|
-
cli: {
|
|
348
|
-
command: ["capacity-offers", "show"],
|
|
349
|
-
vnext: {
|
|
350
|
-
arguments: "semantic",
|
|
351
|
-
audience: "customer",
|
|
352
|
-
category: "resource",
|
|
353
|
-
outcome: "Inspect owner-provided hardware, location, quantity, term, and exact prices.",
|
|
354
|
-
human: "ornn capacity-offers show --offer <id>",
|
|
355
|
-
automation: "ornn capacity-offers show --offer <id> --format json",
|
|
356
|
-
acceptedReferences: ["canonical capacity-offer ID"],
|
|
357
|
-
related: ["capacity-offers list", "capacity-offers forecast"],
|
|
358
|
-
order: 22,
|
|
359
|
-
},
|
|
360
|
-
},
|
|
361
|
-
mcp: false,
|
|
362
|
-
slack: false,
|
|
363
|
-
execute: executeCapacityOfferShow,
|
|
364
|
-
});
|
|
365
|
-
export const capacityOfferCapabilities = [
|
|
366
|
-
capacityOffersListCapability,
|
|
367
|
-
capacityOfferShowCapability,
|
|
368
|
-
];
|