@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
|
@@ -24,7 +24,7 @@ function getVersion() {
|
|
|
24
24
|
catch { }
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
return '0.5.
|
|
27
|
+
return '0.5.3';
|
|
28
28
|
}
|
|
29
29
|
const VERSION = getVersion();
|
|
30
30
|
import ComplexityRouter from '../proxy/ComplexityRouter';
|
|
@@ -78,13 +78,20 @@ async function readJson(file, fallback) {
|
|
|
78
78
|
catch { }
|
|
79
79
|
return fallback;
|
|
80
80
|
}
|
|
81
|
+
const writeQueues = new Map();
|
|
81
82
|
async function writeJson(file, data) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
const chain = writeQueues.get(file) || Promise.resolve();
|
|
84
|
+
const writeOperation = async () => {
|
|
85
|
+
try {
|
|
86
|
+
const tempFile = `${file}.${crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).substring(2, 15)}.tmp`;
|
|
87
|
+
await fs.promises.writeFile(tempFile, JSON.stringify(data, null, 2));
|
|
88
|
+
await fs.promises.rename(tempFile, file);
|
|
89
|
+
}
|
|
90
|
+
catch { }
|
|
91
|
+
};
|
|
92
|
+
const nextChain = chain.then(writeOperation).catch(() => { });
|
|
93
|
+
writeQueues.set(file, nextChain);
|
|
94
|
+
return nextChain;
|
|
88
95
|
}
|
|
89
96
|
function projectHash(name) {
|
|
90
97
|
return crypto.createHash('sha1').update(name).digest('hex').slice(0, 12);
|
|
@@ -378,12 +385,21 @@ export function startBackgroundClipboardWatcher() {
|
|
|
378
385
|
}
|
|
379
386
|
}
|
|
380
387
|
// ── License helpers ────────────────────────────────────────────────────────────
|
|
388
|
+
let isProCached = null;
|
|
389
|
+
let lastLicenseCheck = 0;
|
|
390
|
+
const LICENSE_CACHE_TTL = 10000; // 10 seconds cache
|
|
381
391
|
async function loadLicense() {
|
|
382
392
|
return readJson(LICENSE_FILE, { isPro: false });
|
|
383
393
|
}
|
|
384
394
|
async function isPro() {
|
|
395
|
+
const now = Date.now();
|
|
396
|
+
if (isProCached !== null && (now - lastLicenseCheck) < LICENSE_CACHE_TTL) {
|
|
397
|
+
return isProCached;
|
|
398
|
+
}
|
|
385
399
|
const l = await loadLicense();
|
|
386
|
-
|
|
400
|
+
isProCached = !!(l && l.isPro);
|
|
401
|
+
lastLicenseCheck = now;
|
|
402
|
+
return isProCached;
|
|
387
403
|
}
|
|
388
404
|
async function validateKeyRemote(key) {
|
|
389
405
|
return new Promise((resolve, reject) => {
|
|
@@ -446,16 +462,21 @@ async function recordStat(stats, project, fromCache, latencyMs, provider, tokens
|
|
|
446
462
|
s.total++;
|
|
447
463
|
s.totalLatency += latencyMs;
|
|
448
464
|
fromCache ? s.hits++ : s.misses++;
|
|
449
|
-
|
|
465
|
+
const tkSaved = fromCache ? (tokensSaved || 2000) : (tokensSaved || 0);
|
|
466
|
+
s.totalTokensSaved += tkSaved;
|
|
450
467
|
if (!s.providers[provider])
|
|
451
468
|
s.providers[provider] = { hits: 0, misses: 0 };
|
|
452
469
|
fromCache ? s.providers[provider].hits++ : s.providers[provider].misses++;
|
|
470
|
+
if (fromCache && tkSaved > 0) {
|
|
471
|
+
// Automatically report cache hits directly to the server session ledger for absolute sync consistency
|
|
472
|
+
savingsLedger.recordTokens('cache', tkSaved);
|
|
473
|
+
}
|
|
453
474
|
logEvent({
|
|
454
475
|
type: fromCache ? 'cache:hit' : 'cache:miss',
|
|
455
476
|
project,
|
|
456
477
|
latency: latencyMs,
|
|
457
478
|
provider,
|
|
458
|
-
tokens:
|
|
479
|
+
tokens: tkSaved
|
|
459
480
|
});
|
|
460
481
|
await writeJson(STATS_FILE, stats);
|
|
461
482
|
}
|
|
@@ -476,7 +497,15 @@ function cacheGet(provider, prompt) {
|
|
|
476
497
|
return e;
|
|
477
498
|
}
|
|
478
499
|
function cacheSet(provider, prompt, data) {
|
|
479
|
-
|
|
500
|
+
const MAX_CACHE_SIZE = 5000;
|
|
501
|
+
const k = cacheKey(provider, prompt);
|
|
502
|
+
if (!CACHE.has(k) && CACHE.size >= MAX_CACHE_SIZE) {
|
|
503
|
+
// Evict oldest cache entry (FIFO eviction)
|
|
504
|
+
const oldestKey = CACHE.keys().next().value;
|
|
505
|
+
if (oldestKey)
|
|
506
|
+
CACHE.delete(oldestKey);
|
|
507
|
+
}
|
|
508
|
+
CACHE.set(k, { input: prompt, data, createdAt: Date.now(), ttl: TTL_MS, provider });
|
|
480
509
|
}
|
|
481
510
|
// ── Semantic Search (Pro) ──────────────────────────────────────────────────────
|
|
482
511
|
async function getEmbedding(text) {
|
|
@@ -521,7 +550,7 @@ async function semanticGet(provider, prompt) {
|
|
|
521
550
|
distance = distances[0][i];
|
|
522
551
|
}
|
|
523
552
|
const similarity = Math.max(0, 1 - distance);
|
|
524
|
-
const THRESHOLD = 0.7
|
|
553
|
+
const THRESHOLD = 0.82; // raised from 0.7 to 0.82 to prevent false positives
|
|
525
554
|
if (similarity >= THRESHOLD) {
|
|
526
555
|
return {
|
|
527
556
|
data: JSON.parse(metadata.response),
|
|
@@ -748,6 +777,16 @@ async function relayMissStream(res, body, provider, tier) {
|
|
|
748
777
|
req.end();
|
|
749
778
|
});
|
|
750
779
|
}
|
|
780
|
+
class WriteQueue {
|
|
781
|
+
constructor() {
|
|
782
|
+
this.queue = Promise.resolve();
|
|
783
|
+
}
|
|
784
|
+
async enqueue(fn) {
|
|
785
|
+
this.queue = this.queue.then(fn).catch(() => { });
|
|
786
|
+
return this.queue;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
const usageWriteQueue = new WriteQueue();
|
|
751
790
|
// ── Proxy Server class ─────────────────────────────────────────────────────────
|
|
752
791
|
class LemmaServer {
|
|
753
792
|
constructor(port, projectName, openBrowser = true) {
|
|
@@ -810,6 +849,18 @@ class LemmaServer {
|
|
|
810
849
|
const hitRate = stats.total > 0 ? stats.hits / stats.total : 0;
|
|
811
850
|
const avgLat = stats.total > 0 ? stats.totalLatency / stats.total : 0;
|
|
812
851
|
const secretsMasked = EVENT_LOG.filter(e => e.type === 'privacy:mask').length;
|
|
852
|
+
const ledgerSnap = savingsLedger.getSnapshot();
|
|
853
|
+
let activeAgents = 1; // Always include the Gateway itself
|
|
854
|
+
let totalAgents = 1;
|
|
855
|
+
// Query the stack orchestration API to merge active agents
|
|
856
|
+
try {
|
|
857
|
+
const stackMetrics = await axios.get('http://127.0.0.1:8083/api/metrics', { timeout: 100 });
|
|
858
|
+
if (stackMetrics.data) {
|
|
859
|
+
activeAgents += stackMetrics.data.activeAgents || 0;
|
|
860
|
+
totalAgents += stackMetrics.data.totalAgents || 0;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
catch { }
|
|
813
864
|
res.json({
|
|
814
865
|
timestamp: new Date().toISOString(),
|
|
815
866
|
totalRequests: stats.total,
|
|
@@ -817,30 +868,140 @@ class LemmaServer {
|
|
|
817
868
|
cacheMisses: stats.misses,
|
|
818
869
|
hitRate,
|
|
819
870
|
averageLatency: avgLat,
|
|
820
|
-
tokensSaved:
|
|
821
|
-
costSaved:
|
|
871
|
+
tokensSaved: ledgerSnap.total.tokensSaved,
|
|
872
|
+
costSaved: ledgerSnap.total.costSaved,
|
|
822
873
|
secretsMasked: secretsMasked,
|
|
823
|
-
activeAgents
|
|
824
|
-
totalAgents
|
|
874
|
+
activeAgents,
|
|
875
|
+
totalAgents,
|
|
825
876
|
uptime: process.uptime(),
|
|
826
877
|
tier: (await isPro()) ? 'pro' : 'free'
|
|
827
878
|
});
|
|
828
879
|
});
|
|
829
880
|
this.app.get('/api/agents', async (req, res) => {
|
|
881
|
+
// Load statistics for the Gateway itself
|
|
830
882
|
const stats = await loadStats();
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
883
|
+
const statsProj = stats[this.projectName] || { total: 0, hits: 0 };
|
|
884
|
+
const total = statsProj.total || 0;
|
|
885
|
+
const hits = statsProj.hits || 0;
|
|
886
|
+
const gatewayAgent = {
|
|
887
|
+
id: 'lemma-gateway',
|
|
888
|
+
name: 'Lemma Gateway',
|
|
889
|
+
status: 'active',
|
|
890
|
+
capabilities: ['Privacy Firewall', 'Semantic Cache', 'Complexity Router'],
|
|
891
|
+
tasksCompleted: total,
|
|
892
|
+
lastSeen: new Date().toISOString(),
|
|
893
|
+
errorCount: 0,
|
|
894
|
+
cacheHitRate: total > 0 ? hits / total : 0
|
|
895
|
+
};
|
|
896
|
+
let agentsList = [gatewayAgent];
|
|
897
|
+
// Query the stack orchestration API to merge dynamic websocket agents if running
|
|
898
|
+
try {
|
|
899
|
+
const stackAgentsResponse = await axios.get('http://127.0.0.1:8083/api/agents', { timeout: 100 });
|
|
900
|
+
if (stackAgentsResponse.data && Array.isArray(stackAgentsResponse.data.agents)) {
|
|
901
|
+
const stackAgents = stackAgentsResponse.data.agents.filter((a) => a.id !== 'lemma-gateway');
|
|
902
|
+
agentsList = [...agentsList, ...stackAgents];
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
catch { }
|
|
906
|
+
res.json({ agents: agentsList });
|
|
834
907
|
});
|
|
835
|
-
this.app.get('/api/events', (req, res) => {
|
|
908
|
+
this.app.get('/api/events', async (req, res) => {
|
|
836
909
|
const page = parseInt(req.query.page) || 1;
|
|
837
910
|
const limit = parseInt(req.query.limit) || 50;
|
|
838
911
|
const start = (page - 1) * limit;
|
|
839
|
-
|
|
840
|
-
|
|
912
|
+
// Get local proxy events (normalized schema matching what dashboard expects)
|
|
913
|
+
const localEvents = EVENT_LOG.map(e => ({
|
|
914
|
+
id: e.id,
|
|
915
|
+
timestamp: new Date(e.timestamp).toISOString(),
|
|
916
|
+
type: e.type, // e.g., 'cache:hit' or 'cache:miss' which client normalizes to 'cache_hit' or 'cache_miss'
|
|
917
|
+
latency: e.latency,
|
|
918
|
+
tokensSaved: e.tokens,
|
|
919
|
+
costSaved: e.tokens ? (e.tokens / 1000) * 0.002 : 0,
|
|
920
|
+
metadata: {
|
|
921
|
+
provider: e.provider,
|
|
922
|
+
project: e.project
|
|
923
|
+
}
|
|
924
|
+
}));
|
|
925
|
+
let mergedEvents = [...localEvents];
|
|
926
|
+
// Query the stack orchestration API if running to merge stack events
|
|
927
|
+
try {
|
|
928
|
+
const stackEventsResponse = await axios.get(`http://127.0.0.1:8083/api/events?page=1&limit=100`, { timeout: 100 });
|
|
929
|
+
if (stackEventsResponse.data && Array.isArray(stackEventsResponse.data.events)) {
|
|
930
|
+
mergedEvents = [...mergedEvents, ...stackEventsResponse.data.events];
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
catch { }
|
|
934
|
+
// Sort merged events chronologically (newest first)
|
|
935
|
+
mergedEvents.sort((a, b) => {
|
|
936
|
+
const tA = new Date(a.timestamp).getTime();
|
|
937
|
+
const tB = new Date(b.timestamp).getTime();
|
|
938
|
+
return tB - tA;
|
|
939
|
+
});
|
|
940
|
+
const total = mergedEvents.length;
|
|
941
|
+
const paginatedEvents = mergedEvents.slice(start, start + limit);
|
|
942
|
+
res.json({
|
|
943
|
+
events: paginatedEvents,
|
|
944
|
+
pagination: {
|
|
945
|
+
total,
|
|
946
|
+
page,
|
|
947
|
+
limit,
|
|
948
|
+
hasMore: start + limit < total
|
|
949
|
+
}
|
|
950
|
+
});
|
|
841
951
|
});
|
|
842
|
-
this.app.get('/api/cache-stats', (req, res) => {
|
|
843
|
-
|
|
952
|
+
this.app.get('/api/cache-stats', async (req, res) => {
|
|
953
|
+
let topQueries = [];
|
|
954
|
+
let totalCached = CACHE.size;
|
|
955
|
+
// Try to query ChromaDB for semantic cache count
|
|
956
|
+
try {
|
|
957
|
+
const collection = await chroma.getOrCreateCollection({
|
|
958
|
+
name: 'lemma-cache',
|
|
959
|
+
embeddingFunction: dummyEmbeddingFunction,
|
|
960
|
+
metadata: { "hnsw:space": "cosine" }
|
|
961
|
+
});
|
|
962
|
+
const count = await collection.count();
|
|
963
|
+
totalCached += count;
|
|
964
|
+
}
|
|
965
|
+
catch { }
|
|
966
|
+
// Build top queries from in-memory CACHE
|
|
967
|
+
for (const [key, entry] of CACHE.entries()) {
|
|
968
|
+
const hitCount = EVENT_LOG.filter(e => e.type === 'cache:hit' && e.provider === entry.provider).length || 1;
|
|
969
|
+
const tokens = 2000;
|
|
970
|
+
topQueries.push({
|
|
971
|
+
queryHash: key.substring(0, 16),
|
|
972
|
+
query: entry.input,
|
|
973
|
+
hitCount,
|
|
974
|
+
totalTokens: tokens * hitCount,
|
|
975
|
+
costSaved: (tokens * hitCount / 1000) * 0.002,
|
|
976
|
+
lastHit: new Date(entry.createdAt).toISOString()
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
// Try to query the stack orchestration API if running to append stack cache stats
|
|
980
|
+
try {
|
|
981
|
+
const stackCache = await axios.get('http://127.0.0.1:8083/api/cache-stats', { timeout: 100 });
|
|
982
|
+
if (stackCache.data) {
|
|
983
|
+
totalCached += stackCache.data.totalCached || 0;
|
|
984
|
+
if (Array.isArray(stackCache.data.topQueries)) {
|
|
985
|
+
const stackTop = stackCache.data.topQueries.filter((q) => !topQueries.some(t => t.query === q.query));
|
|
986
|
+
topQueries = [...topQueries, ...stackTop];
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
catch { }
|
|
991
|
+
// Sort by hitCount desc
|
|
992
|
+
topQueries.sort((a, b) => b.hitCount - a.hitCount);
|
|
993
|
+
// Latency percentiles from EVENT_LOG
|
|
994
|
+
const latencies = EVENT_LOG.filter(e => e.latency).map(e => e.latency).sort((a, b) => a - b);
|
|
995
|
+
const latencyPercentiles = {
|
|
996
|
+
p50: latencies[Math.floor(latencies.length * 0.5)] || 0,
|
|
997
|
+
p95: latencies[Math.floor(latencies.length * 0.95)] || 0,
|
|
998
|
+
p99: latencies[Math.floor(latencies.length * 0.99)] || 0,
|
|
999
|
+
};
|
|
1000
|
+
res.json({
|
|
1001
|
+
topQueries: topQueries.slice(0, 10),
|
|
1002
|
+
latencyPercentiles,
|
|
1003
|
+
totalCached
|
|
1004
|
+
});
|
|
844
1005
|
});
|
|
845
1006
|
this.app.get('/api/search', async (req, res) => {
|
|
846
1007
|
const query = req.query.q;
|
|
@@ -877,24 +1038,57 @@ class LemmaServer {
|
|
|
877
1038
|
res.status(500).json({ error: e.message });
|
|
878
1039
|
}
|
|
879
1040
|
});
|
|
880
|
-
this.app.post('/api/cache/clear', (req, res) => {
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1041
|
+
this.app.post('/api/cache/clear', async (req, res) => {
|
|
1042
|
+
const { provider } = req.body || {};
|
|
1043
|
+
let clearedCount = 0;
|
|
1044
|
+
if (!provider) {
|
|
1045
|
+
// Global clear
|
|
1046
|
+
clearedCount = CACHE.size;
|
|
1047
|
+
CACHE.clear();
|
|
1048
|
+
EVENT_LOG.length = 0; // Clear live event log
|
|
1049
|
+
savingsLedger.reset(); // Reset savings counters
|
|
1050
|
+
// Reset and persist statistics for the current project
|
|
1051
|
+
if (this.stats[this.projectName]) {
|
|
1052
|
+
this.stats[this.projectName] = {
|
|
1053
|
+
total: 0,
|
|
1054
|
+
hits: 0,
|
|
1055
|
+
misses: 0,
|
|
1056
|
+
totalLatency: 0,
|
|
1057
|
+
totalTokensSaved: 0,
|
|
1058
|
+
providers: {}
|
|
1059
|
+
};
|
|
1060
|
+
writeJson(STATS_FILE, this.stats).catch(() => { });
|
|
1061
|
+
}
|
|
1062
|
+
// Clear Chroma DB collection
|
|
1063
|
+
try {
|
|
1064
|
+
await chroma.deleteCollection({ name: 'lemma-cache' }).catch(() => { });
|
|
1065
|
+
}
|
|
1066
|
+
catch (e) {
|
|
1067
|
+
console.error(`[CacheClear] Failed to clear Chroma collection: ${e.message}`);
|
|
1068
|
+
}
|
|
896
1069
|
}
|
|
897
|
-
|
|
1070
|
+
else {
|
|
1071
|
+
// Granular clear
|
|
1072
|
+
for (const [k, e] of CACHE.entries()) {
|
|
1073
|
+
if (e.provider === provider) {
|
|
1074
|
+
CACHE.delete(k);
|
|
1075
|
+
clearedCount++;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
// Selective delete from Chroma DB collection using provider metadata filter
|
|
1079
|
+
try {
|
|
1080
|
+
const collection = await chroma.getOrCreateCollection({
|
|
1081
|
+
name: 'lemma-cache',
|
|
1082
|
+
embeddingFunction: dummyEmbeddingFunction,
|
|
1083
|
+
metadata: { "hnsw:space": "cosine" }
|
|
1084
|
+
});
|
|
1085
|
+
await collection.delete({ where: { provider } });
|
|
1086
|
+
}
|
|
1087
|
+
catch (e) {
|
|
1088
|
+
console.error(`[CacheClear] Failed to selectively delete provider ${provider} from Chroma: ${e.message}`);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
res.json({ success: true, clearedEntries: clearedCount });
|
|
898
1092
|
});
|
|
899
1093
|
// Savings breakdown by source for the dashboard
|
|
900
1094
|
this.app.get('/api/savings-breakdown', (req, res) => {
|
|
@@ -938,89 +1132,35 @@ class LemmaServer {
|
|
|
938
1132
|
logEvent({ type: 'privacy:mask', count: Object.keys(tokenMap).length, project: this.projectName });
|
|
939
1133
|
}
|
|
940
1134
|
const originalModel = req.body.model || 'gpt-4o';
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
if (req.body.model)
|
|
944
|
-
req.body.model = model;
|
|
945
|
-
// Record complexity routing savings when model is downgraded
|
|
946
|
-
if (routingDecision.complexity === 'low' && model !== originalModel) {
|
|
947
|
-
// Estimated token savings: cheap model costs ~5x less, so avg prompt of ~800 tokens saves ~640 tokens
|
|
948
|
-
const estimatedPromptTokens = Math.floor(prompt.length / 4);
|
|
949
|
-
const routingSavings = Math.floor(estimatedPromptTokens * 0.8); // conservative 80% cost reduction
|
|
950
|
-
savingsLedger.recordTokens('complexityRouting', routingSavings);
|
|
951
|
-
}
|
|
952
|
-
// Apply the Agent Multiplexer to local-verify the prompt in parallel
|
|
953
|
-
const enrichedPrompt = await agentMultiplexer.multiplexPrompt(prompt, model);
|
|
954
|
-
// Resolve conversation session key and retrieve/create the fileHistory map
|
|
955
|
-
let fileHistory;
|
|
956
|
-
if (req.body.messages && req.body.messages.length > 0) {
|
|
957
|
-
try {
|
|
958
|
-
const firstMsg = req.body.messages[0];
|
|
959
|
-
const content = typeof firstMsg.content === 'string' ? firstMsg.content : JSON.stringify(firstMsg.content);
|
|
960
|
-
const sessionKey = crypto.createHash('sha1').update(content).digest('hex');
|
|
961
|
-
if (!this.sessions.has(sessionKey)) {
|
|
962
|
-
this.sessions.set(sessionKey, { fileHistory: new Map() });
|
|
963
|
-
}
|
|
964
|
-
fileHistory = this.sessions.get(sessionKey).fileHistory;
|
|
965
|
-
}
|
|
966
|
-
catch { }
|
|
967
|
-
}
|
|
968
|
-
// Compress long TS/JS codebase file blocks in the enriched prompt using the Context Squeezer AST Tree-Shaker!
|
|
969
|
-
// The query context is the user's primary prompt to ensure we keep relevant methods!
|
|
970
|
-
const squeezeResult = squeezePrompt(enrichedPrompt, prompt, fileHistory);
|
|
971
|
-
const squeezedPrompt = squeezeResult.squeezed;
|
|
972
|
-
const compressionSavings = squeezeResult.originalSize - squeezeResult.squeezedSize;
|
|
973
|
-
if (compressionSavings > 0) {
|
|
974
|
-
savingsLedger.record('contextSqueeze', squeezeResult.originalSize, squeezeResult.squeezedSize);
|
|
975
|
-
console.log(`⚡ [Squeezer] Compressed prompt from ${squeezeResult.originalSize} to ${squeezeResult.squeezedSize} chars (Saved ${(squeezeResult.compressionRatio * 100).toFixed(1)}% context tokens!)`);
|
|
976
|
-
}
|
|
977
|
-
if (provider === 'openai' || provider === 'anthropic') {
|
|
978
|
-
if (req.body.messages && req.body.messages.length > 0) {
|
|
979
|
-
injectSqueezedContent(req.body.messages[req.body.messages.length - 1], squeezedPrompt);
|
|
980
|
-
// Apply conversational history pruning to save tokens on historical messages!
|
|
981
|
-
const pruneResult = pruneHistoryMessages(req.body.messages);
|
|
982
|
-
req.body.messages = pruneResult.messages;
|
|
983
|
-
historyTokensSaved = pruneResult.tokensSavedEstimate + Math.floor(compressionSavings / 4);
|
|
984
|
-
if (pruneResult.tokensSavedEstimate > 0) {
|
|
985
|
-
savingsLedger.recordTokens('historyPrune', pruneResult.tokensSavedEstimate);
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
else if (provider === 'gemini') {
|
|
990
|
-
if (req.body.contents && req.body.contents.length > 0) {
|
|
991
|
-
const lastContent = req.body.contents[req.body.contents.length - 1];
|
|
992
|
-
if (lastContent.parts && lastContent.parts.length > 0) {
|
|
993
|
-
lastContent.parts[lastContent.parts.length - 1].text = squeezedPrompt;
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
}
|
|
1135
|
+
// ── 1. Cache-First Hot Path ──
|
|
1136
|
+
// Perform exact cache check immediately (resolves in <1ms)
|
|
997
1137
|
const hit = cacheGet(provider, prompt);
|
|
998
1138
|
if (hit) {
|
|
999
|
-
const cacheTokensSaved = 2000
|
|
1000
|
-
savingsLedger.recordTokens('cache', cacheTokensSaved);
|
|
1139
|
+
const cacheTokensSaved = 2000;
|
|
1001
1140
|
await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, cacheTokensSaved);
|
|
1002
1141
|
const unmaskedData = semanticScrubber.unmask(hit.data, tokenMap);
|
|
1003
1142
|
if (isStream)
|
|
1004
|
-
return simulateHitStream(res, unmaskedData, provider,
|
|
1143
|
+
return simulateHitStream(res, unmaskedData, provider, originalModel, tier, 1.0);
|
|
1005
1144
|
res.setHeader('X-Lemma-Cache', 'HIT');
|
|
1006
1145
|
res.setHeader('X-Lemma-Similarity', '1.000');
|
|
1007
1146
|
res.setHeader('X-Lemma-Tier', tier);
|
|
1008
1147
|
return res.json(unmaskedData);
|
|
1009
1148
|
}
|
|
1149
|
+
// Perform semantic cache check (resolves in ~15ms)
|
|
1010
1150
|
if (pro) {
|
|
1011
1151
|
const semHit = await semanticGet(provider, prompt);
|
|
1012
1152
|
if (semHit) {
|
|
1013
1153
|
if (semHit.similarity >= 0.90) {
|
|
1014
|
-
await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000
|
|
1154
|
+
await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000);
|
|
1015
1155
|
const unmaskedData = semanticScrubber.unmask(semHit.data, tokenMap);
|
|
1016
1156
|
if (isStream)
|
|
1017
|
-
return simulateHitStream(res, unmaskedData, provider,
|
|
1157
|
+
return simulateHitStream(res, unmaskedData, provider, originalModel, tier, semHit.similarity);
|
|
1018
1158
|
res.setHeader('X-Lemma-Cache', 'HIT');
|
|
1019
1159
|
res.setHeader('X-Lemma-Similarity', semHit.similarity.toFixed(3));
|
|
1020
1160
|
res.setHeader('X-Lemma-Tier', tier);
|
|
1021
1161
|
return res.json(unmaskedData);
|
|
1022
1162
|
}
|
|
1023
|
-
else if (semHit.similarity >= 0.
|
|
1163
|
+
else if (semHit.similarity >= 0.82) { // raised from 0.70 to 0.82 to prevent false positives
|
|
1024
1164
|
console.log(`💡 [CARS] Near Cache Hit (Similarity: ${(semHit.similarity * 100).toFixed(1)}%). Synthesizing answer locally via Ollama...`);
|
|
1025
1165
|
let cachedContent = '';
|
|
1026
1166
|
if (provider === 'openai') {
|
|
@@ -1058,10 +1198,10 @@ Adjusted Answer:`;
|
|
|
1058
1198
|
else if (provider === 'anthropic') {
|
|
1059
1199
|
synthesizedData.content[0].text = adjustedText;
|
|
1060
1200
|
}
|
|
1061
|
-
await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000
|
|
1201
|
+
await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000);
|
|
1062
1202
|
const unmaskedData = semanticScrubber.unmask(synthesizedData, tokenMap);
|
|
1063
1203
|
if (isStream) {
|
|
1064
|
-
return simulateHitStream(res, unmaskedData, provider,
|
|
1204
|
+
return simulateHitStream(res, unmaskedData, provider, originalModel, tier, semHit.similarity);
|
|
1065
1205
|
}
|
|
1066
1206
|
res.setHeader('X-Lemma-Cache', 'HIT-CARS');
|
|
1067
1207
|
res.setHeader('X-Lemma-Similarity', semHit.similarity.toFixed(3));
|
|
@@ -1078,22 +1218,86 @@ Adjusted Answer:`;
|
|
|
1078
1218
|
const cloudHit = await cloudSync.get(prompt);
|
|
1079
1219
|
if (cloudHit) {
|
|
1080
1220
|
cacheSet(provider, prompt, cloudHit.data);
|
|
1081
|
-
await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000
|
|
1221
|
+
await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000);
|
|
1082
1222
|
const unmaskedData = semanticScrubber.unmask(cloudHit.data, tokenMap);
|
|
1083
1223
|
if (isStream)
|
|
1084
|
-
return simulateHitStream(res, unmaskedData, provider,
|
|
1224
|
+
return simulateHitStream(res, unmaskedData, provider, originalModel, tier, cloudHit.similarity || 0.95);
|
|
1085
1225
|
res.setHeader('X-Lemma-Cache', 'HIT');
|
|
1086
1226
|
res.setHeader('X-Lemma-Similarity', (cloudHit.similarity || 0.95).toFixed(3));
|
|
1087
1227
|
res.setHeader('X-Lemma-Tier', tier);
|
|
1088
1228
|
return res.json(unmaskedData);
|
|
1089
1229
|
}
|
|
1090
1230
|
}
|
|
1231
|
+
// ── 2. Cache Miss Path ──
|
|
1232
|
+
const routingDecision = complexityRouter.evaluate(prompt, originalModel);
|
|
1233
|
+
const model = routingDecision.model;
|
|
1234
|
+
if (req.body.model)
|
|
1235
|
+
req.body.model = model;
|
|
1236
|
+
// Record complexity routing savings when model is downgraded
|
|
1237
|
+
if (routingDecision.complexity === 'low' && model !== originalModel) {
|
|
1238
|
+
const estimatedPromptTokens = Math.floor(prompt.length / 4);
|
|
1239
|
+
const routingSavings = Math.floor(estimatedPromptTokens * 0.8);
|
|
1240
|
+
savingsLedger.recordTokens('complexityRouting', routingSavings);
|
|
1241
|
+
}
|
|
1242
|
+
// Apply the Agent Multiplexer to local-verify the prompt in parallel
|
|
1243
|
+
const enrichedPrompt = await agentMultiplexer.multiplexPrompt(prompt, model);
|
|
1244
|
+
// Resolve conversation session key and retrieve/create the fileHistory map
|
|
1245
|
+
let fileHistory;
|
|
1246
|
+
if (req.body.messages && req.body.messages.length > 0) {
|
|
1247
|
+
try {
|
|
1248
|
+
const firstMsg = req.body.messages[0];
|
|
1249
|
+
const content = typeof firstMsg.content === 'string' ? firstMsg.content : JSON.stringify(firstMsg.content);
|
|
1250
|
+
const sessionKey = crypto.createHash('sha1').update(content).digest('hex');
|
|
1251
|
+
if (!this.sessions.has(sessionKey)) {
|
|
1252
|
+
// Enforce 500 active sessions cap to prevent memory leak (FIFO eviction)
|
|
1253
|
+
const MAX_SESSIONS = 500;
|
|
1254
|
+
if (this.sessions.size >= MAX_SESSIONS) {
|
|
1255
|
+
const oldestKey = this.sessions.keys().next().value;
|
|
1256
|
+
if (oldestKey)
|
|
1257
|
+
this.sessions.delete(oldestKey);
|
|
1258
|
+
}
|
|
1259
|
+
this.sessions.set(sessionKey, { fileHistory: new Map() });
|
|
1260
|
+
}
|
|
1261
|
+
fileHistory = this.sessions.get(sessionKey).fileHistory;
|
|
1262
|
+
}
|
|
1263
|
+
catch { }
|
|
1264
|
+
}
|
|
1265
|
+
// Compress long TS/JS codebase file blocks in the enriched prompt
|
|
1266
|
+
const squeezeResult = squeezePrompt(enrichedPrompt, prompt, fileHistory);
|
|
1267
|
+
const squeezedPrompt = squeezeResult.squeezed;
|
|
1268
|
+
const compressionSavings = squeezeResult.originalSize - squeezeResult.squeezedSize;
|
|
1269
|
+
if (compressionSavings > 0) {
|
|
1270
|
+
savingsLedger.record('contextSqueeze', squeezeResult.originalSize, squeezeResult.squeezedSize);
|
|
1271
|
+
console.log(`⚡ [Squeezer] Compressed prompt from ${squeezeResult.originalSize} to ${squeezeResult.squeezedSize} chars (Saved ${(squeezeResult.compressionRatio * 100).toFixed(1)}% context tokens!)`);
|
|
1272
|
+
}
|
|
1273
|
+
if (provider === 'openai' || provider === 'anthropic') {
|
|
1274
|
+
if (req.body.messages && req.body.messages.length > 0) {
|
|
1275
|
+
injectSqueezedContent(req.body.messages[req.body.messages.length - 1], squeezedPrompt);
|
|
1276
|
+
// Apply conversational history pruning
|
|
1277
|
+
const pruneResult = pruneHistoryMessages(req.body.messages);
|
|
1278
|
+
req.body.messages = pruneResult.messages;
|
|
1279
|
+
historyTokensSaved = pruneResult.tokensSavedEstimate + Math.floor(compressionSavings / 4);
|
|
1280
|
+
if (pruneResult.tokensSavedEstimate > 0) {
|
|
1281
|
+
savingsLedger.recordTokens('historyPrune', pruneResult.tokensSavedEstimate);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
else if (provider === 'gemini') {
|
|
1286
|
+
if (req.body.contents && req.body.contents.length > 0) {
|
|
1287
|
+
const lastContent = req.body.contents[req.body.contents.length - 1];
|
|
1288
|
+
if (lastContent.parts && lastContent.parts.length > 0) {
|
|
1289
|
+
lastContent.parts[lastContent.parts.length - 1].text = squeezedPrompt;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1091
1293
|
if (isStream) {
|
|
1092
1294
|
await recordStat(this.stats, this.projectName, false, Date.now() - t0, provider, historyTokensSaved);
|
|
1093
1295
|
if (!pro) {
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1296
|
+
await usageWriteQueue.enqueue(async () => {
|
|
1297
|
+
const u = await loadUsage();
|
|
1298
|
+
u.count++;
|
|
1299
|
+
await writeJson(USAGE_FILE, u);
|
|
1300
|
+
});
|
|
1097
1301
|
}
|
|
1098
1302
|
return relayMissStream(res, req.body, provider, tier);
|
|
1099
1303
|
}
|
|
@@ -1105,9 +1309,11 @@ Adjusted Answer:`;
|
|
|
1105
1309
|
await cloudSync.set(prompt, data);
|
|
1106
1310
|
}
|
|
1107
1311
|
if (!pro) {
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1312
|
+
await usageWriteQueue.enqueue(async () => {
|
|
1313
|
+
const u = await loadUsage();
|
|
1314
|
+
u.count++;
|
|
1315
|
+
await writeJson(USAGE_FILE, u);
|
|
1316
|
+
});
|
|
1111
1317
|
}
|
|
1112
1318
|
await recordStat(this.stats, this.projectName, false, Date.now() - t0, provider, historyTokensSaved);
|
|
1113
1319
|
return res.json(semanticScrubber.unmask(data, tokenMap));
|
|
@@ -1118,6 +1324,43 @@ Adjusted Answer:`;
|
|
|
1118
1324
|
}
|
|
1119
1325
|
async callUpstream(body, provider) {
|
|
1120
1326
|
let apiKey = provider === 'openai' ? process.env.OPENAI_API_KEY : (provider === 'anthropic' ? process.env.ANTHROPIC_API_KEY : process.env.GEMINI_API_KEY);
|
|
1327
|
+
// If the API key is an OpenAI key (sk-proj-) but requested for Anthropic or Gemini,
|
|
1328
|
+
// multiplex it internally to OpenAI gpt-4o-mini to allow live, cheap simulated provider routing.
|
|
1329
|
+
const isMockedProvider = apiKey && apiKey.startsWith('sk-') && provider !== 'openai';
|
|
1330
|
+
if (isMockedProvider) {
|
|
1331
|
+
console.log(`\n\x1b[36m🔀 [Gateway Router] Multiplexing B2B ${provider.toUpperCase()} request to OpenAI using key: ...${apiKey.substring(8, 14)}...\x1b[0m`);
|
|
1332
|
+
const payload = {
|
|
1333
|
+
model: 'gpt-4o-mini',
|
|
1334
|
+
messages: body.messages || [{ role: 'user', content: extractPrompt(body, provider) || 'Hello' }],
|
|
1335
|
+
stream: false // keep stream false for simulation simplicity
|
|
1336
|
+
};
|
|
1337
|
+
const baseURL = 'https://api.openai.com/v1/chat/completions';
|
|
1338
|
+
const resp = await axios.post(baseURL, payload, {
|
|
1339
|
+
headers: {
|
|
1340
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1341
|
+
'Content-Type': 'application/json'
|
|
1342
|
+
}
|
|
1343
|
+
});
|
|
1344
|
+
if (provider === 'anthropic') {
|
|
1345
|
+
const text = resp.data.choices?.[0]?.message?.content || '';
|
|
1346
|
+
return {
|
|
1347
|
+
id: `msg-${Date.now()}`,
|
|
1348
|
+
type: 'message',
|
|
1349
|
+
role: 'assistant',
|
|
1350
|
+
content: [{ type: 'text', text }],
|
|
1351
|
+
model: body.model || 'claude-3-5-sonnet'
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
else if (provider === 'gemini') {
|
|
1355
|
+
const text = resp.data.choices?.[0]?.message?.content || '';
|
|
1356
|
+
return {
|
|
1357
|
+
candidates: [{
|
|
1358
|
+
content: { parts: [{ text }] },
|
|
1359
|
+
finishReason: 'STOP'
|
|
1360
|
+
}]
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1121
1364
|
if (!apiKey) {
|
|
1122
1365
|
console.log(`\n\x1b[33m⚠️ No API Key found for ${provider.toUpperCase()}. Falling back to local Ollama (Zero-Cost Mode)!\x1b[0m`);
|
|
1123
1366
|
// If the requested model is a cloud model (e.g. gpt-4o, claude), map it to a local model like llama3
|
|
@@ -1144,6 +1387,15 @@ Adjusted Answer:`;
|
|
|
1144
1387
|
}
|
|
1145
1388
|
async start() {
|
|
1146
1389
|
this.stats = await loadStats();
|
|
1390
|
+
// Start active cache eviction sweep every hour
|
|
1391
|
+
setInterval(() => {
|
|
1392
|
+
const now = Date.now();
|
|
1393
|
+
for (const [k, e] of CACHE.entries()) {
|
|
1394
|
+
if (e.ttl > 0 && now - e.createdAt > e.ttl) {
|
|
1395
|
+
CACHE.delete(k);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}, 3600000).unref();
|
|
1147
1399
|
const server = http.createServer(this.app);
|
|
1148
1400
|
server.on('error', (e) => {
|
|
1149
1401
|
if (e.code === 'EADDRINUSE') {
|