Emalawi19 commited on
Commit
4dfeba3
Β·
verified Β·
1 Parent(s): 2d13fdc

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +210 -85
server.js CHANGED
@@ -3,30 +3,30 @@ import http from 'http';
3
  import crypto from 'crypto';
4
  import fs from 'fs';
5
 
6
- const PORT = 7860;
7
  const MODEL_NAME = 'onnx-community/Qwen2.5-0.5B-Instruct';
8
  let generator;
9
  let modelReady = false;
10
 
11
- // ── SYSTEM STATE & MEMORY ─────────────────────────────────────────────────────
12
- const queue = [];
13
- const jobs = new Map(); // Stores job state, checkpoints, and cancellation flags
14
- const rateLimits = new Map(); // IP-based rate limiting
15
- const MAX_PARALLEL = 1; // Keep at 1 for transformers.js to prevent thread blocking
16
- let activeCount = 0;
17
 
18
  // ── UTILITIES ─────────────────────────────────────────────────────────────────
19
  function generateId() { return crypto.randomBytes(8).toString('hex'); }
20
 
21
  function log(event, details = {}) {
22
  const timestamp = new Date().toISOString();
23
- const logEntry = `[${timestamp}] ${event.toUpperCase()} - ${JSON.stringify(details)}\n`;
24
  process.stdout.write(logEntry);
25
- fs.appendFileSync('generation_logs.txt', logEntry); // Log to file
26
  }
27
 
28
  function setCORS(res) {
29
- res.setHeader('Access-Control-Allow-Origin', '*');
30
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
31
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
32
  }
@@ -38,17 +38,18 @@ function sendJSON(res, status, data) {
38
  res.end(JSON.stringify(data));
39
  }
40
 
41
- // ── QUEUE & RATE LIMITING ─────────────────────────────────────────────────────
42
  function checkRateLimit(ip) {
43
- const now = Date.now();
44
  const limit = rateLimits.get(ip) || { count: 0, resetTime: now + 60000 };
45
  if (now > limit.resetTime) { limit.count = 0; limit.resetTime = now + 60000; }
46
- if (limit.count >= 10) return false; // Max 10 requests per minute
47
  limit.count++;
48
  rateLimits.set(ip, limit);
49
  return true;
50
  }
