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.
Files changed (45) hide show
  1. schift_cli-0.1.0/PKG-INFO +12 -0
  2. schift_cli-0.1.0/README.md +303 -0
  3. schift_cli-0.1.0/pyproject.toml +30 -0
  4. schift_cli-0.1.0/schift_cli/__init__.py +1 -0
  5. schift_cli-0.1.0/schift_cli/client.py +119 -0
  6. schift_cli-0.1.0/schift_cli/commands/__init__.py +0 -0
  7. schift_cli-0.1.0/schift_cli/commands/auth.py +68 -0
  8. schift_cli-0.1.0/schift_cli/commands/bench.py +65 -0
  9. schift_cli-0.1.0/schift_cli/commands/catalog.py +74 -0
  10. schift_cli-0.1.0/schift_cli/commands/db.py +96 -0
  11. schift_cli-0.1.0/schift_cli/commands/embed.py +104 -0
  12. schift_cli-0.1.0/schift_cli/commands/migrate.py +127 -0
  13. schift_cli-0.1.0/schift_cli/commands/query.py +66 -0
  14. schift_cli-0.1.0/schift_cli/commands/skill.py +110 -0
  15. schift_cli-0.1.0/schift_cli/commands/usage.py +50 -0
  16. schift_cli-0.1.0/schift_cli/config.py +58 -0
  17. schift_cli-0.1.0/schift_cli/data/schift-best-practices/AGENTS.md +77 -0
  18. schift_cli-0.1.0/schift_cli/data/schift-best-practices/CLAUDE.md +77 -0
  19. schift_cli-0.1.0/schift_cli/data/schift-best-practices/SKILL.md +89 -0
  20. schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/bucket-organization.md +126 -0
  21. schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/bucket-upload.md +116 -0
  22. schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/chatbot-widget.md +238 -0
  23. schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/cost-batching.md +179 -0
  24. schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/cost-storage-tiers.md +183 -0
  25. schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/deploy-cloudrun.md +140 -0
  26. schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/embed-batch-processing.md +86 -0
  27. schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/embed-error-handling.md +155 -0
  28. schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/embed-multimodal.md +100 -0
  29. schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/embed-task-types.md +135 -0
  30. schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/rag-chunking.md +173 -0
  31. schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/rag-workflow-builder.md +205 -0
  32. schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/sdk-async-patterns.md +103 -0
  33. schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/sdk-auth-patterns.md +76 -0
  34. schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/search-collection-design.md +229 -0
  35. schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/search-hybrid.md +163 -0
  36. schift_cli-0.1.0/schift_cli/data/schift-best-practices/references/search-similarity-tuning.md +134 -0
  37. schift_cli-0.1.0/schift_cli/display.py +85 -0
  38. schift_cli-0.1.0/schift_cli/main.py +39 -0
  39. schift_cli-0.1.0/schift_cli.egg-info/PKG-INFO +12 -0
  40. schift_cli-0.1.0/schift_cli.egg-info/SOURCES.txt +43 -0
  41. schift_cli-0.1.0/schift_cli.egg-info/dependency_links.txt +1 -0
  42. schift_cli-0.1.0/schift_cli.egg-info/entry_points.txt +2 -0
  43. schift_cli-0.1.0/schift_cli.egg-info/requires.txt +7 -0
  44. schift_cli-0.1.0/schift_cli.egg-info/top_level.txt +1 -0
  45. 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
+ })