@makeshkumar/blueorch-studio 1.0.0
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/bin/cli.js +69 -0
- package/cache-manager.js +191 -0
- package/chat.routes.js +691 -0
- package/llm.routes.js +285 -0
- package/log.routes.js +42 -0
- package/logger.js +86 -0
- package/mcp.routes.js +320 -0
- package/package.json +37 -0
- package/public/3rdpartylicenses.txt +416 -0
- package/public/browser/assets/monaco/_commonjsHelpers-CT9FvmAN.js +1 -0
- package/public/browser/assets/monaco/abap-D-t0cyap.js +1 -0
- package/public/browser/assets/monaco/apex-CcIm7xu6.js +1 -0
- package/public/browser/assets/monaco/assets/css.worker-HnVq6Ewq.js +93 -0
- package/public/browser/assets/monaco/assets/editor.worker-Be8ye1pW.js +26 -0
- package/public/browser/assets/monaco/assets/html.worker-B51mlPHg.js +470 -0
- package/public/browser/assets/monaco/assets/json.worker-DKiEKt88.js +58 -0
- package/public/browser/assets/monaco/assets/ts.worker-CMbG-7ft.js +67731 -0
- package/public/browser/assets/monaco/azcli-BA0tQDCg.js +1 -0
- package/public/browser/assets/monaco/basic-languages/monaco.contribution.js +1 -0
- package/public/browser/assets/monaco/bat-C397hTD6.js +1 -0
- package/public/browser/assets/monaco/bicep-DF5aW17k.js +2 -0
- package/public/browser/assets/monaco/cameligo-plsz8qhj.js +1 -0
- package/public/browser/assets/monaco/clojure-Y2auQMzK.js +1 -0
- package/public/browser/assets/monaco/coffee-Bu45yuWE.js +1 -0
- package/public/browser/assets/monaco/cpp-CkKPQIni.js +1 -0
- package/public/browser/assets/monaco/csharp-CX28MZyh.js +1 -0
- package/public/browser/assets/monaco/csp-D8uWnyxW.js +1 -0
- package/public/browser/assets/monaco/css-CaeNmE3S.js +3 -0
- package/public/browser/assets/monaco/cssMode-CjiAH6dQ.js +1 -0
- package/public/browser/assets/monaco/cypher-DVThT8BS.js +1 -0
- package/public/browser/assets/monaco/dart-CmGfCvrO.js +1 -0
- package/public/browser/assets/monaco/dockerfile-CZqqYdch.js +1 -0
- package/public/browser/assets/monaco/ecl-30fUercY.js +1 -0
- package/public/browser/assets/monaco/editor/editor.main.css +1 -0
- package/public/browser/assets/monaco/editor/editor.main.js +5 -0
- package/public/browser/assets/monaco/editor.api-CalNCsUg.js +903 -0
- package/public/browser/assets/monaco/elixir-xjPaIfzF.js +1 -0
- package/public/browser/assets/monaco/flow9-DqtmStfK.js +1 -0
- package/public/browser/assets/monaco/freemarker2-Cz_sV6Md.js +3 -0
- package/public/browser/assets/monaco/fsharp-BOMdg4U1.js +1 -0
- package/public/browser/assets/monaco/go-D_hbi-Jt.js +1 -0
- package/public/browser/assets/monaco/graphql-CKUU4kLG.js +1 -0
- package/public/browser/assets/monaco/handlebars-OwglfO-1.js +1 -0
- package/public/browser/assets/monaco/hcl-DTaboeZW.js +1 -0
- package/public/browser/assets/monaco/html-Pa1xEWsY.js +1 -0
- package/public/browser/assets/monaco/htmlMode-Bz67EXwp.js +1 -0
- package/public/browser/assets/monaco/ini-CsNwO04R.js +1 -0
- package/public/browser/assets/monaco/java-CI4ZMsH9.js +1 -0
- package/public/browser/assets/monaco/javascript-PczUCGdz.js +1 -0
- package/public/browser/assets/monaco/jsonMode-DULH5oaX.js +7 -0
- package/public/browser/assets/monaco/julia-BwzEvaQw.js +1 -0
- package/public/browser/assets/monaco/kotlin-IUYPiTV8.js +1 -0
- package/public/browser/assets/monaco/language/css/monaco.contribution.js +1 -0
- package/public/browser/assets/monaco/language/html/monaco.contribution.js +1 -0
- package/public/browser/assets/monaco/language/json/monaco.contribution.js +1 -0
- package/public/browser/assets/monaco/language/typescript/monaco.contribution.js +1 -0
- package/public/browser/assets/monaco/less-C0eDYdqa.js +2 -0
- package/public/browser/assets/monaco/lexon-iON-Kj97.js +1 -0
- package/public/browser/assets/monaco/liquid-DqKjdPGy.js +1 -0
- package/public/browser/assets/monaco/loader.js +1368 -0
- package/public/browser/assets/monaco/lspLanguageFeatures-kM9O9rjY.js +4 -0
- package/public/browser/assets/monaco/lua-DtygF91M.js +1 -0
- package/public/browser/assets/monaco/m3-CsR4AuFi.js +1 -0
- package/public/browser/assets/monaco/markdown-C_rD0bIw.js +1 -0
- package/public/browser/assets/monaco/mdx-DEWtB1K5.js +1 -0
- package/public/browser/assets/monaco/mips-CiYP61RB.js +1 -0
- package/public/browser/assets/monaco/monaco.contribution-D2OdxNBt.js +1 -0
- package/public/browser/assets/monaco/monaco.contribution-DO3azKX8.js +1 -0
- package/public/browser/assets/monaco/monaco.contribution-EcChJV6a.js +1 -0
- package/public/browser/assets/monaco/monaco.contribution-qLAYrEOP.js +1 -0
- package/public/browser/assets/monaco/msdax-C38-sJlp.js +1 -0
- package/public/browser/assets/monaco/mysql-CdtbpvbG.js +1 -0
- package/public/browser/assets/monaco/nls.messages-loader.js +1 -0
- package/public/browser/assets/monaco/nls.messages.cs.js.js +17 -0
- package/public/browser/assets/monaco/nls.messages.de.js.js +17 -0
- package/public/browser/assets/monaco/nls.messages.es.js.js +17 -0
- package/public/browser/assets/monaco/nls.messages.fr.js.js +15 -0
- package/public/browser/assets/monaco/nls.messages.it.js.js +15 -0
- package/public/browser/assets/monaco/nls.messages.ja.js.js +17 -0
- package/public/browser/assets/monaco/nls.messages.js.js +10 -0
- package/public/browser/assets/monaco/nls.messages.ko.js.js +25 -0
- package/public/browser/assets/monaco/nls.messages.pl.js.js +17 -0
- package/public/browser/assets/monaco/nls.messages.pt-br.js.js +6 -0
- package/public/browser/assets/monaco/nls.messages.ru.js.js +17 -0
- package/public/browser/assets/monaco/nls.messages.tr.js.js +15 -0
- package/public/browser/assets/monaco/nls.messages.zh-cn.js.js +17 -0
- package/public/browser/assets/monaco/nls.messages.zh-tw.js.js +15 -0
- package/public/browser/assets/monaco/objective-c-CntZFaHX.js +1 -0
- package/public/browser/assets/monaco/pascal-r6kuqfl_.js +1 -0
- package/public/browser/assets/monaco/pascaligo-BiXoTmXh.js +1 -0
- package/public/browser/assets/monaco/perl-DABw_TcH.js +1 -0
- package/public/browser/assets/monaco/pgsql-me_jFXeX.js +1 -0
- package/public/browser/assets/monaco/php-D_kh-9LK.js +1 -0
- package/public/browser/assets/monaco/pla-VfZjczW0.js +1 -0
- package/public/browser/assets/monaco/postiats-BBSzz8Pk.js +1 -0
- package/public/browser/assets/monaco/powerquery-Dt-g_2cc.js +1 -0
- package/public/browser/assets/monaco/powershell-B-7ap1zc.js +1 -0
- package/public/browser/assets/monaco/protobuf-BmtuEB1A.js +2 -0
- package/public/browser/assets/monaco/pug-BRpRNeEb.js +1 -0
- package/public/browser/assets/monaco/python-Cr0UkIbn.js +1 -0
- package/public/browser/assets/monaco/qsharp-BzsFaUU9.js +1 -0
- package/public/browser/assets/monaco/r-f8dDdrp4.js +1 -0
- package/public/browser/assets/monaco/razor-BYAHOTkz.js +1 -0
- package/public/browser/assets/monaco/redis-fvZQY4PI.js +1 -0
- package/public/browser/assets/monaco/redshift-45Et0LQi.js +1 -0
- package/public/browser/assets/monaco/restructuredtext-C7UUFKFD.js +1 -0
- package/public/browser/assets/monaco/ruby-CZO8zYTz.js +1 -0
- package/public/browser/assets/monaco/rust-Bfetafyc.js +1 -0
- package/public/browser/assets/monaco/sb-3GYllVck.js +1 -0
- package/public/browser/assets/monaco/scala-foMgrKo1.js +1 -0
- package/public/browser/assets/monaco/scheme-CHdMtr7p.js +1 -0
- package/public/browser/assets/monaco/scss-C1cmLt9V.js +3 -0
- package/public/browser/assets/monaco/shell-ClXCKCEW.js +1 -0
- package/public/browser/assets/monaco/solidity-MZ6ExpPy.js +1 -0
- package/public/browser/assets/monaco/sophia-DWkuSsPQ.js +1 -0
- package/public/browser/assets/monaco/sparql-AUGFYSyk.js +1 -0
- package/public/browser/assets/monaco/sql-32GpJSV2.js +1 -0
- package/public/browser/assets/monaco/st-CuDFIVZ_.js +1 -0
- package/public/browser/assets/monaco/swift-n-2HociN.js +3 -0
- package/public/browser/assets/monaco/systemverilog-Ch4vA8Yt.js +1 -0
- package/public/browser/assets/monaco/tcl-D74tq1nH.js +1 -0
- package/public/browser/assets/monaco/tsMode-CZz1Umrk.js +11 -0
- package/public/browser/assets/monaco/twig-C6taOxMV.js +1 -0
- package/public/browser/assets/monaco/typescript-DfOrAzoV.js +1 -0
- package/public/browser/assets/monaco/typespec-D-PIh9Xw.js +1 -0
- package/public/browser/assets/monaco/vb-Dyb2648j.js +1 -0
- package/public/browser/assets/monaco/wgsl-BhLXMOR0.js +298 -0
- package/public/browser/assets/monaco/workers-DcJshg-q.js +1 -0
- package/public/browser/assets/monaco/xml-CdsdnY8S.js +1 -0
- package/public/browser/assets/monaco/yaml-DYGvmE88.js +1 -0
- package/public/browser/favicon.ico +0 -0
- package/public/browser/favicon.svg +4 -0
- package/public/browser/index.html +20 -0
- package/public/browser/main-BRU65EMU.js +80 -0
- package/public/browser/polyfills-FFHMD2TL.js +2 -0
- package/public/browser/styles-R37AZPY2.css +1 -0
- package/server.js +60 -0
- package/system.routes.js +150 -0
- package/usage-normalizer.js +117 -0
package/llm.routes.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
|
|
4
|
+
const router = Router();
|
|
5
|
+
const ts = () => new Date(Date.now() + (5 * 60 + 30) * 60000).toISOString().replace('Z', '+05:30');
|
|
6
|
+
|
|
7
|
+
// ─── LLM Registry ─────────────────────────────────────────────────────────────
|
|
8
|
+
// Map<id, { id, provider, model, apiKey, latency, verifiedAt }>
|
|
9
|
+
// Exported so Phase 3 chat routes can call getActiveConfig() to get the full
|
|
10
|
+
// config (including apiKey) for the currently selected provider.
|
|
11
|
+
export const connectedLlmRegistry = new Map();
|
|
12
|
+
|
|
13
|
+
const llmState = { activeLlmId: null };
|
|
14
|
+
|
|
15
|
+
/** Returns the full config (incl. apiKey) for the active provider, or null. */
|
|
16
|
+
export const getActiveConfig = () => {
|
|
17
|
+
if (!llmState.activeLlmId) return null;
|
|
18
|
+
return connectedLlmRegistry.get(llmState.activeLlmId) ?? null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// ─── POST /api/llm/verify ─────────────────────────────────────────────────────
|
|
22
|
+
// Body: { provider, model, apiKey?, baseUrl? }
|
|
23
|
+
// Returns: { success: true, id, provider, model, latency, verifiedAt }
|
|
24
|
+
router.post('/verify', async (req, res) => {
|
|
25
|
+
const { provider, model, apiKey, baseUrl } = req.body;
|
|
26
|
+
const ollamaBase = (baseUrl ?? 'http://localhost:11434').replace(/\/$/, '');
|
|
27
|
+
const lmstudioBase = (baseUrl ?? 'http://localhost:1234').replace(/\/$/, '');
|
|
28
|
+
|
|
29
|
+
if (!provider || !model) {
|
|
30
|
+
return res.status(400).json({ error: 'provider and model are required' });
|
|
31
|
+
}
|
|
32
|
+
if (provider !== 'ollama' && provider !== 'lmstudio' && !apiKey) {
|
|
33
|
+
return res.status(400).json({ error: 'apiKey is required for cloud providers' });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const start = Date.now();
|
|
37
|
+
console.log(`[INIT] ${ts()} Verifying LLM | provider: ${provider} | model: ${model}`);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
if (provider === 'ollama') {
|
|
41
|
+
// Ping /api/tags — lists local models, confirms Ollama is running
|
|
42
|
+
const resp = await fetch(`${ollamaBase}/api/tags`);
|
|
43
|
+
if (!resp.ok) throw new Error(`Ollama responded with HTTP ${resp.status}`);
|
|
44
|
+
const data = await resp.json();
|
|
45
|
+
const available = (data.models ?? []).map(m => m.name);
|
|
46
|
+
if (!available.includes(model)) {
|
|
47
|
+
throw new Error(`Model "${model}" not found in Ollama. Available: ${available.join(', ') || 'none'}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
} else if (provider === 'gemini') {
|
|
51
|
+
const { GoogleGenAI } = await import('@google/genai');
|
|
52
|
+
const ai = new GoogleGenAI({ apiKey });
|
|
53
|
+
// generateContent with a minimal prompt validates key + model without significant cost
|
|
54
|
+
await ai.models.generateContent({ model, contents: [{ role: 'user', parts: [{ text: 'ping' }] }] });
|
|
55
|
+
|
|
56
|
+
} else if (provider === 'openai') {
|
|
57
|
+
const OpenAI = (await import('openai')).default;
|
|
58
|
+
const openai = new OpenAI({ apiKey });
|
|
59
|
+
// models.list() validates the key without incurring inference cost
|
|
60
|
+
await openai.models.list();
|
|
61
|
+
|
|
62
|
+
} else if (provider === 'claude') {
|
|
63
|
+
const Anthropic = (await import('@anthropic-ai/sdk')).default;
|
|
64
|
+
const client = new Anthropic({ apiKey });
|
|
65
|
+
// Minimal 1-token message to confirm key validity
|
|
66
|
+
await client.messages.create({
|
|
67
|
+
model,
|
|
68
|
+
max_tokens: 1,
|
|
69
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
} else if (provider === 'lmstudio') {
|
|
73
|
+
// ── LM Studio — OpenAI-compatible local server ──────────────────────────
|
|
74
|
+
const OpenAI = (await import('openai')).default;
|
|
75
|
+
const lmstudio = new OpenAI({ baseURL: `${lmstudioBase}/v1`, apiKey: 'lm-studio' });
|
|
76
|
+
let modelsRes;
|
|
77
|
+
try {
|
|
78
|
+
modelsRes = await lmstudio.models.list();
|
|
79
|
+
} catch (connectErr) {
|
|
80
|
+
const isRefused = connectErr.cause?.code === 'ECONNREFUSED' || connectErr.code === 'ECONNREFUSED';
|
|
81
|
+
throw new Error(
|
|
82
|
+
isRefused
|
|
83
|
+
? 'LM Studio Server Not Found. Enable Developer Mode and Start Server in LM Studio.'
|
|
84
|
+
: connectErr.message
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const available = (modelsRes.data ?? []).map(m => m.id);
|
|
88
|
+
if (available.length > 0 && !available.includes(model)) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Model "${model}" is not loaded in LM Studio. Loaded: ${available.join(', ') || 'none'}`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
} else {
|
|
95
|
+
return res.status(400).json({ error: `Unknown provider: "${provider}"` });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const latency = Date.now() - start;
|
|
99
|
+
const id = randomUUID();
|
|
100
|
+
const verifiedAt = ts();
|
|
101
|
+
|
|
102
|
+
connectedLlmRegistry.set(id, {
|
|
103
|
+
id, provider, model,
|
|
104
|
+
apiKey: provider === 'lmstudio' ? 'lm-studio' : (apiKey ?? ''),
|
|
105
|
+
...(provider === 'ollama' && { baseUrl: ollamaBase }),
|
|
106
|
+
...(provider === 'lmstudio' && { baseUrl: lmstudioBase }),
|
|
107
|
+
latency, verifiedAt,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Auto-promote to active if this is the first entry in the registry
|
|
111
|
+
if (!llmState.activeLlmId) {
|
|
112
|
+
llmState.activeLlmId = id;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log(`[SUCCESS] ${ts()} LLM verified | id: ${id} | provider: ${provider} | model: ${model} | latency: ${latency}ms`);
|
|
116
|
+
return res.json({ success: true, id, provider, model, latency, verifiedAt });
|
|
117
|
+
|
|
118
|
+
} catch (err) {
|
|
119
|
+
const latency = Date.now() - start;
|
|
120
|
+
console.error(`[ERROR] ${ts()} LLM verification failed | provider: ${provider} | ${err.message}`);
|
|
121
|
+
return res.status(400).json({ success: false, latency, error: err.message });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ─── GET /api/llm/lmstudio/models ────────────────────────────────────────────
|
|
126
|
+
// Returns the list of models currently loaded in LM Studio.
|
|
127
|
+
// Optional query param: ?baseUrl=http://localhost:1234
|
|
128
|
+
router.get('/lmstudio/models', async (req, res) => {
|
|
129
|
+
const lmstudioBase = ((req.query.baseUrl ?? 'http://localhost:1234') + '').replace(/\/$/, '');
|
|
130
|
+
console.log(`[INIT] ${ts()} Fetching LM Studio models | baseUrl: ${lmstudioBase}`);
|
|
131
|
+
try {
|
|
132
|
+
const OpenAI = (await import('openai')).default;
|
|
133
|
+
const lmstudio = new OpenAI({ baseURL: `${lmstudioBase}/v1`, apiKey: 'lm-studio' });
|
|
134
|
+
const modelsRes = await lmstudio.models.list();
|
|
135
|
+
const models = (modelsRes.data ?? []).map(m => ({ id: m.id }));
|
|
136
|
+
console.log(`[SUCCESS] ${ts()} LM Studio models fetched | ${models.length} models`);
|
|
137
|
+
return res.json({ models });
|
|
138
|
+
} catch (err) {
|
|
139
|
+
const isRefused = err.cause?.code === 'ECONNREFUSED' || err.code === 'ECONNREFUSED';
|
|
140
|
+
console.error(`[ERROR] ${ts()} Failed to fetch LM Studio models | ${err.message}`);
|
|
141
|
+
return res.status(isRefused ? 503 : 500).json({
|
|
142
|
+
error: isRefused
|
|
143
|
+
? 'LM Studio Server Not Found. Enable Developer Mode and Start Server in LM Studio.'
|
|
144
|
+
: err.message,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ─── GET /api/llm/registry ────────────────────────────────────────────────────
|
|
150
|
+
// Returns all verified entries (without apiKeys) and the current activeId.
|
|
151
|
+
router.get('/registry', (_req, res) => {
|
|
152
|
+
const registry = Array.from(connectedLlmRegistry.values()).map(
|
|
153
|
+
({ id, provider, model, latency, verifiedAt }) => ({ id, provider, model, latency, verifiedAt })
|
|
154
|
+
);
|
|
155
|
+
console.log(`[SUCCESS] ${ts()} LLM registry listed | ${registry.length} entries | activeId: ${llmState.activeLlmId}`);
|
|
156
|
+
return res.json({ registry, activeId: llmState.activeLlmId });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ─── PUT /api/llm/active/:id ──────────────────────────────────────────────────
|
|
160
|
+
// Sets which registry entry is the active provider for chat (Phase 3).
|
|
161
|
+
router.put('/active/:id', (req, res) => {
|
|
162
|
+
const { id } = req.params;
|
|
163
|
+
if (!connectedLlmRegistry.has(id)) {
|
|
164
|
+
return res.status(404).json({ error: `LLM entry "${id}" not found in registry` });
|
|
165
|
+
}
|
|
166
|
+
llmState.activeLlmId = id;
|
|
167
|
+
const { provider, model } = connectedLlmRegistry.get(id);
|
|
168
|
+
console.log(`[SUCCESS] ${ts()} Active LLM set | id: ${id} | provider: ${provider} | model: ${model}`);
|
|
169
|
+
return res.json({ success: true, activeId: id });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ─── DELETE /api/llm/:id ──────────────────────────────────────────────────────
|
|
173
|
+
// Removes an entry from the registry. Falls back to the last remaining entry.
|
|
174
|
+
router.delete('/:id', (req, res) => {
|
|
175
|
+
const { id } = req.params;
|
|
176
|
+
if (!connectedLlmRegistry.has(id)) {
|
|
177
|
+
return res.status(404).json({ error: `LLM entry "${id}" not found` });
|
|
178
|
+
}
|
|
179
|
+
connectedLlmRegistry.delete(id);
|
|
180
|
+
if (llmState.activeLlmId === id) {
|
|
181
|
+
const remaining = Array.from(connectedLlmRegistry.keys());
|
|
182
|
+
llmState.activeLlmId = remaining.length > 0 ? remaining[remaining.length - 1] : null;
|
|
183
|
+
}
|
|
184
|
+
console.log(`[SUCCESS] ${ts()} LLM registry entry deleted | id: ${id} | new activeId: ${llmState.activeLlmId}`);
|
|
185
|
+
return res.json({ success: true, activeId: llmState.activeLlmId });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ─── GET /api/llm/active ──────────────────────────────────────────────────────
|
|
189
|
+
// Returns the active entry metadata (no apiKey). Used by Phase 3 UI status.
|
|
190
|
+
router.get('/active', (_req, res) => {
|
|
191
|
+
if (!llmState.activeLlmId || !connectedLlmRegistry.has(llmState.activeLlmId)) {
|
|
192
|
+
return res.status(404).json({ error: 'No active LLM configured' });
|
|
193
|
+
}
|
|
194
|
+
const { id, provider, model, latency, verifiedAt } = connectedLlmRegistry.get(llmState.activeLlmId);
|
|
195
|
+
console.log(`[SUCCESS] ${ts()} Active LLM retrieved | id: ${id} | provider: ${provider} | model: ${model}`);
|
|
196
|
+
return res.json({ id, provider, model, latency, verifiedAt });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ─── POST /api/llm/models ─────────────────────────────────────────────────────
|
|
200
|
+
// Body: { provider, apiKey }
|
|
201
|
+
// Returns: { models: [{ label, value }] }
|
|
202
|
+
// Fetches the available models for cloud providers (gemini, openai, claude).
|
|
203
|
+
router.post('/models', async (req, res) => {
|
|
204
|
+
const { provider, apiKey } = req.body;
|
|
205
|
+
if (!provider || !apiKey) {
|
|
206
|
+
return res.status(400).json({ error: 'provider and apiKey are required' });
|
|
207
|
+
}
|
|
208
|
+
console.log(`[INIT] ${ts()} Fetching models from provider API | provider: ${provider}`);
|
|
209
|
+
try {
|
|
210
|
+
let models = [];
|
|
211
|
+
|
|
212
|
+
if (provider === 'gemini') {
|
|
213
|
+
const resp = await fetch(
|
|
214
|
+
`https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(apiKey)}`
|
|
215
|
+
);
|
|
216
|
+
if (!resp.ok) {
|
|
217
|
+
const errData = await resp.json().catch(() => ({}));
|
|
218
|
+
throw new Error(errData.error?.message ?? `HTTP ${resp.status}`);
|
|
219
|
+
}
|
|
220
|
+
const data = await resp.json();
|
|
221
|
+
models = (data.models ?? [])
|
|
222
|
+
.filter(m => (m.supportedGenerationMethods ?? []).includes('generateContent'))
|
|
223
|
+
.map(m => ({
|
|
224
|
+
label: m.displayName ?? m.name.split('/').pop(),
|
|
225
|
+
value: m.name.split('/').pop(),
|
|
226
|
+
}));
|
|
227
|
+
|
|
228
|
+
} else if (provider === 'openai') {
|
|
229
|
+
const OpenAI = (await import('openai')).default;
|
|
230
|
+
const openai = new OpenAI({ apiKey });
|
|
231
|
+
const modelsRes = await openai.models.list();
|
|
232
|
+
models = (modelsRes.data ?? [])
|
|
233
|
+
.filter(m => /^(gpt|o1|o3|o4|chatgpt)/.test(m.id))
|
|
234
|
+
.sort((a, b) => b.created - a.created)
|
|
235
|
+
.map(m => ({ label: m.id, value: m.id }));
|
|
236
|
+
|
|
237
|
+
} else if (provider === 'claude') {
|
|
238
|
+
const resp = await fetch('https://api.anthropic.com/v1/models', {
|
|
239
|
+
headers: {
|
|
240
|
+
'x-api-key': apiKey,
|
|
241
|
+
'anthropic-version': '2023-06-01',
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
if (!resp.ok) {
|
|
245
|
+
const errData = await resp.json().catch(() => ({}));
|
|
246
|
+
throw new Error(errData.error?.message ?? `HTTP ${resp.status}`);
|
|
247
|
+
}
|
|
248
|
+
const data = await resp.json();
|
|
249
|
+
models = (data.data ?? []).map(m => ({
|
|
250
|
+
label: m.display_name ?? m.id,
|
|
251
|
+
value: m.id,
|
|
252
|
+
}));
|
|
253
|
+
|
|
254
|
+
} else {
|
|
255
|
+
return res.status(400).json({ error: `Model listing not supported for provider: ${provider}` });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
console.log(`[SUCCESS] ${ts()} Models fetched | provider: ${provider} | ${models.length} model(s)`);
|
|
259
|
+
return res.json({ models });
|
|
260
|
+
} catch (err) {
|
|
261
|
+
console.error(`[ERROR] ${ts()} Failed to fetch models | provider: ${provider} | ${err.message}`);
|
|
262
|
+
return res.status(400).json({ error: err.message });
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// ─── GET /api/llm/ollama/models ───────────────────────────────────────────────
|
|
267
|
+
// Query: ?baseUrl=http://localhost:11434
|
|
268
|
+
// Returns the list of locally pulled models from Ollama.
|
|
269
|
+
router.get('/ollama/models', async (req, res) => {
|
|
270
|
+
const baseUrl = ((req.query.baseUrl ?? 'http://localhost:11434')).replace(/\/$/, '');
|
|
271
|
+
console.log(`[INIT] ${ts()} Fetching Ollama models | baseUrl: ${baseUrl}`);
|
|
272
|
+
try {
|
|
273
|
+
const resp = await fetch(`${baseUrl}/api/tags`);
|
|
274
|
+
if (!resp.ok) throw new Error(`Ollama responded with HTTP ${resp.status}`);
|
|
275
|
+
const data = await resp.json();
|
|
276
|
+
const models = (data.models ?? []).map(m => ({ name: m.name, size: m.size }));
|
|
277
|
+
console.log(`[SUCCESS] ${ts()} Ollama models fetched | ${models.length} model(s)`);
|
|
278
|
+
return res.json({ models });
|
|
279
|
+
} catch (err) {
|
|
280
|
+
console.error(`[ERROR] ${ts()} Failed to fetch Ollama models | ${err.message}`);
|
|
281
|
+
return res.status(500).json({ error: err.message });
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
export default router;
|
package/log.routes.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { logBus, getRecentLogs } from './logger.js';
|
|
3
|
+
|
|
4
|
+
const router = Router();
|
|
5
|
+
const ts = () => new Date(Date.now() + (5 * 60 + 30) * 60000).toISOString().replace('Z', '+05:30');
|
|
6
|
+
|
|
7
|
+
// ─── GET /api/logs ─────────────────────────────────────────────────────────────
|
|
8
|
+
// Query params: ?limit=200 ?level=info|error|warn|all
|
|
9
|
+
router.get('/', (req, res) => {
|
|
10
|
+
const limit = parseInt(req.query.limit) || 200;
|
|
11
|
+
const level = req.query.level || 'all';
|
|
12
|
+
const logs = getRecentLogs(limit, level);
|
|
13
|
+
return res.json({ logs, total: logs.length });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// ─── GET /api/logs/stream ──────────────────────────────────────────────────────
|
|
17
|
+
// Server-Sent Events (SSE) — pushes each new log entry in real-time.
|
|
18
|
+
// Clients connect once and receive a stream; connection stays open.
|
|
19
|
+
router.get('/stream', (req, res) => {
|
|
20
|
+
// SSE headers
|
|
21
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
22
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
23
|
+
res.setHeader('Connection', 'keep-alive');
|
|
24
|
+
res.setHeader('X-Accel-Buffering', 'no'); // nginx compat
|
|
25
|
+
res.flushHeaders();
|
|
26
|
+
|
|
27
|
+
const onEntry = (entry) => {
|
|
28
|
+
res.write(`data: ${JSON.stringify(entry)}\n\n`);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
logBus.on('entry', onEntry);
|
|
32
|
+
|
|
33
|
+
// Keep-alive heartbeat every 20s (prevents proxy timeouts)
|
|
34
|
+
const heartbeat = setInterval(() => res.write(': heartbeat\n\n'), 20_000);
|
|
35
|
+
|
|
36
|
+
req.on('close', () => {
|
|
37
|
+
logBus.off('entry', onEntry);
|
|
38
|
+
clearInterval(heartbeat);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export default router;
|
package/logger.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import winston from 'winston';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import { mkdirSync } from 'fs';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { dirname, join } from 'path';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
const LOGS_DIR = join(__dirname, 'logs');
|
|
10
|
+
|
|
11
|
+
// Ensure logs directory exists
|
|
12
|
+
mkdirSync(LOGS_DIR, { recursive: true });
|
|
13
|
+
|
|
14
|
+
const ts = () => new Date(Date.now() + (5 * 60 + 30) * 60000).toISOString().replace('Z', '+05:30');
|
|
15
|
+
|
|
16
|
+
// ─── SSE broadcast bus ────────────────────────────────────────────────────────
|
|
17
|
+
export const logBus = new EventEmitter();
|
|
18
|
+
logBus.setMaxListeners(200); // allow many concurrent SSE clients
|
|
19
|
+
|
|
20
|
+
// ─── In-memory ring buffer (last 300 entries) ──────────────────────────────────
|
|
21
|
+
const MAX_BUFFER = 300;
|
|
22
|
+
const logBuffer = [];
|
|
23
|
+
|
|
24
|
+
export const getRecentLogs = (limit = 200, level = null) => {
|
|
25
|
+
let entries = logBuffer;
|
|
26
|
+
if (level && level !== 'all') entries = entries.filter(e => e.level === level);
|
|
27
|
+
return entries.slice(-Math.min(limit, MAX_BUFFER));
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// ─── Custom transport: feeds the SSE bus + ring buffer ────────────────────────
|
|
31
|
+
class LiveTransport extends winston.Transport {
|
|
32
|
+
constructor(opts) {
|
|
33
|
+
super(opts);
|
|
34
|
+
this.name = 'live';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
log(info, callback) {
|
|
38
|
+
const entry = {
|
|
39
|
+
level: info.level,
|
|
40
|
+
message: info.message,
|
|
41
|
+
timestamp: info.timestamp ?? ts(),
|
|
42
|
+
};
|
|
43
|
+
logBuffer.push(entry);
|
|
44
|
+
if (logBuffer.length > MAX_BUFFER) logBuffer.shift();
|
|
45
|
+
logBus.emit('entry', entry);
|
|
46
|
+
callback();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── Winston logger ────────────────────────────────────────────────────────────
|
|
51
|
+
export const logger = winston.createLogger({
|
|
52
|
+
level: 'info',
|
|
53
|
+
format: winston.format.combine(
|
|
54
|
+
winston.format.timestamp({ format: () => new Date(Date.now() + (5 * 60 + 30) * 60000).toISOString().replace('Z', '+05:30') }),
|
|
55
|
+
winston.format.errors({ stack: true }),
|
|
56
|
+
winston.format.json()
|
|
57
|
+
),
|
|
58
|
+
transports: [
|
|
59
|
+
// File: errors only — max 2 MB, keep 3 rotated files
|
|
60
|
+
new winston.transports.File({
|
|
61
|
+
filename: join(LOGS_DIR, 'error.log'),
|
|
62
|
+
level: 'error',
|
|
63
|
+
maxsize: 2 * 1024 * 1024, // 2 MB
|
|
64
|
+
maxFiles: 3,
|
|
65
|
+
tailable: true,
|
|
66
|
+
}),
|
|
67
|
+
// File: all levels — max 5 MB, keep 3 rotated files
|
|
68
|
+
new winston.transports.File({
|
|
69
|
+
filename: join(LOGS_DIR, 'combined.log'),
|
|
70
|
+
maxsize: 5 * 1024 * 1024, // 5 MB
|
|
71
|
+
maxFiles: 3,
|
|
72
|
+
tailable: true,
|
|
73
|
+
}),
|
|
74
|
+
// Console: colourised plain text
|
|
75
|
+
new winston.transports.Console({
|
|
76
|
+
format: winston.format.combine(
|
|
77
|
+
winston.format.colorize(),
|
|
78
|
+
winston.format.simple()
|
|
79
|
+
),
|
|
80
|
+
}),
|
|
81
|
+
// Live SSE broadcast + ring buffer
|
|
82
|
+
new LiveTransport(),
|
|
83
|
+
],
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
console.log(`[INIT] ${ts()} logger.js loaded | transports: file(error) + file(combined) + console + live`);
|