badgr-cli 1.0.0

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.
@@ -0,0 +1,245 @@
1
+ # badgr-cli — How It Works
2
+
3
+ The `badgr` command is the single interface for provisioning and managing GPU compute on Badgr. You run one command, it finds the cheapest available GPU across Vast.ai / RunPod / SaladCloud, deploys your workload, and hands back an OpenAI-compatible endpoint URL and a receipt.
4
+
5
+ ---
6
+
7
+ ## Setup
8
+
9
+ ```bash
10
+ npm install -g badgr-cli
11
+ badgr login
12
+ ```
13
+
14
+ `badgr login` prompts for your API key and base URL, then writes them to `~/.gpu/config.json`. Every subsequent command reads that file — no env vars required.
15
+
16
+ ---
17
+
18
+ ## The 5 Core Commands
19
+
20
+ ### `badgr serve` — Provision
21
+
22
+ The main command. Two workload modes:
23
+
24
+ | Mode | What it does |
25
+ |------|-------------|
26
+ | `endpoint` | Deploys a vLLM inference server, returns an OpenAI-compatible base URL |
27
+ | `job` | Runs a one-off container and exits |
28
+
29
+ **Flags:**
30
+
31
+ | Flag | Default | Description |
32
+ |------|---------|-------------|
33
+ | `--endpoint` | — | Shorthand for `--type endpoint` |
34
+ | `--job` | — | Shorthand for `--type job` |
35
+ | `--model <id>` | `meta-llama/Llama-3.1-8B-Instruct` | LLM model for endpoint mode |
36
+ | `--image <img>` | `vllm/vllm-openai:latest` | Docker image for job mode |
37
+ | `--gpu <type>` | `RTX_4090` | GPU type (see GPU catalog below) |
38
+ | `--count <n>` | `1` | Number of GPUs (1–8) |
39
+ | `--region US\|EU\|AU` | `US` | Region preference |
40
+ | `--max-price <$/hr>` | none | Hard cap on GPU-hour price |
41
+ | `--name <name>` | auto-generated | Human-readable deployment name |
42
+ | `--dry-run` | — | Preview route plan without provisioning |
43
+
44
+ **Example — the target command from the spec:**
45
+
46
+ ```bash
47
+ badgr serve --endpoint --model meta-llama/Llama-3.3-70B --gpu L40S
48
+ ```
49
+
50
+ Output:
51
+ ```
52
+ ✓ Provisioned
53
+
54
+ Deployment: dep-a3f1bc20
55
+ Name: l40s-a3f1bc
56
+ Type: endpoint
57
+ Model: meta-llama/Llama-3.3-70B
58
+ GPU: L40S × 1
59
+ Provider: vastai
60
+ Endpoint: https://api.gpu.ai/v1
61
+ Rate: $1.10/hr
62
+
63
+ Receipt ID: rcpt-8d4e2f1a90
64
+
65
+ Use with OpenAI client:
66
+ client = OpenAI(api_key="sk-...", base_url="https://api.gpu.ai/v1")
67
+ # model: "meta-llama/Llama-3.3-70B"
68
+ ```
69
+
70
+ **Dry run:**
71
+
72
+ ```bash
73
+ badgr serve --endpoint --model X --gpu RTX_4090 --dry-run
74
+ ```
75
+
76
+ ```
77
+ 🔍 Dry Run — Route Plan
78
+
79
+ Spec
80
+ ────────────────────────────────────────
81
+ type: endpoint
82
+ model: X
83
+ gpu: RTX_4090 × 1
84
+ region: US
85
+
86
+ Lane 1 — Own GPU Hosts
87
+ ────────────────────────────────────────
88
+ checked at runtime against live worker pool
89
+
90
+ Lane 2 — Overflow Providers (cheapest-first)
91
+ ────────────────────────────────────────
92
+ 1. vastai RTX_4090 $0.65/hr reliability: 94% ← primary
93
+ 2. runpod RTX_4090 $0.72/hr reliability: 97%
94
+ 3. tensordock RTX_4090 $0.81/hr reliability: 91%
95
+ 4. salad RTX_4090 $0.89/hr reliability: 89%
96
+
97
+ Estimated range: $0.65–$0.89/hr
98
+ With 25% overhead (startup risk + badgr margin): ~$0.81/hr
99
+ ```
100
+
101
+ ---
102
+
103
+ ### `badgr down` — Terminate
104
+
105
+ ```bash
106
+ badgr down <name|id>
107
+ ```
108
+
109
+ Stops the deployment, removes it from local state, and writes a termination receipt.
110
+
111
+ ---
112
+
113
+ ### `badgr status` — Active Deployments
114
+
115
+ ```bash
116
+ badgr status
117
+ ```
118
+
119
+ Prints a table of everything currently provisioned: name, type, GPU, provider, status, and rate. Endpoint deployments also show their base URL.
120
+
121
+ ---
122
+
123
+ ### `badgr logs` — Logs
124
+
125
+ ```bash
126
+ badgr logs <name|id> [--follow]
127
+ ```
128
+
129
+ For **endpoint** deployments: shows the receipts URL (per-request cost/route/latency records from the API).
130
+
131
+ For **job** deployments: directs you to `badgr receipts` for the execution record.
132
+
133
+ Live log streaming (`--follow`) requires a running backend connection.
134
+
135
+ ---
136
+
137
+ ### `badgr receipts` — Cost & Route Records
138
+
139
+ ```bash
140
+ badgr receipts [n] # default: last 10
141
+ ```
142
+
143
+ Shows two sets of receipts:
144
+
145
+ 1. **CLI action receipts** — every `badgr serve` / `badgr down` recorded locally in `~/.gpu/deployments.json`, with provider, retries, latency, and cost.
146
+ 2. **Inference receipts** — per-request records fetched from `GET /v1/receipts` on the API (requires `badgr login`).
147
+
148
+ Every action — including failures — generates a receipt. Receipt IDs are printed on every command output so you can look them up later.
149
+
150
+ ---
151
+
152
+ ## How Routing Works
153
+
154
+ When you run `badgr serve`, the system tries providers in lane order, cheapest-first within each lane:
155
+
156
+ ```
157
+ Lane 1 — Own GPU Hosts (AI Badgr workers)
158
+ Checked at runtime against the live worker pool.
159
+ Highest margin; used first when capacity is available.
160
+
161
+ Lane 2 — Overflow Providers
162
+ Queried in parallel: Vast.ai, RunPod, TensorDock, SaladCloud
163
+ Sorted by price ascending.
164
+ A 25% overhead is added (startup risk + failure buffer + Badgr margin).
165
+ Max-price cap applied if --max-price was set.
166
+
167
+ Lane 3 — Enterprise Fallback
168
+ Lambda, CoreWeave, Crusoe — manual approval required.
169
+ ```
170
+
171
+ The dry-run output shows exactly which providers will be tried and in what order before any resources are created.
172
+
173
+ ---
174
+
175
+ ## GPU Catalog
176
+
177
+ | ID | Canonical | VRAM | Rate/hr | Good for |
178
+ |----|-----------|------|---------|----------|
179
+ | `rtx-3080` | RTX_3080 | 10 GB | $0.35 | dev, inference |
180
+ | `rtx-4090` | RTX_4090 | 24 GB | $1.10 | inference, training |
181
+ | `l40s` | L40S | 48 GB | $1.40 | inference, training |
182
+ | `a6000` | A6000 | 48 GB | $1.60 | inference, training |
183
+ | `a100-40gb` | A100 | 40 GB | $1.80 | training, inference |
184
+ | `a100-80gb` | A100 | 80 GB | $2.50 | training, large models |
185
+ | `h100` | H100 | 80 GB | $3.50 | training, large models |
186
+
187
+ GPU type aliases are normalized automatically: `rtx-4090`, `rtx4090`, `4090`, `RTX_4090` all resolve to `RTX_4090`.
188
+
189
+ ---
190
+
191
+ ## Provider Pricing (Lane 2, cheapest-first per GPU)
192
+
193
+ | GPU | vastai | runpod | tensordock | salad |
194
+ |-----|--------|--------|------------|-------|
195
+ | RTX_4090 | $0.65 | $0.72 | $0.81 | $0.89 |
196
+ | L40S | $1.10 | $1.25 | — | $1.40 |
197
+ | A6000 | $1.05 | $1.20 | — | $1.35 |
198
+ | A100 | $1.35 | $1.20 | $1.50 | — |
199
+ | H100 | $3.10 | $2.80 | — | — |
200
+
201
+ ---
202
+
203
+ ## Local State
204
+
205
+ All CLI state lives in `~/.gpu/`:
206
+
207
+ ```
208
+ ~/.gpu/
209
+ config.json API key + base URL
210
+ deployments.json Active deployments + receipt log (last 200)
211
+ ```
212
+
213
+ `deployments.json` is updated on every `badgr serve` / `badgr down`. It's what `badgr status`, `badgr logs`, and `badgr receipts` read from. The backend is the source of truth for inference receipts — local state only covers provisioning actions.
214
+
215
+ ---
216
+
217
+ ## OpenAI Compatibility
218
+
219
+ Every endpoint deployment returns a base URL. Point any OpenAI-compatible client at it:
220
+
221
+ ```python
222
+ from openai import OpenAI
223
+
224
+ client = OpenAI(
225
+ api_key="sk-your-key",
226
+ base_url="https://api.gpu.ai/v1", # from `badgr serve` output
227
+ )
228
+
229
+ response = client.chat.completions.create(
230
+ model="meta-llama/Llama-3.1-8B-Instruct",
231
+ messages=[{"role": "user", "content": "Hello"}],
232
+ )
233
+ ```
234
+
235
+ No other code changes needed — it's a drop-in base URL swap.
236
+
237
+ ---
238
+
239
+ ## Other Commands
240
+
241
+ ```bash
242
+ badgr models # List GPU options + available LLM models from the API
243
+ badgr config # Show current config (API key masked)
244
+ badgr run <script> # Quick one-off job alias
245
+ ```
package/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # badgr-cli
2
+
3
+ Run or serve GPU workloads from one command.
4
+
5
+ ```bash
6
+ npm install -g badgr-cli
7
+ ```
8
+
9
+ ---
10
+
11
+ ## Quick start
12
+
13
+ ```bash
14
+ # 1. Authenticate once
15
+ badgr login
16
+
17
+ # 2. Serve an OpenAI-compatible inference endpoint
18
+ badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S
19
+
20
+ # 3. Use the endpoint with any OpenAI SDK client
21
+ # client = OpenAI(api_key="sk-...", base_url="https://dep-xyz.api.badgr.ai/v1")
22
+
23
+ # 4. View cost, provider, and route receipts
24
+ badgr receipts
25
+
26
+ # 5. Stop billing
27
+ badgr down <deployment-id>
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Commands
33
+
34
+ | Command | What it does |
35
+ |---------|-------------|
36
+ | `badgr login` | Save API key to `~/.badgr/config.json` |
37
+ | `badgr serve <model>` | Persistent OpenAI-compatible endpoint |
38
+ | `badgr run <command>` | One-off GPU job (container) |
39
+ | `badgr down <id>` | Terminate a deployment — stops billing |
40
+ | `badgr logs <id>` | Tail logs for a deployment |
41
+ | `badgr receipts [n]` | Cost / route / retry receipts (default 10) |
42
+
43
+ ---
44
+
45
+ ## `badgr serve` options
46
+
47
+ ```bash
48
+ badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S --region EU
49
+ ```
50
+
51
+ | Flag | Default | Description |
52
+ |------|---------|-------------|
53
+ | `--gpu <type>` | RTX_4090 | GPU: RTX_4090, L40S, A6000, A100, H100 |
54
+ | `--region US\|EU\|AU` | US | Region preference |
55
+ | `--count <n>` | 1 | Number of GPUs (1–8) |
56
+ | `--max-price <$/hr>` | — | Hard spend cap per GPU-hour |
57
+ | `--dry-run` | — | Preview routing without provisioning |
58
+
59
+ ---
60
+
61
+ ## `badgr run` options
62
+
63
+ ```bash
64
+ badgr run python train.py --gpu A100 --env HF_TOKEN=$HF_TOKEN
65
+ ```
66
+
67
+ | Flag | Default | Description |
68
+ |------|---------|-------------|
69
+ | `--gpu <type>` | RTX_4090 | GPU type |
70
+ | `--image <img>` | python:3.11-slim | Docker image |
71
+ | `--env KEY=VALUE` | — | Environment variable (repeatable) |
72
+ | `--region US\|EU\|AU` | US | Region preference |
73
+ | `--max-price <$/hr>` | — | Hard spend cap per GPU-hour |
74
+ | `--detach` | — | Launch and return immediately |
75
+
76
+ ---
77
+
78
+ ## Routing
79
+
80
+ Badgr searches providers in order: own GPU hosts → Vast.ai → RunPod → TensorDock → SaladCloud. It picks the cheapest available instance that meets the GPU and region spec, then adds ~25% for routing margin and startup risk.
81
+
82
+ Preview routing before committing:
83
+
84
+ ```bash
85
+ badgr serve mistral-7b --gpu RTX_4090 --dry-run
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Receipts
91
+
92
+ Every `badgr serve` and `badgr run` action generates a receipt:
93
+
94
+ ```bash
95
+ badgr receipts # last 10
96
+ badgr receipts 50 # last 50
97
+ ```
98
+
99
+ Each receipt includes: receipt ID, provider, GPU, provisioning latency, rate/hr, and retry count.
100
+
101
+ ---
102
+
103
+ ## OpenAI compatibility
104
+
105
+ `badgr serve` provisions a vLLM endpoint that is fully OpenAI-compatible:
106
+
107
+ ```python
108
+ from openai import OpenAI
109
+
110
+ client = OpenAI(
111
+ api_key="your-badgr-api-key",
112
+ base_url="https://dep-xyz.api.badgr.ai/v1", # from badgr serve output
113
+ )
114
+ resp = client.chat.completions.create(
115
+ model="meta-llama/Llama-3.1-8B-Instruct",
116
+ messages=[{"role": "user", "content": "Hello"}],
117
+ )
118
+ ```
119
+
120
+ ```js
121
+ import OpenAI from "openai";
122
+ const client = new OpenAI({
123
+ apiKey: process.env.BADGR_API_KEY,
124
+ baseURL: "https://dep-xyz.api.badgr.ai/v1",
125
+ });
126
+ ```
127
+
128
+ ---
129
+
130
+ ## GPU options
131
+
132
+ | Flag value | GPU | VRAM | Est. rate/hr |
133
+ |-----------|-----|------|-------------|
134
+ | RTX_4090 | NVIDIA RTX 4090 | 24 GB | $0.65–0.89 |
135
+ | L40S | NVIDIA L40S | 48 GB | $1.10–1.40 |
136
+ | A6000 | NVIDIA RTX A6000 | 48 GB | $1.05–1.35 |
137
+ | A100 | NVIDIA A100 | 80 GB | $1.20–1.50 |
138
+ | H100 | NVIDIA H100 | 80 GB | $2.80–3.10 |
139
+
140
+ Rates are provider-level. Badgr adds ~25% for routing and startup risk.
141
+
142
+ ---
143
+
144
+ ## Requirements
145
+
146
+ - Node.js 18+
147
+ - A Badgr account — sign up at [badgr.ai](https://badgr.ai)
Binary file
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "badgr-cli",
3
+ "version": "1.0.0",
4
+ "description": "Badgr — run or serve GPU workloads from one command",
5
+ "type": "module",
6
+ "bin": {
7
+ "badgr": "./src/badgr.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node src/badgr.js",
11
+ "test": "vitest run",
12
+ "test:watch": "vitest"
13
+ },
14
+ "dependencies": {
15
+ "@inquirer/prompts": "^5.1.0",
16
+ "chalk": "^5.3.0"
17
+ },
18
+ "devDependencies": {
19
+ "vitest": "^1.6.0"
20
+ },
21
+ "engines": {
22
+ "node": ">=18.0.0"
23
+ },
24
+ "keywords": ["gpu", "cli", "ai", "compute", "modal", "gateway", "openai"],
25
+ "license": "MIT"
26
+ }
package/src/api.js ADDED
@@ -0,0 +1,120 @@
1
+ export async function callApi(path, { method = 'GET', apiKey, baseUrl, body } = {}) {
2
+ const url = `${baseUrl}${path}`;
3
+ const res = await fetch(url, {
4
+ method,
5
+ headers: {
6
+ 'Content-Type': 'application/json',
7
+ 'Authorization': `Bearer ${apiKey}`,
8
+ },
9
+ body: body !== undefined ? JSON.stringify(body) : undefined,
10
+ });
11
+ if (!res.ok) {
12
+ const text = await res.text().catch(() => '');
13
+ throw new Error(`${method} ${path} → ${res.status}: ${text}`);
14
+ }
15
+ return res.json();
16
+ }
17
+
18
+ // ---- Core: run & serve (POST /v1/run, POST /v1/serve) ----------------------
19
+
20
+ export function runJob(config, body) {
21
+ return callApi('/run', {
22
+ method: 'POST',
23
+ apiKey: config.apiKey,
24
+ baseUrl: config.baseUrl,
25
+ body,
26
+ });
27
+ }
28
+
29
+ export function serveModel(config, body) {
30
+ return callApi('/serve', {
31
+ method: 'POST',
32
+ apiKey: config.apiKey,
33
+ baseUrl: config.baseUrl,
34
+ body,
35
+ });
36
+ }
37
+
38
+ // ---- Deployments (POST/GET/DELETE /v1/deployments) --------------------------
39
+
40
+ export function createDeployment(config, spec) {
41
+ return callApi('/deployments', {
42
+ method: 'POST',
43
+ apiKey: config.apiKey,
44
+ baseUrl: config.baseUrl,
45
+ body: spec,
46
+ });
47
+ }
48
+
49
+ export function listDeployments(config) {
50
+ return callApi('/deployments', { apiKey: config.apiKey, baseUrl: config.baseUrl });
51
+ }
52
+
53
+ export function getDeployment(config, deploymentId) {
54
+ return callApi(`/deployments/${deploymentId}`, { apiKey: config.apiKey, baseUrl: config.baseUrl });
55
+ }
56
+
57
+ export function terminateDeployment(config, deploymentId) {
58
+ return callApi(`/deployments/${deploymentId}`, {
59
+ method: 'DELETE',
60
+ apiKey: config.apiKey,
61
+ baseUrl: config.baseUrl,
62
+ });
63
+ }
64
+
65
+ export function getDeploymentLogs(config, deploymentId) {
66
+ return callApi(`/deployments/${deploymentId}/logs`, { apiKey: config.apiKey, baseUrl: config.baseUrl });
67
+ }
68
+
69
+ // ---- Receipts ---------------------------------------------------------------
70
+
71
+ export function listReceipts(config, { limit = 20, status, fromTs, toTs } = {}) {
72
+ const params = new URLSearchParams();
73
+ if (limit) params.set('limit', String(limit));
74
+ if (status) params.set('status', status);
75
+ if (fromTs) params.set('from_timestamp', String(fromTs));
76
+ if (toTs) params.set('to_timestamp', String(toTs));
77
+ const qs = params.toString();
78
+ return callApi(`/receipts${qs ? `?${qs}` : ''}`, {
79
+ apiKey: config.apiKey,
80
+ baseUrl: config.baseUrl,
81
+ });
82
+ }
83
+
84
+ export function getReceipt(config, receiptId) {
85
+ return callApi(`/receipts/${receiptId}`, {
86
+ apiKey: config.apiKey,
87
+ baseUrl: config.baseUrl,
88
+ });
89
+ }
90
+
91
+ // ---- Inference --------------------------------------------------------------
92
+
93
+ export function listModels(config) {
94
+ return callApi('/models', { apiKey: config.apiKey, baseUrl: config.baseUrl });
95
+ }
96
+
97
+ export function chatCompletion(config, messages, options = {}) {
98
+ const { model, stream = false } = options;
99
+ return callApi('/chat/completions', {
100
+ method: 'POST',
101
+ apiKey: config.apiKey,
102
+ baseUrl: config.baseUrl,
103
+ body: { model: model ?? config.defaultModel, messages, stream },
104
+ });
105
+ }
106
+
107
+ // ---- Legacy job routes (kept for backward compat) ---------------------------
108
+
109
+ export function submitJob(config, job) {
110
+ return callApi('/jobs', {
111
+ method: 'POST',
112
+ apiKey: config.apiKey,
113
+ baseUrl: config.baseUrl,
114
+ body: job,
115
+ });
116
+ }
117
+
118
+ export function getJobStatus(config, jobId) {
119
+ return callApi(`/jobs/${jobId}`, { apiKey: config.apiKey, baseUrl: config.baseUrl });
120
+ }
package/src/badgr.js ADDED
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ import chalk from 'chalk';
3
+ import { loadConfig, saveConfig } from './config.js';
4
+ import { loginCommand } from './commands/login.js';
5
+ import { upCommand } from './commands/up.js';
6
+ import { downCommand } from './commands/down.js';
7
+ import { statusCommand } from './commands/status.js';
8
+ import { logsCommand } from './commands/logs.js';
9
+ import { receiptsCommand } from './commands/receipts.js';
10
+ import { runCommand } from './commands/run.js';
11
+ import { serveCommand } from './commands/serve.js';
12
+ import { modelsCommand } from './commands/models.js';
13
+
14
+ const HELP = `
15
+ ${chalk.bold('badgr')} — run or serve GPU workloads from one command
16
+
17
+ ${chalk.bold('CORE COMMANDS')}
18
+ ${chalk.cyan('badgr login')} Authenticate (save API key to ~/.badgr/config.json)
19
+ ${chalk.cyan('badgr run <cmd...> --gpu <type>')} Run a one-off GPU job
20
+ ${chalk.cyan('badgr serve <model> --gpu <type>')} Serve a model (OpenAI-compatible endpoint)
21
+ ${chalk.cyan('badgr status')} Show active deployments + endpoint URLs
22
+ ${chalk.cyan('badgr logs <id>')} Stream logs for a deployment
23
+ ${chalk.cyan('badgr down <id>')} Terminate a deployment (stop billing)
24
+ ${chalk.cyan('badgr receipts [<id>|<n>]')} Show receipts — pass ID for single, number for list
25
+
26
+ ${chalk.bold('badgr run OPTIONS')}
27
+ --gpu <type> GPU: RTX_4090, A100, L40S, H100 (default: RTX_4090)
28
+ --image <image> Docker image (default: python:3.11-slim)
29
+ --count <n> GPU count (default: 1)
30
+ --region US|EU|AU Region preference (default: US)
31
+ --max-price <$/hr> Hard spend cap per GPU-hour
32
+ --name <name> Job name (auto-generated if omitted)
33
+
34
+ ${chalk.bold('badgr serve OPTIONS')}
35
+ --gpu <type> GPU: L40S, A100, H100, RTX_4090 (default: L40S)
36
+ --count <n> GPU count (default: 1)
37
+ --region US|EU|AU Region preference (default: US)
38
+ --max-price <$/hr> Hard spend cap per GPU-hour
39
+ --name <name> Deployment name (auto-generated if omitted)
40
+
41
+ ${chalk.bold('EXAMPLES')}
42
+ badgr login
43
+ badgr run python train.py --gpu A100
44
+ badgr run --image my/image:latest --gpu L40S
45
+ badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S
46
+ badgr serve mistralai/Mistral-7B-v0.1 --gpu RTX_4090
47
+ badgr status
48
+ badgr logs dep_abc123
49
+ badgr down dep_abc123
50
+ badgr receipts
51
+ badgr receipts dep_abc123
52
+
53
+ ${chalk.bold('OPENAI-COMPATIBLE SERVING')}
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")')}
56
+ ${chalk.dim(' client.chat.completions.create(model="dep_xxx", messages=[...])')}
57
+
58
+ ${chalk.bold('PROVIDER ROUTING')}
59
+ ${chalk.dim('own GPUs → Vast.ai / Salad / RunPod overflow → manual enterprise fallback')}
60
+ `;
61
+
62
+ async function main() {
63
+ const [,, cmd, ...rest] = process.argv;
64
+ const config = loadConfig();
65
+
66
+ if (!cmd || cmd === '--help' || cmd === '-h') {
67
+ console.log(HELP);
68
+ return;
69
+ }
70
+
71
+ switch (cmd) {
72
+ case 'login': return loginCommand(chalk, saveConfig);
73
+ case 'run': return runCommand(config, rest, chalk);
74
+ case 'serve': return serveCommand(config, rest, chalk);
75
+ case 'status': return statusCommand(config, rest, chalk);
76
+ case 'logs': return logsCommand(config, rest, chalk);
77
+ case 'down': return downCommand(config, rest, chalk);
78
+ case 'receipts': return receiptsCommand(config, rest, chalk);
79
+ case 'models': return modelsCommand(config, chalk);
80
+ // legacy aliases kept for compatibility
81
+ case 'up': return upCommand(config, rest, chalk);
82
+ case 'config': {
83
+ const { apiKey, ...safe } = config;
84
+ const display = { ...safe, apiKey: apiKey ? `${apiKey.slice(0, 8)}...` : '(not set)' };
85
+ console.log('\nCurrent config:\n');
86
+ console.log(JSON.stringify(display, null, 2));
87
+ console.log();
88
+ return;
89
+ }
90
+ default:
91
+ console.error(chalk.red(`Unknown command: ${cmd}\n`));
92
+ console.log(`Run ${chalk.cyan('badgr --help')} for usage.`);
93
+ process.exit(1);
94
+ }
95
+ }
96
+
97
+ main().catch(err => {
98
+ console.error(chalk.red('Fatal:'), err.message);
99
+ process.exit(1);
100
+ });
@@ -0,0 +1,47 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { findCheapest, findById } from '../router.js';
3
+ import { requireApiKey } from '../config.js';
4
+
5
+ export function parseDeployArgs(args) {
6
+ const nameFlag = args.indexOf('--name');
7
+ const name = nameFlag !== -1 ? args[nameFlag + 1] : null;
8
+ const gpuFlag = args.indexOf('--gpu');
9
+ const gpuId = gpuFlag !== -1 ? args[gpuFlag + 1] : null;
10
+ const cleanArgs = args.filter((a, i) => {
11
+ if (a === '--name' || a === '--gpu') return false;
12
+ if (i > 0 && (args[i - 1] === '--name' || args[i - 1] === '--gpu')) return false;
13
+ return true;
14
+ });
15
+ const script = cleanArgs.find(a => !a.startsWith('--')) ?? null;
16
+ return { script, name, gpuId };
17
+ }
18
+
19
+ export async function deployCommand(config, args, chalk) {
20
+ const { script, name: rawName, gpuId } = parseDeployArgs(args);
21
+
22
+ if (!script) {
23
+ console.error(chalk.red('Usage: gpu deploy <script.py> [--name <name>] [--gpu <type>]'));
24
+ return;
25
+ }
26
+
27
+ requireApiKey(config);
28
+
29
+ const deployId = randomUUID().split('-')[0];
30
+ const name = rawName ?? script.replace(/[^a-z0-9]/gi, '-').replace(/-+/g, '-').toLowerCase();
31
+ const gpu = gpuId ? findById(gpuId) : findCheapest({ tag: 'inference' });
32
+ const endpointBase = config.baseUrl.replace(/\/v1\/?$/, '');
33
+ const endpointUrl = `${endpointBase}/endpoints/${name}-${deployId}`;
34
+
35
+ console.log(chalk.bold('\n🚀 Deploying\n'));
36
+ console.log(` ${chalk.bold('Script:')} ${script}`);
37
+ console.log(` ${chalk.bold('Name:')} ${name}-${deployId}`);
38
+ if (gpu) console.log(` ${chalk.bold('GPU:')} ${gpu.name} ${chalk.dim(`$${gpu.ratePerHour}/hr`)}`);
39
+ console.log();
40
+
41
+ // Real implementation: POST /deployments to backend
42
+ console.log(chalk.green('✓ Deployed!\n'));
43
+ console.log(` ${chalk.bold('Endpoint:')} ${chalk.cyan(endpointUrl)}`);
44
+ console.log(` ${chalk.bold('Logs:')} gpu logs ${name}-${deployId}`);
45
+ console.log(`\n ${chalk.dim('OpenAI-compatible — point your client at this URL.')}\n`);
46
+ return { endpointUrl, deployId, name: `${name}-${deployId}` };
47
+ }