kiku-agent 1.3.1 → 1.3.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.
Files changed (119) hide show
  1. package/README.md +1 -1
  2. package/package.json +4 -12
  3. package/server/agent/eval/AGENTS.md +228 -0
  4. package/server/agent/eval/analyze.py +684 -0
  5. package/server/agent/eval/gates.json +15 -0
  6. package/server/agent/eval/modules/analysis/compare.js +198 -0
  7. package/server/agent/eval/modules/analysis/gates.js +72 -0
  8. package/server/agent/eval/modules/analysis/judge.js +70 -0
  9. package/server/agent/eval/modules/core/cli.js +453 -0
  10. package/server/agent/eval/modules/core/config.js +60 -0
  11. package/server/agent/eval/modules/core/store.js +236 -0
  12. package/server/agent/eval/modules/evaluation/cases.js +689 -0
  13. package/server/agent/eval/modules/evaluation/runner.js +298 -0
  14. package/server/agent/eval/modules/evaluation/scoring.js +351 -0
  15. package/server/agent/eval/modules/evaluation/worker.js +122 -0
  16. package/server/agent/eval/modules/reporting/benchmark.js +265 -0
  17. package/server/agent/eval/modules/reporting/benchmarkPng.js +487 -0
  18. package/server/agent/eval/modules/reporting/dashboard.js +1406 -0
  19. package/server/agent/eval/modules/server/evalApi.js +1099 -0
  20. package/server/agent/eval/modules/ui/evalUi.js +4796 -0
  21. package/server/agent/eval/modules/utils/smokes.js +119 -0
  22. package/server/agent/tests/agentBubbleSystem.test.js +65 -0
  23. package/server/agent/tests/agentFollowUps.test.js +127 -0
  24. package/server/agent/tests/apiRouter.test.js +108 -0
  25. package/server/agent/tests/architectureFixes.test.js +81 -0
  26. package/server/agent/tests/architectureRefactoring.test.js +194 -0
  27. package/server/agent/tests/autoResearch.test.js +64 -0
  28. package/server/agent/tests/bilibiliEmbed.test.js +251 -0
  29. package/server/agent/tests/catalogSync.test.js +130 -0
  30. package/server/agent/tests/citations.test.js +180 -0
  31. package/server/agent/tests/clarificationAnalytics.test.js +219 -0
  32. package/server/agent/tests/clarifyingQuestions.test.js +594 -0
  33. package/server/agent/tests/cliAutomation.test.js +121 -0
  34. package/server/agent/tests/cloudProviders.test.js +234 -0
  35. package/server/agent/tests/commandInput.test.js +24 -0
  36. package/server/agent/tests/commands.test.js +417 -0
  37. package/server/agent/tests/contextIntelligence.test.js +162 -0
  38. package/server/agent/tests/conversationSafety.test.js +53 -0
  39. package/server/agent/tests/decisionAndSearchEnhancements.test.js +135 -0
  40. package/server/agent/tests/dramaAnswerQuality.test.js +73 -0
  41. package/server/agent/tests/dynamicCatalogIngestion.test.js +285 -0
  42. package/server/agent/tests/dynamicRoutingAndSearch.test.js +92 -0
  43. package/server/agent/tests/edgeCasesScenarios.test.js +49 -0
  44. package/server/agent/tests/entertainmentMetrics.test.js +126 -0
  45. package/server/agent/tests/envBootstrap.test.js +87 -0
  46. package/server/agent/tests/envCommand.test.js +215 -0
  47. package/server/agent/tests/errorHandling.test.js +80 -0
  48. package/server/agent/tests/errors.test.js +13 -0
  49. package/server/agent/tests/evalAnalyze.test.js +54 -0
  50. package/server/agent/tests/evalCommand.test.js +54 -0
  51. package/server/agent/tests/evalCorpus.test.js +126 -0
  52. package/server/agent/tests/evalHarness.test.js +62 -0
  53. package/server/agent/tests/evalJudgeContext.test.js +38 -0
  54. package/server/agent/tests/evalMetrics.test.js +312 -0
  55. package/server/agent/tests/evalStore.test.js +190 -0
  56. package/server/agent/tests/evalStudio.test.js +179 -0
  57. package/server/agent/tests/feedbackLearning.test.js +186 -0
  58. package/server/agent/tests/firstRun.test.js +44 -0
  59. package/server/agent/tests/gemma4eOptimization.test.js +95 -0
  60. package/server/agent/tests/hardwareTiers.test.js +206 -0
  61. package/server/agent/tests/hybridWebSearch.test.js +192 -0
  62. package/server/agent/tests/judgeService.test.js +251 -0
  63. package/server/agent/tests/lanDiscovery.test.js +319 -0
  64. package/server/agent/tests/mcpClient.test.js +201 -0
  65. package/server/agent/tests/modelManager.test.js +109 -0
  66. package/server/agent/tests/multilingualScenarios.test.js +69 -0
  67. package/server/agent/tests/newCommands.test.js +347 -0
  68. package/server/agent/tests/newCommandsTui.test.js +141 -0
  69. package/server/agent/tests/newTools.test.js +113 -0
  70. package/server/agent/tests/newsIntelligence.test.js +178 -0
  71. package/server/agent/tests/nextGenArchitecture.test.js +99 -0
  72. package/server/agent/tests/offlineInput.test.js +217 -0
  73. package/server/agent/tests/packagingGuard.test.js +57 -0
  74. package/server/agent/tests/presentation.test.js +61 -0
  75. package/server/agent/tests/providerPersistence.test.js +39 -0
  76. package/server/agent/tests/queryBenchmarks.test.js +83 -0
  77. package/server/agent/tests/queryDetails.test.js +126 -0
  78. package/server/agent/tests/realWorldValidation.test.js +391 -0
  79. package/server/agent/tests/resilienceAndConcurrency.test.js +117 -0
  80. package/server/agent/tests/runtimeWatchdog.test.js +32 -0
  81. package/server/agent/tests/sanitizer.test.js +37 -0
  82. package/server/agent/tests/scenariosData.js +378 -0
  83. package/server/agent/tests/schemaRouting.test.js +54 -0
  84. package/server/agent/tests/securityAuditRemediation.test.js +214 -0
  85. package/server/agent/tests/securityHardening.test.js +146 -0
  86. package/server/agent/tests/securitySanitizer.test.js +38 -0
  87. package/server/agent/tests/sessionAndCopy.test.js +232 -0
  88. package/server/agent/tests/sidebarControls.test.js +101 -0
  89. package/server/agent/tests/statefulMemory.test.js +270 -0
  90. package/server/agent/tests/subagent.test.js +216 -0
  91. package/server/agent/tests/subagentProgress.test.js +109 -0
  92. package/server/agent/tests/subagentTextFallback.test.js +115 -0
  93. package/server/agent/tests/suggestions.test.js +199 -0
  94. package/server/agent/tests/suggestionsTui.test.js +251 -0
  95. package/server/agent/tests/telemetryAndProfiles.test.js +179 -0
  96. package/server/agent/tests/terminalImage.test.js +80 -0
  97. package/server/agent/tests/terminalMarkdown.test.js +166 -0
  98. package/server/agent/tests/terminalText.test.js +133 -0
  99. package/server/agent/tests/textProtocolLoop.test.js +126 -0
  100. package/server/agent/tests/textToolParsing.test.js +139 -0
  101. package/server/agent/tests/theme.test.js +26 -0
  102. package/server/agent/tests/toolRegistryParity.test.js +53 -0
  103. package/server/agent/tests/toolRoutingRegressions.test.js +119 -0
  104. package/server/agent/tests/tools.test.js +259 -0
  105. package/server/agent/tests/unifiedEndpoints.test.js +152 -0
  106. package/server/agent/tests/v110Features.test.js +262 -0
  107. package/server/agent/tests/versionBump.test.js +69 -0
  108. package/server/agent/tests/vramEstimator.test.js +520 -0
  109. package/server/agent/tests/webChatInspection.test.js +300 -0
  110. package/server/agent/tests/webChatMemory.test.js +112 -0
  111. package/server/agent/tests/webChatSecurity.test.js +169 -0
  112. package/server/agent/tests/webExtract.test.js +261 -0
  113. package/server/agent/tests/webSources.test.js +247 -0
  114. package/server/eval/judgeRubrics.js +110 -0
  115. package/server/eval/judgeService.js +290 -0
  116. package/server/eval/runBenchmark.js +182 -0
  117. package/server/mcp/examples/librechat.example.yaml +16 -0
  118. package/server/mcp/tests/connection.test.js +156 -0
  119. package/server/supabase_schema.sql +63 -0
