@fastino-ai/pioneer-cli 0.2.9 → 0.2.10
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 +21 -12
- package/package.json +1 -1
- package/src/api.ts +108 -7
- package/src/index.tsx +1625 -711
package/src/index.tsx
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import React, { useState, useEffect, useRef } from "react";
|
|
8
|
-
import { render, Box, Text, useApp, useInput, useStdin, Static } from "ink";
|
|
8
|
+
import { render, Box, Text, useApp, useInput, useStdin, useStdout, Static } from "ink";
|
|
9
9
|
import Spinner from "ink-spinner";
|
|
10
10
|
import TextInput from "ink-text-input";
|
|
11
11
|
import * as fs from "fs";
|
|
@@ -167,6 +167,8 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
167
167
|
"format-results",
|
|
168
168
|
"reasoning-trace",
|
|
169
169
|
"use-meta-felix",
|
|
170
|
+
"json",
|
|
171
|
+
"all",
|
|
170
172
|
]);
|
|
171
173
|
|
|
172
174
|
function parseArgs(argv: string[]): {
|
|
@@ -2091,7 +2093,7 @@ function ModelCreateInteractive({
|
|
|
2091
2093
|
) : (
|
|
2092
2094
|
<Box flexDirection="column">
|
|
2093
2095
|
{topMatches.map((model, index) => {
|
|
2094
|
-
const suffix = [model.label, model.task_type
|
|
2096
|
+
const suffix = [model.label, model.task_type].filter(Boolean).join(" · ");
|
|
2095
2097
|
const modelLine = `${model.id}${suffix ? ` (${suffix})` : ""}`;
|
|
2096
2098
|
const isHighlighted = index === highlightIndex;
|
|
2097
2099
|
return (
|
|
@@ -2111,6 +2113,1210 @@ function ModelCreateInteractive({
|
|
|
2111
2113
|
);
|
|
2112
2114
|
}
|
|
2113
2115
|
|
|
2116
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2117
|
+
// Job ID Resolver
|
|
2118
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2119
|
+
|
|
2120
|
+
function WithResolvedJobId({
|
|
2121
|
+
rawId,
|
|
2122
|
+
render,
|
|
2123
|
+
}: {
|
|
2124
|
+
rawId: string;
|
|
2125
|
+
render: (jobId: string) => React.ReactElement;
|
|
2126
|
+
}) {
|
|
2127
|
+
const initiallyResolved = isUuid(rawId);
|
|
2128
|
+
const [state, setState] = useState<"resolving" | "resolved" | "error">(
|
|
2129
|
+
initiallyResolved ? "resolved" : "resolving"
|
|
2130
|
+
);
|
|
2131
|
+
const [resolvedId, setResolvedId] = useState(initiallyResolved ? rawId : "");
|
|
2132
|
+
const [error, setError] = useState("");
|
|
2133
|
+
|
|
2134
|
+
useEffect(() => {
|
|
2135
|
+
if (initiallyResolved) {
|
|
2136
|
+
return;
|
|
2137
|
+
}
|
|
2138
|
+
let isActive = true;
|
|
2139
|
+
(async () => {
|
|
2140
|
+
const result = await api.listJobs();
|
|
2141
|
+
if (!isActive) return;
|
|
2142
|
+
if (!result.ok) {
|
|
2143
|
+
setError(result.error ?? "Failed to look up training jobs.");
|
|
2144
|
+
setState("error");
|
|
2145
|
+
return;
|
|
2146
|
+
}
|
|
2147
|
+
const jobs = result.data?.training_jobs ?? [];
|
|
2148
|
+
const norm = rawId.toLowerCase();
|
|
2149
|
+
const matches = jobs.filter((job) =>
|
|
2150
|
+
typeof job.id === "string" && job.id.toLowerCase().startsWith(norm)
|
|
2151
|
+
);
|
|
2152
|
+
if (matches.length === 0) {
|
|
2153
|
+
setError(
|
|
2154
|
+
`No training job matches "${rawId}". Run \`pioneer job list\` to see available IDs.`
|
|
2155
|
+
);
|
|
2156
|
+
setState("error");
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
if (matches.length > 1) {
|
|
2160
|
+
const preview = matches
|
|
2161
|
+
.slice(0, 5)
|
|
2162
|
+
.map((j) => `${j.id} (${j.model_name ?? "—"})`)
|
|
2163
|
+
.join(", ");
|
|
2164
|
+
setError(
|
|
2165
|
+
`Ambiguous job id "${rawId}". Matches: ${preview}${matches.length > 5 ? "…" : ""}. Use a longer prefix or the full UUID.`
|
|
2166
|
+
);
|
|
2167
|
+
setState("error");
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
setResolvedId(matches[0]!.id!);
|
|
2171
|
+
setState("resolved");
|
|
2172
|
+
})();
|
|
2173
|
+
|
|
2174
|
+
return () => {
|
|
2175
|
+
isActive = false;
|
|
2176
|
+
};
|
|
2177
|
+
}, [rawId, initiallyResolved]);
|
|
2178
|
+
|
|
2179
|
+
if (state === "resolving") {
|
|
2180
|
+
return <Loading message={`Resolving job id ${rawId}...`} />;
|
|
2181
|
+
}
|
|
2182
|
+
if (state === "error") {
|
|
2183
|
+
return <ErrorMessage error={error} />;
|
|
2184
|
+
}
|
|
2185
|
+
return render(resolvedId);
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2189
|
+
// Job List Command (tabular output)
|
|
2190
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2191
|
+
|
|
2192
|
+
function shortJobId(id?: string): string {
|
|
2193
|
+
if (!id) return "—";
|
|
2194
|
+
return id.length > 8 ? id.slice(0, 8) : id;
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
function shortBaseModel(baseModel?: string): string {
|
|
2198
|
+
if (!baseModel) return "—";
|
|
2199
|
+
const parts = baseModel.split("/");
|
|
2200
|
+
return parts[parts.length - 1] || baseModel;
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
function relativeTime(iso?: string): string {
|
|
2204
|
+
if (!iso) return "—";
|
|
2205
|
+
const ts = Date.parse(iso);
|
|
2206
|
+
if (Number.isNaN(ts)) return iso;
|
|
2207
|
+
const diffMs = Date.now() - ts;
|
|
2208
|
+
const seconds = Math.round(diffMs / 1000);
|
|
2209
|
+
if (seconds < 60) return `${Math.max(seconds, 0)}s ago`;
|
|
2210
|
+
const minutes = Math.round(seconds / 60);
|
|
2211
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
2212
|
+
const hours = Math.round(minutes / 60);
|
|
2213
|
+
if (hours < 24) return `${hours}h ago`;
|
|
2214
|
+
const days = Math.round(hours / 24);
|
|
2215
|
+
if (days < 30) return `${days}d ago`;
|
|
2216
|
+
const months = Math.round(days / 30);
|
|
2217
|
+
if (months < 12) return `${months}mo ago`;
|
|
2218
|
+
const years = Math.round(days / 365);
|
|
2219
|
+
return `${years}y ago`;
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
function statusColor(status?: string): string | undefined {
|
|
2223
|
+
const normalized = (status ?? "").toLowerCase();
|
|
2224
|
+
if (normalized === "complete" || normalized === "completed" || normalized === "succeeded") {
|
|
2225
|
+
return "green";
|
|
2226
|
+
}
|
|
2227
|
+
if (normalized === "failed" || normalized === "error" || normalized === "cancelled" || normalized === "canceled") {
|
|
2228
|
+
return "red";
|
|
2229
|
+
}
|
|
2230
|
+
if (normalized === "running" || normalized === "training" || normalized === "in_progress") {
|
|
2231
|
+
return "cyan";
|
|
2232
|
+
}
|
|
2233
|
+
if (normalized === "pending" || normalized === "queued" || normalized === "starting") {
|
|
2234
|
+
return "yellow";
|
|
2235
|
+
}
|
|
2236
|
+
return undefined;
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
function padCell(value: string, width: number): string {
|
|
2240
|
+
if (value.length === width) return value;
|
|
2241
|
+
if (value.length > width) {
|
|
2242
|
+
if (width <= 1) return value.slice(0, width);
|
|
2243
|
+
return `${value.slice(0, Math.max(width - 1, 1))}…`;
|
|
2244
|
+
}
|
|
2245
|
+
return value + " ".repeat(width - value.length);
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
interface JobRow {
|
|
2249
|
+
id: string;
|
|
2250
|
+
model: string;
|
|
2251
|
+
baseModel: string;
|
|
2252
|
+
task: string;
|
|
2253
|
+
status: string;
|
|
2254
|
+
created: string;
|
|
2255
|
+
[key: string]: string;
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
function jobsToRows(jobs: api.TrainingJob[]): JobRow[] {
|
|
2259
|
+
return jobs.map((job) => ({
|
|
2260
|
+
id: shortJobId(job.id),
|
|
2261
|
+
model: job.model_name?.trim() || "—",
|
|
2262
|
+
baseModel: shortBaseModel(job.base_model),
|
|
2263
|
+
task: job.task_type?.trim() || "—",
|
|
2264
|
+
status: job.status?.trim() || "—",
|
|
2265
|
+
created: relativeTime(job.created_at),
|
|
2266
|
+
}));
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2270
|
+
// Reusable Table + DetailView primitives
|
|
2271
|
+
// Used by: job list/get, dataset list/get, model base-models, model endpoints get,
|
|
2272
|
+
// model endpoints quality-metrics, model artifacts download.
|
|
2273
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2274
|
+
|
|
2275
|
+
interface TableColumn<TRow> {
|
|
2276
|
+
key: keyof TRow & string;
|
|
2277
|
+
header: string;
|
|
2278
|
+
minWidth: number;
|
|
2279
|
+
maxWidth: number;
|
|
2280
|
+
flexible?: boolean;
|
|
2281
|
+
color?: (row: TRow) => string | undefined;
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
function computeTableWidths<TRow extends Record<string, string>>(
|
|
2285
|
+
columns: TableColumn<TRow>[],
|
|
2286
|
+
rows: TRow[],
|
|
2287
|
+
terminalWidth: number
|
|
2288
|
+
): number[] {
|
|
2289
|
+
const desired = columns.map((column) => {
|
|
2290
|
+
const headerLength = column.header.length;
|
|
2291
|
+
const dataLength = rows.reduce(
|
|
2292
|
+
(max, row) => Math.max(max, ((row[column.key] as string | undefined) ?? "").length),
|
|
2293
|
+
0
|
|
2294
|
+
);
|
|
2295
|
+
const want = Math.max(headerLength, dataLength);
|
|
2296
|
+
return Math.max(column.minWidth, Math.min(column.maxWidth, want));
|
|
2297
|
+
});
|
|
2298
|
+
|
|
2299
|
+
const separators = (columns.length - 1) * 2;
|
|
2300
|
+
const available = Math.max(40, terminalWidth - 1);
|
|
2301
|
+
const total = desired.reduce((sum, w) => sum + w, 0) + separators;
|
|
2302
|
+
|
|
2303
|
+
if (total <= available) return desired;
|
|
2304
|
+
|
|
2305
|
+
let overflow = total - available;
|
|
2306
|
+
const flexIndices = columns
|
|
2307
|
+
.map((column, idx) => (column.flexible ? idx : -1))
|
|
2308
|
+
.filter((idx) => idx >= 0);
|
|
2309
|
+
|
|
2310
|
+
while (overflow > 0) {
|
|
2311
|
+
let trimmedThisPass = false;
|
|
2312
|
+
for (const idx of flexIndices) {
|
|
2313
|
+
if (overflow <= 0) break;
|
|
2314
|
+
if (desired[idx]! > columns[idx]!.minWidth) {
|
|
2315
|
+
desired[idx] = desired[idx]! - 1;
|
|
2316
|
+
overflow -= 1;
|
|
2317
|
+
trimmedThisPass = true;
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
if (!trimmedThisPass) break;
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
return desired;
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
function useTerminalWidth(): number {
|
|
2327
|
+
const { stdout } = useStdout();
|
|
2328
|
+
const [width, setWidth] = useState(() => {
|
|
2329
|
+
const cols = stdout?.columns;
|
|
2330
|
+
if (typeof cols === "number" && cols > 0) return cols;
|
|
2331
|
+
const env = parseInt(process.env.COLUMNS ?? "", 10);
|
|
2332
|
+
return Number.isFinite(env) && env > 0 ? env : 80;
|
|
2333
|
+
});
|
|
2334
|
+
useEffect(() => {
|
|
2335
|
+
if (!stdout) return;
|
|
2336
|
+
const handler = () => {
|
|
2337
|
+
if (typeof stdout.columns === "number" && stdout.columns > 0) {
|
|
2338
|
+
setWidth(stdout.columns);
|
|
2339
|
+
}
|
|
2340
|
+
};
|
|
2341
|
+
stdout.on("resize", handler);
|
|
2342
|
+
return () => {
|
|
2343
|
+
stdout.off("resize", handler);
|
|
2344
|
+
};
|
|
2345
|
+
}, [stdout]);
|
|
2346
|
+
return width;
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
function DataTable<TRow extends Record<string, string>>({
|
|
2350
|
+
columns,
|
|
2351
|
+
rows,
|
|
2352
|
+
footer,
|
|
2353
|
+
}: {
|
|
2354
|
+
columns: TableColumn<TRow>[];
|
|
2355
|
+
rows: TRow[];
|
|
2356
|
+
footer?: string;
|
|
2357
|
+
}) {
|
|
2358
|
+
const terminalWidth = useTerminalWidth();
|
|
2359
|
+
const widths = computeTableWidths(columns, rows, terminalWidth);
|
|
2360
|
+
|
|
2361
|
+
const headerCells = columns.map((column, idx) =>
|
|
2362
|
+
padCell(column.header, widths[idx]!)
|
|
2363
|
+
);
|
|
2364
|
+
|
|
2365
|
+
return (
|
|
2366
|
+
<Box flexDirection="column">
|
|
2367
|
+
<Text bold dimColor>{headerCells.join(" ")}</Text>
|
|
2368
|
+
{rows.map((row, rowIdx) => {
|
|
2369
|
+
const cells = columns.map((column, idx) => {
|
|
2370
|
+
const raw = (row[column.key] as string | undefined) ?? "";
|
|
2371
|
+
return padCell(String(raw), widths[idx]!);
|
|
2372
|
+
});
|
|
2373
|
+
return (
|
|
2374
|
+
<Text key={rowIdx}>
|
|
2375
|
+
{columns.map((column, idx) => {
|
|
2376
|
+
const cell = cells[idx]!;
|
|
2377
|
+
const color = column.color?.(row);
|
|
2378
|
+
return (
|
|
2379
|
+
<React.Fragment key={String(column.key)}>
|
|
2380
|
+
{idx > 0 ? " " : ""}
|
|
2381
|
+
{color ? <Text color={color}>{cell}</Text> : cell}
|
|
2382
|
+
</React.Fragment>
|
|
2383
|
+
);
|
|
2384
|
+
})}
|
|
2385
|
+
</Text>
|
|
2386
|
+
);
|
|
2387
|
+
})}
|
|
2388
|
+
{footer ? (
|
|
2389
|
+
<Box marginTop={1}>
|
|
2390
|
+
<Text dimColor>{footer}</Text>
|
|
2391
|
+
</Box>
|
|
2392
|
+
) : null}
|
|
2393
|
+
</Box>
|
|
2394
|
+
);
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
interface DetailRow {
|
|
2398
|
+
label: string;
|
|
2399
|
+
value: string;
|
|
2400
|
+
valueColor?: string;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
function DetailView({
|
|
2404
|
+
rows,
|
|
2405
|
+
footer,
|
|
2406
|
+
}: {
|
|
2407
|
+
rows: DetailRow[];
|
|
2408
|
+
footer?: string;
|
|
2409
|
+
}) {
|
|
2410
|
+
if (rows.length === 0) {
|
|
2411
|
+
return <Text dimColor>No data.</Text>;
|
|
2412
|
+
}
|
|
2413
|
+
// Render single-record details through the same DataTable primitive used for
|
|
2414
|
+
// list views, so `<thing> get` and `<thing> list` share the exact same
|
|
2415
|
+
// visual style (uppercase headers, column padding, ellipsizing).
|
|
2416
|
+
const tableRows = rows.map((row) => ({
|
|
2417
|
+
field: row.label.toUpperCase(),
|
|
2418
|
+
value: row.value,
|
|
2419
|
+
__valueColor: row.valueColor ?? "",
|
|
2420
|
+
}));
|
|
2421
|
+
const columns: TableColumn<(typeof tableRows)[number]>[] = [
|
|
2422
|
+
{ key: "field", header: "FIELD", minWidth: 6, maxWidth: 24 },
|
|
2423
|
+
{
|
|
2424
|
+
key: "value",
|
|
2425
|
+
header: "VALUE",
|
|
2426
|
+
minWidth: 10,
|
|
2427
|
+
maxWidth: 80,
|
|
2428
|
+
flexible: true,
|
|
2429
|
+
color: (row) => row.__valueColor || undefined,
|
|
2430
|
+
},
|
|
2431
|
+
];
|
|
2432
|
+
return <DataTable columns={columns} rows={tableRows} footer={footer} />;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
const JOB_COLUMNS: TableColumn<JobRow>[] = [
|
|
2436
|
+
{ key: "id", header: "ID", minWidth: 8, maxWidth: 8 },
|
|
2437
|
+
{ key: "model", header: "MODEL", minWidth: 8, maxWidth: 32, flexible: true },
|
|
2438
|
+
{ key: "baseModel", header: "BASE MODEL", minWidth: 10, maxWidth: 30, flexible: true },
|
|
2439
|
+
{ key: "task", header: "TASK", minWidth: 4, maxWidth: 14, flexible: true },
|
|
2440
|
+
{ key: "status", header: "STATUS", minWidth: 6, maxWidth: 10, color: (row) => statusColor(row.status) },
|
|
2441
|
+
{ key: "created", header: "CREATED", minWidth: 7, maxWidth: 10 },
|
|
2442
|
+
];
|
|
2443
|
+
|
|
2444
|
+
export function JobListCommand() {
|
|
2445
|
+
const { exit } = useApp();
|
|
2446
|
+
const [state, setState] = useState<"loading" | "done" | "error">("loading");
|
|
2447
|
+
const [jobs, setJobs] = useState<api.TrainingJob[]>([]);
|
|
2448
|
+
const [error, setError] = useState("");
|
|
2449
|
+
|
|
2450
|
+
useEffect(() => {
|
|
2451
|
+
(async () => {
|
|
2452
|
+
const result = await api.listJobs();
|
|
2453
|
+
if (!result.ok) {
|
|
2454
|
+
setError(result.error ?? "Failed to load training jobs.");
|
|
2455
|
+
setState("error");
|
|
2456
|
+
} else {
|
|
2457
|
+
setJobs(result.data?.training_jobs ?? []);
|
|
2458
|
+
setState("done");
|
|
2459
|
+
}
|
|
2460
|
+
setTimeout(() => exit(), 500);
|
|
2461
|
+
})();
|
|
2462
|
+
}, [exit]);
|
|
2463
|
+
|
|
2464
|
+
if (state === "loading") return <Loading message="Loading training jobs..." />;
|
|
2465
|
+
if (state === "error") return <ErrorMessage error={error} />;
|
|
2466
|
+
if (jobs.length === 0) {
|
|
2467
|
+
return (
|
|
2468
|
+
<Box flexDirection="column">
|
|
2469
|
+
<Text>No training jobs found.</Text>
|
|
2470
|
+
<Text dimColor>Create one with `pioneer agent` — it will guide you through picking a base model and datasets.</Text>
|
|
2471
|
+
</Box>
|
|
2472
|
+
);
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
return (
|
|
2476
|
+
<DataTable
|
|
2477
|
+
columns={JOB_COLUMNS}
|
|
2478
|
+
rows={jobsToRows(jobs)}
|
|
2479
|
+
footer={`${jobs.length} job${jobs.length === 1 ? "" : "s"} · the short ID prefix above is enough for \`pioneer job get/logs/delete\` · use \`--json\` for raw JSON`}
|
|
2480
|
+
/>
|
|
2481
|
+
);
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2485
|
+
// Job Get Command (single-job key/value layout)
|
|
2486
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2487
|
+
|
|
2488
|
+
function formatTimestamp(iso?: string): string {
|
|
2489
|
+
if (!iso) return "—";
|
|
2490
|
+
const ts = Date.parse(iso);
|
|
2491
|
+
if (Number.isNaN(ts)) return iso;
|
|
2492
|
+
const date = new Date(ts);
|
|
2493
|
+
const pad = (n: number) => n.toString().padStart(2, "0");
|
|
2494
|
+
const yyyy = date.getUTCFullYear();
|
|
2495
|
+
const mm = pad(date.getUTCMonth() + 1);
|
|
2496
|
+
const dd = pad(date.getUTCDate());
|
|
2497
|
+
const hh = pad(date.getUTCHours());
|
|
2498
|
+
const mi = pad(date.getUTCMinutes());
|
|
2499
|
+
const ss = pad(date.getUTCSeconds());
|
|
2500
|
+
return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss} UTC (${relativeTime(iso)})`;
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
function formatDuration(startIso?: string, endIso?: string): string {
|
|
2504
|
+
if (!startIso || !endIso) return "—";
|
|
2505
|
+
const start = Date.parse(startIso);
|
|
2506
|
+
const end = Date.parse(endIso);
|
|
2507
|
+
if (Number.isNaN(start) || Number.isNaN(end) || end < start) return "—";
|
|
2508
|
+
const seconds = Math.floor((end - start) / 1000);
|
|
2509
|
+
const days = Math.floor(seconds / 86400);
|
|
2510
|
+
const hours = Math.floor((seconds % 86400) / 3600);
|
|
2511
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
2512
|
+
const remSeconds = seconds % 60;
|
|
2513
|
+
const parts: string[] = [];
|
|
2514
|
+
if (days) parts.push(`${days}d`);
|
|
2515
|
+
if (hours) parts.push(`${hours}h`);
|
|
2516
|
+
if (minutes) parts.push(`${minutes}m`);
|
|
2517
|
+
if (!parts.length || remSeconds) parts.push(`${remSeconds}s`);
|
|
2518
|
+
return parts.join(" ");
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
function formatDatasetList(
|
|
2522
|
+
datasets?: Array<{ name?: string; version?: string | number }>
|
|
2523
|
+
): string {
|
|
2524
|
+
if (!datasets || datasets.length === 0) return "—";
|
|
2525
|
+
return datasets
|
|
2526
|
+
.map((ds) => {
|
|
2527
|
+
const name = ds?.name ?? "(unnamed)";
|
|
2528
|
+
const version = ds?.version != null && ds.version !== "" ? ` (v${ds.version})` : "";
|
|
2529
|
+
return `${name}${version}`;
|
|
2530
|
+
})
|
|
2531
|
+
.join(", ");
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
function formatLabels(labels?: string[]): string {
|
|
2535
|
+
if (!labels || labels.length === 0) return "—";
|
|
2536
|
+
return labels.join(", ");
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
function formatBoolean(value: unknown): string {
|
|
2540
|
+
if (value === true) return "yes";
|
|
2541
|
+
if (value === false) return "no";
|
|
2542
|
+
return "—";
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
function formatNumber(value: unknown): string {
|
|
2546
|
+
if (typeof value !== "number" || Number.isNaN(value)) return "—";
|
|
2547
|
+
return Number.isInteger(value) ? String(value) : value.toString();
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
function formatPercent(value: unknown): string {
|
|
2551
|
+
if (typeof value !== "number" || Number.isNaN(value)) return "—";
|
|
2552
|
+
const pct = value > 1 ? value : value * 100;
|
|
2553
|
+
return `${Number.isInteger(pct) ? pct : pct.toFixed(1)}%`;
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
function formatNonEmpty(value: unknown): string {
|
|
2557
|
+
if (typeof value === "string") {
|
|
2558
|
+
const trimmed = value.trim();
|
|
2559
|
+
return trimmed.length ? trimmed : "—";
|
|
2560
|
+
}
|
|
2561
|
+
return "—";
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
function jobToDetailRows(job: api.TrainingJob): DetailRow[] {
|
|
2565
|
+
const record = job as unknown as Record<string, unknown>;
|
|
2566
|
+
const provider =
|
|
2567
|
+
typeof record.provider_name === "string" ? record.provider_name : undefined;
|
|
2568
|
+
const progressPercent = typeof record.progress_percent === "number" ? record.progress_percent : undefined;
|
|
2569
|
+
const currentEpoch = typeof record.current_epoch === "number" ? record.current_epoch : undefined;
|
|
2570
|
+
|
|
2571
|
+
const rows: DetailRow[] = [
|
|
2572
|
+
{ label: "ID", value: formatNonEmpty(job.id) },
|
|
2573
|
+
{ label: "Model", value: formatNonEmpty(job.model_name) },
|
|
2574
|
+
{ label: "Base model", value: formatNonEmpty(job.base_model) },
|
|
2575
|
+
{ label: "Task", value: formatNonEmpty(job.task_type) },
|
|
2576
|
+
{ label: "Status", value: formatNonEmpty(job.status), valueColor: statusColor(job.status) },
|
|
2577
|
+
{ label: "Provider", value: formatNonEmpty(provider) },
|
|
2578
|
+
{ label: "Datasets", value: formatDatasetList(job.datasets) },
|
|
2579
|
+
{ label: "Labels", value: formatLabels(job.labels) },
|
|
2580
|
+
{ label: "Epochs", value: formatNumber(job.nr_epochs) },
|
|
2581
|
+
{ label: "Batch size", value: formatNumber(job.batch_size) },
|
|
2582
|
+
{ label: "Learning rate", value: formatNumber(job.learning_rate) },
|
|
2583
|
+
{ label: "Validation split", value: formatPercent(job.validation_data_percentage) },
|
|
2584
|
+
{ label: "Auto-selected", value: formatBoolean(job.model_auto_selected) },
|
|
2585
|
+
{ label: "Version", value: formatNonEmpty(job.version_number) },
|
|
2586
|
+
{ label: "Created", value: formatTimestamp(job.created_at) },
|
|
2587
|
+
{ label: "Started", value: formatTimestamp(job.started_at) },
|
|
2588
|
+
{ label: "Completed", value: formatTimestamp(job.completed_at) },
|
|
2589
|
+
{ label: "Duration", value: formatDuration(job.started_at, job.completed_at) },
|
|
2590
|
+
{ label: "Updated", value: formatTimestamp(job.updated_at) },
|
|
2591
|
+
];
|
|
2592
|
+
|
|
2593
|
+
if (progressPercent !== undefined) {
|
|
2594
|
+
rows.splice(5, 0, {
|
|
2595
|
+
label: "Progress",
|
|
2596
|
+
value: `${progressPercent.toFixed(1)}%${currentEpoch != null ? ` (epoch ${currentEpoch})` : ""}`,
|
|
2597
|
+
});
|
|
2598
|
+
}
|
|
2599
|
+
if (job.error_message) {
|
|
2600
|
+
rows.push({
|
|
2601
|
+
label: "Error",
|
|
2602
|
+
value: job.error_message,
|
|
2603
|
+
valueColor: "red",
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
return rows;
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
function JobGetCommand({ jobId }: { jobId: string }) {
|
|
2611
|
+
const { exit } = useApp();
|
|
2612
|
+
const [state, setState] = useState<"loading" | "done" | "error">("loading");
|
|
2613
|
+
const [job, setJob] = useState<api.TrainingJob | null>(null);
|
|
2614
|
+
const [error, setError] = useState("");
|
|
2615
|
+
|
|
2616
|
+
useEffect(() => {
|
|
2617
|
+
let isActive = true;
|
|
2618
|
+
(async () => {
|
|
2619
|
+
const result = await api.getJob(jobId);
|
|
2620
|
+
if (!isActive) return;
|
|
2621
|
+
if (!result.ok) {
|
|
2622
|
+
setError(result.error ?? "Failed to load training job.");
|
|
2623
|
+
setState("error");
|
|
2624
|
+
} else {
|
|
2625
|
+
setJob((result.data as api.TrainingJob | undefined) ?? null);
|
|
2626
|
+
setState("done");
|
|
2627
|
+
}
|
|
2628
|
+
setTimeout(() => exit(), 500);
|
|
2629
|
+
})();
|
|
2630
|
+
return () => {
|
|
2631
|
+
isActive = false;
|
|
2632
|
+
};
|
|
2633
|
+
}, [jobId, exit]);
|
|
2634
|
+
|
|
2635
|
+
if (state === "loading") {
|
|
2636
|
+
return <Loading message={`Loading training job ${jobId}...`} />;
|
|
2637
|
+
}
|
|
2638
|
+
if (state === "error") {
|
|
2639
|
+
return <ErrorMessage error={error} />;
|
|
2640
|
+
}
|
|
2641
|
+
if (!job) {
|
|
2642
|
+
return <ErrorMessage error="Training job response was empty." />;
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
return (
|
|
2646
|
+
<DetailView
|
|
2647
|
+
rows={jobToDetailRows(job)}
|
|
2648
|
+
footer={`Use \`pioneer job get ${jobId} --json\` for the raw JSON payload.`}
|
|
2649
|
+
/>
|
|
2650
|
+
);
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2654
|
+
// Base Models List Command (tabular)
|
|
2655
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2656
|
+
|
|
2657
|
+
interface BaseModelRow {
|
|
2658
|
+
id: string;
|
|
2659
|
+
name: string;
|
|
2660
|
+
type: string;
|
|
2661
|
+
context: string;
|
|
2662
|
+
inference: string;
|
|
2663
|
+
training: string;
|
|
2664
|
+
[key: string]: string;
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
const BASE_MODEL_COLUMNS: TableColumn<BaseModelRow>[] = [
|
|
2668
|
+
{ key: "id", header: "ID", minWidth: 12, maxWidth: 36, flexible: true },
|
|
2669
|
+
{ key: "name", header: "NAME", minWidth: 8, maxWidth: 30, flexible: true },
|
|
2670
|
+
{ key: "type", header: "TYPE", minWidth: 4, maxWidth: 8 },
|
|
2671
|
+
{ key: "context", header: "CONTEXT", minWidth: 7, maxWidth: 8 },
|
|
2672
|
+
{ key: "inference", header: "INFERENCE", minWidth: 9, maxWidth: 9, color: (row) => (row.inference === "yes" ? "green" : "red") },
|
|
2673
|
+
{ key: "training", header: "TRAINING", minWidth: 8, maxWidth: 8, color: (row) => (row.training === "yes" ? "green" : "red") },
|
|
2674
|
+
];
|
|
2675
|
+
|
|
2676
|
+
function formatContextWindow(n?: number): string {
|
|
2677
|
+
if (typeof n !== "number" || !Number.isFinite(n) || n <= 0) return "—";
|
|
2678
|
+
if (n >= 1_000_000) {
|
|
2679
|
+
const v = n / 1_000_000;
|
|
2680
|
+
return `${Number.isInteger(v) ? v : v.toFixed(1)}M`;
|
|
2681
|
+
}
|
|
2682
|
+
if (n >= 1000) {
|
|
2683
|
+
const v = n / 1000;
|
|
2684
|
+
return `${Number.isInteger(v) ? v : v.toFixed(0)}K`;
|
|
2685
|
+
}
|
|
2686
|
+
return String(n);
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
function BaseModelsListCommand() {
|
|
2690
|
+
const { exit } = useApp();
|
|
2691
|
+
const [state, setState] = useState<"loading" | "done" | "error">("loading");
|
|
2692
|
+
const [models, setModels] = useState<api.BaseModelInfo[]>([]);
|
|
2693
|
+
const [error, setError] = useState("");
|
|
2694
|
+
|
|
2695
|
+
useEffect(() => {
|
|
2696
|
+
(async () => {
|
|
2697
|
+
const result = await api.listBaseModels();
|
|
2698
|
+
if (!result.ok) {
|
|
2699
|
+
setError(result.error ?? "Failed to load base models.");
|
|
2700
|
+
setState("error");
|
|
2701
|
+
} else {
|
|
2702
|
+
setModels(normalizeBaseModels(result.data));
|
|
2703
|
+
setState("done");
|
|
2704
|
+
}
|
|
2705
|
+
setTimeout(() => exit(), 500);
|
|
2706
|
+
})();
|
|
2707
|
+
}, [exit]);
|
|
2708
|
+
|
|
2709
|
+
if (state === "loading") return <Loading message="Loading base models..." />;
|
|
2710
|
+
if (state === "error") return <ErrorMessage error={error} />;
|
|
2711
|
+
if (models.length === 0) {
|
|
2712
|
+
return <Text>No base models available.</Text>;
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
const rows: BaseModelRow[] = models.map((m) => {
|
|
2716
|
+
const inference = m.supports_inference ?? m.supports_on_demand_inference;
|
|
2717
|
+
return {
|
|
2718
|
+
id: m.id || "—",
|
|
2719
|
+
name: (m.label || m.name || m.id || "—").trim() || "—",
|
|
2720
|
+
type: m.task_type || "—",
|
|
2721
|
+
context: formatContextWindow(m.context_window),
|
|
2722
|
+
inference: inference === undefined ? "—" : inference ? "yes" : "no",
|
|
2723
|
+
training: m.supports_training === undefined ? "—" : m.supports_training ? "yes" : "no",
|
|
2724
|
+
};
|
|
2725
|
+
});
|
|
2726
|
+
|
|
2727
|
+
return (
|
|
2728
|
+
<DataTable
|
|
2729
|
+
columns={BASE_MODEL_COLUMNS}
|
|
2730
|
+
rows={rows}
|
|
2731
|
+
footer={`${models.length} base model${models.length === 1 ? "" : "s"} · use \`--json\` for raw JSON`}
|
|
2732
|
+
/>
|
|
2733
|
+
);
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2737
|
+
// Model Endpoint Get Command (single-resource key/value layout, with dataset count)
|
|
2738
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2739
|
+
|
|
2740
|
+
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2741
|
+
|
|
2742
|
+
function ModelEndpointGetCommand({ modelId }: { modelId: string }) {
|
|
2743
|
+
const { exit } = useApp();
|
|
2744
|
+
const [state, setState] = useState<"loading" | "done" | "error">("loading");
|
|
2745
|
+
const [project, setProject] = useState<api.ProjectResponse | null>(null);
|
|
2746
|
+
const [datasetCount, setDatasetCount] = useState<api.ProjectDatasetCountResponse | null>(null);
|
|
2747
|
+
const [activeJob, setActiveJob] = useState<api.TrainingJob | null>(null);
|
|
2748
|
+
const [error, setError] = useState("");
|
|
2749
|
+
|
|
2750
|
+
useEffect(() => {
|
|
2751
|
+
let active = true;
|
|
2752
|
+
(async () => {
|
|
2753
|
+
// Sequential to dodge Bun concurrent-fetch keep-alive flakiness.
|
|
2754
|
+
const projectResult = await api.getProject(modelId);
|
|
2755
|
+
if (!active) return;
|
|
2756
|
+
if (!projectResult.ok) {
|
|
2757
|
+
setError(projectResult.error ?? "Failed to load model endpoint.");
|
|
2758
|
+
setState("error");
|
|
2759
|
+
setTimeout(() => exit(), 500);
|
|
2760
|
+
return;
|
|
2761
|
+
}
|
|
2762
|
+
setProject(projectResult.data ?? null);
|
|
2763
|
+
|
|
2764
|
+
const countResult = await api.getProjectDatasetCount(modelId);
|
|
2765
|
+
if (!active) return;
|
|
2766
|
+
if (countResult.ok) setDatasetCount(countResult.data ?? null);
|
|
2767
|
+
|
|
2768
|
+
// If the selected model id is a UUID, it's a training job (a deployed
|
|
2769
|
+
// fine-tuned checkpoint). Hydrate it so we can show a friendly name
|
|
2770
|
+
// instead of just the UUID.
|
|
2771
|
+
const selected = projectResult.data?.selected_model_id ?? "";
|
|
2772
|
+
if (UUID_REGEX.test(selected)) {
|
|
2773
|
+
const jobResult = await api.getJob(selected);
|
|
2774
|
+
if (!active) return;
|
|
2775
|
+
if (jobResult.ok) setActiveJob(jobResult.data ?? null);
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
setState("done");
|
|
2779
|
+
setTimeout(() => exit(), 500);
|
|
2780
|
+
})();
|
|
2781
|
+
return () => {
|
|
2782
|
+
active = false;
|
|
2783
|
+
};
|
|
2784
|
+
}, [modelId, exit]);
|
|
2785
|
+
|
|
2786
|
+
if (state === "loading") return <Loading message={`Loading model endpoint ${modelId}...`} />;
|
|
2787
|
+
if (state === "error") return <ErrorMessage error={error} />;
|
|
2788
|
+
if (!project) return <ErrorMessage error="Model endpoint response was empty." />;
|
|
2789
|
+
|
|
2790
|
+
const selected = project.selected_model_id ?? "";
|
|
2791
|
+
const selectedDisplay = (() => {
|
|
2792
|
+
if (!selected) return "—";
|
|
2793
|
+
if (activeJob) {
|
|
2794
|
+
const parts: string[] = [];
|
|
2795
|
+
if (activeJob.model_name) parts.push(activeJob.model_name);
|
|
2796
|
+
if (activeJob.base_model) parts.push(`base: ${activeJob.base_model}`);
|
|
2797
|
+
const tail = parts.length ? ` (${parts.join(", ")})` : "";
|
|
2798
|
+
return `${shortJobId(selected)}${tail}`;
|
|
2799
|
+
}
|
|
2800
|
+
// Stock model id (or UUID we couldn't resolve) — render as-is.
|
|
2801
|
+
return UUID_REGEX.test(selected) ? shortJobId(selected) : selected;
|
|
2802
|
+
})();
|
|
2803
|
+
|
|
2804
|
+
const rows: DetailRow[] = [
|
|
2805
|
+
{ label: "ID", value: formatNonEmpty(project.id) },
|
|
2806
|
+
{ label: "Name", value: formatNonEmpty(project.name) },
|
|
2807
|
+
{ label: "Description", value: formatNonEmpty(project.description) },
|
|
2808
|
+
{ label: "Icon", value: formatNonEmpty(project.icon) },
|
|
2809
|
+
{ label: "Repo", value: formatNonEmpty(project.repo) },
|
|
2810
|
+
{ label: "Selected model", value: selectedDisplay },
|
|
2811
|
+
{ label: "Created", value: formatTimestamp(project.created_at) },
|
|
2812
|
+
{ label: "Updated", value: formatTimestamp(project.updated_at) },
|
|
2813
|
+
];
|
|
2814
|
+
if (datasetCount) {
|
|
2815
|
+
rows.push(
|
|
2816
|
+
{ label: "Datasets", value: formatNumber(datasetCount.dataset_count) },
|
|
2817
|
+
{
|
|
2818
|
+
label: "Can delete",
|
|
2819
|
+
value: formatBoolean(datasetCount.can_delete),
|
|
2820
|
+
valueColor: datasetCount.can_delete ? "green" : "yellow",
|
|
2821
|
+
}
|
|
2822
|
+
);
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
return (
|
|
2826
|
+
<DetailView
|
|
2827
|
+
rows={rows}
|
|
2828
|
+
footer={`Use \`pioneer model endpoints get ${modelId} --json\` for the raw JSON payload.`}
|
|
2829
|
+
/>
|
|
2830
|
+
);
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2834
|
+
// Model Endpoint Quality Metrics Command (compact key/value layout)
|
|
2835
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2836
|
+
|
|
2837
|
+
function ModelQualityMetricsCommand({ modelId }: { modelId: string }) {
|
|
2838
|
+
const { exit } = useApp();
|
|
2839
|
+
const [state, setState] = useState<"loading" | "done" | "error">("loading");
|
|
2840
|
+
const [data, setData] = useState<api.QualityMetricsResponse | null>(null);
|
|
2841
|
+
const [error, setError] = useState("");
|
|
2842
|
+
|
|
2843
|
+
useEffect(() => {
|
|
2844
|
+
let active = true;
|
|
2845
|
+
(async () => {
|
|
2846
|
+
const result = await api.getProjectQualityMetrics(modelId);
|
|
2847
|
+
if (!active) return;
|
|
2848
|
+
if (!result.ok) {
|
|
2849
|
+
setError(result.error ?? "Failed to load quality metrics.");
|
|
2850
|
+
setState("error");
|
|
2851
|
+
} else {
|
|
2852
|
+
setData(result.data ?? null);
|
|
2853
|
+
setState("done");
|
|
2854
|
+
}
|
|
2855
|
+
setTimeout(() => exit(), 500);
|
|
2856
|
+
})();
|
|
2857
|
+
return () => {
|
|
2858
|
+
active = false;
|
|
2859
|
+
};
|
|
2860
|
+
}, [modelId, exit]);
|
|
2861
|
+
|
|
2862
|
+
if (state === "loading") return <Loading message={`Loading quality metrics for ${modelId}...`} />;
|
|
2863
|
+
if (state === "error") return <ErrorMessage error={error} />;
|
|
2864
|
+
if (!data) return <ErrorMessage error="Quality metrics response was empty." />;
|
|
2865
|
+
|
|
2866
|
+
const rows: DetailRow[] = [
|
|
2867
|
+
{ label: "Model ID", value: formatNonEmpty(data.project_id) },
|
|
2868
|
+
{ label: "Total judged", value: formatNumber(data.total_judged) },
|
|
2869
|
+
{ label: "Pass count", value: formatNumber(data.pass_count), valueColor: "green" },
|
|
2870
|
+
{ label: "Fail count", value: formatNumber(data.fail_count), valueColor: data.fail_count > 0 ? "red" : undefined },
|
|
2871
|
+
{ label: "Uncertain count", value: formatNumber(data.uncertain_count), valueColor: data.uncertain_count > 0 ? "yellow" : undefined },
|
|
2872
|
+
{ label: "Pass rate", value: formatPercent(data.pass_rate), valueColor: "green" },
|
|
2873
|
+
{ label: "Fail rate", value: formatPercent(data.fail_rate), valueColor: data.fail_rate > 0 ? "red" : undefined },
|
|
2874
|
+
];
|
|
2875
|
+
|
|
2876
|
+
return (
|
|
2877
|
+
<DetailView
|
|
2878
|
+
rows={rows}
|
|
2879
|
+
footer={`Use \`pioneer model endpoints quality-metrics ${modelId} --json\` for the raw JSON payload.`}
|
|
2880
|
+
/>
|
|
2881
|
+
);
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2885
|
+
// Deploy Job Picker (interactive picker for `model endpoints deploy` with no --job)
|
|
2886
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2887
|
+
|
|
2888
|
+
function isJobDeployable(status?: string): boolean {
|
|
2889
|
+
const s = (status ?? "").trim().toLowerCase();
|
|
2890
|
+
return s === "complete" || s === "completed" || s === "succeeded" || s === "deployed";
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
/**
|
|
2894
|
+
* Normalize a model identifier to a "family" key for matching.
|
|
2895
|
+
* Strips the org/namespace prefix, lowercases, and drops common
|
|
2896
|
+
* variant suffixes (-instruct, -chat, -base, -it, -hf) so that, e.g.,
|
|
2897
|
+
* "qwen/Qwen3-8B-Instruct" → "qwen3-8b"
|
|
2898
|
+
* "Qwen/Qwen3-8B" → "qwen3-8b"
|
|
2899
|
+
* "meta-llama/Llama-3.1-8B-Instruct" → "llama-3.1-8b"
|
|
2900
|
+
* are treated as the same family.
|
|
2901
|
+
*/
|
|
2902
|
+
function modelFamilyKey(modelId?: string | null): string {
|
|
2903
|
+
if (!modelId) return "";
|
|
2904
|
+
const last = modelId.toLowerCase().split("/").pop() ?? "";
|
|
2905
|
+
return last
|
|
2906
|
+
.replace(/-instruct$/, "")
|
|
2907
|
+
.replace(/-chat$/, "")
|
|
2908
|
+
.replace(/-base$/, "")
|
|
2909
|
+
.replace(/-it$/, "")
|
|
2910
|
+
.replace(/-hf$/, "")
|
|
2911
|
+
.trim();
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2914
|
+
function jobMatchesEndpoint(job: api.TrainingJob, selectedModelId?: string): boolean {
|
|
2915
|
+
if (!selectedModelId) return true;
|
|
2916
|
+
const want = modelFamilyKey(selectedModelId);
|
|
2917
|
+
const have = modelFamilyKey(job.base_model);
|
|
2918
|
+
if (!want || !have) return true;
|
|
2919
|
+
return want === have || want.startsWith(have) || have.startsWith(want);
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
export function DeployJobPickerCommand({
|
|
2923
|
+
modelId,
|
|
2924
|
+
reason,
|
|
2925
|
+
showAll,
|
|
2926
|
+
}: {
|
|
2927
|
+
modelId: string;
|
|
2928
|
+
reason?: string;
|
|
2929
|
+
showAll?: boolean;
|
|
2930
|
+
}) {
|
|
2931
|
+
const { exit } = useApp();
|
|
2932
|
+
const { isRawModeSupported } = useStdin();
|
|
2933
|
+
const [phase, setPhase] = useState<"loading" | "picking" | "deploying" | "done" | "error">("loading");
|
|
2934
|
+
const [jobs, setJobs] = useState<api.TrainingJob[]>([]);
|
|
2935
|
+
const [endpointModel, setEndpointModel] = useState<string | undefined>(undefined);
|
|
2936
|
+
const [filterApplied, setFilterApplied] = useState<boolean>(false);
|
|
2937
|
+
const [filterSource, setFilterSource] = useState<"project" | "family" | "none">("none");
|
|
2938
|
+
const [error, setError] = useState("");
|
|
2939
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
2940
|
+
const [highlightIndex, setHighlightIndex] = useState(0);
|
|
2941
|
+
const [selectedJobId, setSelectedJobId] = useState<string | null>(null);
|
|
2942
|
+
const [deployResult, setDeployResult] = useState<unknown>(null);
|
|
2943
|
+
|
|
2944
|
+
useEffect(() => {
|
|
2945
|
+
let active = true;
|
|
2946
|
+
(async () => {
|
|
2947
|
+
// Run sequentially to avoid Bun concurrent-fetch keep-alive flakiness
|
|
2948
|
+
// ("socket connection was closed unexpectedly"). Retry once on transient
|
|
2949
|
+
// network errors.
|
|
2950
|
+
const isTransient = (err?: string) =>
|
|
2951
|
+
!!err && /socket|ECONNRESET|fetch failed|network|EAI_AGAIN|timeout/i.test(err);
|
|
2952
|
+
|
|
2953
|
+
const fetchWithRetry = async <T,>(fn: () => Promise<api.ApiResult<T>>): Promise<api.ApiResult<T>> => {
|
|
2954
|
+
let r = await fn();
|
|
2955
|
+
if (!r.ok && (r.status === 0 || isTransient(r.error))) {
|
|
2956
|
+
await new Promise((res) => setTimeout(res, 250));
|
|
2957
|
+
r = await fn();
|
|
2958
|
+
}
|
|
2959
|
+
return r;
|
|
2960
|
+
};
|
|
2961
|
+
|
|
2962
|
+
const sortByRecent = (list: api.TrainingJob[]) =>
|
|
2963
|
+
list.slice().sort((a, b) => {
|
|
2964
|
+
const at = Date.parse(a.completed_at ?? a.updated_at ?? a.created_at ?? "");
|
|
2965
|
+
const bt = Date.parse(b.completed_at ?? b.updated_at ?? b.created_at ?? "");
|
|
2966
|
+
if (Number.isNaN(at) || Number.isNaN(bt)) return 0;
|
|
2967
|
+
return bt - at;
|
|
2968
|
+
});
|
|
2969
|
+
|
|
2970
|
+
// 1. Resolve the endpoint. A hard 404/403 means the endpoint doesn't exist
|
|
2971
|
+
// on this backend — fail fast rather than open a picker for a target
|
|
2972
|
+
// that any subsequent deploy call will reject. Other errors (5xx,
|
|
2973
|
+
// transient network) degrade gracefully: we just can't apply the
|
|
2974
|
+
// family filter.
|
|
2975
|
+
const projectResult = await fetchWithRetry(() => api.getProject(modelId));
|
|
2976
|
+
if (!active) return;
|
|
2977
|
+
if (!projectResult.ok && (projectResult.status === 404 || projectResult.status === 403)) {
|
|
2978
|
+
const reason =
|
|
2979
|
+
projectResult.status === 403
|
|
2980
|
+
? "You do not have access to this endpoint."
|
|
2981
|
+
: "This endpoint does not exist on the current backend.";
|
|
2982
|
+
setError(
|
|
2983
|
+
`${reason} (model id: ${modelId})\nList valid endpoints with: pioneer model endpoints list`
|
|
2984
|
+
);
|
|
2985
|
+
setPhase("error");
|
|
2986
|
+
setTimeout(() => exit(), 500);
|
|
2987
|
+
return;
|
|
2988
|
+
}
|
|
2989
|
+
const selected = projectResult.ok ? projectResult.data?.selected_model_id : undefined;
|
|
2990
|
+
setEndpointModel(selected ?? undefined);
|
|
2991
|
+
|
|
2992
|
+
// 2. Try server-side filter by project_id. Today most jobs have project_id=null
|
|
2993
|
+
// so this often returns []; we fall back to all-jobs + family match below.
|
|
2994
|
+
let list: api.TrainingJob[] = [];
|
|
2995
|
+
let didFilter = false;
|
|
2996
|
+
let filterSource: "project" | "family" | "none" = "none";
|
|
2997
|
+
|
|
2998
|
+
if (!showAll) {
|
|
2999
|
+
const scoped = await fetchWithRetry(() => api.listJobs({ project_id: modelId }));
|
|
3000
|
+
if (!active) return;
|
|
3001
|
+
if (scoped.ok) {
|
|
3002
|
+
const scopedDeployable = sortByRecent(
|
|
3003
|
+
(scoped.data?.training_jobs ?? []).filter((j) => isJobDeployable(j.status))
|
|
3004
|
+
);
|
|
3005
|
+
if (scopedDeployable.length > 0) {
|
|
3006
|
+
list = scopedDeployable;
|
|
3007
|
+
didFilter = true;
|
|
3008
|
+
filterSource = "project";
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
// 3. Fall back to all jobs (and optionally family-match against the endpoint).
|
|
3014
|
+
if (list.length === 0) {
|
|
3015
|
+
const allResult = await fetchWithRetry(() => api.listJobs());
|
|
3016
|
+
if (!active) return;
|
|
3017
|
+
if (!allResult.ok) {
|
|
3018
|
+
setError(allResult.error ?? "Failed to load training jobs.");
|
|
3019
|
+
setPhase("error");
|
|
3020
|
+
setTimeout(() => exit(), 500);
|
|
3021
|
+
return;
|
|
3022
|
+
}
|
|
3023
|
+
const allDeployable = sortByRecent(
|
|
3024
|
+
(allResult.data?.training_jobs ?? []).filter((j) => isJobDeployable(j.status))
|
|
3025
|
+
);
|
|
3026
|
+
|
|
3027
|
+
if (!showAll && selected) {
|
|
3028
|
+
const familyMatched = allDeployable.filter((j) => jobMatchesEndpoint(j, selected));
|
|
3029
|
+
if (familyMatched.length > 0) {
|
|
3030
|
+
list = familyMatched;
|
|
3031
|
+
didFilter = true;
|
|
3032
|
+
filterSource = "family";
|
|
3033
|
+
} else {
|
|
3034
|
+
list = allDeployable;
|
|
3035
|
+
}
|
|
3036
|
+
} else {
|
|
3037
|
+
list = allDeployable;
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
setFilterApplied(didFilter);
|
|
3042
|
+
setFilterSource(filterSource);
|
|
3043
|
+
setJobs(list);
|
|
3044
|
+
setPhase("picking");
|
|
3045
|
+
})();
|
|
3046
|
+
return () => {
|
|
3047
|
+
active = false;
|
|
3048
|
+
};
|
|
3049
|
+
}, [exit, modelId, showAll]);
|
|
3050
|
+
|
|
3051
|
+
const matching = jobs.filter((job) => {
|
|
3052
|
+
const q = searchQuery.trim().toLowerCase();
|
|
3053
|
+
if (!q) return true;
|
|
3054
|
+
return (
|
|
3055
|
+
(job.id ?? "").toLowerCase().includes(q) ||
|
|
3056
|
+
(job.model_name ?? "").toLowerCase().includes(q) ||
|
|
3057
|
+
(job.base_model ?? "").toLowerCase().includes(q)
|
|
3058
|
+
);
|
|
3059
|
+
});
|
|
3060
|
+
|
|
3061
|
+
const deploy = async (jobId: string) => {
|
|
3062
|
+
setSelectedJobId(jobId);
|
|
3063
|
+
setPhase("deploying");
|
|
3064
|
+
|
|
3065
|
+
// The deploy endpoint requires `training_jobs.project_id == project_id`
|
|
3066
|
+
// (server-side WHERE clause). Most jobs are created with project_id=null,
|
|
3067
|
+
// so we PATCH first to link the job to the target project. The PATCH is
|
|
3068
|
+
// a no-op if the job is already linked.
|
|
3069
|
+
const job = jobs.find((j) => j.id === jobId);
|
|
3070
|
+
if (!job || job.project_id !== modelId) {
|
|
3071
|
+
const patch = await api.updateTrainingJob(jobId, { project_id: modelId });
|
|
3072
|
+
if (!patch.ok) {
|
|
3073
|
+
const detail =
|
|
3074
|
+
patch.status === 403
|
|
3075
|
+
? "You do not have permission to assign this job to this project."
|
|
3076
|
+
: patch.error ?? "Failed to link job to project.";
|
|
3077
|
+
setError(`Could not assign job to project before deploy: ${detail}`);
|
|
3078
|
+
setPhase("error");
|
|
3079
|
+
setTimeout(() => exit(), 1500);
|
|
3080
|
+
return;
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
const result = await api.deployTrainingJobToProject(modelId, {
|
|
3085
|
+
training_job_id: jobId,
|
|
3086
|
+
...(reason ? { reason } : {}),
|
|
3087
|
+
});
|
|
3088
|
+
if (!result.ok) {
|
|
3089
|
+
let msg = result.error ?? "Deployment failed.";
|
|
3090
|
+
if (result.status === 404) {
|
|
3091
|
+
msg =
|
|
3092
|
+
`Backend rejected the deploy with 404 "${result.error ?? "Resource not found."}".\n` +
|
|
3093
|
+
` • Project ${modelId} and training job ${jobId} both exist and are linked.\n` +
|
|
3094
|
+
` • Inspect job state: pioneer job get ${jobId}`;
|
|
3095
|
+
}
|
|
3096
|
+
setError(msg);
|
|
3097
|
+
setPhase("error");
|
|
3098
|
+
} else {
|
|
3099
|
+
// CLI-side enrichment: the deploy record's `base_model` is null for
|
|
3100
|
+
// training-job deploys (mutually-exclusive with `training_job_id` in
|
|
3101
|
+
// the current schema), but users want to see what base the fine-tune
|
|
3102
|
+
// is built on. Populate it from the picked job until the backend
|
|
3103
|
+
// change lands.
|
|
3104
|
+
const enriched =
|
|
3105
|
+
result.data && typeof result.data === "object" && job?.base_model
|
|
3106
|
+
? { ...result.data, base_model: (result.data as { base_model?: string | null }).base_model ?? job.base_model }
|
|
3107
|
+
: result.data;
|
|
3108
|
+
setDeployResult(enriched);
|
|
3109
|
+
setPhase("done");
|
|
3110
|
+
}
|
|
3111
|
+
setTimeout(() => exit(), 1500);
|
|
3112
|
+
};
|
|
3113
|
+
|
|
3114
|
+
useInput(
|
|
3115
|
+
(input, key) => {
|
|
3116
|
+
if (phase !== "picking" || !matching.length) {
|
|
3117
|
+
if (key.return && phase === "picking" && !matching.length) {
|
|
3118
|
+
setError("No matching jobs.");
|
|
3119
|
+
}
|
|
3120
|
+
return;
|
|
3121
|
+
}
|
|
3122
|
+
if (key.upArrow) {
|
|
3123
|
+
setHighlightIndex((idx) => (idx === 0 ? matching.length - 1 : idx - 1));
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
if (key.downArrow) {
|
|
3127
|
+
setHighlightIndex((idx) => (idx === matching.length - 1 ? 0 : idx + 1));
|
|
3128
|
+
return;
|
|
3129
|
+
}
|
|
3130
|
+
if (key.return) {
|
|
3131
|
+
const selected = matching[highlightIndex];
|
|
3132
|
+
if (selected?.id) {
|
|
3133
|
+
void deploy(selected.id);
|
|
3134
|
+
}
|
|
3135
|
+
return;
|
|
3136
|
+
}
|
|
3137
|
+
if (key.backspace || key.delete) {
|
|
3138
|
+
setSearchQuery((q) => q.slice(0, -1));
|
|
3139
|
+
setHighlightIndex(0);
|
|
3140
|
+
return;
|
|
3141
|
+
}
|
|
3142
|
+
if (input && !key.ctrl && !key.meta) {
|
|
3143
|
+
setSearchQuery((q) => q + input);
|
|
3144
|
+
setHighlightIndex(0);
|
|
3145
|
+
}
|
|
3146
|
+
},
|
|
3147
|
+
{ isActive: isRawModeSupported && phase === "picking" }
|
|
3148
|
+
);
|
|
3149
|
+
|
|
3150
|
+
if (!isRawModeSupported) {
|
|
3151
|
+
return (
|
|
3152
|
+
<Box flexDirection="column">
|
|
3153
|
+
<ErrorMessage error="Interactive job picker requires a TTY." />
|
|
3154
|
+
<Text>List trained jobs with `pioneer job list` and pass an explicit ID:</Text>
|
|
3155
|
+
<Text dimColor> pioneer model endpoints deploy {modelId} --job <training-job-id></Text>
|
|
3156
|
+
</Box>
|
|
3157
|
+
);
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
if (phase === "loading") return <Loading message="Loading deployable training jobs..." />;
|
|
3161
|
+
if (phase === "error") return <ErrorMessage error={error} />;
|
|
3162
|
+
|
|
3163
|
+
if (phase === "deploying") {
|
|
3164
|
+
return <Loading message={`Deploying job ${selectedJobId} to endpoint ${modelId}...`} />;
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3167
|
+
if (phase === "done") {
|
|
3168
|
+
return (
|
|
3169
|
+
<Box flexDirection="column">
|
|
3170
|
+
<Success message={`Deployment initiated for endpoint ${modelId} from job ${selectedJobId}`} />
|
|
3171
|
+
{deployResult ? (
|
|
3172
|
+
<Text dimColor>{JSON.stringify(deployResult, null, 2)}</Text>
|
|
3173
|
+
) : null}
|
|
3174
|
+
</Box>
|
|
3175
|
+
);
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
if (jobs.length === 0) {
|
|
3179
|
+
return (
|
|
3180
|
+
<Box flexDirection="column">
|
|
3181
|
+
<ErrorMessage error="No deployable training jobs found." />
|
|
3182
|
+
<Text dimColor>Only jobs with status `complete` / `succeeded` / `deployed` are eligible.</Text>
|
|
3183
|
+
<Text dimColor>Run `pioneer job list` to inspect job statuses, or start training with `pioneer agent`.</Text>
|
|
3184
|
+
</Box>
|
|
3185
|
+
);
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
const visible = matching.slice(0, 12);
|
|
3189
|
+
const offsetIndex = Math.max(0, Math.min(highlightIndex - visible.length + 1, matching.length - visible.length));
|
|
3190
|
+
const window = matching.slice(offsetIndex, offsetIndex + visible.length);
|
|
3191
|
+
|
|
3192
|
+
const navHint = "Type to filter · ↑/↓ navigate · Enter to deploy";
|
|
3193
|
+
const headerLine = (() => {
|
|
3194
|
+
const n = jobs.length;
|
|
3195
|
+
const noun = `job${n === 1 ? "" : "s"}`;
|
|
3196
|
+
if (showAll) {
|
|
3197
|
+
const tail = endpointModel ? ` Endpoint base model: "${endpointModel}".` : "";
|
|
3198
|
+
return `Showing all ${n} deployable ${noun} (--all).${tail} ${navHint}.`;
|
|
3199
|
+
}
|
|
3200
|
+
if (filterApplied && filterSource === "project") {
|
|
3201
|
+
return `Showing ${n} ${noun} scoped to this endpoint via project_id. ${navHint} · pass --all to see every job.`;
|
|
3202
|
+
}
|
|
3203
|
+
if (filterApplied && filterSource === "family" && endpointModel) {
|
|
3204
|
+
return `Showing ${n} ${noun} matching base model "${endpointModel}". ${navHint} · pass --all to disable family match.`;
|
|
3205
|
+
}
|
|
3206
|
+
if (endpointModel) {
|
|
3207
|
+
return `No jobs scoped to this endpoint and none match base model "${endpointModel}". Showing all ${n} deployable ${noun} as fallback. ${navHint}.`;
|
|
3208
|
+
}
|
|
3209
|
+
return `Showing ${n} deployable ${noun}. ${navHint}.`;
|
|
3210
|
+
})();
|
|
3211
|
+
|
|
3212
|
+
return (
|
|
3213
|
+
<Box flexDirection="column">
|
|
3214
|
+
<Text bold>Pick a training job to deploy to endpoint <Text color="cyan">{modelId}</Text>:</Text>
|
|
3215
|
+
<Text dimColor>{headerLine}</Text>
|
|
3216
|
+
<Text> </Text>
|
|
3217
|
+
{searchQuery ? (
|
|
3218
|
+
<Text>
|
|
3219
|
+
<Text dimColor>Filter: </Text>
|
|
3220
|
+
<Text>{searchQuery}</Text>
|
|
3221
|
+
</Text>
|
|
3222
|
+
) : null}
|
|
3223
|
+
{window.length === 0 ? (
|
|
3224
|
+
<Text color="yellow">No jobs match `{searchQuery}`.</Text>
|
|
3225
|
+
) : (
|
|
3226
|
+
window.map((job, idx) => {
|
|
3227
|
+
const realIndex = offsetIndex + idx;
|
|
3228
|
+
const isHighlighted = realIndex === highlightIndex;
|
|
3229
|
+
const idShort = shortJobId(job.id);
|
|
3230
|
+
const modelName = job.model_name?.trim() || "—";
|
|
3231
|
+
const baseModel = shortBaseModel(job.base_model);
|
|
3232
|
+
const status = job.status ?? "—";
|
|
3233
|
+
const completed = relativeTime(job.completed_at ?? job.updated_at ?? job.created_at);
|
|
3234
|
+
return (
|
|
3235
|
+
<Text key={job.id || realIndex} color={isHighlighted ? "cyan" : undefined} bold={isHighlighted}>
|
|
3236
|
+
{isHighlighted ? "▶ " : " "}
|
|
3237
|
+
{padCell(idShort, 8)} {padCell(modelName, 28)} {padCell(baseModel, 22)} <Text color={statusColor(status)}>{padCell(status, 10)}</Text> {completed}
|
|
3238
|
+
</Text>
|
|
3239
|
+
);
|
|
3240
|
+
})
|
|
3241
|
+
)}
|
|
3242
|
+
{error ? (
|
|
3243
|
+
<Box marginTop={1}>
|
|
3244
|
+
<Text color="red">{error}</Text>
|
|
3245
|
+
</Box>
|
|
3246
|
+
) : null}
|
|
3247
|
+
</Box>
|
|
3248
|
+
);
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3252
|
+
// Model Artifact Download Command (key/value layout for a download URL response)
|
|
3253
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3254
|
+
|
|
3255
|
+
function ModelArtifactDownloadCommand({ jobId }: { jobId: string }) {
|
|
3256
|
+
const { exit } = useApp();
|
|
3257
|
+
const [state, setState] = useState<"loading" | "done" | "error">("loading");
|
|
3258
|
+
const [data, setData] = useState<Record<string, unknown> | null>(null);
|
|
3259
|
+
const [error, setError] = useState("");
|
|
3260
|
+
|
|
3261
|
+
useEffect(() => {
|
|
3262
|
+
let active = true;
|
|
3263
|
+
(async () => {
|
|
3264
|
+
const result = await api.downloadModel(jobId);
|
|
3265
|
+
if (!active) return;
|
|
3266
|
+
if (!result.ok) {
|
|
3267
|
+
setError(result.error ?? "Failed to fetch model artifact download URL.");
|
|
3268
|
+
setState("error");
|
|
3269
|
+
} else {
|
|
3270
|
+
setData((result.data as Record<string, unknown> | undefined) ?? null);
|
|
3271
|
+
setState("done");
|
|
3272
|
+
}
|
|
3273
|
+
setTimeout(() => exit(), 500);
|
|
3274
|
+
})();
|
|
3275
|
+
return () => {
|
|
3276
|
+
active = false;
|
|
3277
|
+
};
|
|
3278
|
+
}, [jobId, exit]);
|
|
3279
|
+
|
|
3280
|
+
if (state === "loading") return <Loading message={`Fetching download URL for ${jobId}...`} />;
|
|
3281
|
+
if (state === "error") return <ErrorMessage error={error} />;
|
|
3282
|
+
if (!data) return <ErrorMessage error="Download response was empty." />;
|
|
3283
|
+
|
|
3284
|
+
const stringField = (key: string) => {
|
|
3285
|
+
const v = data[key];
|
|
3286
|
+
return typeof v === "string" && v.trim() ? v : undefined;
|
|
3287
|
+
};
|
|
3288
|
+
const numberField = (key: string) => {
|
|
3289
|
+
const v = data[key];
|
|
3290
|
+
return typeof v === "number" ? v : undefined;
|
|
3291
|
+
};
|
|
3292
|
+
|
|
3293
|
+
const downloadUrl = stringField("download_url") || stringField("url");
|
|
3294
|
+
const expiresAt = stringField("expires_at") || stringField("expiry");
|
|
3295
|
+
const sizeBytes = numberField("size") ?? numberField("size_bytes");
|
|
3296
|
+
const filename = stringField("filename") || stringField("file_name");
|
|
3297
|
+
const contentType = stringField("content_type") || stringField("mime_type");
|
|
3298
|
+
|
|
3299
|
+
const rows: DetailRow[] = [
|
|
3300
|
+
{ label: "Job ID", value: jobId },
|
|
3301
|
+
{ label: "Filename", value: formatNonEmpty(filename) },
|
|
3302
|
+
{ label: "Content type", value: formatNonEmpty(contentType) },
|
|
3303
|
+
{ label: "Size", value: sizeBytes !== undefined ? `${sizeBytes.toLocaleString()} bytes` : "—" },
|
|
3304
|
+
{ label: "Expires", value: expiresAt ? formatTimestamp(expiresAt) : "—" },
|
|
3305
|
+
{ label: "Download URL", value: downloadUrl || "—", valueColor: downloadUrl ? "cyan" : undefined },
|
|
3306
|
+
];
|
|
3307
|
+
|
|
3308
|
+
return (
|
|
3309
|
+
<DetailView
|
|
3310
|
+
rows={rows}
|
|
3311
|
+
footer={
|
|
3312
|
+
downloadUrl
|
|
3313
|
+
? `Tip: pipe the URL into curl, e.g. \`curl -L -o artifact.zip "$URL"\` · use \`--json\` for raw JSON`
|
|
3314
|
+
: `Use \`pioneer model artifacts download ${jobId} --json\` for the raw JSON payload.`
|
|
3315
|
+
}
|
|
3316
|
+
/>
|
|
3317
|
+
);
|
|
3318
|
+
}
|
|
3319
|
+
|
|
2114
3320
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2115
3321
|
// Job Logs Command (prettified output)
|
|
2116
3322
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -2305,469 +3511,202 @@ function GenerateCommand<T extends GenerateResult>({
|
|
|
2305
3511
|
{savedDatasetId && (
|
|
2306
3512
|
<Text color="green">Saved to remote: {savedDatasetId}</Text>
|
|
2307
3513
|
)}
|
|
2308
|
-
{remoteSaveFailed && (
|
|
2309
|
-
<Text color="yellow">⚠ Remote save failed (check server logs). Saved locally instead.</Text>
|
|
2310
|
-
)}
|
|
2311
|
-
{savedPath && (
|
|
2312
|
-
<Text color="cyan">Saved to: {savedPath}</Text>
|
|
2313
|
-
)}
|
|
2314
|
-
</Box>
|
|
2315
|
-
);
|
|
2316
|
-
}
|
|
2317
|
-
|
|
2318
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
2319
|
-
// Helper: Infer format from file extension
|
|
2320
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
2321
|
-
|
|
2322
|
-
function inferFormatFromPath(
|
|
2323
|
-
path: string | undefined,
|
|
2324
|
-
defaultFormat: string = "jsonl"
|
|
2325
|
-
): "csv" | "jsonl" | "parquet" {
|
|
2326
|
-
if (!path) return defaultFormat as "csv" | "jsonl" | "parquet";
|
|
2327
|
-
|
|
2328
|
-
const ext = path.toLowerCase().split(".").pop();
|
|
2329
|
-
if (ext === "csv" || ext === "jsonl" || ext === "parquet") {
|
|
2330
|
-
return ext;
|
|
2331
|
-
}
|
|
2332
|
-
return defaultFormat as "csv" | "jsonl" | "parquet";
|
|
2333
|
-
}
|
|
2334
|
-
|
|
2335
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
2336
|
-
// Dataset List Command (shows both remote and local)
|
|
2337
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
2338
|
-
|
|
2339
|
-
function DatasetListCommand() {
|
|
2340
|
-
const { exit } = useApp();
|
|
2341
|
-
const [state, setState] = useState<"loading" | "done">("loading");
|
|
2342
|
-
const [remoteData, setRemoteData] = useState<api.DatasetListResponse | null>(null);
|
|
2343
|
-
const [remoteError, setRemoteError] = useState<string | null>(null);
|
|
2344
|
-
const [localDatasets, setLocalDatasets] = useState<LocalDataset[]>([]);
|
|
2345
|
-
|
|
2346
|
-
useEffect(() => {
|
|
2347
|
-
(async () => {
|
|
2348
|
-
// Fetch remote and local datasets
|
|
2349
|
-
const result = await api.listDatasets();
|
|
2350
|
-
const local = listLocalDatasets();
|
|
2351
|
-
setLocalDatasets(local);
|
|
2352
|
-
|
|
2353
|
-
if (result.ok) {
|
|
2354
|
-
setRemoteData(result.data ?? null);
|
|
2355
|
-
} else {
|
|
2356
|
-
setRemoteError(result.error ?? "Unknown error");
|
|
2357
|
-
}
|
|
2358
|
-
setState("done");
|
|
2359
|
-
setTimeout(() => exit(), 500);
|
|
2360
|
-
})();
|
|
2361
|
-
}, [exit]);
|
|
2362
|
-
|
|
2363
|
-
if (state === "loading") {
|
|
2364
|
-
return <Loading />;
|
|
2365
|
-
}
|
|
2366
|
-
|
|
2367
|
-
const remoteDatasets = remoteData?.datasets ?? [];
|
|
2368
|
-
|
|
2369
|
-
return (
|
|
2370
|
-
<Box flexDirection="column">
|
|
2371
|
-
<Text bold color="cyan">Remote Datasets {remoteError ? "" : `(${remoteDatasets.length})`}</Text>
|
|
2372
|
-
{remoteError ? (
|
|
2373
|
-
<Box flexDirection="column">
|
|
2374
|
-
{remoteError.split("\n").map((line, idx) => (
|
|
2375
|
-
<Text key={idx} color="red"> {line}</Text>
|
|
2376
|
-
))}
|
|
2377
|
-
</Box>
|
|
2378
|
-
) : remoteDatasets.length === 0 ? (
|
|
2379
|
-
<Text dimColor> No remote datasets</Text>
|
|
2380
|
-
) : (
|
|
2381
|
-
remoteDatasets.map((ds) => (
|
|
2382
|
-
<Box key={ds.id} flexDirection="column">
|
|
2383
|
-
<Text>
|
|
2384
|
-
{" "}<Text color="yellow">{ds.dataset_name}:{ds.version_number || "v1"}</Text> <Text dimColor>({ds.dataset_type}, {ds.sample_size} examples)</Text>
|
|
2385
|
-
</Text>
|
|
2386
|
-
<Text dimColor> {ds.id}</Text>
|
|
2387
|
-
</Box>
|
|
2388
|
-
))
|
|
2389
|
-
)}
|
|
2390
|
-
<Text> </Text>
|
|
2391
|
-
<Text bold color="cyan">Local Datasets ({localDatasets.length})</Text>
|
|
2392
|
-
{localDatasets.length === 0 ? (
|
|
2393
|
-
<Text dimColor> No local datasets in ./datasets/</Text>
|
|
2394
|
-
) : (
|
|
2395
|
-
localDatasets.map((ds) => (
|
|
2396
|
-
<Text key={ds.id}>
|
|
2397
|
-
{" "}<Text color="green">{ds.name}</Text> <Text dimColor>({ds.type}, {ds.sample_size} examples)</Text>
|
|
2398
|
-
</Text>
|
|
2399
|
-
))
|
|
2400
|
-
)}
|
|
2401
|
-
</Box>
|
|
2402
|
-
);
|
|
2403
|
-
}
|
|
2404
|
-
|
|
2405
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
2406
|
-
// Job Create Command (with auto-upload for local datasets)
|
|
2407
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
2408
|
-
|
|
2409
|
-
interface ParsedDataset {
|
|
2410
|
-
type: "local" | "remote";
|
|
2411
|
-
name?: string; // for remote name:version format
|
|
2412
|
-
version?: string; // for remote name:version format
|
|
2413
|
-
id?: string; // for remote UUID format
|
|
2414
|
-
}
|
|
2415
|
-
|
|
2416
|
-
interface JobCreateCommandProps {
|
|
2417
|
-
modelName: string;
|
|
2418
|
-
datasets: ParsedDataset[];
|
|
2419
|
-
baseModel?: string;
|
|
2420
|
-
epochs?: number;
|
|
2421
|
-
}
|
|
2422
|
-
|
|
2423
|
-
interface DatasetUploadStatus {
|
|
2424
|
-
name: string;
|
|
2425
|
-
path: string;
|
|
2426
|
-
status: "pending" | "uploading" | "done" | "error";
|
|
2427
|
-
error?: string;
|
|
2428
|
-
uploadedRef?: api.DatasetRef;
|
|
2429
|
-
}
|
|
2430
|
-
|
|
2431
|
-
// UUID pattern: 8-4-4-4-12 hex characters
|
|
2432
|
-
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2433
|
-
|
|
2434
|
-
function isUUID(str: string): boolean {
|
|
2435
|
-
return UUID_PATTERN.test(str);
|
|
2436
|
-
}
|
|
2437
|
-
|
|
2438
|
-
function parseDatasetString(ds: string): ParsedDataset | { error: string } {
|
|
2439
|
-
const trimmed = ds.trim();
|
|
2440
|
-
|
|
2441
|
-
if (trimmed.startsWith("local:")) {
|
|
2442
|
-
const name = trimmed.slice(6); // remove "local:"
|
|
2443
|
-
if (!name) {
|
|
2444
|
-
return { error: `Invalid local dataset format: ${ds}. Use local:<name>` };
|
|
2445
|
-
}
|
|
2446
|
-
return { type: "local", name };
|
|
2447
|
-
}
|
|
2448
|
-
|
|
2449
|
-
if (trimmed.startsWith("remote:")) {
|
|
2450
|
-
const rest = trimmed.slice(7); // remove "remote:"
|
|
2451
|
-
|
|
2452
|
-
// Check if it's a UUID
|
|
2453
|
-
if (isUUID(rest)) {
|
|
2454
|
-
return { type: "remote", id: rest };
|
|
2455
|
-
}
|
|
2456
|
-
|
|
2457
|
-
// Otherwise parse as name:version
|
|
2458
|
-
const colonIndex = rest.lastIndexOf(":");
|
|
2459
|
-
if (colonIndex === -1) {
|
|
2460
|
-
// No version, default to latest
|
|
2461
|
-
return { type: "remote", name: rest, version: "latest" };
|
|
2462
|
-
}
|
|
2463
|
-
return {
|
|
2464
|
-
type: "remote",
|
|
2465
|
-
name: rest.slice(0, colonIndex),
|
|
2466
|
-
version: rest.slice(colonIndex + 1),
|
|
2467
|
-
};
|
|
2468
|
-
}
|
|
2469
|
-
|
|
2470
|
-
return { error: `Invalid dataset format: ${ds}. Use remote:<name>:<version>, remote:<uuid>, or local:<name>` };
|
|
2471
|
-
}
|
|
2472
|
-
|
|
2473
|
-
function resolveLocalDatasetPath(name: string): string | null {
|
|
2474
|
-
// Check if it's a local dataset by name (in ./datasets/ directory)
|
|
2475
|
-
const localPath = path.join(DATASETS_DIR, `${name}.json`);
|
|
2476
|
-
if (fs.existsSync(localPath)) {
|
|
2477
|
-
return localPath;
|
|
2478
|
-
}
|
|
2479
|
-
// Check if it's a direct file path
|
|
2480
|
-
if (fs.existsSync(name) && (name.endsWith(".json") || name.endsWith(".jsonl"))) {
|
|
2481
|
-
return name;
|
|
2482
|
-
}
|
|
2483
|
-
return null;
|
|
2484
|
-
}
|
|
2485
|
-
|
|
2486
|
-
function getLocalDatasetType(filePath: string): "ner" | "classification" | "custom" {
|
|
2487
|
-
try {
|
|
2488
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
2489
|
-
const parsed = JSON.parse(content);
|
|
2490
|
-
const data = Array.isArray(parsed) ? parsed : parsed.data ?? [];
|
|
2491
|
-
if (parsed.task_type) {
|
|
2492
|
-
return parsed.task_type as "ner" | "classification" | "custom";
|
|
2493
|
-
}
|
|
2494
|
-
const firstItem = data[0] as Record<string, unknown> | undefined;
|
|
2495
|
-
if (firstItem?.spans) return "ner";
|
|
2496
|
-
if (firstItem?.label) return "classification";
|
|
2497
|
-
return "custom";
|
|
2498
|
-
} catch {
|
|
2499
|
-
return "custom";
|
|
2500
|
-
}
|
|
2501
|
-
}
|
|
2502
|
-
|
|
2503
|
-
function convertJsonToJsonl(filePath: string): string {
|
|
2504
|
-
// Convert JSON array to JSONL format for upload
|
|
2505
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
2506
|
-
const parsed = JSON.parse(content);
|
|
2507
|
-
const data = Array.isArray(parsed) ? parsed : parsed.data ?? [];
|
|
2508
|
-
|
|
2509
|
-
// Create temp JSONL file
|
|
2510
|
-
const tempPath = filePath.replace(".json", ".jsonl");
|
|
2511
|
-
const jsonlContent = data.map((item: unknown) => JSON.stringify(item)).join("\n");
|
|
2512
|
-
fs.writeFileSync(tempPath, jsonlContent);
|
|
2513
|
-
return tempPath;
|
|
2514
|
-
}
|
|
2515
|
-
|
|
2516
|
-
function JobCreateCommand({ modelName, datasets, baseModel, epochs }: JobCreateCommandProps) {
|
|
2517
|
-
const { exit } = useApp();
|
|
2518
|
-
const [state, setState] = useState<"resolving" | "uploading" | "creating" | "done" | "error">("resolving");
|
|
2519
|
-
const [uploadStatuses, setUploadStatuses] = useState<DatasetUploadStatus[]>([]);
|
|
2520
|
-
const [jobResult, setJobResult] = useState<api.TrainingJob | null>(null);
|
|
2521
|
-
const [error, setError] = useState("");
|
|
2522
|
-
|
|
2523
|
-
useEffect(() => {
|
|
2524
|
-
(async () => {
|
|
2525
|
-
// Separate local and remote datasets
|
|
2526
|
-
const localDatasets = datasets.filter((d): d is ParsedDataset & { type: "local"; name: string } =>
|
|
2527
|
-
d.type === "local" && !!d.name
|
|
2528
|
-
);
|
|
2529
|
-
const remoteByNameVersion = datasets.filter((d): d is ParsedDataset & { type: "remote"; name: string } =>
|
|
2530
|
-
d.type === "remote" && !!d.name
|
|
2531
|
-
);
|
|
2532
|
-
const remoteByUUID = datasets.filter((d): d is ParsedDataset & { type: "remote"; id: string } =>
|
|
2533
|
-
d.type === "remote" && !!d.id
|
|
2534
|
-
);
|
|
2535
|
-
|
|
2536
|
-
// Resolve UUIDs to name:version
|
|
2537
|
-
const remoteDatasets: api.DatasetRef[] = remoteByNameVersion.map(d => ({
|
|
2538
|
-
name: d.name,
|
|
2539
|
-
version: d.version ?? "latest"
|
|
2540
|
-
}));
|
|
2541
|
-
|
|
2542
|
-
if (remoteByUUID.length > 0) {
|
|
2543
|
-
// Fetch dataset list to resolve UUIDs
|
|
2544
|
-
const listResult = await api.listDatasets();
|
|
2545
|
-
if (!listResult.ok || !listResult.data) {
|
|
2546
|
-
setError(`Failed to resolve dataset UUIDs: ${listResult.error ?? "Unknown error"}`);
|
|
2547
|
-
setState("error");
|
|
2548
|
-
setTimeout(() => exit(), 500);
|
|
2549
|
-
return;
|
|
2550
|
-
}
|
|
2551
|
-
|
|
2552
|
-
for (const uuidDataset of remoteByUUID) {
|
|
2553
|
-
const found = listResult.data.datasets.find(ds => ds.id === uuidDataset.id);
|
|
2554
|
-
if (!found) {
|
|
2555
|
-
setError(`Dataset not found with UUID: ${uuidDataset.id}`);
|
|
2556
|
-
setState("error");
|
|
2557
|
-
setTimeout(() => exit(), 500);
|
|
2558
|
-
return;
|
|
2559
|
-
}
|
|
2560
|
-
remoteDatasets.push({
|
|
2561
|
-
name: found.dataset_name,
|
|
2562
|
-
version: found.version_number ?? "latest",
|
|
2563
|
-
});
|
|
2564
|
-
}
|
|
2565
|
-
}
|
|
2566
|
-
|
|
2567
|
-
// Validate local datasets exist
|
|
2568
|
-
const statuses: DatasetUploadStatus[] = [];
|
|
2569
|
-
for (const local of localDatasets) {
|
|
2570
|
-
const localPath = resolveLocalDatasetPath(local.name);
|
|
2571
|
-
if (!localPath) {
|
|
2572
|
-
setError(`Local dataset not found: ${local.name}`);
|
|
2573
|
-
setState("error");
|
|
2574
|
-
setTimeout(() => exit(), 500);
|
|
2575
|
-
return;
|
|
2576
|
-
}
|
|
2577
|
-
statuses.push({ name: local.name, path: localPath, status: "pending" });
|
|
2578
|
-
}
|
|
2579
|
-
|
|
2580
|
-
if (statuses.length === 0) {
|
|
2581
|
-
// No local datasets, proceed directly to job creation
|
|
2582
|
-
setState("creating");
|
|
2583
|
-
const result = await api.createJob({
|
|
2584
|
-
model_name: modelName,
|
|
2585
|
-
datasets: remoteDatasets,
|
|
2586
|
-
base_model: baseModel,
|
|
2587
|
-
nr_epochs: epochs,
|
|
2588
|
-
});
|
|
2589
|
-
if (result.ok && result.data) {
|
|
2590
|
-
setJobResult(result.data);
|
|
2591
|
-
setState("done");
|
|
2592
|
-
} else {
|
|
2593
|
-
setError(result.error ?? "Failed to create job");
|
|
2594
|
-
setState("error");
|
|
2595
|
-
}
|
|
2596
|
-
setTimeout(() => exit(), 500);
|
|
2597
|
-
return;
|
|
2598
|
-
}
|
|
2599
|
-
|
|
2600
|
-
// Upload local datasets
|
|
2601
|
-
setUploadStatuses(statuses);
|
|
2602
|
-
setState("uploading");
|
|
2603
|
-
|
|
2604
|
-
const uploadedDatasets: api.DatasetRef[] = [...remoteDatasets];
|
|
2605
|
-
let hasError = false;
|
|
3514
|
+
{remoteSaveFailed && (
|
|
3515
|
+
<Text color="yellow">⚠ Remote save failed (check server logs). Saved locally instead.</Text>
|
|
3516
|
+
)}
|
|
3517
|
+
{savedPath && (
|
|
3518
|
+
<Text color="cyan">Saved to: {savedPath}</Text>
|
|
3519
|
+
)}
|
|
3520
|
+
</Box>
|
|
3521
|
+
);
|
|
3522
|
+
}
|
|
2606
3523
|
|
|
2607
|
-
|
|
2608
|
-
|
|
3524
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3525
|
+
// Helper: Infer format from file extension
|
|
3526
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2609
3527
|
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
3528
|
+
function inferFormatFromPath(
|
|
3529
|
+
path: string | undefined,
|
|
3530
|
+
defaultFormat: string = "jsonl"
|
|
3531
|
+
): "csv" | "jsonl" | "parquet" {
|
|
3532
|
+
if (!path) return defaultFormat as "csv" | "jsonl" | "parquet";
|
|
2614
3533
|
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
isTemp = true;
|
|
2622
|
-
}
|
|
3534
|
+
const ext = path.toLowerCase().split(".").pop();
|
|
3535
|
+
if (ext === "csv" || ext === "jsonl" || ext === "parquet") {
|
|
3536
|
+
return ext;
|
|
3537
|
+
}
|
|
3538
|
+
return defaultFormat as "csv" | "jsonl" | "parquet";
|
|
3539
|
+
}
|
|
2623
3540
|
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
format: "jsonl",
|
|
2628
|
-
});
|
|
3541
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3542
|
+
// Dataset List Command (tabular: remote + local in one table)
|
|
3543
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2629
3544
|
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
3545
|
+
interface DatasetRow {
|
|
3546
|
+
source: string;
|
|
3547
|
+
id: string;
|
|
3548
|
+
name: string;
|
|
3549
|
+
version: string;
|
|
3550
|
+
type: string;
|
|
3551
|
+
samples: string;
|
|
3552
|
+
created: string;
|
|
3553
|
+
[key: string]: string;
|
|
3554
|
+
}
|
|
2634
3555
|
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
};
|
|
2640
|
-
uploadedDatasets.push(uploadedRef);
|
|
2641
|
-
setUploadStatuses(prev => prev.map((s, idx) =>
|
|
2642
|
-
idx === i ? { ...s, status: "done" as const, uploadedRef } : s
|
|
2643
|
-
));
|
|
2644
|
-
} else {
|
|
2645
|
-
hasError = true;
|
|
2646
|
-
setUploadStatuses(prev => prev.map((s, idx) =>
|
|
2647
|
-
idx === i ? { ...s, status: "error" as const, error: result.error } : s
|
|
2648
|
-
));
|
|
2649
|
-
}
|
|
2650
|
-
} catch (err) {
|
|
2651
|
-
hasError = true;
|
|
2652
|
-
setUploadStatuses(prev => prev.map((s, idx) =>
|
|
2653
|
-
idx === i ? { ...s, status: "error" as const, error: err instanceof Error ? err.message : String(err) } : s
|
|
2654
|
-
));
|
|
2655
|
-
}
|
|
2656
|
-
}
|
|
3556
|
+
function shortDatasetId(id?: string): string {
|
|
3557
|
+
if (!id) return "—";
|
|
3558
|
+
return id.length > 8 ? id.slice(0, 8) : id;
|
|
3559
|
+
}
|
|
2657
3560
|
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
return;
|
|
2663
|
-
}
|
|
3561
|
+
function formatSampleCount(n?: number): string {
|
|
3562
|
+
if (typeof n !== "number" || Number.isNaN(n)) return "—";
|
|
3563
|
+
return n.toLocaleString();
|
|
3564
|
+
}
|
|
2664
3565
|
|
|
2665
|
-
|
|
2666
|
-
|
|
3566
|
+
const DATASET_COLUMNS: TableColumn<DatasetRow>[] = [
|
|
3567
|
+
{ key: "source", header: "SOURCE", minWidth: 6, maxWidth: 6, color: (row) => (row.source === "remote" ? "cyan" : "green") },
|
|
3568
|
+
{ key: "id", header: "ID", minWidth: 8, maxWidth: 8 },
|
|
3569
|
+
{ key: "name", header: "NAME", minWidth: 10, maxWidth: 38, flexible: true },
|
|
3570
|
+
{ key: "version", header: "VERSION", minWidth: 4, maxWidth: 8 },
|
|
3571
|
+
{ key: "type", header: "TYPE", minWidth: 4, maxWidth: 14, flexible: true },
|
|
3572
|
+
{ key: "samples", header: "SAMPLES", minWidth: 7, maxWidth: 10 },
|
|
3573
|
+
{ key: "created", header: "CREATED", minWidth: 7, maxWidth: 10 },
|
|
3574
|
+
];
|
|
2667
3575
|
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
3576
|
+
function DatasetListCommand() {
|
|
3577
|
+
const { exit } = useApp();
|
|
3578
|
+
const [state, setState] = useState<"loading" | "done">("loading");
|
|
3579
|
+
const [remoteData, setRemoteData] = useState<api.DatasetListResponse | null>(null);
|
|
3580
|
+
const [remoteError, setRemoteError] = useState<string | null>(null);
|
|
3581
|
+
const [localDatasets, setLocalDatasets] = useState<LocalDataset[]>([]);
|
|
2674
3582
|
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
3583
|
+
useEffect(() => {
|
|
3584
|
+
(async () => {
|
|
3585
|
+
const result = await api.listDatasets();
|
|
3586
|
+
setLocalDatasets(listLocalDatasets());
|
|
3587
|
+
if (result.ok) setRemoteData(result.data ?? null);
|
|
3588
|
+
else setRemoteError(result.error ?? "Unknown error");
|
|
3589
|
+
setState("done");
|
|
2682
3590
|
setTimeout(() => exit(), 500);
|
|
2683
3591
|
})();
|
|
2684
|
-
}, [
|
|
2685
|
-
|
|
2686
|
-
if (state === "resolving") {
|
|
2687
|
-
return <Loading message="Resolving datasets..." />;
|
|
2688
|
-
}
|
|
2689
|
-
|
|
2690
|
-
if (state === "uploading") {
|
|
2691
|
-
return (
|
|
2692
|
-
<Box flexDirection="column">
|
|
2693
|
-
<Text>
|
|
2694
|
-
<Text color="blue"><Spinner type="dots" /></Text>
|
|
2695
|
-
{" "}Uploading local datasets...
|
|
2696
|
-
</Text>
|
|
2697
|
-
{uploadStatuses.map((status, idx) => (
|
|
2698
|
-
<Box key={idx}>
|
|
2699
|
-
<Text>
|
|
2700
|
-
{" "}
|
|
2701
|
-
{status.status === "pending" && <Text color="gray">○</Text>}
|
|
2702
|
-
{status.status === "uploading" && <Text color="yellow"><Spinner type="dots" /></Text>}
|
|
2703
|
-
{status.status === "done" && <Text color="green">✓</Text>}
|
|
2704
|
-
{status.status === "error" && <Text color="red">✗</Text>}
|
|
2705
|
-
{" "}{status.name}
|
|
2706
|
-
{status.status === "done" && status.uploadedRef && (
|
|
2707
|
-
<Text color="gray"> → {status.uploadedRef.name}:{status.uploadedRef.version}</Text>
|
|
2708
|
-
)}
|
|
2709
|
-
{status.status === "error" && status.error && (
|
|
2710
|
-
<Text color="red"> ({status.error})</Text>
|
|
2711
|
-
)}
|
|
2712
|
-
</Text>
|
|
2713
|
-
</Box>
|
|
2714
|
-
))}
|
|
2715
|
-
</Box>
|
|
2716
|
-
);
|
|
2717
|
-
}
|
|
3592
|
+
}, [exit]);
|
|
2718
3593
|
|
|
2719
|
-
if (state === "
|
|
2720
|
-
return <Loading message="Creating training job..." />;
|
|
2721
|
-
}
|
|
3594
|
+
if (state === "loading") return <Loading />;
|
|
2722
3595
|
|
|
2723
|
-
|
|
3596
|
+
const remoteDatasets = remoteData?.datasets ?? [];
|
|
3597
|
+
const remoteRows: DatasetRow[] = remoteDatasets.map((ds) => ({
|
|
3598
|
+
source: "remote",
|
|
3599
|
+
id: shortDatasetId(ds.id),
|
|
3600
|
+
name: ds.dataset_name || "—",
|
|
3601
|
+
version: ds.version_number ? `v${ds.version_number}` : "v1",
|
|
3602
|
+
type: ds.dataset_type || "—",
|
|
3603
|
+
samples: formatSampleCount(ds.sample_size),
|
|
3604
|
+
created: relativeTime(ds.created_at),
|
|
3605
|
+
}));
|
|
3606
|
+
const localRows: DatasetRow[] = localDatasets.map((ds) => ({
|
|
3607
|
+
source: "local",
|
|
3608
|
+
id: "—",
|
|
3609
|
+
name: ds.name,
|
|
3610
|
+
version: "—",
|
|
3611
|
+
type: ds.type || "—",
|
|
3612
|
+
samples: formatSampleCount(ds.sample_size),
|
|
3613
|
+
created: "—",
|
|
3614
|
+
}));
|
|
3615
|
+
const rows = [...remoteRows, ...localRows];
|
|
3616
|
+
|
|
3617
|
+
if (rows.length === 0 && !remoteError) {
|
|
2724
3618
|
return (
|
|
2725
3619
|
<Box flexDirection="column">
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
<Text color="green">✓ Uploaded datasets:</Text>
|
|
2729
|
-
{uploadStatuses.filter(s => s.status === "done").map((status, idx) => (
|
|
2730
|
-
<Text key={idx} color="gray">
|
|
2731
|
-
{" "}{status.name} → {status.uploadedRef?.name}:{status.uploadedRef?.version}
|
|
2732
|
-
</Text>
|
|
2733
|
-
))}
|
|
2734
|
-
</Box>
|
|
2735
|
-
)}
|
|
2736
|
-
<ErrorMessage error={error} />
|
|
3620
|
+
<Text>No datasets found.</Text>
|
|
3621
|
+
<Text dimColor>Use the Pioneer web app at agent.pioneer.ai to create datasets, or drop JSON/JSONL files in ./datasets/.</Text>
|
|
2737
3622
|
</Box>
|
|
2738
3623
|
);
|
|
2739
3624
|
}
|
|
2740
3625
|
|
|
2741
3626
|
return (
|
|
2742
3627
|
<Box flexDirection="column">
|
|
2743
|
-
{
|
|
2744
|
-
<Box flexDirection="column" marginBottom={1}>
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
<Text key={idx} color="gray">
|
|
2748
|
-
{" "}{status.name} → {status.uploadedRef?.name}:{status.uploadedRef?.version}
|
|
2749
|
-
</Text>
|
|
3628
|
+
{remoteError ? (
|
|
3629
|
+
<Box flexDirection="column" marginBottom={rows.length ? 1 : 0}>
|
|
3630
|
+
{remoteError.split("\n").map((line, idx) => (
|
|
3631
|
+
<Text key={idx} color="red">{line}</Text>
|
|
2750
3632
|
))}
|
|
2751
3633
|
</Box>
|
|
2752
|
-
)}
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
{" "}Model: <Text color="yellow">{jobResult.model_name}</Text>
|
|
2761
|
-
</Text>
|
|
2762
|
-
<Text>
|
|
2763
|
-
{" "}Status: <Text color="blue">{jobResult.status}</Text>
|
|
2764
|
-
</Text>
|
|
2765
|
-
</Box>
|
|
2766
|
-
)}
|
|
3634
|
+
) : null}
|
|
3635
|
+
{rows.length > 0 ? (
|
|
3636
|
+
<DataTable
|
|
3637
|
+
columns={DATASET_COLUMNS}
|
|
3638
|
+
rows={rows}
|
|
3639
|
+
footer={`${remoteRows.length} remote · ${localRows.length} local · use \`pioneer dataset get <name[:version]>\` for details · use \`--json\` for raw JSON`}
|
|
3640
|
+
/>
|
|
3641
|
+
) : null}
|
|
2767
3642
|
</Box>
|
|
2768
3643
|
);
|
|
2769
3644
|
}
|
|
2770
3645
|
|
|
3646
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3647
|
+
// Dataset Get Command (single-resource key/value layout)
|
|
3648
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3649
|
+
|
|
3650
|
+
function datasetToDetailRows(ds: api.Dataset): DetailRow[] {
|
|
3651
|
+
const rows: DetailRow[] = [
|
|
3652
|
+
{ label: "ID", value: formatNonEmpty(ds.id) },
|
|
3653
|
+
{ label: "Name", value: formatNonEmpty(ds.dataset_name) },
|
|
3654
|
+
{ label: "Version", value: ds.version_number ? `v${ds.version_number}` : "—" },
|
|
3655
|
+
{ label: "Type", value: formatNonEmpty(ds.dataset_type) },
|
|
3656
|
+
{ label: "Status", value: formatNonEmpty(ds.status), valueColor: statusColor(ds.status) },
|
|
3657
|
+
{ label: "Samples", value: formatSampleCount(ds.sample_size) },
|
|
3658
|
+
{ label: "Train ratio", value: ds.train_ratio != null ? formatPercent(ds.train_ratio) : "—" },
|
|
3659
|
+
{ label: "Visibility", value: formatNonEmpty(ds.visibility) },
|
|
3660
|
+
{ label: "Labels", value: formatLabels(ds.labels) },
|
|
3661
|
+
{ label: "Annotation status", value: formatNonEmpty(ds.annotation_status) },
|
|
3662
|
+
{ label: "Project ID", value: formatNonEmpty(ds.project_id) },
|
|
3663
|
+
{ label: "Root dataset ID", value: formatNonEmpty(ds.root_dataset_id) },
|
|
3664
|
+
{ label: "Created", value: formatTimestamp(ds.created_at) },
|
|
3665
|
+
{ label: "Updated", value: formatTimestamp(ds.updated_at) },
|
|
3666
|
+
];
|
|
3667
|
+
if (ds.processing_error) {
|
|
3668
|
+
rows.push({ label: "Error", value: ds.processing_error, valueColor: "red" });
|
|
3669
|
+
}
|
|
3670
|
+
return rows;
|
|
3671
|
+
}
|
|
3672
|
+
|
|
3673
|
+
function DatasetGetCommand({ dataset }: { dataset: api.DatasetRef }) {
|
|
3674
|
+
const { exit } = useApp();
|
|
3675
|
+
const [state, setState] = useState<"loading" | "done" | "error">("loading");
|
|
3676
|
+
const [data, setData] = useState<api.Dataset | null>(null);
|
|
3677
|
+
const [error, setError] = useState("");
|
|
3678
|
+
|
|
3679
|
+
useEffect(() => {
|
|
3680
|
+
let active = true;
|
|
3681
|
+
(async () => {
|
|
3682
|
+
const result = await api.getDataset(dataset);
|
|
3683
|
+
if (!active) return;
|
|
3684
|
+
if (!result.ok) {
|
|
3685
|
+
setError(result.error ?? "Failed to load dataset.");
|
|
3686
|
+
setState("error");
|
|
3687
|
+
} else {
|
|
3688
|
+
setData(result.data ?? null);
|
|
3689
|
+
setState("done");
|
|
3690
|
+
}
|
|
3691
|
+
setTimeout(() => exit(), 500);
|
|
3692
|
+
})();
|
|
3693
|
+
return () => {
|
|
3694
|
+
active = false;
|
|
3695
|
+
};
|
|
3696
|
+
}, [dataset.name, dataset.version, exit]);
|
|
3697
|
+
|
|
3698
|
+
if (state === "loading") return <Loading message={`Loading dataset ${dataset.name}:${dataset.version}...`} />;
|
|
3699
|
+
if (state === "error") return <ErrorMessage error={error} />;
|
|
3700
|
+
if (!data) return <ErrorMessage error="Dataset response was empty." />;
|
|
3701
|
+
|
|
3702
|
+
return (
|
|
3703
|
+
<DetailView
|
|
3704
|
+
rows={datasetToDetailRows(data)}
|
|
3705
|
+
footer={`Use \`pioneer dataset get ${dataset.name}:${dataset.version} --json\` for the raw JSON payload.`}
|
|
3706
|
+
/>
|
|
3707
|
+
);
|
|
3708
|
+
}
|
|
3709
|
+
|
|
2771
3710
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2772
3711
|
// Dataset Download Command
|
|
2773
3712
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -2943,13 +3882,6 @@ function TrainedModelCard({ model, index }: TrainedModelCardProps) {
|
|
|
2943
3882
|
<Box width={24}>
|
|
2944
3883
|
<Text color="blue">{datasetInfo.substring(0, 22)}</Text>
|
|
2945
3884
|
</Box>
|
|
2946
|
-
<Box width={14}>
|
|
2947
|
-
<Text>
|
|
2948
|
-
<Text color="yellow">{model.nr_epochs}</Text>
|
|
2949
|
-
<Text dimColor>e </Text>
|
|
2950
|
-
<Text color="yellow">{model.learning_rate}</Text>
|
|
2951
|
-
</Text>
|
|
2952
|
-
</Box>
|
|
2953
3885
|
<Box width={30}>
|
|
2954
3886
|
<Text color="green">{metricsDisplay || "N/A"}</Text>
|
|
2955
3887
|
</Box>
|
|
@@ -2960,11 +3892,6 @@ function TrainedModelCard({ model, index }: TrainedModelCardProps) {
|
|
|
2960
3892
|
<Text dimColor>{formatDateShort(model.completed_at || model.started_at || model.created_at)}</Text>
|
|
2961
3893
|
</Box>
|
|
2962
3894
|
</Box>
|
|
2963
|
-
{model.error_message && (
|
|
2964
|
-
<Box marginLeft={2}>
|
|
2965
|
-
<Text color="red">Error: {model.error_message}</Text>
|
|
2966
|
-
</Box>
|
|
2967
|
-
)}
|
|
2968
3895
|
</Box>
|
|
2969
3896
|
);
|
|
2970
3897
|
}
|
|
@@ -3036,15 +3963,33 @@ function DeployedModelCard({ model, index }: DeployedModelCardProps) {
|
|
|
3036
3963
|
interface ProjectModelCardProps {
|
|
3037
3964
|
model: api.ProjectResponse;
|
|
3038
3965
|
index: number;
|
|
3966
|
+
jobIndex?: Map<string, api.TrainingJob>;
|
|
3039
3967
|
}
|
|
3040
3968
|
|
|
3041
|
-
function ProjectModelCard({ model, index }: ProjectModelCardProps) {
|
|
3969
|
+
function ProjectModelCard({ model, index, jobIndex }: ProjectModelCardProps) {
|
|
3042
3970
|
const formatDateShort = (dateStr: string | null | undefined) => {
|
|
3043
3971
|
if (!dateStr) return "N/A";
|
|
3044
3972
|
const date = new Date(dateStr);
|
|
3045
3973
|
return date.toLocaleDateString() + " " + date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
3046
3974
|
};
|
|
3047
3975
|
|
|
3976
|
+
// Drop "<org>/" prefix from HF model ids for compact list display.
|
|
3977
|
+
const stripOrg = (modelId: string) => {
|
|
3978
|
+
const idx = modelId.indexOf("/");
|
|
3979
|
+
return idx >= 0 ? modelId.slice(idx + 1) : modelId;
|
|
3980
|
+
};
|
|
3981
|
+
const selected = model.selected_model_id ?? "";
|
|
3982
|
+
const activeJob = selected && UUID_REGEX.test(selected) ? jobIndex?.get(selected) : undefined;
|
|
3983
|
+
const selectedDisplay = (() => {
|
|
3984
|
+
if (!selected) return "N/A";
|
|
3985
|
+
if (activeJob) {
|
|
3986
|
+
const name = activeJob.model_name ?? shortJobId(selected);
|
|
3987
|
+
const base = activeJob.base_model ? ` ← ${stripOrg(activeJob.base_model)}` : "";
|
|
3988
|
+
return `${name}${base}`;
|
|
3989
|
+
}
|
|
3990
|
+
return UUID_REGEX.test(selected) ? shortJobId(selected) : stripOrg(selected);
|
|
3991
|
+
})();
|
|
3992
|
+
|
|
3048
3993
|
return (
|
|
3049
3994
|
<Box flexDirection="column" marginTop={index > 0 ? 0 : 0}>
|
|
3050
3995
|
<Box>
|
|
@@ -3056,8 +4001,8 @@ function ProjectModelCard({ model, index }: ProjectModelCardProps) {
|
|
|
3056
4001
|
<Box width={38}>
|
|
3057
4002
|
<Text dimColor>{model.id}</Text>
|
|
3058
4003
|
</Box>
|
|
3059
|
-
<Box width={
|
|
3060
|
-
<Text color="magenta"
|
|
4004
|
+
<Box width={36}>
|
|
4005
|
+
<Text color="magenta" wrap="truncate-end">{selectedDisplay}</Text>
|
|
3061
4006
|
</Box>
|
|
3062
4007
|
<Box width={18}>
|
|
3063
4008
|
<Text dimColor>{formatDateShort(model.created_at)}</Text>
|
|
@@ -3086,21 +4031,41 @@ function ModelListCommand({ filter }: ModelListCommandProps) {
|
|
|
3086
4031
|
const { exit } = useApp();
|
|
3087
4032
|
const [state, setState] = useState<"loading" | "done" | "error">("loading");
|
|
3088
4033
|
const [data, setData] = useState<api.AllModelsResponse | null>(null);
|
|
4034
|
+
const [jobIndex, setJobIndex] = useState<Map<string, api.TrainingJob>>(new Map());
|
|
3089
4035
|
const [error, setError] = useState("");
|
|
3090
4036
|
|
|
3091
4037
|
useEffect(() => {
|
|
3092
4038
|
(async () => {
|
|
3093
4039
|
const result = await api.listAllModels();
|
|
3094
|
-
if (result.ok) {
|
|
3095
|
-
setData(result.data ?? null);
|
|
3096
|
-
setState("done");
|
|
3097
|
-
} else {
|
|
4040
|
+
if (!result.ok) {
|
|
3098
4041
|
setError(result.error ?? "Unknown error");
|
|
3099
4042
|
setState("error");
|
|
4043
|
+
setTimeout(() => exit(), 500);
|
|
4044
|
+
return;
|
|
4045
|
+
}
|
|
4046
|
+
setData(result.data ?? null);
|
|
4047
|
+
|
|
4048
|
+
// For "registered" (Model Entries), each project's selected_model_id may
|
|
4049
|
+
// be a training-job UUID. Pull all jobs once and build a lookup so we
|
|
4050
|
+
// can render `model_name (base_model)` in place of the bare UUID.
|
|
4051
|
+
const projects = result.data?.projects ?? [];
|
|
4052
|
+
const needsHydration = projects.some((p) =>
|
|
4053
|
+
p.selected_model_id && UUID_REGEX.test(p.selected_model_id),
|
|
4054
|
+
);
|
|
4055
|
+
if (filter === "registered" && needsHydration) {
|
|
4056
|
+
const jobsResult = await api.listJobs();
|
|
4057
|
+
if (jobsResult.ok) {
|
|
4058
|
+
const map = new Map<string, api.TrainingJob>();
|
|
4059
|
+
for (const j of jobsResult.data?.training_jobs ?? []) {
|
|
4060
|
+
if (j.id) map.set(j.id, j);
|
|
4061
|
+
}
|
|
4062
|
+
setJobIndex(map);
|
|
4063
|
+
}
|
|
3100
4064
|
}
|
|
4065
|
+
setState("done");
|
|
3101
4066
|
setTimeout(() => exit(), 500);
|
|
3102
4067
|
})();
|
|
3103
|
-
}, [exit]);
|
|
4068
|
+
}, [exit, filter]);
|
|
3104
4069
|
|
|
3105
4070
|
if (state === "loading") {
|
|
3106
4071
|
return <Loading />;
|
|
@@ -3131,15 +4096,20 @@ function ModelListCommand({ filter }: ModelListCommandProps) {
|
|
|
3131
4096
|
<Box width={38}>
|
|
3132
4097
|
<Text bold dimColor>Model ID</Text>
|
|
3133
4098
|
</Box>
|
|
3134
|
-
<Box width={
|
|
3135
|
-
<Text bold dimColor>
|
|
4099
|
+
<Box width={36}>
|
|
4100
|
+
<Text bold dimColor>Active Model</Text>
|
|
3136
4101
|
</Box>
|
|
3137
4102
|
<Box width={18}>
|
|
3138
4103
|
<Text bold dimColor>Created</Text>
|
|
3139
4104
|
</Box>
|
|
3140
4105
|
</Box>
|
|
3141
4106
|
{data?.projects.map((model, index) => (
|
|
3142
|
-
<ProjectModelCard
|
|
4107
|
+
<ProjectModelCard
|
|
4108
|
+
key={model.id || index}
|
|
4109
|
+
model={model}
|
|
4110
|
+
index={index}
|
|
4111
|
+
jobIndex={jobIndex}
|
|
4112
|
+
/>
|
|
3143
4113
|
))}
|
|
3144
4114
|
</Box>
|
|
3145
4115
|
)}
|
|
@@ -3200,9 +4170,6 @@ function ModelListCommand({ filter }: ModelListCommandProps) {
|
|
|
3200
4170
|
<Box width={24}>
|
|
3201
4171
|
<Text bold dimColor>Dataset</Text>
|
|
3202
4172
|
</Box>
|
|
3203
|
-
<Box width={14}>
|
|
3204
|
-
<Text bold dimColor>Config</Text>
|
|
3205
|
-
</Box>
|
|
3206
4173
|
<Box width={30}>
|
|
3207
4174
|
<Text bold dimColor>Metrics</Text>
|
|
3208
4175
|
</Box>
|
|
@@ -3265,7 +4232,7 @@ function normalizeModelId(modelId: string): string {
|
|
|
3265
4232
|
|
|
3266
4233
|
function getDecoderTaskType(model: api.BaseModelInfo | null | undefined): string {
|
|
3267
4234
|
if (!model) return "";
|
|
3268
|
-
return `${model.task_type ??
|
|
4235
|
+
return `${model.task_type ?? ""}`.trim().toLowerCase();
|
|
3269
4236
|
}
|
|
3270
4237
|
|
|
3271
4238
|
function modelSupportsDecoderInference(model: api.BaseModelInfo | null | undefined): boolean {
|
|
@@ -3542,112 +4509,11 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
|
|
|
3542
4509
|
<Text dimColor> Dataset format: name[:version] (version defaults to "latest")</Text>
|
|
3543
4510
|
<Text dimColor> Examples: my-dataset, my-dataset:v1, my-dataset:latest</Text>
|
|
3544
4511
|
<Text> </Text>
|
|
3545
|
-
<Text> dataset list
|
|
3546
|
-
<Text> dataset get {"<name[:version]>"} Get dataset details
|
|
3547
|
-
<Text> dataset delete {"<name[:version]>"} Delete a dataset</Text>
|
|
3548
|
-
<Text> dataset analyze {"<name[:version]>"} Analyze a dataset</Text>
|
|
3549
|
-
<Text> dataset analyze-llm {"<name[:version]>"} LLM-only dataset analysis</Text>
|
|
4512
|
+
<Text> dataset list List all datasets (tabular). Add --json for raw JSON.</Text>
|
|
4513
|
+
<Text> dataset get {"<name[:version]>"} Get dataset details (key/value layout). Add --json for raw JSON.</Text>
|
|
3550
4514
|
<Text> </Text>
|
|
3551
|
-
<Text
|
|
3552
|
-
<Text>
|
|
3553
|
-
<Text> --labels {"<l1,l2,...>"} Entity labels (required)</Text>
|
|
3554
|
-
<Text> --num {"<n>"} Number of examples (default: 10)</Text>
|
|
3555
|
-
<Text> --domain {"<desc>"} Domain description</Text>
|
|
3556
|
-
<Text> --save true Save to database</Text>
|
|
3557
|
-
<Text> --name {"<name>"} Dataset name (required if --save)</Text>
|
|
3558
|
-
<Text> </Text>
|
|
3559
|
-
<Text> dataset generate classification</Text>
|
|
3560
|
-
<Text> --labels {"<l1,l2,...>"} Class labels (required)</Text>
|
|
3561
|
-
<Text> --num {"<n>"} Number of examples (default: 10)</Text>
|
|
3562
|
-
<Text> --domain {"<desc>"} Domain description</Text>
|
|
3563
|
-
<Text> --multi-label true Enable multi-label</Text>
|
|
3564
|
-
<Text> --save true Save to database</Text>
|
|
3565
|
-
<Text> --name {"<name>"} Dataset name (required if --save)</Text>
|
|
3566
|
-
<Text> </Text>
|
|
3567
|
-
<Text> dataset generate custom</Text>
|
|
3568
|
-
<Text> --prompt {"<prompt>"} Task description (required)</Text>
|
|
3569
|
-
<Text> --format {"<json>"} Output format as JSON (required)</Text>
|
|
3570
|
-
<Text> --num {"<n>"} Number of examples (default: 10)</Text>
|
|
3571
|
-
<Text> --save true Save to database</Text>
|
|
3572
|
-
<Text> --name {"<name>"} Dataset name (required if --save)</Text>
|
|
3573
|
-
<Text> </Text>
|
|
3574
|
-
<Text> dataset generate decoder</Text>
|
|
3575
|
-
<Text> --domain {"<desc>"} Domain/task description (required)</Text>
|
|
3576
|
-
<Text> --instruction {"<text>"} System instruction (optional, auto-inferred)</Text>
|
|
3577
|
-
<Text> --num {"<n>"} Number of examples (default: 10)</Text>
|
|
3578
|
-
<Text> --save true Save to database</Text>
|
|
3579
|
-
<Text> --name {"<name>"} Dataset name (required if --save)</Text>
|
|
3580
|
-
<Text> Advanced generation flags:</Text>
|
|
3581
|
-
<Text> --quality {"<light|medium|heavy>"} Generation quality profile</Text>
|
|
3582
|
-
<Text> --generation-profile {"<auto|fast|balanced|quality>"} Runtime profile</Text>
|
|
3583
|
-
<Text> --reasoning-trace {"true|false"} Include reasoning traces (decoder only)</Text>
|
|
3584
|
-
<Text> --reasoning-effort {"<low|medium|high>"} Reasoning effort</Text>
|
|
3585
|
-
<Text> --multiplicator {"<json>"} Multiplicator settings</Text>
|
|
3586
|
-
<Text> --use-meta-felix {"true|false"} Use MetaFelix metadata</Text>
|
|
3587
|
-
<Text> --min-criteria {"<n>"} Minimum diversity criteria</Text>
|
|
3588
|
-
<Text> --target-choices {"<n>"} Diversity target choices</Text>
|
|
3589
|
-
<Text> --project-id {"<id>"} Project ID</Text>
|
|
3590
|
-
<Text> --type {"training|evaluation|split"} Dataset type</Text>
|
|
3591
|
-
<Text> --visibility {"private|public"} Dataset visibility</Text>
|
|
3592
|
-
<Text> --split-ratio {"<train:eval>|{json>}"} Split dataset ratio</Text>
|
|
3593
|
-
<Text> --negative-ratio {"<n>"} Percent negative samples</Text>
|
|
3594
|
-
<Text> --classified-examples {"<json>"} Classified examples with feedback</Text>
|
|
3595
|
-
<Text> </Text>
|
|
3596
|
-
<Text bold> Infer Labels:</Text>
|
|
3597
|
-
<Text> dataset infer ner Infer NER labels from description</Text>
|
|
3598
|
-
<Text> dataset infer classification Infer classification labels</Text>
|
|
3599
|
-
<Text> dataset infer fields Infer input/output fields</Text>
|
|
3600
|
-
<Text> --domain {"<desc>"} Domain description (required)</Text>
|
|
3601
|
-
<Text> dataset infer infer-advanced Infer constraints and multiplicator from a prompt</Text>
|
|
3602
|
-
<Text> --prompt {"<prompt>"} Prompt for inference</Text>
|
|
3603
|
-
<Text> --labels {"<l1,l2,...>"} Optional labels to guide suggestions</Text>
|
|
3604
|
-
<Text> --data-type {"<type>"} entity_extraction|classification|json_extraction</Text>
|
|
3605
|
-
<Text> dataset infer improve-prompt Improve a generation prompt</Text>
|
|
3606
|
-
<Text> --prompt {"<prompt>"} Prompt to improve</Text>
|
|
3607
|
-
<Text> --data-type {"<type>"} Optional prompt domain hint</Text>
|
|
3608
|
-
<Text> dataset label-existing ner Label existing NER texts</Text>
|
|
3609
|
-
<Text> --labels {"<l1,l2,...>"} Labels for entities</Text>
|
|
3610
|
-
<Text> --inputs {"[{\"text\":\"...\"},...]"} Input texts JSON array (required)</Text>
|
|
3611
|
-
<Text> --name {"<name>"} Output dataset name (optional if --save false)</Text>
|
|
3612
|
-
<Text> --project-id {"<project_id>"} Assign output dataset to project</Text>
|
|
3613
|
-
<Text> --save {"<true|false>"} Save dataset (default: false)</Text>
|
|
3614
|
-
<Text> dataset label-existing classification Label existing classification texts</Text>
|
|
3615
|
-
<Text> --labels {"<l1,l2,...>"} Labels for classes</Text>
|
|
3616
|
-
<Text> --inputs {"[{\"text\":\"...\"},...]"} Input texts JSON array (required)</Text>
|
|
3617
|
-
<Text> --name {"<name>"} Output dataset name (optional if --save false)</Text>
|
|
3618
|
-
<Text> --project-id {"<project_id>"} Assign output dataset to project</Text>
|
|
3619
|
-
<Text> --save {"<true|false>"} Save dataset (default: false)</Text>
|
|
3620
|
-
<Text> dataset label-existing fields Label existing structured records</Text>
|
|
3621
|
-
<Text> --input-fields {"[{\"name\":\"...\"},...]"} Input schema fields (required)</Text>
|
|
3622
|
-
<Text> --output-fields {"[{\"name\":\"...\"},...]"} Output schema fields (required)</Text>
|
|
3623
|
-
<Text> --inputs {"[{\"f1\":\"v\"},...]"} Input records JSON array (required)</Text>
|
|
3624
|
-
<Text> --name {"<name>"} Output dataset name (optional if --save false)</Text>
|
|
3625
|
-
<Text> --project-id {"<project_id>"} Assign output dataset to project</Text>
|
|
3626
|
-
<Text> --save {"<true|false>"} Save dataset (default: false)</Text>
|
|
3627
|
-
<Text> </Text>
|
|
3628
|
-
<Text bold> Upload/Download:</Text>
|
|
3629
|
-
<Text> dataset upload {"<file>"} Upload local file to Pioneer</Text>
|
|
3630
|
-
<Text> --name {"<name>"} Dataset name (required)</Text>
|
|
3631
|
-
<Text> --type {"<type>"} Type: ner, classification, custom</Text>
|
|
3632
|
-
<Text> dataset upload {"<name[:version]>"} --to hf Upload Pioneer dataset to Hugging Face</Text>
|
|
3633
|
-
<Text> --repo {"<repo>"} HF repo (required, e.g., username/dataset)</Text>
|
|
3634
|
-
<Text> --private Make repo private</Text>
|
|
3635
|
-
<Text dimColor> Note: Set HF token with 'pioneer auth hf' first</Text>
|
|
3636
|
-
<Text> dataset download {"<name[:version]>"} Download from Pioneer to local file</Text>
|
|
3637
|
-
<Text> --format {"<type>"} Format: jsonl, csv, parquet (default: jsonl)</Text>
|
|
3638
|
-
<Text> --output {"<path>"} Output file path</Text>
|
|
3639
|
-
<Text> dataset download --from hf Download from Hugging Face to Pioneer</Text>
|
|
3640
|
-
<Text> --repo {"<repo>"} HF repo (required, e.g., username/dataset)</Text>
|
|
3641
|
-
<Text> --name {"<name>"} Local dataset name (optional)</Text>
|
|
3642
|
-
<Text> --revision {"<rev>"} Git revision/branch (optional)</Text>
|
|
3643
|
-
<Text dimColor> Note: For private repos, set HF token with 'pioneer auth hf'</Text>
|
|
3644
|
-
<Text> </Text>
|
|
3645
|
-
<Text bold> Data Editing:</Text>
|
|
3646
|
-
<Text> dataset edit --help Show data editing commands</Text>
|
|
3647
|
-
<Text> dataset edit scan-pii {"<name[:version]>"} Scan for PII</Text>
|
|
3648
|
-
<Text> dataset edit dismiss-outlier {"<name[:version]>"} Dismiss an outlier fingerprint</Text>
|
|
3649
|
-
<Text> --fingerprint {"<hash>"} Outlier fingerprint from dataset analysis</Text>
|
|
3650
|
-
<Text> dataset edit subsample {"<name[:version]>"} Create a subsample</Text>
|
|
4515
|
+
<Text dimColor> Other dataset subcommands (generate/edit/upload/etc.) are temporarily hidden in this version.</Text>
|
|
4516
|
+
<Text dimColor> Use the Pioneer web app at agent.pioneer.ai to create or edit datasets.</Text>
|
|
3651
4517
|
</Box>
|
|
3652
4518
|
);
|
|
3653
4519
|
}
|
|
@@ -3710,18 +4576,13 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
|
|
|
3710
4576
|
return (
|
|
3711
4577
|
<Box flexDirection="column">
|
|
3712
4578
|
<Text bold>Job Commands:</Text>
|
|
3713
|
-
<Text> job list List training jobs
|
|
3714
|
-
<Text> job get {"<id>"} Get job details
|
|
4579
|
+
<Text> job list List training jobs (tabular). Add --json for raw JSON.</Text>
|
|
4580
|
+
<Text> job get {"<id>"} Get job details (key/value layout). Add --json for raw JSON. Accepts a unique short prefix from `job list`.</Text>
|
|
3715
4581
|
<Text> job logs {"<id>"} Get job logs</Text>
|
|
3716
4582
|
<Text> job delete {"<id>"} Delete a training job</Text>
|
|
3717
|
-
<Text>
|
|
3718
|
-
<Text
|
|
3719
|
-
<Text
|
|
3720
|
-
<Text> remote:{"<name>"}:{"<version>"} - by name and version</Text>
|
|
3721
|
-
<Text> remote:{"<uuid>"} - by UUID</Text>
|
|
3722
|
-
<Text> local:{"<name>"} - auto-upload from ./datasets/</Text>
|
|
3723
|
-
<Text> --base-model {"<model>"} Base model (default: fastino/gliner2-base-v1)</Text>
|
|
3724
|
-
<Text> --epochs {"<n>"} Number of epochs (default: 5)</Text>
|
|
4583
|
+
<Text> </Text>
|
|
4584
|
+
<Text dimColor> To create a training job, use `pioneer agent` — it will help you pick a base model,</Text>
|
|
4585
|
+
<Text dimColor> select datasets, and configure training conversationally.</Text>
|
|
3725
4586
|
</Box>
|
|
3726
4587
|
);
|
|
3727
4588
|
}
|
|
@@ -3740,13 +4601,13 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
|
|
|
3740
4601
|
<Text> --description {"<text>"} Optional</Text>
|
|
3741
4602
|
<Text> --model {"<base-model-id>"} Optional (starts interactive picker when omitted)</Text>
|
|
3742
4603
|
<Text> --example {"<json>"} Optional</Text>
|
|
3743
|
-
<Text> model endpoints get {"<model-id>"} Get endpoint/
|
|
4604
|
+
<Text> model endpoints get {"<model-id>"} Get endpoint details (key/value layout, includes dataset count). Add --json for raw JSON.</Text>
|
|
3744
4605
|
<Text> model endpoints update {"<model-id>"} Update endpoint metadata</Text>
|
|
3745
4606
|
<Text> --name {"<name>"} --icon {"<icon>"} --repo {"<repo-url>"} --description {"<text>"} --model-id {"<id>"}</Text>
|
|
3746
4607
|
<Text> model endpoints delete {"<model-id>"} Delete an endpoint/model entry</Text>
|
|
3747
|
-
<Text> model endpoints
|
|
3748
|
-
<Text> model endpoints
|
|
3749
|
-
<Text
|
|
4608
|
+
<Text> model endpoints quality-metrics {"<model-id>"} Show LLMAJ pass/fail metrics. Add --json for raw JSON.</Text>
|
|
4609
|
+
<Text> model endpoints deploy {"<model-id>"} [--job {"<training-job-id>"}] [--reason {"<text>"}] [--all]</Text>
|
|
4610
|
+
<Text dimColor> If --job is omitted, pick from a list of deployable jobs interactively. By default the list is filtered to jobs whose base model matches the endpoint; pass --all to bypass that filter.</Text>
|
|
3750
4611
|
<Text> model endpoints rollback {"<model-id>"} {"<deployment-id>"} Rollback endpoint to previous deployment</Text>
|
|
3751
4612
|
</Box>
|
|
3752
4613
|
);
|
|
@@ -3761,7 +4622,7 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
|
|
|
3761
4622
|
<Text> model artifacts list Show both trained and deployed artifacts</Text>
|
|
3762
4623
|
<Text> model artifacts trained List trained artifacts</Text>
|
|
3763
4624
|
<Text> model artifacts deployed List deployed artifacts</Text>
|
|
3764
|
-
<Text> model artifacts download {"<job-id>"}
|
|
4625
|
+
<Text> model artifacts download {"<job-id>"} Get a signed download URL for the artifact (key/value layout). Add --json for raw JSON.</Text>
|
|
3765
4626
|
<Text> model artifacts delete {"<job-id>"} Delete deployed artifact record</Text>
|
|
3766
4627
|
<Text> model artifacts upload {"<job-id>"} --to hf Upload trained model artifact to Hugging Face</Text>
|
|
3767
4628
|
<Text> --repo {"<repo>"} HF repo (required, e.g., username/model)</Text>
|
|
@@ -3779,17 +4640,20 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
|
|
|
3779
4640
|
<Text bold>Model Commands:</Text>
|
|
3780
4641
|
<Text> model endpoints ... (alias: model_endpoints) Manage model catalog entries (from /projects)</Text>
|
|
3781
4642
|
<Text> model artifacts ... (alias: model_artifacts) Manage trained/deployed artifacts (from /felix)</Text>
|
|
4643
|
+
<Text> model base-models List available base models (tabular)</Text>
|
|
3782
4644
|
<Text> </Text>
|
|
3783
4645
|
<Text> model endpoints list</Text>
|
|
3784
4646
|
<Text> model endpoints create</Text>
|
|
3785
4647
|
<Text> model endpoints get {"<model-id>"}</Text>
|
|
3786
|
-
<Text> model endpoints
|
|
4648
|
+
<Text> model endpoints quality-metrics {"<model-id>"}</Text>
|
|
4649
|
+
<Text> model endpoints deploy {"<model-id>"} [--job {"<training-job-id>"}] [--reason {"<text>"}] [--all]</Text>
|
|
3787
4650
|
<Text> model endpoints rollback {"<model-id>"} {"<deployment-id>"}</Text>
|
|
3788
4651
|
<Text> model artifacts list</Text>
|
|
3789
4652
|
<Text> model artifacts trained</Text>
|
|
3790
|
-
|
|
3791
|
-
|
|
4653
|
+
<Text> model artifacts deployed</Text>
|
|
4654
|
+
<Text> model artifacts download {"<job-id>"}</Text>
|
|
3792
4655
|
<Text> </Text>
|
|
4656
|
+
<Text dimColor> Most read commands accept `--json` for the raw JSON payload.</Text>
|
|
3793
4657
|
</Box>
|
|
3794
4658
|
);
|
|
3795
4659
|
}
|
|
@@ -3921,13 +4785,8 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
3921
4785
|
rest[0] === "create" &&
|
|
3922
4786
|
parseErrors.length === 1 &&
|
|
3923
4787
|
parseErrors[0] === "--model";
|
|
3924
|
-
const isModelEndpointsDeployMissingJob =
|
|
3925
|
-
group === "model" &&
|
|
3926
|
-
normalizedAction === "endpoints" &&
|
|
3927
|
-
rest[0] === "deploy" &&
|
|
3928
|
-
!flags["job"];
|
|
3929
4788
|
|
|
3930
|
-
if (group === "
|
|
4789
|
+
if (group === "inference" || group === "eval" || group === "benchmark") {
|
|
3931
4790
|
return (
|
|
3932
4791
|
<ErrorMessage
|
|
3933
4792
|
error={`The '${group}' command group is temporarily hidden for this version. Use 'pioneer --help' to see available commands.`}
|
|
@@ -3935,6 +4794,19 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
3935
4794
|
);
|
|
3936
4795
|
}
|
|
3937
4796
|
|
|
4797
|
+
if (group === "dataset") {
|
|
4798
|
+
const allowedDatasetActions = new Set(["list", "get", "help", undefined, ""]);
|
|
4799
|
+
const subAction = action ?? "";
|
|
4800
|
+
const isHelp = flags["help"] === "true" || subAction === "" || subAction === "help";
|
|
4801
|
+
if (!isHelp && !allowedDatasetActions.has(subAction)) {
|
|
4802
|
+
return (
|
|
4803
|
+
<ErrorMessage
|
|
4804
|
+
error={`'dataset ${subAction}' is temporarily hidden for this version. Available: pioneer dataset list, pioneer dataset get <name[:version]>.`}
|
|
4805
|
+
/>
|
|
4806
|
+
);
|
|
4807
|
+
}
|
|
4808
|
+
}
|
|
4809
|
+
|
|
3938
4810
|
if (hasParseErrors && !isModelCreateMissingModel) {
|
|
3939
4811
|
const missingValueHints: Record<string, string> = {
|
|
3940
4812
|
"--model": "<base-model-id>",
|
|
@@ -3960,7 +4832,6 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
3960
4832
|
"--labels": "<json-array>",
|
|
3961
4833
|
"--label-column": "<column>",
|
|
3962
4834
|
"--text-column": "<column>",
|
|
3963
|
-
"--dataset-ids": "<comma-separated-ids>",
|
|
3964
4835
|
"--output": "<path>",
|
|
3965
4836
|
"--format-results": "<true|false>",
|
|
3966
4837
|
"--include-confidence": "<true|false>",
|
|
@@ -3975,28 +4846,6 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
3975
4846
|
return missingValueHints[flag] ?? "<value>";
|
|
3976
4847
|
};
|
|
3977
4848
|
|
|
3978
|
-
if (isModelEndpointsDeployMissingJob) {
|
|
3979
|
-
const errorMessage = rest[1]
|
|
3980
|
-
? `Training job ID required: model endpoints deploy ${rest[1]} --job <training-job-id>`
|
|
3981
|
-
: "Training job ID required: model endpoints deploy <model-id> --job <training-job-id>";
|
|
3982
|
-
|
|
3983
|
-
return (
|
|
3984
|
-
<ApiCommand
|
|
3985
|
-
action={() =>
|
|
3986
|
-
Promise.resolve<api.ApiResult<{ message: string }>>({
|
|
3987
|
-
ok: false,
|
|
3988
|
-
status: 400,
|
|
3989
|
-
error: errorMessage,
|
|
3990
|
-
data: {
|
|
3991
|
-
message: errorMessage,
|
|
3992
|
-
},
|
|
3993
|
-
})
|
|
3994
|
-
}
|
|
3995
|
-
successMessage="Validation failed"
|
|
3996
|
-
/>
|
|
3997
|
-
);
|
|
3998
|
-
}
|
|
3999
|
-
|
|
4000
4849
|
return (
|
|
4001
4850
|
<Box flexDirection="column">
|
|
4002
4851
|
<ErrorMessage error="One or more flags are missing values. Please provide values for: " />
|
|
@@ -4360,6 +5209,9 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
4360
5209
|
}
|
|
4361
5210
|
|
|
4362
5211
|
if (action === "list") {
|
|
5212
|
+
if (flags.json === "true") {
|
|
5213
|
+
return <ApiCommand action={api.listDatasets} />;
|
|
5214
|
+
}
|
|
4363
5215
|
return <DatasetListCommand />;
|
|
4364
5216
|
}
|
|
4365
5217
|
if (action === "get" && rest[0]) {
|
|
@@ -4367,7 +5219,10 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
4367
5219
|
if (!dataset) {
|
|
4368
5220
|
return <ErrorMessage error={`Invalid dataset format: ${rest[0]}. Use name[:version] format (e.g., my-dataset or my-dataset:v1).`} />;
|
|
4369
5221
|
}
|
|
4370
|
-
|
|
5222
|
+
if (flags.json === "true") {
|
|
5223
|
+
return <ApiCommand action={() => api.getDataset(dataset)} />;
|
|
5224
|
+
}
|
|
5225
|
+
return <DatasetGetCommand dataset={dataset} />;
|
|
4371
5226
|
}
|
|
4372
5227
|
if (action === "delete" && rest[0]) {
|
|
4373
5228
|
const dataset = parseDatasetRef(rest[0]);
|
|
@@ -4884,48 +5739,55 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
4884
5739
|
return <Help context="job" />;
|
|
4885
5740
|
}
|
|
4886
5741
|
if (action === "list") {
|
|
4887
|
-
|
|
5742
|
+
if (flags.json === "true") {
|
|
5743
|
+
return <ApiCommand action={api.listJobs} />;
|
|
5744
|
+
}
|
|
5745
|
+
return <JobListCommand />;
|
|
4888
5746
|
}
|
|
4889
5747
|
if (action === "get" && rest[0]) {
|
|
4890
|
-
|
|
5748
|
+
const wantsJson = flags.json === "true";
|
|
5749
|
+
return (
|
|
5750
|
+
<WithResolvedJobId
|
|
5751
|
+
rawId={rest[0]}
|
|
5752
|
+
render={(jobId) =>
|
|
5753
|
+
wantsJson ? (
|
|
5754
|
+
<ApiCommand action={() => api.getJob(jobId)} />
|
|
5755
|
+
) : (
|
|
5756
|
+
<JobGetCommand jobId={jobId} />
|
|
5757
|
+
)
|
|
5758
|
+
}
|
|
5759
|
+
/>
|
|
5760
|
+
);
|
|
4891
5761
|
}
|
|
4892
5762
|
if (action === "logs" && rest[0]) {
|
|
4893
|
-
return
|
|
5763
|
+
return (
|
|
5764
|
+
<WithResolvedJobId
|
|
5765
|
+
rawId={rest[0]}
|
|
5766
|
+
render={(jobId) => <JobLogsCommand jobId={jobId} />}
|
|
5767
|
+
/>
|
|
5768
|
+
);
|
|
4894
5769
|
}
|
|
4895
5770
|
if (action === "delete" && rest[0]) {
|
|
4896
5771
|
return (
|
|
4897
|
-
<
|
|
4898
|
-
|
|
4899
|
-
|
|
5772
|
+
<WithResolvedJobId
|
|
5773
|
+
rawId={rest[0]}
|
|
5774
|
+
render={(jobId) => (
|
|
5775
|
+
<ApiCommand
|
|
5776
|
+
action={() => api.deleteJob(jobId)}
|
|
5777
|
+
successMessage={`Training job ${jobId} deleted`}
|
|
5778
|
+
/>
|
|
5779
|
+
)}
|
|
4900
5780
|
/>
|
|
4901
5781
|
);
|
|
4902
5782
|
}
|
|
4903
5783
|
if (action === "create") {
|
|
4904
|
-
const modelName = flags["model-name"];
|
|
4905
|
-
const datasetStrings = flags["dataset-ids"]?.split(",").filter(Boolean) ?? [];
|
|
4906
|
-
const baseModel = flags["base-model"];
|
|
4907
|
-
const epochs = flags["epochs"] ? parseInt(flags["epochs"], 10) : undefined;
|
|
4908
|
-
|
|
4909
|
-
if (!modelName || datasetStrings.length === 0) {
|
|
4910
|
-
return <ErrorMessage error="--model-name and --dataset-ids are required" />;
|
|
4911
|
-
}
|
|
4912
|
-
|
|
4913
|
-
// Parse dataset strings with explicit local:/remote: prefixes
|
|
4914
|
-
const datasets: ParsedDataset[] = [];
|
|
4915
|
-
for (const ds of datasetStrings) {
|
|
4916
|
-
const parsed = parseDatasetString(ds);
|
|
4917
|
-
if ("error" in parsed) {
|
|
4918
|
-
return <ErrorMessage error={parsed.error} />;
|
|
4919
|
-
}
|
|
4920
|
-
datasets.push(parsed);
|
|
4921
|
-
}
|
|
4922
|
-
|
|
4923
5784
|
return (
|
|
4924
|
-
<
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
5785
|
+
<ErrorMessage
|
|
5786
|
+
error={
|
|
5787
|
+
"`pioneer job create` has been removed.\n" +
|
|
5788
|
+
"Use `pioneer agent` to create training jobs conversationally — it will help you pick a base model,\n" +
|
|
5789
|
+
"select datasets, and configure training without needing to remember flags or dataset IDs."
|
|
5790
|
+
}
|
|
4929
5791
|
/>
|
|
4930
5792
|
);
|
|
4931
5793
|
}
|
|
@@ -4939,7 +5801,10 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
4939
5801
|
}
|
|
4940
5802
|
|
|
4941
5803
|
if (action === "base-models" || action === "models" || action === "list") {
|
|
4942
|
-
|
|
5804
|
+
if (flags.json === "true") {
|
|
5805
|
+
return <ApiCommand action={api.listBaseModels} />;
|
|
5806
|
+
}
|
|
5807
|
+
return <BaseModelsListCommand />;
|
|
4943
5808
|
}
|
|
4944
5809
|
|
|
4945
5810
|
if (action === "encoder") {
|
|
@@ -5137,6 +6002,13 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
5137
6002
|
return <Help context="model" />;
|
|
5138
6003
|
}
|
|
5139
6004
|
|
|
6005
|
+
if (normalizedAction === "base-models" || normalizedAction === "models") {
|
|
6006
|
+
if (flags.json === "true") {
|
|
6007
|
+
return <ApiCommand action={api.listBaseModels} />;
|
|
6008
|
+
}
|
|
6009
|
+
return <BaseModelsListCommand />;
|
|
6010
|
+
}
|
|
6011
|
+
|
|
5140
6012
|
if (normalizedAction === "endpoints") {
|
|
5141
6013
|
const endpointAction = rest[0];
|
|
5142
6014
|
const endpointArgs = rest.slice(1);
|
|
@@ -5213,7 +6085,10 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
5213
6085
|
if (!modelId) {
|
|
5214
6086
|
return <ErrorMessage error="Model ID required: model endpoints get <model-id>" />;
|
|
5215
6087
|
}
|
|
5216
|
-
|
|
6088
|
+
if (flags.json === "true") {
|
|
6089
|
+
return <ApiCommand action={() => api.getProject(modelId)} />;
|
|
6090
|
+
}
|
|
6091
|
+
return <ModelEndpointGetCommand modelId={modelId} />;
|
|
5217
6092
|
}
|
|
5218
6093
|
|
|
5219
6094
|
if (endpointAction === "update") {
|
|
@@ -5264,11 +6139,14 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
5264
6139
|
}
|
|
5265
6140
|
|
|
5266
6141
|
if (endpointAction === "dataset-count" || endpointAction === "count") {
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
6142
|
+
return (
|
|
6143
|
+
<ErrorMessage
|
|
6144
|
+
error={
|
|
6145
|
+
"`pioneer model endpoints dataset-count` has been removed.\n" +
|
|
6146
|
+
"The dataset count and `can_delete` flag are now surfaced inline in `pioneer model endpoints get <model-id>`."
|
|
6147
|
+
}
|
|
6148
|
+
/>
|
|
6149
|
+
);
|
|
5272
6150
|
}
|
|
5273
6151
|
|
|
5274
6152
|
if (endpointAction === "quality-metrics" || endpointAction === "quality") {
|
|
@@ -5276,40 +6154,75 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
5276
6154
|
if (!modelId) {
|
|
5277
6155
|
return <ErrorMessage error="Model ID required: model endpoints quality-metrics <model-id>" />;
|
|
5278
6156
|
}
|
|
5279
|
-
|
|
6157
|
+
if (flags.json === "true") {
|
|
6158
|
+
return <ApiCommand action={() => api.getProjectQualityMetrics(modelId)} />;
|
|
6159
|
+
}
|
|
6160
|
+
return <ModelQualityMetricsCommand modelId={modelId} />;
|
|
5280
6161
|
}
|
|
5281
6162
|
|
|
5282
6163
|
if (endpointAction === "deploy") {
|
|
5283
6164
|
const modelId = endpointArgs[0];
|
|
5284
|
-
const
|
|
6165
|
+
const rawJobId = flags["job"];
|
|
5285
6166
|
|
|
5286
6167
|
if (!modelId) {
|
|
5287
|
-
return <ErrorMessage error="Model ID required: model endpoints deploy <model-id> --job <training-job-id>" />;
|
|
5288
|
-
}
|
|
5289
|
-
if (!jobId) {
|
|
5290
|
-
return <ErrorMessage error="Training job ID required: model endpoints deploy <model-id> --job <training-job-id>" />;
|
|
5291
|
-
}
|
|
5292
|
-
if (jobId.length !== 36) {
|
|
5293
|
-
return (
|
|
5294
|
-
<Box flexDirection="column">
|
|
5295
|
-
<ErrorMessage error="Invalid job ID: must be full UUID (36 characters)" />
|
|
5296
|
-
<Text dimColor> Provided: {jobId} ({jobId.length} characters)</Text>
|
|
5297
|
-
<Text dimColor> Tip: Use 'pioneer model artifacts list' and 'pioneer model artifacts trained' to see full job IDs</Text>
|
|
5298
|
-
</Box>
|
|
5299
|
-
);
|
|
6168
|
+
return <ErrorMessage error="Model ID required: model endpoints deploy <model-id> [--job <training-job-id>]" />;
|
|
5300
6169
|
}
|
|
5301
6170
|
|
|
5302
6171
|
const reason = flags["reason"];
|
|
5303
6172
|
|
|
6173
|
+
if (!rawJobId) {
|
|
6174
|
+
if (!isRawModeSupported) {
|
|
6175
|
+
return (
|
|
6176
|
+
<Box flexDirection="column">
|
|
6177
|
+
<ErrorMessage error="Interactive job picker requires a TTY." />
|
|
6178
|
+
<Text>List trained jobs with `pioneer job list` and pass an explicit ID:</Text>
|
|
6179
|
+
<Text dimColor> pioneer model endpoints deploy {modelId} --job <training-job-id></Text>
|
|
6180
|
+
</Box>
|
|
6181
|
+
);
|
|
6182
|
+
}
|
|
6183
|
+
const showAll = flags["all"] === "true";
|
|
6184
|
+
return <DeployJobPickerCommand modelId={modelId} reason={reason} showAll={showAll} />;
|
|
6185
|
+
}
|
|
6186
|
+
|
|
5304
6187
|
return (
|
|
5305
|
-
<
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
6188
|
+
<WithResolvedJobId
|
|
6189
|
+
rawId={rawJobId}
|
|
6190
|
+
render={(jobId) => (
|
|
6191
|
+
<ApiCommand
|
|
6192
|
+
action={async () => {
|
|
6193
|
+
// Deploy requires the job's project_id to match the target.
|
|
6194
|
+
// PATCH first to link them; the backend treats a no-op assign
|
|
6195
|
+
// as success. While we have the job around, capture its
|
|
6196
|
+
// base_model so we can enrich the deployment record (which
|
|
6197
|
+
// currently returns base_model=null for training-job deploys).
|
|
6198
|
+
const jobInfo = await api.getJob(jobId);
|
|
6199
|
+
const patch = await api.updateTrainingJob(jobId, {
|
|
6200
|
+
project_id: modelId,
|
|
6201
|
+
});
|
|
6202
|
+
if (!patch.ok) {
|
|
6203
|
+
return patch as api.ApiResult<unknown>;
|
|
6204
|
+
}
|
|
6205
|
+
const result = await api.deployTrainingJobToProject(modelId, {
|
|
6206
|
+
training_job_id: jobId,
|
|
6207
|
+
...(reason ? { reason } : {}),
|
|
6208
|
+
});
|
|
6209
|
+
if (
|
|
6210
|
+
result.ok &&
|
|
6211
|
+
result.data &&
|
|
6212
|
+
jobInfo.ok &&
|
|
6213
|
+
jobInfo.data?.base_model &&
|
|
6214
|
+
!result.data.base_model
|
|
6215
|
+
) {
|
|
6216
|
+
return {
|
|
6217
|
+
...result,
|
|
6218
|
+
data: { ...result.data, base_model: jobInfo.data.base_model },
|
|
6219
|
+
};
|
|
6220
|
+
}
|
|
6221
|
+
return result;
|
|
6222
|
+
}}
|
|
6223
|
+
successMessage={`Deployment initiated for project ${modelId} from job ${jobId}`}
|
|
6224
|
+
/>
|
|
6225
|
+
)}
|
|
5313
6226
|
/>
|
|
5314
6227
|
);
|
|
5315
6228
|
}
|
|
@@ -5370,37 +6283,33 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
5370
6283
|
if (!artifactArgs[0]) {
|
|
5371
6284
|
return <ErrorMessage error="Job ID required: model artifacts download <job-id>" />;
|
|
5372
6285
|
}
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
6286
|
+
return (
|
|
6287
|
+
<WithResolvedJobId
|
|
6288
|
+
rawId={artifactArgs[0]}
|
|
6289
|
+
render={(jobId) =>
|
|
6290
|
+
flags.json === "true" ? (
|
|
6291
|
+
<ApiCommand action={() => api.downloadModel(jobId)} />
|
|
6292
|
+
) : (
|
|
6293
|
+
<ModelArtifactDownloadCommand jobId={jobId} />
|
|
6294
|
+
)
|
|
6295
|
+
}
|
|
6296
|
+
/>
|
|
6297
|
+
);
|
|
5384
6298
|
}
|
|
5385
6299
|
|
|
5386
6300
|
if (artifactsAction === "delete") {
|
|
5387
6301
|
if (!artifactArgs[0]) {
|
|
5388
6302
|
return <ErrorMessage error="Model ID required: model artifacts delete <job-id>" />;
|
|
5389
6303
|
}
|
|
5390
|
-
const jobId = artifactArgs[0];
|
|
5391
|
-
if (jobId.length !== 36) {
|
|
5392
|
-
return (
|
|
5393
|
-
<Box flexDirection="column">
|
|
5394
|
-
<ErrorMessage error="Invalid job ID: must be full UUID (36 characters)" />
|
|
5395
|
-
<Text dimColor> Provided: {jobId} ({jobId.length} characters)</Text>
|
|
5396
|
-
<Text dimColor> Tip: Use 'pioneer model artifacts list' to see full job IDs</Text>
|
|
5397
|
-
</Box>
|
|
5398
|
-
);
|
|
5399
|
-
}
|
|
5400
6304
|
return (
|
|
5401
|
-
<
|
|
5402
|
-
|
|
5403
|
-
|
|
6305
|
+
<WithResolvedJobId
|
|
6306
|
+
rawId={artifactArgs[0]}
|
|
6307
|
+
render={(jobId) => (
|
|
6308
|
+
<ApiCommand
|
|
6309
|
+
action={() => api.deleteModel(jobId)}
|
|
6310
|
+
successMessage={`Model ${jobId} deleted`}
|
|
6311
|
+
/>
|
|
6312
|
+
)}
|
|
5404
6313
|
/>
|
|
5405
6314
|
);
|
|
5406
6315
|
}
|
|
@@ -5426,7 +6335,7 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
5426
6335
|
if (!artifactArgs[0]) {
|
|
5427
6336
|
return <ErrorMessage error="Job ID required: model artifacts upload <job-id> --to hf --repo <repo>" />;
|
|
5428
6337
|
}
|
|
5429
|
-
const
|
|
6338
|
+
const rawJobId = artifactArgs[0];
|
|
5430
6339
|
const repo = flags["repo"];
|
|
5431
6340
|
const hfTokenFlag = flags["hf-token"];
|
|
5432
6341
|
const isPrivate = flags["private"]?.toLowerCase() === "true";
|
|
@@ -5450,15 +6359,20 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
5450
6359
|
}
|
|
5451
6360
|
|
|
5452
6361
|
return (
|
|
5453
|
-
<
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
6362
|
+
<WithResolvedJobId
|
|
6363
|
+
rawId={rawJobId}
|
|
6364
|
+
render={(jobId) => (
|
|
6365
|
+
<ApiCommand
|
|
6366
|
+
action={() =>
|
|
6367
|
+
api.pushModelToHub(jobId, {
|
|
6368
|
+
hf_token: hfToken,
|
|
6369
|
+
repo_id: repo,
|
|
6370
|
+
private: isPrivate,
|
|
6371
|
+
})
|
|
6372
|
+
}
|
|
6373
|
+
successMessage={`Model uploaded to Hugging Face: ${repo}`}
|
|
6374
|
+
/>
|
|
6375
|
+
)}
|
|
5462
6376
|
/>
|
|
5463
6377
|
);
|
|
5464
6378
|
}
|