@ornncompute/cli 1.0.48 → 1.0.63
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 +65 -47
- package/package.json +2 -2
- package/src/admin-capacity-reservations.mjs +36 -54
- package/src/admin-command-handlers.mjs +2 -0
- package/src/admin-credits.mjs +57 -0
- package/src/admin-enrollment.mjs +1 -1
- package/src/admin-node-diagnostics.mjs +1 -1
- package/src/admin-node-lifecycle.mjs +1 -1
- package/src/admin-shell.mjs +13 -3
- package/src/catalog-shell.mjs +4 -7
- package/vendor/capabilities/capabilities/admin-capacity-reservations.js +32 -15
- package/vendor/capabilities/capabilities/admin-enrollment.js +5 -5
- package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +8 -8
- package/vendor/capabilities/capabilities/admin.js +3 -3
- package/vendor/capabilities/capabilities/commerce-market.js +122 -22
- package/vendor/capabilities/capabilities/credits.js +78 -0
- package/vendor/capabilities/capabilities/fleet.js +14 -13
- package/vendor/capabilities/capabilities/listings.js +1 -1
- package/vendor/capabilities/capabilities/market-shared.js +2 -0
- package/vendor/capabilities/capabilities/markets.js +255 -222
- package/vendor/capabilities/capabilities/native-listing.js +1 -1
- package/vendor/capabilities/capabilities/reservation-list.js +68 -0
- package/vendor/capabilities/capabilities/staff-shared.js +64 -0
- package/vendor/capabilities/capabilities/tokens.js +1 -1
- package/vendor/capabilities/cli-catalog.js +4 -0
- package/vendor/capabilities/cli-migrations.js +12 -8
|
@@ -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.
|
|
6
|
-
// per GPU slug
|
|
7
|
-
//
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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}/
|
|
32
|
-
input:
|
|
58
|
+
http: { method: "GET", path: `${BASE}/books` },
|
|
59
|
+
input: marketBooksInput,
|
|
33
60
|
cli: {
|
|
34
|
-
command: ["
|
|
35
|
-
vnext: help(["
|
|
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) =>
|
|
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
|
|
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
|
|
45
|
-
id: "
|
|
78
|
+
export const marketBookCapability = defineCapability({
|
|
79
|
+
id: "market.book",
|
|
46
80
|
domain: "markets",
|
|
47
81
|
roles: ["user"],
|
|
48
|
-
description: "Show one
|
|
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}/
|
|
51
|
-
input:
|
|
84
|
+
http: { method: "GET", path: `${BASE}/books/{gpuSlug}` },
|
|
85
|
+
input: marketBookInput,
|
|
52
86
|
cli: {
|
|
53
|
-
command: ["
|
|
54
|
-
vnext: help(["
|
|
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 =
|
|
59
|
-
return ctx.fetch(`${BASE}/
|
|
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
|
|
63
|
-
id: "
|
|
98
|
+
export const marketTradesCapability = defineCapability({
|
|
99
|
+
id: "market.trades",
|
|
64
100
|
domain: "markets",
|
|
65
101
|
roles: ["user"],
|
|
66
|
-
description: "List recent
|
|
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}/
|
|
69
|
-
input:
|
|
104
|
+
http: { method: "GET", path: `${BASE}/books/{gpuSlug}/trades` },
|
|
105
|
+
input: marketBookInput,
|
|
70
106
|
cli: {
|
|
71
|
-
command: ["
|
|
72
|
-
vnext: help(["
|
|
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 =
|
|
77
|
-
return ctx.fetch(`${BASE}/
|
|
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
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
.
|
|
87
|
-
.min(
|
|
88
|
-
.
|
|
89
|
-
.describe("
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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("
|
|
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
|
|
105
|
-
id: "
|
|
170
|
+
export const marketBidCapability = defineCapability({
|
|
171
|
+
id: "market.bid",
|
|
106
172
|
domain: "markets",
|
|
107
173
|
roles: ["user"],
|
|
108
|
-
description: "
|
|
109
|
-
"
|
|
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}/
|
|
112
|
-
input:
|
|
178
|
+
http: { method: "POST", path: `${BASE}/orders` },
|
|
179
|
+
input: marketBidInput,
|
|
113
180
|
cli: {
|
|
114
|
-
command: ["
|
|
115
|
-
vnext: help(["
|
|
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 =
|
|
186
|
+
const parsed = marketBidInput.parse(input);
|
|
120
187
|
const body = {
|
|
121
188
|
gpuSlug: parsed.gpuSlug,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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, "
|
|
199
|
+
const gate = await consent(ctx, "market.bid", "Place bid", [
|
|
133
200
|
{ label: "GPU type", value: gpuDisplay(hardware?.terms ?? [], parsed.gpuSlug) },
|
|
134
|
-
{ label: "
|
|
135
|
-
{ label: "
|
|
136
|
-
{ label: "
|
|
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}/
|
|
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
|
|
148
|
-
export const
|
|
149
|
-
id: "
|
|
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
|
|
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}/
|
|
155
|
-
input:
|
|
222
|
+
http: { method: "GET", path: `${BASE}/orders` },
|
|
223
|
+
input: marketOrdersInput,
|
|
156
224
|
cli: {
|
|
157
|
-
command: ["
|
|
158
|
-
vnext: help(["
|
|
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}/
|
|
229
|
+
execute: async (ctx) => ctx.fetch(`${BASE}/orders`, { method: "GET" }),
|
|
162
230
|
});
|
|
163
|
-
export const
|
|
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
|
|
172
|
-
id: "
|
|
236
|
+
export const marketCancelCapability = defineCapability({
|
|
237
|
+
id: "market.cancel",
|
|
173
238
|
domain: "markets",
|
|
174
239
|
roles: ["user"],
|
|
175
|
-
description: "Cancel a resting
|
|
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}/
|
|
178
|
-
input:
|
|
242
|
+
http: { method: "DELETE", path: `${BASE}/orders/{orderId}` },
|
|
243
|
+
input: marketCancelInput,
|
|
179
244
|
cli: {
|
|
180
|
-
command: ["
|
|
181
|
-
vnext: help(["
|
|
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 =
|
|
186
|
-
const gate = await consent(ctx, "
|
|
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: "
|
|
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}/
|
|
260
|
+
return ctx.fetch(`${BASE}/orders/${seg(parsed.orderId)}`, { method: "DELETE" });
|
|
196
261
|
},
|
|
197
262
|
});
|
|
198
|
-
export const
|
|
263
|
+
export const marketFillsInput = z.object({
|
|
199
264
|
$positionals: positionals,
|
|
200
|
-
|
|
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
|
|
206
|
-
id: "
|
|
267
|
+
export const marketFillsCapability = defineCapability({
|
|
268
|
+
id: "market.fills",
|
|
207
269
|
domain: "markets",
|
|
208
270
|
roles: ["user"],
|
|
209
|
-
description: "List
|
|
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}/
|
|
212
|
-
input:
|
|
273
|
+
http: { method: "GET", path: `${BASE}/fills` },
|
|
274
|
+
input: marketFillsInput,
|
|
213
275
|
cli: {
|
|
214
|
-
command: ["
|
|
215
|
-
vnext: help(["
|
|
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 =
|
|
220
|
-
return ctx.fetch(`${BASE}/
|
|
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
|
|
285
|
+
export const marketEndInput = z.object({
|
|
229
286
|
$positionals: positionals,
|
|
230
|
-
|
|
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
|
|
266
|
-
id: "
|
|
290
|
+
export const marketEndCapability = defineCapability({
|
|
291
|
+
id: "market.end",
|
|
267
292
|
domain: "markets",
|
|
268
293
|
roles: ["user"],
|
|
269
|
-
description: "
|
|
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:
|
|
272
|
-
input:
|
|
296
|
+
http: { method: "POST", path: "/commerce/reservations/{fillId}/stop" },
|
|
297
|
+
input: marketEndInput,
|
|
273
298
|
cli: {
|
|
274
|
-
command: ["
|
|
275
|
-
vnext: help(["
|
|
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 =
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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(
|
|
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({
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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("
|
|
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
|
|
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}/
|
|
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", ["
|
|
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
|
|
389
|
+
const gate = await consent(ctx, "offers.post", "Post offer", [
|
|
359
390
|
{ label: "Listing", value: parsed.listingId },
|
|
360
|
-
{ label: "
|
|
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
|
-
],
|
|
402
|
+
], body);
|
|
366
403
|
if (gate)
|
|
367
404
|
return gate;
|
|
368
|
-
return ctx.fetch(`${BASE}/
|
|
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
|
|
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}/
|
|
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 --
|
|
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
|
|
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
|
|
436
|
+
value: "The buyer's reservation is booked; this listing's overlapping open offers are cancelled.",
|
|
402
437
|
},
|
|
403
|
-
], {
|
|
438
|
+
], { offerId: parsed.offerId });
|
|
404
439
|
if (gate)
|
|
405
440
|
return gate;
|
|
406
|
-
return ctx.fetch(`${BASE}/
|
|
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
|
|
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}/
|
|
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 --
|
|
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
|
|
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}/
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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,
|