sference-cli 0.1.0__tar.gz → 0.1.2__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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sference-cli
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: sference command-line interface
5
5
  Project-URL: Homepage, https://sference.com
6
6
  Project-URL: Repository, https://github.com/s-ference/sference
@@ -21,7 +21,7 @@ Classifier: Programming Language :: Python :: 3.12
21
21
  Classifier: Programming Language :: Python :: 3.13
22
22
  Classifier: Topic :: Utilities
23
23
  Requires-Python: >=3.10
24
- Requires-Dist: sference-sdk>=0.1.0
24
+ Requires-Dist: sference-sdk>=0.1.2
25
25
  Requires-Dist: typer>=0.24.1
26
26
  Description-Content-Type: text/markdown
27
27
 
@@ -33,7 +33,7 @@ Command-line interface for the sference batch API (`sference`). It uses the Pyth
33
33
 
34
34
  ```bash
35
35
  # One-line install (macOS / Linux)
36
- curl -fsSL https://raw.githubusercontent.com/s-ference/sference/main/install.sh | sh
36
+ eval "$(curl -fsSL https://raw.githubusercontent.com/s-ference/sference/main/install.sh)"
37
37
  ```
38
38
 
39
39
  Or install from PyPI:
@@ -109,6 +109,7 @@ sference responses tail --stream-id <stream_id>
109
109
  | Variable | Purpose |
110
110
  |----------|---------|
111
111
  | `SFERENCE_API_KEY` | API key (or JWT); overrides `~/.sference/credentials.json` |
112
+ | `SFERENCE_MODEL` | Default catalog model for `sference launch` (overrides built-in default) |
112
113
  | `SFERENCE_STREAM_CACHE` | Optional path to the stream resumable-cache file (default `~/.sference/stream_cache.json`) |
113
114
  | `SFERENCE_STREAM_CHECKPOINTS` | Optional path for **`responses tail`** event checkpoints (default `~/.sference/stream_checkpoints.json`) |
114
115
 
@@ -121,12 +122,33 @@ sference responses tail --stream-id <stream_id>
121
122
  | `sference auth login` | Store an API key (optional `--api-key`, `--no-browser`) |
122
123
  | `sference auth me` | Show current user (`--json` for machine-readable output) |
123
124
 
