Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,18 @@ Run `zerogpu <command> --help` for command-level help.

#### `login`

Sign in to ZeroGPU. Prompts for your API key (masked input) and project ID, validates them, and persists the credentials.
Sign in to ZeroGPU. Prompts for your API key (masked input), validates it, and persists the credential. The project is derived from the API key; pass `--project-id` only if you want to pin a specific project.

```bash
zerogpu login
zerogpu login --api-key zgpu-api-xxxxxxxx
zerogpu login --api-key zgpu-api-xxxxxxxx --project-id 4ed3e5bb-c2ed-4d4a-8a66-2b161a27fd1a
```

| Option | Description |
|---|---|
| `--api-key <key>` | Provide the API key directly (skips the prompt). |
| `--project-id <id>` | Provide the project ID directly (skips the prompt). |
| `--project-id <id>` | _(optional)_ Pin a specific project ID; derived from the API key if omitted. |

#### `status`

Expand Down
11 changes: 8 additions & 3 deletions docs/ADDING_COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,19 @@ This guide explains how to add a new CLI command to the ZeroGPU CLI.
- Add the import alongside the other `register...` imports.
- Call `registerXxxCommand(program)` inside `buildProgram()`.

3. **Auth-gated commands** should follow this pattern:
3. **Auth-gated commands** should follow this pattern. Only the API key is
required; the project is derived from it. Send `x-project-id` only when a
project ID is configured:
```ts
const apiKey = getApiKey();
const projectId = getProjectId();
if (!apiKey || !projectId) {
const projectId = getProjectId(); // optional
if (!apiKey) {
console.error("You're not fully signed in yet. Run 'zerogpu login' ...");
process.exit(1);
}
// headers:
// "x-api-key": apiKey.apiKey,
// ...(projectId ? { "x-project-id": projectId.projectId } : {}),
```

4. **Error handling**:
Expand Down
10 changes: 5 additions & 5 deletions docs/DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,10 @@ Credentials are persisted to a local config file and `ZEROGPU_API_KEY` is added
| Variable | Purpose |
|---|---|
| `ZEROGPU_API_KEY` | API key. Used as fallback if no config file is present. Written by `zerogpu login`. |
| `ZEROGPU_PROJECT_ID` | Project ID. Used as fallback if no config file is present. |
| `ZEROGPU_PROJECT_ID` | _(optional)_ Project ID. Used as fallback if no config file is present. The backend derives the project from the API key, so this is only needed to pin a specific project. |

### Resolution order
For every request the CLI resolves credentials by checking the **config file first**, then the corresponding **environment variable**. If either credential is missing, the command exits with code `1` and prompts you to run `zerogpu login`.
For every request the CLI resolves credentials by checking the **config file first**, then the corresponding **environment variable**. If the API key is missing, the command exits with code `1` and prompts you to run `zerogpu login`. The project ID is optional and sent (as `x-project-id`) only when present.

---

Expand Down Expand Up @@ -128,7 +128,7 @@ zerogpu login [--api-key <key>] [--project-id <id>]
| Flag | Type | Required | Description |
|---|---|---|---|
| `--api-key <key>` | string | optional | Provide the API key non-interactively. Must start with `zgpu-api-`. If omitted, the CLI shows a masked prompt. |
| `--project-id <id>` | string (UUID) | optional | Provide the Project ID non-interactively. Must be a valid UUID v4 string. If omitted, the CLI prompts plainly. |
| `--project-id <id>` | string (UUID) | optional | Pin a specific Project ID. Must be a valid UUID v4 string. If omitted, the project is derived from the API key (the CLI does not prompt for it). |

**Examples**
```bash
Expand Down Expand Up @@ -624,7 +624,7 @@ All inference commands POST to:
POST https://api.zerogpu.ai/v1/responses
Content-Type: application/json
x-api-key: <ZEROGPU_API_KEY>
x-project-id: <ZEROGPU_PROJECT_ID>
x-project-id: <ZEROGPU_PROJECT_ID> # optional; sent only when a project is configured
```