@@ -0,0 +1,520 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import {
7
+ GIB,
8
+ QUANT_BPW,
9
+ QUANT_LADDER,
10
+ ALLOCATION_THRESHOLDS,
11
+ SWA_ARCH_PROFILES,
12
+ knownSwaProfile,
13
+ parseBpw,
14
+ normalizeQuantName,
15
+ bytesToGb,
16
+ estimateWeightsBytes,
17
+ estimateKvCacheBytes,
18
+ computeKvBytesPerToken,
19
+ estimateActivationBytes,
20
+ estimateVramFootprint,
21
+ classifyAllocation,
22
+ recommendAdjustments,
23
+ } from '../../../src/lib/vram/estimator.js';
24
+ import { findRegistryEntry, MODEL_REGISTRY } from '../../../src/lib/vram/modelRegistry.js';
25
+ import { parseGgufMetadata, parseHfConfigJson, extractQuantFromName, listInstalledGgufs } from '../modelConfig.js';
26
+ import { computeVramEstimate, parseEstimateRequest, probeLmStudioRuntime } from '../vramService.js';
27
+ import { computePercentile, TokenTracker } from '../tokenTracker.js';
28
+ import { routeCommand } from '../commands.js';
29
+
30
+ test('VRAM estimator: quant table matches the calibrated bits-per-weight values', () => {
31
+ assert.equal(QUANT_BPW.fp16, 16.0);
32
+ assert.equal(QUANT_BPW.q8_0, 8.0);
33
+ assert.equal(QUANT_BPW.q6_k, 6.56);
34
+ assert.equal(QUANT_BPW.q5_k_m, 5.5);
35
+ assert.equal(QUANT_BPW.q4_k_m, 4.5);
36
+ assert.equal(QUANT_BPW.q4_0, 4.2);
37
+ assert.equal(QUANT_BPW.q3_k_m, 3.4);
38
+ assert.equal(QUANT_BPW.q2_k, 2.6);
39
+ });
40
+
41
+ test('VRAM estimator: quant names normalize across separators and aliases', () => {
42
+ assert.equal(normalizeQuantName('Q4_K_M'), 'q4_k_m');
43
+ assert.equal(normalizeQuantName('q4-k-m'), 'q4_k_m');
44
+ assert.equal(normalizeQuantName('Q6K'), 'q6_k');
45
+ assert.equal(parseBpw('Q8_0'), 8.0);
46
+ assert.equal(parseBpw(5.5), 5.5);
47
+ assert.equal(parseBpw('nonsense-quant'), null);
48
+ });
49
+
50
+ test('VRAM estimator: weights use P * bpw / 8 in binary gigabytes', () => {
51
+ const { bytes } = estimateWeightsBytes({ paramsB: 32.5, quant: 'q4_k_m' });
52
+ assert.equal(bytes, Math.round(32.5e9 * 4.5 / 8));
53
+ assert.equal(bytesToGb(bytes).toFixed(2), (Math.round(32.5e9 * 4.5 / 8) / GIB).toFixed(2));
54
+ assert.equal(estimateWeightsBytes({ quant: 'q4_k_m' }).bytes, null);
55
+ });
56
+
57
+ test('VRAM estimator: KV cache formula and rapid per-token checkpoint factor', () => {
58
+ const geo = { layers: 32, kvHeads: 8, headDim: 128 };
59
+
60
+ assert.equal(computeKvBytesPerToken({ ...geo, cacheBits: 16 }), 4 * 32 * 8 * 128);
61
+ const ctx = 8192;
62
+ const { bytes } = estimateKvCacheBytes({ ...geo, contextLength: ctx, cacheBits: 16 });
63
+ assert.equal(bytes, 4 * 32 * 8 * 128 * ctx);
64
+
65
+ const q8 = estimateKvCacheBytes({ ...geo, contextLength: ctx, cacheBits: 8 });
66
+ assert.equal(q8.bytes, bytes / 2);
67
+ assert.equal(estimateKvCacheBytes({ layers: 32 }).bytes, null);
68
+ });
69
+
70
+ test('VRAM estimator: SWA hybrid KV keeps full context only on global layers', () => {
71
+
72
+ const geo = { layers: 42, kvHeads: 2, headDim: 512, cacheBits: 16 };
73
+ const perLayer = 2 * 2 * 512 * 2;
74
+ const ctx = 16384;
75
+ const swa = { windowTokens: 512, patternLocal: 5, patternGlobal: 1 };
76
+
77
+ const hybrid = estimateKvCacheBytes({ ...geo, contextLength: ctx, swa });
78
+ assert.equal(hybrid.kvModel, 'swa-hybrid');
79
+ assert.equal(hybrid.swa.globalLayers, 7);
80
+ assert.equal(hybrid.swa.localLayers, 35);
81
+ assert.equal(hybrid.bytes, perLayer * (7 * ctx + 35 * 512));
82
+
83
+ const full = estimateKvCacheBytes({ ...geo, contextLength: ctx });
84
+ assert.equal(full.kvModel, 'full-attention');
85
+ assert.equal(full.swa, null);
86
+ assert.equal(full.bytes, perLayer * 42 * ctx);
87
+ assert.ok(hybrid.bytes * 4 < full.bytes, 'SWA modeling must not be a marginal correction');
88
+
89
+
90
+ const tiny = estimateKvCacheBytes({ ...geo, contextLength: 512, swa });
91
+ assert.equal(tiny.bytes, perLayer * 42 * 512);
92
+ });
93
+
94
+ test('VRAM estimator: SWA profiles resolve per architecture and validate inputs', () => {
95
+ assert.deepEqual(knownSwaProfile('gemma4'), { windowTokens: 1024, patternLocal: 5, patternGlobal: 1 });
96
+ assert.deepEqual(knownSwaProfile('gemma3n-something'), { windowTokens: 512, patternLocal: 5, patternGlobal: 1 });
97
+ assert.equal(knownSwaProfile('qwen2'), null);
98
+ assert.equal(knownSwaProfile(null), null);
99
+ assert.ok(SWA_ARCH_PROFILES.gemma3.windowTokens > 0);
100
+ assert.equal(estimateKvCacheBytes({
101
+ layers: 42, kvHeads: 2, headDim: 512, contextLength: 4096, swa: { windowTokens: 0 },
102
+ }).kvModel, 'full-attention', 'an invalid window falls back to the full-attention formula');
103
+ });
104
+
105
+ test('VRAM estimator: full footprint carries the SWA-aware KV model through', () => {
106
+ const base = {
107
+ paramsB: 7.52, quant: 'q4_k_m', layers: 42, queryHeads: 8, kvHeads: 2,
108
+ headDim: 512, hiddenSize: 2560, contextLength: 16384,
109
+ };
110
+ const worstCase = estimateVramFootprint(base);
111
+ assert.equal(worstCase.kvModel, 'full-attention');
112
+ const hybrid = estimateVramFootprint({
113
+ ...base,
114
+ swa: { windowTokens: 512, patternLocal: 5, patternGlobal: 1 },
115
+ });
116
+ assert.equal(hybrid.kvModel, 'swa-hybrid');
117
+ assert.equal(hybrid.swa.globalLayers, 7);
118
+ assert.equal(hybrid.kvCacheBytes, 2 * 2 * 512 * 2 * (7 * 16384 + 35 * 512));
119
+ assert.ok(hybrid.totalBytes < worstCase.totalBytes);
120
+ assert.equal(hybrid.warnings.length, 0);
121
+ });
122
+
123
+ test('VRAM estimator: activation reserves the static workspace and exposes the analytic value', () => {
124
+ const { analyticBytes, reservedBytes } = estimateActivationBytes({
125
+ hiddenSize: 4096, queryHeads: 32, kvHeads: 8,
126
+ });
127
+ assert.equal(reservedBytes, Math.round(0.25 * GIB));
128
+ assert.equal(analyticBytes, Math.round(4096 * (34 + 5 * 4) * 2));
129
+ assert.equal(estimateActivationBytes({}).reservedBytes, Math.round(0.25 * GIB));
130
+ });
131
+
132
+ test('VRAM estimator: full footprint sums weights + KV + activation + overhead', () => {
133
+ const estimate = estimateVramFootprint({
134
+ modelId: 'test-model',
135
+ paramsB: 8,
136
+ quant: 'q4_k_m',
137
+ layers: 32,
138
+ queryHeads: 32,
139
+ kvHeads: 8,
140
+ headDim: 128,
141
+ hiddenSize: 4096,
142
+ contextLength: 8192,
143
+ });
144
+ const expectedWeights = Math.round(8e9 * 4.5 / 8);
145
+ const expectedKv = 4 * 32 * 8 * 128 * 8192;
146
+ assert.equal(estimate.weightsBytes, expectedWeights);
147
+ assert.equal(estimate.kvCacheBytes, expectedKv);
148
+ assert.equal(estimate.activationBytes, Math.round(0.25 * GIB));
149
+ assert.equal(estimate.overheadBytes, Math.round(0.7 * GIB));
150
+ assert.equal(estimate.totalBytes, expectedWeights + expectedKv + Math.round(0.25 * GIB) + Math.round(0.7 * GIB));
151
+ assert.deepEqual(estimate.warnings, []);
152
+ });
153
+
154
+ test('VRAM estimator: VLM correction extends the context with image tokens', () => {
155
+ const base = {
156
+ paramsB: 7.25, quant: 'q4_k_m', layers: 32, queryHeads: 32, kvHeads: 8,
157
+ headDim: 128, hiddenSize: 4096, contextLength: 4096,
158
+ };
159
+ const plain = estimateVramFootprint(base);
160
+ const withVision = estimateVramFootprint({
161
+ ...base,
162
+ vision: { images: 4, tokensPerImage: 576, encoderParamsB: 0.42 },
163
+ });
164
+ assert.equal(withVision.effectiveContextLength, 4096 + 4 * 576);
165
+ assert.ok(withVision.kvCacheBytes > plain.kvCacheBytes);
166
+ assert.ok(withVision.visionEncoderBytes > 0);
167
+ assert.ok(withVision.totalBytes > plain.totalBytes);
168
+ });
169
+
170
+ test('VRAM estimator: allocation zones partition at 85% and 100%', () => {
171
+ const available = 24 * GIB;
172
+ const green = classifyAllocation({ totalBytes: 0.85 * available, availableBytes: available });
173
+ const yellow = classifyAllocation({ totalBytes: 0.90 * available, availableBytes: available });
174
+ const tight = classifyAllocation({ totalBytes: available, availableBytes: available });
175
+ const over = classifyAllocation({ totalBytes: 25.4 * GIB, availableBytes: available });
176
+ assert.equal(green.zone, 'GREEN');
177
+ assert.equal(yellow.zone, 'YELLOW');
178
+ assert.equal(tight.zone, 'YELLOW', 'an allocation that exactly fills the card still fits');
179
+ assert.equal(over.zone, 'RED');
180
+ assert.match(over.message, /Out of memory/);
181
+ assert.ok(over.headroomBytes < 0);
182
+ assert.ok(over.requiresForce);
183
+ assert.ok(!green.requiresForce);
184
+ assert.equal(classifyAllocation({ totalBytes: 20, availableBytes: null }).zone, 'UNKNOWN');
185
+ });
186
+
187
+ test('VRAM estimator: remediation suggests context, KV-quant or weight changes into GREEN', () => {
188
+ const available = 16 * GIB;
189
+ const estimate = estimateVramFootprint({
190
+ paramsB: 14.77, quant: 'q4_k_m', layers: 48, queryHeads: 40, kvHeads: 8,
191
+ headDim: 128, hiddenSize: 5120, contextLength: 32768,
192
+ });
193
+ assert.equal(classifyAllocation({ totalBytes: estimate.totalBytes, availableBytes: available }).zone, 'YELLOW');
194
+ const suggestions = recommendAdjustments(estimate, available);
195
+ assert.ok(suggestions.length >= 1);
196
+ for (const suggestion of suggestions) {
197
+ assert.ok(['context', 'kv-quant', 'quantization'].includes(suggestion.kind));
198
+ assert.ok(suggestion.projectedBytes <= available * ALLOCATION_THRESHOLDS.green);
199
+ }
200
+ assert.ok(suggestions.some(s => s.kind === 'context'), 'exact max-context suggestion expected');
201
+ assert.ok(suggestions.some(s => s.kind === 'kv-quant'), 'Q8_0 KV cache suggestion expected');
202
+ });
203
+
204
+ test('VRAM estimator: registry resolves known model families', () => {
205
+ const gemma = findRegistryEntry('google/gemma-4-e4b');
206
+ assert.ok(gemma);
207
+ assert.equal(gemma.paramsB, 8.0);
208
+ const qwen = findRegistryEntry('Qwen2.5-Coder-32B-Instruct-Q4_K_M.gguf');
209
+ assert.ok(qwen);
210
+ assert.equal(qwen.layers, 64);
211
+ assert.equal(findRegistryEntry('totally-unknown-model-xyz'), null);
212
+ assert.ok(MODEL_REGISTRY.length >= 10);
213
+ });
214
+
215
+ test('VRAM estimator: registry covers LM Studio tool-calling, vision and reasoning models', () => {
216
+ for (const entry of MODEL_REGISTRY) {
217
+ assert.ok(Array.isArray(entry.capabilities) && entry.capabilities.length >= 1, `capabilities missing on ${entry.label}`);
218
+ assert.ok(entry.capabilities.every(c => ['tools', 'vision', 'reasoning'].includes(c)), `bad capability on ${entry.label}`);
219
+ assert.ok(entry.paramsB > 0 && entry.layers > 0 && entry.kvHeads > 0 && entry.headDim > 0, `geometry missing on ${entry.label}`);
220
+ assert.ok(typeof entry.verified === 'string' && entry.verified.length > 0, `verification tag missing on ${entry.label}`);
221
+ }
222
+
223
+ const qwen3 = findRegistryEntry('Qwen/Qwen3-8B');
224
+ assert.deepEqual([...qwen3.capabilities].sort(), ['reasoning', 'tools']);
225
+ assert.equal(findRegistryEntry('openai/gpt-oss-20b').isMoE, true);
226
+ assert.ok(MODEL_REGISTRY.filter(e => e.capabilities.includes('vision')).length >= 6);
227
+ assert.ok(MODEL_REGISTRY.filter(e => e.capabilities.includes('reasoning')).length >= 5);
228
+ assert.ok(MODEL_REGISTRY.filter(e => e.capabilities.includes('tools')).length >= 8);
229
+ });
230
+
231
+ test('VRAM service: GPT-OSS falls back to its native MXFP4 quantization', async () => {
232
+ const result = await computeVramEstimate({
233
+ model: 'gpt-oss-20b',
234
+ includeHardware: false,
235
+ hardwareOverrideGb: 24,
236
+ });
237
+ assert.equal(result.configSource, 'registry');
238
+ assert.equal(result.input.quant, 'mxfp4');
239
+ assert.equal(result.estimate.quantBpw, 4.25);
240
+ assert.equal(result.estimate.layers, 24);
241
+ assert.equal(result.estimate.paramCount, Math.round(20.91e9));
242
+ assert.equal(result.classification.zone, 'GREEN');
243
+ });
244
+
245
+ test('GGUF parser: reads header metadata and sums tensor parameter counts', () => {
246
+
247
+ const parts = [];
248
+ const w = (fn) => { const buf = Buffer.alloc(8); fn(buf); parts.push(buf); };
249
+ const str = (s) => {
250
+ const bytes = Buffer.from(s, 'utf8');
251
+ const len = Buffer.alloc(8);
252
+ len.writeBigUInt64LE(BigInt(bytes.length));
253
+ parts.push(len, bytes);
254
+ };
255
+
256
+ parts.push(Buffer.from([0x47, 0x47, 0x55, 0x46]));
257
+ const version = Buffer.alloc(4); version.writeUInt32LE(3); parts.push(version);
258
+ const tensorCount = Buffer.alloc(8); tensorCount.writeBigUInt64LE(2n); parts.push(tensorCount);
259
+ const kvCount = Buffer.alloc(8); kvCount.writeBigUInt64LE(8n); parts.push(kvCount);
260
+
261
+ str('general.architecture');
262
+ const strType = Buffer.alloc(4); strType.writeUInt32LE(8); parts.push(strType);
263
+ str('qwen25');
264
+ const kv = (key, type, value) => {
265
+ str(key);
266
+ const t = Buffer.alloc(4); t.writeUInt32LE(type); parts.push(t);
267
+ if (type === 8) str(value); else { const v = Buffer.alloc(4); v.writeUInt32LE(value); parts.push(v); }
268
+ };
269
+ kv('qwen25.block_count', 4, 28);
270
+ kv('qwen25.attention.head_count', 4, 28);
271
+ kv('qwen25.attention.head_count_kv', 4, 4);
272
+ kv('qwen25.attention.key_length', 4, 128);
273
+ kv('qwen25.embedding_length', 4, 3584);
274
+ kv('qwen25.context_length', 4, 32768);
275
+ kv('qwen25.attention.swa_size', 4, 1024);
276
+
277
+ const tensor = (name, dims) => {
278
+ str(name);
279
+ const nDims = Buffer.alloc(4); nDims.writeUInt32LE(dims.length); parts.push(nDims);
280
+ for (const d of dims) {
281
+ const dim = Buffer.alloc(8); dim.writeBigUInt64LE(BigInt(d)); parts.push(dim);
282
+ }
283
+ const type = Buffer.alloc(4); type.writeUInt32LE(0); parts.push(type);
284
+ const offset = Buffer.alloc(8); offset.writeBigUInt64LE(0n); parts.push(offset);
285
+ };
286
+ tensor('token_embd.weight', [3584, 152064]);
287
+ tensor('blk.0.attn_q.weight', [3584, 3584]);
288
+
289
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kiku-gguf-'));
290
+ const file = path.join(dir, 'test-model-Q4_K_M.gguf');
291
+ fs.writeFileSync(file, Buffer.concat(parts));
292
+
293
+ const meta = parseGgufMetadata(file);
294
+ assert.ok(meta && !meta.error, meta && meta.error ? meta.error : 'metadata missing');
295
+ assert.equal(meta.arch, 'qwen25');
296
+ assert.equal(meta.layers, 28);
297
+ assert.equal(meta.queryHeads, 28);
298
+ assert.equal(meta.kvHeads, 4);
299
+ assert.equal(meta.headDim, 128);
300
+ assert.equal(meta.hiddenSize, 3584);
301
+ assert.equal(meta.contextLength, 32768);
302
+ assert.deepEqual(meta.swa, { windowTokens: 1024 });
303
+ assert.equal(meta.quant, 'q4_k_m');
304
+ assert.equal(meta.paramCount, 3584 * 152064 + 3584 * 3584);
305
+
306
+ fs.rmSync(dir, { recursive: true, force: true });
307
+ });
308
+
309
+ test('GGUF parser: rejects non-GGUF payloads and extracts quants from names', () => {
310
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kiku-gguf-'));
311
+ const file = path.join(dir, 'not-a-model.gguf');
312
+ fs.writeFileSync(file, Buffer.from('definitely not gguf'));
313
+ assert.equal(parseGgufMetadata(file), null);
314
+ fs.rmSync(dir, { recursive: true, force: true });
315
+
316
+ assert.equal(extractQuantFromName('model-Q4_K_M.gguf'), 'q4_k_m');
317
+ assert.equal(extractQuantFromName('gemma-4-e4b-Q8_0'), 'q8_0');
318
+ assert.equal(extractQuantFromName('no-quant-here'), null);
319
+ });
320
+
321
+ test('HF config parser: normalizes architecture fields and VLM vision configs', () => {
322
+ const config = parseHfConfigJson({
323
+ model_type: 'qwen2',
324
+ num_hidden_layers: 28,
325
+ num_attention_heads: 28,
326
+ num_key_value_heads: 4,
327
+ hidden_size: 3584,
328
+ max_position_embeddings: 32768,
329
+ sliding_window: 1024,
330
+ }, 'Qwen/Qwen2.5-7B-Instruct');
331
+ assert.equal(config.layers, 28);
332
+ assert.equal(config.kvHeads, 4);
333
+ assert.equal(config.headDim, 128);
334
+ assert.equal(config.contextLength, 32768);
335
+ assert.deepEqual(config.swa, { windowTokens: 1024 });
336
+ assert.equal(config.vision, null);
337
+
338
+ const vlm = parseHfConfigJson({
339
+ model_type: 'llava',
340
+ num_hidden_layers: 32,
341
+ num_attention_heads: 32,
342
+ hidden_size: 4096,
343
+ vision_config: { hidden_size: 1024, num_hidden_layers: 24, patch_size: 14, image_size: 336 },
344
+ });
345
+ assert.ok(vlm.vision);
346
+ assert.equal(vlm.vision.tokensPerImage, 577);
347
+ assert.ok(vlm.vision.encoderParamsB > 0.1 && vlm.vision.encoderParamsB < 2);
348
+ });
349
+
350
+ test('VRAM service: estimate request resolves the built-in registry and classifies with override hardware', async () => {
351
+ const result = await computeVramEstimate({
352
+ model: 'Qwen2.5-Coder-32B-Instruct',
353
+ quant: 'q4_k_m',
354
+ contextLength: 16384,
355
+ includeHardware: false,
356
+ hardwareOverrideGb: 24,
357
+ });
358
+ assert.equal(result.configSource, 'registry');
359
+ assert.equal(result.architecture.layers, 64);
360
+ assert.equal(result.estimate.contextLength, 16384);
361
+ assert.equal(result.classification.zone, 'YELLOW');
362
+ assert.ok(result.estimate.totalBytes > 0);
363
+
364
+ assert.ok(result.classification.usagePct > 80 && result.classification.usagePct < 95);
365
+
366
+ const huge = await computeVramEstimate({
367
+ model: 'Qwen2.5-Coder-32B-Instruct',
368
+ quant: 'fp16',
369
+ contextLength: 32768,
370
+ includeHardware: false,
371
+ hardwareOverrideGb: 24,
372
+ });
373
+ assert.equal(huge.classification.zone, 'RED');
374
+ assert.ok(huge.remediation.length >= 1);
375
+ });
376
+
377
+ test('VRAM service: parseEstimateRequest merges body and query params', () => {
378
+ const searchParams = new URLSearchParams('ctx=8192&quant=q6_k');
379
+ const request = parseEstimateRequest({ model: 'llama-3.1-8b' }, searchParams);
380
+ assert.equal(request.model, 'llama-3.1-8b');
381
+ assert.equal(request.contextLength, 8192);
382
+ assert.equal(request.quant, 'q6_k');
383
+ assert.equal(parseEstimateRequest({ model: 'x', includeHardware: false }).includeHardware, false);
384
+ assert.equal(parseEstimateRequest({}).includeHardware, true);
385
+ });
386
+
387
+ test('TokenTracker: computes TTFT, decode throughput, ITL percentiles and context usage', async () => {
388
+ const tracker = new TokenTracker({ sessionId: 'sess_test', modelId: 'test-model', contextLimit: 32768 });
389
+ tracker.start();
390
+ const segment = tracker.beginSegment('iteration-1');
391
+ tracker.markFirstToken(segment);
392
+
393
+ for (let i = 0; i < 10; i++) {
394
+ segment.tokenTimestamps.push(segment.tFirst + 20 * (i + 1));
395
+ }
396
+ tracker.endSegment({ promptTokens: 1420, completionTokens: 384 }, segment);
397
+ segment.tEnd = segment.tFirst + 200;
398
+ await new Promise(r => setTimeout(r, 5));
399
+ const payload = tracker.finish({ promptTokens: 1420, completionTokens: 384 });
400
+
401
+ assert.equal(payload.session_id, 'sess_test');
402
+ assert.equal(payload.model_id, 'test-model');
403
+ assert.equal(payload.counts.prompt_tokens, 1420);
404
+ assert.equal(payload.counts.completion_tokens, 384);
405
+ assert.equal(payload.counts.total_tokens, 1804);
406
+ assert.equal(payload.counts.remaining_tokens, 32768 - 1804);
407
+ assert.ok(payload.timing_ms.ttft >= 0);
408
+ assert.equal(payload.timing_ms.decode_duration, 200);
409
+ assert.ok(Math.abs(payload.rates.decode_tps - 383 / 0.2) < 0.1);
410
+ assert.equal(payload.rates.itl_mean_ms, 20);
411
+ assert.ok(payload.rates.itl_p95_ms <= 20);
412
+ assert.ok(payload.context_usage_pct > 5 && payload.context_usage_pct < 6);
413
+ assert.ok(payload.rates.e2e_tps > 0);
414
+ });
415
+
416
+ test('TokenTracker: aggregates multiple streaming segments and computes p95', () => {
417
+ const tracker = new TokenTracker({ modelId: 'm', contextLimit: 0 });
418
+ tracker.start();
419
+ const seg1 = tracker.beginSegment('a');
420
+ seg1.tFirst = seg1.tStart + 100;
421
+ for (let i = 0; i < 4; i++) seg1.tokenTimestamps.push(seg1.tFirst + 10 * (i + 1));
422
+ tracker.endSegment({ promptTokens: 100, completionTokens: 50 }, seg1);
423
+ seg1.tEnd = seg1.tFirst + 40;
424
+
425
+ const seg2 = tracker.beginSegment('b');
426
+ seg2.tFirst = seg2.tStart + 200;
427
+ for (let i = 0; i < 6; i++) seg2.tokenTimestamps.push(seg2.tFirst + 30 * (i + 1));
428
+ tracker.endSegment({ promptTokens: 200, completionTokens: 70 }, seg2);
429
+ seg2.tEnd = seg2.tFirst + 180;
430
+
431
+ const payload = tracker.finish({ promptTokens: 300, completionTokens: 120 });
432
+ assert.equal(payload.stream_segments, 2);
433
+ assert.equal(payload.counts.prompt_tokens, 300);
434
+ assert.equal(payload.counts.completion_tokens, 120);
435
+ assert.equal(payload.timing_ms.decode_duration, 40 + 180);
436
+ assert.equal(payload.counts.context_limit, null);
437
+ assert.equal(payload.context_usage_pct, null);
438
+
439
+ assert.equal(computePercentile([...Array(10).keys()], 0.95), 9);
440
+ });
441
+
442
+ test('TokenTracker: reported usage falls back to counted stream deltas', () => {
443
+ const tracker = new TokenTracker({ modelId: 'm' });
444
+ tracker.start();
445
+ const segment = tracker.beginSegment();
446
+ tracker.markToken(segment);
447
+ tracker.markToken(segment);
448
+ tracker.markToken(segment);
449
+ tracker.endSegment({}, segment);
450
+ const payload = tracker.finish();
451
+ assert.equal(payload.counts.completion_tokens, 3);
452
+ assert.ok(payload.timing_ms.ttft >= 0);
453
+ });
454
+
455
+ test('VRAM estimator: quant ladder is exported in descending bpw order', () => {
456
+ assert.ok(QUANT_LADDER.length >= 9);
457
+ for (let i = 1; i < QUANT_LADDER.length; i++) {
458
+ assert.ok(parseBpw(QUANT_LADDER[i - 1]) >= parseBpw(QUANT_LADDER[i]));
459
+ }
460
+ });
461
+
462
+ test('VRAM service: probeLmStudioRuntime degrades gracefully when offline', async () => {
463
+ const result = await probeLmStudioRuntime('http://127.0.0.1:9');
464
+ assert.equal(result.reachable, false);
465
+ assert.equal(result.loadedModel, null);
466
+ assert.equal(result.loadedContextLength, null);
467
+ });
468
+
469
+ test('/vram: live, compare, ctx, list, check and default subcommands render correctly', async () => {
470
+ const agent = { client: { model: 'qwen2.5-7b-instruct', provider: 'lmstudio', baseUrl: 'http://127.0.0.1:9' } };
471
+ assert.equal(await routeCommand('/vram', { agent }), 'HANDLED');
472
+ assert.equal(await routeCommand('/vram list', { agent }), 'HANDLED');
473
+ assert.equal(await routeCommand('/vram live', { agent }), 'HANDLED');
474
+ assert.equal(await routeCommand('/vram compare --ctx 16384', { agent }), 'HANDLED');
475
+ assert.equal(await routeCommand('/vram ctx', { agent }), 'HANDLED');
476
+ assert.equal(await routeCommand('/vram ctx --quant q4_k_m', { agent }), 'HANDLED');
477
+ assert.equal(await routeCommand('/vram check google/gemma-4-e4b', { agent }), 'HANDLED');
478
+ assert.equal(await routeCommand('/vram compare', {}), 'HANDLED', 'falls back to registry defaults without an agent');
479
+ assert.equal(await routeCommand('/resource', { agent }), 'HANDLED');
480
+ });
481
+
482
+ test('VRAM estimator: missing weights yields null totalBytes and UNKNOWN classification', () => {
483
+ const estimate = estimateVramFootprint({ modelId: 'cloud-model-gemini', contextLength: 8192 });
484
+ assert.equal(estimate.weightsBytes, null);
485
+ assert.equal(estimate.totalBytes, null);
486
+ assert.equal(estimate.gb.total, null);
487
+ assert.equal(estimate.gb.weights, null);
488
+ assert.ok(estimate.warnings.some(w => w.includes('Parameter count unknown')));
489
+
490
+ const classification = classifyAllocation({ totalBytes: estimate.totalBytes, availableBytes: 6 * GIB });
491
+ assert.equal(classification.zone, 'UNKNOWN');
492
+ assert.equal(classification.usagePct, null);
493
+ assert.equal(classification.requiresForce, false);
494
+ });
495
+
496
+ test('VRAM service: auto model resolves to local GGUF or registry entry with realistic footprint', async () => {
497
+ const result = await computeVramEstimate({ model: 'auto', contextLength: 4096 });
498
+ assert.equal(result.ok, true);
499
+ assert.ok(result.estimate.weightsBytes > 0, 'weights should not be null for auto');
500
+ assert.ok(result.estimate.totalBytes > 0, 'totalBytes should not be null for auto');
501
+ assert.ok(['GREEN', 'YELLOW', 'RED'].includes(result.classification.zone));
502
+ });
503
+
504
+ test('VRAM service: unknown cloud model produces UNKNOWN classification without fake numbers', async () => {
505
+ const result = await computeVramEstimate({ model: 'gemini-3.7-flash', allowNetwork: false });
506
+ assert.equal(result.estimate.weightsBytes, null);
507
+ assert.equal(result.estimate.totalBytes, null);
508
+ assert.equal(result.classification.zone, 'UNKNOWN');
509
+ assert.equal(result.classification.usagePct, null);
510
+ assert.equal(result.classification.requiresForce, false);
511
+ });
512
+
513
+ test('GGUF discovery: listInstalledGgufs returns array of models with valid metadata', () => {
514
+ const models = listInstalledGgufs();
515
+ assert.ok(Array.isArray(models));
516
+ for (const m of models) {
517
+ assert.ok(m.path && m.fileName);
518
+ assert.ok(typeof m.sizeGb === 'number' && m.sizeGb > 0);
519
+ }
520
+ });