schift-cli 0.1.0__py3-none-any.whl

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 (40) hide show
  1. schift_cli/__init__.py +1 -0
  2. schift_cli/client.py +119 -0
  3. schift_cli/commands/__init__.py +0 -0
  4. schift_cli/commands/auth.py +68 -0
  5. schift_cli/commands/bench.py +65 -0
  6. schift_cli/commands/catalog.py +74 -0
  7. schift_cli/commands/db.py +96 -0
  8. schift_cli/commands/embed.py +104 -0
  9. schift_cli/commands/migrate.py +127 -0
  10. schift_cli/commands/query.py +66 -0
  11. schift_cli/commands/skill.py +110 -0
  12. schift_cli/commands/usage.py +50 -0
  13. schift_cli/config.py +58 -0
  14. schift_cli/data/schift-best-practices/AGENTS.md +77 -0
  15. schift_cli/data/schift-best-practices/CLAUDE.md +77 -0
  16. schift_cli/data/schift-best-practices/SKILL.md +89 -0
  17. schift_cli/data/schift-best-practices/references/bucket-organization.md +126 -0
  18. schift_cli/data/schift-best-practices/references/bucket-upload.md +116 -0
  19. schift_cli/data/schift-best-practices/references/chatbot-widget.md +238 -0
  20. schift_cli/data/schift-best-practices/references/cost-batching.md +179 -0
  21. schift_cli/data/schift-best-practices/references/cost-storage-tiers.md +183 -0
  22. schift_cli/data/schift-best-practices/references/deploy-cloudrun.md +140 -0
  23. schift_cli/data/schift-best-practices/references/embed-batch-processing.md +86 -0
  24. schift_cli/data/schift-best-practices/references/embed-error-handling.md +155 -0
  25. schift_cli/data/schift-best-practices/references/embed-multimodal.md +100 -0
  26. schift_cli/data/schift-best-practices/references/embed-task-types.md +135 -0
  27. schift_cli/data/schift-best-practices/references/rag-chunking.md +173 -0
  28. schift_cli/data/schift-best-practices/references/rag-workflow-builder.md +205 -0
  29. schift_cli/data/schift-best-practices/references/sdk-async-patterns.md +103 -0
  30. schift_cli/data/schift-best-practices/references/sdk-auth-patterns.md +76 -0
  31. schift_cli/data/schift-best-practices/references/search-collection-design.md +229 -0
  32. schift_cli/data/schift-best-practices/references/search-hybrid.md +163 -0
  33. schift_cli/data/schift-best-practices/references/search-similarity-tuning.md +134 -0
  34. schift_cli/display.py +85 -0
  35. schift_cli/main.py +39 -0
  36. schift_cli-0.1.0.dist-info/METADATA +12 -0
  37. schift_cli-0.1.0.dist-info/RECORD +40 -0
  38. schift_cli-0.1.0.dist-info/WHEEL +5 -0
  39. schift_cli-0.1.0.dist-info/entry_points.txt +2 -0
  40. schift_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,183 @@
