badgr-cli 1.0.4 → 1.0.7
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/api.js +44 -11
- package/src/badgr.js +7 -1
- package/src/commands/login.js +4 -5
- package/src/commands/run.js +18 -4
- package/src/commands/serve.js +4 -2
- package/src/config.js +49 -4
- package/tests/config.test.js +24 -1
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
|
+
const DEBUG = process.env.BADGR_DEBUG === '1' || process.env.BADGR_DEBUG === 'true';
|
|
2
|
+
|
|
3
|
+
function dbg(...args) {
|
|
4
|
+
if (DEBUG) console.error('[badgr:debug]', ...args);
|
|
5
|
+
}
|
|
6
|
+
|
|
1
7
|
export async function callApi(path, { method = 'GET', apiKey, baseUrl, body } = {}) {
|
|
2
8
|
const url = `${baseUrl}${path}`;
|
|
9
|
+
const keyPreview = apiKey ? `${apiKey.slice(0, 8)}…` : '(not set)';
|
|
10
|
+
|
|
11
|
+
dbg(`${method} ${url}`);
|
|
12
|
+
dbg(`API key: ${keyPreview}`);
|
|
13
|
+
if (body !== undefined) dbg('Request body:', JSON.stringify(body));
|
|
14
|
+
|
|
3
15
|
let res;
|
|
16
|
+
const startMs = Date.now();
|
|
4
17
|
try {
|
|
5
18
|
res = await fetch(url, {
|
|
6
19
|
method,
|
|
@@ -11,32 +24,52 @@ export async function callApi(path, { method = 'GET', apiKey, baseUrl, body } =
|
|
|
11
24
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
12
25
|
});
|
|
13
26
|
} catch (cause) {
|
|
27
|
+
const elapsed = Date.now() - startMs;
|
|
28
|
+
dbg(`Fetch threw after ${elapsed}ms:`, cause);
|
|
14
29
|
// Network-level failure: DNS, connection refused, timeout, etc.
|
|
15
30
|
const msg = cause?.message ?? String(cause);
|
|
31
|
+
const code = cause?.cause?.code ?? cause?.code ?? '';
|
|
16
32
|
const hint =
|
|
17
|
-
msg.includes('ECONNREFUSED')
|
|
18
|
-
|
|
19
|
-
msg.includes('
|
|
20
|
-
|
|
33
|
+
(code === 'ECONNREFUSED' || msg.includes('ECONNREFUSED'))
|
|
34
|
+
? `\n Hint: Connection refused — is the server running at ${baseUrl}?` :
|
|
35
|
+
(code === 'ENOTFOUND' || msg.includes('ENOTFOUND'))
|
|
36
|
+
? `\n Hint: DNS lookup failed for ${baseUrl}\n Check your internet or set BADGR_API_URL to the correct host` :
|
|
37
|
+
(code === 'ETIMEDOUT' || msg.includes('ETIMEDOUT'))
|
|
38
|
+
? `\n Hint: Request timed out — server may be overloaded` :
|
|
39
|
+
(msg.includes('fetch failed') || msg === 'fetch failed')
|
|
40
|
+
? `\n Hint: Network error reaching ${url}\n • Check internet connection\n • Run: badgr config (verify baseUrl)\n • Try: BADGR_DEBUG=1 badgr run … for full details\n • Test: curl -v ${baseUrl}/models` :
|
|
41
|
+
`\n Hint: Check your network and that BADGR_API_URL is correct (${baseUrl})`;
|
|
21
42
|
throw new Error(`Cannot reach ${url} (${msg})${hint}`);
|
|
22
43
|
}
|
|
44
|
+
|
|
45
|
+
const elapsed = Date.now() - startMs;
|
|
46
|
+
dbg(`Response: HTTP ${res.status} in ${elapsed}ms`);
|
|
47
|
+
|
|
23
48
|
if (!res.ok) {
|
|
24
49
|
let detail = '';
|
|
50
|
+
let rawBody = '';
|
|
51
|
+
let errorData = null;
|
|
25
52
|
try {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const json = JSON.parse(
|
|
29
|
-
|
|
53
|
+
rawBody = await res.text();
|
|
54
|
+
dbg('Error response body:', rawBody);
|
|
55
|
+
const json = JSON.parse(rawBody);
|
|
56
|
+
errorData = json;
|
|
57
|
+
detail = json?.detail ?? json?.message ?? json?.error ?? rawBody;
|
|
30
58
|
} catch {
|
|
31
|
-
detail = res.statusText || '';
|
|
59
|
+
detail = rawBody || res.statusText || '';
|
|
32
60
|
}
|
|
61
|
+
const isCapacityError = errorData?.code === 'NO_CAPACITY_MATCH';
|
|
33
62
|
const hint =
|
|
34
63
|
res.status === 401 ? '\n Hint: Invalid or missing API key — run: badgr login' :
|
|
35
64
|
res.status === 403 ? '\n Hint: Access denied — check your API key permissions' :
|
|
36
65
|
res.status === 404 ? `\n Hint: Endpoint not found — check BADGR_API_URL (currently: ${baseUrl})` :
|
|
37
|
-
res.status === 502 || res.status === 503
|
|
66
|
+
(res.status === 502 || res.status === 503) && !isCapacityError
|
|
67
|
+
? '\n Hint: Server error — no GPU capacity available or backend is down' :
|
|
38
68
|
'';
|
|
39
|
-
|
|
69
|
+
const err = new Error(`${method} ${path} → HTTP ${res.status}: ${detail}${hint}`);
|
|
70
|
+
err.errorData = errorData;
|
|
71
|
+
err.httpStatus = res.status;
|
|
72
|
+
throw err;
|
|
40
73
|
}
|
|
41
74
|
return res.json();
|
|
42
75
|
}
|
package/src/badgr.js
CHANGED
|
@@ -52,11 +52,17 @@ ${chalk.bold('EXAMPLES')}
|
|
|
52
52
|
|
|
53
53
|
${chalk.bold('OPENAI-COMPATIBLE SERVING')}
|
|
54
54
|
${chalk.dim('After `badgr serve`, point any OpenAI client at the returned URL:')}
|
|
55
|
-
${chalk.dim(' client = OpenAI(api_key="sk-...", base_url="https://
|
|
55
|
+
${chalk.dim(' client = OpenAI(api_key="sk-...", base_url="https://aibadgr.com/v1")')}
|
|
56
56
|
${chalk.dim(' client.chat.completions.create(model="dep_xxx", messages=[...])')}
|
|
57
57
|
|
|
58
58
|
${chalk.bold('ROUTING')}
|
|
59
59
|
${chalk.dim('Badgr automatically selects best available GPU capacity for your request.')}
|
|
60
|
+
|
|
61
|
+
${chalk.bold('DEBUGGING')}
|
|
62
|
+
${chalk.dim('Set BADGR_DEBUG=1 to print the full URL, API key prefix, request body, and response status:')}
|
|
63
|
+
${chalk.dim(' BADGR_DEBUG=1 badgr run python train.py --gpu A100')}
|
|
64
|
+
${chalk.dim(' BADGR_DEBUG=1 badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S')}
|
|
65
|
+
${chalk.dim(' badgr config # show current baseUrl and key')}
|
|
60
66
|
`;
|
|
61
67
|
|
|
62
68
|
async function main() {
|
package/src/commands/login.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { input } from '@inquirer/prompts';
|
|
2
|
+
import { DEFAULTS } from '../config.js';
|
|
2
3
|
|
|
3
4
|
export async function loginCommand(chalk, saveConfigFn) {
|
|
4
5
|
console.log(chalk.bold('\n🔑 Badgr Login\n'));
|
|
@@ -8,12 +9,10 @@ export async function loginCommand(chalk, saveConfigFn) {
|
|
|
8
9
|
validate: v => v.trim() ? true : 'API key is required',
|
|
9
10
|
});
|
|
10
11
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const config = saveConfigFn({
|
|
13
|
+
apiKey: apiKey.trim(),
|
|
14
|
+
baseUrl: DEFAULTS.baseUrl,
|
|
14
15
|
});
|
|
15
|
-
|
|
16
|
-
const config = saveConfigFn({ apiKey: apiKey.trim(), baseUrl: baseUrl.trim() });
|
|
17
16
|
console.log(chalk.green('\n✓ Logged in — config saved to ~/.badgr/config.json\n'));
|
|
18
17
|
console.log(chalk.dim(` Base URL: ${config.baseUrl}\n`));
|
|
19
18
|
return config;
|
package/src/commands/run.js
CHANGED
|
@@ -100,6 +100,7 @@ export async function runCommand(config, args, chalk) {
|
|
|
100
100
|
if (detach) console.log(` ${chalk.dim('(detached — returns immediately)')}`);
|
|
101
101
|
console.log();
|
|
102
102
|
console.log(chalk.dim(' Finding best available GPU capacity...'));
|
|
103
|
+
console.log(chalk.dim(` API: ${config.baseUrl}`));
|
|
103
104
|
|
|
104
105
|
let dep;
|
|
105
106
|
try {
|
|
@@ -112,15 +113,28 @@ export async function runCommand(config, args, chalk) {
|
|
|
112
113
|
image,
|
|
113
114
|
gpu: gpu.toUpperCase().replace('-', '_'),
|
|
114
115
|
gpu_count: flags.count || 1,
|
|
115
|
-
region: flags.region
|
|
116
|
+
...(flags.region ? { region: flags.region.toUpperCase() } : {}),
|
|
116
117
|
max_price_per_hour: flags.maxPrice,
|
|
117
118
|
name: flags.name,
|
|
118
119
|
},
|
|
119
120
|
});
|
|
120
121
|
} catch (err) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
const d = err.errorData;
|
|
123
|
+
if (d?.code === 'NO_CAPACITY_MATCH') {
|
|
124
|
+
const f = d.filters ?? {};
|
|
125
|
+
const scope = f.region && f.region !== 'any' ? `in ${f.region}` : 'globally';
|
|
126
|
+
console.error(chalk.red(`\n ✗ No ${f.gpu || gpu} found under ${f.max_price || '$10/hr'} ${scope}.`));
|
|
127
|
+
if (d.suggestions?.length) {
|
|
128
|
+
console.error(chalk.dim(`\n Try:`));
|
|
129
|
+
for (const s of d.suggestions) console.error(chalk.dim(` • ${s}`));
|
|
130
|
+
}
|
|
131
|
+
console.error('');
|
|
132
|
+
} else {
|
|
133
|
+
console.error(chalk.red(`\n ✗ Job failed to start: ${err.message}`));
|
|
134
|
+
console.error(chalk.dim(`\n Debug: BADGR_DEBUG=1 badgr run … — shows full request/response`));
|
|
135
|
+
console.error(chalk.dim(` Config: badgr config`));
|
|
136
|
+
console.error(chalk.dim(` Docs: badgr --help\n`));
|
|
137
|
+
}
|
|
124
138
|
process.exit(1);
|
|
125
139
|
}
|
|
126
140
|
|
package/src/commands/serve.js
CHANGED
|
@@ -69,6 +69,7 @@ export async function serveCommand(config, args, chalk) {
|
|
|
69
69
|
console.log(` ${chalk.bold('GPU:')} ${gpu}`);
|
|
70
70
|
console.log();
|
|
71
71
|
console.log(chalk.dim(' Finding best available GPU capacity...'));
|
|
72
|
+
console.log(chalk.dim(` API: ${config.baseUrl}`));
|
|
72
73
|
|
|
73
74
|
let dep;
|
|
74
75
|
try {
|
|
@@ -87,8 +88,9 @@ export async function serveCommand(config, args, chalk) {
|
|
|
87
88
|
});
|
|
88
89
|
} catch (err) {
|
|
89
90
|
console.error(chalk.red(`\n ✗ Serve failed: ${err.message}`));
|
|
90
|
-
console.error(chalk.dim(`\n
|
|
91
|
-
console.error(chalk.dim(` Config: badgr config
|
|
91
|
+
console.error(chalk.dim(`\n Debug: BADGR_DEBUG=1 badgr serve … — shows full request/response`));
|
|
92
|
+
console.error(chalk.dim(` Config: badgr config`));
|
|
93
|
+
console.error(chalk.dim(` Docs: badgr --help\n`));
|
|
92
94
|
return;
|
|
93
95
|
}
|
|
94
96
|
|
package/src/config.js
CHANGED
|
@@ -6,16 +6,61 @@ export const CONFIG_DIR = join(homedir(), '.badgr');
|
|
|
6
6
|
export const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
7
7
|
|
|
8
8
|
export const DEFAULTS = {
|
|
9
|
-
baseUrl: 'https://
|
|
9
|
+
baseUrl: 'https://aibadgr.com/v1',
|
|
10
10
|
defaultModel: 'meta-llama/Llama-3.1-8B-Instruct',
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
/** URLs that do not resolve or are superseded by aibadgr.com/v1 (nginx) */
|
|
14
|
+
const LEGACY_BASE_URLS = new Set([
|
|
15
|
+
'https://api.badgr.ai/v1',
|
|
16
|
+
'https://api.badgr.ai',
|
|
17
|
+
'http://api.badgr.ai/v1',
|
|
18
|
+
'http://api.badgr.ai',
|
|
19
|
+
'https://api.aibadgr.com/v1',
|
|
20
|
+
'https://api.aibadgr.com',
|
|
21
|
+
'http://api.aibadgr.com/v1',
|
|
22
|
+
'http://api.aibadgr.com',
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
export function normalizeBaseUrl(url) {
|
|
26
|
+
if (!url?.trim()) return DEFAULTS.baseUrl;
|
|
27
|
+
const trimmed = url.trim().replace(/\/+$/, '');
|
|
28
|
+
if (LEGACY_BASE_URLS.has(trimmed)) return DEFAULTS.baseUrl;
|
|
29
|
+
return trimmed.endsWith('/v1') ? trimmed : `${trimmed}/v1`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function applyEnvOverrides(config) {
|
|
33
|
+
const envBase = process.env.BADGR_API_URL?.trim();
|
|
34
|
+
if (envBase) config.baseUrl = normalizeBaseUrl(envBase);
|
|
35
|
+
const envKey = process.env.BADGR_API_KEY?.trim();
|
|
36
|
+
if (envKey) config.apiKey = envKey;
|
|
37
|
+
return config;
|
|
38
|
+
}
|
|
39
|
+
|
|
13
40
|
export function loadConfig(configFile = CONFIG_FILE) {
|
|
14
|
-
if (!existsSync(configFile))
|
|
41
|
+
if (!existsSync(configFile)) {
|
|
42
|
+
return applyEnvOverrides({ ...DEFAULTS });
|
|
43
|
+
}
|
|
15
44
|
try {
|
|
16
|
-
|
|
45
|
+
const parsed = JSON.parse(readFileSync(configFile, 'utf8'));
|
|
46
|
+
const previousBase = parsed.baseUrl;
|
|
47
|
+
const config = applyEnvOverrides({
|
|
48
|
+
...DEFAULTS,
|
|
49
|
+
...parsed,
|
|
50
|
+
baseUrl: normalizeBaseUrl(parsed.baseUrl ?? DEFAULTS.baseUrl),
|
|
51
|
+
});
|
|
52
|
+
if (
|
|
53
|
+
configFile === CONFIG_FILE &&
|
|
54
|
+
previousBase &&
|
|
55
|
+
normalizeBaseUrl(previousBase) !== previousBase
|
|
56
|
+
) {
|
|
57
|
+
const merged = { ...parsed, baseUrl: config.baseUrl };
|
|
58
|
+
mkdirSync(dirname(configFile), { recursive: true });
|
|
59
|
+
writeFileSync(configFile, JSON.stringify(merged, null, 2));
|
|
60
|
+
}
|
|
61
|
+
return config;
|
|
17
62
|
} catch {
|
|
18
|
-
return { ...DEFAULTS };
|
|
63
|
+
return applyEnvOverrides({ ...DEFAULTS });
|
|
19
64
|
}
|
|
20
65
|
}
|
|
21
66
|
|
package/tests/config.test.js
CHANGED
|
@@ -2,7 +2,7 @@ import { describe, it, expect, afterEach } from 'vitest';
|
|
|
2
2
|
import { tmpdir } from 'os';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { rmSync, existsSync } from 'fs';
|
|
5
|
-
import { loadConfig, saveConfig, requireApiKey, DEFAULTS } from '../src/config.js';
|
|
5
|
+
import { loadConfig, saveConfig, requireApiKey, normalizeBaseUrl, DEFAULTS } from '../src/config.js';
|
|
6
6
|
|
|
7
7
|
const tmp = join(tmpdir(), `badgr-cli-test-${process.pid}`);
|
|
8
8
|
const testConfigFile = join(tmp, 'config.json');
|
|
@@ -58,6 +58,29 @@ describe('saveConfig', () => {
|
|
|
58
58
|
});
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
+
describe('normalizeBaseUrl', () => {
|
|
62
|
+
it('rewrites legacy api.badgr.ai to production host', () => {
|
|
63
|
+
expect(normalizeBaseUrl('https://api.badgr.ai/v1')).toBe(DEFAULTS.baseUrl);
|
|
64
|
+
expect(normalizeBaseUrl('https://api.badgr.ai')).toBe(DEFAULTS.baseUrl);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('keeps production and localhost URLs', () => {
|
|
68
|
+
expect(normalizeBaseUrl('https://api.aibadgr.com/v1')).toBe(DEFAULTS.baseUrl);
|
|
69
|
+
expect(normalizeBaseUrl('http://localhost:8000/v1')).toBe('http://localhost:8000/v1');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('loadConfig legacy migration', () => {
|
|
74
|
+
it('migrates saved api.badgr.ai baseUrl on load', () => {
|
|
75
|
+
saveConfig(
|
|
76
|
+
{ apiKey: 'sk-test', baseUrl: 'https://api.badgr.ai/v1' },
|
|
77
|
+
testConfigFile,
|
|
78
|
+
);
|
|
79
|
+
const config = loadConfig(testConfigFile);
|
|
80
|
+
expect(config.baseUrl).toBe(DEFAULTS.baseUrl);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
61
84
|
describe('requireApiKey', () => {
|
|
62
85
|
it('returns the key when present', () => {
|
|
63
86
|
expect(requireApiKey({ apiKey: 'sk-abc' })).toBe('sk-abc');
|