diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5d664e723..10a419c2a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -137,7 +137,7 @@ jobs: - name: Build and test Ollama integration run: | set -e - cargo make --cwd llm build-ollama + cargo make build cd test golem-cli app build -b ollama-debug golem-cli app deploy -b ollama-debug @@ -183,4 +183,4 @@ jobs: - name: Login GH CLI shell: bash run: gh auth login --with-token < <(echo ${{ secrets.GITHUB_TOKEN }}) - - run: gh release upload -R golemcloud/golem-llm --clobber ${{ github.ref_name }} components/release/*.wasm + - run: gh release upload -R golemcloud/golem-llm --clobber ${{ github.ref_name }} components/release/*.wasm \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 0865d6ade..4d028dcc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,6 +71,12 @@ version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +[[package]] +name = "bytemuck" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" + [[package]] name = "bytes" version = "1.10.1" @@ -332,6 +338,79 @@ dependencies = [ "syn", ] +[[package]] +name = "golem-embed" +version = "0.0.0" +dependencies = [ + "golem-rust", + "log", + "mime", + "nom", + "reqwest", + "thiserror", + "wasi-logger", + "wit-bindgen 0.40.0", +] + +[[package]] +name = "golem-embed-cohere" +version = "0.0.0" +dependencies = [ + "base64 0.22.1", + "golem-embed", + "golem-rust", + "log", + "reqwest", + "serde", + "serde_json", + "wit-bindgen-rt 0.40.0", +] + +[[package]] +name = "golem-embed-hugging-face" +version = "0.0.0" +dependencies = [ + "base64 0.22.1", + "bytemuck", + "golem-embed", + "golem-rust", + "log", + "reqwest", + "serde", + "serde_json", + "wit-bindgen-rt 0.40.0", +] + +[[package]] +name = "golem-embed-openai" +version = "0.0.0" +dependencies = [ + "base64 0.22.1", + "bytemuck", + "golem-embed", + "golem-rust", + "log", + "reqwest", + "serde", + "serde_json", + "wit-bindgen-rt 0.40.0", +] + +[[package]] +name = "golem-embed-voyageai" +version = "0.0.0" +dependencies = [ + "base64 0.22.1", + "bytemuck", + "golem-embed", + "golem-rust", + "log", + "reqwest", + "serde", + "serde_json", + "wit-bindgen-rt 0.40.0", +] + [[package]] name = "golem-llm" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index 7bea1e1e5..5f97ad91e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,11 @@ members = [ "llm/ollama", "llm/openai", "llm/openrouter", + "embed/embed", + "embed/cohere", + "embed/hugging-face", + "embed/openai", + "embed/voyageai", ] [profile.release] diff --git a/Makefile.toml b/Makefile.toml index cc443bc6a..3e702b60d 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -6,6 +6,12 @@ skip_core_tasks = true command = "cargo" args = ["clean"] +[tasks.clean-all] +script_runner = "@duckscript" +script = ''' +exec find . -type d \( -name "target" -o -name "wit-generated" -o -name "golem-temp" \) -exec rm -rf {} + +''' + [tasks.unit-tests] command = "cargo" args = ["test"] @@ -13,7 +19,7 @@ args = ["test"] [tasks.build] script_runner = "@duckscript" script = ''' -domains = array llm +domains = array llm embed # if there is no domain passed run for every domain if is_empty ${1} @@ -28,7 +34,7 @@ end [tasks.release-build] script_runner = "@duckscript" script = ''' -domains = array llm +domains = array llm embed # if there is no domain passed run for every domain if is_empty ${1} @@ -44,7 +50,7 @@ end script_runner = "@duckscript" script = ''' #!/bin/bash -domains = array llm +domains = array llm embed # if there is no domain passed run for every domain if is_empty ${1} @@ -60,7 +66,7 @@ end script_runner = "@duckscript" script = ''' #!/bin/bash -domains = array llm +domains = array llm embed # if there is no domain passed run for every domain if is_empty ${1} @@ -75,7 +81,7 @@ end [tasks.wit] script_runner = "@duckscript" script = ''' -domains = array llm +domains = array llm embed # if there is no domain passed run for every domain if is_empty ${1} @@ -91,7 +97,7 @@ end description = "Builds all test components with golem-cli" script_runner = "@duckscript" script = ''' -domains = array llm +domains = array llm embed # if there is no domain passed run for every domain if is_empty ${1} @@ -137,7 +143,7 @@ script = ''' is_portable = eq ${1} "--portable" -targets = array llm_openai llm_anthropic llm_grok llm_openrouter llm_ollama +targets = array llm_openai llm_anthropic llm_grok llm_openrouter llm_ollama embed_openai embed_cohere embed_hugging_face embed_voyageai for target in ${targets} if is_portable cp target/wasm32-wasip1/debug/golem_${target}.wasm components/debug/golem_${target}-portable.wasm @@ -153,7 +159,7 @@ script = ''' is_portable = eq ${1} "--portable" -targets = array llm_openai llm_anthropic llm_grok llm_openrouter llm_ollama +targets = array llm_openai llm_anthropic llm_grok llm_openrouter llm_ollama embed_openai embed_cohere embed_hugging_face embed_voyageai for target in ${targets} if is_portable cp target/wasm32-wasip1/release/golem_${target}.wasm components/release/golem_${target}-portable.wasm @@ -229,4 +235,4 @@ foreach ($file in $cargoFiles) Foreach-Object { $_ -replace "0.0.0", $Env:VERSION } | Set-Content $file.PSPath } -''' +''' \ No newline at end of file diff --git a/embed/Makefile.toml b/embed/Makefile.toml new file mode 100644 index 000000000..fa8e17d79 --- /dev/null +++ b/embed/Makefile.toml @@ -0,0 +1,169 @@ +[config] +default_to_workspace = false +skip_core_tasks = true + +[tasks.build] +run_task = { name = [ + "build-openai", + "build-cohere", + "build-hugging-face", + "build-voyageai", +] } + +[tasks.build-portable] +run_task = { name = [ + "build-openai-portable", + "build-cohere-portable", + "build-hugging-face-portable", + "build-voyageai-portable", +] } + +[tasks.release-build] +run_task = { name = [ + "release-build-openai", + "release-build-cohere", + "release-build-hugging-face", + "release-build-voyageai", +] } + +[tasks.release-build-portable] +run_task = { name = [ + "release-build-openai-portable", + "release-build-cohere-portable", + "release-build-hugging-face-portable", + "release-build-voyageai-portable", +] } + +[tasks.build-openai] +install_crate = { crate_name = "cargo-component", version = "0.20.0" } +command = "cargo-component" +args = ["build", "-p", "golem-embed-openai"] + +[tasks.build-openai-portable] +install_crate = { crate_name = "cargo-component", version = "0.20.0" } +command = "cargo-component" +args = ["build", "-p", "golem-embed-openai", "--no-default-features"] + +[tasks.build-cohere] +install_crate = { crate_name = "cargo-component", version = "0.20.0" } +command = "cargo-component" +args = ["build", "-p", "golem-embed-cohere"] + +[tasks.build-cohere-portable] +install_crate = { crate_name = "cargo-component", version = "0.20.0" } +command = "cargo-component" +args = ["build", "-p", "golem-embed-cohere", "--no-default-features"] + +[tasks.build-hugging-face] +install_crate = { crate_name = "cargo-component", version = "0.20.0" } +command = "cargo-component" +args = ["build", "-p", "golem-embed-hugging-face"] + +[tasks.build-hugging-face-portable] +install_crate = { crate_name = "cargo-component", version = "0.20.0" } +command = "cargo-component" +args = ["build", "-p", "golem-embed-hugging-face", "--no-default-features"] + +[tasks.build-voyageai] +install_crate = { crate_name = "cargo-component", version = "0.20.0" } +command = "cargo-component" +args = ["build", "-p", "golem-embed-voyageai"] + +[tasks.build-voyageai-portable] +install_crate = { crate_name = "cargo-component", version = "0.20.0" } +command = "cargo-component" +args = ["build", "-p", "golem-embed-voyageai", "--no-default-features"] + +[tasks.release-build-openai] +install_crate = { crate_name = "cargo-component", version = "0.20.0" } +command = "cargo-component" +args = ["build", "-p", "golem-embed-openai", "--release"] + +[tasks.release-build-openai-portable] +install_crate = { crate_name = "cargo-component", version = "0.20.0" } +command = "cargo-component" +args = ["build", "-p", "golem-embed-openai", "--release", "--no-default-features"] + +[tasks.release-build-cohere] +install_crate = { crate_name = "cargo-component", version = "0.20.0" } +command = "cargo-component" +args = ["build", "-p", "golem-embed-cohere", "--release"] + +[tasks.release-build-cohere-portable] +install_crate = { crate_name = "cargo-component", version = "0.20.0" } +command = "cargo-component" +args = ["build", "-p", "golem-embed-cohere", "--release", "--no-default-features"] + +[tasks.release-build-hugging-face] +install_crate = { crate_name = "cargo-component", version = "0.20.0" } +command = "cargo-component" +args = ["build", "-p", "golem-embed-hugging-face", "--release"] + +[tasks.release-build-hugging-face-portable] +install_crate = { crate_name = "cargo-component", version = "0.20.0" } +command = "cargo-component" +args = ["build", "-p", "golem-embed-hugging-face", "--release", "--no-default-features"] + +[tasks.release-build-voyageai] +install_crate = { crate_name = "cargo-component", version = "0.20.0" } +command = "cargo-component" +args = ["build", "-p", "golem-embed-voyageai", "--release"] + +[tasks.release-build-voyageai-portable] +install_crate = { crate_name = "cargo-component", version = "0.20.0" } +command = "cargo-component" +args = ["build", "-p", "golem-embed-voyageai", "--release", "--no-default-features"] + +[tasks.wit-update] +install_crate = { crate_name = "wit-deps-cli" } +command = "wit-deps" +args = ["update"] + +[tasks.wit] +dependencies = ["wit-update"] + +script_runner = "@duckscript" +script = """ +modules = array embed openai cohere hugging-face voyageai + +for module in ${modules} + rm -r ${module}/wit/deps + mkdir ${module}/wit/deps/golem-embed + cp wit/golem-embed.wit ${module}/wit/deps/golem-embed/golem-embed.wit + cp wit/deps/wasi:io ${module}/wit/deps + + echo "Copied WIT for module embed::${module}" +end + +# Copy WIT files for integration tests +rm -r test/wit +mkdir test/wit/deps/golem-embed +mkdir test/wit/deps/io +cp wit/golem-embed.wit test/wit/deps/golem-embed/golem-embed.wit +cp wit/deps/wasi:io/error.wit test/wit/deps/io/error.wit +cp wit/deps/wasi:io/poll.wit test/wit/deps/io/poll.wit +cp wit/deps/wasi:io/streams.wit test/wit/deps/io/streams.wit +cp wit/deps/wasi:io/world.wit test/wit/deps/io/world.wit + +echo "Copied WIT for module test" +""" + +[tasks.build-test-components] +dependencies = ["build"] +install_crate = "cargo-binstall" +description = "Builds embed test components with golem-cli" +script = ''' +cargo-binstall golem-cli@1.2.2-dev.11 --locked --no-confirm +cargo-binstall wac-cli --locked --no-confirm +cd test + +golem-cli --version +golem-cli app clean +golem-cli app build -b openai-debug +golem-cli app clean +golem-cli app build -b cohere-debug +golem-cli app clean +golem-cli app build -b hugging-face-debug +golem-cli app clean +golem-cli app build -b voyageai-debug +''' \ No newline at end of file diff --git a/embed/README.md b/embed/README.md new file mode 100644 index 000000000..54754308a --- /dev/null +++ b/embed/README.md @@ -0,0 +1,187 @@ +# golem-embed + +WebAssembly Components providing a unified API for various AI embedding and reranking providers. + +## Versions + +There are 8 published WASM files for each release: + +| Name | Description | +|-------------------------------------------|--------------------------------------------------------------------------------------------| +| `golem-embed-openai.wasm` | Embedding implementation for OpenAI, using custom Golem specific durability features | +| `golem-embed-cohere.wasm` | Embedding implementation for Cohere, using custom Golem specific durability features | +| `golem-embed-hugging-face.wasm` | Embedding implementation for Hugging Face, using custom Golem specific durability features| +| `golem-embed-voyageai.wasm` | Embedding implementation for VoyageAI, using custom Golem specific durability features | +| `golem-embed-openai-portable.wasm` | Embedding implementation for OpenAI, with no Golem specific dependencies. | +| `golem-embed-cohere-portable.wasm` | Embedding implementation for Cohere, with no Golem specific dependencies. | +| `golem-embed-hugging-face-portable.wasm` | Embedding implementation for Hugging Face, with no Golem specific dependencies. | +| `golem-embed-voyageai-portable.wasm` | Embedding implementation for VoyageAI, with no Golem specific dependencies. | + +Every component **exports** the same `golem:embed` interface, [defined here](wit/golem-embed.wit). + +The `-portable` versions only depend on `wasi:io`, `wasi:http` and `wasi:logging`. + +The default versions also depend on [Golem's host API](https://learn.golem.cloud/golem-host-functions) to implement +advanced durability related features. + +## Provider Capabilities + +Each provider supports different functionality and input types: + +| Provider |Text Embedding | Image Embedding | Reranking | +|---------------|-----------|------|-------| +| OpenAI | ✅ | ❌ | ❌ | +| Cohere | ✅ | ✅ | ✅ | +| Hugging Face | ✅ | ❌ | ❌ | +| VoyageAI | ✅ | ❌ | ✅ | + + +## Usage + +Each provider has to be configured with an API key passed as an environment variable: + +| Provider | Environment Variable | +|---------------|--------------------------| +| OpenAI | `OPENAI_API_KEY` | +| Cohere | `COHERE_API_KEY` | +| Hugging Face | `HUGGING_FACE_API_KEY` | +| VoyageAI | `VOYAGEAI_API_KEY` | + +Additionally, setting the `GOLEM_EMBED_LOG=trace` environment variable enables trace logging for all the communication +with the underlying embedding provider. + +### Using with Golem + +#### Using a template + +The easiest way to get started is to use one of the predefined **templates** Golem provides. + +**NOT AVAILABLE YET** + +#### Using a component dependency + +To existing Golem applications the `golem-embed` WASM components can be added as a **binary dependency**. + +**NOT AVAILABLE YET** + +#### Integrating the composing step to the build + +Currently it is necessary to manually add the [`wac`](https://github.com/bytecodealliance/wac) tool call to the +application manifest to link with the selected embedding implementation. The `test` directory of this repository shows an +example of this. + +The summary of the steps to be done, assuming the component was created with `golem-cli component new rust my:example`: + +1. Copy the `profiles` section from `common-rust/golem.yaml` to the component's `golem.yaml` file (for example in + `components-rust/my-example/golem.yaml`) so it can be customized. +2. Add a second **build step** after the `cargo component build` which is calling `wac` to compose with the selected ( + and downloaded) `golem-embed` binary. See the example below. +3. Modify the `componentWasm` field to point to the composed WASM file. +4. Add the `golem-embed.wit` file (from this repository) to the application's root `wit/deps/golem:embed` directory. +5. Import `golem-embed.wit` in your component's WIT file: `import golem:embed/embed@1.0.0;' + +Example app manifest build section: + +```yaml +components: + my:example: + profiles: + debug: + build: + - command: cargo component build + sources: + - src + - wit-generated + - ../../common-rust + targets: + - ../../target/wasm32-wasip1/debug/my_example.wasm + - command: wac plug --plug ../../golem_embed_openai.wasm ../../target/wasm32-wasip1/debug/my_example.wasm -o ../../target/wasm32-wasip1/debug/my_example_plugged.wasm + sources: + - ../../target/wasm32-wasip1/debug/my_example.wasm + - ../../golem_embed_openai.wasm + targets: + - ../../target/wasm32-wasip1/debug/my_example_plugged.wasm + sourceWit: wit + generatedWit: wit-generated + componentWasm: ../../target/wasm32-wasip1/debug/my_example_plugged.wasm + linkedWasm: ../../golem-temp/components/my_example_debug.wasm + clean: + - src/bindings.rs +``` + +### Using without Golem + +To use the embedding provider components in a WebAssembly project independent of Golem you need to do the following: + +1. Download one of the `-portable.wasm` versions +2. Download the `golem-embed.wit` WIT package and import it +3. Use [`wac`](https://github.com/bytecodealliance/wac) to compose your component with the selected embedding implementation. + +## Examples + +Take the [test application](test/components-rust/test-embed/src/lib.rs) as an example of using `golem-embed` from Rust. The +implemented test functions are demonstrating the following: + +| Function Name | Description | +|---------------|--------------------------------------------------------------------------------------------| +| `test1` | Simple text embedding generation | +| `test2` | Demonstrates document reranking functionality | + +### Running the examples + +To run the examples first you need a running Golem instance. This can be Golem Cloud or the single-executable `golem` +binary +started with `golem server run`. + +**NOTE**: `golem-embed` requires the latest (unstable) version of Golem currently. It's going to work with the next public +stable release 1.2.2. + +Then build and deploy the _test application_. Select one of the following profiles to choose which provider to use: +| Profile Name | Description | +|--------------|-----------------------------------------------------------------------------------------------| +| `openai-debug` | Uses the OpenAI embedding implementation and compiles the code in debug profile | +| `openai-release` | Uses the OpenAI embedding implementation and compiles the code in release profile | +| `cohere-debug` | Uses the Cohere embedding implementation and compiles the code in debug profile | +| `cohere-release` | Uses the Cohere embedding implementation and compiles the code in release profile | +| `hugging-face-debug` | Uses the Hugging Face embedding implementation and compiles the code in debug profile | +| `hugging-face-release` | Uses the Hugging Face embedding implementation and compiles the code in release profile | +| `voyageai-debug` | Uses the VoyageAI embedding implementation and compiles the code in debug profile | +| `voyageai-release` | Uses the VoyageAI embedding implementation and compiles the code in release profile | + +```bash +cd test +golem app build -b openai-debug +golem app deploy -b openai-debug +``` + +Depending on the provider selected, an environment variable has to be set for the worker to be started, containing the API key for the given provider: + +```bash +golem worker new test:embed/debug --env OPENAI_API_KEY=xxx --env GOLEM_EMBED_LOG=trace +``` + +Then you can invoke the test functions on this worker: + +```bash +golem worker invoke test:embed/debug test1 --stream +``` + +## Development + +This repository uses [cargo-make](https://github.com/sagiegurari/cargo-make) to automate build tasks. +Some of the important tasks are: + +| Command | Description | +|-------------------------------------|--------------------------------------------------------------------------------------------------------| +| `cargo make build` | Build all components with Golem bindings in Debug | +| `cargo make release-build` | Build all components with Golem bindings in Release | +| `cargo make build-portable` | Build all components with no Golem bindings in Debug | +| `cargo make release-build-portable` | Build all components with no Golem bindings in Release | +| `cargo make unit-tests` | Run all unit tests | +| `cargo make check` | Checks formatting and Clippy rules | +| `cargo make fix` | Fixes formatting and Clippy rules | +| `cargo make wit` | To be used after editing the `wit/golem-embed.wit` file - distributes the changes to all wit directories | + +The `test` directory contains a **Golem application** for testing various features of the embedding components. +Check [the Golem documentation](https://learn.golem.cloud/quickstart) to learn how to install Golem and `golem-cli` to +run these tests. \ No newline at end of file diff --git a/embed/cohere/Cargo.toml b/embed/cohere/Cargo.toml new file mode 100644 index 000000000..8524838a7 --- /dev/null +++ b/embed/cohere/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "golem-embed-cohere" +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 Cohere embeding and reranking APIs, with special support for Golem Cloud" + + +[lib] +path = "src/lib.rs" +crate-type = ["cdylib"] + +[features] +default = ["durability"] +durability = ["golem-rust/durability", "golem-embed/durability"] + + +[dependencies] +golem-embed= {path="../embed", version="0.0.0", default-features= false} +golem-rust = { workspace = true } +log = { workspace = true } +reqwest = { workspace = true } +serde = {workspace = true} +base64 = { workspace = true } +serde_json = { workspace = true } +wit-bindgen-rt = { workspace = true } + + +[package.metadata.component] +package="golem:embed-cohere" + +[package.metadata.component.bindings] +generate_unused_types=true + +[package.metadata.component.bindings.with] +"golem:embed/embed@1.0.0"= "golem_embed::golem::embed::embed" + +[package.metadata.component.target] +path = "wit" + +[package.metadata.component.target.dependencies] +"golem:embed" = { path = "wit/deps/golem-embed" } +"wasi:io" = { path = "wit/deps/wasi:io"} diff --git a/embed/cohere/src/bindings.rs b/embed/cohere/src/bindings.rs new file mode 100644 index 000000000..3f6b7d647 --- /dev/null +++ b/embed/cohere/src/bindings.rs @@ -0,0 +1,43 @@ +// Generated by `wit-bindgen` 0.41.0. DO NOT EDIT! +// Options used: +// * runtime_path: "wit_bindgen_rt" +// * with "golem:embed/embed@1.0.0" = "golem_embed::golem::embed::embed" +// * generate_unused_types +use golem_embed::golem::embed::embed as __with_name0; +#[cfg(target_arch = "wasm32")] +#[unsafe( + link_section = "component-type:wit-bindgen:0.41.0:golem:embed-cohere@1.0.0:embed-library:encoded world" +)] +#[doc(hidden)] +#[allow(clippy::octal_escapes)] +pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 1263] = *b"\ +\0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07\xeb\x08\x01A\x02\x01\ +A\x02\x01B/\x01m\x08\x0fretrieval-query\x12retrieval-document\x13semantic-simila\ +rity\x0eclassification\x0aclustering\x12question-answering\x11fact-verification\x0e\ +code-retrieval\x04\0\x09task-type\x03\0\0\x01m\x03\x0bfloat-array\x06binary\x06b\ +ase64\x04\0\x0doutput-format\x03\0\x02\x01m\x05\x0bfloat-array\x04int8\x05uint8\x06\ +binary\x07ubinary\x04\0\x0coutput-dtype\x03\0\x04\x01m\x08\x0finvalid-request\x0f\ +model-not-found\x0bunsupported\x15authentication-failed\x0eprovider-error\x13rat\ +e-limit-exceeded\x0einternal-error\x07unknown\x04\0\x0aerror-code\x03\0\x06\x01r\ +\x01\x03urls\x04\0\x09image-url\x03\0\x08\x01q\x02\x04text\x01s\0\x05image\x01\x09\ +\0\x04\0\x0ccontent-part\x03\0\x0a\x01r\x02\x03keys\x05values\x04\0\x02kv\x03\0\x0c\ +\x01ks\x01k\x01\x01ky\x01k\x7f\x01k\x03\x01k\x05\x01p\x0d\x01r\x08\x05model\x0e\x09\ +task-type\x0f\x0adimensions\x10\x0atruncation\x11\x0doutput-format\x12\x0coutput\ +-dtype\x13\x04user\x0e\x10provider-options\x14\x04\0\x06config\x03\0\x15\x01r\x02\ +\x0cinput-tokens\x10\x0ctotal-tokens\x10\x04\0\x05usage\x03\0\x17\x01pv\x01r\x02\ +\x05indexy\x06vector\x19\x04\0\x09embedding\x03\0\x1a\x01p\x1b\x01k\x18\x01r\x04\ +\x0aembeddings\x1c\x05usage\x1d\x05models\x16provider-metadata-json\x0e\x04\0\x12\ +embedding-response\x03\0\x1e\x01r\x03\x05indexy\x0frelevance-scorev\x08document\x0e\ +\x04\0\x0drerank-result\x03\0\x20\x01p!\x01r\x04\x07results\"\x05usage\x1d\x05mo\ +dels\x16provider-metadata-json\x0e\x04\0\x0frerank-response\x03\0#\x01r\x03\x04c\ +ode\x07\x07messages\x13provider-error-json\x0e\x04\0\x05error\x03\0%\x01p\x0b\x01\ +j\x01\x1f\x01&\x01@\x02\x06inputs'\x06config\x16\0(\x04\0\x08generate\x01)\x01ps\ +\x01j\x01$\x01&\x01@\x03\x05querys\x09documents*\x06config\x16\0+\x04\0\x06reran\ +k\x01,\x04\0\x17golem:embed/embed@1.0.0\x05\0\x04\0&golem:embed-cohere/embed-lib\ +rary@1.0.0\x04\0\x0b\x13\x01\0\x0dembed-library\x03\0\0\0G\x09producers\x01\x0cp\ +rocessed-by\x02\x0dwit-component\x070.227.1\x10wit-bindgen-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/embed/cohere/src/client.rs b/embed/cohere/src/client.rs new file mode 100644 index 000000000..f22daccc1 --- /dev/null +++ b/embed/cohere/src/client.rs @@ -0,0 +1,261 @@ +use std::fmt::Debug; + +use golem_embed::{ + error::{error_code_from_status, from_reqwest_error}, + golem::embed::embed::Error, +}; +use log::trace; +use reqwest::{Client, Method, Response}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +const BASE_URL: &str = "https://api.cohere.ai"; + +/// The Cohere API client for creating embeddings. +/// +/// Based on https://docs.cohere.com/reference/embed +pub struct EmbeddingsApi { + cohere_api_key: String, + client: Client, +} + +impl EmbeddingsApi { + pub fn new(cohere_api_key: String) -> Self { + let client = Client::builder() + .build() + .expect("Failed to initialize HTTP client"); + Self { + cohere_api_key, + client, + } + } + + pub fn generate_embeding(&self, request: EmbeddingRequest) -> Result { + trace!("Sending request to Cohere API: {request:?}"); + let response = self + .client + .request(Method::POST, format!("{BASE_URL}/v2/embed")) + .bearer_auth(&self.cohere_api_key) + .json(&request) + .send() + .map_err(|err| from_reqwest_error("Request failed", err))?; + trace!("Recived response: {response:#?}"); + parse_response::(response) + } + + pub fn rerank(&self, request: RerankRequest) -> Result { + trace!("Sending request to Cohere API: {request:?}"); + let response = self + .client + .request(Method::POST, format!("{BASE_URL}/v2/rerank")) + .bearer_auth(&self.cohere_api_key) + .json(&request) + .send() + .map_err(|err| from_reqwest_error("Request failed", err))?; + trace!("Recived response: {response:#?}"); + parse_response::(response) + } +} + +fn parse_response(response: Response) -> Result { + let status = response.status(); + let response_text = response + .text() + .map_err(|err| from_reqwest_error("Failed to read response body", err))?; + match serde_json::from_str::(&response_text) { + Ok(response_data) => { + trace!("Response from Hugging Face API: {response_data:?}"); + Ok(response_data) + } + Err(error) => { + trace!("Error parsing response: {error:?}"); + Err(Error { + code: error_code_from_status(status), + message: format!("Failed to decode response body: {response_text}"), + provider_error_json: Some(error.to_string()), + }) + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum InputType { + #[serde(rename = "search_document")] + SearchDocument, + #[serde(rename = "search_query")] + SearchQuery, + #[serde(rename = "classification")] + Classification, + #[serde(rename = "clustering")] + Clustering, + #[serde(rename = "image")] + Image, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum EmbeddingType { + #[serde(rename = "float")] + Float, + #[serde(rename = "int8")] + Int8, + #[serde(rename = "uint8")] + Uint8, + #[serde(rename = "binary")] + Binary, + #[serde(rename = "ubinary")] + Ubinary, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Truncate { + #[serde(rename = "NONE")] + None, + #[serde(rename = "START")] + Start, + #[serde(rename = "END")] + End, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct EmbeddingRequest { + pub model: String, + pub input_type: InputType, + + #[serde(skip_serializing_if = "Option::is_none")] + pub embedding_types: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub texts: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub images: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub max_tokens: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub output_dimension: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub truncate: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct EmbeddingResponse { + pub id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub images: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub texts: Option>, + + pub embeddings: EmbeddingData, + + #[serde(skip_serializing_if = "Option::is_none")] + pub meta: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub response_type: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ImageRespnse { + #[serde(skip_serializing_if = "Option::is_none")] + pub width: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub height: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub format: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub bit_depth: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct EmbeddingData { + #[serde(skip_serializing_if = "Option::is_none")] + pub float: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub int8: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub uint8: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub binary: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub ubinary: Option>>, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct CohereError { + pub message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub code: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct RerankRequest { + pub model: String, + pub query: String, + pub documents: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub top_n: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub max_tokens_per_doc: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct RerankResponse { + pub results: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub meta: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct RerankData { + pub index: u32, + pub relevance_score: f32, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Meta { + #[serde(skip_serializing_if = "Option::is_none")] + pub api_version: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub billed_units: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub tokens: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub warning: Option>, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct MetaTokens { + #[serde(skip_serializing_if = "Option::is_none")] + pub input_tokens: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub output_tokens: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ApiVersion { + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub is_deprecated: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub is_experimental: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct BilledUnits { + #[serde(skip_serializing_if = "Option::is_none")] + pub images: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub input_tokens: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub output_tokens: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub search_units: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub classifications: Option, +} diff --git a/embed/cohere/src/conversions.rs b/embed/cohere/src/conversions.rs new file mode 100644 index 000000000..d60f24606 --- /dev/null +++ b/embed/cohere/src/conversions.rs @@ -0,0 +1,460 @@ +use std::{fs, path::Path}; + +use base64::{engine::general_purpose, Engine}; +use golem_embed::{ + error::unsupported, + golem::embed::embed::{ + Config, ContentPart, Embedding, EmbeddingResponse as GolemEmbeddingResponse, Error, + OutputDtype, RerankResponse as GolemRerankResponse, RerankResult, TaskType, Usage, + }, +}; +use log::trace; +use reqwest::{Client, Url}; + +use crate::client::{ + EmbeddingRequest, EmbeddingResponse, EmbeddingType, InputType, RerankRequest, RerankResponse, +}; + +fn output_dtype_to_cohere_embedding_type(dtype: OutputDtype) -> EmbeddingType { + match dtype { + OutputDtype::FloatArray => EmbeddingType::Float, + OutputDtype::Int8 => EmbeddingType::Int8, + OutputDtype::Uint8 => EmbeddingType::Uint8, + OutputDtype::Binary => EmbeddingType::Binary, + OutputDtype::Ubinary => EmbeddingType::Ubinary, + } +} + +pub fn create_embed_request( + inputs: Vec, + config: Config, +) -> Result { + let mut text_inputs = Vec::new(); + let mut image_inputs = Vec::new(); + for input in inputs { + match input { + ContentPart::Text(text) => text_inputs.push(text), + ContentPart::Image(image) => match image_to_base64(&image.url) { + Ok(base64_data) => image_inputs.push(base64_data), + Err(err) => { + trace!("Failed to encode image: {}\nError: {}\n", image.url, err); + } + }, + } + } + + if !text_inputs.is_empty() && !image_inputs.is_empty() + || text_inputs.is_empty() && image_inputs.is_empty() + { + return Err(unsupported( + "Cohere requires text or image input, not both.", + )); + } + + let input_type = if !image_inputs.is_empty() && text_inputs.is_empty() { + InputType::Image + } else { + config + .task_type + .map(|task_type| match task_type { + TaskType::RetrievalQuery => InputType::SearchQuery, + TaskType::RetrievalDocument => InputType::SearchDocument, + TaskType::Classification => InputType::Classification, + TaskType::Clustering => InputType::Clustering, + _ => InputType::SearchDocument, + }) + .unwrap() + }; + + let model = config + .model + .unwrap_or_else(|| "embed-english-v3.0".to_string()); + + let embedding_types = config + .output_dtype + .map(|dtype| vec![output_dtype_to_cohere_embedding_type(dtype)]); + + Ok(EmbeddingRequest { + model, + input_type, + embedding_types, + images: if !image_inputs.is_empty() { + Some(image_inputs) + } else { + None + }, + texts: if !text_inputs.is_empty() { + Some(text_inputs) + } else { + None + }, + truncate: None, + max_tokens: None, + output_dimension: Some(config.dimensions.unwrap()), + }) +} + +pub fn create_rerank_request( + query: String, + documents: Vec, + config: Config, +) -> Result { + let model = config.model.unwrap_or_else(|| "rerank-2-lite".to_string()); + Ok(RerankRequest { + model, + query, + documents, + top_n: None, + max_tokens_per_doc: None, + }) +} + +pub fn image_to_base64(source: &str) -> Result> { + let bytes = if Url::parse(source).is_ok() { + let client = Client::new(); + let response = client.get(source).send()?; + + response.bytes()?.to_vec() + } else { + let path = Path::new(source); + + fs::read(path)? + }; + + let base64_data = general_purpose::STANDARD.encode(&bytes); + Ok(base64_data) +} + +pub fn process_embedding_response( + response: EmbeddingResponse, + config: Config, +) -> Result { + let mut embeddings: Vec = Vec::new(); + if let Some(emdeddings_array) = &response.embeddings.int8 { + for int_embedding in emdeddings_array { + let float_embedding = int_embedding.iter().map(|&v| v as f32).collect(); + embeddings.push(Embedding { + index: 0, + vector: float_embedding, + }); + } + }; + + if let Some(emdeddings_array) = &response.embeddings.uint8 { + for uint_embedding in emdeddings_array { + let float_embedding = uint_embedding.iter().map(|&v| v as f32).collect(); + embeddings.push(Embedding { + index: 0, + vector: float_embedding, + }); + } + }; + if let Some(emdeddings_array) = &response.embeddings.binary { + for binary_embedding in emdeddings_array { + let float_embedding = binary_embedding.iter().map(|&v| v as f32).collect(); + embeddings.push(Embedding { + index: 0, + vector: float_embedding, + }); + } + }; + if let Some(emdeddings_array) = &response.embeddings.ubinary { + for ubinary_embedding in emdeddings_array { + let float_embedding = ubinary_embedding.iter().map(|&v| v as f32).collect(); + embeddings.push(Embedding { + index: 0, + vector: float_embedding, + }); + } + }; + if let Some(emdeddings_array) = &response.embeddings.float { + for float_embedding in emdeddings_array { + embeddings.push(Embedding { + index: 0, + vector: float_embedding.to_vec(), + }); + } + }; + + Ok(GolemEmbeddingResponse { + embeddings, + provider_metadata_json: Some(get_embed_provider_metadata(response.clone())), + model: config + .model + .unwrap_or_else(|| "embed-english-v3.0".to_string()), + usage: Some(Usage { + input_tokens: response.meta.unwrap().billed_units.unwrap().input_tokens, + total_tokens: None, + }), + }) +} + +pub fn get_embed_provider_metadata(response: EmbeddingResponse) -> String { + let meta = serde_json::to_string(&response.meta.unwrap()).unwrap_or_default(); + format!(r#"{{"id":"{}","meta":"{}",}}"#, response.id, meta) +} + +pub fn process_rerank_response( + response: RerankResponse, + config: Config, +) -> Result { + let results = response + .clone() + .results + .iter() + .map(|result| RerankResult { + index: result.index, + relevance_score: result.relevance_score, + document: None, + }) + .collect(); + + let usage = response.clone().meta.and_then(|meta| { + meta.billed_units.map(|billed_units| Usage { + input_tokens: billed_units.input_tokens, + total_tokens: billed_units.output_tokens, + }) + }); + + Ok(GolemRerankResponse { + results, + usage, + model: config.model.unwrap_or_else(|| "rerank-2-lite".to_string()), + provider_metadata_json: Some(get_rerank_provider_metadata(response)), + }) +} + +fn get_rerank_provider_metadata(response: RerankResponse) -> String { + let meta = serde_json::to_string(&response.meta.unwrap()).unwrap_or_default(); + format!( + r#"{{"id":"{}","meta":"{}",}}"#, + response.id.unwrap_or_default(), + meta + ) +} + +#[cfg(test)] +mod tests { + use crate::client::{ApiVersion, BilledUnits, EmbeddingData, Meta, RerankData}; + + use super::*; + + #[test] + fn test_create_embed_request() { + let inputs = vec![ContentPart::Text("Hello, world!".to_string())]; + let config = Config { + model: Some("embed-english-v3.0".to_string()), + task_type: Some(TaskType::RetrievalQuery), + dimensions: Some(1024), + truncation: Some(true), + output_format: None, + output_dtype: None, + user: None, + provider_options: vec![], + }; + let request = create_embed_request(inputs, config); + let request = request.unwrap(); + assert_eq!(request.model, "embed-english-v3.0"); + // assert_eq!(request.input_type, InputType::SearchQuery); + // assert_eq!(request.embedding_types, Some(vec![EmbeddingType::Float])); + assert_eq!(request.images, None); + assert_eq!(request.texts, Some(vec!["Hello, world!".to_string()])); + // assert_eq!(request.truncate, None); + assert_eq!(request.max_tokens, None); + assert_eq!(request.output_dimension, Some(1024)); + } + + #[test] + fn test_embedding_response_conversion() { + let data = EmbeddingResponse { + id: "54910170-852f-4322-9767-63d36e55c3bf".to_owned(), + images: None, + texts: Some(vec![ + "This is the sentence I want to embed.".to_owned(), + "Hey !".to_owned(), + ]), + embeddings: EmbeddingData { + float: Some(vec![ + vec![ + 0.016967773, + 0.031982422, + 0.041503906, + 0.0021514893, + 0.008178711, + -0.029541016, + -0.018432617, + -0.046875, + 0.021240234, + ], + vec![ + 0.013977051, + 0.012084961, + 0.005554199, + -0.053955078, + -0.026977539, + -0.008361816, + 0.02368164, + -0.013183594, + -0.063964844, + 0.026611328, + ], + ]), + int8: Some(vec![ + vec![ + -15, -65, 0, -31, -43, -14, -48, 59, -34, 15, 36, 49, -5, 3, -49, -34, -74, + 21, + ], + vec![ + 14, 38, -30, -13, -49, 4, -33, -49, 48, 9, -84, 8, 0, -84, -46, -20, 24, + -26, -98, 28, + ], + ]), + uint8: None, + binary: Some(vec![vec![-54, 99, -87, 60, 15, 10, 93, 97, -42, -51, 9]]), + ubinary: None, + }, + meta: Some(Meta { + api_version: Some(ApiVersion { + version: Some("2".to_owned()), + is_experimental: None, + is_deprecated: None, + }), + billed_units: Some(BilledUnits { + input_tokens: Some(11), + classifications: None, + images: None, + output_tokens: None, + search_units: None, + }), + tokens: None, + warning: None, + }), + response_type: Some("embeddings_by_type".to_owned()), + }; + + let config = Config { + model: Some("embed-english-v3.0".to_string()), + task_type: None, + dimensions: None, + truncation: None, + output_format: None, + output_dtype: None, + user: None, + provider_options: vec![], + }; + + let result = process_embedding_response(data.clone(), config); + print!("{result:?}"); + let embedding_response = result.unwrap(); + assert_eq!(embedding_response.embeddings.len(), 5); + assert_eq!(embedding_response.embeddings[0].index, 0); + assert_eq!( + embedding_response.embeddings[0].vector, + vec![ + -15.0, -65.0, 0.0, -31.0, -43.0, -14.0, -48.0, 59.0, -34.0, 15.0, 36.0, 49.0, -5.0, + 3.0, -49.0, -34.0, -74.0, 21.0 + ] + ); + assert_eq!(embedding_response.embeddings[1].index, 0); + assert_eq!( + embedding_response.embeddings[1].vector, + vec![ + 14.0, 38.0, -30.0, -13.0, -49.0, 4.0, -33.0, -49.0, 48.0, 9.0, -84.0, 8.0, 0.0, + -84.0, -46.0, -20.0, 24.0, -26.0, -98.0, 28.0 + ] + ); + assert_eq!( + embedding_response.provider_metadata_json, + Some(get_embed_provider_metadata(data)) + ); + assert_eq!(embedding_response.model, "embed-english-v3.0"); + assert_eq!( + embedding_response.usage, + Some(Usage { + input_tokens: Some(11), + total_tokens: None, + }) + ); + } + + #[test] + fn test_create_rerank_request() { + let query = "What is AI?".to_string(); + let documents = vec![ + "AI is artificial intelligence".to_string(), + "Machine learning is a subset of AI".to_string(), + ]; + let config = Config { + model: Some("rerank-2-lite".to_string()), + task_type: None, + dimensions: None, + truncation: None, + output_format: None, + output_dtype: None, + user: None, + provider_options: vec![], + }; + let request = create_rerank_request(query, documents.clone(), config); + let request = request.unwrap(); + assert_eq!(request.model, "rerank-2-lite"); + assert_eq!(request.query, "What is AI?"); + assert_eq!(request.documents, documents); + assert_eq!(request.top_n, None); + assert_eq!(request.max_tokens_per_doc, None); + } + + #[test] + fn test_rerank_response_conversion() { + let data = RerankResponse { + id: Some("54910170-852f-4322-9767-63d36e55c3bf".to_owned()), + results: vec![RerankData { + index: 0, + relevance_score: 0.9, + }], + meta: Some(Meta { + api_version: Some(ApiVersion { + version: Some("2".to_owned()), + is_experimental: None, + is_deprecated: None, + }), + billed_units: Some(BilledUnits { + input_tokens: Some(11), + classifications: None, + images: None, + output_tokens: Some(111), + search_units: None, + }), + tokens: None, + warning: None, + }), + }; + + let config = Config { + model: Some("rerank-2-lite".to_string()), + task_type: None, + dimensions: None, + truncation: None, + output_format: None, + output_dtype: None, + user: None, + provider_options: vec![], + }; + let result = process_rerank_response(data.clone(), config); + let rerank_response = result.unwrap(); + assert_eq!(rerank_response.results.len(), 1); + assert_eq!(rerank_response.results[0].index, 0); + assert_eq!(rerank_response.results[0].relevance_score, 0.9); + assert_eq!(rerank_response.model, "rerank-2-lite"); + assert_eq!( + rerank_response.provider_metadata_json, + Some(get_rerank_provider_metadata(data)) + ); + assert_eq!( + rerank_response.usage, + Some(Usage { + input_tokens: Some(11), + total_tokens: Some(111), + }) + ); + } +} diff --git a/embed/cohere/src/lib.rs b/embed/cohere/src/lib.rs new file mode 100644 index 000000000..76b42ae6d --- /dev/null +++ b/embed/cohere/src/lib.rs @@ -0,0 +1,83 @@ +use client::EmbeddingsApi; +use conversions::create_embed_request; +use golem_embed::{ + config::with_config_key, + durability::{DurableEmbed, ExtendedGuest}, + golem::embed::embed::{ + Config, ContentPart, EmbeddingResponse as GolemEmbeddingResponse, Error, Guest, + RerankResponse, + }, + LOGGING_STATE, +}; + +use crate::conversions::{ + create_rerank_request, process_embedding_response, process_rerank_response, +}; + +mod client; +mod conversions; + +struct CohereComponent; + +impl CohereComponent { + const ENV_VAR_NAME: &'static str = "COHERE_API_KEY"; + + fn embeddings( + client: EmbeddingsApi, + inputs: Vec, + config: Config, + ) -> Result { + let request = create_embed_request(inputs, config.clone()); + match request { + Ok(request) => match client.generate_embeding(request) { + Ok(response) => process_embedding_response(response, config), + Err(err) => Err(err), + }, + Err(err) => Err(err), + } + } + + fn rerank( + client: EmbeddingsApi, + query: String, + documents: Vec, + config: Config, + ) -> Result { + let request = create_rerank_request(query, documents, config.clone()); + match request { + Ok(request) => match client.rerank(request) { + Ok(response) => process_rerank_response(response, config), + Err(err) => Err(err), + }, + Err(err) => Err(err), + } + } +} + +impl Guest for CohereComponent { + fn generate(inputs: Vec, config: Config) -> Result { + LOGGING_STATE.with_borrow_mut(|state| state.init()); + with_config_key(Self::ENV_VAR_NAME, Err, |cohere_api_key| { + let client = EmbeddingsApi::new(cohere_api_key); + Self::embeddings(client, inputs, config) + }) + } + + fn rerank( + query: String, + documents: Vec, + config: Config, + ) -> Result { + LOGGING_STATE.with_borrow_mut(|state| state.init()); + with_config_key(Self::ENV_VAR_NAME, Err, |cohere_api_key| { + let client = EmbeddingsApi::new(cohere_api_key); + Self::rerank(client, query, documents, config) + }) + } +} + +impl ExtendedGuest for CohereComponent {} + +type DurableCohereComponent = DurableEmbed; + +golem_embed::export_embed!(DurableCohereComponent with_types_in golem_embed); diff --git a/embed/cohere/wit/cohere.wit b/embed/cohere/wit/cohere.wit new file mode 100644 index 000000000..7df362448 --- /dev/null +++ b/embed/cohere/wit/cohere.wit @@ -0,0 +1,5 @@ +package golem:embed-cohere@1.0.0; + +world embed-library { + include golem:embed/embed-library@1.0.0; +} diff --git a/embed/cohere/wit/deps/golem-embed/golem-embed.wit b/embed/cohere/wit/deps/golem-embed/golem-embed.wit new file mode 100644 index 000000000..78d040320 --- /dev/null +++ b/embed/cohere/wit/deps/golem-embed/golem-embed.wit @@ -0,0 +1,129 @@ +package golem:embed@1.0.0; + +interface embed { + // --- Enums --- + + enum task-type { + retrieval-query, + retrieval-document, + semantic-similarity, + classification, + clustering, + question-answering, + fact-verification, + code-retrieval, + } + + enum output-format { + float-array, + binary, + base64, + } + + enum output-dtype { + float-array, + int8, + uint8, + binary, + ubinary, + } + + enum error-code { + invalid-request, + model-not-found, + unsupported, + authentication-failed, + provider-error, + rate-limit-exceeded, + internal-error, + unknown, + } + + // --- Content --- + + record image-url { + url: string, + } + + variant content-part { + text(string), + image(image-url), + } + + // --- Configuration --- + + record kv { + key: string, + value: string, + } + + record config { + model: option, + task-type: option, + dimensions: option, + truncation: option, + output-format: option, + output-dtype: option, + user: option, + provider-options: list, + } + + // --- Embedding Response --- + + record usage { + input-tokens: option, + total-tokens: option, + } + + record embedding { + index: u32, + vector: list, + } + + record embedding-response { + embeddings: list, + usage: option, + model: string, + provider-metadata-json: option, + } + + // --- Rerank Response --- + + record rerank-result { + index: u32, + relevance-score: f32, + document: option, + } + + record rerank-response { + results: list, + usage: option, + model: string, + provider-metadata-json: option, + } + + // --- Error Handling --- + + record error { + code: error-code, + message: string, + provider-error-json: option, + } + + // --- Core Functions --- + + generate: func( + inputs: list, + config: config + ) -> result; + + rerank: func( + query: string, + documents: list, + config: config + ) -> result; +} + +world embed-library { + export embed; +} \ No newline at end of file diff --git a/embed/cohere/wit/deps/wasi:io/error.wit b/embed/cohere/wit/deps/wasi:io/error.wit new file mode 100644 index 000000000..97c606877 --- /dev/null +++ b/embed/cohere/wit/deps/wasi:io/error.wit @@ -0,0 +1,34 @@ +package wasi:io@0.2.3; + +@since(version = 0.2.0) +interface error { + /// A resource which represents some error information. + /// + /// The only method provided by this resource is `to-debug-string`, + /// which provides some human-readable information about the error. + /// + /// In the `wasi:io` package, this resource is returned through the + /// `wasi:io/streams/stream-error` type. + /// + /// To provide more specific error information, other interfaces may + /// offer functions to "downcast" this error into more specific types. For example, + /// errors returned from streams derived from filesystem types can be described using + /// the filesystem's own error-code type. This is done using the function + /// `wasi:filesystem/types/filesystem-error-code`, which takes a `borrow` + /// parameter and returns an `option`. + /// + /// The set of functions which can "downcast" an `error` into a more + /// concrete type is open. + @since(version = 0.2.0) + resource error { + /// Returns a string that is suitable to assist humans in debugging + /// this error. + /// + /// WARNING: The returned string should not be consumed mechanically! + /// It may change across platforms, hosts, or other implementation + /// details. Parsing this string is a major platform-compatibility + /// hazard. + @since(version = 0.2.0) + to-debug-string: func() -> string; + } +} diff --git a/embed/cohere/wit/deps/wasi:io/poll.wit b/embed/cohere/wit/deps/wasi:io/poll.wit new file mode 100644 index 000000000..9bcbe8e03 --- /dev/null +++ b/embed/cohere/wit/deps/wasi:io/poll.wit @@ -0,0 +1,47 @@ +package wasi:io@0.2.3; + +/// A poll API intended to let users wait for I/O events on multiple handles +/// at once. +@since(version = 0.2.0) +interface poll { + /// `pollable` represents a single I/O event which may be ready, or not. + @since(version = 0.2.0) + resource pollable { + + /// Return the readiness of a pollable. This function never blocks. + /// + /// Returns `true` when the pollable is ready, and `false` otherwise. + @since(version = 0.2.0) + ready: func() -> bool; + + /// `block` returns immediately if the pollable is ready, and otherwise + /// blocks until ready. + /// + /// This function is equivalent to calling `poll.poll` on a list + /// containing only this pollable. + @since(version = 0.2.0) + block: func(); + } + + /// Poll for completion on a set of pollables. + /// + /// This function takes a list of pollables, which identify I/O sources of + /// interest, and waits until one or more of the events is ready for I/O. + /// + /// The result `list` contains one or more indices of handles in the + /// argument list that is ready for I/O. + /// + /// This function traps if either: + /// - the list is empty, or: + /// - the list contains more elements than can be indexed with a `u32` value. + /// + /// A timeout can be implemented by adding a pollable from the + /// wasi-clocks API to the list. + /// + /// This function does not return a `result`; polling in itself does not + /// do any I/O so it doesn't fail. If any of the I/O sources identified by + /// the pollables has an error, it is indicated by marking the source as + /// being ready for I/O. + @since(version = 0.2.0) + poll: func(in: list>) -> list; +} diff --git a/embed/cohere/wit/deps/wasi:io/streams.wit b/embed/cohere/wit/deps/wasi:io/streams.wit new file mode 100644 index 000000000..0de084629 --- /dev/null +++ b/embed/cohere/wit/deps/wasi:io/streams.wit @@ -0,0 +1,290 @@ +package wasi:io@0.2.3; + +/// WASI I/O is an I/O abstraction API which is currently focused on providing +/// stream types. +/// +/// In the future, the component model is expected to add built-in stream types; +/// when it does, they are expected to subsume this API. +@since(version = 0.2.0) +interface streams { + @since(version = 0.2.0) + use error.{error}; + @since(version = 0.2.0) + use poll.{pollable}; + + /// An error for input-stream and output-stream operations. + @since(version = 0.2.0) + variant stream-error { + /// The last operation (a write or flush) failed before completion. + /// + /// More information is available in the `error` payload. + /// + /// After this, the stream will be closed. All future operations return + /// `stream-error::closed`. + last-operation-failed(error), + /// The stream is closed: no more input will be accepted by the + /// stream. A closed output-stream will return this error on all + /// future operations. + closed + } + + /// An input bytestream. + /// + /// `input-stream`s are *non-blocking* to the extent practical on underlying + /// platforms. I/O operations always return promptly; if fewer bytes are + /// promptly available than requested, they return the number of bytes promptly + /// available, which could even be zero. To wait for data to be available, + /// use the `subscribe` function to obtain a `pollable` which can be polled + /// for using `wasi:io/poll`. + @since(version = 0.2.0) + resource input-stream { + /// Perform a non-blocking read from the stream. + /// + /// When the source of a `read` is binary data, the bytes from the source + /// are returned verbatim. When the source of a `read` is known to the + /// implementation to be text, bytes containing the UTF-8 encoding of the + /// text are returned. + /// + /// This function returns a list of bytes containing the read data, + /// when successful. The returned list will contain up to `len` bytes; + /// it may return fewer than requested, but not more. The list is + /// empty when no bytes are available for reading at this time. The + /// pollable given by `subscribe` will be ready when more bytes are + /// available. + /// + /// This function fails with a `stream-error` when the operation + /// encounters an error, giving `last-operation-failed`, or when the + /// stream is closed, giving `closed`. + /// + /// When the caller gives a `len` of 0, it represents a request to + /// read 0 bytes. If the stream is still open, this call should + /// succeed and return an empty list, or otherwise fail with `closed`. + /// + /// The `len` parameter is a `u64`, which could represent a list of u8 which + /// is not possible to allocate in wasm32, or not desirable to allocate as + /// as a return value by the callee. The callee may return a list of bytes + /// less than `len` in size while more bytes are available for reading. + @since(version = 0.2.0) + read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Read bytes from a stream, after blocking until at least one byte can + /// be read. Except for blocking, behavior is identical to `read`. + @since(version = 0.2.0) + blocking-read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Skip bytes from a stream. Returns number of bytes skipped. + /// + /// Behaves identical to `read`, except instead of returning a list + /// of bytes, returns the number of bytes consumed from the stream. + @since(version = 0.2.0) + skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Skip bytes from a stream, after blocking until at least one byte + /// can be skipped. Except for blocking behavior, identical to `skip`. + @since(version = 0.2.0) + blocking-skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Create a `pollable` which will resolve once either the specified stream + /// has bytes available to read or the other end of the stream has been + /// closed. + /// The created `pollable` is a child resource of the `input-stream`. + /// Implementations may trap if the `input-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } + + + /// An output bytestream. + /// + /// `output-stream`s are *non-blocking* to the extent practical on + /// underlying platforms. Except where specified otherwise, I/O operations also + /// always return promptly, after the number of bytes that can be written + /// promptly, which could even be zero. To wait for the stream to be ready to + /// accept data, the `subscribe` function to obtain a `pollable` which can be + /// polled for using `wasi:io/poll`. + /// + /// Dropping an `output-stream` while there's still an active write in + /// progress may result in the data being lost. Before dropping the stream, + /// be sure to fully flush your writes. + @since(version = 0.2.0) + resource output-stream { + /// Check readiness for writing. This function never blocks. + /// + /// Returns the number of bytes permitted for the next call to `write`, + /// or an error. Calling `write` with more bytes than this function has + /// permitted will trap. + /// + /// When this function returns 0 bytes, the `subscribe` pollable will + /// become ready when this function will report at least 1 byte, or an + /// error. + @since(version = 0.2.0) + check-write: func() -> result; + + /// Perform a write. This function never blocks. + /// + /// When the destination of a `write` is binary data, the bytes from + /// `contents` are written verbatim. When the destination of a `write` is + /// known to the implementation to be text, the bytes of `contents` are + /// transcoded from UTF-8 into the encoding of the destination and then + /// written. + /// + /// Precondition: check-write gave permit of Ok(n) and contents has a + /// length of less than or equal to n. Otherwise, this function will trap. + /// + /// returns Err(closed) without writing if the stream has closed since + /// the last call to check-write provided a permit. + @since(version = 0.2.0) + write: func( + contents: list + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 bytes, and then flush the stream. Block + /// until all of these operations are complete, or an error occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write`, and `flush`, and is implemented with the + /// following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while !contents.is_empty() { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, contents.len()); + /// let (chunk, rest) = contents.split_at(len); + /// this.write(chunk ); // eliding error handling + /// contents = rest; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + @since(version = 0.2.0) + blocking-write-and-flush: func( + contents: list + ) -> result<_, stream-error>; + + /// Request to flush buffered output. This function never blocks. + /// + /// This tells the output-stream that the caller intends any buffered + /// output to be flushed. the output which is expected to be flushed + /// is all that has been passed to `write` prior to this call. + /// + /// Upon calling this function, the `output-stream` will not accept any + /// writes (`check-write` will return `ok(0)`) until the flush has + /// completed. The `subscribe` pollable will become ready when the + /// flush has completed and the stream can accept more writes. + @since(version = 0.2.0) + flush: func() -> result<_, stream-error>; + + /// Request to flush buffered output, and block until flush completes + /// and stream is ready for writing again. + @since(version = 0.2.0) + blocking-flush: func() -> result<_, stream-error>; + + /// Create a `pollable` which will resolve once the output-stream + /// is ready for more writing, or an error has occurred. When this + /// pollable is ready, `check-write` will return `ok(n)` with n>0, or an + /// error. + /// + /// If the stream is closed, this pollable is always ready immediately. + /// + /// The created `pollable` is a child resource of the `output-stream`. + /// Implementations may trap if the `output-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) + subscribe: func() -> pollable; + + /// Write zeroes to a stream. + /// + /// This should be used precisely like `write` with the exact same + /// preconditions (must use check-write first), but instead of + /// passing a list of bytes, you simply pass the number of zero-bytes + /// that should be written. + @since(version = 0.2.0) + write-zeroes: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 zeroes, and then flush the stream. + /// Block until all of these operations are complete, or an error + /// occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write-zeroes`, and `flush`, and is implemented with + /// the following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while num_zeroes != 0 { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, num_zeroes); + /// this.write-zeroes(len); // eliding error handling + /// num_zeroes -= len; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + @since(version = 0.2.0) + blocking-write-zeroes-and-flush: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Read from one stream and write to another. + /// + /// The behavior of splice is equivalent to: + /// 1. calling `check-write` on the `output-stream` + /// 2. calling `read` on the `input-stream` with the smaller of the + /// `check-write` permitted length and the `len` provided to `splice` + /// 3. calling `write` on the `output-stream` with that read data. + /// + /// Any error reported by the call to `check-write`, `read`, or + /// `write` ends the splice and reports that error. + /// + /// This function returns the number of bytes transferred; it may be less + /// than `len`. + @since(version = 0.2.0) + splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + + /// Read from one stream and write to another, with blocking. + /// + /// This is similar to `splice`, except that it blocks until the + /// `output-stream` is ready for writing, and the `input-stream` + /// is ready for reading, before performing the `splice`. + @since(version = 0.2.0) + blocking-splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + } +} diff --git a/embed/cohere/wit/deps/wasi:io/world.wit b/embed/cohere/wit/deps/wasi:io/world.wit new file mode 100644 index 000000000..f1d2102dc --- /dev/null +++ b/embed/cohere/wit/deps/wasi:io/world.wit @@ -0,0 +1,10 @@ +package wasi:io@0.2.3; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import streams; + + @since(version = 0.2.0) + import poll; +} diff --git a/embed/embed/Cargo.toml b/embed/embed/Cargo.toml new file mode 100644 index 000000000..b1864f279 --- /dev/null +++ b/embed/embed/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "golem-embed" +version = "0.0.0" +edition = "2021" +license = "Apache-2.0" +homepage = "https://golem.cloud" +repository = "https://github.com/golemcloud/golem-llm" +description = "WebAssembly components for working with AI models and providers APIs, with special support for Golem Cloud" + +[lib] +path = "src/lib.rs" +crate-type = ["rlib"] + +[dependencies] +golem-rust = { workspace = true } +log = { workspace = true } +mime = "0.3.17" +nom = { version = "7.1", default-features = false } +reqwest = { workspace = true } +thiserror = "2.0.12" +wasi-logger = "0.1.2" +wit-bindgen = { version = "0.40.0" } + +[features] +default = ["durability"] +durability = ["golem-rust/durability"] diff --git a/embed/embed/src/config.rs b/embed/embed/src/config.rs new file mode 100644 index 000000000..903ec695b --- /dev/null +++ b/embed/embed/src/config.rs @@ -0,0 +1,21 @@ +use crate::golem::embed::embed::{Error, ErrorCode}; +use std::ffi::OsStr; + +pub fn with_config_key( + key: impl AsRef, + fail: impl FnOnce(Error) -> R, + succeed: impl FnOnce(String) -> R, +) -> R { + let key_str = key.as_ref().to_string_lossy().to_string(); + match std::env::var(key) { + Ok(value) => succeed(value), + Err(_) => { + let error = Error { + code: ErrorCode::AuthenticationFailed, + message: format!("Missing config key: {key_str}"), + provider_error_json: None, + }; + fail(error) + } + } +} diff --git a/embed/embed/src/durability.rs b/embed/embed/src/durability.rs new file mode 100644 index 000000000..fa1b59434 --- /dev/null +++ b/embed/embed/src/durability.rs @@ -0,0 +1,184 @@ +use crate::golem::embed::embed::Guest; +use std::marker::PhantomData; + +/// Wraps an embed implementation with custom durability +pub struct DurableEmbed { + phantom: PhantomData, +} + +/// Trait to be implemented in addition to the embed `Guest` trait when wrapping it with `DurableEmbed`. +pub trait ExtendedGuest: Guest + 'static {} + +/// When the durability feature flag is off, wrapping with `DurableEmbed` is just a passthrough +#[cfg(not(feature = "durability"))] +mod passthrough_impl { + use crate::durability::{DurableEmbed, ExtendedGuest}; + use crate::golem::embed::embed::{ + Config, ContentPart, EmbeddingResponse, Error, Guest, RerankResponse, + }; + + impl Guest for DurableEmbed { + fn generate(inputs: Vec, config: Config) -> Result { + Impl::generate(inputs, config) + } + + fn rerank( + query: String, + documents: Vec, + config: Config, + ) -> Result { + Impl::rerank(query, documents, config) + } + } +} + +/// When the durability feature flag is on, wrapping with `DurableEmbed` adds custom durability +/// on top of the provider-specific embed implementation using Golem's special host functions and +/// the `golem-rust` helper library. +/// +/// There will be custom durability entries saved in the oplog, with the full embed request and configuration +/// stored as input, and the full response stored as output. To serialize these in a way it is +/// observable by oplog consumers, each relevant data type has to be converted to/from `ValueAndType` +/// which is implemented using the type classes and builder in the `golem-rust` library. +#[cfg(feature = "durability")] +mod durable_impl { + use crate::durability::{DurableEmbed, ExtendedGuest}; + use crate::golem::embed::embed::{ + Config, ContentPart, EmbeddingResponse, Error, Guest, RerankResponse, + }; + use golem_rust::bindings::golem::durability::durability::DurableFunctionType; + use golem_rust::durability::Durability; + use golem_rust::{with_persistence_level, FromValueAndType, IntoValue, PersistenceLevel}; + use std::fmt::{Display, Formatter}; + + impl Guest for DurableEmbed { + fn generate(inputs: Vec, config: Config) -> Result { + let durability = Durability::, UnusedError>::new( + "golem_embed", + "generate", + DurableFunctionType::WriteRemote, + ); + if durability.is_live() { + let result = with_persistence_level(PersistenceLevel::PersistNothing, || { + Impl::generate(inputs.clone(), config.clone()) + }); + durability.persist_infallible(GenerateInput { inputs, config }, result.clone()) + } else { + durability.replay_infallible() + } + } + + fn rerank( + query: String, + documents: Vec, + config: Config, + ) -> Result { + let durability = Durability::, UnusedError>::new( + "golem_embed", + "rerank", + DurableFunctionType::WriteRemote, + ); + if durability.is_live() { + let result = with_persistence_level(PersistenceLevel::PersistNothing, || { + Impl::rerank(query.clone(), documents.clone(), config.clone()) + }); + durability.persist_infallible( + RerankInput { + query, + documents, + config, + }, + result.clone(), + ) + } else { + durability.replay_infallible() + } + } + } + + #[derive(Debug, Clone, PartialEq, IntoValue, FromValueAndType)] + struct GenerateInput { + inputs: Vec, + config: Config, + } + + #[derive(Debug, Clone, PartialEq, IntoValue, FromValueAndType)] + struct RerankInput { + query: String, + documents: Vec, + config: Config, + } + + #[derive(Debug, FromValueAndType, IntoValue)] + struct UnusedError; + + impl Display for UnusedError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "UnusedError") + } + } + + #[cfg(test)] + mod tests { + use crate::durability::durable_impl::{GenerateInput, RerankInput}; + use crate::golem::embed::embed::{Config, ContentPart, ImageUrl, TaskType}; + use golem_rust::value_and_type::{FromValueAndType, IntoValueAndType}; + use std::fmt::Debug; + + fn roundtrip_test( + value: T, + ) { + let vnt = value.clone().into_value_and_type(); + let extracted = T::from_value_and_type(vnt).unwrap(); + assert_eq!(value, extracted); + } + + #[test] + fn generate_input_encoding() { + let input = GenerateInput { + inputs: vec![ + ContentPart::Text("Hello world".to_string()), + ContentPart::Image(ImageUrl { + url: "https://example.com/image.png".to_string(), + }), + ], + config: Config { + model: Some("text-embedding-3-small".to_string()), + task_type: Some(TaskType::RetrievalQuery), + dimensions: Some(512), + truncation: Some(true), + output_format: None, + output_dtype: None, + user: Some("test-user".to_string()), + provider_options: vec![], + }, + }; + + roundtrip_test(input); + } + + #[test] + fn rerank_input_encoding() { + let input = RerankInput { + query: "What is machine learning?".to_string(), + documents: vec![ + "Machine learning is a subset of AI".to_string(), + "Deep learning uses neural networks".to_string(), + "NLP processes human language".to_string(), + ], + config: Config { + model: Some("rerank-english-v3.0".to_string()), + task_type: None, + dimensions: None, + truncation: None, + output_format: None, + output_dtype: None, + user: None, + provider_options: vec![], + }, + }; + + roundtrip_test(input); + } + } +} diff --git a/embed/embed/src/error.rs b/embed/embed/src/error.rs new file mode 100644 index 000000000..a576e4462 --- /dev/null +++ b/embed/embed/src/error.rs @@ -0,0 +1,41 @@ +use crate::golem::embed::embed::{Error, ErrorCode}; +use reqwest::StatusCode; + +pub fn unsupported(what: impl AsRef) -> Error { + Error { + code: ErrorCode::Unsupported, + message: format!("Unsupported: {}", what.as_ref()), + provider_error_json: None, + } +} + +pub fn model_not_found(model: impl AsRef) -> Error { + Error { + code: ErrorCode::ModelNotFound, + message: format!("Model not found: {}", model.as_ref()), + provider_error_json: None, + } +} + +pub fn from_reqwest_error(details: impl AsRef, err: reqwest::Error) -> Error { + Error { + code: ErrorCode::InternalError, + message: format!("{}: {err}", details.as_ref()), + provider_error_json: None, + } +} + +pub fn error_code_from_status(status: StatusCode) -> ErrorCode { + if status == StatusCode::TOO_MANY_REQUESTS { + ErrorCode::RateLimitExceeded + } else if status == StatusCode::UNAUTHORIZED + || status == StatusCode::FORBIDDEN + || status == StatusCode::PAYMENT_REQUIRED + { + ErrorCode::AuthenticationFailed + } else if status.is_client_error() { + ErrorCode::InvalidRequest + } else { + ErrorCode::InternalError + } +} diff --git a/embed/embed/src/lib.rs b/embed/embed/src/lib.rs new file mode 100644 index 000000000..0223ec879 --- /dev/null +++ b/embed/embed/src/lib.rs @@ -0,0 +1,40 @@ +pub mod config; +pub mod durability; +pub mod error; + +wit_bindgen::generate!({ + path: "../wit", + world: "embed-library", + generate_all, + generate_unused_types: true, + additional_derives: [PartialEq, golem_rust::FromValueAndType, golem_rust::IntoValue], + pub_export_macro: true, +}); + +pub use crate::exports::golem; +pub use __export_embed_library_impl as export_embed; +use std::cell::RefCell; +use std::str::FromStr; + +pub struct LoggingState { + logging_initialized: bool, +} + +impl LoggingState { + pub fn init(&mut self) { + if !self.logging_initialized { + let _ = wasi_logger::Logger::install(); + let max_level: log::LevelFilter = + log::LevelFilter::from_str(&std::env::var("GOLEM_EMBED_LOG").unwrap_or_default()) + .unwrap_or(log::LevelFilter::Info); + log::set_max_level(max_level); + self.logging_initialized = true; + } + } +} + +thread_local! { + pub static LOGGING_STATE: RefCell = const { RefCell::new(LoggingState { + logging_initialized: false, + }) }; +} diff --git a/embed/embed/wit/deps/golem-embed/golem-embed.wit b/embed/embed/wit/deps/golem-embed/golem-embed.wit new file mode 100644 index 000000000..78d040320 --- /dev/null +++ b/embed/embed/wit/deps/golem-embed/golem-embed.wit @@ -0,0 +1,129 @@ +package golem:embed@1.0.0; + +interface embed { + // --- Enums --- + + enum task-type { + retrieval-query, + retrieval-document, + semantic-similarity, + classification, + clustering, + question-answering, + fact-verification, + code-retrieval, + } + + enum output-format { + float-array, + binary, + base64, + } + + enum output-dtype { + float-array, + int8, + uint8, + binary, + ubinary, + } + + enum error-code { + invalid-request, + model-not-found, + unsupported, + authentication-failed, + provider-error, + rate-limit-exceeded, + internal-error, + unknown, + } + + // --- Content --- + + record image-url { + url: string, + } + + variant content-part { + text(string), + image(image-url), + } + + // --- Configuration --- + + record kv { + key: string, + value: string, + } + + record config { + model: option, + task-type: option, + dimensions: option, + truncation: option, + output-format: option, + output-dtype: option, + user: option, + provider-options: list, + } + + // --- Embedding Response --- + + record usage { + input-tokens: option, + total-tokens: option, + } + + record embedding { + index: u32, + vector: list, + } + + record embedding-response { + embeddings: list, + usage: option, + model: string, + provider-metadata-json: option, + } + + // --- Rerank Response --- + + record rerank-result { + index: u32, + relevance-score: f32, + document: option, + } + + record rerank-response { + results: list, + usage: option, + model: string, + provider-metadata-json: option, + } + + // --- Error Handling --- + + record error { + code: error-code, + message: string, + provider-error-json: option, + } + + // --- Core Functions --- + + generate: func( + inputs: list, + config: config + ) -> result; + + rerank: func( + query: string, + documents: list, + config: config + ) -> result; +} + +world embed-library { + export embed; +} \ No newline at end of file diff --git a/embed/embed/wit/deps/wasi:io/error.wit b/embed/embed/wit/deps/wasi:io/error.wit new file mode 100644 index 000000000..97c606877 --- /dev/null +++ b/embed/embed/wit/deps/wasi:io/error.wit @@ -0,0 +1,34 @@ +package wasi:io@0.2.3; + +@since(version = 0.2.0) +interface error { + /// A resource which represents some error information. + /// + /// The only method provided by this resource is `to-debug-string`, + /// which provides some human-readable information about the error. + /// + /// In the `wasi:io` package, this resource is returned through the + /// `wasi:io/streams/stream-error` type. + /// + /// To provide more specific error information, other interfaces may + /// offer functions to "downcast" this error into more specific types. For example, + /// errors returned from streams derived from filesystem types can be described using + /// the filesystem's own error-code type. This is done using the function + /// `wasi:filesystem/types/filesystem-error-code`, which takes a `borrow` + /// parameter and returns an `option`. + /// + /// The set of functions which can "downcast" an `error` into a more + /// concrete type is open. + @since(version = 0.2.0) + resource error { + /// Returns a string that is suitable to assist humans in debugging + /// this error. + /// + /// WARNING: The returned string should not be consumed mechanically! + /// It may change across platforms, hosts, or other implementation + /// details. Parsing this string is a major platform-compatibility + /// hazard. + @since(version = 0.2.0) + to-debug-string: func() -> string; + } +} diff --git a/embed/embed/wit/deps/wasi:io/poll.wit b/embed/embed/wit/deps/wasi:io/poll.wit new file mode 100644 index 000000000..9bcbe8e03 --- /dev/null +++ b/embed/embed/wit/deps/wasi:io/poll.wit @@ -0,0 +1,47 @@ +package wasi:io@0.2.3; + +/// A poll API intended to let users wait for I/O events on multiple handles +/// at once. +@since(version = 0.2.0) +interface poll { + /// `pollable` represents a single I/O event which may be ready, or not. + @since(version = 0.2.0) + resource pollable { + + /// Return the readiness of a pollable. This function never blocks. + /// + /// Returns `true` when the pollable is ready, and `false` otherwise. + @since(version = 0.2.0) + ready: func() -> bool; + + /// `block` returns immediately if the pollable is ready, and otherwise + /// blocks until ready. + /// + /// This function is equivalent to calling `poll.poll` on a list + /// containing only this pollable. + @since(version = 0.2.0) + block: func(); + } + + /// Poll for completion on a set of pollables. + /// + /// This function takes a list of pollables, which identify I/O sources of + /// interest, and waits until one or more of the events is ready for I/O. + /// + /// The result `list` contains one or more indices of handles in the + /// argument list that is ready for I/O. + /// + /// This function traps if either: + /// - the list is empty, or: + /// - the list contains more elements than can be indexed with a `u32` value. + /// + /// A timeout can be implemented by adding a pollable from the + /// wasi-clocks API to the list. + /// + /// This function does not return a `result`; polling in itself does not + /// do any I/O so it doesn't fail. If any of the I/O sources identified by + /// the pollables has an error, it is indicated by marking the source as + /// being ready for I/O. + @since(version = 0.2.0) + poll: func(in: list>) -> list; +} diff --git a/embed/embed/wit/deps/wasi:io/streams.wit b/embed/embed/wit/deps/wasi:io/streams.wit new file mode 100644 index 000000000..0de084629 --- /dev/null +++ b/embed/embed/wit/deps/wasi:io/streams.wit @@ -0,0 +1,290 @@ +package wasi:io@0.2.3; + +/// WASI I/O is an I/O abstraction API which is currently focused on providing +/// stream types. +/// +/// In the future, the component model is expected to add built-in stream types; +/// when it does, they are expected to subsume this API. +@since(version = 0.2.0) +interface streams { + @since(version = 0.2.0) + use error.{error}; + @since(version = 0.2.0) + use poll.{pollable}; + + /// An error for input-stream and output-stream operations. + @since(version = 0.2.0) + variant stream-error { + /// The last operation (a write or flush) failed before completion. + /// + /// More information is available in the `error` payload. + /// + /// After this, the stream will be closed. All future operations return + /// `stream-error::closed`. + last-operation-failed(error), + /// The stream is closed: no more input will be accepted by the + /// stream. A closed output-stream will return this error on all + /// future operations. + closed + } + + /// An input bytestream. + /// + /// `input-stream`s are *non-blocking* to the extent practical on underlying + /// platforms. I/O operations always return promptly; if fewer bytes are + /// promptly available than requested, they return the number of bytes promptly + /// available, which could even be zero. To wait for data to be available, + /// use the `subscribe` function to obtain a `pollable` which can be polled + /// for using `wasi:io/poll`. + @since(version = 0.2.0) + resource input-stream { + /// Perform a non-blocking read from the stream. + /// + /// When the source of a `read` is binary data, the bytes from the source + /// are returned verbatim. When the source of a `read` is known to the + /// implementation to be text, bytes containing the UTF-8 encoding of the + /// text are returned. + /// + /// This function returns a list of bytes containing the read data, + /// when successful. The returned list will contain up to `len` bytes; + /// it may return fewer than requested, but not more. The list is + /// empty when no bytes are available for reading at this time. The + /// pollable given by `subscribe` will be ready when more bytes are + /// available. + /// + /// This function fails with a `stream-error` when the operation + /// encounters an error, giving `last-operation-failed`, or when the + /// stream is closed, giving `closed`. + /// + /// When the caller gives a `len` of 0, it represents a request to + /// read 0 bytes. If the stream is still open, this call should + /// succeed and return an empty list, or otherwise fail with `closed`. + /// + /// The `len` parameter is a `u64`, which could represent a list of u8 which + /// is not possible to allocate in wasm32, or not desirable to allocate as + /// as a return value by the callee. The callee may return a list of bytes + /// less than `len` in size while more bytes are available for reading. + @since(version = 0.2.0) + read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Read bytes from a stream, after blocking until at least one byte can + /// be read. Except for blocking, behavior is identical to `read`. + @since(version = 0.2.0) + blocking-read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Skip bytes from a stream. Returns number of bytes skipped. + /// + /// Behaves identical to `read`, except instead of returning a list + /// of bytes, returns the number of bytes consumed from the stream. + @since(version = 0.2.0) + skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Skip bytes from a stream, after blocking until at least one byte + /// can be skipped. Except for blocking behavior, identical to `skip`. + @since(version = 0.2.0) + blocking-skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Create a `pollable` which will resolve once either the specified stream + /// has bytes available to read or the other end of the stream has been + /// closed. + /// The created `pollable` is a child resource of the `input-stream`. + /// Implementations may trap if the `input-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } + + + /// An output bytestream. + /// + /// `output-stream`s are *non-blocking* to the extent practical on + /// underlying platforms. Except where specified otherwise, I/O operations also + /// always return promptly, after the number of bytes that can be written + /// promptly, which could even be zero. To wait for the stream to be ready to + /// accept data, the `subscribe` function to obtain a `pollable` which can be + /// polled for using `wasi:io/poll`. + /// + /// Dropping an `output-stream` while there's still an active write in + /// progress may result in the data being lost. Before dropping the stream, + /// be sure to fully flush your writes. + @since(version = 0.2.0) + resource output-stream { + /// Check readiness for writing. This function never blocks. + /// + /// Returns the number of bytes permitted for the next call to `write`, + /// or an error. Calling `write` with more bytes than this function has + /// permitted will trap. + /// + /// When this function returns 0 bytes, the `subscribe` pollable will + /// become ready when this function will report at least 1 byte, or an + /// error. + @since(version = 0.2.0) + check-write: func() -> result; + + /// Perform a write. This function never blocks. + /// + /// When the destination of a `write` is binary data, the bytes from + /// `contents` are written verbatim. When the destination of a `write` is + /// known to the implementation to be text, the bytes of `contents` are + /// transcoded from UTF-8 into the encoding of the destination and then + /// written. + /// + /// Precondition: check-write gave permit of Ok(n) and contents has a + /// length of less than or equal to n. Otherwise, this function will trap. + /// + /// returns Err(closed) without writing if the stream has closed since + /// the last call to check-write provided a permit. + @since(version = 0.2.0) + write: func( + contents: list + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 bytes, and then flush the stream. Block + /// until all of these operations are complete, or an error occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write`, and `flush`, and is implemented with the + /// following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while !contents.is_empty() { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, contents.len()); + /// let (chunk, rest) = contents.split_at(len); + /// this.write(chunk ); // eliding error handling + /// contents = rest; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + @since(version = 0.2.0) + blocking-write-and-flush: func( + contents: list + ) -> result<_, stream-error>; + + /// Request to flush buffered output. This function never blocks. + /// + /// This tells the output-stream that the caller intends any buffered + /// output to be flushed. the output which is expected to be flushed + /// is all that has been passed to `write` prior to this call. + /// + /// Upon calling this function, the `output-stream` will not accept any + /// writes (`check-write` will return `ok(0)`) until the flush has + /// completed. The `subscribe` pollable will become ready when the + /// flush has completed and the stream can accept more writes. + @since(version = 0.2.0) + flush: func() -> result<_, stream-error>; + + /// Request to flush buffered output, and block until flush completes + /// and stream is ready for writing again. + @since(version = 0.2.0) + blocking-flush: func() -> result<_, stream-error>; + + /// Create a `pollable` which will resolve once the output-stream + /// is ready for more writing, or an error has occurred. When this + /// pollable is ready, `check-write` will return `ok(n)` with n>0, or an + /// error. + /// + /// If the stream is closed, this pollable is always ready immediately. + /// + /// The created `pollable` is a child resource of the `output-stream`. + /// Implementations may trap if the `output-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) + subscribe: func() -> pollable; + + /// Write zeroes to a stream. + /// + /// This should be used precisely like `write` with the exact same + /// preconditions (must use check-write first), but instead of + /// passing a list of bytes, you simply pass the number of zero-bytes + /// that should be written. + @since(version = 0.2.0) + write-zeroes: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 zeroes, and then flush the stream. + /// Block until all of these operations are complete, or an error + /// occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write-zeroes`, and `flush`, and is implemented with + /// the following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while num_zeroes != 0 { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, num_zeroes); + /// this.write-zeroes(len); // eliding error handling + /// num_zeroes -= len; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + @since(version = 0.2.0) + blocking-write-zeroes-and-flush: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Read from one stream and write to another. + /// + /// The behavior of splice is equivalent to: + /// 1. calling `check-write` on the `output-stream` + /// 2. calling `read` on the `input-stream` with the smaller of the + /// `check-write` permitted length and the `len` provided to `splice` + /// 3. calling `write` on the `output-stream` with that read data. + /// + /// Any error reported by the call to `check-write`, `read`, or + /// `write` ends the splice and reports that error. + /// + /// This function returns the number of bytes transferred; it may be less + /// than `len`. + @since(version = 0.2.0) + splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + + /// Read from one stream and write to another, with blocking. + /// + /// This is similar to `splice`, except that it blocks until the + /// `output-stream` is ready for writing, and the `input-stream` + /// is ready for reading, before performing the `splice`. + @since(version = 0.2.0) + blocking-splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + } +} diff --git a/embed/embed/wit/deps/wasi:io/world.wit b/embed/embed/wit/deps/wasi:io/world.wit new file mode 100644 index 000000000..f1d2102dc --- /dev/null +++ b/embed/embed/wit/deps/wasi:io/world.wit @@ -0,0 +1,10 @@ +package wasi:io@0.2.3; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import streams; + + @since(version = 0.2.0) + import poll; +} diff --git a/embed/embed/wit/embed.wit b/embed/embed/wit/embed.wit new file mode 100644 index 000000000..3e697a31f --- /dev/null +++ b/embed/embed/wit/embed.wit @@ -0,0 +1,5 @@ +package golem:embed-library@1.0.0; + +world embed-library { + export golem:embed/embed@1.0.0; +} diff --git a/embed/hugging-face/Cargo.toml b/embed/hugging-face/Cargo.toml new file mode 100644 index 000000000..578e78d0a --- /dev/null +++ b/embed/hugging-face/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "golem-embed-hugging-face" +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 Hugging face embeding APIs, with special support for Golem Cloud" + + +[lib] +path = "src/lib.rs" +crate-type = ["cdylib"] + +[features] +default = ["durability"] +durability = ["golem-rust/durability", "golem-embed/durability"] + + +[dependencies] +golem-embed = { path = "../embed", version = "0.0.0", default-features = false } +golem-rust = { workspace = true } +log = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +base64 = { workspace = true } +wit-bindgen-rt = { workspace = true } +bytemuck = "1.23.0" + + +[package.metadata.component] +package = "golem:embed-hugging-face" + +[package.metadata.component.bindings] +generate_unused_types = true + +[package.metadata.component.bindings.with] +"golem:embed/embed@1.0.0" = "golem_embed::golem::embed::embed" + +[package.metadata.component.target] +path = "wit" + +[package.metadata.component.target.dependencies] +"golem:embed" = { path = "wit/deps/golem-embed" } +"wasi:io" = { path = "wit/deps/wasi:io" } diff --git a/embed/hugging-face/src/bindings.rs b/embed/hugging-face/src/bindings.rs new file mode 100644 index 000000000..506c76faa --- /dev/null +++ b/embed/hugging-face/src/bindings.rs @@ -0,0 +1,43 @@ +// Generated by `wit-bindgen` 0.41.0. DO NOT EDIT! +// Options used: +// * runtime_path: "wit_bindgen_rt" +// * with "golem:embed/embed@1.0.0" = "golem_embed::golem::embed::embed" +// * generate_unused_types +use golem_embed::golem::embed::embed as __with_name0; +#[cfg(target_arch = "wasm32")] +#[unsafe( + link_section = "component-type:wit-bindgen:0.41.0:golem:embed-hugging-face@1.0.0:embed-library:encoded world" +)] +#[doc(hidden)] +#[allow(clippy::octal_escapes)] +pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 1269] = *b"\ +\0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07\xf1\x08\x01A\x02\x01\ +A\x02\x01B/\x01m\x08\x0fretrieval-query\x12retrieval-document\x13semantic-simila\ +rity\x0eclassification\x0aclustering\x12question-answering\x11fact-verification\x0e\ +code-retrieval\x04\0\x09task-type\x03\0\0\x01m\x03\x0bfloat-array\x06binary\x06b\ +ase64\x04\0\x0doutput-format\x03\0\x02\x01m\x05\x0bfloat-array\x04int8\x05uint8\x06\ +binary\x07ubinary\x04\0\x0coutput-dtype\x03\0\x04\x01m\x08\x0finvalid-request\x0f\ +model-not-found\x0bunsupported\x15authentication-failed\x0eprovider-error\x13rat\ +e-limit-exceeded\x0einternal-error\x07unknown\x04\0\x0aerror-code\x03\0\x06\x01r\ +\x01\x03urls\x04\0\x09image-url\x03\0\x08\x01q\x02\x04text\x01s\0\x05image\x01\x09\ +\0\x04\0\x0ccontent-part\x03\0\x0a\x01r\x02\x03keys\x05values\x04\0\x02kv\x03\0\x0c\ +\x01ks\x01k\x01\x01ky\x01k\x7f\x01k\x03\x01k\x05\x01p\x0d\x01r\x08\x05model\x0e\x09\ +task-type\x0f\x0adimensions\x10\x0atruncation\x11\x0doutput-format\x12\x0coutput\ +-dtype\x13\x04user\x0e\x10provider-options\x14\x04\0\x06config\x03\0\x15\x01r\x02\ +\x0cinput-tokens\x10\x0ctotal-tokens\x10\x04\0\x05usage\x03\0\x17\x01pv\x01r\x02\ +\x05indexy\x06vector\x19\x04\0\x09embedding\x03\0\x1a\x01p\x1b\x01k\x18\x01r\x04\ +\x0aembeddings\x1c\x05usage\x1d\x05models\x16provider-metadata-json\x0e\x04\0\x12\ +embedding-response\x03\0\x1e\x01r\x03\x05indexy\x0frelevance-scorev\x08document\x0e\ +\x04\0\x0drerank-result\x03\0\x20\x01p!\x01r\x04\x07results\"\x05usage\x1d\x05mo\ +dels\x16provider-metadata-json\x0e\x04\0\x0frerank-response\x03\0#\x01r\x03\x04c\ +ode\x07\x07messages\x13provider-error-json\x0e\x04\0\x05error\x03\0%\x01p\x0b\x01\ +j\x01\x1f\x01&\x01@\x02\x06inputs'\x06config\x16\0(\x04\0\x08generate\x01)\x01ps\ +\x01j\x01$\x01&\x01@\x03\x05querys\x09documents*\x06config\x16\0+\x04\0\x06reran\ +k\x01,\x04\0\x17golem:embed/embed@1.0.0\x05\0\x04\0,golem:embed-hugging-face/emb\ +ed-library@1.0.0\x04\0\x0b\x13\x01\0\x0dembed-library\x03\0\0\0G\x09producers\x01\ +\x0cprocessed-by\x02\x0dwit-component\x070.227.1\x10wit-bindgen-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/embed/hugging-face/src/client.rs b/embed/hugging-face/src/client.rs new file mode 100644 index 000000000..190918115 --- /dev/null +++ b/embed/hugging-face/src/client.rs @@ -0,0 +1,124 @@ +use std::fmt::Debug; + +use golem_embed::{ + error::{error_code_from_status, from_reqwest_error}, + golem::embed::embed::Error, +}; +use log::trace; +use reqwest::{Client, Method, Response}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +const BASE_URL: &str = "https://router.huggingface.co/hf-inference"; + +/// The Hugging Face API client for creating embeddings. +/// +/// Based on https://huggingface.co/docs/api-inference/index +pub struct EmbeddingsApi { + huggingface_api_key: String, + client: Client, +} + +impl EmbeddingsApi { + pub fn new(huggingface_api_key: String) -> Self { + let client = Client::builder() + .build() + .expect("Failed to initialize HTTP client"); + Self { + huggingface_api_key, + client, + } + } + + pub fn generate_embedding( + &self, + request: EmbeddingRequest, + model: &str, + ) -> Result { + trace!("Sending request to Hugging Face API: {request:?}"); + let response = self + .client + .request( + Method::POST, + format!("{BASE_URL}/models/{model}/pipeline/feature-extraction"), + ) + .bearer_auth(&self.huggingface_api_key) + .json(&request) + .send() + .map_err(|err| from_reqwest_error("Request failed", err))?; + parse_response::(response) + } +} + +fn parse_response(response: Response) -> Result { + let status = response.status(); + let response_text = response + .text() + .map_err(|err| from_reqwest_error("Failed to read response body", err))?; + match serde_json::from_str::(&response_text) { + Ok(response_data) => { + trace!("Response from Hugging Face API: {response_data:?}"); + Ok(response_data) + } + Err(error) => { + trace!("Error parsing response: {error:?}"); + Err(Error { + code: error_code_from_status(status), + message: format!("Failed to decode response body: {response_text}"), + provider_error_json: Some(error.to_string()), + }) + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EmbeddingRequest { + pub inputs: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub normalize: Option, + /// The name of the prompt that should be used by for encoding. + /// If not set, no prompt will be applied. Must be a key in the + /// `sentence-transformers` configuration `prompts` dictionary. + /// For example if `prompt_name` is "query" and the `prompts` is {"query": "query: ", …}, + /// then the sentence "What is the capital of France?" will be encoded as + /// "query: What is the capital of France?" because the prompt text will + /// be prepended before any text to encode. + #[serde(skip_serializing_if = "Option::is_none")] + pub prompt_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub truncate: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub truncate_direction: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum TruncateDirection { + #[serde(rename = "left")] + Left, + #[serde(rename = "right")] + Right, +} + +pub type EmbeddingResponse = Vec>; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RerankRequest { + pub query: String, + pub documents: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub top_k: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub return_documents: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RerankResponse { + pub results: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RerankResult { + pub index: u32, + pub relevance_score: f32, + #[serde(skip_serializing_if = "Option::is_none")] + pub document: Option, +} diff --git a/embed/hugging-face/src/conversions.rs b/embed/hugging-face/src/conversions.rs new file mode 100644 index 000000000..465a7603e --- /dev/null +++ b/embed/hugging-face/src/conversions.rs @@ -0,0 +1,119 @@ +use golem_embed::error::unsupported; +use golem_embed::golem::embed::embed::{ + Config, ContentPart, EmbeddingResponse as GolemEmbeddingResponse, Error, +}; + +use crate::client::{EmbeddingRequest, EmbeddingResponse}; + +pub fn create_embedding_request( + inputs: Vec, + config: Config, +) -> Result<(EmbeddingRequest, String), Error> { + let mut input_texts = Vec::new(); + for content in inputs { + match content { + ContentPart::Text(text) => input_texts.push(text), + ContentPart::Image(_) => { + return Err(unsupported( + "Image embeddings are not supported by Hugging Face.", + )) + } + } + } + + let model = config + .model + .unwrap_or_else(|| "sentence-transformers/all-MiniLM-L6-v2".to_string()); + + let request = EmbeddingRequest { + inputs: input_texts, + normalize: Some(true), + prompt_name: None, + truncate: config.truncation, + truncate_direction: None, + }; + + Ok((request, model)) +} + +pub fn process_embedding_response( + response: EmbeddingResponse, + model: String, +) -> Result { + let mut embeddings = Vec::new(); + for (index, embedding_vec) in response.iter().enumerate() { + embeddings.push(golem_embed::golem::embed::embed::Embedding { + index: index as u32, + vector: embedding_vec.clone(), + }); + } + + Ok(GolemEmbeddingResponse { + embeddings, + usage: None, + model, + provider_metadata_json: None, + }) +} + +#[cfg(test)] +mod tests { + use golem_embed::golem::embed::embed::{ImageUrl, OutputDtype, OutputFormat, TaskType}; + + use super::*; + + #[test] + fn test_create_embedding_request() { + let inputs = vec![ContentPart::Text("Hello, world!".to_string())]; + let config = Config { + model: Some("sentence-transformers/all-MiniLM-L6-v2".to_string()), + dimensions: Some(384), + user: Some("test_user".to_string()), + task_type: Some(TaskType::RetrievalQuery), + truncation: Some(false), + output_format: Some(OutputFormat::FloatArray), + output_dtype: Some(OutputDtype::FloatArray), + provider_options: vec![], + }; + let result = create_embedding_request(inputs, config); + let (request, model) = result.unwrap(); + assert_eq!(request.inputs, vec!["Hello, world!"]); + assert_eq!(model, "sentence-transformers/all-MiniLM-L6-v2"); + assert_eq!(request.normalize, Some(true)); + assert_eq!(request.truncate, Some(false)); + } + + #[test] + fn test_process_embedding_response() { + let response: EmbeddingResponse = vec![vec![0.1, 0.2, 0.3], vec![0.4, 0.5, 0.6]]; + let model = "sentence-transformers/all-MiniLM-L6-v2".to_string(); + let result = process_embedding_response(response, model.clone()); + let embedding_response = result.unwrap(); + assert_eq!(embedding_response.embeddings.len(), 2); + assert_eq!(embedding_response.embeddings[0].index, 0); + assert_eq!(embedding_response.embeddings[0].vector, vec![0.1, 0.2, 0.3]); + assert_eq!(embedding_response.embeddings[1].index, 1); + assert_eq!(embedding_response.embeddings[1].vector, vec![0.4, 0.5, 0.6]); + assert_eq!(embedding_response.model, model); + assert!(embedding_response.usage.is_none()); + } + + #[test] + fn test_create_embedding_request_with_image() { + let inputs = vec![ContentPart::Image(ImageUrl { + url: "https://example.com/image.png".to_string(), + })]; + let config = Config { + model: Some("sentence-transformers/all-MiniLM-L6-v2".to_string()), + dimensions: Some(384), + user: Some("test_user".to_string()), + task_type: Some(TaskType::RetrievalQuery), + truncation: Some(false), + output_format: Some(OutputFormat::FloatArray), + output_dtype: Some(OutputDtype::FloatArray), + provider_options: vec![], + }; + let request = create_embedding_request(inputs, config); + assert!(request.is_err()); + } +} diff --git a/embed/hugging-face/src/lib.rs b/embed/hugging-face/src/lib.rs new file mode 100644 index 000000000..57df0c45f --- /dev/null +++ b/embed/hugging-face/src/lib.rs @@ -0,0 +1,59 @@ +mod client; +mod conversions; + +use client::EmbeddingsApi; +use conversions::{create_embedding_request, process_embedding_response}; +use golem_embed::{ + config::with_config_key, + durability::{DurableEmbed, ExtendedGuest}, + golem::embed::embed::{ + Config, ContentPart, EmbeddingResponse, Error, ErrorCode, Guest, RerankResponse, + }, + LOGGING_STATE, +}; + +struct HuggingFaceComponent; + +impl HuggingFaceComponent { + const ENV_VAR_NAME: &'static str = "HUGGINGFACE_API_KEY"; + + fn embeddings( + client: EmbeddingsApi, + inputs: Vec, + config: Config, + ) -> Result { + let (request, model) = create_embedding_request(inputs, config)?; + match client.generate_embedding(request, &model) { + Ok(response) => process_embedding_response(response, model), + Err(err) => Err(err), + } + } +} + +impl Guest for HuggingFaceComponent { + fn generate(inputs: Vec, config: Config) -> Result { + LOGGING_STATE.with_borrow_mut(|state| state.init()); + with_config_key(Self::ENV_VAR_NAME, Err, |huggingface_api_key| { + let client = EmbeddingsApi::new(huggingface_api_key); + Self::embeddings(client, inputs, config) + }) + } + + fn rerank( + _query: String, + _documents: Vec, + _config: Config, + ) -> Result { + Err(Error { + code: ErrorCode::Unsupported, + message: "Hugging Face inference does not support rerank".to_string(), + provider_error_json: None, + }) + } +} + +impl ExtendedGuest for HuggingFaceComponent {} + +type DurableHuggingFaceComponent = DurableEmbed; + +golem_embed::export_embed!(DurableHuggingFaceComponent with_types_in golem_embed); diff --git a/embed/hugging-face/wit/deps/golem-embed/golem-embed.wit b/embed/hugging-face/wit/deps/golem-embed/golem-embed.wit new file mode 100644 index 000000000..78d040320 --- /dev/null +++ b/embed/hugging-face/wit/deps/golem-embed/golem-embed.wit @@ -0,0 +1,129 @@ +package golem:embed@1.0.0; + +interface embed { + // --- Enums --- + + enum task-type { + retrieval-query, + retrieval-document, + semantic-similarity, + classification, + clustering, + question-answering, + fact-verification, + code-retrieval, + } + + enum output-format { + float-array, + binary, + base64, + } + + enum output-dtype { + float-array, + int8, + uint8, + binary, + ubinary, + } + + enum error-code { + invalid-request, + model-not-found, + unsupported, + authentication-failed, + provider-error, + rate-limit-exceeded, + internal-error, + unknown, + } + + // --- Content --- + + record image-url { + url: string, + } + + variant content-part { + text(string), + image(image-url), + } + + // --- Configuration --- + + record kv { + key: string, + value: string, + } + + record config { + model: option, + task-type: option, + dimensions: option, + truncation: option, + output-format: option, + output-dtype: option, + user: option, + provider-options: list, + } + + // --- Embedding Response --- + + record usage { + input-tokens: option, + total-tokens: option, + } + + record embedding { + index: u32, + vector: list, + } + + record embedding-response { + embeddings: list, + usage: option, + model: string, + provider-metadata-json: option, + } + + // --- Rerank Response --- + + record rerank-result { + index: u32, + relevance-score: f32, + document: option, + } + + record rerank-response { + results: list, + usage: option, + model: string, + provider-metadata-json: option, + } + + // --- Error Handling --- + + record error { + code: error-code, + message: string, + provider-error-json: option, + } + + // --- Core Functions --- + + generate: func( + inputs: list, + config: config + ) -> result; + + rerank: func( + query: string, + documents: list, + config: config + ) -> result; +} + +world embed-library { + export embed; +} \ No newline at end of file diff --git a/embed/hugging-face/wit/deps/wasi:io/error.wit b/embed/hugging-face/wit/deps/wasi:io/error.wit new file mode 100644 index 000000000..97c606877 --- /dev/null +++ b/embed/hugging-face/wit/deps/wasi:io/error.wit @@ -0,0 +1,34 @@ +package wasi:io@0.2.3; + +@since(version = 0.2.0) +interface error { + /// A resource which represents some error information. + /// + /// The only method provided by this resource is `to-debug-string`, + /// which provides some human-readable information about the error. + /// + /// In the `wasi:io` package, this resource is returned through the + /// `wasi:io/streams/stream-error` type. + /// + /// To provide more specific error information, other interfaces may + /// offer functions to "downcast" this error into more specific types. For example, + /// errors returned from streams derived from filesystem types can be described using + /// the filesystem's own error-code type. This is done using the function + /// `wasi:filesystem/types/filesystem-error-code`, which takes a `borrow` + /// parameter and returns an `option`. + /// + /// The set of functions which can "downcast" an `error` into a more + /// concrete type is open. + @since(version = 0.2.0) + resource error { + /// Returns a string that is suitable to assist humans in debugging + /// this error. + /// + /// WARNING: The returned string should not be consumed mechanically! + /// It may change across platforms, hosts, or other implementation + /// details. Parsing this string is a major platform-compatibility + /// hazard. + @since(version = 0.2.0) + to-debug-string: func() -> string; + } +} diff --git a/embed/hugging-face/wit/deps/wasi:io/poll.wit b/embed/hugging-face/wit/deps/wasi:io/poll.wit new file mode 100644 index 000000000..9bcbe8e03 --- /dev/null +++ b/embed/hugging-face/wit/deps/wasi:io/poll.wit @@ -0,0 +1,47 @@ +package wasi:io@0.2.3; + +/// A poll API intended to let users wait for I/O events on multiple handles +/// at once. +@since(version = 0.2.0) +interface poll { + /// `pollable` represents a single I/O event which may be ready, or not. + @since(version = 0.2.0) + resource pollable { + + /// Return the readiness of a pollable. This function never blocks. + /// + /// Returns `true` when the pollable is ready, and `false` otherwise. + @since(version = 0.2.0) + ready: func() -> bool; + + /// `block` returns immediately if the pollable is ready, and otherwise + /// blocks until ready. + /// + /// This function is equivalent to calling `poll.poll` on a list + /// containing only this pollable. + @since(version = 0.2.0) + block: func(); + } + + /// Poll for completion on a set of pollables. + /// + /// This function takes a list of pollables, which identify I/O sources of + /// interest, and waits until one or more of the events is ready for I/O. + /// + /// The result `list` contains one or more indices of handles in the + /// argument list that is ready for I/O. + /// + /// This function traps if either: + /// - the list is empty, or: + /// - the list contains more elements than can be indexed with a `u32` value. + /// + /// A timeout can be implemented by adding a pollable from the + /// wasi-clocks API to the list. + /// + /// This function does not return a `result`; polling in itself does not + /// do any I/O so it doesn't fail. If any of the I/O sources identified by + /// the pollables has an error, it is indicated by marking the source as + /// being ready for I/O. + @since(version = 0.2.0) + poll: func(in: list>) -> list; +} diff --git a/embed/hugging-face/wit/deps/wasi:io/streams.wit b/embed/hugging-face/wit/deps/wasi:io/streams.wit new file mode 100644 index 000000000..0de084629 --- /dev/null +++ b/embed/hugging-face/wit/deps/wasi:io/streams.wit @@ -0,0 +1,290 @@ +package wasi:io@0.2.3; + +/// WASI I/O is an I/O abstraction API which is currently focused on providing +/// stream types. +/// +/// In the future, the component model is expected to add built-in stream types; +/// when it does, they are expected to subsume this API. +@since(version = 0.2.0) +interface streams { + @since(version = 0.2.0) + use error.{error}; + @since(version = 0.2.0) + use poll.{pollable}; + + /// An error for input-stream and output-stream operations. + @since(version = 0.2.0) + variant stream-error { + /// The last operation (a write or flush) failed before completion. + /// + /// More information is available in the `error` payload. + /// + /// After this, the stream will be closed. All future operations return + /// `stream-error::closed`. + last-operation-failed(error), + /// The stream is closed: no more input will be accepted by the + /// stream. A closed output-stream will return this error on all + /// future operations. + closed + } + + /// An input bytestream. + /// + /// `input-stream`s are *non-blocking* to the extent practical on underlying + /// platforms. I/O operations always return promptly; if fewer bytes are + /// promptly available than requested, they return the number of bytes promptly + /// available, which could even be zero. To wait for data to be available, + /// use the `subscribe` function to obtain a `pollable` which can be polled + /// for using `wasi:io/poll`. + @since(version = 0.2.0) + resource input-stream { + /// Perform a non-blocking read from the stream. + /// + /// When the source of a `read` is binary data, the bytes from the source + /// are returned verbatim. When the source of a `read` is known to the + /// implementation to be text, bytes containing the UTF-8 encoding of the + /// text are returned. + /// + /// This function returns a list of bytes containing the read data, + /// when successful. The returned list will contain up to `len` bytes; + /// it may return fewer than requested, but not more. The list is + /// empty when no bytes are available for reading at this time. The + /// pollable given by `subscribe` will be ready when more bytes are + /// available. + /// + /// This function fails with a `stream-error` when the operation + /// encounters an error, giving `last-operation-failed`, or when the + /// stream is closed, giving `closed`. + /// + /// When the caller gives a `len` of 0, it represents a request to + /// read 0 bytes. If the stream is still open, this call should + /// succeed and return an empty list, or otherwise fail with `closed`. + /// + /// The `len` parameter is a `u64`, which could represent a list of u8 which + /// is not possible to allocate in wasm32, or not desirable to allocate as + /// as a return value by the callee. The callee may return a list of bytes + /// less than `len` in size while more bytes are available for reading. + @since(version = 0.2.0) + read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Read bytes from a stream, after blocking until at least one byte can + /// be read. Except for blocking, behavior is identical to `read`. + @since(version = 0.2.0) + blocking-read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Skip bytes from a stream. Returns number of bytes skipped. + /// + /// Behaves identical to `read`, except instead of returning a list + /// of bytes, returns the number of bytes consumed from the stream. + @since(version = 0.2.0) + skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Skip bytes from a stream, after blocking until at least one byte + /// can be skipped. Except for blocking behavior, identical to `skip`. + @since(version = 0.2.0) + blocking-skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Create a `pollable` which will resolve once either the specified stream + /// has bytes available to read or the other end of the stream has been + /// closed. + /// The created `pollable` is a child resource of the `input-stream`. + /// Implementations may trap if the `input-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } + + + /// An output bytestream. + /// + /// `output-stream`s are *non-blocking* to the extent practical on + /// underlying platforms. Except where specified otherwise, I/O operations also + /// always return promptly, after the number of bytes that can be written + /// promptly, which could even be zero. To wait for the stream to be ready to + /// accept data, the `subscribe` function to obtain a `pollable` which can be + /// polled for using `wasi:io/poll`. + /// + /// Dropping an `output-stream` while there's still an active write in + /// progress may result in the data being lost. Before dropping the stream, + /// be sure to fully flush your writes. + @since(version = 0.2.0) + resource output-stream { + /// Check readiness for writing. This function never blocks. + /// + /// Returns the number of bytes permitted for the next call to `write`, + /// or an error. Calling `write` with more bytes than this function has + /// permitted will trap. + /// + /// When this function returns 0 bytes, the `subscribe` pollable will + /// become ready when this function will report at least 1 byte, or an + /// error. + @since(version = 0.2.0) + check-write: func() -> result; + + /// Perform a write. This function never blocks. + /// + /// When the destination of a `write` is binary data, the bytes from + /// `contents` are written verbatim. When the destination of a `write` is + /// known to the implementation to be text, the bytes of `contents` are + /// transcoded from UTF-8 into the encoding of the destination and then + /// written. + /// + /// Precondition: check-write gave permit of Ok(n) and contents has a + /// length of less than or equal to n. Otherwise, this function will trap. + /// + /// returns Err(closed) without writing if the stream has closed since + /// the last call to check-write provided a permit. + @since(version = 0.2.0) + write: func( + contents: list + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 bytes, and then flush the stream. Block + /// until all of these operations are complete, or an error occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write`, and `flush`, and is implemented with the + /// following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while !contents.is_empty() { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, contents.len()); + /// let (chunk, rest) = contents.split_at(len); + /// this.write(chunk ); // eliding error handling + /// contents = rest; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + @since(version = 0.2.0) + blocking-write-and-flush: func( + contents: list + ) -> result<_, stream-error>; + + /// Request to flush buffered output. This function never blocks. + /// + /// This tells the output-stream that the caller intends any buffered + /// output to be flushed. the output which is expected to be flushed + /// is all that has been passed to `write` prior to this call. + /// + /// Upon calling this function, the `output-stream` will not accept any + /// writes (`check-write` will return `ok(0)`) until the flush has + /// completed. The `subscribe` pollable will become ready when the + /// flush has completed and the stream can accept more writes. + @since(version = 0.2.0) + flush: func() -> result<_, stream-error>; + + /// Request to flush buffered output, and block until flush completes + /// and stream is ready for writing again. + @since(version = 0.2.0) + blocking-flush: func() -> result<_, stream-error>; + + /// Create a `pollable` which will resolve once the output-stream + /// is ready for more writing, or an error has occurred. When this + /// pollable is ready, `check-write` will return `ok(n)` with n>0, or an + /// error. + /// + /// If the stream is closed, this pollable is always ready immediately. + /// + /// The created `pollable` is a child resource of the `output-stream`. + /// Implementations may trap if the `output-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) + subscribe: func() -> pollable; + + /// Write zeroes to a stream. + /// + /// This should be used precisely like `write` with the exact same + /// preconditions (must use check-write first), but instead of + /// passing a list of bytes, you simply pass the number of zero-bytes + /// that should be written. + @since(version = 0.2.0) + write-zeroes: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 zeroes, and then flush the stream. + /// Block until all of these operations are complete, or an error + /// occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write-zeroes`, and `flush`, and is implemented with + /// the following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while num_zeroes != 0 { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, num_zeroes); + /// this.write-zeroes(len); // eliding error handling + /// num_zeroes -= len; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + @since(version = 0.2.0) + blocking-write-zeroes-and-flush: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Read from one stream and write to another. + /// + /// The behavior of splice is equivalent to: + /// 1. calling `check-write` on the `output-stream` + /// 2. calling `read` on the `input-stream` with the smaller of the + /// `check-write` permitted length and the `len` provided to `splice` + /// 3. calling `write` on the `output-stream` with that read data. + /// + /// Any error reported by the call to `check-write`, `read`, or + /// `write` ends the splice and reports that error. + /// + /// This function returns the number of bytes transferred; it may be less + /// than `len`. + @since(version = 0.2.0) + splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + + /// Read from one stream and write to another, with blocking. + /// + /// This is similar to `splice`, except that it blocks until the + /// `output-stream` is ready for writing, and the `input-stream` + /// is ready for reading, before performing the `splice`. + @since(version = 0.2.0) + blocking-splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + } +} diff --git a/embed/hugging-face/wit/deps/wasi:io/world.wit b/embed/hugging-face/wit/deps/wasi:io/world.wit new file mode 100644 index 000000000..f1d2102dc --- /dev/null +++ b/embed/hugging-face/wit/deps/wasi:io/world.wit @@ -0,0 +1,10 @@ +package wasi:io@0.2.3; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import streams; + + @since(version = 0.2.0) + import poll; +} diff --git a/embed/hugging-face/wit/hugging_face.wit b/embed/hugging-face/wit/hugging_face.wit new file mode 100644 index 000000000..ea0e51730 --- /dev/null +++ b/embed/hugging-face/wit/hugging_face.wit @@ -0,0 +1,5 @@ +package golem:embed-hugging-face@1.0.0; + +world embed-library { + include golem:embed/embed-library@1.0.0; +} diff --git a/embed/openai/Cargo.toml b/embed/openai/Cargo.toml new file mode 100644 index 000000000..afd44969b --- /dev/null +++ b/embed/openai/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "golem-embed-openai" +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 OpenAI embeding APIs, with special support for Golem Cloud" + + +[lib] +path = "src/lib.rs" +crate-type = ["cdylib"] + +[features] +default = ["durability"] +durability = ["golem-rust/durability", "golem-embed/durability"] + + +[dependencies] +golem-embed = { path = "../embed", version = "0.0.0", default-features = false } +golem-rust = { workspace = true } +log = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +base64 = { workspace = true } +wit-bindgen-rt = { workspace = true } +bytemuck = "1.23.0" + + +[package.metadata.component] +package = "golem:embed-openai" + +[package.metadata.component.bindings] +generate_unused_types = true + +[package.metadata.component.bindings.with] +"golem:embed/embed@1.0.0" = "golem_embed::golem::embed::embed" + +[package.metadata.component.target] +path = "wit" + +[package.metadata.component.target.dependencies] +"golem:embed" = { path = "wit/deps/golem-embed" } +"wasi:io" = { path = "wit/deps/wasi:io" } diff --git a/embed/openai/src/bindings.rs b/embed/openai/src/bindings.rs new file mode 100644 index 000000000..33bc32f27 --- /dev/null +++ b/embed/openai/src/bindings.rs @@ -0,0 +1,43 @@ +// Generated by `wit-bindgen` 0.41.0. DO NOT EDIT! +// Options used: +// * runtime_path: "wit_bindgen_rt" +// * with "golem:embed/embed@1.0.0" = "golem_embed::golem::embed::embed" +// * generate_unused_types +use golem_embed::golem::embed::embed as __with_name0; +#[cfg(target_arch = "wasm32")] +#[unsafe( + link_section = "component-type:wit-bindgen:0.41.0:golem:embed-openai@1.0.0:embed-library:encoded world" +)] +#[doc(hidden)] +#[allow(clippy::octal_escapes)] +pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 1263] = *b"\ +\0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07\xeb\x08\x01A\x02\x01\ +A\x02\x01B/\x01m\x08\x0fretrieval-query\x12retrieval-document\x13semantic-simila\ +rity\x0eclassification\x0aclustering\x12question-answering\x11fact-verification\x0e\ +code-retrieval\x04\0\x09task-type\x03\0\0\x01m\x03\x0bfloat-array\x06binary\x06b\ +ase64\x04\0\x0doutput-format\x03\0\x02\x01m\x05\x0bfloat-array\x04int8\x05uint8\x06\ +binary\x07ubinary\x04\0\x0coutput-dtype\x03\0\x04\x01m\x08\x0finvalid-request\x0f\ +model-not-found\x0bunsupported\x15authentication-failed\x0eprovider-error\x13rat\ +e-limit-exceeded\x0einternal-error\x07unknown\x04\0\x0aerror-code\x03\0\x06\x01r\ +\x01\x03urls\x04\0\x09image-url\x03\0\x08\x01q\x02\x04text\x01s\0\x05image\x01\x09\ +\0\x04\0\x0ccontent-part\x03\0\x0a\x01r\x02\x03keys\x05values\x04\0\x02kv\x03\0\x0c\ +\x01ks\x01k\x01\x01ky\x01k\x7f\x01k\x03\x01k\x05\x01p\x0d\x01r\x08\x05model\x0e\x09\ +task-type\x0f\x0adimensions\x10\x0atruncation\x11\x0doutput-format\x12\x0coutput\ +-dtype\x13\x04user\x0e\x10provider-options\x14\x04\0\x06config\x03\0\x15\x01r\x02\ +\x0cinput-tokens\x10\x0ctotal-tokens\x10\x04\0\x05usage\x03\0\x17\x01pv\x01r\x02\ +\x05indexy\x06vector\x19\x04\0\x09embedding\x03\0\x1a\x01p\x1b\x01k\x18\x01r\x04\ +\x0aembeddings\x1c\x05usage\x1d\x05models\x16provider-metadata-json\x0e\x04\0\x12\ +embedding-response\x03\0\x1e\x01r\x03\x05indexy\x0frelevance-scorev\x08document\x0e\ +\x04\0\x0drerank-result\x03\0\x20\x01p!\x01r\x04\x07results\"\x05usage\x1d\x05mo\ +dels\x16provider-metadata-json\x0e\x04\0\x0frerank-response\x03\0#\x01r\x03\x04c\ +ode\x07\x07messages\x13provider-error-json\x0e\x04\0\x05error\x03\0%\x01p\x0b\x01\ +j\x01\x1f\x01&\x01@\x02\x06inputs'\x06config\x16\0(\x04\0\x08generate\x01)\x01ps\ +\x01j\x01$\x01&\x01@\x03\x05querys\x09documents*\x06config\x16\0+\x04\0\x06reran\ +k\x01,\x04\0\x17golem:embed/embed@1.0.0\x05\0\x04\0&golem:embed-openai/embed-lib\ +rary@1.0.0\x04\0\x0b\x13\x01\0\x0dembed-library\x03\0\0\0G\x09producers\x01\x0cp\ +rocessed-by\x02\x0dwit-component\x070.227.1\x10wit-bindgen-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/embed/openai/src/client.rs b/embed/openai/src/client.rs new file mode 100644 index 000000000..9d541f6ec --- /dev/null +++ b/embed/openai/src/client.rs @@ -0,0 +1,155 @@ +use std::fmt::Debug; + +use base64::{engine::general_purpose, Engine}; +use golem_embed::{ + error::{error_code_from_status, from_reqwest_error}, + golem::embed::embed::Error, +}; +use log::trace; + +#[allow(dead_code, unused, unused_imports)] +use reqwest::Client; +use reqwest::{Method, Response}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +const BASE_URL: &str = "https://api.openai.com"; + +/// The OpenAI API client for creating embeddings. +/// +/// Based on https://platform.openai.com/docs/api-reference/embeddings/create +pub struct EmbeddingsApi { + openai_api_key: String, + client: reqwest::Client, +} + +impl EmbeddingsApi { + pub fn new(openai_api_key: String) -> Self { + let client = Client::builder() + .build() + .expect("Failed to initialize HTTP client"); + Self { + openai_api_key, + client, + } + } + + pub fn generate_embeding(&self, request: EmbeddingRequest) -> Result { + trace!("Sending request to OpenAI API: {request:?}"); + let response = self + .client + .request(Method::POST, format!("{BASE_URL}/v1/embeddings")) + .bearer_auth(&self.openai_api_key) + .json(&request) + .send() + .map_err(|err| from_reqwest_error("Request failed", err))?; + parse_response::(response) + } +} + +fn parse_response(response: Response) -> Result { + let status = response.status(); + let response_text = response + .text() + .map_err(|err| from_reqwest_error("Failed to read response body", err))?; + match serde_json::from_str::(&response_text) { + Ok(response_data) => { + trace!("Response from Hugging Face API: {response_data:?}"); + Ok(response_data) + } + Err(error) => { + trace!("Error parsing response: {error:?}"); + Err(Error { + code: error_code_from_status(status), + message: format!("Failed to decode response body: {response_text}"), + provider_error_json: Some(error.to_string()), + }) + } + } +} + +/// OpenAI allows only allows float and base64 as output formats. +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub enum EncodingFormat { + #[serde(rename = "float")] + Float, + #[serde(rename = "base64")] + Base64, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct EmbeddingRequest { + pub input: String, + pub model: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub encoding_format: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub dimension: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub user: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct EmbeddingResponse { + pub object: String, + pub data: Vec, + pub model: String, + pub usage: EmbeddingUsage, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct EmbeddingUsage { + pub prompt_tokens: u32, + pub total_tokens: u32, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct EmbeddingData { + pub object: String, + pub embedding: EmbeddingVector, + pub index: usize, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(untagged)] +pub enum EmbeddingVector { + FloatArray(Vec), + Base64(String), +} + +impl EmbeddingVector { + pub fn to_float_vec(&self) -> Result, String> { + match self { + EmbeddingVector::FloatArray(vec) => Ok(vec.clone()), + EmbeddingVector::Base64(base64_str) => { + let bytes = general_purpose::STANDARD + .decode(base64_str) + .map_err(|e| format!("Failed to decode base64: {e}"))?; + + if bytes.len() % 4 != 0 { + return Err("Invalid base64 data: length not divisible by 4".to_string()); + } + + let mut floats: Vec = Vec::with_capacity(bytes.len() / 4); + for chunk in bytes.chunks_exact(4) { + floats.push(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])); + } + + Ok(floats) + } + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct OpenAIError { + pub error: OpenAIErrorDetails, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct OpenAIErrorDetails { + pub message: String, + #[serde(rename = "type")] + pub _type: String, + pub param: Option, + pub code: Option, +} diff --git a/embed/openai/src/conversions.rs b/embed/openai/src/conversions.rs new file mode 100644 index 000000000..7af41dcd3 --- /dev/null +++ b/embed/openai/src/conversions.rs @@ -0,0 +1,150 @@ +use golem_embed::error::unsupported; +use golem_embed::golem::embed::embed::{ + Config, ContentPart, EmbeddingResponse as GolemEmbeddingResponse, Error, +}; + +use crate::client::{EmbeddingRequest, EmbeddingResponse, EncodingFormat}; + +pub fn create_request(inputs: Vec, config: Config) -> Result { + let mut input = String::new(); + for content in inputs { + match content { + ContentPart::Text(text) => input.push_str(&text), + ContentPart::Image(_) => { + return Err(unsupported("Image embeddings is not supported by OpenAI.")) + } + } + } + + let model = config + .model + .unwrap_or_else(|| "text-embedding-ada-002".to_string()); + + let encoding_format = match config.output_format { + Some(golem_embed::golem::embed::embed::OutputFormat::FloatArray) => { + Some(EncodingFormat::Float) + } + Some(golem_embed::golem::embed::embed::OutputFormat::Base64) => { + Some(EncodingFormat::Base64) + } + _ => { + return Err(unsupported( + "OpenAI only supports float and base64 output formats.", + )) + } + }; + + Ok(EmbeddingRequest { + input, + model, + encoding_format, + dimension: config.dimensions, + user: config.user, + }) +} + +pub fn process_embedding_response( + response: EmbeddingResponse, +) -> Result { + let mut embeddings = Vec::new(); + for embeding_data in &response.data { + let embed = embeding_data.embedding.to_float_vec().map_err(|e| Error { + code: golem_embed::golem::embed::embed::ErrorCode::ProviderError, + message: e, + provider_error_json: None, + })?; + embeddings.push(golem_embed::golem::embed::embed::Embedding { + index: embeding_data.index as u32, + vector: embed, + }); + } + + let usage = golem_embed::golem::embed::embed::Usage { + input_tokens: Some(response.usage.prompt_tokens), + total_tokens: Some(response.usage.total_tokens), + }; + + Ok(GolemEmbeddingResponse { + embeddings, + usage: Some(usage), + model: response.model, + provider_metadata_json: None, + }) +} + +#[cfg(test)] +mod tests { + use golem_embed::golem::embed::embed::{ImageUrl, OutputDtype, OutputFormat, TaskType}; + + use crate::client::{EmbeddingData, EmbeddingUsage, EmbeddingVector}; + + use super::*; + + #[test] + fn test_process_embedding_response() { + let response = EmbeddingResponse { + data: vec![EmbeddingData { + embedding: EmbeddingVector::FloatArray(vec![0.1, 0.2, 0.3]), + index: 0, + object: "embedding".to_string(), + }], + model: "text-embedding-ada-002".to_string(), + usage: EmbeddingUsage { + prompt_tokens: 1, + total_tokens: 1, + }, + object: "list".to_string(), + }; + let result = process_embedding_response(response); + let embedding_response = result.unwrap(); + assert_eq!(embedding_response.embeddings.len(), 1); + assert_eq!(embedding_response.embeddings[0].index, 0); + assert_eq!(embedding_response.embeddings[0].vector, vec![0.1, 0.2, 0.3]); + assert_eq!(embedding_response.provider_metadata_json, None); + } + + #[test] + fn test_create_request() { + let inputs = vec![ContentPart::Text("Hello, world!".to_string())]; + let config = Config { + model: Some("text-embedding-ada-002".to_string()), + dimensions: Some(1536), + user: Some("test_user".to_string()), + task_type: Some(TaskType::RetrievalQuery), + truncation: Some(false), + output_format: Some(OutputFormat::FloatArray), + output_dtype: Some(OutputDtype::FloatArray), + provider_options: vec![], + }; + let request = create_request(inputs, config); + let embedding_request = request.unwrap(); + assert_eq!(embedding_request.input, "Hello, world!"); + assert_eq!(embedding_request.model, "text-embedding-ada-002"); + assert_eq!(embedding_request.dimension, Some(1536)); + assert_eq!(embedding_request.user, Some("test_user".to_string())); + assert_eq!( + embedding_request.encoding_format, + Some(EncodingFormat::Float) + ); + } + + #[test] + fn test_create_request_with_image() { + // OpenAI does not support image embeddings so this should return an error + let inputs = vec![ContentPart::Image(ImageUrl { + url: "https://example.com/image.png".to_string(), + })]; + let config = Config { + model: Some("text-embedding-ada-002".to_string()), + dimensions: Some(1536), + user: Some("test_user".to_string()), + task_type: Some(TaskType::RetrievalQuery), + truncation: Some(false), + output_format: Some(OutputFormat::FloatArray), + output_dtype: Some(OutputDtype::FloatArray), + provider_options: vec![], + }; + let request = create_request(inputs, config); + assert!(request.is_err()); + } +} diff --git a/embed/openai/src/lib.rs b/embed/openai/src/lib.rs new file mode 100644 index 000000000..8ade28eb4 --- /dev/null +++ b/embed/openai/src/lib.rs @@ -0,0 +1,62 @@ +mod client; +mod conversions; + +use client::EmbeddingsApi; +use conversions::{create_request, process_embedding_response}; +use golem_embed::{ + config::with_config_key, + durability::{DurableEmbed, ExtendedGuest}, + golem::embed::embed::{ + Config, ContentPart, EmbeddingResponse, Error, ErrorCode, Guest, RerankResponse, + }, + LOGGING_STATE, +}; + +struct OpenAIComponent; + +impl OpenAIComponent { + const ENV_VAR_NAME: &'static str = "OPENAI_API_KEY"; + + fn embeddings( + client: EmbeddingsApi, + inputs: Vec, + config: Config, + ) -> Result { + let request = create_request(inputs, config); + match request { + Ok(request) => match client.generate_embeding(request) { + Ok(response) => process_embedding_response(response), + Err(err) => Err(err), + }, + Err(err) => Err(err), + } + } +} + +impl Guest for OpenAIComponent { + fn generate(inputs: Vec, config: Config) -> Result { + LOGGING_STATE.with_borrow_mut(|state| state.init()); + with_config_key(Self::ENV_VAR_NAME, Err, |openai_api_key| { + let client = EmbeddingsApi::new(openai_api_key); + Self::embeddings(client, inputs, config) + }) + } + + fn rerank( + _query: String, + _documents: Vec, + _config: Config, + ) -> Result { + Err(Error { + code: ErrorCode::Unsupported, + message: "OpenAI does not support rerank".to_string(), + provider_error_json: None, + }) + } +} + +impl ExtendedGuest for OpenAIComponent {} + +type DurableOpenAIComponent = DurableEmbed; + +golem_embed::export_embed!(DurableOpenAIComponent with_types_in golem_embed); diff --git a/embed/openai/wit/deps/golem-embed/golem-embed.wit b/embed/openai/wit/deps/golem-embed/golem-embed.wit new file mode 100644 index 000000000..78d040320 --- /dev/null +++ b/embed/openai/wit/deps/golem-embed/golem-embed.wit @@ -0,0 +1,129 @@ +package golem:embed@1.0.0; + +interface embed { + // --- Enums --- + + enum task-type { + retrieval-query, + retrieval-document, + semantic-similarity, + classification, + clustering, + question-answering, + fact-verification, + code-retrieval, + } + + enum output-format { + float-array, + binary, + base64, + } + + enum output-dtype { + float-array, + int8, + uint8, + binary, + ubinary, + } + + enum error-code { + invalid-request, + model-not-found, + unsupported, + authentication-failed, + provider-error, + rate-limit-exceeded, + internal-error, + unknown, + } + + // --- Content --- + + record image-url { + url: string, + } + + variant content-part { + text(string), + image(image-url), + } + + // --- Configuration --- + + record kv { + key: string, + value: string, + } + + record config { + model: option, + task-type: option, + dimensions: option, + truncation: option, + output-format: option, + output-dtype: option, + user: option, + provider-options: list, + } + + // --- Embedding Response --- + + record usage { + input-tokens: option, + total-tokens: option, + } + + record embedding { + index: u32, + vector: list, + } + + record embedding-response { + embeddings: list, + usage: option, + model: string, + provider-metadata-json: option, + } + + // --- Rerank Response --- + + record rerank-result { + index: u32, + relevance-score: f32, + document: option, + } + + record rerank-response { + results: list, + usage: option, + model: string, + provider-metadata-json: option, + } + + // --- Error Handling --- + + record error { + code: error-code, + message: string, + provider-error-json: option, + } + + // --- Core Functions --- + + generate: func( + inputs: list, + config: config + ) -> result; + + rerank: func( + query: string, + documents: list, + config: config + ) -> result; +} + +world embed-library { + export embed; +} \ No newline at end of file diff --git a/embed/openai/wit/deps/wasi:io/error.wit b/embed/openai/wit/deps/wasi:io/error.wit new file mode 100644 index 000000000..97c606877 --- /dev/null +++ b/embed/openai/wit/deps/wasi:io/error.wit @@ -0,0 +1,34 @@ +package wasi:io@0.2.3; + +@since(version = 0.2.0) +interface error { + /// A resource which represents some error information. + /// + /// The only method provided by this resource is `to-debug-string`, + /// which provides some human-readable information about the error. + /// + /// In the `wasi:io` package, this resource is returned through the + /// `wasi:io/streams/stream-error` type. + /// + /// To provide more specific error information, other interfaces may + /// offer functions to "downcast" this error into more specific types. For example, + /// errors returned from streams derived from filesystem types can be described using + /// the filesystem's own error-code type. This is done using the function + /// `wasi:filesystem/types/filesystem-error-code`, which takes a `borrow` + /// parameter and returns an `option`. + /// + /// The set of functions which can "downcast" an `error` into a more + /// concrete type is open. + @since(version = 0.2.0) + resource error { + /// Returns a string that is suitable to assist humans in debugging + /// this error. + /// + /// WARNING: The returned string should not be consumed mechanically! + /// It may change across platforms, hosts, or other implementation + /// details. Parsing this string is a major platform-compatibility + /// hazard. + @since(version = 0.2.0) + to-debug-string: func() -> string; + } +} diff --git a/embed/openai/wit/deps/wasi:io/poll.wit b/embed/openai/wit/deps/wasi:io/poll.wit new file mode 100644 index 000000000..9bcbe8e03 --- /dev/null +++ b/embed/openai/wit/deps/wasi:io/poll.wit @@ -0,0 +1,47 @@ +package wasi:io@0.2.3; + +/// A poll API intended to let users wait for I/O events on multiple handles +/// at once. +@since(version = 0.2.0) +interface poll { + /// `pollable` represents a single I/O event which may be ready, or not. + @since(version = 0.2.0) + resource pollable { + + /// Return the readiness of a pollable. This function never blocks. + /// + /// Returns `true` when the pollable is ready, and `false` otherwise. + @since(version = 0.2.0) + ready: func() -> bool; + + /// `block` returns immediately if the pollable is ready, and otherwise + /// blocks until ready. + /// + /// This function is equivalent to calling `poll.poll` on a list + /// containing only this pollable. + @since(version = 0.2.0) + block: func(); + } + + /// Poll for completion on a set of pollables. + /// + /// This function takes a list of pollables, which identify I/O sources of + /// interest, and waits until one or more of the events is ready for I/O. + /// + /// The result `list` contains one or more indices of handles in the + /// argument list that is ready for I/O. + /// + /// This function traps if either: + /// - the list is empty, or: + /// - the list contains more elements than can be indexed with a `u32` value. + /// + /// A timeout can be implemented by adding a pollable from the + /// wasi-clocks API to the list. + /// + /// This function does not return a `result`; polling in itself does not + /// do any I/O so it doesn't fail. If any of the I/O sources identified by + /// the pollables has an error, it is indicated by marking the source as + /// being ready for I/O. + @since(version = 0.2.0) + poll: func(in: list>) -> list; +} diff --git a/embed/openai/wit/deps/wasi:io/streams.wit b/embed/openai/wit/deps/wasi:io/streams.wit new file mode 100644 index 000000000..0de084629 --- /dev/null +++ b/embed/openai/wit/deps/wasi:io/streams.wit @@ -0,0 +1,290 @@ +package wasi:io@0.2.3; + +/// WASI I/O is an I/O abstraction API which is currently focused on providing +/// stream types. +/// +/// In the future, the component model is expected to add built-in stream types; +/// when it does, they are expected to subsume this API. +@since(version = 0.2.0) +interface streams { + @since(version = 0.2.0) + use error.{error}; + @since(version = 0.2.0) + use poll.{pollable}; + + /// An error for input-stream and output-stream operations. + @since(version = 0.2.0) + variant stream-error { + /// The last operation (a write or flush) failed before completion. + /// + /// More information is available in the `error` payload. + /// + /// After this, the stream will be closed. All future operations return + /// `stream-error::closed`. + last-operation-failed(error), + /// The stream is closed: no more input will be accepted by the + /// stream. A closed output-stream will return this error on all + /// future operations. + closed + } + + /// An input bytestream. + /// + /// `input-stream`s are *non-blocking* to the extent practical on underlying + /// platforms. I/O operations always return promptly; if fewer bytes are + /// promptly available than requested, they return the number of bytes promptly + /// available, which could even be zero. To wait for data to be available, + /// use the `subscribe` function to obtain a `pollable` which can be polled + /// for using `wasi:io/poll`. + @since(version = 0.2.0) + resource input-stream { + /// Perform a non-blocking read from the stream. + /// + /// When the source of a `read` is binary data, the bytes from the source + /// are returned verbatim. When the source of a `read` is known to the + /// implementation to be text, bytes containing the UTF-8 encoding of the + /// text are returned. + /// + /// This function returns a list of bytes containing the read data, + /// when successful. The returned list will contain up to `len` bytes; + /// it may return fewer than requested, but not more. The list is + /// empty when no bytes are available for reading at this time. The + /// pollable given by `subscribe` will be ready when more bytes are + /// available. + /// + /// This function fails with a `stream-error` when the operation + /// encounters an error, giving `last-operation-failed`, or when the + /// stream is closed, giving `closed`. + /// + /// When the caller gives a `len` of 0, it represents a request to + /// read 0 bytes. If the stream is still open, this call should + /// succeed and return an empty list, or otherwise fail with `closed`. + /// + /// The `len` parameter is a `u64`, which could represent a list of u8 which + /// is not possible to allocate in wasm32, or not desirable to allocate as + /// as a return value by the callee. The callee may return a list of bytes + /// less than `len` in size while more bytes are available for reading. + @since(version = 0.2.0) + read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Read bytes from a stream, after blocking until at least one byte can + /// be read. Except for blocking, behavior is identical to `read`. + @since(version = 0.2.0) + blocking-read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Skip bytes from a stream. Returns number of bytes skipped. + /// + /// Behaves identical to `read`, except instead of returning a list + /// of bytes, returns the number of bytes consumed from the stream. + @since(version = 0.2.0) + skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Skip bytes from a stream, after blocking until at least one byte + /// can be skipped. Except for blocking behavior, identical to `skip`. + @since(version = 0.2.0) + blocking-skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Create a `pollable` which will resolve once either the specified stream + /// has bytes available to read or the other end of the stream has been + /// closed. + /// The created `pollable` is a child resource of the `input-stream`. + /// Implementations may trap if the `input-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } + + + /// An output bytestream. + /// + /// `output-stream`s are *non-blocking* to the extent practical on + /// underlying platforms. Except where specified otherwise, I/O operations also + /// always return promptly, after the number of bytes that can be written + /// promptly, which could even be zero. To wait for the stream to be ready to + /// accept data, the `subscribe` function to obtain a `pollable` which can be + /// polled for using `wasi:io/poll`. + /// + /// Dropping an `output-stream` while there's still an active write in + /// progress may result in the data being lost. Before dropping the stream, + /// be sure to fully flush your writes. + @since(version = 0.2.0) + resource output-stream { + /// Check readiness for writing. This function never blocks. + /// + /// Returns the number of bytes permitted for the next call to `write`, + /// or an error. Calling `write` with more bytes than this function has + /// permitted will trap. + /// + /// When this function returns 0 bytes, the `subscribe` pollable will + /// become ready when this function will report at least 1 byte, or an + /// error. + @since(version = 0.2.0) + check-write: func() -> result; + + /// Perform a write. This function never blocks. + /// + /// When the destination of a `write` is binary data, the bytes from + /// `contents` are written verbatim. When the destination of a `write` is + /// known to the implementation to be text, the bytes of `contents` are + /// transcoded from UTF-8 into the encoding of the destination and then + /// written. + /// + /// Precondition: check-write gave permit of Ok(n) and contents has a + /// length of less than or equal to n. Otherwise, this function will trap. + /// + /// returns Err(closed) without writing if the stream has closed since + /// the last call to check-write provided a permit. + @since(version = 0.2.0) + write: func( + contents: list + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 bytes, and then flush the stream. Block + /// until all of these operations are complete, or an error occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write`, and `flush`, and is implemented with the + /// following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while !contents.is_empty() { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, contents.len()); + /// let (chunk, rest) = contents.split_at(len); + /// this.write(chunk ); // eliding error handling + /// contents = rest; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + @since(version = 0.2.0) + blocking-write-and-flush: func( + contents: list + ) -> result<_, stream-error>; + + /// Request to flush buffered output. This function never blocks. + /// + /// This tells the output-stream that the caller intends any buffered + /// output to be flushed. the output which is expected to be flushed + /// is all that has been passed to `write` prior to this call. + /// + /// Upon calling this function, the `output-stream` will not accept any + /// writes (`check-write` will return `ok(0)`) until the flush has + /// completed. The `subscribe` pollable will become ready when the + /// flush has completed and the stream can accept more writes. + @since(version = 0.2.0) + flush: func() -> result<_, stream-error>; + + /// Request to flush buffered output, and block until flush completes + /// and stream is ready for writing again. + @since(version = 0.2.0) + blocking-flush: func() -> result<_, stream-error>; + + /// Create a `pollable` which will resolve once the output-stream + /// is ready for more writing, or an error has occurred. When this + /// pollable is ready, `check-write` will return `ok(n)` with n>0, or an + /// error. + /// + /// If the stream is closed, this pollable is always ready immediately. + /// + /// The created `pollable` is a child resource of the `output-stream`. + /// Implementations may trap if the `output-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) + subscribe: func() -> pollable; + + /// Write zeroes to a stream. + /// + /// This should be used precisely like `write` with the exact same + /// preconditions (must use check-write first), but instead of + /// passing a list of bytes, you simply pass the number of zero-bytes + /// that should be written. + @since(version = 0.2.0) + write-zeroes: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 zeroes, and then flush the stream. + /// Block until all of these operations are complete, or an error + /// occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write-zeroes`, and `flush`, and is implemented with + /// the following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while num_zeroes != 0 { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, num_zeroes); + /// this.write-zeroes(len); // eliding error handling + /// num_zeroes -= len; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + @since(version = 0.2.0) + blocking-write-zeroes-and-flush: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Read from one stream and write to another. + /// + /// The behavior of splice is equivalent to: + /// 1. calling `check-write` on the `output-stream` + /// 2. calling `read` on the `input-stream` with the smaller of the + /// `check-write` permitted length and the `len` provided to `splice` + /// 3. calling `write` on the `output-stream` with that read data. + /// + /// Any error reported by the call to `check-write`, `read`, or + /// `write` ends the splice and reports that error. + /// + /// This function returns the number of bytes transferred; it may be less + /// than `len`. + @since(version = 0.2.0) + splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + + /// Read from one stream and write to another, with blocking. + /// + /// This is similar to `splice`, except that it blocks until the + /// `output-stream` is ready for writing, and the `input-stream` + /// is ready for reading, before performing the `splice`. + @since(version = 0.2.0) + blocking-splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + } +} diff --git a/embed/openai/wit/deps/wasi:io/world.wit b/embed/openai/wit/deps/wasi:io/world.wit new file mode 100644 index 000000000..f1d2102dc --- /dev/null +++ b/embed/openai/wit/deps/wasi:io/world.wit @@ -0,0 +1,10 @@ +package wasi:io@0.2.3; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import streams; + + @since(version = 0.2.0) + import poll; +} diff --git a/embed/openai/wit/openai.wit b/embed/openai/wit/openai.wit new file mode 100644 index 000000000..1eefcd1b2 --- /dev/null +++ b/embed/openai/wit/openai.wit @@ -0,0 +1,5 @@ +package golem:embed-openai@1.0.0; + +world embed-library { + include golem:embed/embed-library@1.0.0; +} diff --git a/embed/test/wit/deps/golem-embed/golem-embed.wit b/embed/test/wit/deps/golem-embed/golem-embed.wit new file mode 100644 index 000000000..78d040320 --- /dev/null +++ b/embed/test/wit/deps/golem-embed/golem-embed.wit @@ -0,0 +1,129 @@ +package golem:embed@1.0.0; + +interface embed { + // --- Enums --- + + enum task-type { + retrieval-query, + retrieval-document, + semantic-similarity, + classification, + clustering, + question-answering, + fact-verification, + code-retrieval, + } + + enum output-format { + float-array, + binary, + base64, + } + + enum output-dtype { + float-array, + int8, + uint8, + binary, + ubinary, + } + + enum error-code { + invalid-request, + model-not-found, + unsupported, + authentication-failed, + provider-error, + rate-limit-exceeded, + internal-error, + unknown, + } + + // --- Content --- + + record image-url { + url: string, + } + + variant content-part { + text(string), + image(image-url), + } + + // --- Configuration --- + + record kv { + key: string, + value: string, + } + + record config { + model: option, + task-type: option, + dimensions: option, + truncation: option, + output-format: option, + output-dtype: option, + user: option, + provider-options: list, + } + + // --- Embedding Response --- + + record usage { + input-tokens: option, + total-tokens: option, + } + + record embedding { + index: u32, + vector: list, + } + + record embedding-response { + embeddings: list, + usage: option, + model: string, + provider-metadata-json: option, + } + + // --- Rerank Response --- + + record rerank-result { + index: u32, + relevance-score: f32, + document: option, + } + + record rerank-response { + results: list, + usage: option, + model: string, + provider-metadata-json: option, + } + + // --- Error Handling --- + + record error { + code: error-code, + message: string, + provider-error-json: option, + } + + // --- Core Functions --- + + generate: func( + inputs: list, + config: config + ) -> result; + + rerank: func( + query: string, + documents: list, + config: config + ) -> result; +} + +world embed-library { + export embed; +} \ No newline at end of file diff --git a/embed/test/wit/deps/io/error.wit b/embed/test/wit/deps/io/error.wit new file mode 100644 index 000000000..97c606877 --- /dev/null +++ b/embed/test/wit/deps/io/error.wit @@ -0,0 +1,34 @@ +package wasi:io@0.2.3; + +@since(version = 0.2.0) +interface error { + /// A resource which represents some error information. + /// + /// The only method provided by this resource is `to-debug-string`, + /// which provides some human-readable information about the error. + /// + /// In the `wasi:io` package, this resource is returned through the + /// `wasi:io/streams/stream-error` type. + /// + /// To provide more specific error information, other interfaces may + /// offer functions to "downcast" this error into more specific types. For example, + /// errors returned from streams derived from filesystem types can be described using + /// the filesystem's own error-code type. This is done using the function + /// `wasi:filesystem/types/filesystem-error-code`, which takes a `borrow` + /// parameter and returns an `option`. + /// + /// The set of functions which can "downcast" an `error` into a more + /// concrete type is open. + @since(version = 0.2.0) + resource error { + /// Returns a string that is suitable to assist humans in debugging + /// this error. + /// + /// WARNING: The returned string should not be consumed mechanically! + /// It may change across platforms, hosts, or other implementation + /// details. Parsing this string is a major platform-compatibility + /// hazard. + @since(version = 0.2.0) + to-debug-string: func() -> string; + } +} diff --git a/embed/test/wit/deps/io/poll.wit b/embed/test/wit/deps/io/poll.wit new file mode 100644 index 000000000..9bcbe8e03 --- /dev/null +++ b/embed/test/wit/deps/io/poll.wit @@ -0,0 +1,47 @@ +package wasi:io@0.2.3; + +/// A poll API intended to let users wait for I/O events on multiple handles +/// at once. +@since(version = 0.2.0) +interface poll { + /// `pollable` represents a single I/O event which may be ready, or not. + @since(version = 0.2.0) + resource pollable { + + /// Return the readiness of a pollable. This function never blocks. + /// + /// Returns `true` when the pollable is ready, and `false` otherwise. + @since(version = 0.2.0) + ready: func() -> bool; + + /// `block` returns immediately if the pollable is ready, and otherwise + /// blocks until ready. + /// + /// This function is equivalent to calling `poll.poll` on a list + /// containing only this pollable. + @since(version = 0.2.0) + block: func(); + } + + /// Poll for completion on a set of pollables. + /// + /// This function takes a list of pollables, which identify I/O sources of + /// interest, and waits until one or more of the events is ready for I/O. + /// + /// The result `list` contains one or more indices of handles in the + /// argument list that is ready for I/O. + /// + /// This function traps if either: + /// - the list is empty, or: + /// - the list contains more elements than can be indexed with a `u32` value. + /// + /// A timeout can be implemented by adding a pollable from the + /// wasi-clocks API to the list. + /// + /// This function does not return a `result`; polling in itself does not + /// do any I/O so it doesn't fail. If any of the I/O sources identified by + /// the pollables has an error, it is indicated by marking the source as + /// being ready for I/O. + @since(version = 0.2.0) + poll: func(in: list>) -> list; +} diff --git a/embed/test/wit/deps/io/streams.wit b/embed/test/wit/deps/io/streams.wit new file mode 100644 index 000000000..0de084629 --- /dev/null +++ b/embed/test/wit/deps/io/streams.wit @@ -0,0 +1,290 @@ +package wasi:io@0.2.3; + +/// WASI I/O is an I/O abstraction API which is currently focused on providing +/// stream types. +/// +/// In the future, the component model is expected to add built-in stream types; +/// when it does, they are expected to subsume this API. +@since(version = 0.2.0) +interface streams { + @since(version = 0.2.0) + use error.{error}; + @since(version = 0.2.0) + use poll.{pollable}; + + /// An error for input-stream and output-stream operations. + @since(version = 0.2.0) + variant stream-error { + /// The last operation (a write or flush) failed before completion. + /// + /// More information is available in the `error` payload. + /// + /// After this, the stream will be closed. All future operations return + /// `stream-error::closed`. + last-operation-failed(error), + /// The stream is closed: no more input will be accepted by the + /// stream. A closed output-stream will return this error on all + /// future operations. + closed + } + + /// An input bytestream. + /// + /// `input-stream`s are *non-blocking* to the extent practical on underlying + /// platforms. I/O operations always return promptly; if fewer bytes are + /// promptly available than requested, they return the number of bytes promptly + /// available, which could even be zero. To wait for data to be available, + /// use the `subscribe` function to obtain a `pollable` which can be polled + /// for using `wasi:io/poll`. + @since(version = 0.2.0) + resource input-stream { + /// Perform a non-blocking read from the stream. + /// + /// When the source of a `read` is binary data, the bytes from the source + /// are returned verbatim. When the source of a `read` is known to the + /// implementation to be text, bytes containing the UTF-8 encoding of the + /// text are returned. + /// + /// This function returns a list of bytes containing the read data, + /// when successful. The returned list will contain up to `len` bytes; + /// it may return fewer than requested, but not more. The list is + /// empty when no bytes are available for reading at this time. The + /// pollable given by `subscribe` will be ready when more bytes are + /// available. + /// + /// This function fails with a `stream-error` when the operation + /// encounters an error, giving `last-operation-failed`, or when the + /// stream is closed, giving `closed`. + /// + /// When the caller gives a `len` of 0, it represents a request to + /// read 0 bytes. If the stream is still open, this call should + /// succeed and return an empty list, or otherwise fail with `closed`. + /// + /// The `len` parameter is a `u64`, which could represent a list of u8 which + /// is not possible to allocate in wasm32, or not desirable to allocate as + /// as a return value by the callee. The callee may return a list of bytes + /// less than `len` in size while more bytes are available for reading. + @since(version = 0.2.0) + read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Read bytes from a stream, after blocking until at least one byte can + /// be read. Except for blocking, behavior is identical to `read`. + @since(version = 0.2.0) + blocking-read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Skip bytes from a stream. Returns number of bytes skipped. + /// + /// Behaves identical to `read`, except instead of returning a list + /// of bytes, returns the number of bytes consumed from the stream. + @since(version = 0.2.0) + skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Skip bytes from a stream, after blocking until at least one byte + /// can be skipped. Except for blocking behavior, identical to `skip`. + @since(version = 0.2.0) + blocking-skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Create a `pollable` which will resolve once either the specified stream + /// has bytes available to read or the other end of the stream has been + /// closed. + /// The created `pollable` is a child resource of the `input-stream`. + /// Implementations may trap if the `input-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } + + + /// An output bytestream. + /// + /// `output-stream`s are *non-blocking* to the extent practical on + /// underlying platforms. Except where specified otherwise, I/O operations also + /// always return promptly, after the number of bytes that can be written + /// promptly, which could even be zero. To wait for the stream to be ready to + /// accept data, the `subscribe` function to obtain a `pollable` which can be + /// polled for using `wasi:io/poll`. + /// + /// Dropping an `output-stream` while there's still an active write in + /// progress may result in the data being lost. Before dropping the stream, + /// be sure to fully flush your writes. + @since(version = 0.2.0) + resource output-stream { + /// Check readiness for writing. This function never blocks. + /// + /// Returns the number of bytes permitted for the next call to `write`, + /// or an error. Calling `write` with more bytes than this function has + /// permitted will trap. + /// + /// When this function returns 0 bytes, the `subscribe` pollable will + /// become ready when this function will report at least 1 byte, or an + /// error. + @since(version = 0.2.0) + check-write: func() -> result; + + /// Perform a write. This function never blocks. + /// + /// When the destination of a `write` is binary data, the bytes from + /// `contents` are written verbatim. When the destination of a `write` is + /// known to the implementation to be text, the bytes of `contents` are + /// transcoded from UTF-8 into the encoding of the destination and then + /// written. + /// + /// Precondition: check-write gave permit of Ok(n) and contents has a + /// length of less than or equal to n. Otherwise, this function will trap. + /// + /// returns Err(closed) without writing if the stream has closed since + /// the last call to check-write provided a permit. + @since(version = 0.2.0) + write: func( + contents: list + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 bytes, and then flush the stream. Block + /// until all of these operations are complete, or an error occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write`, and `flush`, and is implemented with the + /// following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while !contents.is_empty() { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, contents.len()); + /// let (chunk, rest) = contents.split_at(len); + /// this.write(chunk ); // eliding error handling + /// contents = rest; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + @since(version = 0.2.0) + blocking-write-and-flush: func( + contents: list + ) -> result<_, stream-error>; + + /// Request to flush buffered output. This function never blocks. + /// + /// This tells the output-stream that the caller intends any buffered + /// output to be flushed. the output which is expected to be flushed + /// is all that has been passed to `write` prior to this call. + /// + /// Upon calling this function, the `output-stream` will not accept any + /// writes (`check-write` will return `ok(0)`) until the flush has + /// completed. The `subscribe` pollable will become ready when the + /// flush has completed and the stream can accept more writes. + @since(version = 0.2.0) + flush: func() -> result<_, stream-error>; + + /// Request to flush buffered output, and block until flush completes + /// and stream is ready for writing again. + @since(version = 0.2.0) + blocking-flush: func() -> result<_, stream-error>; + + /// Create a `pollable` which will resolve once the output-stream + /// is ready for more writing, or an error has occurred. When this + /// pollable is ready, `check-write` will return `ok(n)` with n>0, or an + /// error. + /// + /// If the stream is closed, this pollable is always ready immediately. + /// + /// The created `pollable` is a child resource of the `output-stream`. + /// Implementations may trap if the `output-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) + subscribe: func() -> pollable; + + /// Write zeroes to a stream. + /// + /// This should be used precisely like `write` with the exact same + /// preconditions (must use check-write first), but instead of + /// passing a list of bytes, you simply pass the number of zero-bytes + /// that should be written. + @since(version = 0.2.0) + write-zeroes: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 zeroes, and then flush the stream. + /// Block until all of these operations are complete, or an error + /// occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write-zeroes`, and `flush`, and is implemented with + /// the following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while num_zeroes != 0 { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, num_zeroes); + /// this.write-zeroes(len); // eliding error handling + /// num_zeroes -= len; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + @since(version = 0.2.0) + blocking-write-zeroes-and-flush: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Read from one stream and write to another. + /// + /// The behavior of splice is equivalent to: + /// 1. calling `check-write` on the `output-stream` + /// 2. calling `read` on the `input-stream` with the smaller of the + /// `check-write` permitted length and the `len` provided to `splice` + /// 3. calling `write` on the `output-stream` with that read data. + /// + /// Any error reported by the call to `check-write`, `read`, or + /// `write` ends the splice and reports that error. + /// + /// This function returns the number of bytes transferred; it may be less + /// than `len`. + @since(version = 0.2.0) + splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + + /// Read from one stream and write to another, with blocking. + /// + /// This is similar to `splice`, except that it blocks until the + /// `output-stream` is ready for writing, and the `input-stream` + /// is ready for reading, before performing the `splice`. + @since(version = 0.2.0) + blocking-splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + } +} diff --git a/embed/test/wit/deps/io/world.wit b/embed/test/wit/deps/io/world.wit new file mode 100644 index 000000000..f1d2102dc --- /dev/null +++ b/embed/test/wit/deps/io/world.wit @@ -0,0 +1,10 @@ +package wasi:io@0.2.3; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import streams; + + @since(version = 0.2.0) + import poll; +} diff --git a/embed/voyageai/Cargo.toml b/embed/voyageai/Cargo.toml new file mode 100644 index 000000000..e017ed780 --- /dev/null +++ b/embed/voyageai/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "golem-embed-voyageai" +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 VoyageAI embeding and reranking APIs, with special support for Golem Cloud" + + +[lib] +path = "src/lib.rs" +crate-type = ["cdylib"] + +[features] +default = ["durability"] +durability = ["golem-rust/durability", "golem-embed/durability"] + + +[dependencies] +golem-embed = { path = "../embed", version = "0.0.0", default-features = false } +golem-rust = { workspace = true } +log = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +base64 = { workspace = true } +wit-bindgen-rt = { workspace = true } +bytemuck = "1.23.0" + + +[package.metadata.component] +package = "golem:embed-voyageai" + +[package.metadata.component.bindings] +generate_unused_types = true + +[package.metadata.component.bindings.with] +"golem:embed/embed@1.0.0" = "golem_embed::golem::embed::embed" + +[package.metadata.component.target] +path = "wit" + +[package.metadata.component.target.dependencies] +"golem:embed" = { path = "wit/deps/golem-embed" } +"wasi:io" = { path = "wit/deps/wasi:io" } diff --git a/embed/voyageai/src/bindings.rs b/embed/voyageai/src/bindings.rs new file mode 100644 index 000000000..8016990a7 --- /dev/null +++ b/embed/voyageai/src/bindings.rs @@ -0,0 +1,43 @@ +// Generated by `wit-bindgen` 0.41.0. DO NOT EDIT! +// Options used: +// * runtime_path: "wit_bindgen_rt" +// * with "golem:embed/embed@1.0.0" = "golem_embed::golem::embed::embed" +// * generate_unused_types +use golem_embed::golem::embed::embed as __with_name0; +#[cfg(target_arch = "wasm32")] +#[unsafe( + link_section = "component-type:wit-bindgen:0.41.0:golem:embed-voyageai@1.0.0:embed-library:encoded world" +)] +#[doc(hidden)] +#[allow(clippy::octal_escapes)] +pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 1265] = *b"\ +\0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07\xed\x08\x01A\x02\x01\ +A\x02\x01B/\x01m\x08\x0fretrieval-query\x12retrieval-document\x13semantic-simila\ +rity\x0eclassification\x0aclustering\x12question-answering\x11fact-verification\x0e\ +code-retrieval\x04\0\x09task-type\x03\0\0\x01m\x03\x0bfloat-array\x06binary\x06b\ +ase64\x04\0\x0doutput-format\x03\0\x02\x01m\x05\x0bfloat-array\x04int8\x05uint8\x06\ +binary\x07ubinary\x04\0\x0coutput-dtype\x03\0\x04\x01m\x08\x0finvalid-request\x0f\ +model-not-found\x0bunsupported\x15authentication-failed\x0eprovider-error\x13rat\ +e-limit-exceeded\x0einternal-error\x07unknown\x04\0\x0aerror-code\x03\0\x06\x01r\ +\x01\x03urls\x04\0\x09image-url\x03\0\x08\x01q\x02\x04text\x01s\0\x05image\x01\x09\ +\0\x04\0\x0ccontent-part\x03\0\x0a\x01r\x02\x03keys\x05values\x04\0\x02kv\x03\0\x0c\ +\x01ks\x01k\x01\x01ky\x01k\x7f\x01k\x03\x01k\x05\x01p\x0d\x01r\x08\x05model\x0e\x09\ +task-type\x0f\x0adimensions\x10\x0atruncation\x11\x0doutput-format\x12\x0coutput\ +-dtype\x13\x04user\x0e\x10provider-options\x14\x04\0\x06config\x03\0\x15\x01r\x02\ +\x0cinput-tokens\x10\x0ctotal-tokens\x10\x04\0\x05usage\x03\0\x17\x01pv\x01r\x02\ +\x05indexy\x06vector\x19\x04\0\x09embedding\x03\0\x1a\x01p\x1b\x01k\x18\x01r\x04\ +\x0aembeddings\x1c\x05usage\x1d\x05models\x16provider-metadata-json\x0e\x04\0\x12\ +embedding-response\x03\0\x1e\x01r\x03\x05indexy\x0frelevance-scorev\x08document\x0e\ +\x04\0\x0drerank-result\x03\0\x20\x01p!\x01r\x04\x07results\"\x05usage\x1d\x05mo\ +dels\x16provider-metadata-json\x0e\x04\0\x0frerank-response\x03\0#\x01r\x03\x04c\ +ode\x07\x07messages\x13provider-error-json\x0e\x04\0\x05error\x03\0%\x01p\x0b\x01\ +j\x01\x1f\x01&\x01@\x02\x06inputs'\x06config\x16\0(\x04\0\x08generate\x01)\x01ps\ +\x01j\x01$\x01&\x01@\x03\x05querys\x09documents*\x06config\x16\0+\x04\0\x06reran\ +k\x01,\x04\0\x17golem:embed/embed@1.0.0\x05\0\x04\0(golem:embed-voyageai/embed-l\ +ibrary@1.0.0\x04\0\x0b\x13\x01\0\x0dembed-library\x03\0\0\0G\x09producers\x01\x0c\ +processed-by\x02\x0dwit-component\x070.227.1\x10wit-bindgen-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/embed/voyageai/src/client.rs b/embed/voyageai/src/client.rs new file mode 100644 index 000000000..45c2654a6 --- /dev/null +++ b/embed/voyageai/src/client.rs @@ -0,0 +1,214 @@ +use std::fmt::Debug; + +use golem_embed::{ + error::{error_code_from_status, from_reqwest_error}, + golem::embed::embed::Error, +}; +use log::trace; +use reqwest::{Client, Method, Response}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +const BASE_URL: &str = "https://api.voyageai.com"; + +/// The VoyageAI API client for creating embeddings and reranking. +/// +/// Based on https://docs.voyageai.com/reference/embeddings-api +/// and https://docs.voyageai.com/reference/reranker-api +pub struct VoyageAIApi { + voyageai_api_key: String, + client: Client, +} + +impl VoyageAIApi { + pub fn new(voyageai_api_key: String) -> Self { + let client = Client::builder() + .build() + .expect("Failed to initialize HTTP client"); + Self { + voyageai_api_key, + client, + } + } + + pub fn generate_embedding( + &self, + request: EmbeddingRequest, + ) -> Result { + trace!("Sending embedding request to VoyageAI API: {request:?}"); + let response = self + .client + .request(Method::POST, format!("{BASE_URL}/v1/embeddings")) + .bearer_auth(&self.voyageai_api_key) + .json(&request) + .send() + .map_err(|err| from_reqwest_error("Embedding request failed", err))?; + parse_response::(response) + } + + pub fn rerank(&self, request: RerankRequest) -> Result { + trace!("Sending rerank request to VoyageAI API: {request:?}"); + let response = self + .client + .request(Method::POST, format!("{BASE_URL}/v1/rerank")) + .bearer_auth(&self.voyageai_api_key) + .json(&request) + .send() + .map_err(|err| from_reqwest_error("Rerank request failed", err))?; + parse_response::(response) + } +} + +fn parse_response(response: Response) -> Result { + let status = response.status(); + let response_text = response + .text() + .map_err(|err| from_reqwest_error("Failed to read response body", err))?; + + if !status.is_success() { + if let Ok(error_response) = serde_json::from_str::(&response_text) { + return Err(Error { + code: error_code_from_status(status), + message: error_response.error.message, + provider_error_json: Some(response_text), + }); + } + + if let Ok(detail_error) = serde_json::from_str::(&response_text) { + if let Some(detail) = detail_error.get("detail").and_then(|d| d.as_str()) { + return Err(Error { + code: error_code_from_status(status), + message: detail.to_string(), + provider_error_json: Some(response_text), + }); + } + } + + return Err(Error { + code: error_code_from_status(status), + message: format!("Request failed with status {status}: {response_text}"), + provider_error_json: Some(response_text), + }); + } + + match serde_json::from_str::(&response_text) { + Ok(response_data) => { + trace!("Response from VoyageAI API: {response_data:?}"); + Ok(response_data) + } + Err(error) => { + trace!("Error parsing response: {error:?}"); + Err(Error { + code: error_code_from_status(status), + message: format!("Failed to decode response body: {response_text}"), + provider_error_json: Some(error.to_string()), + }) + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct EmbeddingRequest { + pub input: Vec, + pub model: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub input_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub truncation: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub output_dimension: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub output_dtype: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub encoding_format: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum EncodingFormat { + Base64, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum InputType { + #[serde(rename = "document")] + Document, + #[serde(rename = "query")] + Query, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "lowercase")] +pub enum OutputDtype { + Float, + Int8, + Uint8, + Binary, + Ubinary, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct EmbeddingResponse { + pub object: String, + pub data: Vec, + pub model: String, + pub usage: EmbeddingUsage, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct EmbeddingData { + pub object: String, + pub embedding: Vec, + pub index: u32, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct EmbeddingUsage { + pub total_tokens: u32, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct RerankRequest { + pub query: String, + pub documents: Vec, + pub model: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub top_k: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub return_documents: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub truncation: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct RerankResponse { + pub object: String, + pub data: Vec, + pub model: String, + pub usage: RerankUsage, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct RerankResult { + pub index: u32, + pub relevance_score: f32, + #[serde(skip_serializing_if = "Option::is_none")] + pub document: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct RerankUsage { + pub total_tokens: u32, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct VoyageAIError { + pub error: VoyageAIErrorDetails, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct VoyageAIErrorDetails { + pub message: String, + #[serde(rename = "type")] + pub error_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub code: Option, +} diff --git a/embed/voyageai/src/conversitions.rs b/embed/voyageai/src/conversitions.rs new file mode 100644 index 000000000..a998bb96d --- /dev/null +++ b/embed/voyageai/src/conversitions.rs @@ -0,0 +1,189 @@ +use golem_embed::{ + error::unsupported, + golem::embed::embed::{ + Config, ContentPart, Embedding, EmbeddingResponse as GolemEmbeddingResponse, Error, + OutputDtype as GolemOutputDtype, OutputFormat as GolemOutputFormat, + RerankResponse as GolemRerankResponse, RerankResult as GolemRerankResult, TaskType, Usage, + }, +}; + +use crate::client::{ + EmbeddingRequest, EmbeddingResponse, EncodingFormat, InputType, OutputDtype, RerankRequest, + RerankResponse, +}; + +pub fn create_embedding_request( + inputs: Vec, + config: Config, +) -> Result { + let mut text_inputs = Vec::new(); + + for input in inputs { + match input { + ContentPart::Text(text) => text_inputs.push(text), + ContentPart::Image(_) => { + return Err(unsupported( + "VoyageAI text embeddings do not support image inputs. Use multimodal embeddings instead.", + )); + } + } + } + + let model = config + .model + .unwrap_or_else(|| "voyage-3.5-lite".to_string()); + + let input_type = match config.task_type { + Some(TaskType::RetrievalQuery) => Some(InputType::Query), + Some(TaskType::RetrievalDocument) => Some(InputType::Document), + _ => return Err(unsupported("Unsupported task type")), + }; + + let output_dtype = match config.output_dtype { + Some(GolemOutputDtype::FloatArray) => Some(OutputDtype::Float), + Some(GolemOutputDtype::Int8) => Some(OutputDtype::Int8), + Some(GolemOutputDtype::Uint8) => Some(OutputDtype::Uint8), + Some(GolemOutputDtype::Binary) => Some(OutputDtype::Binary), + Some(GolemOutputDtype::Ubinary) => Some(OutputDtype::Ubinary), + _ => None, + }; + + let encoding_format = match config.output_format { + Some(GolemOutputFormat::Base64) => Some(EncodingFormat::Base64), + _ => None, + }; + + Ok(EmbeddingRequest { + input: text_inputs, + model, + input_type, + truncation: config.truncation, + output_dimension: config.dimensions, + output_dtype, + encoding_format, + }) +} + +pub fn process_embedding_response( + response: EmbeddingResponse, +) -> Result { + let mut embeddings = Vec::new(); + + for data in response.data { + embeddings.push(Embedding { + index: data.index, + vector: data.embedding, + }); + } + + let usage = Usage { + input_tokens: None, + total_tokens: Some(response.usage.total_tokens), + }; + + Ok(GolemEmbeddingResponse { + embeddings, + usage: Some(usage), + model: response.model, + provider_metadata_json: None, + }) +} + +pub fn create_rerank_request( + query: String, + documents: Vec, + config: Config, +) -> Result { + let model = config.model.unwrap_or_else(|| "rerank-2-lite".to_string()); + + Ok(RerankRequest { + query, + documents, + model, + top_k: None, + return_documents: Some(true), + truncation: config.truncation, + }) +} + +pub fn process_rerank_response(response: RerankResponse) -> Result { + let mut results = Vec::new(); + for result in response.data { + results.push(GolemRerankResult { + index: result.index, + relevance_score: result.relevance_score, + document: result.document, + }); + } + + let usage = Usage { + input_tokens: Some(response.usage.total_tokens), + total_tokens: Some(response.usage.total_tokens), + }; + + Ok(GolemRerankResponse { + results, + usage: Some(usage), + model: response.model, + provider_metadata_json: None, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use golem_embed::golem::embed::embed::{Config, ContentPart, TaskType}; + + #[test] + fn test_create_embedding_request() { + let inputs = vec![ + ContentPart::Text("Hello world".to_string()), + ContentPart::Text("How are you?".to_string()), + ]; + + let config = Config { + model: Some("voyage-3.5-lite".to_string()), + task_type: Some(TaskType::RetrievalDocument), + dimensions: Some(1024), + truncation: Some(true), + output_format: None, + output_dtype: None, + user: None, + provider_options: vec![], + }; + + let request = create_embedding_request(inputs.clone(), config.clone()); + match &request { + Ok(_request) => {} + Err(_err) => {} + }; + assert!(request.is_ok()); + } + + #[test] + fn test_create_rerank_request() { + let query = "What is AI?".to_string(); + let documents = vec![ + "AI is artificial intelligence".to_string(), + "Machine learning is a subset of AI".to_string(), + ]; + + let config = Config { + model: Some("rerank-2-lite".to_string()), + task_type: None, + dimensions: None, + truncation: Some(false), + output_format: None, + output_dtype: None, + user: None, + provider_options: vec![], + }; + + let request = create_rerank_request(query, documents, config); + match &request { + Ok(_request) => {} + Err(_err) => {} + }; + assert!(request.is_ok()); + } +} diff --git a/embed/voyageai/src/lib.rs b/embed/voyageai/src/lib.rs new file mode 100644 index 000000000..73d28eeba --- /dev/null +++ b/embed/voyageai/src/lib.rs @@ -0,0 +1,84 @@ +use golem_embed::{ + config::with_config_key, + durability::{DurableEmbed, ExtendedGuest}, + golem::embed::embed::{Config, ContentPart, EmbeddingResponse, Error, Guest, RerankResponse}, + LOGGING_STATE, +}; + +use crate::{ + client::VoyageAIApi, + conversitions::{ + create_embedding_request, create_rerank_request, process_embedding_response, + process_rerank_response, + }, +}; + +mod client; +mod conversitions; + +struct VoyageAIApiComponent; + +impl VoyageAIApiComponent { + const ENV_VAR_NAME: &'static str = "VOYAGEAI_API_KEY"; + + fn embeddings( + client: VoyageAIApi, + inputs: Vec, + config: Config, + ) -> Result { + let request = create_embedding_request(inputs, config.clone()); + match request { + Ok(request) => match client.generate_embedding(request) { + Ok(response) => process_embedding_response(response), + Err(err) => Err(err), + }, + Err(err) => Err(err), + } + } + + fn rerank( + client: VoyageAIApi, + query: String, + documents: Vec, + config: Config, + ) -> Result { + let request = create_rerank_request(query, documents, config); + match request { + Ok(request) => match client.rerank(request) { + Ok(response) => process_rerank_response(response), + Err(err) => Err(err), + }, + Err(err) => Err(err), + } + } +} + +impl Guest for VoyageAIApiComponent { + fn generate(inputs: Vec, config: Config) -> Result { + LOGGING_STATE.with_borrow_mut(|state| state.init()); + + with_config_key(Self::ENV_VAR_NAME, Err, |voyageai_api_key| { + let client = VoyageAIApi::new(voyageai_api_key); + Self::embeddings(client, inputs, config) + }) + } + + fn rerank( + query: String, + documents: Vec, + config: Config, + ) -> Result { + LOGGING_STATE.with_borrow_mut(|state| state.init()); + + with_config_key(Self::ENV_VAR_NAME, Err, |voyageai_api_key| { + let client = VoyageAIApi::new(voyageai_api_key); + Self::rerank(client, query, documents, config) + }) + } +} + +impl ExtendedGuest for VoyageAIApiComponent {} + +type DurableVoyageAIApiComponent = DurableEmbed; + +golem_embed::export_embed!(DurableVoyageAIApiComponent with_types_in golem_embed); diff --git a/embed/voyageai/wit/deps/golem-embed/golem-embed.wit b/embed/voyageai/wit/deps/golem-embed/golem-embed.wit new file mode 100644 index 000000000..78d040320 --- /dev/null +++ b/embed/voyageai/wit/deps/golem-embed/golem-embed.wit @@ -0,0 +1,129 @@ +package golem:embed@1.0.0; + +interface embed { + // --- Enums --- + + enum task-type { + retrieval-query, + retrieval-document, + semantic-similarity, + classification, + clustering, + question-answering, + fact-verification, + code-retrieval, + } + + enum output-format { + float-array, + binary, + base64, + } + + enum output-dtype { + float-array, + int8, + uint8, + binary, + ubinary, + } + + enum error-code { + invalid-request, + model-not-found, + unsupported, + authentication-failed, + provider-error, + rate-limit-exceeded, + internal-error, + unknown, + } + + // --- Content --- + + record image-url { + url: string, + } + + variant content-part { + text(string), + image(image-url), + } + + // --- Configuration --- + + record kv { + key: string, + value: string, + } + + record config { + model: option, + task-type: option, + dimensions: option, + truncation: option, + output-format: option, + output-dtype: option, + user: option, + provider-options: list, + } + + // --- Embedding Response --- + + record usage { + input-tokens: option, + total-tokens: option, + } + + record embedding { + index: u32, + vector: list, + } + + record embedding-response { + embeddings: list, + usage: option, + model: string, + provider-metadata-json: option, + } + + // --- Rerank Response --- + + record rerank-result { + index: u32, + relevance-score: f32, + document: option, + } + + record rerank-response { + results: list, + usage: option, + model: string, + provider-metadata-json: option, + } + + // --- Error Handling --- + + record error { + code: error-code, + message: string, + provider-error-json: option, + } + + // --- Core Functions --- + + generate: func( + inputs: list, + config: config + ) -> result; + + rerank: func( + query: string, + documents: list, + config: config + ) -> result; +} + +world embed-library { + export embed; +} \ No newline at end of file diff --git a/embed/voyageai/wit/deps/wasi:io/error.wit b/embed/voyageai/wit/deps/wasi:io/error.wit new file mode 100644 index 000000000..97c606877 --- /dev/null +++ b/embed/voyageai/wit/deps/wasi:io/error.wit @@ -0,0 +1,34 @@ +package wasi:io@0.2.3; + +@since(version = 0.2.0) +interface error { + /// A resource which represents some error information. + /// + /// The only method provided by this resource is `to-debug-string`, + /// which provides some human-readable information about the error. + /// + /// In the `wasi:io` package, this resource is returned through the + /// `wasi:io/streams/stream-error` type. + /// + /// To provide more specific error information, other interfaces may + /// offer functions to "downcast" this error into more specific types. For example, + /// errors returned from streams derived from filesystem types can be described using + /// the filesystem's own error-code type. This is done using the function + /// `wasi:filesystem/types/filesystem-error-code`, which takes a `borrow` + /// parameter and returns an `option`. + /// + /// The set of functions which can "downcast" an `error` into a more + /// concrete type is open. + @since(version = 0.2.0) + resource error { + /// Returns a string that is suitable to assist humans in debugging + /// this error. + /// + /// WARNING: The returned string should not be consumed mechanically! + /// It may change across platforms, hosts, or other implementation + /// details. Parsing this string is a major platform-compatibility + /// hazard. + @since(version = 0.2.0) + to-debug-string: func() -> string; + } +} diff --git a/embed/voyageai/wit/deps/wasi:io/poll.wit b/embed/voyageai/wit/deps/wasi:io/poll.wit new file mode 100644 index 000000000..9bcbe8e03 --- /dev/null +++ b/embed/voyageai/wit/deps/wasi:io/poll.wit @@ -0,0 +1,47 @@ +package wasi:io@0.2.3; + +/// A poll API intended to let users wait for I/O events on multiple handles +/// at once. +@since(version = 0.2.0) +interface poll { + /// `pollable` represents a single I/O event which may be ready, or not. + @since(version = 0.2.0) + resource pollable { + + /// Return the readiness of a pollable. This function never blocks. + /// + /// Returns `true` when the pollable is ready, and `false` otherwise. + @since(version = 0.2.0) + ready: func() -> bool; + + /// `block` returns immediately if the pollable is ready, and otherwise + /// blocks until ready. + /// + /// This function is equivalent to calling `poll.poll` on a list + /// containing only this pollable. + @since(version = 0.2.0) + block: func(); + } + + /// Poll for completion on a set of pollables. + /// + /// This function takes a list of pollables, which identify I/O sources of + /// interest, and waits until one or more of the events is ready for I/O. + /// + /// The result `list` contains one or more indices of handles in the + /// argument list that is ready for I/O. + /// + /// This function traps if either: + /// - the list is empty, or: + /// - the list contains more elements than can be indexed with a `u32` value. + /// + /// A timeout can be implemented by adding a pollable from the + /// wasi-clocks API to the list. + /// + /// This function does not return a `result`; polling in itself does not + /// do any I/O so it doesn't fail. If any of the I/O sources identified by + /// the pollables has an error, it is indicated by marking the source as + /// being ready for I/O. + @since(version = 0.2.0) + poll: func(in: list>) -> list; +} diff --git a/embed/voyageai/wit/deps/wasi:io/streams.wit b/embed/voyageai/wit/deps/wasi:io/streams.wit new file mode 100644 index 000000000..0de084629 --- /dev/null +++ b/embed/voyageai/wit/deps/wasi:io/streams.wit @@ -0,0 +1,290 @@ +package wasi:io@0.2.3; + +/// WASI I/O is an I/O abstraction API which is currently focused on providing +/// stream types. +/// +/// In the future, the component model is expected to add built-in stream types; +/// when it does, they are expected to subsume this API. +@since(version = 0.2.0) +interface streams { + @since(version = 0.2.0) + use error.{error}; + @since(version = 0.2.0) + use poll.{pollable}; + + /// An error for input-stream and output-stream operations. + @since(version = 0.2.0) + variant stream-error { + /// The last operation (a write or flush) failed before completion. + /// + /// More information is available in the `error` payload. + /// + /// After this, the stream will be closed. All future operations return + /// `stream-error::closed`. + last-operation-failed(error), + /// The stream is closed: no more input will be accepted by the + /// stream. A closed output-stream will return this error on all + /// future operations. + closed + } + + /// An input bytestream. + /// + /// `input-stream`s are *non-blocking* to the extent practical on underlying + /// platforms. I/O operations always return promptly; if fewer bytes are + /// promptly available than requested, they return the number of bytes promptly + /// available, which could even be zero. To wait for data to be available, + /// use the `subscribe` function to obtain a `pollable` which can be polled + /// for using `wasi:io/poll`. + @since(version = 0.2.0) + resource input-stream { + /// Perform a non-blocking read from the stream. + /// + /// When the source of a `read` is binary data, the bytes from the source + /// are returned verbatim. When the source of a `read` is known to the + /// implementation to be text, bytes containing the UTF-8 encoding of the + /// text are returned. + /// + /// This function returns a list of bytes containing the read data, + /// when successful. The returned list will contain up to `len` bytes; + /// it may return fewer than requested, but not more. The list is + /// empty when no bytes are available for reading at this time. The + /// pollable given by `subscribe` will be ready when more bytes are + /// available. + /// + /// This function fails with a `stream-error` when the operation + /// encounters an error, giving `last-operation-failed`, or when the + /// stream is closed, giving `closed`. + /// + /// When the caller gives a `len` of 0, it represents a request to + /// read 0 bytes. If the stream is still open, this call should + /// succeed and return an empty list, or otherwise fail with `closed`. + /// + /// The `len` parameter is a `u64`, which could represent a list of u8 which + /// is not possible to allocate in wasm32, or not desirable to allocate as + /// as a return value by the callee. The callee may return a list of bytes + /// less than `len` in size while more bytes are available for reading. + @since(version = 0.2.0) + read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Read bytes from a stream, after blocking until at least one byte can + /// be read. Except for blocking, behavior is identical to `read`. + @since(version = 0.2.0) + blocking-read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Skip bytes from a stream. Returns number of bytes skipped. + /// + /// Behaves identical to `read`, except instead of returning a list + /// of bytes, returns the number of bytes consumed from the stream. + @since(version = 0.2.0) + skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Skip bytes from a stream, after blocking until at least one byte + /// can be skipped. Except for blocking behavior, identical to `skip`. + @since(version = 0.2.0) + blocking-skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Create a `pollable` which will resolve once either the specified stream + /// has bytes available to read or the other end of the stream has been + /// closed. + /// The created `pollable` is a child resource of the `input-stream`. + /// Implementations may trap if the `input-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } + + + /// An output bytestream. + /// + /// `output-stream`s are *non-blocking* to the extent practical on + /// underlying platforms. Except where specified otherwise, I/O operations also + /// always return promptly, after the number of bytes that can be written + /// promptly, which could even be zero. To wait for the stream to be ready to + /// accept data, the `subscribe` function to obtain a `pollable` which can be + /// polled for using `wasi:io/poll`. + /// + /// Dropping an `output-stream` while there's still an active write in + /// progress may result in the data being lost. Before dropping the stream, + /// be sure to fully flush your writes. + @since(version = 0.2.0) + resource output-stream { + /// Check readiness for writing. This function never blocks. + /// + /// Returns the number of bytes permitted for the next call to `write`, + /// or an error. Calling `write` with more bytes than this function has + /// permitted will trap. + /// + /// When this function returns 0 bytes, the `subscribe` pollable will + /// become ready when this function will report at least 1 byte, or an + /// error. + @since(version = 0.2.0) + check-write: func() -> result; + + /// Perform a write. This function never blocks. + /// + /// When the destination of a `write` is binary data, the bytes from + /// `contents` are written verbatim. When the destination of a `write` is + /// known to the implementation to be text, the bytes of `contents` are + /// transcoded from UTF-8 into the encoding of the destination and then + /// written. + /// + /// Precondition: check-write gave permit of Ok(n) and contents has a + /// length of less than or equal to n. Otherwise, this function will trap. + /// + /// returns Err(closed) without writing if the stream has closed since + /// the last call to check-write provided a permit. + @since(version = 0.2.0) + write: func( + contents: list + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 bytes, and then flush the stream. Block + /// until all of these operations are complete, or an error occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write`, and `flush`, and is implemented with the + /// following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while !contents.is_empty() { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, contents.len()); + /// let (chunk, rest) = contents.split_at(len); + /// this.write(chunk ); // eliding error handling + /// contents = rest; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + @since(version = 0.2.0) + blocking-write-and-flush: func( + contents: list + ) -> result<_, stream-error>; + + /// Request to flush buffered output. This function never blocks. + /// + /// This tells the output-stream that the caller intends any buffered + /// output to be flushed. the output which is expected to be flushed + /// is all that has been passed to `write` prior to this call. + /// + /// Upon calling this function, the `output-stream` will not accept any + /// writes (`check-write` will return `ok(0)`) until the flush has + /// completed. The `subscribe` pollable will become ready when the + /// flush has completed and the stream can accept more writes. + @since(version = 0.2.0) + flush: func() -> result<_, stream-error>; + + /// Request to flush buffered output, and block until flush completes + /// and stream is ready for writing again. + @since(version = 0.2.0) + blocking-flush: func() -> result<_, stream-error>; + + /// Create a `pollable` which will resolve once the output-stream + /// is ready for more writing, or an error has occurred. When this + /// pollable is ready, `check-write` will return `ok(n)` with n>0, or an + /// error. + /// + /// If the stream is closed, this pollable is always ready immediately. + /// + /// The created `pollable` is a child resource of the `output-stream`. + /// Implementations may trap if the `output-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) + subscribe: func() -> pollable; + + /// Write zeroes to a stream. + /// + /// This should be used precisely like `write` with the exact same + /// preconditions (must use check-write first), but instead of + /// passing a list of bytes, you simply pass the number of zero-bytes + /// that should be written. + @since(version = 0.2.0) + write-zeroes: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 zeroes, and then flush the stream. + /// Block until all of these operations are complete, or an error + /// occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write-zeroes`, and `flush`, and is implemented with + /// the following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while num_zeroes != 0 { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, num_zeroes); + /// this.write-zeroes(len); // eliding error handling + /// num_zeroes -= len; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + @since(version = 0.2.0) + blocking-write-zeroes-and-flush: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Read from one stream and write to another. + /// + /// The behavior of splice is equivalent to: + /// 1. calling `check-write` on the `output-stream` + /// 2. calling `read` on the `input-stream` with the smaller of the + /// `check-write` permitted length and the `len` provided to `splice` + /// 3. calling `write` on the `output-stream` with that read data. + /// + /// Any error reported by the call to `check-write`, `read`, or + /// `write` ends the splice and reports that error. + /// + /// This function returns the number of bytes transferred; it may be less + /// than `len`. + @since(version = 0.2.0) + splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + + /// Read from one stream and write to another, with blocking. + /// + /// This is similar to `splice`, except that it blocks until the + /// `output-stream` is ready for writing, and the `input-stream` + /// is ready for reading, before performing the `splice`. + @since(version = 0.2.0) + blocking-splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + } +} diff --git a/embed/voyageai/wit/deps/wasi:io/world.wit b/embed/voyageai/wit/deps/wasi:io/world.wit new file mode 100644 index 000000000..f1d2102dc --- /dev/null +++ b/embed/voyageai/wit/deps/wasi:io/world.wit @@ -0,0 +1,10 @@ +package wasi:io@0.2.3; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import streams; + + @since(version = 0.2.0) + import poll; +} diff --git a/embed/voyageai/wit/voyageai.wit b/embed/voyageai/wit/voyageai.wit new file mode 100644 index 000000000..175055391 --- /dev/null +++ b/embed/voyageai/wit/voyageai.wit @@ -0,0 +1,5 @@ +package golem:embed-voyageai@1.0.0; + +world embed-library { + include golem:embed/embed-library@1.0.0; +} diff --git a/embed/wit/deps.lock b/embed/wit/deps.lock new file mode 100644 index 000000000..adc795b3a --- /dev/null +++ b/embed/wit/deps.lock @@ -0,0 +1,4 @@ +["wasi:io"] +url = "https://github.com/WebAssembly/wasi-io/archive/v0.2.3.tar.gz" +sha256 = "1cccbfe4122686ea57a25cd368e8cdfc408cbcad089f47fb6685b6f92e96f050" +sha512 = "7a95f964c13da52611141acd89bc8876226497f128e99dd176a4270c5b5efbd8cc847b5fbd1a91258d028c646db99e0424d72590cf1caf20f9f3a3343fad5017" diff --git a/embed/wit/deps.toml b/embed/wit/deps.toml new file mode 100644 index 000000000..15e1ae691 --- /dev/null +++ b/embed/wit/deps.toml @@ -0,0 +1 @@ +"wasi:io" = "https://github.com/WebAssembly/wasi-io/archive/v0.2.3.tar.gz" diff --git a/embed/wit/deps/wasi:io/error.wit b/embed/wit/deps/wasi:io/error.wit new file mode 100644 index 000000000..97c606877 --- /dev/null +++ b/embed/wit/deps/wasi:io/error.wit @@ -0,0 +1,34 @@ +package wasi:io@0.2.3; + +@since(version = 0.2.0) +interface error { + /// A resource which represents some error information. + /// + /// The only method provided by this resource is `to-debug-string`, + /// which provides some human-readable information about the error. + /// + /// In the `wasi:io` package, this resource is returned through the + /// `wasi:io/streams/stream-error` type. + /// + /// To provide more specific error information, other interfaces may + /// offer functions to "downcast" this error into more specific types. For example, + /// errors returned from streams derived from filesystem types can be described using + /// the filesystem's own error-code type. This is done using the function + /// `wasi:filesystem/types/filesystem-error-code`, which takes a `borrow` + /// parameter and returns an `option`. + /// + /// The set of functions which can "downcast" an `error` into a more + /// concrete type is open. + @since(version = 0.2.0) + resource error { + /// Returns a string that is suitable to assist humans in debugging + /// this error. + /// + /// WARNING: The returned string should not be consumed mechanically! + /// It may change across platforms, hosts, or other implementation + /// details. Parsing this string is a major platform-compatibility + /// hazard. + @since(version = 0.2.0) + to-debug-string: func() -> string; + } +} diff --git a/embed/wit/deps/wasi:io/poll.wit b/embed/wit/deps/wasi:io/poll.wit new file mode 100644 index 000000000..9bcbe8e03 --- /dev/null +++ b/embed/wit/deps/wasi:io/poll.wit @@ -0,0 +1,47 @@ +package wasi:io@0.2.3; + +/// A poll API intended to let users wait for I/O events on multiple handles +/// at once. +@since(version = 0.2.0) +interface poll { + /// `pollable` represents a single I/O event which may be ready, or not. + @since(version = 0.2.0) + resource pollable { + + /// Return the readiness of a pollable. This function never blocks. + /// + /// Returns `true` when the pollable is ready, and `false` otherwise. + @since(version = 0.2.0) + ready: func() -> bool; + + /// `block` returns immediately if the pollable is ready, and otherwise + /// blocks until ready. + /// + /// This function is equivalent to calling `poll.poll` on a list + /// containing only this pollable. + @since(version = 0.2.0) + block: func(); + } + + /// Poll for completion on a set of pollables. + /// + /// This function takes a list of pollables, which identify I/O sources of + /// interest, and waits until one or more of the events is ready for I/O. + /// + /// The result `list` contains one or more indices of handles in the + /// argument list that is ready for I/O. + /// + /// This function traps if either: + /// - the list is empty, or: + /// - the list contains more elements than can be indexed with a `u32` value. + /// + /// A timeout can be implemented by adding a pollable from the + /// wasi-clocks API to the list. + /// + /// This function does not return a `result`; polling in itself does not + /// do any I/O so it doesn't fail. If any of the I/O sources identified by + /// the pollables has an error, it is indicated by marking the source as + /// being ready for I/O. + @since(version = 0.2.0) + poll: func(in: list>) -> list; +} diff --git a/embed/wit/deps/wasi:io/streams.wit b/embed/wit/deps/wasi:io/streams.wit new file mode 100644 index 000000000..0de084629 --- /dev/null +++ b/embed/wit/deps/wasi:io/streams.wit @@ -0,0 +1,290 @@ +package wasi:io@0.2.3; + +/// WASI I/O is an I/O abstraction API which is currently focused on providing +/// stream types. +/// +/// In the future, the component model is expected to add built-in stream types; +/// when it does, they are expected to subsume this API. +@since(version = 0.2.0) +interface streams { + @since(version = 0.2.0) + use error.{error}; + @since(version = 0.2.0) + use poll.{pollable}; + + /// An error for input-stream and output-stream operations. + @since(version = 0.2.0) + variant stream-error { + /// The last operation (a write or flush) failed before completion. + /// + /// More information is available in the `error` payload. + /// + /// After this, the stream will be closed. All future operations return + /// `stream-error::closed`. + last-operation-failed(error), + /// The stream is closed: no more input will be accepted by the + /// stream. A closed output-stream will return this error on all + /// future operations. + closed + } + + /// An input bytestream. + /// + /// `input-stream`s are *non-blocking* to the extent practical on underlying + /// platforms. I/O operations always return promptly; if fewer bytes are + /// promptly available than requested, they return the number of bytes promptly + /// available, which could even be zero. To wait for data to be available, + /// use the `subscribe` function to obtain a `pollable` which can be polled + /// for using `wasi:io/poll`. + @since(version = 0.2.0) + resource input-stream { + /// Perform a non-blocking read from the stream. + /// + /// When the source of a `read` is binary data, the bytes from the source + /// are returned verbatim. When the source of a `read` is known to the + /// implementation to be text, bytes containing the UTF-8 encoding of the + /// text are returned. + /// + /// This function returns a list of bytes containing the read data, + /// when successful. The returned list will contain up to `len` bytes; + /// it may return fewer than requested, but not more. The list is + /// empty when no bytes are available for reading at this time. The + /// pollable given by `subscribe` will be ready when more bytes are + /// available. + /// + /// This function fails with a `stream-error` when the operation + /// encounters an error, giving `last-operation-failed`, or when the + /// stream is closed, giving `closed`. + /// + /// When the caller gives a `len` of 0, it represents a request to + /// read 0 bytes. If the stream is still open, this call should + /// succeed and return an empty list, or otherwise fail with `closed`. + /// + /// The `len` parameter is a `u64`, which could represent a list of u8 which + /// is not possible to allocate in wasm32, or not desirable to allocate as + /// as a return value by the callee. The callee may return a list of bytes + /// less than `len` in size while more bytes are available for reading. + @since(version = 0.2.0) + read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Read bytes from a stream, after blocking until at least one byte can + /// be read. Except for blocking, behavior is identical to `read`. + @since(version = 0.2.0) + blocking-read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Skip bytes from a stream. Returns number of bytes skipped. + /// + /// Behaves identical to `read`, except instead of returning a list + /// of bytes, returns the number of bytes consumed from the stream. + @since(version = 0.2.0) + skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Skip bytes from a stream, after blocking until at least one byte + /// can be skipped. Except for blocking behavior, identical to `skip`. + @since(version = 0.2.0) + blocking-skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Create a `pollable` which will resolve once either the specified stream + /// has bytes available to read or the other end of the stream has been + /// closed. + /// The created `pollable` is a child resource of the `input-stream`. + /// Implementations may trap if the `input-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } + + + /// An output bytestream. + /// + /// `output-stream`s are *non-blocking* to the extent practical on + /// underlying platforms. Except where specified otherwise, I/O operations also + /// always return promptly, after the number of bytes that can be written + /// promptly, which could even be zero. To wait for the stream to be ready to + /// accept data, the `subscribe` function to obtain a `pollable` which can be + /// polled for using `wasi:io/poll`. + /// + /// Dropping an `output-stream` while there's still an active write in + /// progress may result in the data being lost. Before dropping the stream, + /// be sure to fully flush your writes. + @since(version = 0.2.0) + resource output-stream { + /// Check readiness for writing. This function never blocks. + /// + /// Returns the number of bytes permitted for the next call to `write`, + /// or an error. Calling `write` with more bytes than this function has + /// permitted will trap. + /// + /// When this function returns 0 bytes, the `subscribe` pollable will + /// become ready when this function will report at least 1 byte, or an + /// error. + @since(version = 0.2.0) + check-write: func() -> result; + + /// Perform a write. This function never blocks. + /// + /// When the destination of a `write` is binary data, the bytes from + /// `contents` are written verbatim. When the destination of a `write` is + /// known to the implementation to be text, the bytes of `contents` are + /// transcoded from UTF-8 into the encoding of the destination and then + /// written. + /// + /// Precondition: check-write gave permit of Ok(n) and contents has a + /// length of less than or equal to n. Otherwise, this function will trap. + /// + /// returns Err(closed) without writing if the stream has closed since + /// the last call to check-write provided a permit. + @since(version = 0.2.0) + write: func( + contents: list + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 bytes, and then flush the stream. Block + /// until all of these operations are complete, or an error occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write`, and `flush`, and is implemented with the + /// following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while !contents.is_empty() { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, contents.len()); + /// let (chunk, rest) = contents.split_at(len); + /// this.write(chunk ); // eliding error handling + /// contents = rest; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + @since(version = 0.2.0) + blocking-write-and-flush: func( + contents: list + ) -> result<_, stream-error>; + + /// Request to flush buffered output. This function never blocks. + /// + /// This tells the output-stream that the caller intends any buffered + /// output to be flushed. the output which is expected to be flushed + /// is all that has been passed to `write` prior to this call. + /// + /// Upon calling this function, the `output-stream` will not accept any + /// writes (`check-write` will return `ok(0)`) until the flush has + /// completed. The `subscribe` pollable will become ready when the + /// flush has completed and the stream can accept more writes. + @since(version = 0.2.0) + flush: func() -> result<_, stream-error>; + + /// Request to flush buffered output, and block until flush completes + /// and stream is ready for writing again. + @since(version = 0.2.0) + blocking-flush: func() -> result<_, stream-error>; + + /// Create a `pollable` which will resolve once the output-stream + /// is ready for more writing, or an error has occurred. When this + /// pollable is ready, `check-write` will return `ok(n)` with n>0, or an + /// error. + /// + /// If the stream is closed, this pollable is always ready immediately. + /// + /// The created `pollable` is a child resource of the `output-stream`. + /// Implementations may trap if the `output-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) + subscribe: func() -> pollable; + + /// Write zeroes to a stream. + /// + /// This should be used precisely like `write` with the exact same + /// preconditions (must use check-write first), but instead of + /// passing a list of bytes, you simply pass the number of zero-bytes + /// that should be written. + @since(version = 0.2.0) + write-zeroes: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 zeroes, and then flush the stream. + /// Block until all of these operations are complete, or an error + /// occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write-zeroes`, and `flush`, and is implemented with + /// the following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while num_zeroes != 0 { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, num_zeroes); + /// this.write-zeroes(len); // eliding error handling + /// num_zeroes -= len; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + @since(version = 0.2.0) + blocking-write-zeroes-and-flush: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Read from one stream and write to another. + /// + /// The behavior of splice is equivalent to: + /// 1. calling `check-write` on the `output-stream` + /// 2. calling `read` on the `input-stream` with the smaller of the + /// `check-write` permitted length and the `len` provided to `splice` + /// 3. calling `write` on the `output-stream` with that read data. + /// + /// Any error reported by the call to `check-write`, `read`, or + /// `write` ends the splice and reports that error. + /// + /// This function returns the number of bytes transferred; it may be less + /// than `len`. + @since(version = 0.2.0) + splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + + /// Read from one stream and write to another, with blocking. + /// + /// This is similar to `splice`, except that it blocks until the + /// `output-stream` is ready for writing, and the `input-stream` + /// is ready for reading, before performing the `splice`. + @since(version = 0.2.0) + blocking-splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + } +} diff --git a/embed/wit/deps/wasi:io/world.wit b/embed/wit/deps/wasi:io/world.wit new file mode 100644 index 000000000..f1d2102dc --- /dev/null +++ b/embed/wit/deps/wasi:io/world.wit @@ -0,0 +1,10 @@ +package wasi:io@0.2.3; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import streams; + + @since(version = 0.2.0) + import poll; +} diff --git a/embed/wit/golem-embed.wit b/embed/wit/golem-embed.wit new file mode 100644 index 000000000..78d040320 --- /dev/null +++ b/embed/wit/golem-embed.wit @@ -0,0 +1,129 @@ +package golem:embed@1.0.0; + +interface embed { + // --- Enums --- + + enum task-type { + retrieval-query, + retrieval-document, + semantic-similarity, + classification, + clustering, + question-answering, + fact-verification, + code-retrieval, + } + + enum output-format { + float-array, + binary, + base64, + } + + enum output-dtype { + float-array, + int8, + uint8, + binary, + ubinary, + } + + enum error-code { + invalid-request, + model-not-found, + unsupported, + authentication-failed, + provider-error, + rate-limit-exceeded, + internal-error, + unknown, + } + + // --- Content --- + + record image-url { + url: string, + } + + variant content-part { + text(string), + image(image-url), + } + + // --- Configuration --- + + record kv { + key: string, + value: string, + } + + record config { + model: option, + task-type: option, + dimensions: option, + truncation: option, + output-format: option, + output-dtype: option, + user: option, + provider-options: list, + } + + // --- Embedding Response --- + + record usage { + input-tokens: option, + total-tokens: option, + } + + record embedding { + index: u32, + vector: list, + } + + record embedding-response { + embeddings: list, + usage: option, + model: string, + provider-metadata-json: option, + } + + // --- Rerank Response --- + + record rerank-result { + index: u32, + relevance-score: f32, + document: option, + } + + record rerank-response { + results: list, + usage: option, + model: string, + provider-metadata-json: option, + } + + // --- Error Handling --- + + record error { + code: error-code, + message: string, + provider-error-json: option, + } + + // --- Core Functions --- + + generate: func( + inputs: list, + config: config + ) -> result; + + rerank: func( + query: string, + documents: list, + config: config + ) -> result; +} + +world embed-library { + export embed; +} \ No newline at end of file diff --git a/llm/anthropic/src/bindings.rs b/llm/anthropic/src/bindings.rs index 70c5f1fd5..1a54d6167 100644 --- a/llm/anthropic/src/bindings.rs +++ b/llm/anthropic/src/bindings.rs @@ -1,12 +1,15 @@ -// Generated by `wit-bindgen` 0.36.0. DO NOT EDIT! +// Generated by `wit-bindgen` 0.41.0. DO NOT EDIT! // Options used: // * runtime_path: "wit_bindgen_rt" // * with "golem:llm/llm@1.0.0" = "golem_llm::golem::llm::llm" // * generate_unused_types use golem_llm::golem::llm::llm as __with_name0; #[cfg(target_arch = "wasm32")] -#[link_section = "component-type:wit-bindgen:0.36.0:golem:llm-anthropic@1.0.0:llm-library:encoded world"] +#[unsafe( + link_section = "component-type:wit-bindgen:0.41.0:golem:llm-anthropic@1.0.0:llm-library:encoded world" +)] #[doc(hidden)] +#[allow(clippy::octal_escapes)] pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 1762] = *b"\ \0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07\xe0\x0c\x01A\x02\x01\ A\x02\x01BO\x01m\x04\x04user\x09assistant\x06system\x04tool\x04\0\x04role\x03\0\0\ @@ -43,8 +46,8 @@ ng-get-next\x01B\x01p\x15\x01@\x02\x08messages\xc3\0\x06config)\06\x04\0\x04send \0\x06config)\06\x04\0\x08continue\x01G\x01i=\x01@\x02\x08messages\xc3\0\x06conf\ ig)\0\xc8\0\x04\0\x06stream\x01I\x04\0\x13golem:llm/llm@1.0.0\x05\0\x04\0%golem:\ llm-anthropic/llm-library@1.0.0\x04\0\x0b\x11\x01\0\x0bllm-library\x03\0\0\0G\x09\ -producers\x01\x0cprocessed-by\x02\x0dwit-component\x070.220.0\x10wit-bindgen-rus\ -t\x060.36.0"; +producers\x01\x0cprocessed-by\x02\x0dwit-component\x070.227.1\x10wit-bindgen-rus\ +t\x060.41.0"; #[inline(never)] #[doc(hidden)] pub fn __link_custom_section_describing_imports() { diff --git a/llm/anthropic/src/conversions.rs b/llm/anthropic/src/conversions.rs index e332f1391..e7d3175a0 100644 --- a/llm/anthropic/src/conversions.rs +++ b/llm/anthropic/src/conversions.rs @@ -130,7 +130,7 @@ pub fn process_response(response: MessagesResponse) -> ChatEvent { Err(e) => { return ChatEvent::Error(Error { code: ErrorCode::InvalidRequest, - message: format!("Failed to decode base64 image data: {}", e), + message: format!("Failed to decode base64 image data: {e}"), provider_error_json: None, }); } diff --git a/llm/grok/src/bindings.rs b/llm/grok/src/bindings.rs index 2a101583e..c2f601347 100644 --- a/llm/grok/src/bindings.rs +++ b/llm/grok/src/bindings.rs @@ -1,12 +1,15 @@ -// Generated by `wit-bindgen` 0.36.0. DO NOT EDIT! +// Generated by `wit-bindgen` 0.41.0. DO NOT EDIT! // Options used: // * runtime_path: "wit_bindgen_rt" // * with "golem:llm/llm@1.0.0" = "golem_llm::golem::llm::llm" // * generate_unused_types use golem_llm::golem::llm::llm as __with_name0; #[cfg(target_arch = "wasm32")] -#[link_section = "component-type:wit-bindgen:0.36.0:golem:llm-grok@1.0.0:llm-library:encoded world"] +#[unsafe( + link_section = "component-type:wit-bindgen:0.41.0:golem:llm-grok@1.0.0:llm-library:encoded world" +)] #[doc(hidden)] +#[allow(clippy::octal_escapes)] pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 1757] = *b"\ \0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07\xdb\x0c\x01A\x02\x01\ A\x02\x01BO\x01m\x04\x04user\x09assistant\x06system\x04tool\x04\0\x04role\x03\0\0\ @@ -43,8 +46,8 @@ ng-get-next\x01B\x01p\x15\x01@\x02\x08messages\xc3\0\x06config)\06\x04\0\x04send \0\x06config)\06\x04\0\x08continue\x01G\x01i=\x01@\x02\x08messages\xc3\0\x06conf\ ig)\0\xc8\0\x04\0\x06stream\x01I\x04\0\x13golem:llm/llm@1.0.0\x05\0\x04\0\x20gol\ em:llm-grok/llm-library@1.0.0\x04\0\x0b\x11\x01\0\x0bllm-library\x03\0\0\0G\x09p\ -roducers\x01\x0cprocessed-by\x02\x0dwit-component\x070.220.0\x10wit-bindgen-rust\ -\x060.36.0"; +roducers\x01\x0cprocessed-by\x02\x0dwit-component\x070.227.1\x10wit-bindgen-rust\ +\x060.41.0"; #[inline(never)] #[doc(hidden)] pub fn __link_custom_section_describing_imports() { diff --git a/llm/grok/src/conversions.rs b/llm/grok/src/conversions.rs index 68a5d570c..129c128ad 100644 --- a/llm/grok/src/conversions.rs +++ b/llm/grok/src/conversions.rs @@ -183,7 +183,7 @@ fn convert_content_parts(contents: Vec) -> crate::client::Content { let media_type = &image_source.mime_type; // This is already a string result.push(crate::client::ContentPart::ImageInput { image_url: crate::client::ImageUrl { - url: format!("data:{};base64,{}", media_type, base64_data), + url: format!("data:{media_type};base64,{base64_data}"), detail: image_source.detail.map(|d| d.into()), }, }); diff --git a/llm/llm/Cargo.toml b/llm/llm/Cargo.toml index f12ada98b..c796eba28 100644 --- a/llm/llm/Cargo.toml +++ b/llm/llm/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" license = "Apache-2.0" homepage = "https://golem.cloud" repository = "https://github.com/golemcloud/golem-llm" -description = "WebAssembly components for working with LLM APIs, with special support for Golem Cloud" +description = "WebAssembly components for working with AI models and providers APIs, with special support for Golem Cloud" [lib] path = "src/lib.rs" diff --git a/llm/llm/src/event_source/ndjson_stream.rs b/llm/llm/src/event_source/ndjson_stream.rs index e2f4cc1b2..1b8ef3773 100644 --- a/llm/llm/src/event_source/ndjson_stream.rs +++ b/llm/llm/src/event_source/ndjson_stream.rs @@ -126,7 +126,7 @@ fn try_parse_line( return Ok(None); } - trace!("Parsed NDJSON line: {}", line); + trace!("Parsed NDJSON line: {line}"); // Create a MessageEvent with the JSON line as data let event = MessageEvent { diff --git a/llm/llm/src/event_source/stream.rs b/llm/llm/src/event_source/stream.rs index 8f2933676..13a5eeb56 100644 --- a/llm/llm/src/event_source/stream.rs +++ b/llm/llm/src/event_source/stream.rs @@ -56,9 +56,9 @@ where { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Utf8(err) => f.write_fmt(format_args!("UTF8 error: {}", err)), - Self::Parser(err) => f.write_fmt(format_args!("Parse error: {}", err)), - Self::Transport(err) => f.write_fmt(format_args!("Transport error: {}", err)), + Self::Utf8(err) => f.write_fmt(format_args!("UTF8 error: {err}")), + Self::Parser(err) => f.write_fmt(format_args!("Parse error: {err}")), + Self::Transport(err) => f.write_fmt(format_args!("Transport error: {err}")), } } } diff --git a/llm/ollama/src/bindings.rs b/llm/ollama/src/bindings.rs index dbb704704..269cd07fb 100644 --- a/llm/ollama/src/bindings.rs +++ b/llm/ollama/src/bindings.rs @@ -1,12 +1,15 @@ -// Generated by `wit-bindgen` 0.36.0. DO NOT EDIT! +// Generated by `wit-bindgen` 0.41.0. DO NOT EDIT! // Options used: // * runtime_path: "wit_bindgen_rt" // * with "golem:llm/llm@1.0.0" = "golem_llm::golem::llm::llm" // * generate_unused_types use golem_llm::golem::llm::llm as __with_name0; #[cfg(target_arch = "wasm32")] -#[link_section = "component-type:wit-bindgen:0.36.0:golem:llm-ollama@1.0.0:llm-library:encoded world"] +#[unsafe( + link_section = "component-type:wit-bindgen:0.41.0:golem:llm-ollama@1.0.0:llm-library:encoded world" +)] #[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\ @@ -43,8 +46,8 @@ ng-get-next\x01B\x01p\x15\x01@\x02\x08messages\xc3\0\x06config)\06\x04\0\x04send \0\x06config)\06\x04\0\x08continue\x01G\x01i=\x01@\x02\x08messages\xc3\0\x06conf\ ig)\0\xc8\0\x04\0\x06stream\x01I\x04\0\x13golem:llm/llm@1.0.0\x05\0\x04\0\"golem\ :llm-ollama/llm-library@1.0.0\x04\0\x0b\x11\x01\0\x0bllm-library\x03\0\0\0G\x09p\ -roducers\x01\x0cprocessed-by\x02\x0dwit-component\x070.220.0\x10wit-bindgen-rust\ -\x060.36.0"; +roducers\x01\x0cprocessed-by\x02\x0dwit-component\x070.227.1\x10wit-bindgen-rust\ +\x060.41.0"; #[inline(never)] #[doc(hidden)] pub fn __link_custom_section_describing_imports() { diff --git a/llm/ollama/src/client.rs b/llm/ollama/src/client.rs index e9514a8dd..e2901e70c 100644 --- a/llm/ollama/src/client.rs +++ b/llm/ollama/src/client.rs @@ -335,7 +335,7 @@ pub fn image_to_base64(source: &str) -> Result Error { Error { code: ErrorCode::InternalError, - message: format!("{}: {}", context, err), + message: format!("{context}: {err}"), provider_error_json: None, } } diff --git a/llm/ollama/src/conversions.rs b/llm/ollama/src/conversions.rs index b1db65c61..8d64e954f 100644 --- a/llm/ollama/src/conversions.rs +++ b/llm/ollama/src/conversions.rs @@ -214,7 +214,7 @@ pub fn process_response(response: CompletionsResponse) -> ChatEvent { }; ChatEvent::Message(CompleteResponse { - id: format!("ollama-{}", timestamp), + id: format!("ollama-{timestamp}"), content, tool_calls, metadata, diff --git a/llm/openai/src/bindings.rs b/llm/openai/src/bindings.rs index c960248a8..6d0a77280 100644 --- a/llm/openai/src/bindings.rs +++ b/llm/openai/src/bindings.rs @@ -1,12 +1,15 @@ -// Generated by `wit-bindgen` 0.36.0. DO NOT EDIT! +// Generated by `wit-bindgen` 0.41.0. DO NOT EDIT! // Options used: // * runtime_path: "wit_bindgen_rt" // * with "golem:llm/llm@1.0.0" = "golem_llm::golem::llm::llm" // * generate_unused_types use golem_llm::golem::llm::llm as __with_name0; #[cfg(target_arch = "wasm32")] -#[link_section = "component-type:wit-bindgen:0.36.0:golem:llm-openai@1.0.0:llm-library:encoded world"] +#[unsafe( + link_section = "component-type:wit-bindgen:0.41.0:golem:llm-openai@1.0.0:llm-library:encoded world" +)] #[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\ @@ -43,8 +46,8 @@ ng-get-next\x01B\x01p\x15\x01@\x02\x08messages\xc3\0\x06config)\06\x04\0\x04send \0\x06config)\06\x04\0\x08continue\x01G\x01i=\x01@\x02\x08messages\xc3\0\x06conf\ ig)\0\xc8\0\x04\0\x06stream\x01I\x04\0\x13golem:llm/llm@1.0.0\x05\0\x04\0\"golem\ :llm-openai/llm-library@1.0.0\x04\0\x0b\x11\x01\0\x0bllm-library\x03\0\0\0G\x09p\ -roducers\x01\x0cprocessed-by\x02\x0dwit-component\x070.220.0\x10wit-bindgen-rust\ -\x060.36.0"; +roducers\x01\x0cprocessed-by\x02\x0dwit-component\x070.227.1\x10wit-bindgen-rust\ +\x060.41.0"; #[inline(never)] #[doc(hidden)] pub fn __link_custom_section_describing_imports() { diff --git a/llm/openai/src/conversions.rs b/llm/openai/src/conversions.rs index 43694c0f3..a4989b0c1 100644 --- a/llm/openai/src/conversions.rs +++ b/llm/openai/src/conversions.rs @@ -138,7 +138,7 @@ pub fn content_part_to_inner_input_item(content_part: ContentPart) -> InnerInput ImageReference::Inline(image_source) => { let base64_data = general_purpose::STANDARD.encode(&image_source.data); let mime_type = &image_source.mime_type; // This is already a string - let data_url = format!("data:{};base64,{}", mime_type, base64_data); + let data_url = format!("data:{mime_type};base64,{base64_data}"); InnerInputItem::ImageInput { image_url: data_url, diff --git a/llm/openrouter/src/bindings.rs b/llm/openrouter/src/bindings.rs index ba2accf7e..1300cde97 100644 --- a/llm/openrouter/src/bindings.rs +++ b/llm/openrouter/src/bindings.rs @@ -1,12 +1,15 @@ -// Generated by `wit-bindgen` 0.36.0. DO NOT EDIT! +// Generated by `wit-bindgen` 0.41.0. DO NOT EDIT! // Options used: // * runtime_path: "wit_bindgen_rt" // * with "golem:llm/llm@1.0.0" = "golem_llm::golem::llm::llm" // * generate_unused_types use golem_llm::golem::llm::llm as __with_name0; #[cfg(target_arch = "wasm32")] -#[link_section = "component-type:wit-bindgen:0.36.0:golem:llm-openrouter@1.0.0:llm-library:encoded world"] +#[unsafe( + link_section = "component-type:wit-bindgen:0.41.0:golem:llm-openrouter@1.0.0:llm-library:encoded world" +)] #[doc(hidden)] +#[allow(clippy::octal_escapes)] pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 1763] = *b"\ \0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07\xe1\x0c\x01A\x02\x01\ A\x02\x01BO\x01m\x04\x04user\x09assistant\x06system\x04tool\x04\0\x04role\x03\0\0\ @@ -43,8 +46,8 @@ ng-get-next\x01B\x01p\x15\x01@\x02\x08messages\xc3\0\x06config)\06\x04\0\x04send \0\x06config)\06\x04\0\x08continue\x01G\x01i=\x01@\x02\x08messages\xc3\0\x06conf\ ig)\0\xc8\0\x04\0\x06stream\x01I\x04\0\x13golem:llm/llm@1.0.0\x05\0\x04\0&golem:\ llm-openrouter/llm-library@1.0.0\x04\0\x0b\x11\x01\0\x0bllm-library\x03\0\0\0G\x09\ -producers\x01\x0cprocessed-by\x02\x0dwit-component\x070.220.0\x10wit-bindgen-rus\ -t\x060.36.0"; +producers\x01\x0cprocessed-by\x02\x0dwit-component\x070.227.1\x10wit-bindgen-rus\ +t\x060.41.0"; #[inline(never)] #[doc(hidden)] pub fn __link_custom_section_describing_imports() { diff --git a/llm/openrouter/src/conversions.rs b/llm/openrouter/src/conversions.rs index d4db2d34c..61b5f973b 100644 --- a/llm/openrouter/src/conversions.rs +++ b/llm/openrouter/src/conversions.rs @@ -184,7 +184,7 @@ fn convert_content_parts(contents: Vec) -> crate::client::Content { let media_type = &image_source.mime_type; // This is already a string result.push(crate::client::ContentPart::ImageInput { image_url: crate::client::ImageUrl { - url: format!("data:{};base64,{}", media_type, base64_data), + url: format!("data:{media_type};base64,{base64_data}"), detail: image_source.detail.map(|d| d.into()), }, }); diff --git a/test/Cargo.lock b/test/Cargo.lock index 22a6de76b..7150a2d2e 100644 --- a/test/Cargo.lock +++ b/test/Cargo.lock @@ -863,6 +863,18 @@ dependencies = [ "syn", ] +[[package]] +name = "test_embed" +version = "0.0.0" +dependencies = [ + "golem-rust", + "log", + "reqwest", + "serde", + "serde_json", + "wit-bindgen-rt 0.40.0", +] + [[package]] name = "test_helper" version = "0.0.0" diff --git a/test/components-rust/test-embed/Cargo.lock b/test/components-rust/test-embed/Cargo.lock new file mode 100644 index 000000000..bc5f25f2e --- /dev/null +++ b/test/components-rust/test-embed/Cargo.lock @@ -0,0 +1,1376 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" + +[[package]] +name = "cc" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "test-openai" +version = "0.0.0" +dependencies = [ + "golem-rust", + "reqwest", + "serde", + "serde_json", + "wit-bindgen-rt", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[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-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[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-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[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 = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "golem-rust" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c967eb388fb81f9b9f4df5d5b6634de803f21cd410c1bf687202794a4fbc0267" +dependencies = [ + "golem-rust-macro", + "serde", + "serde_json", + "uuid", + "wit-bindgen-rt", +] + +[[package]] +name = "golem-rust-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb87f831cfe4371427c63f5f4cabcc3bae1b66974c8fbcf22be9274fee3a7d1" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[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-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[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", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "js-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[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 = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[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.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[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.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +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 = "reqwest" +version = "0.11.18" +source = "git+https://github.com/zivergetech/reqwest?branch=update-jun-2024#1cf59c67b93aa6292961f8948b93df5bca2753b6" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", + "wit-bindgen-rt", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.38.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.216" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.216" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.134" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[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 = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "syn" +version = "2.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower-service" +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-core", +] + +[[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 = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[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.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" + +[[package]] +name = "web-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[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 = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/test/components-rust/test-embed/Cargo.toml b/test/components-rust/test-embed/Cargo.toml new file mode 100644 index 000000000..177266a40 --- /dev/null +++ b/test/components-rust/test-embed/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "test_embed" +version = "0.0.0" +edition = "2021" + +[lib] +path = "src/lib.rs" +crate-type = ["cdylib"] +required-features = [] + +[features] +default = ["openai"] +openai = [] +cohere = [] +hugging-face = [] +voyageai = [] + +[dependencies] +# To use common shared libs, use the following: +# common-lib = { path = "../../common-rust/common-lib" } + +golem-rust = { workspace = true } +log = { version = "0.4.27" } +reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +wit-bindgen-rt = { workspace = true } + +[package.metadata.component.target] +path = "wit-generated" + +[package.metadata.component.bindings.with] +"wasi:io/poll@0.2.0" = "golem_rust::wasm_rpc::wasi::io::poll" +"wasi:clocks/wall-clock@0.2.0" = "golem_rust::wasm_rpc::wasi::clocks::wall_clock" +"golem:rpc/types@0.2.0" = "golem_rust::wasm_rpc::golem_rpc_0_2_x::types" + +[package.metadata.component.target.dependencies] +"golem:embed" = { path = "wit-generated/deps/golem-embed" } +"wasi:clocks" = { path = "wit-generated/deps/clocks" } +"wasi:io" = { path = "wit-generated/deps/io" } +"golem:rpc" = { path = "wit-generated/deps/golem-rpc" } +"test:helper-client" = { path = "wit-generated/deps/test_helper-client" } +"test:embed-exports" = { path = "wit-generated/deps/test_embed-exports" } + +[package.metadata.component.bindings] +# See https://github.com/bytecodealliance/cargo-component/blob/main/src/metadata.rs#L62 + +# derives = ["serde::Serialize", "serde::Deserialize"] +# generate_unused_types = true \ No newline at end of file diff --git a/test/components-rust/test-embed/golem.yaml b/test/components-rust/test-embed/golem.yaml new file mode 100644 index 000000000..2dc2141ad --- /dev/null +++ b/test/components-rust/test-embed/golem.yaml @@ -0,0 +1,226 @@ +# Schema for IDEA: +# $schema: https://schema.golem.cloud/app/golem/1.1.1/golem.schema.json +# Schema for vscode-yaml +# yaml-language-server: $schema=https://schema.golem.cloud/app/golem/1.1.1/golem.schema.json + +# See https://learn.golem.cloud/docs/app-manifest#field-reference for field reference + +components: + test:embed: + profiles: + # DEBUG PROFILES + openai-debug: + files: + - sourcePath: ../../data/cat.png + targetPath: /data/cat.png + permissions: read-only + build: + - command: cargo component build --no-default-features --features openai + sources: + - src + - wit-generated + - ../../common-rust + targets: + - target/wasm32-wasip1/debug/test_embed.wasm + - command: wac plug --plug ../../../target/wasm32-wasip1/debug/golem_embed_openai.wasm ../../target/wasm32-wasip1/debug/test_embed.wasm -o ../../target/wasm32-wasip1/debug/test_openai_plugged.wasm + sources: + - ../../target/wasm32-wasip1/debug/test_embed.wasm + - ../../../target/wasm32-wasip1/debug/golem_embed_openai.wasm + targets: + - ../../target/wasm32-wasip1/debug/test_openai_plugged.wasm + sourceWit: wit + generatedWit: wit-generated + componentWasm: ../../target/wasm32-wasip1/debug/test_openai_plugged.wasm + linkedWasm: ../../golem-temp/components/test_openai_debug.wasm + clean: + - src/bindings.rs + + cohere-debug: + files: + - sourcePath: ../../data/cat.png + targetPath: /data/cat.png + permissions: read-only + build: + - command: cargo component build --no-default-features --features cohere + sources: + - src + - wit-generated + - ../../common-rust + targets: + - target/wasm32-wasip1/debug/test_embed.wasm + - command: wac plug --plug ../../../target/wasm32-wasip1/debug/golem_embed_cohere.wasm ../../target/wasm32-wasip1/debug/test_embed.wasm -o ../../target/wasm32-wasip1/debug/test_cohere_plugged.wasm + sources: + - ../../target/wasm32-wasip1/debug/test_embed.wasm + - ../../../target/wasm32-wasip1/debug/golem_embed_cohere.wasm + targets: + - ../../target/wasm32-wasip1/debug/test_cohere_plugged.wasm + sourceWit: wit + generatedWit: wit-generated + componentWasm: ../../target/wasm32-wasip1/debug/test_cohere_plugged.wasm + linkedWasm: ../../golem-temp/components/test_cohere_debug.wasm + clean: + - src/bindings.rs + + hugging-face-debug: + files: + - sourcePath: ../../data/cat.png + targetPath: /data/cat.png + permissions: read-only + build: + - command: cargo component build --no-default-features --features hugging-face + sources: + - src + - wit-generated + - ../../common-rust + targets: + - target/wasm32-wasip1/debug/test_embed.wasm + - command: wac plug --plug ../../../target/wasm32-wasip1/debug/golem_embed_hugging_face.wasm ../../target/wasm32-wasip1/debug/test_embed.wasm -o ../../target/wasm32-wasip1/debug/test_hugging_face_plugged.wasm + sources: + - ../../target/wasm32-wasip1/debug/test_embed.wasm + - ../../../target/wasm32-wasip1/debug/golem_embed_hugging_face.wasm + targets: + - ../../target/wasm32-wasip1/debug/test_hugging_face_plugged.wasm + sourceWit: wit + generatedWit: wit-generated + componentWasm: ../../target/wasm32-wasip1/debug/test_hugging_face_plugged.wasm + linkedWasm: ../../golem-temp/components/test_hugging_face_debug.wasm + clean: + - src/bindings.rs + + voyageai-debug: + files: + - sourcePath: ../../data/cat.png + targetPath: /data/cat.png + permissions: read-only + build: + - command: cargo component build --no-default-features --features voyageai + sources: + - src + - wit-generated + - ../../common-rust + targets: + - target/wasm32-wasip1/debug/test_embed.wasm + - command: wac plug --plug ../../../target/wasm32-wasip1/debug/golem_embed_voyageai.wasm ../../target/wasm32-wasip1/debug/test_embed.wasm -o ../../target/wasm32-wasip1/debug/test_voyageai_plugged.wasm + sources: + - ../../target/wasm32-wasip1/debug/test_embed.wasm + - ../../../target/wasm32-wasip1/debug/golem_embed_voyageai.wasm + targets: + - ../../target/wasm32-wasip1/debug/test_voyageai_plugged.wasm + sourceWit: wit + generatedWit: wit-generated + componentWasm: ../../target/wasm32-wasip1/debug/test_voyageai_plugged.wasm + linkedWasm: ../../golem-temp/components/test_voyageai_debug.wasm + clean: + - src/bindings.rs + + # RELEASE PROFILES + openai-release: + files: + - sourcePath: ../../data/cat.png + targetPath: /data/cat.png + permissions: read-only + build: + - command: cargo component build --release --no-default-features --features openai + sources: + - src + - wit-generated + - ../../common-rust + targets: + - ../../target/wasm32-wasip1/release/test_embed.wasm + - command: wac plug --plug ../../../target/wasm32-wasip1/release/golem_embed_openai.wasm ../../target/wasm32-wasip1/release/test_embed.wasm -o ../../target/wasm32-wasip1/release/test_openai_plugged.wasm + sources: + - ../../target/wasm32-wasip1/release/test_embed.wasm + - ../../../target/wasm32-wasip1/release/golem_embed_openai.wasm + targets: + - ../../target/wasm32-wasip1/release/test_openai_plugged.wasm + sourceWit: wit + generatedWit: wit-generated + componentWasm: ../../target/wasm32-wasip1/release/test_openai_plugged.wasm + linkedWasm: ../../golem-temp/components/test_openai_release.wasm + clean: + - src/bindings.rs + + cohere-release: + files: + - sourcePath: ../../data/cat.png + targetPath: /data/cat.png + permissions: read-only + build: + - command: cargo component build --release --no-default-features --features cohere + sources: + - src + - wit-generated + - ../../common-rust + targets: + - ../../target/wasm32-wasip1/release/test_embed.wasm + - command: wac plug --plug ../../../target/wasm32-wasip1/release/golem_embed_cohere.wasm ../../target/wasm32-wasip1/release/test_embed.wasm -o ../../target/wasm32-wasip1/release/test_cohere_plugged.wasm + sources: + - ../../target/wasm32-wasip1/release/test_embed.wasm + - ../../../target/wasm32-wasip1/release/golem_embed_cohere.wasm + targets: + - ../../target/wasm32-wasip1/release/test_cohere_plugged.wasm + sourceWit: wit + generatedWit: wit-generated + componentWasm: ../../target/wasm32-wasip1/release/test_cohere_plugged.wasm + linkedWasm: ../../golem-temp/components/test_cohere_release.wasm + clean: + - src/bindings.rs + + hugging-face-release: + files: + - sourcePath: ../../data/cat.png + targetPath: /data/cat.png + permissions: read-only + build: + - command: cargo component build --release --no-default-features --features hugging-face + sources: + - src + - wit-generated + - ../../common-rust + targets: + - ../../target/wasm32-wasip1/release/test_embed.wasm + - command: wac plug --plug ../../../target/wasm32-wasip1/release/golem_embed_hugging_face.wasm ../../target/wasm32-wasip1/release/test_embed.wasm -o ../../target/wasm32-wasip1/release/test_hugging_face_plugged.wasm + sources: + - ../../target/wasm32-wasip1/release/test_embed.wasm + - ../../../target/wasm32-wasip1/release/golem_embed_hugging_face.wasm + targets: + - ../../target/wasm32-wasip1/release/test_hugging_face_plugged.wasm + sourceWit: wit + generatedWit: wit-generated + componentWasm: ../../target/wasm32-wasip1/release/test_hugging_face_plugged.wasm + linkedWasm: ../../golem-temp/components/test_hugging_face_release.wasm + clean: + - src/bindings.rs + + voyageai-release: + files: + - sourcePath: ../../data/cat.png + targetPath: /data/cat.png + permissions: read-only + build: + - command: cargo component build --release --no-default-features --features voyageai + sources: + - src + - wit-generated + - ../../common-rust + targets: + - ../../target/wasm32-wasip1/release/test_embed.wasm + - command: wac plug --plug ../../../target/wasm32-wasip1/release/golem_embed_voyageai.wasm ../../target/wasm32-wasip1/release/test_embed.wasm -o ../../target/wasm32-wasip1/release/test_voyageai_plugged.wasm + sources: + - ../../target/wasm32-wasip1/release/test_embed.wasm + - ../../../target/wasm32-wasip1/release/golem_embed_voyageai.wasm + targets: + - ../../target/wasm32-wasip1/release/test_voyageai_plugged.wasm + sourceWit: wit + generatedWit: wit-generated + componentWasm: ../../target/wasm32-wasip1/release/test_voyageai_plugged.wasm + linkedWasm: ../../golem-temp/components/test_voyageai_release.wasm + clean: + - src/bindings.rs + + defaultProfile: openai-debug + +dependencies: + test:embed: + - target: test:helper + type: wasm-rpc diff --git a/test/components-rust/test-embed/src/lib.rs b/test/components-rust/test-embed/src/lib.rs new file mode 100644 index 000000000..a55387dce --- /dev/null +++ b/test/components-rust/test-embed/src/lib.rs @@ -0,0 +1,101 @@ +#[allow(static_mut_refs)] +mod bindings; + +use crate::bindings::exports::test::embed_exports::test_embed_api::*; +use crate::bindings::golem::embed::embed; +use crate::bindings::golem::embed::embed::{Config, ContentPart, Error, EmbeddingResponse}; + +struct Component; + +#[cfg(feature = "openai")] +const MODEL: &'static str = "text-embedding-3-small"; +#[cfg(feature = "cohere")] +const MODEL: &'static str = "embed-english-v3.0"; +#[cfg(feature = "hugging-face")] +const MODEL: &'static str = "sentence-transformers/all-MiniLM-L6-v2"; +#[cfg(feature = "voyageai")] +const MODEL: &'static str = "voyage-3"; + +#[cfg(feature = "openai")] +const RERANKING_MODEL: &'static str = ""; +#[cfg(feature = "cohere")] +const RERANKING_MODEL: &'static str = "rerank-v3.5"; +#[cfg(feature = "hugging-face")] +const RERANKING_MODEL: &'static str = "cross-encoder/ms-marco-MiniLM-L-2-v2"; +#[cfg(feature = "voyageai")] +const RERANKING_MODEL: &'static str = "rerank-1"; + +impl Guest for Component { + /// test1 demonstrates text embedding generation. + fn test1() -> String { + let config = Config { + model: Some(MODEL.to_string()), + task_type: Some(embed::TaskType::RetrievalDocument), + dimensions: Some(1024), + truncation: Some(true), + output_format: Some(embed::OutputFormat::FloatArray), + output_dtype: Some(embed::OutputDtype::FloatArray), + user: Some("RutikThakre".to_string()), + provider_options: vec![], + }; + println!("Sending text for embedding generation..."); + let response: Result = embed::generate( + &[ContentPart::Text("Hello, world!".to_string())], + &config, + ); + + match response { + Ok(response) => { + format!("Response: {:?}", response) + } + Err(error) => { + format!( + "Error: {:?} {} {}", + error.code, + error.message, + error.provider_error_json.unwrap_or_default() + ) + } + } + } + + /// test2 demonstrates embedding's reranking + fn test2() -> String { + let config = Config { + model: Some(RERANKING_MODEL.to_string()), + task_type: Some(embed::TaskType::RetrievalDocument), + dimensions: Some(1024), + truncation: Some(true), + output_format: Some(embed::OutputFormat::FloatArray), + output_dtype: Some(embed::OutputDtype::FloatArray), + user: Some("RutikThakre".to_string()), + provider_options: vec![], + }; + let query = "What is the capital of the United States?"; + let documents = vec![ + "Carson City is the capital city of the American state of Nevada.".to_string(), + "The Commonwealth of the Northern Mariana Islands is a group of islands in the Pacific Ocean. Its capital is Saipan.".to_string(), + "Washington, D.C. (also known as simply Washington or D.C., and officially as the District of Columbia) is the capital of the United States. It is a federal district.".to_string(), + "Capitalization or capitalisation in English grammar is the use of a capital letter at the start of a word. English usage varies from capitalization in other languages.".to_string(), + "Capital punishment has existed in the United States since beforethe United States was a country. As of 2017, capital punishment is legal in 30 of the 50 states.".to_string() + ]; + + println!("Sending request for reranking..."); + let response = embed::rerank(query, &documents, &config); + match response { + Ok(response) => { + format!("Response: {:?}", response) + } + Err(error) => { + format!( + "Error: {:?} {} {}", + error.code, + error.message, + error.provider_error_json.unwrap_or_default() + ) + } + } + } +} + +bindings::export!(Component with_types_in bindings); diff --git a/test/components-rust/test-embed/wit/test-embed.wit b/test/components-rust/test-embed/wit/test-embed.wit new file mode 100644 index 000000000..b428f27a9 --- /dev/null +++ b/test/components-rust/test-embed/wit/test-embed.wit @@ -0,0 +1,13 @@ +package test:embed; + +// See https://component-model.bytecodealliance.org/design/wit.html for more details about the WIT syntax + +interface test-embed-api { + test1: func() -> string; + test2: func() -> string; +} + +world test-embed { + import golem:embed/embed@1.0.0; + export test-embed-api; +} diff --git a/test/wit/deps/golem-embed/golem-embed.wit b/test/wit/deps/golem-embed/golem-embed.wit new file mode 100644 index 000000000..78d040320 --- /dev/null +++ b/test/wit/deps/golem-embed/golem-embed.wit @@ -0,0 +1,129 @@ +package golem:embed@1.0.0; + +interface embed { + // --- Enums --- + + enum task-type { + retrieval-query, + retrieval-document, + semantic-similarity, + classification, + clustering, + question-answering, + fact-verification, + code-retrieval, + } + + enum output-format { + float-array, + binary, + base64, + } + + enum output-dtype { + float-array, + int8, + uint8, + binary, + ubinary, + } + + enum error-code { + invalid-request, + model-not-found, + unsupported, + authentication-failed, + provider-error, + rate-limit-exceeded, + internal-error, + unknown, + } + + // --- Content --- + + record image-url { + url: string, + } + + variant content-part { + text(string), + image(image-url), + } + + // --- Configuration --- + + record kv { + key: string, + value: string, + } + + record config { + model: option, + task-type: option, + dimensions: option, + truncation: option, + output-format: option, + output-dtype: option, + user: option, + provider-options: list, + } + + // --- Embedding Response --- + + record usage { + input-tokens: option, + total-tokens: option, + } + + record embedding { + index: u32, + vector: list, + } + + record embedding-response { + embeddings: list, + usage: option, + model: string, + provider-metadata-json: option, + } + + // --- Rerank Response --- + + record rerank-result { + index: u32, + relevance-score: f32, + document: option, + } + + record rerank-response { + results: list, + usage: option, + model: string, + provider-metadata-json: option, + } + + // --- Error Handling --- + + record error { + code: error-code, + message: string, + provider-error-json: option, + } + + // --- Core Functions --- + + generate: func( + inputs: list, + config: config + ) -> result; + + rerank: func( + query: string, + documents: list, + config: config + ) -> result; +} + +world embed-library { + export embed; +} \ No newline at end of file