@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.
Files changed (139) hide show
  1. package/bin/cli.js +69 -0
  2. package/cache-manager.js +191 -0
  3. package/chat.routes.js +691 -0
  4. package/llm.routes.js +285 -0
  5. package/log.routes.js +42 -0
  6. package/logger.js +86 -0
  7. package/mcp.routes.js +320 -0
  8. package/package.json +37 -0
  9. package/public/3rdpartylicenses.txt +416 -0
  10. package/public/browser/assets/monaco/_commonjsHelpers-CT9FvmAN.js +1 -0
  11. package/public/browser/assets/monaco/abap-D-t0cyap.js +1 -0
  12. package/public/browser/assets/monaco/apex-CcIm7xu6.js +1 -0
  13. package/public/browser/assets/monaco/assets/css.worker-HnVq6Ewq.js +93 -0
  14. package/public/browser/assets/monaco/assets/editor.worker-Be8ye1pW.js +26 -0
  15. package/public/browser/assets/monaco/assets/html.worker-B51mlPHg.js +470 -0
  16. package/public/browser/assets/monaco/assets/json.worker-DKiEKt88.js +58 -0
  17. package/public/browser/assets/monaco/assets/ts.worker-CMbG-7ft.js +67731 -0
  18. package/public/browser/assets/monaco/azcli-BA0tQDCg.js +1 -0
  19. package/public/browser/assets/monaco/basic-languages/monaco.contribution.js +1 -0
  20. package/public/browser/assets/monaco/bat-C397hTD6.js +1 -0
  21. package/public/browser/assets/monaco/bicep-DF5aW17k.js +2 -0
  22. package/public/browser/assets/monaco/cameligo-plsz8qhj.js +1 -0
  23. package/public/browser/assets/monaco/clojure-Y2auQMzK.js +1 -0
  24. package/public/browser/assets/monaco/coffee-Bu45yuWE.js +1 -0
  25. package/public/browser/assets/monaco/cpp-CkKPQIni.js +1 -0
  26. package/public/browser/assets/monaco/csharp-CX28MZyh.js +1 -0
  27. package/public/browser/assets/monaco/csp-D8uWnyxW.js +1 -0
  28. package/public/browser/assets/monaco/css-CaeNmE3S.js +3 -0
  29. package/public/browser/assets/monaco/cssMode-CjiAH6dQ.js +1 -0
  30. package/public/browser/assets/monaco/cypher-DVThT8BS.js +1 -0
  31. package/public/browser/assets/monaco/dart-CmGfCvrO.js +1 -0
  32. package/public/browser/assets/monaco/dockerfile-CZqqYdch.js +1 -0
  33. package/public/browser/assets/monaco/ecl-30fUercY.js +1 -0
  34. package/public/browser/assets/monaco/editor/editor.main.css +1 -0
  35. package/public/browser/assets/monaco/editor/editor.main.js +5 -0
  36. package/public/browser/assets/monaco/editor.api-CalNCsUg.js +903 -0
  37. package/public/browser/assets/monaco/elixir-xjPaIfzF.js +1 -0
  38. package/public/browser/assets/monaco/flow9-DqtmStfK.js +1 -0
  39. package/public/browser/assets/monaco/freemarker2-Cz_sV6Md.js +3 -0
  40. package/public/browser/assets/monaco/fsharp-BOMdg4U1.js +1 -0
  41. package/public/browser/assets/monaco/go-D_hbi-Jt.js +1 -0
  42. package/public/browser/assets/monaco/graphql-CKUU4kLG.js +1 -0
  43. package/public/browser/assets/monaco/handlebars-OwglfO-1.js +1 -0
  44. package/public/browser/assets/monaco/hcl-DTaboeZW.js +1 -0
  45. package/public/browser/assets/monaco/html-Pa1xEWsY.js +1 -0
  46. package/public/browser/assets/monaco/htmlMode-Bz67EXwp.js +1 -0
  47. package/public/browser/assets/monaco/ini-CsNwO04R.js +1 -0
  48. package/public/browser/assets/monaco/java-CI4ZMsH9.js +1 -0
  49. package/public/browser/assets/monaco/javascript-PczUCGdz.js +1 -0
  50. package/public/browser/assets/monaco/jsonMode-DULH5oaX.js +7 -0
  51. package/public/browser/assets/monaco/julia-BwzEvaQw.js +1 -0
  52. package/public/browser/assets/monaco/kotlin-IUYPiTV8.js +1 -0
  53. package/public/browser/assets/monaco/language/css/monaco.contribution.js +1 -0
  54. package/public/browser/assets/monaco/language/html/monaco.contribution.js +1 -0
  55. package/public/browser/assets/monaco/language/json/monaco.contribution.js +1 -0
  56. package/public/browser/assets/monaco/language/typescript/monaco.contribution.js +1 -0
  57. package/public/browser/assets/monaco/less-C0eDYdqa.js +2 -0
  58. package/public/browser/assets/monaco/lexon-iON-Kj97.js +1 -0
  59. package/public/browser/assets/monaco/liquid-DqKjdPGy.js +1 -0
  60. package/public/browser/assets/monaco/loader.js +1368 -0
  61. package/public/browser/assets/monaco/lspLanguageFeatures-kM9O9rjY.js +4 -0
  62. package/public/browser/assets/monaco/lua-DtygF91M.js +1 -0
  63. package/public/browser/assets/monaco/m3-CsR4AuFi.js +1 -0
  64. package/public/browser/assets/monaco/markdown-C_rD0bIw.js +1 -0
  65. package/public/browser/assets/monaco/mdx-DEWtB1K5.js +1 -0
  66. package/public/browser/assets/monaco/mips-CiYP61RB.js +1 -0
  67. package/public/browser/assets/monaco/monaco.contribution-D2OdxNBt.js +1 -0
  68. package/public/browser/assets/monaco/monaco.contribution-DO3azKX8.js +1 -0
  69. package/public/browser/assets/monaco/monaco.contribution-EcChJV6a.js +1 -0
  70. package/public/browser/assets/monaco/monaco.contribution-qLAYrEOP.js +1 -0
  71. package/public/browser/assets/monaco/msdax-C38-sJlp.js +1 -0
  72. package/public/browser/assets/monaco/mysql-CdtbpvbG.js +1 -0
  73. package/public/browser/assets/monaco/nls.messages-loader.js +1 -0
  74. package/public/browser/assets/monaco/nls.messages.cs.js.js +17 -0
  75. package/public/browser/assets/monaco/nls.messages.de.js.js +17 -0
  76. package/public/browser/assets/monaco/nls.messages.es.js.js +17 -0
  77. package/public/browser/assets/monaco/nls.messages.fr.js.js +15 -0
  78. package/public/browser/assets/monaco/nls.messages.it.js.js +15 -0
  79. package/public/browser/assets/monaco/nls.messages.ja.js.js +17 -0
  80. package/public/browser/assets/monaco/nls.messages.js.js +10 -0
  81. package/public/browser/assets/monaco/nls.messages.ko.js.js +25 -0
  82. package/public/browser/assets/monaco/nls.messages.pl.js.js +17 -0
  83. package/public/browser/assets/monaco/nls.messages.pt-br.js.js +6 -0
  84. package/public/browser/assets/monaco/nls.messages.ru.js.js +17 -0
  85. package/public/browser/assets/monaco/nls.messages.tr.js.js +15 -0
  86. package/public/browser/assets/monaco/nls.messages.zh-cn.js.js +17 -0
  87. package/public/browser/assets/monaco/nls.messages.zh-tw.js.js +15 -0
  88. package/public/browser/assets/monaco/objective-c-CntZFaHX.js +1 -0
  89. package/public/browser/assets/monaco/pascal-r6kuqfl_.js +1 -0
  90. package/public/browser/assets/monaco/pascaligo-BiXoTmXh.js +1 -0
  91. package/public/browser/assets/monaco/perl-DABw_TcH.js +1 -0
  92. package/public/browser/assets/monaco/pgsql-me_jFXeX.js +1 -0
  93. package/public/browser/assets/monaco/php-D_kh-9LK.js +1 -0
  94. package/public/browser/assets/monaco/pla-VfZjczW0.js +1 -0
  95. package/public/browser/assets/monaco/postiats-BBSzz8Pk.js +1 -0
  96. package/public/browser/assets/monaco/powerquery-Dt-g_2cc.js +1 -0
  97. package/public/browser/assets/monaco/powershell-B-7ap1zc.js +1 -0
  98. package/public/browser/assets/monaco/protobuf-BmtuEB1A.js +2 -0
  99. package/public/browser/assets/monaco/pug-BRpRNeEb.js +1 -0
  100. package/public/browser/assets/monaco/python-Cr0UkIbn.js +1 -0
  101. package/public/browser/assets/monaco/qsharp-BzsFaUU9.js +1 -0
  102. package/public/browser/assets/monaco/r-f8dDdrp4.js +1 -0
  103. package/public/browser/assets/monaco/razor-BYAHOTkz.js +1 -0
  104. package/public/browser/assets/monaco/redis-fvZQY4PI.js +1 -0
  105. package/public/browser/assets/monaco/redshift-45Et0LQi.js +1 -0
  106. package/public/browser/assets/monaco/restructuredtext-C7UUFKFD.js +1 -0
  107. package/public/browser/assets/monaco/ruby-CZO8zYTz.js +1 -0
  108. package/public/browser/assets/monaco/rust-Bfetafyc.js +1 -0
  109. package/public/browser/assets/monaco/sb-3GYllVck.js +1 -0
  110. package/public/browser/assets/monaco/scala-foMgrKo1.js +1 -0
  111. package/public/browser/assets/monaco/scheme-CHdMtr7p.js +1 -0
  112. package/public/browser/assets/monaco/scss-C1cmLt9V.js +3 -0
  113. package/public/browser/assets/monaco/shell-ClXCKCEW.js +1 -0
  114. package/public/browser/assets/monaco/solidity-MZ6ExpPy.js +1 -0
  115. package/public/browser/assets/monaco/sophia-DWkuSsPQ.js +1 -0
  116. package/public/browser/assets/monaco/sparql-AUGFYSyk.js +1 -0
  117. package/public/browser/assets/monaco/sql-32GpJSV2.js +1 -0
  118. package/public/browser/assets/monaco/st-CuDFIVZ_.js +1 -0
  119. package/public/browser/assets/monaco/swift-n-2HociN.js +3 -0
  120. package/public/browser/assets/monaco/systemverilog-Ch4vA8Yt.js +1 -0
  121. package/public/browser/assets/monaco/tcl-D74tq1nH.js +1 -0
  122. package/public/browser/assets/monaco/tsMode-CZz1Umrk.js +11 -0
  123. package/public/browser/assets/monaco/twig-C6taOxMV.js +1 -0
  124. package/public/browser/assets/monaco/typescript-DfOrAzoV.js +1 -0
  125. package/public/browser/assets/monaco/typespec-D-PIh9Xw.js +1 -0
  126. package/public/browser/assets/monaco/vb-Dyb2648j.js +1 -0
  127. package/public/browser/assets/monaco/wgsl-BhLXMOR0.js +298 -0
  128. package/public/browser/assets/monaco/workers-DcJshg-q.js +1 -0
  129. package/public/browser/assets/monaco/xml-CdsdnY8S.js +1 -0
  130. package/public/browser/assets/monaco/yaml-DYGvmE88.js +1 -0
  131. package/public/browser/favicon.ico +0 -0
  132. package/public/browser/favicon.svg +4 -0
  133. package/public/browser/index.html +20 -0
  134. package/public/browser/main-BRU65EMU.js +80 -0
  135. package/public/browser/polyfills-FFHMD2TL.js +2 -0
  136. package/public/browser/styles-R37AZPY2.css +1 -0
  137. package/server.js +60 -0
  138. package/system.routes.js +150 -0
  139. 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`);