@ornncompute/cli 1.0.48 → 1.0.62

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.
@@ -1,11 +1,30 @@
1
1
  import { z } from "zod";
2
2
  import { defineCapability } from "../capability.js";
3
3
  import { gpuDisplay, loadHardwareCatalog } from "../spec-catalog.js";
4
- import { consent, idempotencyHeaders, microUsdLabel, positionals, query, seg, } from "./market-shared.js";
5
- // Markets verbs: one capability per `/v1/markets` route. Spot order books are
6
- // per GPU slug; schedules are per listing. Prices are integer micro-USD per
7
- // GPU-hour, exactly as Markets reads and writes them.
4
+ import { consent, idempotencyHeaders, microUsd, microUsdLabel, positionals, query, seg, } from "./market-shared.js";
5
+ // Markets verbs: one capability per `/v1/markets` route. Order books are
6
+ // per GPU slug, sale type (Spot or On-demand) and region; Spot and On-demand
7
+ // trade GPUs. Clients buy a number of nodes at market for now; Markets also
8
+ // takes GPU counts and limit bids. Offers are per primary listing. Prices are
9
+ // integer micro-USD per GPU-hour, exactly as Markets reads and writes them.
8
10
  const BASE = "/v1/markets";
11
+ const saleType = z
12
+ .enum(["spot", "on-demand"])
13
+ .optional()
14
+ .describe("spot (the default) yields to scheduled bookings; on-demand runs until stopped or the listing ends.");
15
+ const region = z
16
+ .string()
17
+ .min(1)
18
+ .optional()
19
+ .describe("A region slug, for example us-east-11. Omit for every region.");
20
+ const nodes = z.coerce
21
+ .number()
22
+ .int()
23
+ .min(1)
24
+ .describe("Nodes to buy, each the GPUs of one whole machine.");
25
+ function bookQuery(input) {
26
+ return query({ saleType: input.saleType, region: input.region });
27
+ }
9
28
  function help(command, outcome, human, automation, related, order, acceptedReferences = []) {
10
29
  return {
11
30
  arguments: "semantic",
@@ -20,317 +39,326 @@ function help(command, outcome, human, automation, related, order, acceptedRefer
20
39
  order,
21
40
  };
22
41
  }
23
- const microUsd = z.coerce.number().int().positive();
24
- export const spotBooksInput = z.object({ $positionals: positionals });
25
- export const spotBooksCapability = defineCapability({
26
- id: "spot.books",
42
+ export const marketBooksInput = z.object({
43
+ $positionals: positionals,
44
+ gpuSlug: z.string().min(1).optional().describe("Only this catalog GPU slug."),
45
+ saleType,
46
+ region: z
47
+ .string()
48
+ .min(1)
49
+ .optional()
50
+ .describe("Only this region slug, or any for the books of bids in any region."),
51
+ });
52
+ export const marketBooksCapability = defineCapability({
53
+ id: "market.books",
27
54
  domain: "markets",
28
55
  roles: ["user"],
29
- description: "List the spot order books, one per GPU type, with the best bid and ask in micro-USD per GPU-hour.",
56
+ description: "List the order books, one per GPU type, sale type (Spot or On-demand) and region, with the best bid and ask in micro-USD per GPU-hour.",
30
57
  mutation: "read",
31
- http: { method: "GET", path: `${BASE}/spot/books` },
32
- input: spotBooksInput,
58
+ http: { method: "GET", path: `${BASE}/books` },
59
+ input: marketBooksInput,
33
60
  cli: {
34
- command: ["spot", "books"],
35
- vnext: help(["spot", "books"], "List spot order books by GPU type with their best bid and ask.", "ornn spot books", "ornn spot books --format json", ["spot book", "spot bid"], 30),
61
+ command: ["market", "books"],
62
+ vnext: help(["market", "books"], "List order books by GPU type, sale type and region with their best bid and ask.", "ornn market books [--gpu-slug <slug>] [--sale-type spot|on-demand] [--region <slug>]", "ornn market books --sale-type on-demand --format json", ["market book", "market quote", "market bid"], 31),
36
63
  },
37
64
  slack: false,
38
- execute: async (ctx) => ctx.fetch(`${BASE}/spot/books`, { method: "GET" }),
65
+ execute: async (ctx, input) => {
66
+ const parsed = marketBooksInput.parse(input);
67
+ return ctx.fetch(`${BASE}/books${query({ gpuSlug: parsed.gpuSlug, saleType: parsed.saleType, region: parsed.region })}`, {
68
+ method: "GET",
69
+ });
70
+ },
39
71
  });
40
- export const spotBookInput = z.object({
72
+ export const marketBookInput = z.object({
41
73
  $positionals: positionals,
42
74
  gpuSlug: z.string().min(1).describe("Catalog GPU slug, for example nvidia_h100_sxm."),
75
+ saleType,
76
+ region,
43
77
  });
44
- export const spotBookCapability = defineCapability({
45
- id: "spot.book",
78
+ export const marketBookCapability = defineCapability({
79
+ id: "market.book",
46
80
  domain: "markets",
47
81
  roles: ["user"],
48
- description: "Show one spot order book's depth: GPUs and order counts at each bid and ask price level.",
82
+ description: "Show one GPU type's depth for a sale type, in one region or across all of them: nodes and order counts at each bid and ask price level (ask levels also count their GPUs).",
49
83
  mutation: "read",
50
- http: { method: "GET", path: `${BASE}/spot/books/{gpuSlug}` },
51
- input: spotBookInput,
84
+ http: { method: "GET", path: `${BASE}/books/{gpuSlug}` },
85
+ input: marketBookInput,
52
86
  cli: {
53
- command: ["spot", "book"],
54
- vnext: help(["spot", "book"], "Show bid and ask depth for one GPU type.", "ornn spot book --gpu-slug <slug>", "ornn spot book --gpu-slug <slug> --format json", ["spot books", "spot trades", "spot bid"], 31, ["catalog GPU slug"]),
87
+ command: ["market", "book"],
88
+ vnext: help(["market", "book"], "Show bid and ask depth for one GPU type, sale type and region.", "ornn market book --gpu-slug <slug> [--sale-type spot|on-demand] [--region <slug>]", "ornn market book --gpu-slug <slug> --region us-east-11 --format json", ["market books", "market trades", "market quote"], 32, ["catalog GPU slug", "region slug"]),
55
89
  },
56
90
  slack: false,
57
91
  execute: async (ctx, input) => {
58
- const parsed = spotBookInput.parse(input);
59
- return ctx.fetch(`${BASE}/spot/books/${seg(parsed.gpuSlug)}`, { method: "GET" });
92
+ const parsed = marketBookInput.parse(input);
93
+ return ctx.fetch(`${BASE}/books/${seg(parsed.gpuSlug)}${bookQuery(parsed)}`, {
94
+ method: "GET",
95
+ });
60
96
  },
61
97
  });
62
- export const spotTradesCapability = defineCapability({
63
- id: "spot.trades",
98
+ export const marketTradesCapability = defineCapability({
99
+ id: "market.trades",
64
100
  domain: "markets",
65
101
  roles: ["user"],
66
- description: "List recent spot trades for one GPU type: price, GPUs and time, with no identities.",
102
+ description: "List recent trades for one GPU type and sale type, in one region or all: price, nodes, GPUs, region and time, with no identities.",
67
103
  mutation: "read",
68
- http: { method: "GET", path: `${BASE}/spot/books/{gpuSlug}/trades` },
69
- input: spotBookInput,
104
+ http: { method: "GET", path: `${BASE}/books/{gpuSlug}/trades` },
105
+ input: marketBookInput,
70
106
  cli: {
71
- command: ["spot", "trades"],
72
- vnext: help(["spot", "trades"], "List recent spot fills for one GPU type.", "ornn spot trades --gpu-slug <slug>", "ornn spot trades --gpu-slug <slug> --format json", ["spot book"], 32, ["catalog GPU slug"]),
107
+ command: ["market", "trades"],
108
+ vnext: help(["market", "trades"], "List recent fills for one GPU type, sale type and region.", "ornn market trades --gpu-slug <slug> [--sale-type spot|on-demand] [--region <slug>]", "ornn market trades --gpu-slug <slug> --format json", ["market book"], 33, ["catalog GPU slug", "region slug"]),
73
109
  },
74
110
  slack: false,
75
111
  execute: async (ctx, input) => {
76
- const parsed = spotBookInput.parse(input);
77
- return ctx.fetch(`${BASE}/spot/books/${seg(parsed.gpuSlug)}/trades`, { method: "GET" });
112
+ const parsed = marketBookInput.parse(input);
113
+ return ctx.fetch(`${BASE}/books/${seg(parsed.gpuSlug)}/trades${bookQuery(parsed)}`, {
114
+ method: "GET",
115
+ });
78
116
  },
79
117
  });
80
- export const spotBidInput = z
81
- .object({
118
+ export const marketQuoteInput = z.object({
82
119
  $positionals: positionals,
83
120
  gpuSlug: z.string().min(1).describe("Catalog GPU slug, for example nvidia_h100_sxm."),
84
- gpus: z.coerce
85
- .number()
86
- .int()
87
- .min(8)
88
- .max(8)
89
- .describe("Exactly 8 GPUs on one full node. To buy N nodes, place N bids."),
90
- orderType: z
91
- .enum(["limit", "market"])
92
- .default("limit")
93
- .describe("A limit bid rests until filled; a market bid fills now or is cancelled."),
94
- pricePerGpuHourMicroUsd: microUsd
121
+ saleType,
122
+ region: z
123
+ .string()
124
+ .min(1)
125
+ .optional()
126
+ .describe("A region with an order book (see market books). Omit for any region."),
127
+ nodes,
128
+ });
129
+ export const marketQuoteCapability = defineCapability({
130
+ id: "market.quote",
131
+ domain: "markets",
132
+ roles: ["user"],
133
+ description: "Preview a market bid for nodes without placing it: each node it would take now, cheapest first, with its price per GPU-hour, the GPUs taken there, its region and when it is next scheduled to stop. Fewer nodes than asked means the rest would be cancelled.",
134
+ mutation: "read",
135
+ http: { method: "GET", path: `${BASE}/books/{gpuSlug}/quote` },
136
+ input: marketQuoteInput,
137
+ cli: {
138
+ command: ["market", "quote"],
139
+ vnext: help(["market", "quote"], "Preview which nodes a bid would take now and what each costs.", "ornn market quote --gpu-slug <slug> --nodes <n> [--sale-type spot|on-demand] [--region <slug>]", "ornn market quote --gpu-slug <slug> --nodes 2 --format json", ["market book", "market bid"], 34, ["catalog GPU slug", "region slug"]),
140
+ },
141
+ slack: false,
142
+ execute: async (ctx, input) => {
143
+ const parsed = marketQuoteInput.parse(input);
144
+ return ctx.fetch(`${BASE}/books/${seg(parsed.gpuSlug)}/quote${query({
145
+ nodes: String(parsed.nodes),
146
+ orderType: "market",
147
+ saleType: parsed.saleType,
148
+ region: parsed.region,
149
+ })}`, { method: "GET" });
150
+ },
151
+ });
152
+ export const marketBidInput = z.object({
153
+ $positionals: positionals,
154
+ gpuSlug: z.string().min(1).describe("Catalog GPU slug, for example nvidia_h100_sxm."),
155
+ saleType,
156
+ region: z
157
+ .string()
158
+ .min(1)
159
+ .optional()
160
+ .describe("A region with an order book (see market books), for example us-east-11. Omit to fill in any region."),
161
+ nodes,
162
+ diskId: z
163
+ .string()
164
+ .uuid()
95
165
  .optional()
96
- .describe("Limit price in micro-USD per GPU-hour (1000000 = $1.00). Required for limit bids."),
166
+ .describe("The disk every VM boots. Omit to boot your newest disk, else Ornn's newest."),
97
167
  idempotencyKey: z.string().min(1).max(200).optional(),
98
168
  confirm: z.boolean().optional(),
99
- })
100
- .refine((bid) => (bid.orderType === "limit") === (bid.pricePerGpuHourMicroUsd !== undefined), {
101
- message: "A limit bid needs pricePerGpuHourMicroUsd; a market bid takes no price.",
102
- path: ["pricePerGpuHourMicroUsd"],
103
169
  });
104
- export const spotBidCapability = defineCapability({
105
- id: "spot.bid",
170
+ export const marketBidCapability = defineCapability({
171
+ id: "market.bid",
106
172
  domain: "markets",
107
173
  roles: ["user"],
108
- description: "Place a spot bid for a full node with 8 GPUs. A filled bid becomes a running spot reservation, " +
109
- "prepaid hourly from Ornn credits first and then the saved card. Book prices are what you pay; a limit is the most you pay per GPU-hour. Requires confirm: true.",
174
+ description: "Buy nodes at market, each the GPUs of one whole machine, as Spot (the default; a scheduled booking that needs the GPUs can stop it) or On-demand (runs until stopped or the listing ends), " +
175
+ "in one region or any. The bid takes the cheapest nodes for sale now and cancels the rest. What one listing sells becomes a running reservation (a fill), prepaid hourly from Ornn credits first and then the saved card. " +
176
+ "Book prices are what you pay per GPU-hour. Requires confirm: true.",
110
177
  mutation: "preview-confirm",
111
- http: { method: "POST", path: `${BASE}/spot/orders` },
112
- input: spotBidInput,
178
+ http: { method: "POST", path: `${BASE}/orders` },
179
+ input: marketBidInput,
113
180
  cli: {
114
- command: ["spot", "bid"],
115
- vnext: help(["spot", "bid"], "Place one spot bid for a full node with 8 GPUs.", "ornn spot bid --gpu-slug <slug> --gpus 8 [--order-type limit|market] [--price-per-gpu-hour-micro-usd <n>]", "ornn spot bid --gpu-slug <slug> --gpus 8 --price-per-gpu-hour-micro-usd 2500000 --yes --format json", ["spot orders", "spot cancel"], 33, ["catalog GPU slug"]),
181
+ command: ["market", "bid"],
182
+ vnext: help(["market", "bid"], "Buy nodes at market.", "ornn market bid --gpu-slug <slug> --nodes <n> [--sale-type spot|on-demand] [--region <slug>] [--disk-id <id>]", "ornn market bid --gpu-slug <slug> --nodes 2 --yes --format json", ["market quote", "market orders", "market fills", "market cancel"], 35, ["catalog GPU slug"]),
116
183
  },
117
184
  slack: false,
118
185
  execute: async (ctx, input) => {
119
- const parsed = spotBidInput.parse(input);
186
+ const parsed = marketBidInput.parse(input);
120
187
  const body = {
121
188
  gpuSlug: parsed.gpuSlug,
122
- gpus: parsed.gpus,
123
- orderType: parsed.orderType,
124
- ...(parsed.pricePerGpuHourMicroUsd !== undefined
125
- ? { pricePerGpuHourMicroUsd: parsed.pricePerGpuHourMicroUsd }
126
- : {}),
189
+ ...(parsed.saleType ? { saleType: parsed.saleType } : {}),
190
+ ...(parsed.region ? { region: parsed.region } : {}),
191
+ nodes: parsed.nodes,
192
+ orderType: "market",
193
+ ...(parsed.diskId ? { diskId: parsed.diskId } : {}),
127
194
  };
128
195
  // Only the CLI review renders terms; MCP and Slack preview the payload.
129
196
  const hardware = ctx.cli?.interaction
130
197
  ? await loadHardwareCatalog(ctx.fetch).catch(() => null)
131
198
  : null;
132
- const gate = await consent(ctx, "spot.bid", "Place spot bid", [
199
+ const gate = await consent(ctx, "market.bid", "Place bid", [
133
200
  { label: "GPU type", value: gpuDisplay(hardware?.terms ?? [], parsed.gpuSlug) },
134
- { label: "GPUs", value: String(parsed.gpus) },
135
- { label: "Order", value: parsed.orderType },
136
- { label: "Limit price", value: microUsdLabel(parsed.pricePerGpuHourMicroUsd) },
201
+ { label: "Sale type", value: parsed.saleType === "on-demand" ? "On-demand" : "Spot" },
202
+ { label: "Location", value: parsed.region ?? "Any location" },
203
+ { label: "Nodes", value: String(parsed.nodes) },
204
+ { label: "Order", value: "Market: the cheapest nodes for sale now" },
137
205
  ], body);
138
206
  if (gate)
139
207
  return gate;
140
- return ctx.fetch(`${BASE}/spot/orders`, {
208
+ return ctx.fetch(`${BASE}/orders`, {
141
209
  method: "POST",
142
210
  body,
143
211
  headers: idempotencyHeaders(ctx, parsed.idempotencyKey),
144
212
  });
145
213
  },
146
214
  });
147
- export const spotOrdersInput = z.object({ $positionals: positionals });
148
- export const spotOrdersCapability = defineCapability({
149
- id: "spot.orders",
215
+ export const marketOrdersInput = z.object({ $positionals: positionals });
216
+ export const marketOrdersCapability = defineCapability({
217
+ id: "market.orders",
150
218
  domain: "markets",
151
219
  roles: ["user"],
152
- description: "List this organization's spot bids (resting, running and ended) and the asks its listings have on the books.",
220
+ description: "List this organization's bids (resting, filled and cancelled, each with its fill IDs) and the asks its listings have on the books.",
153
221
  mutation: "read",
154
- http: { method: "GET", path: `${BASE}/spot/orders` },
155
- input: spotOrdersInput,
222
+ http: { method: "GET", path: `${BASE}/orders` },
223
+ input: marketOrdersInput,
156
224
  cli: {
157
- command: ["spot", "orders"],
158
- vnext: help(["spot", "orders"], "List your organization's spot bids and listing asks.", "ornn spot orders", "ornn spot orders --format json", ["spot bid", "spot cancel"], 34),
225
+ command: ["market", "orders"],
226
+ vnext: help(["market", "orders"], "List your organization's bids and listing asks.", "ornn market orders", "ornn market orders --format json", ["market bid", "market fills", "market cancel"], 36),
159
227
  },
160
228
  slack: false,
161
- execute: async (ctx) => ctx.fetch(`${BASE}/spot/orders`, { method: "GET" }),
229
+ execute: async (ctx) => ctx.fetch(`${BASE}/orders`, { method: "GET" }),
162
230
  });
163
- export const spotCancelInput = z.object({
231
+ export const marketCancelInput = z.object({
164
232
  $positionals: positionals,
165
- orderId: z
166
- .string()
167
- .uuid()
168
- .describe("A spot bid ID; for a running bid this is also its reservation ID."),
233
+ orderId: z.string().uuid().describe("A resting bid ID."),
169
234
  confirm: z.boolean().optional(),
170
235
  });
171
- export const spotCancelCapability = defineCapability({
172
- id: "spot.cancel",
236
+ export const marketCancelCapability = defineCapability({
237
+ id: "market.cancel",
173
238
  domain: "markets",
174
239
  roles: ["user"],
175
- description: "Cancel a resting spot bid, or end a running spot reservation (the buyer or the seller may end it; the GPUs go back on the book). Requires confirm: true.",
240
+ description: "Cancel what a resting bid has not bought yet. Its fills keep running; end them with market end. Requires confirm: true.",
176
241
  mutation: "preview-confirm",
177
- http: { method: "DELETE", path: `${BASE}/spot/orders/{orderId}` },
178
- input: spotCancelInput,
242
+ http: { method: "DELETE", path: `${BASE}/orders/{orderId}` },
243
+ input: marketCancelInput,
179
244
  cli: {
180
- command: ["spot", "cancel"],
181
- vnext: help(["spot", "cancel"], "Cancel a resting bid or end a running spot reservation.", "ornn spot cancel --order-id <id>", "ornn spot cancel --order-id <id> --yes --format json", ["spot orders"], 35, ["spot order ID", "spot reservation ID"]),
245
+ command: ["market", "cancel"],
246
+ vnext: help(["market", "cancel"], "Cancel the rest of a resting bid.", "ornn market cancel --order-id <id>", "ornn market cancel --order-id <id> --yes --format json", ["market orders", "market end"], 37, ["order ID"]),
182
247
  },
183
248
  slack: false,
184
249
  execute: async (ctx, input) => {
185
- const parsed = spotCancelInput.parse(input);
186
- const gate = await consent(ctx, "spot.cancel", "Cancel spot order", [
250
+ const parsed = marketCancelInput.parse(input);
251
+ const gate = await consent(ctx, "market.cancel", "Cancel bid", [
187
252
  { label: "Order", value: parsed.orderId },
188
253
  {
189
254
  label: "Consequence",
190
- value: "A resting bid is cancelled; a running spot reservation ends and its machine stops.",
255
+ value: "The bid stops buying. What it already bought keeps running.",
191
256
  },
192
257
  ], { orderId: parsed.orderId });
193
258
  if (gate)
194
259
  return gate;
195
- return ctx.fetch(`${BASE}/spot/orders/${seg(parsed.orderId)}`, { method: "DELETE" });
260
+ return ctx.fetch(`${BASE}/orders/${seg(parsed.orderId)}`, { method: "DELETE" });
196
261
  },
197
262
  });
198
- export const schedulesListInput = z.object({
263
+ export const marketFillsInput = z.object({
199
264
  $positionals: positionals,
200
- listingKind: z.enum(["primary", "secondary"]).optional(),
201
- gpuSlug: z.string().min(1).optional(),
202
- from: z.string().min(1).optional().describe("RFC 3339 lower bound for the schedule window."),
203
- to: z.string().min(1).optional().describe("RFC 3339 upper bound for the schedule window."),
265
+ orderId: z.string().uuid().optional().describe("Only this bid's fills."),
204
266
  });
205
- export const schedulesListCapability = defineCapability({
206
- id: "schedules.list",
267
+ export const marketFillsCapability = defineCapability({
268
+ id: "market.fills",
207
269
  domain: "markets",
208
270
  roles: ["user"],
209
- description: "List reservable schedules with their available windows (sold windows removed). Primary schedules are shown only to approved organizations; secondary (relisted) schedules are open to every signed-in organization.",
271
+ description: "List this organization's fills, newest first, or one bid's: each is one trade of GPUs from one listing at one price. Its ID is the reservation it runs as; read its status from the reservation.",
210
272
  mutation: "read",
211
- http: { method: "GET", path: `${BASE}/schedules` },
212
- input: schedulesListInput,
273
+ http: { method: "GET", path: `${BASE}/fills` },
274
+ input: marketFillsInput,
213
275
  cli: {
214
- command: ["schedules", "list"],
215
- vnext: help(["schedules", "list"], "List primary and secondary schedules with their open windows.", "ornn schedules list [--listing-kind primary|secondary] [--gpu-slug <slug>] [--from <time>] [--to <time>]", "ornn schedules list --listing-kind secondary --format json", ["schedules show", "schedules buy", "offers post"], 40),
276
+ command: ["market", "fills"],
277
+ vnext: help(["market", "fills"], "List the nodes your bids bought, one reservation per fill.", "ornn market fills [--order-id <id>]", "ornn market fills --format json", ["market orders", "market end"], 38, ["order ID"]),
216
278
  },
217
279
  slack: false,
218
280
  execute: async (ctx, input) => {
219
- const parsed = schedulesListInput.parse(input ?? {});
220
- return ctx.fetch(`${BASE}/schedules${query({
221
- listingKind: parsed.listingKind,
222
- gpuSlug: parsed.gpuSlug,
223
- from: parsed.from,
224
- to: parsed.to,
225
- })}`, { method: "GET" });
281
+ const parsed = marketFillsInput.parse(input ?? {});
282
+ return ctx.fetch(`${BASE}/fills${query({ orderId: parsed.orderId })}`, { method: "GET" });
226
283
  },
227
284
  });
228
- export const scheduleShowInput = z.object({
285
+ export const marketEndInput = z.object({
229
286
  $positionals: positionals,
230
- listingId: z.string().uuid(),
231
- });
232
- export const scheduleShowCapability = defineCapability({
233
- id: "schedules.show",
234
- domain: "markets",
235
- roles: ["user"],
236
- description: "Show one listing's schedule: a primary's price, down payment and open offer book, or a secondary's asking price, and its sold windows.",
237
- mutation: "read",
238
- http: { method: "GET", path: `${BASE}/schedules/{listingId}` },
239
- input: scheduleShowInput,
240
- cli: {
241
- command: ["schedules", "show"],
242
- vnext: help(["schedules", "show"], "Show one schedule with its sold windows and offers.", "ornn schedules show --listing-id <id>", "ornn schedules show --listing-id <id> --format json", ["schedules list", "schedules buy", "offers post", "offers accept"], 41, ["listing ID"]),
243
- },
244
- slack: false,
245
- execute: async (ctx, input) => {
246
- const parsed = scheduleShowInput.parse(input);
247
- return ctx.fetch(`${BASE}/schedules/${seg(parsed.listingId)}`, { method: "GET" });
248
- },
249
- });
250
- export const schedulePurchaseInput = z.object({
251
- $positionals: positionals,
252
- listingId: z.string().uuid().describe("A secondary (relisted) listing."),
253
- startsAt: z
254
- .string()
255
- .min(1)
256
- .describe("RFC 3339 start of the window: inside the listing, now or later, on the hour."),
257
- endsAt: z
258
- .string()
259
- .min(1)
260
- .describe("RFC 3339 end of the window, at least an hour after the start."),
261
- askPricePerGpuHourMicroUsd: microUsd.describe("The listing's current asking price in micro-USD per GPU-hour, from schedules show. A changed price refuses the purchase."),
262
- idempotencyKey: z.string().min(1).max(200).optional(),
287
+ fillId: z.string().uuid().describe("A fill ID; it is also its reservation ID."),
263
288
  confirm: z.boolean().optional(),
264
289
  });
265
- export const schedulePurchaseCapability = defineCapability({
266
- id: "schedules.purchase",
290
+ export const marketEndCapability = defineCapability({
291
+ id: "market.end",
267
292
  domain: "markets",
268
293
  roles: ["user"],
269
- description: "Buy a window of a secondary (relisted) listing at its asking price, for every node on it. There is no offer: the window is yours once it is paid. You pay the asking price up front, from Ornn credits and then your saved card; $10,000 or more is invoiced by wire. Requires confirm: true.",
294
+ description: "End a running fill: its reservation stops, its VMs stop, and its GPUs go back on the book. Requires confirm: true.",
270
295
  mutation: "preview-confirm",
271
- http: { method: "POST", path: `${BASE}/schedules/purchases` },
272
- input: schedulePurchaseInput,
296
+ http: { method: "POST", path: "/commerce/reservations/{fillId}/stop" },
297
+ input: marketEndInput,
273
298
  cli: {
274
- command: ["schedules", "buy"],
275
- vnext: help(["schedules", "buy"], "Buy a window of relisted capacity at its asking price.", "ornn schedules buy --listing-id <id> --starts-at <time> --ends-at <time> --ask-price-per-gpu-hour-micro-usd <n>", "ornn schedules buy --listing-id <id> --starts-at <time> --ends-at <time> --ask-price-per-gpu-hour-micro-usd 4000000 --yes --format json", ["schedules show", "reservations show"], 42, ["listing ID"]),
299
+ command: ["market", "end"],
300
+ vnext: help(["market", "end"], "End a running fill and stop its VMs.", "ornn market end --fill-id <id>", "ornn market end --fill-id <id> --yes --format json", ["market fills"], 39, ["fill ID", "reservation ID"]),
276
301
  },
277
302
  slack: false,
278
303
  execute: async (ctx, input) => {
279
- const parsed = schedulePurchaseInput.parse(input);
280
- const body = {
281
- legs: [
282
- {
283
- listingId: parsed.listingId,
284
- startsAt: parsed.startsAt,
285
- endsAt: parsed.endsAt,
286
- askPricePerGpuHourMicroUsd: parsed.askPricePerGpuHourMicroUsd,
287
- },
288
- ],
289
- };
290
- const gate = await consent(ctx, "schedules.purchase", "Buy relisted window", [
291
- { label: "Listing", value: parsed.listingId },
292
- { label: "Window", value: `${parsed.startsAt} to ${parsed.endsAt}` },
293
- { label: "Asking price", value: microUsdLabel(parsed.askPricePerGpuHourMicroUsd) },
294
- {
295
- label: "Payment",
296
- value: "Up front: Ornn credits, then your saved card.",
297
- },
298
- ], body);
304
+ const parsed = marketEndInput.parse(input);
305
+ const gate = await consent(ctx, "market.end", "End fill", [
306
+ { label: "Fill", value: parsed.fillId },
307
+ { label: "Consequence", value: "The reservation ends and its VMs stop." },
308
+ ], { fillId: parsed.fillId });
299
309
  if (gate)
300
310
  return gate;
301
- return ctx.fetch(`${BASE}/schedules/purchases`, {
302
- method: "POST",
303
- body,
304
- headers: idempotencyHeaders(ctx, parsed.idempotencyKey),
305
- });
311
+ return ctx.fetch(`/commerce/reservations/${seg(parsed.fillId)}/stop`, { method: "POST" });
306
312
  },
307
313
  });
308
- export const offersListInput = z.object({ $positionals: positionals });
314
+ export const offersListInput = z.object({
315
+ $positionals: positionals,
316
+ listingId: z
317
+ .string()
318
+ .uuid()
319
+ .optional()
320
+ .describe("A primary listing whose offer book to read. Omit for your organization's offers."),
321
+ });
309
322
  export const offersListCapability = defineCapability({
310
323
  id: "offers.list",
311
324
  domain: "markets",
312
325
  roles: ["user"],
313
- description: "List the offers this organization has posted on schedules, with their status.",
326
+ description: "List offers with their status. Without listingId: the offers this organization made and the offers on listings it owns (onMyListing: true, with netPricePerGpuHourMicroUsd, what it receives). With listingId: that listing's offer book.",
314
327
  mutation: "read",
315
328
  http: { method: "GET", path: `${BASE}/offers` },
316
329
  input: offersListInput,
317
330
  cli: {
318
331
  command: ["offers", "list"],
319
- vnext: help(["offers", "list"], "List the offers your organization has posted.", "ornn offers list", "ornn offers list --format json", ["offers post", "offers cancel"], 42),
332
+ vnext: help(["offers", "list"], "List offers your organization made or received, or one listing's offer book.", "ornn offers list [--listing-id <id>]", "ornn offers list --format json", ["offers post", "offers accept", "offers cancel"], 42, ["listing ID"]),
320
333
  },
321
334
  slack: false,
322
- execute: async (ctx) => ctx.fetch(`${BASE}/offers`, { method: "GET" }),
335
+ execute: async (ctx, input) => {
336
+ const parsed = offersListInput.parse(input ?? {});
337
+ return ctx.fetch(`${BASE}/offers${query({ listingId: parsed.listingId })}`, {
338
+ method: "GET",
339
+ });
340
+ },
323
341
  });
324
342
  export const offerPostInput = z.object({
325
343
  $positionals: positionals,
326
- listingId: z.string().uuid(),
327
- pricePerGpuHourMicroUsd: microUsd.describe("Offer price in micro-USD per GPU-hour (1000000 = $1.00). Covers every node for the rest of the listing."),
344
+ listingId: z.string().uuid().describe("A primary listing."),
345
+ listingTermId: z
346
+ .string()
347
+ .uuid()
348
+ .describe("The listingTermsId of a non-spot term of that listing, from listings.terms."),
349
+ durationSeconds: z.coerce
350
+ .number()
351
+ .int()
352
+ .positive()
353
+ .optional()
354
+ .describe("How long the reservation runs, within the term's minimum and maximum duration. Omit only when they are equal, or when the term has none (the rest of the listing)."),
355
+ pricePerGpuHourMicroUsd: microUsd.describe("Offer price in micro-USD per GPU-hour (1000000 = $1.00). Covers every node on the listing."),
328
356
  downPaymentCents: z.coerce
329
357
  .number()
330
358
  .int()
331
359
  .min(0)
332
360
  .optional()
333
- .describe("Primary listings only: the down payment you offer, in cents."),
361
+ .describe("The down payment you offer, in cents."),
334
362
  idempotencyKey: z.string().min(1).max(200).optional(),
335
363
  confirm: z.boolean().optional(),
336
364
  });
@@ -338,34 +366,43 @@ export const offerPostCapability = defineCapability({
338
366
  id: "offers.post",
339
367
  domain: "markets",
340
368
  roles: ["user"],
341
- description: "Offer a price for a whole primary listing: every node, from the later of the listing's start and now to its end. The listing owner accepts it to create your reservation. Relisted (secondary) capacity takes no offers: buy it with schedules purchase. Requires confirm: true.",
369
+ description: "Offer a price for a whole primary listing on one of its terms: every node, from the later of the listing's start and now, for durationSeconds. The listing owner accepts it to create your reservation. Relisted (secondary) capacity takes no offers: buy its windows with reservations.purchase. Requires confirm: true.",
342
370
  mutation: "preview-confirm",
343
- http: { method: "POST", path: `${BASE}/schedules/{listingId}/offers` },
371
+ http: { method: "POST", path: `${BASE}/offers` },
344
372
  input: offerPostInput,
345
373
  cli: {
346
374
  command: ["offers", "post"],
347
- vnext: help(["offers", "post"], "Offer a price for a whole primary listing.", "ornn offers post --listing-id <id> --price-per-gpu-hour-micro-usd <n> [--down-payment-cents <n>]", "ornn offers post --listing-id <id> --price-per-gpu-hour-micro-usd 2500000 --yes --format json", ["schedules show", "offers list", "offers cancel"], 43, ["listing ID"]),
375
+ vnext: help(["offers", "post"], "Offer a price for a whole primary listing on one of its terms.", "ornn offers post --listing-id <id> --listing-term-id <id> [--duration-seconds <n>] --price-per-gpu-hour-micro-usd <n> [--down-payment-cents <n>]", "ornn offers post --listing-id <id> --listing-term-id <id> --duration-seconds 2592000 --price-per-gpu-hour-micro-usd 2500000 --yes --format json", ["capacity-offers terms", "offers list", "offers cancel"], 43, ["listing ID", "listingTermsId from capacity-offers terms"]),
348
376
  },
349
377
  slack: false,
350
378
  execute: async (ctx, input) => {
351
379
  const parsed = offerPostInput.parse(input);
352
380
  const body = {
381
+ listingId: parsed.listingId,
382
+ listingTermId: parsed.listingTermId,
383
+ ...(parsed.durationSeconds !== undefined ? { durationSeconds: parsed.durationSeconds } : {}),
353
384
  pricePerGpuHourMicroUsd: parsed.pricePerGpuHourMicroUsd,
354
385
  ...(parsed.downPaymentCents !== undefined
355
386
  ? { downPaymentCents: parsed.downPaymentCents }
356
387
  : {}),
357
388
  };
358
- const gate = await consent(ctx, "offers.post", "Post schedule offer", [
389
+ const gate = await consent(ctx, "offers.post", "Post offer", [
359
390
  { label: "Listing", value: parsed.listingId },
360
- { label: "Window", value: "The rest of the listing" },
391
+ { label: "Term", value: parsed.listingTermId },
392
+ {
393
+ label: "Duration",
394
+ value: parsed.durationSeconds !== undefined
395
+ ? `${parsed.durationSeconds} seconds from the later of the listing's start and now`
396
+ : "The term's only duration, or the rest of the listing",
397
+ },
361
398
  { label: "Price", value: microUsdLabel(parsed.pricePerGpuHourMicroUsd) },
362
399
  ...(parsed.downPaymentCents !== undefined
363
400
  ? [{ label: "Down payment", value: `$${(parsed.downPaymentCents / 100).toFixed(2)}` }]
364
401
  : []),
365
- ], { listingId: parsed.listingId, ...body });
402
+ ], body);
366
403
  if (gate)
367
404
  return gate;
368
- return ctx.fetch(`${BASE}/schedules/${seg(parsed.listingId)}/offers`, {
405
+ return ctx.fetch(`${BASE}/offers`, {
369
406
  method: "POST",
370
407
  body,
371
408
  headers: idempotencyHeaders(ctx, parsed.idempotencyKey),
@@ -374,7 +411,6 @@ export const offerPostCapability = defineCapability({
374
411
  });
375
412
  export const offerDecisionInput = z.object({
376
413
  $positionals: positionals,
377
- listingId: z.string().uuid(),
378
414
  offerId: z.string().uuid(),
379
415
  confirm: z.boolean().optional(),
380
416
  });
@@ -382,66 +418,63 @@ export const offerAcceptCapability = defineCapability({
382
418
  id: "offers.accept",
383
419
  domain: "markets",
384
420
  roles: ["user"],
385
- description: "Accept an offer on a primary listing you own. The accepted window becomes the buyer's reservation and overlapping offers and relists from the same holding are cancelled. Requires confirm: true.",
421
+ description: "Accept an offer on a primary listing you own. The buyer's reservation is booked and the listing's overlapping open offers are cancelled. Requires confirm: true.",
386
422
  mutation: "preview-confirm",
387
- http: { method: "POST", path: `${BASE}/schedules/{listingId}/offers/{offerId}/accept` },
423
+ http: { method: "POST", path: `${BASE}/offers/{offerId}/accept` },
388
424
  input: offerDecisionInput,
389
425
  cli: {
390
426
  command: ["offers", "accept"],
391
- vnext: help(["offers", "accept"], "Accept one offer on a listing you own.", "ornn offers accept --listing-id <id> --offer-id <id>", "ornn offers accept --listing-id <id> --offer-id <id> --yes --format json", ["schedules show"], 44, ["listing ID", "offer ID"]),
427
+ vnext: help(["offers", "accept"], "Accept one offer on a listing you own.", "ornn offers accept --offer-id <id>", "ornn offers accept --offer-id <id> --yes --format json", ["offers list"], 44, ["offer ID"]),
392
428
  },
393
429
  slack: false,
394
430
  execute: async (ctx, input) => {
395
431
  const parsed = offerDecisionInput.parse(input);
396
- const gate = await consent(ctx, "offers.accept", "Accept schedule offer", [
397
- { label: "Listing", value: parsed.listingId },
432
+ const gate = await consent(ctx, "offers.accept", "Accept offer", [
398
433
  { label: "Offer", value: parsed.offerId },
399
434
  {
400
435
  label: "Consequence",
401
- value: "The window is sold; overlapping open offers and conflicting relists from this holding are cancelled.",
436
+ value: "The buyer's reservation is booked; this listing's overlapping open offers are cancelled.",
402
437
  },
403
- ], { listingId: parsed.listingId, offerId: parsed.offerId });
438
+ ], { offerId: parsed.offerId });
404
439
  if (gate)
405
440
  return gate;
406
- return ctx.fetch(`${BASE}/schedules/${seg(parsed.listingId)}/offers/${seg(parsed.offerId)}/accept`, { method: "POST", body: {} });
441
+ return ctx.fetch(`${BASE}/offers/${seg(parsed.offerId)}/accept`, {
442
+ method: "POST",
443
+ body: {},
444
+ });
407
445
  },
408
446
  });
409
447
  export const offerCancelCapability = defineCapability({
410
448
  id: "offers.cancel",
411
449
  domain: "markets",
412
450
  roles: ["user"],
413
- description: "Cancel an open offer you posted, or one on a listing you own. Requires confirm: true.",
451
+ description: "Cancel an open offer you made, or one on a listing you own. Requires confirm: true.",
414
452
  mutation: "preview-confirm",
415
- http: { method: "DELETE", path: `${BASE}/schedules/{listingId}/offers/{offerId}` },
453
+ http: { method: "DELETE", path: `${BASE}/offers/{offerId}` },
416
454
  input: offerDecisionInput,
417
455
  cli: {
418
456
  command: ["offers", "cancel"],
419
- vnext: help(["offers", "cancel"], "Cancel one open offer.", "ornn offers cancel --listing-id <id> --offer-id <id>", "ornn offers cancel --listing-id <id> --offer-id <id> --yes --format json", ["offers list"], 45, ["listing ID", "offer ID"]),
457
+ vnext: help(["offers", "cancel"], "Cancel one open offer.", "ornn offers cancel --offer-id <id>", "ornn offers cancel --offer-id <id> --yes --format json", ["offers list"], 45, ["offer ID"]),
420
458
  },
421
459
  slack: false,
422
460
  execute: async (ctx, input) => {
423
461
  const parsed = offerDecisionInput.parse(input);
424
- const gate = await consent(ctx, "offers.cancel", "Cancel schedule offer", [
425
- { label: "Listing", value: parsed.listingId },
426
- { label: "Offer", value: parsed.offerId },
427
- ], { listingId: parsed.listingId, offerId: parsed.offerId });
462
+ const gate = await consent(ctx, "offers.cancel", "Cancel offer", [{ label: "Offer", value: parsed.offerId }], { offerId: parsed.offerId });
428
463
  if (gate)
429
464
  return gate;
430
- return ctx.fetch(`${BASE}/schedules/${seg(parsed.listingId)}/offers/${seg(parsed.offerId)}`, {
431
- method: "DELETE",
432
- });
465
+ return ctx.fetch(`${BASE}/offers/${seg(parsed.offerId)}`, { method: "DELETE" });
433
466
  },
434
467
  });
435
468
  export const marketCapabilities = [
436
- spotBooksCapability,
437
- spotBookCapability,
438
- spotTradesCapability,
439
- spotBidCapability,
440
- spotOrdersCapability,
441
- spotCancelCapability,
442
- schedulesListCapability,
443
- scheduleShowCapability,
444
- schedulePurchaseCapability,
469
+ marketBooksCapability,
470
+ marketBookCapability,
471
+ marketTradesCapability,
472
+ marketQuoteCapability,
473
+ marketBidCapability,
474
+ marketOrdersCapability,
475
+ marketCancelCapability,
476
+ marketFillsCapability,
477
+ marketEndCapability,
445
478
  offersListCapability,
446
479
  offerPostCapability,
447
480
  offerAcceptCapability,