diff --git a/.cargo/config.toml b/.cargo/config.toml index e6afd7ff530..c3206b2662e 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -17,3 +17,6 @@ rustflags = ["-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"] [target.aarch64-apple-darwin] rustflags = ["-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"] + +[env] +SQLX_OFFLINE = "true" diff --git a/Makefile b/Makefile index 79c18f6fe82..cad3242fbce 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ .PHONY: help test test-unit test-unit-llms test-unit-proxy-guardrails test-unit-proxy-core test-unit-proxy-misc \ test-unit-integrations test-unit-core-utils test-unit-other test-unit-root \ test-proxy-unit-a test-proxy-unit-b test-integration test-unit-helm \ - test-rust-extension \ + test-rust-extension rust-sqlx-prepare \ info lint lint-inner lint-dev lint-checks format \ lint-basedpyright lint-e2e-basedpyright lint-basedpyright-budget-update lint-type-discipline lint-type-discipline-budget-update \ lint-ruff-budget lint-ruff-budget-update lint-budget-update lint-gate \ @@ -56,6 +56,7 @@ help: @echo " make test-integration - Run integration tests" @echo " make test-unit-helm - Run helm unit tests" @echo " make test-rust-extension - Build the Rust extension and run its public Python tests" + @echo " make rust-sqlx-prepare - Refresh litellm-rust/crates/db/.sqlx against a migrated Postgres container" @echo "" @echo "Heavy targets (check, lint) queue for LITELLM_GATE_SLOTS machine-wide" @echo "slots (default 2; 0 disables) so parallel sessions don't thrash one machine." @@ -306,6 +307,9 @@ test-rust-extension: LITELLM_RUST=1 LITELLM_LOCAL_MODEL_COST_MAP=True \ "$$temporary/venv/bin/python" -I -m pytest --import-mode=importlib -m requires_rust_extension tests/test_litellm_rust +rust-sqlx-prepare: + cd litellm-rust && cargo run -p litellm-db-testing --bin sqlx-prepare + test: install-test-deps $(UV_RUN) pytest tests/ diff --git a/litellm-rust/Cargo.lock b/litellm-rust/Cargo.lock index f3682dacc61..19458cf34b1 100644 --- a/litellm-rust/Cargo.lock +++ b/litellm-rust/Cargo.lock @@ -146,6 +146,22 @@ dependencies = [ "serde_json", ] +[[package]] +name = "astral-tokio-tar" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b18457efd137254e016bbde5e1d88df61c4e1a5ae2223746e56123bac6af2463" +dependencies = [ + "futures-core", + "libc", + "portable-atomic", + "rustc-hash", + "rustix", + "tokio", + "tokio-stream", + "xattr", +] + [[package]] name = "async-compression" version = "0.4.46" @@ -202,6 +218,15 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -893,6 +918,9 @@ name = "bitflags" version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +dependencies = [ + "serde_core", +] [[package]] name = "block-buffer" @@ -912,6 +940,80 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "bollard" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee04c4c84f1f811b017f2fbb7dd8815c976e7ca98593de9c1e2afad0f636bff4" +dependencies = [ + "async-stream", + "base64 0.22.1", + "bitflags 2.13.1", + "bollard-buildkit-proto", + "bollard-stubs", + "bytes", + "futures-core", + "futures-util", + "hex", + "home", + "http 1.4.2", + "http-body-util", + "hyper 1.10.1", + "hyper-named-pipe", + "hyper-rustls 0.27.9", + "hyper-util", + "hyperlocal", + "log", + "num", + "pin-project-lite", + "rand 0.9.5", + "rustls 0.23.42", + "rustls-native-certs", + "rustls-pki-types", + "serde", + "serde_derive", + "serde_json", + "serde_urlencoded", + "thiserror 2.0.19", + "time", + "tokio", + "tokio-stream", + "tokio-util", + "tonic", + "tower-service", + "url", + "winapi", +] + +[[package]] +name = "bollard-buildkit-proto" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a885520bf6249ab931a764ffdb87b0ceef48e6e7d807cfdb21b751e086e1ad" +dependencies = [ + "prost", + "prost-types", + "tonic", + "tonic-prost", + "ureq", +] + +[[package]] +name = "bollard-stubs" +version = "1.52.1-rc.29.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0a8ca8799131c1837d1282c3f81f31e76ceb0ce426e04a7fe1ccee3287c066" +dependencies = [ + "base64 0.22.1", + "bollard-buildkit-proto", + "bytes", + "prost", + "serde", + "serde_json", + "serde_repr", + "time", +] + [[package]] name = "borrow-or-share" version = "0.2.4" @@ -1179,6 +1281,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + [[package]] name = "crc-fast" version = "1.10.0" @@ -1276,6 +1393,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.22" @@ -1560,6 +1686,23 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "docker_credential" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29547a1dc60885a552306986316bc9701ba120c1a8db6769fa68691529ad373d" +dependencies = [ + "base64 0.22.1", + "serde", + "serde_json", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "dunce" version = "1.0.5" @@ -1577,6 +1720,9 @@ name = "either" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +dependencies = [ + "serde", +] [[package]] name = "email_address" @@ -1618,6 +1764,16 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6" +[[package]] +name = "etcetera" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + [[package]] name = "event-listener" version = "5.4.2" @@ -1678,6 +1834,17 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +[[package]] +name = "ferroid" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee93edf3c501f0035bbeffeccfed0b79e14c311f12195ec0e661e114a0f60da4" +dependencies = [ + "portable-atomic", + "rand 0.10.2", + "web-time", +] + [[package]] name = "filetime" version = "0.2.29" @@ -1716,6 +1883,17 @@ dependencies = [ "serde", ] +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.9", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1795,6 +1973,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.33" @@ -2210,6 +2399,8 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] @@ -2226,11 +2417,11 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.12.2" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a596f1b20ed2cc5ecac41a164aaebc7258057060f06c0cf7a2ba3991ee7990fb" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" dependencies = [ - "hashbrown 0.17.1", + "hashbrown 0.16.1", ] [[package]] @@ -2251,6 +2442,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.13.0" @@ -2260,6 +2460,15 @@ dependencies = [ "digest 0.11.3", ] +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "http" version = "0.2.12" @@ -2382,6 +2591,20 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-named-pipe" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fab3637d6b04a8037af8a266fdf6cf92ea957e8c53981a2bf6136572531025bf" +dependencies = [ + "hex", + "hyper 1.10.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-rustls" version = "0.24.2" @@ -2450,6 +2673,21 @@ dependencies = [ "tracing", ] +[[package]] +name = "hyperlocal" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" +dependencies = [ + "hex", + "http-body-util", + "hyper 1.10.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -2816,9 +3054,9 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libsqlite3-sys" -version = "0.38.2" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1d20bef17f513b9b3004532233187769cd072d790971f4e4da0e346eb6401e8" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" dependencies = [ "cc", "pkg-config", @@ -3197,6 +3435,26 @@ dependencies = [ "rstest", ] +[[package]] +name = "litellm-db" +version = "0.1.0" +dependencies = [ + "serde", + "sqlx", +] + +[[package]] +name = "litellm-db-testing" +version = "0.1.0" +dependencies = [ + "rstest", + "sqlx", + "tempfile", + "testcontainers-modules", + "thiserror 2.0.19", + "tokio", +] + [[package]] name = "litellm-framing" version = "0.1.0" @@ -4151,6 +4409,31 @@ dependencies = [ "windows-link", ] +[[package]] +name = "parse-display" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a" +dependencies = [ + "parse-display-derive", + "regex", + "regex-syntax", +] + +[[package]] +name = "parse-display-derive" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae7800a4c974efd12df917266338e79a7a74415173caf7e70aa0a0707345281" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "regex-syntax", + "structmeta", + "syn 2.0.119", +] + [[package]] name = "paste" version = "1.0.15" @@ -5025,9 +5308,9 @@ dependencies = [ [[package]] name = "rusqlite" -version = "0.40.2" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3" +checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" dependencies = [ "bitflags 2.13.1", "fallible-iterator", @@ -5420,6 +5703,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "serde_spanned" version = "1.1.1" @@ -5593,6 +5887,9 @@ name = "smallvec" version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -5619,6 +5916,9 @@ name = "spin" version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] [[package]] name = "spin" @@ -5650,6 +5950,183 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "sqlx" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" +dependencies = [ + "base64 0.22.1", + "bytes", + "cfg-if", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.16.1", + "hashlink", + "indexmap 2.14.0", + "log", + "memchr", + "percent-encoding", + "rustls 0.23.42", + "rustls-native-certs", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "thiserror 2.0.19", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.119", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" +dependencies = [ + "cfg-if", + "dotenvy", + "either", + "heck", + "hex", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.119", + "thiserror 2.0.19", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" +dependencies = [ + "bitflags 2.13.1", + "byteorder", + "bytes", + "chrono", + "crc", + "digest 0.11.3", + "dotenvy", + "either", + "futures-core", + "futures-util", + "generic-array", + "log", + "percent-encoding", + "serde", + "sha1 0.11.0", + "sha2 0.11.0", + "sqlx-core", + "thiserror 2.0.19", + "tracing", +] + +[[package]] +name = "sqlx-postgres" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.13.1", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "rand 0.10.2", + "serde", + "serde_json", + "sha2 0.11.0", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.19", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" +dependencies = [ + "atoi", + "chrono", + "flume", + "form_urlencoded", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "thiserror 2.0.19", + "tracing", + "url", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -5662,12 +6139,46 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "structmeta" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329" +dependencies = [ + "proc-macro2", + "quote", + "structmeta-derive", + "syn 2.0.119", +] + +[[package]] +name = "structmeta-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "strum" version = "0.28.0" @@ -5796,6 +6307,46 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "testcontainers" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfd5785b5483672915ed5fe3cddf9f546802779fc1eceff0a6fb7321fac81c1e" +dependencies = [ + "astral-tokio-tar", + "async-trait", + "bollard", + "bytes", + "docker_credential", + "either", + "etcetera", + "ferroid", + "futures", + "http 1.4.2", + "itertools 0.14.0", + "log", + "memchr", + "parse-display", + "pin-project-lite", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.19", + "tokio", + "tokio-stream", + "tokio-util", + "url", +] + +[[package]] +name = "testcontainers-modules" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5985fde5befe4ffa77a052e035e16c2da86e8bae301baa9f9904ad3c494d357" +dependencies = [ + "testcontainers", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -6373,6 +6924,12 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-general-category" version = "1.1.0" @@ -6385,6 +6942,15 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-normalization-alignments" version = "0.1.12" @@ -6394,6 +6960,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + [[package]] name = "unicode-segmentation" version = "1.13.3" @@ -6424,6 +6996,33 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc" +dependencies = [ + "base64 0.23.1", + "log", + "percent-encoding", + "rustls 0.23.42", + "rustls-pki-types", + "ureq-proto", + "utf8-zero", +] + +[[package]] +name = "ureq-proto" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86fd172ccca569e458f61b6bdd6220965a9ef36e672a6852953b51a0e1583be" +dependencies = [ + "base64 0.23.1", + "http 1.4.2", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.8" @@ -6434,6 +7033,7 @@ dependencies = [ "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -6448,6 +7048,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -6701,6 +7307,12 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "whoami" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c" + [[package]] name = "winapi" version = "0.3.9" diff --git a/litellm-rust/Cargo.toml b/litellm-rust/Cargo.toml index e73cc7cccb4..7d4305b3a32 100644 --- a/litellm-rust/Cargo.toml +++ b/litellm-rust/Cargo.toml @@ -37,6 +37,8 @@ litellm-http = { path = "crates/http" } litellm-llms = { path = "crates/llms" } litellm-types = { path = "crates/types" } litellm-core-utils = { path = "crates/core-utils" } +litellm-db = { path = "crates/db" } +litellm-db-testing = { path = "crates/db-testing" } litellm-cache = { path = "crates/cache" } litellm-cache-azure-blob = { path = "crates/cache-azure-blob" } litellm-cache-memory = { path = "crates/cache-memory" } @@ -81,6 +83,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["float_roundtrip"] } serde_with = { version = "=3.16.1", default-features = false, features = ["std", "macros"] } sha2 = "0.10" +sqlx = { version = "0.9.0", default-features = false, features = ["json", "macros", "postgres", "runtime-tokio", "chrono", "tls-rustls-ring-native-roots"] } subtle = "2" thiserror = "2.0" tokenizers = { version = "0.23.1", default-features = false, features = ["onig"] } diff --git a/litellm-rust/clippy.toml b/litellm-rust/clippy.toml index 0e2ff770d27..197a9ebba3d 100644 --- a/litellm-rust/clippy.toml +++ b/litellm-rust/clippy.toml @@ -12,6 +12,13 @@ disallowed-methods = [ { path = "reqwest::ClientBuilder::danger_accept_invalid_certs", reason = "set HttpClientConfig::verify instead" }, { path = "reqwest::ClientBuilder::identity", reason = "set HttpClientConfig::client_certificate instead" }, { path = "reqwest::ClientBuilder::use_preconfigured_tls", reason = "HttpClientConfig owns the TLS configuration" }, + { path = "sqlx::query", reason = "use sqlx::query! or query_file! so the SQL is checked against the migrated schema" }, + { path = "sqlx::query_as", reason = "use sqlx::query_as! or query_file_as! so the SQL is checked against the migrated schema" }, + { path = "sqlx::query_scalar", reason = "use sqlx::query_scalar! so the SQL is checked against the migrated schema" }, + { path = "sqlx::query_with", reason = "use sqlx::query! or query_file! so the SQL is checked against the migrated schema" }, + { path = "sqlx::query_as_with", reason = "use sqlx::query_as! or query_file_as! so the SQL is checked against the migrated schema" }, + { path = "sqlx::query_scalar_with", reason = "use sqlx::query_scalar! so the SQL is checked against the migrated schema" }, + { path = "sqlx::raw_sql", reason = "raw_sql is unchecked; use the checked query macros" }, ] # Every outbound client comes from litellm_http::HttpClientPool so it honors the host's TLS, diff --git a/litellm-rust/crates/cache-disk/Cargo.toml b/litellm-rust/crates/cache-disk/Cargo.toml index 5cb75f7f129..825b23899e7 100644 --- a/litellm-rust/crates/cache-disk/Cargo.toml +++ b/litellm-rust/crates/cache-disk/Cargo.toml @@ -9,7 +9,7 @@ repository.workspace = true litellm-cache.workspace = true py_literal = "0.4.0" rand.workspace = true -rusqlite = { version = "0.40", features = ["bundled"] } +rusqlite = { version = "0.39", features = ["bundled"] } serde-pickle = "1.2" serde_json.workspace = true tokio.workspace = true diff --git a/litellm-rust/crates/db-testing/Cargo.toml b/litellm-rust/crates/db-testing/Cargo.toml new file mode 100644 index 00000000000..3ba71fbf644 --- /dev/null +++ b/litellm-rust/crates/db-testing/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "litellm-db-testing" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +sqlx = { workspace = true, features = ["migrate"] } +testcontainers-modules = { version = "0.15.0", features = ["postgres"] } +thiserror.workspace = true +tokio.workspace = true + +[dev-dependencies] +rstest.workspace = true +tempfile.workspace = true diff --git a/litellm-rust/crates/db-testing/src/bin/sqlx-prepare.rs b/litellm-rust/crates/db-testing/src/bin/sqlx-prepare.rs new file mode 100644 index 00000000000..f61ee1e02c7 --- /dev/null +++ b/litellm-rust/crates/db-testing/src/bin/sqlx-prepare.rs @@ -0,0 +1,26 @@ +use std::{ + env, + process::{Command, ExitCode}, +}; + +use litellm_db_testing::{Error, MigratedPostgres}; + +#[tokio::main] +async fn main() -> Result { + let postgres = MigratedPostgres::start().await?; + let status = Command::new(env::var("CARGO").unwrap_or_else(|_| "cargo".to_owned())) + .args(["sqlx", "prepare"]) + .args(env::args().skip(1)) + .args(["--", "--all-targets", "--all-features"]) + .current_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/../db")) + .env("DATABASE_URL", postgres.url()) + .env("SQLX_OFFLINE", "false") + .status() + .map_err(Error::Prepare)?; + + Ok(if status.success() { + ExitCode::SUCCESS + } else { + ExitCode::FAILURE + }) +} diff --git a/litellm-rust/crates/db-testing/src/error.rs b/litellm-rust/crates/db-testing/src/error.rs new file mode 100644 index 00000000000..8dfae4fa0cb --- /dev/null +++ b/litellm-rust/crates/db-testing/src/error.rs @@ -0,0 +1,19 @@ +use std::{io, path::PathBuf}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("reading {path}")] + Read { + path: PathBuf, + #[source] + source: io::Error, + }, + #[error("starting the Postgres container")] + Container(#[from] testcontainers_modules::testcontainers::TestcontainersError), + #[error("connecting to the Postgres container")] + Connect(#[from] sqlx::Error), + #[error("applying the Prisma migrations")] + Migrate(#[from] sqlx::migrate::MigrateError), + #[error("running cargo sqlx prepare")] + Prepare(#[source] io::Error), +} diff --git a/litellm-rust/crates/db-testing/src/lib.rs b/litellm-rust/crates/db-testing/src/lib.rs new file mode 100644 index 00000000000..9a656951908 --- /dev/null +++ b/litellm-rust/crates/db-testing/src/lib.rs @@ -0,0 +1,7 @@ +mod error; +mod migrations; +mod postgres; + +pub use error::Error; +pub use migrations::{PRISMA_MIGRATIONS_DIR, prisma_migrations}; +pub use postgres::MigratedPostgres; diff --git a/litellm-rust/crates/db-testing/src/migrations.rs b/litellm-rust/crates/db-testing/src/migrations.rs new file mode 100644 index 00000000000..d340057dee7 --- /dev/null +++ b/litellm-rust/crates/db-testing/src/migrations.rs @@ -0,0 +1,50 @@ +use std::{collections::BTreeSet, fs, path::Path}; + +use sqlx::{ + AssertSqlSafe, SqlSafeStr, + migrate::{Migration, MigrationType}, +}; + +use crate::Error; + +pub const PRISMA_MIGRATIONS_DIR: &str = concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../../litellm-proxy-extras/litellm_proxy_extras/migrations" +); + +const MIGRATION_FILENAME: &str = "migration.sql"; + +pub fn prisma_migrations(directory: &Path) -> Result, Error> { + migration_names(directory)? + .into_iter() + .zip(1..) + .map(|(name, version)| { + let path = directory.join(&name).join(MIGRATION_FILENAME); + let sql = fs::read_to_string(&path).map_err(|source| Error::Read { path, source })?; + Ok(Migration::new( + version, + name.into(), + MigrationType::Simple, + AssertSqlSafe(sql).into_sql_str(), + true, + )) + }) + .collect() +} + +fn migration_names(directory: &Path) -> Result, Error> { + let read_error = |source| Error::Read { + path: directory.to_owned(), + source, + }; + + fs::read_dir(directory) + .map_err(read_error)? + .map(|entry| { + let entry = entry.map_err(read_error)?; + let is_dir = entry.file_type().map_err(read_error)?.is_dir(); + Ok(is_dir.then(|| entry.file_name().to_string_lossy().into_owned())) + }) + .filter_map(Result::transpose) + .collect() +} diff --git a/litellm-rust/crates/db-testing/src/postgres.rs b/litellm-rust/crates/db-testing/src/postgres.rs new file mode 100644 index 00000000000..8f9925d1205 --- /dev/null +++ b/litellm-rust/crates/db-testing/src/postgres.rs @@ -0,0 +1,46 @@ +use std::path::Path; + +use sqlx::{PgPool, migrate::Migrator}; +use testcontainers_modules::{ + postgres::Postgres, + testcontainers::{ContainerAsync, ImageExt, runners::AsyncRunner}, +}; + +use crate::{Error, PRISMA_MIGRATIONS_DIR, prisma_migrations}; + +const POSTGRES_TAG: &str = + "16@sha256:e17e86066e5ef83e0952a9347f5c792b7ece00972e2aa787a6986f471b3dd3d5"; + +pub struct MigratedPostgres { + _container: ContainerAsync, + url: String, + pool: PgPool, +} + +impl MigratedPostgres { + pub async fn start() -> Result { + let container = Postgres::default().with_tag(POSTGRES_TAG).start().await?; + let url = format!( + "postgres://postgres:postgres@{}:{}/postgres", + container.get_host().await?, + container.get_host_port_ipv4(5432).await?, + ); + let pool = PgPool::connect(&url).await?; + Migrator::with_migrations(prisma_migrations(Path::new(PRISMA_MIGRATIONS_DIR))?) + .run(&pool) + .await?; + Ok(Self { + _container: container, + url, + pool, + }) + } + + pub fn url(&self) -> &str { + &self.url + } + + pub fn pool(&self) -> &PgPool { + &self.pool + } +} diff --git a/litellm-rust/crates/db-testing/tests/migrations.rs b/litellm-rust/crates/db-testing/tests/migrations.rs new file mode 100644 index 00000000000..40d1bcb0c2b --- /dev/null +++ b/litellm-rust/crates/db-testing/tests/migrations.rs @@ -0,0 +1,65 @@ +use std::fs; + +use litellm_db_testing::prisma_migrations; +use rstest::rstest; +use tempfile::TempDir; + +fn prisma_folder(migrations: &[(&str, &str)]) -> TempDir { + let directory = tempfile::tempdir().unwrap(); + for (name, sql) in migrations { + fs::create_dir(directory.path().join(name)).unwrap(); + fs::write(directory.path().join(name).join("migration.sql"), sql).unwrap(); + } + fs::write( + directory.path().join("migration_lock.toml"), + "provider = \"postgresql\"", + ) + .unwrap(); + directory +} + +#[rstest] +fn folders_apply_in_name_order_numbered_from_one_outside_a_transaction() { + let directory = prisma_folder(&[ + ("20260305000000_b", "SELECT 2;"), + ("20260108_short_prefix", "SELECT 3;"), + ("20260305000000_a", "BEGIN; SELECT 1; COMMIT;"), + ("20260108000000_first", "SELECT 0;"), + ]); + + let migrations = prisma_migrations(directory.path()).unwrap(); + + let applied: Vec<(i64, &str, &str, bool)> = migrations + .iter() + .map(|migration| { + ( + migration.version, + migration.description.as_ref(), + migration.sql.as_str(), + migration.no_tx, + ) + }) + .collect(); + assert_eq!( + applied, + vec![ + (1, "20260108000000_first", "SELECT 0;", true), + (2, "20260108_short_prefix", "SELECT 3;", true), + (3, "20260305000000_a", "BEGIN; SELECT 1; COMMIT;", true), + (4, "20260305000000_b", "SELECT 2;", true), + ] + ); +} + +#[rstest] +fn a_folder_without_migration_sql_is_an_error() { + let directory = prisma_folder(&[]); + fs::create_dir(directory.path().join("20260101000000_empty")).unwrap(); + + let error = prisma_migrations(directory.path()).unwrap_err(); + + assert!( + error.to_string().contains("20260101000000_empty"), + "{error}" + ); +} diff --git a/litellm-rust/crates/db-testing/tests/postgres.rs b/litellm-rust/crates/db-testing/tests/postgres.rs new file mode 100644 index 00000000000..8d89a767d39 --- /dev/null +++ b/litellm-rust/crates/db-testing/tests/postgres.rs @@ -0,0 +1,24 @@ +use std::path::Path; + +use litellm_db_testing::{MigratedPostgres, PRISMA_MIGRATIONS_DIR, prisma_migrations}; +use rstest::rstest; + +#[rstest] +#[tokio::test] +async fn real_prisma_migrations_apply_to_a_fresh_postgres() { + let postgres = MigratedPostgres::start().await.unwrap(); + + #[expect( + clippy::disallowed_methods, + reason = "the test schema has no offline query data" + )] + let applied: i64 = sqlx::query_scalar("SELECT count(*) FROM _sqlx_migrations WHERE success") + .fetch_one(postgres.pool()) + .await + .unwrap(); + + let expected = prisma_migrations(Path::new(PRISMA_MIGRATIONS_DIR)) + .unwrap() + .len(); + assert_eq!(applied as usize, expected); +} diff --git a/litellm-rust/crates/db/Cargo.toml b/litellm-rust/crates/db/Cargo.toml new file mode 100644 index 00000000000..ca9d6f555b0 --- /dev/null +++ b/litellm-rust/crates/db/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "litellm-db" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +serde.workspace = true +sqlx.workspace = true diff --git a/litellm-rust/crates/db/src/failure.rs b/litellm-rust/crates/db/src/failure.rs new file mode 100644 index 00000000000..85c52454bed --- /dev/null +++ b/litellm-rust/crates/db/src/failure.rs @@ -0,0 +1,10 @@ +use serde::Serialize; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum DbFailure { + UniqueViolation, + ForeignKeyViolation, + WriteConflict, + NotFound, +} diff --git a/litellm-rust/crates/db/src/lib.rs b/litellm-rust/crates/db/src/lib.rs new file mode 100644 index 00000000000..b7ce211d906 --- /dev/null +++ b/litellm-rust/crates/db/src/lib.rs @@ -0,0 +1,3 @@ +mod failure; + +pub use failure::DbFailure;