@ornncompute/cli 1.0.67 → 1.0.72

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 CHANGED
@@ -58,7 +58,7 @@ ornn resources
58
58
 
59
59
  The CLI uses Authorization's newest membership on every request, regardless of
60
60
  approval. `ornn context` displays that current organization; login and launch
61
- use the same context. Restricted operations retain their approval requirements.
61
+ use the same context. Reserve browsing and purchasing still require approval.
62
62
 
63
63
  Advanced customer resources are registered under one noun and one verb:
64
64
 
@@ -100,7 +100,7 @@ relisted (secondary) listing term with Markets `POST /v1/markets/bookings`
100
100
  when the window starts. `market` and `offers` call Markets (`/v1/markets`):
101
101
  buy `--nodes` at market (Markets also takes GPU counts and limit bids; the
102
102
  CLI does not send them yet), end a fill with
103
- `market end` (Commerce `POST /commerce/reservations/{id}/stop`), post an offer
103
+ `market end` (Commerce `DELETE /commerce/reservations/{id}`), post an offer
104
104
  on a whole primary listing for one of its terms (`--listing-term-id`, optional
105
105
  `--duration-seconds`), list the offers your organization made or received, and
106
106
  accept or cancel offers by `--offer-id`.
@@ -123,11 +123,15 @@ customer and takes `--organization-id UUID`, `--listing-id UUID`,
123
123
  whole seconds, the term is non-renewable Reserved, and the price is the term's.
124
124
  Reuse the key and payload on retries. Creation requires an eligible human
125
125
  terminal. Native capacity creation takes
126
- `--gpu-slug`, `--capacity-node-count`, `--gpus-per-node`, `--facility-id`,
127
- `--provisioning-type`, `--state`, `--terms` (a JSON array) and `--idempotency-key`,
128
- plus optional `--organization-id`. Supply availability dates or `--days-to-rfs`.
129
- Reuse the key and the same body on retries. Prices are integer micro-USD inside
130
- terms, with explicit duration bounds, nullable deposit and renewable flag.
126
+ `--gpu-slug`, `--gpus-per-node`, `--facility-id`, `--provisioning-type`,
127
+ `--state`, `--terms` (a JSON array) and `--idempotency-key`, plus optional
128
+ `--organization-id`. Give `--capacity-node-count`, or `--node-ids` (a JSON array)
129
+ to attach nodes at creation; the count then defaults to the number of nodes.
130
+ Primary capacity takes `--days-to-rfs` (0 when ready now) or both
131
+ `--available-at` and `--available-until`, not both. Reuse the key and the same
132
+ body on retries. Prices are integer micro-USD inside terms, with explicit
133
+ duration bounds, nullable deposit, renewable flag and optional
134
+ `preemptNoticeSeconds`.
131
135
  `ornn admin capacity-offers update` edits a selected term using `--listing-id`,
132
136
  `--listing-terms-id`, the complete term fields and `--idempotency-key`. Financial
133
137
  editing remains staff-only. The old customer `capacity-offers update` is removed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ornncompute/cli",
3
- "version": "1.0.67",
3
+ "version": "1.0.72",
4
4
  "description": "Command-line interface for Ornn compute access workflows.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -236,17 +236,11 @@ function retiredSyntaxError(argv, findLegacyCliMigration, migrationGuideUrl) {
236
236
  );
237
237
  }
238
238
 
