Pepguy commited on
Commit
7486fe7
·
verified ·
1 Parent(s): f11837d

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +236 -473
app.js CHANGED
@@ -1,21 +1,12 @@
1
- // ================================
2
- // SERVER.JS (UPDATED)
3
- // ================================
4
-
5
- import express from 'express';
6
  import cors from 'cors';
7
  import dotenv from 'dotenv';
8
  import { createClient } from '@supabase/supabase-js';
9
- import {
10
- BedrockRuntimeClient,
11
- ConverseStreamCommand
12
- } from "@aws-sdk/client-bedrock-runtime";
13
  import { NodeHttpHandler } from "@smithy/node-http-handler";
14
  import path from 'path';
15
  import { fileURLToPath } from 'url';
16
 
17
  dotenv.config();
18
-
19
  const app = express();
20
  const PORT = process.env.PORT || 7860;
21
 
@@ -23,523 +14,295 @@ const __filename = fileURLToPath(import.meta.url);
23
  const __dirname = path.dirname(__filename);
24
 
25
  app.use(cors());
 
 
26
 
27
- app.use(express.json({
28
- limit: '50mb'
29
- }));
30
-
31
- app.use(express.static(path.join(__dirname, 'public')));
32
-
33
- // ================================
34
- // LOGGER
35
- // ================================
36
-
37
  const log = {
38
- info: (msg, id = "SYS") =>
39
- console.log(`[${new Date().toISOString()}] [INFO] [${id}] ${msg}`),
40
-
41
- warn: (msg, id = "SYS") =>
42
- console.warn(`[${new Date().toISOString()}] ⚠️ [WARN] [${id}] ${msg}`),
43
-
44
- error: (msg, err, id = "SYS") =>
45
- console.error(
46
- `[${new Date().toISOString()}] ❌ [ERROR] [${id}] ${msg}`,
47
- err?.message || err,
48
- err?.stack || ""
49
- )
50
  };
51
 
52
- // ================================
53
- // PROMPTS
54
- // ================================
55
-
56
- const CLAUDE_SYSTEM_PROMPT =
57
- "You are a pro. Provide elite, high-level technical responses.";
58
-
59
- // ================================
60
- // BEDROCK
61
- // ================================
62
 
 
63
  const bedrockClient = new BedrockRuntimeClient({
64
- region: "us-east-1",
65
- requestHandler: new NodeHttpHandler({
66
- http2Handler: undefined
67
- })
68
  });
69
 
70
  function getBedrockModelId(modelName) {
71
- switch (modelName) {
72
- case "haiku":
73
- return "arn:aws:bedrock:us-east-1:106774395747:inference-profile/global.anthropic.claude-haiku-4-5-20251001-v1:0";
74
-
75
- case "maverick":
76
- return "arn:aws:bedrock:us-east-1:106774395747:inference-profile/us.meta.llama4-maverick-17b-instruct-v1:0";
77
-
78
- case "claude":
79
- default:
80
- return "arn:aws:bedrock:us-east-1:106774395747:inference-profile/global.anthropic.claude-sonnet-4-6";
81
- }
82
  }
83
 
84
- // ================================
85
- // SUPABASE
86
- // ================================
87
-
88
  const supabase = createClient(
89
- process.env.SUPABASE_URL || '',
90
- process.env.SUPABASE_KEY || ''
91
  );
92
 
93
- let memoryChats = {};
94
- let dirtyChats = new Set();
95
- const activeGenerations = new Map();
96
-
97
- // ================================
98
- // INIT DB
99
- // ================================
100
 
