badgr-cli 1.0.43 → 1.0.45
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 +180 -230
- package/package.json +1 -1
- package/src/badgr.js +12 -0
- package/src/catalog.js +46 -0
- package/src/commands/comfyui.js +70 -56
- package/src/commands/detect.js +58 -0
- package/src/commands/receipts.js +39 -2
- package/src/commands/run.js +150 -68
- package/src/commands/serve.js +278 -115
- package/src/commands/train.js +22 -27
- package/src/detect.js +362 -0
- package/src/progress.js +202 -0
- package/src/store.js +11 -0
- package/tests/detect.test.js +191 -0
- package/tests/job-progress-poll.test.js +136 -0
- package/tests/productized-runners.test.js +7 -0
- package/tests/run-lifecycle.test.js +111 -1
- package/tests/serve-apps.test.js +189 -0
- package/tests/serve-lifecycle.test.js +116 -2
- package/tests/store.test.js +22 -1
- package/tests/template.test.js +4 -4
- package/tests/workload-templates.test.js +22 -0
|
@@ -239,6 +239,27 @@ describe('deployment fails during startup', () => {
|
|
|
239
239
|
const errLines = console.error.mock.calls.flat().join('\n');
|
|
240
240
|
expect(errLines).toContain('likely an OOM');
|
|
241
241
|
});
|
|
242
|
+
|
|
243
|
+
it('prints failure_class/next_action from the backend using the shared vocabulary', async () => {
|
|
244
|
+
api.callApi
|
|
245
|
+
.mockResolvedValueOnce(makeServeDep())
|
|
246
|
+
.mockResolvedValueOnce({ status: 'running' })
|
|
247
|
+
.mockResolvedValueOnce({
|
|
248
|
+
status: 'failed',
|
|
249
|
+
error: 'pod_exited',
|
|
250
|
+
fix_hint: 'The pod stopped running before the endpoint ever responded.',
|
|
251
|
+
failure_class: 'container_start_failed',
|
|
252
|
+
next_action: 'Check the image and command, then retry.',
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const p = serveCommand(config, ['meta-llama/Llama-3.1-8B-Instruct', '--max-cost', '5'], chalk);
|
|
256
|
+
await vi.advanceTimersByTimeAsync(10000);
|
|
257
|
+
await p;
|
|
258
|
+
|
|
259
|
+
const errLines = console.error.mock.calls.flat().join('\n');
|
|
260
|
+
expect(errLines).toContain('Class: container_start_failed');
|
|
261
|
+
expect(errLines).toContain('Next: Check the image and command, then retry.');
|
|
262
|
+
});
|
|
242
263
|
});
|
|
243
264
|
|
|
244
265
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -510,13 +531,106 @@ describe('input validation for serve', () => {
|
|
|
510
531
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
511
532
|
|
|
512
533
|
describe('--list-aliases', () => {
|
|
513
|
-
it('prints
|
|
534
|
+
it('prints tested model routes without requiring a model or API call', async () => {
|
|
514
535
|
await serveCommand({ apiKey: null, baseUrl: 'https://api.test/v1' }, ['--list-aliases'], chalk);
|
|
515
536
|
expect(api.callApi).not.toHaveBeenCalled();
|
|
516
537
|
expect(process.exitCode).toBeFalsy();
|
|
517
538
|
const logged = console.log.mock.calls.flat().join('\n');
|
|
539
|
+
expect(logged).toContain('Tested model routes:');
|
|
518
540
|
expect(logged).toContain('qwen-7b');
|
|
519
|
-
expect(logged).toContain('
|
|
541
|
+
expect(logged).toContain('You can also try a Hugging Face model ID:');
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
546
|
+
// Model support-level messaging — blessed route vs best-effort HF model vs
|
|
547
|
+
// custom container, plus gated-model HF_TOKEN guidance shown only on failure.
|
|
548
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
549
|
+
|
|
550
|
+
describe('model support-level messaging', () => {
|
|
551
|
+
it('labels a full Hugging Face model ID as best-effort, not tested', async () => {
|
|
552
|
+
api.callApi
|
|
553
|
+
.mockResolvedValueOnce(makeServeDep({ model: 'Qwen/Qwen2.5-7B-Instruct' }))
|
|
554
|
+
.mockResolvedValueOnce({ status: 'running' })
|
|
555
|
+
.mockResolvedValueOnce(readyStatus());
|
|
556
|
+
|
|
557
|
+
const p = serveCommand(config, ['Qwen/Qwen2.5-7B-Instruct', '--max-cost', '10'], chalk);
|
|
558
|
+
await vi.advanceTimersByTimeAsync(5000);
|
|
559
|
+
await p;
|
|
560
|
+
|
|
561
|
+
const logged = console.log.mock.calls.flat().join('\n');
|
|
562
|
+
expect(logged).toContain('Route: best-effort Hugging Face model');
|
|
563
|
+
expect(logged).not.toContain('Route: tested');
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('labels a blessed alias as a tested route without extra caveats', async () => {
|
|
567
|
+
api.callApi
|
|
568
|
+
.mockResolvedValueOnce(makeServeDep({ model: 'Qwen/Qwen2.5-7B-Instruct' }))
|
|
569
|
+
.mockResolvedValueOnce({ status: 'running' })
|
|
570
|
+
.mockResolvedValueOnce(readyStatus());
|
|
571
|
+
|
|
572
|
+
const p = serveCommand(config, ['qwen-7b', '--max-cost', '10'], chalk);
|
|
573
|
+
await vi.advanceTimersByTimeAsync(5000);
|
|
574
|
+
await p;
|
|
575
|
+
|
|
576
|
+
const logged = console.log.mock.calls.flat().join('\n');
|
|
577
|
+
expect(logged).toContain('Route: tested');
|
|
578
|
+
expect(logged).not.toContain('best-effort');
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('does not show HF_TOKEN guidance up front for a gated model that launches fine', async () => {
|
|
582
|
+
api.callApi
|
|
583
|
+
.mockResolvedValueOnce(makeServeDep({ model: 'meta-llama/Llama-3.1-8B-Instruct' }))
|
|
584
|
+
.mockResolvedValueOnce({ status: 'running' })
|
|
585
|
+
.mockResolvedValueOnce(readyStatus());
|
|
586
|
+
|
|
587
|
+
const p = serveCommand(config, ['meta-llama/Llama-3.1-8B-Instruct', '--max-cost', '10'], chalk);
|
|
588
|
+
await vi.advanceTimersByTimeAsync(5000);
|
|
589
|
+
await p;
|
|
590
|
+
|
|
591
|
+
const logged = console.log.mock.calls.flat().join('\n');
|
|
592
|
+
expect(logged).not.toContain('may require Hugging Face access');
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it('shows HF_TOKEN guidance only once a gated model actually fails to start', async () => {
|
|
596
|
+
api.callApi
|
|
597
|
+
.mockResolvedValueOnce(makeServeDep({ model: 'meta-llama/Llama-3.1-8B-Instruct' }))
|
|
598
|
+
.mockResolvedValueOnce({ status: 'failed', error: 'gated repo — 401' });
|
|
599
|
+
|
|
600
|
+
const p = serveCommand(config, ['meta-llama/Llama-3.1-8B-Instruct', '--max-cost', '10'], chalk);
|
|
601
|
+
await p;
|
|
602
|
+
|
|
603
|
+
const logged = console.error.mock.calls.flat().join('\n');
|
|
604
|
+
expect(logged).toContain('may require Hugging Face access');
|
|
605
|
+
expect(logged).toContain('--env HF_TOKEN=$HF_TOKEN');
|
|
606
|
+
expect(process.exitCode).toBe(1);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it('omits HF_TOKEN guidance on failure when HF_TOKEN is already provided', async () => {
|
|
610
|
+
api.callApi
|
|
611
|
+
.mockResolvedValueOnce(makeServeDep({ model: 'meta-llama/Llama-3.1-8B-Instruct' }))
|
|
612
|
+
.mockResolvedValueOnce({ status: 'failed', error: 'crashed' });
|
|
613
|
+
|
|
614
|
+
const p = serveCommand(config, [
|
|
615
|
+
'meta-llama/Llama-3.1-8B-Instruct', '--max-cost', '10', '--env', 'HF_TOKEN=hf_abc123',
|
|
616
|
+
], chalk);
|
|
617
|
+
await p;
|
|
618
|
+
|
|
619
|
+
const logged = console.error.mock.calls.flat().join('\n');
|
|
620
|
+
expect(logged).not.toContain('may require Hugging Face access');
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('labels a custom container as "custom server", not a Hugging Face model', async () => {
|
|
624
|
+
api.callApi.mockResolvedValueOnce(makeServeDep({ model: undefined, image: 'my/custom-server:latest' }));
|
|
625
|
+
|
|
626
|
+
const p = serveCommand(config, [
|
|
627
|
+
'--image', 'my/custom-server:latest', '--max-cost', '10', '--no-wait',
|
|
628
|
+
], chalk);
|
|
629
|
+
await p;
|
|
630
|
+
|
|
631
|
+
const logged = console.log.mock.calls.flat().join('\n');
|
|
632
|
+
expect(logged).toContain('custom server');
|
|
633
|
+
expect(logged).toContain('Your container owns the app behavior.');
|
|
520
634
|
});
|
|
521
635
|
});
|
|
522
636
|
|
package/tests/store.test.js
CHANGED
|
@@ -5,7 +5,7 @@ import { rmSync, existsSync } from 'fs';
|
|
|
5
5
|
import {
|
|
6
6
|
loadStore, saveStore,
|
|
7
7
|
addDeployment, updateDeployment, removeDeployment, findDeployment, listDeployments,
|
|
8
|
-
addReceipt, updateReceipt, listReceipts,
|
|
8
|
+
addReceipt, updateReceipt, listReceipts, findReceipt,
|
|
9
9
|
generateDeploymentId, generateReceiptId,
|
|
10
10
|
} from '../src/store.js';
|
|
11
11
|
|
|
@@ -125,6 +125,27 @@ describe('addReceipt / listReceipts', () => {
|
|
|
125
125
|
});
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
+
describe('findReceipt', () => {
|
|
129
|
+
// comfy.batch / train.lora mint a local rcpt- id that points at a job_id —
|
|
130
|
+
// `badgr receipts <id>` needs to resolve either the receipt id itself or
|
|
131
|
+
// the job_id back to the same local record (see receipts.js).
|
|
132
|
+
it('finds a receipt by its own receiptId', () => {
|
|
133
|
+
addReceipt({ receiptId: 'rcpt-1', type: 'comfy.batch', job_id: 'job_abc' }, file);
|
|
134
|
+
const found = findReceipt('rcpt-1', file);
|
|
135
|
+
expect(found?.job_id).toBe('job_abc');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('finds a receipt by job_id', () => {
|
|
139
|
+
addReceipt({ receiptId: 'rcpt-2', type: 'train.lora', job_id: 'job_def' }, file);
|
|
140
|
+
const found = findReceipt('job_def', file);
|
|
141
|
+
expect(found?.receiptId).toBe('rcpt-2');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('returns null when nothing matches', () => {
|
|
145
|
+
expect(findReceipt('nope', file)).toBeNull();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
128
149
|
describe('updateReceipt', () => {
|
|
129
150
|
it('merges updates into an existing receipt', () => {
|
|
130
151
|
addReceipt({ receiptId: 'r-upd', action: 'badgr run', status: 'running' }, file);
|
package/tests/template.test.js
CHANGED
|
@@ -105,14 +105,14 @@ afterEach(() => {
|
|
|
105
105
|
|
|
106
106
|
describe('TEMPLATES catalog', () => {
|
|
107
107
|
const EXPECTED_NAMES = [
|
|
108
|
-
'comfyui', 'axolotl', 'batch-inference', 'unsloth', 'vllm', 'llama-cpp',
|
|
108
|
+
'comfyui', 'axolotl', 'batch-inference', 'unsloth', 'vllm', 'openwebui', 'llama-cpp',
|
|
109
109
|
'invokeai', 'kohya-ss', 'text-gen-webui', 'sglang', 'tgi',
|
|
110
110
|
'auto1111', 'forge', 'nerfstudio', 'openfold', 'blender-render',
|
|
111
111
|
'openmm', 'gromacs', 'lammps', 'diffusers', 'torchtune',
|
|
112
112
|
];
|
|
113
113
|
|
|
114
|
-
it('contains exactly
|
|
115
|
-
expect(TEMPLATES).toHaveLength(
|
|
114
|
+
it('contains exactly 22 templates', () => {
|
|
115
|
+
expect(TEMPLATES).toHaveLength(22);
|
|
116
116
|
});
|
|
117
117
|
|
|
118
118
|
it('contains all expected template names', () => {
|
|
@@ -180,7 +180,7 @@ describe('TEMPLATES catalog', () => {
|
|
|
180
180
|
for (const [k, t] of Object.entries(TEMPLATE_MAP)) {
|
|
181
181
|
expect(k).toBe(t.name);
|
|
182
182
|
}
|
|
183
|
-
expect(Object.keys(TEMPLATE_MAP)).toHaveLength(
|
|
183
|
+
expect(Object.keys(TEMPLATE_MAP)).toHaveLength(22);
|
|
184
184
|
});
|
|
185
185
|
});
|
|
186
186
|
|
|
@@ -300,6 +300,28 @@ describe('comfyuiCommand', () => {
|
|
|
300
300
|
}));
|
|
301
301
|
expect(process.exitCode).toBe(1);
|
|
302
302
|
});
|
|
303
|
+
|
|
304
|
+
it('prints failure_class/next_action from the backend using the shared vocabulary', async () => {
|
|
305
|
+
fs.existsSync.mockReturnValue(true);
|
|
306
|
+
fs.readFileSync.mockReturnValue(JSON.stringify({ '1': {} }));
|
|
307
|
+
fallback.callWithFallback.mockResolvedValue(makeServeDep());
|
|
308
|
+
|
|
309
|
+
api.callApi.mockResolvedValue({
|
|
310
|
+
status: 'failed',
|
|
311
|
+
error: 'OOM',
|
|
312
|
+
failure_class: 'container_start_failed',
|
|
313
|
+
next_action: 'Check the image and command, then retry.',
|
|
314
|
+
});
|
|
315
|
+
global.fetch = vi.fn().mockRejectedValue(new Error('Connection refused'));
|
|
316
|
+
|
|
317
|
+
const p = comfyuiCommand(config, ['wf.json', '--max-cost', '5'], chalk);
|
|
318
|
+
await vi.advanceTimersByTimeAsync(10000);
|
|
319
|
+
await p;
|
|
320
|
+
|
|
321
|
+
const errLines = console.error.mock.calls.flat().join('\n');
|
|
322
|
+
expect(errLines).toContain('Class: container_start_failed');
|
|
323
|
+
expect(errLines).toContain('Next: Check the image and command, then retry.');
|
|
324
|
+
});
|
|
303
325
|
});
|
|
304
326
|
|
|
305
327
|
// ─────────────────────────────────────────────────────────────────────────────
|