@nxuss/lemma 0.5.2 → 0.5.4
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 +1 -1
- package/dashboard/dist/assets/index-0N21ZtpO.js +481 -0
- package/dashboard/dist/assets/index-0N21ZtpO.js.map +1 -0
- package/dashboard/dist/assets/index-DUOrThix.css +1 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/cjs/cli/lemma-proxy.d.ts.map +1 -1
- package/dist/cjs/cli/lemma-proxy.js +365 -113
- package/dist/cjs/cli/lemma-proxy.js.map +1 -1
- package/dist/cjs/config/index.d.ts.map +1 -1
- package/dist/cjs/config/index.js +2 -1
- package/dist/cjs/config/index.js.map +1 -1
- package/dist/cjs/errors/LemmaError.d.ts +52 -0
- package/dist/cjs/errors/LemmaError.d.ts.map +1 -0
- package/dist/cjs/errors/LemmaError.js +84 -0
- package/dist/cjs/errors/LemmaError.js.map +1 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +24 -22
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/subconscious/SemanticCache.d.ts +4 -4
- package/dist/cjs/subconscious/SemanticCache.d.ts.map +1 -1
- package/dist/cjs/subconscious/SemanticCache.js +14 -10
- package/dist/cjs/subconscious/SemanticCache.js.map +1 -1
- package/dist/cjs/types/index.d.ts +8 -8
- package/dist/cjs/types/index.d.ts.map +1 -1
- package/dist/cjs/utils/SavingsLedger.d.ts +2 -0
- package/dist/cjs/utils/SavingsLedger.d.ts.map +1 -1
- package/dist/cjs/utils/SavingsLedger.js +49 -0
- package/dist/cjs/utils/SavingsLedger.js.map +1 -1
- package/dist/cjs/utils/logger.d.ts +27 -6
- package/dist/cjs/utils/logger.d.ts.map +1 -1
- package/dist/cjs/utils/logger.js +96 -28
- package/dist/cjs/utils/logger.js.map +1 -1
- package/dist/esm/cli/lemma-proxy.d.ts.map +1 -1
- package/dist/esm/cli/lemma-proxy.js +365 -113
- package/dist/esm/cli/lemma-proxy.js.map +1 -1
- package/dist/esm/config/index.d.ts.map +1 -1
- package/dist/esm/config/index.js +2 -1
- package/dist/esm/config/index.js.map +1 -1
- package/dist/esm/errors/LemmaError.d.ts +52 -0
- package/dist/esm/errors/LemmaError.d.ts.map +1 -0
- package/dist/esm/errors/LemmaError.js +75 -0
- package/dist/esm/errors/LemmaError.js.map +1 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +24 -22
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/subconscious/SemanticCache.d.ts +4 -4
- package/dist/esm/subconscious/SemanticCache.d.ts.map +1 -1
- package/dist/esm/subconscious/SemanticCache.js +14 -10
- package/dist/esm/subconscious/SemanticCache.js.map +1 -1
- package/dist/esm/types/index.d.ts +8 -8
- package/dist/esm/types/index.d.ts.map +1 -1
- package/dist/esm/utils/SavingsLedger.d.ts +2 -0
- package/dist/esm/utils/SavingsLedger.d.ts.map +1 -1
- package/dist/esm/utils/SavingsLedger.js +46 -0
- package/dist/esm/utils/SavingsLedger.js.map +1 -1
- package/dist/esm/utils/logger.d.ts +27 -6
- package/dist/esm/utils/logger.d.ts.map +1 -1
- package/dist/esm/utils/logger.js +93 -28
- package/dist/esm/utils/logger.js.map +1 -1
- package/package.json +28 -24
- package/dashboard/dist/assets/index-CIlkFwDs.js +0 -490
- package/dashboard/dist/assets/index-CIlkFwDs.js.map +0 -1
- package/dashboard/dist/assets/index-DNqq3gA9.css +0 -1
|
@@ -32,7 +32,7 @@ function getVersion() {
|
|
|
32
32
|
catch { }
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
return '0.5.
|
|
35
|
+
return '0.5.3';
|
|
36
36
|
}
|
|
37
37
|
const VERSION = getVersion();
|
|
38
38
|
const ComplexityRouter_1 = __importDefault(require("../proxy/ComplexityRouter"));
|
|
@@ -86,13 +86,20 @@ async function readJson(file, fallback) {
|
|
|
86
86
|
catch { }
|
|
87
87
|
return fallback;
|
|
88
88
|
}
|
|
89
|
+
const writeQueues = new Map();
|
|
89
90
|
async function writeJson(file, data) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
const chain = writeQueues.get(file) || Promise.resolve();
|
|
92
|
+
const writeOperation = async () => {
|
|
93
|
+
try {
|
|
94
|
+
const tempFile = `${file}.${crypto_1.default.randomUUID ? crypto_1.default.randomUUID() : Math.random().toString(36).substring(2, 15)}.tmp`;
|
|
95
|
+
await fs_1.default.promises.writeFile(tempFile, JSON.stringify(data, null, 2));
|
|
96
|
+
await fs_1.default.promises.rename(tempFile, file);
|
|
97
|
+
}
|
|
98
|
+
catch { }
|
|
99
|
+
};
|
|
100
|
+
const nextChain = chain.then(writeOperation).catch(() => { });
|
|
101
|
+
writeQueues.set(file, nextChain);
|
|
102
|
+
return nextChain;
|
|
96
103
|
}
|
|
97
104
|
function projectHash(name) {
|
|
98
105
|
return crypto_1.default.createHash('sha1').update(name).digest('hex').slice(0, 12);
|
|
@@ -386,12 +393,21 @@ function startBackgroundClipboardWatcher() {
|
|
|
386
393
|
}
|
|
387
394
|
}
|
|
388
395
|
// ── License helpers ────────────────────────────────────────────────────────────
|
|
396
|
+
let isProCached = null;
|
|
397
|
+
let lastLicenseCheck = 0;
|
|
398
|
+
const LICENSE_CACHE_TTL = 10000; // 10 seconds cache
|
|
389
399
|
async function loadLicense() {
|
|
390
400
|
return readJson(LICENSE_FILE, { isPro: false });
|
|
391
401
|
}
|
|
392
402
|
async function isPro() {
|
|
403
|
+
const now = Date.now();
|
|
404
|
+
if (isProCached !== null && (now - lastLicenseCheck) < LICENSE_CACHE_TTL) {
|
|
405
|
+
return isProCached;
|
|
406
|
+
}
|
|
393
407
|
const l = await loadLicense();
|
|
394
|
-
|
|
408
|
+
isProCached = !!(l && l.isPro);
|
|
409
|
+
lastLicenseCheck = now;
|
|
410
|
+
return isProCached;
|
|
395
411
|
}
|
|
396
412
|
async function validateKeyRemote(key) {
|
|
397
413
|
return new Promise((resolve, reject) => {
|
|
@@ -454,16 +470,21 @@ async function recordStat(stats, project, fromCache, latencyMs, provider, tokens
|
|
|
454
470
|
s.total++;
|
|
455
471
|
s.totalLatency += latencyMs;
|
|
456
472
|
fromCache ? s.hits++ : s.misses++;
|
|
457
|
-
|
|
473
|
+
const tkSaved = fromCache ? (tokensSaved || 2000) : (tokensSaved || 0);
|
|
474
|
+
s.totalTokensSaved += tkSaved;
|
|
458
475
|
if (!s.providers[provider])
|
|
459
476
|
s.providers[provider] = { hits: 0, misses: 0 };
|
|
460
477
|
fromCache ? s.providers[provider].hits++ : s.providers[provider].misses++;
|
|
478
|
+
if (fromCache && tkSaved > 0) {
|
|
479
|
+
// Automatically report cache hits directly to the server session ledger for absolute sync consistency
|
|
480
|
+
SavingsLedger_1.savingsLedger.recordTokens('cache', tkSaved);
|
|
481
|
+
}
|
|
461
482
|
logEvent({
|
|
462
483
|
type: fromCache ? 'cache:hit' : 'cache:miss',
|
|
463
484
|
project,
|
|
464
485
|
latency: latencyMs,
|
|
465
486
|
provider,
|
|
466
|
-
tokens:
|
|
487
|
+
tokens: tkSaved
|
|
467
488
|
});
|
|
468
489
|
await writeJson(STATS_FILE, stats);
|
|
469
490
|
}
|
|
@@ -484,7 +505,15 @@ function cacheGet(provider, prompt) {
|
|
|
484
505
|
return e;
|
|
485
506
|
}
|
|
486
507
|
function cacheSet(provider, prompt, data) {
|
|
487
|
-
|
|
508
|
+
const MAX_CACHE_SIZE = 5000;
|
|
509
|
+
const k = cacheKey(provider, prompt);
|
|
510
|
+
if (!CACHE.has(k) && CACHE.size >= MAX_CACHE_SIZE) {
|
|
511
|
+
// Evict oldest cache entry (FIFO eviction)
|
|
512
|
+
const oldestKey = CACHE.keys().next().value;
|
|
513
|
+
if (oldestKey)
|
|
514
|
+
CACHE.delete(oldestKey);
|
|
515
|
+
}
|
|
516
|
+
CACHE.set(k, { input: prompt, data, createdAt: Date.now(), ttl: TTL_MS, provider });
|
|
488
517
|
}
|
|
489
518
|
// ── Semantic Search (Pro) ──────────────────────────────────────────────────────
|
|
490
519
|
async function getEmbedding(text) {
|
|
@@ -529,7 +558,7 @@ async function semanticGet(provider, prompt) {
|
|
|
529
558
|
distance = distances[0][i];
|
|
530
559
|
}
|
|
531
560
|
const similarity = Math.max(0, 1 - distance);
|
|
532
|
-
const THRESHOLD = 0.7
|
|
561
|
+
const THRESHOLD = 0.82; // raised from 0.7 to 0.82 to prevent false positives
|
|
533
562
|
if (similarity >= THRESHOLD) {
|
|
534
563
|
return {
|
|
535
564
|
data: JSON.parse(metadata.response),
|
|
@@ -756,6 +785,16 @@ async function relayMissStream(res, body, provider, tier) {
|
|
|
756
785
|
req.end();
|
|
757
786
|
});
|
|
758
787
|
}
|
|
788
|
+
class WriteQueue {
|
|
789
|
+
constructor() {
|
|
790
|
+
this.queue = Promise.resolve();
|
|
791
|
+
}
|
|
792
|
+
async enqueue(fn) {
|
|
793
|
+
this.queue = this.queue.then(fn).catch(() => { });
|
|
794
|
+
return this.queue;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
const usageWriteQueue = new WriteQueue();
|
|
759
798
|
// ── Proxy Server class ─────────────────────────────────────────────────────────
|
|
760
799
|
class LemmaServer {
|
|
761
800
|
constructor(port, projectName, openBrowser = true) {
|
|
@@ -818,6 +857,18 @@ class LemmaServer {
|
|
|
818
857
|
const hitRate = stats.total > 0 ? stats.hits / stats.total : 0;
|
|
819
858
|
const avgLat = stats.total > 0 ? stats.totalLatency / stats.total : 0;
|
|
820
859
|
const secretsMasked = EVENT_LOG.filter(e => e.type === 'privacy:mask').length;
|
|
860
|
+
const ledgerSnap = SavingsLedger_1.savingsLedger.getSnapshot();
|
|
861
|
+
let activeAgents = 1; // Always include the Gateway itself
|
|
862
|
+
let totalAgents = 1;
|
|
863
|
+
// Query the stack orchestration API to merge active agents
|
|
864
|
+
try {
|
|
865
|
+
const stackMetrics = await axios_1.default.get('http://127.0.0.1:8083/api/metrics', { timeout: 100 });
|
|
866
|
+
if (stackMetrics.data) {
|
|
867
|
+
activeAgents += stackMetrics.data.activeAgents || 0;
|
|
868
|
+
totalAgents += stackMetrics.data.totalAgents || 0;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
catch { }
|
|
821
872
|
res.json({
|
|
822
873
|
timestamp: new Date().toISOString(),
|
|
823
874
|
totalRequests: stats.total,
|
|
@@ -825,30 +876,140 @@ class LemmaServer {
|
|
|
825
876
|
cacheMisses: stats.misses,
|
|
826
877
|
hitRate,
|
|
827
878
|
averageLatency: avgLat,
|
|
828
|
-
tokensSaved:
|
|
829
|
-
costSaved:
|
|
879
|
+
tokensSaved: ledgerSnap.total.tokensSaved,
|
|
880
|
+
costSaved: ledgerSnap.total.costSaved,
|
|
830
881
|
secretsMasked: secretsMasked,
|
|
831
|
-
activeAgents
|
|
832
|
-
totalAgents
|
|
882
|
+
activeAgents,
|
|
883
|
+
totalAgents,
|
|
833
884
|
uptime: process.uptime(),
|
|
834
885
|
tier: (await isPro()) ? 'pro' : 'free'
|
|
835
886
|
});
|
|
836
887
|
});
|
|
837
888
|
this.app.get('/api/agents', async (req, res) => {
|
|
889
|
+
// Load statistics for the Gateway itself
|
|
838
890
|
const stats = await loadStats();
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
891
|
+
const statsProj = stats[this.projectName] || { total: 0, hits: 0 };
|
|
892
|
+
const total = statsProj.total || 0;
|
|
893
|
+
const hits = statsProj.hits || 0;
|
|
894
|
+
const gatewayAgent = {
|
|
895
|
+
id: 'lemma-gateway',
|
|
896
|
+
name: 'Lemma Gateway',
|
|
897
|
+
status: 'active',
|
|
898
|
+
capabilities: ['Privacy Firewall', 'Semantic Cache', 'Complexity Router'],
|
|
899
|
+
tasksCompleted: total,
|
|
900
|
+
lastSeen: new Date().toISOString(),
|
|
901
|
+
errorCount: 0,
|
|
902
|
+
cacheHitRate: total > 0 ? hits / total : 0
|
|
903
|
+
};
|
|
904
|
+
let agentsList = [gatewayAgent];
|
|
905
|
+
// Query the stack orchestration API to merge dynamic websocket agents if running
|
|
906
|
+
try {
|
|
907
|
+
const stackAgentsResponse = await axios_1.default.get('http://127.0.0.1:8083/api/agents', { timeout: 100 });
|
|
908
|
+
if (stackAgentsResponse.data && Array.isArray(stackAgentsResponse.data.agents)) {
|
|
909
|
+
const stackAgents = stackAgentsResponse.data.agents.filter((a) => a.id !== 'lemma-gateway');
|
|
910
|
+
agentsList = [...agentsList, ...stackAgents];
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
catch { }
|
|
914
|
+
res.json({ agents: agentsList });
|
|
842
915
|
});
|
|
843
|
-
this.app.get('/api/events', (req, res) => {
|
|
916
|
+
this.app.get('/api/events', async (req, res) => {
|
|
844
917
|
const page = parseInt(req.query.page) || 1;
|
|
845
918
|
const limit = parseInt(req.query.limit) || 50;
|
|
846
919
|
const start = (page - 1) * limit;
|
|
847
|
-
|
|
848
|
-
|
|
920
|
+
// Get local proxy events (normalized schema matching what dashboard expects)
|
|
921
|
+
const localEvents = EVENT_LOG.map(e => ({
|
|
922
|
+
id: e.id,
|
|
923
|
+
timestamp: new Date(e.timestamp).toISOString(),
|
|
924
|
+
type: e.type, // e.g., 'cache:hit' or 'cache:miss' which client normalizes to 'cache_hit' or 'cache_miss'
|
|
925
|
+
latency: e.latency,
|
|
926
|
+
tokensSaved: e.tokens,
|
|
927
|
+
costSaved: e.tokens ? (e.tokens / 1000) * 0.002 : 0,
|
|
928
|
+
metadata: {
|
|
929
|
+
provider: e.provider,
|
|
930
|
+
project: e.project
|
|
931
|
+
}
|
|
932
|
+
}));
|
|
933
|
+
let mergedEvents = [...localEvents];
|
|
934
|
+
// Query the stack orchestration API if running to merge stack events
|
|
935
|
+
try {
|
|
936
|
+
const stackEventsResponse = await axios_1.default.get(`http://127.0.0.1:8083/api/events?page=1&limit=100`, { timeout: 100 });
|
|
937
|
+
if (stackEventsResponse.data && Array.isArray(stackEventsResponse.data.events)) {
|
|
938
|
+
mergedEvents = [...mergedEvents, ...stackEventsResponse.data.events];
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
catch { }
|
|
942
|
+
// Sort merged events chronologically (newest first)
|
|
943
|
+
mergedEvents.sort((a, b) => {
|
|
944
|
+
const tA = new Date(a.timestamp).getTime();
|
|
945
|
+
const tB = new Date(b.timestamp).getTime();
|
|
946
|
+
return tB - tA;
|
|
947
|
+
});
|
|
948
|
+
const total = mergedEvents.length;
|
|
949
|
+
const paginatedEvents = mergedEvents.slice(start, start + limit);
|
|
950
|
+
res.json({
|
|
951
|
+
events: paginatedEvents,
|
|
952
|
+
pagination: {
|
|
953
|
+
total,
|
|
954
|
+
page,
|
|
955
|
+
limit,
|
|
956
|
+
hasMore: start + limit < total
|
|
957
|
+
}
|
|
958
|
+
});
|
|
849
959
|
});
|
|
850
|
-
this.app.get('/api/cache-stats', (req, res) => {
|
|
851
|
-
|
|
960
|
+
this.app.get('/api/cache-stats', async (req, res) => {
|
|
961
|
+
let topQueries = [];
|
|
962
|
+
let totalCached = CACHE.size;
|
|
963
|
+
// Try to query ChromaDB for semantic cache count
|
|
964
|
+
try {
|
|
965
|
+
const collection = await chroma.getOrCreateCollection({
|
|
966
|
+
name: 'lemma-cache',
|
|
967
|
+
embeddingFunction: dummyEmbeddingFunction,
|
|
968
|
+
metadata: { "hnsw:space": "cosine" }
|
|
969
|
+
});
|
|
970
|
+
const count = await collection.count();
|
|
971
|
+
totalCached += count;
|
|
972
|
+
}
|
|
973
|
+
catch { }
|
|
974
|
+
// Build top queries from in-memory CACHE
|
|
975
|
+
for (const [key, entry] of CACHE.entries()) {
|
|
976
|
+
const hitCount = EVENT_LOG.filter(e => e.type === 'cache:hit' && e.provider === entry.provider).length || 1;
|
|
977
|
+
const tokens = 2000;
|
|
978
|
+
topQueries.push({
|
|
979
|
+
queryHash: key.substring(0, 16),
|
|
980
|
+
query: entry.input,
|
|
981
|
+
hitCount,
|
|
982
|
+
totalTokens: tokens * hitCount,
|
|
983
|
+
costSaved: (tokens * hitCount / 1000) * 0.002,
|
|
984
|
+
lastHit: new Date(entry.createdAt).toISOString()
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
// Try to query the stack orchestration API if running to append stack cache stats
|
|
988
|
+
try {
|
|
989
|
+
const stackCache = await axios_1.default.get('http://127.0.0.1:8083/api/cache-stats', { timeout: 100 });
|
|
990
|
+
if (stackCache.data) {
|
|
991
|
+
totalCached += stackCache.data.totalCached || 0;
|
|
992
|
+
if (Array.isArray(stackCache.data.topQueries)) {
|
|
993
|
+
const stackTop = stackCache.data.topQueries.filter((q) => !topQueries.some(t => t.query === q.query));
|
|
994
|
+
topQueries = [...topQueries, ...stackTop];
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
catch { }
|
|
999
|
+
// Sort by hitCount desc
|
|
1000
|
+
topQueries.sort((a, b) => b.hitCount - a.hitCount);
|
|
1001
|
+
// Latency percentiles from EVENT_LOG
|
|
1002
|
+
const latencies = EVENT_LOG.filter(e => e.latency).map(e => e.latency).sort((a, b) => a - b);
|
|
1003
|
+
const latencyPercentiles = {
|
|
1004
|
+
p50: latencies[Math.floor(latencies.length * 0.5)] || 0,
|
|
1005
|
+
p95: latencies[Math.floor(latencies.length * 0.95)] || 0,
|
|
1006
|
+
p99: latencies[Math.floor(latencies.length * 0.99)] || 0,
|
|
1007
|
+
};
|
|
1008
|
+
res.json({
|
|
1009
|
+
topQueries: topQueries.slice(0, 10),
|
|
1010
|
+
latencyPercentiles,
|
|
1011
|
+
totalCached
|
|
1012
|
+
});
|
|
852
1013
|
});
|
|
853
1014
|
this.app.get('/api/search', async (req, res) => {
|
|
854
1015
|
const query = req.query.q;
|
|
@@ -885,24 +1046,57 @@ class LemmaServer {
|
|
|
885
1046
|
res.status(500).json({ error: e.message });
|
|
886
1047
|
}
|
|
887
1048
|
});
|
|
888
|
-
this.app.post('/api/cache/clear', (req, res) => {
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1049
|
+
this.app.post('/api/cache/clear', async (req, res) => {
|
|
1050
|
+
const { provider } = req.body || {};
|
|
1051
|
+
let clearedCount = 0;
|
|
1052
|
+
if (!provider) {
|
|
1053
|
+
// Global clear
|
|
1054
|
+
clearedCount = CACHE.size;
|
|
1055
|
+
CACHE.clear();
|
|
1056
|
+
EVENT_LOG.length = 0; // Clear live event log
|
|
1057
|
+
SavingsLedger_1.savingsLedger.reset(); // Reset savings counters
|
|
1058
|
+
// Reset and persist statistics for the current project
|
|
1059
|
+
if (this.stats[this.projectName]) {
|
|
1060
|
+
this.stats[this.projectName] = {
|
|
1061
|
+
total: 0,
|
|
1062
|
+
hits: 0,
|
|
1063
|
+
misses: 0,
|
|
1064
|
+
totalLatency: 0,
|
|
1065
|
+
totalTokensSaved: 0,
|
|
1066
|
+
providers: {}
|
|
1067
|
+
};
|
|
1068
|
+
writeJson(STATS_FILE, this.stats).catch(() => { });
|
|
1069
|
+
}
|
|
1070
|
+
// Clear Chroma DB collection
|
|
1071
|
+
try {
|
|
1072
|
+
await chroma.deleteCollection({ name: 'lemma-cache' }).catch(() => { });
|
|
1073
|
+
}
|
|
1074
|
+
catch (e) {
|
|
1075
|
+
console.error(`[CacheClear] Failed to clear Chroma collection: ${e.message}`);
|
|
1076
|
+
}
|
|
904
1077
|
}
|
|
905
|
-
|
|
1078
|
+
else {
|
|
1079
|
+
// Granular clear
|
|
1080
|
+
for (const [k, e] of CACHE.entries()) {
|
|
1081
|
+
if (e.provider === provider) {
|
|
1082
|
+
CACHE.delete(k);
|
|
1083
|
+
clearedCount++;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
// Selective delete from Chroma DB collection using provider metadata filter
|
|
1087
|
+
try {
|
|
1088
|
+
const collection = await chroma.getOrCreateCollection({
|
|
1089
|
+
name: 'lemma-cache',
|
|
1090
|
+
embeddingFunction: dummyEmbeddingFunction,
|
|
1091
|
+
metadata: { "hnsw:space": "cosine" }
|
|
1092
|
+
});
|
|
1093
|
+
await collection.delete({ where: { provider } });
|
|
1094
|
+
}
|
|
1095
|
+
catch (e) {
|
|
1096
|
+
console.error(`[CacheClear] Failed to selectively delete provider ${provider} from Chroma: ${e.message}`);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
res.json({ success: true, clearedEntries: clearedCount });
|
|
906
1100
|
});
|
|
907
1101
|
// Savings breakdown by source for the dashboard
|
|
908
1102
|
this.app.get('/api/savings-breakdown', (req, res) => {
|
|
@@ -946,89 +1140,35 @@ class LemmaServer {
|
|
|
946
1140
|
logEvent({ type: 'privacy:mask', count: Object.keys(tokenMap).length, project: this.projectName });
|
|
947
1141
|
}
|
|
948
1142
|
const originalModel = req.body.model || 'gpt-4o';
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
if (req.body.model)
|
|
952
|
-
req.body.model = model;
|
|
953
|
-
// Record complexity routing savings when model is downgraded
|
|
954
|
-
if (routingDecision.complexity === 'low' && model !== originalModel) {
|
|
955
|
-
// Estimated token savings: cheap model costs ~5x less, so avg prompt of ~800 tokens saves ~640 tokens
|
|
956
|
-
const estimatedPromptTokens = Math.floor(prompt.length / 4);
|
|
957
|
-
const routingSavings = Math.floor(estimatedPromptTokens * 0.8); // conservative 80% cost reduction
|
|
958
|
-
SavingsLedger_1.savingsLedger.recordTokens('complexityRouting', routingSavings);
|
|
959
|
-
}
|
|
960
|
-
// Apply the Agent Multiplexer to local-verify the prompt in parallel
|
|
961
|
-
const enrichedPrompt = await agentMultiplexer.multiplexPrompt(prompt, model);
|
|
962
|
-
// Resolve conversation session key and retrieve/create the fileHistory map
|
|
963
|
-
let fileHistory;
|
|
964
|
-
if (req.body.messages && req.body.messages.length > 0) {
|
|
965
|
-
try {
|
|
966
|
-
const firstMsg = req.body.messages[0];
|
|
967
|
-
const content = typeof firstMsg.content === 'string' ? firstMsg.content : JSON.stringify(firstMsg.content);
|
|
968
|
-
const sessionKey = crypto_1.default.createHash('sha1').update(content).digest('hex');
|
|
969
|
-
if (!this.sessions.has(sessionKey)) {
|
|
970
|
-
this.sessions.set(sessionKey, { fileHistory: new Map() });
|
|
971
|
-
}
|
|
972
|
-
fileHistory = this.sessions.get(sessionKey).fileHistory;
|
|
973
|
-
}
|
|
974
|
-
catch { }
|
|
975
|
-
}
|
|
976
|
-
// Compress long TS/JS codebase file blocks in the enriched prompt using the Context Squeezer AST Tree-Shaker!
|
|
977
|
-
// The query context is the user's primary prompt to ensure we keep relevant methods!
|
|
978
|
-
const squeezeResult = (0, ContextSqueezer_1.squeezePrompt)(enrichedPrompt, prompt, fileHistory);
|
|
979
|
-
const squeezedPrompt = squeezeResult.squeezed;
|
|
980
|
-
const compressionSavings = squeezeResult.originalSize - squeezeResult.squeezedSize;
|
|
981
|
-
if (compressionSavings > 0) {
|
|
982
|
-
SavingsLedger_1.savingsLedger.record('contextSqueeze', squeezeResult.originalSize, squeezeResult.squeezedSize);
|
|
983
|
-
console.log(`⚡ [Squeezer] Compressed prompt from ${squeezeResult.originalSize} to ${squeezeResult.squeezedSize} chars (Saved ${(squeezeResult.compressionRatio * 100).toFixed(1)}% context tokens!)`);
|
|
984
|
-
}
|
|
985
|
-
if (provider === 'openai' || provider === 'anthropic') {
|
|
986
|
-
if (req.body.messages && req.body.messages.length > 0) {
|
|
987
|
-
injectSqueezedContent(req.body.messages[req.body.messages.length - 1], squeezedPrompt);
|
|
988
|
-
// Apply conversational history pruning to save tokens on historical messages!
|
|
989
|
-
const pruneResult = (0, ContextSqueezer_1.pruneHistoryMessages)(req.body.messages);
|
|
990
|
-
req.body.messages = pruneResult.messages;
|
|
991
|
-
historyTokensSaved = pruneResult.tokensSavedEstimate + Math.floor(compressionSavings / 4);
|
|
992
|
-
if (pruneResult.tokensSavedEstimate > 0) {
|
|
993
|
-
SavingsLedger_1.savingsLedger.recordTokens('historyPrune', pruneResult.tokensSavedEstimate);
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
else if (provider === 'gemini') {
|
|
998
|
-
if (req.body.contents && req.body.contents.length > 0) {
|
|
999
|
-
const lastContent = req.body.contents[req.body.contents.length - 1];
|
|
1000
|
-
if (lastContent.parts && lastContent.parts.length > 0) {
|
|
1001
|
-
lastContent.parts[lastContent.parts.length - 1].text = squeezedPrompt;
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1143
|
+
// ── 1. Cache-First Hot Path ──
|
|
1144
|
+
// Perform exact cache check immediately (resolves in <1ms)
|
|
1005
1145
|
const hit = cacheGet(provider, prompt);
|
|
1006
1146
|
if (hit) {
|
|
1007
|
-
const cacheTokensSaved = 2000
|
|
1008
|
-
SavingsLedger_1.savingsLedger.recordTokens('cache', cacheTokensSaved);
|
|
1147
|
+
const cacheTokensSaved = 2000;
|
|
1009
1148
|
await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, cacheTokensSaved);
|
|
1010
1149
|
const unmaskedData = semanticScrubber.unmask(hit.data, tokenMap);
|
|
1011
1150
|
if (isStream)
|
|
1012
|
-
return simulateHitStream(res, unmaskedData, provider,
|
|
1151
|
+
return simulateHitStream(res, unmaskedData, provider, originalModel, tier, 1.0);
|
|
1013
1152
|
res.setHeader('X-Lemma-Cache', 'HIT');
|
|
1014
1153
|
res.setHeader('X-Lemma-Similarity', '1.000');
|
|
1015
1154
|
res.setHeader('X-Lemma-Tier', tier);
|
|
1016
1155
|
return res.json(unmaskedData);
|
|
1017
1156
|
}
|
|
1157
|
+
// Perform semantic cache check (resolves in ~15ms)
|
|
1018
1158
|
if (pro) {
|
|
1019
1159
|
const semHit = await semanticGet(provider, prompt);
|
|
1020
1160
|
if (semHit) {
|
|
1021
1161
|
if (semHit.similarity >= 0.90) {
|
|
1022
|
-
await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000
|
|
1162
|
+
await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000);
|
|
1023
1163
|
const unmaskedData = semanticScrubber.unmask(semHit.data, tokenMap);
|
|
1024
1164
|
if (isStream)
|
|
1025
|
-
return simulateHitStream(res, unmaskedData, provider,
|
|
1165
|
+
return simulateHitStream(res, unmaskedData, provider, originalModel, tier, semHit.similarity);
|
|
1026
1166
|
res.setHeader('X-Lemma-Cache', 'HIT');
|
|
1027
1167
|
res.setHeader('X-Lemma-Similarity', semHit.similarity.toFixed(3));
|
|
1028
1168
|
res.setHeader('X-Lemma-Tier', tier);
|
|
1029
1169
|
return res.json(unmaskedData);
|
|
1030
1170
|
}
|
|
1031
|
-
else if (semHit.similarity >= 0.
|
|
1171
|
+
else if (semHit.similarity >= 0.82) { // raised from 0.70 to 0.82 to prevent false positives
|
|
1032
1172
|
console.log(`💡 [CARS] Near Cache Hit (Similarity: ${(semHit.similarity * 100).toFixed(1)}%). Synthesizing answer locally via Ollama...`);
|
|
1033
1173
|
let cachedContent = '';
|
|
1034
1174
|
if (provider === 'openai') {
|
|
@@ -1066,10 +1206,10 @@ Adjusted Answer:`;
|
|
|
1066
1206
|
else if (provider === 'anthropic') {
|
|
1067
1207
|
synthesizedData.content[0].text = adjustedText;
|
|
1068
1208
|
}
|
|
1069
|
-
await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000
|
|
1209
|
+
await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000);
|
|
1070
1210
|
const unmaskedData = semanticScrubber.unmask(synthesizedData, tokenMap);
|
|
1071
1211
|
if (isStream) {
|
|
1072
|
-
return simulateHitStream(res, unmaskedData, provider,
|
|
1212
|
+
return simulateHitStream(res, unmaskedData, provider, originalModel, tier, semHit.similarity);
|
|
1073
1213
|
}
|
|
1074
1214
|
res.setHeader('X-Lemma-Cache', 'HIT-CARS');
|
|
1075
1215
|
res.setHeader('X-Lemma-Similarity', semHit.similarity.toFixed(3));
|
|
@@ -1086,22 +1226,86 @@ Adjusted Answer:`;
|
|
|
1086
1226
|
const cloudHit = await cloudSync.get(prompt);
|
|
1087
1227
|
if (cloudHit) {
|
|
1088
1228
|
cacheSet(provider, prompt, cloudHit.data);
|
|
1089
|
-
await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000
|
|
1229
|
+
await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000);
|
|
1090
1230
|
const unmaskedData = semanticScrubber.unmask(cloudHit.data, tokenMap);
|
|
1091
1231
|
if (isStream)
|
|
1092
|
-
return simulateHitStream(res, unmaskedData, provider,
|
|
1232
|
+
return simulateHitStream(res, unmaskedData, provider, originalModel, tier, cloudHit.similarity || 0.95);
|
|
1093
1233
|
res.setHeader('X-Lemma-Cache', 'HIT');
|
|
1094
1234
|
res.setHeader('X-Lemma-Similarity', (cloudHit.similarity || 0.95).toFixed(3));
|
|
1095
1235
|
res.setHeader('X-Lemma-Tier', tier);
|
|
1096
1236
|
return res.json(unmaskedData);
|
|
1097
1237
|
}
|
|
1098
1238
|
}
|
|
1239
|
+
// ── 2. Cache Miss Path ──
|
|
1240
|
+
const routingDecision = complexityRouter.evaluate(prompt, originalModel);
|
|
1241
|
+
const model = routingDecision.model;
|
|
1242
|
+
if (req.body.model)
|
|
1243
|
+
req.body.model = model;
|
|
1244
|
+
// Record complexity routing savings when model is downgraded
|
|
1245
|
+
if (routingDecision.complexity === 'low' && model !== originalModel) {
|
|
1246
|
+
const estimatedPromptTokens = Math.floor(prompt.length / 4);
|
|
1247
|
+
const routingSavings = Math.floor(estimatedPromptTokens * 0.8);
|
|
1248
|
+
SavingsLedger_1.savingsLedger.recordTokens('complexityRouting', routingSavings);
|
|
1249
|
+
}
|
|
1250
|
+
// Apply the Agent Multiplexer to local-verify the prompt in parallel
|
|
1251
|
+
const enrichedPrompt = await agentMultiplexer.multiplexPrompt(prompt, model);
|
|
1252
|
+
// Resolve conversation session key and retrieve/create the fileHistory map
|
|
1253
|
+
let fileHistory;
|
|
1254
|
+
if (req.body.messages && req.body.messages.length > 0) {
|
|
1255
|
+
try {
|
|
1256
|
+
const firstMsg = req.body.messages[0];
|
|
1257
|
+
const content = typeof firstMsg.content === 'string' ? firstMsg.content : JSON.stringify(firstMsg.content);
|
|
1258
|
+
const sessionKey = crypto_1.default.createHash('sha1').update(content).digest('hex');
|
|
1259
|
+
if (!this.sessions.has(sessionKey)) {
|
|
1260
|
+
// Enforce 500 active sessions cap to prevent memory leak (FIFO eviction)
|
|
1261
|
+
const MAX_SESSIONS = 500;
|
|
1262
|
+
if (this.sessions.size >= MAX_SESSIONS) {
|
|
1263
|
+
const oldestKey = this.sessions.keys().next().value;
|
|
1264
|
+
if (oldestKey)
|
|
1265
|
+
this.sessions.delete(oldestKey);
|
|
1266
|
+
}
|
|
1267
|
+
this.sessions.set(sessionKey, { fileHistory: new Map() });
|
|
1268
|
+
}
|
|
1269
|
+
fileHistory = this.sessions.get(sessionKey).fileHistory;
|
|
1270
|
+
}
|
|
1271
|
+
catch { }
|
|
1272
|
+
}
|
|
1273
|
+
// Compress long TS/JS codebase file blocks in the enriched prompt
|
|
1274
|
+
const squeezeResult = (0, ContextSqueezer_1.squeezePrompt)(enrichedPrompt, prompt, fileHistory);
|
|
1275
|
+
const squeezedPrompt = squeezeResult.squeezed;
|
|
1276
|
+
const compressionSavings = squeezeResult.originalSize - squeezeResult.squeezedSize;
|
|
1277
|
+
if (compressionSavings > 0) {
|
|
1278
|
+
SavingsLedger_1.savingsLedger.record('contextSqueeze', squeezeResult.originalSize, squeezeResult.squeezedSize);
|
|
1279
|
+
console.log(`⚡ [Squeezer] Compressed prompt from ${squeezeResult.originalSize} to ${squeezeResult.squeezedSize} chars (Saved ${(squeezeResult.compressionRatio * 100).toFixed(1)}% context tokens!)`);
|
|
1280
|
+
}
|
|
1281
|
+
if (provider === 'openai' || provider === 'anthropic') {
|
|
1282
|
+
if (req.body.messages && req.body.messages.length > 0) {
|
|
1283
|
+
injectSqueezedContent(req.body.messages[req.body.messages.length - 1], squeezedPrompt);
|
|
1284
|
+
// Apply conversational history pruning
|
|
1285
|
+
const pruneResult = (0, ContextSqueezer_1.pruneHistoryMessages)(req.body.messages);
|
|
1286
|
+
req.body.messages = pruneResult.messages;
|
|
1287
|
+
historyTokensSaved = pruneResult.tokensSavedEstimate + Math.floor(compressionSavings / 4);
|
|
1288
|
+
if (pruneResult.tokensSavedEstimate > 0) {
|
|
1289
|
+
SavingsLedger_1.savingsLedger.recordTokens('historyPrune', pruneResult.tokensSavedEstimate);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
else if (provider === 'gemini') {
|
|
1294
|
+
if (req.body.contents && req.body.contents.length > 0) {
|
|
1295
|
+
const lastContent = req.body.contents[req.body.contents.length - 1];
|
|
1296
|
+
if (lastContent.parts && lastContent.parts.length > 0) {
|
|
1297
|
+
lastContent.parts[lastContent.parts.length - 1].text = squeezedPrompt;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1099
1301
|
if (isStream) {
|
|
1100
1302
|
await recordStat(this.stats, this.projectName, false, Date.now() - t0, provider, historyTokensSaved);
|
|
1101
1303
|
if (!pro) {
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1304
|
+
await usageWriteQueue.enqueue(async () => {
|
|
1305
|
+
const u = await loadUsage();
|
|
1306
|
+
u.count++;
|
|
1307
|
+
await writeJson(USAGE_FILE, u);
|
|
1308
|
+
});
|
|
1105
1309
|
}
|
|
1106
1310
|
return relayMissStream(res, req.body, provider, tier);
|
|
1107
1311
|
}
|
|
@@ -1113,9 +1317,11 @@ Adjusted Answer:`;
|
|
|
1113
1317
|
await cloudSync.set(prompt, data);
|
|
1114
1318
|
}
|
|
1115
1319
|
if (!pro) {
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1320
|
+
await usageWriteQueue.enqueue(async () => {
|
|
1321
|
+
const u = await loadUsage();
|
|
1322
|
+
u.count++;
|
|
1323
|
+
await writeJson(USAGE_FILE, u);
|
|
1324
|
+
});
|
|
1119
1325
|
}
|
|
1120
1326
|
await recordStat(this.stats, this.projectName, false, Date.now() - t0, provider, historyTokensSaved);
|
|
1121
1327
|
return res.json(semanticScrubber.unmask(data, tokenMap));
|
|
@@ -1126,6 +1332,43 @@ Adjusted Answer:`;
|
|
|
1126
1332
|
}
|
|
1127
1333
|
async callUpstream(body, provider) {
|
|
1128
1334
|
let apiKey = provider === 'openai' ? process.env.OPENAI_API_KEY : (provider === 'anthropic' ? process.env.ANTHROPIC_API_KEY : process.env.GEMINI_API_KEY);
|
|
1335
|
+
// If the API key is an OpenAI key (sk-proj-) but requested for Anthropic or Gemini,
|
|
1336
|
+
// multiplex it internally to OpenAI gpt-4o-mini to allow live, cheap simulated provider routing.
|
|
1337
|
+
const isMockedProvider = apiKey && apiKey.startsWith('sk-') && provider !== 'openai';
|
|
1338
|
+
if (isMockedProvider) {
|
|
1339
|
+
console.log(`\n\x1b[36m🔀 [Gateway Router] Multiplexing B2B ${provider.toUpperCase()} request to OpenAI using key: ...${apiKey.substring(8, 14)}...\x1b[0m`);
|
|
1340
|
+
const payload = {
|
|
1341
|
+
model: 'gpt-4o-mini',
|
|
1342
|
+
messages: body.messages || [{ role: 'user', content: extractPrompt(body, provider) || 'Hello' }],
|
|
1343
|
+
stream: false // keep stream false for simulation simplicity
|
|
1344
|
+
};
|
|
1345
|
+
const baseURL = 'https://api.openai.com/v1/chat/completions';
|
|
1346
|
+
const resp = await axios_1.default.post(baseURL, payload, {
|
|
1347
|
+
headers: {
|
|
1348
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1349
|
+
'Content-Type': 'application/json'
|
|
1350
|
+
}
|
|
1351
|
+
});
|
|
1352
|
+
if (provider === 'anthropic') {
|
|
1353
|
+
const text = resp.data.choices?.[0]?.message?.content || '';
|
|
1354
|
+
return {
|
|
1355
|
+
id: `msg-${Date.now()}`,
|
|
1356
|
+
type: 'message',
|
|
1357
|
+
role: 'assistant',
|
|
1358
|
+
content: [{ type: 'text', text }],
|
|
1359
|
+
model: body.model || 'claude-3-5-sonnet'
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
else if (provider === 'gemini') {
|
|
1363
|
+
const text = resp.data.choices?.[0]?.message?.content || '';
|
|
1364
|
+
return {
|
|
1365
|
+
candidates: [{
|
|
1366
|
+
content: { parts: [{ text }] },
|
|
1367
|
+
finishReason: 'STOP'
|
|
1368
|
+
}]
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1129
1372
|
if (!apiKey) {
|
|
1130
1373
|
console.log(`\n\x1b[33m⚠️ No API Key found for ${provider.toUpperCase()}. Falling back to local Ollama (Zero-Cost Mode)!\x1b[0m`);
|
|
1131
1374
|
// If the requested model is a cloud model (e.g. gpt-4o, claude), map it to a local model like llama3
|
|
@@ -1152,6 +1395,15 @@ Adjusted Answer:`;
|
|
|
1152
1395
|
}
|
|
1153
1396
|
async start() {
|
|
1154
1397
|
this.stats = await loadStats();
|
|
1398
|
+
// Start active cache eviction sweep every hour
|
|
1399
|
+
setInterval(() => {
|
|
1400
|
+
const now = Date.now();
|
|
1401
|
+
for (const [k, e] of CACHE.entries()) {
|
|
1402
|
+
if (e.ttl > 0 && now - e.createdAt > e.ttl) {
|
|
1403
|
+
CACHE.delete(k);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
}, 3600000).unref();
|
|
1155
1407
|
const server = http_1.default.createServer(this.app);
|
|
1156
1408
|
server.on('error', (e) => {
|
|
1157
1409
|
if (e.code === 'EADDRINUSE') {
|