sference-cli 0.0.1__tar.gz → 0.0.3__tar.gz

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.
@@ -8,3 +8,5 @@ build/
8
8
  .venv/
9
9
  *.egg
10
10
  .pytest_cache/
11
+ cli/.env
12
+
@@ -0,0 +1,232 @@
1
+ Metadata-Version: 2.4
2
+ Name: sference-cli
3
+ Version: 0.0.3
4
+ Summary: sference command-line interface
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: sference-sdk>=0.0.3
7
+ Requires-Dist: typer>=0.24.1
8
+ Description-Content-Type: text/markdown
9
+
10
+ # sference CLI
11
+
12
+ Command-line interface for the sference batch API (`sference`). It uses the Python SDK (`sference-sdk`) and is published on PyPI as `sference-cli`.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pip install sference-cli
18
+ # or isolated install on PATH:
19
+ uv tool install sference-cli
20
+ # or:
21
+ pipx install sference-cli
22
+ ```
23
+
24
+ Then:
25
+
26
+ ```bash
27
+ sference --help
28
+ ```
29
+
30
+ ## Authentication
31
+
32
+ 1. **Interactive (browser):** `sference auth login` — opens the console login page, then prompts for an API key from **Console → API keys**.
33
+ 2. **Non-interactive / CI:** `sference auth login --api-key 'sk_...'`
34
+ 3. **Environment variable:** `SFERENCE_API_KEY` overrides the saved credential file.
35
+
36
+ Credentials are stored in `~/.sference/credentials.json` unless `SFERENCE_API_KEY` is set.
37
+
38
+ Verify the current credential:
39
+
40
+ ```bash
41
+ sference auth me
42
+ sference auth me --json
43
+ ```
44
+
45
+ ## Quick examples (batches and streams)
46
+
47
+ Use a `model` string supported by your sference deployment (for self-hosted stacks, match the model your workers consume).
48
+
49
+ **Batches**
50
+
51
+ ```bash
52
+ sference batch submit --input-file ./workload.jsonl --model Qwen/Qwen2.5-7B-Instruct --window 24h
53
+ sference batch status --batch-id <batch_id>
54
+ sference batch wait --batch-id <batch_id>
55
+ sference batch results --batch-id <batch_id>
56
+ sference batch download-results --batch-id <batch_id> --out ./out.jsonl
57
+ # Submit, wait, print JSONL results on stdout (stderr: progress; resumable cache)
58
+ sference batch stream --input-file ./workload.jsonl --model Qwen/Qwen2.5-7B-Instruct --window 24h
59
+ ```
60
+
61
+ **Streams**
62
+
63
+ ```bash
64
+ sference stream create --name "my-stream" --window 24h
65
+ sference stream list
66
+ sference stream submit --stream-id <stream_id> --input-file ./lines.jsonl --model Qwen/Qwen2.5-7B-Instruct
67
+ sference stream status --stream-id <stream_id>
68
+ sference responses tail --stream-id <stream_id>
69
+ ```
70
+
71
+ ### cURL: OpenAI-compatible `/v1/responses`
72
+
73
+ The CLI uses this API for `stream submit` (via `POST /v1/responses`). You can call it directly with the same API key as `sference auth login` (or `SFERENCE_API_KEY`). For self-hosted APIs, set `SFERENCE_BASE_URL` to your API origin.
74
+
75
+ ```bash
76
+ export TOKEN=sk_... # or: export TOKEN="$SFERENCE_API_KEY"
77
+
78
+ RID=$(curl -sS -X POST "${SFERENCE_BASE_URL:-https://api.sference.com}/v1/responses" \
79
+ -H "X-API-Key: $TOKEN" \
80
+ -H 'Content-Type: application/json' \
81
+ -d '{
82
+ "model": "Qwen/Qwen2.5-7B-Instruct",
83
+ "input": [{"role": "user", "content": "Hello"}],
84
+ "metadata": {"completion_window": "24h"}
85
+ }' | jq -r '.id')
86
+
87
+ curl -sS "${SFERENCE_BASE_URL:-https://api.sference.com}/v1/responses/${RID}" \
88
+ -H "X-API-Key: $TOKEN"
89
+ ```
90
+
91
+ ## Environment variables
92
+
93
+ | Variable | Purpose |
94
+ |----------|---------|
95
+ | `SFERENCE_API_KEY` | API key (or JWT); overrides `~/.sference/credentials.json` |
96
+ | `SFERENCE_BASE_URL` | API base URL (default `https://api.sference.com`) |
97
+ | `SFERENCE_CONSOLE_URL` | Console URL for `auth login` browser step (default `https://console.sference.com`) |
98
+ | `SFERENCE_STREAM_CACHE` | Optional path to the stream resumable-cache file (default `~/.sference/stream_cache.json`) |
99
+ | `SFERENCE_STREAM_CHECKPOINTS` | Optional path for **`responses tail`** event checkpoints (default `~/.sference/stream_checkpoints.json`) |
100
+
101
+ ## Commands
102
+
103
+ ### Auth
104
+
105
+ | Command | Description |
106
+ |---------|-------------|
107
+ | `sference auth login` | Store an API key (optional `--api-key`, `--console-url`, `--no-browser`) |
108
+ | `sference auth me` | Show current user (`--json` for machine-readable output) |
109
+
110
+ ### Batch
111
+
112
+ | Command | Description |
113
+ |---------|-------------|
114
+ | `sference batch list` | List batches (table; `--json` for raw payload) |
115
+ | `sference batch submit` | Submit a JSONL file (`--input-file`, optional `--model` for content-only lines, `--window` must be `24h`) |
116
+ | `sference batch stream` | Submit, wait, print **JSONL results on stdout** (see below) |
117
+ | `sference batch status` | Get one batch (`--batch-id`, `--json`) |
118
+ | `sference batch wait` | Poll until terminal state (`--batch-id`, `--poll-interval`, `--timeout`, `--json`) |
119
+ | `sference batch results` | JSON results payload (`--batch-id`, `--json`) |
120
+ | `sference batch cancel` | Cancel a batch (`--batch-id`, `--json`) |
121
+ | `sference batch download-results` | Download results JSONL to a file (`--batch-id`, `--out`, `--format jsonl`) |
122
+
123
+ Global options on most batch commands: `--base-url` (default `https://api.sference.com`).
124
+
125
+ ### Responses (`/v1/responses`)
126
+
127
+ | Command | Description |
128
+ |---------|-------------|
129
+ | `sference responses create` | Create one response (`--model`, `--content`, optional `--wait`, `--poll-ms`, `--timeout-s`) |
130
+ | `sference responses result` | Poll until terminal state (`--id`, `--poll-ms`) |
131
+ | `sference responses tail` | Print completion events as JSONL via `GET /v1/responses/events` (optional `--stream-id` to scope to a stream; omit for non-stream completions). Flags: `--consumer`, `--from-latest`, `--no-checkpoint`, `--poll-ms` |
132
+
133
+ ### Stream (first-class streams API)
134
+
135
+ Long-lived **streams** are separate from **batches**: you create a stream, submit **responses** tied to it over time (`POST /v1/responses` with `metadata.stream_id`), and consume **completion events** with cursor-based pagination on **`GET /v1/responses/events`** (pass **`stream_id`** when scoping to a stream). Authenticate with your **secret API key** like other `/v1` calls.
136
+
137
+ | Command | Description |
138
+ |---------|-------------|
139
+ | `sference stream create` | Create a stream (`--name`, `--window` `1h` or `24h`, `--json`) |
140
+ | `sference stream list` | List streams (`--json`) |
141
+ | `sference stream status` | Full detail + counters (`--stream-id`, `--json`) |
142
+ | `sference stream submit` | Create responses from JSONL via `POST /v1/responses` per line (`metadata.stream_id` set automatically; `--stream-id`, `--input-file`, `--model` required for content-only lines) — per line: OpenAI batch-style `{custom_id?, method, url, body}` or content-only `{content}` |
143
+ | `sference stream cancel` | Stop accepting new items and stop enqueueing pending work; does not auto-cancel in-flight requests (`--stream-id`, `--json`) |
144
+ | `sference stream archive` | Finalize the stream (optional after cancel); no new items (`--stream-id`, `--json`) |
145
+
146
+ Example JSONL lines for `stream submit` (both accepted):
147
+
148
+ ```json
149
+ {"custom_id":"req-1","method":"POST","url":"/v1/chat/completions","body":{"model":"Qwen/Qwen3.5-4B","messages":[{"role":"user","content":"hi"}]}}
150
+ ```
151
+
152
+ ```json
153
+ {"content":"hi"}
154
+ ```
155
+
156
+ ---
157
+
158
+ ## Streaming batches (`batch stream`)
159
+
160
+ Use **`sference batch stream`** when you want a **single command** that submits a JSONL file, waits until the batch finishes, and **writes result lines to stdout** so you can pipe or redirect them.
161
+
162
+ ### Pipe-friendly UX
163
+
164
+ - **Stdout:** only the **results JSONL** (one JSON object per line, same shape as `GET /v1/batches/{id}/results.jsonl`).
165
+ - **Stderr:** status lines while waiting, e.g. `Batch batch_abc status=running (42s)`.
166
+
167
+ Example:
168
+
169
+ ```bash
170
+ sference batch stream --input-file workload.jsonl > results.jsonl
171
+ ```
172
+
173
+ Content-only JSONL (model supplied globally):
174
+
175
+ ```bash
176
+ sference batch stream --input-file prompts.jsonl --model Qwen/Qwen2.5-7B-Instruct > results.jsonl
177
+ ```
178
+
179
+ ### Resumable cache
180
+
181
+ Batches can take a long time. If you **interrupt** the command (e.g. Ctrl+C) and run it again with the **same input file contents**, the CLI **reuses the cached batch id** instead of submitting a duplicate job.
182
+
183
+ - Cache file: **`~/.sference/stream_cache.json`** (override with **`SFERENCE_STREAM_CACHE`**).
184
+ - Key: **SHA-256** of the raw input file bytes (same bytes ⇒ same key, regardless of path).
185
+ - Stored fields: `batch_id`, `base_url` (must match current `--base-url`), `created_at`.
186
+ - After results are written to stdout, the entry for that input is **removed** so the cache does not grow forever.
187
+ - If the cached batch no longer exists on the server (404), the cache entry is dropped and a **new** batch is submitted.
188
+
189
+ Force a **fresh** submission (ignore cache):
190
+
191
+ ```bash
192
+ sference batch stream --input-file workload.jsonl --no-cache > results.jsonl
193
+ ```
194
+
195
+ ### Polling
196
+
197
+ - **`--poll-interval`** (default `2`): seconds between `GET /v1/batches/{id}` polls. There is **no** built-in maximum wait time (suited to 24h-style batches).
198
+
199
+ ### Exit codes
200
+
201
+ - **0** — batch status is `completed`.
202
+ - **1** — batch status is `failed` or `cancelled` (results JSONL is still printed when available).
203
+
204
+ ### End-to-end example
205
+
206
+ ```bash
207
+ export SFERENCE_API_KEY=sk_...
208
+ sference batch stream --input-file fixtures/example_batch.jsonl --poll-interval 5 > out.jsonl
209
+ ```
210
+
211
+ ---
212
+
213
+ ## JSONL input formats
214
+
215
+ The SDK and CLI accept two line shapes (see also [`fixtures/example_batch.jsonl`](fixtures/example_batch.jsonl)):
216
+
217
+ 1. **OpenAI-compatible:** each line has `custom_id`, `method`, `url`, and `body` (e.g. chat completions payload with per-line `model`). The CLI `--model` flag is ignored for these lines (a warning may be emitted by the SDK).
218
+ 2. **Content-only:** each line is `{"content": "..."}`. Then **`--model` is required** on submit/stream.
219
+
220
+ ---
221
+
222
+ ## Python SDK
223
+
224
+ The CLI uses the sync **`SferenceClient`** from **`sference-sdk`** (`import sference_sdk`).
225
+
226
+ For your own code, see **[`../sdk-python/README.md`](../sdk-python/README.md)** for:
227
+
228
+ - **Batches (sync):** `submit_batch`, `wait_for_completion`, `get_results`
229
+ - **`/v1/responses` (sync):** `create_response`, `get_response` (standalone or `metadata.stream_id` for streams)
230
+ - **Async:** **`AsyncSferenceClient`** — same surface as sync with `await`, plus `iter_responses_events` / `list_responses_events` for completion tailing (`GET /v1/responses/events`)
231
+
232
+ That README also documents when to prefer **batches** vs **streams** and includes cURL examples.
@@ -0,0 +1,223 @@
1
+ # sference CLI
2
+
3
+ Command-line interface for the sference batch API (`sference`). It uses the Python SDK (`sference-sdk`) and is published on PyPI as `sference-cli`.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install sference-cli
9
+ # or isolated install on PATH:
10
+ uv tool install sference-cli
11
+ # or:
12
+ pipx install sference-cli
13
+ ```
14
+
15
+ Then:
16
+
17
+ ```bash
18
+ sference --help
19
+ ```
20
+
21
+ ## Authentication
22
+
23
+ 1. **Interactive (browser):** `sference auth login` — opens the console login page, then prompts for an API key from **Console → API keys**.
24
+ 2. **Non-interactive / CI:** `sference auth login --api-key 'sk_...'`
25
+ 3. **Environment variable:** `SFERENCE_API_KEY` overrides the saved credential file.
26
+
27
+ Credentials are stored in `~/.sference/credentials.json` unless `SFERENCE_API_KEY` is set.
28
+
29
+ Verify the current credential:
30
+
31
+ ```bash
32
+ sference auth me
33
+ sference auth me --json
34
+ ```
35
+
36
+ ## Quick examples (batches and streams)
37
+
38
+ Use a `model` string supported by your sference deployment (for self-hosted stacks, match the model your workers consume).
39
+
40
+ **Batches**
41
+
42
+ ```bash
43
+ sference batch submit --input-file ./workload.jsonl --model Qwen/Qwen2.5-7B-Instruct --window 24h
44
+ sference batch status --batch-id <batch_id>
45
+ sference batch wait --batch-id <batch_id>
46
+ sference batch results --batch-id <batch_id>
47
+ sference batch download-results --batch-id <batch_id> --out ./out.jsonl
48
+ # Submit, wait, print JSONL results on stdout (stderr: progress; resumable cache)
49
+ sference batch stream --input-file ./workload.jsonl --model Qwen/Qwen2.5-7B-Instruct --window 24h
50
+ ```
51
+
52
+ **Streams**
53
+
54
+ ```bash
55
+ sference stream create --name "my-stream" --window 24h
56
+ sference stream list
57
+ sference stream submit --stream-id <stream_id> --input-file ./lines.jsonl --model Qwen/Qwen2.5-7B-Instruct
58
+ sference stream status --stream-id <stream_id>
59
+ sference responses tail --stream-id <stream_id>
60
+ ```
61
+
62
+ ### cURL: OpenAI-compatible `/v1/responses`
63
+
64
+ The CLI uses this API for `stream submit` (via `POST /v1/responses`). You can call it directly with the same API key as `sference auth login` (or `SFERENCE_API_KEY`). For self-hosted APIs, set `SFERENCE_BASE_URL` to your API origin.
65
+
66
+ ```bash
67
+ export TOKEN=sk_... # or: export TOKEN="$SFERENCE_API_KEY"
68
+
69
+ RID=$(curl -sS -X POST "${SFERENCE_BASE_URL:-https://api.sference.com}/v1/responses" \
70
+ -H "X-API-Key: $TOKEN" \
71
+ -H 'Content-Type: application/json' \
72
+ -d '{
73
+ "model": "Qwen/Qwen2.5-7B-Instruct",
74
+ "input": [{"role": "user", "content": "Hello"}],
75
+ "metadata": {"completion_window": "24h"}
76
+ }' | jq -r '.id')
77
+
78
+ curl -sS "${SFERENCE_BASE_URL:-https://api.sference.com}/v1/responses/${RID}" \
79
+ -H "X-API-Key: $TOKEN"
80
+ ```
81
+
82
+ ## Environment variables
83
+
84
+ | Variable | Purpose |
85
+ |----------|---------|
86
+ | `SFERENCE_API_KEY` | API key (or JWT); overrides `~/.sference/credentials.json` |
87
+ | `SFERENCE_BASE_URL` | API base URL (default `https://api.sference.com`) |
88
+ | `SFERENCE_CONSOLE_URL` | Console URL for `auth login` browser step (default `https://console.sference.com`) |
89
+ | `SFERENCE_STREAM_CACHE` | Optional path to the stream resumable-cache file (default `~/.sference/stream_cache.json`) |
90
+ | `SFERENCE_STREAM_CHECKPOINTS` | Optional path for **`responses tail`** event checkpoints (default `~/.sference/stream_checkpoints.json`) |
91
+
92
+ ## Commands
93
+
94
+ ### Auth
95
+
96
+ | Command | Description |
97
+ |---------|-------------|
98
+ | `sference auth login` | Store an API key (optional `--api-key`, `--console-url`, `--no-browser`) |
99
+ | `sference auth me` | Show current user (`--json` for machine-readable output) |
100
+
101
+ ### Batch
102
+
103
+ | Command | Description |
104
+ |---------|-------------|
105
+ | `sference batch list` | List batches (table; `--json` for raw payload) |
106
+ | `sference batch submit` | Submit a JSONL file (`--input-file`, optional `--model` for content-only lines, `--window` must be `24h`) |
107
+ | `sference batch stream` | Submit, wait, print **JSONL results on stdout** (see below) |
108
+ | `sference batch status` | Get one batch (`--batch-id`, `--json`) |
109
+ | `sference batch wait` | Poll until terminal state (`--batch-id`, `--poll-interval`, `--timeout`, `--json`) |
110
+ | `sference batch results` | JSON results payload (`--batch-id`, `--json`) |
111
+ | `sference batch cancel` | Cancel a batch (`--batch-id`, `--json`) |
112
+ | `sference batch download-results` | Download results JSONL to a file (`--batch-id`, `--out`, `--format jsonl`) |
113
+
114
+ Global options on most batch commands: `--base-url` (default `https://api.sference.com`).
115
+
116
+ ### Responses (`/v1/responses`)
117
+
118
+ | Command | Description |
119
+ |---------|-------------|
120
+ | `sference responses create` | Create one response (`--model`, `--content`, optional `--wait`, `--poll-ms`, `--timeout-s`) |
121
+ | `sference responses result` | Poll until terminal state (`--id`, `--poll-ms`) |
122
+ | `sference responses tail` | Print completion events as JSONL via `GET /v1/responses/events` (optional `--stream-id` to scope to a stream; omit for non-stream completions). Flags: `--consumer`, `--from-latest`, `--no-checkpoint`, `--poll-ms` |
123
+
124
+ ### Stream (first-class streams API)
125
+
126
+ Long-lived **streams** are separate from **batches**: you create a stream, submit **responses** tied to it over time (`POST /v1/responses` with `metadata.stream_id`), and consume **completion events** with cursor-based pagination on **`GET /v1/responses/events`** (pass **`stream_id`** when scoping to a stream). Authenticate with your **secret API key** like other `/v1` calls.
127
+
128
+ | Command | Description |
129
+ |---------|-------------|
130
+ | `sference stream create` | Create a stream (`--name`, `--window` `1h` or `24h`, `--json`) |
131
+ | `sference stream list` | List streams (`--json`) |
132
+ | `sference stream status` | Full detail + counters (`--stream-id`, `--json`) |
133
+ | `sference stream submit` | Create responses from JSONL via `POST /v1/responses` per line (`metadata.stream_id` set automatically; `--stream-id`, `--input-file`, `--model` required for content-only lines) — per line: OpenAI batch-style `{custom_id?, method, url, body}` or content-only `{content}` |
134
+ | `sference stream cancel` | Stop accepting new items and stop enqueueing pending work; does not auto-cancel in-flight requests (`--stream-id`, `--json`) |
135
+ | `sference stream archive` | Finalize the stream (optional after cancel); no new items (`--stream-id`, `--json`) |
136
+
137
+ Example JSONL lines for `stream submit` (both accepted):
138
+
139
+ ```json
140
+ {"custom_id":"req-1","method":"POST","url":"/v1/chat/completions","body":{"model":"Qwen/Qwen3.5-4B","messages":[{"role":"user","content":"hi"}]}}
141
+ ```
142
+
143
+ ```json
144
+ {"content":"hi"}
145
+ ```
146
+
147
+ ---
148
+
149
+ ## Streaming batches (`batch stream`)
150
+
151
+ Use **`sference batch stream`** when you want a **single command** that submits a JSONL file, waits until the batch finishes, and **writes result lines to stdout** so you can pipe or redirect them.
152
+
153
+ ### Pipe-friendly UX
154
+
155
+ - **Stdout:** only the **results JSONL** (one JSON object per line, same shape as `GET /v1/batches/{id}/results.jsonl`).
156
+ - **Stderr:** status lines while waiting, e.g. `Batch batch_abc status=running (42s)`.
157
+
158
+ Example:
159
+
160
+ ```bash
161
+ sference batch stream --input-file workload.jsonl > results.jsonl
162
+ ```
163
+
164
+ Content-only JSONL (model supplied globally):
165
+
166
+ ```bash
167
+ sference batch stream --input-file prompts.jsonl --model Qwen/Qwen2.5-7B-Instruct > results.jsonl
168
+ ```
169
+
170
+ ### Resumable cache
171
+
172
+ Batches can take a long time. If you **interrupt** the command (e.g. Ctrl+C) and run it again with the **same input file contents**, the CLI **reuses the cached batch id** instead of submitting a duplicate job.
173
+
174
+ - Cache file: **`~/.sference/stream_cache.json`** (override with **`SFERENCE_STREAM_CACHE`**).
175
+ - Key: **SHA-256** of the raw input file bytes (same bytes ⇒ same key, regardless of path).
176
+ - Stored fields: `batch_id`, `base_url` (must match current `--base-url`), `created_at`.
177
+ - After results are written to stdout, the entry for that input is **removed** so the cache does not grow forever.
178
+ - If the cached batch no longer exists on the server (404), the cache entry is dropped and a **new** batch is submitted.
179
+
180
+ Force a **fresh** submission (ignore cache):
181
+
182
+ ```bash
183
+ sference batch stream --input-file workload.jsonl --no-cache > results.jsonl
184
+ ```
185
+
186
+ ### Polling
187
+
188
+ - **`--poll-interval`** (default `2`): seconds between `GET /v1/batches/{id}` polls. There is **no** built-in maximum wait time (suited to 24h-style batches).
189
+
190
+ ### Exit codes
191
+
192
+ - **0** — batch status is `completed`.
193
+ - **1** — batch status is `failed` or `cancelled` (results JSONL is still printed when available).
194
+
195
+ ### End-to-end example
196
+
197
+ ```bash
198
+ export SFERENCE_API_KEY=sk_...
199
+ sference batch stream --input-file fixtures/example_batch.jsonl --poll-interval 5 > out.jsonl
200
+ ```
201
+
202
+ ---
203
+
204
+ ## JSONL input formats
205
+
206
+ The SDK and CLI accept two line shapes (see also [`fixtures/example_batch.jsonl`](fixtures/example_batch.jsonl)):
207
+
208
+ 1. **OpenAI-compatible:** each line has `custom_id`, `method`, `url`, and `body` (e.g. chat completions payload with per-line `model`). The CLI `--model` flag is ignored for these lines (a warning may be emitted by the SDK).
209
+ 2. **Content-only:** each line is `{"content": "..."}`. Then **`--model` is required** on submit/stream.
210
+
211
+ ---
212
+
213
+ ## Python SDK
214
+
215
+ The CLI uses the sync **`SferenceClient`** from **`sference-sdk`** (`import sference_sdk`).
216
+
217
+ For your own code, see **[`../sdk-python/README.md`](../sdk-python/README.md)** for:
218
+
219
+ - **Batches (sync):** `submit_batch`, `wait_for_completion`, `get_results`
220
+ - **`/v1/responses` (sync):** `create_response`, `get_response` (standalone or `metadata.stream_id` for streams)
221
+ - **Async:** **`AsyncSferenceClient`** — same surface as sync with `await`, plus `iter_responses_events` / `list_responses_events` for completion tailing (`GET /v1/responses/events`)
222
+
223
+ That README also documents when to prefer **batches** vs **streams** and includes cURL examples.
@@ -1,11 +1,12 @@
1
1
  [project]