1
+ ---
2
+ title: Understand Storage Tier Lifecycle to Optimize Costs
3
+ impact: MEDIUM
4
+ impactDescription: Using the wrong storage tier causes either data loss (Free tier deletes S3 data after 30 days) or unnecessary cost (storing cold-access data in FAISS hot storage). Matching tier to access pattern is essential for production reliability.
5
+ tags:
6
+ - cost
7
+ - storage
8
+ - tiers
9
+ - lifecycle
10
+ - faiss
11
+ - s3
12
+ ---
13
+
14
+ ## Understand Storage Tier Lifecycle to Optimize Costs
15
+
16
+ Schift stores your vector data in two layers: FAISS (hot, in-memory vector index for fast search) and S3 (durable object storage for the original chunks and metadata). Each pricing tier has different retention policies for both layers. Mismatching your use case to the wrong tier causes data loss or unnecessary cost.
17
+
18
+ The critical mistake is treating the Free tier as suitable for production data — S3 data is deleted after 30 days, and once deleted, your collection cannot be rebuilt without re-uploading and re-embedding all source documents.
19
+
20
+ ### Storage Tier Reference
21
+
22
+ | Tier | FAISS Hot Retention | S3 Retention | Use Case |
23
+ |------|--------------------|--------------|-|
24
+ | Free | 7 days | Deleted after 30 days | Development, demos, prototypes |
25
+ | Pro | 30 days | → IA after 30d → Glacier after 90d | Production applications |
26
+ | Enterprise | 90 days | → IA after 90d | High-frequency search workloads |
27
+
28
+ **Key terms:**
29
+ - **FAISS hot**: Vector index loaded in memory — sub-millisecond search latency
30
+ - **FAISS cold**: Index evicted from memory, reloaded on next query (adds ~1–3s warmup)
31
+ - **S3 Standard**: Instant retrieval, billed at standard rate
32
+ - **S3 IA** (Infrequent Access): Lower storage cost, slightly higher retrieval cost
33
+ - **S3 Glacier**: Cheapest storage, minutes-to-hours retrieval time (not suitable for live search)
34
+
35
+ ### Incorrect
36
+
37
+ Using Free tier for production data — collection becomes unreachable after 30 days:
38
+
39
+ ```python
40
+ # Python — Free tier for production: S3 data deleted at day 30
41
+ from schift import Schift
42
+
43
+ # Free tier API key used in production application
44
+ client = Schift(api_key="sch_free_...")
45
+
46
+ # Upload 50,000 documents — 3 months of engineering work
47
+ result = client.bucket.upload_bulk(bucket_id, documents)
48
+ print(f"Indexed {result.chunk_count} chunks")
49
+
50
+ # Day 7: FAISS evicted — first query adds 2s cold-start latency (no warning)
51
+ # Day 30: S3 data deleted — collection.search() returns empty results with no error
52
+ # Day 31: client.search(collection_id, query) → [] (silently empty, data gone)
53
+
54
+ # There is no recovery path. Source documents must be re-uploaded.
55
+ ```
56
+
57
+ ```typescript
58
+ // TypeScript — Free tier with no monitoring: invisible data expiry
59
+ import { Schift } from '@schift-io/sdk';
60
+
61
+ const client = new Schift({ apiKey: 'sch_free_...' }); // Free tier
62
+
63
+ // Build a production chatbot — works fine for 29 days
64
+ const answer = await client.workflow.run(workflowId, { query: userQuestion });
65
+
66
+ // Day 30+: search returns no results, LLM generates hallucinated answers
67
+ // because context is empty. No exception thrown — just silent data loss.
68
+ ```
69
+
70
+ ### Correct
71
+
72
+ Use Pro for production data; pin frequently-accessed collections to hot storage:
73
+
74
+ ```python
75
+ # Python — Pro tier with explicit tier awareness
76
+ from schift import Schift
77
+
78
+ client = Schift(api_key="sch_pro_...") # Pro tier
79
+
80
+ # Create collection with tier-appropriate settings
81
+ collection = client.collection.create(
82
+ name="docs-prod",
83
+ tier="pro", # 30d FAISS hot, S3 → IA → Glacier lifecycle
84
+ region="ap-northeast-2",
85
+ )
86
+
87
+ # Pin high-traffic collections to hot storage indefinitely
88
+ # (overrides the 30-day FAISS eviction, billed at hot storage rate)
89
+ client.collection.pin(collection.id, hot=True)
90
+
91
+ # Upload production data — durable beyond 90 days
92
+ result = client.bucket.upload_bulk(
93
+ bucket_id,
94
+ documents,
95
+ metadata={"env": "production", "indexed_at": "2026-03-17"}
96
+ )
97
+
98
+ # Monitor cold-start risk: check FAISS warmth before high-stakes queries
99
+ status = client.collection.status(collection.id)
100
+ if not status.faiss_hot:
101
+ client.collection.warm(collection.id) # pre-warm before query
102
+ # warm() is async — poll status or add a short wait for latency-sensitive paths
103
+ ```
104
+
105
+ ```typescript
106
+ // TypeScript — Pro tier with lifecycle-aware patterns
107
+ import { Schift } from '@schift-io/sdk';
108
+
109
+ const client = new Schift({ apiKey: 'sch_pro_...' }); // Pro tier
110
+
111
+ // Setup: create production collection with hot pin for frequently searched data
112
+ async function setupProductionCollection() {
113
+ const collection = await client.collection.create({
114
+ name: 'docs-prod',
115
+ tier: 'pro',
116
+ region: 'ap-northeast-2',
117
+ });
118
+
119
+ // Pin to hot storage if this collection is queried frequently (>100 queries/day)
120
+ await client.collection.pin(collection.id, { hot: true });
121
+
122
+ return collection.id;
123
+ }
124
+
125
+ // Runtime: check warmth before user-facing queries to avoid cold-start surprises
126
+ async function searchWithWarmGuard(collectionId: string, query: string) {
127
+ const status = await client.collection.status(collectionId);
128
+
129
+ if (!status.faissHot) {
130
+ // Trigger warm-up for low-latency access on next query
131
+ // For latency-sensitive paths, await the warm before proceeding
132
+ await client.collection.warm(collectionId);
133
+ }
134
+
135
+ return client.search(collectionId, query, { topK: 5 });
136
+ }
137
+ ```
138
+
139
+ **Tier selection guide:**
140
+
141
+ ```python
142
+ # Python — choose tier based on access pattern
143
+ def choose_tier(access_frequency: str, data_criticality: str) -> str:
144
+ """
145
+ access_frequency: "daily" | "weekly" | "monthly" | "one-time"
146
+ data_criticality: "production" | "staging" | "dev"
147
+ """
148
+ if data_criticality == "dev":
149
+ return "free" # OK to lose after 30 days
150
+
151
+ if access_frequency == "daily":
152
+ return "pro" # FAISS stays hot 30d, pin for longer
153
+
154
+ if access_frequency in ("weekly", "monthly") and data_criticality == "production":
155
+ return "pro" # S3 lifecycle keeps data, FAISS cold-starts acceptable
156
+
157
+ if access_frequency == "one-time":
158
+ return "free" # Temporary indexing, no need for retention
159
+
160
+ return "pro" # Default to Pro for any production use
161
+ ```
162
+
163
+ **Cost impact of correct tier usage (10GB collection):**
164
+
165
+ | Tier | Storage Cost/Month | Risk |
166
+ |------|--------------------|------|
167
+ | Free | $0 | Data deleted at day 30 |
168
+ | Pro | $1.50 (10GB × $0.15) | Safe for production |
169
+ | Enterprise | Custom | High-frequency, SLA-backed |
170
+
171
+ **Production checklist:**
172
+
173
+ - Never use a Free tier key in any environment where data loss is unacceptable
174
+ - Pin collections to hot storage if queried more than 50 times per day
175
+ - Monitor `collection.status().faissHot` before latency-sensitive query paths
176
+ - Set up alerts for S3 lifecycle transitions if you use collections infrequently
177
+ - Store collection IDs and creation dates — helps audit what tier data lives on
178
+
179
+ ## Reference
180
+
181
+ - https://docs.schift.io/storage/tiers
182
+ - https://docs.schift.io/storage/lifecycle
183
+ - https://docs.schift.io/pricing
@@ -0,0 +1,140 @@
1
+ ---
2
+ title: Deploy Schift-Powered Apps on Cloud Run for Zero Idle Cost
3
+ impact: LOW-MEDIUM
4
+ impactDescription: Zero idle cost with proper Cloud Run configuration
5
+ tags:
6
+ - deployment
7
+ - cloud-run
8
+ - docker
9
+ - infrastructure
10
+ - cost
11
+ - scaling
12
+ ---
13
+
14
+ ## Deploy Schift-Powered Apps on Cloud Run for Zero Idle Cost
15
+
16
+ Cloud Run scales to zero when no traffic is coming in, so you pay nothing while your app is idle. This is ideal for Schift-backed services that handle sporadic traffic — chatbots, search APIs, document Q&A endpoints. The common mistake is deploying these on an always-on VM, which bills 24/7 regardless of actual usage.
17
+
18
+ ### Incorrect
19
+
20
+ Running a Schift-powered chatbot on an always-on VM that pays for compute even during zero-traffic hours:
21
+
22
+ ```bash
23
+ # Always-on EC2 / Compute Engine VM approach
24
+ # Costs ~$15–30/month even when nobody is using the app
25
+ # Manual scaling, manual restarts, no request-based autoscaling
26
+
27
+ ssh ec2-user@your-server
28
+ pm2 start server.js # just stays running forever
29
+ ```
30
+
31
+ ```typescript
32
+ // server.ts — no health check, no graceful shutdown
33
+ // Deployed on an always-on VM: billed 24/7
34
+ import { Hono } from 'hono';
35
+ import { Schift } from '@schift-io/sdk';
36
+
37
+ const app = new Hono();
38
+ const client = new Schift({ apiKey: process.env.SCHIFT_API_KEY! });
39
+
40
+ app.post('/chat', async (c) => {
41
+ const { query } = await c.req.json();
42
+ const results = await client.search('col_...', query, { topK: 3 });
43
+ return c.json({ results });
44
+ });
45
+
46
+ export default app;
47
+ // No /health endpoint → load balancer cannot confirm readiness
48
+ // No memory limit set → OOM-killed silently
49
+ ```
50
+
51
+ This approach pays for a full VM instance even when traffic is zero, lacks health checks for Cloud Run readiness probes, and requires manual scaling during traffic spikes.
52
+
53
+ ### Correct
54
+
55
+ Deploy on Cloud Run with `min-instances=0`, set memory for embedding operations, and expose a `/health` endpoint:
56
+
57
+ ```dockerfile
58
+ # Dockerfile — multi-stage build for a minimal runtime image
59
+ FROM node:20-alpine AS builder
60
+ WORKDIR /app
61
+ COPY package*.json ./
62
+ RUN npm ci --only=production
63
+ COPY . .
64
+ RUN npm run build
65
+
66
+ FROM node:20-alpine AS runtime
67
+ WORKDIR /app
68
+ # Copy only compiled output and production deps
69
+ COPY --from=builder /app/dist ./dist
70
+ COPY --from=builder /app/node_modules ./node_modules
71
+ COPY package.json ./
72
+
73
+ # Cloud Run injects PORT; default to 8080
74
+ ENV PORT=8080
75
+ EXPOSE 8080
76
+
77
+ CMD ["node", "dist/server.js"]
78
+ ```
79
+
80
+ ```typescript
81
+ // server.ts — production-ready Cloud Run handler
82
+ import { Hono } from 'hono';
83
+ import { serve } from '@hono/node-server';
84
+ import { Schift } from '@schift-io/sdk';
85
+
86
+ const app = new Hono();
87
+
88
+ // SCHIFT_API_KEY is injected from Cloud Run secret at runtime
89
+ const client = new Schift({ apiKey: process.env.SCHIFT_API_KEY! });
90
+
91
+ // Required: Cloud Run sends readiness/liveness probes to /health
92
+ app.get('/health', (c) => c.json({ status: 'ok' }));
93
+
94
+ app.post('/search', async (c) => {
95
+ const { query, collectionId } = await c.req.json();
96
+ const results = await client.search(collectionId, query, {
97
+ topK: 5,
98
+ scoreThreshold: 0.72,
99
+ });
100
+ return c.json({ results });
101
+ });
102
+
103
+ const port = Number(process.env.PORT) || 8080;
104
+ serve({ fetch: app.fetch, port });
105
+ ```
106
+
107
+ ```bash
108
+ # Build and push the container image
109
+ gcloud builds submit --tag gcr.io/YOUR_PROJECT/schift-app
110
+
111
+ # Deploy to Cloud Run
112
+ gcloud run deploy schift-app \
113
+ --image gcr.io/YOUR_PROJECT/schift-app \
114
+ --platform managed \
115
+ --region us-central1 \
116
+ --allow-unauthenticated \
117
+ --min-instances 0 \ # scale to zero → $0 idle cost
118
+ --max-instances 10 \
119
+ --concurrency 80 \ # requests handled per instance before scaling out
120
+ --timeout 300 \ # 5 min max for long embedding/search ops
121
+ --memory 512Mi \ # minimum for embedding operations
122
+ --set-secrets SCHIFT_API_KEY=schift-api-key:latest # inject from Secret Manager
123
+ ```
124
+
125
+ **Key settings explained:**
126
+
127
+ | Setting | Value | Why |
128
+ |---------|-------|-----|
129
+ | `min-instances` | `0` | Scales to zero when idle — $0 cost |
130
+ | `concurrency` | `80` | Cloud Run default; safe for I/O-bound search APIs |
131
+ | `timeout` | `300s` | Allows time for cold starts + embedding latency |
132
+ | `memory` | `512Mi` | Minimum for Schift SDK + embedding response buffers |
133
+ | `SCHIFT_API_KEY` | Secret Manager | Never hardcode API keys in the image |
134
+
135
+ **Cold start note:** With `min-instances=0`, the first request after idle incurs a cold start (~1–3 seconds). If your use case requires consistent low latency, set `min-instances=1` — this costs ~$5–10/month but eliminates cold starts.
136
+
137
+ ## Reference
138
+
139
+ - https://docs.schift.io/deployment/cloud-run
140
+ - https://cloud.google.com/run/docs/configuring/min-instances
@@ -0,0 +1,86 @@
1
+ ---
2
+ title: Use embed_batch() for multiple texts
3
+ impact: CRITICAL
4
+ impactDescription: Looping embed() one-by-one produces 10-50x more API calls than a single embed_batch() call, increasing latency and cost proportionally.
5
+ tags: [embedding, performance, cost, batch]
6
+ ---
7
+
8
+ ## Use embed_batch() for multiple texts
9
+
10
+ When embedding more than one piece of text, always use `embed_batch()` instead of calling `embed()` in a loop. Each `embed()` call is a separate HTTP round-trip. `embed_batch()` sends all texts in a single request (up to 100 per call) and returns embeddings in the same order.
11
+
12
+ **Incorrect** — calling `embed()` in a loop creates one HTTP request per text:
13
+
14
+ ```python
15
+ # Python - DON'T do this
16
+ from schift import Schift
17
+
18
+ client = Schift(api_key="sch_...")
19
+ texts = ["cat", "dog", "bird", "fish"]
20
+
21
+ embeddings = []
22
+ for text in texts:
23
+ result = client.embed(text) # 4 separate API calls
24
+ embeddings.append(result.embedding)
25
+ ```
26
+
27
+ ```typescript
28
+ // TypeScript - DON'T do this
29
+ import { Schift } from '@schift-io/sdk';
30
+
31
+ const client = new Schift({ apiKey: 'sch_...' });
32
+ const texts = ['cat', 'dog', 'bird', 'fish'];
33
+
34
+ const embeddings = [];
35
+ for (const text of texts) {
36
+ const result = await client.embed(text); // 4 separate API calls
37
+ embeddings.push(result.embedding);
38
+ }
39
+ ```
40
+
41
+ **Correct** — one `embed_batch()` call handles the entire list:
42
+
43
+ ```python
44
+ # Python - DO this
45
+ from schift import Schift
46
+
47
+ client = Schift(api_key="sch_...")
48
+ texts = ["cat", "dog", "bird", "fish"]
49
+
50
+ results = client.embed_batch(texts) # 1 API call
51
+ embeddings = [r.embedding for r in results] # same order as input
52
+
53
+ # For large datasets, chunk into batches of up to 100
54
+ BATCH_SIZE = 100
55
+ all_embeddings = []
56
+ for i in range(0, len(texts), BATCH_SIZE):
57
+ batch = texts[i : i + BATCH_SIZE]
58
+ results = client.embed_batch(batch)
59
+ all_embeddings.extend(r.embedding for r in results)
60
+ ```
61
+
62
+ ```typescript
63
+ // TypeScript - DO this
64
+ import { Schift } from '@schift-io/sdk';
65
+
66
+ const client = new Schift({ apiKey: 'sch_...' });
67
+ const texts = ['cat', 'dog', 'bird', 'fish'];
68
+
69
+ const results = await client.embedBatch(texts); // 1 API call
70
+ const embeddings = results.map((r) => r.embedding); // same order as input
71
+
72
+ // For large datasets, chunk into batches of up to 100
73
+ const BATCH_SIZE = 100;
74
+ const allEmbeddings: number[][] = [];
75
+ for (let i = 0; i < texts.length; i += BATCH_SIZE) {
76
+ const batch = texts.slice(i, i + BATCH_SIZE);
77
+ const batchResults = await client.embedBatch(batch);
78
+ allEmbeddings.push(...batchResults.map((r) => r.embedding));
79
+ }
80
+ ```
81
+
82
+ The `embed_batch()` / `embedBatch()` limit is 100 texts per call. For larger datasets, split into chunks of 100 as shown above. Results are always returned in the same order as the input list.
83
+
84
+ ## Reference
85
+
86
+ - https://docs.schift.io/api/embed#batch
@@ -0,0 +1,155 @@
1
+ ---
2
+ title: Handle AuthError, QuotaError, and rate limits properly
3
+ impact: HIGH
4
+ impactDescription: Unhandled quota or rate-limit errors crash production jobs and waste partially processed batches. Proper error handling with exponential backoff recovers automatically and avoids re-processing already-embedded documents.
5
+ tags: [embedding, error-handling, rate-limits, quota, retry]
6
+ ---
7
+
8
+ ## Handle AuthError, QuotaError, and rate limits properly
9
+
10
+ Schift raises three primary error types you must handle:
11
+
12
+ | Error | HTTP status | Cause |
13
+ |---|---|---|
14
+ | `AuthError` | 401 | Invalid or missing API key |
15
+ | `QuotaError` | 402 | Monthly credit limit reached |
16
+ | `RateLimitError` | 429 | Too many requests per second |
17
+
18
+ `AuthError` is a hard failure — check your key. `QuotaError` means you need to top up credits or wait for the next billing cycle. `RateLimitError` is transient and should be retried with exponential backoff.
19
+
20
+ **Incorrect** — no error handling; a single quota error crashes the entire job:
21
+
22
+ ```python
23
+ # Python - DON'T do this
24
+ from schift import Schift
25
+
26
+ client = Schift(api_key="sch_...")
27
+ texts = load_large_dataset() # 10,000 documents
28
+
29
+ # If quota runs out halfway through, the whole job crashes
30
+ # and there is no way to resume from where it stopped
31
+ results = client.embed_batch(texts)
32
+ ```
33
+
34
+ ```typescript
35
+ // TypeScript - DON'T do this
36
+ import { Schift } from '@schift-io/sdk';
37
+
38
+ const client = new Schift({ apiKey: 'sch_...' });
39
+ const texts = await loadLargeDataset();
40
+
41
+ // Unhandled promise rejection on quota exceeded
42
+ const results = await client.embedBatch(texts);
43
+ ```
44
+
45
+ **Correct** — catch specific errors and apply exponential backoff for rate limits:
46
+
47
+ ```python
48
+ # Python - DO this
49
+ import time
50
+ from schift import Schift, AuthError, QuotaError, SchiftError
51
+
52
+ client = Schift(api_key="sch_...")
53
+
54
+ def embed_with_retry(texts: list[str], max_retries: int = 5) -> list:
55
+ """Embed texts with exponential backoff on rate limit errors."""
56
+ for attempt in range(max_retries):
57
+ try:
58
+ return client.embed_batch(texts)
59
+
60
+ except AuthError:
61
+ # Hard failure — wrong API key, do not retry
62
+ raise RuntimeError(
63
+ "Invalid Schift API key. Check your SCHIFT_API_KEY env var."
64
+ )
65
+
66
+ except QuotaError as e:
67
+ # Credits exhausted — log and stop; retrying won't help
68
+ raise RuntimeError(
69
+ f"Schift quota exceeded. Top up credits at https://app.schift.io/billing. "
70
+ f"Details: {e}"
71
+ )
72
+
73
+ except SchiftError as e:
74
+ if e.status_code == 429:
75
+ # Rate limit — wait and retry with exponential backoff
76
+ wait = 2 ** attempt # 1s, 2s, 4s, 8s, 16s
77
+ print(f"Rate limited. Retrying in {wait}s (attempt {attempt + 1}/{max_retries})")
78
+ time.sleep(wait)
79
+ else:
80
+ raise # Unexpected error — surface it
81
+
82
+ raise RuntimeError(f"embed_with_retry failed after {max_retries} attempts")
83
+
84
+
85
+ # Check remaining credits before starting a large batch job
86
+ def safe_batch_embed(texts: list[str]) -> list:
87
+ account = client.account.get()
88
+ if account.credits_remaining < len(texts):
89
+ raise RuntimeError(
90
+ f"Insufficient credits: need {len(texts)}, have {account.credits_remaining}"
91
+ )
92
+ return embed_with_retry(texts)
93
+ ```
94
+
95
+ ```typescript
96
+ // TypeScript - DO this
97
+ import { Schift, AuthError, QuotaError, SchiftError } from '@schift-io/sdk';
98
+
99
+ const client = new Schift({ apiKey: 'sch_...' });
100
+
101
+ async function embedWithRetry(
102
+ texts: string[],
103
+ maxRetries = 5,
104
+ ): Promise<{ embedding: number[] }[]> {
105
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
106
+ try {
107
+ return await client.embedBatch(texts);
108
+
109
+ } catch (err) {
110
+ if (err instanceof AuthError) {
111
+ // Hard failure — wrong API key, do not retry
112
+ throw new Error(
113
+ 'Invalid Schift API key. Check your SCHIFT_API_KEY env var.',
114
+ );
115
+ }
116
+
117
+ if (err instanceof QuotaError) {
118
+ // Credits exhausted — log and stop
119
+ throw new Error(
120
+ `Schift quota exceeded. Top up credits at https://app.schift.io/billing. ${err.message}`,
121
+ );
122
+ }
123
+
124
+ if (err instanceof SchiftError && err.statusCode === 429) {
125
+ // Rate limit — exponential backoff
126
+ const wait = 2 ** attempt * 1000; // 1s, 2s, 4s, 8s, 16s
127
+ console.warn(`Rate limited. Retrying in ${wait / 1000}s (attempt ${attempt + 1}/${maxRetries})`);
128
+ await new Promise((resolve) => setTimeout(resolve, wait));
129
+ continue;
130
+ }
131
+
132
+ throw err; // Unexpected error — surface it
133
+ }
134
+ }
135
+ throw new Error(`embedWithRetry failed after ${maxRetries} attempts`);
136
+ }
137
+
138
+ // Check credits before a large batch job
139
+ async function safeBatchEmbed(texts: string[]) {
140
+ const account = await client.account.get();
141
+ if (account.creditsRemaining < texts.length) {
142
+ throw new Error(
143
+ `Insufficient credits: need ${texts.length}, have ${account.creditsRemaining}`,
144
+ );
145
+ }
146
+ return embedWithRetry(texts);
147
+ }
148
+ ```
149
+
150
+ For long-running indexing jobs, track which documents have already been embedded (e.g. store their IDs in a database) so you can resume from where you left off after a `QuotaError` rather than restarting from scratch.
151
+
152
+ ## Reference
153
+
154
+ - https://docs.schift.io/api/errors
155
+ - https://docs.schift.io/guides/rate-limits
@@ -0,0 +1,100 @@
1
+ ---
2
+ title: Use modality parameter for non-text inputs
3
+ impact: CRITICAL
4
+ impactDescription: Passing raw image bytes or file content as a plain text string produces garbage embeddings. The modality parameter routes the input through the correct encoder before projection into canonical space.
5
+ tags: [embedding, multimodal, image, document, audio]
6
+ ---
7
+
8
+ ## Use modality parameter for non-text inputs
9
+
10
+ Schift supports five modalities: `text`, `image`, `audio`, `video`, and `document`. Every modality is projected into the same canonical 1024-dimensional space, so cross-modal search (e.g. text query against image collection) works out of the box — but only if you specify the correct `modality` when embedding.
11
+
12
+ Passing raw binary content without setting `modality` causes Schift to treat the input as UTF-8 text, producing meaningless embeddings.
13
+
14
+ **Incorrect** — passing image bytes as plain text produces garbage vectors:
15
+
16
+ ```python
17
+ # Python - DON'T do this
18
+ from schift import Schift
19
+
20
+ client = Schift(api_key="sch_...")
21
+
22
+ with open("photo.jpg", "rb") as f:
23
+ image_bytes = f.read()
24
+
25
+ # Wrong: embed() with no modality treats input as text
26
+ result = client.embed(image_bytes) # garbage embedding
27
+ ```
28
+
29
+ ```typescript
30
+ // TypeScript - DON'T do this
31
+ import { Schift } from '@schift-io/sdk';
32
+ import { readFileSync } from 'fs';
33
+
34
+ const client = new Schift({ apiKey: 'sch_...' });
35
+
36
+ const imageBuffer = readFileSync('photo.jpg');
37
+
38
+ // Wrong: no modality — treated as text
39
+ const result = await client.embed(imageBuffer.toString()); // garbage embedding
40
+ ```
41
+
42
+ **Correct** — specify `modality="image"` (or the appropriate modality) and pass the file path or bytes:
43
+
44
+ ```python
45
+ # Python - DO this
46
+ from schift import Schift
47
+
48
+ client = Schift(api_key="sch_...")
49
+
50
+ # Embed a single image
51
+ result = client.embed("photo.jpg", modality="image")
52
+ image_vector = result.embedding # 1024d, same space as text embeddings
53
+
54
+ # Embed a PDF document
55
+ doc_result = client.embed("report.pdf", modality="document")
56
+
57
+ # Cross-modal search: text query against image collection
58
+ text_query = client.embed("a golden retriever running in a park", modality="text")
59
+ # text_query.embedding can now be used to search an image collection directly
60
+
61
+ # Batch embed multiple images
62
+ image_paths = ["img1.jpg", "img2.jpg", "img3.jpg"]
63
+ results = client.embed_batch(image_paths, modality="image")
64
+ vectors = [r.embedding for r in results]
65
+ ```
66
+
67
+ ```typescript
68
+ // TypeScript - DO this
69
+ import { Schift } from '@schift-io/sdk';
70
+
71
+ const client = new Schift({ apiKey: 'sch_...' });
72
+
73
+ // Embed a single image by file path
74
+ const result = await client.embed('photo.jpg', { modality: 'image' });
75
+ const imageVector = result.embedding; // 1024d, same space as text
76
+
77
+ // Embed a PDF document
78
+ const docResult = await client.embed('report.pdf', { modality: 'document' });
79
+
80
+ // Cross-modal: text query against image collection
81
+ const textQuery = await client.embed(
82
+ 'a golden retriever running in a park',
83
+ { modality: 'text' },
84
+ );
85
+ // textQuery.embedding searches image collections directly
86
+
87
+ // Batch embed multiple images
88
+ const imagePaths = ['img1.jpg', 'img2.jpg', 'img3.jpg'];
89
+ const results = await client.embedBatch(imagePaths, { modality: 'image' });
90
+ const vectors = results.map((r) => r.embedding);
91
+ ```
92
+
93
+ Because all modalities share the same canonical 1024d space, you can search across modalities without any additional conversion step. A text embedding and an image embedding are directly comparable with cosine similarity.
94
+
95
+ Supported modality values: `text` (default), `image`, `audio`, `video`, `document`.
96
+
97
+ ## Reference
98
+
99
+ - https://docs.schift.io/api/embed#modality
100
+ - https://docs.schift.io/concepts/canonical-space