@ornncompute/cli 1.0.25 → 1.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +76 -23
  2. package/package.json +1 -1
  3. package/src/admin-capacity-reservations.mjs +68 -43
  4. package/src/admin-enrollment.mjs +14 -13
  5. package/src/admin-node-lifecycle.mjs +10 -13
  6. package/src/admin-shell.mjs +146 -12
  7. package/src/catalog-shell.mjs +58 -45
  8. package/src/device-auth.mjs +1 -1
  9. package/src/evidence-commands.mjs +25 -11
  10. package/src/gpu-catalog.mjs +21 -0
  11. package/src/identity-file.mjs +28 -0
  12. package/src/kubernetes-commands.mjs +12 -6
  13. package/src/machine-reference.mjs +10 -2
  14. package/src/mount-commands.mjs +4 -2
  15. package/src/slurm-commands.mjs +11 -0
  16. package/src/storage-commands.mjs +27 -7
  17. package/src/vm-commands.mjs +42 -7
  18. package/vendor/capabilities/capabilities/admin-capacity-reservations.js +31 -61
  19. package/vendor/capabilities/capabilities/admin-enrollment.js +9 -9
  20. package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +6 -6
  21. package/vendor/capabilities/capabilities/admin.js +7 -7
  22. package/vendor/capabilities/capabilities/commerce-market.js +330 -0
  23. package/vendor/capabilities/capabilities/fleet.js +57 -21
  24. package/vendor/capabilities/capabilities/listings.js +137 -146
  25. package/vendor/capabilities/capabilities/market-shared.js +44 -0
  26. package/vendor/capabilities/capabilities/markets.js +449 -0
  27. package/vendor/capabilities/capabilities/networks.js +7 -0
  28. package/vendor/capabilities/capabilities/tokens.js +12 -9
  29. package/vendor/capabilities/capability.js +2 -1
  30. package/vendor/capabilities/cli-catalog.js +4 -2
  31. package/vendor/capabilities/cli-migrations.js +16 -43
  32. package/vendor/capabilities/cli.js +1 -1
  33. package/vendor/capabilities/spec-catalog.js +26 -3
  34. package/vendor/capabilities/supplier-directory.js +20 -0
  35. 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 capacity-offers forecast" },
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 capacity-offers forecast" },
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 capacity-offers forecast" },
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
- legacy: ["buy"],
23
- replacement: "ornn capacity-offers list (use the Ornn web app for commercial workflows)",
24
- },
25
- {
26
- legacy: ["bid", "create"],
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
- legacy: ["tenant", "reservations"],
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 operators list" },
256
- { legacy: ["operators", "list"], replacement: "ornn admin operators list" },
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 operators list" },
274
- { legacy: ["operators"], replacement: "ornn admin operators list" },
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
- if (resolved.display) {
97
- payload[field] = resolved.display;
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
- ];