51
 
 
52
  function enqueue(jobId, task) {
53
  return new Promise((resolve, reject) => {
54
  queue.push({ jobId, task, resolve, reject });
@@ -60,13 +61,10 @@ async function processQueue() {
60
  if (activeCount >= MAX_PARALLEL || queue.length === 0) return;
61
  activeCount++;
62
  const { jobId, task, resolve, reject } = queue.shift();
63
-
64
- // Check if job was cancelled while in queue
65
  if (jobs.get(jobId)?.status === 'cancelled') {
66
  activeCount--;
67
  return processQueue();
68
  }
69
-
70
  try {
71
  jobs.get(jobId).status = 'processing';
72
  resolve(await task());
@@ -78,34 +76,36 @@ async function processQueue() {
78
  }
79
  }
80
 
81
- // ── MODEL INITIALIZATION ──────────────────────────────────────────────────────
82
  async function loadModel() {
83
  log('system', { message: "Loading model..." });
84
- generator = await pipeline('text-generation', MODEL_NAME, { dtype: 'q4' });
85
  modelReady = true;
86
  log('system', { message: "Model ready!" });
87
  }
88
 
89
- // ── GENERATION & VALIDATION ───────────────────────────────────────────────────
90
  async function generateChunk(messages, maxTokens, attempt = 0) {
91
  const startTime = Date.now();
92
  try {
93
- const output = await generator(messages, {
94
- max_new_tokens: maxTokens,
95
- temperature: 0.3,
96
  repetition_penalty: 1.15,
97
- do_sample: false
98
  });
99
  const generated = output[0].generated_text;
100
- let text = Array.isArray(generated) ? generated.at(-1)?.content || '' : String(generated || '');
101
-
102
- // Output Validation: Detect truncated code blocks
 
 
103
  const openBlocks = (text.match(/```/g) || []).length;
104
- if (openBlocks % 2 !== 0) text += '\n```'; // Repair formatting
105
-
106
  return { text, duration: Date.now() - startTime };
107
  } catch (err) {
108
- if (attempt < 3) { // Failure Recovery: Exponential backoff
109
  log('retry', { attempt: attempt + 1, error: err.message });
110
  await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
111
  return generateChunk(messages, maxTokens, attempt + 1);
@@ -114,72 +114,173 @@ async function generateChunk(messages, maxTokens, attempt = 0) {
114
  }
115
  }
116
 
117
- // ── SLIDING WINDOW ENGINE (AUTO-RESUME & CHUNKING) ────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  async function slidingWindowEngine(res, jobId, systemPrompt, userPrompt, totalTokens) {
119
- const CHUNK_TOKENS = 250;
120
- const CONTEXT_CHARS = 800; // Expanded for better Context Awareness
121
- const totalChunks = Math.ceil(totalTokens / CHUNK_TOKENS);
122
- const job = jobs.get(jobId);
123
 
124
- let fullOutput = job.checkpoint || '';
125
- let previousContext = fullOutput.slice(-CONTEXT_CHARS);
126
- let chunksCompleted = job.chunksCompleted || 0;
127
- let tokensGenerated = 0;
128
- const globalStartTime = Date.now();
129
 
130
  for (let chunkIndex = chunksCompleted; chunkIndex < totalChunks; chunkIndex++) {
131
- // Cancellation Check
132
  if (job.status === 'cancelled') {
133
  res.write(`data: ${JSON.stringify({ type: 'cancelled', message: 'Job stopped by user.' })}\n\n`);
134
  return res.end();
135
  }
136
 
137
- const isFirst = chunkIndex === 0;
138
  const chunkNum = chunkIndex + 1;
139
 
140
- let chunkUserPrompt = isFirst ? userPrompt :
141
- `[SYSTEM: Intelligent Context Compression]\n` +
142
- `Original Request: ${userPrompt}\n` +
143
- `Recent Output Context (Maintain consistency, variables, and formatting):\n...${previousContext}\n\n` +
144
- `INSTRUCTION: Continue generating seamlessly from the exact last character above. Do not repeat the context.`;
 
 
 
 
 
 
 
145
 
146
  const messages = [
147
  { role: 'system', content: systemPrompt },
148
- { role: 'user', content: chunkUserPrompt }
149
  ];
150
 
151
  try {
152
  const { text: chunkText, duration } = await enqueue(jobId, () => generateChunk(messages, CHUNK_TOKENS));
153
-
154
  if (!chunkText || chunkText.trim().length === 0) continue;
155
 
156
- const words = chunkText.split(' ');
157
- tokensGenerated += words.length; // Rough estimation
158
-
159
- // Progress Tracking Metrics
160
- const speed = (words.length / (duration / 1000)).toFixed(2); // words per second
161
  const percentComplete = Math.round((chunkNum / totalChunks) * 100);
162
- const eta = ((totalChunks - chunkNum) * (duration / 1000)).toFixed(0);
163
 
164
- // Stream Output
165
  for (let i = 0; i < words.length; i++) {
166
  const token = (i === 0 && !isFirst ? '' : i === 0 ? '' : ' ') + words[i];
167
- res.write(`data: ${JSON.stringify({
168
- type: 'token', text: token, speed: `${speed} w/s`, eta: `${eta}s`, progress: `${percentComplete}%`
 
 
169
  })}\n\n`);
170
- await new Promise(r => setTimeout(r, 10)); // Streaming effect
171
  }
172
 
173
- // Auto Resume & Checkpoint System Update
174
- fullOutput += (isFirst ? '' : ' ') + chunkText;
175
- previousContext = fullOutput.slice(-CONTEXT_CHARS);
176
-
177
- job.checkpoint = fullOutput;
 
178
  job.chunksCompleted = chunkNum;
179
  jobs.set(jobId, job);
180
  log('checkpoint_saved', { jobId, chunkNum });
181
 
182
- // Stop condition
183
  if (chunkText.trim().length < 50 && !isFirst) break;
184
 
185
  } catch (err) {
@@ -190,7 +291,9 @@ async function slidingWindowEngine(res, jobId, systemPrompt, userPrompt, totalTo
190
  }
191
 
192
  job.status = 'completed';
193
- res.write(`data: ${JSON.stringify({ type: 'done', result: fullOutput.trim(), totalTokens: tokensGenerated })}\n\n`);
 
 
194
  res.end();
195
  }
196
 
@@ -199,61 +302,83 @@ const server = http.createServer(async (req, res) => {
199
  setCORS(res);
200
  if (req.method === 'OPTIONS') { res.writeHead(200); return res.end('{}'); }
201
 
202
- const url = new URL(req.url, `http://${req.headers.host}`);
203
  const pathname = url.pathname;
204
  const clientIp = req.socket.remoteAddress;
205
 
206
- // Rate Limiting
207
  if (!checkRateLimit(clientIp)) return sendJSON(res, 429, { error: "Too many requests. Please wait." });
208
 
 
209
  if (pathname === '/' && req.method === 'GET') {
210
  return sendJSON(res, 200, { status: modelReady ? "ready" : "loading", queue: queue.length });
211
  }
212
 
213
- // Cancellation Endpoint
214
  if (pathname === '/cancel' && req.method === 'POST') {
215
  let body = '';
216
  req.on('data', c => { body += c.toString(); });
217
  req.on('end', () => {
218
- const { jobId } = JSON.parse(body);
219
- if (jobs.has(jobId)) {
220
- jobs.get(jobId).status = 'cancelled';
221
- log('cancelled', { jobId });
222
- return sendJSON(res, 200, { message: `Job ${jobId} cancelled.` });
 
 
 
 
 
223
  }
224
- return sendJSON(res, 404, { error: "Job not found." });
225
  });
226
  return;
227
  }
228
 
 
229
  if (pathname === '/generate' && req.method === 'POST') {
230
  let body = '';
231
  req.on('data', c => { body += c.toString(); });
232
  req.on('end', async () => {
233
  let parsed;
234
- try { parsed = JSON.parse(body); } catch { return sendJSON(res, 400, { error: "Invalid JSON" }); }
 
235
 
236
  const { prompt, resumeJobId } = parsed;
237
- if (!modelReady) return sendJSON(res, 503, { error: "Model loading..." });
238
 
239
- // Initialize or Resume Job
 
 
 
 
 
 
 
 
 
 
 
 
240
  const jobId = resumeJobId && jobs.has(resumeJobId) ? resumeJobId : generateId();
241
  if (!jobs.has(jobId)) {
242
  jobs.set(jobId, { status: 'queued', checkpoint: '', chunksCompleted: 0 });
243
  }
244
 
245
- const totalTokens = 2000; // Simulated dynamic estimate
246
- log('job_started', { jobId, promptLength: prompt?.length });
247
 
248
- res.setHeader('Content-Type', 'text/event-stream');
249
- res.setHeader('Cache-Control', 'no-cache');
250
- res.setHeader('Connection', 'keep-alive');
 
 
 
251
  res.writeHead(200);
252
 
253
- res.write(`data: ${JSON.stringify({ type: 'start', jobId, queuePosition: queue.length })}\n\n`);
 
 
 
254
 
255
  try {
256
- await slidingWindowEngine(res, jobId, "You are a helpful coding assistant.", prompt || "Continue", totalTokens);
257
  } catch (err) {
258
  res.write(`data: ${JSON.stringify({ type: 'fatal_error', error: err.message })}\n\n`);
259
  res.end();
@@ -266,5 +391,5 @@ const server = http.createServer(async (req, res) => {
266
  });
267
 
268
  loadModel().then(() => {
269
- server.listen(PORT, '0.0.0.0', () => console.log(`Engine running on port ${PORT}`));
270
  });
 
3
  import crypto from 'crypto';
4
  import fs from 'fs';
5
 
6
+ const PORT = 7860;
7
  const MODEL_NAME = 'onnx-community/Qwen2.5-0.5B-Instruct';
8
  let generator;
9
  let modelReady = false;
10
 
11
+ // ── SYSTEM STATE ──────────────────────────────────────────────────────────────
12
+ const queue = [];
13
+ const jobs = new Map();
14
+ const rateLimits = new Map();
15
+ const MAX_PARALLEL = 1;
16
+ let activeCount = 0;
17
 
18
  // ── UTILITIES ─────────────────────────────────────────────────────────────────
19
  function generateId() { return crypto.randomBytes(8).toString('hex'); }
20
 
21
  function log(event, details = {}) {
22
  const timestamp = new Date().toISOString();
23
+ const logEntry = `[${timestamp}] ${event.toUpperCase()} - ${JSON.stringify(details)}\n`;
24
  process.stdout.write(logEntry);
25
+ fs.appendFileSync('generation_logs.txt', logEntry);
26
  }
27
 
28
  function setCORS(res) {
29
+ res.setHeader('Access-Control-Allow-Origin', '*');
30
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
31
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
32
  }
 
38
  res.end(JSON.stringify(data));
39
  }
40
 
41
+ // ── RATE LIMITING ─────────────────────────────────────────────────────────────
42
  function checkRateLimit(ip) {
43
+ const now = Date.now();
44
  const limit = rateLimits.get(ip) || { count: 0, resetTime: now + 60000 };
45
  if (now > limit.resetTime) { limit.count = 0; limit.resetTime = now + 60000; }
46
+ if (limit.count >= 10) return false;
47
  limit.count++;
48
  rateLimits.set(ip, limit);
49
  return true;
50
  }
51
 
52
+ // ── QUEUE ─────────────────────────────────────────────────────────────────────
53
  function enqueue(jobId, task) {
54
  return new Promise((resolve, reject) => {
55
  queue.push({ jobId, task, resolve, reject });
 
61
  if (activeCount >= MAX_PARALLEL || queue.length === 0) return;
62
  activeCount++;
63
  const { jobId, task, resolve, reject } = queue.shift();
 
 
64
  if (jobs.get(jobId)?.status === 'cancelled') {
65
  activeCount--;
66
  return processQueue();
67
  }
 
68
  try {
69
  jobs.get(jobId).status = 'processing';
70
  resolve(await task());
 
76
  }
77
  }
78
 
79
+ // ── MODEL ─────────────────────────────────────────────────────────────────────
80
  async function loadModel() {
81
  log('system', { message: "Loading model..." });
82
+ generator = await pipeline('text-generation', MODEL_NAME, { dtype: 'q4' });
83
  modelReady = true;
84
  log('system', { message: "Model ready!" });
85
  }
86
 
87
+ // ── CHUNK GENERATION WITH RETRY ───────────────────────────────────────────────
88
  async function generateChunk(messages, maxTokens, attempt = 0) {
89
  const startTime = Date.now();
90
  try {
91
+ const output = await generator(messages, {
92
+ max_new_tokens: maxTokens,
93
+ temperature: 0.2,
94
  repetition_penalty: 1.15,
95
+ do_sample: false
96
  });
97
  const generated = output[0].generated_text;
98
+ let text = Array.isArray(generated)
99
+ ? generated.at(-1)?.content || ''
100
+ : String(generated || '');
101
+
102
+ // Repair unclosed code blocks
103
  const openBlocks = (text.match(/```/g) || []).length;
104
+ if (openBlocks % 2 !== 0) text += '\n```';
105
+
106
  return { text, duration: Date.now() - startTime };
107
  } catch (err) {
108
+ if (attempt < 3) {
109
  log('retry', { attempt: attempt + 1, error: err.message });
110
  await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
111
  return generateChunk(messages, maxTokens, attempt + 1);
 
114
  }
115
  }
116
 
117
+ // ── PROMPT CLASSIFIER ─────────────────────────────────────────────────────────
118
+ function classifyPrompt(prompt) {
119
+ const lower = prompt.toLowerCase();
120
+
121
+ // Identity questions
122
+ const identityWords = ['who are you','what are you','your name','who made you',
123
+ 'who created you','your company','your founder','about you','introduce yourself'];
124
+ if (identityWords.some(w => lower.includes(w))) return 'identity';
125
+
126
+ // Non-coding general questions
127
+ const generalWords = ['what is','what are','explain','describe','tell me about',
128
+ 'history of','meaning of','definition','how does','why is','who is','who was',
129
+ 'when did','where is','function of','functions of'];
130
+ if (generalWords.some(w => lower.includes(w))) return 'general';
131
+
132
+ // Coding / website / app requests
133
+ const codingWords = ['generate','create','build','make','write','code','website',
134
+ 'webpage','page','app','application','login','register','form','html','css',
135
+ 'javascript','js','node','express','backend','frontend','server','api',
136
+ 'function','script','component','template','dashboard','portfolio','ecommerce',
137
+ 'shop','blog','landing page','navbar','footer','button','database'];
138
+ if (codingWords.some(w => lower.includes(w))) return 'coding';
139
+
140
+ return 'general';
141
+ }
142
+
143
+ // ── SYSTEM PROMPT BUILDER ─────────────────────────────────────────────────────
144
+ function buildSystemPrompt(type) {
145
+
146
+ const IDENTITY = `You are Gini AI, a full-stack web development AI assistant.
147
+ - Created by Emalawi19.
148
+ - Founded by Professor Roosevelt Chinkwende.
149
+ - You are an AI assistant, not a human.
150
+ - When asked your name: "I am Gini AI."
151
+ - When asked who made you: "I was created by Emalawi19."
152
+ - When asked about the founder: "Emalawi19 was founded by Professor Roosevelt Chinkwende."`;
153
+
154
+ if (type === 'identity') {
155
+ return `${IDENTITY}
156
+ Introduce yourself clearly and warmly. State your name, your purpose, your company, and your founder.`;
157
+ }
158
+
159
+ if (type === 'general') {
160
+ return `${IDENTITY}
161
+ GENERAL BEHAVIOR:
162
+ - Answer the question directly and clearly in plain English.
163
+ - Be concise and helpful.
164
+ - Do NOT generate any code for non-coding questions.
165
+ - Do NOT use programming languages to answer everyday questions.`;
166
+ }
167
+
168
+ // type === 'coding'
169
+ return `${IDENTITY}
170
+
171
+ YOU ARE A FULL-STACK WEB DEVELOPER AI. FOLLOW THESE RULES STRICTLY:
172
+
173
+ RULE 1 β€” SINGLE FILE OUTPUT (MOST IMPORTANT):
174
+ When asked to build any website, webpage, app, or UI component:
175
+ - ALWAYS output ONE single complete HTML file.
176
+ - That single file MUST contain ALL HTML structure, ALL CSS styles, and ALL JavaScript β€” nothing external.
177
+ - ALL CSS goes inside a <style> tag inside <head>.
178
+ - ALL JavaScript goes inside a <script> tag at the bottom of <body>.
179
+ - NEVER say "create a separate CSS file" or "create a separate JS file".
180
+ - NEVER split code across multiple files unless the user specifically asks for backend server code.
181
+
182
+ RULE 2 β€” COMPLETE CODE ONLY:
183
+ - The file must be 100% complete and copy-paste ready.
184
+ - Start ALWAYS with <!DOCTYPE html> on the very first line.
185
+ - End ALWAYS with </body> then </html> as the absolute last lines.
186
+ - NEVER use placeholders like "add your code here" or "// TODO".
187
+ - NEVER truncate or cut off. Always finish the complete file.
188
+
189
+ RULE 3 β€” CODE QUALITY:
190
+ - Write clean, modern, well-commented HTML/CSS/JS.
191
+ - Use responsive design (flexbox or grid, mobile-friendly).
192
+ - Include hover effects, smooth transitions, and professional styling.
193
+ - Use CSS custom properties (variables) for colors and theming.
194
+ - JavaScript must be functional β€” forms should validate, buttons should work.
195
+
196
+ RULE 4 β€” BACKEND REQUESTS:
197
+ - If the user asks for a backend (Node.js, Express, API, server), provide it as a SEPARATE clearly labeled code block AFTER the frontend file.
198
+ - Backend code goes in a \`\`\`javascript block labeled "server.js".
199
+ - Still provide the complete frontend HTML file first.
200
+
201
+ RULE 5 β€” STEP BY STEP (only for complex multi-feature apps):
202
+ - If building a complex app, give a brief overview of what the file contains BEFORE the code.
203
+ - Then provide the single complete file.
204
+ - End with: "This is the complete file. Copy and save it as index.html and open in your browser."
205
+
206
+ RULE 6 β€” FORMAT:
207
+ - Always wrap the HTML file in a \`\`\`html code block.
208
+ - Always wrap any JS server code in a \`\`\`javascript code block.
209
+ - Add a short explanation after the code of what was built and how to use it.`;
210
+ }
211
+
212
+ // ── SLIDING WINDOW ENGINE ─────────────────────────────────────────────────────
213
  async function slidingWindowEngine(res, jobId, systemPrompt, userPrompt, totalTokens) {
214
+ const CHUNK_TOKENS = 250;
215
+ const CONTEXT_CHARS = 800;
216
+ const totalChunks = Math.ceil(totalTokens / CHUNK_TOKENS);
217
+ const job = jobs.get(jobId);
218
 
219
+ let fullOutput = job.checkpoint || '';
220
+ let previousContext = fullOutput.slice(-CONTEXT_CHARS);
221
+ let chunksCompleted = job.chunksCompleted || 0;
222
+ let tokensGenerated = 0;
 
223
 
224
  for (let chunkIndex = chunksCompleted; chunkIndex < totalChunks; chunkIndex++) {
 
225
  if (job.status === 'cancelled') {
226
  res.write(`data: ${JSON.stringify({ type: 'cancelled', message: 'Job stopped by user.' })}\n\n`);
227
  return res.end();
228
  }
229
 
230
+ const isFirst = chunkIndex === 0;
231
  const chunkNum = chunkIndex + 1;
232
 
233
+ // First chunk: original prompt
234
+ // Subsequent chunks: sliding window context
235
+ const chunkUserPrompt = isFirst
236
+ ? userPrompt
237
+ : `[CONTINUATION INSTRUCTION]\n` +
238
+ `Original request: ${userPrompt}\n\n` +
239
+ `Here is the end of the code/text you have written so far:\n` +
240
+ `...${previousContext}\n\n` +
241
+ `IMPORTANT: Continue EXACTLY from the last character above. ` +
242
+ `Do NOT repeat any code already written. ` +
243
+ `Do NOT restart from <!DOCTYPE html>. ` +
244
+ `Just continue the code seamlessly.`;
245
 
246
  const messages = [
247
  { role: 'system', content: systemPrompt },
248
+ { role: 'user', content: chunkUserPrompt }
249
  ];
250
 
251
  try {
252
  const { text: chunkText, duration } = await enqueue(jobId, () => generateChunk(messages, CHUNK_TOKENS));
253
+
254
  if (!chunkText || chunkText.trim().length === 0) continue;
255
 
256
+ const words = chunkText.split(' ');
257
+ tokensGenerated += words.length;
258
+ const speed = (words.length / (duration / 1000)).toFixed(2);
 
 
259
  const percentComplete = Math.round((chunkNum / totalChunks) * 100);
260
+ const eta = ((totalChunks - chunkNum) * (duration / 1000)).toFixed(0);
261
 
262
+ // Stream word by word
263
  for (let i = 0; i < words.length; i++) {
264
  const token = (i === 0 && !isFirst ? '' : i === 0 ? '' : ' ') + words[i];
265
+ res.write(`data: ${JSON.stringify({
266
+ type: 'token', text: token,
267
+ speed: `${speed} w/s`, eta: `${eta}s`,
268
+ progress: `${percentComplete}%`
269
  })}\n\n`);
270
+ await new Promise(r => setTimeout(r, 10));
271
  }
272
 
273
+ // Update sliding window
274
+ fullOutput += (isFirst ? '' : ' ') + chunkText;
275
+ previousContext = fullOutput.slice(-CONTEXT_CHARS);
276
+
277
+ // Save checkpoint for resume
278
+ job.checkpoint = fullOutput;
279
  job.chunksCompleted = chunkNum;
280
  jobs.set(jobId, job);
281
  log('checkpoint_saved', { jobId, chunkNum });
282
 
283
+ // Early stop if model finished naturally
284
  if (chunkText.trim().length < 50 && !isFirst) break;
285
 
286
  } catch (err) {
 
291
  }
292
 
293
  job.status = 'completed';
294
+ res.write(`data: ${JSON.stringify({
295
+ type: 'done', result: fullOutput.trim(), totalTokens: tokensGenerated
296
+ })}\n\n`);
297
  res.end();
298
  }
299
 
 
302
  setCORS(res);
303
  if (req.method === 'OPTIONS') { res.writeHead(200); return res.end('{}'); }
304
 
305
+ const url = new URL(req.url, `http://${req.headers.host}`);
306
  const pathname = url.pathname;
307
  const clientIp = req.socket.remoteAddress;
308
 
 
309
  if (!checkRateLimit(clientIp)) return sendJSON(res, 429, { error: "Too many requests. Please wait." });
310
 
311
+ // Status
312
  if (pathname === '/' && req.method === 'GET') {
313
  return sendJSON(res, 200, { status: modelReady ? "ready" : "loading", queue: queue.length });
314
  }
315
 
316
+ // Cancel job
317
  if (pathname === '/cancel' && req.method === 'POST') {
318
  let body = '';
319
  req.on('data', c => { body += c.toString(); });
320
  req.on('end', () => {
321
+ try {
322
+ const { jobId } = JSON.parse(body);
323
+ if (jobs.has(jobId)) {
324
+ jobs.get(jobId).status = 'cancelled';
325
+ log('cancelled', { jobId });
326
+ return sendJSON(res, 200, { message: `Job ${jobId} cancelled.` });
327
+ }
328
+ return sendJSON(res, 404, { error: "Job not found." });
329
+ } catch {
330
+ return sendJSON(res, 400, { error: "Invalid JSON" });
331
  }
 
332
  });
333
  return;
334
  }
335
 
336
+ // Generate
337
  if (pathname === '/generate' && req.method === 'POST') {
338
  let body = '';
339
  req.on('data', c => { body += c.toString(); });
340
  req.on('end', async () => {
341
  let parsed;
342
+ try { parsed = JSON.parse(body); }
343
+ catch { return sendJSON(res, 400, { error: "Invalid JSON" }); }
344
 
345
  const { prompt, resumeJobId } = parsed;
 
346
 
347
+ if (!prompt || !prompt.trim()) return sendJSON(res, 400, { error: "prompt is required" });
348
+ if (!modelReady) return sendJSON(res, 503, { error: "Model loading, please wait..." });
349
+
350
+ // Classify the request
351
+ const promptType = classifyPrompt(prompt);
352
+ const systemPrompt = buildSystemPrompt(promptType);
353
+
354
+ // Decide token budget
355
+ const totalTokens = promptType === 'coding' ? 2000
356
+ : promptType === 'identity' ? 150
357
+ : 300;
358
+
359
+ // Resume or new job
360
  const jobId = resumeJobId && jobs.has(resumeJobId) ? resumeJobId : generateId();
361
  if (!jobs.has(jobId)) {
362
  jobs.set(jobId, { status: 'queued', checkpoint: '', chunksCompleted: 0 });
363
  }
364
 
365
+ log('job_started', { jobId, promptType, totalTokens, prompt: prompt.slice(0, 80) });
 
366
 
367
+ // SSE headers
368
+ res.setHeader('Content-Type', 'text/event-stream');
369
+ res.setHeader('Cache-Control', 'no-cache');
370
+ res.setHeader('Connection', 'keep-alive');
371
+ res.setHeader('X-Accel-Buffering', 'no');
372
+ setCORS(res);
373
  res.writeHead(200);
374
 
375
+ res.write(`data: ${JSON.stringify({
376
+ type: 'start', jobId, promptType,
377
+ queuePosition: queue.length
378
+ })}\n\n`);
379
 
380
  try {
381
+ await slidingWindowEngine(res, jobId, systemPrompt, prompt.trim(), totalTokens);
382
  } catch (err) {
383
  res.write(`data: ${JSON.stringify({ type: 'fatal_error', error: err.message })}\n\n`);
384
  res.end();
 
391
  });
392
 
393
  loadModel().then(() => {
394
+ server.listen(PORT, '0.0.0.0', () => console.log(`Gini AI engine running on port ${PORT}`));
395
  });