101
  async function initDB() {
102
- try {
103
- log.info("Connecting to Supabase...");
104
-
105
- const { data: dbChats, error } =
106
- await supabase.from('chats').select('*');
107
-
108
- if (error) throw error;
 
 
 
 
 
 
 
 
 
109
 
110
- if (dbChats) {
111
- dbChats.forEach(c => {
112
- memoryChats[c.id] = {
113
- ...c,
114
- inputTokens: c.inputTokens || 0,
115
- outputTokens: c.outputTokens || 0,
116
- isGenerating: false
117
- };
118
- });
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
- log.info(`Hydrated ${dbChats.length} chats.`);
 
121
  }
122
-
123
- setInterval(async () => {
124
- if (dirtyChats.size === 0) return;
125
-
126
- const toSync = Array.from(dirtyChats);
127
- dirtyChats.clear();
128
-
129
- const rowsToUpsert = toSync.map(id => {
130
- const chat = memoryChats[id];
131
-
132
- if (!chat) return null;
133
-
134
- chat.updatedAt = new Date().toISOString();
135
-
136
- return {
137
- id: chat.id,
138
- title: chat.title,
139
- totalTokens: chat.totalTokens,
140
- inputTokens: chat.inputTokens,
141
- outputTokens: chat.outputTokens,
142
- messages: chat.messages,
143
- updatedAt: chat.updatedAt
144
- };
145
- }).filter(Boolean);
146
-
147
- if (rowsToUpsert.length > 0) {
148
- const { error } =
149
- await supabase.from('chats').upsert(rowsToUpsert);
150
-
151
- if (error) {
152
- log.error("Supabase Sync Error", error);
153
- } else {
154
- log.info(`Synced ${rowsToUpsert.length} chats`);
155
- }
156
- }
157
- }, 15000);
158
-
159
- } catch (err) {
160
- log.error("Supabase init failed", err);
161
- }
162
  }
163
-
164
  initDB();
165
 
166
- // ================================
167
- // ROUTES
168
- // ================================
169
 
170
  app.get('/api/chats', (req, res) => {
171
- const chatsList = Object.values(memoryChats)
172
- .map(c => ({
173
- id: c.id,
174
- title: c.title,
175
- totalTokens: c.totalTokens,
176
- inputTokens: c.inputTokens,
177
- outputTokens: c.outputTokens,
178
- updatedAt: c.updatedAt
179
- }))
180
- .sort((a, b) =>
181
- new Date(b.updatedAt) - new Date(a.updatedAt)
182
- );
183
-
184
- res.json(chatsList);
185
  });
186
 
187
  app.get('/api/chats/:id', (req, res) => {
188
- const chat = memoryChats[req.params.id];
189
-
190
- if (!chat) {
191
- return res.status(404).json({
192
- error: "Chat not found"
193
- });
194
- }
195
-
196
- res.json(chat);
197
  });
198
 
199
  app.post('/api/chats', (req, res) => {
200
- const newId = Date.now().toString();
201
-
202
- memoryChats[newId] = {
203
- id: newId,
204
- title: "New Chat",
205
- totalTokens: 0,
206
- inputTokens: 0,
207
- outputTokens: 0,
208
- messages: [],
209
- isGenerating: false,
210
- updatedAt: new Date().toISOString()
211
- };
212
-
213
- dirtyChats.add(newId);
214
-
215
- res.json(memoryChats[newId]);
216
  });
217
 
218
  app.put('/api/chats/:id/title', (req, res) => {
219
- const { id } = req.params;
220
- const { title } = req.body;
221
-
222
- if (!memoryChats[id]) {
223
- return res.status(404).json({
224
- error: "Chat not found"
225
- });
226
- }
227
-
228
- memoryChats[id].title = String(title || '').trim();
229
-
230
- dirtyChats.add(id);
231
-
232
- res.json({
233
- success: true
234
- });
235
  });
236
 
237
  app.delete('/api/chats/:id', async (req, res) => {
238
- const { id } = req.params;
239
-
240
- if (activeGenerations.has(id)) {
241
- activeGenerations.get(id).abort();
242
- activeGenerations.delete(id);
243
- }
244
-
245
- delete memoryChats[id];
246
- dirtyChats.delete(id);
247
-
248
- await supabase
249
- .from('chats')
250
- .delete()
251
- .eq('id', id);
252
-
253
- res.json({
254
- success: true
255
- });
256
  });
257
 
258
  app.post('/api/chats/:id/stop', (req, res) => {
259
- const { id } = req.params;
260
-
261
- if (activeGenerations.has(id)) {
262
- activeGenerations.get(id).abort();
263
- activeGenerations.delete(id);
264
- }
265
-
266
- if (memoryChats[id]) {
267
- memoryChats[id].isGenerating = false;
268
- dirtyChats.add(id);
269
- }
270
-
271
- res.json({
272
- success: true
273
- });
274
  });
275
 
