diff --git a/Cargo.lock b/Cargo.lock index 0865d6ad..66c4cbda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # 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" @@ -47,6 +56,326 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "aws-config" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02a18fd934af6ae7ca52410d4548b98eb895aab0f1ea417d168d85db1434a141" +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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c4063282c69991e57faab9e5cb21ae557e59f5b0fb285c196335243df8dc25c" +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.90.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d62ffce46fc724dc4a132822cffb1c43ea5b7036f643d07e834b721ac73d261" +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.71.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17b984a66491ec08b4f4097af8911251db79296b3e4a763060b45805746264f" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3734aecf9ff79aa401a6ca099d076535ab465ff76b46440cf567c8e70b65dc13" +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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c45d3dddac16c5c59d553ece225a88870cf81b7b813c9cc17b78cf4685eac7a" +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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92144e45819cae7dc62af23eac5a038a58aa544432d2102609654376a900bd07" +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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e5d9e3a80a18afa109391fb5ad09c3daf887b516c6fd805a157c6ea7994a57" +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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40076bd09fadbc12d5e026ae080d0930defa606856186e31d83ccc6a255eeaf3" +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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc" +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 +388,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 +425,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" @@ -145,6 +503,15 @@ 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 +521,36 @@ 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 = "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 +562,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 +583,12 @@ 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 = "flate2" version = "1.1.1" @@ -300,6 +709,16 @@ dependencies = [ "slab", ] +[[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 +728,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" @@ -360,6 +785,25 @@ dependencies = [ "wit-bindgen-rt 0.40.0", ] +[[package]] +name = "golem-llm-bedrock" +version = "0.0.0" +dependencies = [ + "aws-config", + "aws-sdk-bedrockruntime", + "aws-smithy-wasm", + "aws-types", + "base64 0.22.1", + "golem-llm", + "golem-rust", + "log", + "reqwest", + "serde", + "serde_json", + "tokio", + "wit-bindgen-rt 0.40.0", +] + [[package]] name = "golem-llm-grok" version = "0.0.0" @@ -446,9 +890,15 @@ dependencies = [ [[package]] name = "golem-wasm-rpc" + +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5854474e6b845dc89c1a32233661ec0a8e221c43aa2edc8128b01e82a2b0909" + version = "1.3.0-dev.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a82c94d17e7b23ad4335012364825ce9a30977aaca9ebb385c3e4274be0e0f60" + dependencies = [ "cargo_metadata", "chrono", @@ -481,6 +931,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 +968,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" @@ -733,6 +1277,21 @@ dependencies = [ "minimal-lexical", ] +[[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,12 +1301,27 @@ 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 = "percent-encoding" version = "2.3.1" @@ -776,6 +1350,15 @@ dependencies = [ ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -809,6 +1392,12 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + [[package]] name = "reqwest" version = "0.12.15" @@ -819,7 +1408,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "http", + "http 1.3.1", "mime", "percent-encoding", "serde", @@ -831,6 +1420,21 @@ dependencies = [ "wit-bindgen-rt 0.41.0", ] +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[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" @@ -902,6 +1506,17 @@ 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 = "shlex" version = "1.3.0" @@ -938,6 +1553,12 @@ 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 +1610,36 @@ dependencies = [ "syn", ] +[[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 +1650,29 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tokio" +version = "1.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +dependencies = [ + "backtrace", + "bytes", + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "topological-sort" version = "0.2.2" @@ -1011,6 +1685,49 @@ 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.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[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" @@ -1047,6 +1764,15 @@ dependencies = [ ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1063,6 +1789,39 @@ dependencies = [ "serde", "sha1_smol", "wasm-bindgen", + +] + +[[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]] @@ -1278,6 +2037,79 @@ 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_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 +2328,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" @@ -1541,6 +2379,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 f1dee241..dfa93e11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" + members = [ "llm", "llm-anthropic", @@ -8,6 +9,7 @@ members = [ "llm-ollama", "llm-openai", "llm-openrouter", + "llm-bedrock" ] [profile.release] diff --git a/Makefile.toml b/Makefile.toml index 37053ddc..0235d74f 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -62,12 +62,23 @@ install_crate = { crate_name = "cargo-component", version = "0.20.0" } command = "cargo-component" args = ["build", "-p", "golem-llm-openrouter", "--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] dependencies = [ "build-anthropic", "build-grok", "build-openai", "build-openrouter", + "build-bedrock", "build-ollama", ] @@ -77,6 +88,7 @@ dependencies = [ "build-grok-portable", "build-openai-portable", "build-openrouter-portable", + "build-bedrock-portable", "build-ollama-portable", ] @@ -92,6 +104,7 @@ cp target/wasm32-wasip1/debug/golem_llm_anthropic.wasm components/debug/golem_ll cp target/wasm32-wasip1/debug/golem_llm_grok.wasm components/debug/golem_llm_grok.wasm cp target/wasm32-wasip1/debug/golem_llm_openai.wasm components/debug/golem_llm_openai.wasm cp target/wasm32-wasip1/debug/golem_llm_openrouter.wasm components/debug/golem_llm_openrouter.wasm +cp target/wasm32-wasip1/debug/golem_llm_bedrock.wasm components/debug/golem_llm_bedrock.wasm cp target/wasm32-wasip1/debug/golem_llm_ollama.wasm components/debug/golem_llm_ollama.wasm cm_run_task clean @@ -101,6 +114,7 @@ cp target/wasm32-wasip1/debug/golem_llm_anthropic.wasm components/debug/golem_ll cp target/wasm32-wasip1/debug/golem_llm_grok.wasm components/debug/golem_llm_grok-portable.wasm cp target/wasm32-wasip1/debug/golem_llm_openai.wasm components/debug/golem_llm_openai-portable.wasm cp target/wasm32-wasip1/debug/golem_llm_openrouter.wasm components/debug/golem_llm_openrouter-portable.wasm +cp target/wasm32-wasip1/debug/golem_llm_bedrock.wasm components/debug/golem_llm_bedrock-portable.wasm cp target/wasm32-wasip1/debug/golem_llm_ollama.wasm components/debug/golem_llm_ollama-portable.wasm ''' @@ -167,12 +181,29 @@ args = [ "--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] dependencies = [ "release-build-anthropic", "release-build-grok", "release-build-openai", "release-build-openrouter", + "release-build-bedrock", "release-build-ollama", ] @@ -182,6 +213,7 @@ dependencies = [ "release-build-grok-portable", "release-build-openai-portable", "release-build-openrouter-portable", + "release-build-bedrock-portable", "release-build-ollama-portable", ] @@ -199,6 +231,7 @@ cp target/wasm32-wasip1/release/golem_llm_anthropic.wasm components/release/gole cp target/wasm32-wasip1/release/golem_llm_grok.wasm components/release/golem_llm_grok.wasm cp target/wasm32-wasip1/release/golem_llm_openai.wasm components/release/golem_llm_openai.wasm cp target/wasm32-wasip1/release/golem_llm_openrouter.wasm components/release/golem_llm_openrouter.wasm +cp target/wasm32-wasip1/release/golem_llm_bedrock.wasm components/release/golem_llm_bedrock.wasm cp target/wasm32-wasip1/release/golem_llm_ollama.wasm components/release/golem_llm_ollama.wasm cm_run_task clean @@ -208,6 +241,7 @@ cp target/wasm32-wasip1/release/golem_llm_anthropic.wasm components/release/gole cp target/wasm32-wasip1/release/golem_llm_grok.wasm components/release/golem_llm_grok-portable.wasm cp target/wasm32-wasip1/release/golem_llm_openai.wasm components/release/golem_llm_openai-portable.wasm cp target/wasm32-wasip1/release/golem_llm_openrouter.wasm components/release/golem_llm_openrouter-portable.wasm +cp target/wasm32-wasip1/release/golem_llm_bedrock.wasm components/release/golem_llm_bedrock-portable.wasm cp target/wasm32-wasip1/release/golem_llm_ollama.wasm components/release/golem_llm_ollama-portable.wasm ''' @@ -228,6 +262,7 @@ dependencies = ["wit-update"] # "llm-grok/wit/deps/golem-llm/golem-llm.wit", # "llm-openai/wit/deps/golem-llm/golem-llm.wit", # "llm-openrouter/wit/deps/golem-llm/golem-llm.wit", +# "llm-bedrock/wit/deps/golem-llm/golem-llm.wit", #] } } script_runner = "@duckscript" @@ -252,6 +287,10 @@ rm -r llm-openrouter/wit/deps mkdir llm-openrouter/wit/deps/golem-llm cp wit/golem-llm.wit llm-openrouter/wit/deps/golem-llm/golem-llm.wit cp wit/deps/wasi:io llm-openrouter/wit/deps +rm -r llm-bedrock/wit/deps +mkdir llm-bedrock/wit/deps/golem-llm +cp wit/golem-llm.wit llm-bedrock/wit/deps/golem-llm/golem-llm.wit +cp wit/deps/wasi:io llm-bedrock/wit/deps rm -r llm-ollama/wit/deps mkdir llm-ollama/wit/deps/golem-llm cp wit/golem-llm.wit llm-ollama/wit/deps/golem-llm/golem-llm.wit @@ -320,6 +359,8 @@ golem-cli --version golem-cli app clean golem-cli app build -b anthropic-debug golem-cli app clean +golem-cli app build -b bedrock-debug +golem-cli app clean golem-cli app build -b grok-debug golem-cli app clean golem-cli app build -b openai-debug diff --git a/README.md b/README.md index 8cf5f70a..b2472f96 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 AWS 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 AWS Bedrock, with no Golem specific dependencies. | Every component **exports** the same `golem:llm` interface, [defined here](wit/golem-llm.wit). @@ -36,6 +38,8 @@ Each provider has to be configured with an API key passed as an environment vari | Grok | `XAI_API_KEY` | | OpenAI | `OPENAI_API_KEY` | | OpenRouter | `OPENROUTER_API_KEY` | +| Bedrock | AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN (optional), AWS_REGION (or AWS_DEFAULT_REGION). Relies on standard AWS SDK credential chain. The region can also be set via provider-options in the Config with key AWS_REGION. | +======= | Ollama | `GOLEM_OLLAMA_BASE_URL` | Additionally, setting the `GOLEM_LLM_LOG=trace` environment variable enables trace logging for all the communication diff --git a/llm-bedrock/Cargo.toml b/llm-bedrock/Cargo.toml new file mode 100644 index 00000000..341c6c65 --- /dev/null +++ b/llm-bedrock/Cargo.toml @@ -0,0 +1,55 @@ +[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 AWS Bedrock APIs, with special support for Golem Cloud" + +[lib] +path = "src/lib.rs" +crate-type = ["cdylib"] + +[features] +default = ["durability"] +durability = ["golem-rust/durability", "golem-llm/durability"] + +[dependencies] +golem-llm = { path = "../llm", version = "0.0.0", default-features = false } +golem-rust = { workspace = true } +log = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +wit-bindgen-rt = { workspace = true } + +# AWS SDK with WASI support - minimal configuration +aws-config = { version = "1.5.19", default-features = false, features = ["behavior-version-latest"] } +aws-sdk-bedrockruntime = { version = "1.56.0", default-features = false } +aws-smithy-wasm = { version = "0.1.4", default-features = false } +aws-types = { version = "1.3.4", default-features = false } +# Minimal tokio for WASI (only WASM-compatible features) +tokio = { version = "1.40", default-features = false, features = ["macros", "rt", "time", "io-util"] } +# HTTP client for image fetching (Bedrock requires image bytes, not URLs) +reqwest = { workspace = true } +# Base64 decoding for data URL images +base64 = "0.22" + + + +[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" +"wasi:io/poll@0.2.0" = "golem_rust::wasm_rpc::wasi::io::poll" + +[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"} \ No newline at end of file diff --git a/llm-bedrock/src/bindings.rs b/llm-bedrock/src/bindings.rs new file mode 100644 index 00000000..c5311b42 --- /dev/null +++ b/llm-bedrock/src/bindings.rs @@ -0,0 +1,60 @@ +// Generated by `wit-bindgen` 0.41.0. DO NOT EDIT! +// Options used: +// * runtime_path: "wit_bindgen_rt" +// * with "wasi:io/poll@0.2.0" = "golem_rust::wasm_rpc::wasi::io::poll" +// * with "golem:llm/llm@1.0.0" = "golem_llm::golem::llm::llm" +// * generate_unused_types +use golem_rust::wasm_rpc::wasi::io::poll as __with_name0; +use golem_llm::golem::llm::llm as __with_name1; +#[cfg(target_arch = "wasm32")] +#[unsafe( + link_section = "component-type:wit-bindgen:0.41.0:golem:llm-bedrock@1.0.0:llm-library:encoded world" +)] +#[doc(hidden)] +#[allow(clippy::octal_escapes)] +pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 1892] = *b"\ +\0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07\xe2\x0d\x01A\x02\x01\ +A\x05\x01B\x0a\x04\0\x08pollable\x03\x01\x01h\0\x01@\x01\x04self\x01\0\x7f\x04\0\ +\x16[method]pollable.ready\x01\x02\x01@\x01\x04self\x01\x01\0\x04\0\x16[method]p\ +ollable.block\x01\x03\x01p\x01\x01py\x01@\x01\x02in\x04\0\x05\x04\0\x04poll\x01\x06\ +\x03\0\x12wasi:io/poll@0.2.0\x05\0\x02\x03\0\0\x08pollable\x01BO\x02\x03\x02\x01\ +\x01\x04\0\x08pollable\x03\0\0\x01m\x04\x04user\x09assistant\x06system\x04tool\x04\ +\0\x04role\x03\0\x02\x01m\x06\x0finvalid-request\x15authentication-failed\x13rat\ +e-limit-exceeded\x0einternal-error\x0bunsupported\x07unknown\x04\0\x0aerror-code\ +\x03\0\x04\x01m\x06\x04stop\x06length\x0atool-calls\x0econtent-filter\x05error\x05\ +other\x04\0\x0dfinish-reason\x03\0\x06\x01m\x03\x03low\x04high\x04auto\x04\0\x0c\ +image-detail\x03\0\x08\x01k\x09\x01r\x02\x03urls\x06detail\x0a\x04\0\x09image-ur\ +l\x03\0\x0b\x01q\x02\x04text\x01s\0\x05image\x01\x0c\0\x04\0\x0ccontent-part\x03\ +\0\x0d\x01ks\x01p\x0e\x01r\x03\x04role\x03\x04name\x0f\x07content\x10\x04\0\x07m\ +essage\x03\0\x11\x01r\x03\x04names\x0bdescription\x0f\x11parameters-schemas\x04\0\ +\x0ftool-definition\x03\0\x13\x01r\x03\x02ids\x04names\x0earguments-jsons\x04\0\x09\ +tool-call\x03\0\x15\x01ky\x01r\x04\x02ids\x04names\x0bresult-jsons\x11execution-\ +time-ms\x17\x04\0\x0ctool-success\x03\0\x18\x01r\x04\x02ids\x04names\x0derror-me\ +ssages\x0aerror-code\x0f\x04\0\x0ctool-failure\x03\0\x1a\x01q\x02\x07success\x01\ +\x19\0\x05error\x01\x1b\0\x04\0\x0btool-result\x03\0\x1c\x01r\x02\x03keys\x05val\ +ues\x04\0\x02kv\x03\0\x1e\x01kv\x01ps\x01k!\x01p\x14\x01p\x1f\x01r\x07\x05models\ +\x0btemperature\x20\x0amax-tokens\x17\x0estop-sequences\"\x05tools#\x0btool-choi\ +ce\x0f\x10provider-options$\x04\0\x06config\x03\0%\x01r\x03\x0cinput-tokens\x17\x0d\ +output-tokens\x17\x0ctotal-tokens\x17\x04\0\x05usage\x03\0'\x01k\x07\x01k(\x01r\x05\ +\x0dfinish-reason)\x05usage*\x0bprovider-id\x0f\x09timestamp\x0f\x16provider-met\ +adata-json\x0f\x04\0\x11response-metadata\x03\0+\x01p\x16\x01r\x04\x02ids\x07con\ +tent\x10\x0atool-calls-\x08metadata,\x04\0\x11complete-response\x03\0.\x01r\x03\x04\ +code\x05\x07messages\x13provider-error-json\x0f\x04\0\x05error\x03\00\x01q\x03\x07\ +message\x01/\0\x0ctool-request\x01-\0\x05error\x011\0\x04\0\x0achat-event\x03\02\ +\x01k\x10\x01k-\x01r\x02\x07content4\x0atool-calls5\x04\0\x0cstream-delta\x03\06\ +\x01q\x03\x05delta\x017\0\x06finish\x01,\0\x05error\x011\0\x04\0\x0cstream-event\ +\x03\08\x04\0\x0bchat-stream\x03\x01\x01h:\x01p9\x01k<\x01@\x01\x04self;\0=\x04\0\ +\x1c[method]chat-stream.get-next\x01>\x01@\x01\x04self;\0<\x04\0%[method]chat-st\ +ream.blocking-get-next\x01?\x01i\x01\x01@\x01\x04self;\0\xc0\0\x04\0\x1d[method]\ +chat-stream.subscribe\x01A\x01p\x12\x01@\x02\x08messages\xc2\0\x06config&\03\x04\ +\0\x04send\x01C\x01o\x02\x16\x1d\x01p\xc4\0\x01@\x03\x08messages\xc2\0\x0ctool-r\ +esults\xc5\0\x06config&\03\x04\0\x08continue\x01F\x01i:\x01@\x02\x08messages\xc2\ +\0\x06config&\0\xc7\0\x04\0\x06stream\x01H\x04\0\x13golem:llm/llm@1.0.0\x05\x02\x04\ +\0#golem:llm-bedrock/llm-library@1.0.0\x04\0\x0b\x11\x01\0\x0bllm-library\x03\0\0\ +\0G\x09producers\x01\x0cprocessed-by\x02\x0dwit-component\x070.227.1\x10wit-bind\ +gen-rust\x060.41.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 00000000..3498de2f --- /dev/null +++ b/llm-bedrock/src/client.rs @@ -0,0 +1,231 @@ +use aws_config::meta::region::RegionProviderChain; +use aws_sdk_bedrockruntime::config::Credentials; +use aws_sdk_bedrockruntime::error::ProvideErrorMetadata; +use aws_sdk_bedrockruntime::operation::converse::ConverseOutput; +use aws_sdk_bedrockruntime::operation::converse_stream::ConverseStreamOutput; +use aws_sdk_bedrockruntime::types::{Message, SystemContentBlock}; +use aws_sdk_bedrockruntime::Client as AwsBedrockClient; +use aws_smithy_wasm::wasi::WasiHttpClientBuilder; +use golem_llm::golem::llm::llm::{Error, ErrorCode}; +use log::{debug, error, trace}; + +pub struct BedrockClient { + client: AwsBedrockClient, +} + +#[derive(Debug, Clone)] +pub struct BedrockRequest { + pub model_id: String, + pub messages: Vec, + pub system_prompt: Option, + pub max_tokens: Option, + pub temperature: Option, +} + +#[derive(Debug, Clone)] +pub struct AwsCredentials { + pub access_key_id: String, + pub secret_access_key: String, + pub session_token: Option, +} + +impl BedrockClient { + pub fn new( + aws_region_opt: Option, + aws_credentials_opt: Option, + ) -> Result { + debug!( + "Initializing BedrockClient with region: {:?} and credentials: {}", + aws_region_opt, + aws_credentials_opt.is_some() + ); + + // This will be executed in a Tokio context, but synchronously + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|e| Error { + code: ErrorCode::InternalError, + message: format!("Failed to create Tokio runtime: {e}"), + provider_error_json: None, + })?; + + let client = rt.block_on(async { + // Create WASI HTTP client + let wasi_client = WasiHttpClientBuilder::new().build(); + + // Set up region provider chain + let region_provider = + RegionProviderChain::first_try(aws_region_opt.map(aws_types::region::Region::new)) + .or_default_provider() + .or_else(aws_types::region::Region::new("us-east-1")); + + // Build SDK config with WASI client + let mut config_builder = aws_config::defaults(aws_config::BehaviorVersion::latest()) + .region(region_provider) + .http_client(wasi_client); + + // Add credentials if provided + if let Some(creds) = aws_credentials_opt { + let credentials = Credentials::new( + &creds.access_key_id, + &creds.secret_access_key, + creds.session_token, + None, // Expiry + "golem-llm-bedrock", + ); + config_builder = config_builder.credentials_provider(credentials); + } + + let sdk_config = config_builder.load().await; + AwsBedrockClient::new(&sdk_config) + }); + + Ok(Self { client }) + } + + pub fn converse(&self, request: BedrockRequest) -> Result { + trace!( + "Bedrock converse request. Model ID: {}, Messages: {}", + request.model_id, + request.messages.len() + ); + + // Create a runtime for this specific call + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|e| Error { + code: ErrorCode::InternalError, + message: format!("Failed to create Tokio runtime: {e}"), + provider_error_json: None, + })?; + + rt.block_on(async { + let mut converse_request = self.client + .converse() + .model_id(&request.model_id) + .set_messages(Some(request.messages)); + + // Add system prompt if provided + if let Some(system_prompt) = request.system_prompt { + converse_request = converse_request.system(SystemContentBlock::Text(system_prompt)); + } + + // Add inference configuration if provided + if request.max_tokens.is_some() || request.temperature.is_some() { + let mut inference_config = aws_sdk_bedrockruntime::types::InferenceConfiguration::builder(); + + if let Some(max_tokens) = request.max_tokens { + inference_config = inference_config.max_tokens(max_tokens); + } + + if let Some(temperature) = request.temperature { + inference_config = inference_config.temperature(temperature); + } + + converse_request = converse_request.inference_config(inference_config.build()); + } + + converse_request.send().await + }).map_err(|sdk_err| { + let error_message = format!("Bedrock Converse SDK error: {sdk_err:?}"); + error!("{error_message}"); + let provider_error_json = Some(error_message.clone()); + let message = sdk_err.message().unwrap_or("Unknown Bedrock SDK error").to_string(); + + let code = match sdk_err.as_service_error() { + Some(err) => match err { + aws_sdk_bedrockruntime::operation::converse::ConverseError::ValidationException(_) => ErrorCode::InvalidRequest, + aws_sdk_bedrockruntime::operation::converse::ConverseError::AccessDeniedException(_) => ErrorCode::AuthenticationFailed, + aws_sdk_bedrockruntime::operation::converse::ConverseError::ResourceNotFoundException(_) => ErrorCode::InvalidRequest, + aws_sdk_bedrockruntime::operation::converse::ConverseError::ThrottlingException(_) => ErrorCode::RateLimitExceeded, + aws_sdk_bedrockruntime::operation::converse::ConverseError::ModelTimeoutException(_) => ErrorCode::InternalError, + aws_sdk_bedrockruntime::operation::converse::ConverseError::InternalServerException(_) => ErrorCode::InternalError, + aws_sdk_bedrockruntime::operation::converse::ConverseError::ModelNotReadyException(_) => ErrorCode::Unsupported, + aws_sdk_bedrockruntime::operation::converse::ConverseError::ModelErrorException(_) => ErrorCode::InternalError, + _ => ErrorCode::InternalError, + }, + None => ErrorCode::InternalError, + }; + Error { + code, + message, + provider_error_json, + } + }) + } + + pub fn converse_stream(&self, request: BedrockRequest) -> Result { + trace!( + "Bedrock converse_stream request. Model ID: {}, Messages: {}", + request.model_id, + request.messages.len() + ); + + // Create a runtime for this specific call + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|e| Error { + code: ErrorCode::InternalError, + message: format!("Failed to create Tokio runtime: {e}"), + provider_error_json: None, + })?; + + rt.block_on(async { + let mut converse_request = self.client + .converse_stream() + .model_id(&request.model_id) + .set_messages(Some(request.messages)); + + // Add system prompt if provided + if let Some(system_prompt) = request.system_prompt { + converse_request = converse_request.system(SystemContentBlock::Text(system_prompt)); + } + + // Add inference configuration if provided + if request.max_tokens.is_some() || request.temperature.is_some() { + let mut inference_config = aws_sdk_bedrockruntime::types::InferenceConfiguration::builder(); + + if let Some(max_tokens) = request.max_tokens { + inference_config = inference_config.max_tokens(max_tokens); + } + + if let Some(temperature) = request.temperature { + inference_config = inference_config.temperature(temperature); + } + + converse_request = converse_request.inference_config(inference_config.build()); + } + + converse_request.send().await + }).map_err(|sdk_err| { + let error_message = format!("Bedrock ConverseStream SDK error: {sdk_err:?}"); + error!("{error_message}"); + let provider_error_json = Some(error_message.clone()); + let message = sdk_err.message().unwrap_or("Unknown Bedrock SDK error for stream").to_string(); + + let code = match sdk_err.as_service_error() { + Some(err) => match err { + aws_sdk_bedrockruntime::operation::converse_stream::ConverseStreamError::ValidationException(_) => ErrorCode::InvalidRequest, + aws_sdk_bedrockruntime::operation::converse_stream::ConverseStreamError::AccessDeniedException(_) => ErrorCode::AuthenticationFailed, + aws_sdk_bedrockruntime::operation::converse_stream::ConverseStreamError::ResourceNotFoundException(_) => ErrorCode::InvalidRequest, + aws_sdk_bedrockruntime::operation::converse_stream::ConverseStreamError::ThrottlingException(_) => ErrorCode::RateLimitExceeded, + aws_sdk_bedrockruntime::operation::converse_stream::ConverseStreamError::ModelTimeoutException(_) => ErrorCode::InternalError, + aws_sdk_bedrockruntime::operation::converse_stream::ConverseStreamError::InternalServerException(_) => ErrorCode::InternalError, + aws_sdk_bedrockruntime::operation::converse_stream::ConverseStreamError::ModelNotReadyException(_) => ErrorCode::Unsupported, + aws_sdk_bedrockruntime::operation::converse_stream::ConverseStreamError::ModelErrorException(_) => ErrorCode::InternalError, + aws_sdk_bedrockruntime::operation::converse_stream::ConverseStreamError::ModelStreamErrorException(_) => ErrorCode::InternalError, + _ => ErrorCode::InternalError, + }, + None => ErrorCode::InternalError, + }; + Error { + code, + message, + provider_error_json, + } + }) + } +} diff --git a/llm-bedrock/src/conversions.rs b/llm-bedrock/src/conversions.rs new file mode 100644 index 00000000..de51e292 --- /dev/null +++ b/llm-bedrock/src/conversions.rs @@ -0,0 +1,305 @@ +use aws_sdk_bedrockruntime::types::{ContentBlock, ConversationRole, Message as BedrockMessage, ImageBlock, ImageSource, ImageFormat}; +use aws_sdk_bedrockruntime::primitives::Blob; +use golem_llm::golem::llm::llm::{ + ChatEvent, CompleteResponse, ContentPart, Error, ErrorCode, FinishReason, Message, + ResponseMetadata, Role, ToolCall, ToolResult, Usage, +}; +use log::{trace, warn}; +use base64::{Engine as _, engine::general_purpose}; + +/// Convert golem-llm messages to Bedrock Converse API format +pub fn messages_to_bedrock_converse( + messages: &[Message], +) -> Result, Error> { + let mut bedrock_messages = Vec::new(); + + for message in messages { + let role = match message.role { + Role::User => ConversationRole::User, + Role::Assistant => ConversationRole::Assistant, + _ => { + return Err(Error { + code: ErrorCode::InvalidRequest, + message: format!("Unsupported message role: {:?}", message.role), + provider_error_json: None, + }); + } + }; + + let mut content_blocks = Vec::new(); + + for content in &message.content { + match content { + ContentPart::Text(text) => { + content_blocks.push(ContentBlock::Text(text.clone())); + } + ContentPart::Image(image) => { + // Convert image to Bedrock ImageBlock + match convert_image_to_bedrock_block(&image.url) { + Ok(image_block) => { + content_blocks.push(ContentBlock::Image(image_block)); + } + Err(err) => { + warn!("Failed to convert image {}: {:?}. Using fallback.", image.url, err); + // Fallback: Add a text description instead of failing + content_blocks.push(ContentBlock::Text(format!( + "[Image: {}]", + image.url + ))); + } + } + } + } + } + + if content_blocks.is_empty() { + content_blocks.push(ContentBlock::Text("".to_string())); + } + + let bedrock_message = BedrockMessage::builder() + .role(role) + .set_content(Some(content_blocks)) + .build() + .map_err(|e| Error { + code: ErrorCode::InternalError, + message: format!("Failed to build Bedrock message: {e}"), + provider_error_json: None, + })?; + + bedrock_messages.push(bedrock_message); + } + + Ok(bedrock_messages) +} + +/// Convert an image URL to a Bedrock ImageBlock +/// Currently supports data URLs, with HTTP URLs planned for future implementation +fn convert_image_to_bedrock_block(url: &str) -> Result { + // Handle base64 data URLs (supported) + if url.starts_with("data:") { + return convert_data_url_to_image_block(url); + } + + // For HTTP URLs, return an informative error for now + // TODO: Implement async HTTP fetching in a future version + Err(Error { + code: ErrorCode::Unsupported, + message: format!( + "HTTP image URLs not yet supported. Use data URLs like 'data:image/png;base64,...' or configure images via S3. URL: {}", + url + ), + provider_error_json: None, + }) +} + +/// Convert a data URL to ImageBlock +fn convert_data_url_to_image_block(data_url: &str) -> Result { + // Parse data URL format: data:image/png;base64, + if !data_url.starts_with("data:image/") { + return Err(Error { + code: ErrorCode::InvalidRequest, + message: "Only image data URLs are supported".to_string(), + provider_error_json: None, + }); + } + + let parts: Vec<&str> = data_url.splitn(2, ',').collect(); + if parts.len() != 2 { + return Err(Error { + code: ErrorCode::InvalidRequest, + message: "Invalid data URL format".to_string(), + provider_error_json: None, + }); + } + + let header = parts[0]; + let data = parts[1]; + + // Extract format from header: data:image/png;base64 + let format = if header.contains("image/png") { + "png" + } else if header.contains("image/jpeg") || header.contains("image/jpg") { + "jpeg" + } else if header.contains("image/gif") { + "gif" + } else if header.contains("image/webp") { + "webp" + } else { + return Err(Error { + code: ErrorCode::InvalidRequest, + message: "Unsupported image format in data URL".to_string(), + provider_error_json: None, + }); + }; + + // Decode base64 data + let image_bytes = general_purpose::STANDARD.decode(data).map_err(|e| Error { + code: ErrorCode::InvalidRequest, + message: format!("Failed to decode base64 image data: {}", e), + provider_error_json: None, + })?; + + // Create AWS Blob from bytes + let blob = Blob::new(image_bytes); + let image_source = ImageSource::Bytes(blob); + + // Build ImageBlock + let image_block = ImageBlock::builder() + .format(ImageFormat::from(format)) + .source(image_source) + .build() + .map_err(|e| Error { + code: ErrorCode::InternalError, + message: format!("Failed to build ImageBlock: {}", e), + provider_error_json: None, + })?; + + Ok(image_block) +} + +/// Convert Bedrock Converse response to ChatEvent +pub fn bedrock_converse_to_chat_event( + output: aws_sdk_bedrockruntime::operation::converse::ConverseOutput, + _model_id: &str, +) -> Result { + let message = output + .output() + .ok_or_else(|| Error { + code: ErrorCode::InternalError, + message: "No output in Bedrock response".to_string(), + provider_error_json: None, + })? + .as_message() + .map_err(|_| Error { + code: ErrorCode::InternalError, + message: "Bedrock output is not a message".to_string(), + provider_error_json: None, + })?; + + // Extract text and tool calls from content blocks + let mut response_text = String::new(); + let mut tool_calls = Vec::new(); + + for content_block in message.content() { + match content_block { + aws_sdk_bedrockruntime::types::ContentBlock::Text(text) => { + response_text.push_str(&text); + } + aws_sdk_bedrockruntime::types::ContentBlock::ToolUse(tool_use) => { + // Convert Bedrock tool use to golem-llm ToolCall + let arguments_json = format!("{:?}", tool_use.input()); + + let tool_call = ToolCall { + id: tool_use.tool_use_id().to_string(), + name: tool_use.name().to_string(), + arguments_json, + }; + tool_calls.push(tool_call); + } + _ => { + // Handle other content block types if needed + trace!("Unhandled content block type in Bedrock response"); + } + } + } + + // Extract usage information + let usage = output.usage().map(|bedrock_usage| Usage { + input_tokens: Some(bedrock_usage.input_tokens() as u32), + output_tokens: Some(bedrock_usage.output_tokens() as u32), + total_tokens: Some((bedrock_usage.input_tokens() + bedrock_usage.output_tokens()) as u32), + }); + + // Determine finish reason + let finish_reason = match output.stop_reason() { + aws_sdk_bedrockruntime::types::StopReason::EndTurn => Some(FinishReason::Stop), + aws_sdk_bedrockruntime::types::StopReason::ToolUse => Some(FinishReason::ToolCalls), + aws_sdk_bedrockruntime::types::StopReason::MaxTokens => Some(FinishReason::Length), + aws_sdk_bedrockruntime::types::StopReason::StopSequence => Some(FinishReason::Stop), + aws_sdk_bedrockruntime::types::StopReason::ContentFiltered => { + Some(FinishReason::ContentFilter) + } + _ => Some(FinishReason::Stop), // Default to Stop for unknown reasons + }; + + let metadata = ResponseMetadata { + provider_id: None, + usage, + finish_reason, + timestamp: None, + provider_metadata_json: None, + }; + + Ok(ChatEvent::Message(CompleteResponse { + id: String::new(), // Bedrock doesn't provide message IDs in basic converse + content: vec![ContentPart::Text(response_text)], + tool_calls, + metadata, + })) +} + +/// Extract model configuration from Config +pub fn extract_model_config( + config: &golem_llm::golem::llm::llm::Config, +) -> (Option, Option, Option) { + let max_tokens = config + .provider_options + .iter() + .find(|kv| { + kv.key.eq_ignore_ascii_case("max_tokens") || kv.key.eq_ignore_ascii_case("maxTokens") + }) + .and_then(|kv| kv.value.parse().ok()); + + let temperature = config + .provider_options + .iter() + .find(|kv| kv.key.eq_ignore_ascii_case("temperature")) + .and_then(|kv| kv.value.parse().ok()); + + let system_prompt = config + .provider_options + .iter() + .find(|kv| { + kv.key.eq_ignore_ascii_case("system") || kv.key.eq_ignore_ascii_case("system_prompt") + }) + .map(|kv| kv.value.clone()); + + (max_tokens, temperature, system_prompt) +} + +/// Convert tool results to Bedrock messages for the continue_ flow +pub fn tool_results_to_bedrock_messages( + tool_results: Vec<(ToolCall, ToolResult)>, +) -> Vec { + let mut messages = Vec::new(); + + for (tool_call, tool_result) in tool_results { + // Convert tool calls and results to text messages + + // Add assistant message describing the tool call + let tool_call_text = format!("Tool call: {} with id {}", tool_call.name, tool_call.id); + let assistant_message = BedrockMessage::builder() + .role(ConversationRole::Assistant) + .content(ContentBlock::Text(tool_call_text)) + .build() + .expect("Failed to build assistant message with tool call"); + + messages.push(assistant_message); + + // Add user message with the tool result + let result_text = match tool_result { + ToolResult::Success(success) => format!("Tool result: {}", success.result_json), + ToolResult::Error(error) => format!("Tool error: {}", error.error_message), + }; + + let user_message = BedrockMessage::builder() + .role(ConversationRole::User) + .content(ContentBlock::Text(result_text)) + .build() + .expect("Failed to build user message with tool result"); + + messages.push(user_message); + } + + messages +} diff --git a/llm-bedrock/src/lib.rs b/llm-bedrock/src/lib.rs new file mode 100644 index 00000000..c1d2fad1 --- /dev/null +++ b/llm-bedrock/src/lib.rs @@ -0,0 +1,390 @@ +mod client; +mod conversions; +mod stream_bridge; + +use golem_llm::chat_stream::{LlmChatStream, LlmChatStreamState}; +use golem_llm::durability::{DurableLLM, ExtendedGuest}; +use golem_llm::event_source::EventSource; +use golem_llm::golem::llm::llm::{ + ChatEvent, ChatStream, Config, Error as LlmError, ErrorCode, Guest, Message, StreamEvent, + ToolCall, ToolResult, +}; +use golem_llm::LOGGING_STATE; +use log::{debug, error, info, trace, warn}; +use std::cell::{Ref, RefCell, RefMut}; + +use client::{AwsCredentials, BedrockClient, BedrockRequest}; +use conversions::{ + bedrock_converse_to_chat_event, extract_model_config, messages_to_bedrock_converse, + tool_results_to_bedrock_messages, +}; +use stream_bridge::BedrockSdkStreamWrapper; + +// Bedrock stream implementation +pub struct BedrockChatStreamState { + // keep a dummy EventSource for framework compatibility + dummy_stream: RefCell>, + failure: Option, + finished: RefCell, + // Background task handle + _stream_wrapper: RefCell>, +} + +impl BedrockChatStreamState { + pub fn new(stream_wrapper: BedrockSdkStreamWrapper) -> LlmChatStream { + LlmChatStream::new(BedrockChatStreamState { + dummy_stream: RefCell::new(None), + failure: None, + finished: RefCell::new(false), + _stream_wrapper: RefCell::new(Some(stream_wrapper)), + }) + } + + pub fn failed(error: LlmError) -> LlmChatStream { + LlmChatStream::new(BedrockChatStreamState { + dummy_stream: RefCell::new(None), + failure: Some(error), + finished: RefCell::new(true), + _stream_wrapper: RefCell::new(None), + }) + } +} + +impl LlmChatStreamState for BedrockChatStreamState { + fn failure(&self) -> &Option { + &self.failure + } + + fn is_finished(&self) -> bool { + *self.finished.borrow() + } + + fn set_finished(&self) { + *self.finished.borrow_mut() = true; + } + + fn stream(&self) -> Ref> { + // Return the dummy stream - for now this won't be used + self.dummy_stream.borrow() + } + + fn stream_mut(&self) -> RefMut> { + // Return the dummy stream - for now this won't be used + self.dummy_stream.borrow_mut() + } + + fn decode_message(&self, raw: &str) -> Result, String> { + // this method is used for JSON parsing of events generate ourselves + trace!("Bedrock decode_message: {raw}"); + + if raw.trim().is_empty() { + return Ok(None); + } + + // Try to parse as JSON and convert back to StreamEvent + match serde_json::from_str::(raw) { + Ok(json) => { + if let Some(event_type) = json.get("type").and_then(|t| t.as_str()) { + match event_type { + "delta" => { + let mut content = None; + let mut tool_calls = None; + + if let Some(text) = json.get("content").and_then(|c| c.as_str()) { + content = + Some(vec![golem_llm::golem::llm::llm::ContentPart::Text( + text.to_string(), + )]); + } + + if let Some(tc_array) = + json.get("tool_calls").and_then(|tc| tc.as_array()) + { + let parsed_tool_calls: Vec = tc_array + .iter() + .filter_map(|tc| { + if let (Some(id), Some(name), Some(args)) = ( + tc.get("id").and_then(|i| i.as_str()), + tc.get("name").and_then(|n| n.as_str()), + tc.get("arguments").and_then(|a| a.as_str()), + ) { + Some(ToolCall { + id: id.to_string(), + name: name.to_string(), + arguments_json: args.to_string(), + }) + } else { + None + } + }) + .collect(); + if !parsed_tool_calls.is_empty() { + tool_calls = Some(parsed_tool_calls); + } + } + + Ok(Some(StreamEvent::Delta( + golem_llm::golem::llm::llm::StreamDelta { + content, + tool_calls, + }, + ))) + } + "finish" => { + let finish_reason = json + .get("finish_reason") + .and_then(|fr| fr.as_str()) + .map(|fr_str| match fr_str { + "stop" => golem_llm::golem::llm::llm::FinishReason::Stop, + "length" => golem_llm::golem::llm::llm::FinishReason::Length, + "tool_calls" => { + golem_llm::golem::llm::llm::FinishReason::ToolCalls + } + "content_filter" => { + golem_llm::golem::llm::llm::FinishReason::ContentFilter + } + "error" => golem_llm::golem::llm::llm::FinishReason::Error, + _ => golem_llm::golem::llm::llm::FinishReason::Other, + }); + + let usage = + json.get("usage") + .map(|u| golem_llm::golem::llm::llm::Usage { + input_tokens: u + .get("input_tokens") + .and_then(|it| it.as_u64()) + .map(|v| v as u32), + output_tokens: u + .get("output_tokens") + .and_then(|ot| ot.as_u64()) + .map(|v| v as u32), + total_tokens: u + .get("total_tokens") + .and_then(|tt| tt.as_u64()) + .map(|v| v as u32), + }); + + let provider_id = json + .get("provider_id") + .and_then(|pid| pid.as_str()) + .map(|s| s.to_string()); + + Ok(Some(StreamEvent::Finish( + golem_llm::golem::llm::llm::ResponseMetadata { + finish_reason, + usage, + provider_id, + timestamp: None, + provider_metadata_json: None, + }, + ))) + } + "error" => { + let message = json + .get("message") + .and_then(|m| m.as_str()) + .unwrap_or("Unknown error") + .to_string(); + + Ok(Some(StreamEvent::Error(LlmError { + code: ErrorCode::InternalError, + message, + provider_error_json: None, + }))) + } + _ => { + warn!("Unknown event type in decode_message: {event_type}"); + Ok(None) + } + } + } else { + warn!("No event type in JSON: {raw}"); + Ok(None) + } + } + Err(e) => { + warn!("Failed to parse JSON in decode_message: {raw} - Error: {e}"); + Ok(None) + } + } + } +} + +impl Drop for BedrockChatStreamState { + fn drop(&mut self) { + debug!("BedrockChatStreamState dropped"); + } +} + +struct BedrockComponent; + +impl BedrockComponent { + fn get_aws_region(config: &Config) -> Option { + config + .provider_options + .iter() + .find(|kv| { + kv.key.eq_ignore_ascii_case("AWS_REGION") || kv.key.eq_ignore_ascii_case("REGION") + }) + .map(|kv| kv.value.clone()) + } + + fn get_aws_credentials(config: &Config) -> Option { + let access_key_id = config + .provider_options + .iter() + .find(|kv| kv.key.eq_ignore_ascii_case("AWS_ACCESS_KEY_ID")) + .map(|kv| kv.value.clone())?; + + let secret_access_key = config + .provider_options + .iter() + .find(|kv| kv.key.eq_ignore_ascii_case("AWS_SECRET_ACCESS_KEY")) + .map(|kv| kv.value.clone())?; + + let session_token = config + .provider_options + .iter() + .find(|kv| kv.key.eq_ignore_ascii_case("AWS_SESSION_TOKEN")) + .map(|kv| kv.value.clone()); + + Some(AwsCredentials { + access_key_id, + secret_access_key, + session_token, + }) + } + + fn create_client(config: &Config) -> Result { + let region = Self::get_aws_region(config); + let credentials = Self::get_aws_credentials(config); + BedrockClient::new(region, credentials) + } + + fn create_bedrock_request( + messages: &[Message], + config: &Config, + ) -> Result { + let bedrock_messages = messages_to_bedrock_converse(messages)?; + let (max_tokens, temperature, system_prompt) = extract_model_config(config); + + Ok(BedrockRequest { + model_id: config.model.clone(), + messages: bedrock_messages, + system_prompt, + max_tokens, + temperature, + }) + } + + fn request(client: BedrockClient, request: BedrockRequest) -> ChatEvent { + let model_id = request.model_id.clone(); + match client.converse(request) { + Ok(response) => { + bedrock_converse_to_chat_event(response, &model_id).unwrap_or_else(ChatEvent::Error) + } + Err(err) => ChatEvent::Error(err), + } + } + + fn streaming_request( + client: BedrockClient, + request: BedrockRequest, + ) -> LlmChatStream { + match client.converse_stream(request) { + Ok(aws_stream_output) => { + debug!("Successfully created AWS Bedrock stream"); + let stream_wrapper = stream_bridge::BedrockSdkStreamWrapper::new(aws_stream_output); + BedrockChatStreamState::new(stream_wrapper) + } + Err(err) => { + error!("Failed to create Bedrock stream: {err:?}"); + BedrockChatStreamState::failed(err) + } + } + } +} + +impl Guest for BedrockComponent { + type ChatStream = LlmChatStream; + + fn send(messages: Vec, config: Config) -> ChatEvent { + LOGGING_STATE.with_borrow_mut(|state| state.init()); + info!("Bedrock: send called. Model: {}", config.model); + + let client = match Self::create_client(&config) { + Ok(client) => client, + Err(err) => return ChatEvent::Error(err), + }; + + let request = match Self::create_bedrock_request(&messages, &config) { + Ok(request) => request, + Err(err) => return ChatEvent::Error(err), + }; + + Self::request(client, request) + } + + fn continue_( + messages: Vec, + tool_results: Vec<(ToolCall, ToolResult)>, + config: Config, + ) -> ChatEvent { + LOGGING_STATE.with_borrow_mut(|state| state.init()); + info!("Bedrock: continue_ called. Model: {}", config.model); + + let client = match Self::create_client(&config) { + Ok(client) => client, + Err(err) => return ChatEvent::Error(err), + }; + + // Convert original messages to Bedrock format + let mut bedrock_messages = match messages_to_bedrock_converse(&messages) { + Ok(msgs) => msgs, + Err(err) => return ChatEvent::Error(err), + }; + + // Add tool results as additional messages + let tool_result_messages = tool_results_to_bedrock_messages(tool_results); + bedrock_messages.extend(tool_result_messages); + + let (max_tokens, temperature, system_prompt) = extract_model_config(&config); + + let request = BedrockRequest { + model_id: config.model.clone(), + messages: bedrock_messages, + system_prompt, + max_tokens, + temperature, + }; + + Self::request(client, request) + } + + fn stream(messages: Vec, config: Config) -> ChatStream { + ChatStream::new(Self::unwrapped_stream(messages, config)) + } +} + +impl ExtendedGuest for BedrockComponent { + fn unwrapped_stream(messages: Vec, config: Config) -> Self::ChatStream { + LOGGING_STATE.with_borrow_mut(|state| state.init()); + info!("Bedrock: stream called. Model: {}", config.model); + + let client = match Self::create_client(&config) { + Ok(client) => client, + Err(err) => return BedrockChatStreamState::failed(err), + }; + + let request = match Self::create_bedrock_request(&messages, &config) { + Ok(request) => request, + Err(err) => return BedrockChatStreamState::failed(err), + }; + + Self::streaming_request(client, request) + } +} + +type DurableBedrockComponent = DurableLLM; + +golem_llm::export_llm!(DurableBedrockComponent with_types_in golem_llm); diff --git a/llm-bedrock/src/stream_bridge.rs b/llm-bedrock/src/stream_bridge.rs new file mode 100644 index 00000000..8bfd1228 --- /dev/null +++ b/llm-bedrock/src/stream_bridge.rs @@ -0,0 +1,239 @@ +use aws_sdk_bedrockruntime::operation::converse_stream::ConverseStreamOutput as AwsConverseStreamOutput; +use aws_sdk_bedrockruntime::types::ConverseStreamOutput as AwsConverseStreamEventVariant; +use golem_llm::golem::llm::llm::{ + ContentPart, Error as LlmError, ErrorCode, FinishReason, ResponseMetadata, StreamDelta, + StreamEvent as GolemStreamEvent, ToolCall, Usage, +}; +use log::{debug, error, trace, warn}; +use std::collections::HashMap; +use std::time::{SystemTime, UNIX_EPOCH}; + +// State for tracking tool call fragments during streaming +#[derive(Default, Debug, Clone)] +struct ToolCallFragment { + id: String, + name: String, + arguments_json: String, +} + +// This struct will wrap the AWS SDK's event receiver. +pub struct BedrockSdkStreamWrapper { + // Store the whole stream output to work with its public interface + stream_output: AwsConverseStreamOutput, + // State to accumulate usage information from Metadata events + accumulated_usage: Option, + // State for message ID from MessageStart events + message_id: Option, + // State for tracking tool calls by content block index + tool_call_fragments: HashMap, +} + +impl BedrockSdkStreamWrapper { + pub fn new(aws_stream_output: AwsConverseStreamOutput) -> Self { + BedrockSdkStreamWrapper { + stream_output: aws_stream_output, + accumulated_usage: None, + message_id: None, + tool_call_fragments: HashMap::new(), + } + } + + // This method will attempt to pull the next event from the AWS stream + // and convert it into a GolemStreamEvent. + // It needs to be async to call stream methods. + pub async fn next_golem_event(&mut self) -> Option> { + // Use the stream() method to get access to the receiver + match self.stream_output.stream.recv().await { + Ok(Some(aws_event)) => { + trace!("Received AWS SDK stream event: {aws_event:?}"); + self.convert_aws_event_to_golem_stream_event(aws_event) + } + Ok(None) => { + debug!("AWS SDK stream ended."); + None // Stream ended + } + Err(sdk_err) => { + error!("AWS SDK stream error: {sdk_err:?}"); + let code = ErrorCode::InternalError; + let message = format!("SDK Stream Error: {sdk_err:?}"); + Some(Err(LlmError { + code, + message, + provider_error_json: Some(format!("{sdk_err:?}")), + })) + } + } + } + + fn convert_aws_event_to_golem_stream_event( + &mut self, + aws_event: AwsConverseStreamEventVariant, + ) -> Option> { + match aws_event { + AwsConverseStreamEventVariant::ContentBlockStart(start_event) => { + let content_block_index = start_event.content_block_index(); + + // Check if this is a tool use start + if let Some(start) = start_event.start() { + if let Ok(tool_use_start) = start.as_tool_use() { + let tool_id = tool_use_start.tool_use_id().to_string(); + let tool_name = tool_use_start.name().to_string(); + + trace!( + "Tool use started: id={tool_id}, name={tool_name}, index={content_block_index}" + ); + + // Store the tool call fragment + self.tool_call_fragments.insert( + content_block_index, + ToolCallFragment { + id: tool_id, + name: tool_name, + arguments_json: String::new(), + }, + ); + } + } + + None // Don't emit an event for ContentBlockStart + } + AwsConverseStreamEventVariant::ContentBlockDelta(delta) => { + match delta.delta() { + Some(aws_sdk_bedrockruntime::types::ContentBlockDelta::Text(text_delta)) => { + Some(Ok(GolemStreamEvent::Delta(StreamDelta { + content: Some(vec![ContentPart::Text(text_delta.to_string())]), + tool_calls: None, + }))) + } + Some(aws_sdk_bedrockruntime::types::ContentBlockDelta::ToolUse(tool_delta)) => { + // Get the content block index from the delta event + let content_block_index = delta.content_block_index(); + + if let Some(fragment) = + self.tool_call_fragments.get_mut(&content_block_index) + { + // Accumulate the partial JSON input + let input_delta = tool_delta.input(); + fragment.arguments_json.push_str(input_delta); + + trace!( + "Accumulated tool input for index {}: {} chars", + content_block_index, + fragment.arguments_json.len() + ); + } else { + warn!( + "Received ToolUse delta for unknown content block index: {content_block_index}" + ); + } + + None // Don't emit an event for partial tool input + } + Some(_) => { + warn!("Unhandled ContentBlockDelta variant"); + None + } + None => { + warn!("ContentBlockDelta has no delta content"); + None + } + } + } + AwsConverseStreamEventVariant::ContentBlockStop(stop_event) => { + let content_block_index = stop_event.content_block_index(); + + // check for a completed tool call + if let Some(fragment) = self.tool_call_fragments.remove(&content_block_index) { + trace!( + "Tool call completed: id={}, name={}, args={}", + fragment.id, + fragment.name, + fragment.arguments_json + ); + + // Emit the completed tool call + Some(Ok(GolemStreamEvent::Delta(StreamDelta { + content: None, + tool_calls: Some(vec![ToolCall { + id: fragment.id, + name: fragment.name, + arguments_json: fragment.arguments_json, + }]), + }))) + } else { + None // No tool call to complete + } + } + AwsConverseStreamEventVariant::MessageStop(stop_event) => { + // Extract stop reason and map to golem-llm FinishReason + let finish_reason = match stop_event.stop_reason() { + aws_sdk_bedrockruntime::types::StopReason::EndTurn => Some(FinishReason::Stop), + aws_sdk_bedrockruntime::types::StopReason::StopSequence => { + Some(FinishReason::Stop) + } + aws_sdk_bedrockruntime::types::StopReason::MaxTokens => { + Some(FinishReason::Length) + } + aws_sdk_bedrockruntime::types::StopReason::ToolUse => { + Some(FinishReason::ToolCalls) + } + aws_sdk_bedrockruntime::types::StopReason::ContentFiltered => { + Some(FinishReason::ContentFilter) + } + aws_sdk_bedrockruntime::types::StopReason::GuardrailIntervened => { + Some(FinishReason::ContentFilter) + } + _ => { + warn!( + "Unknown stop reason from Bedrock: {:?}", + stop_event.stop_reason() + ); + Some(FinishReason::Other) + } + }; + + // Extract additional metadata if available + let provider_metadata_json = stop_event + .additional_model_response_fields() + .map(|doc| format!("{doc:?}")); // Convert Document to string representation + + // Generate timestamp + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs().to_string()) + .ok(); + + Some(Ok(GolemStreamEvent::Finish(ResponseMetadata { + finish_reason, + usage: self.accumulated_usage.take(), // Use accumulated usage and clear it + provider_id: self.message_id.clone(), // Use message ID from MessageStart + timestamp, + provider_metadata_json, + }))) + } + AwsConverseStreamEventVariant::MessageStart(start_event) => { + // Extract role information from MessageStart event + let role = start_event.role().as_str(); + self.message_id = Some(format!("bedrock_{role}_message")); + + trace!("Message started with role: {role}"); + None // Don't emit an event for MessageStart + } + AwsConverseStreamEventVariant::Metadata(metadata_event) => { + // Extract and store usage information for later use in MessageStop + if let Some(usage) = metadata_event.usage() { + self.accumulated_usage = Some(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), + }); + } + None // Don't emit an event for Metadata + } + _ => { + warn!("Unknown or unhandled AWS Bedrock stream event variant encountered."); + None + } + } + } +} diff --git a/llm-bedrock/wit/bedrock.wit b/llm-bedrock/wit/bedrock.wit new file mode 100644 index 00000000..95bea9de --- /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; +} \ No newline at end of file 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 00000000..f5b5af2d --- /dev/null +++ b/llm-bedrock/wit/deps/golem-llm/golem-llm.wit @@ -0,0 +1,192 @@ +package golem:llm@1.0.0; + +interface llm { + use wasi:io/poll@0.2.0.{pollable}; + + // --- 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, + } + + variant content-part { + text(string), + image(image-url), + } + + 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; + subscribe: func() -> pollable; + } + + // --- 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 { + import wasi:io/poll@0.2.0; + export llm; +} + +// UPDATE NOTES +// send() and continue() returning chat-event directly as it already encodes the error +// made continue's `tool-results` parameter get the original `tool-call` for each `tool-result` - according to OpenAI's docs the call has to be send back too +// made the streaming API use wasi:io/poll.pollable 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 00000000..22e5b648 --- /dev/null +++ b/llm-bedrock/wit/deps/wasi:io/error.wit @@ -0,0 +1,34 @@ +package wasi:io@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 + /// provide functions to further "downcast" this error into more specific + /// error information. For example, `error`s returned in streams derived + /// from filesystem types to be described using the filesystem's own + /// error-code type, using the function + /// `wasi:filesystem/types/filesystem-error-code`, which takes a parameter + /// `borrow` and returns + /// `option`. + /// + /// The set of functions which can "downcast" an `error` into a more + /// concrete type is open. + 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. + 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 00000000..ddc67f8b --- /dev/null +++ b/llm-bedrock/wit/deps/wasi:io/poll.wit @@ -0,0 +1,41 @@ +package wasi:io@0.2.0; + +/// A poll API intended to let users wait for I/O events on multiple handles +/// at once. +interface poll { + /// `pollable` represents a single I/O event which may be ready, or not. + resource pollable { + + /// Return the readiness of a pollable. This function never blocks. + /// + /// Returns `true` when the pollable is ready, and `false` otherwise. + 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. + 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. + /// + /// If the list contains more elements than can be indexed with a `u32` + /// value, this function traps. + /// + /// 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 reaedy for I/O. + 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 00000000..6d2f871e --- /dev/null +++ b/llm-bedrock/wit/deps/wasi:io/streams.wit @@ -0,0 +1,262 @@ +package wasi:io@0.2.0; + +/// 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. +interface streams { + use error.{error}; + use poll.{pollable}; + + /// An error for input-stream and output-stream operations. + variant stream-error { + /// The last operation (a write or flush) failed before completion. + /// + /// More information is available in the `error` payload. + 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`. + 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. + 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`. + 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. + 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`. + 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. + 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`. + 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. + 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. + 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 + /// ``` + 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. + flush: func() -> result<_, stream-error>; + + /// Request to flush buffered output, and block until flush completes + /// and stream is ready for writing again. + 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 occured. 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. + 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. + 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 + /// ``` + 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 equivelant 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`. + 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`. + 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 00000000..5f0b43fe --- /dev/null +++ b/llm-bedrock/wit/deps/wasi:io/world.wit @@ -0,0 +1,6 @@ +package wasi:io@0.2.0; + +world imports { + import streams; + import poll; +} diff --git a/llm-openai/src/bindings.rs b/llm-openai/src/bindings.rs index 6d0a7728..4b5a1d02 100644 --- a/llm-openai/src/bindings.rs +++ b/llm-openai/src/bindings.rs @@ -1,6 +1,8 @@ // Generated by `wit-bindgen` 0.41.0. DO NOT EDIT! // Options used: // * runtime_path: "wit_bindgen_rt" +// * with "wasi:io/poll@0.2.0" = "golem_rust::wasm_rpc::wasi::io::poll" + // * with "golem:llm/llm@1.0.0" = "golem_llm::golem::llm::llm" // * generate_unused_types use golem_llm::golem::llm::llm as __with_name0; @@ -10,6 +12,7 @@ use golem_llm::golem::llm::llm as __with_name0; )] #[doc(hidden)] #[allow(clippy::octal_escapes)] + pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 1759] = *b"\ \0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07\xdd\x0c\x01A\x02\x01\ A\x02\x01BO\x01m\x04\x04user\x09assistant\x06system\x04tool\x04\0\x04role\x03\0\0\ diff --git a/llm-openrouter/src/bindings.rs b/llm-openrouter/src/bindings.rs index 1300cde9..4f7c0581 100644 --- a/llm-openrouter/src/bindings.rs +++ b/llm-openrouter/src/bindings.rs @@ -1,6 +1,8 @@ // Generated by `wit-bindgen` 0.41.0. DO NOT EDIT! // Options used: // * runtime_path: "wit_bindgen_rt" +// * with "wasi:io/poll@0.2.0" = "golem_rust::wasm_rpc::wasi::io::poll" + // * with "golem:llm/llm@1.0.0" = "golem_llm::golem::llm::llm" // * generate_unused_types use golem_llm::golem::llm::llm as __with_name0; diff --git a/llm/src/event_source/event_stream.rs b/llm/src/event_source/event_stream.rs index 48b44f10..141f6e81 100644 --- a/llm/src/event_source/event_stream.rs +++ b/llm/src/event_source/event_stream.rs @@ -214,6 +214,47 @@ impl LlmStream for EventStream { } } +/// Error thrown while parsing an event line +#[derive(Debug, PartialEq)] +pub enum EventStreamError { + /// Source stream is not valid UTF8 + Utf8(FromUtf8Error), + /// Source stream is not a valid EventStream + Parser(NomError), + /// Underlying source stream error + Transport(E), +} + +impl From> for EventStreamError { + fn from(err: Utf8StreamError) -> Self { + match err { + Utf8StreamError::Utf8(err) => Self::Utf8(err), + Utf8StreamError::Transport(err) => Self::Transport(err), + } + } +} + +impl From> for EventStreamError { + fn from(err: NomError<&str>) -> Self { + EventStreamError::Parser(NomError::new(err.input.to_string(), err.code)) + } +} + +impl fmt::Display for EventStreamError +where + E: fmt::Display, +{ + 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}")), + } + } +} + +impl std::error::Error for EventStreamError where E: fmt::Display + fmt::Debug + Send + Sync {} + fn parse_event( buffer: &mut String, builder: &mut EventBuilder, diff --git a/test/components-rust/test-llm/Cargo.toml b/test/components-rust/test-llm/Cargo.toml index ca8b9eb7..d00e0cb0 100644 --- a/test/components-rust/test-llm/Cargo.toml +++ b/test/components-rust/test-llm/Cargo.toml @@ -11,6 +11,7 @@ required-features = [] [features] default = ["openai"] anthropic = [] +bedrock = [] grok = [] openai = [] openrouter = [] diff --git a/test/components-rust/test-llm/golem.yaml b/test/components-rust/test-llm/golem.yaml index 6efa177c..20a048d9 100644 --- a/test/components-rust/test-llm/golem.yaml +++ b/test/components-rust/test-llm/golem.yaml @@ -139,6 +139,28 @@ components: clean: - src/bindings.rs + bedrock-debug: + 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 + # RELEASE PROFILES openai-release: files: @@ -244,6 +266,10 @@ components: clean: - src/bindings.rs + bedrock-release: + build: + - command: cargo component build --release --no-default-features --features bedrock + ollama-release: files: - sourcePath: ../../data/cat.png @@ -251,12 +277,25 @@ components: permissions: read-only build: - command: cargo component build --release --no-default-features --features ollama + 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 + - command: wac plug --plug ../../../target/wasm32-wasip1/release/golem_llm_ollama.wasm ../../target/wasm32-wasip1/release/test_llm.wasm -o ../../target/wasm32-wasip1/release/test_ollama_plugged.wasm sources: - ../../target/wasm32-wasip1/release/test_llm.wasm @@ -267,6 +306,7 @@ components: generatedWit: wit-generated componentWasm: ../../target/wasm32-wasip1/release/test_ollama_plugged.wasm linkedWasm: ../../golem-temp/components/test_ollama_release.wasm + clean: - src/bindings.rs diff --git a/test/components-rust/test-llm/src/lib.rs b/test/components-rust/test-llm/src/lib.rs index fa11684d..1bce2ebe 100644 --- a/test/components-rust/test-llm/src/lib.rs +++ b/test/components-rust/test-llm/src/lib.rs @@ -13,6 +13,8 @@ struct Component; const MODEL: &'static str = "gpt-3.5-turbo"; #[cfg(feature = "anthropic")] const MODEL: &'static str = "claude-3-7-sonnet-20250219"; +#[cfg(feature = "bedrock")] +const MODEL: &'static str = "anthropic.claude-3-5-sonnet-20241022-v2:0"; #[cfg(feature = "grok")] const MODEL: &'static str = "grok-3-beta"; #[cfg(feature = "openrouter")] @@ -24,6 +26,8 @@ const MODEL: &'static str = "qwen3:1.7b"; const IMAGE_MODEL: &'static str = "gpt-4o-mini"; #[cfg(feature = "anthropic")] const IMAGE_MODEL: &'static str = "claude-3-7-sonnet-20250219"; +#[cfg(feature = "bedrock")] +const IMAGE_MODEL: &'static str = "anthropic.claude-3-5-sonnet-20241022-v2:0"; #[cfg(feature = "grok")] const IMAGE_MODEL: &'static str = "grok-2-vision-latest"; #[cfg(feature = "openrouter")]