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.
- {sference_cli-0.0.1 → sference_cli-0.0.3}/.gitignore +2 -0
- sference_cli-0.0.3/PKG-INFO +232 -0
- sference_cli-0.0.3/README.md +223 -0
- {sference_cli-0.0.1 → sference_cli-0.0.3}/pyproject.toml +4 -3
- {sference_cli-0.0.1 → sference_cli-0.0.3}/sference_cli/main.py +174 -53
- sference_cli-0.0.1/PKG-INFO +0 -7
- {sference_cli-0.0.1 → sference_cli-0.0.3}/sference_cli/__init__.py +0 -0
- {sference_cli-0.0.1 → sference_cli-0.0.3}/sference_cli/stream_cache.py +0 -0
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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"),
|
sference_cli-0.0.1/PKG-INFO
DELETED
|
File without changes
|
|
File without changes
|