276
- // ================================
277
- // STREAM
278
- // ================================
279
-
280
  app.post('/api/chats/:id/stream', async (req, res) => {
 
 
281
 
282
- const { id } = req.params;
283
-
284
- const {
285
- model,
286
- prompt,
287
- system_prompt,
288
- images
289
- } = req.body;
290
-
291
- const chat = memoryChats[id];
292
-
293
- if (!chat) {
294
- return res.status(404).json({
295
- error: "Chat not found"
296
- });
297
- }
298
-
299
- if (!prompt || !prompt.trim()) {
300
- return res.status(400).json({
301
- error: "Prompt empty"
302
- });
303
- }
304
-
305
- if (chat.isGenerating) {
306
- return res.status(409).json({
307
- error: "Already generating"
308
- });
309
- }
310
-
311
- chat.isGenerating = true;
312
-
313
- if (
314
- chat.messages.length === 0 &&
315
- chat.title === "New Chat"
316
- ) {
317
- chat.title =
318
- prompt.substring(0, 30) +
319
- (prompt.length > 30 ? '...' : '');
320
- }
321
-
322
- chat.messages.push({
323
- role: "user",
324
- content: prompt
325
- });
326
-
327
- const aiMessage = {
328
- role: "assistant",
329
- content: "",
330
- reasoning: ""
331
- };
332
-
333
- chat.messages.push(aiMessage);
334
-
335
- dirtyChats.add(id);
336
-
337
- const abortController = new AbortController();
338
-
339
- activeGenerations.set(id, abortController);
340
-
341
- // SSE
342
- res.setHeader('Content-Type', 'text/event-stream');
343
- res.setHeader('Cache-Control', 'no-cache');
344
- res.setHeader('Connection', 'keep-alive');
345
-
346
- res.flushHeaders();
347
-
348
- const sendEvent = (type, data) => {
349
- if (res.writableEnded) return;
350
-
351
- res.write(
352
- `event: ${type}\n` +
353
- `data: ${JSON.stringify(data)}\n\n`
354
- );
355
- };
356
-
357
- let streamInputTokens = 0;
358
- let streamOutputTokens = 0;
359
- let streamTotalTokens = 0;
360
-
361
- try {
362
-
363
- const bedrockModelId =
364
- getBedrockModelId(model);
365
-
366
- let contentBlock = [{
367
- text: prompt
368
- }];
369
-
370
- if (images?.length) {
371
-
372
- const imageBlocks = images.map(imgStr => {
373
-
374
- const base64Data =
375
- imgStr.replace(
376
- /^data:image\/\w+;base64,/,
377
- ""
378
- );
379
-
380
- return {
381
- image: {
382
- format: 'png',
383
- source: {
384
- bytes: Buffer.from(base64Data, 'base64')
385
- }
386
- }
387
- };
388
- });
389
-
390
- contentBlock = [
391
- ...imageBlocks,
392
- ...contentBlock
393
- ];
394
  }
395
 
396
- const historicalMessages =
397
- chat.messages
398
- .slice(0, -2)
399
- .map(m => ({
400
- role: m.role,
401
- content: [{
402
- text: m.content || "[empty]"
403
- }]
404
- }));
405
-
406
- historicalMessages.push({
407
- role: "user",
408
- content: contentBlock
409
- });
410
-
411
- const command = new ConverseStreamCommand({
412
- modelId: bedrockModelId,
413
-
414
- system: [{
415
- text:
416
- system_prompt ||
417
- CLAUDE_SYSTEM_PROMPT
418
- }],
419
-
420
- messages: historicalMessages,
421
-
422
- inferenceConfig: {
423
- maxTokens: model.includes("claude")
424
- ? 64000
425
- : 8192,
426
-
427
- temperature: 1
428
- }
429
- });
430
-
431
- const response =
432
- await bedrockClient.send(
433
- command,
434
- {
435
- abortSignal:
436
- abortController.signal
437
- }
438
- );
439
-
440
- for await (const chunk of response.stream) {
441
-
442
- if (chunk.contentBlockDelta) {
443
-
444
- const delta =
445
- chunk.contentBlockDelta.delta;
446
-
447
- if (
448
- delta.reasoningContent?.text
449
- ) {
450
-
451
- aiMessage.reasoning +=
452
- delta.reasoningContent.text;
453
-
454
- sendEvent('thinking', {
455
- text:
456
- delta.reasoningContent.text
457
- });
458
-
459
- } else if (delta.text) {
460
-
461
- aiMessage.content += delta.text;
462
-
463
- sendEvent('text', {
464
- text: delta.text
465
- });
466
- }
467
- }
468
-
469
- if (
470
- chunk.metadata?.usage
471
- ) {
472
-
473
- streamInputTokens =
474
- chunk.metadata.usage.inputTokens || 0;
475
-
476
- streamOutputTokens =
477
- chunk.metadata.usage.outputTokens || 0;
478
-
479
- streamTotalTokens =
480
- streamInputTokens +
481
- streamOutputTokens;
482
- }
483
  }
484
 
485
- sendEvent('usage', {
486
- inputTokens: streamInputTokens,
487
- outputTokens: streamOutputTokens,
488
- totalTokens: streamTotalTokens
489
- });
490
-
491
- sendEvent('done', {});
492
-
493
- } catch (err) {
494
-
495
- if (
496
- err.name === 'AbortError' ||
497
- err.name === 'TimeoutError'
498
- ) {
499
 
500
- aiMessage.content +=
501
- "\n\n*[Generation stopped]*";
502
-
503
- sendEvent('stopped', {
504
- message: "Generation stopped"
505
- });
506
-
507
- } else {
508
-
509
- log.error("Stream error", err, id);
510
-
511
- aiMessage.content +=
512
- `\n\n[ERROR]: ${err.message}`;
513
-
514
- sendEvent('error', {
515
- message: err.message
516
- });
517
  }
518
 
519
- } finally {
520
-
521
- activeGenerations.delete(id);
522
-
523
- if (memoryChats[id]) {
524
-
525
- memoryChats[id].inputTokens +=
526
- streamInputTokens;
527
 
528
- memoryChats[id].outputTokens +=
529
- streamOutputTokens;
530
 
531
- memoryChats[id].totalTokens +=
532
- streamTotalTokens;
 
 
533
 
534
- memoryChats[id].isGenerating = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
535
 
536
- dirtyChats.add(id);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
537
  }
538
-
539
- res.end();
540
- }
541
  });
