diff --git a/Cargo.lock b/Cargo.lock index 0865d6ade..faac50112 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,48 @@ # 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -29,6 +65,12 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "async-iterator" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "742b2f12ff517f144b6181d24f3f2481b503e05650ee79feec1f090048089f88" + [[package]] name = "auditable-serde" version = "0.8.0" @@ -47,6 +89,326 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "aws-config" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "455e9fb7743c6f6267eb2830ccc08686fbb3d13c9a689369562fd4d4ef9ea462" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 1.3.1", + "time", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687bc16bc431a8533fe0097c7f0182874767f920989d7260950172ae8e3c4465" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-runtime" +version = "1.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f6c68419d8ba16d9a7463671593c54f81ba58cab466e9b759418da606dcc2e2" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http-body 0.4.6", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-bedrockruntime" +version = "1.93.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c46900e6ef102ae75cd2ff16d5a73085228a8b0fcccb987d85f792673da00ef" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "hyper", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.75.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3258fa707f2f585ee3049d9550954b959002abd59176975150a01d5cf38ae3f" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfb9021f581b71870a17eac25b52335b82211cdc092e02b6876b2bcefa61666" +dependencies = [ + "aws-credential-types", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "form_urlencoded", + "hex", + "hmac", + "http 0.2.12", + "http 1.3.1", + "percent-encoding", + "sha2", + "time", + "tracing", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e190749ea56f8c42bf15dd76c65e14f8f765233e6df9b0506d9d934ebef867c" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-eventstream" +version = "0.60.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338a3642c399c0a5d157648426110e199ca7fd1c689cc395676b81aa563700c4" +dependencies = [ + "aws-smithy-types", + "bytes", + "crc32fast", +] + +[[package]] +name = "aws-smithy-http" +version = "0.62.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99335bec6cdc50a346fda1437f9fefe33abf8c99060739a546a16457f2862ca9" +dependencies = [ + "aws-smithy-eventstream", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.3.1", + "http-body 0.4.6", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.61.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a16e040799d29c17412943bdbf488fd75db04112d0c0d4b9290bacf5ae0014b9" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9364d5989ac4dd918e5cc4c4bdcc61c9be17dcd2586ea7f69e348fc7c6cab393" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14302f06d1d5b7d333fd819943075b13d27c7700b414f574c3c35859bfb55d5e" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.3.1", + "http-body 0.4.6", + "http-body 1.0.1", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9852b9226cb60b78ce9369022c0df678af1cac231c882d5da97a0c4e03be6e67" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.3.1", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-types" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d498595448e43de7f4296b7b7a18a8a02c61ec9349128c80a368f7c3b4ab11a8" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "http 0.2.12", + "http 1.3.1", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", +] + +[[package]] +name = "aws-smithy-wasm" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4c57c179a9f0c23cf3a9c38a89605bb6439c05be5df6674a7e1b039b6937bd" +dependencies = [ + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "http 1.3.1", + "tracing", + "wasi 0.12.1+wasi-0.2.0", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db87b96cb1b16c024980f133968d52882ca0daaee3a086c6decc500f6c99728" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a322fec39e4df22777ed3ad8ea868ac2f94cd15e1a55f6ee8d8d6305057689a" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "rustc_version", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + [[package]] name = "base64" version = "0.21.7" @@ -59,12 +421,31 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "bitflags" version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.17.0" @@ -77,6 +458,16 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + [[package]] name = "camino" version = "1.1.9" @@ -139,12 +530,31 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cordyceps" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a" +dependencies = [ + "loom", + "tracing", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -154,6 +564,42 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "diatomic-waker" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -165,6 +611,12 @@ dependencies = [ "syn", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -180,6 +632,18 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.1" @@ -226,6 +690,19 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-buffered" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe940397c8b744b9c2c974791c2c08bca2c3242ce0290393249e98f215a00472" +dependencies = [ + "cordyceps", + "diatomic-waker", + "futures-core", + "pin-project-lite", + "spin", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -236,6 +713,21 @@ dependencies = [ "futures-sink", ] +[[package]] +name = "futures-concurrency" +version = "7.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eb68017df91f2e477ed4bea586c59eaecaa47ed885a770d0444e21e62572cd2" +dependencies = [ + "fixedbitset", + "futures-buffered", + "futures-core", + "futures-lite", + "pin-project", + "slab", + "smallvec", +] + [[package]] name = "futures-core" version = "0.3.31" @@ -259,6 +751,19 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -300,6 +805,30 @@ dependencies = [ "slab", ] +[[package]] +name = "generator" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.3.3" @@ -309,9 +838,15 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi", + "wasi 0.14.2+wasi-0.2.4", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "git-version" version = "0.3.9" @@ -340,7 +875,7 @@ dependencies = [ "log", "mime", "nom", - "reqwest", + "reqwest 0.12.15 (git+https://github.com/golemcloud/reqwest?branch=update-may-2025)", "thiserror", "wasi-logger", "wit-bindgen 0.40.0", @@ -354,9 +889,33 @@ dependencies = [ "golem-llm", "golem-rust", "log", - "reqwest", + "reqwest 0.12.15 (git+https://github.com/golemcloud/reqwest?branch=update-may-2025)", + "serde", + "serde_json", + "wit-bindgen-rt 0.40.0", +] + +[[package]] +name = "golem-llm-bedrock" +version = "0.0.0" +dependencies = [ + "aws-config", + "aws-sdk-bedrockruntime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-wasm", + "aws-types", + "base64 0.22.1", + "bytes", + "golem-llm", + "golem-rust", + "infer", + "log", + "reqwest 0.12.15 (git+https://github.com/golemcloud/reqwest?branch=update-july-2025)", "serde", "serde_json", + "wasi 0.12.1+wasi-0.2.0", + "wasi-async-runtime", "wit-bindgen-rt 0.40.0", ] @@ -368,7 +927,7 @@ dependencies = [ "golem-llm", "golem-rust", "log", - "reqwest", + "reqwest 0.12.15 (git+https://github.com/golemcloud/reqwest?branch=update-may-2025)", "serde", "serde_json", "wit-bindgen-rt 0.40.0", @@ -383,7 +942,7 @@ dependencies = [ "golem-rust", "log", "mime_guess", - "reqwest", + "reqwest 0.12.15 (git+https://github.com/golemcloud/reqwest?branch=update-may-2025)", "serde", "serde_json", "url", @@ -398,7 +957,7 @@ dependencies = [ "golem-llm", "golem-rust", "log", - "reqwest", + "reqwest 0.12.15 (git+https://github.com/golemcloud/reqwest?branch=update-may-2025)", "serde", "serde_json", "wit-bindgen-rt 0.40.0", @@ -412,7 +971,7 @@ dependencies = [ "golem-llm", "golem-rust", "log", - "reqwest", + "reqwest 0.12.15 (git+https://github.com/golemcloud/reqwest?branch=update-may-2025)", "serde", "serde_json", "wit-bindgen-rt 0.40.0", @@ -457,6 +1016,16 @@ dependencies = [ "wit-bindgen-rt 0.40.0", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + [[package]] name = "hashbrown" version = "0.15.3" @@ -481,6 +1050,32 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http" version = "1.3.1" @@ -492,6 +1087,74 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.3.1", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.3.1", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "iana-time-zone" version = "0.1.63" @@ -636,10 +1299,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.3", "serde", ] +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" + [[package]] name = "itoa" version = "1.0.15" @@ -656,6 +1325,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "leb128" version = "0.2.5" @@ -686,6 +1361,28 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "memchr" version = "2.7.4" @@ -733,6 +1430,31 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -742,18 +1464,65 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -775,6 +1544,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "prettyplease" version = "0.2.32" @@ -804,10 +1579,82 @@ dependencies = [ ] [[package]] -name = "r-efi" -version = "5.2.0" +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + +[[package]] +name = "regex-syntax" +version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.15" +source = "git+https://github.com/golemcloud/reqwest?branch=update-july-2025#9e0c586a3f2fc2f9fe32ddf46c2a49152777693b" +dependencies = [ + "async-iterator", + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures", + "futures-concurrency", + "http 1.3.1", + "mime", + "percent-encoding", + "serde", + "serde_json", + "serde_urlencoded", + "url", + "wasi 0.12.1+wasi-0.2.0", + "wasi-async-runtime", +] [[package]] name = "reqwest" @@ -819,7 +1666,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "http", + "http 1.3.1", "mime", "percent-encoding", "serde", @@ -831,6 +1678,21 @@ dependencies = [ "wit-bindgen-rt 0.41.0", ] +[[package]] +name = "rustc-demangle" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustversion" version = "1.0.21" @@ -843,6 +1705,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "semver" version = "1.0.26" @@ -902,6 +1770,26 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[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" @@ -932,12 +1820,24 @@ dependencies = [ "smallvec", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.101" @@ -989,6 +1889,45 @@ 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.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.1" @@ -999,6 +1938,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tokio" +version = "1.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +dependencies = [ + "backtrace", + "pin-project-lite", +] + [[package]] name = "topological-sort" version = "0.2.2" @@ -1011,6 +1960,79 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +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.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + [[package]] name = "unicase" version = "2.8.1" @@ -1046,6 +2068,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -1065,6 +2093,42 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.12.1+wasi-0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af274f03e73b7d85551b3f9e97b8a04d5c9aec703cfc227a3fe0595a7561c67a" +dependencies = [ + "wit-bindgen 0.19.2", +] + [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" @@ -1074,6 +2138,17 @@ dependencies = [ "wit-bindgen-rt 0.39.0", ] +[[package]] +name = "wasi-async-runtime" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df0b7f89869b578aa56d8e4749776be8949ba9abda52fc8d5c15f02e901e022" +dependencies = [ + "hashbrown 0.14.5", + "slab", + "wasi 0.12.1+wasi-0.2.0", +] + [[package]] name = "wasi-logger" version = "0.1.2" @@ -1214,11 +2289,55 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" dependencies = [ "bitflags", - "hashbrown", + "hashbrown 0.15.3", "indexmap", "semver", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core", +] + [[package]] name = "windows-core" version = "0.61.2" @@ -1232,6 +2351,17 @@ dependencies = [ "windows-strings", ] +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.60.0" @@ -1260,6 +2390,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core", + "windows-link", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -1278,6 +2418,88 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b37d270da94012e0ac490ac633ad5bdd76a10a3fb15069edb033c1b771ce931f" +dependencies = [ + "bitflags", +] + [[package]] name = "wit-bindgen" version = "0.24.0" @@ -1496,6 +2718,12 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[package]] name = "yoke" version = "0.8.0" @@ -1520,6 +2748,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.6" @@ -1541,6 +2789,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zerotrie" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 7bea1e1e5..08f354fd9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "llm/llm", + "llm/bedrock", "llm/anthropic", "llm/grok", "llm/ollama", @@ -16,9 +17,9 @@ lto = true opt-level = 's' [workspace.dependencies] +golem-llm = { path = "llm/llm", version = "0.0.0", default-features = false } golem-rust = "1.6.0" log = "0.4.27" -golem-llm = { path = "llm/llm", version = "0.0.0", default-features = false } reqwest = { git = "https://github.com/golemcloud/reqwest", branch = "update-may-2025", features = [ "json", ] } diff --git a/Makefile.toml b/Makefile.toml index cc443bc6a..bfc2c6acb 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -137,7 +137,7 @@ script = ''' is_portable = eq ${1} "--portable" -targets = array llm_openai llm_anthropic llm_grok llm_openrouter llm_ollama +targets = array llm_openai llm_anthropic llm_grok llm_openrouter llm_ollama llm_bedrock for target in ${targets} if is_portable cp target/wasm32-wasip1/debug/golem_${target}.wasm components/debug/golem_${target}-portable.wasm @@ -153,7 +153,7 @@ script = ''' is_portable = eq ${1} "--portable" -targets = array llm_openai llm_anthropic llm_grok llm_openrouter llm_ollama +targets = array llm_openai llm_anthropic llm_grok llm_openrouter llm_ollama llm_bedrock for target in ${targets} if is_portable cp target/wasm32-wasip1/release/golem_${target}.wasm components/release/golem_${target}-portable.wasm diff --git a/README.md b/README.md index 8cf5f70a1..26ecdc83d 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,13 @@ There are 8 published WASM files for each release: | `golem-llm-grok.wasm` | LLM implementation for xAI (Grok), using custom Golem specific durability features | | `golem-llm-openai.wasm` | LLM implementation for OpenAI, using custom Golem specific durability features | | `golem-llm-openrouter.wasm` | LLM implementation for OpenRouter, using custom Golem specific durability features | +| `golem-llm-bedrock.wasm` | LLM implementation for Amazon Bedrock, using custom Golem specific durability features | | `golem-llm-anthropic-portable.wasm` | LLM implementation for Anthropic AI, with no Golem specific dependencies. | | `golem-llm-ollama-portable.wasm` | LLM implementation for Ollama, with no Golem specific dependencies. | | `golem-llm-grok-portable.wasm` | LLM implementation for xAI (Grok), with no Golem specific dependencies. | | `golem-llm-openai-portable.wasm` | LLM implementation for OpenAI, with no Golem specific dependencies. | | `golem-llm-openrouter-portable.wasm` | LLM implementation for OpenRouter, with no Golem specific dependencies. | +| `golem-llm-bedrock-portable.wasm` | LLM implementation for Amazon Bedrock, with no Golem specific dependencies. | Every component **exports** the same `golem:llm` interface, [defined here](wit/golem-llm.wit). @@ -37,6 +39,7 @@ Each provider has to be configured with an API key passed as an environment vari | OpenAI | `OPENAI_API_KEY` | | OpenRouter | `OPENROUTER_API_KEY` | | Ollama | `GOLEM_OLLAMA_BASE_URL` | +| Amazon Bedrock | `AWS_ACCESS_KEY_ID`, `AWS_REGION`, `AWS_SECRET_ACCESS_KEY` and `AWS_SESSION_TOKEN` (optional) | Additionally, setting the `GOLEM_LLM_LOG=trace` environment variable enables trace logging for all the communication with the underlying LLM provider. @@ -145,6 +148,8 @@ Then build and deploy the _test application_. Select one of the following profil | `openai-release` | Uses the OpenAI LLM implementation and compiles the code in release profile | | `openrouter-debug` | Uses the OpenRouter LLM implementation and compiles the code in debug profile | | `openrouter-release` | Uses the OpenRouter LLM implementation and compiles the code in release profile | +| `bedrock-debug` | Uses the Amazon Bedrock LLM implementation and compiles the code in debug profile | +| `bedrock-release` | Uses the Amazon Bedrock LLM implementation and compiles the code in release profile | ```bash cd test diff --git a/llm/Makefile.toml b/llm/Makefile.toml index 5b7b92a89..f1ddb40fe 100644 --- a/llm/Makefile.toml +++ b/llm/Makefile.toml @@ -5,6 +5,7 @@ skip_core_tasks = true [tasks.build] run_task = { name = [ "build-anthropic", + "build-bedrock", "build-grok", "build-openai", "build-openrouter", @@ -14,6 +15,7 @@ run_task = { name = [ [tasks.build-portable] run_task = { name = [ "build-anthropic-portable", + "build-bedrock-portable", "build-grok-portable", "build-openai-portable", "build-openrouter-portable", @@ -23,6 +25,7 @@ run_task = { name = [ [tasks.release-build] run_task = { name = [ "release-build-anthropic", + "release-build-bedrock", "release-build-grok", "release-build-openai", "release-build-openrouter", @@ -32,6 +35,7 @@ run_task = { name = [ [tasks.release-build-portable] run_task = { name = [ "release-build-anthropic-portable", + "release-build-bedrock-portable", "release-build-grok-portable", "release-build-openai-portable", "release-build-openrouter-portable", @@ -49,6 +53,17 @@ install_crate = { crate_name = "cargo-component", version = "0.20.0" } command = "cargo-component" args = ["build", "-p", "golem-llm-ollama", "--no-default-features"] +[tasks.build-bedrock] +install_crate = { crate_name = "cargo-component", version = "0.20.0" } +command = "cargo-component" +args = ["build", "-p", "golem-llm-bedrock"] + + +[tasks.build-bedrock-portable] +install_crate = { crate_name = "cargo-component", version = "0.20.0" } +command = "cargo-component" +args = ["build", "-p", "golem-llm-bedrock", "--no-default-features"] + [tasks.build-anthropic] install_crate = { crate_name = "cargo-component", version = "0.20.0" } @@ -100,6 +115,21 @@ install_crate = { crate_name = "cargo-component", version = "0.20.0" } command = "cargo-component" args = ["build", "-p", "golem-llm-ollama", "--release", "--no-default-features"] +[tasks.release-build-bedrock] +install_crate = { crate_name = "cargo-component", version = "0.20.0" } +command = "cargo-component" +args = ["build", "-p", "golem-llm-bedrock", "--release"] + +[tasks.release-build-bedrock-portable] +install_crate = { crate_name = "cargo-component", version = "0.20.0" } +command = "cargo-component" +args = [ + "build", + "-p", + "golem-llm-bedrock", + "--release", + "--no-default-features", +] [tasks.release-build-anthropic] install_crate = { crate_name = "cargo-component", version = "0.20.0" } @@ -163,7 +193,7 @@ dependencies = ["wit-update"] script_runner = "@duckscript" script = """ -modules = array llm openai anthropic grok openrouter ollama +modules = array llm openai anthropic grok openrouter ollama bedrock for module in ${modules} rm -r ${module}/wit/deps @@ -207,4 +237,6 @@ golem-cli app clean golem-cli app build -b openrouter-debug golem-cli app clean golem-cli app build -b ollama-debug +golem-cli app clean +golem-cli app build -b bedrock-debug ''' diff --git a/llm/anthropic/src/conversions.rs b/llm/anthropic/src/conversions.rs index e332f1391..e7d3175a0 100644 --- a/llm/anthropic/src/conversions.rs +++ b/llm/anthropic/src/conversions.rs @@ -130,7 +130,7 @@ pub fn process_response(response: MessagesResponse) -> ChatEvent { Err(e) => { return ChatEvent::Error(Error { code: ErrorCode::InvalidRequest, - message: format!("Failed to decode base64 image data: {}", e), + message: format!("Failed to decode base64 image data: {e}"), provider_error_json: None, }); } diff --git a/llm/bedrock/Cargo.toml b/llm/bedrock/Cargo.toml new file mode 100644 index 000000000..69122536d --- /dev/null +++ b/llm/bedrock/Cargo.toml @@ -0,0 +1,67 @@ +[package] +name = "golem-llm-bedrock" +version = "0.0.0" +edition = "2021" +license = "Apache-2.0" +homepage = "https://golem.cloud" +repository = "https://github.com/golemcloud/golem-llm" +description = "WebAssembly component for working with Amazon Bedrock APIs, with special support for Golem Cloud" + +[dependencies] +# AWS SDK crates +aws-config = { version = "1.5.19", default-features = false, features = [ + "behavior-version-latest", +] } +aws-types = { version = "1.3.4", default-features = false } +aws-smithy-wasm = { version = "0.1.4", default-features = false } +aws-sdk-bedrockruntime = { version = "1.56.0", default-features = false } +aws-smithy-types = { version = "1.3.1" } +aws-smithy-runtime-api = "1.8.3" + +wasi-async-runtime = "0.1.2" +wasi = "0.12.1+wasi-0.2.0" + +# To infer mime types of downloaded images before passing to bedrock +infer = { version = "0.19.0", default-features = false } + +golem-llm = { workspace = true } + +golem-rust = { workspace = true } +log = { workspace = true } +reqwest = { git = "https://github.com/golemcloud/reqwest", branch = "update-july-2025", features = [ + "json", + "async", +] } +serde = { workspace = true } +serde_json = { workspace = true } +wit-bindgen-rt = { workspace = true } +base64 = { workspace = true } +bytes = "1.10.1" + +[lib] +crate-type = ["cdylib"] +path = "src/lib.rs" + +[features] +default = ["durability"] +durability = [ + "golem-rust/durability", + "golem-llm/durability", + "golem-llm/nopoll", +] + +[package.metadata.component] +package = "golem:llm-bedrock" + +[package.metadata.component.bindings] +generate_unused_types = true + +[package.metadata.component.bindings.with] +"golem:llm/llm@1.0.0" = "golem_llm::golem::llm::llm" + +[package.metadata.component.target] +path = "wit" + +[package.metadata.component.target.dependencies] +"golem:llm" = { path = "wit/deps/golem-llm" } +"wasi:io" = { path = "wit/deps/wasi:io" } diff --git a/llm/bedrock/src/async_utils.rs b/llm/bedrock/src/async_utils.rs new file mode 100644 index 000000000..42b6a980d --- /dev/null +++ b/llm/bedrock/src/async_utils.rs @@ -0,0 +1,49 @@ +use std::future::Future; + +pub fn get_async_runtime() -> AsyncRuntime { + AsyncRuntime +} + +pub struct AsyncRuntime; + +impl AsyncRuntime { + pub fn block_on(self, f: F) -> Fut::Output + where + F: FnOnce(wasi_async_runtime::Reactor) -> Fut, + Fut: Future, + { + wasi_async_runtime::block_on(f) + } +} + +#[derive(Clone)] +pub struct UnsafeFuture { + inner: Fut, +} + +impl UnsafeFuture +where + F: Future, +{ + pub fn new(inner: F) -> Self { + Self { inner } + } +} + +unsafe impl Send for UnsafeFuture where F: Future {} +unsafe impl Sync for UnsafeFuture where F: Future {} + +impl Future for UnsafeFuture +where + F: Future, +{ + type Output = F::Output; + + fn poll( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + let pinned_future = unsafe { self.as_mut().map_unchecked_mut(|this| &mut this.inner) }; + pinned_future.poll(cx) + } +} diff --git a/llm/bedrock/src/bindings.rs b/llm/bedrock/src/bindings.rs new file mode 100644 index 000000000..6120a310e --- /dev/null +++ b/llm/bedrock/src/bindings.rs @@ -0,0 +1,52 @@ +// Generated by `wit-bindgen` 0.36.0. DO NOT EDIT! +// Options used: +// * runtime_path: "wit_bindgen_rt" +// * with "golem:llm/llm@1.0.0" = "golem_llm::golem::llm::llm" +// * generate_unused_types +use golem_llm::golem::llm::llm as __with_name0; +#[cfg(target_arch = "wasm32")] +#[link_section = "component-type:wit-bindgen:0.36.0:golem:llm-bedrock@1.0.0:llm-library:encoded world"] +#[doc(hidden)] +pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 1760] = *b"\ +\0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07\xde\x0c\x01A\x02\x01\ +A\x02\x01BO\x01m\x04\x04user\x09assistant\x06system\x04tool\x04\0\x04role\x03\0\0\ +\x01m\x06\x0finvalid-request\x15authentication-failed\x13rate-limit-exceeded\x0e\ +internal-error\x0bunsupported\x07unknown\x04\0\x0aerror-code\x03\0\x02\x01m\x06\x04\ +stop\x06length\x0atool-calls\x0econtent-filter\x05error\x05other\x04\0\x0dfinish\ +-reason\x03\0\x04\x01m\x03\x03low\x04high\x04auto\x04\0\x0cimage-detail\x03\0\x06\ +\x01k\x07\x01r\x02\x03urls\x06detail\x08\x04\0\x09image-url\x03\0\x09\x01p}\x01r\ +\x03\x04data\x0b\x09mime-types\x06detail\x08\x04\0\x0cimage-source\x03\0\x0c\x01\ +q\x02\x03url\x01\x0a\0\x06inline\x01\x0d\0\x04\0\x0fimage-reference\x03\0\x0e\x01\ +q\x02\x04text\x01s\0\x05image\x01\x0f\0\x04\0\x0ccontent-part\x03\0\x10\x01ks\x01\ +p\x11\x01r\x03\x04role\x01\x04name\x12\x07content\x13\x04\0\x07message\x03\0\x14\ +\x01r\x03\x04names\x0bdescription\x12\x11parameters-schemas\x04\0\x0ftool-defini\ +tion\x03\0\x16\x01r\x03\x02ids\x04names\x0earguments-jsons\x04\0\x09tool-call\x03\ +\0\x18\x01ky\x01r\x04\x02ids\x04names\x0bresult-jsons\x11execution-time-ms\x1a\x04\ +\0\x0ctool-success\x03\0\x1b\x01r\x04\x02ids\x04names\x0derror-messages\x0aerror\ +-code\x12\x04\0\x0ctool-failure\x03\0\x1d\x01q\x02\x07success\x01\x1c\0\x05error\ +\x01\x1e\0\x04\0\x0btool-result\x03\0\x1f\x01r\x02\x03keys\x05values\x04\0\x02kv\ +\x03\0!\x01kv\x01ps\x01k$\x01p\x17\x01p\"\x01r\x07\x05models\x0btemperature#\x0a\ +max-tokens\x1a\x0estop-sequences%\x05tools&\x0btool-choice\x12\x10provider-optio\ +ns'\x04\0\x06config\x03\0(\x01r\x03\x0cinput-tokens\x1a\x0doutput-tokens\x1a\x0c\ +total-tokens\x1a\x04\0\x05usage\x03\0*\x01k\x05\x01k+\x01r\x05\x0dfinish-reason,\ +\x05usage-\x0bprovider-id\x12\x09timestamp\x12\x16provider-metadata-json\x12\x04\ +\0\x11response-metadata\x03\0.\x01p\x19\x01r\x04\x02ids\x07content\x13\x0atool-c\ +alls0\x08metadata/\x04\0\x11complete-response\x03\01\x01r\x03\x04code\x03\x07mes\ +sages\x13provider-error-json\x12\x04\0\x05error\x03\03\x01q\x03\x07message\x012\0\ +\x0ctool-request\x010\0\x05error\x014\0\x04\0\x0achat-event\x03\05\x01k\x13\x01k\ +0\x01r\x02\x07content7\x0atool-calls8\x04\0\x0cstream-delta\x03\09\x01q\x03\x05d\ +elta\x01:\0\x06finish\x01/\0\x05error\x014\0\x04\0\x0cstream-event\x03\0;\x04\0\x0b\ +chat-stream\x03\x01\x01h=\x01p<\x01k?\x01@\x01\x04self>\0\xc0\0\x04\0\x1c[method\ +]chat-stream.get-next\x01A\x01@\x01\x04self>\0?\x04\0%[method]chat-stream.blocki\ +ng-get-next\x01B\x01p\x15\x01@\x02\x08messages\xc3\0\x06config)\06\x04\0\x04send\ +\x01D\x01o\x02\x19\x20\x01p\xc5\0\x01@\x03\x08messages\xc3\0\x0ctool-results\xc6\ +\0\x06config)\06\x04\0\x08continue\x01G\x01i=\x01@\x02\x08messages\xc3\0\x06conf\ +ig)\0\xc8\0\x04\0\x06stream\x01I\x04\0\x13golem:llm/llm@1.0.0\x05\0\x04\0#golem:\ +llm-bedrock/llm-library@1.0.0\x04\0\x0b\x11\x01\0\x0bllm-library\x03\0\0\0G\x09p\ +roducers\x01\x0cprocessed-by\x02\x0dwit-component\x070.220.0\x10wit-bindgen-rust\ +\x060.36.0"; +#[inline(never)] +#[doc(hidden)] +pub fn __link_custom_section_describing_imports() { + wit_bindgen_rt::maybe_link_cabi_realloc(); +} diff --git a/llm/bedrock/src/client.rs b/llm/bedrock/src/client.rs new file mode 100644 index 000000000..a63503457 --- /dev/null +++ b/llm/bedrock/src/client.rs @@ -0,0 +1,193 @@ +use crate::{ + async_utils::UnsafeFuture, + conversions::{self, from_converse_sdk_error, from_converse_stream_sdk_error, BedrockInput}, + stream::BedrockChatStream, + wasi_client::WasiClient, +}; +use aws_config::BehaviorVersion; +use aws_sdk_bedrockruntime::{ + self as bedrock, + config::{AsyncSleep, Sleep}, + operation::{ + converse::builders::ConverseFluentBuilder, + converse_stream::builders::ConverseStreamFluentBuilder, + }, +}; +use aws_types::region; +use golem_llm::{ + config::{get_config_key, get_config_key_or_none}, + golem::llm::llm, +}; +use log::trace; +use wasi::clocks::monotonic_clock; + +#[derive(Debug)] +pub struct Bedrock { + client: bedrock::Client, +} + +impl Bedrock { + pub async fn new(reactor: wasi_async_runtime::Reactor) -> Result { + let environment = BedrockEnvironment::load_from_env()?; + + let sdk_config = aws_config::defaults(BehaviorVersion::latest()) + .region(environment.aws_region()) + .http_client(WasiClient::new(reactor.clone())) + .credentials_provider(environment.aws_credentials()) + .sleep_impl(WasiSleep::new(reactor)) + .load() + .await; + let client = bedrock::Client::new(&sdk_config); + Ok(Self { client }) + } + + pub async fn converse( + &self, + messages: Vec, + config: llm::Config, + tool_results: Option>, + ) -> llm::ChatEvent { + let bedrock_input = BedrockInput::from(messages, config, tool_results); + + match bedrock_input { + Err(err) => llm::ChatEvent::Error(err), + Ok(input) => { + trace!("Sending request to AWS Bedrock: {input:?}"); + let model_id = input.model_id.clone(); + let response = self + .init_converse(input) + .send() + .await + .map_err(|e| from_converse_sdk_error(model_id, e)); + + match response { + Err(err) => llm::ChatEvent::Error(err), + Ok(response) => { + let event = match response.stop_reason() { + bedrock::types::StopReason::ToolUse => { + conversions::converse_output_to_tool_calls(response) + .map(llm::ChatEvent::ToolRequest) + } + _ => conversions::converse_output_to_complete_response(response) + .map(llm::ChatEvent::Message), + }; + + event.unwrap_or_else(llm::ChatEvent::Error) + } + } + } + } + } + + pub async fn converse_stream( + &self, + messages: Vec, + config: llm::Config, + ) -> BedrockChatStream { + let bedrock_input = BedrockInput::from(messages, config, None); + + match bedrock_input { + Err(err) => BedrockChatStream::failed(err), + Ok(input) => { + trace!("Sending request to AWS Bedrock: {input:?}"); + let model_id = input.model_id.clone(); + let response = self + .init_converse_stream(input) + .send() + .await + .map_err(|e| from_converse_stream_sdk_error(model_id, e)); + + trace!("Creating AWS Bedrock event stream"); + match response { + Ok(response) => BedrockChatStream::new(response.stream), + Err(error) => BedrockChatStream::failed(error), + } + } + } + } + + fn init_converse(&self, input: conversions::BedrockInput) -> ConverseFluentBuilder { + self.client + .converse() + .model_id(input.model_id) + .set_system(Some(input.system_instructions)) + .set_messages(Some(input.messages)) + .inference_config(input.inference_configuration) + .set_tool_config(input.tools) + .additional_model_request_fields(input.additional_fields) + } + + fn init_converse_stream( + &self, + input: conversions::BedrockInput, + ) -> ConverseStreamFluentBuilder { + self.client + .converse_stream() + .model_id(input.model_id) + .set_system(Some(input.system_instructions)) + .set_messages(Some(input.messages)) + .inference_config(input.inference_configuration) + .set_tool_config(input.tools) + .additional_model_request_fields(input.additional_fields) + } +} + +#[derive(Debug)] +pub struct BedrockEnvironment { + access_key_id: String, + region: String, + secret_access_key: String, + session_token: Option, +} + +impl BedrockEnvironment { + pub fn load_from_env() -> Result { + Ok(Self { + access_key_id: get_config_key("AWS_ACCESS_KEY_ID")?, + region: get_config_key("AWS_REGION")?, + secret_access_key: get_config_key("AWS_SECRET_ACCESS_KEY")?, + session_token: get_config_key_or_none("AWS_SESSION_TOKEN"), + }) + } + + fn aws_region(&self) -> region::Region { + region::Region::new(self.region.clone()) + } + + fn aws_credentials(&self) -> bedrock::config::Credentials { + bedrock::config::Credentials::new( + self.access_key_id.clone(), + self.secret_access_key.clone(), + self.session_token.clone(), + None, + "llm-bedrock", + ) + } +} + +#[derive(Debug, Clone)] +struct WasiSleep { + reactor: wasi_async_runtime::Reactor, +} + +impl WasiSleep { + fn new(reactor: wasi_async_runtime::Reactor) -> Self { + Self { reactor } + } +} + +impl AsyncSleep for WasiSleep { + fn sleep(&self, duration: std::time::Duration) -> Sleep { + let reactor = self.reactor.clone(); + let fut = async move { + let nanos = duration.as_nanos() as u64; + let pollable = monotonic_clock::subscribe_duration(nanos); + + reactor.wait_for(pollable).await; + }; + Sleep::new(Box::pin(UnsafeFuture::new(fut))) + } +} + +unsafe impl Send for WasiSleep {} +unsafe impl Sync for WasiSleep {} diff --git a/llm/bedrock/src/conversions.rs b/llm/bedrock/src/conversions.rs new file mode 100644 index 000000000..f0733bdfd --- /dev/null +++ b/llm/bedrock/src/conversions.rs @@ -0,0 +1,620 @@ +use aws_smithy_types::{Document, Number}; +use std::collections::HashMap; + +use aws_sdk_bedrockruntime::{ + self as bedrock, + error::SdkError, + operation::{converse, converse_stream}, + types::{ + ContentBlockDeltaEvent, ContentBlockStartEvent, ConversationRole, + ConverseStreamMetadataEvent, ConverseStreamOutput, ImageBlock, ImageFormat, + InferenceConfiguration, MessageStopEvent, SystemContentBlock, Tool, ToolConfiguration, + ToolInputSchema, ToolSpecification, ToolUseBlock, + }, +}; +use golem_llm::golem::llm::llm; + +use crate::async_utils::get_async_runtime; + +#[derive(Debug)] +pub struct BedrockInput { + pub model_id: String, + pub system_instructions: Vec, + pub messages: Vec, + pub inference_configuration: InferenceConfiguration, + pub tools: Option, + pub additional_fields: aws_smithy_types::Document, +} + +impl BedrockInput { + pub fn from( + messages: Vec, + config: llm::Config, + tool_results: Option>, + ) -> Result { + let (mut user_messages, system_instructions) = + messages_to_bedrock_message_groups(messages)?; + + if let Some(tool_results) = tool_results { + user_messages.extend(tool_call_results_to_bedrock_tools(tool_results)?); + } + + let options = config + .provider_options + .into_iter() + .map(|kv| (kv.key, Document::String(kv.value))) + .collect::>(); + + Ok(BedrockInput { + model_id: config.model, + inference_configuration: InferenceConfiguration::builder() + .set_max_tokens(config.max_tokens.map(|x| x as i32)) + .set_temperature(config.temperature) + .set_stop_sequences(config.stop_sequences) + .set_top_p(options.get("top_p").and_then(|v| match v { + Document::String(v) => v.parse::().ok(), + _ => None, + })) + .build(), + messages: user_messages, + system_instructions, + tools: tool_defs_to_bedrock_tool_config(config.tools)?, + additional_fields: Document::Object(options), + }) + } +} + +fn tool_call_results_to_bedrock_tools( + results: Vec<(llm::ToolCall, llm::ToolResult)>, +) -> Result, llm::Error> { + let mut tool_calls: Vec = vec![]; + let mut tool_results: Vec = vec![]; + + for (tool_call, tool_result) in results { + tool_calls.push(bedrock::types::ContentBlock::ToolUse( + bedrock::types::ToolUseBlock::builder() + .tool_use_id(tool_call.id.clone()) + .name(tool_call.name) + .input(json_str_to_smithy_document(&tool_call.arguments_json)?) + .build() + .unwrap(), + )); + + tool_results.push(bedrock::types::ContentBlock::ToolResult( + bedrock::types::ToolResultBlock::builder() + .tool_use_id(tool_call.id) + .content(bedrock::types::ToolResultContentBlock::Text( + match tool_result { + llm::ToolResult::Success(success) => success.result_json, + llm::ToolResult::Error(failure) => failure.error_message, + }, + )) + .build() + .unwrap(), + )); + } + + Ok(vec![ + bedrock::types::Message::builder() + .role(ConversationRole::Assistant) + .set_content(Some(tool_calls)) + .build() + .unwrap(), + bedrock::types::Message::builder() + .role(ConversationRole::User) + .set_content(Some(tool_results)) + .build() + .unwrap(), + ]) +} + +fn tool_defs_to_bedrock_tool_config( + tools: Vec, +) -> Result, llm::Error> { + if tools.is_empty() { + return Ok(None); + } + + let mut specs: Vec = vec![]; + + for def in tools { + let schema = json_str_to_smithy_document(&def.parameters_schema)?; + + specs.push(Tool::ToolSpec( + ToolSpecification::builder() + .name(def.name) + .set_description(def.description) + .input_schema(ToolInputSchema::Json(schema)) + .build() + .unwrap(), + )); + } + + Ok(Some( + ToolConfiguration::builder() + .set_tools(Some(specs)) + .build() + .unwrap(), + )) +} + +fn messages_to_bedrock_message_groups( + messages: Vec, +) -> Result<(Vec, Vec), llm::Error> { + let mut user_messages: Vec = vec![]; + let mut system_instructions: Vec = vec![]; + + for message in messages { + if message.role == llm::Role::System { + for content in message.content { + if let llm::ContentPart::Text(text) = content { + system_instructions.push(SystemContentBlock::Text(text)); + } + } + } else { + let bedrock_content = content_part_to_bedrock_content_blocks(message.content)?; + user_messages.push( + bedrock::types::Message::builder() + .role(if message.role == llm::Role::User { + ConversationRole::User + } else { + ConversationRole::Assistant + }) + .set_content(Some(bedrock_content)) + .build() + .unwrap(), + ); + } + } + Ok((user_messages, system_instructions)) +} + +fn content_part_to_bedrock_content_blocks( + content_parts: Vec, +) -> Result, llm::Error> { + let mut bedrock_content_blocks: Vec = vec![]; + for part in content_parts { + match part { + llm::ContentPart::Text(text) => { + bedrock_content_blocks.push(bedrock::types::ContentBlock::Text(text.to_owned())); + } + llm::ContentPart::Image(image) => { + bedrock_content_blocks.push(image_ref_to_bedrock_image_content_block(image)?); + } + } + } + + Ok(bedrock_content_blocks) +} + +fn image_ref_to_bedrock_image_content_block( + image_reference: llm::ImageReference, +) -> Result { + Ok(match image_reference { + llm::ImageReference::Inline(image) => bedrock::types::ContentBlock::Image( + ImageBlock::builder() + .format(str_to_bedrock_mime_type(image.mime_type.as_ref())?) + .source(bedrock::types::ImageSource::Bytes(image.data.into())) + .build() + .unwrap(), + ), + llm::ImageReference::Url(url) => get_image_content_block_from_url(url.url.as_ref())?, + }) +} + +fn get_image_content_block_from_url(url: &str) -> Result { + let bytes = get_bytes_from_url(url)?; + + let kind = infer::get(&bytes); + + let mime = match kind { + Some(kind) => str_to_bedrock_mime_type(kind.mime_type())?, + None => { + return Err(custom_error( + llm::ErrorCode::InvalidRequest, + format!("Could not infer the mime type of the image downloaded from url: {url}"), + )); + } + }; + + Ok(bedrock::types::ContentBlock::Image( + ImageBlock::builder() + .format(mime) + .source(bedrock::types::ImageSource::Bytes(bytes.into())) + .build() + .unwrap(), + )) +} + +fn get_bytes_from_url(url: &str) -> Result, llm::Error> { + let runtime = get_async_runtime(); + + runtime.block_on(|reactor| async { + let client = reqwest::Client::builder(reactor) + .build() + .expect("Failed to initialize HTTP client"); + + let response = client.get(url).send().await.map_err(|err| { + custom_error( + llm::ErrorCode::InvalidRequest, + format!("Could not read image bytes from url: {url}, cause: {err}"), + ) + })?; + if !response.status().is_success() { + return Err(custom_error( + llm::ErrorCode::InvalidRequest, + format!( + "Could not read image bytes from url: {url}, cause: request failed with status: {}", + response.status() + ), + )); + } + + let bytes = response.bytes().await.map_err(|err| { + custom_error( + llm::ErrorCode::InvalidRequest, + format!("Could not read image bytes from url: {url}, cause: {err}"), + ) + })?; + + Ok(bytes.to_vec()) + + }) +} + +fn str_to_bedrock_mime_type(mime_type: &str) -> Result { + match mime_type { + "image/png" => Ok(ImageFormat::Png), + "image/jpeg" => Ok(ImageFormat::Jpeg), + "image/webp" => Ok(ImageFormat::Webp), + "image/gif" => Ok(ImageFormat::Gif), + other => Err(llm::Error { + code: llm::ErrorCode::Unsupported, + message: format!("Unsupported image type: {other}"), + provider_error_json: None, + }), + } +} + +pub fn converse_output_to_tool_calls( + response: converse::ConverseOutput, +) -> Result, llm::Error> { + let output = response.output().ok_or(custom_error( + llm::ErrorCode::InternalError, + "An error occurred while converting to tool calls: expected output to not be None" + .to_owned(), + ))?; + + match output.as_message() { + Err(_) => Err(custom_error( + llm::ErrorCode::InternalError, + "An error occurred while converting to tool calls: expected output to be a Message" + .to_owned(), + )), + Ok(message) => { + let mut tool_calls: Vec = vec![]; + for block in message.content.clone() { + if let bedrock::types::ContentBlock::ToolUse(tool) = block { + tool_calls.push(bedrock_tool_use_to_llm_tool_call(tool)?); + } + } + Ok(tool_calls) + } + } +} + +pub fn converse_output_to_complete_response( + response: converse::ConverseOutput, +) -> Result { + let output = response.output().ok_or(custom_error( + llm::ErrorCode::InternalError, + "An error occurred while converting to complete response: expected output to be not be None" + .to_owned(), + ))?; + + match output.as_message() { + Err(_) => Err(custom_error( + llm::ErrorCode::InternalError, + "An error occurred while converting to complete response: expected output to be a Message" + .to_owned(), + )), + Ok(message) => { + let mut content_parts: Vec = vec![]; + let mut tool_calls: Vec = vec![]; + for block in message.content.clone() { + match block { + bedrock::types::ContentBlock::Text(text) => { + content_parts.push(llm::ContentPart::Text(text.to_owned())); + } + bedrock::types::ContentBlock::Image(image) => { + content_parts.push(bedrock_image_to_llm_content_part(image)); + } + bedrock::types::ContentBlock::ToolUse(tool) => { + tool_calls.push(bedrock_tool_use_to_llm_tool_call(tool)?); + } + _ => {} + } + } + let metadata = converse_output_to_response_metadata(&response); + Ok(llm::CompleteResponse { + // bedrock does not return an id as part of the response struct. + // there may be one present in `additional_model_response_fields` + // but the schema varies depending on the model being invoked. Leaving it empty for now + // until we have a better solution for this. + id: "".to_owned(), + content: content_parts, + tool_calls, + metadata, + }) + } + } +} + +fn bedrock_tool_use_to_llm_tool_call(tool: ToolUseBlock) -> Result { + Ok(llm::ToolCall { + id: tool.tool_use_id, + name: tool.name, + arguments_json: serde_json::to_string(&smithy_document_to_json_value(tool.input)).map_err( + |err| { + custom_error( + llm::ErrorCode::InternalError, + format!("An error occurred while deserializing tool use arguments: {err}"), + ) + }, + )?, + }) +} + +fn converse_output_to_response_metadata( + response: &converse::ConverseOutput, +) -> llm::ResponseMetadata { + llm::ResponseMetadata { + finish_reason: Some(bedrock_stop_reason_to_finish_reason(response.stop_reason())), + usage: response.usage().map(bedrock_usage_to_llm_usage), + provider_id: Some("bedrock".to_owned()), + provider_metadata_json: response + .additional_model_response_fields + .clone() + .and_then(smithy_document_to_metadata_json), + timestamp: None, + } +} + +fn smithy_document_to_metadata_json(doc: Document) -> Option { + serde_json::to_string(&smithy_document_to_json_value(doc)).ok() +} + +fn bedrock_usage_to_llm_usage(usage: &bedrock::types::TokenUsage) -> llm::Usage { + llm::Usage { + input_tokens: Some(usage.input_tokens() as u32), + output_tokens: Some(usage.output_tokens() as u32), + total_tokens: Some(usage.total_tokens() as u32), + } +} + +fn bedrock_stop_reason_to_finish_reason(reason: &bedrock::types::StopReason) -> llm::FinishReason { + match reason { + bedrock::types::StopReason::StopSequence | bedrock::types::StopReason::EndTurn => { + llm::FinishReason::Stop + } + bedrock::types::StopReason::ToolUse => llm::FinishReason::ToolCalls, + bedrock::types::StopReason::MaxTokens => llm::FinishReason::Length, + bedrock::types::StopReason::ContentFiltered + | bedrock::types::StopReason::GuardrailIntervened => llm::FinishReason::ContentFilter, + _ => llm::FinishReason::Other, + } +} + +fn bedrock_image_to_llm_content_part(block: bedrock::types::ImageBlock) -> llm::ContentPart { + let mime_type = format!("image/{}", block.format.as_str()); + + let reference = match block.source { + Some(bedrock::types::ImageSource::Bytes(bytes)) => { + llm::ImageReference::Inline(llm::ImageSource { + mime_type, + data: bytes.into(), + detail: None, + }) + } + Some(bedrock::types::ImageSource::S3Location(location)) => { + llm::ImageReference::Url(llm::ImageUrl { + url: location.uri, + detail: None, + }) + } + _ => llm::ImageReference::Inline(llm::ImageSource { + mime_type, + data: vec![], + detail: None, + }), + }; + + llm::ContentPart::Image(reference) +} + +pub fn converse_stream_output_to_stream_event( + event: ConverseStreamOutput, +) -> Option { + match event { + ConverseStreamOutput::ContentBlockStart(block) => process_content_block_start_event(block), + ConverseStreamOutput::ContentBlockDelta(block) => process_content_block_delta_event(block), + ConverseStreamOutput::Metadata(metadata) => process_metadata_event(metadata), + ConverseStreamOutput::MessageStop(event) => process_message_stop_event(event), + _ => None, + } +} + +fn process_content_block_start_event(block: ContentBlockStartEvent) -> Option { + if let Some(start_info) = block.start { + if let Ok(tool_use) = start_info.as_tool_use() { + return Some(llm::StreamEvent::Delta(llm::StreamDelta { + content: None, + tool_calls: Some(vec![llm::ToolCall { + id: tool_use.tool_use_id.clone(), + name: tool_use.name.clone(), + arguments_json: "".to_owned(), + }]), + })); + } + } + None +} + +fn process_content_block_delta_event(block: ContentBlockDeltaEvent) -> Option { + if let Some(block_info) = block.delta { + if let Ok(tool_use) = block_info.as_tool_use() { + return Some(llm::StreamEvent::Delta(llm::StreamDelta { + content: None, + tool_calls: Some(vec![llm::ToolCall { + id: "".to_owned(), + name: "".to_owned(), + arguments_json: tool_use.input.clone(), + }]), + })); + } else if let Ok(text) = block_info.as_text() { + return Some(llm::StreamEvent::Delta(llm::StreamDelta { + content: Some(vec![llm::ContentPart::Text(text.clone())]), + tool_calls: None, + })); + } + } + None +} + +fn process_metadata_event(metadata: ConverseStreamMetadataEvent) -> Option { + Some(llm::StreamEvent::Finish(llm::ResponseMetadata { + finish_reason: None, + timestamp: None, + usage: metadata.usage().map(bedrock_usage_to_llm_usage), + provider_id: Some("bedrock".to_owned()), + provider_metadata_json: None, + })) +} + +fn process_message_stop_event(event: MessageStopEvent) -> Option { + Some(llm::StreamEvent::Finish(llm::ResponseMetadata { + finish_reason: Some(bedrock_stop_reason_to_finish_reason(event.stop_reason())), + timestamp: None, + usage: None, + provider_id: None, + provider_metadata_json: event + .additional_model_response_fields + .clone() + .and_then(smithy_document_to_metadata_json), + })) +} + +fn json_str_to_smithy_document(value: &str) -> Result { + let json_value: serde_json::Value = serde_json::from_str(value).map_err(|err| llm::Error { + code: llm::ErrorCode::InvalidRequest, + message: format!("Invalid tool schema: {err}"), + provider_error_json: None, + })?; + Ok(serde_json_to_smithy_document(json_value)) +} + +fn smithy_document_to_json_value(document: Document) -> serde_json::Value { + match document { + Document::Null => serde_json::Value::Null, + Document::Bool(b) => serde_json::Value::Bool(b), + Document::Number(num) => match num { + Number::NegInt(i) => serde_json::Value::Number(serde_json::Number::from(i)), + Number::PosInt(i) => serde_json::Value::Number(serde_json::Number::from(i)), + Number::Float(f) => serde_json::Value::Number(serde_json::Number::from_f64(f).unwrap()), + }, + Document::String(s) => serde_json::Value::String(s), + Document::Array(arr) => { + let mut items = vec![]; + for item in arr { + items.push(smithy_document_to_json_value(item)); + } + serde_json::Value::Array(items) + } + Document::Object(map) => { + let mut object = serde_json::Map::new(); + for (key, value) in map { + object.insert(key, smithy_document_to_json_value(value)); + } + serde_json::Value::Object(object) + } + } +} + +fn serde_json_to_smithy_document(value: serde_json::Value) -> Document { + match value { + serde_json::Value::Null => Document::Null, + serde_json::Value::Bool(b) => Document::Bool(b), + serde_json::Value::Number(num) => { + if num.is_i64() { + Document::Number(Number::NegInt(num.as_i64().unwrap())) + } else if num.is_u64() { + Document::Number(Number::PosInt(num.as_u64().unwrap())) + } else if num.is_f64() { + Document::Number(Number::Float(num.as_f64().unwrap())) + } else { + // fallback to string if not any of above number types + Document::String(num.to_string()) + } + } + serde_json::Value::String(s) => Document::String(s), + serde_json::Value::Array(arr) => { + let mut items = vec![]; + for item in arr { + items.push(serde_json_to_smithy_document(item)); + } + Document::Array(items) + } + serde_json::Value::Object(map) => { + let mut object = HashMap::new(); + for (key, value) in map { + object.insert(key, serde_json_to_smithy_document(value)); + } + Document::Object(object) + } + } +} + +pub fn from_converse_sdk_error( + model_id: String, + sdk_error: SdkError, +) -> llm::Error { + llm::Error { + code: llm::ErrorCode::InternalError, + message: format!("Error calling Bedrock model {model_id}: {sdk_error:?}",), + provider_error_json: None, + } +} + +pub fn from_converse_stream_sdk_error( + model_id: String, + sdk_error: SdkError, +) -> llm::Error { + llm::Error { + code: llm::ErrorCode::InternalError, + message: format!("Error calling Bedrock model {model_id}: {sdk_error:?}",), + provider_error_json: None, + } +} + +pub fn custom_error(code: llm::ErrorCode, message: String) -> llm::Error { + llm::Error { + code, + message, + provider_error_json: None, + } +} + +pub fn merge_metadata( + mut metadata1: llm::ResponseMetadata, + metadata2: llm::ResponseMetadata, +) -> llm::ResponseMetadata { + metadata1.usage = metadata1.usage.or(metadata2.usage); + metadata1.timestamp = metadata1.timestamp.or(metadata2.timestamp); + metadata1.provider_id = metadata1.provider_id.or(metadata2.provider_id); + metadata1.finish_reason = metadata1.finish_reason.or(metadata2.finish_reason); + metadata1.provider_metadata_json = metadata1 + .provider_metadata_json + .or(metadata2.provider_metadata_json); + + metadata1 +} diff --git a/llm/bedrock/src/lib.rs b/llm/bedrock/src/lib.rs new file mode 100644 index 000000000..54638f68d --- /dev/null +++ b/llm/bedrock/src/lib.rs @@ -0,0 +1,144 @@ +use async_utils::get_async_runtime; +use client::Bedrock; +use golem_llm::{ + durability::{DurableLLM, ExtendedGuest}, + golem::llm::llm::{self, ChatEvent, ChatStream, Config, Guest, Message, ToolCall, ToolResult}, + LOGGING_STATE, +}; +use golem_rust::bindings::wasi::clocks::monotonic_clock; +use stream::BedrockChatStream; + +mod async_utils; +mod client; +mod conversions; +mod stream; +mod wasi_client; + +struct BedrockComponent; + +impl Guest for BedrockComponent { + type ChatStream = BedrockChatStream; + + fn send(messages: Vec, config: Config) -> ChatEvent { + LOGGING_STATE.with_borrow_mut(|state| state.init()); + + let runtime = get_async_runtime(); + + runtime.block_on(|reactor| async { + let bedrock = get_bedrock_client(reactor).await; + + match bedrock { + Ok(client) => client.converse(messages, config, None).await, + Err(err) => ChatEvent::Error(err), + } + }) + } + + fn continue_( + messages: Vec, + tool_results: Vec<(ToolCall, ToolResult)>, + config: Config, + ) -> ChatEvent { + LOGGING_STATE.with_borrow_mut(|state| state.init()); + + let runtime = get_async_runtime(); + + runtime.block_on(|reactor| async { + let bedrock = get_bedrock_client(reactor).await; + + match bedrock { + Ok(client) => client.converse(messages, config, Some(tool_results)).await, + Err(err) => ChatEvent::Error(err), + } + }) + } + + fn stream(messages: Vec, config: Config) -> ChatStream { + ChatStream::new(Self::unwrapped_stream(messages, config)) + } +} + +impl ExtendedGuest for BedrockComponent { + fn unwrapped_stream( + messages: Vec, + config: golem_llm::golem::llm::llm::Config, + ) -> Self::ChatStream { + LOGGING_STATE.with_borrow_mut(|state| state.init()); + + let runtime = get_async_runtime(); + + runtime.block_on(|reactor| async { + let bedrock = get_bedrock_client(reactor).await; + + match bedrock { + Ok(client) => client.converse_stream(messages, config).await, + Err(err) => BedrockChatStream::failed(err), + } + }) + } + + fn retry_prompt( + original_messages: &[Message], + partial_result: &[llm::StreamDelta], + ) -> Vec { + let mut extended_messages = Vec::new(); + extended_messages.push(Message { + role: llm::Role::System, + name: None, + content: vec![ + llm::ContentPart::Text( + "You were asked the same question previously, but the response was interrupted before completion. \ + Please continue your response from where you left off. \ + Do not include the part of the response that was already seen. If the response starts with a new word and no punctuation then add a space to the beginning".to_string()), + ], + }); + extended_messages.push(Message { + role: llm::Role::User, + name: None, + content: vec![llm::ContentPart::Text( + "Here is the original question:".to_string(), + )], + }); + extended_messages.extend_from_slice(original_messages); + + let mut partial_result_as_content = Vec::new(); + for delta in partial_result { + if let Some(contents) = &delta.content { + partial_result_as_content.extend_from_slice(contents); + } + if let Some(tool_calls) = &delta.tool_calls { + for tool_call in tool_calls { + partial_result_as_content.push(llm::ContentPart::Text(format!( + "", + tool_call.id, tool_call.name, tool_call.arguments_json, + ))); + } + } + } + + extended_messages.push(Message { + role: llm::Role::User, + name: None, + content: vec![llm::ContentPart::Text( + "Here is the partial response that was successfully received:".to_string(), + )] + .into_iter() + .chain(partial_result_as_content) + .collect(), + }); + extended_messages + } + + fn subscribe(_stream: &Self::ChatStream) -> golem_rust::wasm_rpc::Pollable { + // this function will never get called in bedrock implementation because of `golem-llm/nopoll` feature flag + monotonic_clock::subscribe_duration(0) + } +} + +async fn get_bedrock_client(reactor: wasi_async_runtime::Reactor) -> Result { + Bedrock::new(reactor).await +} + +type DurableBedrockComponent = DurableLLM; + +golem_llm::export_llm!(DurableBedrockComponent with_types_in golem_llm); diff --git a/llm/bedrock/src/stream.rs b/llm/bedrock/src/stream.rs new file mode 100644 index 000000000..e9d37d799 --- /dev/null +++ b/llm/bedrock/src/stream.rs @@ -0,0 +1,121 @@ +use aws_sdk_bedrockruntime::{ + self as bedrock, primitives::event_stream::EventReceiver, + types::error::ConverseStreamOutputError, +}; +use golem_llm::golem::llm::llm; +use std::cell::{RefCell, RefMut}; + +use crate::{ + async_utils, + conversions::{converse_stream_output_to_stream_event, custom_error, merge_metadata}, +}; + +type BedrockEventSource = + EventReceiver; + +pub struct BedrockChatStream { + stream: RefCell>, + failure: Option, + finished: RefCell, +} + +impl BedrockChatStream { + pub fn new(stream: BedrockEventSource) -> BedrockChatStream { + BedrockChatStream { + stream: RefCell::new(Some(stream)), + failure: None, + finished: RefCell::new(false), + } + } + + pub fn failed(error: llm::Error) -> BedrockChatStream { + BedrockChatStream { + stream: RefCell::new(None), + failure: Some(error), + finished: RefCell::new(true), + } + } + + fn stream_mut(&self) -> RefMut> { + self.stream.borrow_mut() + } + + fn failure(&self) -> &Option { + &self.failure + } + + fn is_finished(&self) -> bool { + *self.finished.borrow() + } + + fn set_finished(&self) { + *self.finished.borrow_mut() = true; + } + fn get_single_event(&self) -> Option { + if let Some(stream) = self.stream_mut().as_mut() { + let runtime = async_utils::get_async_runtime(); + + runtime.block_on(|_| async move { + let token = stream.recv().await; + log::trace!("Bedrock stream event: {token:?}"); + + match token { + Ok(Some(output)) => { + log::trace!("Processing bedrock stream event: {output:?}"); + converse_stream_output_to_stream_event(output) + } + Ok(None) => { + log::trace!("running set_finished on stream due to None event received"); + self.set_finished(); + None + } + Err(error) => { + log::trace!("running set_finished on stream due to error: {error:?}"); + self.set_finished(); + Some(llm::StreamEvent::Error(custom_error( + llm::ErrorCode::InternalError, + format!("An error occurred while reading event stream: {error}"), + ))) + } + } + }) + } else if let Some(error) = self.failure() { + self.set_finished(); + Some(llm::StreamEvent::Error(error.clone())) + } else { + None + } + } +} + +impl llm::GuestChatStream for BedrockChatStream { + fn get_next(&self) -> Option> { + if self.is_finished() { + return Some(vec![]); + } + self.get_single_event().map(|event| { + if let llm::StreamEvent::Finish(metadata) = event.clone() { + if let Some(llm::StreamEvent::Finish(final_metadata)) = self.get_single_event() { + return vec![llm::StreamEvent::Finish(merge_metadata( + metadata, + final_metadata, + ))]; + } + } + vec![event] + }) + } + + fn blocking_get_next(&self) -> Vec { + let mut result = Vec::new(); + loop { + match self.get_next() { + Some(events) => { + result.extend(events); + break result; + } + None => continue, + } + } + } +} diff --git a/llm/bedrock/src/wasi_client.rs b/llm/bedrock/src/wasi_client.rs new file mode 100644 index 000000000..b1b91812e --- /dev/null +++ b/llm/bedrock/src/wasi_client.rs @@ -0,0 +1,174 @@ +use std::{io::Read, str::FromStr, sync::Arc}; + +use aws_sdk_bedrockruntime::{config, error::ConnectorError}; +use aws_smithy_runtime_api::{ + client::http::{ + HttpConnector, HttpConnectorFuture, HttpConnectorSettings, SharedHttpConnector, + }, + http::{Headers, Response, StatusCode}, +}; +use aws_smithy_types::body::SdkBody; +use reqwest::{ + header::{HeaderMap, HeaderName, HeaderValue}, + Method, +}; + +use crate::async_utils::UnsafeFuture; + +#[derive(Debug)] +pub struct WasiClient { + reactor: wasi_async_runtime::Reactor, +} + +impl WasiClient { + pub fn new(reactor: wasi_async_runtime::Reactor) -> Self { + Self { reactor } + } +} + +impl config::HttpClient for WasiClient { + fn http_connector( + &self, + settings: &HttpConnectorSettings, + _components: &config::RuntimeComponents, + ) -> SharedHttpConnector { + let client = reqwest::Client::builder(self.reactor.clone()) + .connect_timeout(settings.connect_timeout()) + .timeout(settings.read_timeout()) + .build() + .expect("Valid http client configuration"); + let connector = SharedWasiConnector::new(client); + SharedHttpConnector::new(connector) + } +} + +unsafe impl Send for WasiClient {} +unsafe impl Sync for WasiClient {} + +#[derive(Debug)] +struct SharedWasiConnector { + inner: Arc, +} + +impl SharedWasiConnector { + fn new(client: reqwest::Client) -> Self { + Self { + inner: Arc::new(WasiConnector(client)), + } + } +} + +#[derive(Debug)] +struct WasiConnector(reqwest::Client); + +unsafe impl Send for WasiConnector {} +unsafe impl Sync for WasiConnector {} + +impl WasiConnector { + async fn handle( + &self, + request: config::http::HttpRequest, + ) -> Result { + let method = Method::from_bytes(request.method().as_bytes()).expect("Valid http method"); + let url = request.uri().to_owned(); + let parts = request.into_parts(); + + let mut header_map = HeaderMap::new(); + + for header in parts.headers.iter() { + header_map.append( + HeaderName::from_str(header.0).expect("Valid http header name"), + HeaderValue::from_str(header.1).expect("Valid http header value"), + ); + } + + self.0 + .request(method, url) + .headers(header_map) + .body(reqwest::Body::new::(parts.body.into())) + .send() + .await + .map_err(|e| ConnectorError::other(e.into(), None)) + } +} + +impl HttpConnector for SharedWasiConnector { + fn call(&self, request: config::http::HttpRequest) -> HttpConnectorFuture { + let inner_clone = Arc::clone(&self.inner); + + let future = async move { + let response = inner_clone.handle(request).await?; + log::trace!("WasiConnector: response received {response:?}"); + + let status_code: StatusCode = response.status().into(); + let headers_map = response.headers().clone(); + let extensions = response.extensions().clone(); + + let body = response + .bytes() + .await + .map(|body| { + if body.is_empty() { + SdkBody::empty() + } else { + SdkBody::from(body) + } + }) + .map_err(|e| ConnectorError::other(e.into(), None))?; + + let mut headers = Headers::new(); + for header in headers_map { + if let Some(key) = header.0 { + if let Ok(value) = header.1.to_str() { + headers.insert(key.to_string(), value.to_string()); + } + } + } + + let mut sdk_response = Response::new(status_code, body); + *sdk_response.headers_mut() = headers; + sdk_response.add_extension(extensions); + + Ok(sdk_response) + }; + + HttpConnectorFuture::new(UnsafeFuture::new(future)) + } +} + +struct BodyReader { + body: SdkBody, + position: usize, +} + +impl From for BodyReader { + fn from(value: SdkBody) -> Self { + Self::new(value) + } +} + +impl BodyReader { + fn new(body: SdkBody) -> Self { + Self { body, position: 0 } + } +} + +impl Read for BodyReader { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let body_bytes = self.body.bytes(); + + match body_bytes { + Some(bytes) => { + if self.position >= bytes.len() { + return Ok(0); // EOF + } + let remaining = &bytes[self.position..]; + let amt = std::cmp::min(buf.len(), remaining.len()); + buf[..amt].copy_from_slice(&remaining[..amt]); + self.position += amt; + Ok(amt) + } + None => Ok(0), + } + } +} diff --git a/llm/bedrock/wit/bedrock.wit b/llm/bedrock/wit/bedrock.wit new file mode 100644 index 000000000..c2c3d19b9 --- /dev/null +++ b/llm/bedrock/wit/bedrock.wit @@ -0,0 +1,5 @@ +package golem:llm-bedrock@1.0.0; + +world llm-library { + include golem:llm/llm-library@1.0.0; +} diff --git a/llm/bedrock/wit/deps/golem-llm/golem-llm.wit b/llm/bedrock/wit/deps/golem-llm/golem-llm.wit new file mode 100644 index 000000000..67854470a --- /dev/null +++ b/llm/bedrock/wit/deps/golem-llm/golem-llm.wit @@ -0,0 +1,194 @@ +package golem:llm@1.0.0; + +interface llm { + // --- Roles, Error Codes, Finish Reasons --- + + enum role { + user, + assistant, + system, + tool, + } + + enum error-code { + invalid-request, + authentication-failed, + rate-limit-exceeded, + internal-error, + unsupported, + unknown, + } + + enum finish-reason { + stop, + length, + tool-calls, + content-filter, + error, + other, + } + + enum image-detail { + low, + high, + auto, + } + + // --- Message Content --- + + record image-url { + url: string, + detail: option, + } + + record image-source { + data: list, + mime-type: string, + detail: option, + } + + variant image-reference { + url(image-url), + inline(image-source), + } + + variant content-part { + text(string), + image(image-reference), + } + + record message { + role: role, + name: option, + content: list, + } + + // --- Tooling --- + + record tool-definition { + name: string, + description: option, + parameters-schema: string, + } + + record tool-call { + id: string, + name: string, + arguments-json: string, + } + + record tool-success { + id: string, + name: string, + result-json: string, + execution-time-ms: option, + } + + record tool-failure { + id: string, + name: string, + error-message: string, + error-code: option, + } + + variant tool-result { + success(tool-success), + error(tool-failure), + } + + // --- Configuration --- + + record kv { + key: string, + value: string, + } + + record config { + model: string, + temperature: option, + max-tokens: option, + stop-sequences: option>, + tools: list, + tool-choice: option, + provider-options: list, + } + + // --- Usage / Metadata --- + + record usage { + input-tokens: option, + output-tokens: option, + total-tokens: option, + } + + record response-metadata { + finish-reason: option, + usage: option, + provider-id: option, + timestamp: option, + provider-metadata-json: option, + } + + record complete-response { + id: string, + content: list, + tool-calls: list, + metadata: response-metadata, + } + + // --- Error Handling --- + + record error { + code: error-code, + message: string, + provider-error-json: option, + } + + // --- Chat Response Variants --- + + variant chat-event { + message(complete-response), + tool-request(list), + error(error), + } + + // --- Streaming --- + + record stream-delta { + content: option>, + tool-calls: option>, + } + + variant stream-event { + delta(stream-delta), + finish(response-metadata), + error(error), + } + + resource chat-stream { + get-next: func() -> option>; + blocking-get-next: func() -> list; + } + + // --- Core Functions --- + + send: func( + messages: list, + config: config + ) -> chat-event; + + continue: func( + messages: list, + tool-results: list>, + config: config + ) -> chat-event; + + %stream: func( + messages: list, + config: config + ) -> chat-stream; +} + +world llm-library { + export llm; +} diff --git a/llm/bedrock/wit/deps/wasi:io/error.wit b/llm/bedrock/wit/deps/wasi:io/error.wit new file mode 100644 index 000000000..97c606877 --- /dev/null +++ b/llm/bedrock/wit/deps/wasi:io/error.wit @@ -0,0 +1,34 @@ +package wasi:io@0.2.3; + +@since(version = 0.2.0) +interface error { + /// A resource which represents some error information. + /// + /// The only method provided by this resource is `to-debug-string`, + /// which provides some human-readable information about the error. + /// + /// In the `wasi:io` package, this resource is returned through the + /// `wasi:io/streams/stream-error` type. + /// + /// To provide more specific error information, other interfaces may + /// offer functions to "downcast" this error into more specific types. For example, + /// errors returned from streams derived from filesystem types can be described using + /// the filesystem's own error-code type. This is done using the function + /// `wasi:filesystem/types/filesystem-error-code`, which takes a `borrow` + /// parameter and returns an `option`. + /// + /// The set of functions which can "downcast" an `error` into a more + /// concrete type is open. + @since(version = 0.2.0) + resource error { + /// Returns a string that is suitable to assist humans in debugging + /// this error. + /// + /// WARNING: The returned string should not be consumed mechanically! + /// It may change across platforms, hosts, or other implementation + /// details. Parsing this string is a major platform-compatibility + /// hazard. + @since(version = 0.2.0) + to-debug-string: func() -> string; + } +} diff --git a/llm/bedrock/wit/deps/wasi:io/poll.wit b/llm/bedrock/wit/deps/wasi:io/poll.wit new file mode 100644 index 000000000..9bcbe8e03 --- /dev/null +++ b/llm/bedrock/wit/deps/wasi:io/poll.wit @@ -0,0 +1,47 @@ +package wasi:io@0.2.3; + +/// A poll API intended to let users wait for I/O events on multiple handles +/// at once. +@since(version = 0.2.0) +interface poll { + /// `pollable` represents a single I/O event which may be ready, or not. + @since(version = 0.2.0) + resource pollable { + + /// Return the readiness of a pollable. This function never blocks. + /// + /// Returns `true` when the pollable is ready, and `false` otherwise. + @since(version = 0.2.0) + ready: func() -> bool; + + /// `block` returns immediately if the pollable is ready, and otherwise + /// blocks until ready. + /// + /// This function is equivalent to calling `poll.poll` on a list + /// containing only this pollable. + @since(version = 0.2.0) + block: func(); + } + + /// Poll for completion on a set of pollables. + /// + /// This function takes a list of pollables, which identify I/O sources of + /// interest, and waits until one or more of the events is ready for I/O. + /// + /// The result `list` contains one or more indices of handles in the + /// argument list that is ready for I/O. + /// + /// This function traps if either: + /// - the list is empty, or: + /// - the list contains more elements than can be indexed with a `u32` value. + /// + /// A timeout can be implemented by adding a pollable from the + /// wasi-clocks API to the list. + /// + /// This function does not return a `result`; polling in itself does not + /// do any I/O so it doesn't fail. If any of the I/O sources identified by + /// the pollables has an error, it is indicated by marking the source as + /// being ready for I/O. + @since(version = 0.2.0) + poll: func(in: list>) -> list; +} diff --git a/llm/bedrock/wit/deps/wasi:io/streams.wit b/llm/bedrock/wit/deps/wasi:io/streams.wit new file mode 100644 index 000000000..0de084629 --- /dev/null +++ b/llm/bedrock/wit/deps/wasi:io/streams.wit @@ -0,0 +1,290 @@ +package wasi:io@0.2.3; + +/// WASI I/O is an I/O abstraction API which is currently focused on providing +/// stream types. +/// +/// In the future, the component model is expected to add built-in stream types; +/// when it does, they are expected to subsume this API. +@since(version = 0.2.0) +interface streams { + @since(version = 0.2.0) + use error.{error}; + @since(version = 0.2.0) + use poll.{pollable}; + + /// An error for input-stream and output-stream operations. + @since(version = 0.2.0) + variant stream-error { + /// The last operation (a write or flush) failed before completion. + /// + /// More information is available in the `error` payload. + /// + /// After this, the stream will be closed. All future operations return + /// `stream-error::closed`. + last-operation-failed(error), + /// The stream is closed: no more input will be accepted by the + /// stream. A closed output-stream will return this error on all + /// future operations. + closed + } + + /// An input bytestream. + /// + /// `input-stream`s are *non-blocking* to the extent practical on underlying + /// platforms. I/O operations always return promptly; if fewer bytes are + /// promptly available than requested, they return the number of bytes promptly + /// available, which could even be zero. To wait for data to be available, + /// use the `subscribe` function to obtain a `pollable` which can be polled + /// for using `wasi:io/poll`. + @since(version = 0.2.0) + resource input-stream { + /// Perform a non-blocking read from the stream. + /// + /// When the source of a `read` is binary data, the bytes from the source + /// are returned verbatim. When the source of a `read` is known to the + /// implementation to be text, bytes containing the UTF-8 encoding of the + /// text are returned. + /// + /// This function returns a list of bytes containing the read data, + /// when successful. The returned list will contain up to `len` bytes; + /// it may return fewer than requested, but not more. The list is + /// empty when no bytes are available for reading at this time. The + /// pollable given by `subscribe` will be ready when more bytes are + /// available. + /// + /// This function fails with a `stream-error` when the operation + /// encounters an error, giving `last-operation-failed`, or when the + /// stream is closed, giving `closed`. + /// + /// When the caller gives a `len` of 0, it represents a request to + /// read 0 bytes. If the stream is still open, this call should + /// succeed and return an empty list, or otherwise fail with `closed`. + /// + /// The `len` parameter is a `u64`, which could represent a list of u8 which + /// is not possible to allocate in wasm32, or not desirable to allocate as + /// as a return value by the callee. The callee may return a list of bytes + /// less than `len` in size while more bytes are available for reading. + @since(version = 0.2.0) + read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Read bytes from a stream, after blocking until at least one byte can + /// be read. Except for blocking, behavior is identical to `read`. + @since(version = 0.2.0) + blocking-read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Skip bytes from a stream. Returns number of bytes skipped. + /// + /// Behaves identical to `read`, except instead of returning a list + /// of bytes, returns the number of bytes consumed from the stream. + @since(version = 0.2.0) + skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Skip bytes from a stream, after blocking until at least one byte + /// can be skipped. Except for blocking behavior, identical to `skip`. + @since(version = 0.2.0) + blocking-skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Create a `pollable` which will resolve once either the specified stream + /// has bytes available to read or the other end of the stream has been + /// closed. + /// The created `pollable` is a child resource of the `input-stream`. + /// Implementations may trap if the `input-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } + + + /// An output bytestream. + /// + /// `output-stream`s are *non-blocking* to the extent practical on + /// underlying platforms. Except where specified otherwise, I/O operations also + /// always return promptly, after the number of bytes that can be written + /// promptly, which could even be zero. To wait for the stream to be ready to + /// accept data, the `subscribe` function to obtain a `pollable` which can be + /// polled for using `wasi:io/poll`. + /// + /// Dropping an `output-stream` while there's still an active write in + /// progress may result in the data being lost. Before dropping the stream, + /// be sure to fully flush your writes. + @since(version = 0.2.0) + resource output-stream { + /// Check readiness for writing. This function never blocks. + /// + /// Returns the number of bytes permitted for the next call to `write`, + /// or an error. Calling `write` with more bytes than this function has + /// permitted will trap. + /// + /// When this function returns 0 bytes, the `subscribe` pollable will + /// become ready when this function will report at least 1 byte, or an + /// error. + @since(version = 0.2.0) + check-write: func() -> result; + + /// Perform a write. This function never blocks. + /// + /// When the destination of a `write` is binary data, the bytes from + /// `contents` are written verbatim. When the destination of a `write` is + /// known to the implementation to be text, the bytes of `contents` are + /// transcoded from UTF-8 into the encoding of the destination and then + /// written. + /// + /// Precondition: check-write gave permit of Ok(n) and contents has a + /// length of less than or equal to n. Otherwise, this function will trap. + /// + /// returns Err(closed) without writing if the stream has closed since + /// the last call to check-write provided a permit. + @since(version = 0.2.0) + write: func( + contents: list + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 bytes, and then flush the stream. Block + /// until all of these operations are complete, or an error occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write`, and `flush`, and is implemented with the + /// following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while !contents.is_empty() { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, contents.len()); + /// let (chunk, rest) = contents.split_at(len); + /// this.write(chunk ); // eliding error handling + /// contents = rest; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + @since(version = 0.2.0) + blocking-write-and-flush: func( + contents: list + ) -> result<_, stream-error>; + + /// Request to flush buffered output. This function never blocks. + /// + /// This tells the output-stream that the caller intends any buffered + /// output to be flushed. the output which is expected to be flushed + /// is all that has been passed to `write` prior to this call. + /// + /// Upon calling this function, the `output-stream` will not accept any + /// writes (`check-write` will return `ok(0)`) until the flush has + /// completed. The `subscribe` pollable will become ready when the + /// flush has completed and the stream can accept more writes. + @since(version = 0.2.0) + flush: func() -> result<_, stream-error>; + + /// Request to flush buffered output, and block until flush completes + /// and stream is ready for writing again. + @since(version = 0.2.0) + blocking-flush: func() -> result<_, stream-error>; + + /// Create a `pollable` which will resolve once the output-stream + /// is ready for more writing, or an error has occurred. When this + /// pollable is ready, `check-write` will return `ok(n)` with n>0, or an + /// error. + /// + /// If the stream is closed, this pollable is always ready immediately. + /// + /// The created `pollable` is a child resource of the `output-stream`. + /// Implementations may trap if the `output-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) + subscribe: func() -> pollable; + + /// Write zeroes to a stream. + /// + /// This should be used precisely like `write` with the exact same + /// preconditions (must use check-write first), but instead of + /// passing a list of bytes, you simply pass the number of zero-bytes + /// that should be written. + @since(version = 0.2.0) + write-zeroes: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 zeroes, and then flush the stream. + /// Block until all of these operations are complete, or an error + /// occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write-zeroes`, and `flush`, and is implemented with + /// the following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while num_zeroes != 0 { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, num_zeroes); + /// this.write-zeroes(len); // eliding error handling + /// num_zeroes -= len; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + @since(version = 0.2.0) + blocking-write-zeroes-and-flush: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Read from one stream and write to another. + /// + /// The behavior of splice is equivalent to: + /// 1. calling `check-write` on the `output-stream` + /// 2. calling `read` on the `input-stream` with the smaller of the + /// `check-write` permitted length and the `len` provided to `splice` + /// 3. calling `write` on the `output-stream` with that read data. + /// + /// Any error reported by the call to `check-write`, `read`, or + /// `write` ends the splice and reports that error. + /// + /// This function returns the number of bytes transferred; it may be less + /// than `len`. + @since(version = 0.2.0) + splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + + /// Read from one stream and write to another, with blocking. + /// + /// This is similar to `splice`, except that it blocks until the + /// `output-stream` is ready for writing, and the `input-stream` + /// is ready for reading, before performing the `splice`. + @since(version = 0.2.0) + blocking-splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + } +} diff --git a/llm/bedrock/wit/deps/wasi:io/world.wit b/llm/bedrock/wit/deps/wasi:io/world.wit new file mode 100644 index 000000000..f1d2102dc --- /dev/null +++ b/llm/bedrock/wit/deps/wasi:io/world.wit @@ -0,0 +1,10 @@ +package wasi:io@0.2.3; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import streams; + + @since(version = 0.2.0) + import poll; +} diff --git a/llm/grok/src/conversions.rs b/llm/grok/src/conversions.rs index 68a5d570c..129c128ad 100644 --- a/llm/grok/src/conversions.rs +++ b/llm/grok/src/conversions.rs @@ -183,7 +183,7 @@ fn convert_content_parts(contents: Vec) -> crate::client::Content { let media_type = &image_source.mime_type; // This is already a string result.push(crate::client::ContentPart::ImageInput { image_url: crate::client::ImageUrl { - url: format!("data:{};base64,{}", media_type, base64_data), + url: format!("data:{media_type};base64,{base64_data}"), detail: image_source.detail.map(|d| d.into()), }, }); diff --git a/llm/llm/Cargo.toml b/llm/llm/Cargo.toml index f12ada98b..24ab8374e 100644 --- a/llm/llm/Cargo.toml +++ b/llm/llm/Cargo.toml @@ -24,3 +24,4 @@ wit-bindgen = { version = "0.40.0" } [features] default = ["durability"] durability = ["golem-rust/durability"] +nopoll = [] diff --git a/llm/llm/src/config.rs b/llm/llm/src/config.rs index de9822c36..507ff0f72 100644 --- a/llm/llm/src/config.rs +++ b/llm/llm/src/config.rs @@ -21,3 +21,16 @@ pub fn with_config_key( } } } + +pub fn get_config_key(key: impl AsRef) -> Result { + let key_str = key.as_ref().to_string_lossy().to_string(); + std::env::var(key).map_err(|_| Error { + code: ErrorCode::InternalError, + message: format!("Missing config key: {key_str}"), + provider_error_json: None, + }) +} + +pub fn get_config_key_or_none(key: impl AsRef) -> Option { + std::env::var(key).ok() +} diff --git a/llm/llm/src/durability.rs b/llm/llm/src/durability.rs index 8a0782c00..fdbc5d45f 100644 --- a/llm/llm/src/durability.rs +++ b/llm/llm/src/durability.rs @@ -105,9 +105,9 @@ mod durable_impl { ChatEvent, ChatStream, Config, Guest, GuestChatStream, Message, StreamDelta, StreamEvent, ToolCall, ToolResult, }; - use golem_rust::bindings::golem::durability::durability::{ - DurableFunctionType, LazyInitializedPollable, - }; + use golem_rust::bindings::golem::durability::durability::DurableFunctionType; + #[cfg(not(feature = "nopoll"))] + use golem_rust::bindings::golem::durability::durability::LazyInitializedPollable; use golem_rust::durability::Durability; use golem_rust::wasm_rpc::Pollable; use golem_rust::{with_persistence_level, FromValueAndType, IntoValue, PersistenceLevel}; @@ -186,6 +186,9 @@ mod durable_impl { /// /// In live mode it directly calls the underlying LLM stream which is implemented on /// top of an SSE parser using the wasi-http response body stream. + /// When the `nopoll` feature flag is enabled, all polling related features are disabled + /// and events rely solely on the mechanism defined in the Implementation. Useful for implementations + /// that do not expose a wasi-http response body stream e.g AWS Bedrock. /// /// In replay mode it buffers the replayed messages, and also tracks the created pollables /// to be able to reattach them to the new live stream when the switch to live mode @@ -197,11 +200,13 @@ mod durable_impl { enum DurableChatStreamState { Live { stream: Impl::ChatStream, + #[cfg(not(feature = "nopoll"))] pollables: Vec, }, Replay { original_messages: Vec, config: Config, + #[cfg(not(feature = "nopoll"))] pollables: Vec, partial_result: Vec, finished: bool, @@ -218,6 +223,7 @@ mod durable_impl { Self { state: RefCell::new(Some(DurableChatStreamState::Live { stream, + #[cfg(not(feature = "nopoll"))] pollables: Vec::new(), })), subscription: RefCell::new(None), @@ -229,6 +235,7 @@ mod durable_impl { state: RefCell::new(Some(DurableChatStreamState::Replay { original_messages, config, + #[cfg(not(feature = "nopoll"))] pollables: Vec::new(), partial_result: Vec::new(), finished: false, @@ -236,7 +243,7 @@ mod durable_impl { subscription: RefCell::new(None), } } - + #[cfg(not(feature = "nopoll"))] fn subscribe(&self) -> Pollable { let mut state = self.state.borrow_mut(); match &mut *state { @@ -257,17 +264,25 @@ mod durable_impl { impl Drop for DurableChatStream { fn drop(&mut self) { let _ = self.subscription.take(); + match self.state.take() { Some(DurableChatStreamState::Live { + #[cfg(not(feature = "nopoll"))] mut pollables, stream, }) => { with_persistence_level(PersistenceLevel::PersistNothing, move || { + #[cfg(not(feature = "nopoll"))] pollables.clear(); drop(stream); }); } - Some(DurableChatStreamState::Replay { mut pollables, .. }) => { + Some(DurableChatStreamState::Replay { + #[cfg(not(feature = "nopoll"))] + mut pollables, + .. + }) => { + #[cfg(not(feature = "nopoll"))] pollables.clear(); } None => {} @@ -295,6 +310,7 @@ mod durable_impl { Some(DurableChatStreamState::Replay { original_messages, config, + #[cfg(not(feature = "nopoll"))] pollables, partial_result, finished, @@ -311,7 +327,7 @@ mod durable_impl { extended_messages, config.clone(), ); - + #[cfg(not(feature = "nopoll"))] for lazy_initialized_pollable in pollables { lazy_initialized_pollable.set(Impl::subscribe(&stream)); } @@ -330,6 +346,7 @@ mod durable_impl { }; if let Some(stream) = new_live_stream { + #[cfg(not(feature = "nopoll"))] let pollables = match state.take() { Some(DurableChatStreamState::Live { pollables, .. }) => pollables, Some(DurableChatStreamState::Replay { pollables, .. }) => pollables, @@ -337,7 +354,11 @@ mod durable_impl { unreachable!() } }; - *state = Some(DurableChatStreamState::Live { stream, pollables }); + *state = Some(DurableChatStreamState::Live { + stream, + #[cfg(not(feature = "nopoll"))] + pollables, + }); } result @@ -378,13 +399,17 @@ mod durable_impl { } fn blocking_get_next(&self) -> Vec { + #[cfg(not(feature = "nopoll"))] let mut subscription = self.subscription.borrow_mut(); + #[cfg(not(feature = "nopoll"))] if subscription.is_none() { *subscription = Some(self.subscribe()); } + #[cfg(not(feature = "nopoll"))] let subscription = subscription.as_mut().unwrap(); let mut result = Vec::new(); loop { + #[cfg(not(feature = "nopoll"))] subscription.block(); match self.get_next() { Some(events) => { diff --git a/llm/llm/src/event_source/ndjson_stream.rs b/llm/llm/src/event_source/ndjson_stream.rs index e2f4cc1b2..1b8ef3773 100644 --- a/llm/llm/src/event_source/ndjson_stream.rs +++ b/llm/llm/src/event_source/ndjson_stream.rs @@ -126,7 +126,7 @@ fn try_parse_line( return Ok(None); } - trace!("Parsed NDJSON line: {}", line); + trace!("Parsed NDJSON line: {line}"); // Create a MessageEvent with the JSON line as data let event = MessageEvent { diff --git a/llm/llm/src/event_source/stream.rs b/llm/llm/src/event_source/stream.rs index 8f2933676..13a5eeb56 100644 --- a/llm/llm/src/event_source/stream.rs +++ b/llm/llm/src/event_source/stream.rs @@ -56,9 +56,9 @@ where { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Utf8(err) => f.write_fmt(format_args!("UTF8 error: {}", err)), - Self::Parser(err) => f.write_fmt(format_args!("Parse error: {}", err)), - Self::Transport(err) => f.write_fmt(format_args!("Transport error: {}", err)), + Self::Utf8(err) => f.write_fmt(format_args!("UTF8 error: {err}")), + Self::Parser(err) => f.write_fmt(format_args!("Parse error: {err}")), + Self::Transport(err) => f.write_fmt(format_args!("Transport error: {err}")), } } } diff --git a/llm/ollama/src/client.rs b/llm/ollama/src/client.rs index e9514a8dd..e2901e70c 100644 --- a/llm/ollama/src/client.rs +++ b/llm/ollama/src/client.rs @@ -335,7 +335,7 @@ pub fn image_to_base64(source: &str) -> Result Error { Error { code: ErrorCode::InternalError, - message: format!("{}: {}", context, err), + message: format!("{context}: {err}"), provider_error_json: None, } } diff --git a/llm/ollama/src/conversions.rs b/llm/ollama/src/conversions.rs index b1db65c61..8d64e954f 100644 --- a/llm/ollama/src/conversions.rs +++ b/llm/ollama/src/conversions.rs @@ -214,7 +214,7 @@ pub fn process_response(response: CompletionsResponse) -> ChatEvent { }; ChatEvent::Message(CompleteResponse { - id: format!("ollama-{}", timestamp), + id: format!("ollama-{timestamp}"), content, tool_calls, metadata, diff --git a/llm/openai/src/conversions.rs b/llm/openai/src/conversions.rs index 43694c0f3..a4989b0c1 100644 --- a/llm/openai/src/conversions.rs +++ b/llm/openai/src/conversions.rs @@ -138,7 +138,7 @@ pub fn content_part_to_inner_input_item(content_part: ContentPart) -> InnerInput ImageReference::Inline(image_source) => { let base64_data = general_purpose::STANDARD.encode(&image_source.data); let mime_type = &image_source.mime_type; // This is already a string - let data_url = format!("data:{};base64,{}", mime_type, base64_data); + let data_url = format!("data:{mime_type};base64,{base64_data}"); InnerInputItem::ImageInput { image_url: data_url, diff --git a/llm/openrouter/src/conversions.rs b/llm/openrouter/src/conversions.rs index d4db2d34c..61b5f973b 100644 --- a/llm/openrouter/src/conversions.rs +++ b/llm/openrouter/src/conversions.rs @@ -184,7 +184,7 @@ fn convert_content_parts(contents: Vec) -> crate::client::Content { let media_type = &image_source.mime_type; // This is already a string result.push(crate::client::ContentPart::ImageInput { image_url: crate::client::ImageUrl { - url: format!("data:{};base64,{}", media_type, base64_data), + url: format!("data:{media_type};base64,{base64_data}"), detail: image_source.detail.map(|d| d.into()), }, }); diff --git a/test/components-rust/test-llm/Cargo.toml b/test/components-rust/test-llm/Cargo.toml index 7f6242874..893f7da52 100644 --- a/test/components-rust/test-llm/Cargo.toml +++ b/test/components-rust/test-llm/Cargo.toml @@ -15,6 +15,7 @@ grok = [] openai = [] openrouter = [] ollama = [] +bedrock = [] [dependencies] # To use common shared libs, use the following: @@ -37,8 +38,8 @@ path = "wit-generated" [package.metadata.component.target.dependencies] "golem:llm" = { path = "wit-generated/deps/golem-llm" } -"wasi:clocks" = { path = "wit-generated/deps/clocks" } "wasi:io" = { path = "wit-generated/deps/io" } +"wasi:clocks" = { path = "wit-generated/deps/clocks" } "golem:rpc" = { path = "wit-generated/deps/golem-rpc" } "test:helper-client" = { path = "wit-generated/deps/test_helper-client" } "test:llm-exports" = { path = "wit-generated/deps/test_llm-exports" } @@ -47,4 +48,4 @@ path = "wit-generated" # See https://github.com/bytecodealliance/cargo-component/blob/main/src/metadata.rs#L62 # derives = ["serde::Serialize", "serde::Deserialize"] -# generate_unused_types = true \ No newline at end of file +# generate_unused_types = true diff --git a/test/components-rust/test-llm/golem.yaml b/test/components-rust/test-llm/golem.yaml index 6efa177c7..b2a301024 100644 --- a/test/components-rust/test-llm/golem.yaml +++ b/test/components-rust/test-llm/golem.yaml @@ -35,6 +35,32 @@ components: clean: - src/bindings.rs + bedrock-debug: + files: + - sourcePath: ../../data/cat.png + targetPath: /data/cat.png + permissions: read-only + build: + - command: cargo component build --no-default-features --features bedrock + sources: + - src + - wit-generated + - ../../common-rust + targets: + - ../../target/wasm32-wasip1/debug/test_llm.wasm + - command: wac plug --plug ../../../target/wasm32-wasip1/debug/golem_llm_bedrock.wasm ../../target/wasm32-wasip1/debug/test_llm.wasm -o ../../target/wasm32-wasip1/debug/test_bedrock_plugged.wasm + sources: + - ../../target/wasm32-wasip1/debug/test_llm.wasm + - ../../../target/wasm32-wasip1/debug/golem_llm_bedrock.wasm + targets: + - ../../target/wasm32-wasip1/debug/test_bedrock_plugged.wasm + sourceWit: wit + generatedWit: wit-generated + componentWasm: ../../target/wasm32-wasip1/debug/test_bedrock_plugged.wasm + linkedWasm: ../../golem-temp/components/test_bedrock_debug.wasm + clean: + - src/bindings.rs + anthropic-debug: files: - sourcePath: ../../data/cat.png @@ -166,6 +192,32 @@ components: clean: - src/bindings.rs + bedrock-release: + files: + - sourcePath: ../../data/cat.png + targetPath: /data/cat.png + permissions: read-only + build: + - command: cargo component build --release --no-default-features --features bedrock + sources: + - src + - wit-generated + - ../../common-rust + targets: + - ../../target/wasm32-wasip1/release/test_llm.wasm + - command: wac plug --plug ../../../target/wasm32-wasip1/release/golem_llm_bedrock.wasm ../../target/wasm32-wasip1/release/test_llm.wasm -o ../../target/wasm32-wasip1/release/test_bedrock_plugged.wasm + sources: + - ../../target/wasm32-wasip1/release/test_llm.wasm + - ../../../target/wasm32-wasip1/release/golem_llm_bedrock.wasm + targets: + - ../../target/wasm32-wasip1/release/test_bedrock_plugged.wasm + sourceWit: wit + generatedWit: wit-generated + componentWasm: ../../target/wasm32-wasip1/release/test_bedrock_plugged.wasm + linkedWasm: ../../golem-temp/components/test_bedrock_release.wasm + clean: + - src/bindings.rs + anthropic-release: files: - sourcePath: ../../data/cat.png diff --git a/test/components-rust/test-llm/src/lib.rs b/test/components-rust/test-llm/src/lib.rs index fa11684de..933fd0989 100644 --- a/test/components-rust/test-llm/src/lib.rs +++ b/test/components-rust/test-llm/src/lib.rs @@ -1,34 +1,38 @@ #[allow(static_mut_refs)] mod bindings; -use golem_rust::atomically; use crate::bindings::exports::test::llm_exports::test_llm_api::*; use crate::bindings::golem::llm::llm; use crate::bindings::golem::llm::llm::StreamEvent; use crate::bindings::test::helper_client::test_helper_client::TestHelperApi; +use golem_rust::atomically; struct Component; #[cfg(feature = "openai")] const MODEL: &'static str = "gpt-3.5-turbo"; +#[cfg(feature = "bedrock")] +const MODEL: &'static str = "anthropic.claude-3-5-sonnet-20240620-v1:0"; #[cfg(feature = "anthropic")] const MODEL: &'static str = "claude-3-7-sonnet-20250219"; #[cfg(feature = "grok")] const MODEL: &'static str = "grok-3-beta"; #[cfg(feature = "openrouter")] const MODEL: &'static str = "openrouter/auto"; -#[cfg(feature = "ollama")] +#[cfg(feature = "ollama")] const MODEL: &'static str = "qwen3:1.7b"; #[cfg(feature = "openai")] const IMAGE_MODEL: &'static str = "gpt-4o-mini"; +#[cfg(feature = "bedrock")] +const IMAGE_MODEL: &'static str = "anthropic.claude-3-5-sonnet-20240620-v1:0"; #[cfg(feature = "anthropic")] const IMAGE_MODEL: &'static str = "claude-3-7-sonnet-20250219"; #[cfg(feature = "grok")] const IMAGE_MODEL: &'static str = "grok-2-vision-latest"; #[cfg(feature = "openrouter")] const IMAGE_MODEL: &'static str = "openrouter/auto"; -#[cfg(feature = "ollama")] +#[cfg(feature = "ollama")] const IMAGE_MODEL: &'static str = "gemma3:4b"; impl Guest for Component { @@ -67,9 +71,14 @@ impl Guest for Component { .map(|content| match content { llm::ContentPart::Text(txt) => txt, llm::ContentPart::Image(image_ref) => match image_ref { - llm::ImageReference::Url(url_data) => format!("[IMAGE URL: {}]", url_data.url), - llm::ImageReference::Inline(inline_data) => format!("[INLINE IMAGE: {} bytes, mime: {}]", inline_data.data.len(), inline_data.mime_type), - } + llm::ImageReference::Url(url_data) => + format!("[IMAGE URL: {}]", url_data.url), + llm::ImageReference::Inline(inline_data) => format!( + "[INLINE IMAGE: {} bytes, mime: {}]", + inline_data.data.len(), + inline_data.mime_type + ), + }, }) .collect::>() .join(", ") @@ -154,7 +163,7 @@ impl Guest for Component { vec![] } }; - + if !tool_request.is_empty() { let mut calls = Vec::new(); for call in tool_request { @@ -385,9 +394,14 @@ impl Guest for Component { .map(|content| match content { llm::ContentPart::Text(txt) => txt, llm::ContentPart::Image(image_ref) => match image_ref { - llm::ImageReference::Url(url_data) => format!("[IMAGE URL: {}]", url_data.url), - llm::ImageReference::Inline(inline_data) => format!("[INLINE IMAGE: {} bytes, mime: {}]", inline_data.data.len(), inline_data.mime_type), - } + llm::ImageReference::Url(url_data) => + format!("[IMAGE URL: {}]", url_data.url), + llm::ImageReference::Inline(inline_data) => format!( + "[INLINE IMAGE: {} bytes, mime: {}]", + inline_data.data.len(), + inline_data.mime_type + ), + }, }) .collect::>() .join(", ") @@ -407,7 +421,7 @@ impl Guest for Component { } } - /// test6 simulates a crash during a streaming LLM response, but only first time. + /// test6 simulates a crash during a streaming LLM response, but only first time. /// after the automatic recovery it will continue and finish the request successfully. fn test6() -> String { let config = llm::Config { @@ -456,12 +470,20 @@ impl Guest for Component { } llm::ContentPart::Image(image_ref) => match image_ref { llm::ImageReference::Url(url_data) => { - result.push_str(&format!("IMAGE URL: {} ({:?})\n", url_data.url, url_data.detail)); + result.push_str(&format!( + "IMAGE URL: {} ({:?})\n", + url_data.url, url_data.detail + )); } llm::ImageReference::Inline(inline_data) => { - result.push_str(&format!("INLINE IMAGE: {} bytes, mime: {}, detail: {:?}\n", inline_data.data.len(), inline_data.mime_type, inline_data.detail)); + result.push_str(&format!( + "INLINE IMAGE: {} bytes, mime: {}, detail: {:?}\n", + inline_data.data.len(), + inline_data.mime_type, + inline_data.detail + )); } - } + }, } } } @@ -528,7 +550,10 @@ impl Guest for Component { role: llm::Role::User, name: None, content: vec![ - llm::ContentPart::Text("Please describe this cat image in detail. What breed might it be?".to_string()), + llm::ContentPart::Text( + "Please describe this cat image in detail. What breed might it be?" + .to_string(), + ), llm::ContentPart::Image(llm::ImageReference::Inline(llm::ImageSource { data: buffer, mime_type: "image/png".to_string(), @@ -549,9 +574,14 @@ impl Guest for Component { .map(|content| match content { llm::ContentPart::Text(txt) => txt, llm::ContentPart::Image(image_ref) => match image_ref { - llm::ImageReference::Url(url_data) => format!("[IMAGE URL: {}]", url_data.url), - llm::ImageReference::Inline(inline_data) => format!("[INLINE IMAGE: {} bytes, mime: {}]", inline_data.data.len(), inline_data.mime_type), - } + llm::ImageReference::Url(url_data) => + format!("[IMAGE URL: {}]", url_data.url), + llm::ImageReference::Inline(inline_data) => format!( + "[INLINE IMAGE: {} bytes, mime: {}]", + inline_data.data.len(), + inline_data.mime_type + ), + }, }) .collect::>() .join(", ")