2
2
  name = "sference-cli"
3
- version = "0.0.1"
3
+ version = "0.0.3"
4
4
  description = "sference command-line interface"
5
+ readme = "README.md"
5
6
  requires-python = ">=3.12"
6
7
  dependencies = [
7
8
  "typer>=0.24.1",
8
- "sference-sdk>=0.1.0",
9
+ "sference-sdk>=0.0.3",
9
10
  ]
10
11
 
11
12
  [project.scripts]
@@ -19,4 +20,4 @@ build-backend = "hatchling.build"
19
20
  packages = ["sference_cli"]
20
21
 
21
22
  [tool.hatch.build.targets.sdist]
22
- include = ["sference_cli"]
23
+ include = ["sference_cli", "README.md"]
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ import logging
4
5
  import os
5
6
  import sys
6
7
  import time
@@ -18,14 +19,18 @@ from . import stream_cache as stream_cache_mod
18
19
 
19
20
  _T = TypeVar("_T")
20
21
 
22
+ _logger = logging.getLogger(__name__)
23
+
21
24
 
22
25
  app = typer.Typer(help="sference CLI", invoke_without_command=True)
23
26
  auth_app = typer.Typer(help="Auth commands", invoke_without_command=True)
24
27
  batch_app = typer.Typer(help="Batch commands", invoke_without_command=True)