239
- function resolveRequest(catalog, argv, findLegacyCliMigration, migrationGuideUrl) {
240
- if (argv.length === 0 || (argv.length === 1 && (argv[0] === "--help" || argv[0] === "-h"))) {
239
+ function resolveRequest(catalog, argvIn, findLegacyCliMigration, migrationGuideUrl) {
240
+ const argv = argvIn[0] === "--help" || argvIn[0] === "-h" ? ["help", ...argvIn.slice(1)] : argvIn;
241
+ if (argv.length === 0) {
241
242
  return { kind: "root", command: "help" };
242
243
  }
243
- if (argv[0] === "--help" || argv[0] === "-h") {
244
- return {
245
- kind: "error",
246
- command: "help",
247
- error: new VnextUsageError("Usage: ornn [--format human|json|ndjson]"),
248
- };
249
- }
250
244
 
251
245
  const retired = retiredSyntaxError(argv, findLegacyCliMigration, migrationGuideUrl);
252
246
  if (retired) {
@@ -311,15 +305,7 @@ function resolveRequest(catalog, argv, findLegacyCliMigration, migrationGuideUrl
311
305
  if (capability) {
312
306
  const command = vnextCommand(capability);
313
307
  const rest = argv.slice(command.length);
314
- const helpFlags = rest.filter((argument) => argument === "--help" || argument === "-h");
315
- if (helpFlags.length) {
316
- if (rest.length !== 1) {
317
- return {
318
- kind: "error",
319
- command: command.join(" "),
320
- error: new VnextUsageError(`Usage: ${capability.cli.vnext.human}`),
321
- };
322
- }
308
+ if (rest.some((argument) => argument === "--help" || argument === "-h")) {
323
309
  return { kind: "help", command: `help ${command.join(" ")}`, capability };
324
310
  }
325
311
  if (rest.length && capability.cli.vnext.arguments !== "semantic") {
@@ -875,8 +875,8 @@ async function selectedOrganization(context, runtime) {
875
875
  const selected = record(record(result.data).selected_organization);
876
876
  const id = text(selected.id);
877
877
  const name = text(selected.name);
878
- if (!id || !name || selected.approved !== true) {
879
- runtime.usage("Your current organization must be approved before launch.", {
878
+ if (!id || !name) {
879
+ runtime.usage("Join an organization before launching compute.", {
880
880
  code: "organization_required",
881
881
  field: "organization",
882
882
  });
@@ -415,8 +415,8 @@ async function selectedOrganization(context, interaction, expectedId) {
415
415
  const selected = record(record(result.data).selected_organization);
416
416
  const id = text(selected.id);
417
417
  const name = text(selected.name);
418
- if (!id || !name || selected.approved !== true) {
419
- interaction.usage("Your current organization must be approved before launch.", {
418
+ if (!id || !name) {
419
+ interaction.usage("Join an organization before launching compute.", {
420
420
  code: "organization_required",
421
421
  field: "organization",
422
422
  });
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { defineCapability, preview } from "../capability.js";
3
+ import { ensureFacility, locateRegion, unplacedLocation } from "./locality.js";
3
4
  import { nativeListingCreateInput } from "./native-listing.js";
4
5
  import { idempotencyHeaders, microUsd } from "./market-shared.js";
5
6
  function adminHelp(command, outcome, human, automation, review) {
@@ -18,22 +19,28 @@ export const adminCapacityOffersCreateCapability = defineCapability({
18
19
  id: "listing.create",
19
20
  domain: "listings",
20
21
  roles: ["admin"],
21
- description: "Create native capacity with explicit organization/facility IDs, hardware, availability and advertised terms. Returns {listing, terms}. Requires an idempotencyKey reused on retries and confirm: true.",
22
+ description: "Create native capacity with an organization, a facilityId or a location, hardware, availability and advertised terms. A location is any state, city, address, or catalog region (us-west-3); Ornn places it in a catalog region and uses the organization's facility there. Primary capacity needs daysToRfs (0 when ready now) or both availableAt and availableUntil. nodeIds attach nodes at creation. Returns {listing, terms}. Requires an idempotencyKey reused on retries and confirm: true.",
22
23
  mutation: "preview-confirm",
23
24
  http: { method: "POST", path: "/commerce/v2/listings" },
24
25
  input: nativeListingCreateInput,
25
26
  slack: false,
26
27
  cli: {
27
28
  command: ["admin", "capacity-offers", "create"],
28
- vnext: adminHelp(["admin", "capacity-offers", "create"], "Create native capacity and its advertised financial terms.", "ornn admin capacity-offers create --gpu-slug GPU --capacity-node-count COUNT --gpus-per-node COUNT --facility-id UUID --provisioning-type bare-metal|virtual-machine --state draft|available --terms JSON --idempotency-key KEY [--organization-id UUID] [--available-at TIME --available-until TIME] [--days-to-rfs DAYS]", "Requires an eligible human terminal; reuse --idempotency-key on retries.", "Review the native parent fields and each financial term before default-no consent."),
29
+ vnext: adminHelp(["admin", "capacity-offers", "create"], "Create native capacity and its advertised financial terms.", "ornn admin capacity-offers create --gpu-slug GPU --gpus-per-node COUNT (--facility-id UUID | --location PLACE) --provisioning-type bare-metal|virtual-machine --state draft|available --terms JSON --idempotency-key KEY (--days-to-rfs DAYS | --available-at TIME --available-until TIME) [--capacity-node-count COUNT] [--node-ids JSON] [--organization-id UUID]", "Requires an eligible human terminal; --location also needs --organization-id; reuse --idempotency-key on retries.", "Review the native parent fields and each financial term before default-no consent."),
29
30
  },
30
31
  execute: async (ctx, input) => {
31
- const { confirm: _confirm, idempotencyKey, ...body } = nativeListingCreateInput.parse(input);
32
+ const { confirm: _confirm, idempotencyKey, location, ...body } = nativeListingCreateInput.parse(input);
33
+ const located = location ? await locateRegion(ctx.fetch, location) : null;
34
+ if (location && !located)
35
+ throw new Error(unplacedLocation(location));
32
36
  if (!ctx.confirmed)
33
- return preview("listing.create", body);
37
+ return preview("listing.create", located ? { ...body, located } : body);
38
+ const facilityId = located
39
+ ? (await ensureFacility(ctx.fetch, body.organizationId, located.region)).facilityId
40
+ : body.facilityId;
34
41
  return ctx.fetch("/commerce/v2/listings", {
35
42
  method: "POST",
36
- body,
43
+ body: { ...body, facilityId },
37
44
  headers: idempotencyHeaders(ctx, idempotencyKey),
38
45
  });
39
46
  },
@@ -1,6 +1,7 @@
1
1
  import { listSupplierOrganizations } from "../supplier-directory.js";
2
2
  import { z } from "zod";
3
3
  import { defineCapability, preview } from "../capability.js";
4
+ import { ensureFacility, LOCATION_LOOKUP_UNAVAILABLE, locateRegion, unplacedLocation, } from "./locality.js";
4
5
  import { bindHardwareFields, HARDWARE_LISTING_FIELDS, HARDWARE_LISTING_FILTERS, loadHardwareCatalog, projectCatalogTerms, resolveCatalogTerm, } from "../spec-catalog.js";
5
6
  function queryPath(path, params) {
6
7
  const parts = [];
@@ -30,7 +31,10 @@ export const listingCreateInput = z.object({
30
31
  fabric_type: z.string().optional(),
31
32
  node_count: z.number().int().optional(),
32
33
  gpus_per_node: z.number().int().optional(),
33
- site_nickname: z.string().optional(),
34
+ site_nickname: z
35
+ .string()
36
+ .optional()
37
+ .describe("A state, city, address, or catalog region such as us-west-3."),
34
38
  site_supplier: z.string().optional(),
35
39
  ram: z.string().optional(),
36
40
  storage: z.string().optional(),
@@ -67,7 +71,10 @@ const slackListingCreateParams = {
67
71
  fabric_type: { type: "string" },
68
72
  node_count: { type: "integer" },
69
73
  gpus_per_node: { type: "integer" },
70
- site_nickname: { type: "string" },
74
+ site_nickname: {
75
+ type: "string",
76
+ description: "Where the capacity is, exactly as the user said it: a state, city, address, or catalog region such as us-west-3. Ornn places it in a catalog region.",
77
+ },
71
78
  site_supplier: { type: "string" },
72
79
  ram: { type: "string" },
73
80
  storage: { type: "string" },
@@ -242,7 +249,7 @@ export const createListingCapability = defineCapability({
242
249
  let organizationId = parsed.organizationId ?? parsed.organization_id;
243
250
  let facilityId = parsed.facilityId ?? parsed.facility_id;
244
251
  const supplierQuery = (parsed.site_supplier ?? "").trim().toLowerCase();
245
- const facilityQuery = (parsed.site_nickname ?? parsed.site ?? "").trim().toLowerCase();
252
+ const place = (parsed.site_nickname ?? parsed.site ?? "").trim();
246
253
  if (!organizationId && supplierQuery) {
247
254
  const matches = (await listSupplierOrganizations(ctx.fetch)).filter((org) => org.id.toLowerCase() === supplierQuery || org.name.toLowerCase() === supplierQuery);
248
255
  if (matches.length !== 1) {
@@ -250,7 +257,19 @@ export const createListingCapability = defineCapability({
250
257
  }
251
258
  organizationId = matches[0].id;
252
259
  }
253
- if ((facilityId && !organizationId) || (!facilityId && (facilityQuery || organizationId))) {
260
+ let located = null;
261
+ let lookupUnavailable = false;
262
+ if (!facilityId && place) {
263
+ try {
264
+ located = await locateRegion(ctx.fetch, place);
265
+ }
266
+ catch (error) {
267
+ if (ctx.confirmed)
268
+ throw error;
269
+ lookupUnavailable = true;
270
+ }
271
+ }
272
+ if ((facilityId && !organizationId) || (!facilityId && !place && organizationId)) {
254
273
  const matches = [];
255
274
  const seenCursors = new Set();
256
275
  let cursor;
@@ -262,14 +281,7 @@ export const createListingCapability = defineCapability({
262
281
  throw new Error("invalid facility catalog");
263
282
  matches.push(...response.facilities.filter((facility) => (!organizationId ||
264
283
  (facility.organizationId ?? facility.organization_id) === organizationId) &&
265
- (facilityId
266
- ? (facility.facilityId ?? facility.id) === facilityId
267
- : !facilityQuery ||
268
- [
269
- facility.facilityId ?? facility.id,
270
- facility.region,
271
- facility.displayName ?? facility.display_name,
272
- ].some((value) => value?.toLowerCase() === facilityQuery))));
284
+ (!facilityId || (facility.facilityId ?? facility.id) === facilityId)));
273
285
  cursor = response.nextCursor ?? undefined;
274
286
  if (cursor) {
275
287
  if (seenCursors.has(cursor) || seenCursors.size >= 49) {
@@ -302,6 +314,10 @@ export const createListingCapability = defineCapability({
302
314
  const gpusPerNode = parsed.gpus_per_node ?? 8;
303
315
  const boundPayload = bound.payload;
304
316
  const draft = Object.fromEntries(Object.entries(boundPayload).filter(([key]) => !LISTING_CREATE_INPUT_ONLY_KEYS.includes(key)));
317
+ if (located)
318
+ draft.site_nickname = located.name;
319
+ else if (place && !facilityId && !lookupUnavailable)
320
+ delete draft.site_nickname;
305
321
  const gpuSlug = normalized.gpu_type
306
322
  ? resolveCatalogTerm(hardware.terms, "gpu", normalized.gpu_type).canonical
307
323
  : null;
@@ -318,11 +334,12 @@ export const createListingCapability = defineCapability({
318
334
  percent: parsed.deposit_percent,
319
335
  })
320
336
  : 0);
321
- const listing = organizationId && facilityId && gpuSlug && buyerMicro != null
337
+ const priced = Boolean(organizationId && gpuSlug && buyerMicro != null);
338
+ const listingAt = (facility) => priced && facility
322
339
  ? primaryListingBody({
323
- organizationId,
324
- facilityId,
325
- gpuSlug,
340
+ organizationId: organizationId,
341
+ facilityId: facility,
342
+ gpuSlug: gpuSlug,
326
343
  gpusPerNode,
327
344
  provisioningType: parsed.access_mode ?? "virtual-machine",
328
345
  availableAt: startsAt,
@@ -337,13 +354,24 @@ export const createListingCapability = defineCapability({
337
354
  }, buyerMicro, downPaymentCents)
338
355
  : null;
339
356
  if (!ctx.confirmed) {
357
+ const listing = listingAt(facilityId);
340
358
  return preview("create_listing", {
341
359
  ...draft,
342
360
  node_count: nodeCount,
343
361
  gpus_per_node: gpusPerNode,
362
+ ...(located ? { facility_region: located.region } : {}),
363
+ ...(lookupUnavailable
364
+ ? { location_unavailable: place, location_hint: LOCATION_LOOKUP_UNAVAILABLE }
365
+ : place && !facilityId && !located
366
+ ? { location_unresolved: place, location_hint: unplacedLocation(place) }
367
+ : {}),
344
368
  ...(listing ? { listing } : {}),
345
369
  });
346
370
  }
371
+ if (located && !facilityId && priced) {
372
+ facilityId = (await ensureFacility(ctx.fetch, organizationId, located.region)).facilityId;
373
+ }
374
+ const listing = listingAt(facilityId);
347
375
  if (!listing) {
348
376
  return {
349
377
  ok: false,
@@ -0,0 +1,42 @@
1
+ import { statusOf } from "./staff-shared.js";
2
+ export const LOCALITY_GEOLOCATE_PATH = "/v1/orchestrator/locality/geolocate";
3
+ export const FACILITY_ENSURE_PATH = "/provisioning/facilities/ensure";
4
+ /**
5
+ * `GET /v1/orchestrator/locality/geolocate?address=`: the catalog region a typed
6
+ * place stands in. Orchestrator answers a region code or name as itself and
7
+ * geocodes anything else (state, city, address) to its nearest region.
8
+ * Null means Orchestrator could not place the text (404); an unavailable
9
+ * geocoder or gateway throws, so callers ask for a retry rather than new text.
10
+ */
11
+ export async function locateRegion(fetch, place) {
12
+ const address = place.trim();
13
+ if (!address)
14
+ return null;
15
+ let located;
16
+ try {
17
+ located = (await fetch(`${LOCALITY_GEOLOCATE_PATH}?address=${encodeURIComponent(address)}`, {
18
+ method: "GET",
19
+ }));
20
+ }
21
+ catch (error) {
22
+ if (statusOf(error) === 404)
23
+ return null;
24
+ throw error;
25
+ }
26
+ const region = typeof located?.facility_slug === "string" ? located.facility_slug.trim() : "";
27
+ if (!region)
28
+ return null;
29
+ const name = typeof located?.region === "string" && located.region.trim() ? located.region.trim() : region;
30
+ return { region, name };
31
+ }
32
+ export function unplacedLocation(place) {
33
+ return `Ornn could not place "${place}". Give a state, city, address, or catalog region such as us-west-3.`;
34
+ }
35
+ export const LOCATION_LOOKUP_UNAVAILABLE = "Ornn's location lookup is unavailable right now. Keep this location and try again in a moment.";
36
+ /** `POST /internal/v1/facilities/ensure`: the organization's facility at a catalog region. */
37
+ export async function ensureFacility(fetch, organizationId, region) {
38
+ return (await fetch(FACILITY_ENSURE_PATH, {
39
+ method: "POST",
40
+ body: { organizationId, region },
41
+ }));
42
+ }
@@ -293,7 +293,7 @@ export const marketEndCapability = defineCapability({
293
293
  roles: ["user"],
294
294
  description: "End a running fill: its reservation stops, its VMs stop, and its GPUs go back on the book. Requires confirm: true.",
295
295
  mutation: "preview-confirm",
296
- http: { method: "POST", path: "/commerce/reservations/{fillId}/stop" },
296
+ http: { method: "DELETE", path: "/commerce/reservations/{fillId}" },
297
297
  input: marketEndInput,
298
298
  cli: {
299
299
  command: ["market", "end"],
@@ -308,7 +308,7 @@ export const marketEndCapability = defineCapability({
308
308
  ], { fillId: parsed.fillId });
309
309
  if (gate)
310
310
  return gate;
311
- return ctx.fetch(`/commerce/reservations/${seg(parsed.fillId)}/stop`, { method: "POST" });
311
+ return ctx.fetch(`/commerce/reservations/${seg(parsed.fillId)}`, { method: "DELETE" });
312
312
  },
313
313
  });
314
314
  export const offersListInput = z.object({
@@ -7,35 +7,62 @@ export const nativeTermFields = {
7
7
  pricePerGpuHourMicroUsd: z.coerce.number().int().positive(),
8
8
  downPaymentCents: z.coerce.number().int().nonnegative().nullable().optional(),
9
9
  renewable: flag,
10
+ preemptNoticeSeconds: z.coerce.number().int().nonnegative().optional(),
10
11
  };
11
12
  export const nativeTermInput = z.object(nativeTermFields).strict();
12
- export const nativeTermsInput = z.preprocess((value) => {
13
- if (typeof value !== "string")
14
- return value;
15
- try {
16
- return JSON.parse(value);
17
- }
18
- catch {
19
- return value;
20
- }
21
- }, z.array(nativeTermInput));
13
+ function jsonArray(item) {
14
+ return z.preprocess((value) => {
15
+ if (typeof value !== "string")
16
+ return value;
17
+ try {
18
+ return JSON.parse(value);
19
+ }
20
+ catch {
21
+ return value;
22
+ }
23
+ }, z.array(item));
24
+ }
25
+ export const nativeTermsInput = jsonArray(nativeTermInput);
22
26
  export const listingWriteControls = {
23
27
  confirm: z.boolean().optional(),
24
28
  idempotencyKey: z.string().min(1).max(200),
25
29
  };
30
+ const listingLocation = z
31
+ .string()
32
+ .trim()
33
+ .min(1)
34
+ .max(256)
35
+ .optional()
36
+ .describe("Instead of facilityId: a state, city, address, or catalog region such as us-west-3. Ornn places it in a catalog region and uses the supplier's facility there.");
26
37
  export const nativeListingCreateInput = z
27
38
  .object({
28
39
  organizationId: z.string().uuid().nullable().optional(),
29
40
  kind: z.enum(["primary", "secondary"]).default("primary"),
30
41
  gpuSlug: z.string().min(1),
31
42
  gpusPerNode: z.coerce.number().int().positive(),
32
- capacityNodeCount: z.coerce.number().int().positive(),
33
- facilityId: z.string().uuid(),
43
+ capacityNodeCount: z.coerce
44
+ .number()
45
+ .int()
46
+ .positive()
47
+ .optional()
48
+ .describe("Defaults to the number of nodeIds."),
49
+ nodeIds: jsonArray(z.string().uuid()).optional(),
50
+ facilityId: z.string().uuid().optional(),
51
+ location: listingLocation,
34
52
  provisioningType: z.enum(["bare-metal", "virtual-machine"]),
35
53
  state: z.enum(["draft", "available"]),
36
- availableAt: z.iso.datetime({ offset: true }).nullable().optional(),
54
+ availableAt: z.iso
55
+ .datetime({ offset: true })
56
+ .nullable()
57
+ .optional()
58
+ .describe("Primary capacity takes availableAt and availableUntil, or daysToRfs, not both."),
37
59
  availableUntil: z.iso.datetime({ offset: true }).nullable().optional(),
38
- daysToRfs: z.coerce.number().int().nonnegative().optional(),
60
+ daysToRfs: z.coerce
61
+ .number()
62
+ .int()
63
+ .nonnegative()
64
+ .optional()
65
+ .describe("Days until ready for service; 0 when ready now. Required unless both dates are set."),
39
66
  cpu: z.string().optional(),
40
67
  ram: z.string().optional(),
41
68
  storage: z.string().optional(),
@@ -44,13 +71,22 @@ export const nativeListingCreateInput = z
44
71
  terms: nativeTermsInput,
45
72
  ...listingWriteControls,
46
73
  })
47
- .strict();
74
+ .strict()
75
+ .refine((value) => Boolean(value.facilityId) !== Boolean(value.location), {
76
+ message: "Give exactly one of facilityId or location.",
77
+ path: ["facilityId"],
78
+ })
79
+ .refine((value) => !value.location || Boolean(value.organizationId), {
80
+ message: "location needs organizationId, the supplier whose facility it is.",
81
+ path: ["organizationId"],
82
+ });
48
83
  export const nativeListingUpdateFields = {
49
84
  state: z.enum(["draft", "available", "delisted"]).optional(),
50
85
  nodeCount: z.coerce.number().int().positive().optional(),
51
86
  gpuSlug: z.string().min(1).optional(),
52
87
  gpusPerNode: z.coerce.number().int().positive().optional(),
53
88
  facilityId: z.string().uuid().optional(),
89
+ location: listingLocation,
54
90
  provisioningType: z.enum(["bare-metal", "virtual-machine"]).optional(),
55
91
  availableAt: z.iso.datetime({ offset: true }).optional(),
56
92
  availableUntil: z.iso.datetime({ offset: true }).nullable().optional(),