@fluxsoft/fluxvector 0.1.0

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.
package/README.md ADDED
@@ -0,0 +1,234 @@
1
+ # @fluxsoft/fluxvector
2
+
3
+ Official TypeScript SDK for [FluxVector](https://fluxsoftlabs.com/fluxvector) — semantic search API by FluxSoft Labs.
4
+
5
+ - Zero runtime dependencies (native `fetch`, works in Node 18+, Deno, Bun, edge runtimes)
6
+ - Full TypeScript types for every request and response
7
+ - Automatic retry with exponential backoff
8
+ - Auto-chunking for large upserts
9
+ - ESM + CJS dual package
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install @fluxsoft/fluxvector
15
+ ```
16
+
17
+ ```bash
18
+ pnpm add @fluxsoft/fluxvector
19
+ ```
20
+
21
+ ```bash
22
+ yarn add @fluxsoft/fluxvector
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```typescript
28
+ import { FluxVector } from '@fluxsoft/fluxvector';
29
+
30
+ const fv = new FluxVector({ apiKey: 'fv_live_abc123' });
31
+
32
+ // Create a collection
33
+ await fv.collections.create({ name: 'products' });
34
+
35
+ // Upsert vectors
36
+ await fv.vectors.upsert('products', [
37
+ { id: 'p1', text: 'Red shoes', metadata: { price: 89 } },
38
+ { id: 'p2', text: 'Blue sneakers', metadata: { price: 120 } },
39
+ ]);
40
+
41
+ // Search
42
+ const results = await fv.search('products', 'comfortable shoes', {
43
+ topK: 5,
44
+ filter: { price: { $lt: 100 } },
45
+ });
46
+
47
+ for (const r of results) {
48
+ console.log(`${r.id}: ${r.score.toFixed(2)} — ${r.text}`);
49
+ }
50
+ ```
51
+
52
+ ## Configuration
53
+
54
+ ```typescript
55
+ const fv = new FluxVector({
56
+ apiKey: 'fv_live_abc123', // Required
57
+ baseUrl: 'https://fluxvector.dev', // Default, configurable for self-hosted
58
+ maxRetries: 3, // Default: 3 (retries on 429 and 5xx)
59
+ timeout: 30000, // Default: 30s
60
+ });
61
+ ```
62
+
63
+ ## API Reference
64
+
65
+ ### Collections
66
+
67
+ ```typescript
68
+ // Create
69
+ const col = await fv.collections.create({
70
+ name: 'products',
71
+ dimension: 1536, // Optional, default: auto
72
+ metric: 'cosine', // 'cosine' | 'euclidean' | 'dotproduct'
73
+ description: 'Product catalog',
74
+ });
75
+
76
+ // List (cursor pagination)
77
+ const { data, has_more, next_cursor } = await fv.collections.list({ limit: 10 });
78
+
79
+ // Get
80
+ const col = await fv.collections.get('products');
81
+
82
+ // Delete
83
+ await fv.collections.delete('products');
84
+ ```
85
+
86
+ ### Vectors
87
+
88
+ ```typescript
89
+ // Upsert (auto-chunks at 1000 vectors)
90
+ await fv.vectors.upsert('products', [
91
+ { id: 'p1', text: 'Red shoes', metadata: { price: 89, category: 'footwear' } },
92
+ { id: 'p2', values: [0.1, 0.2, ...], metadata: { price: 120 } },
93
+ ]);
94
+
95
+ // Query
96
+ const { results, usage, took_ms } = await fv.vectors.query({
97
+ collection: 'products',
98
+ text: 'comfortable shoes',
99
+ top_k: 10,
100
+ filter: { category: { $eq: 'footwear' } },
101
+ include_metadata: true,
102
+ include_text: true,
103
+ });
104
+
105
+ // Fetch by IDs
106
+ const { vectors } = await fv.vectors.fetch('products', ['p1', 'p2']);
107
+
108
+ // Delete by IDs
109
+ await fv.vectors.delete('products', { ids: ['p1', 'p2'] });
110
+
111
+ // Delete by filter
112
+ await fv.vectors.delete('products', { filter: { category: { $eq: 'old' } } });
113
+ ```
114
+
115
+ ### Search
116
+
117
+ The top-level `fv.search()` is the simplest way to do semantic search:
118
+
119
+ ```typescript
120
+ // Returns QueryResult[] directly
121
+ const results = await fv.search('products', 'comfortable shoes', {
122
+ topK: 5,
123
+ filter: { price: { $lt: 100 } },
124
+ mode: 'hybrid',
125
+ });
126
+
127
+ // Each result: { id, score, text?, metadata? }
128
+ ```
129
+
130
+ ### Embeddings
131
+
132
+ ```typescript
133
+ // Single text
134
+ const { embedding, dimension, model } = await fv.embeddings.create('hello world');
135
+
136
+ // Batch
137
+ const { embeddings, dimension, model } = await fv.embeddings.createBatch([
138
+ 'hello world',
139
+ 'goodbye world',
140
+ ]);
141
+ ```
142
+
143
+ ### API Keys
144
+
145
+ ```typescript
146
+ // Create
147
+ const key = await fv.apiKeys.create({ name: 'Production', env: 'live' });
148
+ console.log(key.key); // fv_live_xxx — shown only once
149
+
150
+ // List
151
+ const { data } = await fv.apiKeys.list();
152
+
153
+ // Update
154
+ await fv.apiKeys.update('key_123', { name: 'Renamed' });
155
+
156
+ // Delete
157
+ await fv.apiKeys.delete('key_123');
158
+ ```
159
+
160
+ ### Usage
161
+
162
+ ```typescript
163
+ // Current period
164
+ const usage = await fv.usage.get();
165
+ // { plan, period_start, requests, embeddings, vectors_stored, collections }
166
+
167
+ // History
168
+ const { data } = await fv.usage.history({ days: 30 });
169
+ // [{ date, requests, embeddings, vectors }]
170
+ ```
171
+
172
+ ## Filter Operators
173
+
174
+ Filters use MongoDB-style operators on metadata fields:
175
+
176
+ | Operator | Description |
177
+ |----------|-------------|
178
+ | `$eq` | Equal to |
179
+ | `$ne` | Not equal to |
180
+ | `$gt` | Greater than |
181
+ | `$gte` | Greater than or equal |
182
+ | `$lt` | Less than |
183
+ | `$lte` | Less than or equal |
184
+ | `$in` | In array |
185
+ | `$nin` | Not in array |
186
+
187
+ ```typescript
188
+ // Examples
189
+ { price: { $lt: 100 } }
190
+ { category: { $in: ['shoes', 'boots'] } }
191
+ { status: { $ne: 'archived' } }
192
+ ```
193
+
194
+ ## Error Handling
195
+
196
+ All errors extend `FluxVectorError` with `status`, `code`, and optional `requestId`:
197
+
198
+ ```typescript
199
+ import { FluxVector, AuthenticationError, NotFoundError, RateLimitError } from '@fluxsoft/fluxvector';
200
+
201
+ try {
202
+ await fv.collections.get('missing');
203
+ } catch (err) {
204
+ if (err instanceof NotFoundError) {
205
+ console.log('Collection does not exist');
206
+ } else if (err instanceof AuthenticationError) {
207
+ console.log('Check your API key');
208
+ } else if (err instanceof RateLimitError) {
209
+ console.log(`Retry after ${err.retryAfter}s`);
210
+ }
211
+ }
212
+ ```
213
+
214
+ | Error Class | Status | When |
215
+ |-------------|--------|------|
216
+ | `AuthenticationError` | 401 | Invalid or missing API key |
217
+ | `NotFoundError` | 404 | Resource not found |
218
+ | `ValidationError` | 422 | Invalid request body |
219
+ | `RateLimitError` | 429 | Too many requests |
220
+ | `ServerError` | 5xx | Server-side error |
221
+ | `FluxVectorError` | any | Base class for all errors |
222
+
223
+ ## Retry Behavior
224
+
225
+ The SDK automatically retries on 429 (rate limit) and 5xx (server errors):
226
+
227
+ - Default: 3 retries with exponential backoff (500ms, 1s, 2s + jitter)
228
+ - Respects `Retry-After` headers on 429 responses
229
+ - Non-retryable errors (400, 401, 404, 422) fail immediately
230
+ - Set `maxRetries: 0` to disable retries
231
+
232
+ ## License
233
+
234
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,495 @@
1
+ 'use strict';
2
+
3
+ // src/errors.ts
4
+ var FluxVectorError = class extends Error {
5
+ status;
6
+ code;
7
+ requestId;
8
+ constructor(message, status, code = "unknown_error", requestId) {
9
+ super(message);
10
+ this.name = "FluxVectorError";
11
+ this.status = status;
12
+ this.code = code;
13
+ this.requestId = requestId;
14
+ Object.setPrototypeOf(this, new.target.prototype);
15
+ }
16
+ };
17
+ var AuthenticationError = class extends FluxVectorError {
18
+ constructor(message = "Invalid or missing API key", requestId) {
19
+ super(message, 401, "authentication_error", requestId);
20
+ this.name = "AuthenticationError";
21
+ Object.setPrototypeOf(this, new.target.prototype);
22
+ }
23
+ };
24
+ var NotFoundError = class extends FluxVectorError {
25
+ constructor(message = "Resource not found", requestId) {
26
+ super(message, 404, "not_found", requestId);
27
+ this.name = "NotFoundError";
28
+ Object.setPrototypeOf(this, new.target.prototype);
29
+ }
30
+ };
31
+ var RateLimitError = class extends FluxVectorError {
32
+ retryAfter;
33
+ constructor(message = "Rate limit exceeded", retryAfter, requestId) {
34
+ super(message, 429, "rate_limit_exceeded", requestId);
35
+ this.name = "RateLimitError";
36
+ this.retryAfter = retryAfter;
37
+ Object.setPrototypeOf(this, new.target.prototype);
38
+ }
39
+ };
40
+ var ValidationError = class extends FluxVectorError {
41
+ constructor(message = "Validation failed", requestId) {
42
+ super(message, 422, "validation_error", requestId);
43
+ this.name = "ValidationError";
44
+ Object.setPrototypeOf(this, new.target.prototype);
45
+ }
46
+ };
47
+ var ServerError = class extends FluxVectorError {
48
+ constructor(message = "Internal server error", status = 500, requestId) {
49
+ super(message, status, "server_error", requestId);
50
+ this.name = "ServerError";
51
+ Object.setPrototypeOf(this, new.target.prototype);
52
+ }
53
+ };
54
+
55
+ // src/client.ts
56
+ var DEFAULT_BASE_URL = "https://fluxvector.dev";
57
+ var DEFAULT_MAX_RETRIES = 3;
58
+ var DEFAULT_TIMEOUT = 3e4;
59
+ var RETRY_STATUS_CODES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
60
+ var HttpClient = class {
61
+ config;
62
+ constructor(config) {
63
+ this.config = {
64
+ apiKey: config.apiKey,
65
+ baseUrl: (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ""),
66
+ maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
67
+ timeout: config.timeout ?? DEFAULT_TIMEOUT
68
+ };
69
+ }
70
+ async request(options) {
71
+ const url = this.buildUrl(options.path, options.query);
72
+ const headers = {
73
+ "Authorization": `Bearer ${this.config.apiKey}`,
74
+ "Content-Type": "application/json",
75
+ "User-Agent": "@fluxsoft/fluxvector/0.1.0"
76
+ };
77
+ const init = {
78
+ method: options.method,
79
+ headers,
80
+ body: options.body !== void 0 ? JSON.stringify(options.body) : void 0,
81
+ signal: AbortSignal.timeout(this.config.timeout)
82
+ };
83
+ let lastError;
84
+ for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
85
+ if (attempt > 0) {
86
+ await this.sleep(this.getRetryDelay(attempt, lastError));
87
+ }
88
+ try {
89
+ const response = await fetch(url, init);
90
+ if (response.ok) {
91
+ return await response.json();
92
+ }
93
+ const requestId = response.headers.get("x-request-id") ?? void 0;
94
+ if (!RETRY_STATUS_CODES.has(response.status) || attempt === this.config.maxRetries) {
95
+ throw this.buildError(response.status, await this.safeJson(response), requestId);
96
+ }
97
+ lastError = this.buildError(response.status, await this.safeJson(response), requestId);
98
+ if (response.status === 429) {
99
+ const retryAfter = response.headers.get("retry-after");
100
+ if (retryAfter && lastError instanceof RateLimitError) {
101
+ lastError.retryAfter = Number(retryAfter);
102
+ }
103
+ }
104
+ } catch (error) {
105
+ if (error instanceof FluxVectorError) {
106
+ throw error;
107
+ }
108
+ if (attempt === this.config.maxRetries) {
109
+ if (error instanceof DOMException && error.name === "TimeoutError") {
110
+ throw new FluxVectorError("Request timed out", 0, "timeout");
111
+ }
112
+ throw new FluxVectorError(
113
+ error instanceof Error ? error.message : "Unknown network error",
114
+ 0,
115
+ "network_error"
116
+ );
117
+ }
118
+ lastError = error instanceof Error ? error : new Error(String(error));
119
+ }
120
+ }
121
+ throw lastError ?? new FluxVectorError("Request failed after retries", 0, "retry_exhausted");
122
+ }
123
+ buildUrl(path, query) {
124
+ const url = new URL(`${this.config.baseUrl}${path}`);
125
+ if (query) {
126
+ for (const [key, value] of Object.entries(query)) {
127
+ if (value !== void 0) {
128
+ url.searchParams.set(key, String(value));
129
+ }
130
+ }
131
+ }
132
+ return url.toString();
133
+ }
134
+ buildError(status, body, requestId) {
135
+ const message = body?.message ?? body?.error ?? `Request failed with status ${status}`;
136
+ switch (status) {
137
+ case 401:
138
+ return new AuthenticationError(message, requestId);
139
+ case 404:
140
+ return new NotFoundError(message, requestId);
141
+ case 422:
142
+ return new ValidationError(message, requestId);
143
+ case 429: {
144
+ const retryAfter = body?.retry_after;
145
+ return new RateLimitError(message, retryAfter, requestId);
146
+ }
147
+ default:
148
+ if (status >= 500) {
149
+ return new ServerError(message, status, requestId);
150
+ }
151
+ return new FluxVectorError(message, status, "api_error", requestId);
152
+ }
153
+ }
154
+ async safeJson(response) {
155
+ try {
156
+ return await response.json();
157
+ } catch {
158
+ return null;
159
+ }
160
+ }
161
+ getRetryDelay(attempt, lastError) {
162
+ if (lastError instanceof RateLimitError && lastError.retryAfter) {
163
+ return lastError.retryAfter * 1e3;
164
+ }
165
+ const base = Math.min(500 * Math.pow(2, attempt - 1), 4e3);
166
+ const jitter = Math.random() * base * 0.5;
167
+ return base + jitter;
168
+ }
169
+ sleep(ms) {
170
+ return new Promise((resolve) => setTimeout(resolve, ms));
171
+ }
172
+ };
173
+
174
+ // src/resources/collections.ts
175
+ var Collections = class {
176
+ constructor(client) {
177
+ this.client = client;
178
+ }
179
+ /**
180
+ * Create a new collection.
181
+ */
182
+ async create(params) {
183
+ return this.client.request({
184
+ method: "POST",
185
+ path: "/v1/collections",
186
+ body: params
187
+ });
188
+ }
189
+ /**
190
+ * List all collections with cursor-based pagination.
191
+ */
192
+ async list(params) {
193
+ return this.client.request({
194
+ method: "GET",
195
+ path: "/v1/collections",
196
+ query: {
197
+ cursor: params?.cursor,
198
+ limit: params?.limit
199
+ }
200
+ });
201
+ }
202
+ /**
203
+ * Get a collection by name.
204
+ */
205
+ async get(name) {
206
+ return this.client.request({
207
+ method: "GET",
208
+ path: `/v1/collections/${encodeURIComponent(name)}`
209
+ });
210
+ }
211
+ /**
212
+ * Delete a collection by name.
213
+ */
214
+ async delete(name) {
215
+ return this.client.request({
216
+ method: "DELETE",
217
+ path: `/v1/collections/${encodeURIComponent(name)}`
218
+ });
219
+ }
220
+ };
221
+
222
+ // src/resources/vectors.ts
223
+ var MAX_UPSERT_BATCH_SIZE = 1e3;
224
+ var Vectors = class {
225
+ constructor(client) {
226
+ this.client = client;
227
+ }
228
+ /**
229
+ * Upsert vectors into a collection.
230
+ * Automatically chunks into batches of 1000 vectors.
231
+ */
232
+ async upsert(collection, vectors) {
233
+ if (vectors.length <= MAX_UPSERT_BATCH_SIZE) {
234
+ return this.client.request({
235
+ method: "POST",
236
+ path: "/v1/vectors/upsert",
237
+ body: { collection, vectors }
238
+ });
239
+ }
240
+ let totalUpserted = 0;
241
+ for (let i = 0; i < vectors.length; i += MAX_UPSERT_BATCH_SIZE) {
242
+ const chunk = vectors.slice(i, i + MAX_UPSERT_BATCH_SIZE);
243
+ const result = await this.client.request({
244
+ method: "POST",
245
+ path: "/v1/vectors/upsert",
246
+ body: { collection, vectors: chunk }
247
+ });
248
+ totalUpserted += result.upserted;
249
+ }
250
+ return { upserted: totalUpserted };
251
+ }
252
+ /**
253
+ * Query vectors by text or vector with optional filtering.
254
+ */
255
+ async query(params) {
256
+ return this.client.request({
257
+ method: "POST",
258
+ path: "/v1/vectors/query",
259
+ body: {
260
+ collection: params.collection,
261
+ text: params.text,
262
+ vector: params.vector,
263
+ top_k: params.top_k,
264
+ filter: params.filter,
265
+ include_metadata: params.include_metadata,
266
+ include_text: params.include_text,
267
+ mode: params.mode
268
+ }
269
+ });
270
+ }
271
+ /**
272
+ * Delete vectors by IDs or filter.
273
+ */
274
+ async delete(collection, options) {
275
+ return this.client.request({
276
+ method: "POST",
277
+ path: "/v1/vectors/delete",
278
+ body: {
279
+ collection,
280
+ ids: options.ids,
281
+ filter: options.filter
282
+ }
283
+ });
284
+ }
285
+ /**
286
+ * Fetch vectors by their IDs.
287
+ */
288
+ async fetch(collection, ids) {
289
+ return this.client.request({
290
+ method: "GET",
291
+ path: "/v1/vectors/fetch",
292
+ query: {
293
+ collection,
294
+ ids: ids.join(",")
295
+ }
296
+ });
297
+ }
298
+ };
299
+
300
+ // src/resources/search.ts
301
+ var Search = class {
302
+ constructor(client) {
303
+ this.client = client;
304
+ }
305
+ /**
306
+ * Semantic search across a collection.
307
+ * Returns matching results ranked by relevance.
308
+ */
309
+ async query(collection, text, options) {
310
+ const response = await this.client.request({
311
+ method: "POST",
312
+ path: "/v1/search",
313
+ body: {
314
+ collection,
315
+ text,
316
+ top_k: options?.topK,
317
+ filter: options?.filter,
318
+ mode: options?.mode
319
+ }
320
+ });
321
+ return response.results;
322
+ }
323
+ /**
324
+ * Semantic search with full response (includes usage and timing).
325
+ */
326
+ async queryWithMeta(collection, text, options) {
327
+ return this.client.request({
328
+ method: "POST",
329
+ path: "/v1/search",
330
+ body: {
331
+ collection,
332
+ text,
333
+ top_k: options?.topK,
334
+ filter: options?.filter,
335
+ mode: options?.mode
336
+ }
337
+ });
338
+ }
339
+ };
340
+
341
+ // src/resources/embeddings.ts
342
+ var Embeddings = class {
343
+ constructor(client) {
344
+ this.client = client;
345
+ }
346
+ /**
347
+ * Generate an embedding for a single text.
348
+ */
349
+ async create(text) {
350
+ return this.client.request({
351
+ method: "POST",
352
+ path: "/v1/embed",
353
+ body: { text }
354
+ });
355
+ }
356
+ /**
357
+ * Generate embeddings for multiple texts in a single request.
358
+ */
359
+ async createBatch(texts) {
360
+ return this.client.request({
361
+ method: "POST",
362
+ path: "/v1/embed/batch",
363
+ body: { texts }
364
+ });
365
+ }
366
+ };
367
+
368
+ // src/resources/api-keys.ts
369
+ var ApiKeys = class {
370
+ constructor(client) {
371
+ this.client = client;
372
+ }
373
+ /**
374
+ * Create a new API key.
375
+ */
376
+ async create(params) {
377
+ return this.client.request({
378
+ method: "POST",
379
+ path: "/v1/api-keys",
380
+ body: params
381
+ });
382
+ }
383
+ /**
384
+ * List all API keys.
385
+ */
386
+ async list() {
387
+ return this.client.request({
388
+ method: "GET",
389
+ path: "/v1/api-keys"
390
+ });
391
+ }
392
+ /**
393
+ * Update an API key's name.
394
+ */
395
+ async update(id, params) {
396
+ return this.client.request({
397
+ method: "PATCH",
398
+ path: `/v1/api-keys/${encodeURIComponent(id)}`,
399
+ body: params
400
+ });
401
+ }
402
+ /**
403
+ * Delete an API key.
404
+ */
405
+ async delete(id) {
406
+ return this.client.request({
407
+ method: "DELETE",
408
+ path: `/v1/api-keys/${encodeURIComponent(id)}`
409
+ });
410
+ }
411
+ };
412
+
413
+ // src/resources/usage.ts
414
+ var Usage = class {
415
+ constructor(client) {
416
+ this.client = client;
417
+ }
418
+ /**
419
+ * Get current usage overview for the billing period.
420
+ */
421
+ async get() {
422
+ return this.client.request({
423
+ method: "GET",
424
+ path: "/v1/usage"
425
+ });
426
+ }
427
+ /**
428
+ * Get historical usage data.
429
+ */
430
+ async history(params) {
431
+ return this.client.request({
432
+ method: "GET",
433
+ path: "/v1/usage/history",
434
+ query: {
435
+ days: params?.days
436
+ }
437
+ });
438
+ }
439
+ };
440
+
441
+ // src/index.ts
442
+ var FluxVector = class {
443
+ collections;
444
+ vectors;
445
+ embeddings;
446
+ apiKeys;
447
+ usage;
448
+ searchResource;
449
+ constructor(config) {
450
+ if (!config.apiKey) {
451
+ throw new Error("FluxVector: apiKey is required");
452
+ }
453
+ const client = new HttpClient({
454
+ apiKey: config.apiKey,
455
+ baseUrl: config.baseUrl,
456
+ maxRetries: config.maxRetries,
457
+ timeout: config.timeout
458
+ });
459
+ this.collections = new Collections(client);
460
+ this.vectors = new Vectors(client);
461
+ this.searchResource = new Search(client);
462
+ this.embeddings = new Embeddings(client);
463
+ this.apiKeys = new ApiKeys(client);
464
+ this.usage = new Usage(client);
465
+ }
466
+ /**
467
+ * Semantic search across a collection.
468
+ *
469
+ * @example
470
+ * const results = await fv.search('products', 'comfortable shoes', {
471
+ * topK: 5,
472
+ * filter: { price: { $lt: 100 } },
473
+ * });
474
+ */
475
+ async search(collection, text, options) {
476
+ return this.searchResource.query(collection, text, options);
477
+ }
478
+ };
479
+
480
+ exports.ApiKeys = ApiKeys;
481
+ exports.AuthenticationError = AuthenticationError;
482
+ exports.Collections = Collections;
483
+ exports.Embeddings = Embeddings;
484
+ exports.FluxVector = FluxVector;
485
+ exports.FluxVectorError = FluxVectorError;
486
+ exports.HttpClient = HttpClient;
487
+ exports.NotFoundError = NotFoundError;
488
+ exports.RateLimitError = RateLimitError;
489
+ exports.Search = Search;
490
+ exports.ServerError = ServerError;
491
+ exports.Usage = Usage;
492
+ exports.ValidationError = ValidationError;
493
+ exports.Vectors = Vectors;
494
+ //# sourceMappingURL=index.cjs.map
495
+ //# sourceMappingURL=index.cjs.map