DeepTutor v1.5.16 Release Notes
Release Date: 2026.08.22
v1.5.15 added a capture inbox under the Book reader and, in doing so, stopped its pages from turning. That is fixed here, alongside a new connected knowledge source — a MarginNote 4 library that its own add-on fills — and a run of failures that were only reachable through a particular gateway: tool-call ids that grew until the provider refused them, embeddings rejected for a parameter we deliberately omit, and a temperature limit that only applied if you had named the right binding. Drop-in — no migrations, nothing to re-index.
What's New
MarginNote 4 as a knowledge base
Connect a library and the MN4 add-on pushes your notes, excerpts, cards and mindmap nodes into it — no upload, no index, nothing copied out of MarginNote. Pair a device from the library's Devices tab, paste the one-time token into the add-on, and syncs arrive incrementally into a store DeepTutor owns.
A turn on that library runs on seven tools of its own rather than on rag: search across excerpt and note text, read a single object, list by type, walk a document's children, follow links, filter by tag, and read mindmap cards. The chat loop hands the turn to them the way it does for an Obsidian vault, because a library that holds no index has nothing for retrieval to chunk.
Contributed as #931 by @evan188199-tech. This is phase one — read-only navigation; write-back is planned.
Book pages turn again
The capture inbox landed as a second child of the reader's flex row without making that row a flex column, so PageReader's h-full collapsed to auto: the body stopped scrolling and the page-turn footer was clipped out of view entirely. A one-line layout fix; every gate we run was green through the regression, since nothing here is visible to types or tests.
Streamed tool-call ids stop growing
A router in front of an OpenAI-compatible provider may re-send the whole tool-call id on every delta chunk. Three core sites appended it instead of assigning it, so the id grew one copy per chunk — #937 saw 47,241 characters — and past the provider's 64-character ceiling every round died with Invalid 'messages[i].tool_calls[0].id': string too long, surfacing as "I could not produce a useful response from the model output." name had the identical defect at both sites, latent on that gateway and fatal on any that repeats the name. The three sites now share one accumulator: id and name arrive whole and are assigned, arguments arrives in fragments and is concatenated.
Model limits follow the model, not the route
Kimi models lock temperature server-side and reject an explicit value with HTTP 400. The override for that has existed since before v1.5.5, but it was resolved from the configured binding's spec — so it never fired for #938, who did the ordinary thing and pointed binding="openai" at Moonshot. The route is not what enforces the limit, so the route no longer decides: the configured spec wins when it says something about the model, otherwise the model's own vendor spec answers. Every model-intrinsic override was being lost the same way behind a generic binding.
Embeddings recover from a gateway that demands encoding_format
ModelScope reads a missing encoding_format as '' and refuses the request (#934); SiliconFlow refuses it when the field is present, which is why we omit it. The same Qwen3-Embedding weights are served on both sides of that disagreement, so no default and no model-family rule can satisfy both. The happy path is byte-identical — the field is still omitted — and a 400 that names both the parameter and the value we are about to send earns exactly one retry with encoding_format="float".
Images in an index describe in parallel
The LlamaIndex pipeline described extracted images one at a time. They now run concurrently with a per-image timeout and a progress line, tunable via image_description_concurrency (default 4) and image_description_timeout_seconds (default 60). A model that turns out not to accept images is no longer retried without them — the image is the question there, and a description invented from the text around it would be indexed as fact. Contributed as #933.
LightRAG converges instead of hanging
Transport failures against a LightRAG base are now classified before they are retried: retryable HTTP statuses get up to three bounded attempts with a capped Retry-After, anything else fails immediately, and the two retry layers no longer multiply. Cancellation cleans up rather than leaking, and a Codex transport failure propagates instead of being swallowed. Contributed as #940.
Assorted
- Indexing progress lines are translatable. All 22 producers sent hardcoded English to the log box. The wire now carries the template plus its values and the frontend renders it in your language; consumers without i18n — logs, snapshots, the CLI — read exactly what they read before.
- A deleted knowledge base stops haunting your sessions. Its name lingered in each session's context chips (#936).
- The development Docker image has a frontend again. Its supervisor ran
node scripts/dev.mjsagainst a production standalone bundle that contains no sources, no configs and noscripts/, sodocker compose -f docker-compose.dev.yml upcame up dead (#906). The stage now takes the builder's wholeweb/tree. The production image was never affected. - A provider's rejection body is capped before it reaches the log. That log exists so you can attach
deeptutor.jsonlto a bug report, and some providers echo the rejected request — schemas, sometimes messages — back into it (#930). - Two MarginNote libraries can no longer claim one store. "My Lib" and "My/Lib" both derive
My_Lib.db; registration now rejects the collision by name, and deleting a library removes the store we created for it. - The MN4 device bridge checks the credential first.
/syncand/heartbeatresolved their store before validating the token, so an unauthenticated POST created a database per distinct header value — and pairing wrote into the caller's workspace while the device path read the default one, which could 403 every sync forever.
Upgrade Notes
pip install -U deeptutor; Docker users pull ghcr.io/hkuds/deeptutor:latest. No schema changes, no re-index, no migration.
- A MarginNote 4 library starts empty. Connect it, pair a device from the Devices tab, then paste the one-time token into the MN4 add-on — the token is shown once and only its hash is kept. The store lives at
data/user/marginnote4/<name>.dband is deleted with the library; nothing in MarginNote itself is touched. - MarginNote libraries are not swept by
rag. Like an Obsidian vault, they are reached only through their own tools, so Book andrag_searchskip them instead of reporting an empty result. - The new image-description knobs apply to the next index build, not the next question, and live under Settings → Knowledge Base.
Full Changelog: v1.5.15...v1.5.16