Store chat images separately; JPEG for large PNGs; gzip/brotli responses

Images of messages (pasted images, screenshots, generated images) were base64
inside the message JSON, so opening a chat downloaded all of them at once
(66 MB for a chat with 120 screenshots). They now live in a `blobs` table,
addressed by SHA-256; messages keep a reference (blob, size). The UI loads
them from /api/blobs/<hash> lazily with an immutable cache header, the runner
fills the bytes in before model requests, JSON exports stay self-contained.

- Store::add_message moves inline images out; large opaque PNGs become JPEG
  (q85) when smaller. Older chats are converted once at start-up in the
  background, unused images are removed (also after deleting a chat) and the
  database is vacuumed when that frees 32 MB or more.
- Responses are compressed with gzip/brotli (not images, SSE streams or
  binary downloads).

Example (copy of production): chat load 66 MB -> 1.5 MB (370 KB brotli),
database 90 MB -> 27 MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-09 20:21:24 +01:00
1 parent 6d656a355d
commit e8e455b779
27 files changed
+474 -31

No files matched your search

Generated
+67
View File
@@ -52,6 +52,21 @@ dependencies = [
"memchr",
]
[[package]]
name = "alloc-no-stdlib"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2fb6cfd47bf496ff64095c20eaba0c201404ee38714d4142fcfa1dc334fcc7a"
[[package]]
name = "alloc-stdlib"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5c1865780388bfa186411ab5f247819487fc4864c6e9c3106611fa347586e1"
dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
@@ -153,6 +168,18 @@ dependencies = [
"password-hash",
]
[[package]]
name = "async-compression"
version = "0.4.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ad47a740fb906fffdd891e8b6d5fde2511600f7bfaebd68939fa08a14999459"
dependencies = [
"compression-codecs",
"compression-core",
"pin-project-lite",
"tokio",
]
[[package]]
name = "async-trait"
version = "0.1.92"
@@ -363,6 +390,27 @@ dependencies = [
"hybrid-array",
]
[[package]]
name = "brotli"
version = "9.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "627b7cfb0578f0eafce087a90fcd40add1231900b69e4d4de2d456260a4f5d81"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
"brotli-decompressor",
]
[[package]]
name = "brotli-decompressor"
version = "6.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75c74c9c51b9140663e196b5fb88bc94c334d7a497c8311cb759d8557874747f"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
]
[[package]]
name = "bstr"
version = "1.13.1"
@@ -682,6 +730,24 @@ dependencies = [
"static_assertions",
]
[[package]]
name = "compression-codecs"
version = "0.4.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84f840cff84d10085260e460023934d0af699e71212b09b2a41452e8bd9cda43"
dependencies = [
"brotli",
"compression-core",
"flate2",
"memchr",
]
[[package]]
name = "compression-core"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ebdda89edc534bf8d5dd2857562633b6495557e5846552ade994b99ed13c36a"
[[package]]
name = "const-oid"
version = "0.10.2"
@@ -4035,6 +4101,7 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08a05a66a4fdd61cbbe0a1d755ffe0ca6aba159dd4820936a0ff8a8278245b9c"
dependencies = [
"async-compression",
"bitflags 2.13.2",
"bytes",
"futures-core",