Spaces:
Running
Running
Update app.js
Browse files
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 |
-
|
| 28 |
-
limit: '50mb'
|
| 29 |
-
}));
|
| 30 |
-
|
| 31 |
-
app.use(express.static(path.join(__dirname, 'public')));
|
| 32 |
-
|
| 33 |
-
// ================================
|
| 34 |
-
// LOGGER
|
| 35 |
-
// ================================
|
| 36 |
-
|
| 37 |
const log = {
|
| 38 |
-
|
| 39 |
-
console.
|
| 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 |
-
|
| 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 |
-
|
| 65 |
-
|
| 66 |
-
http2Handler: undefined
|
| 67 |
-
})
|
| 68 |
});
|
| 69 |
|
| 70 |
function getBedrockModelId(modelName) {
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 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 |
-
|
| 90 |
-
|
| 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 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
-
|
|
|
|
| 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 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 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 |
-
|
| 189 |
-
|
| 190 |
-
|
| 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 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 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 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 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 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 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 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
activeGenerations.
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 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 |
-
|
| 283 |
-
|
| 284 |
-
|
| 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 |
-
|
| 397 |
-
|
| 398 |
-
.
|
| 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 |
-
|
| 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 |
-
|
| 501 |
-
|
| 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 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
memoryChats[id].inputTokens +=
|
| 526 |
-
streamInputTokens;
|
| 527 |
|
| 528 |
-
|
| 529 |
-
|
| 530 |
|
| 531 |
-
|
| 532 |
-
|
|
|
|
|
|
|
| 533 |
|
| 534 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 535 |
|
| 536 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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}`));
|
|
|
|
|
|