← back to Qwen38 Viewer
server.js
130 lines
'use strict';
/**
* qwen38-viewer — a minimal web chat front-end for the local uncensored
* Qwen3.8-27B Heretic model served by Ollama on this Mac (macstudio3).
*
* The model runs in Ollama (localhost:11434); this server is a thin,
* auth-gated proxy + static host. It MUST stay co-located with Ollama —
* a remote host (Kamatera) cannot reach this box's 11434.
*/
const express = require('express');
const path = require('path');
const app = express();
const PORT = process.env.PORT || 9821;
const OLLAMA = process.env.OLLAMA_URL || 'http://127.0.0.1:11434';
const MODEL = process.env.MODEL || 'qwen3.8-27b-heretic';
const KEEP_ALIVE = process.env.KEEP_ALIVE || '-1'; // -1 = never unload (pinned warm, instant responses)
// ---- Basic auth gate — multi-user ------------------------------------------
// USERS env = "user1:pass1,user2:pass2". Falls back to admin/DW2024!.
const USERS = (process.env.USERS || 'admin:DW2024!')
.split(',').map(s => s.trim()).filter(Boolean)
.reduce((m, pair) => {
const i = pair.indexOf(':');
if (i > 0) m[pair.slice(0, i)] = pair.slice(i + 1);
return m;
}, {});
// Case-insensitive shared access codes (any username). CODES env = comma list.
const CI_CODES = new Set((process.env.CODES || 'Dust2026')
.split(',').map(s => s.trim().toLowerCase()).filter(Boolean));
app.use((req, res, next) => {
if (req.path === '/health') return next(); // health is open for canaries
const hdr = req.headers.authorization || '';
const [scheme, b64] = hdr.split(' ');
if (scheme === 'Basic' && b64) {
const s = Buffer.from(b64, 'base64').toString();
const i = s.indexOf(':');
if (i > 0) { // require a colon + non-empty user
const u = s.slice(0, i), p = s.slice(i + 1);
if (USERS[u] === p) return next(); // exact user:pass
if (CI_CODES.has(p.toLowerCase())) return next(); // case-insensitive shared code
}
}
res.set('WWW-Authenticate', 'Basic realm="qwen38"');
return res.status(401).send('Auth required');
});
app.use(express.json({ limit: '1mb' }));
app.use(express.static(path.join(__dirname, 'public')));
app.get('/health', (_req, res) =>
res.json({ status: 'PASS', model: MODEL, ollama: OLLAMA }));
// Model metadata for the UI
app.get('/api/model', async (_req, res) => {
try {
const r = await fetch(`${OLLAMA}/api/tags`);
const j = await r.json();
const m = (j.models || []).find(x => x.name.startsWith(MODEL)) || null;
res.json({ model: MODEL, size: m && m.size, present: !!m });
} catch (e) {
res.status(502).json({ error: String(e) });
}
});
// Streaming chat proxy -> Ollama /api/chat (NDJSON passthrough)
app.post('/api/chat', async (req, res) => {
const messages = Array.isArray(req.body.messages) ? req.body.messages : [];
if (!messages.length) return res.status(400).json({ error: 'messages required' });
const options = {
temperature: clamp(req.body.temperature, 0, 2, 0.8),
num_predict: clamp(req.body.num_predict, 1, 8192, 1024),
};
const think = req.body.think === true; // default false = fast, no reasoning
res.setHeader('Content-Type', 'application/x-ndjson');
res.setHeader('Cache-Control', 'no-cache');
// Idle-timeout guard: if Ollama sends NO data for IDLE_MS (wedged / overloaded /
// thrashing on a cold load) we abort and return a clean, actionable error instead
// of hanging the browser for minutes. Reset on every chunk, so a slow-but-steady
// stream never trips it.
const IDLE_MS = Number(process.env.IDLE_MS) || 90000;
const controller = new AbortController();
let idle, aborted = false;
const bump = () => { clearTimeout(idle); idle = setTimeout(() => { aborted = true; controller.abort(); }, IDLE_MS); };
try {
bump();
const upstream = await fetch(`${OLLAMA}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
signal: controller.signal,
body: JSON.stringify({ model: MODEL, messages, stream: true, options, think,
keep_alive: KEEP_ALIVE === '-1' ? -1 : KEEP_ALIVE }), // -1 (number) = keep forever
});
if (!upstream.ok || !upstream.body) {
if (upstream.body) upstream.body.cancel().catch(() => {}); // don't leak the TCP conn
res.write(JSON.stringify({ error: `ollama ${upstream.status}` }) + '\n');
return res.end();
}
// Abort upstream if the client disconnects
const reader = upstream.body.getReader();
const onClose = () => { clearTimeout(idle); reader.cancel().catch(() => {}); };
req.once('close', onClose);
const dec = new TextDecoder();
for (;;) {
const { done, value } = await reader.read();
if (done) break;
bump(); // got data -> reset the idle timer
res.write(dec.decode(value, { stream: true }));
}
clearTimeout(idle);
req.off('close', onClose);
res.end();
} catch (e) {
clearTimeout(idle);
const msg = aborted
? `model is not responding (overloaded or loading) — no output for ${IDLE_MS/1000}s, please retry`
: String(e);
res.write(JSON.stringify({ error: msg }) + '\n');
res.end();
}
});
function clamp(v, lo, hi, dflt) {
const n = Number(v);
return Number.isFinite(n) ? Math.min(hi, Math.max(lo, n)) : dflt;
}
app.listen(PORT, '0.0.0.0', () =>
console.log(`qwen38-viewer on :${PORT} -> ${OLLAMA} (${MODEL})`));