TreeMap
A free, open-source disk-space visualizer. Squarified treemap, duplicate hunting, disk-full forecasting, and cleanup that only ever moves files to the Trash.
Several AI models discuss one question together, taking turns and building on each other. Bring your own keys, runs in your browser — local models included via Ollama.
ProductivityTypeScript · CSSGitHub @va001-a11yOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Loading the full README. The saved excerpt is shown below.
Universal multi-model AI: bring your own keys, activate the models you want, and let them work through a question together — taking turns and building on each other, not shouting over each other. A Stop button next to Send kills all activity instantly.
Nothing to install. Pick Melon demo (no key) and watch two models take turns, or paste your own provider key. Everything runs in your browser: there is no Melon server, and your keys go straight from your browser to the provider you chose.Read the full README ↗Universal multi-model AI: bring your own keys, activate the models you want, and let them work through a question together — taking turns and building on each other, not shouting over each other. A Stop button next to Send kills all activity instantly.
Nothing to install. Pick Melon demo (no key) and watch two models take turns, or paste your own provider key. Everything runs in your browser: there is no Melon server, and your keys go straight from your browser to the provider you chose.
The hosted version reaches 17 of the 23 providers. Local models (Ollama, LM Studio, llama.cpp) and three services that refuse browser requests need the desktop version below — they are listed in the picker, greyed out, saying which.
Windows: double-click Melon.bat. macOS / Linux: run ./melon.command (on macOS, chmod +x melon.command once makes it double-clickable in Finder). Both are two-line wrappers around scripts/launch.mjs, so all three platforms share one implementation and cannot drift apart.
The launcher opens your default browser, whatever it is — Firefox, Vivaldi, Brave, Opera, Edge, Safari, Chrome. Melon uses only standard web APIs, so it runs in any current browser.
It checks for Node, installs dependencies on first run, reclaims any processes a previous run left behind, picks ports that are actually free on your machine, starts both servers, and opens the app. Close the window to shut it down. It works from any location — a desktop shortcut is fine.
npm install && npm run dev
One container holds the whole app — the right Node version, the dependencies, the built interface. Nothing is installed on your system, and removing the container leaves nothing behind.
The image is published, so you do not need this repository at all:
docker run -d -p 127.0.0.1:8080:8080 \
--add-host host.docker.internal:host-gateway \
-e OLLAMA_HOST=http://host.docker.internal:11434 \
--name melon ghcr.io/va001-a11y/melon:latest
Melon is then at http://localhost:8080. Images are built for amd64 and arm64, so a Raspberry Pi or Apple Silicon works the same way.
To build it from this checkout instead — which is what you want if you are changing anything:
docker compose up -d
docker compose down stops it; docker compose up -d --build rebuilds after a git pull.
If that port is already taken, Docker says so plainly and refuses to start:
Error: Bind for 127.0.0.1:8080 failed: port is already allocated
Change the left-hand number only — "127.0.0.1:9000:8080" in docker-compose.yml — and use http://localhost:9000. The right-hand number is Melon's own port inside the container, which has its own private network and can never clash with anything on your machine.
Then leave that number alone. Browser storage is tied to the origin, and the origin includes the port, so changing it later makes every saved chat, agent and API key appear to have been deleted. They have not been — they are still under the old address — but there is no way to move them across.
By default Melon listens on 127.0.0.1, so only this machine can reach it. To use it from your phone or another computer, drop that prefix in docker-compose.yml so the line reads "8080:8080". Think before you do: anyone who can reach that address can use your Melon and make it fetch addresses on your network on their behalf — your router's admin page, other services on the LAN. Expose it only on a network where you trust everyone. To reach it from anywhere else, see Using Melon from your phone or another computer under Security below: use a VPN or a proxy with a login, never an open port.
Using Ollama from the container. Ollama runs on your machine, not inside the container, so localhost from in there means the container itself. The compose file already points Melon at the host:
environment:
OLLAMA_HOST: "http://host.docker.internal:11434"
extra_hosts:
- "host.docker.internal:host-gateway"
Docker Desktop on Windows and macOS defines that name already; the extra_hosts line is what makes it work on plain Linux too. If Ollama is on a different machine, put its address there instead.
You will probably also have to change something on Ollama's side. Ollama listens on 127.0.0.1 only, and a request arriving from a container does not come from loopback — so Ollama refuses it, and Melon reports Ollama as unreachable while it is plainly running. This is the commonest reason a containerised app cannot see a local Ollama. Tell Ollama to accept the host's other addresses:
OLLAMA_HOST to 0.0.0.0, then quit Ollama completely — right-click its system-tray icon and choose Quit. Closing its window is not enough; the background process keeps running with the old setting, and that is what makes this change appear not to work. If it will not let go, taskkill /F /IM "ollama app.exe" then taskkill /F /IM ollama.exe, and start it again from the Start menu. Windows will ask about the firewall the first time Ollama binds beyond loopback — allow it on private networks only.launchctl setenv OLLAMA_HOST "0.0.0.0", then restart Ollama.sudo systemctl edit ollama, add Environment="OLLAMA_HOST=0.0.0.0", then sudo systemctl restart ollama.Check it rather than assuming it. On Windows run netstat -an | findstr 11434, elsewhere ss -ltn | grep 11434. You want 0.0.0.0:11434 (and [::]:11434, which is the same thing over IPv6). If it still says 127.0.0.1:11434, the change has not taken effect and the container will be refused.
On Windows, do not check with echo $env:OLLAMA_HOST. Any terminal that was already open — including a new tab in a running Windows Terminal — inherited the old environment and will report the variable missing while it is saved perfectly well. To read what is actually stored, use [Environment]::GetEnvironmentVariable("OLLAMA_HOST", "User").
Be aware of what that does: Ollama has no authentication, so anyone who can reach that port can use your models. On a home network that is usually fine; on shared or public wifi, firewall the port or leave Melon on the desktop launcher instead.
This image runs the server build, not the hosted web build, and that is deliberate: the hosted build runs everything inside the page, where it cannot reach a local Ollama. Here the model requests leave the server process, so local runtimes work and no cross-origin rules apply.
Unified API layer — adapters for Anthropic, OpenAI (and any OpenAI-compatible endpoint via custom base URL: Groq, Mistral, Together, OpenRouter, llama.cpp, ...), Google Gemini, and local Ollama. All streaming, all BYOK — keys live in your browser's localStorage and are only sent to your own local server, never stored server-side.
How agents converse — tucked away in Settings, because the default should just work:
In the two multi-turn modes each agent is told who just spoke and asked to engage with a specific point — agree with a reason, push back, or ask a question — rather than restating itself.
Why is it waiting? — Melon adds no delay of its own between turns (measured at 0 ms). A wait is always one of four things, and the card says which: the model is still writing (writing… 4s), a local model has not started yet because it is loading into memory and reading the prompt (preparing… 40s — the first request after a pause is the slowest, and a large model without a GPU can take minutes), an agent is queued behind another in relay mode (waiting for Ana), or your token pace is deliberately holding it back (paused to slow spending, with a banner naming the limit). Per-model time-to-first-token is in Stats.
Formatted replies — models write Markdown by default, so Melon renders it: headings, bold, lists, blockquotes, code blocks, links and proper tables (which scroll rather than stretching the card). The renderer builds elements directly and never touches innerHTML, so model output cannot inject markup. Turn it off in Settings to see the raw source.
Token pace — a slider capping output tokens per minute. It paces the stream itself, so a single agent's reply visibly slows too. Agents pause between turns to stay under it, so a long discussion stays readable and you have time to react. A live tok/min meter sits in the top bar and shows "pacing…" while it holds back.
Presets export as plain .json (melon-preset-YYYY-MM-DD.json), deliberately: anyone can open one in a text editor and see exactly what they are about to share or import. That transparency is the point — an opaque format would hide whether a secret slipped in.
Secrets never leave the browser. On export the API key is omitted entirely, and the base URL is stripped of query parameters and any embedded user:pass@ credentials, because some APIs accept a key in the URL. On import Melon discards any apiKey it finds — a preset must never spend someone else's quota — and warns you so you can tell the sender to revoke it.
Chats, agents, API keys, presets, teams and settings are saved in your browser's storage, on your machine. They survive closing Melon, restarting the computer, and updating the app — nothing expires and there is no account.
Chats are kept in the browser's IndexedDB, which has room for hundreds of megabytes, so a history full of photos is fine. Everything else is small and sits in localStorage. Chats saved by a version before 0.7.1 are moved across automatically the first time you open the new one. If your browser blocks IndexedDB, chats stay in localStorage, which holds about 5 MB per site, roughly one phone photo. When that fills, Melon saves the chat without its images rather than losing it, and tells you.
The catch worth knowing: browser storage is tied to the exact address, including the port. Melon therefore remembers the port it used (in .melon-ports.json) and reuses it every launch, so your data is always where you left it. If something else has taken that port, Melon moves to another one, tells you clearly that this session will look empty, and explains how to get back.
Since it is browser storage, clearing your browsing data — or using a different browser — starts you fresh. That is also why deleting Melon's folder loses nothing: the code was in the folder, your data never was.
Settings → Backup and restore writes it to one file: every chat, every agent, your presets, teams and settings. It is also how you move a chat history to another computer, which is otherwise impossible.
What to export picks how much goes in, because the two halves travel for different reasons:
A partial restore only replaces its own half. Restoring a chats file replaces your chats and leaves your agents and keys exactly as they were; restoring an agents file leaves your chats alone. Melon tells you which it is about to do before you confirm, and the file records its own scope so it cannot be misread later.
There are two export buttons, and the difference matters:
melon-backup-CONTAINS-API-KEYS-<date>.json, and puts a warning at the top of the file itself. Treat that file exactly like a password. Anyone who opens it can spend money on your accounts, so keep it off email, chat, shared drives and repositories, and delete it once you have restored from it.Restoring replaces everything currently in Melon rather than merging, and tells you what is in the file — how many chats and agents, and whether it holds keys — before you commit to it.
Presets → Export my agents still exists for sharing a line-up with someone else; it never contains keys.
Five providers search the web natively — Anthropic, Google, OpenRouter, Perplexity, and OpenAI's -search-preview models. Switch it on per agent and the model looks things up while it answers, billed by the provider.
Everything else has no search tool at all. For those, Melon can do the searching itself: it queries a search API, hands the results to the model as source material, and lists them as sources on the reply. That includes local models — an Ollama model on your own machine can answer from today's web.
Set it up in Settings → Web search. It needs its own key, separate from your model keys:
| Engine | What it is | Returns | Hosted site |
|---|---|---|---|
| Tavily | Built for AI | Cleaned page text | ✅ |
| Serper | Google's ranking, cheapest | One-line snippets | ✅ |
| Linkup | Built for AI | Long passages of text | ✅ |
| Firecrawl | Searches and reads | Whole pages | ✅ |
| Brave Search | Independent index | Short descriptions | Desktop only |
| Exa | Neural — finds pages by meaning | Page text | Desktop only |
| SerpAPI | Google results | One-line snippets | Desktop only |
"Desktop only" is measured, not guessed. Each API was sent the same request a browser sends from the hosted site, and Brave, Exa and SerpAPI refuse it — so there they appear but cannot be chosen. In the desktop app and in Docker, Melon's server makes the request and every engine works.
Each engine keeps its own key, and only the one you have selected is ever sent. Switching engines never hands one company another's key.
Read full pages (Settings → Web search) has Melon read the top three results in full through Jina Reader — free, no key, no account — rather than relying on the engine's one-line summaries. Those summaries are where stale figures hide: asked for the latest Node.js release, one engine's snippets of secondary sites said 24.19.0, while the official page, read in full, said 24.22.1. The cost is speed, more of each model's context, and Jina seeing which pages were read. A page that cannot be read keeps its snippet, so the search never fails because of it. Firecrawl reads pages itself, so the option is not needed there.
Pairing Serper with Read full pages is the strongest cheap combination: Google chooses which pages matter, then Melon reads them.
To check an engine with your own key before relying on it, run node scripts/probe-search.mjs. It asks for keys without echoing them, shows what each engine returned, and never prints a key.
The Web search toggle in the agents panel switches it on for every agent that can search, instead of editing each one. Agents whose provider cannot search are left alone — an agent told to search when it cannot fails its turn deliberately, and a bulk action should not arm that.
Agents see each other's sources. In a relay, each agent is shown what its teammates cited, attributed to them by name. Melon's own search is shared across a run so those agents already read the same pages; provider-native search cannot be shared, since Claude, Gemini and Perplexity each search inside their own reasoning. Passing the source list on means an agent can say "you used a social-media clip, I used Reuters" instead of flatly contradicting a teammate for reasons neither of them can see.
Providers that can search natively keep doing so; Melon's search is the fallback, and each agent's settings say which one it will use. If search is switched on and neither is available, that agent fails with a message rather than quietly answering from memory — an answer that looks researched and is not is worse than no answer.
Search results — and pages read in full — are material from the open web, handed to the model as sources to weigh rather than instructions to follow. A whole page carries far more of that text than a snippet does, which is worth bearing in mind when you switch reading on.
Melon does not transcribe audio, and refuses audio attachments rather than passing them to a model that will quietly ignore them. Chat models mostly cannot listen, and the ones that can are a minority of the catalog — while a model that silently drops the audio and answers anyway is the worst outcome of all.
Transcribe it elsewhere, then attach the text. A local transcriber — Vibe, MacWhisper, whisper.cpp — runs on your machine, so the recording never leaves it, and handles long files, timestamps and speakers far better than anything Melon could embed. Then:
.txtAttach the file rather than pasting the text. Same content reaches the model, but the conversation stays readable — a named chip instead of ten thousand words in the composer.
The thing to watch is not file size but context. A lesson transcript read by four agents is four copies of it in input tokens, measured against the smallest active model's window. The meter above the composer shows the total before you send, and Melon blocks rather than letting a provider fail halfway through. For a long recording, use fewer agents or models with larger windows.
Melon is bring-your-own-key. Your keys stay in your browser, requests go straight from your machine to the provider you chose, and there is no Melon server, no account and no logging. Nobody but you and that provider sees a conversation.
Melon adds no content filtering of its own, deliberately. It is your key, your machine, and your agreement with the provider — who already enforce their own policies and can act on a key, which Melon cannot. A filter inside an MIT-licensed app you compile yourself would stop nobody who wanted it gone, while blocking legitimate questions for everyone who did not. The exception would be a hosted Melon running on someone else's key: that changes who the operator is, and it would need rate limiting and refusal. The demo on the web version is canned and keyless for exactly this reason.
What Melon does defend against is the opposite direction: text arriving from outside trying to give your agents orders. Agents are told that only your message sets the task, and that teammates' turns, web search results and attachments are material to weigh rather than instructions to obey. This matters more here than in a single-model chat, because Melon is a relay — one agent's reply becomes the next agent's input, so a hostile web page could otherwise travel down the whole line.
That is a prompt instruction, not a sandbox, so treat it as a speed bump rather than a wall. The standing advice is the one Melon shows you in the app: a reply that cites no sources is unverified, whoever wrote it.
Melon's local server, and what it refuses. The desktop launcher and the Docker image both run a small server on your machine, and a server is something other programs can talk to. An external security review in October 2026 found four ways that could be abused; all four were fixed in v0.6.3, and the details are in docs/BUGS.md.
localhost or by IP address. That defeats DNS rebinding, where a malicious domain is made to point at your own machine so a web page can read Melon's replies. Rebinding needs a domain name, so IP addresses stay allowed — http://192.168.1.20:8080 from your phone still works.
If you put Melon behind a reverse proxy with its own hostname, add that name to MELON_ALLOWED_HOSTS (comma-separated), or Melon will refuse it and tell you exactly this.Do not share one Melon between people. It is built for one person. Anyone who can reach an exposed instance can use every key stored in their own browser through it, can reset the spending guard that everyone else is relying on, and can make it fetch addresses on the network it runs in. If several people want Melon, each should run their own.
Using Melon from your phone or another computer. Melon has no login. It does not need one on the machine it runs on, which is why it listens only there by default. Once other devices can reach it, so can anyone else on that network, and none of them needs a password to:
Your keys and chats are not on that list. They live in each browser, not on the server, so a visitor gets an empty Melon of their own.
There is one more thing a login would not fix. Your API key travels to Melon's server with each message, so at a plain http://192.168.x.x address it crosses the network unencrypted. On your own home wifi, that is fine. On shared wifi — a café, a university, an office — it is not.
So:
"8080:8080", see Run it with Docker) is fine as it is."100.101.102.103:8080:8080", and open that address on your phone. To use the machine's Tailscale name instead (melon-box.your-tailnet.ts.net), add that name to MELON_ALLOWED_HOSTS.MELON_ALLOWED_HOSTS.Each address is its own origin to the browser, so Melon at localhost, at an IP and at a Tailscale name keeps three separate sets of keys and chats. Pick one address per device and stay with it.
Where your keys actually live, and who could reach them. Browser storage is scoped to the origin serving the page, and Melon does not own its origin in either build.
| Build | Origin | Who else can read that storage |
|---|---|---|
| Hosted | va001-a11y.github.io | Any page published to that GitHub account |
| Desktop | http://127.0.0.1:5173 | Any other local dev server you run on that port — 5173 is Vite's default |
For the hosted version this means something worth stating outright: using it is trusting whoever publishes it, not only the code. A different page served from that same origin could read a key stored by Melon, and nothing ties the deployed files to this repository in a way a visitor can check. That is true of every hosted bring-your-own-key tool, not a quirk of this one — but it is the reason the hosted build exists mainly to try Melon with the keyless demo.
If that matters to you, run it from source. Clone the repository, read what you are about to run, and start it yourself. Then the only parties are you and your model provider, which is the arrangement the rest of this section describes.
Lock your keys with a passphrase. By default, keys are stored in the browser as plain text. Settings → Lock your API keys encrypts them (AES-256, with a key derived from your passphrase by PBKDF2 at 600,000 rounds), and Melon then asks for the passphrase each time it opens. The passphrase is never stored; it is held in memory until you close the tab or press Lock now. The lock is remembered by the browser like everything else, so it stays on across restarts until you turn it off.
localhost. Browsers only provide the encryption it uses there, so it is unavailable at a plain http://192.168.x.x address. See Using Melon from your phone or another computer.The rule that follows from this: nothing else gets published to that origin. Other projects go on a separate account, an organisation, or their own domain. Browser storage cannot tell one page on an origin from another, so a second site there would sit next to Melon's keys whether or not anyone intended it to.
And if something else ever does share the origin, the narrower rule is no third-party scripts on it. A third-party script is a <script src="…"> pointing at a domain you do not control — analytics, a chat widget, a comment system, an ad tag, a library pulled from a CDN. It runs inside the page rather than in a sandbox, so it can read everything the page can, including another site's stored keys. It does not need to be malicious to be a problem; it needs only to be compromised once. The same applies, less visibly, to an npm package bundled into a build: after bundling it is indistinguishable from your own code and has the same access. An <iframe> is different — it runs in its own origin and cannot reach your storage.
Melon itself loads nothing external. Its page references its own bundle and nothing else: no analytics, no CDN libraries, no web fonts. Every network request the app makes goes either to the model provider you chose or to your own machine.
The desktop row is the same property with a different neighbour: 127.0.0.1:5173 is Vite's default port, so another project of your own could share that storage. Melon pins its port deliberately — moving it would change the origin and make every saved chat, agent and key vanish, which is a worse outcome than the risk.
Don't copy the whole folder — run this instead:
npm run package
It writes Melon-<date>.zip next to the platform folder: about 110 KB, versus 176 MB for the folder as it sits on disk. Send that one file by email, USB, or chat.
On the receiving computer:
Melon.bat (Windows) or ./melon.command (macOS/Linux).Node.js must be installed there (nodejs.org, LTS). Melon is a local web app and Node is the engine that runs it; if it's missing the launcher explains this and offers to open the download page.
Two reasons not to copy node_modules yourself:
esbuild.exe, rollup-win32-x64-msvc.node). Carrying those to a Mac doesn't just waste space, it breaks the install.Your chats, agents and API keys live in the browser, not in these files, so they don't travel with the zip — add your keys again on the new machine.
Push the source tree, not the zip. A repository holding a single .zip is the shape malware takes; a repository holding readable TypeScript is the opposite, and readable source is the only real answer to "should I trust this?".
git init && git add . && git commit -m "Melon"
.gitignore already excludes node_modules/, dist/, .melon-ports.json and the Melon-*.zip build products, so what lands in the repo is exactly what a reader wants to see. Melon is bring-your-own-key and keeps keys in browser storage, so there is no .env to leak — but .gitignore blocks one anyway as a backstop.
The zip still has a job: attach it to a GitHub Release, for people who would rather download one file than use git. Repo for reading, release for running.
One thing to avoid: don't email the zip. Gmail blocks .bat attachments outright, including inside archives, and a downloaded .bat gets tagged by Windows so SmartScreen challenges it on first run. Send a link to the release instead — the same file, without the warnings.
Melon also builds as a static site that runs entirely in the browser — no
Node, no install, no server. The same core does the orchestrating; only the
transport differs. It is live at
va001-a11y.github.io/melon, published
from this repository by .github/workflows/pages.yml.
To run that build locally:
npm run web
Then open http://127.0.0.1:5180/melon/.
Two things are not available there, and the app says so rather than failing quietly:
Both appear in the provider list, greyed out with the reason attached, so it is clear what the desktop version adds rather than looking like a shorter list. The other 17 work exactly as they do on the desktop, and keys still go straight from your browser to the provider.
.github/workflows/pages.yml builds and deploys to GitHub Pages on any v*
tag, or on demand from the Actions tab. It needs Settings → Pages → Source:
GitHub Actions switched on once; after that a release publishes the web
version as a side effect of tagging.
The /melon/ path in the URL is the repo name — GitHub serves project sites
from a subdirectory, and that prefix is baked into the build. A custom domain
would make it / again (client/vite.config.ts).
docs/BUGS.md records every defect found in Melon and how it
was fixed, including the ones from before this repository existed. It is
organised by the patterns that produced them, because most were instances of
five recurring mistakes rather than isolated accidents.
Its companion, docs/FEATURES.md, logs what each release
added, changed or removed, and why.
npm test
The suite in tests/ runs in about half a minute with no network and no
keys: every outside service is replaced by a stand-in. Most tests are a bug
that shipped, named after the release it was fixed in, so a change that
brings one back fails here rather than for a user. GitHub runs it on every
push, and neither the Docker image nor the web version is published unless
it passes.
Pick a provider from the grouped list, type the model id exactly as the provider writes it (lower case, dashes instead of spaces — e.g. llama-3.3-70b-versatile), paste your key, and press Test connection before saving.
The model box is deliberately never pre-filled: providers retire models on their own schedule, so a hardcoded default eventually points at something that no longer exists. Press Fetch and Melon asks the provider which models it serves today, then offers those in the dropdown. Melon knows each provider's endpoint, so you never type a URL unless you're using a local runtime or a custom service — and switching provider clears the previous endpoint and key, so an agent can never post to the wrong service.
Supported out of the box: Anthropic, OpenAI, Google Gemini, xAI (Grok), Mistral, DeepSeek, Cohere, Perplexity, NVIDIA NIM (Nemotron), Moonshot (Kimi), Groq, Cerebras, Together, Fireworks, DeepInfra, OpenRouter, GitHub Models, Azure AI, Ollama, LM Studio, llama.cpp/vLLM, plus Custom OpenAI-compatible for anything else — which covers most chat APIs listed on directories like AIxploria. OpenRouter alone reaches 300+ models with one key.
On Copilot: GitHub Copilot has no public chat API for third-party apps. GitHub Models is Microsoft's supported equivalent and is included — use a GitHub token with the
modelsscope.
No keys? Pick the Melon demo provider, or install Ollama and run models locally for free.
Select a local provider and press Detect local models — Melon scans the usual ports, reports which runtimes are reachable, and fills the model list from what you actually have installed. If nothing is found it tells you what to run.
Local endpoints default to 127.0.0.1 rather than localhost, and Melon transparently retries the other loopback address family on failure: Node resolves localhost to IPv6 ::1 on some systems while Ollama binds to IPv4 only, which otherwise looks like "connection refused" even though Ollama is running.
Both are bound to 127.0.0.1 deliberately. On Windows localhost frequently resolves to IPv6 ::1 first, which breaks the client→server proxy and shows up in the app as "Server not reachable" even though the server is running perfectly. Using the IPv4 address on both sides removes that whole class of failure — and keeps Melon off your local network.
client (Vite/React :5173)
└─ /api proxy ─► server (Express :5175)
├─ /api/registry model capability registry (curated catalog)
├─ /api/run SSE: fan out to active agents in parallel (dynamic limit applied)
├─ /api/stop STOP TOKEN FLOW — terminal kill of all in-flight requests
├─ /api/guard token guard state (session usage, budget)
├─ /api/guard/reset reset session usage counter
├─ /api/analytics per-model usage/performance (+ /reset)
├─ /api/extract-pdf PDF → text so any model can read it
├─ /api/detect-local local runtime scan + Ollama diagnosis
├─ /api/marketplace curated agent bundles
├─ /api/demo/v1 built-in keyless demo provider
└─ providers/ anthropic | openai-compatible | google | ollama
Key server modules: orchestrator.ts (round loop + SSE), catalog.ts (providers, context windows, vision support), stop.ts (circuit breaker), guard.ts (token guard + dynamic limit), prompts.ts (roles, tones, personality, reasoning contract), files.ts (PDF extraction). Client theming lives in client/src/themes.ts, cost modelling in client/src/cost.ts.
Settings (⚙) — themes, discussion length, reply length, simultaneous answering, reasoning display and token budget in one place.
Select theme — six palettes (Paper, Mist, Sepia; Dusk, Ink, High contrast), applied instantly and remembered. Each also tells the browser which scheme it is, so scrollbars and menus match rather than staying stubbornly light on a dark theme.
Attachments — 📎 or paste to attach images (PNG, JPEG, GIF, WebP), PDFs (text extracted in your browser, so any model can read them and nothing is uploaded to extract it), and text or code files (inlined). Audio and video are refused with an explanation rather than failing silently: chat models cannot listen.
Context guard — the meter shows usage against the smallest active model's window. When a chat fills it, sending is blocked with a clear message instead of the provider erroring mid-run.
Collaboration by default — agents answer one at a time in sidebar order, each receiving the full text of every teammate who answered before it in the same round, plus an instruction to build on it, correct it, or fill gaps. Each reply shows a ↳ read … chip naming who it could see. Reorder speakers via right-click → Move up/down. Tick Answer simultaneously for the all-at-once mode — faster, but agents can't see each other within that round (they're running at the same instant); they still see everyone's answers from previous rounds either way.
Role templates — Researcher, Technical Writer, Simplifier, Critic, Synthesizer, Generalist keep agents from duplicating work.
Click-to-toggle agents — clicking an agent turns just that one on or off and never touches the others, so any combination is a few clicks. Right-click for a context menu (edit properties, turn on/off, use only this one, move up/down, duplicate, remove). Per-role group toggles switch whole roles at once.
Analytics dashboard (Phase 3) — per-model calls, success rate, input/output tokens, output share, average latency and time to first token. Plus session totals: runs, agent calls, bursts, extra discussion rounds, budget aborts.
Shareable pipelines — a preset carries the whole workflow, not just the cast: stage names, the brief each stage works to, and which agents sit where. Installing Essay Pipeline rebuilds Sources → Draft → Simplify in one click. Exporting hands that entire arrangement to someone else as one small JSON file — a pipeline you have tuned is a thing you can give away.
Agent presets — one-click install of ready-made agent sets (Research Desk, Fact-Check Panel, Explain It Two Ways, Local Only, Writers' Room, and a keyless Demo Team) with provider, model, role and personality pre-filled. Export your own agents as a shareable JSON preset or import someone else's; presets never carry API keys.
Teams (pipelines) — split the line-up into stages that run in order, each handed everything the previous stages produced. Every reply is labelled with the stage that produced it (① Sources, ② Draft…), and the label is stored on the reply, so reopening an old chat shows how it actually ran even if the line-up has changed since. Single-team runs show no label, since there would be nothing to distinguish. Right-click an agent → Move to Team 2 to start one. Name each team and give it a brief, e.g. Sources (Perplexity, Grok — gather evidence) → Draft (Claude, GPT — write the technical essay) → Simplify (Kimi, Llama — make it readable). Within a team agents work together as usual; between teams it is always sequential. Merge collapses everything back to one team.
Re-ask a question — hover a message you sent and choose Edit to reword it and run it again. The replies below it are replaced, because they answered wording that no longer exists; Branch from here sits next to it if you want to keep both versions. Attachments come with the edited question rather than quietly detaching.
Saved selections — separately, save which agents are currently on so you can restore the same line-up later.
finish_reason, stop_reason, finishReason, done_reason — and Melon normalises them./chat/completions is the de-facto standard, most services listed on directories like AIxploria plug in with just a base URL and a key — OpenRouter alone proxies 300+ models.