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.
- {sference_cli-0.1.0 → sference_cli-0.1.2}/PKG-INFO +38 -6
- {sference_cli-0.1.0 → sference_cli-0.1.2}/README.md +36 -4
- {sference_cli-0.1.0 → sference_cli-0.1.2}/pyproject.toml +2 -2
- sference_cli-0.1.2/sference_cli/launch.py +304 -0
- {sference_cli-0.1.0 → sference_cli-0.1.2}/sference_cli/main.py +13 -19
- {sference_cli-0.1.0 → sference_cli-0.1.2}/.gitignore +0 -0
- {sference_cli-0.1.0 → sference_cli-0.1.2}/LICENSE +0 -0
- {sference_cli-0.1.0 → sference_cli-0.1.2}/sference_cli/__init__.py +0 -0
- {sference_cli-0.1.0 → sference_cli-0.1.2}/sference_cli/stream_cache.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sference-cli
|
|
3
|
-
Version: 0.1.
|
|
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.
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
156
|
-
if value
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
511
|
-
callback=
|
|
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
|
|
542
|
-
callback=
|
|
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" (
|
|
658
|
-
callback=
|
|
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
|
|
File without changes
|
|
File without changes
|