25
28
  stream_app = typer.Typer(help="Stream commands", invoke_without_command=True)
29
+ responses_app = typer.Typer(help="Responses commands", invoke_without_command=True)
26
30
  app.add_typer(auth_app, name="auth")
27
31
  app.add_typer(batch_app, name="batch")
28
32
  app.add_typer(stream_app, name="stream")
33
+ app.add_typer(responses_app, name="responses")
29
34
 
30
35
  CREDENTIALS_PATH = Path.home() / ".sference" / "credentials.json"
31
36
 
@@ -52,6 +57,12 @@ def _read_token() -> str | None:
52
57
 
53
58
 
54
59
  def _client(base_url: Optional[str] = None) -> SferenceClient:
60
+ # If the caller didn't explicitly pass --base-url, allow SFERENCE_BASE_URL
61
+ # to override the default.
62
+ env_base_url = os.environ.get("SFERENCE_BASE_URL")
63
+ if env_base_url and (base_url is None or base_url == "https://api.sference.com"):
64
+ _logger.info(f"Using SFERENCE_BASE_URL: {env_base_url}")
65
+ base_url = env_base_url
55
66
  return SferenceClient(base_url=base_url, api_key=_read_token())
56
67
 
57
68
 
@@ -121,6 +132,26 @@ def _print(value: object, as_json: bool) -> None:
121
132
  typer.echo(value)
