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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "badgr-cli",
3
- "version": "1.0.4",
3
+ "version": "1.0.7",
4
4
  "description": "Badgr — run or serve GPU workloads from one command",
5
5
  "type": "module",
6
6
  "bin": {
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') ? `\n Hint: Connection refused — is the server running at ${baseUrl}?` :
18
- msg.includes('ENOTFOUND') ? `\n Hint: DNS lookup failed check your internet connection or BADGR_API_URL` :
19
- msg.includes('ETIMEDOUT') ? `\n Hint: Request timed out — the server may be overloaded` :
20
- `\n Hint: Check your network connection and that BADGR_API_URL is correct`;
33
+ (code === 'ECONNREFUSED' || msg.includes('ECONNREFUSED'))
34
+ ? `\n Hint: Connection refusedis 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
- const text = await res.text();
27
- // Try to extract a readable detail from JSON error bodies
28
- const json = JSON.parse(text);
29
- detail = json?.detail ?? json?.message ?? json?.error ?? text;
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 ? '\n Hint: Server error — no GPU capacity available or backend is down' :
66
+ (res.status === 502 || res.status === 503) && !isCapacityError
67
+ ? '\n Hint: Server error — no GPU capacity available or backend is down' :
38
68
  '';
39
- throw new Error(`${method} ${path} → HTTP ${res.status}: ${detail}${hint}`);
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://api.badgr.ai/v1")')}
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() {
@@ -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 baseUrl = await input({
12
- message: 'API base URL:',
13
- default: 'https://api.badgr.ai/v1',
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;
@@ -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 || 'US',
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
- console.error(chalk.red(`\n ✗ Job failed to start: ${err.message}`));
122
- console.error(chalk.dim(`\n API URL: ${config.baseUrl}`));
123
- console.error(chalk.dim(` Config: badgr config Docs: badgr --help\n`));
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
 
@@ -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 API URL: ${config.baseUrl}`));
91
- console.error(chalk.dim(` Config: badgr config Docs: badgr --help\n`));
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://api.badgr.ai/v1',
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)) return { ...DEFAULTS };
41
+ if (!existsSync(configFile)) {
42
+ return applyEnvOverrides({ ...DEFAULTS });
43
+ }
15
44
  try {
16
- return { ...DEFAULTS, ...JSON.parse(readFileSync(configFile, 'utf8')) };
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
 
@@ -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');