@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.
- package/.turbo/turbo-build.log +4 -0
- package/LICENSE +21 -0
- package/dist/fairness-metrics.d.ts +65 -0
- package/dist/fairness-metrics.d.ts.map +1 -0
- package/dist/fairness-metrics.js +394 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/inference/browser-inference.d.ts +60 -0
- package/dist/inference/browser-inference.d.ts.map +1 -0
- package/dist/inference/browser-inference.js +221 -0
- package/dist/inference/capability-detection.d.ts +183 -0
- package/dist/inference/capability-detection.d.ts.map +1 -0
- package/dist/inference/capability-detection.js +552 -0
- package/dist/inference/index.d.ts +43 -0
- package/dist/inference/index.d.ts.map +1 -0
- package/dist/inference/index.js +97 -0
- package/dist/inference/inference-factory.d.ts +73 -0
- package/dist/inference/inference-factory.d.ts.map +1 -0
- package/dist/inference/inference-factory.js +175 -0
- package/dist/inference/ollama-client.d.ts +114 -0
- package/dist/inference/ollama-client.d.ts.map +1 -0
- package/dist/inference/ollama-client.js +386 -0
- package/dist/legal-checks/annex-iv-report.d.ts +131 -0
- package/dist/legal-checks/annex-iv-report.d.ts.map +1 -0
- package/dist/legal-checks/annex-iv-report.js +62 -0
- package/dist/legal-checks/bias-metrics.d.ts +162 -0
- package/dist/legal-checks/bias-metrics.d.ts.map +1 -0
- package/dist/legal-checks/bias-metrics.js +408 -0
- package/dist/legal-checks/data-governance.d.ts +30 -0
- package/dist/legal-checks/data-governance.d.ts.map +1 -0
- package/dist/legal-checks/data-governance.js +99 -0
- package/dist/legal-checks/human-oversight-logging.d.ts +27 -0
- package/dist/legal-checks/human-oversight-logging.d.ts.map +1 -0
- package/dist/legal-checks/human-oversight-logging.js +106 -0
- package/dist/legal-checks/index.d.ts +13 -0
- package/dist/legal-checks/index.d.ts.map +1 -0
- package/dist/legal-checks/index.js +6 -0
- package/dist/legal-checks/risk-classifier.d.ts +35 -0
- package/dist/legal-checks/risk-classifier.d.ts.map +1 -0
- package/dist/legal-checks/risk-classifier.js +234 -0
- package/dist/legal-checks/robustness-transparency.d.ts +45 -0
- package/dist/legal-checks/robustness-transparency.d.ts.map +1 -0
- package/dist/legal-checks/robustness-transparency.js +111 -0
- package/dist/metrics/crows-pairs-bias.d.ts +41 -0
- package/dist/metrics/crows-pairs-bias.d.ts.map +1 -0
- package/dist/metrics/crows-pairs-bias.js +172 -0
- package/dist/metrics/index.d.ts +6 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +5 -0
- package/dist/metrics/stereotype-bias.d.ts +89 -0
- package/dist/metrics/stereotype-bias.d.ts.map +1 -0
- package/dist/metrics/stereotype-bias.js +157 -0
- package/dist/risk-engine.d.ts +23 -0
- package/dist/risk-engine.d.ts.map +1 -0
- package/dist/risk-engine.js +451 -0
- package/dist/types.d.ts +131 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/package.json +28 -0
- package/src/fairness-metrics.ts +498 -0
- package/src/index.ts +150 -0
- package/src/inference/browser-inference.ts +258 -0
- package/src/inference/capability-detection.ts +714 -0
- package/src/inference/index.ts +156 -0
- package/src/inference/inference-factory.ts +222 -0
- package/src/inference/ollama-client.ts +514 -0
- package/src/inference/transformers.d.ts +62 -0
- package/src/legal-checks/annex-iv-report.ts +157 -0
- package/src/legal-checks/bias-metrics.ts +590 -0
- package/src/legal-checks/data-governance.ts +148 -0
- package/src/legal-checks/human-oversight-logging.ts +135 -0
- package/src/legal-checks/index.ts +55 -0
- package/src/legal-checks/risk-classifier.ts +285 -0
- package/src/legal-checks/robustness-transparency.ts +162 -0
- package/src/metrics/crows-pairs-bias.ts +245 -0
- package/src/metrics/index.ts +22 -0
- package/src/metrics/stereotype-bias.ts +267 -0
- package/src/risk-engine.ts +525 -0
- package/src/types.ts +154 -0
- package/tests/annex-iv-report.test.ts +323 -0
- package/tests/crows-pairs-bias.test.ts +342 -0
- package/tests/fairness-metrics.test.ts +187 -0
- package/tests/integration/bias-calculation.integration.test.ts +688 -0
- package/tests/legal-checks.test.ts +53 -0
- package/tests/ollama-client.test.ts +427 -0
- package/tests/risk-engine.test.ts +149 -0
- package/tsconfig.json +11 -0
- 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
|
+
}
|