vecbox 0.1.0 → 0.2.1

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.
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "llama-embedding-native",
3
+ "version": "1.0.0",
4
+ "description": "Native Node.js bindings for Llama.cpp embeddings",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "install": "node-gyp rebuild",
8
+ "build": "node-gyp rebuild"
9
+ },
10
+ "dependencies": {
11
+ "node-addon-api": "^7.1.0"
12
+ },
13
+ "devDependencies": {
14
+ "node-gyp": "^10.1.0"
15
+ },
16
+ "gypfile": true,
17
+ "keywords": [
18
+ "llama.cpp",
19
+ "embedding",
20
+ "native",
21
+ "n-api",
22
+ "node.js"
23
+ ],
24
+ "author": "Vecbox Team",
25
+ "license": "MIT"
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vecbox",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "A minimal and powerful embedding library that supports multiple providers with automatic detection and fallback capabilities",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -8,19 +8,24 @@
8
8
  "files": [
9
9
  "dist",
10
10
  "src",
11
+ "native",
11
12
  "README.md",
12
13
  "CHANGELOG.md",
13
14
  "LICENSE"
14
15
  ],
15
16
  "scripts": {
16
17
  "build": "tsup",
18
+ "build:native": "node scripts/build-native.cjs",
19
+ "build:all": "npm run build && npm run build:native",
17
20
  "dev": "tsx watch main.ts",
18
21
  "test": "vitest",
19
22
  "test:run": "vitest run",
23
+ "test:native": "vitest test/native.test.js",
24
+ "test:integration": "vitest test/integration.test.js",
20
25
  "example": "tsx examples/basic-usage.ts",
21
26
  "lint": "eslint . --ext .ts,.js",
22
27
  "lint:fix": "eslint . --ext .ts,.js --fix",
23
- "prepublishOnly": "npm run build"
28
+ "prepublishOnly": "npm run build:all"
24
29
  },
25
30
  "keywords": [
26
31
  "embeddings",
@@ -29,9 +34,7 @@
29
34
  "machine-learning",
30
35
  "openai",
31
36
  "gemini",
32
- "claude",
33
37
  "mistral",
34
- "deepseek",
35
38
  "llamacpp",
36
39
  "semantic-search",
37
40
  "similarity",
@@ -69,10 +72,8 @@
69
72
  "vitest": "^4.0.18"
70
73
  },
71
74
  "dependencies": {
72
- "@anthropic-ai/sdk": "^0.74.0",
73
75
  "@google/generative-ai": "^0.24.1",
74
76
  "@mistralai/mistralai": "^1.14.0",
75
- "deepseek": "^0.0.2",
76
77
  "dotenv": "^17.3.1",
77
78
  "openai": "^6.21.0"
78
79
  }
@@ -2,9 +2,7 @@ import type { EmbedConfig, ProviderType } from '@src/types/index';
2
2
  import { EmbeddingProvider } from '@providers/base/EmbeddingProvider';
3
3
  import { OpenAIProvider } from '@providers/openai';
4
4
  import { GeminiProvider } from '@providers/gemini';
5
- import { ClaudeProvider } from '@providers/claude';
6
5
  import { MistralProvider } from '@providers/mistral';
7
- import { DeepSeekProvider } from '@providers/deepseek';
8
6
  import { LlamaCppProvider } from '@providers/llamacpp';
9
7
  import { Logger } from '@src/util/logger';
10
8
 
