@euconform/core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/LICENSE +21 -0
  3. package/dist/fairness-metrics.d.ts +65 -0
  4. package/dist/fairness-metrics.d.ts.map +1 -0
  5. package/dist/fairness-metrics.js +394 -0
  6. package/dist/index.d.ts +14 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +17 -0
  9. package/dist/inference/browser-inference.d.ts +60 -0
  10. package/dist/inference/browser-inference.d.ts.map +1 -0
  11. package/dist/inference/browser-inference.js +221 -0
  12. package/dist/inference/capability-detection.d.ts +183 -0
  13. package/dist/inference/capability-detection.d.ts.map +1 -0
  14. package/dist/inference/capability-detection.js +552 -0
  15. package/dist/inference/index.d.ts +43 -0
  16. package/dist/inference/index.d.ts.map +1 -0
  17. package/dist/inference/index.js +97 -0
  18. package/dist/inference/inference-factory.d.ts +73 -0
  19. package/dist/inference/inference-factory.d.ts.map +1 -0
  20. package/dist/inference/inference-factory.js +175 -0
  21. package/dist/inference/ollama-client.d.ts +114 -0
  22. package/dist/inference/ollama-client.d.ts.map +1 -0
  23. package/dist/inference/ollama-client.js +386 -0
  24. package/dist/legal-checks/annex-iv-report.d.ts +131 -0
  25. package/dist/legal-checks/annex-iv-report.d.ts.map +1 -0
  26. package/dist/legal-checks/annex-iv-report.js +62 -0
  27. package/dist/legal-checks/bias-metrics.d.ts +162 -0
  28. package/dist/legal-checks/bias-metrics.d.ts.map +1 -0
  29. package/dist/legal-checks/bias-metrics.js +408 -0
  30. package/dist/legal-checks/data-governance.d.ts +30 -0
  31. package/dist/legal-checks/data-governance.d.ts.map +1 -0
  32. package/dist/legal-checks/data-governance.js +99 -0
  33. package/dist/legal-checks/human-oversight-logging.d.ts +27 -0
  34. package/dist/legal-checks/human-oversight-logging.d.ts.map +1 -0
  35. package/dist/legal-checks/human-oversight-logging.js +106 -0
  36. package/dist/legal-checks/index.d.ts +13 -0
  37. package/dist/legal-checks/index.d.ts.map +1 -0
  38. package/dist/legal-checks/index.js +6 -0
  39. package/dist/legal-checks/risk-classifier.d.ts +35 -0
  40. package/dist/legal-checks/risk-classifier.d.ts.map +1 -0
  41. package/dist/legal-checks/risk-classifier.js +234 -0
  42. package/dist/legal-checks/robustness-transparency.d.ts +45 -0
  43. package/dist/legal-checks/robustness-transparency.d.ts.map +1 -0
  44. package/dist/legal-checks/robustness-transparency.js +111 -0
  45. package/dist/metrics/crows-pairs-bias.d.ts +41 -0
  46. package/dist/metrics/crows-pairs-bias.d.ts.map +1 -0
  47. package/dist/metrics/crows-pairs-bias.js +172 -0
  48. package/dist/metrics/index.d.ts +6 -0
  49. package/dist/metrics/index.d.ts.map +1 -0
  50. package/dist/metrics/index.js +5 -0
  51. package/dist/metrics/stereotype-bias.d.ts +89 -0
  52. package/dist/metrics/stereotype-bias.d.ts.map +1 -0
  53. package/dist/metrics/stereotype-bias.js +157 -0
  54. package/dist/risk-engine.d.ts +23 -0
  55. package/dist/risk-engine.d.ts.map +1 -0
  56. package/dist/risk-engine.js +451 -0
  57. package/dist/types.d.ts +131 -0
  58. package/dist/types.d.ts.map +1 -0
  59. package/dist/types.js +5 -0
  60. package/package.json +28 -0
  61. package/src/fairness-metrics.ts +498 -0
  62. package/src/index.ts +150 -0
  63. package/src/inference/browser-inference.ts +258 -0
  64. package/src/inference/capability-detection.ts +714 -0
  65. package/src/inference/index.ts +156 -0
  66. package/src/inference/inference-factory.ts +222 -0
  67. package/src/inference/ollama-client.ts +514 -0
  68. package/src/inference/transformers.d.ts +62 -0
  69. package/src/legal-checks/annex-iv-report.ts +157 -0
  70. package/src/legal-checks/bias-metrics.ts +590 -0
  71. package/src/legal-checks/data-governance.ts +148 -0
  72. package/src/legal-checks/human-oversight-logging.ts +135 -0
  73. package/src/legal-checks/index.ts +55 -0
  74. package/src/legal-checks/risk-classifier.ts +285 -0
  75. package/src/legal-checks/robustness-transparency.ts +162 -0
  76. package/src/metrics/crows-pairs-bias.ts +245 -0
  77. package/src/metrics/index.ts +22 -0
  78. package/src/metrics/stereotype-bias.ts +267 -0
  79. package/src/risk-engine.ts +525 -0
  80. package/src/types.ts +154 -0
  81. package/tests/annex-iv-report.test.ts +323 -0
  82. package/tests/crows-pairs-bias.test.ts +342 -0
  83. package/tests/fairness-metrics.test.ts +187 -0
  84. package/tests/integration/bias-calculation.integration.test.ts +688 -0
  85. package/tests/legal-checks.test.ts +53 -0
  86. package/tests/ollama-client.test.ts +427 -0
  87. package/tests/risk-engine.test.ts +149 -0
  88. package/tsconfig.json +11 -0
  89. package/vitest.config.ts +17 -0