125
+ ### Launch
126
+
127
+ | Command | Description |
128
+ |---------|-------------|
129
+ | `sference launch claude` | Launch Claude Code with Sference API routing (`ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_MODEL`) |
130
+ | `sference launch claude --dry-run` | Print env + command without starting Claude Code |
131
+ | `sference launch claude --model moonshotai/Kimi-K2.7-Code` | Override catalog model |
132
+ | `sference launch claude --enable-tool-search` | Set `ENABLE_TOOL_SEARCH=true` on custom hosts |
133
+ | `sference launch claude --resume` | Forward Claude Code flags/args after `sference launch claude` |
134
+
135
+ Requires the [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) on `PATH`. Uses `~/.sference/credentials.json` or `SFERENCE_API_KEY`. Default model: `moonshotai/Kimi-K2.7-Code` (override with `--model` or `SFERENCE_MODEL`).
136
+
137
+ | Command | Description |
138
+ |---------|-------------|
139
+ | `sference launch pi` | Launch Pi with Sference API routing (writes `~/.pi/agent/models.json`) |
140
+ | `sference launch pi --dry-run` | Print provider config and command without launching Pi |
141
+ | `sference launch pi --model moonshotai/Kimi-K2.7-Code` | Override catalog model |
142
+ | `sference launch pi -- /path/to/project` | Forward Pi args after `sference launch pi` |
143
+
144
+ Requires the [Pi CLI](https://pi.dev/docs) on `PATH`. Writes a `sference` provider block to `~/.pi/agent/models.json` with `baseUrl`, `apiKey`, and the chosen `model`, then execs `pi --provider sference --model <id>`. Uses `~/.sference/credentials.json` or `SFERENCE_API_KEY`. Default model: `moonshotai/Kimi-K2.7-Code` (override with `--model` or `SFERENCE_MODEL`).
145
+
124
146
  ### Batch
125
147
 
126
148
  | Command | Description |
127
149
  |---------|-------------|
128
150
  | `sference batch list` | List batches (table; `--json` for raw payload) |
129
- | `sference batch submit` | Submit a JSONL file (`--input-file`, optional `--model` for content-only lines, `--window` must be `24h`) |
151
+ | `sference batch submit` | Submit a JSONL file (`--input-file`, optional `--model` for content-only lines, `--window` `15m`/`1h`/`24h`) |
130
152
  | `sference batch stream` | Submit, wait, print **JSONL results on stdout** (see below) |
131
153
  | `sference batch status` | Get one batch (`--batch-id`, `--json`) |
132
154
  | `sference batch wait` | Poll until terminal state (`--batch-id`, `--poll-interval`, `--timeout`, `--json`) |
@@ -148,7 +170,7 @@ Long-lived **streams** are separate from **batches**: you create a stream, submi
148
170
 
149
171
  | Command | Description |
150
172
  |---------|-------------|
151
- | `sference stream create` | Create a stream (`--name`, `--window` `1h` or `24h`, `--json`) |
173
+ | `sference stream create` | Create a stream (`--name`, `--window` `15m`/`1h`/`24h`, `--json`) |
152
174
  | `sference stream list` | List streams (`--json`) |
153
175
  | `sference stream status` | Full detail + counters (`--stream-id`, `--json`) |
154
176
  | `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}` |
@@ -226,9 +248,19 @@ sference batch stream --input-file fixtures/example_batch.jsonl --poll-interval
226
248
 
227
249
  The SDK and CLI accept two line shapes (see also [`fixtures/example_batch.jsonl`](fixtures/example_batch.jsonl)):
228
250
 
229
- 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).
251
+ 1. **OpenAI-compatible envelope:** each line has `custom_id`, `method`, `url`, and `body`. The API receives only `custom_id` + inner `body` (`method`/`url` are ignored). Inner `body` may use:
252
+ - **Chat completions:** `messages` (non-empty array), plus optional `temperature`, `max_tokens`, `tools`, …
253
+ - **Responses API:** `input` (string or message array), optional `instructions`, `max_output_tokens`, … — normalized to chat format at batch create.
230
254
  2. **Content-only:** each line is `{"content": "..."}`. Then **`--model` is required** on submit/stream.
231
255
 
256
+ Invalid row bodies return **HTTP 400** at create with `requests[i]` and `custom_id` in the error message (nothing is enqueued). Do not set `background: true` inside batch row bodies.
257
+
258
+ Example Responses-shaped JSONL line:
259
+
260
+ ```json
261
+ {"custom_id":"r2","method":"POST","url":"/v1/responses","body":{"model":"Qwen/Qwen3.6-35B-A3B","input":[{"role":"user","content":"hi"}],"max_output_tokens":256}}
262
+ ```
263
+
232
264
  ---
233
265
 
234
266
  ## Python SDK
@@ -6,7 +6,7 @@ Command-line interface for the sference batch API (`sference`). It uses the Pyth
6
6
 
7
7
  ```bash
8
8
  # One-line install (macOS / Linux)
9
- curl -fsSL https://raw.githubusercontent.com/s-ference/sference/main/install.sh | sh
9
+ eval "$(curl -fsSL https://raw.githubusercontent.com/s-ference/sference/main/install.sh)"
10
10
  ```
11
11
 
12
12
  Or install from PyPI:
@@ -82,6 +82,7 @@ sference responses tail --stream-id <stream_id>
82
82
  | Variable | Purpose |
83
83
  |----------|---------|
84
84
  | `SFERENCE_API_KEY` | API key (or JWT); overrides `~/.sference/credentials.json` |
85
+ | `SFERENCE_MODEL` | Default catalog model for `sference launch` (overrides built-in default) |
85
86
  | `SFERENCE_STREAM_CACHE` | Optional path to the stream resumable-cache file (default `~/.sference/stream_cache.json`) |
86
87
  | `SFERENCE_STREAM_CHECKPOINTS` | Optional path for **`responses tail`** event checkpoints (default `~/.sference/stream_checkpoints.json`) |
87
88
 
@@ -94,12 +95,33 @@ sference responses tail --stream-id <stream_id>
94
95
  | `sference auth login` | Store an API key (optional `--api-key`, `--no-browser`) |
95
96
  | `sference auth me` | Show current user (`--json` for machine-readable output) |
96
97
 