122
133
 
123
134
 
135
+ def _wait_for_response(
136
+ client: SferenceClient, response_id: str, *, poll_ms: int, timeout_s: int
137
+ ) -> Any:
138
+ """Poll GET /v1/responses/{id} until terminal status or timeout."""
139
+ deadline = time.time() + max(1, timeout_s)
140
+ last: Any = None
141
+ while True:
142
+ last = _call_api(lambda: client.get_response(response_id))
143
+ if hasattr(last, "model_dump"):
144
+ d = last.model_dump()
145
+ status = d.get("status") if isinstance(d, dict) else None
146
+ else:
147
+ status = getattr(last, "status", None)
148
+ if status in ("completed", "failed", "cancelled"):
149
+ return last
150
+ if time.time() >= deadline:
151
+ return last
152
+ time.sleep(max(10, poll_ms) / 1000.0)
153
+
154
+
124
155
  def _mvp_batch_window_only(value: str) -> str:
125
156
  if value != "24h":
126
157
  raise typer.BadParameter('Only "24h" is supported in MVP.', param_hint="--window")
@@ -165,6 +196,14 @@ def _stream_root(ctx: typer.Context) -> None:
165
196
  raise typer.Exit(code=0)
166
197
 
167
198
 
199
+ @responses_app.callback()
200
+ def _responses_root(ctx: typer.Context) -> None:
201
+ """Print help when invoked without a command."""
202
+ if ctx.invoked_subcommand is None:
203
+ typer.echo(ctx.get_help())
204
+ raise typer.Exit(code=0)
205
+
206
+
168
207
  @auth_app.command("login")
