sference-cli 0.0.2__tar.gz → 0.0.4__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.2 → sference_cli-0.0.4}/.gitignore +2 -0
- sference_cli-0.0.2/README.md → sference_cli-0.0.4/PKG-INFO +21 -5
- sference_cli-0.0.2/PKG-INFO → sference_cli-0.0.4/README.md +12 -14
- {sference_cli-0.0.2 → sference_cli-0.0.4}/pyproject.toml +2 -2
- {sference_cli-0.0.2 → sference_cli-0.0.4}/sference_cli/main.py +183 -53
- {sference_cli-0.0.2 → sference_cli-0.0.4}/sference_cli/__init__.py +0 -0
- {sference_cli-0.0.2 → sference_cli-0.0.4}/sference_cli/stream_cache.py +0 -0
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sference-cli
|
|
3
|
+
Version: 0.0.4
|
|
4
|
+
Summary: sference command-line interface
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: sference-sdk>=0.0.4
|
|
7
|
+
Requires-Dist: typer>=0.24.1
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
1
10
|
# sference CLI
|
|
2
11
|
|
|
3
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`.
|
|
@@ -56,7 +65,7 @@ sference stream create --name "my-stream" --window 24h
|
|
|
56
65
|
sference stream list
|
|
57
66
|
sference stream submit --stream-id <stream_id> --input-file ./lines.jsonl --model Qwen/Qwen2.5-7B-Instruct
|
|
58
67
|
sference stream status --stream-id <stream_id>
|
|
59
|
-
sference
|
|
68
|
+
sference responses tail --stream-id <stream_id>
|
|
60
69
|
```
|
|
61
70
|
|
|
62
71
|
### cURL: OpenAI-compatible `/v1/responses`
|
|
@@ -87,7 +96,7 @@ curl -sS "${SFERENCE_BASE_URL:-https://api.sference.com}/v1/responses/${RID}" \
|
|
|
87
96
|
| `SFERENCE_BASE_URL` | API base URL (default `https://api.sference.com`) |
|
|
88
97
|
| `SFERENCE_CONSOLE_URL` | Console URL for `auth login` browser step (default `https://console.sference.com`) |
|
|
89
98
|
| `SFERENCE_STREAM_CACHE` | Optional path to the stream resumable-cache file (default `~/.sference/stream_cache.json`) |
|
|
90
|
-
| `SFERENCE_STREAM_CHECKPOINTS` | Optional path for
|
|
99
|
+
| `SFERENCE_STREAM_CHECKPOINTS` | Optional path for **`responses tail`** event checkpoints (default `~/.sference/stream_checkpoints.json`) |
|
|
91
100
|
|
|
92
101
|
## Commands
|
|
93
102
|
|
|
@@ -113,9 +122,17 @@ curl -sS "${SFERENCE_BASE_URL:-https://api.sference.com}/v1/responses/${RID}" \
|
|
|
113
122
|
|
|
114
123
|
Global options on most batch commands: `--base-url` (default `https://api.sference.com`).
|
|
115
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
|
+
|
|
116
133
|
### Stream (first-class streams API)
|
|
117
134
|
|
|
118
|
-
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
|
|
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.
|
|
119
136
|
|
|
120
137
|
| Command | Description |
|
|
121
138
|
|---------|-------------|
|
|
@@ -125,7 +142,6 @@ Long-lived **streams** are separate from **batches**: you create a stream, submi
|
|
|
125
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}` |
|
|
126
143
|
| `sference stream cancel` | Stop accepting new items and stop enqueueing pending work; does not auto-cancel in-flight requests (`--stream-id`, `--json`) |
|
|
127
144
|
| `sference stream archive` | Finalize the stream (optional after cancel); no new items (`--stream-id`, `--json`) |
|
|
128
|
-
| `sference stream tail` | Print events as JSONL; checkpoints after each event (override with `SFERENCE_STREAM_CHECKPOINTS`). Flags: `--consumer`, `--from-latest`, `--no-checkpoint`, `--poll-ms` |
|
|
129
145
|
|
|
130
146
|
Example JSONL lines for `stream submit` (both accepted):
|
|
131
147
|
|
|
@@ -211,6 +227,6 @@ For your own code, see **[`../sdk-python/README.md`](../sdk-python/README.md)**
|
|
|
211
227
|
|
|
212
228
|
- **Batches (sync):** `submit_batch`, `wait_for_completion`, `get_results`
|
|
213
229
|
- **`/v1/responses` (sync):** `create_response`, `get_response` (standalone or `metadata.stream_id` for streams)
|
|
214
|
-
- **Async:** **`AsyncSferenceClient`** — same surface as sync with `await`, plus `
|
|
230
|
+
- **Async:** **`AsyncSferenceClient`** — same surface as sync with `await`, plus `iter_responses_events` / `list_responses_events` for completion tailing (`GET /v1/responses/events`)
|
|
215
231
|
|
|
216
232
|
That README also documents when to prefer **batches** vs **streams** and includes cURL examples.
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: sference-cli
|
|
3
|
-
Version: 0.0.2
|
|
4
|
-
Summary: sference command-line interface
|
|
5
|
-
Requires-Python: >=3.12
|
|
6
|
-
Requires-Dist: sference-sdk>=0.0.1
|
|
7
|
-
Requires-Dist: typer>=0.24.1
|
|
8
|
-
Description-Content-Type: text/markdown
|
|
9
|
-
|
|
10
1
|
# sference CLI
|
|
11
2
|
|
|
12
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`.
|
|
@@ -65,7 +56,7 @@ sference stream create --name "my-stream" --window 24h
|
|
|
65
56
|
sference stream list
|
|
66
57
|
sference stream submit --stream-id <stream_id> --input-file ./lines.jsonl --model Qwen/Qwen2.5-7B-Instruct
|
|
67
58
|
sference stream status --stream-id <stream_id>
|
|
68
|
-
sference
|
|
59
|
+
sference responses tail --stream-id <stream_id>
|
|
69
60
|
```
|
|
70
61
|
|
|
71
62
|
### cURL: OpenAI-compatible `/v1/responses`
|
|
@@ -96,7 +87,7 @@ curl -sS "${SFERENCE_BASE_URL:-https://api.sference.com}/v1/responses/${RID}" \
|
|
|
96
87
|
| `SFERENCE_BASE_URL` | API base URL (default `https://api.sference.com`) |
|
|
97
88
|
| `SFERENCE_CONSOLE_URL` | Console URL for `auth login` browser step (default `https://console.sference.com`) |
|
|
98
89
|
| `SFERENCE_STREAM_CACHE` | Optional path to the stream resumable-cache file (default `~/.sference/stream_cache.json`) |
|
|
99
|
-
| `SFERENCE_STREAM_CHECKPOINTS` | Optional path for
|
|
90
|
+
| `SFERENCE_STREAM_CHECKPOINTS` | Optional path for **`responses tail`** event checkpoints (default `~/.sference/stream_checkpoints.json`) |
|
|
100
91
|
|
|
101
92
|
## Commands
|
|
102
93
|
|
|
@@ -122,9 +113,17 @@ curl -sS "${SFERENCE_BASE_URL:-https://api.sference.com}/v1/responses/${RID}" \
|
|
|
122
113
|
|
|
123
114
|
Global options on most batch commands: `--base-url` (default `https://api.sference.com`).
|
|
124
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
|
+
|
|
125
124
|
### Stream (first-class streams API)
|
|
126
125
|
|
|
127
|
-
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
|
|
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.
|
|
128
127
|
|
|
129
128
|
| Command | Description |
|
|
130
129
|
|---------|-------------|
|
|
@@ -134,7 +133,6 @@ Long-lived **streams** are separate from **batches**: you create a stream, submi
|
|
|
134
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}` |
|
|
135
134
|
| `sference stream cancel` | Stop accepting new items and stop enqueueing pending work; does not auto-cancel in-flight requests (`--stream-id`, `--json`) |
|
|
136
135
|
| `sference stream archive` | Finalize the stream (optional after cancel); no new items (`--stream-id`, `--json`) |
|
|
137
|
-
| `sference stream tail` | Print events as JSONL; checkpoints after each event (override with `SFERENCE_STREAM_CHECKPOINTS`). Flags: `--consumer`, `--from-latest`, `--no-checkpoint`, `--poll-ms` |
|
|
138
136
|
|
|
139
137
|
Example JSONL lines for `stream submit` (both accepted):
|
|
140
138
|
|
|
@@ -220,6 +218,6 @@ For your own code, see **[`../sdk-python/README.md`](../sdk-python/README.md)**
|
|
|
220
218
|
|
|
221
219
|
- **Batches (sync):** `submit_batch`, `wait_for_completion`, `get_results`
|
|
222
220
|
- **`/v1/responses` (sync):** `create_response`, `get_response` (standalone or `metadata.stream_id` for streams)
|
|
223
|
-
- **Async:** **`AsyncSferenceClient`** — same surface as sync with `await`, plus `
|
|
221
|
+
- **Async:** **`AsyncSferenceClient`** — same surface as sync with `await`, plus `iter_responses_events` / `list_responses_events` for completion tailing (`GET /v1/responses/events`)
|
|
224
222
|
|
|
225
223
|
That README also documents when to prefer **batches** vs **streams** and includes cURL examples.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sference-cli"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.4"
|
|
4
4
|
description = "sference command-line interface"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
7
7
|
dependencies = [
|
|
8
8
|
"typer>=0.24.1",
|
|
9
|
-
"sference-sdk>=0.0.
|
|
9
|
+
"sference-sdk>=0.0.4",
|
|
10
10
|
]
|
|
11
11
|
|
|
12
12
|
[project.scripts]
|
|
@@ -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,122 @@ 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
|
+
include_reasoning: bool = typer.Option(
|
|
299
|
+
True,
|
|
300
|
+
"--include-reasoning/--no-include-reasoning",
|
|
301
|
+
help="Include extracted reasoning in the response output (default: true).",
|
|
302
|
+
),
|
|
303
|
+
wait: bool = typer.Option(False, "--wait/--no-wait"),
|
|
304
|
+
poll_ms: int = typer.Option(500, "--poll-ms", help="Polling interval when --wait is enabled."),
|
|
305
|
+
timeout_s: int = typer.Option(60, "--timeout-s", help="Timeout in seconds when --wait is enabled."),
|
|
306
|
+
base_url: str = typer.Option("https://api.sference.com"),
|
|
307
|
+
) -> None:
|
|
308
|
+
_ensure_api_credential()
|
|
309
|
+
client = _client(base_url)
|
|
310
|
+
resp = _call_api(
|
|
311
|
+
lambda: client.create_response(
|
|
312
|
+
model=model,
|
|
313
|
+
input=[{"role": "user", "content": content}],
|
|
314
|
+
include_reasoning=include_reasoning,
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
if wait:
|
|
318
|
+
final = _wait_for_response(client, resp.id, poll_ms=poll_ms, timeout_s=timeout_s)
|
|
319
|
+
if getattr(final, "status", None) not in ("completed", "failed", "cancelled"):
|
|
320
|
+
_print(final.model_dump(), True)
|
|
321
|
+
raise typer.Exit(code=1)
|
|
322
|
+
resp = final
|
|
323
|
+
_print(resp.model_dump(), True)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@responses_app.command("result")
|
|
327
|
+
def responses_result(
|
|
328
|
+
id: str = typer.Option(..., "--id"),
|
|
329
|
+
poll_ms: int = typer.Option(500, "--poll-ms", help="Polling interval while waiting for completion."),
|
|
330
|
+
base_url: str = typer.Option("https://api.sference.com"),
|
|
331
|
+
) -> None:
|
|
332
|
+
"""Wait for a response to reach a terminal status and print the final JSON.
|
|
333
|
+
|
|
334
|
+
Use Ctrl-C to stop waiting.
|
|
335
|
+
"""
|
|
336
|
+
_ensure_api_credential()
|
|
337
|
+
client = _client(base_url)
|
|
338
|
+
try:
|
|
339
|
+
while True:
|
|
340
|
+
resp = _call_api(lambda: client.get_response(id))
|
|
341
|
+
d = resp.model_dump()
|
|
342
|
+
status = d.get("status") if isinstance(d, dict) else None
|
|
343
|
+
if status in ("completed", "failed", "cancelled"):
|
|
344
|
+
_print(d, True)
|
|
345
|
+
return
|
|
346
|
+
time.sleep(max(10, poll_ms) / 1000.0)
|
|
347
|
+
except KeyboardInterrupt:
|
|
348
|
+
raise typer.Exit(code=130) from None
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@responses_app.command("tail")
|
|
352
|
+
def responses_tail(
|
|
353
|
+
stream_id: Optional[str] = typer.Option(
|
|
354
|
+
None,
|
|
355
|
+
"--stream-id",
|
|
356
|
+
help="Optional stream UUID; omit to tail non-stream completion events only.",
|
|
357
|
+
),
|
|
358
|
+
consumer: str = typer.Option("default", "--consumer", help="Checkpoint namespace for resume."),
|
|
359
|
+
from_latest: bool = typer.Option(
|
|
360
|
+
False,
|
|
361
|
+
"--from-latest",
|
|
362
|
+
help="Ignore saved checkpoint and start from the latest events page.",
|
|
363
|
+
),
|
|
364
|
+
no_checkpoint: bool = typer.Option(False, "--no-checkpoint", help="Do not read or write checkpoints."),
|
|
365
|
+
poll_ms: int = typer.Option(5000, "--poll-ms", help="Long-poll wait_ms when catching up (max 30000)."),
|
|
366
|
+
base_url: str = typer.Option("https://api.sference.com"),
|
|
367
|
+
as_json: bool = typer.Option(False, "--json"),
|
|
368
|
+
) -> None:
|
|
369
|
+
"""Print completion events as JSONL (GET /v1/responses/events). Ctrl-C to stop.
|
|
370
|
+
|
|
371
|
+
Omit --stream-id for standalone/batch completions; pass --stream-id to scope to a stream.
|
|
372
|
+
"""
|
|
373
|
+
_ = as_json
|
|
374
|
+
_ensure_api_credential()
|
|
375
|
+
client = _client(base_url)
|
|
376
|
+
bu = base_url.rstrip("/")
|
|
377
|
+
ck = stream_id if stream_id else "__responses_events__"
|
|
378
|
+
|
|
379
|
+
if from_latest and not no_checkpoint:
|
|
380
|
+
clear_checkpoint(bu, ck, consumer)
|
|
381
|
+
|
|
382
|
+
last: str | None = None
|
|
383
|
+
if not no_checkpoint and not from_latest:
|
|
384
|
+
last = load_checkpoint(bu, ck, consumer)
|
|
385
|
+
|
|
386
|
+
try:
|
|
387
|
+
while True:
|
|
388
|
+
|
|
389
|
+
def fetch() -> Any:
|
|
390
|
+
return client.list_responses_events(
|
|
391
|
+
stream_id=stream_id,
|
|
392
|
+
limit=50,
|
|
393
|
+
starting_after=last,
|
|
394
|
+
wait_ms=min(poll_ms, 30000),
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
page = _call_api(fetch)
|
|
398
|
+
if not page.data:
|
|
399
|
+
time.sleep(max(1, poll_ms) / 1000.0)
|
|
400
|
+
continue
|
|
401
|
+
for ev in page.data:
|
|
402
|
+
typer.echo(json.dumps(ev.model_dump(), default=str))
|
|
403
|
+
last = ev.completion_id
|
|
404
|
+
if not no_checkpoint:
|
|
405
|
+
save_checkpoint(bu, ck, consumer, ev.completion_id)
|
|
406
|
+
except KeyboardInterrupt:
|
|
407
|
+
typer.echo("Interrupted.", err=True)
|
|
408
|
+
raise typer.Exit(code=130) from None
|
|
409
|
+
|
|
229
410
|
@batch_app.command("list")
|
|
230
411
|
def batch_list(
|
|
231
412
|
base_url: str = typer.Option("https://api.sference.com"),
|
|
@@ -519,7 +700,8 @@ def stream_submit(
|
|
|
519
700
|
typer.echo(f"Unrecognized line format: {line[:100]}", err=True)
|
|
520
701
|
raise typer.Exit(code=1)
|
|
521
702
|
|
|
522
|
-
|
|
703
|
+
# SDK signature is keyword-only; pass the payload as kwargs.
|
|
704
|
+
resp = _call_api(lambda: client.create_response(**body))
|
|
523
705
|
created_responses.append(resp)
|
|
524
706
|
|
|
525
707
|
# Return stream detail-like response for compatibility
|
|
@@ -556,58 +738,6 @@ def stream_cancel(
|
|
|
556
738
|
_print(st.model_dump(), as_json)
|
|
557
739
|
|
|
558
740
|
|
|
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
741
|
@batch_app.command("download-results")
|
|
612
742
|
def batch_download_results(
|
|
613
743
|
batch_id: str = typer.Option(..., "--batch-id"),
|
|
File without changes
|
|
File without changes
|