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.
- package/HOW_IT_WORKS.md +245 -0
- package/README.md +147 -0
- package/badgr-cli-1.0.0.tgz +0 -0
- package/package.json +26 -0
- package/src/api.js +120 -0
- package/src/badgr.js +100 -0
- package/src/commands/deploy.js +47 -0
- package/src/commands/down.js +55 -0
- package/src/commands/login.js +20 -0
- package/src/commands/logs.js +49 -0
- package/src/commands/models.js +39 -0
- package/src/commands/receipts.js +82 -0
- package/src/commands/run.js +162 -0
- package/src/commands/serve.js +160 -0
- package/src/commands/shell.js +21 -0
- package/src/commands/status.js +97 -0
- package/src/commands/up.js +134 -0
- package/src/config.js +33 -0
- package/src/router.js +104 -0
- package/src/spec.js +92 -0
- package/src/store.js +88 -0
- package/tests/api.test.js +140 -0
- package/tests/commands.test.js +81 -0
- package/tests/config.test.js +73 -0
- package/tests/router.test.js +157 -0
- package/tests/spec.test.js +143 -0
- package/tests/store.test.js +126 -0
package/HOW_IT_WORKS.md
ADDED
|
@@ -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
|
+
}
|