cactus-react-native 0.1.3 → 0.1.4

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 (51) hide show
  1. package/README.md +5 -3
  2. package/ios/cactus.xcframework/ios-arm64_x86_64-simulator/cactus.framework/cactus +0 -0
  3. package/ios/cactus.xcframework/tvos-arm64_x86_64-simulator/cactus.framework/cactus +0 -0
  4. package/lib/commonjs/NativeCactus.js +10 -0
  5. package/lib/commonjs/chat.js +37 -0
  6. package/lib/commonjs/grammar.js +560 -0
  7. package/lib/commonjs/index.js +459 -0
  8. package/lib/commonjs/index.js.map +1 -1
  9. package/lib/commonjs/lm.js +72 -0
  10. package/lib/commonjs/lm.js.map +1 -1
  11. package/lib/commonjs/telemetry.js +97 -0
  12. package/lib/commonjs/telemetry.js.map +1 -0
  13. package/lib/commonjs/tools.js +79 -0
  14. package/lib/commonjs/tools.js.map +1 -0
  15. package/lib/commonjs/tts.js +32 -0
  16. package/lib/commonjs/tts.js.map +1 -1
  17. package/lib/commonjs/vlm.js +83 -0
  18. package/lib/commonjs/vlm.js.map +1 -0
  19. package/lib/module/NativeCactus.js +8 -0
  20. package/lib/module/chat.js +33 -0
  21. package/lib/module/grammar.js +553 -0
  22. package/lib/module/index.js +392 -0
  23. package/lib/module/index.js.map +1 -1
  24. package/lib/module/lm.js +67 -0
  25. package/lib/module/lm.js.map +1 -0
  26. package/lib/module/telemetry.js +92 -0
  27. package/lib/module/telemetry.js.map +1 -0
  28. package/lib/module/tools.js +73 -0
  29. package/lib/module/tools.js.map +1 -0
  30. package/lib/module/tts.js +27 -0
  31. package/lib/module/tts.js.map +1 -1
  32. package/lib/module/vlm.js +78 -0
  33. package/lib/module/vlm.js.map +1 -1
  34. package/lib/typescript/index.d.ts.map +1 -1
  35. package/lib/typescript/lm.d.ts +9 -33
  36. package/lib/typescript/lm.d.ts.map +1 -1
  37. package/lib/typescript/telemetry.d.ts +21 -0
  38. package/lib/typescript/telemetry.d.ts.map +1 -0
  39. package/lib/typescript/tools.d.ts +0 -3
  40. package/lib/typescript/tools.d.ts.map +1 -1
  41. package/lib/typescript/tts.d.ts.map +1 -1
  42. package/lib/typescript/vlm.d.ts +12 -34
  43. package/lib/typescript/vlm.d.ts.map +1 -1
  44. package/package.json +1 -1
  45. package/src/index.ts +64 -41
  46. package/src/lm.ts +45 -5
  47. package/src/telemetry.ts +123 -0
  48. package/src/tools.ts +17 -58
  49. package/src/vlm.ts +50 -8
  50. package/android/src/main/jniLibs/x86_64/libcactus.so +0 -0
  51. package/android/src/main/jniLibs/x86_64/libcactus_x86_64.so +0 -0
package/src/lm.ts CHANGED
@@ -7,20 +7,43 @@ import type {
7
7
  EmbeddingParams,
8
8
  NativeEmbeddingResult,
9
9
  } from './index'
10
+ import { Telemetry } from './telemetry'
11
+
12
+ interface CactusLMReturn {
13
+ lm: CactusLM | null
14
+ error: Error | null
15
+ }
10
16
 
