@nxuss/lemma 0.3.0 → 0.3.1
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/dist/cjs/core/GhostAlchemist.d.ts +18 -0
- package/dist/cjs/core/GhostAlchemist.d.ts.map +1 -0
- package/dist/cjs/core/GhostAlchemist.js +55 -0
- package/dist/cjs/core/GhostAlchemist.js.map +1 -0
- package/dist/cjs/core/GhostListener.d.ts +20 -0
- package/dist/cjs/core/GhostListener.d.ts.map +1 -0
- package/dist/cjs/core/GhostListener.js +97 -0
- package/dist/cjs/core/GhostListener.js.map +1 -0
- package/dist/esm/core/GhostAlchemist.d.ts +18 -0
- package/dist/esm/core/GhostAlchemist.d.ts.map +1 -0
- package/dist/esm/core/GhostAlchemist.js +48 -0
- package/dist/esm/core/GhostAlchemist.js.map +1 -0
- package/dist/esm/core/GhostListener.d.ts +20 -0
- package/dist/esm/core/GhostListener.d.ts.map +1 -0
- package/dist/esm/core/GhostListener.js +93 -0
- package/dist/esm/core/GhostListener.js.map +1 -0
- package/lemma-proxy.cjs +477 -0
- package/package.json +9 -7
- package/lemma-proxy.js +0 -810
package/lemma-proxy.cjs
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/**
|
|
4
|
+
* Lemma Proxy v0.3.1 — Universal AI Cache CLI
|
|
5
|
+
* Commands: start, stop, stats, status, activate <key>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { program } = require('commander');
|
|
9
|
+
const express = require('express');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const crypto = require('crypto');
|
|
13
|
+
const { ChromaClient } = require('chromadb');
|
|
14
|
+
const chroma = new ChromaClient({ host: 'localhost', port: 8000 });
|
|
15
|
+
const dummyEmbeddingFunction = { generate: (texts) => Promise.resolve([]) };
|
|
16
|
+
const https = require('https');
|
|
17
|
+
const http = require('http');
|
|
18
|
+
const axios = require('axios');
|
|
19
|
+
const WebSocket = require('ws');
|
|
20
|
+
|
|
21
|
+
// ── Paths ──────────────────────────────────────────────────────────────────────
|
|
22
|
+
const HOME = process.env.HOME || process.env.USERPROFILE || '~';
|
|
23
|
+
const CACHE_DIR = path.join(HOME, '.lemma-cache');
|
|
24
|
+
const PID_FILE = path.join(CACHE_DIR, 'proxy.pid');
|
|
25
|
+
const PORT_FILE = path.join(CACHE_DIR, 'proxy.port');
|
|
26
|
+
const STATS_FILE = path.join(CACHE_DIR, 'stats.json');
|
|
27
|
+
const USAGE_FILE = path.join(CACHE_DIR, 'usage.json');
|
|
28
|
+
const LICENSE_FILE = path.join(CACHE_DIR, 'license.json');
|
|
29
|
+
|
|
30
|
+
const EVENT_LOG = [];
|
|
31
|
+
const MAX_EVENTS = 100;
|
|
32
|
+
|
|
33
|
+
function logEvent(event) {
|
|
34
|
+
const ev = { id: Math.random().toString(36).slice(2, 11), timestamp: Date.now(), ...event };
|
|
35
|
+
EVENT_LOG.push(ev);
|
|
36
|
+
if (EVENT_LOG.length > MAX_EVENTS) EVENT_LOG.shift();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const FREE_LIMIT = 300;
|
|
40
|
+
const WARN_PCT = 0.8;
|
|
41
|
+
const VALIDATE_URL = 'https://lemma.nxus.studio/api/v1/validate';
|
|
42
|
+
|
|
43
|
+
if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
44
|
+
|
|
45
|
+
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
46
|
+
function readJson(file, fallback) {
|
|
47
|
+
try { if (fs.existsSync(file)) return JSON.parse(fs.readFileSync(file, 'utf8')); } catch {}
|
|
48
|
+
return fallback;
|
|
49
|
+
}
|
|
50
|
+
function writeJson(file, data) {
|
|
51
|
+
try { fs.writeFileSync(file, JSON.stringify(data, null, 2)); } catch {}
|
|
52
|
+
}
|
|
53
|
+
function projectHash(name) {
|
|
54
|
+
return crypto.createHash('sha1').update(name).digest('hex').slice(0, 12);
|
|
55
|
+
}
|
|
56
|
+
function detectProject() {
|
|
57
|
+
const pkg = path.join(process.cwd(), 'package.json');
|
|
58
|
+
if (fs.existsSync(pkg)) {
|
|
59
|
+
try { const p = JSON.parse(fs.readFileSync(pkg, 'utf8')); if (p.name) return p.name; } catch {}
|
|
60
|
+
}
|
|
61
|
+
return path.basename(process.cwd());
|
|
62
|
+
}
|
|
63
|
+
function ensureProjectDir(name) {
|
|
64
|
+
const dir = path.join(CACHE_DIR, projectHash(name));
|
|
65
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
66
|
+
const marker = path.join(dir, 'project-name.txt');
|
|
67
|
+
if (!fs.existsSync(marker)) fs.writeFileSync(marker, name);
|
|
68
|
+
return dir;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── License helpers ────────────────────────────────────────────────────────────
|
|
72
|
+
function loadLicense() {
|
|
73
|
+
return readJson(LICENSE_FILE, { isPro: false });
|
|
74
|
+
}
|
|
75
|
+
function isPro() { const l = loadLicense(); return !!(l && l.isPro); }
|
|
76
|
+
|
|
77
|
+
async function validateKeyRemote(key) {
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
const body = JSON.stringify({ key });
|
|
80
|
+
const u = new URL(VALIDATE_URL);
|
|
81
|
+
const req = https.request({ hostname: u.hostname, port: 443, path: u.pathname, method: 'POST',
|
|
82
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), 'User-Agent': 'lemma-proxy/0.3.1' }
|
|
83
|
+
}, res => {
|
|
84
|
+
let d = '';
|
|
85
|
+
res.on('data', c => d += c);
|
|
86
|
+
res.on('end', () => { try { resolve(JSON.parse(d)); } catch { reject(new Error('bad json')); } });
|
|
87
|
+
});
|
|
88
|
+
req.on('error', reject);
|
|
89
|
+
const t = setTimeout(() => { req.destroy(); reject(new Error('timeout')); }, 5000);
|
|
90
|
+
req.on('close', () => clearTimeout(t));
|
|
91
|
+
req.write(body); req.end();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Usage helpers ──────────────────────────────────────────────────────────────
|
|
96
|
+
function loadUsage() {
|
|
97
|
+
const u = readJson(USAGE_FILE, { count: 0, lastReset: new Date().toISOString(), history: [] });
|
|
98
|
+
const now = new Date(), last = new Date(u.lastReset || 0);
|
|
99
|
+
if (now.getMonth() !== last.getMonth() || now.getFullYear() !== last.getFullYear()) {
|
|
100
|
+
const fresh = { count: 0, lastReset: now.toISOString(), history: [...(u.history||[]), { month: last.toISOString().slice(0,7), count: u.count }] };
|
|
101
|
+
writeJson(USAGE_FILE, fresh); return fresh;
|
|
102
|
+
}
|
|
103
|
+
return u;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Stats helpers ──────────────────────────────────────────────────────────────
|
|
107
|
+
function loadStats() { return readJson(STATS_FILE, {}); }
|
|
108
|
+
function recordStat(stats, project, fromCache, latencyMs, provider, tokensSaved) {
|
|
109
|
+
if (!stats[project]) stats[project] = { total:0, hits:0, misses:0, totalLatency:0, totalTokensSaved:0, providers:{} };
|
|
110
|
+
const s = stats[project];
|
|
111
|
+
s.total++; s.totalLatency += latencyMs;
|
|
112
|
+
fromCache ? s.hits++ : s.misses++;
|
|
113
|
+
s.totalTokensSaved += (fromCache ? (tokensSaved||2000) : 0);
|
|
114
|
+
if (!s.providers[provider]) s.providers[provider] = { hits:0, misses:0 };
|
|
115
|
+
fromCache ? s.providers[provider].hits++ : s.providers[provider].misses++;
|
|
116
|
+
|
|
117
|
+
logEvent({
|
|
118
|
+
type: fromCache ? 'cache:hit' : 'cache:miss',
|
|
119
|
+
project,
|
|
120
|
+
latency: latencyMs,
|
|
121
|
+
provider,
|
|
122
|
+
tokens: fromCache ? (tokensSaved||2000) : 0
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
writeJson(STATS_FILE, stats);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Cache (in-process) ─────────────────────────────────────────────────────────
|
|
129
|
+
const CACHE = new Map();
|
|
130
|
+
const TTL_MS = 86400000 * 7;
|
|
131
|
+
|
|
132
|
+
function cacheKey(prompt) {
|
|
133
|
+
return crypto.createHash('sha256').update(prompt.toLowerCase().trim()).digest('hex');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Semantic Search (Pro) ──────────────────────────────────────────────────────
|
|
137
|
+
async function getEmbedding(text) {
|
|
138
|
+
try {
|
|
139
|
+
const resp = await axios.post('http://localhost:11434/api/embeddings', {
|
|
140
|
+
model: 'nomic-embed-text',
|
|
141
|
+
prompt: text
|
|
142
|
+
}, { timeout: 5000 });
|
|
143
|
+
return resp.data.embedding;
|
|
144
|
+
} catch (e) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function semanticGet(prompt) {
|
|
150
|
+
if (!isPro()) return null;
|
|
151
|
+
const emb = await getEmbedding(prompt);
|
|
152
|
+
if (!emb) return null;
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const collection = await chroma.getOrCreateCollection({
|
|
156
|
+
name: 'lemma-cache',
|
|
157
|
+
embeddingFunction: dummyEmbeddingFunction,
|
|
158
|
+
metadata: { "hnsw:space": "cosine" }
|
|
159
|
+
});
|
|
160
|
+
const res = await collection.query({
|
|
161
|
+
queryEmbeddings: [emb],
|
|
162
|
+
nResults: 1
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (!res.ids[0] || res.ids[0].length === 0) return null;
|
|
166
|
+
|
|
167
|
+
const distance = res.distances[0][0];
|
|
168
|
+
const similarity = Math.max(0, 1 - distance);
|
|
169
|
+
const THRESHOLD = 0.7;
|
|
170
|
+
|
|
171
|
+
if (similarity >= THRESHOLD) {
|
|
172
|
+
const metadata = res.metadatas[0][0];
|
|
173
|
+
return {
|
|
174
|
+
data: JSON.parse(metadata.response),
|
|
175
|
+
similarity,
|
|
176
|
+
id: res.ids[0][0]
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
} catch (e) {}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function semanticSet(prompt, data) {
|
|
184
|
+
if (!isPro()) return;
|
|
185
|
+
const emb = await getEmbedding(prompt);
|
|
186
|
+
if (!emb) return;
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const collection = await chroma.getOrCreateCollection({
|
|
190
|
+
name: 'lemma-cache',
|
|
191
|
+
embeddingFunction: dummyEmbeddingFunction,
|
|
192
|
+
metadata: { "hnsw:space": "cosine" }
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await collection.add({
|
|
196
|
+
ids: [crypto.randomUUID?.() || Math.random().toString(36).substring(7)],
|
|
197
|
+
embeddings: [emb],
|
|
198
|
+
metadatas: [{
|
|
199
|
+
prompt,
|
|
200
|
+
response: JSON.stringify(data),
|
|
201
|
+
timestamp: Date.now()
|
|
202
|
+
}],
|
|
203
|
+
documents: [prompt]
|
|
204
|
+
});
|
|
205
|
+
} catch (e) {}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function cacheGet(prompt) {
|
|
209
|
+
const k = cacheKey(prompt);
|
|
210
|
+
const e = CACHE.get(k);
|
|
211
|
+
if (!e) return null;
|
|
212
|
+
if (e.ttl > 0 && Date.now() - e.createdAt > e.ttl) { CACHE.delete(k); return null; }
|
|
213
|
+
return e;
|
|
214
|
+
}
|
|
215
|
+
function cacheSet(prompt, data) {
|
|
216
|
+
CACHE.set(cacheKey(prompt), { input: prompt, data, createdAt: Date.now(), ttl: TTL_MS });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Prompt extraction ──────────────────────────────────────────────────────────
|
|
220
|
+
function extractPrompt(body, provider) {
|
|
221
|
+
const msgs = body.messages || [];
|
|
222
|
+
if (provider === 'openai' || provider === 'anthropic') return msgs.map(m => (typeof m.content === 'string' ? m.content : JSON.stringify(m.content))).join('\n');
|
|
223
|
+
if (provider === 'gemini') {
|
|
224
|
+
const contents = body.contents || [];
|
|
225
|
+
return contents.map(c => c.parts.map(p => p.text).join(' ')).join('\n');
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── SSE helpers ────────────────────────────────────────────────────────────────
|
|
231
|
+
function setSseHeaders(res, fromCache, similarity, tier) {
|
|
232
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
233
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
234
|
+
res.setHeader('Connection', 'keep-alive');
|
|
235
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
236
|
+
res.setHeader('X-Lemma-Cache', fromCache ? 'HIT' : 'MISS');
|
|
237
|
+
res.setHeader('X-Lemma-Similarity', (similarity || 1.0).toFixed(3));
|
|
238
|
+
res.setHeader('X-Lemma-Tier', tier);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function buildOaiChunk(id, model, text, stop) {
|
|
242
|
+
return { id, object:'chat.completion.chunk', created: Math.floor(Date.now()/1000), model,
|
|
243
|
+
choices:[{ index:0, delta: stop ? {} : { content: text }, finish_reason: stop ? 'stop' : null }] };
|
|
244
|
+
}
|
|
245
|
+
function buildAntChunk(text, stop) {
|
|
246
|
+
if (stop) return { type:'message_stop' };
|
|
247
|
+
return { type:'content_block_delta', index:0, delta:{ type:'text_delta', text } };
|
|
248
|
+
}
|
|
249
|
+
function buildGemChunk(text, stop) {
|
|
250
|
+
return { candidates: [{ content: { parts: [{ text }] }, finishReason: stop ? 'STOP' : undefined }] };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function simulateHitStream(res, cachedData, provider, model, tier, similarity) {
|
|
254
|
+
setSseHeaders(res, true, similarity, tier);
|
|
255
|
+
const id = `lemma-${Date.now()}`;
|
|
256
|
+
let text = '';
|
|
257
|
+
try {
|
|
258
|
+
if (provider === 'openai') text = cachedData?.choices?.[0]?.message?.content || '';
|
|
259
|
+
else if (provider === 'anthropic') { const c = cachedData?.content; text = Array.isArray(c) ? c.map(x=>x.text||'').join('') : String(c||''); }
|
|
260
|
+
else if (provider === 'gemini') { text = cachedData?.candidates?.[0]?.content?.parts?.[0]?.text || ''; }
|
|
261
|
+
} catch {}
|
|
262
|
+
const words = text.split(' ');
|
|
263
|
+
for (let i = 0; i < words.length; i += 5) {
|
|
264
|
+
const chunk = words.slice(i, i+5).join(' ');
|
|
265
|
+
let evt;
|
|
266
|
+
if (provider === 'openai') evt = buildOaiChunk(id, model, chunk, false);
|
|
267
|
+
else if (provider === 'anthropic') evt = buildAntChunk(chunk, false);
|
|
268
|
+
else if (provider === 'gemini') evt = buildGemChunk(chunk, false);
|
|
269
|
+
|
|
270
|
+
res.write(provider === 'gemini' ? `${JSON.stringify(evt)}\n` : `data: ${JSON.stringify(evt)}\n\n`);
|
|
271
|
+
await new Promise(r => setTimeout(r, 15));
|
|
272
|
+
}
|
|
273
|
+
const stopEvt = provider === 'openai' ? buildOaiChunk(id, model, '', true) : (provider === 'anthropic' ? buildAntChunk('', true) : buildGemChunk('', true));
|
|
274
|
+
res.write(provider === 'gemini' ? `${JSON.stringify(stopEvt)}\n` : `data: ${JSON.stringify(stopEvt)}\n\n`);
|
|
275
|
+
if (provider !== 'gemini') res.write('data: [DONE]\n\n');
|
|
276
|
+
res.end();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function relayMissStream(res, body, provider, tier) {
|
|
280
|
+
setSseHeaders(res, false, 1.0, tier);
|
|
281
|
+
const apiKey = provider === 'openai' ? process.env.OPENAI_API_KEY : process.env.ANTHROPIC_API_KEY;
|
|
282
|
+
if (!apiKey) { res.write(`data: ${JSON.stringify({error:`${provider.toUpperCase()}_API_KEY not set`})}\n\ndata: [DONE]\n\n`); res.end(); return; }
|
|
283
|
+
const hostname = provider === 'openai' ? 'api.openai.com' : 'api.anthropic.com';
|
|
284
|
+
const upPath = provider === 'openai' ? '/v1/chat/completions' : '/v1/messages';
|
|
285
|
+
const headers = provider === 'openai'
|
|
286
|
+
? { Authorization: `Bearer ${apiKey}`, 'Content-Type':'application/json' }
|
|
287
|
+
: { 'x-api-key': apiKey, 'anthropic-version':'2023-06-01', 'Content-Type':'application/json' };
|
|
288
|
+
const payload = JSON.stringify({ ...body, stream: true });
|
|
289
|
+
await new Promise(resolve => {
|
|
290
|
+
const req = https.request({ hostname, port:443, path:upPath, method:'POST',
|
|
291
|
+
headers: { ...headers, 'Content-Length': Buffer.byteLength(payload) }
|
|
292
|
+
}, upstream => {
|
|
293
|
+
upstream.on('data', chunk => res.write(chunk));
|
|
294
|
+
upstream.on('end', () => { res.write('data: [DONE]\n\n'); res.end(); resolve(); });
|
|
295
|
+
upstream.on('error', err => { res.write(`data: ${JSON.stringify({error:err.message})}\n\ndata: [DONE]\n\n`); res.end(); resolve(); });
|
|
296
|
+
});
|
|
297
|
+
req.on('error', err => { res.write(`data: ${JSON.stringify({error:err.message})}\n\ndata: [DONE]\n\n`); res.end(); resolve(); });
|
|
298
|
+
req.write(payload); req.end();
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── Proxy Server class ─────────────────────────────────────────────────────────
|
|
303
|
+
class LemmaServer {
|
|
304
|
+
constructor(port, projectName) {
|
|
305
|
+
this.port = port;
|
|
306
|
+
this.projectName = projectName || detectProject();
|
|
307
|
+
this.projectDir = ensureProjectDir(this.projectName);
|
|
308
|
+
this.stats = loadStats();
|
|
309
|
+
this.app = express();
|
|
310
|
+
this.setupMiddleware();
|
|
311
|
+
this.setupRoutes();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
setupMiddleware() {
|
|
315
|
+
this.app.use(express.json({ limit: '10mb' }));
|
|
316
|
+
this.app.use((req, res, next) => {
|
|
317
|
+
res.header('Access-Control-Allow-Origin', '*');
|
|
318
|
+
res.header('Access-Control-Allow-Headers', '*');
|
|
319
|
+
res.header('Access-Control-Allow-Methods', '*');
|
|
320
|
+
if (req.method === 'OPTIONS') return res.sendStatus(200);
|
|
321
|
+
next();
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
setupRoutes() {
|
|
326
|
+
this.app.get('/health', (req, res) => {
|
|
327
|
+
const pro = isPro();
|
|
328
|
+
res.json({ status:'ok', project: this.projectName, mode: pro?'pro':'standard', tier: pro?'pro':'free', port: this.port });
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
this.app.get('/v1/models', (req, res) => {
|
|
332
|
+
res.json({ object:'list', data:[
|
|
333
|
+
{ id:'gpt-4o', object:'model', owned_by:'lemma-proxy' },
|
|
334
|
+
{ id:'claude-3-5-sonnet-20241022', object:'model', owned_by:'lemma-proxy' }
|
|
335
|
+
]});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
this.app.post('/v1/chat/completions', (req, res) => this.handleCompletion(req, res, 'openai'));
|
|
339
|
+
this.app.post('/v1/messages', (req, res) => this.handleCompletion(req, res, 'anthropic'));
|
|
340
|
+
this.app.post('/v1beta/models/:modelId:generateContent', (req, res) => this.handleCompletion(req, res, 'gemini'));
|
|
341
|
+
|
|
342
|
+
// Dashboard APIs
|
|
343
|
+
const dashboardPath = path.join(__dirname, 'dashboard', 'dist');
|
|
344
|
+
if (fs.existsSync(dashboardPath)) {
|
|
345
|
+
this.app.use('/dashboard/', express.static(dashboardPath));
|
|
346
|
+
this.app.get('/api/metrics', (req, res) => {
|
|
347
|
+
const stats = this.stats[this.projectName] || { total:0, hits:0, misses:0, totalLatency:0, totalTokensSaved:0 };
|
|
348
|
+
res.json({ timestamp: new Date().toISOString(), totalRequests: stats.total, cacheHits: stats.hits, tokensSaved: stats.totalTokensSaved, tier: isPro()?'pro':'free' });
|
|
349
|
+
});
|
|
350
|
+
this.app.get('/dashboard/*', (req, res) => res.sendFile(path.join(dashboardPath, 'index.html')));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async handleCompletion(req, res, provider) {
|
|
355
|
+
const t0 = Date.now();
|
|
356
|
+
const pro = isPro();
|
|
357
|
+
const tier = pro ? 'pro' : 'standard';
|
|
358
|
+
const isStream = !!req.body.stream;
|
|
359
|
+
|
|
360
|
+
if (!pro) {
|
|
361
|
+
const usage = loadUsage();
|
|
362
|
+
if (usage.count >= FREE_LIMIT) return res.status(429).json({ error:{ message:`Free tier limit (${FREE_LIMIT}/mo). Upgrade: https://lemma.nxus.studio/upgrade` }});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const prompt = extractPrompt(req.body, provider);
|
|
366
|
+
if (!prompt) return res.status(400).json({ error:'Invalid request format' });
|
|
367
|
+
|
|
368
|
+
const model = req.body.model || 'gpt-4o';
|
|
369
|
+
const hit = cacheGet(prompt);
|
|
370
|
+
if (hit) {
|
|
371
|
+
recordStat(this.stats, this.projectName, true, Date.now()-t0, provider, 2000);
|
|
372
|
+
if (isStream) return simulateHitStream(res, hit.data, provider, model, tier, 1.0);
|
|
373
|
+
return res.json(hit.data);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (pro) {
|
|
377
|
+
const semHit = await semanticGet(prompt);
|
|
378
|
+
if (semHit) {
|
|
379
|
+
recordStat(this.stats, this.projectName, true, Date.now()-t0, provider, 2000);
|
|
380
|
+
if (isStream) return simulateHitStream(res, semHit.data, provider, model, tier, semHit.similarity);
|
|
381
|
+
return res.json(semHit.data);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (isStream) {
|
|
386
|
+
recordStat(this.stats, this.projectName, false, Date.now()-t0, provider, 0);
|
|
387
|
+
if (!pro) { const u = loadUsage(); u.count++; writeJson(USAGE_FILE, u); }
|
|
388
|
+
return relayMissStream(res, req.body, provider, tier);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
const data = await this.callUpstream(req.body, provider);
|
|
393
|
+
cacheSet(prompt, data);
|
|
394
|
+
if (pro) semanticSet(prompt, data);
|
|
395
|
+
if (!pro) { const u = loadUsage(); u.count++; writeJson(USAGE_FILE, u); }
|
|
396
|
+
recordStat(this.stats, this.projectName, false, Date.now()-t0, provider, 0);
|
|
397
|
+
return res.json(data);
|
|
398
|
+
} catch (err) {
|
|
399
|
+
return res.status(500).json({ error: err.message });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async callUpstream(body, provider) {
|
|
404
|
+
const apiKey = provider === 'openai' ? process.env.OPENAI_API_KEY : (provider === 'anthropic' ? process.env.ANTHROPIC_API_KEY : process.env.GEMINI_API_KEY);
|
|
405
|
+
const baseURL = provider === 'openai' ? 'https://api.openai.com/v1/chat/completions' : (provider === 'anthropic' ? 'https://api.anthropic.com/v1/messages' : `https://generativelanguage.googleapis.com/v1beta/models/${body.model || 'gemini-1.5-pro'}:generateContent?key=${apiKey}`);
|
|
406
|
+
const headers = provider === 'openai' ? { Authorization:`Bearer ${apiKey}` } : (provider === 'anthropic' ? { 'x-api-key':apiKey, 'anthropic-version':'2023-06-01' } : {});
|
|
407
|
+
const resp = await axios.post(baseURL, body, { headers: { ...headers, 'Content-Type':'application/json' } });
|
|
408
|
+
return resp.data;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
start() {
|
|
412
|
+
const server = http.createServer(this.app);
|
|
413
|
+
server.listen(this.port, () => {
|
|
414
|
+
fs.writeFileSync(PID_FILE, String(process.pid));
|
|
415
|
+
fs.writeFileSync(PORT_FILE, String(this.port));
|
|
416
|
+
console.log(`\n🚀 Lemma Proxy v0.3.1\n📁 Project : ${this.projectName}\n🔌 Port : ${this.port}\n`);
|
|
417
|
+
});
|
|
418
|
+
process.on('SIGTERM', () => server.close());
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ── CLI ────────────────────────────────────────────────────────────────────────
|
|
423
|
+
program.name('lemma').description('Lemma Proxy CLI').version('0.3.1');
|
|
424
|
+
|
|
425
|
+
program.command('start')
|
|
426
|
+
.description('Start the proxy server')
|
|
427
|
+
.option('-p, --port <number>', 'Port to listen on', '8081')
|
|
428
|
+
.option('--project <name>', 'Override project name')
|
|
429
|
+
.option('--stack', 'Launch full development stack (Chroma + Router + Dashboard)')
|
|
430
|
+
.action(async (opts) => {
|
|
431
|
+
const port = parseInt(opts.port, 10);
|
|
432
|
+
|
|
433
|
+
if (opts.stack) {
|
|
434
|
+
const { spawn } = require('child_process');
|
|
435
|
+
console.log('🚀 Launching Lemma Full Stack...');
|
|
436
|
+
|
|
437
|
+
// 1. Start Chroma
|
|
438
|
+
const chroma = spawn('chroma', ['run', '--path', './chroma_data'], { stdio: 'ignore', detached: true });
|
|
439
|
+
chroma.unref();
|
|
440
|
+
console.log(' ✅ ChromaDB requested');
|
|
441
|
+
|
|
442
|
+
// 2. Start Dashboard
|
|
443
|
+
const dash = spawn('npm', ['run', 'dev', '--prefix', 'dashboard', '--', '--port', '3000'], { stdio: 'ignore', detached: true });
|
|
444
|
+
dash.unref();
|
|
445
|
+
console.log(' ✅ Dashboard requested (http://localhost:3000)');
|
|
446
|
+
|
|
447
|
+
// 3. Start Router (in the same process to see logs)
|
|
448
|
+
console.log(' 🚀 Starting Router & Proxy...\n');
|
|
449
|
+
const router = spawn('npx', ['tsx', 'start-dev.ts'], {
|
|
450
|
+
stdio: 'inherit',
|
|
451
|
+
env: { ...process.env, PORT: '8080' }
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Start Proxy Server alongside
|
|
455
|
+
const server = new LemmaServer(port, opts.project || null);
|
|
456
|
+
server.start();
|
|
457
|
+
|
|
458
|
+
router.on('exit', (code) => {
|
|
459
|
+
console.log(`\n🛑 Router exited with code ${code}`);
|
|
460
|
+
process.exit(code || 0);
|
|
461
|
+
});
|
|
462
|
+
} else {
|
|
463
|
+
const server = new LemmaServer(port, opts.project || null);
|
|
464
|
+
server.start();
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
program.command('stop').action(() => {
|
|
469
|
+
try {
|
|
470
|
+
const pid = fs.readFileSync(PID_FILE, 'utf8');
|
|
471
|
+
process.kill(parseInt(pid), 'SIGTERM');
|
|
472
|
+
console.log('✅ Stopped');
|
|
473
|
+
} catch { console.log('⚠️ Not running'); }
|
|
474
|
+
});
|
|
475
|
+
program.command('status').action(() => console.log(fs.existsSync(PID_FILE) ? '✅ Running' : '❌ Stopped'));
|
|
476
|
+
|
|
477
|
+
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxuss/lemma",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Semantic cache for AI apps — stop paying for the same LLM call twice",
|
|
5
5
|
"main": "./dist/cjs/index.js",
|
|
6
6
|
"module": "./dist/esm/index.js",
|
|
7
7
|
"types": "./dist/esm/index.d.ts",
|
|
8
8
|
"bin": {
|
|
9
|
-
"lemma
|
|
9
|
+
"lemma": "./lemma-proxy.cjs"
|
|
10
10
|
},
|
|
11
11
|
"exports": {
|
|
12
12
|
".": {
|
|
@@ -78,17 +78,17 @@
|
|
|
78
78
|
"LICENSE"
|
|
79
79
|
],
|
|
80
80
|
"scripts": {
|
|
81
|
-
"proxy:start": "node lemma-proxy.
|
|
82
|
-
"proxy:stop": "node lemma-proxy.
|
|
83
|
-
"proxy:stats": "node lemma-proxy.
|
|
84
|
-
"proxy:status": "node lemma-proxy.
|
|
81
|
+
"proxy:start": "node lemma-proxy.cjs start",
|
|
82
|
+
"proxy:stop": "node lemma-proxy.cjs stop",
|
|
83
|
+
"proxy:stats": "node lemma-proxy.cjs stats",
|
|
84
|
+
"proxy:status": "node lemma-proxy.cjs status",
|
|
85
85
|
"build": "npm run build:esm && npm run build:cjs && npm run postbuild",
|
|
86
86
|
"build:full": "npm run build && npm run build:sdks",
|
|
87
87
|
"build:esm": "tsc --outDir ./dist/esm",
|
|
88
88
|
"build:cjs": "tsc --project tsconfig.cjs.json",
|
|
89
89
|
"build:sdks": "npm run sdk:build",
|
|
90
90
|
"postbuild": "node scripts/post-build.js",
|
|
91
|
-
"dev": "
|
|
91
|
+
"dev": "npx tsx start-dev.ts",
|
|
92
92
|
"start": "node dist/cjs/index.js",
|
|
93
93
|
"test": "jest",
|
|
94
94
|
"lint": "eslint src/**/*.ts",
|
|
@@ -150,6 +150,8 @@
|
|
|
150
150
|
},
|
|
151
151
|
"homepage": "https://github.com/Nxusbets/lemma#readme",
|
|
152
152
|
"dependencies": {
|
|
153
|
+
"@chroma-core/default-embed": "^1.1.4",
|
|
154
|
+
"@nxuss/lemma": "^0.3.1",
|
|
153
155
|
"@types/cors": "^2.8.19",
|
|
154
156
|
"axios": "^1.6.0",
|
|
155
157
|
"commander": "^14.0.3",
|