cactus-react-native 0.2.9 → 0.2.11

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 (59) hide show
  1. package/README.md +49 -782
  2. package/android/src/main/CMakeLists.txt +2 -1
  3. package/android/src/main/java/com/cactus/CactusPackage.java +5 -5
  4. package/android/src/main/java/com/cactus/LlamaContext.java +1 -67
  5. package/android/src/main/jniLibs/arm64-v8a/libcactus.so +0 -0
  6. package/android/src/main/jniLibs/arm64-v8a/libcactus_v8.so +0 -0
  7. package/android/src/main/jniLibs/arm64-v8a/libcactus_v8_2.so +0 -0
  8. package/android/src/main/jniLibs/arm64-v8a/libcactus_v8_2_dotprod.so +0 -0
  9. package/android/src/main/jniLibs/arm64-v8a/libcactus_v8_2_dotprod_i8mm.so +0 -0
  10. package/android/src/main/jniLibs/arm64-v8a/libcactus_v8_2_i8mm.so +0 -0
  11. package/android/src/newarch/java/com/cactus/CactusModule.java +0 -2
  12. package/ios/cactus.xcframework/ios-arm64/cactus.framework/Info.plist +0 -0
  13. package/ios/cactus.xcframework/ios-arm64/cactus.framework/cactus +0 -0
  14. package/ios/cactus.xcframework/ios-arm64_x86_64-simulator/cactus.framework/Info.plist +0 -0
  15. package/ios/cactus.xcframework/ios-arm64_x86_64-simulator/cactus.framework/_CodeSignature/CodeResources +1 -1
  16. package/ios/cactus.xcframework/ios-arm64_x86_64-simulator/cactus.framework/cactus +0 -0
  17. package/ios/cactus.xcframework/tvos-arm64_x86_64-simulator/cactus.framework/Info.plist +0 -0
  18. package/ios/cactus.xcframework/tvos-arm64_x86_64-simulator/cactus.framework/_CodeSignature/CodeResources +1 -1
  19. package/ios/cactus.xcframework/tvos-arm64_x86_64-simulator/cactus.framework/cactus +0 -0
  20. package/lib/commonjs/agent.js +3 -0
  21. package/lib/commonjs/agent.js.map +1 -1
  22. package/lib/commonjs/index.js.map +1 -1
  23. package/lib/commonjs/lm.js +3 -0
  24. package/lib/commonjs/lm.js.map +1 -1
  25. package/lib/commonjs/projectId.js +2 -1
  26. package/lib/commonjs/projectId.js.map +1 -1
  27. package/lib/commonjs/tts.js +124 -12
  28. package/lib/commonjs/tts.js.map +1 -1
  29. package/lib/commonjs/vlm.js +3 -0
  30. package/lib/commonjs/vlm.js.map +1 -1
  31. package/lib/module/agent.js +3 -0
  32. package/lib/module/agent.js.map +1 -1
  33. package/lib/module/index.js.map +1 -1
  34. package/lib/module/lm.js +3 -0
  35. package/lib/module/lm.js.map +1 -1
  36. package/lib/module/projectId.js +2 -1
  37. package/lib/module/projectId.js.map +1 -1
  38. package/lib/module/tts.js +123 -12
  39. package/lib/module/tts.js.map +1 -1
  40. package/lib/module/vlm.js +3 -0
  41. package/lib/module/vlm.js.map +1 -1
  42. package/lib/typescript/agent.d.ts +1 -0
  43. package/lib/typescript/agent.d.ts.map +1 -1
  44. package/lib/typescript/index.d.ts +1 -1
  45. package/lib/typescript/index.d.ts.map +1 -1
  46. package/lib/typescript/lm.d.ts +1 -0
  47. package/lib/typescript/lm.d.ts.map +1 -1
  48. package/lib/typescript/projectId.d.ts +1 -1
  49. package/lib/typescript/projectId.d.ts.map +1 -1
  50. package/lib/typescript/tts.d.ts +46 -2
  51. package/lib/typescript/tts.d.ts.map +1 -1
  52. package/lib/typescript/vlm.d.ts +1 -0
  53. package/lib/typescript/vlm.d.ts.map +1 -1
  54. package/package.json +1 -1
  55. package/src/agent.ts +3 -0
  56. package/src/index.ts +1 -0
  57. package/src/lm.ts +3 -0
  58. package/src/tts.ts +209 -15
  59. package/src/vlm.ts +4 -0
