From 95575bfd935a4033d32019eea6e54c85a09a1ecc Mon Sep 17 00:00:00 2001 From: kerthcet Date: Sun, 26 Apr 2026 11:53:19 +0100 Subject: [PATCH 01/12] add server support Signed-off-by: kerthcet --- Cargo.lock | 389 ++++++++++++++++++++- Cargo.toml | 10 + README.md | 126 ++++++- hack/README.md | 68 ++++ hack/scripts/test_api.sh | 53 +++ src/api/chat.rs | 254 ++++++++++++++ src/api/completions.rs | 101 ++++++ src/api/mod.rs | 7 + src/api/models.rs | 80 +++++ src/api/routes.rs | 57 +++ src/api/types/mod.rs | 5 + src/api/types/request.rs | 87 +++++ src/api/types/response.rs | 119 +++++++ src/backend/engine.rs | 34 ++ src/backend/mock.rs | 74 ++++ src/backend/mod.rs | 4 + src/cli/commands.rs | 20 ++ src/cli/mod.rs | 1 + src/cli/serve.rs | 48 +++ src/lib.rs | 10 +- src/main.rs | 2 + src/storage/storage_trait.rs | 2 +- tests/api_test.rs | 333 ++++++++++++++++++ tests/{integration_test.rs => cli_test.rs} | 5 + 24 files changed, 1883 insertions(+), 6 deletions(-) create mode 100644 hack/README.md create mode 100755 hack/scripts/test_api.sh create mode 100644 src/api/chat.rs create mode 100644 src/api/completions.rs create mode 100644 src/api/mod.rs create mode 100644 src/api/models.rs create mode 100644 src/api/routes.rs create mode 100644 src/api/types/mod.rs create mode 100644 src/api/types/request.rs create mode 100644 src/api/types/response.rs create mode 100644 src/backend/engine.rs create mode 100644 src/backend/mock.rs create mode 100644 src/backend/mod.rs create mode 100644 src/cli/serve.rs create mode 100644 tests/api_test.rs rename tests/{integration_test.rs => cli_test.rs} (97%) diff --git a/Cargo.lock b/Cargo.lock index a63ea2f..8fe1ab6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,6 +97,23 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -109,6 +126,61 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -531,6 +603,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -667,6 +745,19 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + [[package]] name = "gimli" version = "0.31.1" @@ -706,6 +797,9 @@ name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "foldhash", +] [[package]] name = "hashlink" @@ -798,6 +892,12 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "humantime" version = "2.1.0" @@ -817,6 +917,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -1018,6 +1119,12 @@ dependencies = [ "syn", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "idna" version = "1.0.3" @@ -1047,6 +1154,7 @@ checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", "hashbrown 0.15.2", + "serde", ] [[package]] @@ -1107,6 +1215,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.170" @@ -1168,6 +1282,12 @@ version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "memchr" version = "2.7.4" @@ -1355,6 +1475,26 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1394,6 +1534,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "prettytable-rs" version = "0.10.0" @@ -1421,11 +1571,13 @@ dependencies = [ name = "puma" version = "0.0.2" dependencies = [ + "axum", "chrono", "clap", "colored", "dirs", "env_logger", + "futures", "hf-hub", "indicatif", "log", @@ -1440,6 +1592,10 @@ dependencies = [ "sysinfo", "tempfile", "tokio", + "tokio-stream", + "tower 0.4.13", + "tower-http", + "uuid", ] [[package]] @@ -1451,6 +1607,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.9.4" @@ -1596,7 +1758,7 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-util", - "tower", + "tower 0.5.2", "tower-service", "url", "wasm-bindgen", @@ -1754,6 +1916,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.228" @@ -1796,6 +1964,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2104,6 +2283,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.13" @@ -2117,6 +2307,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.5.2" @@ -2130,6 +2335,23 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", ] [[package]] @@ -2150,6 +2372,7 @@ version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ + "log", "pin-project-lite", "tracing-core", ] @@ -2187,6 +2410,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unit-prefix" version = "0.5.2" @@ -2270,6 +2499,18 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -2306,6 +2547,24 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -2377,6 +2636,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -2390,6 +2671,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.2", + "indexmap", + "semver", +] + [[package]] name = "web-sys" version = "0.3.77" @@ -2683,6 +2976,32 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + [[package]] name = "wit-bindgen-rt" version = "0.33.0" @@ -2692,6 +3011,74 @@ dependencies = [ "bitflags", ] +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "write16" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index e00701d..547ee4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,5 +26,15 @@ rusqlite = { version = "0.32", features = ["bundled"] } rusqlite_migration = "1.3" regex = "1.11" +# Web server +axum = "0.7" +tower = "0.4" +tower-http = { version = "0.5", features = ["cors"] } +uuid = { version = "1.0", features = ["v4", "serde"] } +futures = "0.3" +tokio-stream = "0.1" + [dev-dependencies] tempfile = "3.12" +tower = { version = "0.4", features = ["util"] } +serde_json = "1.0" diff --git a/README.md b/README.md index 2ec2821..a86af67 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ 💻 **System Detection** - Automatic GPU detection and resource reporting +🚀 **OpenAI-Compatible API** - RESTful API with streaming support + ## Installation ### Install with Cargo @@ -45,6 +47,8 @@ make build ## Quick Start +### CLI Usage + ```bash # Download a model puma pull inftyai/tiny-random-gpt2 @@ -62,6 +66,38 @@ puma info puma rm inftyai/tiny-random-gpt2 ``` +### API Server + +```bash +# Start the inference server +puma serve + +# Server will start on http://0.0.0.0:8000 +# API endpoints: +# POST /v1/chat/completions +# POST /v1/completions +# GET /v1/models +# GET /health +``` + +**Test the API:** + +```bash +# Health check +curl http://localhost:8000/health + +# Chat completion +curl http://localhost:8000/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "inftyai/tiny-random-gpt2", + "messages": [{"role": "user", "content": "Hello!"}] + }' + +# Or use the test script +./hack/scripts/test_api.sh +``` + ## Commands | Command | Status | Description | @@ -72,6 +108,7 @@ puma rm inftyai/tiny-random-gpt2 | `rm ` | ✅ | Remove model and cache | | `info` | ✅ | Display system information | | `version` | ✅ | Show PUMA version | +| `serve` | ✅ | Start OpenAI-compatible API server | | `ps` | 🚧 | List running models | | `run` | 🚧 | Start model inference | | `stop` | 🚧 | Stop running model | @@ -106,6 +143,81 @@ puma ls llama -l author=meta **Available filters:** `author`, `task`, `license`, `provider`, `model_series` +## API Server + +PUMA provides an OpenAI-compatible API server for model inference. + +### Starting the Server + +```bash +# Default: 0.0.0.0:8000 +puma serve + +# Custom host and port +puma serve --host 127.0.0.1 --port 3000 +``` + +### API Endpoints + +#### Chat Completions (Recommended) +```bash +curl http://localhost:8000/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "inftyai/tiny-random-gpt2", + "messages": [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Hello!"} + ], + "max_tokens": 100, + "temperature": 0.7 + }' +``` + +#### Streaming (Server-Sent Events) +```bash +curl http://localhost:8000/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "inftyai/tiny-random-gpt2", + "messages": [{"role": "user", "content": "Tell me a story"}], + "stream": true + }' +``` + +#### List Models +```bash +curl http://localhost:8000/v1/models +``` + +#### Health Check +```bash +curl http://localhost:8000/health +# Returns: {"status":"ok","version":"0.0.2"} +``` + +### OpenAI Python Client + +PUMA is compatible with the OpenAI Python SDK: + +```python +from openai import OpenAI + +client = OpenAI( + base_url="http://localhost:8000/v1", + api_key="dummy" # Not required +) + +response = client.chat.completions.create( + model="inftyai/tiny-random-gpt2", + messages=[ + {"role": "user", "content": "Hello!"} + ] +) + +print(response.choices[0].message.content) +``` + ### Inspect Output ```bash @@ -146,8 +258,15 @@ Models are stored with lowercase names for case-insensitive matching. # Build make build -# Run tests (67 unit + 22 integration) +# Run all tests (152 tests: 67 unit + 36 integration + 49 lib) make test + +# Run specific test suites +cargo test --test api_test # API integration tests +cargo test --test cli_test # CLI integration tests + +# Test API manually +./hack/scripts/test_api.sh ``` ### Project Structure @@ -155,13 +274,16 @@ make test ``` puma/ ├── src/ -│ ├── cli/ # Command implementations (ls, rm, inspect) +│ ├── api/ # OpenAI-compatible API +│ ├── backend/ # Inference backends (Mock, MLX) +│ ├── cli/ # Command implementations │ ├── downloader/ # HuggingFace download logic │ ├── registry/ # Model registry & metadata │ ├── storage/ # SQLite storage backend │ ├── system/ # System info detection │ └── utils/ # Formatting & helpers ├── tests/ # Integration tests +├── hack/ # Development scripts ├── Cargo.toml # Rust dependencies └── Makefile # Build commands ``` diff --git a/hack/README.md b/hack/README.md new file mode 100644 index 0000000..ca9895f --- /dev/null +++ b/hack/README.md @@ -0,0 +1,68 @@ +# Hack Directory + +Development and testing utilities for PUMA. + +## Structure + +``` +hack/ +└── scripts/ # Test and utility scripts + └── test_api.sh +``` + +## Scripts + +### `scripts/test_api.sh` + +Tests all PUMA API endpoints manually. + +**Usage:** +```bash +# Start PUMA server first +./puma serve + +# In another terminal +./hack/scripts/test_api.sh +``` + +**Tests:** +- Health check +- List models +- Chat completion (non-streaming) +- Chat completion (streaming) +- Text completion + +**Requirements:** +- Running PUMA server +- `curl` and `jq` installed + +--- + + +## Adding New Scripts + +Place development and testing scripts in `hack/scripts/`: + +```bash +# Create new script +cat > hack/scripts/my_script.sh << 'EOF' +#!/bin/bash +# Your script here +EOF + +# Make executable +chmod +x hack/scripts/my_script.sh +``` + +--- + +## Why "hack"? + +The `hack/` directory is a convention from Kubernetes and other projects for: +- Development utilities +- Test scripts +- Build helpers +- CI/CD scripts +- One-off tools + +It keeps the root directory clean while providing a place for development tools. diff --git a/hack/scripts/test_api.sh b/hack/scripts/test_api.sh new file mode 100755 index 0000000..8caf5d4 --- /dev/null +++ b/hack/scripts/test_api.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +echo "Testing PUMA OpenAI-Compatible API" +echo "====================================" +echo + +# Base URL +BASE_URL="http://localhost:8000" + +echo "1. Health Check" +curl -s "$BASE_URL/health" +echo -e "\n" + +echo "2. List Models" +curl -s "$BASE_URL/v1/models" | jq '.' +echo + +echo "3. Chat Completion (Non-streaming)" +curl -s "$BASE_URL/v1/chat/completions" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "test-model", + "messages": [ + {"role": "user", "content": "Hello!"} + ], + "max_tokens": 50 + }' | jq '.' +echo + +echo "4. Chat Completion (Streaming)" +curl -s -N "$BASE_URL/v1/chat/completions" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "test-model", + "messages": [ + {"role": "user", "content": "Tell me a story"} + ], + "stream": true, + "max_tokens": 50 + }' +echo -e "\n" + +echo "5. Legacy Text Completion" +curl -s "$BASE_URL/v1/completions" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "test-model", + "prompt": "Once upon a time", + "max_tokens": 50 + }' | jq '.' +echo + +echo "Done!" diff --git a/src/api/chat.rs b/src/api/chat.rs new file mode 100644 index 0000000..c2b36ba --- /dev/null +++ b/src/api/chat.rs @@ -0,0 +1,254 @@ +use axum::{ + extract::State, + response::{ + sse::{Event, KeepAlive, Sse}, + IntoResponse, Response, + }, + Json, +}; +use futures::stream::StreamExt; +use std::sync::Arc; +use tokio_stream::wrappers::ReceiverStream; +use uuid::Uuid; + +use crate::api::routes::AppState; +use crate::api::types::{ + ChatChoice, ChatChoiceDelta, ChatCompletionChunk, ChatCompletionRequest, + ChatCompletionResponse, ChatMessage, ChatMessageDelta, ErrorResponse, Usage, +}; +use crate::backend::InferenceEngine; + +/// Main handler for chat completions +pub async fn chat_completions( + State(state): State>, + Json(req): Json, +) -> Response { + let engine = state.engine; + let registry = state.registry; + + // Validate request + if req.messages.is_empty() { + return ( + axum::http::StatusCode::BAD_REQUEST, + Json(ErrorResponse::new( + "messages cannot be empty".to_string(), + "invalid_request_error".to_string(), + )), + ) + .into_response(); + } + + // Validate model exists + match registry.get_model(&req.model) { + Ok(None) => { + return ( + axum::http::StatusCode::NOT_FOUND, + Json(ErrorResponse::new( + format!("Model '{}' not found", req.model), + "model_not_found".to_string(), + )), + ) + .into_response(); + } + Err(e) => { + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse::new( + format!("Failed to check model: {}", e), + "internal_error".to_string(), + )), + ) + .into_response(); + } + Ok(Some(_)) => { + // Model exists, continue + } + } + + if req.stream { + chat_completions_stream(engine, req).await.into_response() + } else { + match chat_completions_non_stream(engine, req).await { + Ok(response) => Json(response).into_response(), + Err(err) => ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse::new( + err.to_string(), + "internal_error".to_string(), + )), + ) + .into_response(), + } + } +} + +/// Non-streaming chat completion +async fn chat_completions_non_stream( + engine: Arc, + req: ChatCompletionRequest, +) -> Result> { + let id = format!("chatcmpl-{}", Uuid::new_v4()); + let created = chrono::Utc::now().timestamp(); + + // Convert messages to prompt + let prompt = format_chat_messages(&req.messages); + + // Generate + let response = engine + .generate( + &req.model, + &prompt, + req.max_tokens.unwrap_or(100), + req.temperature.unwrap_or(0.7), + ) + .await?; + + Ok(ChatCompletionResponse { + id, + object: "chat.completion".to_string(), + created, + model: req.model, + choices: vec![ChatChoice { + index: 0, + message: ChatMessage { + role: "assistant".to_string(), + content: response.text, + }, + finish_reason: "stop".to_string(), + }], + usage: Usage { + prompt_tokens: response.prompt_tokens, + completion_tokens: response.completion_tokens, + total_tokens: response.prompt_tokens + response.completion_tokens, + }, + }) +} + +/// Streaming chat completion +async fn chat_completions_stream( + engine: Arc, + req: ChatCompletionRequest, +) -> Sse>> { + let id = format!("chatcmpl-{}", Uuid::new_v4()); + let created = chrono::Utc::now().timestamp(); + let model = req.model.clone(); + + let (tx, rx) = tokio::sync::mpsc::channel(100); + + // Spawn task to generate tokens + tokio::spawn(async move { + let prompt = format_chat_messages(&req.messages); + + // Send initial chunk with role + let initial_chunk = ChatCompletionChunk { + id: id.clone(), + object: "chat.completion.chunk".to_string(), + created, + model: model.clone(), + choices: vec![ChatChoiceDelta { + index: 0, + delta: ChatMessageDelta { + role: Some("assistant".to_string()), + content: None, + }, + finish_reason: None, + }], + }; + + if tx + .send(Ok( + Event::default().data(serde_json::to_string(&initial_chunk).unwrap()) + )) + .await + .is_err() + { + return; + } + + // Stream tokens + match engine + .generate_stream( + &model, + &prompt, + req.max_tokens.unwrap_or(100), + req.temperature.unwrap_or(0.7), + ) + .await + { + Ok(mut stream) => { + while let Some(token) = stream.next().await { + let chunk = ChatCompletionChunk { + id: id.clone(), + object: "chat.completion.chunk".to_string(), + created, + model: model.clone(), + choices: vec![ChatChoiceDelta { + index: 0, + delta: ChatMessageDelta { + role: None, + content: Some(token), + }, + finish_reason: None, + }], + }; + + if tx + .send(Ok( + Event::default().data(serde_json::to_string(&chunk).unwrap()) + )) + .await + .is_err() + { + break; + } + } + } + Err(e) => { + eprintln!("Error generating stream: {}", e); + return; + } + } + + // Send final chunk + let final_chunk = ChatCompletionChunk { + id: id.clone(), + object: "chat.completion.chunk".to_string(), + created, + model: model.clone(), + choices: vec![ChatChoiceDelta { + index: 0, + delta: ChatMessageDelta { + role: None, + content: None, + }, + finish_reason: Some("stop".to_string()), + }], + }; + + let _ = tx + .send(Ok( + Event::default().data(serde_json::to_string(&final_chunk).unwrap()) + )) + .await; + let _ = tx.send(Ok(Event::default().data("[DONE]"))).await; + }); + + Sse::new(ReceiverStream::new(rx)).keep_alive(KeepAlive::default()) +} + +/// Format chat messages into a prompt +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") +} diff --git a/src/api/completions.rs b/src/api/completions.rs new file mode 100644 index 0000000..addb075 --- /dev/null +++ b/src/api/completions.rs @@ -0,0 +1,101 @@ +use axum::{extract::State, response::IntoResponse, Json}; +use uuid::Uuid; + +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>, + Json(req): Json, +) -> impl IntoResponse { + let engine = state.engine; + let registry = state.registry; + + // Validate request + let prompt = req.prompt.to_string(); + if prompt.is_empty() { + return ( + axum::http::StatusCode::BAD_REQUEST, + Json(ErrorResponse::new( + "prompt cannot be empty".to_string(), + "invalid_request_error".to_string(), + )), + ) + .into_response(); + } + + // Validate model exists + match registry.get_model(&req.model) { + Ok(None) => { + return ( + axum::http::StatusCode::NOT_FOUND, + Json(ErrorResponse::new( + format!("Model '{}' not found", req.model), + "model_not_found".to_string(), + )), + ) + .into_response(); + } + Err(e) => { + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse::new( + format!("Failed to check model: {}", e), + "internal_error".to_string(), + )), + ) + .into_response(); + } + Ok(Some(_)) => { + // Model exists, continue + } + } + + let id = format!("cmpl-{}", Uuid::new_v4()); + let created = chrono::Utc::now().timestamp(); + + // Generate + match engine + .generate( + &req.model, + &prompt, + req.max_tokens.unwrap_or(100), + req.temperature.unwrap_or(0.7), + ) + .await + { + Ok(response) => { + let completion = CompletionResponse { + id, + object: "text_completion".to_string(), + created, + model: req.model, + choices: vec![CompletionChoice { + text: response.text, + index: 0, + logprobs: None, + finish_reason: "stop".to_string(), + }], + usage: Usage { + prompt_tokens: response.prompt_tokens, + completion_tokens: response.completion_tokens, + total_tokens: response.prompt_tokens + response.completion_tokens, + }, + }; + + Json(completion).into_response() + } + Err(e) => ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse::new( + e.to_string(), + "internal_error".to_string(), + )), + ) + .into_response(), + } +} diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..510f1a4 --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,7 @@ +pub mod chat; +pub mod completions; +pub mod models; +pub mod routes; +pub mod types; + +pub use routes::create_router; diff --git a/src/api/models.rs b/src/api/models.rs new file mode 100644 index 0000000..f790a7d --- /dev/null +++ b/src/api/models.rs @@ -0,0 +1,80 @@ +use axum::{ + extract::{Path, State}, + response::IntoResponse, + Json, +}; + +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 { + let registry = state.registry; + match registry.load_models(None) { + Ok(models) => { + let model_list = ModelList { + object: "list".to_string(), + data: models + .into_iter() + .map(|m| Model { + id: m.name.clone(), + object: "model".to_string(), + created: chrono::DateTime::parse_from_rfc3339(&m.created_at) + .map(|dt| dt.timestamp()) + .unwrap_or(0), + owned_by: m.author.unwrap_or_else(|| "puma".to_string()), + }) + .collect(), + }; + Json(model_list).into_response() + } + Err(e) => ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse::new( + format!("Failed to load models: {}", e), + "internal_error".to_string(), + )), + ) + .into_response(), + } +} + +/// Get a specific model by ID +pub async fn get_model( + State(state): State>, + Path(model_id): Path, +) -> impl IntoResponse { + let registry = state.registry; + match registry.get_model(&model_id) { + Ok(Some(model)) => { + let model_info = Model { + id: model.name.clone(), + object: "model".to_string(), + created: chrono::DateTime::parse_from_rfc3339(&model.created_at) + .map(|dt| dt.timestamp()) + .unwrap_or(0), + owned_by: model.author.unwrap_or_else(|| "puma".to_string()), + }; + Json(model_info).into_response() + } + Ok(None) => ( + axum::http::StatusCode::NOT_FOUND, + Json(ErrorResponse::new( + format!("Model '{}' not found", model_id), + "not_found_error".to_string(), + )), + ) + .into_response(), + Err(e) => ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse::new( + format!("Failed to get model: {}", e), + "internal_error".to_string(), + )), + ) + .into_response(), + } +} diff --git a/src/api/routes.rs b/src/api/routes.rs new file mode 100644 index 0000000..91d3832 --- /dev/null +++ b/src/api/routes.rs @@ -0,0 +1,57 @@ +use axum::{ + routing::{get, post}, + Json, Router, +}; +use serde::Serialize; +use std::sync::Arc; +use tower_http::cors::CorsLayer; + +use crate::backend::InferenceEngine; +use crate::registry::model_registry::ModelRegistry; + +use super::{chat, completions, models}; + +/// Shared application state +#[derive(Clone)] +pub struct AppState { + pub engine: Arc, + pub registry: Arc, +} + +/// Create the API router with all endpoints +pub fn create_router( + engine: Arc, + registry: Arc, +) -> Router { + let state = AppState { engine, registry }; + + Router::new() + // Chat completions (most important) + .route("/v1/chat/completions", post(chat::chat_completions::)) + // Legacy completions + .route("/v1/completions", post(completions::completions::)) + // Models + .route("/v1/models", get(models::list_models::)) + .route("/v1/models/:model", get(models::get_model::)) + // Health check + .route("/health", get(health_check)) + // Pass state + .with_state(state) + // Enable CORS for browser clients + .layer(CorsLayer::permissive()) +} + +/// Health check response +#[derive(Serialize)] +struct HealthResponse { + status: String, + version: String, +} + +/// Health check endpoint +async fn health_check() -> Json { + Json(HealthResponse { + status: "ok".to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + }) +} diff --git a/src/api/types/mod.rs b/src/api/types/mod.rs new file mode 100644 index 0000000..6415885 --- /dev/null +++ b/src/api/types/mod.rs @@ -0,0 +1,5 @@ +pub mod request; +pub mod response; + +pub use request::*; +pub use response::*; diff --git a/src/api/types/request.rs b/src/api/types/request.rs new file mode 100644 index 0000000..40fde9c --- /dev/null +++ b/src/api/types/request.rs @@ -0,0 +1,87 @@ +use serde::{Deserialize, Serialize}; + +/// Chat completion request (OpenAI compatible) +#[derive(Debug, Clone, Deserialize)] +pub struct ChatCompletionRequest { + pub model: String, + pub messages: Vec, + #[serde(default = "default_max_tokens")] + pub max_tokens: Option, + #[serde(default = "default_temperature")] + pub temperature: Option, + #[serde(default = "default_top_p")] + pub top_p: Option, + #[serde(default = "default_n")] + pub n: Option, + #[serde(default)] + pub stream: bool, + pub stop: Option>, + #[serde(default)] + pub presence_penalty: Option, + #[serde(default)] + pub frequency_penalty: Option, +} + +/// Chat message +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ChatMessage { + pub role: String, // "system", "user", "assistant" + pub content: String, +} + +/// Legacy text completion request +#[derive(Debug, Clone, Deserialize)] +pub struct CompletionRequest { + pub model: String, + #[serde(alias = "prompt")] + pub prompt: StringOrArray, + #[serde(default = "default_max_tokens")] + pub max_tokens: Option, + #[serde(default = "default_temperature")] + pub temperature: Option, + #[serde(default = "default_top_p")] + pub top_p: Option, + #[serde(default = "default_n")] + pub n: Option, + #[serde(default)] + pub stream: bool, + pub stop: Option>, + #[serde(default)] + pub presence_penalty: Option, + #[serde(default)] + pub frequency_penalty: Option, +} + +/// Prompt can be string or array of strings +#[derive(Debug, Clone, Deserialize)] +#[serde(untagged)] +pub enum StringOrArray { + String(String), + Array(Vec), +} + +impl StringOrArray { + pub fn to_string(&self) -> String { + match self { + StringOrArray::String(s) => s.clone(), + StringOrArray::Array(arr) => arr.join("\n"), + } + } +} + +// Default values +fn default_max_tokens() -> Option { + Some(100) +} + +fn default_temperature() -> Option { + Some(0.7) +} + +fn default_top_p() -> Option { + Some(1.0) +} + +fn default_n() -> Option { + Some(1) +} diff --git a/src/api/types/response.rs b/src/api/types/response.rs new file mode 100644 index 0000000..462ca03 --- /dev/null +++ b/src/api/types/response.rs @@ -0,0 +1,119 @@ +use serde::{Deserialize, Serialize}; + +use super::request::ChatMessage; + +/// Chat completion response +#[derive(Debug, Serialize)] +pub struct ChatCompletionResponse { + pub id: String, + pub object: String, // "chat.completion" + pub created: i64, + pub model: String, + pub choices: Vec, + pub usage: Usage, +} + +/// Choice in chat completion +#[derive(Debug, Serialize)] +pub struct ChatChoice { + pub index: usize, + pub message: ChatMessage, + pub finish_reason: String, // "stop", "length", "content_filter" +} + +/// Streaming chat completion chunk +#[derive(Debug, Serialize)] +pub struct ChatCompletionChunk { + pub id: String, + pub object: String, // "chat.completion.chunk" + pub created: i64, + pub model: String, + pub choices: Vec, +} + +/// Delta choice for streaming +#[derive(Debug, Serialize)] +pub struct ChatChoiceDelta { + pub index: usize, + pub delta: ChatMessageDelta, + #[serde(skip_serializing_if = "Option::is_none")] + pub finish_reason: Option, +} + +/// Delta message for streaming +#[derive(Debug, Serialize)] +pub struct ChatMessageDelta { + #[serde(skip_serializing_if = "Option::is_none")] + pub role: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub content: Option, +} + +/// Legacy completion response +#[derive(Debug, Serialize)] +pub struct CompletionResponse { + pub id: String, + pub object: String, // "text_completion" + pub created: i64, + pub model: String, + pub choices: Vec, + pub usage: Usage, +} + +/// Choice in completion +#[derive(Debug, Serialize)] +pub struct CompletionChoice { + pub text: String, + pub index: usize, + pub logprobs: Option, + pub finish_reason: String, +} + +/// Token usage statistics +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Usage { + pub prompt_tokens: usize, + pub completion_tokens: usize, + pub total_tokens: usize, +} + +/// Model list response +#[derive(Debug, Serialize)] +pub struct ModelList { + pub object: String, // "list" + pub data: Vec, +} + +/// Model information +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Model { + pub id: String, + pub object: String, // "model" + pub created: i64, + pub owned_by: String, +} + +/// Error response +#[derive(Debug, Serialize)] +pub struct ErrorResponse { + pub error: ErrorDetail, +} + +#[derive(Debug, Serialize)] +pub struct ErrorDetail { + pub message: String, + pub r#type: String, + pub code: Option, +} + +impl ErrorResponse { + pub fn new(message: String, error_type: String) -> Self { + Self { + error: ErrorDetail { + message, + r#type: error_type, + code: None, + }, + } + } +} diff --git a/src/backend/engine.rs b/src/backend/engine.rs new file mode 100644 index 0000000..2d464a5 --- /dev/null +++ b/src/backend/engine.rs @@ -0,0 +1,34 @@ +use std::io; +use std::pin::Pin; +use tokio_stream::Stream; + +/// Inference engine trait +pub trait InferenceEngine: Send + Sync { + /// Generate text completion + fn generate( + &self, + model: &str, + prompt: &str, + max_tokens: usize, + temperature: f32, + ) -> impl std::future::Future> + Send; + + /// Generate text with streaming + fn generate_stream( + &self, + model: &str, + prompt: &str, + max_tokens: usize, + temperature: f32, + ) -> impl std::future::Future< + Output = Result + Send>>, io::Error>, + > + Send; +} + +/// Generation response +#[derive(Debug, Clone)] +pub struct GenerateResponse { + pub text: String, + pub prompt_tokens: usize, + pub completion_tokens: usize, +} diff --git a/src/backend/mock.rs b/src/backend/mock.rs new file mode 100644 index 0000000..92f056e --- /dev/null +++ b/src/backend/mock.rs @@ -0,0 +1,74 @@ +use futures::stream::{self, StreamExt}; +use std::io; +use std::pin::Pin; +use tokio_stream::Stream; + +use super::engine::{GenerateResponse, InferenceEngine}; + +/// Mock engine for testing (replace with MLX later) +#[derive(Clone)] +pub struct MockEngine; + +impl MockEngine { + pub fn new() -> Self { + Self + } +} + +impl InferenceEngine for MockEngine { + async fn generate( + &self, + model: &str, + prompt: &str, + max_tokens: usize, + _temperature: f32, + ) -> Result { + // Mock response for testing + let response_text = format!( + "This is a mock response from model '{}' for prompt: '{}' (max_tokens: {})", + model, + prompt.chars().take(50).collect::(), + max_tokens + ); + + Ok(GenerateResponse { + text: response_text, + prompt_tokens: prompt.split_whitespace().count(), + completion_tokens: 20, + }) + } + + async fn generate_stream( + &self, + model: &str, + _prompt: &str, + max_tokens: usize, + _temperature: f32, + ) -> Result + Send>>, io::Error> { + // Mock streaming response + let tokens = vec![ + "This ".to_string(), + "is ".to_string(), + "a ".to_string(), + "mock ".to_string(), + "streaming ".to_string(), + "response ".to_string(), + format!("from model '{}' ", model), + format!("(max_tokens: {}).", max_tokens), + ]; + + // Simulate delay between tokens + let stream = stream::iter(tokens).then(|token| async move { + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + token + }); + + Ok(Box::pin(stream)) + } +} + +impl Default for MockEngine { + fn default() -> Self { + Self::new() + } +} diff --git a/src/backend/mod.rs b/src/backend/mod.rs new file mode 100644 index 0000000..1631447 --- /dev/null +++ b/src/backend/mod.rs @@ -0,0 +1,4 @@ +pub mod engine; +pub mod mock; + +pub use engine::*; diff --git a/src/cli/commands.rs b/src/cli/commands.rs index f07be0a..ae37255 100644 --- a/src/cli/commands.rs +++ b/src/cli/commands.rs @@ -37,6 +37,19 @@ enum Commands { INSPECT(InspectArgs), /// Returns the version of PUMA. VERSION, + /// Start the inference server + SERVE(ServeArgs), +} + +#[derive(Parser)] +struct ServeArgs { + /// Host address to bind to + #[arg(long, default_value = "0.0.0.0")] + host: String, + + /// Port to listen on + #[arg(short, long, default_value = "8000")] + port: u16, } #[derive(Parser)] @@ -204,6 +217,13 @@ pub async fn run(cli: Cli) { Commands::VERSION => { println!("PUMA {}", env!("CARGO_PKG_VERSION")); } + + Commands::SERVE(args) => { + if let Err(e) = crate::cli::serve::execute(&args.host, args.port).await { + eprintln!("Error starting server: {}", e); + std::process::exit(1); + } + } } } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 65f2452..fbfc92f 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -2,3 +2,4 @@ pub mod commands; pub mod inspect; pub mod ls; pub mod rm; +pub mod serve; diff --git a/src/cli/serve.rs b/src/cli/serve.rs new file mode 100644 index 0000000..e136228 --- /dev/null +++ b/src/cli/serve.rs @@ -0,0 +1,48 @@ +use colored::Colorize; +use std::sync::Arc; + +use crate::api::create_router; +use crate::backend::mock::MockEngine; +use crate::registry::model_registry::ModelRegistry; + +/// Execute the serve command +pub async fn execute(host: &str, port: u16) -> Result<(), Box> { + println!("{} Starting PUMA inference server...", "🚀".bright_green()); + + // Initialize backend (MockEngine for now, replace with MLX later) + let engine = Arc::new(MockEngine::new()); + println!("{} Inference engine initialized", "✓".green()); + + // Initialize model registry + let registry = Arc::new(ModelRegistry::new(None)); + println!("{} Model registry loaded", "✓".green()); + + // Create router + let app = create_router(engine, registry); + + // Bind address + let addr = format!("{}:{}", host, port); + let listener = tokio::net::TcpListener::bind(&addr).await?; + + println!(); + println!("{}", "PUMA Inference Server".bright_cyan().bold()); + println!("{}", "━━━━━━━━━━━━━━━━━━━━━━".bright_black()); + println!( + " Listening on: {}", + format!("http://{}", addr).bright_white().underline() + ); + println!(); + println!(" Endpoints:"); + println!(" {} /v1/chat/completions", "POST".bright_yellow()); + println!(" {} /v1/completions", "POST".bright_yellow()); + println!(" {} /v1/models", "GET ".bright_green()); + println!(" {} /health", "GET ".bright_green()); + println!(); + println!(" {}", "Press Ctrl+C to stop".bright_black()); + println!(); + + // Start server + axum::serve(listener, app).await?; + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index 4e70de3..127229a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,8 @@ -// lib.rs is intentionally minimal - puma is a binary-first application -// Internal modules are not exposed as public API +// lib.rs exposes modules for integration tests +// Internal modules are not intended for external use + +pub mod api; +pub mod backend; +pub mod registry; +pub mod storage; +pub mod utils; diff --git a/src/main.rs b/src/main.rs index f57d893..fab58c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +mod api; +mod backend; mod cli; mod downloader; mod registry; diff --git a/src/storage/storage_trait.rs b/src/storage/storage_trait.rs index 3968704..a39395b 100644 --- a/src/storage/storage_trait.rs +++ b/src/storage/storage_trait.rs @@ -4,7 +4,7 @@ use std::io; use std::collections::HashMap; /// Trait for model storage backends -pub trait ModelStorage { +pub trait ModelStorage: Send + Sync { /// Load models from storage with optional filtering by column values (e.g., author=InftyAI, license=mit) fn load_models( &self, diff --git a/tests/api_test.rs b/tests/api_test.rs new file mode 100644 index 0000000..5d57f76 --- /dev/null +++ b/tests/api_test.rs @@ -0,0 +1,333 @@ +//! API Integration Tests +//! +//! Tests the PUMA API endpoints using the Axum test utilities. +//! These tests verify the entire request/response cycle through the router. + +use axum::{ + body::Body, + http::{Request, StatusCode}, +}; +use serde_json::{json, Value}; +use std::sync::Arc; +use tower::ServiceExt; // for `oneshot` and `ready` + +use puma::api::create_router; +use puma::backend::mock::MockEngine; +use puma::registry::model_registry::{ArtifactInfo, ModelInfo, ModelMetadata, ModelRegistry}; + +/// Helper to create test app with a pre-registered test model +fn create_test_app() -> axum::Router { + let engine = Arc::new(MockEngine::new()); + let registry = Arc::new(ModelRegistry::new(None)); + + // Register a test model + let test_model = ModelInfo { + uuid: "test-uuid".to_string(), + name: "test-model".to_string(), + author: Some("test-author".to_string()), + task: Some("text-generation".to_string()), + model_series: Some("test-series".to_string()), + provider: "test".to_string(), + license: Some("MIT".to_string()), + created_at: chrono::Utc::now().to_rfc3339(), + updated_at: chrono::Utc::now().to_rfc3339(), + metadata: ModelMetadata { + artifact: ArtifactInfo { + revision: "test-rev".to_string(), + size: 1000, + path: "/tmp/test-model".to_string(), + }, + context_window: Some(2048), + safetensors: None, + }, + }; + + registry.register_model(test_model).ok(); + + create_router(engine, registry) +} + +/// Helper to make a JSON request +async fn make_json_request( + app: axum::Router, + method: &str, + uri: &str, + body: Option, +) -> (StatusCode, Value) { + let mut request = Request::builder().uri(uri).method(method); + + if body.is_some() { + request = request.header("content-type", "application/json"); + } + + let request = if let Some(body) = body { + request.body(Body::from(serde_json::to_vec(&body).unwrap())) + } else { + request.body(Body::empty()) + } + .unwrap(); + + let response = app.oneshot(request).await.unwrap(); + + let status = response.status(); + let body = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap(); + let json: Value = serde_json::from_slice(&body).unwrap_or(json!({})); + + (status, json) +} + +#[tokio::test] +async fn test_health_check() { + let app = create_test_app(); + let (status, json) = make_json_request(app, "GET", "/health", None).await; + + assert_eq!(status, StatusCode::OK); + assert_eq!(json["status"], "ok"); + assert!(json["version"].is_string()); +} + +#[tokio::test] +async fn test_list_models() { + let app = create_test_app(); + let (status, json) = make_json_request(app, "GET", "/v1/models", None).await; + + assert_eq!(status, StatusCode::OK); + assert_eq!(json["object"], "list"); + assert!(json["data"].is_array()); +} + +#[tokio::test] +async fn test_chat_completion_non_streaming() { + let app = create_test_app(); + let request_body = json!({ + "model": "test-model", + "messages": [ + {"role": "user", "content": "Hello"} + ], + "max_tokens": 50, + "stream": false + }); + + let (status, json) = + make_json_request(app, "POST", "/v1/chat/completions", Some(request_body)).await; + + assert_eq!(status, StatusCode::OK); + assert_eq!(json["object"], "chat.completion"); + assert!(json["id"].is_string()); + assert_eq!(json["model"], "test-model"); + assert!(json["choices"].is_array()); + assert_eq!(json["choices"][0]["index"], 0); + assert_eq!(json["choices"][0]["message"]["role"], "assistant"); + assert!(json["choices"][0]["message"]["content"].is_string()); + assert_eq!(json["choices"][0]["finish_reason"], "stop"); + assert!(json["usage"]["prompt_tokens"].is_number()); + assert!(json["usage"]["completion_tokens"].is_number()); + assert!(json["usage"]["total_tokens"].is_number()); +} + +#[tokio::test] +async fn test_chat_completion_empty_messages() { + let app = create_test_app(); + let request_body = json!({ + "model": "test-model", + "messages": [], + "stream": false + }); + + let (status, json) = + make_json_request(app, "POST", "/v1/chat/completions", Some(request_body)).await; + + assert_eq!(status, StatusCode::BAD_REQUEST); + assert_eq!(json["error"]["type"], "invalid_request_error"); + assert!(json["error"]["message"] + .as_str() + .unwrap() + .contains("messages cannot be empty")); +} + +#[tokio::test] +async fn test_text_completion() { + let app = create_test_app(); + let request_body = json!({ + "model": "test-model", + "prompt": "Once upon a time", + "max_tokens": 50 + }); + + let (status, json) = + make_json_request(app, "POST", "/v1/completions", Some(request_body)).await; + + assert_eq!(status, StatusCode::OK); + assert_eq!(json["object"], "text_completion"); + assert!(json["id"].is_string()); + assert_eq!(json["model"], "test-model"); + assert!(json["choices"].is_array()); + assert_eq!(json["choices"][0]["index"], 0); + assert!(json["choices"][0]["text"].is_string()); + assert_eq!(json["choices"][0]["finish_reason"], "stop"); + assert!(json["usage"]["prompt_tokens"].is_number()); +} + +#[tokio::test] +async fn test_text_completion_empty_prompt() { + let app = create_test_app(); + let request_body = json!({ + "model": "test-model", + "prompt": "" + }); + + let (status, json) = + make_json_request(app, "POST", "/v1/completions", Some(request_body)).await; + + assert_eq!(status, StatusCode::BAD_REQUEST); + assert_eq!(json["error"]["type"], "invalid_request_error"); + assert!(json["error"]["message"] + .as_str() + .unwrap() + .contains("prompt cannot be empty")); +} + +#[tokio::test] +async fn test_chat_completion_with_system_message() { + let app = create_test_app(); + let request_body = json!({ + "model": "test-model", + "messages": [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Hello"} + ], + "max_tokens": 50 + }); + + let (status, json) = + make_json_request(app, "POST", "/v1/chat/completions", Some(request_body)).await; + + assert_eq!(status, StatusCode::OK); + assert_eq!(json["object"], "chat.completion"); + assert!(json["choices"][0]["message"]["content"].is_string()); +} + +#[tokio::test] +async fn test_chat_completion_with_temperature() { + let app = create_test_app(); + let request_body = json!({ + "model": "test-model", + "messages": [ + {"role": "user", "content": "Hello"} + ], + "temperature": 0.5, + "max_tokens": 50 + }); + + let (status, json) = + make_json_request(app, "POST", "/v1/chat/completions", Some(request_body)).await; + + assert_eq!(status, StatusCode::OK); + assert_eq!(json["object"], "chat.completion"); +} + +#[tokio::test] +async fn test_chat_completion_default_values() { + let app = create_test_app(); + let request_body = json!({ + "model": "test-model", + "messages": [ + {"role": "user", "content": "Hello"} + ] + // No max_tokens, temperature, etc. - should use defaults + }); + + let (status, json) = + make_json_request(app, "POST", "/v1/chat/completions", Some(request_body)).await; + + assert_eq!(status, StatusCode::OK); + assert_eq!(json["object"], "chat.completion"); +} + +#[tokio::test] +async fn test_cors_headers() { + let app = create_test_app(); + let request = Request::builder() + .uri("/health") + .method("GET") + .header("Origin", "https://example.com") + .body(Body::empty()) + .unwrap(); + + let response = app.oneshot(request).await.unwrap(); + + // CORS should be permissive + assert!(response + .headers() + .contains_key("access-control-allow-origin")); +} + +#[tokio::test] +async fn test_invalid_route() { + let app = create_test_app(); + let request = Request::builder() + .uri("/invalid/route") + .method("GET") + .body(Body::empty()) + .unwrap(); + + let response = app.oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::NOT_FOUND); +} + +#[tokio::test] +async fn test_method_not_allowed() { + let app = create_test_app(); + // Try POST on GET-only endpoint + let request = Request::builder() + .uri("/health") + .method("POST") + .body(Body::empty()) + .unwrap(); + + let response = app.oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED); +} + +#[tokio::test] +async fn test_chat_completion_nonexistent_model() { + let app = create_test_app(); + let request_body = json!({ + "model": "nonexistent-model", + "messages": [ + {"role": "user", "content": "Hello"} + ], + "max_tokens": 50 + }); + + let (status, json) = + make_json_request(app, "POST", "/v1/chat/completions", Some(request_body)).await; + + assert_eq!(status, StatusCode::NOT_FOUND); + assert_eq!(json["error"]["type"], "model_not_found"); + assert!(json["error"]["message"] + .as_str() + .unwrap() + .contains("nonexistent-model")); +} + +#[tokio::test] +async fn test_text_completion_nonexistent_model() { + let app = create_test_app(); + let request_body = json!({ + "model": "nonexistent-model", + "prompt": "Hello world" + }); + + let (status, json) = + make_json_request(app, "POST", "/v1/completions", Some(request_body)).await; + + assert_eq!(status, StatusCode::NOT_FOUND); + assert_eq!(json["error"]["type"], "model_not_found"); + assert!(json["error"]["message"] + .as_str() + .unwrap() + .contains("nonexistent-model")); +} diff --git a/tests/integration_test.rs b/tests/cli_test.rs similarity index 97% rename from tests/integration_test.rs rename to tests/cli_test.rs index 83307f9..a2e1593 100644 --- a/tests/integration_test.rs +++ b/tests/cli_test.rs @@ -1,3 +1,8 @@ +//! CLI Integration Tests +//! +//! Tests the PUMA command-line interface by executing the binary +//! and verifying output. These tests use real processes and temporary directories. + use std::process::Command; use tempfile::TempDir; From d622695e8b0a0f8a077473f71ea77af547848ea2 Mon Sep 17 00:00:00 2001 From: kerthcet Date: Sun, 26 Apr 2026 12:27:32 +0100 Subject: [PATCH 02/12] Add logo to serve Signed-off-by: kerthcet --- LOGGING.md | 234 +++++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 +- src/cli/serve.rs | 46 ++++++---- src/main.rs | 8 +- 4 files changed, 266 insertions(+), 24 deletions(-) create mode 100644 LOGGING.md diff --git a/LOGGING.md b/LOGGING.md new file mode 100644 index 0000000..d8f04ce --- /dev/null +++ b/LOGGING.md @@ -0,0 +1,234 @@ +# Logging Guidelines for PUMA + +## Overview + +PUMA uses a hybrid approach for output: +- **User-facing output**: `println!` for CLI commands +- **Internal logging**: `log::*` macros for debugging and monitoring + +--- + +## When to Use Each + +### Use `println!` / `eprintln!` + +For **direct user communication** in CLI commands: + +✅ **Good Examples:** +```rust +// Success messages +println!("✓ Model downloaded successfully"); + +// Progress updates +println!("Downloading... 50%"); + +// Command output (ls, inspect, etc.) +println!("MODEL PROVIDER SIZE"); + +// User-facing startup banners +println!("🚀 Starting PUMA inference server..."); +``` + +❌ **Bad Examples:** +```rust +// Internal state (use log::debug instead) +println!("Initializing database connection"); + +// Error details (use log::error instead) +println!("Failed to parse config: {}", err); +``` + +--- + +### Use `log::*` Macros + +For **internal operations** and **debugging**: + +#### `log::error!` - Errors +```rust +log::error!("Failed to connect to database: {}", err); +log::error!("Model validation failed for: {}", model_name); +``` + +#### `log::warn!` - Warnings +```rust +log::warn!("Cache directory not found, creating new one"); +log::warn!("Model file corrupt, re-downloading"); +``` + +#### `log::info!` - Important events +```rust +log::info!("Server listening on http://{}", addr); +log::info!("Model registry loaded with {} models", count); +log::info!("Starting inference for model: {}", model); +``` + +#### `log::debug!` - Debug details +```rust +log::debug!("Using MockEngine backend"); +log::debug!("Cache hit for model: {}", model); +log::debug!("Request took {}ms", elapsed); +``` + +--- + +## Examples from Codebase + +### Good: `serve.rs` + +Hybrid approach - both user-facing and logging: + +```rust +pub async fn execute(host: &str, port: u16) -> Result<(), Box> { + // Logging for monitoring + info!("Starting PUMA inference server"); + + // User-facing output + println!("🚀 Starting PUMA inference server..."); + + let engine = Arc::new(MockEngine::new()); + + // Internal logging + info!("Inference engine initialized"); + debug!("Using MockEngine backend"); + + // User-facing confirmation + println!("✓ Inference engine initialized"); + + // ... server starts + + // Logging for monitoring + info!("Server listening on http://{}", addr); + + // User-facing banner + println!(" Listening on: http://{}", addr); +} +``` + +### Good: `downloader.rs` + +Internal operations use logging only: + +```rust +pub async fn download_model(&self, name: &str) -> Result<(), Error> { + debug!("Downloading model {} from Hugging Face...", name); + + // ... download logic + + debug!("Model info for {}: {:?}", name, model_info); +} +``` + +--- + +## Log Levels in Practice + +### Development +```bash +# See all logs +RUST_LOG=debug cargo run -- serve + +# See only warnings and errors +RUST_LOG=warn cargo run -- serve +``` + +### Production +```bash +# Default: info level +RUST_LOG=info ./puma serve + +# Quiet mode (errors only) +RUST_LOG=error ./puma serve +``` + +--- + +## Best Practices + +### ✅ DO + +1. **Use both** when appropriate: + ```rust + info!("Server starting on {}", addr); // For logs + println!("🚀 Starting server..."); // For users + ``` + +2. **Log structured data**: + ```rust + info!("Request completed: method={} path={} status={} duration={}ms", + method, path, status, duration); + ``` + +3. **Use appropriate levels**: + ```rust + error!("Critical failure"); // Something broke + warn!("Potential issue"); // Might be a problem + info!("Important event"); // Normal but notable + debug!("Internal detail"); // For debugging only + ``` + +### ❌ DON'T + +1. **Don't log sensitive data**: + ```rust + // Bad + debug!("API key: {}", api_key); + + // Good + debug!("API key provided: {}", !api_key.is_empty()); + ``` + +2. **Don't use println for internal operations**: + ```rust + // Bad + println!("Database connection established"); + + // Good + info!("Database connection established"); + ``` + +3. **Don't be too verbose**: + ```rust + // Bad - logs every iteration + for item in items { + debug!("Processing item: {}", item); + } + + // Good - log summary + debug!("Processing {} items", items.len()); + ``` + +--- + +## Testing Logs + +Use `env_logger` for tests: + +```rust +#[test] +fn test_something() { + let _ = env_logger::builder().is_test(true).try_init(); + + info!("Running test"); + // ... test code +} +``` + +Run with logs: +```bash +RUST_LOG=debug cargo test -- --nocapture +``` + +--- + +## Summary + +| Output Type | Use | Example | +|-------------|-----|---------| +| **User-facing** | `println!` | "✓ Model downloaded" | +| **Errors** | `log::error!` | "Failed to connect: {}" | +| **Warnings** | `log::warn!` | "Cache miss for model" | +| **Events** | `log::info!` | "Server started on :8000" | +| **Debug** | `log::debug!` | "Using MockEngine" | + +**Key principle**: If a human is directly waiting for the output → `println!`. If it's for monitoring/debugging → `log::*`. diff --git a/README.md b/README.md index a86af67..043c6d4 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ puma serve # Server will start on http://0.0.0.0:8000 # API endpoints: # POST /v1/chat/completions -# POST /v1/completions +# POST /v1/completions # GET /v1/models # GET /health ``` diff --git a/src/cli/serve.rs b/src/cli/serve.rs index e136228..68aaa40 100644 --- a/src/cli/serve.rs +++ b/src/cli/serve.rs @@ -1,4 +1,4 @@ -use colored::Colorize; +use log::{debug, info}; use std::sync::Arc; use crate::api::create_router; @@ -7,15 +7,28 @@ use crate::registry::model_registry::ModelRegistry; /// Execute the serve command pub async fn execute(host: &str, port: u16) -> Result<(), Box> { - println!("{} Starting PUMA inference server...", "🚀".bright_green()); + println!( + " + ███████████ █████ █████ ██████ ██████ █████████ +░░███░░░░░███░░███ ░░███ ░░██████ ██████ ███░░░░░███ + ░███ ░███ ░███ ░███ ░███░█████░███ ░███ ░███ + ░██████████ ░███ ░███ ░███░░███ ░███ ░███████████ + ░███░░░░░░ ░███ ░███ ░███ ░░░ ░███ ░███░░░░░███ + ░███ ░███ ░███ ░███ ░███ ░███ ░███ + █████ ░░████████ █████ █████ █████ █████ +░░░░░ ░░░░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ + " + ); + info!("Starting PUMA inference server"); // Initialize backend (MockEngine for now, replace with MLX later) let engine = Arc::new(MockEngine::new()); - println!("{} Inference engine initialized", "✓".green()); + info!("Inference engine initialized"); + debug!("Using MockEngine backend"); // Initialize model registry let registry = Arc::new(ModelRegistry::new(None)); - println!("{} Model registry loaded", "✓".green()); + info!("Model registry loaded"); // Create router let app = create_router(engine, registry); @@ -24,25 +37,18 @@ pub async fn execute(host: &str, port: u16) -> Result<(), Box Date: Sun, 26 Apr 2026 13:26:26 +0100 Subject: [PATCH 03/12] fix tests Signed-off-by: kerthcet --- README.md | 1 + src/api/mod.rs | 3 +++ tests/api_test.rs => src/api/tests.rs | 10 ++++++---- src/cli/serve.rs | 4 ++++ src/lib.rs | 14 +++++++------- 5 files changed, 21 insertions(+), 11 deletions(-) rename tests/api_test.rs => src/api/tests.rs (96%) diff --git a/README.md b/README.md index 043c6d4..58268da 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ puma serve # POST /v1/chat/completions # POST /v1/completions # GET /v1/models +# GET /v1/models/:model # GET /health ``` diff --git a/src/api/mod.rs b/src/api/mod.rs index 510f1a4..490e2d7 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -5,3 +5,6 @@ pub mod routes; pub mod types; pub use routes::create_router; + +#[cfg(test)] +mod tests; diff --git a/tests/api_test.rs b/src/api/tests.rs similarity index 96% rename from tests/api_test.rs rename to src/api/tests.rs index 5d57f76..783db06 100644 --- a/tests/api_test.rs +++ b/src/api/tests.rs @@ -9,16 +9,18 @@ use axum::{ }; use serde_json::{json, Value}; use std::sync::Arc; +use tempfile::TempDir; use tower::ServiceExt; // for `oneshot` and `ready` -use puma::api::create_router; -use puma::backend::mock::MockEngine; -use puma::registry::model_registry::{ArtifactInfo, ModelInfo, ModelMetadata, ModelRegistry}; +use crate::api::create_router; +use crate::backend::mock::MockEngine; +use crate::registry::model_registry::{ArtifactInfo, ModelInfo, ModelMetadata, ModelRegistry}; /// Helper to create test app with a pre-registered test model fn create_test_app() -> axum::Router { let engine = Arc::new(MockEngine::new()); - let registry = Arc::new(ModelRegistry::new(None)); + let temp_dir = TempDir::new().unwrap(); + let registry = Arc::new(ModelRegistry::new(Some(temp_dir.path().to_path_buf()))); // Register a test model let test_model = ModelInfo { diff --git a/src/cli/serve.rs b/src/cli/serve.rs index 68aaa40..444d841 100644 --- a/src/cli/serve.rs +++ b/src/cli/serve.rs @@ -1,3 +1,4 @@ +use colored::Colorize; use log::{debug, info}; use std::sync::Arc; @@ -8,6 +9,7 @@ use crate::registry::model_registry::ModelRegistry; /// Execute the serve command pub async fn execute(host: &str, port: u16) -> Result<(), Box> { println!( + "{}", " ███████████ █████ █████ ██████ ██████ █████████ ░░███░░░░░███░░███ ░░███ ░░██████ ██████ ███░░░░░███ @@ -18,6 +20,8 @@ pub async fn execute(host: &str, port: u16) -> Result<(), Box Date: Sun, 26 Apr 2026 13:26:41 +0100 Subject: [PATCH 04/12] fix lint Signed-off-by: kerthcet --- src/api/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 490e2d7..cd742f5 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -4,7 +4,6 @@ pub mod models; pub mod routes; pub mod types; -pub use routes::create_router; #[cfg(test)] mod tests; From e07923fcaafca77dde8e6b79cd537a8a2b26bbd2 Mon Sep 17 00:00:00 2001 From: kerthcet Date: Sun, 26 Apr 2026 14:01:28 +0100 Subject: [PATCH 05/12] fix tests Signed-off-by: kerthcet --- src/api/mod.rs | 1 - src/api/models.rs | 2 +- src/api/tests.rs | 49 ++++++++++++++++++++++++++++++----------------- src/cli/serve.rs | 2 +- src/lib.rs | 2 ++ 5 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index cd742f5..5de16ff 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -4,6 +4,5 @@ pub mod models; pub mod routes; pub mod types; - #[cfg(test)] mod tests; diff --git a/src/api/models.rs b/src/api/models.rs index f790a7d..c8304e1 100644 --- a/src/api/models.rs +++ b/src/api/models.rs @@ -64,7 +64,7 @@ pub async fn get_model( axum::http::StatusCode::NOT_FOUND, Json(ErrorResponse::new( format!("Model '{}' not found", model_id), - "not_found_error".to_string(), + "model_not_found".to_string(), )), ) .into_response(), diff --git a/src/api/tests.rs b/src/api/tests.rs index 783db06..463ab52 100644 --- a/src/api/tests.rs +++ b/src/api/tests.rs @@ -12,12 +12,13 @@ use std::sync::Arc; use tempfile::TempDir; use tower::ServiceExt; // for `oneshot` and `ready` -use crate::api::create_router; +use super::routes::create_router; use crate::backend::mock::MockEngine; use crate::registry::model_registry::{ArtifactInfo, ModelInfo, ModelMetadata, ModelRegistry}; /// Helper to create test app with a pre-registered test model -fn create_test_app() -> axum::Router { +/// 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()); let temp_dir = TempDir::new().unwrap(); let registry = Arc::new(ModelRegistry::new(Some(temp_dir.path().to_path_buf()))); @@ -44,9 +45,11 @@ fn create_test_app() -> axum::Router { }, }; - registry.register_model(test_model).ok(); + registry + .register_model(test_model) + .expect("failed to register test model"); - create_router(engine, registry) + (create_router(engine, registry), temp_dir) } /// Helper to make a JSON request @@ -77,12 +80,22 @@ async fn make_json_request( .unwrap(); let json: Value = serde_json::from_slice(&body).unwrap_or(json!({})); + // Debug output for failed requests + if !status.is_success() { + eprintln!("Request failed: {} {}", method, uri); + eprintln!("Status: {}", status); + eprintln!( + "Response: {}", + serde_json::to_string_pretty(&json).unwrap_or_default() + ); + } + (status, json) } #[tokio::test] async fn test_health_check() { - let app = create_test_app(); + let (app, _temp_dir) = create_test_app(); let (status, json) = make_json_request(app, "GET", "/health", None).await; assert_eq!(status, StatusCode::OK); @@ -92,7 +105,7 @@ async fn test_health_check() { #[tokio::test] async fn test_list_models() { - let app = create_test_app(); + let (app, _temp_dir) = create_test_app(); let (status, json) = make_json_request(app, "GET", "/v1/models", None).await; assert_eq!(status, StatusCode::OK); @@ -102,7 +115,7 @@ async fn test_list_models() { #[tokio::test] async fn test_chat_completion_non_streaming() { - let app = create_test_app(); + let (app, _temp_dir) = create_test_app(); let request_body = json!({ "model": "test-model", "messages": [ @@ -131,7 +144,7 @@ async fn test_chat_completion_non_streaming() { #[tokio::test] async fn test_chat_completion_empty_messages() { - let app = create_test_app(); + let (app, _temp_dir) = create_test_app(); let request_body = json!({ "model": "test-model", "messages": [], @@ -151,7 +164,7 @@ async fn test_chat_completion_empty_messages() { #[tokio::test] async fn test_text_completion() { - let app = create_test_app(); + let (app, _temp_dir) = create_test_app(); let request_body = json!({ "model": "test-model", "prompt": "Once upon a time", @@ -174,7 +187,7 @@ async fn test_text_completion() { #[tokio::test] async fn test_text_completion_empty_prompt() { - let app = create_test_app(); + let (app, _temp_dir) = create_test_app(); let request_body = json!({ "model": "test-model", "prompt": "" @@ -193,7 +206,7 @@ async fn test_text_completion_empty_prompt() { #[tokio::test] async fn test_chat_completion_with_system_message() { - let app = create_test_app(); + let (app, _temp_dir) = create_test_app(); let request_body = json!({ "model": "test-model", "messages": [ @@ -213,7 +226,7 @@ async fn test_chat_completion_with_system_message() { #[tokio::test] async fn test_chat_completion_with_temperature() { - let app = create_test_app(); + let (app, _temp_dir) = create_test_app(); let request_body = json!({ "model": "test-model", "messages": [ @@ -232,7 +245,7 @@ async fn test_chat_completion_with_temperature() { #[tokio::test] async fn test_chat_completion_default_values() { - let app = create_test_app(); + let (app, _temp_dir) = create_test_app(); let request_body = json!({ "model": "test-model", "messages": [ @@ -250,7 +263,7 @@ async fn test_chat_completion_default_values() { #[tokio::test] async fn test_cors_headers() { - let app = create_test_app(); + let (app, _temp_dir) = create_test_app(); let request = Request::builder() .uri("/health") .method("GET") @@ -268,7 +281,7 @@ async fn test_cors_headers() { #[tokio::test] async fn test_invalid_route() { - let app = create_test_app(); + let (app, _temp_dir) = create_test_app(); let request = Request::builder() .uri("/invalid/route") .method("GET") @@ -281,7 +294,7 @@ async fn test_invalid_route() { #[tokio::test] async fn test_method_not_allowed() { - let app = create_test_app(); + let (app, _temp_dir) = create_test_app(); // Try POST on GET-only endpoint let request = Request::builder() .uri("/health") @@ -295,7 +308,7 @@ async fn test_method_not_allowed() { #[tokio::test] async fn test_chat_completion_nonexistent_model() { - let app = create_test_app(); + let (app, _temp_dir) = create_test_app(); let request_body = json!({ "model": "nonexistent-model", "messages": [ @@ -317,7 +330,7 @@ async fn test_chat_completion_nonexistent_model() { #[tokio::test] async fn test_text_completion_nonexistent_model() { - let app = create_test_app(); + let (app, _temp_dir) = create_test_app(); let request_body = json!({ "model": "nonexistent-model", "prompt": "Hello world" diff --git a/src/cli/serve.rs b/src/cli/serve.rs index 444d841..309e00f 100644 --- a/src/cli/serve.rs +++ b/src/cli/serve.rs @@ -2,7 +2,7 @@ use colored::Colorize; use log::{debug, info}; use std::sync::Arc; -use crate::api::create_router; +use crate::api::routes::create_router; use crate::backend::mock::MockEngine; use crate::registry::model_registry::ModelRegistry; diff --git a/src/lib.rs b/src/lib.rs index 042a2d1..8cacc2c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,8 @@ // lib.rs - internal modules for the PUMA binary // Modules are private as PUMA is primarily a CLI tool, not a library +#![allow(dead_code)] + mod api; mod backend; mod registry; From a825f3bcc2dc15d8191c8a3c316f0edb78ee671e Mon Sep 17 00:00:00 2001 From: kerthcet Date: Sun, 26 Apr 2026 14:08:57 +0100 Subject: [PATCH 06/12] fix tests Signed-off-by: kerthcet --- README.md | 2 +- src/api/chat.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 58268da..017dd72 100644 --- a/README.md +++ b/README.md @@ -263,7 +263,7 @@ make build make test # Run specific test suites -cargo test --test api_test # API integration tests +cargo test --test api:: # API integration tests cargo test --test cli_test # CLI integration tests # Test API manually diff --git a/src/api/chat.rs b/src/api/chat.rs index c2b36ba..12953b1 100644 --- a/src/api/chat.rs +++ b/src/api/chat.rs @@ -204,7 +204,7 @@ async fn chat_completions_stream( } } Err(e) => { - eprintln!("Error generating stream: {}", e); + log::error!("Error generating stream: {}", e); return; } } From ab81570d103f569d856af3b870ff779780b34e86 Mon Sep 17 00:00:00 2001 From: kerthcet Date: Sun, 26 Apr 2026 14:19:20 +0100 Subject: [PATCH 07/12] fix test Signed-off-by: kerthcet --- src/api/tests.rs | 70 ++++++++++++++++++++++++++++++++++++++++ src/api/types/request.rs | 24 +++++--------- 2 files changed, 78 insertions(+), 16 deletions(-) diff --git a/src/api/tests.rs b/src/api/tests.rs index 463ab52..a64303b 100644 --- a/src/api/tests.rs +++ b/src/api/tests.rs @@ -346,3 +346,73 @@ async fn test_text_completion_nonexistent_model() { .unwrap() .contains("nonexistent-model")); } + +#[tokio::test] +async fn test_chat_completion_streaming() { + let (app, _temp_dir) = create_test_app(); + let request_body = json!({ + "model": "test-model", + "messages": [ + {"role": "user", "content": "Hello"} + ], + "max_tokens": 50, + "stream": true + }); + + let request = Request::builder() + .uri("/v1/chat/completions") + .method("POST") + .header("content-type", "application/json") + .body(Body::from(serde_json::to_vec(&request_body).unwrap())) + .unwrap(); + + let response = app.oneshot(request).await.unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get("content-type").unwrap(), + "text/event-stream" + ); + + // Read the streaming body + let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap(); + let body_text = String::from_utf8_lossy(&body_bytes); + + // Parse SSE events (format: "data: {...}\n\n") + let events: Vec<&str> = body_text + .split("\n\n") + .filter(|line| line.starts_with("data: ")) + .map(|line| line.strip_prefix("data: ").unwrap()) + .collect(); + + assert!(!events.is_empty(), "Should have at least one event"); + + // Separate JSON events from [DONE] + let json_events: Vec<&str> = events.iter().filter(|&&e| e != "[DONE]").copied().collect(); + + assert!( + !json_events.is_empty(), + "Should have at least one JSON event" + ); + + // Check first event has role + let first_event: Value = serde_json::from_str(json_events[0]).unwrap(); + assert_eq!(first_event["object"], "chat.completion.chunk"); + assert_eq!(first_event["model"], "test-model"); + assert_eq!(first_event["choices"][0]["delta"]["role"], "assistant"); + + // Check middle events have content + if json_events.len() > 2 { + let middle_event: Value = serde_json::from_str(json_events[1]).unwrap(); + assert!(middle_event["choices"][0]["delta"]["content"].is_string()); + } + + // Check last JSON event has finish_reason + let last_json_event: Value = serde_json::from_str(json_events[json_events.len() - 1]).unwrap(); + assert_eq!(last_json_event["choices"][0]["finish_reason"], "stop"); + + // Check stream ends with [DONE] + assert!(events.contains(&"[DONE]"), "Stream should end with [DONE]"); +} diff --git a/src/api/types/request.rs b/src/api/types/request.rs index 40fde9c..f8ff213 100644 --- a/src/api/types/request.rs +++ b/src/api/types/request.rs @@ -9,9 +9,9 @@ pub struct ChatCompletionRequest { pub max_tokens: Option, #[serde(default = "default_temperature")] pub temperature: Option, - #[serde(default = "default_top_p")] + #[serde(default)] pub top_p: Option, - #[serde(default = "default_n")] + #[serde(default)] pub n: Option, #[serde(default)] pub stream: bool, @@ -39,9 +39,9 @@ pub struct CompletionRequest { pub max_tokens: Option, #[serde(default = "default_temperature")] pub temperature: Option, - #[serde(default = "default_top_p")] + #[serde(default)] pub top_p: Option, - #[serde(default = "default_n")] + #[serde(default)] pub n: Option, #[serde(default)] pub stream: bool, @@ -60,11 +60,11 @@ pub enum StringOrArray { Array(Vec), } -impl StringOrArray { - pub fn to_string(&self) -> String { +impl std::fmt::Display for StringOrArray { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - StringOrArray::String(s) => s.clone(), - StringOrArray::Array(arr) => arr.join("\n"), + StringOrArray::String(s) => write!(f, "{}", s), + StringOrArray::Array(arr) => write!(f, "{}", arr.join("\n")), } } } @@ -77,11 +77,3 @@ fn default_max_tokens() -> Option { fn default_temperature() -> Option { Some(0.7) } - -fn default_top_p() -> Option { - Some(1.0) -} - -fn default_n() -> Option { - Some(1) -} From 70d20a273d8d4371fecfe63a54407326eb174c60 Mon Sep 17 00:00:00 2001 From: kerthcet Date: Sun, 26 Apr 2026 14:21:56 +0100 Subject: [PATCH 08/12] fix lint Signed-off-by: kerthcet --- src/api/types/request.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/api/types/request.rs b/src/api/types/request.rs index f8ff213..f4021de 100644 --- a/src/api/types/request.rs +++ b/src/api/types/request.rs @@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize}; /// Chat completion request (OpenAI compatible) #[derive(Debug, Clone, Deserialize)] +#[allow(dead_code)] pub struct ChatCompletionRequest { pub model: String, pub messages: Vec, @@ -31,6 +32,7 @@ pub struct ChatMessage { /// Legacy text completion request #[derive(Debug, Clone, Deserialize)] +#[allow(dead_code)] pub struct CompletionRequest { pub model: String, #[serde(alias = "prompt")] From 0387e38b27e2616ac273d1fae3dbc68303774362 Mon Sep 17 00:00:00 2001 From: kerthcet Date: Sun, 26 Apr 2026 16:07:07 +0100 Subject: [PATCH 09/12] remove libs Signed-off-by: kerthcet --- src/lib.rs | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 src/lib.rs diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 8cacc2c..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,10 +0,0 @@ -// lib.rs - internal modules for the PUMA binary -// Modules are private as PUMA is primarily a CLI tool, not a library - -#![allow(dead_code)] - -mod api; -mod backend; -mod registry; -mod storage; -mod utils; From d39b98fbf1ed1130c2663be72d4222bf4152a231 Mon Sep 17 00:00:00 2001 From: kerthcet Date: Sun, 26 Apr 2026 16:26:29 +0100 Subject: [PATCH 10/12] fix tests Signed-off-by: kerthcet --- Cargo.lock | 980 +++++++++++++++++++-------------------- LOGGING.md | 234 ---------- README.md | 6 +- src/api/completions.rs | 12 + src/api/tests.rs | 22 +- src/api/types/request.rs | 4 +- 6 files changed, 501 insertions(+), 757 deletions(-) delete mode 100644 LOGGING.md diff --git a/Cargo.lock b/Cargo.lock index 8fe1ab6..1ef1d27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,20 +2,11 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "ahash" @@ -31,9 +22,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -49,9 +40,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -64,37 +55,37 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "once_cell", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.61.2", ] [[package]] @@ -122,9 +113,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" @@ -154,7 +145,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", "tracing", @@ -181,21 +172,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "backtrace" -version = "0.3.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets", -] - [[package]] name = "base64" version = "0.22.1" @@ -210,15 +186,15 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bitflags" -version = "2.8.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" @@ -228,24 +204,25 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.15" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ + "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" @@ -262,9 +239,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.30" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -272,9 +249,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.30" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -284,9 +261,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.28" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -296,15 +273,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "colored" @@ -324,7 +301,7 @@ checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" dependencies = [ "encode_unicode", "libc", - "unicode-width 0.2.0", + "unicode-width 0.2.2", "windows-sys 0.61.2", ] @@ -367,6 +344,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -409,21 +396,21 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "csv" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" dependencies = [ "csv-core", "itoa", "ryu", - "serde", + "serde_core", ] [[package]] name = "csv-core" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" dependencies = [ "memchr", ] @@ -474,7 +461,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users 0.5.0", + "redox_users 0.5.2", "windows-sys 0.61.2", ] @@ -532,9 +519,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "0.1.3" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" dependencies = [ "log", "regex", @@ -542,14 +529,14 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.6" +version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" dependencies = [ "anstream", "anstyle", "env_filter", - "humantime", + "jiff", "log", ] @@ -561,12 +548,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -583,9 +570,15 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" @@ -626,18 +619,18 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -650,9 +643,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -660,15 +653,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -683,9 +676,9 @@ checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -694,21 +687,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -718,31 +711,30 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "getrandom" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets", + "r-efi 5.3.0", + "wasip2", ] [[package]] @@ -753,22 +745,16 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "h2" -version = "0.4.8" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -794,13 +780,19 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + [[package]] name = "hashlink" version = "0.9.1" @@ -816,12 +808,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" - [[package]] name = "hermit-abi" version = "0.5.2" @@ -846,7 +832,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.18", "tokio", "ureq", "windows-sys 0.61.2", @@ -854,12 +840,11 @@ dependencies = [ [[package]] name = "http" -version = "1.2.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -875,12 +860,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", + "futures-core", "http", "http-body", "pin-project-lite", @@ -888,9 +873,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -898,21 +883,16 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" -version = "1.6.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http", "http-body", @@ -927,16 +907,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.5" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ - "futures-util", "http", "hyper", "hyper-util", "rustls", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -960,21 +938,27 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64", "bytes", "futures-channel", "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -1003,21 +987,23 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", + "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1026,99 +1012,61 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ - "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", + "icu_locale_core", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "id-arena" version = "2.3.0" @@ -1127,9 +1075,9 @@ checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1138,9 +1086,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -1148,13 +1096,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.17.0", "serde", + "serde_core", ] [[package]] @@ -1165,46 +1114,82 @@ checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" dependencies = [ "console", "portable-atomic", - "unicode-width 0.2.0", + "unicode-width 0.2.2", "unit-prefix", "web-time", ] [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] [[package]] name = "is-terminal" -version = "0.4.15" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ - "hermit-abi 0.4.0", + "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -1223,17 +1208,16 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.170" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags", "libc", ] @@ -1250,15 +1234,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.15" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.7.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "litrs" @@ -1268,19 +1252,18 @@ checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.26" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "matchit" @@ -1290,9 +1273,9 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" -version = "2.7.4" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mime" @@ -1302,9 +1285,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.8.5" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", "simd-adler32", @@ -1312,20 +1295,20 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.3" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] name = "native-tls" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ "libc", "log", @@ -1368,30 +1351,27 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi 0.5.2", + "hermit-abi", "libc", ] [[package]] -name = "object" -version = "0.36.7" +name = "once_cell" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] -name = "once_cell" -version = "1.20.3" +name = "once_cell_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openssl" -version = "0.10.71" +version = "0.10.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" +checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" dependencies = [ "bitflags", "cfg-if", @@ -1415,15 +1395,15 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.1.6" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.106" +version = "0.9.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" +checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" dependencies = [ "cc", "libc", @@ -1439,9 +1419,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1449,15 +1429,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-link", ] [[package]] @@ -1471,9 +1451,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project" @@ -1497,21 +1477,15 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "portable-atomic" @@ -1519,6 +1493,24 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1536,9 +1528,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.34" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn", @@ -1560,9 +1552,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -1594,19 +1586,25 @@ dependencies = [ "tokio", "tokio-stream", "tower 0.4.13", - "tower-http", + "tower-http 0.5.2", "uuid", ] [[package]] name = "quote" -version = "1.0.38" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -1639,7 +1637,7 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom 0.3.1", + "getrandom 0.3.4", ] [[package]] @@ -1664,9 +1662,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.9" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] @@ -1677,27 +1675,27 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.17", "libredox", "thiserror 1.0.69", ] [[package]] name = "redox_users" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.17", "libredox", - "thiserror 2.0.11", + "thiserror 2.0.18", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -1707,9 +1705,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -1718,15 +1716,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" -version = "0.12.12" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", @@ -1741,42 +1739,39 @@ dependencies = [ "hyper-rustls", "hyper-tls", "hyper-util", - "ipnet", "js-sys", "log", "mime", "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", - "system-configuration", "tokio", "tokio-native-tls", "tokio-util", - "tower 0.5.2", + "tower 0.5.3", + "tower-http 0.6.8", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", - "windows-registry", ] [[package]] name = "ring" -version = "0.17.11" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da5349ae27d3887ca812fb375b45a4fbb36d8d12d2df394968cd86e35683fe73" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -1806,30 +1801,24 @@ dependencies = [ "rusqlite", ] -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - [[package]] name = "rustix" -version = "0.38.44" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.23" +version = "0.23.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" +checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e" dependencies = [ "log", "once_cell", @@ -1841,25 +1830,19 @@ dependencies = [ ] [[package]] -name = "rustls-pemfile" -version = "2.2.0" +name = "rustls-pki-types" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ - "rustls-pki-types", + "zeroize", ] -[[package]] -name = "rustls-pki-types" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" - [[package]] name = "rustls-webpki" -version = "0.102.8" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", @@ -1868,23 +1851,23 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.19" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.19" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1895,12 +1878,12 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "2.11.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -1908,9 +1891,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -1954,14 +1937,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.139" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] @@ -1995,10 +1979,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -2010,27 +1995,24 @@ checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.8" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2046,9 +2028,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "strsim" @@ -2064,9 +2046,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.98" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -2084,9 +2066,9 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", @@ -2109,12 +2091,12 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -2130,16 +2112,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.17.1" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "cfg-if", "fastrand", - "getrandom 0.3.1", + "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2164,11 +2145,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl 2.0.18", ] [[package]] @@ -2184,9 +2165,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -2226,9 +2207,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -2236,11 +2217,10 @@ dependencies = [ [[package]] name = "tokio" -version = "1.43.0" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ - "backtrace", "bytes", "libc", "mio", @@ -2249,14 +2229,14 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -2275,9 +2255,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.1" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -2296,9 +2276,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.13" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -2324,9 +2304,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -2354,6 +2334,24 @@ dependencies = [ "tower-service", ] +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower 0.5.3", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -2368,9 +2366,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -2379,9 +2377,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] @@ -2394,9 +2392,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "unicode-ident" -version = "1.0.17" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-width" @@ -2406,9 +2404,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unicode-xid" @@ -2466,21 +2464,16 @@ dependencies = [ [[package]] name = "url" -version = "2.5.4" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8-zero" version = "0.8.1" @@ -2534,18 +2527,9 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasi" -version = "0.13.3+wasi-0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" -dependencies = [ - "wit-bindgen-rt", -] +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" @@ -2567,48 +2551,32 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2616,22 +2584,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -2678,16 +2646,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", - "hashbrown 0.15.2", + "hashbrown 0.15.5", "indexmap", "semver", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -2775,7 +2743,7 @@ dependencies = [ "windows-interface 0.59.3", "windows-link", "windows-result 0.4.1", - "windows-strings 0.5.1", + "windows-strings", ] [[package]] @@ -2830,13 +2798,13 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-registry" -version = "0.2.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-result 0.2.0", - "windows-strings 0.1.0", - "windows-targets", + "windows-link", + "windows-result 0.4.1", + "windows-strings", ] [[package]] @@ -2848,15 +2816,6 @@ dependencies = [ "windows-targets", ] -[[package]] -name = "windows-result" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" -dependencies = [ - "windows-targets", -] - [[package]] name = "windows-result" version = "0.4.1" @@ -2866,16 +2825,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-strings" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result 0.2.0", - "windows-targets", -] - [[package]] name = "windows-strings" version = "0.5.1" @@ -3002,15 +2951,6 @@ dependencies = [ "wit-parser", ] -[[package]] -name = "wit-bindgen-rt" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" -dependencies = [ - "bitflags", -] - [[package]] name = "wit-bindgen-rust" version = "0.51.0" @@ -3079,25 +3019,18 @@ dependencies = [ "wasmparser", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -3105,9 +3038,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -3117,18 +3050,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", @@ -3137,18 +3070,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.5" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.5" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -3158,15 +3091,26 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -3175,11 +3119,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/LOGGING.md b/LOGGING.md deleted file mode 100644 index d8f04ce..0000000 --- a/LOGGING.md +++ /dev/null @@ -1,234 +0,0 @@ -# Logging Guidelines for PUMA - -## Overview - -PUMA uses a hybrid approach for output: -- **User-facing output**: `println!` for CLI commands -- **Internal logging**: `log::*` macros for debugging and monitoring - ---- - -## When to Use Each - -### Use `println!` / `eprintln!` - -For **direct user communication** in CLI commands: - -✅ **Good Examples:** -```rust -// Success messages -println!("✓ Model downloaded successfully"); - -// Progress updates -println!("Downloading... 50%"); - -// Command output (ls, inspect, etc.) -println!("MODEL PROVIDER SIZE"); - -// User-facing startup banners -println!("🚀 Starting PUMA inference server..."); -``` - -❌ **Bad Examples:** -```rust -// Internal state (use log::debug instead) -println!("Initializing database connection"); - -// Error details (use log::error instead) -println!("Failed to parse config: {}", err); -``` - ---- - -### Use `log::*` Macros - -For **internal operations** and **debugging**: - -#### `log::error!` - Errors -```rust -log::error!("Failed to connect to database: {}", err); -log::error!("Model validation failed for: {}", model_name); -``` - -#### `log::warn!` - Warnings -```rust -log::warn!("Cache directory not found, creating new one"); -log::warn!("Model file corrupt, re-downloading"); -``` - -#### `log::info!` - Important events -```rust -log::info!("Server listening on http://{}", addr); -log::info!("Model registry loaded with {} models", count); -log::info!("Starting inference for model: {}", model); -``` - -#### `log::debug!` - Debug details -```rust -log::debug!("Using MockEngine backend"); -log::debug!("Cache hit for model: {}", model); -log::debug!("Request took {}ms", elapsed); -``` - ---- - -## Examples from Codebase - -### Good: `serve.rs` - -Hybrid approach - both user-facing and logging: - -```rust -pub async fn execute(host: &str, port: u16) -> Result<(), Box> { - // Logging for monitoring - info!("Starting PUMA inference server"); - - // User-facing output - println!("🚀 Starting PUMA inference server..."); - - let engine = Arc::new(MockEngine::new()); - - // Internal logging - info!("Inference engine initialized"); - debug!("Using MockEngine backend"); - - // User-facing confirmation - println!("✓ Inference engine initialized"); - - // ... server starts - - // Logging for monitoring - info!("Server listening on http://{}", addr); - - // User-facing banner - println!(" Listening on: http://{}", addr); -} -``` - -### Good: `downloader.rs` - -Internal operations use logging only: - -```rust -pub async fn download_model(&self, name: &str) -> Result<(), Error> { - debug!("Downloading model {} from Hugging Face...", name); - - // ... download logic - - debug!("Model info for {}: {:?}", name, model_info); -} -``` - ---- - -## Log Levels in Practice - -### Development -```bash -# See all logs -RUST_LOG=debug cargo run -- serve - -# See only warnings and errors -RUST_LOG=warn cargo run -- serve -``` - -### Production -```bash -# Default: info level -RUST_LOG=info ./puma serve - -# Quiet mode (errors only) -RUST_LOG=error ./puma serve -``` - ---- - -## Best Practices - -### ✅ DO - -1. **Use both** when appropriate: - ```rust - info!("Server starting on {}", addr); // For logs - println!("🚀 Starting server..."); // For users - ``` - -2. **Log structured data**: - ```rust - info!("Request completed: method={} path={} status={} duration={}ms", - method, path, status, duration); - ``` - -3. **Use appropriate levels**: - ```rust - error!("Critical failure"); // Something broke - warn!("Potential issue"); // Might be a problem - info!("Important event"); // Normal but notable - debug!("Internal detail"); // For debugging only - ``` - -### ❌ DON'T - -1. **Don't log sensitive data**: - ```rust - // Bad - debug!("API key: {}", api_key); - - // Good - debug!("API key provided: {}", !api_key.is_empty()); - ``` - -2. **Don't use println for internal operations**: - ```rust - // Bad - println!("Database connection established"); - - // Good - info!("Database connection established"); - ``` - -3. **Don't be too verbose**: - ```rust - // Bad - logs every iteration - for item in items { - debug!("Processing item: {}", item); - } - - // Good - log summary - debug!("Processing {} items", items.len()); - ``` - ---- - -## Testing Logs - -Use `env_logger` for tests: - -```rust -#[test] -fn test_something() { - let _ = env_logger::builder().is_test(true).try_init(); - - info!("Running test"); - // ... test code -} -``` - -Run with logs: -```bash -RUST_LOG=debug cargo test -- --nocapture -``` - ---- - -## Summary - -| Output Type | Use | Example | -|-------------|-----|---------| -| **User-facing** | `println!` | "✓ Model downloaded" | -| **Errors** | `log::error!` | "Failed to connect: {}" | -| **Warnings** | `log::warn!` | "Cache miss for model" | -| **Events** | `log::info!` | "Server started on :8000" | -| **Debug** | `log::debug!` | "Using MockEngine" | - -**Key principle**: If a human is directly waiting for the output → `println!`. If it's for monitoring/debugging → `log::*`. diff --git a/README.md b/README.md index 017dd72..4c05803 100644 --- a/README.md +++ b/README.md @@ -259,13 +259,9 @@ Models are stored with lowercase names for case-insensitive matching. # Build make build -# Run all tests (152 tests: 67 unit + 36 integration + 49 lib) +# Run all tests make test -# Run specific test suites -cargo test --test api:: # API integration tests -cargo test --test cli_test # CLI integration tests - # Test API manually ./hack/scripts/test_api.sh ``` diff --git a/src/api/completions.rs b/src/api/completions.rs index addb075..497736b 100644 --- a/src/api/completions.rs +++ b/src/api/completions.rs @@ -28,6 +28,18 @@ pub async fn completions( .into_response(); } + // TODO: Implement streaming support for /v1/completions + if req.stream { + return ( + axum::http::StatusCode::BAD_REQUEST, + Json(ErrorResponse::new( + "Streaming not supported for /v1/completions endpoint".to_string(), + "invalid_request_error".to_string(), + )), + ) + .into_response(); + } + // Validate model exists match registry.get_model(&req.model) { Ok(None) => { diff --git a/src/api/tests.rs b/src/api/tests.rs index a64303b..f5a44f2 100644 --- a/src/api/tests.rs +++ b/src/api/tests.rs @@ -10,7 +10,7 @@ use axum::{ use serde_json::{json, Value}; use std::sync::Arc; use tempfile::TempDir; -use tower::ServiceExt; // for `oneshot` and `ready` +use tower::util::ServiceExt; // for `oneshot` and `ready` use super::routes::create_router; use crate::backend::mock::MockEngine; @@ -204,6 +204,26 @@ async fn test_text_completion_empty_prompt() { .contains("prompt cannot be empty")); } +#[tokio::test] +async fn test_text_completion_streaming_not_supported() { + let (app, _temp_dir) = create_test_app(); + let request_body = json!({ + "model": "test-model", + "prompt": "Hello world", + "stream": true + }); + + let (status, json) = + make_json_request(app, "POST", "/v1/completions", Some(request_body)).await; + + assert_eq!(status, StatusCode::BAD_REQUEST); + assert_eq!(json["error"]["type"], "invalid_request_error"); + assert!(json["error"]["message"] + .as_str() + .unwrap() + .contains("Streaming not supported")); +} + #[tokio::test] async fn test_chat_completion_with_system_message() { let (app, _temp_dir) = create_test_app(); diff --git a/src/api/types/request.rs b/src/api/types/request.rs index f4021de..17e2556 100644 --- a/src/api/types/request.rs +++ b/src/api/types/request.rs @@ -16,7 +16,7 @@ pub struct ChatCompletionRequest { pub n: Option, #[serde(default)] pub stream: bool, - pub stop: Option>, + pub stop: Option, #[serde(default)] pub presence_penalty: Option, #[serde(default)] @@ -47,7 +47,7 @@ pub struct CompletionRequest { pub n: Option, #[serde(default)] pub stream: bool, - pub stop: Option>, + pub stop: Option, #[serde(default)] pub presence_penalty: Option, #[serde(default)] From 9ad99dbd390b265805a551a6537f4fe233af719e Mon Sep 17 00:00:00 2001 From: kerthcet Date: Sun, 26 Apr 2026 16:38:39 +0100 Subject: [PATCH 11/12] change log lib Signed-off-by: kerthcet --- Cargo.lock | 145 ++++++++++++++++++++-------------- Cargo.toml | 6 +- src/api/chat.rs | 2 +- src/api/routes.rs | 4 +- src/cli/serve.rs | 2 +- src/downloader/huggingface.rs | 2 +- src/main.rs | 8 +- 7 files changed, 102 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1ef1d27..09eceeb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -517,29 +517,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "env_filter" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -1158,30 +1135,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" -[[package]] -name = "jiff" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", -] - -[[package]] -name = "jiff-static" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "js-sys" version = "0.3.95" @@ -1265,6 +1218,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "matchit" version = "0.7.3" @@ -1330,6 +1292,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-conv" version = "0.2.1" @@ -1493,15 +1464,6 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" -[[package]] -name = "portable-atomic-util" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" -dependencies = [ - "portable-atomic", -] - [[package]] name = "potential_utf" version = "0.1.5" @@ -1568,11 +1530,9 @@ dependencies = [ "clap", "colored", "dirs", - "env_logger", "futures", "hf-hub", "indicatif", - "log", "prettytable-rs", "regex", "reqwest", @@ -1587,6 +1547,8 @@ dependencies = [ "tokio-stream", "tower 0.4.13", "tower-http 0.5.2", + "tracing", + "tracing-subscriber", "uuid", ] @@ -1971,6 +1933,15 @@ dependencies = [ "serde", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -2174,6 +2145,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.47" @@ -2332,6 +2312,7 @@ dependencies = [ "pin-project-lite", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -2372,9 +2353,21 @@ checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" version = "0.1.36" @@ -2382,6 +2375,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -2504,6 +2527,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 547ee4d..d01e472 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,8 +13,8 @@ reqwest = { version = "0.12", features = ["json"] } tokio = { version = "1", features = ["full"] } serde = { version = "1.0", features = ["derive"] } serde_derive = "1.0" -env_logger = "0.11.6" -log = "0.4.26" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } indicatif = "0.18" dirs = "6.0.0" hf-hub = { version = "0.5.0", features = ["tokio"] } @@ -29,7 +29,7 @@ regex = "1.11" # Web server axum = "0.7" tower = "0.4" -tower-http = { version = "0.5", features = ["cors"] } +tower-http = { version = "0.5", features = ["cors", "trace"] } uuid = { version = "1.0", features = ["v4", "serde"] } futures = "0.3" tokio-stream = "0.1" diff --git a/src/api/chat.rs b/src/api/chat.rs index 12953b1..0dfcf4f 100644 --- a/src/api/chat.rs +++ b/src/api/chat.rs @@ -204,7 +204,7 @@ async fn chat_completions_stream( } } Err(e) => { - log::error!("Error generating stream: {}", e); + tracing::error!("Error generating stream: {}", e); return; } } diff --git a/src/api/routes.rs b/src/api/routes.rs index 91d3832..cc98149 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -4,7 +4,7 @@ use axum::{ }; use serde::Serialize; use std::sync::Arc; -use tower_http::cors::CorsLayer; +use tower_http::{cors::CorsLayer, trace::TraceLayer}; use crate::backend::InferenceEngine; use crate::registry::model_registry::ModelRegistry; @@ -37,6 +37,8 @@ pub fn create_router( .route("/health", get(health_check)) // Pass state .with_state(state) + // Enable request/response logging + .layer(TraceLayer::new_for_http()) // Enable CORS for browser clients .layer(CorsLayer::permissive()) } diff --git a/src/cli/serve.rs b/src/cli/serve.rs index 309e00f..d0bb814 100644 --- a/src/cli/serve.rs +++ b/src/cli/serve.rs @@ -1,6 +1,6 @@ use colored::Colorize; -use log::{debug, info}; use std::sync::Arc; +use tracing::{debug, info}; use crate::api::routes::create_router; use crate::backend::mock::MockEngine; diff --git a/src/downloader/huggingface.rs b/src/downloader/huggingface.rs index 72ffae1..8d7ec73 100644 --- a/src/downloader/huggingface.rs +++ b/src/downloader/huggingface.rs @@ -1,5 +1,5 @@ use colored::Colorize; -use log::debug; +use tracing::debug; use hf_hub::api::tokio::{ApiBuilder, Progress}; use indicatif::{ProgressBar, ProgressStyle}; diff --git a/src/main.rs b/src/main.rs index a5e38a7..614a698 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,8 +14,12 @@ use crate::cli::commands::{run, Cli}; use crate::utils::file; fn main() { - // set hf_hub to warn to disable the info logs. - env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info,hf_hub=warn")) + // Setup tracing subscriber for tower-http TraceLayer + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "info,hf_hub=warn,tower_http=info".into()), + ) .init(); // Create the root folder if it doesn't exist. From 2f3fceef34ed8fe61451f4813372f4df00bfbfd6 Mon Sep 17 00:00:00 2001 From: kerthcet Date: Sun, 26 Apr 2026 16:50:02 +0100 Subject: [PATCH 12/12] change the request log level Signed-off-by: kerthcet --- src/api/routes.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/api/routes.rs b/src/api/routes.rs index cc98149..403dd5b 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -4,7 +4,11 @@ use axum::{ }; use serde::Serialize; use std::sync::Arc; -use tower_http::{cors::CorsLayer, trace::TraceLayer}; +use tower_http::{ + cors::CorsLayer, + trace::{DefaultMakeSpan, DefaultOnRequest, DefaultOnResponse, TraceLayer}, + LatencyUnit, +}; use crate::backend::InferenceEngine; use crate::registry::model_registry::ModelRegistry; @@ -37,8 +41,17 @@ pub fn create_router( .route("/health", get(health_check)) // Pass state .with_state(state) - // Enable request/response logging - .layer(TraceLayer::new_for_http()) + // Enable request/response logging at INFO level + .layer( + TraceLayer::new_for_http() + .make_span_with(DefaultMakeSpan::new().level(tracing::Level::INFO)) + .on_request(DefaultOnRequest::new().level(tracing::Level::INFO)) + .on_response( + DefaultOnResponse::new() + .level(tracing::Level::INFO) + .latency_unit(LatencyUnit::Millis), + ), + ) // Enable CORS for browser clients .layer(CorsLayer::permissive()) }