@emilshirokikh/slyos-sdk 1.3.3 → 1.4.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.
- package/README.md +78 -14
- package/dist/index.d.ts +23 -1
- package/dist/index.js +359 -18
- package/package.json +1 -1
- package/src/index.ts +363 -18
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ const response = await sdk.generate('quantum-1.7b',
|
|
|
37
37
|
);
|
|
38
38
|
|
|
39
39
|
console.log(response);
|
|
40
|
-
// AI runs locally -
|
|
40
|
+
// AI runs locally - no per-inference charges!
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
---
|
|
@@ -67,7 +67,7 @@ Authenticates with SlyOS backend and registers device.
|
|
|
67
67
|
await sdk.initialize();
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
**Returns:** `Promise<
|
|
70
|
+
**Returns:** `Promise<DeviceProfile>`
|
|
71
71
|
|
|
72
72
|
---
|
|
73
73
|
|
|
@@ -116,6 +116,70 @@ const response = await sdk.generate('quantum-1.7b',
|
|
|
116
116
|
|
|
117
117
|
---
|
|
118
118
|
|
|
119
|
+
#### `chatCompletion(modelId, request)`
|
|
120
|
+
OpenAI-compatible chat completions.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
#### `transcribe(modelId, audio, options?)`
|
|
125
|
+
Speech-to-text using voicecore models.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
#### `recommendModel(category?)`
|
|
130
|
+
Returns best model for the current device's hardware.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
#### `searchModels(query, options?)`
|
|
135
|
+
Search HuggingFace Hub for ONNX-compatible models.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
#### `getDeviceProfile()`
|
|
140
|
+
Returns the device's hardware profile (CPU, RAM, GPU, screen, network).
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
#### `getModelContextWindow()`
|
|
145
|
+
Returns current model's context window size in tokens.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
#### `getDeviceId()`
|
|
150
|
+
Returns the persistent device identifier.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
#### `destroy()`
|
|
155
|
+
Flushes pending telemetry and cleans up timers. Call before shutting down.
|
|
156
|
+
```javascript
|
|
157
|
+
await sdk.destroy(); // Ensures telemetry is sent
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
#### `getSdkVersion()`
|
|
161
|
+
Returns the current SDK version string (e.g. `'1.4.0'`).
|
|
162
|
+
|
|
163
|
+
#### `getAvailableModels()`
|
|
164
|
+
Returns available models grouped by category (`llm`, `stt`).
|
|
165
|
+
|
|
166
|
+
#### `canRunModel(modelId, quant?)`
|
|
167
|
+
Checks if the current device can run a specific model based on hardware profile.
|
|
168
|
+
|
|
169
|
+
#### `ragQuery(modelId, knowledgeBaseId, query, options?)`
|
|
170
|
+
Performs a RAG query against a cloud-indexed knowledge base. Requires Hybrid RAG plan.
|
|
171
|
+
|
|
172
|
+
#### `ragQueryLocal(modelId, knowledgeBaseId, query, options?)`
|
|
173
|
+
Performs a RAG query using locally-cached embeddings for offline-capable retrieval.
|
|
174
|
+
|
|
175
|
+
#### `ragQueryOffline(modelId, knowledgeBaseId, query, options?)`
|
|
176
|
+
Fully offline RAG query using pre-synced knowledge base data.
|
|
177
|
+
|
|
178
|
+
#### `syncKnowledgeBase(knowledgeBaseId)`
|
|
179
|
+
Downloads and caches a knowledge base locally for offline RAG queries.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
119
183
|
## 🌐 Platform Support
|
|
120
184
|
|
|
121
185
|
| Platform | Status | Notes |
|
|
@@ -125,7 +189,7 @@ const response = await sdk.generate('quantum-1.7b',
|
|
|
125
189
|
| **Edge** | ✅ Supported | Chromium-based |
|
|
126
190
|
| **Firefox** | ⚠️ Limited | Some models work |
|
|
127
191
|
| **Node.js** | ✅ Supported | v18+ |
|
|
128
|
-
| **React Native** | 🚧 Coming Soon |
|
|
192
|
+
| **React Native** | 🚧 Coming Soon | Q3 2026 |
|
|
129
193
|
|
|
130
194
|
---
|
|
131
195
|
|
|
@@ -221,25 +285,25 @@ const sdk = new SlyOS({
|
|
|
221
285
|
### Multiple Models
|
|
222
286
|
```javascript
|
|
223
287
|
await sdk.loadModel('quantum-1.7b');
|
|
224
|
-
await sdk.loadModel('quantum-
|
|
288
|
+
await sdk.loadModel('quantum-3b');
|
|
225
289
|
|
|
226
290
|
// Use different models
|
|
227
291
|
const fast = await sdk.generate('quantum-1.7b', 'Quick question?');
|
|
228
|
-
const detailed = await sdk.generate('quantum-
|
|
292
|
+
const detailed = await sdk.generate('quantum-3b', 'Complex question?');
|
|
229
293
|
```
|
|
230
294
|
|
|
231
295
|
---
|
|
232
296
|
|
|
233
297
|
## 📊 Performance
|
|
234
298
|
|
|
235
|
-
### Benchmarks (Quantum
|
|
299
|
+
### Benchmarks (Quantum 1.7B)
|
|
236
300
|
|
|
237
301
|
| Metric | Browser | Node.js |
|
|
238
302
|
|--------|---------|---------|
|
|
239
303
|
| First load | 60-120s | 30-60s |
|
|
240
304
|
| Cached load | <1s | <0.5s |
|
|
241
|
-
| Inference |
|
|
242
|
-
| Memory |
|
|
305
|
+
| Inference | 10-15 tok/s | 15-25 tok/s |
|
|
306
|
+
| Memory | 1.2GB | 900MB |
|
|
243
307
|
|
|
244
308
|
---
|
|
245
309
|
|
|
@@ -271,7 +335,7 @@ const detailed = await sdk.generate('quantum-1.7b', 'Complex question?');
|
|
|
271
335
|
|
|
272
336
|
- API keys stored client-side (localStorage)
|
|
273
337
|
- All inference happens locally (private)
|
|
274
|
-
-
|
|
338
|
+
- Inference telemetry batched locally (flushed every 10 inferences or 60s)
|
|
275
339
|
- No user data sent to cloud
|
|
276
340
|
|
|
277
341
|
---
|
|
@@ -279,9 +343,9 @@ const detailed = await sdk.generate('quantum-1.7b', 'Complex question?');
|
|
|
279
343
|
## 📦 Package Info
|
|
280
344
|
|
|
281
345
|
- **Package:** `@emilshirokikh/slyos-sdk`
|
|
282
|
-
- **Version:** 1.
|
|
346
|
+
- **Version:** 1.4.0
|
|
283
347
|
- **License:** MIT
|
|
284
|
-
- **Size:**
|
|
348
|
+
- **Size:** 168 KB (unpacked)
|
|
285
349
|
- **Dependencies:** axios, @huggingface/transformers
|
|
286
350
|
|
|
287
351
|
---
|
|
@@ -289,8 +353,8 @@ const detailed = await sdk.generate('quantum-1.7b', 'Complex question?');
|
|
|
289
353
|
## 🤝 Contributing
|
|
290
354
|
```bash
|
|
291
355
|
# Clone repo
|
|
292
|
-
git clone https://github.com/BeltoAI/sly.git
|
|
293
|
-
cd sly/sdk
|
|
356
|
+
git clone https://github.com/BeltoAI/sly.os.git
|
|
357
|
+
cd sly.os/sdk
|
|
294
358
|
|
|
295
359
|
# Install dependencies
|
|
296
360
|
npm install
|
|
@@ -321,6 +385,6 @@ Built with Hugging Face Transformers.js
|
|
|
321
385
|
## 📞 Support
|
|
322
386
|
|
|
323
387
|
- **npm:** https://www.npmjs.com/package/@emilshirokikh/slyos-sdk
|
|
324
|
-
- **GitHub:** https://github.com/BeltoAI/sly
|
|
388
|
+
- **GitHub:** https://github.com/BeltoAI/sly.os
|
|
325
389
|
- **Docs:** See main README.md
|
|
326
390
|
- **Email:** support@slyos.world
|
package/dist/index.d.ts
CHANGED
|
@@ -23,6 +23,19 @@ interface DeviceProfile {
|
|
|
23
23
|
os: string;
|
|
24
24
|
recommendedQuant: QuantizationLevel;
|
|
25
25
|
maxContextWindow: number;
|
|
26
|
+
deviceFingerprint?: string;
|
|
27
|
+
gpuRenderer?: string;
|
|
28
|
+
gpuVramMb?: number;
|
|
29
|
+
screenWidth?: number;
|
|
30
|
+
screenHeight?: number;
|
|
31
|
+
pixelRatio?: number;
|
|
32
|
+
browserName?: string;
|
|
33
|
+
browserVersion?: string;
|
|
34
|
+
networkType?: string;
|
|
35
|
+
latencyToApiMs?: number;
|
|
36
|
+
timezone?: string;
|
|
37
|
+
wasmAvailable?: boolean;
|
|
38
|
+
webgpuAvailable?: boolean;
|
|
26
39
|
}
|
|
27
40
|
interface ProgressEvent {
|
|
28
41
|
stage: 'initializing' | 'profiling' | 'downloading' | 'loading' | 'ready' | 'generating' | 'transcribing' | 'error';
|
|
@@ -31,7 +44,7 @@ interface ProgressEvent {
|
|
|
31
44
|
detail?: any;
|
|
32
45
|
}
|
|
33
46
|
interface SlyEvent {
|
|
34
|
-
type: 'auth' | 'device_registered' | 'device_profiled' | 'model_download_start' | 'model_download_progress' | 'model_loaded' | 'inference_start' | 'inference_complete' | 'error' | 'fallback_success' | 'fallback_error';
|
|
47
|
+
type: 'auth' | 'device_registered' | 'device_profiled' | 'model_download_start' | 'model_download_progress' | 'model_loaded' | 'inference_start' | 'inference_complete' | 'error' | 'fallback_success' | 'fallback_error' | 'telemetry_flushed';
|
|
35
48
|
data?: any;
|
|
36
49
|
timestamp: number;
|
|
37
50
|
}
|
|
@@ -162,12 +175,21 @@ declare class SlyOS {
|
|
|
162
175
|
private onEvent;
|
|
163
176
|
private fallbackConfig;
|
|
164
177
|
private modelContextWindow;
|
|
178
|
+
private telemetryBuffer;
|
|
179
|
+
private telemetryFlushTimer;
|
|
180
|
+
private static readonly TELEMETRY_BATCH_SIZE;
|
|
181
|
+
private static readonly TELEMETRY_FLUSH_INTERVAL;
|
|
165
182
|
constructor(config: SlyOSConfigWithFallback);
|
|
166
183
|
private emitProgress;
|
|
167
184
|
private emitEvent;
|
|
185
|
+
private recordTelemetry;
|
|
186
|
+
private flushTelemetry;
|
|
168
187
|
analyzeDevice(): Promise<DeviceProfile>;
|
|
169
188
|
getDeviceProfile(): DeviceProfile | null;
|
|
170
189
|
getModelContextWindow(): number;
|
|
190
|
+
getDeviceId(): string;
|
|
191
|
+
getSdkVersion(): string;
|
|
192
|
+
destroy(): Promise<void>;
|
|
171
193
|
recommendModel(category?: ModelCategory): {
|
|
172
194
|
modelId: string;
|
|
173
195
|
quant: QuantizationLevel;
|
package/dist/index.js
CHANGED
|
@@ -100,6 +100,186 @@ async function detectContextWindowFromHF(hfModelId) {
|
|
|
100
100
|
return 2048;
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
|
+
// ─── SDK Version ────────────────────────────────────────────────────
|
|
104
|
+
const SDK_VERSION = '1.4.1';
|
|
105
|
+
// ─── Persistent Device Identity ─────────────────────────────────────
|
|
106
|
+
async function hashString(str) {
|
|
107
|
+
const isNode = typeof window === 'undefined';
|
|
108
|
+
if (isNode) {
|
|
109
|
+
const crypto = await import('crypto');
|
|
110
|
+
return crypto.createHash('sha256').update(str).digest('hex').substring(0, 32);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const encoder = new TextEncoder();
|
|
114
|
+
const data = encoder.encode(str);
|
|
115
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
116
|
+
return Array.from(new Uint8Array(hashBuffer))
|
|
117
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
118
|
+
.join('')
|
|
119
|
+
.substring(0, 32);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function getOrCreateDeviceId() {
|
|
123
|
+
const isNode = typeof window === 'undefined';
|
|
124
|
+
if (isNode) {
|
|
125
|
+
// Node.js: persist in ~/.slyos/device-id
|
|
126
|
+
try {
|
|
127
|
+
const fs = await import('fs');
|
|
128
|
+
const path = await import('path');
|
|
129
|
+
const os = await import('os');
|
|
130
|
+
const slyosDir = path.join(os.homedir(), '.slyos');
|
|
131
|
+
const idFile = path.join(slyosDir, 'device-id');
|
|
132
|
+
try {
|
|
133
|
+
const existing = fs.readFileSync(idFile, 'utf-8').trim();
|
|
134
|
+
if (existing)
|
|
135
|
+
return existing;
|
|
136
|
+
}
|
|
137
|
+
catch { }
|
|
138
|
+
const deviceId = `device-${Date.now()}-${Math.random().toString(36).substr(2, 12)}`;
|
|
139
|
+
fs.mkdirSync(slyosDir, { recursive: true });
|
|
140
|
+
fs.writeFileSync(idFile, deviceId);
|
|
141
|
+
return deviceId;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return `device-${Date.now()}-${Math.random().toString(36).substr(2, 12)}`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
// Browser: persist in localStorage
|
|
149
|
+
const key = 'slyos_device_id';
|
|
150
|
+
try {
|
|
151
|
+
const existing = localStorage.getItem(key);
|
|
152
|
+
if (existing)
|
|
153
|
+
return existing;
|
|
154
|
+
}
|
|
155
|
+
catch { }
|
|
156
|
+
const deviceId = `device-${Date.now()}-${Math.random().toString(36).substr(2, 12)}`;
|
|
157
|
+
try {
|
|
158
|
+
localStorage.setItem(key, deviceId);
|
|
159
|
+
}
|
|
160
|
+
catch { }
|
|
161
|
+
return deviceId;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function generateDeviceFingerprint() {
|
|
165
|
+
const isNode = typeof window === 'undefined';
|
|
166
|
+
let components = [];
|
|
167
|
+
if (isNode) {
|
|
168
|
+
try {
|
|
169
|
+
const os = await import('os');
|
|
170
|
+
const cpus = os.cpus();
|
|
171
|
+
components.push(cpus[0]?.model || 'unknown-cpu');
|
|
172
|
+
components.push(String(os.totalmem()));
|
|
173
|
+
components.push(os.platform());
|
|
174
|
+
components.push(os.arch());
|
|
175
|
+
components.push(String(cpus.length));
|
|
176
|
+
}
|
|
177
|
+
catch { }
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
components.push(String(navigator.hardwareConcurrency || 0));
|
|
181
|
+
components.push(String(navigator.deviceMemory || 0));
|
|
182
|
+
components.push(navigator.platform || 'unknown');
|
|
183
|
+
// WebGL renderer for GPU fingerprint
|
|
184
|
+
try {
|
|
185
|
+
const canvas = document.createElement('canvas');
|
|
186
|
+
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
|
187
|
+
if (gl) {
|
|
188
|
+
const ext = gl.getExtension('WEBGL_debug_renderer_info');
|
|
189
|
+
if (ext) {
|
|
190
|
+
components.push(gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) || 'unknown-gpu');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch { }
|
|
195
|
+
components.push(String(screen.width || 0));
|
|
196
|
+
components.push(String(screen.height || 0));
|
|
197
|
+
}
|
|
198
|
+
return await hashString(components.join('|'));
|
|
199
|
+
}
|
|
200
|
+
// ─── Enhanced Device Profiling ──────────────────────────────────────
|
|
201
|
+
function detectGPU() {
|
|
202
|
+
if (typeof window === 'undefined')
|
|
203
|
+
return { renderer: null, vramMb: 0 };
|
|
204
|
+
try {
|
|
205
|
+
const canvas = document.createElement('canvas');
|
|
206
|
+
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
|
207
|
+
if (!gl)
|
|
208
|
+
return { renderer: null, vramMb: 0 };
|
|
209
|
+
const ext = gl.getExtension('WEBGL_debug_renderer_info');
|
|
210
|
+
const renderer = ext ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) : null;
|
|
211
|
+
// Rough VRAM estimate from renderer string
|
|
212
|
+
let vramMb = 0;
|
|
213
|
+
if (renderer) {
|
|
214
|
+
const match = renderer.match(/(\d+)\s*MB/i);
|
|
215
|
+
if (match)
|
|
216
|
+
vramMb = parseInt(match[1]);
|
|
217
|
+
else if (/RTX\s*40/i.test(renderer))
|
|
218
|
+
vramMb = 8192;
|
|
219
|
+
else if (/RTX\s*30/i.test(renderer))
|
|
220
|
+
vramMb = 6144;
|
|
221
|
+
else if (/GTX/i.test(renderer))
|
|
222
|
+
vramMb = 4096;
|
|
223
|
+
else if (/Apple M[2-4]/i.test(renderer))
|
|
224
|
+
vramMb = 8192;
|
|
225
|
+
else if (/Apple M1/i.test(renderer))
|
|
226
|
+
vramMb = 4096;
|
|
227
|
+
else if (/Intel/i.test(renderer))
|
|
228
|
+
vramMb = 1024;
|
|
229
|
+
}
|
|
230
|
+
return { renderer, vramMb };
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return { renderer: null, vramMb: 0 };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function detectBrowser() {
|
|
237
|
+
if (typeof window === 'undefined' || typeof navigator === 'undefined')
|
|
238
|
+
return { name: 'node', version: process.version || 'unknown' };
|
|
239
|
+
const ua = navigator.userAgent;
|
|
240
|
+
if (/Edg\//i.test(ua)) {
|
|
241
|
+
const m = ua.match(/Edg\/([\d.]+)/);
|
|
242
|
+
return { name: 'Edge', version: m?.[1] || '' };
|
|
243
|
+
}
|
|
244
|
+
if (/Chrome\//i.test(ua)) {
|
|
245
|
+
const m = ua.match(/Chrome\/([\d.]+)/);
|
|
246
|
+
return { name: 'Chrome', version: m?.[1] || '' };
|
|
247
|
+
}
|
|
248
|
+
if (/Firefox\//i.test(ua)) {
|
|
249
|
+
const m = ua.match(/Firefox\/([\d.]+)/);
|
|
250
|
+
return { name: 'Firefox', version: m?.[1] || '' };
|
|
251
|
+
}
|
|
252
|
+
if (/Safari\//i.test(ua)) {
|
|
253
|
+
const m = ua.match(/Version\/([\d.]+)/);
|
|
254
|
+
return { name: 'Safari', version: m?.[1] || '' };
|
|
255
|
+
}
|
|
256
|
+
return { name: 'unknown', version: '' };
|
|
257
|
+
}
|
|
258
|
+
function detectNetworkType() {
|
|
259
|
+
if (typeof navigator === 'undefined')
|
|
260
|
+
return 'unknown';
|
|
261
|
+
const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
|
262
|
+
if (!conn)
|
|
263
|
+
return 'unknown';
|
|
264
|
+
return conn.effectiveType || conn.type || 'unknown';
|
|
265
|
+
}
|
|
266
|
+
async function measureApiLatency(apiUrl) {
|
|
267
|
+
try {
|
|
268
|
+
const start = Date.now();
|
|
269
|
+
await axios.head(`${apiUrl}/api/health`, { timeout: 5000 });
|
|
270
|
+
return Date.now() - start;
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
try {
|
|
274
|
+
const start = Date.now();
|
|
275
|
+
await axios.get(`${apiUrl}/api/health`, { timeout: 5000 });
|
|
276
|
+
return Date.now() - start;
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return -1;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
103
283
|
// ─── Device Profiling ───────────────────────────────────────────────
|
|
104
284
|
async function profileDevice() {
|
|
105
285
|
const isNode = typeof window === 'undefined';
|
|
@@ -151,6 +331,29 @@ async function profileDevice() {
|
|
|
151
331
|
}
|
|
152
332
|
const recommendedQuant = selectQuantization(memoryMB, 'quantum-1.7b'); // default baseline
|
|
153
333
|
const maxContextWindow = recommendContextWindow(memoryMB, recommendedQuant);
|
|
334
|
+
// Enhanced profiling
|
|
335
|
+
const gpu = detectGPU();
|
|
336
|
+
const browser = detectBrowser();
|
|
337
|
+
const networkType = detectNetworkType();
|
|
338
|
+
const timezone = Intl?.DateTimeFormat?.()?.resolvedOptions?.()?.timeZone || 'unknown';
|
|
339
|
+
let screenWidth = 0, screenHeight = 0, pixelRatio = 0;
|
|
340
|
+
let wasmAvailable = false, webgpuAvailable = false;
|
|
341
|
+
if (!isNode) {
|
|
342
|
+
screenWidth = screen?.width || 0;
|
|
343
|
+
screenHeight = screen?.height || 0;
|
|
344
|
+
pixelRatio = window?.devicePixelRatio || 1;
|
|
345
|
+
}
|
|
346
|
+
// Capability detection
|
|
347
|
+
try {
|
|
348
|
+
wasmAvailable = typeof WebAssembly !== 'undefined';
|
|
349
|
+
}
|
|
350
|
+
catch { }
|
|
351
|
+
if (!isNode) {
|
|
352
|
+
try {
|
|
353
|
+
webgpuAvailable = !!navigator.gpu;
|
|
354
|
+
}
|
|
355
|
+
catch { }
|
|
356
|
+
}
|
|
154
357
|
return {
|
|
155
358
|
cpuCores,
|
|
156
359
|
memoryMB,
|
|
@@ -159,15 +362,28 @@ async function profileDevice() {
|
|
|
159
362
|
os,
|
|
160
363
|
recommendedQuant,
|
|
161
364
|
maxContextWindow,
|
|
365
|
+
gpuRenderer: gpu.renderer || undefined,
|
|
366
|
+
gpuVramMb: gpu.vramMb || undefined,
|
|
367
|
+
screenWidth: screenWidth || undefined,
|
|
368
|
+
screenHeight: screenHeight || undefined,
|
|
369
|
+
pixelRatio: pixelRatio || undefined,
|
|
370
|
+
browserName: browser.name,
|
|
371
|
+
browserVersion: browser.version,
|
|
372
|
+
networkType,
|
|
373
|
+
timezone,
|
|
374
|
+
wasmAvailable,
|
|
375
|
+
webgpuAvailable,
|
|
162
376
|
};
|
|
163
377
|
}
|
|
164
|
-
// ─── Main SDK Class ─────────────────────────────────────────────────
|
|
165
378
|
class SlyOS {
|
|
166
379
|
constructor(config) {
|
|
167
380
|
this.token = null;
|
|
168
381
|
this.models = new Map();
|
|
169
382
|
this.deviceProfile = null;
|
|
170
383
|
this.modelContextWindow = 0;
|
|
384
|
+
// Telemetry batching
|
|
385
|
+
this.telemetryBuffer = [];
|
|
386
|
+
this.telemetryFlushTimer = null;
|
|
171
387
|
// ═══════════════════════════════════════════════════════════
|
|
172
388
|
// RAG — Retrieval Augmented Generation
|
|
173
389
|
// ═══════════════════════════════════════════════════════════
|
|
@@ -175,7 +391,7 @@ class SlyOS {
|
|
|
175
391
|
this.offlineIndexes = new Map();
|
|
176
392
|
this.apiKey = config.apiKey;
|
|
177
393
|
this.apiUrl = config.apiUrl || 'https://api.slyos.world';
|
|
178
|
-
this.deviceId =
|
|
394
|
+
this.deviceId = ''; // Set asynchronously in initialize()
|
|
179
395
|
this.onProgress = config.onProgress || null;
|
|
180
396
|
this.onEvent = config.onEvent || null;
|
|
181
397
|
this.fallbackConfig = config.fallback || null;
|
|
@@ -191,13 +407,57 @@ class SlyOS {
|
|
|
191
407
|
this.onEvent({ type, data, timestamp: Date.now() });
|
|
192
408
|
}
|
|
193
409
|
}
|
|
410
|
+
// ── Telemetry Batching ─────────────────────────────────────────
|
|
411
|
+
recordTelemetry(entry) {
|
|
412
|
+
this.telemetryBuffer.push(entry);
|
|
413
|
+
if (this.telemetryBuffer.length >= SlyOS.TELEMETRY_BATCH_SIZE) {
|
|
414
|
+
this.flushTelemetry();
|
|
415
|
+
}
|
|
416
|
+
else if (!this.telemetryFlushTimer) {
|
|
417
|
+
this.telemetryFlushTimer = setTimeout(() => this.flushTelemetry(), SlyOS.TELEMETRY_FLUSH_INTERVAL);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async flushTelemetry() {
|
|
421
|
+
if (this.telemetryFlushTimer) {
|
|
422
|
+
clearTimeout(this.telemetryFlushTimer);
|
|
423
|
+
this.telemetryFlushTimer = null;
|
|
424
|
+
}
|
|
425
|
+
if (this.telemetryBuffer.length === 0 || !this.token)
|
|
426
|
+
return;
|
|
427
|
+
const batch = [...this.telemetryBuffer];
|
|
428
|
+
this.telemetryBuffer = [];
|
|
429
|
+
try {
|
|
430
|
+
await axios.post(`${this.apiUrl}/api/devices/telemetry`, {
|
|
431
|
+
device_id: this.deviceId,
|
|
432
|
+
metrics: batch,
|
|
433
|
+
}, {
|
|
434
|
+
headers: { Authorization: `Bearer ${this.token}` },
|
|
435
|
+
timeout: 10000,
|
|
436
|
+
});
|
|
437
|
+
this.emitEvent('telemetry_flushed', { count: batch.length });
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
// Put back on failure for next attempt
|
|
441
|
+
this.telemetryBuffer.unshift(...batch);
|
|
442
|
+
// Cap buffer to prevent memory leak
|
|
443
|
+
if (this.telemetryBuffer.length > 100) {
|
|
444
|
+
this.telemetryBuffer = this.telemetryBuffer.slice(-100);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
194
448
|
// ── Device Analysis ─────────────────────────────────────────────
|
|
195
449
|
async analyzeDevice() {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
450
|
+
try {
|
|
451
|
+
this.emitProgress('profiling', 10, 'Analyzing device capabilities...');
|
|
452
|
+
this.deviceProfile = await profileDevice();
|
|
453
|
+
this.emitProgress('profiling', 100, `Device: ${this.deviceProfile.cpuCores} cores, ${Math.round(this.deviceProfile.memoryMB / 1024 * 10) / 10}GB RAM`);
|
|
454
|
+
this.emitEvent('device_profiled', this.deviceProfile);
|
|
455
|
+
return this.deviceProfile;
|
|
456
|
+
}
|
|
457
|
+
catch (err) {
|
|
458
|
+
this.emitEvent('error', { method: 'analyzeDevice', error: err.message });
|
|
459
|
+
throw new Error(`Device analysis failed: ${err.message}`);
|
|
460
|
+
}
|
|
201
461
|
}
|
|
202
462
|
getDeviceProfile() {
|
|
203
463
|
return this.deviceProfile;
|
|
@@ -205,6 +465,20 @@ class SlyOS {
|
|
|
205
465
|
getModelContextWindow() {
|
|
206
466
|
return this.modelContextWindow;
|
|
207
467
|
}
|
|
468
|
+
getDeviceId() {
|
|
469
|
+
return this.deviceId;
|
|
470
|
+
}
|
|
471
|
+
getSdkVersion() {
|
|
472
|
+
return SDK_VERSION;
|
|
473
|
+
}
|
|
474
|
+
// Flush remaining telemetry and clean up timers
|
|
475
|
+
async destroy() {
|
|
476
|
+
await this.flushTelemetry();
|
|
477
|
+
if (this.telemetryFlushTimer) {
|
|
478
|
+
clearTimeout(this.telemetryFlushTimer);
|
|
479
|
+
this.telemetryFlushTimer = null;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
208
482
|
// ── Smart Model Recommendation ──────────────────────────────────
|
|
209
483
|
recommendModel(category = 'llm') {
|
|
210
484
|
if (!this.deviceProfile) {
|
|
@@ -240,12 +514,16 @@ class SlyOS {
|
|
|
240
514
|
// ── Initialize ──────────────────────────────────────────────────
|
|
241
515
|
async initialize() {
|
|
242
516
|
this.emitProgress('initializing', 0, 'Starting SlyOS...');
|
|
243
|
-
// Step 1:
|
|
517
|
+
// Step 1: Persistent device ID
|
|
518
|
+
this.deviceId = await getOrCreateDeviceId();
|
|
519
|
+
// Step 2: Profile device (enhanced)
|
|
244
520
|
this.emitProgress('profiling', 5, 'Detecting device capabilities...');
|
|
245
521
|
this.deviceProfile = await profileDevice();
|
|
246
|
-
|
|
522
|
+
// Step 2b: Generate device fingerprint
|
|
523
|
+
this.deviceProfile.deviceFingerprint = await generateDeviceFingerprint();
|
|
524
|
+
this.emitProgress('profiling', 20, `Detected: ${this.deviceProfile.cpuCores} CPU cores, ${Math.round(this.deviceProfile.memoryMB / 1024 * 10) / 10}GB RAM${this.deviceProfile.gpuRenderer ? ', GPU: ' + this.deviceProfile.gpuRenderer.substring(0, 30) : ''}`);
|
|
247
525
|
this.emitEvent('device_profiled', this.deviceProfile);
|
|
248
|
-
// Step
|
|
526
|
+
// Step 3: Authenticate
|
|
249
527
|
this.emitProgress('initializing', 40, 'Authenticating with API key...');
|
|
250
528
|
try {
|
|
251
529
|
const authRes = await axios.post(`${this.apiUrl}/api/auth/sdk`, {
|
|
@@ -260,29 +538,65 @@ class SlyOS {
|
|
|
260
538
|
this.emitEvent('error', { stage: 'auth', error: err.message });
|
|
261
539
|
throw new Error(`SlyOS auth failed: ${err.response?.data?.error || err.message}`);
|
|
262
540
|
}
|
|
263
|
-
// Step
|
|
541
|
+
// Step 4: Measure API latency
|
|
542
|
+
const latency = await measureApiLatency(this.apiUrl);
|
|
543
|
+
if (latency > 0)
|
|
544
|
+
this.deviceProfile.latencyToApiMs = latency;
|
|
545
|
+
// Step 5: Register device with full intelligence profile
|
|
264
546
|
this.emitProgress('initializing', 70, 'Registering device...');
|
|
265
547
|
try {
|
|
548
|
+
// Determine supported quantizations based on memory
|
|
549
|
+
const mem = this.deviceProfile.memoryMB;
|
|
550
|
+
const supportedQuants = ['q4'];
|
|
551
|
+
if (mem >= 4096)
|
|
552
|
+
supportedQuants.push('q8');
|
|
553
|
+
if (mem >= 8192)
|
|
554
|
+
supportedQuants.push('fp16');
|
|
555
|
+
if (mem >= 16384)
|
|
556
|
+
supportedQuants.push('fp32');
|
|
557
|
+
// Determine recommended tier
|
|
558
|
+
let recommendedTier = 1;
|
|
559
|
+
if (mem >= 8192 && this.deviceProfile.cpuCores >= 4)
|
|
560
|
+
recommendedTier = 2;
|
|
561
|
+
if (mem >= 16384 && this.deviceProfile.cpuCores >= 8)
|
|
562
|
+
recommendedTier = 3;
|
|
266
563
|
await axios.post(`${this.apiUrl}/api/devices/register`, {
|
|
267
564
|
device_id: this.deviceId,
|
|
565
|
+
device_fingerprint: this.deviceProfile.deviceFingerprint,
|
|
268
566
|
platform: this.deviceProfile.platform,
|
|
269
567
|
os_version: this.deviceProfile.os,
|
|
270
568
|
total_memory_mb: this.deviceProfile.memoryMB,
|
|
271
569
|
cpu_cores: this.deviceProfile.cpuCores,
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
570
|
+
// Enhanced fields
|
|
571
|
+
gpu_renderer: this.deviceProfile.gpuRenderer || null,
|
|
572
|
+
gpu_vram_mb: this.deviceProfile.gpuVramMb || null,
|
|
573
|
+
screen_width: this.deviceProfile.screenWidth || null,
|
|
574
|
+
screen_height: this.deviceProfile.screenHeight || null,
|
|
575
|
+
pixel_ratio: this.deviceProfile.pixelRatio || null,
|
|
576
|
+
browser_name: this.deviceProfile.browserName || null,
|
|
577
|
+
browser_version: this.deviceProfile.browserVersion || null,
|
|
578
|
+
sdk_version: SDK_VERSION,
|
|
579
|
+
network_type: this.deviceProfile.networkType || null,
|
|
580
|
+
latency_to_api_ms: this.deviceProfile.latencyToApiMs || null,
|
|
581
|
+
timezone: this.deviceProfile.timezone || null,
|
|
582
|
+
// Capabilities
|
|
583
|
+
wasm_available: this.deviceProfile.wasmAvailable || false,
|
|
584
|
+
webgpu_available: this.deviceProfile.webgpuAvailable || false,
|
|
585
|
+
supported_quants: supportedQuants,
|
|
586
|
+
recommended_tier: recommendedTier,
|
|
275
587
|
}, {
|
|
276
588
|
headers: { Authorization: `Bearer ${this.token}` },
|
|
277
589
|
});
|
|
278
590
|
this.emitProgress('initializing', 90, 'Device registered');
|
|
279
|
-
this.emitEvent('device_registered', { deviceId: this.deviceId });
|
|
591
|
+
this.emitEvent('device_registered', { deviceId: this.deviceId, fingerprint: this.deviceProfile.deviceFingerprint });
|
|
280
592
|
}
|
|
281
593
|
catch (err) {
|
|
282
594
|
// Non-fatal — device registration shouldn't block usage
|
|
283
595
|
this.emitProgress('initializing', 90, 'Device registration skipped (non-fatal)');
|
|
284
596
|
}
|
|
285
|
-
|
|
597
|
+
// Step 6: Start telemetry flush timer
|
|
598
|
+
this.telemetryFlushTimer = setTimeout(() => this.flushTelemetry(), SlyOS.TELEMETRY_FLUSH_INTERVAL);
|
|
599
|
+
this.emitProgress('ready', 100, `SlyOS v${SDK_VERSION} ready — ${this.deviceProfile.recommendedQuant.toUpperCase()}, ${this.deviceProfile.gpuRenderer ? 'GPU detected' : 'CPU only'}`);
|
|
286
600
|
return this.deviceProfile;
|
|
287
601
|
}
|
|
288
602
|
// ── Model Loading ───────────────────────────────────────────────
|
|
@@ -478,7 +792,11 @@ class SlyOS {
|
|
|
478
792
|
if (!this.models.has(modelId)) {
|
|
479
793
|
await this.loadModel(modelId);
|
|
480
794
|
}
|
|
481
|
-
const
|
|
795
|
+
const loaded = this.models.get(modelId);
|
|
796
|
+
if (!loaded) {
|
|
797
|
+
throw new Error(`Model "${modelId}" failed to load. Check your connection and model ID.`);
|
|
798
|
+
}
|
|
799
|
+
const { pipe, info, contextWindow } = loaded;
|
|
482
800
|
if (info.category !== 'llm') {
|
|
483
801
|
throw new Error(`Model "${modelId}" is not an LLM. Use transcribe() for STT models.`);
|
|
484
802
|
}
|
|
@@ -504,6 +822,15 @@ class SlyOS {
|
|
|
504
822
|
const tokensPerSec = (tokensGenerated / (latency / 1000)).toFixed(1);
|
|
505
823
|
this.emitProgress('ready', 100, `Generated ${tokensGenerated} tokens in ${(latency / 1000).toFixed(1)}s (${tokensPerSec} tok/s)`);
|
|
506
824
|
this.emitEvent('inference_complete', { modelId, latencyMs: latency, tokensGenerated, tokensPerSec: parseFloat(tokensPerSec) });
|
|
825
|
+
// Batch telemetry (new device intelligence)
|
|
826
|
+
this.recordTelemetry({
|
|
827
|
+
latency_ms: latency,
|
|
828
|
+
tokens_generated: tokensGenerated,
|
|
829
|
+
success: true,
|
|
830
|
+
model_id: modelId,
|
|
831
|
+
timestamp: Date.now(),
|
|
832
|
+
});
|
|
833
|
+
// Legacy telemetry (backwards compatible)
|
|
507
834
|
if (this.token) {
|
|
508
835
|
await axios.post(`${this.apiUrl}/api/telemetry`, {
|
|
509
836
|
device_id: this.deviceId,
|
|
@@ -521,6 +848,14 @@ class SlyOS {
|
|
|
521
848
|
catch (error) {
|
|
522
849
|
this.emitProgress('error', 0, `Generation failed: ${error.message}`);
|
|
523
850
|
this.emitEvent('error', { stage: 'inference', modelId, error: error.message });
|
|
851
|
+
// Batch telemetry (failure)
|
|
852
|
+
this.recordTelemetry({
|
|
853
|
+
latency_ms: 0,
|
|
854
|
+
tokens_generated: 0,
|
|
855
|
+
success: false,
|
|
856
|
+
model_id: modelId,
|
|
857
|
+
timestamp: Date.now(),
|
|
858
|
+
});
|
|
524
859
|
if (this.token) {
|
|
525
860
|
await axios.post(`${this.apiUrl}/api/telemetry`, {
|
|
526
861
|
device_id: this.deviceId,
|
|
@@ -540,7 +875,11 @@ class SlyOS {
|
|
|
540
875
|
if (!this.models.has(modelId)) {
|
|
541
876
|
await this.loadModel(modelId);
|
|
542
877
|
}
|
|
543
|
-
const
|
|
878
|
+
const loaded = this.models.get(modelId);
|
|
879
|
+
if (!loaded) {
|
|
880
|
+
throw new Error(`Model "${modelId}" failed to load. Check your connection and model ID.`);
|
|
881
|
+
}
|
|
882
|
+
const { pipe, info } = loaded;
|
|
544
883
|
if (info.category !== 'stt') {
|
|
545
884
|
throw new Error(`Model "${modelId}" is not an STT model. Use generate() for LLMs.`);
|
|
546
885
|
}
|
|
@@ -1137,4 +1476,6 @@ class SlyOS {
|
|
|
1137
1476
|
};
|
|
1138
1477
|
}
|
|
1139
1478
|
}
|
|
1479
|
+
SlyOS.TELEMETRY_BATCH_SIZE = 10;
|
|
1480
|
+
SlyOS.TELEMETRY_FLUSH_INTERVAL = 60000; // 60 seconds
|
|
1140
1481
|
export default SlyOS;
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -45,6 +45,20 @@ interface DeviceProfile {
|
|
|
45
45
|
os: string;
|
|
46
46
|
recommendedQuant: QuantizationLevel;
|
|
47
47
|
maxContextWindow: number;
|
|
48
|
+
// Enhanced device intelligence fields
|
|
49
|
+
deviceFingerprint?: string;
|
|
50
|
+
gpuRenderer?: string;
|
|
51
|
+
gpuVramMb?: number;
|
|
52
|
+
screenWidth?: number;
|
|
53
|
+
screenHeight?: number;
|
|
54
|
+
pixelRatio?: number;
|
|
55
|
+
browserName?: string;
|
|
56
|
+
browserVersion?: string;
|
|
57
|
+
networkType?: string;
|
|
58
|
+
latencyToApiMs?: number;
|
|
59
|
+
timezone?: string;
|
|
60
|
+
wasmAvailable?: boolean;
|
|
61
|
+
webgpuAvailable?: boolean;
|
|
48
62
|
}
|
|
49
63
|
|
|
50
64
|
interface ProgressEvent {
|
|
@@ -55,7 +69,7 @@ interface ProgressEvent {
|
|
|
55
69
|
}
|
|
56
70
|
|
|
57
71
|
interface SlyEvent {
|
|
58
|
-
type: 'auth' | 'device_registered' | 'device_profiled' | 'model_download_start' | 'model_download_progress' | 'model_loaded' | 'inference_start' | 'inference_complete' | 'error' | 'fallback_success' | 'fallback_error';
|
|
72
|
+
type: 'auth' | 'device_registered' | 'device_profiled' | 'model_download_start' | 'model_download_progress' | 'model_loaded' | 'inference_start' | 'inference_complete' | 'error' | 'fallback_success' | 'fallback_error' | 'telemetry_flushed';
|
|
59
73
|
data?: any;
|
|
60
74
|
timestamp: number;
|
|
61
75
|
}
|
|
@@ -305,6 +319,162 @@ async function detectContextWindowFromHF(hfModelId: string): Promise<number> {
|
|
|
305
319
|
}
|
|
306
320
|
}
|
|
307
321
|
|
|
322
|
+
// ─── SDK Version ────────────────────────────────────────────────────
|
|
323
|
+
const SDK_VERSION = '1.4.1';
|
|
324
|
+
|
|
325
|
+
// ─── Persistent Device Identity ─────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
async function hashString(str: string): Promise<string> {
|
|
328
|
+
const isNode = typeof window === 'undefined';
|
|
329
|
+
if (isNode) {
|
|
330
|
+
const crypto = await import('crypto');
|
|
331
|
+
return crypto.createHash('sha256').update(str).digest('hex').substring(0, 32);
|
|
332
|
+
} else {
|
|
333
|
+
const encoder = new TextEncoder();
|
|
334
|
+
const data = encoder.encode(str);
|
|
335
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
336
|
+
return Array.from(new Uint8Array(hashBuffer))
|
|
337
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
338
|
+
.join('')
|
|
339
|
+
.substring(0, 32);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function getOrCreateDeviceId(): Promise<string> {
|
|
344
|
+
const isNode = typeof window === 'undefined';
|
|
345
|
+
|
|
346
|
+
if (isNode) {
|
|
347
|
+
// Node.js: persist in ~/.slyos/device-id
|
|
348
|
+
try {
|
|
349
|
+
const fs = await import('fs');
|
|
350
|
+
const path = await import('path');
|
|
351
|
+
const os = await import('os');
|
|
352
|
+
const slyosDir = path.join(os.homedir(), '.slyos');
|
|
353
|
+
const idFile = path.join(slyosDir, 'device-id');
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
const existing = fs.readFileSync(idFile, 'utf-8').trim();
|
|
357
|
+
if (existing) return existing;
|
|
358
|
+
} catch {}
|
|
359
|
+
|
|
360
|
+
const deviceId = `device-${Date.now()}-${Math.random().toString(36).substr(2, 12)}`;
|
|
361
|
+
fs.mkdirSync(slyosDir, { recursive: true });
|
|
362
|
+
fs.writeFileSync(idFile, deviceId);
|
|
363
|
+
return deviceId;
|
|
364
|
+
} catch {
|
|
365
|
+
return `device-${Date.now()}-${Math.random().toString(36).substr(2, 12)}`;
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
// Browser: persist in localStorage
|
|
369
|
+
const key = 'slyos_device_id';
|
|
370
|
+
try {
|
|
371
|
+
const existing = localStorage.getItem(key);
|
|
372
|
+
if (existing) return existing;
|
|
373
|
+
} catch {}
|
|
374
|
+
|
|
375
|
+
const deviceId = `device-${Date.now()}-${Math.random().toString(36).substr(2, 12)}`;
|
|
376
|
+
try { localStorage.setItem(key, deviceId); } catch {}
|
|
377
|
+
return deviceId;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function generateDeviceFingerprint(): Promise<string> {
|
|
382
|
+
const isNode = typeof window === 'undefined';
|
|
383
|
+
let components: string[] = [];
|
|
384
|
+
|
|
385
|
+
if (isNode) {
|
|
386
|
+
try {
|
|
387
|
+
const os = await import('os');
|
|
388
|
+
const cpus = os.cpus();
|
|
389
|
+
components.push(cpus[0]?.model || 'unknown-cpu');
|
|
390
|
+
components.push(String(os.totalmem()));
|
|
391
|
+
components.push(os.platform());
|
|
392
|
+
components.push(os.arch());
|
|
393
|
+
components.push(String(cpus.length));
|
|
394
|
+
} catch {}
|
|
395
|
+
} else {
|
|
396
|
+
components.push(String(navigator.hardwareConcurrency || 0));
|
|
397
|
+
components.push(String((navigator as any).deviceMemory || 0));
|
|
398
|
+
components.push(navigator.platform || 'unknown');
|
|
399
|
+
// WebGL renderer for GPU fingerprint
|
|
400
|
+
try {
|
|
401
|
+
const canvas = document.createElement('canvas');
|
|
402
|
+
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl') as WebGLRenderingContext | null;
|
|
403
|
+
if (gl) {
|
|
404
|
+
const ext = gl.getExtension('WEBGL_debug_renderer_info');
|
|
405
|
+
if (ext) {
|
|
406
|
+
components.push(gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) || 'unknown-gpu');
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
} catch {}
|
|
410
|
+
components.push(String(screen.width || 0));
|
|
411
|
+
components.push(String(screen.height || 0));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return await hashString(components.join('|'));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ─── Enhanced Device Profiling ──────────────────────────────────────
|
|
418
|
+
|
|
419
|
+
function detectGPU(): { renderer: string | null; vramMb: number } {
|
|
420
|
+
if (typeof window === 'undefined') return { renderer: null, vramMb: 0 };
|
|
421
|
+
try {
|
|
422
|
+
const canvas = document.createElement('canvas');
|
|
423
|
+
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl') as WebGLRenderingContext | null;
|
|
424
|
+
if (!gl) return { renderer: null, vramMb: 0 };
|
|
425
|
+
const ext = gl.getExtension('WEBGL_debug_renderer_info');
|
|
426
|
+
const renderer = ext ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) : null;
|
|
427
|
+
// Rough VRAM estimate from renderer string
|
|
428
|
+
let vramMb = 0;
|
|
429
|
+
if (renderer) {
|
|
430
|
+
const match = renderer.match(/(\d+)\s*MB/i);
|
|
431
|
+
if (match) vramMb = parseInt(match[1]);
|
|
432
|
+
else if (/RTX\s*40/i.test(renderer)) vramMb = 8192;
|
|
433
|
+
else if (/RTX\s*30/i.test(renderer)) vramMb = 6144;
|
|
434
|
+
else if (/GTX/i.test(renderer)) vramMb = 4096;
|
|
435
|
+
else if (/Apple M[2-4]/i.test(renderer)) vramMb = 8192;
|
|
436
|
+
else if (/Apple M1/i.test(renderer)) vramMb = 4096;
|
|
437
|
+
else if (/Intel/i.test(renderer)) vramMb = 1024;
|
|
438
|
+
}
|
|
439
|
+
return { renderer, vramMb };
|
|
440
|
+
} catch {
|
|
441
|
+
return { renderer: null, vramMb: 0 };
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function detectBrowser(): { name: string; version: string } {
|
|
446
|
+
if (typeof window === 'undefined' || typeof navigator === 'undefined') return { name: 'node', version: process.version || 'unknown' };
|
|
447
|
+
const ua = navigator.userAgent;
|
|
448
|
+
if (/Edg\//i.test(ua)) { const m = ua.match(/Edg\/([\d.]+)/); return { name: 'Edge', version: m?.[1] || '' }; }
|
|
449
|
+
if (/Chrome\//i.test(ua)) { const m = ua.match(/Chrome\/([\d.]+)/); return { name: 'Chrome', version: m?.[1] || '' }; }
|
|
450
|
+
if (/Firefox\//i.test(ua)) { const m = ua.match(/Firefox\/([\d.]+)/); return { name: 'Firefox', version: m?.[1] || '' }; }
|
|
451
|
+
if (/Safari\//i.test(ua)) { const m = ua.match(/Version\/([\d.]+)/); return { name: 'Safari', version: m?.[1] || '' }; }
|
|
452
|
+
return { name: 'unknown', version: '' };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function detectNetworkType(): string {
|
|
456
|
+
if (typeof navigator === 'undefined') return 'unknown';
|
|
457
|
+
const conn = (navigator as any).connection || (navigator as any).mozConnection || (navigator as any).webkitConnection;
|
|
458
|
+
if (!conn) return 'unknown';
|
|
459
|
+
return conn.effectiveType || conn.type || 'unknown';
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function measureApiLatency(apiUrl: string): Promise<number> {
|
|
463
|
+
try {
|
|
464
|
+
const start = Date.now();
|
|
465
|
+
await axios.head(`${apiUrl}/api/health`, { timeout: 5000 });
|
|
466
|
+
return Date.now() - start;
|
|
467
|
+
} catch {
|
|
468
|
+
try {
|
|
469
|
+
const start = Date.now();
|
|
470
|
+
await axios.get(`${apiUrl}/api/health`, { timeout: 5000 });
|
|
471
|
+
return Date.now() - start;
|
|
472
|
+
} catch {
|
|
473
|
+
return -1;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
308
478
|
// ─── Device Profiling ───────────────────────────────────────────────
|
|
309
479
|
|
|
310
480
|
async function profileDevice(): Promise<DeviceProfile> {
|
|
@@ -358,6 +528,27 @@ async function profileDevice(): Promise<DeviceProfile> {
|
|
|
358
528
|
const recommendedQuant = selectQuantization(memoryMB, 'quantum-1.7b'); // default baseline
|
|
359
529
|
const maxContextWindow = recommendContextWindow(memoryMB, recommendedQuant);
|
|
360
530
|
|
|
531
|
+
// Enhanced profiling
|
|
532
|
+
const gpu = detectGPU();
|
|
533
|
+
const browser = detectBrowser();
|
|
534
|
+
const networkType = detectNetworkType();
|
|
535
|
+
const timezone = Intl?.DateTimeFormat?.()?.resolvedOptions?.()?.timeZone || 'unknown';
|
|
536
|
+
|
|
537
|
+
let screenWidth = 0, screenHeight = 0, pixelRatio = 0;
|
|
538
|
+
let wasmAvailable = false, webgpuAvailable = false;
|
|
539
|
+
|
|
540
|
+
if (!isNode) {
|
|
541
|
+
screenWidth = screen?.width || 0;
|
|
542
|
+
screenHeight = screen?.height || 0;
|
|
543
|
+
pixelRatio = window?.devicePixelRatio || 1;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Capability detection
|
|
547
|
+
try { wasmAvailable = typeof WebAssembly !== 'undefined'; } catch {}
|
|
548
|
+
if (!isNode) {
|
|
549
|
+
try { webgpuAvailable = !!(navigator as any).gpu; } catch {}
|
|
550
|
+
}
|
|
551
|
+
|
|
361
552
|
return {
|
|
362
553
|
cpuCores,
|
|
363
554
|
memoryMB,
|
|
@@ -366,11 +557,30 @@ async function profileDevice(): Promise<DeviceProfile> {
|
|
|
366
557
|
os,
|
|
367
558
|
recommendedQuant,
|
|
368
559
|
maxContextWindow,
|
|
560
|
+
gpuRenderer: gpu.renderer || undefined,
|
|
561
|
+
gpuVramMb: gpu.vramMb || undefined,
|
|
562
|
+
screenWidth: screenWidth || undefined,
|
|
563
|
+
screenHeight: screenHeight || undefined,
|
|
564
|
+
pixelRatio: pixelRatio || undefined,
|
|
565
|
+
browserName: browser.name,
|
|
566
|
+
browserVersion: browser.version,
|
|
567
|
+
networkType,
|
|
568
|
+
timezone,
|
|
569
|
+
wasmAvailable,
|
|
570
|
+
webgpuAvailable,
|
|
369
571
|
};
|
|
370
572
|
}
|
|
371
573
|
|
|
372
574
|
// ─── Main SDK Class ─────────────────────────────────────────────────
|
|
373
575
|
|
|
576
|
+
interface TelemetryEntry {
|
|
577
|
+
latency_ms: number;
|
|
578
|
+
tokens_generated: number;
|
|
579
|
+
success: boolean;
|
|
580
|
+
model_id: string;
|
|
581
|
+
timestamp: number;
|
|
582
|
+
}
|
|
583
|
+
|
|
374
584
|
class SlyOS {
|
|
375
585
|
private apiKey: string;
|
|
376
586
|
private apiUrl: string;
|
|
@@ -382,11 +592,16 @@ class SlyOS {
|
|
|
382
592
|
private onEvent: EventCallback | null;
|
|
383
593
|
private fallbackConfig: FallbackConfig | null;
|
|
384
594
|
private modelContextWindow: number = 0;
|
|
595
|
+
// Telemetry batching
|
|
596
|
+
private telemetryBuffer: TelemetryEntry[] = [];
|
|
597
|
+
private telemetryFlushTimer: any = null;
|
|
598
|
+
private static readonly TELEMETRY_BATCH_SIZE = 10;
|
|
599
|
+
private static readonly TELEMETRY_FLUSH_INTERVAL = 60000; // 60 seconds
|
|
385
600
|
|
|
386
601
|
constructor(config: SlyOSConfigWithFallback) {
|
|
387
602
|
this.apiKey = config.apiKey;
|
|
388
603
|
this.apiUrl = config.apiUrl || 'https://api.slyos.world';
|
|
389
|
-
this.deviceId =
|
|
604
|
+
this.deviceId = ''; // Set asynchronously in initialize()
|
|
390
605
|
this.onProgress = config.onProgress || null;
|
|
391
606
|
this.onEvent = config.onEvent || null;
|
|
392
607
|
this.fallbackConfig = config.fallback || null;
|
|
@@ -406,14 +621,59 @@ class SlyOS {
|
|
|
406
621
|
}
|
|
407
622
|
}
|
|
408
623
|
|
|
624
|
+
// ── Telemetry Batching ─────────────────────────────────────────
|
|
625
|
+
|
|
626
|
+
private recordTelemetry(entry: TelemetryEntry) {
|
|
627
|
+
this.telemetryBuffer.push(entry);
|
|
628
|
+
if (this.telemetryBuffer.length >= SlyOS.TELEMETRY_BATCH_SIZE) {
|
|
629
|
+
this.flushTelemetry();
|
|
630
|
+
} else if (!this.telemetryFlushTimer) {
|
|
631
|
+
this.telemetryFlushTimer = setTimeout(() => this.flushTelemetry(), SlyOS.TELEMETRY_FLUSH_INTERVAL);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
private async flushTelemetry() {
|
|
636
|
+
if (this.telemetryFlushTimer) {
|
|
637
|
+
clearTimeout(this.telemetryFlushTimer);
|
|
638
|
+
this.telemetryFlushTimer = null;
|
|
639
|
+
}
|
|
640
|
+
if (this.telemetryBuffer.length === 0 || !this.token) return;
|
|
641
|
+
|
|
642
|
+
const batch = [...this.telemetryBuffer];
|
|
643
|
+
this.telemetryBuffer = [];
|
|
644
|
+
|
|
645
|
+
try {
|
|
646
|
+
await axios.post(`${this.apiUrl}/api/devices/telemetry`, {
|
|
647
|
+
device_id: this.deviceId,
|
|
648
|
+
metrics: batch,
|
|
649
|
+
}, {
|
|
650
|
+
headers: { Authorization: `Bearer ${this.token}` },
|
|
651
|
+
timeout: 10000,
|
|
652
|
+
});
|
|
653
|
+
this.emitEvent('telemetry_flushed', { count: batch.length });
|
|
654
|
+
} catch {
|
|
655
|
+
// Put back on failure for next attempt
|
|
656
|
+
this.telemetryBuffer.unshift(...batch);
|
|
657
|
+
// Cap buffer to prevent memory leak
|
|
658
|
+
if (this.telemetryBuffer.length > 100) {
|
|
659
|
+
this.telemetryBuffer = this.telemetryBuffer.slice(-100);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
409
664
|
// ── Device Analysis ─────────────────────────────────────────────
|
|
410
665
|
|
|
411
666
|
async analyzeDevice(): Promise<DeviceProfile> {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
667
|
+
try {
|
|
668
|
+
this.emitProgress('profiling', 10, 'Analyzing device capabilities...');
|
|
669
|
+
this.deviceProfile = await profileDevice();
|
|
670
|
+
this.emitProgress('profiling', 100, `Device: ${this.deviceProfile.cpuCores} cores, ${Math.round(this.deviceProfile.memoryMB / 1024 * 10) / 10}GB RAM`);
|
|
671
|
+
this.emitEvent('device_profiled', this.deviceProfile);
|
|
672
|
+
return this.deviceProfile;
|
|
673
|
+
} catch (err: any) {
|
|
674
|
+
this.emitEvent('error', { method: 'analyzeDevice', error: err.message });
|
|
675
|
+
throw new Error(`Device analysis failed: ${err.message}`);
|
|
676
|
+
}
|
|
417
677
|
}
|
|
418
678
|
|
|
419
679
|
getDeviceProfile(): DeviceProfile | null {
|
|
@@ -424,6 +684,23 @@ class SlyOS {
|
|
|
424
684
|
return this.modelContextWindow;
|
|
425
685
|
}
|
|
426
686
|
|
|
687
|
+
getDeviceId(): string {
|
|
688
|
+
return this.deviceId;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
getSdkVersion(): string {
|
|
692
|
+
return SDK_VERSION;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Flush remaining telemetry and clean up timers
|
|
696
|
+
async destroy(): Promise<void> {
|
|
697
|
+
await this.flushTelemetry();
|
|
698
|
+
if (this.telemetryFlushTimer) {
|
|
699
|
+
clearTimeout(this.telemetryFlushTimer);
|
|
700
|
+
this.telemetryFlushTimer = null;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
427
704
|
// ── Smart Model Recommendation ──────────────────────────────────
|
|
428
705
|
|
|
429
706
|
recommendModel(category: ModelCategory = 'llm'): { modelId: string; quant: QuantizationLevel; contextWindow: number; reason: string } | null {
|
|
@@ -467,13 +744,20 @@ class SlyOS {
|
|
|
467
744
|
async initialize(): Promise<DeviceProfile> {
|
|
468
745
|
this.emitProgress('initializing', 0, 'Starting SlyOS...');
|
|
469
746
|
|
|
470
|
-
// Step 1:
|
|
747
|
+
// Step 1: Persistent device ID
|
|
748
|
+
this.deviceId = await getOrCreateDeviceId();
|
|
749
|
+
|
|
750
|
+
// Step 2: Profile device (enhanced)
|
|
471
751
|
this.emitProgress('profiling', 5, 'Detecting device capabilities...');
|
|
472
752
|
this.deviceProfile = await profileDevice();
|
|
473
|
-
|
|
753
|
+
|
|
754
|
+
// Step 2b: Generate device fingerprint
|
|
755
|
+
this.deviceProfile.deviceFingerprint = await generateDeviceFingerprint();
|
|
756
|
+
|
|
757
|
+
this.emitProgress('profiling', 20, `Detected: ${this.deviceProfile.cpuCores} CPU cores, ${Math.round(this.deviceProfile.memoryMB / 1024 * 10) / 10}GB RAM${this.deviceProfile.gpuRenderer ? ', GPU: ' + this.deviceProfile.gpuRenderer.substring(0, 30) : ''}`);
|
|
474
758
|
this.emitEvent('device_profiled', this.deviceProfile);
|
|
475
759
|
|
|
476
|
-
// Step
|
|
760
|
+
// Step 3: Authenticate
|
|
477
761
|
this.emitProgress('initializing', 40, 'Authenticating with API key...');
|
|
478
762
|
try {
|
|
479
763
|
const authRes = await axios.post(`${this.apiUrl}/api/auth/sdk`, {
|
|
@@ -488,29 +772,63 @@ class SlyOS {
|
|
|
488
772
|
throw new Error(`SlyOS auth failed: ${err.response?.data?.error || err.message}`);
|
|
489
773
|
}
|
|
490
774
|
|
|
491
|
-
// Step
|
|
775
|
+
// Step 4: Measure API latency
|
|
776
|
+
const latency = await measureApiLatency(this.apiUrl);
|
|
777
|
+
if (latency > 0) this.deviceProfile.latencyToApiMs = latency;
|
|
778
|
+
|
|
779
|
+
// Step 5: Register device with full intelligence profile
|
|
492
780
|
this.emitProgress('initializing', 70, 'Registering device...');
|
|
493
781
|
try {
|
|
782
|
+
// Determine supported quantizations based on memory
|
|
783
|
+
const mem = this.deviceProfile.memoryMB;
|
|
784
|
+
const supportedQuants: string[] = ['q4'];
|
|
785
|
+
if (mem >= 4096) supportedQuants.push('q8');
|
|
786
|
+
if (mem >= 8192) supportedQuants.push('fp16');
|
|
787
|
+
if (mem >= 16384) supportedQuants.push('fp32');
|
|
788
|
+
|
|
789
|
+
// Determine recommended tier
|
|
790
|
+
let recommendedTier = 1;
|
|
791
|
+
if (mem >= 8192 && this.deviceProfile.cpuCores >= 4) recommendedTier = 2;
|
|
792
|
+
if (mem >= 16384 && this.deviceProfile.cpuCores >= 8) recommendedTier = 3;
|
|
793
|
+
|
|
494
794
|
await axios.post(`${this.apiUrl}/api/devices/register`, {
|
|
495
795
|
device_id: this.deviceId,
|
|
796
|
+
device_fingerprint: this.deviceProfile.deviceFingerprint,
|
|
496
797
|
platform: this.deviceProfile.platform,
|
|
497
798
|
os_version: this.deviceProfile.os,
|
|
498
799
|
total_memory_mb: this.deviceProfile.memoryMB,
|
|
499
800
|
cpu_cores: this.deviceProfile.cpuCores,
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
801
|
+
// Enhanced fields
|
|
802
|
+
gpu_renderer: this.deviceProfile.gpuRenderer || null,
|
|
803
|
+
gpu_vram_mb: this.deviceProfile.gpuVramMb || null,
|
|
804
|
+
screen_width: this.deviceProfile.screenWidth || null,
|
|
805
|
+
screen_height: this.deviceProfile.screenHeight || null,
|
|
806
|
+
pixel_ratio: this.deviceProfile.pixelRatio || null,
|
|
807
|
+
browser_name: this.deviceProfile.browserName || null,
|
|
808
|
+
browser_version: this.deviceProfile.browserVersion || null,
|
|
809
|
+
sdk_version: SDK_VERSION,
|
|
810
|
+
network_type: this.deviceProfile.networkType || null,
|
|
811
|
+
latency_to_api_ms: this.deviceProfile.latencyToApiMs || null,
|
|
812
|
+
timezone: this.deviceProfile.timezone || null,
|
|
813
|
+
// Capabilities
|
|
814
|
+
wasm_available: this.deviceProfile.wasmAvailable || false,
|
|
815
|
+
webgpu_available: this.deviceProfile.webgpuAvailable || false,
|
|
816
|
+
supported_quants: supportedQuants,
|
|
817
|
+
recommended_tier: recommendedTier,
|
|
503
818
|
}, {
|
|
504
819
|
headers: { Authorization: `Bearer ${this.token}` },
|
|
505
820
|
});
|
|
506
821
|
this.emitProgress('initializing', 90, 'Device registered');
|
|
507
|
-
this.emitEvent('device_registered', { deviceId: this.deviceId });
|
|
822
|
+
this.emitEvent('device_registered', { deviceId: this.deviceId, fingerprint: this.deviceProfile.deviceFingerprint });
|
|
508
823
|
} catch (err: any) {
|
|
509
824
|
// Non-fatal — device registration shouldn't block usage
|
|
510
825
|
this.emitProgress('initializing', 90, 'Device registration skipped (non-fatal)');
|
|
511
826
|
}
|
|
512
827
|
|
|
513
|
-
|
|
828
|
+
// Step 6: Start telemetry flush timer
|
|
829
|
+
this.telemetryFlushTimer = setTimeout(() => this.flushTelemetry(), SlyOS.TELEMETRY_FLUSH_INTERVAL);
|
|
830
|
+
|
|
831
|
+
this.emitProgress('ready', 100, `SlyOS v${SDK_VERSION} ready — ${this.deviceProfile.recommendedQuant.toUpperCase()}, ${this.deviceProfile.gpuRenderer ? 'GPU detected' : 'CPU only'}`);
|
|
514
832
|
|
|
515
833
|
return this.deviceProfile;
|
|
516
834
|
}
|
|
@@ -739,7 +1057,11 @@ class SlyOS {
|
|
|
739
1057
|
await this.loadModel(modelId);
|
|
740
1058
|
}
|
|
741
1059
|
|
|
742
|
-
const
|
|
1060
|
+
const loaded = this.models.get(modelId);
|
|
1061
|
+
if (!loaded) {
|
|
1062
|
+
throw new Error(`Model "${modelId}" failed to load. Check your connection and model ID.`);
|
|
1063
|
+
}
|
|
1064
|
+
const { pipe, info, contextWindow } = loaded;
|
|
743
1065
|
if (info.category !== 'llm') {
|
|
744
1066
|
throw new Error(`Model "${modelId}" is not an LLM. Use transcribe() for STT models.`);
|
|
745
1067
|
}
|
|
@@ -771,6 +1093,16 @@ class SlyOS {
|
|
|
771
1093
|
this.emitProgress('ready', 100, `Generated ${tokensGenerated} tokens in ${(latency / 1000).toFixed(1)}s (${tokensPerSec} tok/s)`);
|
|
772
1094
|
this.emitEvent('inference_complete', { modelId, latencyMs: latency, tokensGenerated, tokensPerSec: parseFloat(tokensPerSec) });
|
|
773
1095
|
|
|
1096
|
+
// Batch telemetry (new device intelligence)
|
|
1097
|
+
this.recordTelemetry({
|
|
1098
|
+
latency_ms: latency,
|
|
1099
|
+
tokens_generated: tokensGenerated,
|
|
1100
|
+
success: true,
|
|
1101
|
+
model_id: modelId,
|
|
1102
|
+
timestamp: Date.now(),
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
// Legacy telemetry (backwards compatible)
|
|
774
1106
|
if (this.token) {
|
|
775
1107
|
await axios.post(`${this.apiUrl}/api/telemetry`, {
|
|
776
1108
|
device_id: this.deviceId,
|
|
@@ -789,6 +1121,15 @@ class SlyOS {
|
|
|
789
1121
|
this.emitProgress('error', 0, `Generation failed: ${error.message}`);
|
|
790
1122
|
this.emitEvent('error', { stage: 'inference', modelId, error: error.message });
|
|
791
1123
|
|
|
1124
|
+
// Batch telemetry (failure)
|
|
1125
|
+
this.recordTelemetry({
|
|
1126
|
+
latency_ms: 0,
|
|
1127
|
+
tokens_generated: 0,
|
|
1128
|
+
success: false,
|
|
1129
|
+
model_id: modelId,
|
|
1130
|
+
timestamp: Date.now(),
|
|
1131
|
+
});
|
|
1132
|
+
|
|
792
1133
|
if (this.token) {
|
|
793
1134
|
await axios.post(`${this.apiUrl}/api/telemetry`, {
|
|
794
1135
|
device_id: this.deviceId,
|
|
@@ -811,7 +1152,11 @@ class SlyOS {
|
|
|
811
1152
|
await this.loadModel(modelId);
|
|
812
1153
|
}
|
|
813
1154
|
|
|
814
|
-
const
|
|
1155
|
+
const loaded = this.models.get(modelId);
|
|
1156
|
+
if (!loaded) {
|
|
1157
|
+
throw new Error(`Model "${modelId}" failed to load. Check your connection and model ID.`);
|
|
1158
|
+
}
|
|
1159
|
+
const { pipe, info } = loaded;
|
|
815
1160
|
if (info.category !== 'stt') {
|
|
816
1161
|
throw new Error(`Model "${modelId}" is not an STT model. Use generate() for LLMs.`);
|
|
817
1162
|
}
|