98
+ ### Launch
99
+
100
+ | Command | Description |
101
+ |---------|-------------|
102
+ | `sference launch claude` | Launch Claude Code with Sference API routing (`ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_MODEL`) |
103
+ | `sference launch claude --dry-run` | Print env + command without starting Claude Code |
104
+ | `sference launch claude --model moonshotai/Kimi-K2.7-Code` | Override catalog model |
105
+ | `sference launch claude --enable-tool-search` | Set `ENABLE_TOOL_SEARCH=true` on custom hosts |
106
+ | `sference launch claude --resume` | Forward Claude Code flags/args after `sference launch claude` |
107
+
108
+ Requires the [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) on `PATH`. Uses `~/.sference/credentials.json` or `SFERENCE_API_KEY`. Default model: `moonshotai/Kimi-K2.7-Code` (override with `--model` or `SFERENCE_MODEL`).
109
+
110
+ | Command | Description |
111
+ |---------|-------------|
112
+ | `sference launch pi` | Launch Pi with Sference API routing (writes `~/.pi/agent/models.json`) |
113
+ | `sference launch pi --dry-run` | Print provider config and command without launching Pi |
114
+ | `sference launch pi --model moonshotai/Kimi-K2.7-Code` | Override catalog model |
115
+ | `sference launch pi -- /path/to/project` | Forward Pi args after `sference launch pi` |
116
+
117
+ Requires the [Pi CLI](https://pi.dev/docs) on `PATH`. Writes a `sference` provider block to `~/.pi/agent/models.json` with `baseUrl`, `apiKey`, and the chosen `model`, then execs `pi --provider sference --model <id>`. Uses `~/.sference/credentials.json` or `SFERENCE_API_KEY`. Default model: `moonshotai/Kimi-K2.7-Code` (override with `--model` or `SFERENCE_MODEL`).
118
+
97
119
  ### Batch
98
120
 
99
121
  | Command | Description |
100
122
  |---------|-------------|
101
123
  | `sference batch list` | List batches (table; `--json` for raw payload) |
102
- | `sference batch submit` | Submit a JSONL file (`--input-file`, optional `--model` for content-only lines, `--window` must be `24h`) |
124
+ | `sference batch submit` | Submit a JSONL file (`--input-file`, optional `--model` for content-only lines, `--window` `15m`/`1h`/`24h`) |
103
125
  | `sference batch stream` | Submit, wait, print **JSONL results on stdout** (see below) |
104
126
  | `sference batch status` | Get one batch (`--batch-id`, `--json`) |
105
127
  | `sference batch wait` | Poll until terminal state (`--batch-id`, `--poll-interval`, `--timeout`, `--json`) |
@@ -121,7 +143,7 @@ Long-lived **streams** are separate from **batches**: you create a stream, submi
121
143
 
122
144
  | Command | Description |
123
145
  |---------|-------------|
124
- | `sference stream create` | Create a stream (`--name`, `--window` `1h` or `24h`, `--json`) |
146
+ | `sference stream create` | Create a stream (`--name`, `--window` `15m`/`1h`/`24h`, `--json`) |
125
147
  | `sference stream list` | List streams (`--json`) |
126
148
  | `sference stream status` | Full detail + counters (`--stream-id`, `--json`) |
127
149
  | `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}` |
@@ -199,9 +221,19 @@ sference batch stream --input-file fixtures/example_batch.jsonl --poll-interval
199
221
 
200
222
  The SDK and CLI accept two line shapes (see also [`fixtures/example_batch.jsonl`](fixtures/example_batch.jsonl)):
201
223
 
202
- 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).
224
+ 1. **OpenAI-compatible envelope:** each line has `custom_id`, `method`, `url`, and `body`. The API receives only `custom_id` + inner `body` (`method`/`url` are ignored). Inner `body` may use:
225
+ - **Chat completions:** `messages` (non-empty array), plus optional `temperature`, `max_tokens`, `tools`, …
226
+ - **Responses API:** `input` (string or message array), optional `instructions`, `max_output_tokens`, … — normalized to chat format at batch create.
203
227
  2. **Content-only:** each line is `{"content": "..."}`. Then **`--model` is required** on submit/stream.
204
228
 
