diff --git a/examples/aiproxy/.gitignore b/examples/aiproxy/.gitignore new file mode 100644 index 0000000..1de5659 --- /dev/null +++ b/examples/aiproxy/.gitignore @@ -0,0 +1 @@ +target \ No newline at end of file diff --git a/examples/aiproxy/README.md b/examples/aiproxy/README.md new file mode 100644 index 0000000..3d56ecd --- /dev/null +++ b/examples/aiproxy/README.md @@ -0,0 +1,26 @@ +# AI Proxy + +This folder contains a [Spin](https://www.fermyon.com/spin) application, based on the WASI 2 and the WebAssembly Component Model ( https://component-model.bytecodealliance.org/ ). It is implemented in Rust as a serverless proxy for the OpenAI API. + +There is a simple example of a web client in the [web](./web/) folder. + +The application will keep track of of token usage per conversation in the built-in key-value storage of Spin. The initial balance for a conversation is retrieved from the Fungible Token smart contract. + +To launch the application, make sure to have the Spin SDK installed. Set the environment variable `SPIN_VARIABLE_OPENAI_API_KEY` to your OpenAI API key. + +Then run the following commands: + +``` +spin build +spin up +``` + +This will start the OpenAI proxy server at http://localhost:3000 + +You can also launch the web client using for example [http-server](https://www.npmjs.com/package/http-server): + +``` +http-server web +``` + +You will then find the web client at http://localhost:8080. Here you can have a conversation with the AI model. diff --git a/examples/aiproxy/openai-proxy/Cargo.lock b/examples/aiproxy/openai-proxy/Cargo.lock new file mode 100644 index 0000000..cb6ac61 --- /dev/null +++ b/examples/aiproxy/openai-proxy/Cargo.lock @@ -0,0 +1,927 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[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 = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bytes" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[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 = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "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.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + +[[package]] +name = "libc" +version = "0.2.161" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "openai-proxy" +version = "0.1.0" +dependencies = [ + "anyhow", + "bs58", + "ed25519-dalek", + "futures", + "hex", + "serde", + "serde_json", + "sha2", + "spin-sdk", + "url", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "proc-macro2" +version = "1.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "routefinder" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0971d3c8943a6267d6bd0d782fdc4afa7593e7381a92a3df950ff58897e066b5" +dependencies = [ + "smartcow", + "smartstring", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.213" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.213" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "serde_json" +version = "1.0.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "smartcow" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "656fcb1c1fca8c4655372134ce87d8afdf5ec5949ebabe8d314be0141d8b5da2" +dependencies = [ + "smartstring", +] + +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check", +] + +[[package]] +name = "spdx" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47317bbaf63785b53861e1ae2d11b80d6b624211d42cb20efcd210ee6f8a14bc" +dependencies = [ + "smallvec", +] + +[[package]] +name = "spin-executor" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2df1a5e2cc70a628c9ea6914770c234cc4a292218091e6707ae8be68b4a5de76" +dependencies = [ + "futures", + "once_cell", + "wit-bindgen", +] + +[[package]] +name = "spin-macro" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef3d03e5a205a641d85ace3af1604b39dba63d3ffe3865a71bda02fb482ae60a" +dependencies = [ + "anyhow", + "bytes", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "spin-sdk" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed97f54a15f2d8b1fa15e436d88bacb95a5b379a3e0f8fbd8042eb8696ca048a" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "form_urlencoded", + "futures", + "http", + "once_cell", + "routefinder", + "serde", + "serde_json", + "spin-executor", + "spin-macro", + "thiserror", + "wit-bindgen", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-encoder" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad2b51884de9c7f4fe2fd1043fccb8dcad4b1e29558146ee57a144d15779f3f" +dependencies = [ + "leb128", +] + +[[package]] +name = "wasm-encoder" +version = "0.41.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "972f97a5d8318f908dded23594188a90bcd09365986b1163e66d70170e5287ae" +dependencies = [ + "leb128", +] + +[[package]] +name = "wasm-metadata" +version = "0.10.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18ebaa7bd0f9e7a5e5dd29b9a998acf21c4abed74265524dd7e85934597bfb10" +dependencies = [ + "anyhow", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "spdx", + "wasm-encoder 0.41.2", + "wasmparser 0.121.2", +] + +[[package]] +name = "wasmparser" +version = "0.118.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77f1154f1ab868e2a01d9834a805faca7bf8b50d041b4ca714d005d0dab1c50c" +dependencies = [ + "indexmap", + "semver", +] + +[[package]] +name = "wasmparser" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dbe55c8f9d0dbd25d9447a5a889ff90c0cc3feaa7395310d3d826b2c703eaab" +dependencies = [ + "bitflags", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b76f1d099678b4f69402a421e888bbe71bf20320c2f3f3565d0e7484dbe5bc20" +dependencies = [ + "bitflags", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75d55e1a488af2981fb0edac80d8d20a51ac36897a1bdef4abde33c29c1b6d0d" +dependencies = [ + "anyhow", + "wit-component", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01ff9cae7bf5736750d94d91eb8a49f5e3a04aff1d1a3218287d9b2964510f8" +dependencies = [ + "anyhow", + "heck", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804a98e2538393d47aa7da65a7348116d6ff403b426665152b70a168c0146d49" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.85", + "wit-bindgen-core", + "wit-bindgen-rust", + "wit-component", +] + +[[package]] +name = "wit-component" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a35a2a9992898c9d27f1664001860595a4bc99d32dd3599d547412e17d7e2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.38.1", + "wasm-metadata", + "wasmparser 0.118.2", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "316b36a9f0005f5aa4b03c39bc3728d045df136f8c13a73b7db4510dec725e08" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/examples/aiproxy/openai-proxy/Cargo.toml b/examples/aiproxy/openai-proxy/Cargo.toml new file mode 100644 index 0000000..d1ef6c7 --- /dev/null +++ b/examples/aiproxy/openai-proxy/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "openai-proxy" +authors = ["Peter Salomonsen "] +description = "" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +anyhow = "1" +bs58 = "0.5.1" +ed25519-dalek = "2.1.1" +futures = "0.3.31" +hex = "0.4.3" +serde = {version = "1.0.213", features = ["serde_derive"]} +serde_json = "1.0.128" +sha2 = "0.10.8" +spin-sdk = "3.0.1" +url = "2.5.2" diff --git a/examples/aiproxy/openai-proxy/src/bindings.rs b/examples/aiproxy/openai-proxy/src/bindings.rs new file mode 100644 index 0000000..af8ec8a --- /dev/null +++ b/examples/aiproxy/openai-proxy/src/bindings.rs @@ -0,0 +1,18 @@ +// Generated by `wit-bindgen` 0.20.0. DO NOT EDIT! +// Options used: + +#[cfg(target_arch = "wasm32")] +#[link_section = "component-type:wit-bindgen:0.20.0:openai-proxy:encoded world"] +#[doc(hidden)] +pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 176] = *b"\ +\0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07.\x01A\x02\x01A\0\x04\ +\x01#component:openai-proxy/openai-proxy\x04\0\x0b\x12\x01\0\x0copenai-proxy\x03\ +\0\0\0G\x09producers\x01\x0cprocessed-by\x02\x0dwit-component\x070.201.0\x10wit-\ +bindgen-rust\x060.20.0"; + +#[inline(never)] +#[doc(hidden)] +#[cfg(target_arch = "wasm32")] +pub fn __link_custom_section_describing_imports() { + wit_bindgen_rt::maybe_link_cabi_realloc(); +} diff --git a/examples/aiproxy/openai-proxy/src/lib.rs b/examples/aiproxy/openai-proxy/src/lib.rs new file mode 100644 index 0000000..fdda48c --- /dev/null +++ b/examples/aiproxy/openai-proxy/src/lib.rs @@ -0,0 +1,463 @@ +use ed25519_dalek::{ed25519::signature::SignerMut, SigningKey}; +use futures::{SinkExt, StreamExt}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::Value; +use sha2::{Digest, Sha256}; +use spin_sdk::{ + http::{self, Headers, IncomingResponse, Method, OutgoingResponse, Request, ResponseOutparam}, + http_component, + key_value::Store, + variables, +}; + +const MIN_TOKENS: u64 = 128_000 + 16384; + +#[derive(Deserialize, Serialize)] +struct ConversationBalance { + receiver_id: String, + #[serde(deserialize_with = "string_to_u64", serialize_with = "u64_to_string")] + amount: u64, + #[serde(default)] + locked_for_ongoing_request: bool, + #[serde(default)] + refund_requested: bool, +} + +impl Default for ConversationBalance { + fn default() -> Self { + Self { + receiver_id: Default::default(), + amount: 0, + locked_for_ongoing_request: true, + refund_requested: false, + } + } +} + +#[derive(Serialize)] +struct RefundMessage { + conversation_id: String, + receiver_id: String, + #[serde(serialize_with = "u64_to_string")] + refund_amount: u64, +} + +#[derive(Serialize)] +struct RefundRequest { + refund_message: String, + #[serde(serialize_with = "serialize_signature")] + signature: [u8; 64], +} + +// Custom serialization function for [u8; 64] +fn serialize_signature(signature: &[u8; 64], serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_bytes(signature) +} + +// Custom serialization function for "amount" field +fn u64_to_string(amount: &u64, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(&amount.to_string()) +} + +fn conversation_id_to_hash_string(conversation_id: &str) -> String { + let mut hasher = Sha256::new(); + hasher.update(conversation_id.as_bytes()); + hex::encode(hasher.finalize()) +} + +fn string_to_u64<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + u64::from_str_radix(&s, 10).map_err(serde::de::Error::custom) +} + +fn get_signing_key(b58_key: &str) -> SigningKey { + // Decode the base58 string to bytes + let key_bytes: [u8; 32] = bs58::decode(b58_key).into_vec().unwrap()[..32] + .try_into() + .unwrap(); + + // Create the SigningKey from bytes + SigningKey::from_bytes(&key_bytes) +} + +fn cors_headers() -> Headers { + Headers::from_list(&[ + ( + "Access-Control-Allow-Origin".to_string(), + "*".to_string().into_bytes(), + ), + ( + "Access-Control-Allow-Methods".to_string(), + "POST, GET, OPTIONS".to_string().into_bytes(), + ), + ( + "Access-Control-Allow-Headers".to_string(), + "Content-Type, Authorization".to_string().into_bytes(), + ), + ]) + .unwrap() +} + +#[http_component] +async fn handle_request(request: Request, response_out: ResponseOutparam) { + let headers = cors_headers(); + match (request.method(), request.path_and_query().as_deref()) { + (Method::Options, Some("/refund-conversation")) => { + let response = OutgoingResponse::new(headers); + response.set_status_code(200).unwrap(); + response_out.set(response); + } + (Method::Post, Some("/refund-conversation")) => { + let incoming_request_body: Value = + serde_json::from_slice(&request.into_body()[..]).unwrap(); + let conversation_id = incoming_request_body["conversation_id"].as_str().unwrap(); + let conversation_balance_store = Store::open_default().unwrap(); + let mut conversation_balance: ConversationBalance = + match conversation_balance_store.get_json(conversation_id) { + Ok(None) => { + return forbidden(response_out, "Conversation does not exist").await; + } + Ok(Some(stored_conversation_balance)) => stored_conversation_balance, + Err(_) => { + eprintln!("Unable to get conversation balance"); + return server_error(response_out); + } + }; + + if conversation_balance.locked_for_ongoing_request { + return forbidden( + response_out, + "There is already an ongoing request for this conversation", + ) + .await; + } + if conversation_balance.refund_requested { + return forbidden( + response_out, + "Refund has already been requested for this conversation", + ) + .await; + } + let refund_message = RefundMessage { + conversation_id: conversation_id_to_hash_string(conversation_id), + receiver_id: conversation_balance.receiver_id.clone(), + refund_amount: conversation_balance.amount, + }; + let response = OutgoingResponse::new(headers); + response.set_status_code(200).unwrap(); + + let refund_message_str = serde_json::to_string(&refund_message).unwrap(); + let bytes = refund_message_str.clone().into_bytes(); + let mut hasher = Sha256::new(); + hasher.update(bytes); + let hashed_message = hasher.finalize(); + + let mut signing_key = + get_signing_key(variables::get("refund_signing_key").unwrap().as_str()); + let signature = signing_key.sign(&hashed_message); + + let refund_request = RefundRequest { + refund_message: refund_message_str, + signature: signature.to_bytes(), + }; + response_out + .set_with_body(response, serde_json::to_vec(&refund_request).unwrap()) + .await + .unwrap(); + + conversation_balance.refund_requested = true; + conversation_balance_store + .set_json(conversation_id, &conversation_balance) + .unwrap(); + } + (Method::Options, Some("/proxy-openai")) => { + let response = OutgoingResponse::new(headers); + response.set_status_code(200).unwrap(); + response_out.set(response); + } + (Method::Post, Some("/proxy-openai")) => { + let incoming_request_body: Value = + serde_json::from_slice(&request.into_body()[..]).unwrap(); + let conversation_id = incoming_request_body["conversation_id"].as_str().unwrap(); + let messages = incoming_request_body["messages"].clone(); + + let conversation_balance_store = Store::open_default().unwrap(); + let mut conversation_balance: ConversationBalance = + match conversation_balance_store.get_json(conversation_id) { + Ok(None) => { + conversation_balance_store + .set_json(conversation_id, &ConversationBalance::default()) + .unwrap(); + match get_initial_token_balance_for_conversation(conversation_id).await { + Ok(result) => result, + Err(_) => { + eprintln!("Unable to get initial conversation balance"); + return server_error(response_out); + } + } + } + Ok(Some(stored_conversation_balance)) => stored_conversation_balance, + Err(_) => { + eprintln!("Unable to get conversation balance"); + return server_error(response_out); + } + }; + + if conversation_balance.locked_for_ongoing_request { + return forbidden( + response_out, + "There is already an ongoing request for this conversation", + ) + .await; + } + + if conversation_balance.refund_requested { + return forbidden( + response_out, + "Refund has been requested for this conversation", + ) + .await; + } + conversation_balance.locked_for_ongoing_request = true; + conversation_balance_store + .set_json(conversation_id, &conversation_balance) + .unwrap(); + + if conversation_balance.amount < MIN_TOKENS { + return forbidden( + response_out, + format!( + "Insufficient tokens ( {} / {}), get a refund and start a new conversation", + conversation_balance.amount, MIN_TOKENS + ) + .as_str(), + ) + .await; + } + + match proxy_openai(messages).await { + Ok(incoming_response) => { + let mut incoming_response_body = incoming_response.take_body_stream(); + let outgoing_response = OutgoingResponse::new(headers); + let mut outgoing_response_body = outgoing_response.take_body(); + + response_out.set(outgoing_response); + + let mut complete_response = String::new(); + let mut completion_id = String::new(); + let mut usage_info: Option = None; + + // Stream the OpenAI response chunks back to the client + while let Some(chunk) = incoming_response_body.next().await { + match chunk { + Ok(data) => { + // Clone the chunk for further processing to avoid move errors + let data_clone = data.clone(); + + // Convert the chunk to a string + if let Ok(chunk_str) = String::from_utf8(data_clone) { + // Split the chunk string by lines + for line in chunk_str.lines() { + if line.starts_with("data: ") { + let json_str = &line[6..]; + if json_str.trim() == "[DONE]" { + break; + } + complete_response.push_str(json_str); + complete_response.push('\n'); + + // Extract completion ID from the first chunk containing metadata + if completion_id.is_empty() { + if let Ok(json_value) = + serde_json::from_str::(json_str) + { + if let Some(id) = + json_value.get("id").and_then(Value::as_str) + { + completion_id = id.to_string(); + } + } + } + + // Extract usage information if present + if let Ok(json_value) = + serde_json::from_str::(json_str) + { + let usage_info_in_json = json_value.get("usage"); + if usage_info_in_json.is_some() { + usage_info = json_value.get("usage").cloned(); + } + } + } + } + } + + // Stream the response chunk back to the client + if let Err(e) = outgoing_response_body.send(data).await { + eprintln!("Error sending response chunk: {e}"); + return; + } + } + Err(e) => { + eprintln!("Error reading response chunk: {e}"); + return; + } + } + } + + // Log usage information if available + if let Some(usage) = usage_info { + if let Some(total_tokens) = usage.get("total_tokens") { + let total_tokens = total_tokens.as_u64().unwrap_or(0); + let model = usage + .get("model") + .and_then(Value::as_str) + .unwrap_or("gpt-3.5-turbo"); + + // Calculate the cost based on the model used + let cost_per_1k_tokens = match model { + "gpt-4" => 0.03, + _ => 0.002, + }; + let cost = (total_tokens as f64 / 1000.0) * cost_per_1k_tokens; + + conversation_balance.amount -= total_tokens; + conversation_balance.locked_for_ongoing_request = false; + conversation_balance_store + .set_json(conversation_id, &conversation_balance) + .unwrap(); + // Log the token usage and cost + eprintln!("Total tokens used: {}", total_tokens); + eprintln!("Model: {}", model); + eprintln!("Cost for this request: ${:.4}", cost); + } + } + } + Err(_e) => { + server_error(response_out); + } + } + } + (Method::Get, _) => { + let response = OutgoingResponse::new(Headers::new()); + response.set_status_code(200).unwrap(); + + let body_content = b"

