@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.
Files changed (63) hide show
  1. package/README.md +1 -1
  2. package/dashboard/dist/assets/index-0N21ZtpO.js +481 -0
  3. package/dashboard/dist/assets/index-0N21ZtpO.js.map +1 -0
  4. package/dashboard/dist/assets/index-DUOrThix.css +1 -0
  5. package/dashboard/dist/index.html +2 -2
  6. package/dist/cjs/cli/lemma-proxy.d.ts.map +1 -1
  7. package/dist/cjs/cli/lemma-proxy.js +365 -113
  8. package/dist/cjs/cli/lemma-proxy.js.map +1 -1
  9. package/dist/cjs/config/index.d.ts.map +1 -1
  10. package/dist/cjs/config/index.js +2 -1
  11. package/dist/cjs/config/index.js.map +1 -1
  12. package/dist/cjs/errors/LemmaError.d.ts +52 -0
  13. package/dist/cjs/errors/LemmaError.d.ts.map +1 -0
  14. package/dist/cjs/errors/LemmaError.js +84 -0
  15. package/dist/cjs/errors/LemmaError.js.map +1 -0
  16. package/dist/cjs/index.d.ts.map +1 -1
  17. package/dist/cjs/index.js +24 -22
  18. package/dist/cjs/index.js.map +1 -1
  19. package/dist/cjs/subconscious/SemanticCache.d.ts +4 -4
  20. package/dist/cjs/subconscious/SemanticCache.d.ts.map +1 -1
  21. package/dist/cjs/subconscious/SemanticCache.js +14 -10
  22. package/dist/cjs/subconscious/SemanticCache.js.map +1 -1
  23. package/dist/cjs/types/index.d.ts +8 -8
  24. package/dist/cjs/types/index.d.ts.map +1 -1
  25. package/dist/cjs/utils/SavingsLedger.d.ts +2 -0
  26. package/dist/cjs/utils/SavingsLedger.d.ts.map +1 -1
  27. package/dist/cjs/utils/SavingsLedger.js +49 -0
  28. package/dist/cjs/utils/SavingsLedger.js.map +1 -1
  29. package/dist/cjs/utils/logger.d.ts +27 -6
  30. package/dist/cjs/utils/logger.d.ts.map +1 -1
  31. package/dist/cjs/utils/logger.js +96 -28
  32. package/dist/cjs/utils/logger.js.map +1 -1
  33. package/dist/esm/cli/lemma-proxy.d.ts.map +1 -1
  34. package/dist/esm/cli/lemma-proxy.js +365 -113
  35. package/dist/esm/cli/lemma-proxy.js.map +1 -1
  36. package/dist/esm/config/index.d.ts.map +1 -1
  37. package/dist/esm/config/index.js +2 -1
  38. package/dist/esm/config/index.js.map +1 -1
  39. package/dist/esm/errors/LemmaError.d.ts +52 -0
  40. package/dist/esm/errors/LemmaError.d.ts.map +1 -0
  41. package/dist/esm/errors/LemmaError.js +75 -0
  42. package/dist/esm/errors/LemmaError.js.map +1 -0
  43. package/dist/esm/index.d.ts.map +1 -1
  44. package/dist/esm/index.js +24 -22
  45. package/dist/esm/index.js.map +1 -1
  46. package/dist/esm/subconscious/SemanticCache.d.ts +4 -4
  47. package/dist/esm/subconscious/SemanticCache.d.ts.map +1 -1
  48. package/dist/esm/subconscious/SemanticCache.js +14 -10
  49. package/dist/esm/subconscious/SemanticCache.js.map +1 -1
  50. package/dist/esm/types/index.d.ts +8 -8
  51. package/dist/esm/types/index.d.ts.map +1 -1
  52. package/dist/esm/utils/SavingsLedger.d.ts +2 -0
  53. package/dist/esm/utils/SavingsLedger.d.ts.map +1 -1
  54. package/dist/esm/utils/SavingsLedger.js +46 -0
  55. package/dist/esm/utils/SavingsLedger.js.map +1 -1
  56. package/dist/esm/utils/logger.d.ts +27 -6
  57. package/dist/esm/utils/logger.d.ts.map +1 -1
  58. package/dist/esm/utils/logger.js +93 -28
  59. package/dist/esm/utils/logger.js.map +1 -1
  60. package/package.json +28 -24
  61. package/dashboard/dist/assets/index-CIlkFwDs.js +0 -490
  62. package/dashboard/dist/assets/index-CIlkFwDs.js.map +0 -1
  63. 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.2';
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
- try {
83
- const tempFile = file + '.tmp';
84
- await fs.promises.writeFile(tempFile, JSON.stringify(data, null, 2));
85
- await fs.promises.rename(tempFile, file);
86
- }
87
- catch { }
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
- return !!(l && l.isPro);
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
- s.totalTokensSaved += (fromCache ? (tokensSaved || 2000) : (tokensSaved || 0));
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: fromCache ? (tokensSaved || 2000) : (tokensSaved || 0)
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
- CACHE.set(cacheKey(provider, prompt), { input: prompt, data, createdAt: Date.now(), ttl: TTL_MS });
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: stats.totalTokensSaved,
821
- costSaved: (stats.totalTokensSaved / 1000) * 0.02,
871
+ tokensSaved: ledgerSnap.total.tokensSaved,
872
+ costSaved: ledgerSnap.total.costSaved,
822
873
  secretsMasked: secretsMasked,
823
- activeAgents: 1,
824
- totalAgents: 1,
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
- res.json({ agents: [
832
- { id: 'lemma-gateway', name: 'Lemma Gateway', status: 'active', capabilities: ['Privacy Firewall', 'Semantic Cache', 'Complexity Router'], tasksCompleted: stats[this.projectName]?.total || 0, lastSeen: new Date().toISOString(), errorCount: 0, cacheHitRate: 0.85 }
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
- const events = EVENT_LOG.slice().reverse().slice(start, start + limit);
840
- res.json({ events, pagination: { total: EVENT_LOG.length, page, limit } });
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
- res.json({ topQueries: [] });
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 count = CACHE.size;
882
- CACHE.clear();
883
- EVENT_LOG.length = 0; // Clear live event log
884
- savingsLedger.reset(); // Reset savings counters
885
- // Reset and persist statistics for the current project
886
- if (this.stats[this.projectName]) {
887
- this.stats[this.projectName] = {
888
- total: 0,
889
- hits: 0,
890
- misses: 0,
891
- totalLatency: 0,
892
- totalTokensSaved: 0,
893
- providers: {}
894
- };
895
- writeJson(STATS_FILE, this.stats).catch(() => { });
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
- res.json({ success: true, clearedEntries: count });
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
- const routingDecision = complexityRouter.evaluate(prompt, originalModel);
942
- const model = routingDecision.model;
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 + historyTokensSaved;
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, model, tier, 1.0);
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 + historyTokensSaved);
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, model, tier, semHit.similarity);
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.70) {
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 + historyTokensSaved);
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, model, tier, semHit.similarity);
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 + historyTokensSaved);
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, model, tier, cloudHit.similarity || 0.95);
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
- const u = await loadUsage();
1095
- u.count++;
1096
- await writeJson(USAGE_FILE, u);
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
- const u = await loadUsage();
1109
- u.count++;
1110
- await writeJson(USAGE_FILE, u);
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') {