229
+ Invalid row bodies return **HTTP 400** at create with `requests[i]` and `custom_id` in the error message (nothing is enqueued). Do not set `background: true` inside batch row bodies.
230
+
231
+ Example Responses-shaped JSONL line:
232
+
233
+ ```json
234
+ {"custom_id":"r2","method":"POST","url":"/v1/responses","body":{"model":"Qwen/Qwen3.6-35B-A3B","input":[{"role":"user","content":"hi"}],"max_output_tokens":256}}
235
+ ```
236
+
205
237
  ---
206
238
 
207
239
  ## Python SDK
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sference-cli"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  description = "sference command-line interface"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -23,7 +23,7 @@ classifiers = [
23
23
  ]
24
24
  dependencies = [
25
25
  "typer>=0.24.1",
26
- "sference-sdk>=0.1.0",
26
+ "sference-sdk>=0.1.2",
27
27
  ]
28
28
 
29
29
  [project.urls]
@@ -0,0 +1,304 @@
1
+ """Launch external agent tools against the Sference API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ import typer
14
+
15
+ DEFAULT_LAUNCH_MODEL = "moonshotai/Kimi-K2.7-Code"
16
+ DEFAULT_API_BASE_URL = "https://api.sference.com"
17
+
18
+
19
+ def resolve_api_base_url(explicit: Optional[str]) -> str:
20
+ if explicit:
21
+ return explicit.rstrip("/")
22
+ env = os.environ.get("SFERENCE_BASE_URL")
23
+ if env:
24
+ return env.strip().rstrip("/")
25
+ return DEFAULT_API_BASE_URL
26
+
27
+
28
+ def resolve_launch_model(explicit: Optional[str]) -> str:
29
+ """Default catalog model for ``launch`` subcommands (--model overrides)."""
30
+ if explicit:
31
+ return explicit
32
+ env = os.environ.get("SFERENCE_MODEL")
33
+ if env and env.strip():
34
+ return env.strip()
35
+ return DEFAULT_LAUNCH_MODEL
36
+
37
+
38
+ def resolve_openai_base_url_for_pi(explicit: Optional[str]) -> str:
39
+ """OpenAI-compatible base URL with ``/v1`` for Pi ``models.json``."""
40
+ base = resolve_api_base_url(explicit)
41
+ base = base.rstrip("/")
42
+ if base.endswith("/v1"):
43
+ return base
44
+ return f"{base}/v1"
45
+
46
+
47
+ def find_claude_executable() -> str | None:
48
+ return shutil.which("claude")
49
+
50
+
51
+ def find_pi_executable() -> str | None:
52
+ return shutil.which("pi")
53
+
54
+
55
+ def build_claude_env(
56
+ *,
57
+ api_key: str,
58
+ base_url: str,
59
+ model: str,
60
+ enable_tool_search: bool,
61
+ ) -> dict[str, str]:
62
+ env = os.environ.copy()
63
+ env["ANTHROPIC_BASE_URL"] = base_url
64
+ env["ANTHROPIC_AUTH_TOKEN"] = api_key
65
+ env["ANTHROPIC_MODEL"] = model
66
+ # Claude Code prefers AUTH_TOKEN; drop API_KEY to avoid precedence confusion.
67
+ env.pop("ANTHROPIC_API_KEY", None)
68
+ if enable_tool_search:
69
+ env["ENABLE_TOOL_SEARCH"] = "true"
70
+ else:
71
+ env.pop("ENABLE_TOOL_SEARCH", None)
72
+ return env
73
+
74
+
75
+ def launch_claude_code(
76
+ *,
77
+ api_key: str,
78
+ base_url: str,
79
+ model: str,
80
+ enable_tool_search: bool,
81
+ claude_args: list[str],
82
+ dry_run: bool,
83
+ ) -> None:
84
+ claude_bin = find_claude_executable()
85
+ if claude_bin is None:
86
+ typer.echo(
87
+ "Claude Code CLI not found on PATH.\n"
88
+ "Install from https://docs.anthropic.com/en/docs/claude-code "
89
+ "and ensure the `claude` command is available.",
90
+ err=True,
91
+ )
92
+ raise typer.Exit(code=1)
93
+
94
+ env = build_claude_env(
95
+ api_key=api_key,
96
+ base_url=base_url,
97
+ model=model,
98
+ enable_tool_search=enable_tool_search,
99
+ )
100
+ cmd = [claude_bin, *claude_args]
101
+
102
+ if dry_run:
103
+ typer.echo(f"ANTHROPIC_BASE_URL={env['ANTHROPIC_BASE_URL']}")
104
+ typer.echo(f"ANTHROPIC_MODEL={env['ANTHROPIC_MODEL']}")
105
+ typer.echo("ANTHROPIC_AUTH_TOKEN=<redacted>")
106
+ if enable_tool_search:
107
+ typer.echo("ENABLE_TOOL_SEARCH=true")
108
+ typer.echo(f"command: {' '.join(cmd)}")
109
+ return
110
+
111
+ if sys.platform == "win32":
112
+ raise SystemExit(subprocess.call(cmd, env=env))
113
+
114
+ os.execvpe(claude_bin, cmd, env)
115
+
116
+
117
+ def _pi_models_json_path() -> Path:
118
+ return Path.home() / ".pi" / "agent" / "models.json"
119
+
120
+
121
+ def _write_pi_models_json(*, base_url: str, api_key: str, model: str) -> Path:
122
+ """Write or update ``~/.pi/agent/models.json`` with a Sference provider."""
123
+ path = _pi_models_json_path()
124
+ path.parent.mkdir(parents=True, exist_ok=True)
125
+
126
+ provider = {
127
+ "api": "openai-completions",
128
+ "baseUrl": base_url,
129
+ "apiKey": api_key,
130
+ "models": [{"id": model, "name": model}],
131
+ }
132
+
133
+ if path.exists():
134
+ with path.open("r", encoding="utf-8") as fh:
135
+ try:
136
+ data = json.load(fh)
137
+ except json.JSONDecodeError:
138
+ data = {}
139
+ else:
140
+ data = {}
141
+
142
+ data.setdefault("providers", {})
143
+ data["providers"]["sference"] = provider
144
+
145
+ with path.open("w", encoding="utf-8") as fh:
146
+ json.dump(data, fh, indent=2)
147
+ fh.write("\n")
148
+
149
+ return path
150
+
151
+
152
+ def launch_pi(
153
+ *,
154
+ api_key: str,
155
+ base_url: str,
156
+ model: str,
157
+ pi_args: list[str],
158
+ dry_run: bool,
159
+ ) -> None:
160
+ pi_bin = find_pi_executable()
161
+ if pi_bin is None:
162
+ typer.echo(
163
+ "Pi CLI not found on PATH.\n"
164
+ "Install from https://pi.dev/docs and ensure the `pi` command is available.",
165
+ err=True,
166
+ )
167
+ raise typer.Exit(code=1)
168
+
169
+ models_json = _write_pi_models_json(base_url=base_url, api_key=api_key, model=model)
170
+ env = os.environ.copy()
171
+ # Pi reads providers from ~/.pi/agent/models.json; no env vars needed.
172
+ cmd = [pi_bin, "--provider", "sference", "--model", model, *pi_args]
173
+
174
+ if dry_run:
175
+ typer.echo(f"Wrote provider 'sference' to {models_json}")
176
+ typer.echo(f"baseUrl: {base_url}")
177
+ typer.echo(f"model: {model}")
178
+ typer.echo("apiKey: <redacted>")
179
+ typer.echo(f"command: {' '.join(cmd)}")
180
+ return
181
+
182
+ if sys.platform == "win32":
183
+ raise SystemExit(subprocess.call(cmd, env=env))
184
+
185
+ os.execvpe(pi_bin, cmd, env)
186
+
187
+
188
+ def register_launch_commands(app: typer.Typer) -> None:
189
+ launch_app = typer.Typer(help="Launch external tools configured for Sference.", invoke_without_command=True)
190
+
191
+ @launch_app.callback()
192
+ def launch_root(ctx: typer.Context) -> None:
193
+ if ctx.invoked_subcommand is None:
194
+ typer.echo(ctx.get_help())
195
+ raise typer.Exit(code=0)
196
+
197
+ @launch_app.command(
198
+ "claude",
199
+ context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
200
+ help="Launch Claude Code with Sference API routing.",
201
+ )
202
+ def claude(
203
+ ctx: typer.Context,
204
+ model: Optional[str] = typer.Option(
205
+ None,
206
+ "--model",
207
+ "-m",
208
+ help=f"Catalog model id (default: {DEFAULT_LAUNCH_MODEL} or SFERENCE_MODEL).",
209
+ ),
210
+ base_url: Optional[str] = typer.Option(
211
+ None,
212
+ "--base-url",
213
+ envvar="SFERENCE_BASE_URL",
214
+ help=f"Sference API base URL (default: {DEFAULT_API_BASE_URL}).",
215
+ ),
216
+ enable_tool_search: bool = typer.Option(
217
+ False,
218
+ "--enable-tool-search",
219
+ help="Set ENABLE_TOOL_SEARCH=true for Claude Code on a custom API host.",
220
+ ),
221
+ dry_run: bool = typer.Option(
222
+ False,
223
+ "--dry-run",
224
+ help="Print env and command without launching Claude Code.",
225
+ ),
226
+ ) -> None:
227
+ """Run ``claude`` with Sference credentials.
228
+
229
+ Uses ``~/.sference/credentials.json`` or ``SFERENCE_API_KEY``. Unknown options
230
+ and trailing args are forwarded to Claude Code, e.g.
231
+ ``sference launch claude --resume`` or ``sference launch claude -p "fix the bug"``.
232
+ """
233
+ from sference_cli.main import _ensure_api_credential, _read_token
234
+
235
+ _ensure_api_credential()
236
+ api_key = _read_token()
237
+ if api_key is None:
238
+ raise typer.Exit(code=1)
239
+
240
+ forwarded = list(ctx.args)
241
+ if forwarded and forwarded[0] == "--":
242
+ forwarded = forwarded[1:]
243
+
244
+ launch_claude_code(
245
+ api_key=api_key,
246
+ base_url=resolve_api_base_url(base_url),
247
+ model=resolve_launch_model(model),
248
+ enable_tool_search=enable_tool_search,
249
+ claude_args=forwarded,
250
+ dry_run=dry_run,
251
+ )
252
+
253
+ @launch_app.command(
254
+ "pi",
255
+ context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
256
+ help="Launch Pi with Sference OpenAI-compatible routing.",
257
+ )
258
+ def pi(
259
+ ctx: typer.Context,
260
+ model: Optional[str] = typer.Option(
261
+ None,
262
+ "--model",
263
+ "-m",
264
+ help=f"Catalog model id (default: {DEFAULT_LAUNCH_MODEL} or SFERENCE_MODEL).",
265
+ ),
266
+ base_url: Optional[str] = typer.Option(
267
+ None,
268
+ "--base-url",
269
+ envvar="SFERENCE_BASE_URL",
270
+ help=f"Sference API base URL (default: {DEFAULT_API_BASE_URL}).",
271
+ ),
272
+ dry_run: bool = typer.Option(
273
+ False,
274
+ "--dry-run",
275
+ help="Print provider config and command without launching Pi.",
276
+ ),
277
+ ) -> None:
278
+ """Run ``pi`` with Sference credentials.
279
+
280
+ Writes a ``sference`` provider block to ``~/.pi/agent/models.json`` with
281
+ ``baseUrl``, ``apiKey``, and the chosen ``model``, then execs
282
+ ``pi --provider sference --model <id>``. Unknown options and trailing args
283
+ are forwarded to Pi, e.g. ``sference launch pi -- /path/to/project``.
284
+ """
285
+ from sference_cli.main import _ensure_api_credential, _read_token
286
+
287
+ _ensure_api_credential()
288
+ api_key = _read_token()
289
+ if api_key is None:
290
+ raise typer.Exit(code=1)
291
+
292
+ forwarded = list(ctx.args)
293
+ if forwarded and forwarded[0] == "--":
294
+ forwarded = forwarded[1:]
295
+
296
+ launch_pi(
297
+ api_key=api_key,
298
+ base_url=resolve_openai_base_url_for_pi(base_url),
299
+ model=resolve_launch_model(model),
300
+ pi_args=forwarded,
301
+ dry_run=dry_run,
302
+ )
303
+
304
+ app.add_typer(launch_app, name="launch")
@@ -13,12 +13,13 @@ from typing import Any, Optional, TypeVar
13
13
  import typer
14
14
 
15
15
  import sference_sdk
16
- from sference_sdk import ApiError, SferenceClient
16
+ from sference_sdk import COMPLETION_WINDOWS, ApiError, SferenceClient
17
17
 
18
18
  from sference_cli import __version__ as _CLI_VERSION
19
19
  from sference_sdk.checkpoint import clear_checkpoint, load_checkpoint, save_checkpoint
20
20
 
21
21
  from . import stream_cache as stream_cache_mod
22
+ from .launch import register_launch_commands
22
23
 
23
24
  _T = TypeVar("_T")
24
25
 
@@ -36,6 +37,7 @@ app.add_typer(batch_app, name="batch")
36
37
  app.add_typer(stream_app, name="stream")
37
38
  app.add_typer(responses_app, name="responses")
38
39
  app.add_typer(models_app, name="models")
40
+ register_launch_commands(app)
39
41
 
40
42
  CREDENTIALS_PATH = Path.home() / ".sference" / "credentials.json"
41
43
 
@@ -72,11 +74,8 @@ def _read_token() -> str | None:
72
74
 
73
75
 
74
76
  def _client(base_url: Optional[str] = None, *, timeout: float | None = None) -> SferenceClient:
75
- # If the caller didn't explicitly pass --base-url, allow SFERENCE_BASE_URL
76
- # to override the default.
77
77
  env_base_url = os.environ.get("SFERENCE_BASE_URL")
78
78
  if env_base_url and (base_url is None or base_url == "https://api.sference.com"):
79
- _logger.info(f"Using SFERENCE_BASE_URL: {env_base_url}")
80
79
  base_url = env_base_url
81
80
  return SferenceClient(base_url=base_url, api_key=_read_token(), timeout=timeout)
82
81
 
@@ -152,15 +151,10 @@ def _list_models(*, client: SferenceClient, as_json: bool) -> None:
152
151
  typer.echo(json.dumps(payload, indent=None if as_json else 2))
153
152
 
154
153
 
155
- def _mvp_batch_window_only(value: str) -> str:
156
- if value != "24h":
157
- raise typer.BadParameter('Only "24h" is supported in MVP.', param_hint="--window")
158
- return value
159
-
160
-
161
- def _stream_window_only(value: str) -> str:
162
- if value not in ("1h", "24h"):
163
- raise typer.BadParameter('Window must be "1h" or "24h".', param_hint="--window")
154
+ def _window_choice(value: str) -> str:
155
+ if value not in COMPLETION_WINDOWS:
156
+ allowed = ", ".join(f'"{w}"' for w in COMPLETION_WINDOWS)
157
+ raise typer.BadParameter(f"Window must be one of {allowed}.", param_hint="--window")
164
158
  return value
165
159
 
166
160
 
@@ -507,8 +501,8 @@ def batch_submit(
507
501
  window: str = typer.Option(
508
502
  "24h",
509
503
  "--window",
510
- help='Batch SLA window (MVP: only "24h").',
511
- callback=_mvp_batch_window_only,
504
+ help='Batch SLA window: "15m", "1h", or "24h".',
505
+ callback=_window_choice,
512
506
  ),
513
507
  base_url: str = typer.Option("https://api.sference.com"),
514
508
  as_json: bool = typer.Option(False, "--json"),
@@ -538,8 +532,8 @@ def batch_stream(
538
532
  window: str = typer.Option(
539
533
  "24h",
540
534
  "--window",
541
- help='Batch SLA window (MVP: only "24h").',
542
- callback=_mvp_batch_window_only,
535
+ help='Batch SLA window: "15m", "1h", or "24h".',
536
+ callback=_window_choice,
543
537
  ),
544
538
  poll_interval: float = typer.Option(2.0, "--poll-interval", help="Seconds between status polls while the batch is running."),
545
539
  no_cache: bool = typer.Option(
@@ -654,8 +648,8 @@ def stream_create(
654
648
  window: str = typer.Option(
655
649
  "24h",
656
650
  "--window",
657
- help='Per-item SLA window: "1h" or "24h" (recorded only in MVP).',
658
- callback=_stream_window_only,
651
+ help='Per-item SLA window: "15m", "1h", or "24h" (default for items in this stream).',
652
+ callback=_window_choice,
659
653
  ),
660
654
  base_url: str = typer.Option("https://api.sference.com"),
661
655
  as_json: bool = typer.Option(False, "--json"),
File without changes
File without changes