Embedding model migration SDK. Switch models without re-embedding.
Project description
Schift Python SDK
Python client and local migration toolkit for Schift.
The package currently exposes two public client styles:
Schift: modular client for live API operations such as bucket ingest, catalog lookup, embedding, routing, search, usage, and hosted bucket management.Client: legacy projection client for fitting and downloadingProjectionobjects that run locally.
Install
Base package:
pip install schift
Optional adapters:
pip install "schift[postgres]"
pip install "schift[qdrant]"
pip install "schift[all]"
Development extras:
pip install -e ".[dev]"
Authentication And Base URLs
The modular client reads SCHIFT_API_KEY automatically:
export SCHIFT_API_KEY=sch_your_key_here
Equivalent explicit construction:
from schift import Schift
client = Schift(
api_key="sch_your_key_here",
base_url="https://api.schift.io/v1",
timeout=60.0,
)
Notes:
Schiftexpects a/v1base URL.- The legacy
Clientdefaults tohttps://api.schift.ioand appends/v1/...internally.
Client Lifecycle
Schift holds a shared httpx.Client, so prefer a context manager for short-lived scripts:
from schift import Schift
with Schift() as client:
models = client.catalog.list()
For long-running processes, keep one Schift instance and call close() during shutdown.
Bucket Ingest And Search
from schift import Schift
with Schift() as client:
bucket = client.buckets.create(name="finance-docs")
upload = client.buckets.upload(
bucket["id"],
[("files", ("q1-report.pdf", open("q1-report.pdf", "rb").read(), "application/pdf"))],
)
jobs = client.buckets.list_jobs(bucket_id=bucket["id"])
hits = client.buckets.search(
bucket["id"],
"revenue guidance",
top_k=5,
filter={"metadata": {"quarter": "Q1"}},
)
print(upload["bucket_id"])
print(jobs[0]["status"] if jobs else "queued")
print(hits[0] if hits else "no hits")
Use client.buckets and client.query(..., bucket=...) for retrieval, POST /v1/chat for bucket-backed RAG chat with sources, and POST /v1/chat/completions for OpenAI-compatible LLM routing without bucket context.
The bucket helper also exposes list_collections(), get_job(), list_jobs(), wait_for_job(), and poll_job() so scripts can keep ingest orchestration in one place.
Quickstart
from schift import Schift
with Schift() as client:
models = client.catalog.list()
vector = client.embed(
"quarterly revenue report",
model="openai/text-embedding-3-small",
)
bucket = client.buckets.create("finance-docs")
client.db.upsert(
collection="finance-docs",
vectors=[
{
"id": "doc-1",
"values": vector.tolist(),
"metadata": {"source": "q1-report"},
}
],
)
hits = client.query(
"revenue guidance",
bucket="finance-docs",
top_k=5,
)
print(models[0]["id"] if models else "no models")
print(hits[0] if hits else "no hits")
Module Reference
catalog
List available models and inspect one model by ID.
from schift import Schift
with Schift() as client:
models = client.catalog.list()
model = client.catalog.get("openai/text-embedding-3-small")
embed
The embed module is callable for single-text requests and exposes batch() for multiple texts.
from schift import Schift
with Schift() as client:
one = client.embed(
"hello world",
model="openai/text-embedding-3-small",
)
many = client.embed.batch(
texts=["hello", "goodbye"],
model="openai/text-embedding-3-small",
dimensions=1024,
)
Return values are NumPy arrays.
providers
Register your own LLM provider API key (BYOK) so client.chat and client.completions call the provider directly instead of consuming Schift Cloud's shared LLM quota. Supported providers: "openai", "google", "anthropic".
from schift import Schift
with Schift() as client:
# Register a key
client.providers.set(
"google",
api_key="AIza...",
# endpoint_url="https://custom-proxy.example.com", # optional
)
# Check whether a provider is configured (api_key is never returned)
status = client.providers.get("openai")
# {"provider": "openai", "configured": True | False, "endpoint_url": None | str}
Rotation: a stored BYOK record shadows any server-side env var or secret for that provider. To rotate, call set() again with the new key — changing env vars alone has no effect on orgs with a BYOK record.
routing
Read or update the server-side routing policy used by Schift when a model is omitted.
from schift import Schift
with Schift() as client:
current = client.routing.get()
updated = client.routing.set(
primary="openai/text-embedding-3-small",
fallback="google/gemini-embedding-001",
mode="failover",
)
pii
Mask Korean PII before sending text to an LLM, vector store, or workflow step.
Use types when you want the masking contract to be visible and selectable.
from schift import Schift
PII_TYPES = [
"resident_id",
"alien_registration",
"passport",
"driver_license",
"address",
"phone",
"bank_account",
]
with Schift() as client:
result = client.redact_pii(
"주민등록번호 850205-1234567, 연락처 010-1234-5678",
types=PII_TYPES,
)
print(result["masked"])
masked = client.mask(
"계좌 123-45-678901",
types=["bank_account"],
)
The default token format returns tokens such as [PII_PHONE_1] and a
reverse_map so the caller can restore the original values after the workflow
step. Keep that map only in your temporary restore path; Schift does not
persist or gateway-cache it for pii_type_index requests. Set
token_format="label_index" only when you need legacy tokens such as [PHONE_1].
Send only masked into the LLM, agent, vector store, or workflow step. Never
include reverse_map in the AI payload; use it only after the AI result returns
if your app needs to restore values.
restored = client.restore_pii(
"고객 연락처 [PII_PHONE_1]로 안내하세요.",
result["reverse_map"],
)
restore_pii() is a stateless API call. It requires an API key and does not
persist or cache the map.
bench
Run a server-side benchmark between two model IDs. The SDK returns BenchReport.
from schift import Schift
with Schift() as client:
report = client.bench.run(
source="openai/text-embedding-3-small",
target="google/gemini-embedding-001",
data="./eval_queries.jsonl",
)
print(report.verdict)
print(report.summary())
db
Manage hosted buckets and write vectors or raw documents.
from schift import Schift
with Schift() as client:
vector = client.embed(
"Schift reduces vector migration downtime.",
model="openai/text-embedding-3-small",
)
bucket = client.db.create_collection(
name="product-docs",
dimension=len(vector),
)
client.db.upsert(
collection="product-docs",
vectors=[
{
"id": "doc-1",
"values": vector.tolist(),
"metadata": {"title": "Launch plan"},
}
],
)
client.db.upsert_text(
collection="product-docs",
documents=[
{
"id": "doc-2",
"text": "Schift reduces vector migration downtime.",
"metadata": {"title": "Overview"},
}
],
model="openai/text-embedding-3-small",
)
stats = client.db.collection_stats("product-docs")
Available methods:
create_collection(name, dimension)list_buckets()get_collection(name)collection_stats(name)delete_collection(name)upsert(collection, vectors)upsert_text(collection, documents, model)
query
The query module is callable and supports hosted buckets or an external DB handle.
from schift import Schift
with Schift() as client:
hosted = client.query(
"vector migration rollback plan",
bucket="product-docs",
top_k=10,
rerank=True,
rerank_top_k=5,
)
passthrough = client.query(
"incident response",
db="prod-search-db",
model="openai/text-embedding-3-small",
top_k=5,
)
rerank
Rerank a list of candidate documents with a cross-encoder style endpoint.
from schift import Schift
with Schift() as client:
reranked = client.rerank(
"incident response",
documents=[
{"id": "doc-1", "text": "..."},
{"id": "doc-2", "text": "..."},
],
top_k=2,
)
usage
Fetch aggregate usage for billing or dashboards.
from schift import Schift
with Schift() as client:
usage = client.usage.get(period="30d", granularity="day")
Workflows
Build and run RAG pipelines as composable DAGs.
Quick Start
from schift import Schift
with Schift() as client:
# Create from template
wf = client.workflow.create_rag("Product Search")
# Run with inputs
result = client.workflow.run(wf.id, inputs={"query": "best laptop"})
print(result.outputs["answer"])
CRUD
wf = client.workflow.create("My Pipeline", description="Custom RAG")
workflows = client.workflow.list()
wf = client.workflow.get(wf.id)
wf = client.workflow.update(wf.id, name="Renamed")
client.workflow.delete(wf.id)
Building a Graph
wf = client.workflow.create("Custom RAG")
# Add blocks
start = client.workflow.add_block(wf.id, "start", title="Start")
retriever = client.workflow.add_block(wf.id, "retriever", config={
"bucket": "my-docs",
"top_k": 5,
"rerank": True,
"rerank_top_k": 3,
})
prompt = client.workflow.add_block(wf.id, "prompt_template", config={
"template": "Answer based on:\n{{results}}\n\nQuestion: {{query}}",
})
llm = client.workflow.add_block(wf.id, "llm", config={
"model": "openai/gpt-4o-mini", # or "anthropic/claude-sonnet-4-20250514", "gemini-2.5-flash"
"temperature": 0.7,
})
end = client.workflow.add_block(wf.id, "end")
# Connect blocks
client.workflow.add_edge(wf.id, start["id"], retriever["id"])
client.workflow.add_edge(wf.id, retriever["id"], prompt["id"])
client.workflow.add_edge(wf.id, prompt["id"], llm["id"])
client.workflow.add_edge(wf.id, llm["id"], end["id"])
Multiple Inputs
The start node forwards all input variables to downstream blocks:
result = client.workflow.run(wf.id, inputs={
"query": "maternity leave policy",
"user_id": "u-123",
"language": "ko",
})
Reference variables in prompt templates with {{variable}} syntax.
Validation & Run History
# Validate graph (cycles, missing connections, etc.)
v = client.workflow.validate(wf.id)
print(v.valid, v.errors)
# List past runs
runs = client.workflow.runs(wf.id)
for r in runs:
print(r.run_id, r.status, r.outputs)
YAML Import / Export
# Export to YAML
yaml_str = client.workflow.to_yaml(wf.id, path="pipeline.yaml")
# Load from file
definition = client.workflow.from_yaml("pipeline.yaml")
# Push YAML to create on server
wf = client.workflow.push_yaml("pipeline.yaml")
Templates
| Method | Template |
|---|---|
create_rag(name) |
Retriever -> Prompt -> LLM |
create_doc_qa(name) |
Document QA with sources |
create_chat(name) |
Conversational RAG |
create_ocr_ingest(name) |
OCR -> Chunk -> Embed |
Async Workflows
from schift import AsyncSchift
async with AsyncSchift() as client:
wf = await client.workflow.create_rag("Async RAG")
result = await client.workflow.run(wf.id, inputs={"query": "hello"})
Block Types
| Category | Types |
|---|---|
| Control | start, end, conditional, loop |
| Retrieval | retriever (with rerank toggle), reranker |
| LLM | llm (OpenAI/Anthropic/Google), prompt_template, answer |
| Data | document_loader, chunker, embedder, text_processor |
| Integration | api_call, webhook, code_executor |
| Storage | vector_store, cache |
API Reference
| Method | Description |
|---|---|
workflow.create(name) |
Create workflow |
workflow.get(id) |
Get workflow |
workflow.list() |
List workflows |
workflow.update(id, **kw) |
Update workflow |
workflow.delete(id) |
Delete workflow |
workflow.run(id, inputs) |
Run workflow |
workflow.runs(id) |
List past runs |
workflow.validate(id) |
Validate graph |
workflow.add_block(id, type, config) |
Add block |
workflow.add_edge(id, src, tgt) |
Add edge |
workflow.to_yaml(id) |
Export as YAML |
workflow.from_yaml(path) |
Load YAML definition |
workflow.push_yaml(path) |
Import YAML to server |
Projection Workflow
Projection creation still lives in the legacy Client API. It returns a local Projection object that can transform vectors without further API calls.
from schift import Client
legacy = Client(api_key="sch_your_key_here")
# Embed the same sample texts with both providers before fitting.
source_pairs = old_model_embeddings
target_pairs = new_model_embeddings
projection = legacy.fit(
source=source_pairs,
target=target_pairs,
source_model="openai/text-embedding-3-small",
target_model="google/gemini-embedding-001",
project_name="openai-to-gemini",
)
converted = projection.transform(source_pairs[:10])
projection.save("./projection-openai-to-gemini")
You can later reload a saved projection:
from schift import Projection
projection = Projection.load("./projection-openai-to-gemini")
The legacy client also supports:
fit(...)bench(...)list_projections()get_projection(project_id)
Local Migration With Adapters
The local migration engine reads vectors from a source adapter, applies a Projection, and writes to a sink adapter. This path does not depend on hosted collection APIs.
from schift import Projection
from schift.migrate import migrate
from schift.adapters.file import NpyAdapter
projection = Projection.load("./projection-openai-to-gemini")
source = NpyAdapter("old_embeddings.npy")
sink = NpyAdapter("new_embeddings.npy")
result = migrate(
source=source,
sink=sink,
projection=projection,
batch_size=2048,
dry_run=True,
)
print(result)
The same engine is exposed through Schift().migrate.run(...).
Built-in Adapters
NumPy files
from schift.adapters.file import NpyAdapter
adapter = NpyAdapter("embeddings.npy")
pgvector
Requires pip install "schift[postgres]".
from schift.adapters.pgvector import PgVectorAdapter
adapter = PgVectorAdapter(
conninfo="postgresql://user:password@localhost/mydb",
table="documents",
embedding_column="embedding",
id_column="id",
)
Qdrant
Requires pip install "schift[qdrant]".
from schift.adapters.qdrant import QdrantAdapter
adapter = QdrantAdapter(
url="http://localhost:6333",
collection="documents",
api_key=None,
)
Registry Helpers
from schift.adapters import get_adapter, list_adapters
print(list_adapters())
adapter = get_adapter(
{
"type": "npy",
"path": "embeddings.npy",
}
)
Errors
Both client styles raise exceptions from schift.client:
AuthErrorQuotaErrorSchiftError
Example:
from schift import Schift
from schift.client import AuthError, QuotaError, SchiftError
try:
with Schift() as client:
client.catalog.list()
except AuthError:
...
except QuotaError:
...
except SchiftError:
...
Development
From clients/sdk/python/:
python -m pip install -e ".[dev]"
python -m ruff check .
python -m pytest
Source Layout
clients/sdk/python/
├── schift/
│ ├── schift_client.py # modular SDK entry point
│ ├── client.py # legacy projection client
│ ├── projection.py # local projection object
│ ├── adapters/ # npy, pgvector, qdrant
│ ├── workflow.py # workflow CRUD, blocks, edges, YAML, templates
│ └── *.py # catalog, embed, routing, db, query, rerank, usage
└── docs/ # request/response contracts and schemas
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file schift-0.7.0.tar.gz.
File metadata
- Download URL: schift-0.7.0.tar.gz
- Upload date:
- Size: 360.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a354e4bf6896c88da41c72d3cb6076825307e90ada73671a35a6655c7a4777dc
|
|
| MD5 |
8c693617c35efbf4cc09a51a1db19d6a
|
|
| BLAKE2b-256 |
a640effd2ca7a5a7a0754f971d0f9c94195b510131ba69898c20ec92cb146d89
|
File details
Details for the file schift-0.7.0-py3-none-any.whl.
File metadata
- Download URL: schift-0.7.0-py3-none-any.whl
- Upload date:
- Size: 68.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
533fe497e24868133ecbded67e850286cc3ebcff57a27519d56571dda3a0e237
|
|
| MD5 |
202987da839472b1851eb383d392108a
|
|
| BLAKE2b-256 |
72d80e4724d11edf425a12988967c16d806bfe0e362e25b8e589f7b3cee51e1e
|