542
 
543
- app.listen(PORT, '0.0.0.0', () => {
544
- log.info(`Server live on ${PORT}`);
545
- });
 
 
 
 
 
 
1
  import cors from 'cors';
2
  import dotenv from 'dotenv';
3
  import { createClient } from '@supabase/supabase-js';
4
+ import { BedrockRuntimeClient, ConverseStreamCommand } from "@aws-sdk/client-bedrock-runtime";
 
 
 
5
  import { NodeHttpHandler } from "@smithy/node-http-handler";
6
  import path from 'path';
7
  import { fileURLToPath } from 'url';
8
 
9
  dotenv.config();
 
10
  const app = express();
11
  const PORT = process.env.PORT || 7860;
12
 
 
14
  const __dirname = path.dirname(__filename);
15
 
16
  app.use(cors());
17
+ app.use(express.json({ limit: '50mb' }));
18
+ app.use(express.static(path.join(__dirname, 'public')));
19
 
20
+ // --- LOGGER HELPER ---
 
 
 
 
 
 
 
 
 
21
  const log = {
22
+ info: (msg, id = "SYS") => console.log(`[${new Date().toISOString()}] [INFO] [${id}] ${msg}`),
23
+ warn: (msg, id = "SYS") => console.warn(`[${new Date().toISOString()}] ⚠️ [WARN][${id}] ${msg}`),
24
+ error: (msg, err, id = "SYS") => console.error(`[${new Date().toISOString()}] ❌ [ERROR] [${id}] ${msg}`, err?.message || err, err?.stack || "")
 
 
 
 
 
 
 
 
 
25
  };
26
 
27
+ // --- SYSTEM PROMPT DEFINITIONS ---
28
+ const CLAUDE_SYSTEM_PROMPT = "You are a pro. Provide elite, high-level technical responses.";
 
 
 
 
 
 
 
 
29
 
30
+ // --- AI CLIENTS ---
31
  const bedrockClient = new BedrockRuntimeClient({
32
+ region: "us-east-1",
33
+ requestHandler: new NodeHttpHandler({ http2Handler: undefined })
 
 
34
  });
35
 
