Download README.md from Devseis/Nox: direct link, hf CLI and curl.
- Browser
- Download file 13.8 kB
-
https://huggingface.co/spaces/Devseis/Nox/resolve/main/README.md
- Command line
-
hf download hf://spaces/Devseis/Nox/README.md
-
curl -L -o README.md https://huggingface.co/spaces/Devseis/Nox/resolve/main/README.md
title: Devseis Chat
emoji: π¬
colorFrom: blue
colorTo: indigo
sdk: static
pinned: false
license: apache-2.0
Nox β Devseis's free AI assistant
Nox is a general-purpose AI chat assistant built for Devseis to a hard $0/month operating constraint. Everything below runs on free tiers β no paid APIs, no rented servers, no billing surprises. This file is meant to fully brief a new session (human or AI) on what exists today and how it works, without needing local machine access.
Architecture
- Frontend: this HF Space (
Devseis/Nox),sdk: staticβ a singleindex.html(plain HTML/CSS/vanilla JS, no framework, no build step) plus a small service worker (sw.js) for PWA installability. Served atdevseis-nox.static.hf.space. - Backend: Cloudflare Worker
devseis-chat-proxy(devseis-chat-proxy.umer-wasim.workers.dev), source lives outside this repo (deployed viawrangler, see Deploying changes below β it is not tracked in this HF repo). - Storage: one Cloudflare KV namespace (
USAGE_KV) β usage counters, feedback records, and server-side RAG document chunks (all with TTLs, nothing kept forever). - Models: Cloudflare Workers AI (
@cf/meta/llama-3.2-1b-instruct"Quick",@cf/meta/llama-3.2-3b-instruct"Standard",@cf/meta/llama-3.1-8b-instruct-fp8"Smart"),@cf/llava-hf/llava-1.5-7b-hffor vision,@cf/baai/bge-m3for embeddings.
Features, and how each is actually built
Chat
Standard streamed chat against Cloudflare Workers AI (SSE), with three model tiers the
visitor can pick from. A daily message cap (DAILY_MESSAGE_CAP = 1000) protects the
free Neuron allocation; a crisis-keyword regex (CRISIS_RE) intercepts self-harm
language before any model call and returns a fixed, sourced response (SOS DΓ©tresse
Luxembourg / 112) β a hard code-level guard, not a prompt instruction, because prompt-only
guards have been proven to fail under sustained conversational pressure (see Lex's
history below).
Live data grounding (no paid search API)
Regex intent-matchers in the worker route specific question types to free, deterministic APIs instead of letting the model guess or hallucinate:
- Weather / air quality / moon phase β Open-Meteo
- News β BBC RSS
- Currency conversion β Frankfurter.app (ECB rates)
- Luxembourg public holidays β date.nager.at
- Current time β deterministic timezone math
- General facts / population / office-holders β DuckDuckGo Instant Answer + Wikipedia REST summary + Wikidata claims (with entity-description disambiguation so "mayor of Luxembourg" the city doesn't resolve to a head of state of the wrong entity)
Google/Bing search and any paid maps/places API were ruled out by the cost constraint; DuckDuckGo's own HTML search page was tested directly and rejected (anti-bot challenge on automated requests) before landing on the Instant Answer API.
Documents β server-side RAG
Upload a PDF/text document while in RAG mode β the worker chunks it
(RAG_CHUNK_SIZE = 800 chars, 100 overlap), embeds each chunk (bge-m3), and stores
{text, vector, name} in KV, scoped by conversation id, TTL 24h, capped at 300 chunks/
conversation. Every later question in that conversation gets its own question embedded
and compared via in-Worker cosine similarity (deliberately not Vectorize β a chat-
uploaded document is a handful of pages, a plain JS loop is simpler and fast enough at
this scale). Replaces an older design that stuffed up to 12,000 chars of raw document
text into every message.
- Adjustable retrieval breadth: sidebar "Chunks per answer" (1β8,
ragTopKin the request body, clamped server-side) β shared by both server and offline RAG. - Source transparency: every reply that used retrieved excerpts gets a "π N" badge;
clicking it shows each chunk's document name, closeness score, and a text preview.
Delivered via an
X-Rag-Chunksresponse header on the streamed reply (headers arrive before the body finishes, so this doesn't require touching the raw SSE stream) or aragChunksJSON field on the buffered fallback path; CORSAccess-Control-Expose-Headershad to explicitly list it or the browser hides it. - Manage indexed documents: sidebar "π View" (only shown once β₯1 document is indexed, hidden again once the last one is removed) lists every document with its chunk count and a running total ("N documents β M chunks / M embeddings" β one embedding per chunk, always, so that's really one number shown two ways), with a Remove button per document. Removing a document is just filtering it out of the flat chunk list and re-saving β there's no separate index to rebuild.
- Multi-file upload: selecting several files at once indexes each individually with combined progress/summary feedback.
Documents β fully offline RAG (local mode)
When "Local AI mode" is active (see below), RAG works identically but entirely
client-side: chunking mirrors the server's algorithm exactly, embedding uses
Xenova/all-MiniLM-L6-v2 via @huggingface/transformers on WebGPU, and chunks are
stored in IndexedDB instead of KV. The document never leaves the device. Source
transparency and the adjustable top-K setting both work here too, computed from the
same ragTopK value.
Local AI mode (offline fallback)
A toggle (or automatic offer, once the free server allocation is exhausted for the day)
loads Llama-3.2-1B-Instruct-q4f16_1-MLC via WebLLM directly in the browser (requires
WebGPU). Chosen deliberately small so it downloads and runs on phones, not just
desktops β a real capability gap vs. the server's best model, disclosed to the user in
an in-app tooltip, not hidden.
Two real bugs were found and fixed here through actual testing, not by inspection:
- Context-window overflow: the local model's real context window is 4096 tokens.
Chat mode's conversation history was never bounded the way the server path is (capped
at the last 20 messages server-side) β a conversation with even one document
attachment kept growing every turn until it hit
ContextWindowSizeExceededError, which then crashed the WebGPU device entirely ("Device was lost"), breaking every later local message until a full page reload. Fixed withtrimHistoryForLocalEngine()(keeps the system message + as many recent messages as fit a conservative chars/2.5-per-token budget, always keeping at least the message just sent), a lower one-shot-document cap (2,500 chars, down from an initial 4,000), and a catch block that tells context-overflow and device-loss apart from generic errors, clearing the cached engine reference on device loss so the next attempt auto-recovers instead of staying broken. - Double-wrapped attachments: one-shot document text was being labeled
[Attached file: name]at file-select time AND wrapped again at send time, on every attachment (not just multi-file) β a real, confusing malformed-prompt bug that had been produacing part of what first looked like pure "the model is too small" garbage output. Fixed by only pre-labeling per-file when multiple files are combined into one message, and skipping the outer wrapper in that case.
Local mode cannot read images (text-only engine β a real capability gap, not a size-tunable limit).
Reliability
If Cloudflare Workers AI returns a confirmed Neuron-exhaustion error, the worker
transparently falls back to Google Gemini's free tier
(gemini-flash-lite-latest β the "-latest" bigger alias was seen hitting real 503
"high demand" errors in testing, the lite variant didn't), then OpenRouter's free tier
(nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free β OpenRouter's own auto-router
picked models that returned empty content more often than not; this one didn't), each
with an 8s timeout so one slow provider can't stall the whole request. Scoped to the
text chat path only β the vision endpoint already degrades gracefully on its own and
would need real separate per-provider multimodal handling to extend this to it.
OpenAI, scoped narrowly, unlike Lex: OPENAI_API_KEY is set on this worker too, but
tried first only when the reply is genuinely grounded in retrieved document excerpts
(ragChunksMeta.length > 0 β a document was actually uploaded and something relevant
was found for this question), streamed via callOpenAIStream the same way Cloudflare's
own reply streams. Plain chat and live-data questions never touch it and stay entirely
on the free Cloudflare tiers regardless of which model tier the visitor picked β a
deliberate choice to keep OpenAI's real cost scoped to where it was shown to matter most
(synthesizing several retrieved excerpts faithfully) rather than all of Nox's traffic
the way it ended up scoped for Lex. Capped at OPENAI_DAILY_CALL_CAP = 200/day
(KV-tracked, separate from DAILY_MESSAGE_CAP), a smaller number than Lex's cap since
this only fires for document-grounded replies, not every message.
Everything else
- PWA: installable, small app-shell cache via
sw.js(network-first forindex.htmlso a redeploy is visible immediately when online; falls back to cache only when genuinely offline). - Multi-language: UI + reply language for English, French, German, Luxembourgish, Portuguese; speech-to-text input and text-to-speech output (Luxembourgish has no browser TTS/STT support anywhere, so it maps to English rather than silently failing).
- Sharing/export: read-only share links, QR code, text/PDF export.
- Feedback: π/π per reply, stored in KV.
- Privacy: a visitor id (not an account) is used only for daily-cap/unique-user
counting; local mode and offline RAG keep data entirely on-device; dedicated
privacy.htmlpage.
Deploying changes
- Frontend: edit
index.html(or other files) in this repo,git commit,git pushβ the HF static Space rebuilds and is live within seconds. There is no staging/preview for the frontend; test locally (a plainpython3 -m http.serverplus a headless-browser smoke test against a Cloudflare preview worker version has been the working pattern) before pushing. - Worker: source is NOT in this repo. Deploy via
wrangler versions uploadfirst (produces a preview URL, e.g.https://<version>-devseis-chat-proxy.umer-wasim.workers.dev) to test against real Cloudflare infra without touching production, thenwrangler versions deploy <id> --yesto promote to 100% traffic. Neverwrangler deploydirectly for changes to the live service. - Pulling the current live worker source or settings requires a Cloudflare API token,
read from
~/Library/Preferences/.wrangler/config/default.tomlvia a Python script (not an inline bash one-liner β sandboxed shells tend to block inline commands that read the token and curl with it, even for the account's own resources).
Known limitations (disclosed, not hidden)
- The local 1B model's summarization/instruction-following quality is noticeably worse than the server models β expected from a model this size, not a bug. RAG mode generally gives more reliable answers than local Chat mode's one-shot document dump, since it retrieves small named excerpts rather than asking the model to hold and reason over a whole document in one pass.
- No automated test suite β changes are verified by real preview-URL/browser testing before promotion, not by a CI pipeline.
Future plan: document β fine-tuned model pipeline
Goal: a visitor uploads documents, the app generates synthetic Q&A training examples from them, trains a LoRA adapter, and serves it back for download β still $0/month.
Chosen approach: Hugging Face Spaces + ZeroGPU (shared, free, on-demand H200 access,
GPU allocated only while a decorated @spaces.GPU function runs). This requires a
Gradio Space β Nox's current frontend is static HTML, not Gradio, so this cannot be
bolted onto the existing Nox Space. It would need to be built as a separate,
companion HF Space (still free to host) rather than a feature added to Nox itself.
The real open risk, not yet validated: ZeroGPU's daily quota is small β 120 seconds/day for an unauthenticated visitor, 210 seconds/day for a free signed-in HF account, 1,500 seconds/day for HF PRO ($9/month). A single LoRA training call alone could plausibly need most or all of that budget, before accounting for the synthetic-example-generation step (also LLM inference, ideally also GPU-backed to stay free). Paying for HF PRO to make this practically usable would also break the $0 constraint. Before any further build work: time a real LoRA training call against the smallest realistic base model and dataset, and check whether it fits inside the 120β210 second window at all. That single number determines whether this pipeline is viable as designed, or needs a fundamentally different approach (e.g., training asynchronously across multiple queued GPU calls with real checkpoint save/resume, which is a meaningfully bigger build than currently sketched).
Stages, at a glance (see prior planning notes for full detail): upload (PDF/DOCX/TXT,
chunked like RAG) β synthetic Q&A generation per chunk via an LLM call β dataset
format/validation β @spaces.GPU-wrapped LoRA training (peft + transformers) β
package adapter_model.safetensors + adapter_config.json for download. Optional,
separate follow-on: serving the resulting adapter via Cloudflare Workers AI has its own
constraints (allowed base models, rank β€ 8, exact filenames, model_type field added
manually, <100MB) that should be re-verified against current Cloudflare docs before
relying on them, since they weren't independently confirmed when first written down.