@@ -0,0 +1,714 @@
1
+ /**
2
+ * Model Capability Detection Service
3
+ * Real-time detection of model capabilities with smart caching
4
+ */
5
+
6
+ import type { BiasCalculationMethod } from "../types";
7
+
8
+ /**
9
+ * Detection status for model capabilities
10
+ */
11
+ export type CapabilityDetectionStatus = "detecting" | "available" | "unavailable" | "error";
12
+
13
+ /**
14
+ * Model capability information
15
+ */
16
+ export interface ModelCapability {
17
+ modelId: string;
18
+ engine: "browser" | "ollama";
19
+ method: BiasCalculationMethod;
20
+ status: CapabilityDetectionStatus;
21
+ lastTested?: string;
22
+ error?: string;
23
+ recommended?: boolean;
24
+ }
25
+
26
+ /**
27
+ * Model selection state for UI components
28
+ */
29
+ export interface ModelSelectionState {
30
+ models: ModelCapability[];
31
+ selectedModel?: string;
32
+ isLoading: boolean;
33
+ showExplanations: boolean;
34
+ }
35
+
36
+ /**
37
+ * Cache entry structure with expiration and version tracking
38
+ */
39
+ export interface CapabilityCacheEntry {
40
+ modelId: string;
41
+ capability: ModelCapability;
42
+ cachedAt: string;
43
+ expiresAt: string;
44
+ ollamaVersion?: string;
45
+ }
46
+
47
+ /**
48
+ * Detection progress tracking
49
+ */
50
+ export interface DetectionProgress {
51
+ total: number;
52
+ completed: number;
53
+ inProgress: string[];
54
+ }
55
+
56
+ /**
57
+ * User preferences for model selection
58
+ */
59
+ export interface UserPreferences {
60
+ showExplanations: boolean;
61
+ preferExactMethods: boolean;
62
+ hideUnavailableModels: boolean;
63
+ }
64
+
65
+ /**
66
+ * Enhanced model selection state with preferences
67
+ */
68
+ export interface EnhancedModelSelectionState extends ModelSelectionState {
69
+ detectionProgress: DetectionProgress;
70
+ userPreferences: UserPreferences;
71
+ lastRefresh: string;
72
+ }
73
+ /**
74
+ * Service interface for capability detection
75
+ */
76
+ export interface CapabilityDetectionService {
77
+ detectAllCapabilities(): Promise<ModelCapability[]>;
78
+ detectModelCapability(modelId: string, engine: "browser" | "ollama"): Promise<ModelCapability>;
79
+ getCachedCapability(modelId: string): CapabilityCacheEntry | null;
80
+ setCachedCapability(capability: ModelCapability): void;
81
+ clearCache(): void;
82
+ refreshCapabilities(): Promise<ModelCapability[]>;
83
+ }
84
+
85
+ /**
86
+ * Implementation of capability detection service
87
+ */
88
+ export class DefaultCapabilityDetectionService implements CapabilityDetectionService {
89
+ private cache: Map<string, CapabilityCacheEntry> = new Map();
90
+ private readonly CACHE_DURATION_MS = 24 * 60 * 60 * 1000; // 24 hours
91
+ private readonly CACHE_KEY_PREFIX = "euconform_capability_";
92
+
93
+ constructor() {
94
+ this.loadCacheFromStorage();
95
+ }
96
+
97
+ /**
98
+ * Detect capabilities for all available models
99
+ * Includes browser inference and parallel Ollama model testing
100
+ */
101
+ async detectAllCapabilities(): Promise<ModelCapability[]> {
102
+ const capabilities: ModelCapability[] = [];
103
+
104
+ try {
105
+ // Import browser models dynamically
106
+ const { BROWSER_MODELS } = await import("./browser-inference");
107
+
108
+ for (const model of BROWSER_MODELS) {
109
+ if (!model.supported) continue;
110
+ capabilities.push({
111
+ modelId: model.id,
112
+ engine: "browser",
113
+ method: "logprobs_exact",
114
+ status: "available",
115
+ recommended: true,
116
+ lastTested: new Date().toISOString(),
117
+ });
118
+ }
119
+ } catch (error) {
120
+ console.warn("Failed to load browser models:", error);
121
+ // Fallback if import fails
122
+ capabilities.push({
123
+ modelId: "Xenova/distilgpt2",
124
+ engine: "browser",
125
+ method: "logprobs_exact",
126
+ status: "available",
127
+ recommended: true,
128
+ lastTested: new Date().toISOString(),
129
+ });
130
+ }
131
+
132
+ try {
133
+ // Import Ollama functions dynamically to avoid issues if not available
134
+ const { detectOllama, listOllamaModels } = await import("./ollama-client");
135
+
136
+ // Check if Ollama is available
137
+ const ollamaAvailable = await detectOllama();
138
+
139
+ if (ollamaAvailable) {
140
+ // Get list of available Ollama models
141
+ const ollamaModels = await listOllamaModels();
142
+
143
+ // Detect capabilities for all Ollama models in parallel
144
+ const detectionPromises = ollamaModels.map((model) =>
145
+ this.detectModelCapability(model.name, "ollama")
146
+ );
147
+
148
+ const ollamaCapabilities = await Promise.allSettled(detectionPromises);
149
+
150
+ // Process results and add successful detections
151
+ ollamaCapabilities.forEach((result, index) => {
152
+ if (result.status === "fulfilled") {
153
+ capabilities.push(result.value);
154
+ } else {
155
+ // Add error capability for failed detections
156
+ const model = ollamaModels[index];
157
+ if (model) {
158
+ capabilities.push({
159
+ modelId: model.name,
160
+ engine: "ollama",
161
+ method: "logprobs_fallback_latency",
162
+ status: "error",
163
+ error: result.reason?.message || "Detection failed",
164
+ lastTested: new Date().toISOString(),
165
+ });
166
+ }
167
+ }
168
+ });
169
+ }
170
+ } catch (error) {
171
+ console.warn("Failed to detect Ollama capabilities:", error);
172
+ }
173
+
174
+ // Sort capabilities by recommendation score
175
+ return this.sortByRecommendation(capabilities);
176
+ }
177
+
178
+ /**
179
+ * Detect capability for a specific model
180
+ * Uses cache-first strategy with live fallback
181
+ */
182
+ async detectModelCapability(
183
+ modelId: string,
184
+ engine: "browser" | "ollama"
185
+ ): Promise<ModelCapability> {
186
+ // Check cache first
187
+ const cached = this.getCachedCapability(modelId);
188
+ if (cached && this.isCacheValid(cached)) {
189
+ return cached.capability;
190
+ }
191
+
192
+ // Browser inference always supports exact log-probabilities
193
+ if (engine === "browser") {
194
+ const capability: ModelCapability = {
195
+ modelId,
196
+ engine: "browser",
197
+ method: "logprobs_exact",
198
+ status: "available",
199
+ recommended: true,
200
+ lastTested: new Date().toISOString(),
201
+ };
202
+
203
+ this.setCachedCapability(capability);
204
+ return capability;
205
+ }
206
+
207
+ // Detect Ollama model capability
208
+ try {
209
+ const { OllamaClient } = await import("./ollama-client");
210
+ const client = new OllamaClient(modelId);
211
+
212
+ // Test if model supports log-probabilities
213
+ const supportsLogProbs = await client.detectLogProbSupport();
214
+
215
+ const capability: ModelCapability = {
216
+ modelId,
217
+ engine: "ollama",
218
+ method: supportsLogProbs ? "logprobs_exact" : "logprobs_fallback_latency",
219
+ status: "available",
220
+ lastTested: new Date().toISOString(),
221
+ };
222
+
223
+ this.setCachedCapability(capability);
224
+ return capability;
225
+ } catch (error) {
226
+ const capability: ModelCapability = {
227
+ modelId,
228
+ engine: "ollama",
229
+ method: "logprobs_fallback_latency",
230
+ status: "error",
231
+ error: error instanceof Error ? error.message : "Unknown error",
232
+ lastTested: new Date().toISOString(),
233
+ };
234
+
235
+ // Cache error results for shorter duration to allow retry
236
+ this.setCachedCapability(capability, 5 * 60 * 1000); // 5 minutes for errors
237
+ return capability;
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Get cached capability result
243
+ */
244
+ getCachedCapability(modelId: string): CapabilityCacheEntry | null {
245
+ const cached = this.cache.get(modelId);
246
+ if (cached && this.isCacheValid(cached)) {
247
+ return cached;
248
+ }
249
+ return null;
250
+ }
251
+
252
+ /**
253
+ * Cache capability result with optional custom duration
254
+ */
255
+ setCachedCapability(capability: ModelCapability, durationMs?: number): void {
256
+ const duration = durationMs || this.CACHE_DURATION_MS;
257
+ const now = new Date();
258
+ const expiresAt = new Date(now.getTime() + duration);
259
+
260
+ const cacheEntry: CapabilityCacheEntry = {
261
+ modelId: capability.modelId,
262
+ capability,
263
+ cachedAt: now.toISOString(),
264
+ expiresAt: expiresAt.toISOString(),
265
+ };
266
+
267
+ // Store in memory cache
268
+ this.cache.set(capability.modelId, cacheEntry);
269
+
270
+ // Persist to localStorage
271
+ this.saveCacheToStorage(capability.modelId, cacheEntry);
272
+ }
273
+
274
+ /**
275
+ * Clear all cached capabilities
276
+ */
277
+ clearCache(): void {
278
+ this.cache.clear();
279
+
280
+ // Clear from localStorage
281
+ try {
282
+ const keys = Object.keys(localStorage);
283
+ for (const key of keys) {
284
+ if (key.startsWith(this.CACHE_KEY_PREFIX)) {
285
+ localStorage.removeItem(key);
286
+ }
287
+ }
288
+ } catch (error) {
289
+ console.warn("Failed to clear capability cache from localStorage:", error);
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Refresh all capabilities by clearing cache and re-detecting
295
+ */
296
+ async refreshCapabilities(): Promise<ModelCapability[]> {
297
+ this.clearCache();
298
+ return this.detectAllCapabilities();
299
+ }
300
+
301
+ /**
302
+ * Check if cache entry is still valid
303
+ */
304
+ protected isCacheValid(cacheEntry: CapabilityCacheEntry): boolean {
305
+ const now = new Date();
306
+ const expiresAt = new Date(cacheEntry.expiresAt);
307
+ return now < expiresAt;
308
+ }
309
+
310
+ /**
311
+ * Load cache from localStorage
312
+ */
313
+ private loadCacheFromStorage(): void {
314
+ try {
315
+ const keys = Object.keys(localStorage);
316
+ for (const key of keys) {
317
+ if (key.startsWith(this.CACHE_KEY_PREFIX)) {
318
+ const cached = localStorage.getItem(key);
319
+ if (cached) {
320
+ const cacheEntry: CapabilityCacheEntry = JSON.parse(cached);
321
+ if (this.isCacheValid(cacheEntry)) {
322
+ const modelId = key.replace(this.CACHE_KEY_PREFIX, "");
323
+ this.cache.set(modelId, cacheEntry);
324
+ } else {
325
+ // Remove expired cache entries
326
+ localStorage.removeItem(key);
327
+ }
328
+ }
329
+ }
330
+ }
331
+ } catch (error) {
332
+ console.warn("Failed to load capability cache from localStorage:", error);
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Save cache entry to localStorage
338
+ */
339
+ private saveCacheToStorage(modelId: string, cacheEntry: CapabilityCacheEntry): void {
340
+ try {
341
+ const key = `${this.CACHE_KEY_PREFIX}${modelId}`;
342
+ localStorage.setItem(key, JSON.stringify(cacheEntry));
343
+ } catch (error) {
344
+ console.warn("Failed to save capability cache to localStorage:", error);
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Sort capabilities by recommendation score
350
+ */
351
+ protected sortByRecommendation(capabilities: ModelCapability[]): ModelCapability[] {
352
+ return capabilities.sort((a, b) => {
353
+ const scoreA = this.getRecommendationScore(a);
354
+ const scoreB = this.getRecommendationScore(b);
355
+ return scoreB - scoreA;
356
+ });
357
+ }
358
+
359
+ /**
360
+ * Calculate recommendation score for a capability
361
+ */
362
+ protected getRecommendationScore(capability: ModelCapability): number {
363
+ if (capability.status !== "available") return 0;
364
+
365
+ let score = 0;
366
+
367
+ // Browser inference gets highest score
368
+ if (capability.engine === "browser") score += 100;
369
+
370
+ // Exact methods get bonus
371
+ if (capability.method === "logprobs_exact") score += 50;
372
+
373
+ // Recommended models get bonus
374
+ if (capability.recommended) score += 25;
375
+
376
+ return score;
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Enhanced capability detection service with timeout and retry logic
382
+ */
383
+ export class EnhancedCapabilityDetectionService extends DefaultCapabilityDetectionService {
384
+ private readonly DETECTION_TIMEOUT_MS = 5000; // 5 seconds
385
+ private readonly MAX_RETRIES = 3;
386
+ private readonly RETRY_DELAY_BASE_MS = 1000; // Base delay for exponential backoff
387
+
388
+ /**
389
+ * Detect model capability with timeout and retry logic
390
+ */
391
+ async detectModelCapability(
392
+ modelId: string,
393
+ engine: "browser" | "ollama"
394
+ ): Promise<ModelCapability> {
395
+ // Check cache first
396
+ const cached = this.getCachedCapability(modelId);
397
+ if (cached && this.isCacheValid(cached)) {
398
+ return cached.capability;
399
+ }
400
+
401
+ // Browser inference always supports exact log-probabilities
402
+ if (engine === "browser") {
403
+ const capability: ModelCapability = {
404
+ modelId,
405
+ engine: "browser",
406
+ method: "logprobs_exact",
407
+ status: "available",
408
+ recommended: true,
409
+ lastTested: new Date().toISOString(),
410
+ };
411
+
412
+ this.setCachedCapability(capability);
413
+ return capability;
414
+ }
415
+
416
+ // Detect Ollama model capability with timeout and retry
417
+ return this.detectOllamaCapabilityWithRetry(modelId);
418
+ }
419
+
420
+ /**
421
+ * Detect Ollama capability with retry logic and exponential backoff
422
+ */
423
+ private async detectOllamaCapabilityWithRetry(modelId: string): Promise<ModelCapability> {
424
+ let lastError: Error | null = null;
425
+
426
+ for (let attempt = 0; attempt < this.MAX_RETRIES; attempt++) {
427
+ try {
428
+ return await this.detectOllamaCapabilityWithTimeout(modelId);
429
+ } catch (error) {
430
+ lastError = error instanceof Error ? error : new Error("Unknown error");
431
+
432
+ // Don't retry on certain errors
433
+ if (this.isNonRetryableError(lastError)) {
434
+ break;
435
+ }
436
+
437
+ // Wait before retry with exponential backoff
438
+ if (attempt < this.MAX_RETRIES - 1) {
439
+ const delay = this.RETRY_DELAY_BASE_MS * 2 ** attempt;
440
+ await this.sleep(delay);
441
+ }
442
+ }
443
+ }
444
+
445
+ // All retries failed, return error capability
446
+ const errorMessage = this.getErrorMessage(lastError);
447
+ const capability: ModelCapability = {
448
+ modelId,
449
+ engine: "ollama",
450
+ method: "logprobs_fallback_latency",
451
+ status: this.isOllamaUnavailableError(lastError) ? "unavailable" : "error",
452
+ error: errorMessage,
453
+ lastTested: new Date().toISOString(),
454
+ };
455
+
456
+ // Cache error results for shorter duration
457
+ this.setCachedCapability(capability, 5 * 60 * 1000); // 5 minutes for errors
458
+ return capability;
459
+ }
460
+
461
+ /**
462
+ * Detect Ollama capability with timeout
463
+ */
464
+ private async detectOllamaCapabilityWithTimeout(modelId: string): Promise<ModelCapability> {
465
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
466
+
467
+ const timeoutPromise = new Promise<never>((_resolve, reject) => {
468
+ timeoutId = setTimeout(() => {
469
+ reject(new Error(`Detection timeout after ${this.DETECTION_TIMEOUT_MS}ms`));
470
+ }, this.DETECTION_TIMEOUT_MS);
471
+ });
472
+
473
+ const detectionPromise = (async () => {
474
+ const { OllamaClient, detectOllama } = await import("./ollama-client");
475
+
476
+ // First check if Ollama is available
477
+ const ollamaAvailable = await detectOllama();
478
+ if (!ollamaAvailable) {
479
+ throw new Error("Ollama service is not available");
480
+ }
481
+
482
+ const client = new OllamaClient(modelId);
483
+
484
+ // Test if model supports log-probabilities
485
+ const supportsLogProbs = await client.detectLogProbSupport();
486
+
487
+ const capability: ModelCapability = {
488
+ modelId,
489
+ engine: "ollama",
490
+ method: supportsLogProbs ? "logprobs_exact" : "logprobs_fallback_latency",
491
+ status: "available",
492
+ lastTested: new Date().toISOString(),
493
+ };
494
+
495
+ this.setCachedCapability(capability);
496
+ return capability;
497
+ })();
498
+
499
+ try {
500
+ return await Promise.race([detectionPromise, timeoutPromise]);
501
+ } finally {
502
+ if (timeoutId) clearTimeout(timeoutId);
503
+ }
504
+ }
505
+
506
+ /**
507
+ * Enhanced detectAllCapabilities with better error handling
508
+ */
509
+ async detectAllCapabilities(): Promise<ModelCapability[]> {
510
+ const capabilities: ModelCapability[] = [];
511
+
512
+ // Add all supported browser models dynamically
513
+ try {
514
+ const { BROWSER_MODELS } = await import("./browser-inference");
515
+
516
+ for (const model of BROWSER_MODELS) {
517
+ if (!model.supported) continue;
518
+ capabilities.push({
519
+ modelId: model.id,
520
+ engine: "browser",
521
+ method: "logprobs_exact",
522
+ status: "available",
523
+ recommended: true,
524
+ lastTested: new Date().toISOString(),
525
+ });
526
+ }
527
+ } catch (error) {
528
+ console.warn("Failed to load browser models:", error);
529
+ // Fallback if import fails
530
+ capabilities.push({
531
+ modelId: "Xenova/distilgpt2",
532
+ engine: "browser",
533
+ method: "logprobs_exact",
534
+ status: "available",
535
+ recommended: true,
536
+ lastTested: new Date().toISOString(),
537
+ });
538
+ }
539
+
540
+ try {
541
+ // Import Ollama functions dynamically
542
+ const { detectOllama, listOllamaModels } = await import("./ollama-client");
543
+
544
+ // Check if Ollama is available with timeout
545
+ const ollamaAvailable = await this.withTimeout(
546
+ detectOllama(),
547
+ 2000, // 2 second timeout for Ollama availability check
548
+ "Ollama availability check timeout"
549
+ );
550
+
551
+ if (ollamaAvailable) {
552
+ // Get list of available Ollama models with timeout
553
+ const ollamaModels = await this.withTimeout(
554
+ listOllamaModels(),
555
+ 5000, // 5 second timeout for model listing
556
+ "Ollama model listing timeout"
557
+ );
558
+
559
+ if (ollamaModels.length === 0) {
560
+ console.warn("No Ollama models found");
561
+ return capabilities;
562
+ }
563
+
564
+ // Detect capabilities for all Ollama models in parallel
565
+ const detectionPromises = ollamaModels.map((model) =>
566
+ this.detectModelCapability(model.name, "ollama")
567
+ );
568
+
569
+ const ollamaCapabilities = await Promise.allSettled(detectionPromises);
570
+
571
+ // Process results and add all capabilities (including errors)
572
+ ollamaCapabilities.forEach((result, index) => {
573
+ if (result.status === "fulfilled") {
574
+ capabilities.push(result.value);
575
+ } else {
576
+ // Add error capability for failed detections
577
+ const model = ollamaModels[index];
578
+ if (model) {
579
+ const errorMessage = result.reason?.message || "Detection failed";
580
+ capabilities.push({
581
+ modelId: model.name,
582
+ engine: "ollama",
583
+ method: "logprobs_fallback_latency",
584
+ status: "error",
585
+ error: errorMessage,
586
+ lastTested: new Date().toISOString(),
587
+ });
588
+ }
589
+ }
590
+ });
591
+ } else {
592
+ console.info("Ollama is not available, only browser inference will be offered");
593
+ }
594
+ } catch (error) {
595
+ console.warn("Failed to detect Ollama capabilities:", error);
596
+ // Don't throw - we still have browser inference available
597
+ }
598
+
599
+ // Sort capabilities by recommendation score
600
+ return this.sortByRecommendation(capabilities);
601
+ }
602
+
603
+ /**
604
+ * Utility function to add timeout to any promise
605
+ */
606
+ private withTimeout<T>(promise: Promise<T>, timeoutMs: number, errorMessage: string): Promise<T> {
607
+ return new Promise((resolve, reject) => {
608
+ const timeoutId = setTimeout(() => {
609
+ reject(new Error(errorMessage));
610
+ }, timeoutMs);
611
+
612
+ promise
613
+ .then((result) => {
614
+ clearTimeout(timeoutId);
615
+ resolve(result);
616
+ })
617
+ .catch((error) => {
618
+ clearTimeout(timeoutId);
619
+ reject(error);
620
+ });
621
+ });
622
+ }
623
+
624
+ /**
625
+ * Sleep utility for retry delays
626
+ */
627
+ private sleep(ms: number): Promise<void> {
628
+ return new Promise((resolve) => setTimeout(resolve, ms));
629
+ }
630
+
631
+ /**
632
+ * Check if error should not be retried
633
+ */
634
+ private isNonRetryableError(error: Error): boolean {
635
+ const message = error.message.toLowerCase();
636
+ return (
637
+ message.includes("not found") ||
638
+ (message.includes("model") && message.includes("not available")) ||
639
+ message.includes("permission denied") ||
640
+ message.includes("unauthorized")
641
+ );
642
+ }
643
+
644
+ /**
645
+ * Check if error indicates Ollama is unavailable
646
+ */
647
+ private isOllamaUnavailableError(error: Error | null): boolean {
648
+ if (!error) return false;
649
+ const message = error.message.toLowerCase();
650
+ return (
651
+ message.includes("ollama service is not available") ||
652
+ message.includes("connection refused") ||
653
+ message.includes("network error") ||
654
+ message.includes("fetch failed")
655
+ );
656
+ }
657
+
658
+ /**
659
+ * Get user-friendly error message
660
+ */
661
+ private getErrorMessage(error: Error | null): string {
662
+ if (!error) return "Unknown error";
663
+
664
+ const message = error.message;
665
+
666
+ // Timeout errors
667
+ if (message.includes("timeout")) {
668
+ return "Model detection timed out. The model may be slow to respond.";
669
+ }
670
+
671
+ // Ollama unavailable
672
+ if (this.isOllamaUnavailableError(error)) {
673
+ return "Ollama service is not running. Please start Ollama to use local models.";
674
+ }
675
+
676
+ // Model not found
677
+ if (message.includes("not found")) {
678
+ return "Model not found. Please ensure the model is installed in Ollama.";
679
+ }
680
+
681
+ // Network errors
682
+ if (message.includes("network") || message.includes("fetch")) {
683
+ return "Network error. Please check your connection and try again.";
684
+ }
685
+
686
+ // Generic error
687
+ return `Detection failed: ${message}`;
688
+ }
689
+
690
+ /**
691
+ * Check if cache entry is still valid (override to handle version tracking)
692
+ */
693
+ protected isCacheValid(cacheEntry: CapabilityCacheEntry): boolean {
694
+ const now = new Date();
695
+ const expiresAt = new Date(cacheEntry.expiresAt);
696
+
697
+ // Check expiration
698
+ if (now >= expiresAt) {
699
+ return false;
700
+ }
701
+
702
+ // TODO: Add Ollama version checking when available
703
+ // For now, we rely on time-based expiration
704
+
705
+ return true;
706
+ }
707
+ }
708
+
709
+ /**
710
+ * Create a capability detection service instance
711
+ */
712
+ export function createCapabilityDetectionService(): CapabilityDetectionService {
713
+ return new EnhancedCapabilityDetectionService();
714
+ }