llm-checker 3.2.0 → 3.2.2
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 +106 -7
- package/analyzer/compatibility.js +20 -0
- package/bin/cli.js +14 -0
- package/bin/enhanced_cli.js +580 -36
- package/package.json +11 -3
- package/src/ai/multi-objective-selector.js +28 -4
- package/src/hardware/backends/cuda-detector.js +32 -11
- package/src/hardware/detector.js +107 -5
- package/src/hardware/specs.js +8 -1
- package/src/index.js +161 -31
- package/src/models/deterministic-selector.js +406 -22
- package/src/models/expanded_database.js +8 -2
- package/src/models/intelligent-selector.js +89 -4
- package/src/models/scoring-engine.js +4 -0
- package/src/models/speculative-decoding-estimator.js +245 -0
- package/src/policy/audit-reporter.js +420 -0
- package/src/policy/cli-policy.js +403 -0
- package/src/policy/policy-engine.js +497 -0
- package/src/policy/policy-manager.js +324 -0
- package/src/provenance/model-provenance.js +176 -0
- package/src/runtime/runtime-support.js +174 -0
- package/bin/CLAUDE.md +0 -27
- package/src/CLAUDE.md +0 -18
- package/src/data/CLAUDE.md +0 -17
- package/src/hardware/CLAUDE.md +0 -18
- package/src/hardware/backends/CLAUDE.md +0 -17
- package/src/models/CLAUDE.md +0 -23
- package/src/ollama/CLAUDE.md +0 -30
- package/src/plugins/CLAUDE.md +0 -17
- package/src/utils/CLAUDE.md +0 -17
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
function isPlainObject(value) {
|
|
2
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function toLowerString(value) {
|
|
6
|
+
if (value === undefined || value === null) return '';
|
|
7
|
+
return String(value).trim().toLowerCase();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function parseParamsB(value) {
|
|
11
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const text = String(value || '');
|
|
16
|
+
const match = text.match(/([0-9]+(?:\.[0-9]+)?)\s*b\b/i);
|
|
17
|
+
if (!match) return null;
|
|
18
|
+
|
|
19
|
+
return Number.parseFloat(match[1]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseQuant(value) {
|
|
23
|
+
if (!value) return null;
|
|
24
|
+
|
|
25
|
+
const text = String(value).toLowerCase();
|
|
26
|
+
const quantMatch = text.match(
|
|
27
|
+
/(q\d(?:_[a-z0-9]+)+|q\d(?:\.[a-z0-9]+)+|q\d(?:_k)?|iq\d(?:_[a-z0-9]+)+|fp16|f16)/i
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
return quantMatch ? quantMatch[1].toUpperCase() : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getModelKey(model) {
|
|
34
|
+
if (!isPlainObject(model)) return null;
|
|
35
|
+
|
|
36
|
+
const key =
|
|
37
|
+
model.model_identifier ||
|
|
38
|
+
model.modelIdentifier ||
|
|
39
|
+
model.identifier ||
|
|
40
|
+
model.tag ||
|
|
41
|
+
model.model_id ||
|
|
42
|
+
model.modelId ||
|
|
43
|
+
model.name ||
|
|
44
|
+
model.model_name;
|
|
45
|
+
|
|
46
|
+
const normalized = toLowerString(key);
|
|
47
|
+
return normalized || null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function uniqueModels(models) {
|
|
51
|
+
const deduped = new Map();
|
|
52
|
+
|
|
53
|
+
(Array.isArray(models) ? models : []).forEach((model, index) => {
|
|
54
|
+
if (!isPlainObject(model)) return;
|
|
55
|
+
|
|
56
|
+
const key = getModelKey(model) || `idx:${index}`;
|
|
57
|
+
if (!deduped.has(key)) {
|
|
58
|
+
deduped.set(key, model);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return Array.from(deduped.values());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function collectCandidatesFromAnalysis(analysis) {
|
|
66
|
+
if (!isPlainObject(analysis)) return [];
|
|
67
|
+
|
|
68
|
+
const all = []
|
|
69
|
+
.concat(Array.isArray(analysis.compatible) ? analysis.compatible : [])
|
|
70
|
+
.concat(Array.isArray(analysis.marginal) ? analysis.marginal : []);
|
|
71
|
+
|
|
72
|
+
return uniqueModels(all);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function toPolicyCandidateFromSummary(modelSummary) {
|
|
76
|
+
if (!isPlainObject(modelSummary)) return null;
|
|
77
|
+
|
|
78
|
+
const identifier = modelSummary.identifier || modelSummary.model_identifier || modelSummary.name;
|
|
79
|
+
if (!identifier) return null;
|
|
80
|
+
|
|
81
|
+
const paramsB = parseParamsB(identifier) ?? parseParamsB(modelSummary.size);
|
|
82
|
+
const quant = parseQuant(identifier) || parseQuant(modelSummary.name);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
model_identifier: identifier,
|
|
86
|
+
tag: identifier,
|
|
87
|
+
name: modelSummary.name || identifier,
|
|
88
|
+
size: modelSummary.size || null,
|
|
89
|
+
params_b: paramsB,
|
|
90
|
+
quant: quant || undefined,
|
|
91
|
+
source: modelSummary.source || 'local',
|
|
92
|
+
license: modelSummary.license,
|
|
93
|
+
version: modelSummary.version,
|
|
94
|
+
digest: modelSummary.digest,
|
|
95
|
+
provenance: modelSummary.provenance
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function collectCandidatesFromRecommendationData(recommendationData) {
|
|
100
|
+
const summary = recommendationData?.summary;
|
|
101
|
+
if (!isPlainObject(summary)) return [];
|
|
102
|
+
|
|
103
|
+
const candidates = [];
|
|
104
|
+
|
|
105
|
+
if (isPlainObject(summary.by_category)) {
|
|
106
|
+
Object.values(summary.by_category).forEach((categoryModel) => {
|
|
107
|
+
const candidate = toPolicyCandidateFromSummary(categoryModel);
|
|
108
|
+
if (candidate) candidates.push(candidate);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (isPlainObject(summary.best_overall)) {
|
|
113
|
+
const candidate = toPolicyCandidateFromSummary(summary.best_overall);
|
|
114
|
+
if (candidate) candidates.push(candidate);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return uniqueModels(candidates);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function buildPolicyRuntimeContext({ hardware, runtimeBackend } = {}) {
|
|
121
|
+
const summary = hardware?.summary || {};
|
|
122
|
+
const memory = hardware?.memory || {};
|
|
123
|
+
|
|
124
|
+
const bestBackend = summary.bestBackend || null;
|
|
125
|
+
const ramGB =
|
|
126
|
+
(typeof memory.total === 'number' ? memory.total : null) ??
|
|
127
|
+
(typeof summary.systemRAM === 'number' ? summary.systemRAM : null) ??
|
|
128
|
+
(typeof summary.effectiveMemory === 'number' ? summary.effectiveMemory : null);
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
backend: bestBackend,
|
|
132
|
+
runtimeBackend: runtimeBackend || bestBackend,
|
|
133
|
+
ramGB,
|
|
134
|
+
totalRamGB: ramGB,
|
|
135
|
+
hardware
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getCandidateTargets(model) {
|
|
140
|
+
if (!isPlainObject(model)) return [];
|
|
141
|
+
|
|
142
|
+
const targets = [
|
|
143
|
+
model.model_identifier,
|
|
144
|
+
model.modelIdentifier,
|
|
145
|
+
model.identifier,
|
|
146
|
+
model.tag,
|
|
147
|
+
model.model_id,
|
|
148
|
+
model.modelId,
|
|
149
|
+
model.name,
|
|
150
|
+
model.model_name
|
|
151
|
+
]
|
|
152
|
+
.map((entry) => toLowerString(entry))
|
|
153
|
+
.filter(Boolean);
|
|
154
|
+
|
|
155
|
+
return Array.from(new Set(targets));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function patternToRegex(pattern) {
|
|
159
|
+
const escaped = String(pattern || '')
|
|
160
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
161
|
+
.replace(/\*/g, '.*');
|
|
162
|
+
|
|
163
|
+
return new RegExp(`^${escaped}$`, 'i');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function parseExceptionExpiry(expiresAt) {
|
|
167
|
+
if (!expiresAt) return null;
|
|
168
|
+
|
|
169
|
+
const raw = String(expiresAt).trim();
|
|
170
|
+
if (!raw) return null;
|
|
171
|
+
|
|
172
|
+
const hasTime = /t|\d{2}:\d{2}/i.test(raw);
|
|
173
|
+
const candidate = hasTime ? raw : `${raw}T23:59:59.999Z`;
|
|
174
|
+
const parsed = new Date(candidate);
|
|
175
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isExceptionEntryActive(entry, now) {
|
|
179
|
+
const expiry = parseExceptionExpiry(entry?.expires_at);
|
|
180
|
+
if (!expiry) return true;
|
|
181
|
+
return expiry.getTime() >= now.getTime();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function findMatchingException(policy, model, now = new Date()) {
|
|
185
|
+
if (policy?.enforcement?.allow_exceptions !== true) return null;
|
|
186
|
+
if (!Array.isArray(policy?.exceptions) || policy.exceptions.length === 0) return null;
|
|
187
|
+
|
|
188
|
+
const targets = getCandidateTargets(model);
|
|
189
|
+
if (targets.length === 0) return null;
|
|
190
|
+
|
|
191
|
+
for (const entry of policy.exceptions) {
|
|
192
|
+
if (!isPlainObject(entry)) continue;
|
|
193
|
+
const pattern = String(entry.model || '').trim();
|
|
194
|
+
if (!pattern) continue;
|
|
195
|
+
if (!isExceptionEntryActive(entry, now)) continue;
|
|
196
|
+
|
|
197
|
+
const matcher = patternToRegex(pattern);
|
|
198
|
+
if (targets.some((target) => matcher.test(target))) {
|
|
199
|
+
return {
|
|
200
|
+
model: pattern,
|
|
201
|
+
reason: entry.reason || '',
|
|
202
|
+
approver: entry.approver || '',
|
|
203
|
+
expires_at: entry.expires_at || '',
|
|
204
|
+
matched_target: targets.find((target) => matcher.test(target)) || targets[0]
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function applyPolicyExceptions(policy, evaluated) {
|
|
213
|
+
const now = new Date();
|
|
214
|
+
let exceptionsAppliedCount = 0;
|
|
215
|
+
let suppressedViolationCount = 0;
|
|
216
|
+
|
|
217
|
+
const updated = (Array.isArray(evaluated) ? evaluated : []).map((item) => {
|
|
218
|
+
if (!isPlainObject(item)) return item;
|
|
219
|
+
|
|
220
|
+
const policyResult = isPlainObject(item.policyResult) ? item.policyResult : null;
|
|
221
|
+
const violations = Array.isArray(policyResult?.violations) ? policyResult.violations : [];
|
|
222
|
+
|
|
223
|
+
if (!policyResult || violations.length === 0) {
|
|
224
|
+
return item;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const matchedException = findMatchingException(policy, item, now);
|
|
228
|
+
if (!matchedException) {
|
|
229
|
+
return item;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
exceptionsAppliedCount += 1;
|
|
233
|
+
suppressedViolationCount += violations.length;
|
|
234
|
+
|
|
235
|
+
const rationale = Array.isArray(policyResult.rationale) ? [...policyResult.rationale] : [];
|
|
236
|
+
rationale.push(
|
|
237
|
+
`EXCEPTION_APPLIED: ${matchedException.model}` +
|
|
238
|
+
(matchedException.reason ? ` (${matchedException.reason})` : '')
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
...item,
|
|
243
|
+
policyResult: {
|
|
244
|
+
...policyResult,
|
|
245
|
+
pass: true,
|
|
246
|
+
violationCount: 0,
|
|
247
|
+
violations: [],
|
|
248
|
+
suppressedViolations: violations,
|
|
249
|
+
exceptionApplied: matchedException,
|
|
250
|
+
rationale
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
evaluated: updated,
|
|
257
|
+
exceptionsAppliedCount,
|
|
258
|
+
suppressedViolationCount
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function flattenFindings(evaluated) {
|
|
263
|
+
const findings = [];
|
|
264
|
+
|
|
265
|
+
(Array.isArray(evaluated) ? evaluated : []).forEach((item) => {
|
|
266
|
+
if (!isPlainObject(item)) return;
|
|
267
|
+
|
|
268
|
+
const modelIdentifier =
|
|
269
|
+
item.model_identifier ||
|
|
270
|
+
item.modelIdentifier ||
|
|
271
|
+
item.identifier ||
|
|
272
|
+
item.tag ||
|
|
273
|
+
item.model_id ||
|
|
274
|
+
item.modelId ||
|
|
275
|
+
item.name ||
|
|
276
|
+
item.model_name ||
|
|
277
|
+
'unknown:model';
|
|
278
|
+
|
|
279
|
+
const modelName = item.name || item.model_name || modelIdentifier;
|
|
280
|
+
const policyResult = isPlainObject(item.policyResult) ? item.policyResult : {};
|
|
281
|
+
const activeViolations = Array.isArray(policyResult.violations) ? policyResult.violations : [];
|
|
282
|
+
const suppressedViolations = Array.isArray(policyResult.suppressedViolations)
|
|
283
|
+
? policyResult.suppressedViolations
|
|
284
|
+
: [];
|
|
285
|
+
|
|
286
|
+
const baseFinding = {
|
|
287
|
+
model_identifier: modelIdentifier,
|
|
288
|
+
model_name: modelName,
|
|
289
|
+
source: item.source || item?.provenance?.source || 'unknown',
|
|
290
|
+
registry: item.registry || item?.provenance?.registry || 'unknown',
|
|
291
|
+
version: item.version || item?.provenance?.version || 'unknown',
|
|
292
|
+
license: item.license || item?.provenance?.license || 'unknown',
|
|
293
|
+
digest: item.digest || item?.provenance?.digest || 'unknown',
|
|
294
|
+
exception: policyResult.exceptionApplied || null
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
activeViolations.forEach((violation) => {
|
|
298
|
+
findings.push({
|
|
299
|
+
...baseFinding,
|
|
300
|
+
status: 'active',
|
|
301
|
+
violation
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
suppressedViolations.forEach((violation) => {
|
|
306
|
+
findings.push({
|
|
307
|
+
...baseFinding,
|
|
308
|
+
status: 'suppressed',
|
|
309
|
+
violation
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
return findings;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function evaluatePolicyCandidates(policyEngine, candidates, context = {}, policy = null) {
|
|
318
|
+
if (!policyEngine || typeof policyEngine.evaluateModels !== 'function') {
|
|
319
|
+
throw new Error('Invalid policy engine instance.');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const evaluatedRaw = policyEngine.evaluateModels(Array.isArray(candidates) ? candidates : [], context);
|
|
323
|
+
const activePolicy = isPlainObject(policy) ? policy : policyEngine.policy;
|
|
324
|
+
|
|
325
|
+
const exceptionResult = applyPolicyExceptions(activePolicy, evaluatedRaw);
|
|
326
|
+
const evaluated = exceptionResult.evaluated;
|
|
327
|
+
|
|
328
|
+
const totalChecked = evaluated.length;
|
|
329
|
+
const passCount = evaluated.filter((item) => item?.policyResult?.pass === true).length;
|
|
330
|
+
const failCount = totalChecked - passCount;
|
|
331
|
+
|
|
332
|
+
const violationCounts = new Map();
|
|
333
|
+
evaluated.forEach((item) => {
|
|
334
|
+
const violations = Array.isArray(item?.policyResult?.violations) ? item.policyResult.violations : [];
|
|
335
|
+
violations.forEach((violation) => {
|
|
336
|
+
const code = violation?.code || 'UNKNOWN';
|
|
337
|
+
violationCounts.set(code, (violationCounts.get(code) || 0) + 1);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const topViolations = Array.from(violationCounts.entries())
|
|
342
|
+
.map(([code, count]) => ({ code, count }))
|
|
343
|
+
.sort((a, b) => {
|
|
344
|
+
if (b.count !== a.count) return b.count - a.count;
|
|
345
|
+
return a.code.localeCompare(b.code);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const findings = flattenFindings(evaluated);
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
evaluated,
|
|
352
|
+
findings,
|
|
353
|
+
totalChecked,
|
|
354
|
+
passCount,
|
|
355
|
+
failCount,
|
|
356
|
+
topViolations,
|
|
357
|
+
exceptionsAppliedCount: exceptionResult.exceptionsAppliedCount,
|
|
358
|
+
suppressedViolationCount: exceptionResult.suppressedViolationCount
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function getPolicyMode(policy) {
|
|
363
|
+
return policy?.mode === 'enforce' ? 'enforce' : 'audit';
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function getOnViolationBehavior(policy) {
|
|
367
|
+
if (policy?.enforcement?.on_violation === 'warn') return 'warn';
|
|
368
|
+
return 'error';
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function getViolationExitCode(policy) {
|
|
372
|
+
const configured = policy?.enforcement?.exit_code;
|
|
373
|
+
if (Number.isInteger(configured) && configured >= 1 && configured <= 255) {
|
|
374
|
+
return configured;
|
|
375
|
+
}
|
|
376
|
+
return 1;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function resolvePolicyEnforcement(policy, evaluation) {
|
|
380
|
+
const mode = getPolicyMode(policy);
|
|
381
|
+
const onViolation = getOnViolationBehavior(policy);
|
|
382
|
+
const failCount = evaluation?.failCount || 0;
|
|
383
|
+
const hasFailures = failCount > 0;
|
|
384
|
+
|
|
385
|
+
const shouldBlock = mode === 'enforce' && onViolation !== 'warn' && hasFailures;
|
|
386
|
+
const exitCode = shouldBlock ? getViolationExitCode(policy) : 0;
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
mode,
|
|
390
|
+
onViolation,
|
|
391
|
+
hasFailures,
|
|
392
|
+
shouldBlock,
|
|
393
|
+
exitCode
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
module.exports = {
|
|
398
|
+
collectCandidatesFromAnalysis,
|
|
399
|
+
collectCandidatesFromRecommendationData,
|
|
400
|
+
buildPolicyRuntimeContext,
|
|
401
|
+
evaluatePolicyCandidates,
|
|
402
|
+
resolvePolicyEnforcement
|
|
403
|
+
};
|