schift-cli 0.1.0__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.
- schift_cli-0.1.0/PKG-INFO +12 -0
- schift_cli-0.1.0/README.md +303 -0
- schift_cli-0.1.0/pyproject.toml +30 -0
- schift_cli-0.1.0/schift_cli/__init__.py +1 -0
- schift_cli-0.1.0/schift_cli/client.py +119 -0
- schift_cli-0.1.0/schift_cli/commands/__init__.py +0 -0
- schift_cli-0.1.0/schift_cli/commands/auth.py +68 -0
- schift_cli-0.1.0/schift_cli/commands/bench.py +65 -0
- schift_cli-0.1.0/schift_cli/commands/catalog.py +74 -0
- schift_cli-0.1.0/schift_cli/commands/db.py +96 -0
- schift_cli-0.1.0/schift_cli/commands/embed.py +104 -0
- schift_cli-0.1.0/schift_cli/commands/migrate.py +127 -0
- schift_cli-0.1.0/schift_cli/commands/query.py +66 -0
- schift_cli-0.1.0/schift_cli/commands/skill.py +110 -0
- schift_cli-0.1.0/schift_cli/commands/usage.py +50 -0
- schift_cli-0.1.0/schift_cli/config.py +58 -0
- schift_cli-0.1.0/schift_cli/data/schift-best-practices/AGENTS.md +77 -0
- schift_cli-0.1.0/schift_cli/data/schift-best-practices/CLAUDE.md +77 -0
- schift_cli-0.1.0/schift_cli/data/schift-best-practices/SKILL.md +89 -0
- schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/bucket-organization.md +126 -0
- schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/bucket-upload.md +116 -0
- schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/chatbot-widget.md +238 -0
- schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/cost-batching.md +179 -0
- schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/cost-storage-tiers.md +183 -0
- schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/deploy-cloudrun.md +140 -0
- schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/embed-batch-processing.md +86 -0
- schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/embed-error-handling.md +155 -0
- schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/embed-multimodal.md +100 -0
- schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/embed-task-types.md +135 -0
- schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/rag-chunking.md +173 -0
- schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/rag-workflow-builder.md +205 -0
- schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/sdk-async-patterns.md +103 -0
- schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/sdk-auth-patterns.md +76 -0
- schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/search-collection-design.md +229 -0
- schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/search-hybrid.md +163 -0
- schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/search-similarity-tuning.md +134 -0
- schift_cli-0.1.0/schift_cli/display.py +85 -0
- schift_cli-0.1.0/schift_cli/main.py +39 -0
- schift_cli-0.1.0/schift_cli.egg-info/PKG-INFO +12 -0
- schift_cli-0.1.0/schift_cli.egg-info/SOURCES.txt +43 -0
- schift_cli-0.1.0/schift_cli.egg-info/dependency_links.txt +1 -0
- schift_cli-0.1.0/schift_cli.egg-info/entry_points.txt +2 -0
- schift_cli-0.1.0/schift_cli.egg-info/requires.txt +7 -0
- schift_cli-0.1.0/schift_cli.egg-info/top_level.txt +1 -0
- schift_cli-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: schift-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Schift CLI — manage agents, embeddings, and vector collections
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: click>=8.1
|
|
8
|
+
Requires-Dist: httpx>=0.27
|
|
9
|
+
Requires-Dist: rich>=13.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
12
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# Schift CLI
|
|
2
|
+
|
|
3
|
+
Command-line interface for Schift, the operational layer for embedding catalogs, vector collections, benchmark runs, and model migration rollouts.
|
|
4
|
+
|
|
5
|
+
## What it covers
|
|
6
|
+
|
|
7
|
+
- Authenticate against the Schift API
|
|
8
|
+
- Inspect the embedding model catalog
|
|
9
|
+
- Generate single or batch embeddings
|
|
10
|
+
- Create and inspect vector collections
|
|
11
|
+
- Run semantic search queries
|
|
12
|
+
- Benchmark a source-to-target migration before rollout
|
|
13
|
+
- Fit and execute projection-based migrations
|
|
14
|
+
- Inspect usage summaries
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
From this repository:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
cd sdk/cli
|
|
22
|
+
python3 -m pip install -e .
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
For local development with test dependencies:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
cd sdk/cli
|
|
29
|
+
python3 -m pip install -e '.[dev]'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The package installs a `schift` executable via the console entry point in `pyproject.toml`.
|
|
33
|
+
|
|
34
|
+
## Configuration
|
|
35
|
+
|
|
36
|
+
The CLI reads configuration from two places:
|
|
37
|
+
|
|
38
|
+
1. `SCHIFT_API_KEY`
|
|
39
|
+
2. `~/.schift/config.json`
|
|
40
|
+
|
|
41
|
+
If both are present, `SCHIFT_API_KEY` wins.
|
|
42
|
+
|
|
43
|
+
The API base URL is resolved as:
|
|
44
|
+
|
|
45
|
+
1. `SCHIFT_API_URL`
|
|
46
|
+
2. `https://api.schift.io/v1`
|
|
47
|
+
|
|
48
|
+
`schift auth login` stores the API key in `~/.schift/config.json` and writes the file with `0600` permissions.
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
export SCHIFT_API_KEY=sch_your_key_here
|
|
54
|
+
export SCHIFT_API_URL=http://localhost:8080/v1
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
schift auth login
|
|
61
|
+
schift auth status
|
|
62
|
+
|
|
63
|
+
schift catalog list
|
|
64
|
+
schift catalog get openai/text-embedding-3-large
|
|
65
|
+
|
|
66
|
+
schift embed "hello world" --model openai/text-embedding-3-large
|
|
67
|
+
|
|
68
|
+
schift db create my-docs --dim 3072 --metric cosine
|
|
69
|
+
schift db list
|
|
70
|
+
schift query "revenue report" --collection my-docs --top-k 5
|
|
71
|
+
|
|
72
|
+
schift bench \
|
|
73
|
+
--source openai/text-embedding-3-large \
|
|
74
|
+
--target google/gemini-embedding-004 \
|
|
75
|
+
--data ./queries.jsonl
|
|
76
|
+
|
|
77
|
+
schift migrate fit \
|
|
78
|
+
--source openai/text-embedding-3-large \
|
|
79
|
+
--target google/gemini-embedding-004 \
|
|
80
|
+
--sample 0.1
|
|
81
|
+
|
|
82
|
+
schift migrate run \
|
|
83
|
+
--projection proj_abc123 \
|
|
84
|
+
--db pgvector://user:password@localhost:5432/app \
|
|
85
|
+
--dry-run
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Command Groups
|
|
89
|
+
|
|
90
|
+
| Command | Purpose |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| `schift auth ...` | Manage local authentication state |
|
|
93
|
+
| `schift catalog ...` | Browse supported embedding models |
|
|
94
|
+
| `schift embed ...` | Generate embeddings from text |
|
|
95
|
+
| `schift bench ...` | Evaluate migration quality between two models |
|
|
96
|
+
| `schift migrate ...` | Fit a projection and apply it to a database |
|
|
97
|
+
| `schift db ...` | Create, list, and inspect collections |
|
|
98
|
+
| `schift query ...` | Run semantic search against a collection |
|
|
99
|
+
| `schift usage ...` | Show aggregated usage and billing summary |
|
|
100
|
+
|
|
101
|
+
## Authentication
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
schift auth login
|
|
105
|
+
schift auth status
|
|
106
|
+
schift auth logout
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- `login` prompts for an API key and stores it in the config file.
|
|
110
|
+
- `status` reports whether the CLI is using the environment variable or the config file.
|
|
111
|
+
- `logout` removes the stored key from the config file. It does not unset `SCHIFT_API_KEY` from your shell.
|
|
112
|
+
|
|
113
|
+
## Catalog Commands
|
|
114
|
+
|
|
115
|
+
List all models:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
schift catalog list
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Show one model:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
schift catalog get openai/text-embedding-3-large
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Output is rendered as a Rich table or key-value panel and typically includes provider, dimensions, token limit, status, and description.
|
|
128
|
+
|
|
129
|
+
## Embedding Commands
|
|
130
|
+
|
|
131
|
+
Single text:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
schift embed "quarterly revenue report" --model openai/text-embedding-3-large
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The CLI prints a success line plus a short preview of the first embedding values instead of dumping the full vector.
|
|
138
|
+
|
|
139
|
+
Batch mode:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
schift embed batch \
|
|
143
|
+
--file ./texts.jsonl \
|
|
144
|
+
--model google/gemini-embedding-004 \
|
|
145
|
+
--output ./embeddings.jsonl
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Input format for `--file`:
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{"text":"First document"}
|
|
152
|
+
{"text":"Second document"}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Output format for `--output`:
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{"text":"First document","embedding":[0.123,0.456]}
|
|
159
|
+
{"text":"Second document","embedding":[0.789,0.012]}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
If `--output` is omitted, the CLI prints only a summary count and embedding dimension.
|
|
163
|
+
|
|
164
|
+
## Collection Commands
|
|
165
|
+
|
|
166
|
+
Create a collection:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
schift db create my-docs --dim 3072 --metric cosine
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
List collections:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
schift db list
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Inspect one collection:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
schift db stats my-docs
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`db list` prints a table with collection name, dimensions, metric, vector count, and creation time. `db stats` prints a detailed panel with index and storage metadata when the API returns it.
|
|
185
|
+
|
|
186
|
+
## Query Command
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
schift query \
|
|
190
|
+
"revenue guidance" \
|
|
191
|
+
--collection my-docs \
|
|
192
|
+
--top-k 10 \
|
|
193
|
+
--model openai/text-embedding-3-large \
|
|
194
|
+
--threshold 0.8
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
- `--collection` is required.
|
|
198
|
+
- `--model` is optional.
|
|
199
|
+
- `--threshold` lets you filter low-score results.
|
|
200
|
+
|
|
201
|
+
The CLI prints a ranked result table with ID, score, and a truncated text preview.
|
|
202
|
+
|
|
203
|
+
## Benchmark Command
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
schift bench \
|
|
207
|
+
--source openai/text-embedding-3-large \
|
|
208
|
+
--target google/gemini-embedding-004 \
|
|
209
|
+
--data ./queries.jsonl \
|
|
210
|
+
--top-k 10
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
- `--data` must be an existing local file path.
|
|
214
|
+
- The command shows an indeterminate spinner while the API runs the benchmark.
|
|
215
|
+
- Output includes recall, MRR, cosine similarity, and latency metrics when available.
|
|
216
|
+
|
|
217
|
+
This command is the safety gate before a live migration. Use it first and treat low recall as a rollout blocker.
|
|
218
|
+
|
|
219
|
+
## Migration Commands
|
|
220
|
+
|
|
221
|
+
Fit a projection:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
schift migrate fit \
|
|
225
|
+
--source openai/text-embedding-3-large \
|
|
226
|
+
--target google/gemini-embedding-004 \
|
|
227
|
+
--sample 0.1
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- `--sample` must be greater than `0` and less than or equal to `1`.
|
|
231
|
+
- The CLI returns a projection ID you pass into `migrate run`.
|
|
232
|
+
|
|
233
|
+
Dry-run a migration:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
schift migrate run \
|
|
237
|
+
--projection proj_abc123 \
|
|
238
|
+
--db pgvector://user:password@localhost:5432/app \
|
|
239
|
+
--dry-run \
|
|
240
|
+
--batch-size 1000
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Execute a live migration:
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
schift migrate run \
|
|
247
|
+
--projection proj_abc123 \
|
|
248
|
+
--db pgvector://user:password@localhost:5432/app \
|
|
249
|
+
--batch-size 1000
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Operational guidance:
|
|
253
|
+
|
|
254
|
+
- Start with `--dry-run`. It previews the migration without applying changes.
|
|
255
|
+
- A live run asks for interactive confirmation before proceeding.
|
|
256
|
+
- The displayed connection string masks the password in terminal output.
|
|
257
|
+
- Output includes processed vector count, skipped vector count, duration, and status.
|
|
258
|
+
|
|
259
|
+
## Usage Command
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
schift usage --period 30d
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Accepted values are free-form strings such as `7d`, `30d`, or `90d`; the server decides what periods it supports.
|
|
266
|
+
|
|
267
|
+
The command prints:
|
|
268
|
+
|
|
269
|
+
- A summary panel with total requests, embeddings, projections, queries, storage, and cost
|
|
270
|
+
- A per-model usage table when the API returns `by_model`
|
|
271
|
+
|
|
272
|
+
## Error Handling and Exit Behavior
|
|
273
|
+
|
|
274
|
+
- Authentication failures raise a direct action message telling you to run `schift auth login`.
|
|
275
|
+
- Connection failures mention the resolved API URL and suggest checking `SCHIFT_API_URL`.
|
|
276
|
+
- API errors exit non-zero and surface server-provided detail text when available.
|
|
277
|
+
- Empty result sets are handled as normal output, not crashes.
|
|
278
|
+
|
|
279
|
+
## Local Development
|
|
280
|
+
|
|
281
|
+
Install editable dependencies:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
cd sdk/cli
|
|
285
|
+
python3 -m pip install -e '.[dev]'
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Useful commands:
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
schift --version
|
|
292
|
+
schift --help
|
|
293
|
+
schift auth --help
|
|
294
|
+
schift migrate --help
|
|
295
|
+
pytest
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
When testing against a local API:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
export SCHIFT_API_URL=http://localhost:8080/v1
|
|
302
|
+
schift auth status
|
|
303
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "schift-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Schift CLI — manage agents, embeddings, and vector collections"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
dependencies = [
|
|
12
|
+
"click>=8.1",
|
|
13
|
+
"httpx>=0.27",
|
|
14
|
+
"rich>=13.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
dev = [
|
|
19
|
+
"pytest>=8.0",
|
|
20
|
+
"pytest-cov>=5.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
schift = "schift_cli.main:cli"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
include = ["schift_cli*"]
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.package-data]
|
|
30
|
+
schift_cli = ["data/schift-best-practices/**/*.md"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from schift_cli.config import get_api_key, get_api_url
|
|
9
|
+
|
|
10
|
+
# Timeout: 30s connect, 120s read (migrations can be slow)
|
|
11
|
+
DEFAULT_TIMEOUT = httpx.Timeout(30.0, read=120.0)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SchiftAPIError(Exception):
|
|
15
|
+
"""Raised when the Schift API returns a non-2xx response."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, status_code: int, detail: str):
|
|
18
|
+
self.status_code = status_code
|
|
19
|
+
self.detail = detail
|
|
20
|
+
super().__init__(f"HTTP {status_code}: {detail}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SchiftClient:
|
|
24
|
+
"""HTTP client for the Schift API.
|
|
25
|
+
|
|
26
|
+
Handles authentication headers, base URL resolution, and consistent
|
|
27
|
+
error handling so command modules can stay focused on CLI logic.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, api_key: str | None = None, base_url: str | None = None):
|
|
31
|
+
self.api_key = api_key or get_api_key()
|
|
32
|
+
self.base_url = (base_url or get_api_url()).rstrip("/")
|
|
33
|
+
self._http = httpx.Client(
|
|
34
|
+
base_url=self.base_url,
|
|
35
|
+
timeout=DEFAULT_TIMEOUT,
|
|
36
|
+
headers=self._build_headers(),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def _build_headers(self) -> dict[str, str]:
|
|
40
|
+
headers: dict[str, str] = {
|
|
41
|
+
"User-Agent": "schift-cli/0.1.0",
|
|
42
|
+
"Accept": "application/json",
|
|
43
|
+
}
|
|
44
|
+
if self.api_key:
|
|
45
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
46
|
+
return headers
|
|
47
|
+
|
|
48
|
+
# -- HTTP verbs ----------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
def get(self, path: str, **kwargs: Any) -> Any:
|
|
51
|
+
return self._request("GET", path, **kwargs)
|
|
52
|
+
|
|
53
|
+
def post(self, path: str, **kwargs: Any) -> Any:
|
|
54
|
+
return self._request("POST", path, **kwargs)
|
|
55
|
+
|
|
56
|
+
def put(self, path: str, **kwargs: Any) -> Any:
|
|
57
|
+
return self._request("PUT", path, **kwargs)
|
|
58
|
+
|
|
59
|
+
def delete(self, path: str, **kwargs: Any) -> Any:
|
|
60
|
+
return self._request("DELETE", path, **kwargs)
|
|
61
|
+
|
|
62
|
+
# -- Internal -------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
65
|
+
try:
|
|
66
|
+
resp = self._http.request(method, path, **kwargs)
|
|
67
|
+
except httpx.ConnectError:
|
|
68
|
+
raise click.ClickException(
|
|
69
|
+
f"Could not connect to Schift API at {self.base_url}\n"
|
|
70
|
+
" The server may be unavailable. Check your network or set "
|
|
71
|
+
"SCHIFT_API_URL if using a custom endpoint."
|
|
72
|
+
)
|
|
73
|
+
except httpx.TimeoutException:
|
|
74
|
+
raise click.ClickException(
|
|
75
|
+
"Request timed out. The server may be under heavy load — try again."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if resp.status_code == 401:
|
|
79
|
+
raise click.ClickException(
|
|
80
|
+
"Authentication failed. Run `schift auth login` to set your API key."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if resp.status_code >= 400:
|
|
84
|
+
try:
|
|
85
|
+
body = resp.json()
|
|
86
|
+
detail = body.get("detail") or body.get("message") or resp.text
|
|
87
|
+
except Exception:
|
|
88
|
+
detail = resp.text
|
|
89
|
+
raise SchiftAPIError(resp.status_code, str(detail))
|
|
90
|
+
|
|
91
|
+
if resp.status_code == 204:
|
|
92
|
+
return None
|
|
93
|
+
return resp.json()
|
|
94
|
+
|
|
95
|
+
def close(self) -> None:
|
|
96
|
+
self._http.close()
|
|
97
|
+
|
|
98
|
+
def __enter__(self) -> SchiftClient:
|
|
99
|
+
return self
|
|
100
|
+
|
|
101
|
+
def __exit__(self, *args: Any) -> None:
|
|
102
|
+
self.close()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def require_api_key() -> str:
|
|
106
|
+
"""Return the API key or abort with a helpful message."""
|
|
107
|
+
key = get_api_key()
|
|
108
|
+
if not key:
|
|
109
|
+
raise click.ClickException(
|
|
110
|
+
"No API key configured.\n"
|
|
111
|
+
" Run `schift auth login` or set the SCHIFT_API_KEY environment variable."
|
|
112
|
+
)
|
|
113
|
+
return key
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_client() -> SchiftClient:
|
|
117
|
+
"""Create a client, ensuring an API key is present."""
|
|
118
|
+
api_key = require_api_key()
|
|
119
|
+
return SchiftClient(api_key=api_key)
|
|
File without changes
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from schift_cli.config import clear_api_key, get_api_key, set_api_key, CONFIG_FILE
|
|
6
|
+
from schift_cli.display import success, info, error
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group("auth")
|
|
10
|
+
def auth() -> None:
|
|
11
|
+
"""Manage authentication with the Schift platform."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@auth.command()
|
|
15
|
+
def login() -> None:
|
|
16
|
+
"""Set your Schift API key."""
|
|
17
|
+
existing = get_api_key()
|
|
18
|
+
if existing:
|
|
19
|
+
overwrite = click.confirm(
|
|
20
|
+
"An API key is already configured. Overwrite?", default=False
|
|
21
|
+
)
|
|
22
|
+
if not overwrite:
|
|
23
|
+
info("Keeping existing API key.")
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
api_key = click.prompt("Enter your Schift API key", hide_input=True)
|
|
27
|
+
api_key = api_key.strip()
|
|
28
|
+
|
|
29
|
+
if not api_key:
|
|
30
|
+
raise click.ClickException("API key cannot be empty.")
|
|
31
|
+
|
|
32
|
+
set_api_key(api_key)
|
|
33
|
+
success(f"API key saved to {CONFIG_FILE}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@auth.command()
|
|
37
|
+
def logout() -> None:
|
|
38
|
+
"""Remove the stored API key."""
|
|
39
|
+
if not get_api_key():
|
|
40
|
+
info("No API key is currently stored.")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
clear_api_key()
|
|
44
|
+
success("API key removed.")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@auth.command()
|
|
48
|
+
def status() -> None:
|
|
49
|
+
"""Show current authentication status."""
|
|
50
|
+
import os
|
|
51
|
+
from schift_cli.config import ENV_API_KEY
|
|
52
|
+
|
|
53
|
+
env_key = os.environ.get(ENV_API_KEY)
|
|
54
|
+
file_key = None
|
|
55
|
+
try:
|
|
56
|
+
from schift_cli.config import load_config
|
|
57
|
+
file_key = load_config().get("api_key")
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
if env_key:
|
|
62
|
+
masked = env_key[:8] + "..." + env_key[-4:] if len(env_key) > 12 else "***"
|
|
63
|
+
success(f"Authenticated via {ENV_API_KEY} env var (key: {masked})")
|
|
64
|
+
elif file_key:
|
|
65
|
+
masked = file_key[:8] + "..." + file_key[-4:] if len(file_key) > 12 else "***"
|
|
66
|
+
success(f"Authenticated via config file (key: {masked})")
|
|
67
|
+
else:
|
|
68
|
+
error("Not authenticated. Run `schift auth login` to get started.")
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from schift_cli.client import get_client, SchiftAPIError
|
|
8
|
+
from schift_cli.display import console, error, info, print_kv, spinner, success
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command("bench")
|
|
12
|
+
@click.option("--source", "-s", required=True, help="Source model ID (e.g. openai/text-embedding-3-large)")
|
|
13
|
+
@click.option("--target", "-t", required=True, help="Target model ID (e.g. google/gemini-embedding-004)")
|
|
14
|
+
@click.option("--data", "-d", type=click.Path(exists=True, path_type=Path), required=True,
|
|
15
|
+
help="JSONL file with benchmark queries")
|
|
16
|
+
@click.option("--top-k", "-k", type=int, default=10, show_default=True,
|
|
17
|
+
help="Number of results to compare per query")
|
|
18
|
+
def bench(source: str, target: str, data: Path, top_k: int) -> None:
|
|
19
|
+
"""Benchmark embedding quality between two models.
|
|
20
|
+
|
|
21
|
+
Measures how well a Schift projection preserves retrieval quality
|
|
22
|
+
when switching from SOURCE to TARGET model.
|
|
23
|
+
"""
|
|
24
|
+
info(f"Benchmarking projection: {source} -> {target}")
|
|
25
|
+
info(f"Data: {data} | top-k: {top_k}")
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
with get_client() as client:
|
|
29
|
+
with spinner("Running benchmark...") as progress:
|
|
30
|
+
progress.add_task("Running benchmark...", total=None)
|
|
31
|
+
result = client.post(
|
|
32
|
+
"/bench",
|
|
33
|
+
json={
|
|
34
|
+
"source_model": source,
|
|
35
|
+
"target_model": target,
|
|
36
|
+
"data_path": str(data),
|
|
37
|
+
"top_k": top_k,
|
|
38
|
+
},
|
|
39
|
+
)
|
|
40
|
+
except SchiftAPIError as e:
|
|
41
|
+
error(f"Benchmark failed: {e.detail}")
|
|
42
|
+
raise SystemExit(1)
|
|
43
|
+
except click.ClickException:
|
|
44
|
+
raise
|
|
45
|
+
|
|
46
|
+
report = result.get("report", result)
|
|
47
|
+
print_kv("Benchmark Report", {
|
|
48
|
+
"Source Model": source,
|
|
49
|
+
"Target Model": target,
|
|
50
|
+
"Queries": report.get("num_queries", "-"),
|
|
51
|
+
"Recall@k": report.get("recall_at_k", "-"),
|
|
52
|
+
"MRR": report.get("mrr", "-"),
|
|
53
|
+
"Cosine Similarity (avg)": report.get("avg_cosine_similarity", "-"),
|
|
54
|
+
"Latency (p50)": report.get("latency_p50_ms", "-"),
|
|
55
|
+
"Latency (p99)": report.get("latency_p99_ms", "-"),
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
quality = report.get("recall_at_k")
|
|
59
|
+
if quality is not None:
|
|
60
|
+
if float(quality) >= 0.95:
|
|
61
|
+
success("Projection quality is excellent.")
|
|
62
|
+
elif float(quality) >= 0.85:
|
|
63
|
+
console.print("[yellow]Projection quality is acceptable but may degrade edge cases.[/]")
|
|
64
|
+
else:
|
|
65
|
+
console.print("[red]Projection quality is low. Consider increasing sample size in `migrate fit`.[/]")
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from schift_cli.client import get_client, SchiftAPIError
|
|
6
|
+
from schift_cli.display import print_table, print_kv, error
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group("catalog")
|
|
10
|
+
def catalog() -> None:
|
|
11
|
+
"""Browse available embedding models."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@catalog.command("list")
|
|
15
|
+
def list_models() -> None:
|
|
16
|
+
"""List all supported embedding models."""
|
|
17
|
+
try:
|
|
18
|
+
with get_client() as client:
|
|
19
|
+
data = client.get("/catalog/models")
|
|
20
|
+
except SchiftAPIError as e:
|
|
21
|
+
error(f"Failed to fetch model catalog: {e.detail}")
|
|
22
|
+
raise SystemExit(1)
|
|
23
|
+
except click.ClickException:
|
|
24
|
+
raise
|
|
25
|
+
|
|
26
|
+
models = data.get("models", [])
|
|
27
|
+
if not models:
|
|
28
|
+
click.echo("No models found in the catalog.")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
rows = [
|
|
32
|
+
(
|
|
33
|
+
m.get("id", ""),
|
|
34
|
+
m.get("provider", ""),
|
|
35
|
+
str(m.get("dimensions", "")),
|
|
36
|
+
m.get("max_tokens", ""),
|
|
37
|
+
m.get("status", ""),
|
|
38
|
+
)
|
|
39
|
+
for m in models
|
|
40
|
+
]
|
|
41
|
+
print_table(
|
|
42
|
+
"Embedding Model Catalog",
|
|
43
|
+
["Model ID", "Provider", "Dimensions", "Max Tokens", "Status"],
|
|
44
|
+
rows,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@catalog.command("get")
|
|
49
|
+
@click.argument("model_id")
|
|
50
|
+
def get_model(model_id: str) -> None:
|
|
51
|
+
"""Show details for a specific model.
|
|
52
|
+
|
|
53
|
+
MODEL_ID is the fully qualified model name, e.g. openai/text-embedding-3-large
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
with get_client() as client:
|
|
57
|
+
data = client.get(f"/catalog/models/{model_id}")
|
|
58
|
+
except SchiftAPIError as e:
|
|
59
|
+
if e.status_code == 404:
|
|
60
|
+
error(f"Model not found: {model_id}")
|
|
61
|
+
else:
|
|
62
|
+
error(f"Failed to fetch model: {e.detail}")
|
|
63
|
+
raise SystemExit(1)
|
|
64
|
+
except click.ClickException:
|
|
65
|
+
raise
|
|
66
|
+
|
|
67
|
+
model = data.get("model", data)
|
|
68
|
+
print_kv(f"Model: {model_id}", {
|
|
69
|
+
"Provider": model.get("provider", "-"),
|
|
70
|
+
"Dimensions": model.get("dimensions", "-"),
|
|
71
|
+
"Max Tokens": model.get("max_tokens", "-"),
|
|
72
|
+
"Status": model.get("status", "-"),
|
|
73
|
+
"Description": model.get("description", "-"),
|
|
74
|
+
})
|