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.
@@ -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://console.sference.com`) |
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://console.sference.com`) |
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.6"
3
+ version = "0.0.8"
4
4
  description = "sference command-line interface"
5
5
  readme = "README.md"
6
- requires-python = ">=3.12"
6
+ requires-python = ">=3.10"
7
7
  dependencies = [
8
8
  "typer>=0.24.1",
9
- "sference-sdk>=0.0.6",
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://console.sference.com"
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 _wait_for_response(
139
- client: SferenceClient, response_id: str, *, poll_ms: int, timeout_s: int
140
- ) -> Any:
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://console.sference.com).",
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
- wait: bool = typer.Option(False, "--wait/--no-wait"),
324
- poll_ms: int = typer.Option(500, "--poll-ms", help="Polling interval when --wait is enabled."),
325
- timeout_s: int = typer.Option(60, "--timeout-s", help="Timeout in seconds when --wait is enabled."),
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
- poll_ms: int = typer.Option(500, "--poll-ms", help="Polling interval while waiting for completion."),
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(10, poll_ms) / 1000.0)
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
- poll_ms: int = typer.Option(5000, "--poll-ms", help="Long-poll wait_ms when catching up (max 30000)."),
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=min(poll_ms, 30000),
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(1, poll_ms) / 1000.0)
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