Proxy OpenAI API

"; + let mut body = response.take_body(); + if let Err(e) = body.send(body_content.to_vec()).await { + eprintln!("Error writing body content: {e}"); + server_error(response_out); + return; + } + + response_out.set(response); + } + _ => { + eprintln!("Method not allowed"); + method_not_allowed(response_out); + } + } +} + +async fn get_initial_token_balance_for_conversation( + conversation_id: &str, +) -> anyhow::Result { + let ft_contract_id = variables::get("ft_contract_id")?; + + let initial_token_balance_request_uri = format!("https://{}.page/web4/contract/{}/view_js_func?function_name=view_ai_conversation&conversation_id={}", ft_contract_id, ft_contract_id, conversation_id_to_hash_string(conversation_id)); + let request = Request::get(initial_token_balance_request_uri).build(); + match http::send::<_, IncomingResponse>(request).await { + Ok(resp) => { + let result: Result = + serde_json::from_slice(&resp.into_body().await?[..]); + match result { + Ok(result) => Ok(result), + Err(e) => { + eprintln!("Error getting initial balance for conversation: {e}"); + return Err(anyhow::anyhow!( + "Error getting initial balance for conversation: {e}" + )); + } + } + } + Err(e) => { + eprintln!("Error getting initial balance for conversation: {e}"); + return Err(anyhow::anyhow!( + "Error getting initial balance for conversation: {e}" + )); + } + } +} + +// Function to handle the actual proxy logic +async fn proxy_openai(messages: Value) -> anyhow::Result { + let request_body = serde_json::json!({ + "model": "gpt-4o", + "messages": messages, + "stream": true, + "stream_options": { + "include_usage": true + } + }); + + let openai_completions_endpoint = variables::get("openai_completions_endpoint")?; + let outgoing_request = Request::builder() + .method(Method::Post) + .uri(openai_completions_endpoint) + .header( + "Authorization", + format!("Bearer {}", variables::get("openai_api_key").unwrap()), + ) + .header("Content-Type", "application/json") + .body(request_body.to_string()) + .build(); + + let response = match http::send::<_, IncomingResponse>(outgoing_request).await { + Ok(resp) => resp, + Err(e) => { + eprintln!("Error sending request to OpenAI: {e}"); + return Err(anyhow::anyhow!("Error sending request to OpenAI: {e}")); + } + }; + + Ok(response) +} + +// Helper functions for error responses +fn server_error(response_out: ResponseOutparam) { + eprintln!("Internal server error"); + respond(500, response_out) +} + +fn method_not_allowed(response_out: ResponseOutparam) { + eprintln!("Method not allowed"); + respond(405, response_out) +} + +async fn forbidden(response_out: ResponseOutparam, reason: &str) { + eprintln!("Forbidden: {}", reason); + let response = OutgoingResponse::new(cors_headers()); + response.set_status_code(403).unwrap(); + if let Err(e) = response.take_body().send(reason.as_bytes().to_vec()).await { + eprintln!("Error writing body content: {e}"); + server_error(response_out); + return; + } + response_out.set(response); +} + +fn respond(status: u16, response_out: ResponseOutparam) { + let response = OutgoingResponse::new(cors_headers()); + response.set_status_code(status).unwrap(); + + response_out.set(response); +} diff --git a/examples/aiproxy/spin.toml b/examples/aiproxy/spin.toml new file mode 100644 index 0000000..d2f7a98 --- /dev/null +++ b/examples/aiproxy/spin.toml @@ -0,0 +1,38 @@ +spin_manifest_version = 2 + +[application] +name = "openai-proxy" +version = "0.1.0" +authors = ["Peter Salomonsen "] +description = "" + +[[trigger.http]] +route = "/..." +component = "openai-proxy" + +[component.openai-proxy] +source = "openai-proxy/target/wasm32-wasi/release/openai_proxy.wasm" +allowed_outbound_hosts = ["https://api.openai.com:443", "https://aitoken.testnet.page:443"] +key_value_stores = ["default"] + +[component.openai-proxy.build] +command = "cargo build --target wasm32-wasi --release" +workdir = "openai-proxy" +watch = ["src/**/*.rs", "Cargo.toml"] + +[variables] +openai_api_key = { required = true } +refund_signing_key = { required = true } +openai_completions_endpoint = { required = true } +ft_contract_id = { required = true } + +[component.openai-proxy.variables] +openai_api_key = "{{ openai_api_key }}" +refund_signing_key = "{{ refund_signing_key }}" +openai_completions_endpoint = "{{ openai_completions_endpoint }}" +ft_contract_id = "{{ ft_contract_id }}" + +[component.openai-proxy.tool.spin-test] +source = "tests/target/wasm32-wasip1/release/tests.wasm" +build = "cargo component build --release" +workdir = "tests" diff --git a/examples/aiproxy/tests/Cargo.lock b/examples/aiproxy/tests/Cargo.lock new file mode 100644 index 0000000..682fd9c --- /dev/null +++ b/examples/aiproxy/tests/Cargo.lock @@ -0,0 +1,725 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "anyhow" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[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 = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[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 = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[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.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.0", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + +[[package]] +name = "libc" +version = "0.2.161" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.213" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.213" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "spdx" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47317bbaf63785b53861e1ae2d11b80d6b624211d42cb20efcd210ee6f8a14bc" +dependencies = [ + "smallvec", +] + +[[package]] +name = "spin-test-sdk" +version = "0.1.0" +source = "git+https://github.com/fermyon/spin-test#c2492696a054abd6439d47c5754d9d230517aafe" +dependencies = [ + "spin-test-sdk-macro", + "wit-bindgen", +] + +[[package]] +name = "spin-test-sdk-macro" +version = "0.1.0" +source = "git+https://github.com/fermyon/spin-test#c2492696a054abd6439d47c5754d9d230517aafe" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", + "wit-component 0.211.1", + "wit-parser 0.211.1", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[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.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tests" +version = "0.1.0" +dependencies = [ + "bs58", + "ed25519-dalek", + "serde_json", + "sha2", + "spin-test-sdk", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-encoder" +version = "0.209.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4a05336882dae732ce6bd48b7e11fe597293cb72c13da4f35d7d5f8d53b2a7" +dependencies = [ + "leb128", +] + +[[package]] +name = "wasm-encoder" +version = "0.211.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e7d931a1120ef357f32b74547646b6fa68ea25e377772b72874b131a9ed70d4" +dependencies = [ + "leb128", + "wasmparser 0.211.1", +] + +[[package]] +name = "wasm-metadata" +version = "0.209.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d32029ce424f6d3c2b39b4419fb45a0e2d84fb0751e0c0a32b7ce8bd5d97f46" +dependencies = [ + "anyhow", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "spdx", + "wasm-encoder 0.209.1", + "wasmparser 0.209.1", +] + +[[package]] +name = "wasm-metadata" +version = "0.211.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f56bad1c68558c44e7f60be865117dc9d8c3a066bcf3b2232cb9a9858965fd5" +dependencies = [ + "anyhow", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "spdx", + "wasm-encoder 0.211.1", + "wasmparser 0.211.1", +] + +[[package]] +name = "wasmparser" +version = "0.209.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07035cc9a9b41e62d3bb3a3815a66ab87c993c06fe1cf6b2a3f2a18499d937db" +dependencies = [ + "ahash", + "bitflags", + "hashbrown 0.14.5", + "indexmap", + "semver", +] + +[[package]] +name = "wasmparser" +version = "0.211.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3189cc8a91f547390e2f043ca3b3e3fe0892f7d581767fd4e4b7f3dc3fe8e561" +dependencies = [ + "ahash", + "bitflags", + "hashbrown 0.14.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84376ff4f74ed07674a1157c0bd19e6627ab01fc90952a27ccefb52a24530f0" +dependencies = [ + "wit-bindgen-rt", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36d4706efb67fadfbbde77955b299b111dd096e6776d8c6561d92f6147941880" +dependencies = [ + "anyhow", + "heck", + "wit-parser 0.209.1", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c7526379ace8709ee9ab9f2bb50f112d95581063a59ef3097d9c10153886c9" +dependencies = [ + "bitflags", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514295193d1a2f42e6a948cd7d9fd81e2b8fadc319667dcf19fd7aceaf2113a2" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "wasm-metadata 0.209.1", + "wit-bindgen-core", + "wit-component 0.209.1", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0409a3356ca02599aff78f717968fd7f12df4bf879f325e2a97b45c84c90fff" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.209.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bb5b039f9cb03425e1d5a6e54b441ca4ca1b1d4fa6a0924db67a55168f99" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.209.1", + "wasm-metadata 0.209.1", + "wasmparser 0.209.1", + "wit-parser 0.209.1", +] + +[[package]] +name = "wit-component" +version = "0.211.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "079a38b7d679867424bf2bcbdd553a2acf364525307e43dfb910fa4a2c6fd9f2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.211.1", + "wasm-metadata 0.211.1", + "wasmparser 0.211.1", + "wit-parser 0.211.1", +] + +[[package]] +name = "wit-parser" +version = "0.209.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e79b9e3c0b6bb589dec46317e645851e0db2734c44e2be5e251b03ff4a51269" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.209.1", +] + +[[package]] +name = "wit-parser" +version = "0.211.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3cc90c50c7ec8a824b5d2cddddff13b2dc12b7a96bf8684d11474223c2ea22f" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.211.1", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/examples/aiproxy/tests/Cargo.toml b/examples/aiproxy/tests/Cargo.toml new file mode 100644 index 0000000..d2f7b08 --- /dev/null +++ b/examples/aiproxy/tests/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "tests" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +spin-test-sdk = { git = "https://github.com/fermyon/spin-test", version = "0.1.0" } +serde_json = "1.0.128" +ed25519-dalek = "2.1.1" +bs58 = "0.5.1" +sha2 = "0.10.8" diff --git a/examples/aiproxy/tests/src/bindings.rs b/examples/aiproxy/tests/src/bindings.rs new file mode 100644 index 0000000..5d9b2cf --- /dev/null +++ b/examples/aiproxy/tests/src/bindings.rs @@ -0,0 +1,18 @@ +// Generated by `wit-bindgen` 0.20.0. DO NOT EDIT! +// Options used: + +#[cfg(target_arch = "wasm32")] +#[link_section = "component-type:wit-bindgen:0.20.0:tests:encoded world"] +#[doc(hidden)] +pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 155] = *b"\ +\0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07\x20\x01A\x02\x01A\0\x04\ +\x01\x15component:tests/tests\x04\0\x0b\x0b\x01\0\x05tests\x03\0\0\0G\x09produce\ +rs\x01\x0cprocessed-by\x02\x0dwit-component\x070.201.0\x10wit-bindgen-rust\x060.\ +20.0"; + +#[inline(never)] +#[doc(hidden)] +#[cfg(target_arch = "wasm32")] +pub fn __link_custom_section_describing_imports() { + wit_bindgen_rt::maybe_link_cabi_realloc(); +} diff --git a/examples/aiproxy/tests/src/lib.rs b/examples/aiproxy/tests/src/lib.rs new file mode 100644 index 0000000..fbff5d7 --- /dev/null +++ b/examples/aiproxy/tests/src/lib.rs @@ -0,0 +1,311 @@ +use ed25519_dalek::{Signature, VerifyingKey}; +use serde_json::{json, Value}; +use sha2::{Digest, Sha256}; +use spin_test_sdk::{ + bindings::{ + fermyon::{spin_test_virt, spin_wasi_virt::http_handler}, + wasi::{ + self, + http::{self, types::OutgoingResponse}, + }, + }, + spin_test, +}; + +const SIGNING_PUBLIC_KEY: &str = "63LxSTBisoUfp3Gu7eGY8kAVcRAmZacZjceJ2jNeGZLH"; + +fn handle_openai_request() { + let openai_response = http::types::OutgoingResponse::new(http::types::Headers::new()); + openai_response.write_body("data: {\"id\":\"chatcmpl-AMaFCyZmtLWFTUrXg0ZyEI9gz0wbj\",\"object\":\"chat.completion.chunk\",\"created\":1729945902,\"model\":\"gpt-4o-2024-08-06\",\"system_fingerprint\":\"fp_72bbfa6014\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-AMaFCyZmtLWFTUrXg0ZyEI9gz0wbj\",\"object\":\"chat.completion.chunk\",\"created\":1729945902,\"model\":\"gpt-4o-2024-08-06\",\"system_fingerprint\":\"fp_72bbfa6014\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Hello\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\n +data: {\"id\":\"chatcmpl-AMaFCyZmtLWFTUrXg0ZyEI9gz0wbj\",\"object\":\"chat.completion.chunk\",\"created\":1729945902,\"model\":\"gpt-4o-2024-08-06\",\"system_fingerprint\":\"fp_72bbfa6014\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"!\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-AMaFCyZmtLWFTUrXg0ZyEI9gz0wbj\",\"object\":\"chat.completion.chunk\",\"created\":1729945902,\"model\":\"gpt-4o-2024-08-06\",\"system_fingerprint\":\"fp_72bbfa6014\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" How\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\n +data: {\"id\":\"chatcmpl-AMaFCyZmtLWFTUrXg0ZyEI9gz0wbj\",\"object\":\"chat.completion.chunk\",\"created\":1729945902,\"model\":\"gpt-4o-2024-08-06\",\"system_fingerprint\":\"fp_72bbfa6014\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" can\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-AMaFCyZmtLWFTUrXg0ZyEI9gz0wbj\",\"object\":\"chat.completion.chunk\",\"created\":1729945902,\"model\":\"gpt-4o-2024-08-06\",\"system_fingerprint\":\"fp_72bbfa6014\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" I\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\n +data: {\"id\":\"chatcmpl-AMaFCyZmtLWFTUrXg0ZyEI9gz0wbj\",\"object\":\"chat.completion.chunk\",\"created\":1729945902,\"model\":\"gpt-4o-2024-08-06\",\"system_fingerprint\":\"fp_72bbfa6014\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" assist\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-AMaFCyZmtLWFTUrXg0ZyEI9gz0wbj\",\"object\":\"chat.completion.chunk\",\"created\":1729945902,\"model\":\"gpt-4o-2024-08-06\",\"system_fingerprint\":\"fp_72bbfa6014\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" you\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\n +data: {\"id\":\"chatcmpl-AMaFCyZmtLWFTUrXg0ZyEI9gz0wbj\",\"object\":\"chat.completion.chunk\",\"created\":1729945902,\"model\":\"gpt-4o-2024-08-06\",\"system_fingerprint\":\"fp_72bbfa6014\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" today\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-AMaFCyZmtLWFTUrXg0ZyEI9gz0wbj\",\"object\":\"chat.completion.chunk\",\"created\":1729945902,\"model\":\"gpt-4o-2024-08-06\",\"system_fingerprint\":\"fp_72bbfa6014\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"?\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-AMaFCyZmtLWFTUrXg0ZyEI9gz0wbj\",\"object\":\"chat.completion.chunk\",\"created\":1729945902,\"model\":\"gpt-4o-2024-08-06\",\"system_fingerprint\":\"fp_72bbfa6014\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-AMaFCyZmtLWFTUrXg0ZyEI9gz0wbj\",\"object\":\"chat.completion.chunk\",\"created\":1729945902,\"model\":\"gpt-4o-2024-08-06\",\"system_fingerprint\":\"fp_72bbfa6014\",\"choices\":[],\"usage\":{\"prompt_tokens\":18,\"completion_tokens\":9,\"total_tokens\":27,\"prompt_tokens_details\":{\"cached_tokens\":0},\"completion_tokens_details\":{\"reasoning_tokens\":0}}}\n\ndata: [DONE]\n\n +".as_bytes()); + + http_handler::set_response( + "https://api.openai.com/v1/chat/completions", + http_handler::ResponseHandler::Response(openai_response), + ); +} + +fn handle_near_ai_token_request() { + let response = http::types::OutgoingResponse::new(http::types::Headers::new()); + response.write_body( + json!({"receiver_id":"aiuser.testnet","amount":"256000"}) + .to_string() + .as_bytes(), + ); + + http_handler::set_response("https://aitoken.testnet.page/web4/contract/aitoken.testnet/view_js_func?function_name=view_ai_conversation&conversation_id=aiuser.testnet_1729432017818", http_handler::ResponseHandler::Response(response)); + + let unknown_conversation_response = + http::types::OutgoingResponse::new(http::types::Headers::new()); + unknown_conversation_response.write_body("invalid json response body at https://rpc.web4.testnet.page/account/aitoken.testnet/view/view_js_func reason: Unexpected end of JSON input\n".as_bytes()); + unknown_conversation_response.set_status_code(400).unwrap(); + + http_handler::set_response("https://aitoken.testnet.page/web4/contract/aitoken.testnet/view_js_func?function_name=view_ai_conversation&conversation_id=aiuser.testnet_1729432017819", http_handler::ResponseHandler::Response(unknown_conversation_response)); + + let insufficient_funds_response = + http::types::OutgoingResponse::new(http::types::Headers::new()); + insufficient_funds_response.write_body( + json!({"receiver_id":"aiuser.testnet","amount":"26"}) + .to_string() + .as_bytes(), + ); + + http_handler::set_response("https://aitoken.testnet.page/web4/contract/aitoken.testnet/view_js_func?function_name=view_ai_conversation&conversation_id=aiuser.testnet_1729432017820", http_handler::ResponseHandler::Response(insufficient_funds_response)); +} + +fn set_variables() { + spin_test_virt::variables::set( + "refund_signing_key", + "5J4fAKqUQj1RT3JD2d58gWiXBNGavrZQPYbNMJwDjHnhF8J8KVC1UHxVu3f7Ng2tFkA9fXcECNW9xuf7iZpcYh1X", + ); + spin_test_virt::variables::set("openai_api_key", "hello"); + spin_test_virt::variables::set( + "openai_completions_endpoint", + "https://api.openai.com/v1/chat/completions", + ); + spin_test_virt::variables::set("ft_contract_id", "aitoken.testnet"); +} + +#[spin_test] +fn test_hello_api() { + // Perform the request + let request = http::types::OutgoingRequest::new(http::types::Headers::new()); + request.set_path_with_query(Some("/")).unwrap(); + let response = spin_test_sdk::perform_request(request); + + // Assert response status and body is 404 + assert_eq!(response.status(), 200); + assert!(response + .body_as_string() + .unwrap() + .contains("Proxy OpenAI API")); +} + +#[spin_test] +fn openai_request() { + set_variables(); + + handle_openai_request(); + handle_near_ai_token_request(); + + let request = http::types::OutgoingRequest::new(http::types::Headers::new()); + request.set_method(&http::types::Method::Post).unwrap(); + request.set_path_with_query(Some("/proxy-openai")).unwrap(); + request.body().unwrap().write_bytes(json!( + { + "conversation_id": "aiuser.testnet_1729432017818", + "messages":[{"role":"system","content":"You are a helpful assistant."},{"role":"user","content":"hello"}] + }).to_string().as_bytes()); + let response = spin_test_sdk::perform_request(request); + + assert_eq!(response.status(), 200); + assert!(response.body_as_string().unwrap().contains("[DONE]\n\n")); + let store = spin_test_virt::key_value::Store::open("default"); + let stored_conversation_balance: serde_json::Value = + serde_json::from_slice(&store.get("aiuser.testnet_1729432017818").unwrap()[..]).unwrap(); + + assert_eq!( + u64::from_str_radix(stored_conversation_balance["amount"].as_str().unwrap(), 10).unwrap(), + (256000 - 27) as u64 + ); + assert_eq!( + stored_conversation_balance["locked_for_ongoing_request"], + false + ); +} + +#[spin_test] +fn openai_request_unknown_conversation() { + set_variables(); + + handle_openai_request(); + handle_near_ai_token_request(); + + let request = http::types::OutgoingRequest::new(http::types::Headers::new()); + request.set_method(&http::types::Method::Post).unwrap(); + request.set_path_with_query(Some("/proxy-openai")).unwrap(); + request.body().unwrap().write_bytes(json!( + { + "conversation_id": "aiuser.testnet_1729432017819", + "messages":[{"role":"system","content":"You are a helpful assistant."},{"role":"user","content":"hello"}] + }).to_string().as_bytes()); + let response = spin_test_sdk::perform_request(request); + assert_eq!(response.status(), 500); +} + +#[spin_test] +fn openai_request_insufficient_funds_deposited() { + set_variables(); + + handle_openai_request(); + handle_near_ai_token_request(); + + let request = http::types::OutgoingRequest::new(http::types::Headers::new()); + request.set_method(&http::types::Method::Post).unwrap(); + request.set_path_with_query(Some("/proxy-openai")).unwrap(); + request.body().unwrap().write_bytes(json!( + { + "conversation_id": "aiuser.testnet_1729432017820", + "messages":[{"role":"system","content":"You are a helpful assistant."},{"role":"user","content":"hello"}] + }).to_string().as_bytes()); + let response = spin_test_sdk::perform_request(request); + + assert_eq!(response.status(), 403); + assert!(response + .body_as_string() + .unwrap() + .contains("Insufficient tokens")); + let store = spin_test_virt::key_value::Store::open("default"); + + let stored_conversation_balance: serde_json::Value = + serde_json::from_slice(&store.get("aiuser.testnet_1729432017820").unwrap()[..]).unwrap(); + assert_eq!(stored_conversation_balance["amount"], "26"); +} + +#[spin_test] +fn openai_request_insufficient_funds_ongoing_conversation() { + set_variables(); + + handle_openai_request(); + + let store = spin_test_virt::key_value::Store::open("default"); + + store.set( + "aiuser.testnet_1729432017818", + json!({ + "receiver_id": "aiuser.testnet", + "amount": "128000" + }) + .to_string() + .as_bytes(), + ); + + let request = http::types::OutgoingRequest::new(http::types::Headers::new()); + request.set_method(&http::types::Method::Post).unwrap(); + request.set_path_with_query(Some("/proxy-openai")).unwrap(); + request.body().unwrap().write_bytes(json!( + { + "conversation_id": "aiuser.testnet_1729432017818", + "messages":[{"role":"system","content":"You are a helpful assistant."},{"role":"user","content":"hello"}] + }).to_string().as_bytes()); + let response = spin_test_sdk::perform_request(request); + + assert_eq!(response.status(), 403); + assert!(response + .body_as_string() + .unwrap() + .contains("Insufficient tokens")); + + let stored_conversation_balance: serde_json::Value = + serde_json::from_slice(&store.get("aiuser.testnet_1729432017818").unwrap()[..]).unwrap(); + assert_eq!(stored_conversation_balance["amount"], "128000"); +} + +#[spin_test] +fn concurrent_requests_should_throw_error() { + set_variables(); + + handle_openai_request(); + + let store = spin_test_virt::key_value::Store::open("default"); + + store.set( + "aiuser.testnet_1729432017818", + json!({ + "receiver_id": "aiuser.testnet", + "amount": "128000", + "locked_for_ongoing_request": true + }) + .to_string() + .as_bytes(), + ); + + let request = http::types::OutgoingRequest::new(http::types::Headers::new()); + request.set_method(&http::types::Method::Post).unwrap(); + request.set_path_with_query(Some("/proxy-openai")).unwrap(); + request.body().unwrap().write_bytes(json!( + { + "conversation_id": "aiuser.testnet_1729432017818", + "messages":[{"role":"system","content":"You are a helpful assistant."},{"role":"user","content":"hello"}] + }).to_string().as_bytes()); + let response = spin_test_sdk::perform_request(request); + + assert_eq!(response.status(), 403); + assert!(response + .body_as_string() + .unwrap() + .contains("There is already an ongoing request for this conversation")); + + let stored_conversation_balance: serde_json::Value = + serde_json::from_slice(&store.get("aiuser.testnet_1729432017818").unwrap()[..]).unwrap(); + assert_eq!(stored_conversation_balance["amount"], "128000"); +} + +#[spin_test] +fn request_refund() { + set_variables(); + + let store = spin_test_virt::key_value::Store::open("default"); + + store.set( + "aiuser.testnet_1729432017818", + json!({ + "receiver_id": "aiuser.testnet", + "amount": "128000", + "locked_for_ongoing_request": false + }) + .to_string() + .as_bytes(), + ); + + let request = http::types::OutgoingRequest::new(http::types::Headers::new()); + request.set_method(&http::types::Method::Post).unwrap(); + request + .set_path_with_query(Some("/refund-conversation")) + .unwrap(); + request.body().unwrap().write_bytes( + json!( + { + "conversation_id": "aiuser.testnet_1729432017818" + }) + .to_string() + .as_bytes(), + ); + let response = spin_test_sdk::perform_request(request); + + assert_eq!(response.status(), 200); + let result: Value = serde_json::from_str(response.body_as_string().unwrap().as_str()).unwrap(); + let refund_message_str = result["refund_message"].as_str().unwrap(); + let refund_message: Value = + serde_json::from_str(result["refund_message"].as_str().unwrap()).unwrap(); + assert_eq!( + refund_message["receiver_id"].as_str().unwrap(), + "aiuser.testnet" + ); + assert_eq!(refund_message["refund_amount"].as_str().unwrap(), "128000"); + + let public_key_bytes: [u8; 32] = bs58::decode(SIGNING_PUBLIC_KEY).into_vec().unwrap()[..32] + .try_into() + .unwrap(); + let public_key = VerifyingKey::from_bytes(&public_key_bytes).unwrap(); + let mut hasher = Sha256::new(); + hasher.update(refund_message_str.as_bytes()); + let hashed_message = hasher.finalize(); + + let signature_vec = result["signature"] + .as_array() + .expect("Expected 'signature' to be an array") + .iter() + .map(|v| v.as_u64().expect("Expected each item to be a number") as u8) + .collect::>(); + + let mut signature: [u8; 64] = [0u8; 64]; + signature.copy_from_slice(&signature_vec[..64]); + assert!(public_key + .verify_strict(&hashed_message, &Signature::from_bytes(&signature)) + .is_ok()); +} diff --git a/examples/aiproxy/web/index.html b/examples/aiproxy/web/index.html new file mode 100644 index 0000000..2e1f6a9 --- /dev/null +++ b/examples/aiproxy/web/index.html @@ -0,0 +1,152 @@ + + + + + + OpenAI Proxy Streaming + + + +

OpenAI Proxy Streaming UI

+
+
+
+ + + + +
+
+
+            
+        
+
+ + + \ No newline at end of file