sference-cli 0.0.6__tar.gz → 0.0.8__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.6/README.md → sference_cli-0.0.8/PKG-INFO +20 -1
- sference_cli-0.0.6/PKG-INFO → sference_cli-0.0.8/README.md +11 -10
- {sference_cli-0.0.6 → sference_cli-0.0.8}/pyproject.toml +3 -3
- {sference_cli-0.0.6 → sference_cli-0.0.8}/sference_cli/main.py +60 -37
- {sference_cli-0.0.6 → sference_cli-0.0.8}/.gitignore +0 -0
- {sference_cli-0.0.6 → sference_cli-0.0.8}/sference_cli/__init__.py +0 -0
- {sference_cli-0.0.6 → sference_cli-0.0.8}/sference_cli/stream_cache.py +0 -0
|
@@ -1,9 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sference-cli
|
|
3
|
+
Version: 0.0.8
|
|
4
|
+
Summary: sference command-line interface
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: sference-sdk>=0.0.8
|
|
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`.
|
|
4
13
|
|
|
5
14
|
## Installation
|
|
6
15
|
|
|
16
|
+
```bash
|
|
17
|
+
# One-line install (macOS / Linux)
|
|
18
|
+
curl -fsSL https://raw.githubusercontent.com/s-ference/sference/main/install.sh | sh
|
|
19
|
+
|
|
20
|
+
# Windows (PowerShell)
|
|
21
|
+
powershell -ExecutionPolicy ByPass -c "irm https://raw.githubusercontent.com/s-ference/sference/main/install.ps1 | iex"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or install from PyPI:
|
|
25
|
+
|
|
7
26
|
```bash
|
|
8
27
|
pip install sference-cli
|
|
9
28
|
# or isolated install on PATH:
|
|
@@ -85,7 +104,7 @@ curl -sS "${SFERENCE_BASE_URL:-https://api.sference.com}/v1/responses/${RID}" \
|
|
|
85
104
|
|----------|---------|
|
|
86
105
|
| `SFERENCE_API_KEY` | API key (or JWT); overrides `~/.sference/credentials.json` |
|
|
87
106
|
| `SFERENCE_BASE_URL` | API base URL (default `https://api.sference.com`) |
|
|
88
|
-
| `SFERENCE_CONSOLE_URL` | Console URL for `auth login` browser step (default `https://
|
|
107
|
+
| `SFERENCE_CONSOLE_URL` | Console URL for `auth login` browser step (default `https://app.sference.com`) |
|
|
89
108
|
| `SFERENCE_STREAM_CACHE` | Optional path to the stream resumable-cache file (default `~/.sference/stream_cache.json`) |
|
|
90
109
|
| `SFERENCE_STREAM_CHECKPOINTS` | Optional path for **`responses tail`** event checkpoints (default `~/.sference/stream_checkpoints.json`) |
|
|
91
110
|
|
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: sference-cli
|
|
3
|
-
Version: 0.0.6
|
|
4
|
-
Summary: sference command-line interface
|
|
5
|
-
Requires-Python: >=3.12
|
|
6
|
-
Requires-Dist: sference-sdk>=0.0.6
|
|
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`.
|
|
13
4
|
|
|
14
5
|
## Installation
|
|
15
6
|
|
|
7
|
+
```bash
|
|
8
|
+
# One-line install (macOS / Linux)
|
|
9
|
+
curl -fsSL https://raw.githubusercontent.com/s-ference/sference/main/install.sh | sh
|
|
10
|
+
|
|
11
|
+
# Windows (PowerShell)
|
|
12
|
+
powershell -ExecutionPolicy ByPass -c "irm https://raw.githubusercontent.com/s-ference/sference/main/install.ps1 | iex"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or install from PyPI:
|
|
16
|
+
|
|
16
17
|
```bash
|
|
17
18
|
pip install sference-cli
|
|
18
19
|
# or isolated install on PATH:
|
|
@@ -94,7 +95,7 @@ curl -sS "${SFERENCE_BASE_URL:-https://api.sference.com}/v1/responses/${RID}" \
|
|
|
94
95
|
|----------|---------|
|
|
95
96
|
| `SFERENCE_API_KEY` | API key (or JWT); overrides `~/.sference/credentials.json` |
|
|
96
97
|
| `SFERENCE_BASE_URL` | API base URL (default `https://api.sference.com`) |
|
|
97
|
-
| `SFERENCE_CONSOLE_URL` | Console URL for `auth login` browser step (default `https://
|
|
98
|
+
| `SFERENCE_CONSOLE_URL` | Console URL for `auth login` browser step (default `https://app.sference.com`) |
|
|
98
99
|
| `SFERENCE_STREAM_CACHE` | Optional path to the stream resumable-cache file (default `~/.sference/stream_cache.json`) |
|
|
99
100
|
| `SFERENCE_STREAM_CHECKPOINTS` | Optional path for **`responses tail`** event checkpoints (default `~/.sference/stream_checkpoints.json`) |
|
|
100
101
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sference-cli"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.8"
|
|
4
4
|
description = "sference command-line interface"
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
7
|
dependencies = [
|
|
8
8
|
"typer>=0.24.1",
|
|
9
|
-
"sference-sdk>=0.0.
|
|
9
|
+
"sference-sdk>=0.0.8",
|
|
10
10
|
]
|
|
11
11
|
|
|
12
12
|
[project.scripts]
|
|
@@ -30,14 +30,16 @@ auth_app = typer.Typer(help="Auth commands", invoke_without_command=True)
|
|
|
30
30
|
batch_app = typer.Typer(help="Batch commands", invoke_without_command=True)
|
|
31
31
|
stream_app = typer.Typer(help="Stream commands", invoke_without_command=True)
|
|
32
32
|
responses_app = typer.Typer(help="Responses commands", invoke_without_command=True)
|
|
33
|
+
models_app = typer.Typer(help="List models (GET /v1/models)", invoke_without_command=True)
|
|
33
34
|
app.add_typer(auth_app, name="auth")
|
|
34
35
|
app.add_typer(batch_app, name="batch")
|
|
35
36
|
app.add_typer(stream_app, name="stream")
|
|
36
37
|
app.add_typer(responses_app, name="responses")
|
|
38
|
+
app.add_typer(models_app, name="models")
|
|
37
39
|
|
|
38
40
|
CREDENTIALS_PATH = Path.home() / ".sference" / "credentials.json"
|
|
39
41
|
|
|
40
|
-
DEFAULT_CONSOLE_URL = "https://
|
|
42
|
+
DEFAULT_CONSOLE_URL = "https://app.sference.com"
|
|
41
43
|
|
|
42
44
|
|
|
43
45
|
def _write_token(token: str) -> None:
|
|
@@ -59,14 +61,14 @@ def _read_token() -> str | None:
|
|
|
59
61
|
return None
|
|
60
62
|
|
|
61
63
|
|
|
62
|
-
def _client(base_url: Optional[str] = None) -> SferenceClient:
|
|
64
|
+
def _client(base_url: Optional[str] = None, *, timeout: float | None = None) -> SferenceClient:
|
|
63
65
|
# If the caller didn't explicitly pass --base-url, allow SFERENCE_BASE_URL
|
|
64
66
|
# to override the default.
|
|
65
67
|
env_base_url = os.environ.get("SFERENCE_BASE_URL")
|
|
66
68
|
if env_base_url and (base_url is None or base_url == "https://api.sference.com"):
|
|
67
69
|
_logger.info(f"Using SFERENCE_BASE_URL: {env_base_url}")
|
|
68
70
|
base_url = env_base_url
|
|
69
|
-
return SferenceClient(base_url=base_url, api_key=_read_token())
|
|
71
|
+
return SferenceClient(base_url=base_url, api_key=_read_token(), timeout=timeout)
|
|
70
72
|
|
|
71
73
|
|
|
72
74
|
def _ensure_api_credential() -> None:
|
|
@@ -135,24 +137,9 @@ def _print(value: object, as_json: bool) -> None:
|
|
|
135
137
|
typer.echo(value)
|
|
136
138
|
|
|
137
139
|
|
|
138
|
-
def
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
"""Poll GET /v1/responses/{id} until terminal status or timeout."""
|
|
142
|
-
deadline = time.time() + max(1, timeout_s)
|
|
143
|
-
last: Any = None
|
|
144
|
-
while True:
|
|
145
|
-
last = _call_api(lambda: client.get_response(response_id))
|
|
146
|
-
if hasattr(last, "model_dump"):
|
|
147
|
-
d = last.model_dump()
|
|
148
|
-
status = d.get("status") if isinstance(d, dict) else None
|
|
149
|
-
else:
|
|
150
|
-
status = getattr(last, "status", None)
|
|
151
|
-
if status in ("completed", "failed", "cancelled"):
|
|
152
|
-
return last
|
|
153
|
-
if time.time() >= deadline:
|
|
154
|
-
return last
|
|
155
|
-
time.sleep(max(10, poll_ms) / 1000.0)
|
|
140
|
+
def _list_models(*, client: SferenceClient, as_json: bool) -> None:
|
|
141
|
+
payload = _call_api(client.list_models)
|
|
142
|
+
typer.echo(json.dumps(payload, indent=None if as_json else 2))
|
|
156
143
|
|
|
157
144
|
|
|
158
145
|
def _mvp_batch_window_only(value: str) -> str:
|
|
@@ -219,6 +206,31 @@ def _responses_root(ctx: typer.Context) -> None:
|
|
|
219
206
|
raise typer.Exit(code=0)
|
|
220
207
|
|
|
221
208
|
|
|
209
|
+
@models_app.callback()
|
|
210
|
+
def _models_root(
|
|
211
|
+
ctx: typer.Context,
|
|
212
|
+
as_json: bool = typer.Option(False, "--json", help="Print GET /v1/models JSON."),
|
|
213
|
+
base_url: str = typer.Option("https://api.sference.com"),
|
|
214
|
+
) -> None:
|
|
215
|
+
"""List models (OpenAI-compatible GET /v1/models)."""
|
|
216
|
+
if ctx.invoked_subcommand is not None:
|
|
217
|
+
return
|
|
218
|
+
_ensure_api_credential()
|
|
219
|
+
client = _client(base_url)
|
|
220
|
+
_list_models(client=client, as_json=as_json)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@models_app.command("list")
|
|
224
|
+
def models_list(
|
|
225
|
+
as_json: bool = typer.Option(False, "--json", help="Print GET /v1/models JSON."),
|
|
226
|
+
base_url: str = typer.Option("https://api.sference.com"),
|
|
227
|
+
) -> None:
|
|
228
|
+
"""List models (OpenAI-compatible GET /v1/models)."""
|
|
229
|
+
_ensure_api_credential()
|
|
230
|
+
client = _client(base_url)
|
|
231
|
+
_list_models(client=client, as_json=as_json)
|
|
232
|
+
|
|
233
|
+
|
|
222
234
|
@auth_app.command("login")
|
|
223
235
|
def auth_login(
|
|
224
236
|
api_key: Optional[str] = typer.Option(
|
|
@@ -236,7 +248,7 @@ def auth_login(
|
|
|
236
248
|
None,
|
|
237
249
|
"--console-url",
|
|
238
250
|
envvar="SFERENCE_CONSOLE_URL",
|
|
239
|
-
help="Console base URL for the browser step (default: https://
|
|
251
|
+
help="Console base URL for the browser step (default: https://app.sference.com).",
|
|
240
252
|
),
|
|
241
253
|
no_browser: bool = typer.Option(False, "--no-browser", help="Do not open a browser; print the URL and prompt only."),
|
|
242
254
|
) -> None:
|
|
@@ -320,34 +332,40 @@ def responses_create(
|
|
|
320
332
|
"--enable-thinking/--disable-thinking",
|
|
321
333
|
help="Qwen3: forward to tokenizer apply_chat_template. Omit for tokenizer default.",
|
|
322
334
|
),
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
335
|
+
background: bool = typer.Option(
|
|
336
|
+
False,
|
|
337
|
+
"--background/--no-background",
|
|
338
|
+
help="Submit asynchronously and return immediately. Default blocks until the response is terminal (matches POST /v1/responses).",
|
|
339
|
+
),
|
|
340
|
+
timeout: float = typer.Option(
|
|
341
|
+
600.0,
|
|
342
|
+
"--timeout",
|
|
343
|
+
help="HTTP read timeout in seconds.",
|
|
344
|
+
),
|
|
326
345
|
base_url: str = typer.Option("https://api.sference.com"),
|
|
327
346
|
) -> None:
|
|
328
347
|
_ensure_api_credential()
|
|
329
|
-
client = _client(base_url)
|
|
348
|
+
client = _client(base_url, timeout=timeout)
|
|
330
349
|
resp = _call_api(
|
|
331
350
|
lambda: client.create_response(
|
|
332
351
|
model=model,
|
|
333
352
|
input=[{"role": "user", "content": content}],
|
|
334
353
|
include_reasoning=include_reasoning,
|
|
335
354
|
enable_thinking=enable_thinking,
|
|
355
|
+
background=background,
|
|
336
356
|
)
|
|
337
357
|
)
|
|
338
|
-
if wait:
|
|
339
|
-
final = _wait_for_response(client, resp.id, poll_ms=poll_ms, timeout_s=timeout_s)
|
|
340
|
-
if getattr(final, "status", None) not in ("completed", "failed", "cancelled"):
|
|
341
|
-
_print(final.model_dump(), True)
|
|
342
|
-
raise typer.Exit(code=1)
|
|
343
|
-
resp = final
|
|
344
358
|
_print(resp.model_dump(), True)
|
|
345
359
|
|
|
346
360
|
|
|
347
361
|
@responses_app.command("result")
|
|
348
362
|
def responses_result(
|
|
349
363
|
id: str = typer.Option(..., "--id"),
|
|
350
|
-
|
|
364
|
+
poll_interval: float = typer.Option(
|
|
365
|
+
0.5,
|
|
366
|
+
"--poll-interval",
|
|
367
|
+
help="Seconds between status polls while waiting for completion.",
|
|
368
|
+
),
|
|
351
369
|
base_url: str = typer.Option("https://api.sference.com"),
|
|
352
370
|
) -> None:
|
|
353
371
|
"""Wait for a response to reach a terminal status and print the final JSON.
|
|
@@ -364,7 +382,7 @@ def responses_result(
|
|
|
364
382
|
if status in ("completed", "failed", "cancelled"):
|
|
365
383
|
_print(d, True)
|
|
366
384
|
return
|
|
367
|
-
time.sleep(max(
|
|
385
|
+
time.sleep(max(0.01, poll_interval))
|
|
368
386
|
except KeyboardInterrupt:
|
|
369
387
|
raise typer.Exit(code=130) from None
|
|
370
388
|
|
|
@@ -383,7 +401,11 @@ def responses_tail(
|
|
|
383
401
|
help="Ignore saved checkpoint and start from the latest events page.",
|
|
384
402
|
),
|
|
385
403
|
no_checkpoint: bool = typer.Option(False, "--no-checkpoint", help="Do not read or write checkpoints."),
|
|
386
|
-
|
|
404
|
+
poll_interval: float = typer.Option(
|
|
405
|
+
5.0,
|
|
406
|
+
"--poll-interval",
|
|
407
|
+
help="Seconds to long-poll while catching up (capped at 30s server-side).",
|
|
408
|
+
),
|
|
387
409
|
base_url: str = typer.Option("https://api.sference.com"),
|
|
388
410
|
as_json: bool = typer.Option(False, "--json"),
|
|
389
411
|
) -> None:
|
|
@@ -404,6 +426,7 @@ def responses_tail(
|
|
|
404
426
|
if not no_checkpoint and not from_latest:
|
|
405
427
|
last = load_checkpoint(bu, ck, consumer)
|
|
406
428
|
|
|
429
|
+
wait_ms = int(min(poll_interval, 30.0) * 1000)
|
|
407
430
|
try:
|
|
408
431
|
while True:
|
|
409
432
|
|
|
@@ -412,12 +435,12 @@ def responses_tail(
|
|
|
412
435
|
stream_id=stream_id,
|
|
413
436
|
limit=50,
|
|
414
437
|
starting_after=last,
|
|
415
|
-
wait_ms=
|
|
438
|
+
wait_ms=wait_ms,
|
|
416
439
|
)
|
|
417
440
|
|
|
418
441
|
page = _call_api(fetch)
|
|
419
442
|
if not page.data:
|
|
420
|
-
time.sleep(max(
|
|
443
|
+
time.sleep(max(0.001, poll_interval))
|
|
421
444
|
continue
|
|
422
445
|
for ev in page.data:
|
|
423
446
|
typer.echo(json.dumps(ev.model_dump(), default=str))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|