* fix(static): vendor KaTeX and Mermaid instead of loading them from a CDN
index.html pulled katex.min.{js,css} and mermaid.min.js from cdn.jsdelivr.net on
every page load. For self-hosted software that is three problems at once: an
air-gapped or offline install renders no math and no diagrams at all, every
session announces its IP, User-Agent and Referer to a third party, and the "runs
on your own hardware" promise quietly isn't true.
static/lib/ already vendors highlight.js, docx, xlsx, mammoth, html2pdf and
qrcode, so the CDN usage was an inconsistency rather than a policy. Vendoring
also pins Mermaid, which was floating on the `11` tag, to 11.16.1.
Behaviour is unchanged: both libraries still load eagerly from <head>, just from
this machine.
- KaTeX goes in its own directory because its stylesheet resolves fonts with a
relative url(fonts/...), so the vendored CSS needs no rewrite. Only the .woff2
variants ship, matching static/fonts/, since a browser that supports woff2
never requests the .woff/.ttf alternatives the stylesheet also lists.
- The service worker precaches KaTeX and its fonts so offline math is typeset
rather than falling back to system glyphs, and CACHE_NAME is bumped. Mermaid
is left to the existing cache-first rule: at 3.5 MB, precaching it would mean
re-downloading it on every cache bump for a library most sessions never touch.
- Licence texts travel with the bundles in licenses/, following the convention
the repo already uses for OpenDyslexic and DeepResearch.
- .gitattributes turns the whitespace check off for static/lib/ so `git diff
--check` passes without stripping bytes from the published npm artifacts,
which would desync them from upstream.
* perf(markdown): load KaTeX and Mermaid on first use, not on every page load
Both libraries loaded eagerly from <head>, costing every session ~985 KB on the
wire (929 KB of that Mermaid) even though most chats contain neither a formula
nor a diagram. Measured on a cold profile via the Resource Timing API: JS bytes
per page load drop from 3,102,141 to 2,098,634, a saving of 1,003,507 bytes, and
third-party requests per load go from 3 to 0.
markdown.js now fetches each library the first time one is actually needed:
- renderMermaid() checks for an unprocessed mermaid fence before touching the
network, and re-queries the DOM after the load so a diagram replaced mid-stream
still renders.
- mdToHtml() is synchronous, so when KaTeX is not in yet it banks the math source
in an inert placeholder and schedules a flush that loads the library and swaps
the placeholders in. Once KaTeX is loaded it typesets inline exactly as before,
so callers that never call a render helper still get their math.
Both loaders memoise the promise rather than the module, so concurrent callers
share one fetch and a double trigger cannot start two loads; a failed load clears
the memo so the next formula retries instead of being poisoned for the session.
The flush is scheduled with setTimeout rather than requestAnimationFrame, which
is throttled to a stop in a background tab and never fires at all in a headless
browser, so math would have sat as plain source text until the tab was focused.
If neither library ever loads, math degrades to readable source text and diagrams
to their fence contents, rather than to nothing.
* fix(markdown): unescape & last so math entities survive intact
The math pass unescaped & before < and >. mdToHtml escapes the source
first, so a literal "<" typed inside a formula arrives here as "&lt;",
turns back into "<" on the ampersand pass, and is then eaten by the very next
one. Typing $a < b$ rendered as "a < b" instead of the literal text.
The code-block pass in the same function already unescapes & last; only the
math paths were the outlier, in all four of the copies this branch consolidated
into pushMath(). Reordering to match makes them consistent and clears the
js/double-escaping alert CodeQL raised on this PR.
Math containing a genuinely typed "<" is unaffected, which is why this went
unnoticed for so long. Covered by a regression test asserting both cases.
* fix(markdown): decode entity-spelled math in one pass
mdToHtml escapes the source before the math pass, so a typed "<" reaches
the delimiters as "<" and a typed "<" reaches them as "&lt;".
KaTeX has no entity syntax and reads the leftover "&" as an alignment
marker, so "$a < b$" rendered as a red .katex-error instead of a
formula, on both the inline and the deferred path.
Chained replaces cannot fix it in either order: unescaping "&" first
lets the next pass eat the "<" it just wrote, and unescaping it last
leaves the entity spelling for KaTeX to choke on. One alternation,
longest form first, decodes every spelling and never rescans its own
output.
The tests now drive the vendored KaTeX build rather than a renderer that
echoes its input, which is why the old assertion looked correct.
* fix(document): typeset deferred math before the PDF export
exportAsPdf() renders the document into a detached container and hands
it straight to html2pdf. On a page where KaTeX has not loaded yet,
mdToHtml() returns pending placeholders and schedules a flush scoped to
document, which never reaches a node that was never attached, so the
PDF printed raw formula source.
Render the container's own math first. renderMath() returns immediately
without fetching anything when there is nothing pending, so a document
with no formulas still exports without pulling KaTeX.
9 KiB
Acknowledgments
Odysseus stands on the shoulders of a lot of open-source work. This file credits the projects whose code, assets, or designs are included in or adapted by this repository, and notes their licenses.
If you believe something here is mis-attributed or missing, please open an issue — it will be corrected promptly.
Adapted / borrowed code
Portions of this project were adapted from other open-source repositories. Their original authors retain copyright over the adapted portions, under the licenses noted below.
The sources below are under permissive licenses (MIT / Apache-2.0), which permit
this use as long as their original copyright and license notices are preserved.
The full license texts are kept in licenses/.
- opencode — open-source AI coding
agent (originally opencode-ai/opencode,
archived Sep 2025; now maintained at
anomalyco/opencode). Copyright © the opencode authors. MIT License. Adapted for agent-loop / tool-execution patterns and UI concepts. - llmfit by Alex Jones — the
engine behind the Cookbook's model download / serve / "What Fits?" feature.
Copyright © Alex Jones. MIT License. Adapted in
services/hwfit/(hardware detection, quant-aware fit scoring, model catalog),routes/cookbook_*.py,routes/hwfit_routes.py,static/js/cookbook*.js, andscripts/odysseus-cookbook. - Tongyi DeepResearch by
Alibaba-NLP / Tongyi Lab — the multi-step deep-research agent pipeline.
Copyright © Alibaba-NLP / Tongyi Lab. Apache-2.0. Adapted for Odysseus's
Deep Research feature (
services/research/,src/research_handler.py,routes/research_routes.py,services/search/). Full text inlicenses/DeepResearch-Apache-2.0.txt.
Bundled via Docker Compose
These services are pulled as images by the project's docker-compose.yml
and run alongside Odysseus on docker compose up. They are not modified —
just composed.
| Service | Image | Purpose | License |
|---|---|---|---|
| SearXNG | searxng/searxng:2026.5.31-7159b8aed (pinned tag; see compose) |
Default metasearch backend | AGPL-3.0 |
| ChromaDB | chromadb/chroma:latest |
Vector store for memory / RAG | Apache-2.0 |
| ntfy | binwiederhier/ntfy |
Push notifications (self-hosted reminders) | Apache-2.0 / GPL-2.0 |
Bundled front-end libraries
Vendored in static/lib/ and served directly:
| Library | Purpose | License |
|---|---|---|
| highlight.js v11.9.0 | Code syntax highlighting | BSD-3-Clause |
SheetJS / xlsx (xlsx.full.min.js) |
Spreadsheet (.xlsx) read/write |
Apache-2.0 |
docx (docx.umd.min.js) |
Generate .docx documents |
MIT |
| mammoth.js | Convert .docx → HTML |
BSD-2-Clause |
| html2pdf.js | HTML → PDF export (bundles jsPDF + html2canvas) | MIT |
| jsPDF (bundled in html2pdf) | PDF generation | MIT |
| html2canvas (bundled in html2pdf) | DOM → canvas rasterization | MIT |
node-qrcode (qrcode.min.js) |
QR-code rendering (2FA setup) | MIT |
KaTeX v0.16.22 (katex/katex.min.{js,css} + katex/fonts/*.woff2) |
Math typesetting | MIT (licenses/KaTeX-MIT-LICENSE.txt) |
Mermaid v11.16.1 (mermaid.min.js) |
Diagrams from text | MIT (licenses/Mermaid-MIT-LICENSE.txt) |
KaTeX and Mermaid are loaded on first use by static/js/markdown.js rather than
from index.html, so a session that renders no math and no diagram never fetches
either. Only the .woff2 KaTeX fonts are shipped, matching static/fonts/; the
.woff and .ttf variants its stylesheet also lists are never requested by a
browser that supports woff2. The bundles are the published npm artifacts,
unmodified — .gitattributes turns the whitespace check off for static/lib/
so they can stay byte-identical to upstream.
Front-end libraries loaded at runtime (CDN)
Referenced from cdn.jsdelivr.net / cdnjs.cloudflare.com at runtime — not vendored:
| Library | Purpose | License |
|---|---|---|
| Pyodide 0.27.5 | In-browser Python runtime | MPL-2.0 |
| PDFObject 2.1.1 | Inline PDF embedding | MIT |
Fonts
Bundled in static/fonts/:
| Font | License | Author |
|---|---|---|
| Fira Code | SIL Open Font License 1.1 | Nikita Prokopov & contributors |
| Inter | SIL Open Font License 1.1 | Rasmus Andersson |
GohuFont (fonts/custom/GohuFont.ttf) |
WTFPL | Hugo Chargois |
OpenDyslexic (fonts/OpenDyslexic-{Regular,Bold}.woff2) |
SIL Open Font License 1.1 (licenses/OpenDyslexic-OFL.txt) |
Abbie Gonzalez |
Python dependencies
Core (requirements.txt) and optional (requirements-optional.txt):
| Package | License |
|---|---|
| FastAPI | MIT |
| Uvicorn | BSD-3-Clause |
| python-multipart | Apache-2.0 |
| python-dotenv | BSD-3-Clause |
| HTTPX | BSD-3-Clause |
| Pydantic / pydantic-settings | MIT |
| SQLAlchemy | MIT |
| pypdf | BSD-3-Clause |
| BeautifulSoup4 | MIT |
| charset-normalizer | MIT |
| NumPy | BSD-3-Clause |
| ChromaDB (chromadb-client) | Apache-2.0 |
| fastembed | Apache-2.0 |
| youtube-transcript-api | MIT |
| markdown | BSD-3-Clause |
| icalendar | BSD-2-Clause |
| caldav | GPL-3.0-or-later OR Apache-2.0 (dual; used under Apache-2.0) |
| cryptography | Apache-2.0 / BSD-3-Clause |
| bcrypt | Apache-2.0 |
| MCP (Model Context Protocol SDK) | MIT |
| pyotp | MIT |
| qrcode[pil] | BSD-3-Clause |
| croniter | MIT |
| pytest / pytest-asyncio | MIT / Apache-2.0 |
| duckduckgo-search (optional) | MIT |
| markitdown (optional — Office/EPUB text extraction) | MIT |
| PyMuPDF (optional — form-filling only) | AGPL-3.0 — see note below |
Companion services (interoperated with, not bundled)
Odysseus talks to these over the network/API. They are not distributed with this project; their licenses do not bind this codebase, but they deserve credit:
- Ollama — local model serving (MIT)
- Radicale — CardDAV/CalDAV server (GPL-3.0)
- Dovecot — IMAP server
- isync / mbsync — IMAP mailbox sync (GPL-2.0)
- tmux — terminal multiplexer; Cookbook shells out to it on Linux/macOS for background model downloads and serves (ISC)
- OpenSSH (
ssh,ssh-keygen,ssh-copy-id) — Cookbook shells out to it to manage remote model servers and provision keys (BSD-style permissive) - Model/API providers: Anthropic, OpenAI, Google (Gemini), DuckDuckGo
License-compatibility notes (for the repo's own LICENSE choice)
The core ships fully permissive (MIT-compatible), so the two copyleft concerns from earlier are resolved:
- PDF text extraction now uses
pypdf(BSD-3-Clause) and encoding detection usescharset-normalizer(MIT). chardet (LGPL-2.1) has been removed entirely. - PyMuPDF (AGPL-3.0) is no longer a core dependency. It is optional and
used only by the PDF form-filling feature (
src/pdf_forms.pyand the form endpoints inroutes/document_routes.py), lazy-imported and listed inrequirements-optional.txt. The MIT core runs without it. If you choose to install it, AGPL's network clause then applies to that feature for your deployment (Artifex also sells a commercial PyMuPDF license that lifts this). caldav(Python lib) is dual-licensed GPL-3.0-or-later OR Apache-2.0. Odysseus uses it under Apache-2.0, which is permissive and MIT-compatible.markitdown(Microsoft) is MIT and used only as an optional dependency for Office/EPUB text extraction (src/markitdown_runtime.py), lazy-imported with graceful fallback — the MIT core runs without it. The cloudaz-doc-intelextra is deliberately not installed, keeping extraction fully local.
Thanks to
Most of Odysseus's code was written with AI models, not just by a human. The project would not exist without them — credit where credit is due:
- gpt-oss-120b — the legend that kicked this project off.
- Qwen3-235B
- DeepSeek V3.1 · DeepSeek V4 Pro · DeepSeek V4 Flash
- Claude (Anthropic)
- Codex (OpenAI)
- Friends, for helping me debug.