@@ -14,9 +12,7 @@ export class EmbeddingFactory {
14
12
  private static providers = new Map<ProviderType, new (config: EmbedConfig) => EmbeddingProvider>([
15
13
  ['openai', OpenAIProvider],
16
14
  ['gemini', GeminiProvider],
17
- ['claude', ClaudeProvider],
18
15
  ['mistral', MistralProvider],
19
- ['deepseek', DeepSeekProvider],
20
16
  ['llamacpp', LlamaCppProvider], // Local embeddings with llama.cpp
21
17
  ]);
22
18
 
Binary file
@@ -74,11 +74,11 @@ export class GeminiProvider extends EmbeddingProvider {
74
74
 
75
75
  getDimensions(): number {
76
76
  const model = this.getModel();
77
- if (model.includes('gemini-embedding-001')) return 768;
77
+ if (model.includes('gemini-embedding-001')) return 3072;
78
78
  if (model.includes('text-embedding-004')) return 768;
79
79
  if (model.includes('embedding-001')) return 768;
80
80
  if (model.includes('multimodalembedding')) return 768;
81
- return 768; // default for Gemini embeddings
81
+ return 3072; // default for Gemini embeddings
82
82
  }
83
83
 
84
84
  getProviderName(): string {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Llama.cpp Provider - Local embeddings using llama.cpp directly
3
- * Uses llama-embedding binary without any external dependencies
3
+ * Uses native N-API module for better performance
4
4
  */
5
5
 
6
6
  import { access, constants } from 'fs/promises';
@@ -10,6 +10,15 @@ import type { EmbedConfig, EmbedInput, EmbedResult, BatchEmbedResult } from '@sr
10
10
  import { logger } from '@src/util/logger';
11
11
  import * as http from 'http';
12
12
 
13
+ // Try to import native module
14
+ let nativeModule: any = null;
15
+ try {
16
+ nativeModule = require('../../native');
17
+ logger.info('Using native Llama.cpp module');
18
+ } catch (error) {
19
+ logger.warn('Native module not available, falling back to HTTP');
20
+ }
21
+
13
22
  // Extend EmbedConfig to include llamaPath
14
23
  interface LlamaCppConfig extends EmbedConfig {
15
24
  llamaPath?: string;
@@ -18,12 +27,26 @@ interface LlamaCppConfig extends EmbedConfig {
18
27
  export class LlamaCppProvider extends EmbeddingProvider {
19
28
  private llamaPath: string;
20
29
  private modelPath: string;
30
+ private useNative: boolean;
31
+ private nativeModel: any = null;
21
32
 
22
33
  constructor(config: LlamaCppConfig) {
23
34
  super({ ...config, provider: 'llamacpp' });
24
35
  this.modelPath = config.model || 'nomic-embed-text-v1.5.Q4_K_M.gguf';
25
36
  this.llamaPath = config.llamaPath || './llama.cpp/build/bin/llama-embedding';
26
- logger.info(`Llama.cpp provider initialized with model: ${this.modelPath}`);
37
+ this.useNative = !!nativeModule;
38
+
39
+ if (this.useNative) {
40
+ try {
41
+ this.nativeModel = nativeModule.create(this.modelPath);
42
+ logger.info(`Llama.cpp provider initialized with native module: ${this.modelPath}`);
43
+ } catch (error) {
44
+ logger.error(`Failed to initialize native module: ${error}`);
45
+ this.useNative = false;
46
+ }
47
+ } else {
48
+ logger.info(`Llama.cpp provider initialized with HTTP fallback: ${this.modelPath}`);
49
+ }
27
50
  }
28
51
 
29
52
  // Public API methods
@@ -44,7 +67,12 @@ export class LlamaCppProvider extends EmbeddingProvider {
44
67
 
45
68
  async isReady(): Promise<boolean> {
46
69
  try {
47
- // Check if llama-embedding exists and is executable
70
+ if (this.useNative && this.nativeModel) {
71
+ // Native module is ready if model was loaded successfully
72
+ return true;
73
+ }
74
+
75
+ // Fallback to HTTP check
48
76
  await access(this.llamaPath, constants.F_OK);
49
77
  await access(this.llamaPath, constants.X_OK);
50
78
 
@@ -69,7 +97,19 @@ export class LlamaCppProvider extends EmbeddingProvider {
69
97
  throw new Error('Text input cannot be empty');
70
98
  }
71
99
 
72
- // Use HTTP API instead of CLI arguments
100
+ if (this.useNative && this.nativeModel) {
101
+ // Use native module
102
+ const embedding = this.nativeModel.embed(text);
103
+
104
+ return {
105
+ embedding,
106
+ dimensions: embedding.length,
107
+ model: this.getModel(),
108
+ provider: 'llamacpp',
109
+ };
110
+ }
111
+
112
+ // Fallback to HTTP
73
113
  const requestBody = {
74
114
  input: text,
75
115
  model: await this.getModelPath(),
@@ -99,6 +139,31 @@ export class LlamaCppProvider extends EmbeddingProvider {
99
139
  try {
100
140
  logger.debug(`Batch embedding ${inputs.length} texts with llama.cpp`);
101
141
 
142
+ if (this.useNative && this.nativeModel) {
143
+ // Use native module for batch
144
+ const embeddings: number[][] = [];
145
+
146
+ for (const input of inputs) {
147
+ const text = await this.readInput(input);
148
+ if (text.trim()) {
149
+ const embedding = this.nativeModel.embed(text);
150
+ embeddings.push(embedding);
151
+ }
152
+ }
153
+
154
+ if (embeddings.length === 0) {
155
+ throw new Error('No valid texts to embed');
156
+ }
157
+
158
+ return {
159
+ embeddings,
160
+ dimensions: embeddings[0]?.length || 0,
161
+ model: this.getModel(),
162
+ provider: 'llamacpp',
163
+ };
164
+ }
165
+
166
+ // Fallback to HTTP batch processing
102
167
  const texts = [];
103
168
  for (const input of inputs) {
104
169
  const text = await this.readInput(input);
@@ -140,6 +205,19 @@ export class LlamaCppProvider extends EmbeddingProvider {
140
205
  }
141
206
  }
142
207
 
208
+ // Cleanup method
209
+ async cleanup(): Promise<void> {
210
+ if (this.useNative && this.nativeModel) {
211
+ try {
212
+ this.nativeModel.close();
213
+ this.nativeModel = null;
214
+ logger.info('Native Llama.cpp model closed');
215
+ } catch (error) {
216
+ logger.error(`Error closing native model: ${error}`);
217
+ }
218
+ }
219
+ }
220
+
143
221
  // Protected methods
144
222
  protected getModel(): string {
145
223
  return this.modelPath;
@@ -268,8 +346,8 @@ export class LlamaCppProvider extends EmbeddingProvider {
268
346
  throw new Error(`Unexpected format: ${JSON.stringify(Object.keys(response))}`);
269
347
 
270
348
  } catch (error: unknown) {
271
- const errorMessage = error instanceof Error ? (error instanceof Error ? error.message : String(error)) : 'Unknown error';
272
- throw new Error(`Parse failed: ${errorMessage}`, { cause: error });
349
+ const errorMessage = error instanceof Error ? error.message : String(error);
350
+ throw new Error(`Parse failed: ${errorMessage}`);
273
351
  }
274
352
  }
275
353
 
@@ -1,9 +1,7 @@
1
1
  export type ProviderType =
2
2
  | 'openai'
3
3
  | 'gemini'
4
- | 'claude'
5
4
  | 'mistral'
6
- | 'deepseek'
7
5
  | 'llamacpp';
8
6
 
9
7
  export interface EmbedConfig {
Binary file
@@ -1,78 +0,0 @@
1
- import Anthropic from '@anthropic-ai/sdk';
2
- import { EmbeddingProvider } from '@providers/base/EmbeddingProvider';
3
- import type { EmbedConfig, EmbedResult, BatchEmbedResult } from '@src/types/index';
4
- import { Logger } from '@src/util/logger';
5
-
6
- const logger = Logger.createModuleLogger('claude');
7
-
8
- export class ClaudeProvider extends EmbeddingProvider {
9
- private client: Anthropic;
10
-
11
- constructor(config: EmbedConfig) {
12
- super(config);
13
-
14
- if (!config.apiKey) {
15
- throw new Error('Anthropic API key is required');
16
- }
17
-
18
- this.client = new Anthropic({
19
- apiKey: config.apiKey,
20
- baseURL: config.baseUrl,
21
- timeout: config.timeout || 30000,
22
- });
23
-
24
- logger.info('Claude provider initialized');
25
- }
26
-
27
- async embed(): Promise<EmbedResult> {
28
- try {
29
- logger.debug(`Embedding text with model: ${this.getModel()}`);
30
-
31
- // Note: Claude doesn't have native embeddings API yet
32
- // This is a placeholder implementation
33
- throw new Error('Claude embeddings API not yet available. Please use another provider.');
34
-
35
- } catch (error: unknown) {
36
- const errorMessage = error instanceof Error ? (error instanceof Error ? error.message : String(error)) : 'Unknown error';
37
- logger.error(`Claude embedding failed: ${errorMessage}`);
38
- throw error;
39
- }
40
- }
41
-
42
- async embedBatch(): Promise<BatchEmbedResult> {
43
- try {
44
- // Note: Claude doesn't have native embeddings API yet
45
- throw new Error('Claude embeddings API not yet available. Please use another provider.');
46
-
47
- } catch (error: unknown) {
48
- const errorMessage = error instanceof Error ? (error instanceof Error ? error.message : String(error)) : 'Unknown error';
49
- logger.error(`Claude batch embedding failed: ${errorMessage}`);
50
- throw error;
51
- }
52
- }
53
-
54
- getDimensions(): number {
55
- // Claude doesn't have embeddings yet
56
- return 0;
57
- }
58
-
59
- getProviderName(): string {
60
- return 'Anthropic Claude';
61
- }
62
-
63
- async isReady(): Promise<boolean> {
64
- try {
65
- // Test API access
66
- await this.client.messages.create({
67
- model: 'claude-3-haiku-20240307',
68
- max_tokens: 10,
69
- messages: [{ role: 'user', content: 'test' }]
70
- });
71
- return true;
72
- } catch (error: unknown) {
73
- const errorMessage = error instanceof Error ? (error instanceof Error ? error.message : String(error)) : 'Unknown error';
74
- logger.error(`Claude readiness check failed: ${errorMessage}`);
75
- return false;
76
- }
77
- }
78
- }
@@ -1,115 +0,0 @@
1
- import { DeepSeek } from 'deepseek';
2
- import { EmbeddingProvider } from '@providers/base/EmbeddingProvider';
3
- import type { EmbedConfig, EmbedInput, EmbedResult, BatchEmbedResult } from '@src/types/index';
4
- import { Logger } from '@src/util/logger';
5
-
6
- const logger = Logger.createModuleLogger('deepseek');
7
-
8
- export class DeepSeekProvider extends EmbeddingProvider {
9
- private client: DeepSeek;
10
-
11
- constructor(config: EmbedConfig) {
12
- super(config);
13
-
14
- if (!config.apiKey) {
15
- throw new Error('DeepSeek API key is required');
16
- }
17
-
18
- const clientOptions: { apiKey: string; timeout: number; baseURL?: string } = {
19
- apiKey: config.apiKey,
20
- timeout: config.timeout || 30000,
21
- };
22
-
23
- if (config.baseUrl) {
24
- clientOptions.baseURL = config.baseUrl;
25
- }
26
-
27
- this.client = new DeepSeek(clientOptions);
28
-
29
- logger.info('DeepSeek provider initialized');
30
- }
31
-
32
- async embed(input: EmbedInput): Promise<EmbedResult> {
33
- try {
34
- const text = await this.readInput(input);
35
- logger.debug(`Embedding text with model: ${this.getModel()}`);
36
-
37
- const response = await this.client.embeddings.create({
38
- model: this.getModel(),
39
- input: text,
40
- });
41
-
42
- const embedding = response.data[0];
43
- if (!embedding) {
44
- throw new Error('No embedding returned from DeepSeek API');
45
- }
46
-
47
- return {
48
- embedding: embedding.embedding || [],
49
- dimensions: embedding.embedding?.length || 0,
50
- model: embedding.model || this.getModel(),
51
- provider: 'deepseek',
52
- usage: response.usage ? {
53
- promptTokens: response.usage.prompt_tokens,
54
- totalTokens: response.usage.total_tokens,
55
- } : undefined,
56
- };
57
- } catch (error: unknown) {
58
- logger.error(`DeepSeek embedding failed: ${(error instanceof Error ? error.message : String(error))}`);
59
- throw error;
60
- }
61
- }
62
-
63
- async embedBatch(inputs: EmbedInput[]): Promise<BatchEmbedResult> {
64
- try {
65
- const texts = await Promise.all(inputs.map(input => this.readInput(input)));
66
- logger.debug(`Batch embedding ${texts.length} texts with model: ${this.getModel()}`);
67
-
68
- const response = await this.client.embeddings.create({
69
- model: this.getModel(),
70
- input: texts,
71
- });
72
-
73
- const embeddings = response.data.map((item: { embedding: number[] }) => item.embedding);
74
-
75
- return {
76
- embeddings,
77
- dimensions: embeddings[0]?.length || 0,
78
- model: response.model,
79
- provider: 'deepseek',
80
- usage: response.usage ? {
81
- promptTokens: response.usage.prompt_tokens,
82
- totalTokens: response.usage.total_tokens,
83
- } : undefined,
84
- };
85
- } catch (error: unknown) {
86
- logger.error(`DeepSeek batch embedding failed: ${(error instanceof Error ? error.message : String(error))}`);
87
- throw error;
88
- }
89
- }
90
-
91
- getDimensions(): number {
92
- // DeepSeek embedding dimensions
93
- const model = this.getModel();
94
- if (model.includes('deepseek-chat')) return 4096;
95
- return 4096; // default for DeepSeek
96
- }
97
-
98
- getProviderName(): string {
99
- return 'DeepSeek';
100
- }
101
-
102
- async isReady(): Promise<boolean> {
103
- try {
104
- // Test with a simple embedding request
105
- await this.client.embeddings.create({
106
- model: this.getModel(),
107
- input: 'test',
108
- });
109
- return true;
110
- } catch (error: unknown) {
111
- logger.error(`DeepSeek readiness check failed: ${(error instanceof Error ? error.message : String(error))}`);
112
- return false;
113
- }
114
- }
115
- }
@@ -1,15 +0,0 @@
1
- declare module 'deepseek' {
2
- export class DeepSeek {
3
- constructor(options: { apiKey: string; baseURL?: string; timeout?: number });
4
- embeddings: {
5
- create: (options: { model: string; input: string | string[] }) => Promise<{
6
- data: Array<{ embedding: number[]; model: string }>;
7
- model: string;
8
- usage?: {
9
- prompt_tokens: number;
10
- total_tokens: number;
11
- };
12
- }>;
13
- };
14
- }
15
- }
@@ -1,43 +0,0 @@
1
- export type ProviderType =
2
- | 'openai'
3
- | 'gemini'
4
- | 'claude'
5
- | 'mistral'
6
- | 'deepseek'
7
- | 'llamacpp';
8
-
9
- export interface EmbedConfig {
10
- provider: ProviderType;
11
- model?: string;
12
- apiKey?: string;
13
- baseUrl?: string;
14
- timeout?: number;
15
- maxRetries?: number;
16
- }
17
-
18
- export interface EmbedInput {
19
- text?: string;
20
- filePath?: string;
21
- }
22
-
23
- export interface EmbedResult {
24
- embedding: number[];
25
- dimensions: number;
26
- model: string;
27
- provider: string;
28
- usage?: {
29
- promptTokens?: number;
30
- totalTokens?: number;
31
- } | undefined;
32
- }
33
-
34
- export interface BatchEmbedResult {
35
- embeddings: number[][];
36
- dimensions: number;
37
- model: string;
38
- provider: string;
39
- usage?: {
40
- promptTokens?: number;
41
- totalTokens?: number;
42
- } | undefined;
43
- }
@@ -1,7 +0,0 @@
1
- declare module '@xenova/transformers' {
2
- export function pipeline(task: string, model: string): Promise<unknown>;
3
- export const env: {
4
- cacheDir: string;
5
- allowLocalModels: boolean;
6
- };
7
- }