The request body is:
Expand Down Expand Up @@ -663,7 +663,7 @@ It picks the first `content` entry whose `type === "output_text"` (falling back

| Symptom | Likely cause | Fix |
|---|---|---|
| `You're not fully signed in yet.` | Missing config + missing env var | Run `zerogpu login`, or export `ZEROGPU_API_KEY` and `ZEROGPU_PROJECT_ID`. |
| `You're not fully signed in yet.` | Missing config + missing env var | Run `zerogpu login`, or export `ZEROGPU_API_KEY` (`ZEROGPU_PROJECT_ID` is optional). |
| `That doesn't look like a valid API key` | Key doesn't start with `zgpu-api-` | Copy the full key from the ZeroGPU console. |
| `Project ID must be a UUID.` | Pasted a name/slug | Use the UUID shown next to the project in the console. |
| `Request failed with status 401` | Bad/revoked key or wrong project | Re-run `zerogpu login`. |
Expand Down
6 changes: 3 additions & 3 deletions src/commands/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ export function registerChatCommand(program: Command): void {
const apiKey = getApiKey();
const projectId = getProjectId();

if (!apiKey || !projectId) {
if (!apiKey) {
console.error(
"You're not fully signed in yet. Run 'zerogpu login' to set your API key and project ID.",
"You're not fully signed in yet. Run 'zerogpu login' to set your API key.",
);
process.exit(1);
}
Expand All @@ -41,7 +41,7 @@ export function registerChatCommand(program: Command): void {
headers: {
"content-type": "application/json",
"x-api-key": apiKey.apiKey,
"x-project-id": projectId.projectId,
...(projectId ? { "x-project-id": projectId.projectId } : {}),
},
body: JSON.stringify(body),
});
Expand Down
6 changes: 3 additions & 3 deletions src/commands/chatThinking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ export function registerChatThinkingCommand(program: Command): void {
const apiKey = getApiKey();
const projectId = getProjectId();

if (!apiKey || !projectId) {
if (!apiKey) {
console.error(
"You're not fully signed in yet. Run 'zerogpu login' to set your API key and project ID.",
"You're not fully signed in yet. Run 'zerogpu login' to set your API key.",
);
process.exit(1);
}
Expand All @@ -33,7 +33,7 @@ export function registerChatThinkingCommand(program: Command): void {
headers: {
"content-type": "application/json",
"x-api-key": apiKey.apiKey,
"x-project-id": projectId.projectId,
...(projectId ? { "x-project-id": projectId.projectId } : {}),
},
body: JSON.stringify({
model: MODEL,
Expand Down
6 changes: 3 additions & 3 deletions src/commands/classifyIab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ export function registerClassifyIabCommand(program: Command): void {
const apiKey = getApiKey();
const projectId = getProjectId();

if (!apiKey || !projectId) {
if (!apiKey) {
console.error(
"You're not fully signed in yet. Run 'zerogpu login' to set your API key and project ID.",
"You're not fully signed in yet. Run 'zerogpu login' to set your API key.",
);
process.exit(1);
}
Expand All @@ -33,7 +33,7 @@ export function registerClassifyIabCommand(program: Command): void {
headers: {
"content-type": "application/json",
"x-api-key": apiKey.apiKey,
"x-project-id": projectId.projectId,
...(projectId ? { "x-project-id": projectId.projectId } : {}),
},
body: JSON.stringify({
model: MODEL,
Expand Down
6 changes: 3 additions & 3 deletions src/commands/classifyIabEnriched.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ export function registerClassifyIabEnrichedCommand(program: Command): void {
const apiKey = getApiKey();
const projectId = getProjectId();

if (!apiKey || !projectId) {
if (!apiKey) {
console.error(
"You're not fully signed in yet. Run 'zerogpu login' to set your API key and project ID.",
"You're not fully signed in yet. Run 'zerogpu login' to set your API key.",
);
process.exit(1);
}
Expand All @@ -33,7 +33,7 @@ export function registerClassifyIabEnrichedCommand(program: Command): void {
headers: {
"content-type": "application/json",
"x-api-key": apiKey.apiKey,
"x-project-id": projectId.projectId,
...(projectId ? { "x-project-id": projectId.projectId } : {}),
},
body: JSON.stringify({
model: MODEL,
Expand Down
6 changes: 3 additions & 3 deletions src/commands/classifyStructured.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ export function registerClassifyStructuredCommand(program: Command): void {
const apiKey = getApiKey();
const projectId = getProjectId();

if (!apiKey || !projectId) {
if (!apiKey) {
console.error(
"You're not fully signed in yet. Run 'zerogpu login' to set your API key and project ID.",
"You're not fully signed in yet. Run 'zerogpu login' to set your API key.",
);
process.exit(1);
}
Expand All @@ -46,7 +46,7 @@ export function registerClassifyStructuredCommand(program: Command): void {
headers: {
"content-type": "application/json",
"x-api-key": apiKey.apiKey,
"x-project-id": projectId.projectId,
...(projectId ? { "x-project-id": projectId.projectId } : {}),
},
body: JSON.stringify({
model: MODEL,
Expand Down
6 changes: 3 additions & 3 deletions src/commands/classifyZeroShot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ export function registerClassifyZeroShotCommand(program: Command): void {
const apiKey = getApiKey();
const projectId = getProjectId();

if (!apiKey || !projectId) {
if (!apiKey) {
console.error(
"You're not fully signed in yet. Run 'zerogpu login' to set your API key and project ID.",
"You're not fully signed in yet. Run 'zerogpu login' to set your API key.",
);
process.exit(1);
}
Expand All @@ -66,7 +66,7 @@ export function registerClassifyZeroShotCommand(program: Command): void {
headers: {
"content-type": "application/json",
"x-api-key": apiKey.apiKey,
"x-project-id": projectId.projectId,
...(projectId ? { "x-project-id": projectId.projectId } : {}),
},
body: JSON.stringify({
model: MODEL,
Expand Down
6 changes: 3 additions & 3 deletions src/commands/extractEntities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ export function registerExtractEntitiesCommand(program: Command): void {
const apiKey = getApiKey();
const projectId = getProjectId();

if (!apiKey || !projectId) {
if (!apiKey) {
console.error(
"You're not fully signed in yet. Run 'zerogpu login' to set your API key and project ID.",
"You're not fully signed in yet. Run 'zerogpu login' to set your API key.",
);
process.exit(1);
}
Expand Down Expand Up @@ -76,7 +76,7 @@ export function registerExtractEntitiesCommand(program: Command): void {
headers: {
"content-type": "application/json",
"x-api-key": apiKey.apiKey,
"x-project-id": projectId.projectId,
...(projectId ? { "x-project-id": projectId.projectId } : {}),
},
body: JSON.stringify({
model: MODEL,
Expand Down
6 changes: 3 additions & 3 deletions src/commands/extractJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ export function registerExtractJsonCommand(program: Command): void {
const apiKey = getApiKey();
const projectId = getProjectId();

if (!apiKey || !projectId) {
if (!apiKey) {
console.error(
"You're not fully signed in yet. Run 'zerogpu login' to set your API key and project ID.",
"You're not fully signed in yet. Run 'zerogpu login' to set your API key.",
);
process.exit(1);
}
Expand All @@ -46,7 +46,7 @@ export function registerExtractJsonCommand(program: Command): void {
headers: {
"content-type": "application/json",
"x-api-key": apiKey.apiKey,
"x-project-id": projectId.projectId,
...(projectId ? { "x-project-id": projectId.projectId } : {}),
},
body: JSON.stringify({
model: MODEL,
Expand Down
6 changes: 3 additions & 3 deletions src/commands/extractPii.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ export function registerExtractPiiCommand(program: Command): void {
const apiKey = getApiKey();
const projectId = getProjectId();

if (!apiKey || !projectId) {
if (!apiKey) {
console.error(
"You're not fully signed in yet. Run 'zerogpu login' to set your API key and project ID.",
"You're not fully signed in yet. Run 'zerogpu login' to set your API key.",
);
process.exit(1);
}
Expand All @@ -59,7 +59,7 @@ export function registerExtractPiiCommand(program: Command): void {
headers: {
"content-type": "application/json",
"x-api-key": apiKey.apiKey,
"x-project-id": projectId.projectId,
...(projectId ? { "x-project-id": projectId.projectId } : {}),
},
body: JSON.stringify({
model: MODEL,
Expand Down
6 changes: 3 additions & 3 deletions src/commands/generateFollowups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ export function registerGenerateFollowupsCommand(program: Command): void {
const apiKey = getApiKey();
const projectId = getProjectId();

if (!apiKey || !projectId) {
if (!apiKey) {
console.error(
"You're not fully signed in yet. Run 'zerogpu login' to set your API key and project ID.",
"You're not fully signed in yet. Run 'zerogpu login' to set your API key.",
);
process.exit(1);
}
Expand All @@ -33,7 +33,7 @@ export function registerGenerateFollowupsCommand(program: Command): void {
headers: {
"content-type": "application/json",
"x-api-key": apiKey.apiKey,
"x-project-id": projectId.projectId,
...(projectId ? { "x-project-id": projectId.projectId } : {}),
},
body: JSON.stringify({
model: MODEL,
Expand Down
56 changes: 29 additions & 27 deletions src/commands/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
import { Command } from "commander";
import { validateApiKey, validateProjectId } from "../lib/auth.js";
import { writeConfig, readConfig } from "../lib/config.js";
import { promptMasked, promptPlain } from "../lib/prompt.js";
import { promptMasked } from "../lib/prompt.js";
import { upsertEnvExport } from "../lib/shellEnv.js";

interface LoginOptions {
Expand Down Expand Up @@ -49,21 +49,21 @@ export function registerLoginCommand(program: Command): void {
.option("--api-key <key>", "Provide your API key directly (skips the prompt).")
.option(
"--project-id <id>",
"Provide your ZeroGPU project ID directly (skips the prompt).",
"Optionally pin a ZeroGPU project ID. If omitted, the project is derived from your API key.",
)
.action(async (options: LoginOptions) => {
let rawKey = options.apiKey;
let rawProjectId = options.projectId;
const rawProjectId = options.projectId;

if (!rawKey || !rawProjectId) {
if (!rawKey) {
const opened = await openBrowser(DASHBOARD_URL);
if (opened) {
console.log(
`Opening ${DASHBOARD_URL} in your browser — grab your Project ID and API Key from there.`,
`Opening ${DASHBOARD_URL} in your browser — grab your API Key from there.`,
);
} else {
console.log(
`Tip: open ${DASHBOARD_URL} in your browser to grab your Project ID and API Key.`,
`Tip: open ${DASHBOARD_URL} in your browser to grab your API Key.`,
);
}
}
Expand All @@ -84,32 +84,34 @@ export function registerLoginCommand(program: Command): void {
process.exit(1);
}

if (!rawProjectId) {
try {
rawProjectId = await promptPlain("Please enter your ZeroGPU project ID: ");
} catch {
console.error("Login cancelled. No changes were made.");
// Project ID is optional — the backend derives the project from the API key.
// Validate and store it only when the caller explicitly provided one.
let projectId: string | undefined;
if (rawProjectId) {
const projectResult = validateProjectId(rawProjectId);
if (!projectResult.ok) {
console.error(
`That doesn't look like a valid project ID — ${projectResult.reason}`,
);
console.error(
"It should be a UUID like 4ed3e5bb-c2ed-4d4a-8a66-2b161a27fd1a. Please try again.",
);
process.exit(1);
}
}

const projectResult = validateProjectId(rawProjectId ?? "");
if (!projectResult.ok) {
console.error(
`That doesn't look like a valid project ID — ${projectResult.reason}`,
);
console.error(
"It should be a UUID like 4ed3e5bb-c2ed-4d4a-8a66-2b161a27fd1a. Please try again.",
);
process.exit(1);
projectId = projectResult.key;
}

const existing = readConfig();
writeConfig({
...existing,
apiKey: result.key,
projectId: projectResult.key,
});
// Re-login clears any previously pinned project ID; it is restored only
// when --project-id is passed this run, so login without it returns to the
// unpinned (derive-from-key) state.
const next = { ...existing, apiKey: result.key };
if (projectId) {
next.projectId = projectId;
} else {
delete next.projectId;
}
writeConfig(next);

const env = upsertEnvExport("ZEROGPU_API_KEY", result.key);
process.env["ZEROGPU_API_KEY"] = result.key;
Expand Down
Loading