169
208
  def auth_login(
170
209
  api_key: Optional[str] = typer.Option(
@@ -172,6 +211,12 @@ def auth_login(
172
211
  "--api-key",
173
212
  help="API key (sk_...) or JWT. Non-interactive: saves immediately (e.g. CI).",
174
213
  ),
214
+ as_json: bool = typer.Option(False, "--json", help="Print authenticated user as JSON after validation."),
215
+ validate: bool = typer.Option(
216
+ True,
217
+ "--validate/--no-validate",
218
+ help="Validate the credential by calling GET /v1/auth/me and print the authenticated user.",
219
+ ),
175
220
  console_url: Optional[str] = typer.Option(
176
221
  None,
177
222
  "--console-url",
@@ -188,6 +233,16 @@ def auth_login(
188
233
  raise typer.Exit(code=1)
189
234
  _write_token(key)
190
235
  typer.echo(f"Credentials saved to {CREDENTIALS_PATH}")
236
+ if validate:
237
+ client = _client(None)
238
+ me = _call_api(client.get_me)
239
+ if as_json:
240
+ _print(me, True)
241
+ else:
242
+ if isinstance(me, dict) and "username" in me and "role" in me:
243
+ typer.echo(f"Authenticated as {me['username']} ({me['role']}).")
244
+ else:
245
+ typer.echo(f"Authenticated. {me}")
191
246
  return
192
247
 
193
248
  base = (console_url or DEFAULT_CONSOLE_URL).rstrip("/")
@@ -213,6 +268,16 @@ def auth_login(
213
268
  raise typer.Exit(code=1)
214
269
  _write_token(token.strip())
215
270
  typer.echo(f"Credentials saved to {CREDENTIALS_PATH}")
271
+ if validate:
272
+ client = _client(None)
273
+ me = _call_api(client.get_me)
274
+ if as_json:
275
+ _print(me, True)
276
+ else:
277
+ if isinstance(me, dict) and "username" in me and "role" in me:
278
+ typer.echo(f"Authenticated as {me['username']} ({me['role']}).")
279
+ else:
280
+ typer.echo(f"Authenticated. {me}")
216
281
 
217
282
 
218
283
  @auth_app.command("me")
@@ -226,6 +291,113 @@ def auth_me(
226
291
  _print(me, as_json)
227
292
 
228
293
 
294
+ @responses_app.command("create")
295
+ def responses_create(
296
+ model: str = typer.Option(..., "--model"),
297
+ content: str = typer.Option(..., "--content"),
298
+ wait: bool = typer.Option(False, "--wait/--no-wait"),
299
+ poll_ms: int = typer.Option(500, "--poll-ms", help="Polling interval when --wait is enabled."),
300
+ timeout_s: int = typer.Option(60, "--timeout-s", help="Timeout in seconds when --wait is enabled."),
301
+ base_url: str = typer.Option("https://api.sference.com"),
302
+ ) -> None:
303
+ _ensure_api_credential()
304
+ client = _client(base_url)
305
+ resp = _call_api(
306
+ lambda: client.create_response(model=model, input=[{"role": "user", "content": content}])
307
+ )
308
+ if wait:
309
+ final = _wait_for_response(client, resp.id, poll_ms=poll_ms, timeout_s=timeout_s)
310
+ if getattr(final, "status", None) not in ("completed", "failed", "cancelled"):
311
+ _print(final.model_dump(), True)
312
+ raise typer.Exit(code=1)
313
+ resp = final
314
+ _print(resp.model_dump(), True)
315
+
316
+
317
+ @responses_app.command("result")
318
+ def responses_result(
319
+ id: str = typer.Option(..., "--id"),
320
+ poll_ms: int = typer.Option(500, "--poll-ms", help="Polling interval while waiting for completion."),
321
+ base_url: str = typer.Option("https://api.sference.com"),
322
+ ) -> None:
323
+ """Wait for a response to reach a terminal status and print the final JSON.
324
+
325
+ Use Ctrl-C to stop waiting.
326
+ """
327
+ _ensure_api_credential()
328
+ client = _client(base_url)
329
+ try:
330
+ while True:
331
+ resp = _call_api(lambda: client.get_response(id))
332
+ d = resp.model_dump()
333
+ status = d.get("status") if isinstance(d, dict) else None
334
+ if status in ("completed", "failed", "cancelled"):
335
+ _print(d, True)
336
+ return
337
+ time.sleep(max(10, poll_ms) / 1000.0)
338
+ except KeyboardInterrupt:
339
+ raise typer.Exit(code=130) from None
340
+
341
+
342
+ @responses_app.command("tail")
343
+ def responses_tail(
344
+ stream_id: Optional[str] = typer.Option(
345
+ None,
346
+ "--stream-id",
347
+ help="Optional stream UUID; omit to tail non-stream completion events only.",
348
+ ),
349
+ consumer: str = typer.Option("default", "--consumer", help="Checkpoint namespace for resume."),
350
+ from_latest: bool = typer.Option(
351
+ False,
352
+ "--from-latest",
353
+ help="Ignore saved checkpoint and start from the latest events page.",
354
+ ),
355
+ no_checkpoint: bool = typer.Option(False, "--no-checkpoint", help="Do not read or write checkpoints."),
356
+ poll_ms: int = typer.Option(5000, "--poll-ms", help="Long-poll wait_ms when catching up (max 30000)."),
357
+ base_url: str = typer.Option("https://api.sference.com"),
358
+ as_json: bool = typer.Option(False, "--json"),
359
+ ) -> None:
360
+ """Print completion events as JSONL (GET /v1/responses/events). Ctrl-C to stop.
361
+
362
+ Omit --stream-id for standalone/batch completions; pass --stream-id to scope to a stream.
363
+ """
364
+ _ = as_json
365
+ _ensure_api_credential()
366
+ client = _client(base_url)
367
+ bu = base_url.rstrip("/")
368
+ ck = stream_id if stream_id else "__responses_events__"
369
+
370
+ if from_latest and not no_checkpoint:
371
+ clear_checkpoint(bu, ck, consumer)
372
+
373
+ last: str | None = None
374
+ if not no_checkpoint and not from_latest:
375
+ last = load_checkpoint(bu, ck, consumer)
376
+
377
+ try:
378
+ while True:
379
+
380
+ def fetch() -> Any:
381
+ return client.list_responses_events(
382
+ stream_id=stream_id,
383
+ limit=50,
384
+ starting_after=last,
385
+ wait_ms=min(poll_ms, 30000),
386
+ )
387
+
388
+ page = _call_api(fetch)
389
+ if not page.data:
390
+ time.sleep(max(1, poll_ms) / 1000.0)
391
+ continue
392
+ for ev in page.data:
393
+ typer.echo(json.dumps(ev.model_dump(), default=str))
394
+ last = ev.completion_id
395
+ if not no_checkpoint:
396
+ save_checkpoint(bu, ck, consumer, ev.completion_id)
397
+ except KeyboardInterrupt:
398
+ typer.echo("Interrupted.", err=True)
399
+ raise typer.Exit(code=130) from None
400
+
229
401
  @batch_app.command("list")
230
402
  def batch_list(
231
403
  base_url: str = typer.Option("https://api.sference.com"),
@@ -519,7 +691,8 @@ def stream_submit(
519
691
  typer.echo(f"Unrecognized line format: {line[:100]}", err=True)
520
692
  raise typer.Exit(code=1)
521
693
 
522
- resp = _call_api(lambda: client.create_response(body))
694
+ # SDK signature is keyword-only; pass the payload as kwargs.
695
+ resp = _call_api(lambda: client.create_response(**body))
523
696
  created_responses.append(resp)
524
697
 
525
698
  # Return stream detail-like response for compatibility
@@ -556,58 +729,6 @@ def stream_cancel(
556
729
  _print(st.model_dump(), as_json)
557
730
 
558
731
 
559
- @stream_app.command("tail")
560
- def stream_tail(
561
- stream_id: str = typer.Option(..., "--stream-id"),
562
- consumer: str = typer.Option("default", "--consumer", help="Checkpoint namespace for resume."),
563
- from_latest: bool = typer.Option(
564
- False,
565
- "--from-latest",
566
- help="Ignore saved checkpoint and start from the latest events page.",
567
- ),
568
- no_checkpoint: bool = typer.Option(False, "--no-checkpoint", help="Do not read or write checkpoints."),
569
- poll_ms: int = typer.Option(5000, "--poll-ms", help="Long-poll wait_ms when catching up (max 30000)."),
570
- base_url: str = typer.Option("https://api.sference.com"),
571
- as_json: bool = typer.Option(False, "--json"),
572
- ) -> None:
573
- """Print stream result events as JSONL (stdout). Checkpoints after each event unless --no-checkpoint."""
574
- _ = as_json
575
- _ensure_api_credential()
576
- client = _client(base_url)
577
- bu = base_url.rstrip("/")
578
-
579
- if from_latest and not no_checkpoint:
580
- clear_checkpoint(bu, stream_id, consumer)
581
-
582
- last: str | None = None
583
- if not no_checkpoint and not from_latest:
584
- last = load_checkpoint(bu, stream_id, consumer)
585
-
586
- try:
587
- while True:
588
-
589
- def fetch() -> Any:
590
- return client.list_stream_events(
591
- stream_id,
592
- limit=50,
593
- starting_after=last,
594
- wait_ms=min(poll_ms, 30000),
595
- )
596
-
597
- page = _call_api(fetch)
598
- if not page.data:
599
- time.sleep(max(1, poll_ms) / 1000.0)
600
- continue
601
- for ev in page.data:
602
- typer.echo(json.dumps(ev.model_dump(), default=str))
603
- last = ev.completion_id
604
- if not no_checkpoint:
605
- save_checkpoint(bu, stream_id, consumer, ev.completion_id)
606
- except KeyboardInterrupt:
607
- typer.echo("Interrupted.", err=True)
608
- raise typer.Exit(code=130) from None
609
-
610
-
611
732
  @batch_app.command("download-results")
612
733
  def batch_download_results(
613
734
  batch_id: str = typer.Option(..., "--batch-id"),
@@ -1,7 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: sference-cli
3
- Version: 0.0.1
4
- Summary: sference command-line interface
5
- Requires-Python: >=3.12
6
- Requires-Dist: sference-sdk>=0.1.0
7
- Requires-Dist: typer>=0.24.1