badgr-cli 1.0.26 → 1.0.27
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/package.json +1 -1
- package/src/commands/models.js +25 -6
- package/src/commands/run.js +14 -69
- package/src/commands/serve.js +1 -1
- package/src/commands/test-run.js +34 -8
- package/src/router.js +16 -12
- package/tests/commands.test.js +3 -32
package/package.json
CHANGED
package/src/commands/models.js
CHANGED
|
@@ -1,19 +1,38 @@
|
|
|
1
|
-
import { listModels } from '../api.js';
|
|
1
|
+
import { listModels, callApi } from '../api.js';
|
|
2
2
|
import { listAll } from '../router.js';
|
|
3
3
|
|
|
4
4
|
export async function modelsCommand(config, chalk) {
|
|
5
|
-
const gpus = listAll();
|
|
6
|
-
|
|
7
5
|
console.log(chalk.bold('\n📦 GPU Options (cheapest first)\n'));
|
|
6
|
+
|
|
7
|
+
let gpus = null;
|
|
8
|
+
if (config.apiKey) {
|
|
9
|
+
try {
|
|
10
|
+
const data = await callApi('/gpus', { apiKey: config.apiKey, baseUrl: config.baseUrl });
|
|
11
|
+
if (data?.gpus?.length) {
|
|
12
|
+
gpus = data.gpus.map(g => ({
|
|
13
|
+
id: g.id,
|
|
14
|
+
name: g.name,
|
|
15
|
+
vramGb: g.vram_gb,
|
|
16
|
+
ratePerHour: g.rate_per_hour,
|
|
17
|
+
})).sort((a, b) => a.ratePerHour - b.ratePerHour);
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
// fallback to local catalog on error
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!gpus) gpus = listAll();
|
|
25
|
+
|
|
8
26
|
console.log(
|
|
9
|
-
` ${'ID'.padEnd(14)} ${'Name'.padEnd(24)} ${'VRAM'.padEnd(8)} Rate/hr`
|
|
27
|
+
` ${'ID'.padEnd(14)} ${'Name'.padEnd(24)} ${'VRAM'.padEnd(8)} Rate/hr (indicative)`
|
|
10
28
|
);
|
|
11
|
-
console.log(` ${'─'.repeat(
|
|
29
|
+
console.log(` ${'─'.repeat(68)}`);
|
|
12
30
|
gpus.forEach(g => {
|
|
13
31
|
console.log(
|
|
14
|
-
` ${chalk.cyan(g.id.padEnd(14))} ${g.name.padEnd(24)} ${`${g.vramGb}GB`.padEnd(8)}
|
|
32
|
+
` ${chalk.cyan(g.id.padEnd(14))} ${g.name.padEnd(24)} ${`${g.vramGb}GB`.padEnd(8)} ~$${g.ratePerHour.toFixed(2)}`
|
|
15
33
|
);
|
|
16
34
|
});
|
|
35
|
+
console.log(chalk.dim(' Actual billing is set at job start — use `badgr run` to see live rates.'));
|
|
17
36
|
|
|
18
37
|
console.log(chalk.bold('\n🤖 LLM Models\n'));
|
|
19
38
|
if (!config.apiKey) {
|
package/src/commands/run.js
CHANGED
|
@@ -89,7 +89,7 @@ async function waitForRunning(config, depId, chalk) {
|
|
|
89
89
|
const TIMEOUT_MS = 5 * 60 * 1000; // 5 min startup grace (image pull + container init)
|
|
90
90
|
const startMs = Date.now();
|
|
91
91
|
const PHASES = [
|
|
92
|
-
{ afterMs: 0, label: ' Starting
|
|
92
|
+
{ afterMs: 0, label: ' Starting container' },
|
|
93
93
|
{ afterMs: 15000, label: ' Pulling image' },
|
|
94
94
|
{ afterMs: 60000, label: ' Starting container' },
|
|
95
95
|
{ afterMs: 180000, label: ' Running command' },
|
|
@@ -285,63 +285,6 @@ async function attachToJob(config, depId, { chalk, maxRuntimeMs = null, maxCost
|
|
|
285
285
|
}
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
-
/**
|
|
289
|
-
* Infer a workload profile name from the command the user wants to run.
|
|
290
|
-
* The backend uses this to enforce a VRAM floor when picking a GPU.
|
|
291
|
-
*
|
|
292
|
-
* Note: \b word boundaries are intentionally avoided for keyword checks
|
|
293
|
-
* because keywords commonly appear inside filenames joined by underscores
|
|
294
|
-
* (e.g. lora_train.py, run_sdxl.py) where `_` is a word character.
|
|
295
|
-
*/
|
|
296
|
-
export function inferWorkload(command) {
|
|
297
|
-
if (!command || command.length === 0) return 'general';
|
|
298
|
-
|
|
299
|
-
const cmd = command.join(' ').toLowerCase();
|
|
300
|
-
|
|
301
|
-
// Trivial one-liner / smoke test
|
|
302
|
-
if (/print\s*\(|['"]hello/.test(cmd) && cmd.length < 80) return 'smoke_test';
|
|
303
|
-
|
|
304
|
-
// LoRA / QLoRA / PEFT fine-tuning
|
|
305
|
-
if (/lora|qlora|finetune|fine[_-]tun|peft/.test(cmd)) return 'lora_finetune';
|
|
306
|
-
|
|
307
|
-
// Diffusion / image generation
|
|
308
|
-
if (/diffusion|stable.?diff|sdxl|sd.?xl|comfyui|a1111|invoke|kohya/.test(cmd)) return 'image_gen';
|
|
309
|
-
|
|
310
|
-
// vLLM / TGI / inference server
|
|
311
|
-
if (/vllm|[^a-z]tgi[^a-z]|^tgi\b|tgi$|text.generation.inference/.test(cmd)) return 'inference_small';
|
|
312
|
-
|
|
313
|
-
// Explicit training script
|
|
314
|
-
if (/\btrain\.py\b/.test(cmd)) return 'lora_finetune';
|
|
315
|
-
|
|
316
|
-
return 'general';
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const WORKLOAD_LABELS = {
|
|
320
|
-
smoke_test: 'smoke test',
|
|
321
|
-
general: 'GPU job',
|
|
322
|
-
lora_finetune: 'fine-tuning (40GB+ VRAM)',
|
|
323
|
-
image_gen: 'image generation',
|
|
324
|
-
inference_small: 'inference (7B–8B model)',
|
|
325
|
-
inference_medium: 'inference (30B–34B model)',
|
|
326
|
-
inference_large: 'inference (70B+ model)',
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
// Ask the backend for the best available GPU for a given workload.
|
|
330
|
-
// Routes by: workload → min VRAM → provider tier → cheapest match.
|
|
331
|
-
// Returns { gpu, region, price, workload, workload_desc } or null when nothing is available.
|
|
332
|
-
async function findAutoGpu(config, chalk, tier = '1', workload = 'general') {
|
|
333
|
-
try {
|
|
334
|
-
const params = new URLSearchParams({ max_price: '10', tier, workload });
|
|
335
|
-
return await callApi(`/capacity/auto?${params}`, {
|
|
336
|
-
apiKey: config.apiKey,
|
|
337
|
-
baseUrl: config.baseUrl,
|
|
338
|
-
});
|
|
339
|
-
} catch (err) {
|
|
340
|
-
if (err.errorData?.code === 'NO_CAPACITY_MATCH') return null;
|
|
341
|
-
throw err;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
288
|
function askConfirm(prompt) {
|
|
346
289
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
347
290
|
return new Promise(resolve => rl.question(prompt, ans => { rl.close(); resolve(ans.trim()); }));
|
|
@@ -359,10 +302,10 @@ export async function runCommand(config, args, chalk) {
|
|
|
359
302
|
requireApiKey(config);
|
|
360
303
|
|
|
361
304
|
const command = positional.length > 0 ? positional : undefined;
|
|
362
|
-
// Use alpine for
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
305
|
+
// Use alpine for trivial one-liners (7MB vs 50MB — much faster pull)
|
|
306
|
+
const cmdStr = command ? command.join(' ') : '';
|
|
307
|
+
const isSmoke = cmdStr.length < 80 && /print\s*\(|['"]hello/i.test(cmdStr);
|
|
308
|
+
const inferredImage = isSmoke ? 'python:3.11-alpine' : 'python:3.11-slim';
|
|
366
309
|
const image = flags.image || (command ? inferredImage : undefined);
|
|
367
310
|
const detach = flags.detach || false;
|
|
368
311
|
const fallbackMode = flags.noFallback ? 'none' : (flags.fallback || 'closest');
|
|
@@ -377,12 +320,8 @@ export async function runCommand(config, args, chalk) {
|
|
|
377
320
|
// ── Auto GPU selection (no --gpu specified) ────────────────────────────────
|
|
378
321
|
let gpu = flags.gpu ? flags.gpu.toUpperCase().replace('-', '_') : 'auto';
|
|
379
322
|
|
|
380
|
-
// Infer workload from the command so the backend can apply the correct VRAM floor.
|
|
381
|
-
const workload = command ? inferWorkload(command) : 'general';
|
|
382
|
-
const workloadLabel = WORKLOAD_LABELS[workload] || 'GPU job';
|
|
383
|
-
|
|
384
323
|
if (gpu === 'auto') {
|
|
385
|
-
console.log(chalk.bold(
|
|
324
|
+
console.log(chalk.bold('\nâš¡ Running GPU job\n'));
|
|
386
325
|
if (command) console.log(` ${chalk.bold('Command:')} ${command.join(' ')}`);
|
|
387
326
|
if (image) console.log(` ${chalk.bold('Image:')} ${image}`);
|
|
388
327
|
if (effectiveTier === '2') console.log(` ${chalk.dim('(tier 2 — marketplace routing)')}`)
|
|
@@ -404,7 +343,7 @@ export async function runCommand(config, args, chalk) {
|
|
|
404
343
|
}
|
|
405
344
|
}
|
|
406
345
|
|
|
407
|
-
console.log(chalk.dim('
|
|
346
|
+
console.log(chalk.dim(' Finding reliable capacity...'));
|
|
408
347
|
if (process.env.BADGR_DEBUG === '1' || process.env.BADGR_DEBUG === 'true') {
|
|
409
348
|
console.log(chalk.dim(` API: ${config.baseUrl}`));
|
|
410
349
|
}
|
|
@@ -517,6 +456,7 @@ export async function runCommand(config, args, chalk) {
|
|
|
517
456
|
console.log();
|
|
518
457
|
console.log(chalk.bold(` Job ID: ${chalk.cyan(dep.deployment_id)}`));
|
|
519
458
|
console.log(` ${chalk.bold('GPU:')} ${dep.gpu_type} × ${dep.gpu_count}`);
|
|
459
|
+
if (dep.workload_desc) console.log(` ${chalk.bold('Workload:')} ${dep.workload_desc}`);
|
|
520
460
|
if (dep.tier) console.log(` ${chalk.bold('Tier:')} ${dep.tier}`);
|
|
521
461
|
if (dep.cost_per_hour > 0) console.log(` ${chalk.bold('Rate:')} $${dep.cost_per_hour.toFixed(2)}/hr`);
|
|
522
462
|
console.log(` ${chalk.bold('Receipt:')} ${chalk.dim(rcptId)}`);
|
|
@@ -607,6 +547,11 @@ export async function runCommand(config, args, chalk) {
|
|
|
607
547
|
console.log();
|
|
608
548
|
process.exit(exitCode ?? 1);
|
|
609
549
|
} else if ((finalStatus === 'completed' || finalStatus === 'succeeded') && (exitCode === 0 || exitCode === null)) {
|
|
610
|
-
|
|
550
|
+
try {
|
|
551
|
+
await terminateDeployment(config, dep.deployment_id);
|
|
552
|
+
} catch { /* already stopped */ }
|
|
553
|
+
console.log(chalk.green(`\n ✓ Complete`));
|
|
554
|
+
console.log(chalk.dim(` Billing ended`));
|
|
555
|
+
console.log();
|
|
611
556
|
}
|
|
612
557
|
}
|
package/src/commands/serve.js
CHANGED
|
@@ -84,7 +84,7 @@ export async function serveCommand(config, args, chalk) {
|
|
|
84
84
|
console.log(` ${chalk.bold('GPU:')} ${gpuLabel}`);
|
|
85
85
|
if (effectiveTier === '2') console.log(` ${chalk.dim('(tier 2 — marketplace routing)')}`);
|
|
86
86
|
console.log();
|
|
87
|
-
process.stdout.write(chalk.dim(' Finding
|
|
87
|
+
process.stdout.write(chalk.dim(' Finding reliable capacity...\n'));
|
|
88
88
|
|
|
89
89
|
const effectiveRegion = flags.region ? flags.region.toUpperCase() : undefined;
|
|
90
90
|
|
package/src/commands/test-run.js
CHANGED
|
@@ -2,8 +2,8 @@ import { requireApiKey } from '../config.js';
|
|
|
2
2
|
import { callApi, terminateDeployment } from '../api.js';
|
|
3
3
|
import { addReceipt, generateReceiptId } from '../store.js';
|
|
4
4
|
|
|
5
|
-
// max $
|
|
6
|
-
const TEST_MAX_PRICE =
|
|
5
|
+
// max $0.80/hr × 2 min ≈ $0.027 total spend cap (smoke / Modal T4 tier)
|
|
6
|
+
const TEST_MAX_PRICE = 0.80;
|
|
7
7
|
const TEST_MAX_RUNTIME_MS = 2 * 60 * 1000;
|
|
8
8
|
const TEST_COMMAND = ['python', '-c', "print('hello from badgr')"];
|
|
9
9
|
// Use alpine (7MB) instead of slim (50MB) — dramatically faster image pull for smoke tests.
|
|
@@ -20,6 +20,7 @@ export function parseTestArgs(args) {
|
|
|
20
20
|
let i = 0;
|
|
21
21
|
while (i < args.length) {
|
|
22
22
|
if (args[i] === '--provider' && args[i + 1]) { flags.provider = args[++i]; i++; continue; }
|
|
23
|
+
if (args[i] === '--no-tier-fallback') { flags.noTierFallback = true; i++; continue; }
|
|
23
24
|
i++;
|
|
24
25
|
}
|
|
25
26
|
return flags;
|
|
@@ -46,20 +47,36 @@ async function pollStatus(config, depId, targetStatuses, timeoutMs) {
|
|
|
46
47
|
return null;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
async function
|
|
50
|
+
async function pollOutputOrDone(config, depId, expected, timeoutMs) {
|
|
50
51
|
const deadline = Date.now() + timeoutMs;
|
|
51
52
|
while (Date.now() < deadline) {
|
|
52
|
-
await new Promise(r => setTimeout(r,
|
|
53
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
53
54
|
try {
|
|
55
|
+
const dep = await callApi(`/deployments/${depId}`, {
|
|
56
|
+
apiKey: config.apiKey,
|
|
57
|
+
baseUrl: config.baseUrl,
|
|
58
|
+
});
|
|
59
|
+
if (dep.status === 'succeeded' || dep.status === 'failed') {
|
|
60
|
+
const data = await callApi(`/deployments/${depId}/logs`, {
|
|
61
|
+
apiKey: config.apiKey,
|
|
62
|
+
baseUrl: config.baseUrl,
|
|
63
|
+
});
|
|
64
|
+
const lines = data?.logs ?? [];
|
|
65
|
+
return {
|
|
66
|
+
done: true,
|
|
67
|
+
ok: dep.status === 'succeeded' && lines.some(l => l.includes(expected)),
|
|
68
|
+
exitCode: dep.exit_code,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
54
71
|
const data = await callApi(`/deployments/${depId}/logs`, {
|
|
55
72
|
apiKey: config.apiKey,
|
|
56
73
|
baseUrl: config.baseUrl,
|
|
57
74
|
});
|
|
58
75
|
const lines = data?.logs ?? [];
|
|
59
|
-
if (lines.some(l => l.includes(expected))) return true;
|
|
76
|
+
if (lines.some(l => l.includes(expected))) return { done: true, ok: true, exitCode: 0 };
|
|
60
77
|
} catch { /* retry */ }
|
|
61
78
|
}
|
|
62
|
-
return false;
|
|
79
|
+
return { done: false, ok: false, exitCode: null };
|
|
63
80
|
}
|
|
64
81
|
|
|
65
82
|
export async function testCommand(config, args, chalk) {
|
|
@@ -119,7 +136,7 @@ export async function testCommand(config, args, chalk) {
|
|
|
119
136
|
body: { ...baseBody, tier },
|
|
120
137
|
});
|
|
121
138
|
} catch (err) {
|
|
122
|
-
if (err.errorData?.code === 'NO_CAPACITY_MATCH' && tier === '1') {
|
|
139
|
+
if (err.errorData?.code === 'NO_CAPACITY_MATCH' && tier === '1' && !flags.noTierFallback) {
|
|
123
140
|
process.stdout.write('\n');
|
|
124
141
|
process.stdout.write(chalk.dim(' No tier 1 capacity — trying tier 2 marketplace routing...'));
|
|
125
142
|
try {
|
|
@@ -136,6 +153,12 @@ export async function testCommand(config, args, chalk) {
|
|
|
136
153
|
console.error(chalk.red(' Test failed — no GPU capacity available on any provider.\n'));
|
|
137
154
|
process.exit(1);
|
|
138
155
|
}
|
|
156
|
+
} else if (err.errorData?.code === 'NO_CAPACITY_MATCH' && flags.noTierFallback) {
|
|
157
|
+
process.stdout.write('\n');
|
|
158
|
+
step(chalk, false, 'Provisioned', 'no tier 1 capacity');
|
|
159
|
+
console.log();
|
|
160
|
+
console.error(chalk.red(' Test failed — no tier 1 capacity (strict mode, no tier 2 fallback).\n'));
|
|
161
|
+
process.exit(1);
|
|
139
162
|
} else {
|
|
140
163
|
process.stdout.write('\n');
|
|
141
164
|
step(chalk, false, 'Provisioned', err.message);
|
|
@@ -168,10 +191,13 @@ export async function testCommand(config, args, chalk) {
|
|
|
168
191
|
|
|
169
192
|
// ── 3. Command output ────────────────────────────────────────────────────
|
|
170
193
|
process.stdout.write(chalk.dim(' Checking command output...'));
|
|
171
|
-
const
|
|
194
|
+
const outputResult = await pollOutputOrDone(config, depId, EXPECTED_OUTPUT, 90_000);
|
|
172
195
|
process.stdout.write('\n');
|
|
196
|
+
const gotOutput = outputResult.ok;
|
|
173
197
|
if (gotOutput) {
|
|
174
198
|
step(chalk, true, 'Command printed output');
|
|
199
|
+
} else if (outputResult.done && outputResult.exitCode !== 0) {
|
|
200
|
+
step(chalk, false, 'Command printed output', `exit ${outputResult.exitCode}`);
|
|
175
201
|
} else {
|
|
176
202
|
step(chalk, false, 'Command printed output', 'not found in logs (logs may be buffered)');
|
|
177
203
|
}
|
package/src/router.js
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* GPU catalog — static
|
|
2
|
+
* GPU catalog — static display info only (name, VRAM, tags, indicative rate).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* For live
|
|
7
|
-
*
|
|
4
|
+
* ratePerHour values are indicative market averages for reference display.
|
|
5
|
+
* Actual billing is always set by the backend and returned as cost_per_hour
|
|
6
|
+
* on every deployment response. For live rates, use GET /v1/gpus.
|
|
7
|
+
*
|
|
8
|
+
* Provider routing logic lives entirely in the backend.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
export const GPU_CATALOG = [
|
|
11
|
-
{ id: 'rtx-3080', canonical: 'RTX_3080',
|
|
12
|
-
{ id: 'rtx-
|
|
13
|
-
{ id: '
|
|
14
|
-
{ id: '
|
|
15
|
-
{ id: '
|
|
16
|
-
{ id: '
|
|
17
|
-
{ id: '
|
|
12
|
+
{ id: 'rtx-3080', canonical: 'RTX_3080', name: 'NVIDIA RTX 3080', vramGb: 10, ratePerHour: 0.35, tags: ['inference', 'dev'] },
|
|
13
|
+
{ id: 'rtx-3090', canonical: 'RTX_3090', name: 'NVIDIA RTX 3090', vramGb: 24, ratePerHour: 0.60, tags: ['inference', 'dev'] },
|
|
14
|
+
{ id: 'rtx-4090', canonical: 'RTX_4090', name: 'NVIDIA RTX 4090', vramGb: 24, ratePerHour: 1.10, tags: ['inference', 'training', 'dev'] },
|
|
15
|
+
{ id: 'a4000', canonical: 'A4000', name: 'NVIDIA RTX A4000', vramGb: 16, ratePerHour: 0.50, tags: ['inference', 'dev'] },
|
|
16
|
+
{ id: 'a5000', canonical: 'A5000', name: 'NVIDIA RTX A5000', vramGb: 24, ratePerHour: 0.70, tags: ['inference', 'dev'] },
|
|
17
|
+
{ id: 'l40s', canonical: 'L40S', name: 'NVIDIA L40S', vramGb: 48, ratePerHour: 1.40, tags: ['inference', 'training'] },
|
|
18
|
+
{ id: 'a6000', canonical: 'A6000', name: 'NVIDIA RTX A6000', vramGb: 48, ratePerHour: 1.60, tags: ['inference', 'training'] },
|
|
19
|
+
{ id: 'a100-40gb', canonical: 'A100', name: 'NVIDIA A100 40GB', vramGb: 40, ratePerHour: 1.80, tags: ['training', 'inference'] },
|
|
20
|
+
{ id: 'a100-80gb', canonical: 'A100_80GB', name: 'NVIDIA A100 80GB', vramGb: 80, ratePerHour: 2.50, tags: ['training', 'large-model'] },
|
|
21
|
+
{ id: 'h100', canonical: 'H100', name: 'NVIDIA H100 80GB', vramGb: 80, ratePerHour: 3.50, tags: ['training', 'large-model'] },
|
|
18
22
|
];
|
|
19
23
|
|
|
20
24
|
export function findById(id) {
|
package/tests/commands.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { parseRunArgs, classifyFailure
|
|
2
|
+
import { parseRunArgs, classifyFailure } from '../src/commands/run.js';
|
|
3
3
|
import { parseServeArgs } from '../src/commands/serve.js';
|
|
4
4
|
import { testCommand, parseTestArgs } from '../src/commands/test-run.js';
|
|
5
5
|
import { rankAlternatives, diffDescription, promptFallback } from '../src/fallback.js';
|
|
@@ -232,38 +232,9 @@ describe('parseTestArgs', () => {
|
|
|
232
232
|
it('parses --provider secondary', () => {
|
|
233
233
|
expect(parseTestArgs(['--provider', 'secondary'])).toEqual({ provider: 'secondary' });
|
|
234
234
|
});
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
describe('inferWorkload', () => {
|
|
238
|
-
it('returns general for empty command', () => {
|
|
239
|
-
expect(inferWorkload([])).toBe('general');
|
|
240
|
-
expect(inferWorkload(null)).toBe('general');
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('returns smoke_test for short print() one-liners', () => {
|
|
244
|
-
expect(inferWorkload(['python', '-c', "print('hello')"])).toBe('smoke_test');
|
|
245
|
-
expect(inferWorkload(['python', '-c', "print('hello from badgr')"])).toBe('smoke_test');
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it('returns general for a typical script', () => {
|
|
249
|
-
expect(inferWorkload(['python', 'script.py'])).toBe('general');
|
|
250
|
-
expect(inferWorkload(['python', 'run.py', '--epochs', '10'])).toBe('general');
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('returns lora_finetune for LoRA/fine-tuning keywords', () => {
|
|
254
|
-
expect(inferWorkload(['python', 'lora_train.py'])).toBe('lora_finetune');
|
|
255
|
-
expect(inferWorkload(['python', '-c', 'import peft; finetune()'])).toBe('lora_finetune');
|
|
256
|
-
expect(inferWorkload(['python', 'train.py', '--epochs', '3'])).toBe('lora_finetune');
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it('returns image_gen for diffusion keywords', () => {
|
|
260
|
-
expect(inferWorkload(['python', 'stable_diff.py'])).toBe('image_gen');
|
|
261
|
-
expect(inferWorkload(['python', 'run_sdxl.py'])).toBe('image_gen');
|
|
262
|
-
});
|
|
263
235
|
|
|
264
|
-
it('
|
|
265
|
-
expect(
|
|
266
|
-
expect(inferWorkload(['python', 'serve_tgi.py'])).toBe('inference_small');
|
|
236
|
+
it('parses --no-tier-fallback', () => {
|
|
237
|
+
expect(parseTestArgs(['--no-tier-fallback'])).toEqual({ noTierFallback: true });
|
|
267
238
|
});
|
|
268
239
|
});
|
|
269
240
|
|