11
17
  export class CactusLM {
12
18
  private context: LlamaContext
19
+ private initParams: ContextParams
13
20
 
14
- private constructor(context: LlamaContext) {
21
+ private constructor(context: LlamaContext, initParams: ContextParams) {
15
22
  this.context = context
23
+ this.initParams = initParams
16
24
  }
17
25
 
18
26
  static async init(
19
27
  params: ContextParams,
20
28
  onProgress?: (progress: number) => void,
21
- ): Promise<CactusLM> {
22
- const context = await initLlama(params, onProgress)
23
- return new CactusLM(context)
29
+ ): Promise<CactusLMReturn> {
30
+ const configs = [
31
+ params,
32
+ { ...params, n_gpu_layers: 0 }
33
+ ];
34
+
35
+ for (const config of configs) {
36
+ try {
37
+ const context = await initLlama(config, onProgress);
38
+ return { lm: new CactusLM(context, config), error: null };
39
+ } catch (e) {
40
+ Telemetry.error(e as Error, config);
41
+ if (configs.indexOf(config) === configs.length - 1) {
42
+ return { lm: null, error: e as Error };
43
+ }
44
+ }
45
+ }
46
+ return { lm: null, error: new Error('Failed to initialize CactusLM') };
24
47
  }
25
48
 
26
49
  async completion(
@@ -28,7 +51,24 @@ export class CactusLM {
28
51
  params: CompletionParams = {},
29
52
  callback?: (data: any) => void,
30
53
  ): Promise<NativeCompletionResult> {
31
- return this.context.completion({ messages, ...params }, callback)
54
+ const startTime = Date.now();
55
+ let firstTokenTime: number | null = null;
56
+
57
+ const wrappedCallback = callback ? (data: any) => {
58
+ if (firstTokenTime === null) firstTokenTime = Date.now();
59
+ callback(data);
60
+ } : undefined;
61
+
62
+ const result = await this.context.completion({ messages, ...params }, wrappedCallback);
63
+
64
+ Telemetry.track({
65
+ event: 'completion',
66
+ tok_per_sec: (result as any).timings?.predicted_per_second,
67
+ toks_generated: (result as any).timings?.predicted_n,
68
+ ttft: firstTokenTime ? firstTokenTime - startTime : null,
69
+ }, this.initParams);
70
+
71
+ return result;
32
72
  }
33
73
 
34
74
  async embedding(
@@ -0,0 +1,123 @@
1
+ import { Platform } from 'react-native'
2
+ import type { ContextParams } from './index';
3
+ // Import package.json to get version
4
+ const packageJson = require('../package.json');
5
+
6
+ interface TelemetryRecord {
7
+ os: 'iOS' | 'Android';
8
+ os_version: string;
9
+ framework: string;
10
+ framework_version: string;
11
+ telemetry_payload?: Record<string, any>;
12
+ error_payload?: Record<string, any>;
13
+ timestamp: string;
14
+ model_filename: string;
15
+ n_ctx?: number;
16
+ n_gpu_layers?: number;
17
+ }
18
+
19
+ interface TelemetryConfig {
20
+ supabaseUrl: string;
21
+ supabaseKey: string;
22
+ table?: string;
23
+ }
24
+
25
+ export class Telemetry {
26
+ private static instance: Telemetry | null = null;
27
+ private config: Required<TelemetryConfig>;
28
+
29
+ private constructor(config: TelemetryConfig) {
30
+ this.config = {
31
+ table: 'telemetry',
32
+ ...config
33
+ };
34
+ }
35
+
36
+ private static getFilename(path: string): string {
37
+ try {
38
+ return path.split('/').pop() || path.split('\\').pop() || 'unknown';
39
+ } catch {
40
+ return 'unknown';
41
+ }
42
+ }
43
+
44
+ static autoInit(): void {
45
+ if (!Telemetry.instance) {
46
+ Telemetry.instance = new Telemetry({
47
+ supabaseUrl: 'https://vlqqczxwyaodtcdmdmlw.supabase.co',
48
+ supabaseKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZscXFjenh3eWFvZHRjZG1kbWx3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTE1MTg2MzIsImV4cCI6MjA2NzA5NDYzMn0.nBzqGuK9j6RZ6mOPWU2boAC_5H9XDs-fPpo5P3WZYbI', // Anon!
49
+ });
50
+ }
51
+ }
52
+
53
+ static init(config: TelemetryConfig): void {
54
+ if (!Telemetry.instance) {
55
+ Telemetry.instance = new Telemetry(config);
56
+ }
57
+ }
58
+
59
+ static track(payload: Record<string, any>, options: ContextParams): void {
60
+ if (!Telemetry.instance) {
61
+ Telemetry.autoInit();
62
+ }
63
+ Telemetry.instance!.trackInternal(payload, options);
64
+ }
65
+
66
+ static error(error: Error, options: ContextParams): void {
67
+ if (!Telemetry.instance) {
68
+ Telemetry.autoInit();
69
+ }
70
+ Telemetry.instance!.errorInternal(error, options);
71
+ }
72
+
73
+ private trackInternal(payload: Record<string, any>, options: ContextParams): void {
74
+ const record: TelemetryRecord = {
75
+ os: Platform.OS === 'ios' ? 'iOS' : 'Android',
76
+ os_version: Platform.Version.toString(),
77
+ framework: 'react-native',
78
+ framework_version: packageJson.version,
79
+ telemetry_payload: payload,
80
+ timestamp: new Date().toISOString(),
81
+ model_filename: Telemetry.getFilename(options.model),
82
+ n_ctx: options.n_ctx,
83
+ n_gpu_layers: options.n_gpu_layers
84
+ };
85
+
86
+ this.sendRecord(record).catch(() => {});
87
+ }
88
+
89
+ private errorInternal(error: Error, options: ContextParams): void {
90
+ const errorPayload = {
91
+ message: error.message,
92
+ stack: error.stack,
93
+ name: error.name,
94
+ };
95
+
96
+ const record: TelemetryRecord = {
97
+ os: Platform.OS === 'ios' ? 'iOS' : 'Android',
98
+ os_version: Platform.Version.toString(),
99
+ framework: 'react-native',
100
+ framework_version: packageJson.version,
101
+ error_payload: errorPayload,
102
+ timestamp: new Date().toISOString(),
103
+ model_filename: Telemetry.getFilename(options.model),
104
+ n_ctx: options.n_ctx,
105
+ n_gpu_layers: options.n_gpu_layers
106
+ };
107
+
108
+ this.sendRecord(record).catch(() => {});
109
+ }
110
+
111
+ private async sendRecord(record: TelemetryRecord): Promise<void> {
112
+ await (globalThis as any).fetch(`${this.config.supabaseUrl}/rest/v1/${this.config.table}`, {
113
+ method: 'POST',
114
+ headers: {
115
+ 'apikey': this.config.supabaseKey,
116
+ 'Authorization': `Bearer ${this.config.supabaseKey}`,
117
+ 'Content-Type': 'application/json',
118
+ 'Prefer': 'return=minimal'
119
+ },
120
+ body: JSON.stringify([record])
121
+ });
122
+ }
123
+ }
package/src/tools.ts CHANGED
@@ -1,4 +1,3 @@
1
- import type { CactusOAICompatibleMessage } from "./chat";
2
1
  import type { NativeCompletionResult } from "./NativeCactus";
3
2
 
4
3
  interface Parameter {
@@ -55,73 +54,33 @@ export class Tools {
55
54
  }
56
55
  }
57
56
 
58
- export function injectToolsIntoMessages(messages: CactusOAICompatibleMessage[], tools: Tools): CactusOAICompatibleMessage[] {
59
- const newMessages = [...messages];
60
- const toolsSchemas = tools.getSchemas();
61
- const promptToolInjection = `You have access to the following functions. Use them if required -
62
- ${JSON.stringify(toolsSchemas, null, 2)}
63
- Only use an available tool if needed. If a tool is chosen, respond ONLY with a JSON object matching the following schema:
64
- \`\`\`json
65
- {
66
- "tool_name": "<name of the tool>",
67
- "tool_input": {
68
- "<parameter_name>": "<parameter_value>",
69
- ...
70
- }
71
- }
72
- \`\`\`
73
- Remember, if you are calling a tool, you must respond with the JSON object and the JSON object ONLY!
74
- If no tool is needed, respond normally.
75
- `;
76
-
77
- const systemMessage = newMessages.find(m => m.role === 'system');
78
- if (!systemMessage) {
79
- newMessages.unshift({
80
- role: 'system',
81
- content: promptToolInjection
82
- });
83
- } else {
84
- systemMessage.content = `${systemMessage.content}\n\n${promptToolInjection}`;
85
- }
86
-
87
- return newMessages;
88
- }
89
-
90
57
  export async function parseAndExecuteTool(result: NativeCompletionResult, tools: Tools): Promise<{toolCalled: boolean, toolName?: string, toolInput?: any, toolOutput?: any}> {
91
- const match = result.content.match(/```json\s*([\s\S]*?)\s*```/);
92
-
93
- if (!match || !match[1]) return {toolCalled: false};
58
+ if (!result.tool_calls || result.tool_calls.length === 0) {
59
+ // console.log('No tool calls found');
60
+ return {toolCalled: false};
61
+ }
94
62
 
95
63
  try {
96
- const jsonContent = JSON.parse(match[1]);
97
- const { tool_name, tool_input } = jsonContent;
98
- // console.log('Calling tool:', tool_name, tool_input);
99
- const toolOutput = await tools.execute(tool_name, tool_input) || true;
64
+ const toolCall = result.tool_calls[0];
65
+ if (!toolCall) {
66
+ // console.log('No tool call found');
67
+ return {toolCalled: false};
68
+ }
69
+ const toolName = toolCall.function.name;
70
+ const toolInput = JSON.parse(toolCall.function.arguments);
71
+
72
+ // console.log('Calling tool:', toolName, toolInput);
73
+ const toolOutput = await tools.execute(toolName, toolInput);
100
74
  // console.log('Tool called result:', toolOutput);
101
75
 
102
76
  return {
103
77
  toolCalled: true,
104
- toolName: tool_name,
105
- toolInput: tool_input,
78
+ toolName,
79
+ toolInput,
106
80
  toolOutput
107
81
  };
108
82
  } catch (error) {
109
- // console.error('Error parsing JSON:', match, error);
83
+ // console.error('Error parsing tool call:', error);
110
84
  return {toolCalled: false};
111
85
  }
112
- }
113
-
114
- export function updateMessagesWithToolCall(messages: CactusOAICompatibleMessage[], toolName: string, toolInput: any, toolOutput: any): CactusOAICompatibleMessage[] {
115
- const newMessages = [...messages];
116
-
117
- newMessages.push({
118
- role: 'function-call',
119
- content: JSON.stringify({name: toolName, arguments: toolInput}, null, 2)
120
- })
121
- newMessages.push({
122
- role: 'function-response',
123
- content: JSON.stringify(toolOutput, null, 2)
124
- })
125
-
126
- return newMessages;
127
86
  }
package/src/vlm.ts CHANGED
@@ -10,6 +10,12 @@ import type {
10
10
  CactusOAICompatibleMessage,
11
11
  NativeCompletionResult,
12
12
  } from './index'
13
+ import { Telemetry } from './telemetry'
14
+
15
+ interface CactusVLMReturn {
16
+ vlm: CactusVLM | null
17
+ error: Error | null
18
+ }
13
19
 
14
20
  export type VLMContextParams = ContextParams & {
15
21
  mmproj: string
@@ -21,21 +27,37 @@ export type VLMCompletionParams = Omit<CompletionParams, 'prompt'> & {
21
27
 
22
28
  export class CactusVLM {
23
29
  private context: LlamaContext
30
+ private initParams: VLMContextParams
24
31
 
25
- private constructor(context: LlamaContext) {
32
+ private constructor(context: LlamaContext, initParams: VLMContextParams) {
26
33
  this.context = context
34
+ this.initParams = initParams
27
35
  }
28
36
 
29
37
  static async init(
30
38
  params: VLMContextParams,
31
39
  onProgress?: (progress: number) => void,
32
- ): Promise<CactusVLM> {
33
- const context = await initLlama(params, onProgress)
40
+ ): Promise<CactusVLMReturn> {
41
+ const configs = [
42
+ params,
43
+ { ...params, n_gpu_layers: 0 }
44
+ ];
34
45
 
35
- // Explicitly disable GPU for the multimodal projector for stability.
36
- await initMultimodal(context.id, params.mmproj, false)
46
+ for (const config of configs) {
47
+ try {
48
+ const context = await initLlama(config, onProgress)
49
+ // Explicitly disable GPU for the multimodal projector for stability.
50
+ await initMultimodal(context.id, params.mmproj, false)
51
+ return {vlm: new CactusVLM(context, params), error: null}
52
+ } catch (e) {
53
+ Telemetry.error(e as Error, config);
54
+ if (configs.indexOf(config) === configs.length - 1) {
55
+ return {vlm: null, error: e as Error}
56
+ }
57
+ }
58
+ }
37
59
 
38
- return new CactusVLM(context)
60
+ return {vlm: null, error: new Error('Failed to initialize CactusVLM')}
39
61
  }
40
62
 
41
63
  async completion(
@@ -43,20 +65,40 @@ export class CactusVLM {
43
65
  params: VLMCompletionParams = {},
44
66
  callback?: (data: any) => void,
45
67
  ): Promise<NativeCompletionResult> {
68
+ const startTime = Date.now();
69
+ let firstTokenTime: number | null = null;
70
+
71
+ const wrappedCallback = callback ? (data: any) => {
72
+ if (firstTokenTime === null) firstTokenTime = Date.now();
73
+ callback(data);
74
+ } : undefined;
75
+
76
+ let result: NativeCompletionResult;
46
77
  if (params.images && params.images.length > 0) {
47
78
  const formattedPrompt = await this.context.getFormattedChat(messages)
48
79
  const prompt =
49
80
  typeof formattedPrompt === 'string'
50
81
  ? formattedPrompt
51
82
  : formattedPrompt.prompt
52
- return multimodalCompletion(
83
+ result = await multimodalCompletion(
53
84
  this.context.id,
54
85
  prompt,
55
86
  params.images,
56
87
  { ...params, prompt, emit_partial_completion: !!callback },
57
88
  )
89
+ } else {
90
+ result = await this.context.completion({ messages, ...params }, wrappedCallback)
58
91
  }
59
- return this.context.completion({ messages, ...params }, callback)
92
+
93
+ Telemetry.track({
94
+ event: 'completion',
95
+ tok_per_sec: (result as any).timings?.predicted_per_second,
96
+ toks_generated: (result as any).timings?.predicted_n,
97
+ ttft: firstTokenTime ? firstTokenTime - startTime : null,
98
+ num_images: params.images?.length,
99
+ }, this.initParams);
100
+
101
+ return result;
60
102
  }
61
103
 
62
104
  async rewind(): Promise<void> {