diff --git a/Cargo.lock b/Cargo.lock
index 22e04fb..6c71338 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -94,6 +94,28 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+[[package]]
+name = "async-stream"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
+dependencies = [
+ "async-stream-impl",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-stream-impl"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "async-trait"
version = "0.1.89"
@@ -172,6 +194,12 @@ dependencies = [
"tracing",
]
+[[package]]
+name = "base64"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+
[[package]]
name = "base64"
version = "0.22.1"
@@ -224,6 +252,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+[[package]]
+name = "cfg_aliases"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
+
[[package]]
name = "chrono"
version = "0.4.44"
@@ -277,6 +311,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+[[package]]
+name = "clipboard-win"
+version = "5.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4"
+dependencies = [
+ "error-code",
+]
+
[[package]]
name = "colorchoice"
version = "1.0.5"
@@ -293,6 +336,19 @@ dependencies = [
"windows-sys 0.59.0",
]
+[[package]]
+name = "console"
+version = "0.15.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
+dependencies = [
+ "encode_unicode",
+ "libc",
+ "once_cell",
+ "unicode-width 0.2.2",
+ "windows-sys 0.59.0",
+]
+
[[package]]
name = "console"
version = "0.16.3"
@@ -415,6 +471,41 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "darling"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "der"
version = "0.8.0"
@@ -434,6 +525,37 @@ dependencies = [
"powerfmt",
]
+[[package]]
+name = "derive_builder"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
+dependencies = [
+ "derive_builder_macro",
+]
+
+[[package]]
+name = "derive_builder_core"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "derive_builder_macro"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
+dependencies = [
+ "derive_builder_core",
+ "syn",
+]
+
[[package]]
name = "dirs"
version = "6.0.0"
@@ -517,6 +639,12 @@ dependencies = [
"cfg-if",
]
+[[package]]
+name = "endian-type"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
+
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -533,6 +661,21 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "error-code"
+version = "3.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
+
+[[package]]
+name = "esaxx-rs"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6"
+dependencies = [
+ "cc",
+]
+
[[package]]
name = "fallible-iterator"
version = "0.3.0"
@@ -551,6 +694,17 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+[[package]]
+name = "fd-lock"
+version = "4.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
+dependencies = [
+ "cfg-if",
+ "rustix",
+ "windows-sys 0.59.0",
+]
+
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
@@ -800,12 +954,12 @@ dependencies = [
"dirs",
"futures",
"http",
- "indicatif",
+ "indicatif 0.18.4",
"libc",
"log",
"native-tls",
"num_cpus",
- "rand",
+ "rand 0.9.4",
"reqwest",
"serde",
"serde_json",
@@ -815,6 +969,15 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "home"
+version = "0.5.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "http"
version = "1.4.0"
@@ -919,7 +1082,7 @@ version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
- "base64",
+ "base64 0.22.1",
"bytes",
"futures-channel",
"futures-util",
@@ -1050,6 +1213,12 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
[[package]]
name = "idna"
version = "1.1.0"
@@ -1083,13 +1252,26 @@ dependencies = [
"serde_core",
]
+[[package]]
+name = "indicatif"
+version = "0.17.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
+dependencies = [
+ "console 0.15.11",
+ "number_prefix",
+ "portable-atomic",
+ "unicode-width 0.2.2",
+ "web-time",
+]
+
[[package]]
name = "indicatif"
version = "0.18.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb"
dependencies = [
- "console",
+ "console 0.16.3",
"portable-atomic",
"unicode-width 0.2.2",
"unit-prefix",
@@ -1129,6 +1311,24 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+[[package]]
+name = "itertools"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itertools"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
+dependencies = [
+ "either",
+]
+
[[package]]
name = "itoa"
version = "1.0.18"
@@ -1218,6 +1418,22 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+[[package]]
+name = "macro_rules_attribute"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520"
+dependencies = [
+ "macro_rules_attribute-proc_macro",
+ "paste",
+]
+
+[[package]]
+name = "macro_rules_attribute-proc_macro"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30"
+
[[package]]
name = "matchers"
version = "0.2.0"
@@ -1245,6 +1461,12 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -1266,6 +1488,28 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "monostate"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3341a273f6c9d5bef1908f17b7267bbab0e95c9bf69a0d4dcf8e9e1b2c76ef67"
+dependencies = [
+ "monostate-impl",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "monostate-impl"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "native-tls"
version = "0.2.18"
@@ -1283,6 +1527,37 @@ dependencies = [
"tempfile",
]
+[[package]]
+name = "nibble_vec"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43"
+dependencies = [
+ "smallvec",
+]
+
+[[package]]
+name = "nix"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
[[package]]
name = "ntapi"
version = "0.4.3"
@@ -1326,6 +1601,12 @@ dependencies = [
"libc",
]
+[[package]]
+name = "number_prefix"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
+
[[package]]
name = "once_cell"
version = "1.21.4"
@@ -1338,6 +1619,28 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+[[package]]
+name = "onig"
+version = "6.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2"
+dependencies = [
+ "bitflags",
+ "libc",
+ "once_cell",
+ "onig_sys",
+]
+
+[[package]]
+name = "onig_sys"
+version = "69.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e68317604e77e53b85896388e1a803c1d21b74c899ec9e5e1112db90735edd7"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
[[package]]
name = "openssl"
version = "0.10.78"
@@ -1411,6 +1714,12 @@ dependencies = [
"windows-link",
]
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
[[package]]
name = "pem-rfc7468"
version = "1.0.0"
@@ -1525,6 +1834,7 @@ dependencies = [
name = "puma"
version = "0.0.3"
dependencies = [
+ "async-stream",
"axum",
"chrono",
"clap",
@@ -1532,17 +1842,21 @@ dependencies = [
"dirs",
"futures",
"hf-hub",
- "indicatif",
+ "indicatif 0.18.4",
"prettytable-rs",
"regex",
"reqwest",
"rusqlite",
"rusqlite_migration",
+ "rustyline",
+ "rustyline-derive",
"serde",
"serde_derive",
"serde_json",
"sysinfo",
"tempfile",
+ "thiserror 2.0.18",
+ "tokenizers",
"tokio",
"tokio-stream",
"tower 0.4.13",
@@ -1573,14 +1887,45 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+[[package]]
+name = "radix_trie"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd"
+dependencies = [
+ "endian-type",
+ "nibble_vec",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
[[package]]
name = "rand"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
dependencies = [
- "rand_chacha",
- "rand_core",
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
]
[[package]]
@@ -1590,7 +1935,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
- "rand_core",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.17",
]
[[package]]
@@ -1612,6 +1966,17 @@ dependencies = [
"rayon-core",
]
+[[package]]
+name = "rayon-cond"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "059f538b55efd2309c9794130bc149c6a553db90e9d99c2030785c82f0bd7df9"
+dependencies = [
+ "either",
+ "itertools 0.11.0",
+ "rayon",
+]
+
[[package]]
name = "rayon-core"
version = "1.13.0"
@@ -1688,7 +2053,7 @@ version = "0.12.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
- "base64",
+ "base64 0.22.1",
"bytes",
"encoding_rs",
"futures-core",
@@ -1817,6 +2182,39 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+[[package]]
+name = "rustyline"
+version = "14.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7803e8936da37efd9b6d4478277f4b2b9bb5cdb37a113e8d63222e58da647e63"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "clipboard-win",
+ "fd-lock",
+ "home",
+ "libc",
+ "log",
+ "memchr",
+ "nix",
+ "radix_trie",
+ "unicode-segmentation",
+ "unicode-width 0.1.14",
+ "utf8parse",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustyline-derive"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5af959c8bf6af1aff6d2b463a57f71aae53d1332da58419e30ad8dc7011d951"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "ryu"
version = "1.0.23"
@@ -1997,6 +2395,18 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "spm_precompiled"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5851699c4033c63636f7ea4cf7b7c1f1bf06d0cc03cfb42e711de5a5c46cf326"
+dependencies = [
+ "base64 0.13.1",
+ "nom",
+ "serde",
+ "unicode-segmentation",
+]
+
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
@@ -2195,6 +2605,38 @@ dependencies = [
"zerovec",
]
+[[package]]
+name = "tokenizers"
+version = "0.20.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b08cc37428a476fc9e20ac850132a513a2e1ce32b6a31addf2b74fa7033b905"
+dependencies = [
+ "aho-corasick",
+ "derive_builder",
+ "esaxx-rs",
+ "getrandom 0.2.17",
+ "indicatif 0.17.11",
+ "itertools 0.12.1",
+ "lazy_static",
+ "log",
+ "macro_rules_attribute",
+ "monostate",
+ "onig",
+ "paste",
+ "rand 0.8.7",
+ "rayon",
+ "rayon-cond",
+ "regex",
+ "regex-syntax",
+ "serde",
+ "serde_json",
+ "spm_precompiled",
+ "thiserror 1.0.69",
+ "unicode-normalization-alignments",
+ "unicode-segmentation",
+ "unicode_categories",
+]
+
[[package]]
name = "tokio"
version = "1.52.1"
@@ -2419,6 +2861,21 @@ version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+[[package]]
+name = "unicode-normalization-alignments"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43f613e4fa046e69818dd287fdc4bc78175ff20331479dab6e1b0f98d57062de"
+dependencies = [
+ "smallvec",
+]
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
+
[[package]]
name = "unicode-width"
version = "0.1.14"
@@ -2437,6 +2894,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+[[package]]
+name = "unicode_categories"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
+
[[package]]
name = "unit-prefix"
version = "0.5.2"
@@ -2455,7 +2918,7 @@ version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0"
dependencies = [
- "base64",
+ "base64 0.22.1",
"cookie_store",
"der",
"flate2",
@@ -2479,7 +2942,7 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c"
dependencies = [
- "base64",
+ "base64 0.22.1",
"http",
"httparse",
"log",
diff --git a/Cargo.toml b/Cargo.toml
index 622000e..40cd30b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,7 +2,7 @@
name = "puma"
version = "0.0.3"
edition = "2021"
-description = "A lightweight, high-performance inference engine for local AI."
+description = "A lightweight, high-performance model engine for local AI."
license = "Apache-2.0"
repository = "https://github.com/InftyAI/PUMA"
homepage = "https://github.com/InftyAI/PUMA"
@@ -31,6 +31,7 @@ sysinfo = "0.32"
rusqlite = { version = "0.32", features = ["bundled"] }
rusqlite_migration = "1.3"
regex = "1.11"
+thiserror = "2.0"
# Web server
axum = "0.7"
@@ -39,6 +40,10 @@ tower-http = { version = "0.5", features = ["cors", "trace"] }
uuid = { version = "1.0", features = ["v4", "serde"] }
futures = "0.3"
tokio-stream = "0.1"
+rustyline = "14.0"
+rustyline-derive = "0.10"
+tokenizers = "0.20"
+async-stream = "0.3"
[dev-dependencies]
tempfile = "3.12"
diff --git a/Makefile b/Makefile
index 0e72459..d48a626 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,5 @@
build:
+ rm -rf ./puma
cargo build && cp target/debug/puma ./puma
test:
diff --git a/README.md b/README.md
index 846ad16..b2558c2 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
-**A lightweight, high-performance inference engine for local AI**
+**A lightweight, high-performance model engine for local AI**
[](https://github.com/InftyAI/PUMA)
[](https://github.com/InftyAI/PUMA/releases)
diff --git a/docs/fsm_architecture.md b/docs/fsm_architecture.md
new file mode 100644
index 0000000..2ddccf5
--- /dev/null
+++ b/docs/fsm_architecture.md
@@ -0,0 +1,240 @@
+# FSM Architecture for PUMA
+
+Finite State Machine pattern for sequence lifecycle management. The FSM states
+and events are defined in `src/fsm/`; the transition logic lives in the
+`SequenceManager` (`src/sequence_manager/mod.rs`), which the `Scheduler` drives.
+
+## Core Concepts
+
+### State Machine per Sequence
+
+Each sequence is an independent state machine. Multiple sequences can be in different states simultaneously.
+
+**Implementation:** `src/fsm/states.rs`
+
+**States:**
+- `Empty` - Pre-birth placeholder; the `from` state of `Create`, never persisted
+- `Waiting` - In queue, no resources allocated
+- `Scheduling` - Being scheduled (transitional)
+- `Prefilling` - Processing prompt
+- `Decoding` - Generating tokens
+- `Preempted` - Temporarily suspended (blocks freed)
+- `Finished` - Completed successfully
+- `Aborted` - Error or cancelled
+
+### States Own Resources
+
+Each state owns the resources it needs:
+- `WaitingState` has no blocks (not allocated yet)
+- `DecodingState` owns blocks via `Arc>`
+- `PreemptedState` has no blocks (freed)
+
+**Benefit:** Type system enforces resource invariants - can't access blocks that don't exist.
+
+### Events as State Transformations
+
+Events transform states: `Event + OldState → NewState`
+
+**Implementation:** `src/fsm/events.rs`
+
+**Events:**
+- `Create` - Empty → Waiting (birth a new sequence)
+- `Schedule` - Waiting → Prefilling (allocate blocks)
+- `AppendTokens` - Prefilling → Decoding or Decoding → Decoding/Finished
+- `Preempt` - Decoding → Preempted (free blocks on OOM)
+- `Resume` - Preempted → Waiting (re-queue)
+- `Complete` - * → Finished (free blocks)
+- `Abort` - * → Aborted (cleanup)
+
+Invalid transitions return `Error::InvalidTransition`.
+
+Forking (Decoding → (Decoding, Decoding) for beam search) is **not** an event:
+it is a dedicated `SequenceManager::fork` method, since it produces a second
+sequence rather than transforming one state into another.
+
+### FSM Integration with SequenceManager
+
+The FSM logic lives in the **`SequenceManager`** (the *mechanism* layer), which
+owns `BlockManager`. The `Scheduler` (the *policy* layer) drives it with events
+and never touches `BlockManager` or transition logic directly.
+
+**Public API:** `SequenceManager::advance(seq_id, event)` - Event-based abstraction
+- Single entry point for all state transitions
+- Easy to add logging, metrics, debugging
+- Backs up state and restores it on error (no block leaks)
+
+**Internal Implementation:** Private `transition()` / `transition_*()` methods
+- Type-safe helpers that enforce correct state types
+- Direct access to `self.block_manager` - no parameter passing
+- Called by `advance()` after looking up the current state
+
+```rust
+impl SequenceManager {
+ pub fn advance(&mut self, seq_id: SequenceId, event: Event) -> Result<&SequenceState> {
+ // Remove current state, run transition(), reinsert on success
+ // or restore the backup on error.
+ }
+
+ fn transition(&mut self, state: SequenceState, event: Event) -> Result {
+ let seq_id = state.seq_id();
+ match (state, event) {
+ (SequenceState::Waiting(s), Event::Schedule{..}) =>
+ self.transition_schedule(s),
+ // ... dispatches to internal methods
+ }
+ }
+
+ fn transition_schedule(&mut self, state: WaitingState) -> Result {
+ // Direct access to self.block_manager, self.tokens_per_block
+ }
+}
+```
+
+## State Diagram
+
+```
+ AddRequest
+ ↓
+ ┌─────────────→ Waiting
+ │ ↓ ScheduleEvent
+ │ Prefilling
+ │ ↓ AppendTokens
+ │ Decoding
+ │ ├─→ AppendTokens (continue)
+ │ │ ├─ Decoding (more tokens)
+ │ │ └─ Finished (max_tokens)
+ │ │
+ │ ├─→ fork() method (beam search, not an event)
+ │ │ ├─ Parent: Decoding
+ │ │ └─ Child: Decoding
+ │ │
+ │ ├─→ PreemptEvent (OOM)
+ │ │ ↓ Preempted
+ │ │ ↓ ResumeEvent
+ │ └───┘
+ │
+ └─ CompleteEvent → Finished
+```
+
+## Scheduler / SequenceManager Integration
+
+The `SequenceManager` owns the FSM and executes all state transitions; the
+`Scheduler` owns the queues/batches and decides *which* events to fire.
+
+**Implementation:** `src/sequence_manager/mod.rs` (mechanism),
+`src/scheduler/core.rs` (policy)
+
+**Key features:**
+- Synchronous API (LLMEngine handles async coordination)
+- `SequenceManager` maintains `HashMap`
+- `SequenceManager` owns `BlockManager` directly - no parameter passing needed
+- Clones state before transitions (prevents loss on failure)
+- Restores backup on error (no block leaks)
+- Event-based `advance()` method for maintainability
+
+**Tests:** `cargo test --lib`
+
+## Arc Optimization
+
+States use `Arc>` for efficient cloning:
+
+- **State cloning** (for backup/fork): **O(1)** - just increment ref count
+- **Modifying blocks**: Copy-on-write via `Arc::make_mut()` - only copies if shared
+- **Fork sequences**: Share blocks via Arc, diverge only when modified
+
+**Benefit:** Hot path performance - state cloning costs ~50 bytes instead of O(n) vector copy.
+
+## Error Handling
+
+All state transitions are wrapped with backup/restore:
+
+1. Remove state from HashMap
+2. Clone state as backup
+3. Apply event transformation
+4. On success: insert new state
+5. On failure: restore backup, log warning
+
+**Prevents:**
+- Sequence loss on transition failure
+- Block leaks (blocks remain owned by restored state)
+- Duplicate sequences (check before insert)
+
+## Type Safety
+
+**Runtime checks:**
+- Invalid transitions caught by pattern matching
+- Return `Error::InvalidTransition` with clear message
+
+**Resource ownership:**
+- States own their blocks via Arc
+- Type system prevents accessing freed blocks
+- Drop trait ensures cleanup
+
+## Benefits
+
+1. **Type Safety** - States enforce resource invariants, invalid transitions caught explicitly
+2. **Clear Ownership** - Resources belong to states, automatic cleanup on drop
+3. **Testability** - Events are pure functions, test state transitions in isolation
+4. **Explicitness** - Every transition is explicit, state diagram maps to code
+5. **Performance** - Arc enables O(1) cloning, copy-on-write for efficiency
+6. **Reliability** - Backup/restore pattern prevents data loss on errors
+
+## Two-Level Event Architecture
+
+PUMA uses a two-level event system (inspired by TokenSpeed):
+
+**Level 1: FSM Events (Internal)** - `src/fsm/events.rs`
+- Created by the Scheduler internally
+- Include scheduler context (tokens_per_block, etc.)
+- Applied via `SequenceManager::advance(seq_id, event)`
+- Examples: `Event::Schedule`, `Event::AppendTokens`
+
+**Level 2: Scheduler Events (External)** - `src/scheduler/events.rs`
+- Created by external components (clients, GPU workers)
+- Simple data, no resource pointers
+- LLMEngine translates these to FSM events
+- Examples: `SchedulerEvent::AddRequest`, `SchedulerEvent::CancelRequest`
+
+**Why two levels?** External components cannot create FSM events because:
+- They don't own `BlockManager`
+- They don't know scheduler policies (tokens_per_block)
+- FSM events require scheduler context
+
+## Key Files
+
+- `src/fsm/states.rs` - State definitions and helper methods
+- `src/fsm/events.rs` - FSM event enum (internal transitions)
+- `src/sequence_manager/mod.rs` - Owns memory + FSM transition logic (`advance()`)
+- `src/scheduler/core.rs` - Scheduling policy that drives the SequenceManager
+- `src/scheduler/events.rs` - External scheduler events
+- `src/backend/llm_engine.rs` - Event coordinator
+
+## Usage Example
+
+```rust
+// External component sends a high-level event
+scheduler_tx.send(SchedulerEvent::AddRequest {
+ seq_id: SequenceId(1),
+ token_ids: vec![/* tokenized prompt */],
+ max_tokens: 100,
+ response_tx, // channel the result is delivered on
+})?;
+
+// The Scheduler translates it to an FSM event and drives the SequenceManager,
+// which owns BlockManager and performs the transition.
+impl Scheduler {
+ fn schedule_prefill(&mut self) {
+ while let Some(seq_id) = self.waiting_queue.pop_front() {
+ // Build the FSM event with scheduler context
+ let event = Event::Schedule {
+ tokens_per_block: self.tokens_per_block,
+ };
+
+ match self.sequences.advance(seq_id, event) {
+ Ok(_) => self.prefill_batch.push(seq_id),
+ Err(e) => { /* apply OOM / queueing policy */ }
+ }
+ }
+ }
+}
+```
diff --git a/src/api/chat.rs b/src/api/chat.rs
index 0dfcf4f..c87f1f3 100644
--- a/src/api/chat.rs
+++ b/src/api/chat.rs
@@ -7,7 +7,6 @@ use axum::{
Json,
};
use futures::stream::StreamExt;
-use std::sync::Arc;
use tokio_stream::wrappers::ReceiverStream;
use uuid::Uuid;
@@ -16,11 +15,11 @@ use crate::api::types::{
ChatChoice, ChatChoiceDelta, ChatCompletionChunk, ChatCompletionRequest,
ChatCompletionResponse, ChatMessage, ChatMessageDelta, ErrorResponse, Usage,
};
-use crate::backend::InferenceEngine;
+use crate::backend::EngineHandle;
/// Main handler for chat completions
-pub async fn chat_completions(
- State(state): State>,
+pub async fn chat_completions(
+ State(state): State,
Json(req): Json,
) -> Response {
let engine = state.engine;
@@ -83,8 +82,8 @@ pub async fn chat_completions(
}
/// Non-streaming chat completion
-async fn chat_completions_non_stream(
- engine: Arc,
+async fn chat_completions_non_stream(
+ engine: EngineHandle,
req: ChatCompletionRequest,
) -> Result> {
let id = format!("chatcmpl-{}", Uuid::new_v4());
@@ -125,8 +124,8 @@ async fn chat_completions_non_stream(
}
/// Streaming chat completion
-async fn chat_completions_stream(
- engine: Arc,
+async fn chat_completions_stream(
+ engine: EngineHandle,
req: ChatCompletionRequest,
) -> Sse>> {
let id = format!("chatcmpl-{}", Uuid::new_v4());
@@ -236,19 +235,11 @@ async fn chat_completions_stream(
Sse::new(ReceiverStream::new(rx)).keep_alive(KeepAlive::default())
}
-/// Format chat messages into a prompt
+/// Format chat messages into a prompt via the shared prompt formatter.
fn format_chat_messages(messages: &[ChatMessage]) -> String {
- messages
- .iter()
- .map(|m| {
- if m.role == "system" {
- format!("System: {}", m.content)
- } else if m.role == "user" {
- format!("User: {}", m.content)
- } else {
- format!("Assistant: {}", m.content)
- }
- })
- .collect::>()
- .join("\n")
+ crate::utils::prompt::format_conversation(
+ messages
+ .iter()
+ .map(|m| (m.role.as_str(), m.content.as_str())),
+ )
}
diff --git a/src/api/completions.rs b/src/api/completions.rs
index 497736b..c9bedf8 100644
--- a/src/api/completions.rs
+++ b/src/api/completions.rs
@@ -5,11 +5,10 @@ use crate::api::routes::AppState;
use crate::api::types::{
CompletionChoice, CompletionRequest, CompletionResponse, ErrorResponse, Usage,
};
-use crate::backend::InferenceEngine;
/// Handler for legacy text completions
-pub async fn completions(
- State(state): State>,
+pub async fn completions(
+ State(state): State,
Json(req): Json,
) -> impl IntoResponse {
let engine = state.engine;
diff --git a/src/api/models.rs b/src/api/models.rs
index c8304e1..00b3be6 100644
--- a/src/api/models.rs
+++ b/src/api/models.rs
@@ -6,12 +6,9 @@ use axum::{
use crate::api::routes::AppState;
use crate::api::types::{ErrorResponse, Model, ModelList};
-use crate::backend::InferenceEngine;
/// List all available models
-pub async fn list_models(
- State(state): State>,
-) -> impl IntoResponse {
+pub async fn list_models(State(state): State) -> impl IntoResponse {
let registry = state.registry;
match registry.load_models(None) {
Ok(models) => {
@@ -43,8 +40,8 @@ pub async fn list_models(
}
/// Get a specific model by ID
-pub async fn get_model(
- State(state): State>,
+pub async fn get_model(
+ State(state): State,
Path(model_id): Path,
) -> impl IntoResponse {
let registry = state.registry;
diff --git a/src/api/routes.rs b/src/api/routes.rs
index dae6c0e..15875be 100644
--- a/src/api/routes.rs
+++ b/src/api/routes.rs
@@ -10,33 +10,30 @@ use tower_http::{
LatencyUnit,
};
-use crate::backend::InferenceEngine;
+use crate::backend::EngineHandle;
use crate::registry::model_registry::ModelRegistry;
use super::{chat, completions, models};
/// Shared application state
#[derive(Clone)]
-pub struct AppState {
- pub engine: Arc,
+pub struct AppState {
+ pub engine: EngineHandle,
pub registry: Arc,
}
/// Create the API router with all endpoints
-pub fn create_router(
- engine: Arc,
- registry: Arc,
-) -> Router {
+pub fn create_router(engine: EngineHandle, registry: Arc) -> Router {
let state = AppState { engine, registry };
Router::new()
// Chat completions (most important)
- .route("/v1/chat/completions", post(chat::chat_completions::))
+ .route("/v1/chat/completions", post(chat::chat_completions))
// Legacy completions
- .route("/v1/completions", post(completions::completions::))
+ .route("/v1/completions", post(completions::completions))
// Models
- .route("/v1/models", get(models::list_models::))
- .route("/v1/models/:model", get(models::get_model::))
+ .route("/v1/models", get(models::list_models))
+ .route("/v1/models/:model", get(models::get_model))
// Health check
.route("/health", get(health_check))
// Pass state
diff --git a/src/api/tests.rs b/src/api/tests.rs
index fba05ca..fd8939e 100644
--- a/src/api/tests.rs
+++ b/src/api/tests.rs
@@ -13,13 +13,21 @@ use tempfile::TempDir;
use tower::util::ServiceExt; // for `oneshot` and `ready`
use super::routes::create_router;
+use crate::backend::engine;
use crate::backend::mock::MockEngine;
use crate::registry::model_registry::{CacheInfo, ModelInfo, ModelMetadata, ModelRegistry};
/// Helper to create test app with a pre-registered test model
/// Returns the router and the temp directory (which must be kept alive)
fn create_test_app() -> (axum::Router, TempDir) {
- let engine = Arc::new(MockEngine::new());
+ // Build the engine and spawn its runner; the handle drives the router
+ let (handle, runner) = engine(
+ MockEngine::new(),
+ create_test_tokenizer(),
+ "test-model".to_string(),
+ );
+ tokio::spawn(runner.serve());
+
let temp_dir = TempDir::new().unwrap();
let registry = Arc::new(ModelRegistry::new(Some(temp_dir.path().to_path_buf())));
@@ -49,7 +57,13 @@ fn create_test_app() -> (axum::Router, TempDir) {
.register_model(test_model)
.expect("failed to register test model");
- (create_router(engine, registry), temp_dir)
+ (create_router(handle, registry), temp_dir)
+}
+
+/// Simple BPE tokenizer for tests
+fn create_test_tokenizer() -> tokenizers::Tokenizer {
+ use tokenizers::models::bpe::BPE;
+ tokenizers::Tokenizer::new(BPE::default())
}
/// Helper to make a JSON request
diff --git a/src/backend/engine.rs b/src/backend/engine.rs
index 2d464a5..a3bb6f2 100644
--- a/src/backend/engine.rs
+++ b/src/backend/engine.rs
@@ -1,34 +1,30 @@
+use crate::block_manager::types::TokenId;
use std::io;
use std::pin::Pin;
use tokio_stream::Stream;
-/// Inference engine trait
-pub trait InferenceEngine: Send + Sync {
- /// Generate text completion
+/// Backend trait - low-level inference that works with token IDs
+///
+/// LLMEngine handles tokenization (text → tokens)
+/// Backend handles inference (tokens → tokens)
+pub trait Backend: Send + Sync {
+ /// Generate tokens from input token_ids
+ /// Returns generated token IDs
fn generate(
&self,
- model: &str,
- prompt: &str,
+ token_ids: Vec,
max_tokens: usize,
temperature: f32,
- ) -> impl std::future::Future