package/README.md CHANGED
@@ -1,837 +1,104 @@
1
- # Cactus React Native
1
+ ![Cactus Logo](assets/logo.png)
2
2
 
3
- Run LLMs, VLMs, and TTS models directly on mobile devices.
3
+ Official React-Native plugin for Cactus, a framework for deploying LLM/VLM/TTS models locally in your app. Requires iOS 12.0+, Android API 24+ and Yarn. For iOS apps, ensure you have cocoapods or install with `brew install cocoapods`. For Android apps, you need Java 17 installed. Expo is strongly recommended.
4
4
 
5
- ## Installation
5
+ ## Resources
6
+ [![cactus](https://img.shields.io/badge/cactus-000000?logo=github&logoColor=white)](https://github.com/cactus-compute/cactus) [![HuggingFace](https://img.shields.io/badge/HuggingFace-FFD21E?logo=huggingface&logoColor=black)](https://huggingface.co/Cactus-Compute/models?sort=downloads) [![Discord](https://img.shields.io/badge/Discord-5865F2?logo=discord&logoColor=white)](https://discord.gg/bNurx3AXTJ) [![Documentation](https://img.shields.io/badge/Documentation-4285F4?logo=googledocs&logoColor=white)](https://cactuscompute.com/docs/react-native)
6
7
 
7
- ```json
8
- {
9
- "dependencies": {
10
- "cactus-react-native": "^0.2.4",
11
- "react-native-fs": "^2.20.0"
12
- }
13
- }
8
+ ## Installation
9
+ Execute the following command in your project terminal:
10
+ ```bash
11
+ npm install cactus-react-native
12
+ # or
13
+ yarn add cactus-react-native
14
14
  ```
15
+ *N/B*: To build locally or use this repo, see instructions in `example/README.md`
15
16
 
16
- **Setup:**
17
- - iOS: `cd ios && npx pod-install`
18
- - Android: Ensure `minSdkVersion` 24+
19
-
20
- ## Quick Start
21
-
17
+ ## Text Completion
22
18
  ```typescript
23
19
  import { CactusLM } from 'cactus-react-native';
24
- import RNFS from 'react-native-fs';
25
-
26
- const modelPath = `${RNFS.DocumentDirectoryPath}/model.gguf`;
27
20
 
28
21
  const { lm, error } = await CactusLM.init({
29
- model: modelPath,
30
- n_ctx: 2048,
31
- n_threads: 4,
22
+ model: '/path/to/model.gguf', // this is a local model file inside the app sandbox
23
+ n_ctx: 2048,
32
24
  });
33
25
 
34
- if (error) throw error;
35
-
36
26
  const messages = [{ role: 'user', content: 'Hello!' }];
37
- const result = await lm.completion(messages, { n_predict: 100 });
38
- console.log(result.text);
39
- lm.release();
40
- ```
41
-
42
- ## Streaming Chat
43
-
44
- ```typescript
45
- import React, { useState, useEffect } from 'react';
46
- import { View, Text, TextInput, TouchableOpacity, ScrollView, ActivityIndicator } from 'react-native';
47
- import { CactusLM } from 'cactus-react-native';
48
- import RNFS from 'react-native-fs';
49
-
50
- interface Message {
51
- role: 'user' | 'assistant';
52
- content: string;
53
- }
54
-
55
- export default function ChatScreen() {
56
- const [lm, setLM] = useState<CactusLM | null>(null);
57
- const [messages, setMessages] = useState<Message[]>([]);
58
- const [input, setInput] = useState('');
59
- const [isLoading, setIsLoading] = useState(true);
60
- const [isGenerating, setIsGenerating] = useState(false);
61
-
62
- useEffect(() => {
63
- initializeModel();
64
- return () => {
65
- lm?.release();
66
- };
67
- }, []);
68
-
69
- const initializeModel = async () => {
70
- try {
71
- const modelUrl = 'https://huggingface.co/Cactus-Compute/Qwen3-600m-Instruct-GGUF/resolve/main/Qwen3-0.6B-Q8_0.gguf';
72
- const modelPath = await downloadModel(modelUrl, 'qwen-600m.gguf');
73
-
74
- const { lm: model, error } = await CactusLM.init({
75
- model: modelPath,
76
- n_ctx: 2048,
77
- n_threads: 4,
78
- n_gpu_layers: 99,
79
- });
80
-
81
- if (error) throw error;
82
- setLM(model);
83
- } catch (error) {
84
- console.error('Failed to initialize model:', error);
85
- } finally {
86
- setIsLoading(false);
87
- }
88
- };
89
-
90
- const downloadModel = async (url: string, filename: string): Promise<string> => {
91
- const path = `${RNFS.DocumentDirectoryPath}/${filename}`;
92
-
93
- if (await RNFS.exists(path)) return path;
94
-
95
- console.log('Downloading model...');
96
- await RNFS.downloadFile({
97
- fromUrl: url,
98
- toFile: path,
99
- progress: (res) => {
100
- const progress = res.bytesWritten / res.contentLength;
101
- console.log(`Download progress: ${(progress * 100).toFixed(1)}%`);
102
- },
103
- }).promise;
104
-
105
- return path;
106
- };
107
-
108
- const sendMessage = async () => {
109
- if (!lm || !input.trim() || isGenerating) return;
110
-
111
- const userMessage: Message = { role: 'user', content: input.trim() };
112
- const newMessages = [...messages, userMessage];
113
- setMessages([...newMessages, { role: 'assistant', content: '' }]);
114
- setInput('');
115
- setIsGenerating(true);
116
-
117
- try {
118
- let response = '';
119
- await lm.completion(newMessages, {
120
- n_predict: 200,
121
- temperature: 0.7,
122
- stop: ['</s>', '<|end|>'],
123
- }, (token) => {
124
- response += token.token;
125
- setMessages(prev => [
126
- ...prev.slice(0, -1),
127
- { role: 'assistant', content: response }
128
- ]);
129
- });
130
- } catch (error) {
131
- console.error('Generation failed:', error);
132
- setMessages(prev => [
133
- ...prev.slice(0, -1),
134
- { role: 'assistant', content: 'Error generating response' }
135
- ]);
136
- } finally {
137
- setIsGenerating(false);
138
- }
139
- };
140
-
141
- if (isLoading) {
142
- return (
143
- <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
144
- <ActivityIndicator size="large" />
145
- <Text style={{ marginTop: 16 }}>Loading model...</Text>
146
- </View>
147
- );
148
- }
149
-
150
- return (
151
- <View style={{ flex: 1, backgroundColor: '#f5f5f5' }}>
152
- <ScrollView style={{ flex: 1, padding: 16 }}>
153
- {messages.map((msg, index) => (
154
- <View
155
- key={index}
156
- style={{
157
- backgroundColor: msg.role === 'user' ? '#007AFF' : '#ffffff',
158
- padding: 12,
159
- marginVertical: 4,
160
- borderRadius: 12,
161
- alignSelf: msg.role === 'user' ? 'flex-end' : 'flex-start',
162
- maxWidth: '80%',
163
- shadowColor: '#000',
164
- shadowOffset: { width: 0, height: 1 },
165
- shadowOpacity: 0.2,
166
- shadowRadius: 2,
167
- elevation: 2,
168
- }}
169
- >
170
- <Text style={{
171
- color: msg.role === 'user' ? '#ffffff' : '#000000',
172
- fontSize: 16,
173
- }}>
174
- {msg.content}
175
- </Text>
176
- </View>
177
- ))}
178
- </ScrollView>
179
-
180
- <View style={{
181
- flexDirection: 'row',
182
- padding: 16,
183
- backgroundColor: '#ffffff',
184
- borderTopWidth: 1,
185
- borderTopColor: '#e0e0e0',
186
- }}>
187
- <TextInput
188
- style={{
189
- flex: 1,
190
- borderWidth: 1,
191
- borderColor: '#e0e0e0',
192
- borderRadius: 20,
193
- paddingHorizontal: 16,
194
- paddingVertical: 10,
195
- fontSize: 16,
196
- backgroundColor: '#f8f8f8',
197
- }}
198
- value={input}
199
- onChangeText={setInput}
200
- placeholder="Type a message..."
201
- multiline
202
- onSubmitEditing={sendMessage}
203
- />
204
- <TouchableOpacity
205
- onPress={sendMessage}
206
- disabled={isGenerating || !input.trim()}
207
- style={{
208
- backgroundColor: isGenerating ? '#cccccc' : '#007AFF',
209
- borderRadius: 20,
210
- paddingHorizontal: 16,
211
- paddingVertical: 10,
212
- marginLeft: 8,
213
- justifyContent: 'center',
214
- }}
215
- >
216
- <Text style={{ color: '#ffffff', fontWeight: 'bold' }}>
217
- {isGenerating ? '...' : 'Send'}
218
- </Text>
219
- </TouchableOpacity>
220
- </View>
221
- </View>
222
- );
223
- }
27
+ const params = { n_predict: 100, temperature: 0.7 };
28
+ const response = await lm.completion(messages, params);
224
29
  ```
225
-
226
- ## Core APIs
227
-
228
- ### CactusLM
229
-
230
- ```typescript
30
+ ## Embeddings
31
+ ```typescript
231
32
  import { CactusLM } from 'cactus-react-native';
232
33
 
233
34
  const { lm, error } = await CactusLM.init({
234
- model: '/path/to/model.gguf',
235
- n_ctx: 2048,
236
- n_threads: 4,
237
- n_gpu_layers: 99,
238
- embedding: true,
35
+ model: '/path/to/model.gguf', // local model file inside the app sandbox
36
+ n_ctx: 2048,
37
+ embedding: true,
239
38
  });
240
39
 
241
- const messages = [{ role: 'user', content: 'What is AI?' }];
242
- const result = await lm.completion(messages, {
243
- n_predict: 200,
244
- temperature: 0.7,
245
- stop: ['</s>'],
246
- });
247
-
248
- const embedding = await lm.embedding('Your text here');
249
- await lm.rewind();
250
- await lm.release();
40
+ const text = 'Your text to embed';
41
+ const params = { normalize: true };
42
+ const result = await lm.embedding(text, params);
251
43
  ```
252
-
253
- ### CactusVLM
254
-
44
+ ## Visual Language Models
255
45
  ```typescript
256
46
  import { CactusVLM } from 'cactus-react-native';
257
47
 
258
48
  const { vlm, error } = await CactusVLM.init({
259
- model: '/path/to/vision-model.gguf',
260
- mmproj: '/path/to/mmproj.gguf',
261
- n_ctx: 2048,
49
+ model: '/path/to/vision-model.gguf', // local model file inside the app sandbox
50
+ mmproj: '/path/to/mmproj.gguf', // local model file inside the app sandbox
262
51
  });
263
52
 
264
53
  const messages = [{ role: 'user', content: 'Describe this image' }];
265
- const result = await vlm.completion(messages, {
266
- images: ['/path/to/image.jpg'],
267
- n_predict: 200,
268
- temperature: 0.3,
269
- });
270
-
271
- await vlm.release();
272
- ```
273
-
274
- ### CactusTTS
275
-
276
- ```typescript
277
- import { CactusTTS, initLlama } from 'cactus-react-native';
278
-
279
- const context = await initLlama({
280
- model: '/path/to/tts-model.gguf',
281
- n_ctx: 1024,
282
- });
283
-
284
- const tts = await CactusTTS.init(context, '/path/to/vocoder.gguf');
285
-
286
- const audio = await tts.generate(
287
- 'Hello, this is text-to-speech',
288
- '{"speaker_id": 0}'
289
- );
290
-
291
- await tts.release();
292
- ```
293
-
294
- ## Advanced Usage
295
-
296
- ### Model Manager
297
-
298
- ```typescript
299
- class ModelManager {
300
- private models = new Map<string, CactusLM | CactusVLM>();
301
-
302
- async loadLM(name: string, modelPath: string): Promise<CactusLM> {
303
- if (this.models.has(name)) {
304
- return this.models.get(name) as CactusLM;
305
- }
306
-
307
- const { lm, error } = await CactusLM.init({
308
- model: modelPath,
309
- n_ctx: 2048,
310
- });
311
-
312
- if (error) throw error;
313
- this.models.set(name, lm);
314
- return lm;
315
- }
316
-
317
- async loadVLM(name: string, modelPath: string, mmprojPath: string): Promise<CactusVLM> {
318
- if (this.models.has(name)) {
319
- return this.models.get(name) as CactusVLM;
320
- }
321
-
322
- const { vlm, error } = await CactusVLM.init({
323
- model: modelPath,
324
- mmproj: mmprojPath,
325
- });
326
-
327
- if (error) throw error;
328
- this.models.set(name, vlm);
329
- return vlm;
330
- }
331
-
332
- async releaseModel(name: string): Promise<void> {
333
- const model = this.models.get(name);
334
- if (model) {
335
- await model.release();
336
- this.models.delete(name);
337
- }
338
- }
339
-
340
- async releaseAll(): Promise<void> {
341
- await Promise.all(
342
- Array.from(this.models.values()).map(model => model.release())
343
- );
344
- this.models.clear();
345
- }
346
- }
347
-
348
- const modelManager = new ModelManager();
349
- ```
350
54
 
351
- ### File Management Hook
352
-
353
- ```typescript
354
- import { useState, useCallback } from 'react';
355
- import RNFS from 'react-native-fs';
356
-
357
- interface DownloadProgress {
358
- progress: number;
359
- isDownloading: boolean;
360
- error: string | null;
361
- }
362
-
363
- export const useModelDownload = () => {
364
- const [downloads, setDownloads] = useState<Map<string, DownloadProgress>>(new Map());
365
-
366
- const downloadModel = useCallback(async (url: string, filename: string): Promise<string> => {
367
- const path = `${RNFS.DocumentDirectoryPath}/${filename}`;
368
-
369
- if (await RNFS.exists(path)) {
370
- const stats = await RNFS.stat(path);
371
- if (stats.size > 0) return path;
372
- }
373
-
374
- setDownloads(prev => new Map(prev.set(filename, {
375
- progress: 0,
376
- isDownloading: true,
377
- error: null,
378
- })));
379
-
380
- try {
381
- await RNFS.downloadFile({
382
- fromUrl: url,
383
- toFile: path,
384
- progress: (res) => {
385
- const progress = res.bytesWritten / res.contentLength;
386
- setDownloads(prev => new Map(prev.set(filename, {
387
- progress,
388
- isDownloading: true,
389
- error: null,
390
- })));
391
- },
392
- }).promise;
393
-
394
- setDownloads(prev => new Map(prev.set(filename, {
395
- progress: 1,
396
- isDownloading: false,
397
- error: null,
398
- })));
399
-
400
- return path;
401
- } catch (error) {
402
- setDownloads(prev => new Map(prev.set(filename, {
403
- progress: 0,
404
- isDownloading: false,
405
- error: error.message,
406
- })));
407
- throw error;
408
- }
409
- }, []);
410
-
411
- return { downloadModel, downloads };
55
+ const params = {
56
+ images: ['/absolute/path/to/image.jpg'],
57
+ n_predict: 200,
58
+ temperature: 0.3,
412
59
  };
413
- ```
414
-
415
- ### Vision Chat Component
416
-
417
- ```typescript
418
- import React, { useState, useEffect } from 'react';
419
- import { View, Text, TouchableOpacity, Image, Alert } from 'react-native';
420
- import { launchImageLibrary } from 'react-native-image-picker';
421
- import { CactusVLM } from 'cactus-react-native';
422
- import RNFS from 'react-native-fs';
423
-
424
- export default function VisionChat() {
425
- const [vlm, setVLM] = useState<CactusVLM | null>(null);
426
- const [imagePath, setImagePath] = useState<string | null>(null);
427
- const [response, setResponse] = useState('');
428
- const [isLoading, setIsLoading] = useState(true);
429
- const [isAnalyzing, setIsAnalyzing] = useState(false);
430
-
431
- useEffect(() => {
432
- initializeVLM();
433
- return () => {
434
- vlm?.release();
435
- };
436
- }, []);
437
-
438
- const initializeVLM = async () => {
439
- try {
440
- const modelUrl = 'https://huggingface.co/Cactus-Compute/SmolVLM2-500m-Instruct-GGUF/resolve/main/SmolVLM2-500M-Video-Instruct-Q8_0.gguf';
441
- const mmprojUrl = 'https://huggingface.co/Cactus-Compute/SmolVLM2-500m-Instruct-GGUF/resolve/main/mmproj-SmolVLM2-500M-Video-Instruct-Q8_0.gguf';
442
-
443
- const [modelPath, mmprojPath] = await Promise.all([
444
- downloadFile(modelUrl, 'smolvlm-model.gguf'),
445
- downloadFile(mmprojUrl, 'smolvlm-mmproj.gguf'),
446
- ]);
447
60
 
448
- const { vlm: model, error } = await CactusVLM.init({
449
- model: modelPath,
450
- mmproj: mmprojPath,
451
- n_ctx: 2048,
452
- });
453
-
454
- if (error) throw error;
455
- setVLM(model);
456
- } catch (error) {
457
- console.error('Failed to initialize VLM:', error);
458
- Alert.alert('Error', 'Failed to initialize vision model');
459
- } finally {
460
- setIsLoading(false);
461
- }
462
- };
463
-
464
- const downloadFile = async (url: string, filename: string): Promise<string> => {
465
- const path = `${RNFS.DocumentDirectoryPath}/${filename}`;
466
-
467
- if (await RNFS.exists(path)) return path;
468
-
469
- await RNFS.downloadFile({ fromUrl: url, toFile: path }).promise;
470
- return path;
471
- };
472
-
473
- const pickImage = () => {
474
- launchImageLibrary(
475
- {
476
- mediaType: 'photo',
477
- quality: 0.8,
478
- includeBase64: false,
479
- },
480
- (response) => {
481
- if (response.assets && response.assets[0]) {
482
- setImagePath(response.assets[0].uri!);
483
- setResponse('');
484
- }
485
- }
486
- );
487
- };
488
-
489
- const analyzeImage = async () => {
490
- if (!vlm || !imagePath) return;
491
-
492
- setIsAnalyzing(true);
493
- try {
494
- const messages = [{ role: 'user', content: 'Describe this image in detail' }];
495
-
496
- let analysisResponse = '';
497
- const result = await vlm.completion(messages, {
498
- images: [imagePath],
499
- n_predict: 300,
500
- temperature: 0.3,
501
- }, (token) => {
502
- analysisResponse += token.token;
503
- setResponse(analysisResponse);
504
- });
505
-
506
- setResponse(analysisResponse || result.text);
507
- } catch (error) {
508
- console.error('Analysis failed:', error);
509
- Alert.alert('Error', 'Failed to analyze image');
510
- } finally {
511
- setIsAnalyzing(false);
512
- }
513
- };
514
-
515
- if (isLoading) {
516
- return (
517
- <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
518
- <Text>Loading vision model...</Text>
519
- </View>
520
- );
521
- }
522
-
523
- return (
524
- <View style={{ flex: 1, padding: 16 }}>
525
- <Text style={{ fontSize: 24, fontWeight: 'bold', marginBottom: 20 }}>
526
- Vision Chat
527
- </Text>
528
-
529
- {imagePath && (
530
- <Image
531
- source={{ uri: imagePath }}
532
- style={{
533
- width: '100%',
534
- height: 200,
535
- borderRadius: 8,
536
- marginBottom: 16,
537
- }}
538
- resizeMode="contain"
539
- />
540
- )}
541
-
542
- <View style={{ flexDirection: 'row', marginBottom: 16 }}>
543
- <TouchableOpacity
544
- onPress={pickImage}
545
- style={{
546
- backgroundColor: '#007AFF',
547
- padding: 12,
548
- borderRadius: 8,
549
- marginRight: 8,
550
- flex: 1,
551
- }}
552
- >
553
- <Text style={{ color: 'white', textAlign: 'center', fontWeight: 'bold' }}>
554
- Pick Image
555
- </Text>
556
- </TouchableOpacity>
557
-
558
- <TouchableOpacity
559
- onPress={analyzeImage}
560
- disabled={!imagePath || isAnalyzing}
561
- style={{
562
- backgroundColor: !imagePath || isAnalyzing ? '#cccccc' : '#34C759',
563
- padding: 12,
564
- borderRadius: 8,
565
- flex: 1,
566
- }}
567
- >
568
- <Text style={{ color: 'white', textAlign: 'center', fontWeight: 'bold' }}>
569
- {isAnalyzing ? 'Analyzing...' : 'Analyze'}
570
- </Text>
571
- </TouchableOpacity>
572
- </View>
573
-
574
- <View style={{
575
- flex: 1,
576
- backgroundColor: '#f8f8f8',
577
- borderRadius: 8,
578
- padding: 16,
579
- }}>
580
- <Text style={{ fontSize: 16, lineHeight: 24 }}>
581
- {response || 'Select an image and tap Analyze to get started'}
582
- </Text>
583
- </View>
584
- </View>
585
- );
586
- }
61
+ const response = await vlm.completion(messages, params);
587
62
  ```
588
-
589
- ### Cloud Fallback
590
-
63
+ ## Cloud Fallback
591
64
  ```typescript
592
65
  const { lm } = await CactusLM.init({
593
66
  model: '/path/to/model.gguf',
594
67
  n_ctx: 2048,
595
68
  }, undefined, 'your_cactus_token');
596
69
 
597
- // Try local first, fallback to cloud if local fails
70
+ // Try local first, fallback to cloud if local fails (its blazing fast)
598
71
  const embedding = await lm.embedding('text', undefined, 'localfirst');
599
72
 
600
- // Vision models also support cloud fallback
601
- const { vlm } = await CactusVLM.init({
602
- model: '/path/to/model.gguf',
603
- mmproj: '/path/to/mmproj.gguf',
604
- }, undefined, 'your_cactus_token');
605
-
606
- const result = await vlm.completion(messages, {
607
- images: ['/path/to/image.jpg'],
608
- mode: 'localfirst',
609
- });
610
- ```
611
-
612
- ### Embeddings & Similarity
613
-
614
- ```typescript
615
- const { lm } = await CactusLM.init({
616
- model: '/path/to/model.gguf',
617
- embedding: true,
618
- });
619
-
620
- const embedding1 = await lm.embedding('machine learning');
621
- const embedding2 = await lm.embedding('artificial intelligence');
622
-
623
- function cosineSimilarity(a: number[], b: number[]): number {
624
- const dotProduct = a.reduce((sum, ai, i) => sum + ai * b[i], 0);
625
- const magnitudeA = Math.sqrt(a.reduce((sum, ai) => sum + ai * ai, 0));
626
- const magnitudeB = Math.sqrt(b.reduce((sum, bi) => sum + bi * bi, 0));
627
- return dotProduct / (magnitudeA * magnitudeB);
628
- }
629
-
630
- const similarity = cosineSimilarity(embedding1.embedding, embedding2.embedding);
631
- console.log('Similarity:', similarity);
73
+ // local (default): strictly only run on-device
74
+ // localfirst: fallback to cloud if device fails
75
+ // remotefirst: primarily remote, run local if API fails
76
+ // remote: strictly run on cloud
632
77
  ```
633
78
 
634
- ## Error Handling & Performance
635
-
636
- ### Production Error Handling
637
-
638
- ```typescript
639
- async function safeModelInit(modelPath: string): Promise<CactusLM> {
640
- const configs = [
641
- { model: modelPath, n_ctx: 4096, n_gpu_layers: 99 },
642
- { model: modelPath, n_ctx: 2048, n_gpu_layers: 99 },
643
- { model: modelPath, n_ctx: 2048, n_gpu_layers: 0 },
644
- { model: modelPath, n_ctx: 1024, n_gpu_layers: 0 },
645
- ];
646
-
647
- for (const config of configs) {
648
- try {
649
- const { lm, error } = await CactusLM.init(config);
650
- if (error) throw error;
651
- return lm;
652
- } catch (error) {
653
- console.warn('Config failed:', config, error.message);
654
- if (configs.indexOf(config) === configs.length - 1) {
655
- throw new Error(`All configurations failed. Last error: ${error.message}`);
656
- }
657
- }
658
- }
659
-
660
- throw new Error('Model initialization failed');
661
- }
662
-
663
- async function safeCompletion(lm: CactusLM, messages: any[], retries = 3): Promise<any> {
664
- for (let i = 0; i < retries; i++) {
665
- try {
666
- return await lm.completion(messages, { n_predict: 200 });
667
- } catch (error) {
668
- if (error.message.includes('Context is busy') && i < retries - 1) {
669
- await new Promise(resolve => setTimeout(resolve, 1000));
670
- continue;
671
- }
672
- throw error;
673
- }
674
- }
675
- }
676
- ```
677
-
678
- ### Memory Management
679
-
680
- ```typescript
681
- import { AppState, AppStateStatus } from 'react-native';
682
-
683
- class AppModelManager {
684
- private modelManager = new ModelManager();
685
-
686
- constructor() {
687
- AppState.addEventListener('change', this.handleAppStateChange);
688
- }
689
-
690
- private handleAppStateChange = (nextAppState: AppStateStatus) => {
691
- if (nextAppState === 'background') {
692
- // Release non-essential models when app goes to background
693
- this.modelManager.releaseAll();
694
- }
695
- };
696
-
697
- async getModel(name: string, modelPath: string): Promise<CactusLM> {
698
- try {
699
- return await this.modelManager.loadLM(name, modelPath);
700
- } catch (error) {
701
- // Handle low memory by releasing other models
702
- await this.modelManager.releaseAll();
703
- return await this.modelManager.loadLM(name, modelPath);
704
- }
705
- }
706
- }
707
- ```
708
-
709
- ### Performance Optimization
710
-
711
- ```typescript
712
- // Optimize for device capabilities
713
- const getOptimalConfig = () => {
714
- const { OS } = Platform;
715
- const isHighEndDevice = true; // Implement device detection logic
716
-
717
- return {
718
- n_ctx: isHighEndDevice ? 4096 : 2048,
719
- n_gpu_layers: OS === 'ios' ? 99 : 0, // iOS generally has better GPU support
720
- n_threads: isHighEndDevice ? 6 : 4,
721
- n_batch: isHighEndDevice ? 512 : 256,
722
- };
723
- };
724
-
725
- const config = getOptimalConfig();
726
- const { lm } = await CactusLM.init({
727
- model: modelPath,
728
- ...config,
729
- });
730
- ```
731
-
732
- ## Tool Calling with CactusAgent
733
-
734
- The `CactusAgent` class extends `CactusLM` with built-in tool calling capabilities:
735
-
79
+ ## Agents
736
80
  ```typescript
737
81
  import { CactusAgent } from 'cactus-react-native';
738
82
 
83
+ // we recommend Qwen 3 family, 0.6B is great
739
84
  const { agent, error } = await CactusAgent.init({
740
- model: '/path/to/model.gguf', // we recommend Qwen 3 family, 0.6B is great
741
- n_ctx: 2048,
85
+ model: '/path/to/model.gguf',
86
+ n_ctx: 2048,
742
87
  });
743
88
 
744
89
  const weatherTool = agent.addTool(
745
- (location: string) => `Weather in ${location}: 72°F, sunny`,
746
- 'Get current weather for a location',
747
- {
748
- location: { type: 'string', description: 'City name', required: true }
749
- }
90
+ (location: string) => `Weather in ${location}: 72°F, sunny`,
91
+ 'Get current weather for a location',
92
+ {
93
+ location: { type: 'string', description: 'City name', required: true }
94
+ }
750
95
  );
751
96
 
752
97
  const messages = [{ role: 'user', content: 'What\'s the weather in NYC?' }];
753
- const result = await agent.completionWithTools(messages, {
98
+ const result = await agent.completionWithTools(messages, {
754
99
  n_predict: 200,
755
100
  temperature: 0.7,
756
101
  });
757
102
 
758
103
  await agent.release();
759
- ```
760
-
761
- ### Custom Tools
762
-
763
- ```typescript
764
- // Math calculator tool
765
- const calculator = agent.addTool(
766
- (expression: string) => {
767
- try {
768
- return `Result: ${eval(expression)}`;
769
- } catch (e) {
770
- return 'Invalid expression';
771
- }
772
- },
773
- 'Evaluate mathematical expressions',
774
- {
775
- expression: { type: 'string', description: 'Math expression to evaluate', required: true }
776
- }
777
- );
778
- ```
779
-
780
- ## API Reference
781
-
782
- ### CactusLM
783
-
784
- **init(params, onProgress?, cactusToken?)**
785
- - `model: string` - Path to GGUF model file
786
- - `n_ctx?: number` - Context size (default: 2048)
787
- - `n_threads?: number` - CPU threads (default: 4)
788
- - `n_gpu_layers?: number` - GPU layers (default: 99)
789
- - `embedding?: boolean` - Enable embeddings (default: false)
790
- - `n_batch?: number` - Batch size (default: 512)
791
-
792
- **completion(messages, params?, callback?)**
793
- - `messages: Array<{role: string, content: string}>` - Chat messages
794
- - `n_predict?: number` - Max tokens (default: -1)
795
- - `temperature?: number` - Randomness 0.0-2.0 (default: 0.8)
796
- - `top_p?: number` - Nucleus sampling (default: 0.95)
797
- - `top_k?: number` - Top-k sampling (default: 40)
798
- - `stop?: string[]` - Stop sequences
799
- - `callback?: (token) => void` - Streaming callback
800
-
801
- **embedding(text, params?, mode?)**
802
- - `text: string` - Text to embed
803
- - `mode?: string` - 'local' | 'localfirst' | 'remotefirst' | 'remote'
804
-
805
- ### CactusVLM
806
-
807
- **init(params, onProgress?, cactusToken?)**
808
- - All CactusLM params plus:
809
- - `mmproj: string` - Path to multimodal projector
810
-
811
- **completion(messages, params?, callback?)**
812
- - All CactusLM completion params plus:
813
- - `images?: string[]` - Array of image paths
814
- - `mode?: string` - Cloud fallback mode
815
-
816
- ### Types
817
-
818
- ```typescript
819
- interface CactusOAICompatibleMessage {
820
- role: 'system' | 'user' | 'assistant';
821
- content: string;
822
- }
823
-
824
- interface NativeCompletionResult {
825
- text: string;
826
- tokens_predicted: number;
827
- tokens_evaluated: number;
828
- timings: {
829
- predicted_per_second: number;
830
- prompt_per_second: number;
831
- };
832
- }
833
-
834
- interface NativeEmbeddingResult {
835
- embedding: number[];
836
- }
837
- ```
104
+ ```