36
  function getBedrockModelId(modelName) {
37
+ switch(modelName) {
38
+ case "haiku": return "arn:aws:bedrock:us-east-1:106774395747:inference-profile/global.anthropic.claude-haiku-4-5-20251001-v1:0";
39
+ case "maverick": return "arn:aws:bedrock:us-east-1:106774395747:inference-profile/us.meta.llama4-maverick-17b-instruct-v1:0";
40
+ case "claude": default: return "arn:aws:bedrock:us-east-1:106774395747:inference-profile/global.anthropic.claude-sonnet-4-6";
41
+ }
 
 
 
 
 
 
42
  }
43
 
44
+ // --- DB & MEMORY MANAGEMENT (SUPABASE) ---
 
 
 
45
  const supabase = createClient(
46
+ process.env.SUPABASE_URL || '',
47
+ process.env.SUPABASE_KEY || ''
48
  );
49
 
50
+ let memoryChats = {};
51
+ let dirtyChats = new Set();
52
+ const activeGenerations = new Map();
 
 
 
 
53
 
54
  async function initDB() {
55
+ try {
56
+ log.info("Connecting to Supabase...");
57
+ const { data: dbChats, error } = await supabase.from('chats').select('*');
58
+ if (error) throw error;
59
+
60
+ if (dbChats) {
61
+ dbChats.forEach(c => {
62
+ memoryChats[c.id] = {
63
+ ...c,
64
+ inputTokens: c.inputTokens || 0,
65
+ outputTokens: c.outputTokens || 0,
66
+ isGenerating: false
67
+ };
68
+ });
69
+ log.info(`Hydrated ${dbChats.length} chats from DB.`);
70
+ }
71
 
72
+ setInterval(async () => {
73
+ if (dirtyChats.size === 0) return;
74
+ const toSync = Array.from(dirtyChats);
75
+ dirtyChats.clear();
76
+
77
+ const rowsToUpsert = toSync.map(id => {
78
+ const chat = memoryChats[id];
79
+ chat.updatedAt = new Date().toISOString();
80
+ return {
81
+ id: chat.id, title: chat.title, totalTokens: chat.totalTokens,
82
+ inputTokens: chat.inputTokens, outputTokens: chat.outputTokens,
83
+ messages: chat.messages, updatedAt: chat.updatedAt
84
+ };
85
+ });
86
+
87
+ if (rowsToUpsert.length > 0) {
88
+ const { error } = await supabase.from('chats').upsert(rowsToUpsert);
89
+ if (error) log.error(`Supabase Sync Error.`, error);
90
+ else log.info(`Synced ${rowsToUpsert.length} chats to Supabase.`);
91
+ }
92
+ }, 15000);
93
 
94
+ } catch (err) {
95
+ log.error('Supabase Initialization Error.', err);
96
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  }
 
98
  initDB();
99
 
100
+ // --- API ENDPOINTS ---
 
 
101
 
102
  app.get('/api/chats', (req, res) => {
103
+ const chatsList = Object.values(memoryChats).map(c => ({
104
+ id: c.id, title: c.title, totalTokens: c.totalTokens,
105
+ inputTokens: c.inputTokens, outputTokens: c.outputTokens, updatedAt: c.updatedAt
106
+ })).sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
107
+ res.json(chatsList);
 
 
 
 
 
 
 
 
 
108
  });
109
 
110
  app.get('/api/chats/:id', (req, res) => {
111
+ const chat = memoryChats[req.params.id];
112
+ if (!chat) return res.status(404).json({ error: "Chat not found" });
113
+ res.json(chat);
 
 
 
 
 
 
114
  });
115
 
116
  app.post('/api/chats', (req, res) => {
117
+ const newId = Date.now().toString();
118
+ memoryChats[newId] = {
119
+ id: newId, title: "New Chat", totalTokens: 0, inputTokens: 0, outputTokens: 0,
120
+ messages:[], isGenerating: false, updatedAt: new Date().toISOString()
121
+ };
122
+ dirtyChats.add(newId);
123
+ log.info("Created new chat.", newId);
124
+ res.json(memoryChats[newId]);
 
 
 
 
 
 
 
 
125
  });
126
 
127
  app.put('/api/chats/:id/title', (req, res) => {
128
+ const { id } = req.params;
129
+ const { title } = req.body;
130
+ if (!memoryChats[id]) return res.status(404).json({ error: "Chat not found" });
131
+ if (!title || typeof title !== 'string') return res.status(400).json({ error: "Invalid title" });
132
+
133
+ memoryChats[id].title = title.trim();
134
+ dirtyChats.add(id);
135
+ log.info(`Title updated to: "${title.trim()}"`, id);
136
+ res.json({ success: true, title: memoryChats[id].title });
 
 
 
 
 
 
 
137
  });
138
 
139
  app.delete('/api/chats/:id', async (req, res) => {
140
+ const { id } = req.params;
141
+ if (activeGenerations.has(id)) {
142
+ activeGenerations.get(id).abort();
143
+ activeGenerations.delete(id);
144
+ }
145
+ delete memoryChats[id];
146
+ dirtyChats.delete(id);
147
+ await supabase.from('chats').delete().eq('id', id);
148
+ log.info("Deleted chat permanently.", id);
149
+ res.json({ success: true });
 
 
 
 
 
 
 
 
150
  });
151
 
152
  app.post('/api/chats/:id/stop', (req, res) => {
153
+ const { id } = req.params;
154
+ log.info("User requested to stop generation.", id);
155
+
156
+ if (activeGenerations.has(id)) {
157
+ activeGenerations.get(id).abort();
158
+ activeGenerations.delete(id);
159
+ }
160
+ if (memoryChats[id]) {
161
+ memoryChats[id].isGenerating = false;
162
+ dirtyChats.add(id);
163
+ }
164
+ res.json({ success: true });
 
 
 
165
  });
166
 
167
+ // --- STREAM ENDPOINT ---
 
 
 
168
  app.post('/api/chats/:id/stream', async (req, res) => {
169
+ const { id } = req.params;
170
+ const { model, prompt, system_prompt, images } = req.body;
171
 
172
+ if (!memoryChats[id]) return res.status(404).send("Chat not found");
173
+ if (!prompt || typeof prompt !== 'string' || prompt.trim() === '') {
174
+ return res.status(400).send("Prompt cannot be empty");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  }
176
 
177
+ if (memoryChats[id].isGenerating) {
178
+ log.warn("Attempted concurrent generation. Rejecting request.", id);
179
+ return res.status(409).json({ error: "Chat is currently generating." });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  }
181
 
182
+ log.info(`Starting stream. Model: ${model} | Prompt length: ${prompt.length} | Images: ${images?.length || 0}`, id);
 
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
+ if (memoryChats[id].messages.length === 0 && memoryChats[id].title === "New Chat") {
185
+ memoryChats[id].title = prompt.substring(0, 30) + (prompt.length > 30 ? '...' : '');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  }
187
 
188
+ memoryChats[id].messages.push({ role: "user", content: prompt });
189
+ const aiMessage = { role: "assistant", content: "", reasoning: "" };
190
+ memoryChats[id].messages.push(aiMessage);
191
+ memoryChats[id].isGenerating = true;
192
+ dirtyChats.add(id);
 
 
 
193
 
194
+ const abortController = new AbortController();
195
+ activeGenerations.set(id, abortController);
196
 
197
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
198
+ res.setHeader('Transfer-Encoding', 'chunked');
199
+ res.setHeader('X-Accel-Buffering', 'no');
200
+ res.flushHeaders();
201
 
202
+ const safeWrite = (data) => {
203
+ if (!req.socket.destroyed && !res.writableEnded) {
204
+ try { res.write(data); } catch (e) { log.warn("Socket disconnected during write.", id); }
205
+ }
206
+ };
207
+ const safeEnd = () => {
208
+ if (!req.socket.destroyed && !res.writableEnded) {
209
+ try { res.end(); } catch (e) {}
210
+ }
211
+ };
212
+
213
+ let streamInputTokens = 0;
214
+ let streamOutputTokens = 0;
215
+ let streamTotalTokens = 0;
216
+
217
+ try {
218
+ const bedrockModelId = getBedrockModelId(model);
219
+ let contentBlock = [{ text: prompt }];
220
+
221
+ if (images && images.length > 0) {
222
+ const imageBlocks = images.map(imgStr => {
223
+ const base64Data = imgStr.replace(/^data:image\/\w+;base64,/, "");
224
+ return { image: { format: 'png', source: { bytes: Buffer.from(base64Data, 'base64') } } };
225
+ });
226
+ contentBlock =[...imageBlocks, ...contentBlock];
227
+ }
228
 
229
+ const historicalMessages = memoryChats[id].messages.slice(0, -2).map(m => {
230
+ let safeText = m.content;
231
+ if (!safeText || safeText.trim() === "") {
232
+ safeText = "[System Note: The model failed to generate a response here previously.]";
233
+ }
234
+ return { role: m.role, content:[{ text: safeText }] };
235
+ });
236
+
237
+ historicalMessages.push({ role: "user", content: contentBlock });
238
+
239
+ // THE FIX: Uncapped Token limit for Claude 3.7 to allow massive reasoning + coding
240
+ const commandMaxTokens = model.includes("claude") ? 64000 : 8192;
241
+
242
+ const command = new ConverseStreamCommand({
243
+ modelId: bedrockModelId,
244
+ system:[{ text: system_prompt || CLAUDE_SYSTEM_PROMPT }],
245
+ messages: historicalMessages,
246
+ inferenceConfig: { maxTokens: commandMaxTokens, temperature: 1 },
247
+ /* additionalModelRequestFields: model.includes("claude") ? {
248
+ thinking: { type: "adaptive" }
249
+ } : undefined
250
+ */
251
+ });
252
+
253
+ const response = await bedrockClient.send(command, { abortSignal: abortController.signal });
254
+ log.info(`Bedrock connected successfully (Max Tokens: ${commandMaxTokens}), streaming chunks...`, id);
255
+
256
+ for await (const chunk of response.stream) {
257
+ if (chunk.contentBlockDelta) {
258
+ const delta = chunk.contentBlockDelta.delta;
259
+ if (delta.reasoningContent && delta.reasoningContent.text) {
260
+ aiMessage.reasoning += delta.reasoningContent.text;
261
+ safeWrite(`__THINK__${delta.reasoningContent.text}`);
262
+ } else if (delta.text) {
263
+ aiMessage.content += delta.text;
264
+ safeWrite(delta.text);
265
+ }
266
+ }
267
+ // Log exactly why it stopped (e.g. max_tokens vs end_turn)
268
+ if (chunk.messageStop) {
269
+ log.info(`Stream stopped. Reason: ${chunk.messageStop.stopReason}`, id);
270
+ }
271
+ if (chunk.metadata && chunk.metadata.usage) {
272
+ streamInputTokens = chunk.metadata.usage.inputTokens || 0;
273
+ streamOutputTokens = chunk.metadata.usage.outputTokens || 0;
274
+ streamTotalTokens = streamInputTokens + streamOutputTokens;
275
+ }
276
+ }
277
+
278
+ log.info(`Stream completed normally. (In: ${streamInputTokens}, Out: ${streamOutputTokens})`, id);
279
+
280
+ } catch (err) {
281
+ if (err.name === 'AbortError' || err.name === 'TimeoutError') {
282
+ log.warn("Generation aborted by user or timeout.", id);
283
+ aiMessage.content += "\n\n*[Generation stopped by user]*";
284
+ safeWrite("\n\n*[Generation stopped by user]*");
285
+ } else {
286
+ log.error("Generation failed during stream processing.", err, id);
287
+ aiMessage.content += `\n\n**[Error]**: ${err.message}`;
288
+ safeWrite(`\n\n**ERROR**: ${err.message}`);
289
+ }
290
+ } finally {
291
+ activeGenerations.delete(id);
292
+
293
+ memoryChats[id].inputTokens += streamInputTokens;
294
+ memoryChats[id].outputTokens += streamOutputTokens;
295
+ memoryChats[id].totalTokens += streamTotalTokens;
296
+ memoryChats[id].isGenerating = false;
297
+ dirtyChats.add(id);
298
+
299
+ safeWrite(`__USAGE__${JSON.stringify({
300
+ inputTokens: streamInputTokens,
301
+ outputTokens: streamOutputTokens,
302
+ totalTokens: streamTotalTokens
303
+ })}`);
304
+ safeEnd();
305
  }
 
 
 
306
  });
307
 
308
+ app.listen(PORT, '0.0.0.0', () => log.info(`AI Server live on http://localhost:${PORT}`));