first commit
This commit is contained in:
74
examples/plugins/hello-world-wasm-go/Makefile
Normal file
74
examples/plugins/hello-world-wasm-go/Makefile
Normal file
@@ -0,0 +1,74 @@
|
||||
.PHONY: all build clean help check-tinygo
|
||||
|
||||
# Colors
|
||||
COLOR_RESET = \033[0m
|
||||
COLOR_INFO = \033[36m
|
||||
COLOR_SUCCESS = \033[32m
|
||||
COLOR_WARNING = \033[33m
|
||||
COLOR_ERROR = \033[31m
|
||||
COLOR_BOLD = \033[1m
|
||||
|
||||
# Plugin configuration
|
||||
PLUGIN_NAME = hello-world
|
||||
OUTPUT_DIR = build
|
||||
OUTPUT = $(OUTPUT_DIR)/$(PLUGIN_NAME).wasm
|
||||
|
||||
# TinyGo build flags
|
||||
TINYGO_TARGET = wasi
|
||||
TINYGO_SCHEDULER = none
|
||||
|
||||
help: ## Show this help message
|
||||
@echo '$(COLOR_BOLD)Hello World WASM Plugin$(COLOR_RESET)'
|
||||
@echo ''
|
||||
@echo '$(COLOR_BOLD)Usage:$(COLOR_RESET) make [target]'
|
||||
@echo ''
|
||||
@echo '$(COLOR_BOLD)Prerequisites:$(COLOR_RESET)'
|
||||
@echo ' - TinyGo (https://tinygo.org/getting-started/install/)'
|
||||
@echo ' macOS: brew install tinygo'
|
||||
@echo ' Linux: See TinyGo installation docs'
|
||||
@echo ''
|
||||
@echo '$(COLOR_BOLD)Available targets:$(COLOR_RESET)'
|
||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " $(COLOR_INFO)%-15s$(COLOR_RESET) %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
|
||||
check-tinygo: ## Check if TinyGo is installed
|
||||
@which tinygo > /dev/null 2>&1 || (echo "$(COLOR_ERROR)Error: TinyGo is not installed$(COLOR_RESET)"; \
|
||||
echo "$(COLOR_INFO)Install TinyGo:$(COLOR_RESET)"; \
|
||||
echo " macOS: brew install tinygo"; \
|
||||
echo " Linux: See https://tinygo.org/getting-started/install/"; \
|
||||
exit 1)
|
||||
@echo "$(COLOR_SUCCESS)✓ TinyGo found: $$(tinygo version)$(COLOR_RESET)"
|
||||
|
||||
build: check-tinygo ## Build the WASM plugin
|
||||
@mkdir -p $(OUTPUT_DIR)
|
||||
@echo "$(COLOR_INFO)Building WASM plugin...$(COLOR_RESET)"
|
||||
GOWORK=off tinygo build -o $(OUTPUT) -target=$(TINYGO_TARGET) -scheduler=$(TINYGO_SCHEDULER) .
|
||||
@echo "$(COLOR_SUCCESS)✓ Plugin built successfully: $(OUTPUT)$(COLOR_RESET)"
|
||||
@ls -lh $(OUTPUT) | awk '{print " Size: " $$5}'
|
||||
|
||||
build-optimized: check-tinygo ## Build the WASM plugin with size optimizations
|
||||
@mkdir -p $(OUTPUT_DIR)
|
||||
@echo "$(COLOR_INFO)Building optimized WASM plugin...$(COLOR_RESET)"
|
||||
GOWORK=off tinygo build -o $(OUTPUT) -target=$(TINYGO_TARGET) -scheduler=$(TINYGO_SCHEDULER) -no-debug -gc=leaking .
|
||||
@echo "$(COLOR_SUCCESS)✓ Optimized plugin built: $(OUTPUT)$(COLOR_RESET)"
|
||||
@ls -lh $(OUTPUT) | awk '{print " Size: " $$5}'
|
||||
|
||||
clean: ## Remove build artifacts
|
||||
@echo "$(COLOR_INFO)Cleaning build artifacts...$(COLOR_RESET)"
|
||||
@rm -rf $(OUTPUT_DIR)
|
||||
@echo "$(COLOR_SUCCESS)✓ Clean complete$(COLOR_RESET)"
|
||||
|
||||
info: ## Show build information
|
||||
@echo "$(COLOR_BOLD)Build Configuration$(COLOR_RESET)"
|
||||
@echo " Plugin Name: $(PLUGIN_NAME)"
|
||||
@echo " Output: $(OUTPUT)"
|
||||
@echo " Target: $(TINYGO_TARGET)"
|
||||
@echo " Scheduler: $(TINYGO_SCHEDULER)"
|
||||
@echo ""
|
||||
@if [ -f "$(OUTPUT)" ]; then \
|
||||
echo "$(COLOR_SUCCESS)Plugin exists:$(COLOR_RESET)"; \
|
||||
ls -lh $(OUTPUT) | awk '{print " " $$9 " (" $$5 ")"}'; \
|
||||
else \
|
||||
echo "$(COLOR_WARNING)Plugin not built yet$(COLOR_RESET)"; \
|
||||
fi
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
170
examples/plugins/hello-world-wasm-go/README.md
Normal file
170
examples/plugins/hello-world-wasm-go/README.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# Hello World WASM Plugin
|
||||
|
||||
A minimal example of a Bifrost plugin written in Go and compiled to WebAssembly using TinyGo.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### TinyGo Installation
|
||||
|
||||
TinyGo is required to compile Go code to WebAssembly with a small binary size.
|
||||
|
||||
**macOS:**
|
||||
```bash
|
||||
brew install tinygo
|
||||
```
|
||||
|
||||
**Linux (Ubuntu/Debian):**
|
||||
```bash
|
||||
wget https://github.com/tinygo-org/tinygo/releases/download/v0.32.0/tinygo_0.32.0_amd64.deb
|
||||
sudo dpkg -i tinygo_0.32.0_amd64.deb
|
||||
```
|
||||
|
||||
**Other platforms:**
|
||||
See [TinyGo Installation Guide](https://tinygo.org/getting-started/install/)
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
# Build the WASM plugin
|
||||
make build
|
||||
|
||||
# Build with size optimizations
|
||||
make build-optimized
|
||||
|
||||
# Clean build artifacts
|
||||
make clean
|
||||
```
|
||||
|
||||
The compiled plugin will be at `build/hello-world.wasm`.
|
||||
|
||||
## Plugin Structure
|
||||
|
||||
WASM plugins must export the following functions:
|
||||
|
||||
| Export | Signature | Description |
|
||||
|--------|-----------|-------------|
|
||||
| `plugin_malloc` | `(size: u32) -> u32` | Allocate memory for host to write data (or `malloc` for non-TinyGo) |
|
||||
| `plugin_free` | `(ptr: u32)` | Free allocated memory (optional, or `free` for non-TinyGo) |
|
||||
| `get_name` | `() -> u64` | Returns packed ptr+len of plugin name |
|
||||
| `http_transport_intercept` | `(ctx_ptr, ctx_len, req_ptr, req_len: u32) -> u64` | HTTP transport intercept |
|
||||
| `pre_hook` | `(ctx_ptr, ctx_len, req_ptr, req_len: u32) -> u64` | Pre-request hook |
|
||||
| `post_hook` | `(ctx_ptr, ctx_len, resp_ptr, resp_len, err_ptr, err_len: u32) -> u64` | Post-response hook |
|
||||
| `cleanup` | `() -> i32` | Cleanup resources (0 = success) |
|
||||
| `init` | `(config_ptr, config_len: u32) -> i32` | Initialize with config (optional) |
|
||||
|
||||
### Return Value Format
|
||||
|
||||
Functions returning data use a packed `u64` format:
|
||||
- Upper 32 bits: pointer to data in WASM memory
|
||||
- Lower 32 bits: length of data
|
||||
|
||||
### Data Exchange
|
||||
|
||||
All complex data is exchanged as JSON:
|
||||
|
||||
**HTTPTransportIntercept Input:**
|
||||
- `ctx`: `{"request_id": "..."}` (context info)
|
||||
- `req`: HTTP request JSON
|
||||
```json
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/v1/chat/completions",
|
||||
"headers": {"Content-Type": "application/json"},
|
||||
"query": {},
|
||||
"body": "base64-encoded-body"
|
||||
}
|
||||
```
|
||||
|
||||
**HTTPTransportIntercept Output:**
|
||||
```json
|
||||
{
|
||||
"response": null,
|
||||
"error": ""
|
||||
}
|
||||
```
|
||||
To short-circuit, return a response:
|
||||
```json
|
||||
{
|
||||
"response": {
|
||||
"status_code": 401,
|
||||
"headers": {"Content-Type": "application/json"},
|
||||
"body": "base64-encoded-body"
|
||||
},
|
||||
"error": ""
|
||||
}
|
||||
```
|
||||
|
||||
**PreLLMHook Input:**
|
||||
- `ctx`: `{"request_id": "..."}` (context info)
|
||||
- `req`: Bifrost request JSON
|
||||
|
||||
**PreLLMHook Output:**
|
||||
```json
|
||||
{
|
||||
"request": { ... },
|
||||
"short_circuit": null,
|
||||
"error": ""
|
||||
}
|
||||
```
|
||||
|
||||
**PostLLMHook Input:**
|
||||
- `ctx`: Context JSON
|
||||
- `resp`: Bifrost response JSON
|
||||
- `err`: Bifrost error JSON (or null)
|
||||
|
||||
**PostLLMHook Output:**
|
||||
```json
|
||||
{
|
||||
"response": { ... },
|
||||
"bifrost_error": null,
|
||||
"error": ""
|
||||
}
|
||||
```
|
||||
|
||||
## Usage with Bifrost
|
||||
|
||||
Configure the plugin in your Bifrost config:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": [
|
||||
{
|
||||
"path": "/path/to/hello-world.wasm",
|
||||
"name": "hello-world-wasm",
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Or load from URL:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": [
|
||||
{
|
||||
"path": "https://example.com/plugins/hello-world.wasm",
|
||||
"name": "hello-world-wasm",
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
WASM plugins have some limitations compared to native `.so` plugins:
|
||||
|
||||
1. **Performance**: JSON serialization/deserialization adds overhead compared to native plugins.
|
||||
|
||||
2. **Memory**: WASM modules have a linear memory model with limited addressing.
|
||||
|
||||
3. **TinyGo Constraints**: Some Go standard library features are not available in TinyGo.
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Cross-platform**: Single `.wasm` binary runs on any OS/architecture
|
||||
2. **Security**: WASM provides sandboxed execution
|
||||
3. **No CGO**: Pure Go compilation, no C dependencies needed on the host
|
||||
4. **Portability**: Easy to distribute and deploy
|
||||
5. **Full feature parity**: HTTP transport intercept, PreLLMHook, and PostLLMHook all supported
|
||||
30
examples/plugins/hello-world-wasm-go/go.mod
Normal file
30
examples/plugins/hello-world-wasm-go/go.mod
Normal file
@@ -0,0 +1,30 @@
|
||||
module github.com/maximhq/bifrost/examples/plugins/hello-world-wasm
|
||||
|
||||
go 1.26.2
|
||||
|
||||
require github.com/maximhq/bifrost/core v1.4.17
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.2 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/mailru/easyjson v0.9.1 // indirect
|
||||
github.com/mark3labs/mcp-go v0.43.2 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.68.0 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
golang.org/x/arch v0.23.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
83
examples/plugins/hello-world-wasm-go/go.sum
Normal file
83
examples/plugins/hello-world-wasm-go/go.sum
Normal file
@@ -0,0 +1,83 @@
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
|
||||
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
|
||||
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
||||
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
|
||||
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I=
|
||||
github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
|
||||
github.com/maximhq/bifrost/core v1.3.3 h1:r2llMAfzIHeSxwY2L55UaSOsY17JSg5zYcqF2JtaRVY=
|
||||
github.com/maximhq/bifrost/core v1.3.3/go.mod h1:abKQRnJQPZz8/UMxCcbuNHEyq19Db+IX4KlGJdlLY8E=
|
||||
github.com/maximhq/bifrost/core v1.4.17/go.mod h1:O6VEP2MHkQgo1iLYoxGQ7a+3VBBlHoETCH+pOR6Q5X8=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
|
||||
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
||||
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
226
examples/plugins/hello-world-wasm-go/main.go
Normal file
226
examples/plugins/hello-world-wasm-go/main.go
Normal file
@@ -0,0 +1,226 @@
|
||||
// Package main provides a hello-world WASM plugin example for Bifrost.
|
||||
// This plugin demonstrates the basic structure and exports required for WASM plugins.
|
||||
//
|
||||
// Build with TinyGo:
|
||||
//
|
||||
// tinygo build -o build/hello-world.wasm -target=wasi -scheduler=none main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Plugin Exports
|
||||
// ============================================================================
|
||||
|
||||
//export get_name
|
||||
func get_name() uint64 {
|
||||
return writeBytes([]byte("Hello World WASM Plugin"))
|
||||
}
|
||||
|
||||
//export init
|
||||
func init_plugin(configPtr, configLen uint32) int32 {
|
||||
println("WASM Plugin: Init called")
|
||||
if configLen > 0 {
|
||||
configData := readInput(configPtr, configLen)
|
||||
println("WASM Plugin: Config received:", string(configData))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
//export http_intercept
|
||||
func http_intercept(inputPtr, inputLen uint32) uint64 {
|
||||
println("WASM Plugin: http_intercept called")
|
||||
|
||||
inputData := readInput(inputPtr, inputLen)
|
||||
if inputData == nil {
|
||||
return writeError("no input data")
|
||||
}
|
||||
|
||||
// Parse input
|
||||
var input HTTPInterceptInput
|
||||
if err := json.Unmarshal(inputData, &input); err != nil {
|
||||
println("WASM Plugin: parse error:", err.Error())
|
||||
return writeError("parse error: " + err.Error())
|
||||
}
|
||||
|
||||
// Log parsed data
|
||||
println("WASM Plugin: HTTP", input.Request.Method, input.Request.Path)
|
||||
if ct, ok := input.Request.Headers["content-type"]; ok {
|
||||
println("WASM Plugin: Content-Type:", ct)
|
||||
}
|
||||
input.Context["from-http"] = "123"
|
||||
// Return pass-through
|
||||
output := HTTPInterceptOutput{
|
||||
Context: input.Context,
|
||||
Request: input.Request,
|
||||
HasResponse: false,
|
||||
Error: "",
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(output)
|
||||
return writeBytes(data)
|
||||
}
|
||||
|
||||
//export pre_hook
|
||||
func pre_hook(inputPtr, inputLen uint32) uint64 {
|
||||
println("WASM Plugin: pre_hook called")
|
||||
|
||||
inputData := readInput(inputPtr, inputLen)
|
||||
if inputData == nil {
|
||||
return writePreHookError("no input data")
|
||||
}
|
||||
|
||||
println("WASM Plugin: Pre-hook input:", string(inputData))
|
||||
|
||||
// Parse input
|
||||
var input PreHookInput
|
||||
if err := json.Unmarshal(inputData, &input); err != nil {
|
||||
println("WASM Plugin: parse error:", err.Error())
|
||||
return writePreHookError("parse error: " + err.Error())
|
||||
}
|
||||
|
||||
// Print existing context
|
||||
for k, v := range input.Context {
|
||||
println("WASM Plugin: Context", k, "=", v)
|
||||
}
|
||||
|
||||
input.Context["from-pre-hook"] = "789"
|
||||
|
||||
// Return with custom context value
|
||||
output := PreHookOutput{
|
||||
Context: input.Context,
|
||||
Request: input.Request,
|
||||
HasShortCircuit: false,
|
||||
Error: "",
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(output)
|
||||
return writeBytes(data)
|
||||
}
|
||||
|
||||
//export post_hook
|
||||
func post_hook(inputPtr, inputLen uint32) uint64 {
|
||||
println("WASM Plugin: post_hook called")
|
||||
|
||||
inputData := readInput(inputPtr, inputLen)
|
||||
if inputData == nil {
|
||||
return writePostHookError("no input data")
|
||||
}
|
||||
|
||||
// Parse input
|
||||
var input PostHookInput
|
||||
if err := json.Unmarshal(inputData, &input); err != nil {
|
||||
println("WASM Plugin: parse error:", err.Error())
|
||||
return writePostHookError("parse error: " + err.Error())
|
||||
}
|
||||
|
||||
println("WASM Plugin: Post-hook input:", string(inputData))
|
||||
// Print existing context
|
||||
for k, v := range input.Context {
|
||||
println("WASM Plugin: Context", k, "=", v)
|
||||
}
|
||||
|
||||
// Parse response for logging
|
||||
|
||||
if processed, ok := input.Context["wasm_plugin_processed"].(bool); ok && processed {
|
||||
println("WASM Plugin: Pre-hook context value present")
|
||||
}
|
||||
|
||||
input.Context["from-post-hook"] = "456"
|
||||
// Return pass-through
|
||||
output := PostHookOutput{
|
||||
Context: input.Context,
|
||||
Response: input.Response,
|
||||
Error: input.Error,
|
||||
HasError: false,
|
||||
HookError: "",
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(output)
|
||||
return writeBytes(data)
|
||||
}
|
||||
|
||||
//export http_stream_chunk_hook
|
||||
func http_stream_chunk_hook(inputPtr, inputLen uint32) uint64 {
|
||||
println("WASM Plugin: http_stream_chunk_hook called")
|
||||
|
||||
inputData := readInput(inputPtr, inputLen)
|
||||
if inputData == nil {
|
||||
return writeStreamChunkError("no input data")
|
||||
}
|
||||
|
||||
// Parse input
|
||||
var input HTTPStreamChunkHookInput
|
||||
if err := json.Unmarshal(inputData, &input); err != nil {
|
||||
println("WASM Plugin: parse error:", err.Error())
|
||||
return writeStreamChunkError("parse error: " + err.Error())
|
||||
}
|
||||
|
||||
println("WASM Plugin: Stream chunk received")
|
||||
|
||||
// Add context value
|
||||
input.Context["from-stream-chunk"] = "wasm-plugin"
|
||||
|
||||
// Pass through chunk unchanged
|
||||
output := HTTPStreamChunkHookOutput{
|
||||
Context: input.Context,
|
||||
Chunk: input.Chunk,
|
||||
HasChunk: true,
|
||||
Skip: false,
|
||||
Error: "",
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(output)
|
||||
return writeBytes(data)
|
||||
}
|
||||
|
||||
//export cleanup
|
||||
func cleanup() int32 {
|
||||
println("WASM Plugin: Cleanup called")
|
||||
return 0
|
||||
}
|
||||
|
||||
// Helper functions for error responses
|
||||
func writeError(msg string) uint64 {
|
||||
output := HTTPInterceptOutput{HasResponse: false, Error: msg}
|
||||
data, _ := json.Marshal(output)
|
||||
return writeBytes(data)
|
||||
}
|
||||
|
||||
func writePreHookError(msg string) uint64 {
|
||||
output := PreHookOutput{
|
||||
Context: map[string]interface{}{},
|
||||
Request: nil,
|
||||
HasShortCircuit: false,
|
||||
Error: msg,
|
||||
}
|
||||
data, _ := json.Marshal(output)
|
||||
return writeBytes(data)
|
||||
}
|
||||
|
||||
func writePostHookError(msg string) uint64 {
|
||||
output := PostHookOutput{
|
||||
Context: map[string]interface{}{},
|
||||
Response: nil,
|
||||
HasError: false,
|
||||
HookError: msg,
|
||||
}
|
||||
data, _ := json.Marshal(output)
|
||||
return writeBytes(data)
|
||||
}
|
||||
|
||||
func writeStreamChunkError(msg string) uint64 {
|
||||
output := HTTPStreamChunkHookOutput{
|
||||
Context: map[string]interface{}{},
|
||||
Chunk: nil,
|
||||
HasChunk: false,
|
||||
Skip: false,
|
||||
Error: msg,
|
||||
}
|
||||
data, _ := json.Marshal(output)
|
||||
return writeBytes(data)
|
||||
}
|
||||
|
||||
func main() {}
|
||||
89
examples/plugins/hello-world-wasm-go/memory.go
Normal file
89
examples/plugins/hello-world-wasm-go/memory.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package main
|
||||
|
||||
import "unsafe"
|
||||
|
||||
// ============================================================================
|
||||
// Memory Management
|
||||
// ============================================================================
|
||||
|
||||
// heapSize is the fixed size of the pre-allocated heap.
|
||||
// This must be large enough to handle all allocations during the plugin lifetime.
|
||||
// The heap is never reallocated to ensure all pointers remain valid.
|
||||
const heapSize = 4 * 1024 * 1024 // 4MB fixed heap
|
||||
|
||||
// heapBase is a fixed-size buffer that is never reallocated.
|
||||
// All allocations come from this buffer to ensure pointer stability.
|
||||
var heapBase []byte
|
||||
|
||||
// heapOffset tracks the next available position in heapBase.
|
||||
var heapOffset uint32 = 0
|
||||
|
||||
// heapBasePtr caches the base pointer of heapBase for efficient offset-to-pointer conversion.
|
||||
var heapBasePtr uintptr
|
||||
|
||||
func init() {
|
||||
// Pre-allocate the fixed heap once at startup.
|
||||
// This ensures heapBase is never reallocated after pointers are handed out.
|
||||
heapBase = make([]byte, heapSize)
|
||||
heapBasePtr = uintptr(unsafe.Pointer(&heapBase[0]))
|
||||
}
|
||||
|
||||
//export plugin_malloc
|
||||
func plugin_malloc(size uint32) uint32 {
|
||||
if size == 0 {
|
||||
return 0
|
||||
}
|
||||
// Align to 8-byte boundary
|
||||
alignedSize := (size + 7) &^ 7
|
||||
// Check if we have enough space (no reallocation allowed)
|
||||
if heapOffset+alignedSize > uint32(len(heapBase)) {
|
||||
// Allocation failure - heap exhausted
|
||||
// Return 0 to indicate failure rather than reallocating
|
||||
return 0
|
||||
}
|
||||
// Return pointer to the allocated region
|
||||
ptr := uint32(heapBasePtr + uintptr(heapOffset))
|
||||
heapOffset += alignedSize
|
||||
return ptr
|
||||
}
|
||||
|
||||
//export plugin_free
|
||||
func plugin_free(ptr uint32) {
|
||||
// No-op: we use a simple bump allocator without individual frees.
|
||||
// Memory is reclaimed when the plugin is unloaded.
|
||||
}
|
||||
|
||||
// plugin_reset resets the heap allocator, allowing memory to be reused.
|
||||
// This should only be called when no allocated memory is in use.
|
||||
//
|
||||
//export plugin_reset
|
||||
func plugin_reset() {
|
||||
heapOffset = 0
|
||||
}
|
||||
|
||||
func packResult(ptr uint32, length uint32) uint64 {
|
||||
return (uint64(ptr) << 32) | uint64(length)
|
||||
}
|
||||
|
||||
func writeBytes(data []byte) uint64 {
|
||||
if len(data) == 0 {
|
||||
return 0
|
||||
}
|
||||
// Allocate from the stable heap
|
||||
ptr := plugin_malloc(uint32(len(data)))
|
||||
if ptr == 0 {
|
||||
// Allocation failed
|
||||
return 0
|
||||
}
|
||||
// Copy data into the allocated region
|
||||
offset := ptr - uint32(heapBasePtr)
|
||||
copy(heapBase[offset:offset+uint32(len(data))], data)
|
||||
return packResult(ptr, uint32(len(data)))
|
||||
}
|
||||
|
||||
func readInput(ptr, length uint32) []byte {
|
||||
if length == 0 {
|
||||
return nil
|
||||
}
|
||||
return unsafe.Slice((*byte)(unsafe.Pointer(uintptr(ptr))), length)
|
||||
}
|
||||
70
examples/plugins/hello-world-wasm-go/types.go
Normal file
70
examples/plugins/hello-world-wasm-go/types.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package main
|
||||
|
||||
import "github.com/maximhq/bifrost/core/schemas"
|
||||
|
||||
// ============================================================================
|
||||
// Input/Output Structs
|
||||
// ============================================================================
|
||||
|
||||
// HTTPInterceptInput is the input for http_intercept
|
||||
type HTTPInterceptInput struct {
|
||||
Context map[string]interface{} `json:"context"`
|
||||
Request *schemas.HTTPRequest `json:"request,omitempty"`
|
||||
}
|
||||
|
||||
// HTTPInterceptOutput is the output for http_intercept
|
||||
type HTTPInterceptOutput struct {
|
||||
Context map[string]interface{} `json:"context"`
|
||||
Request *schemas.HTTPRequest `json:"request,omitempty"`
|
||||
Response *schemas.HTTPResponse `json:"response,omitempty"`
|
||||
HasResponse bool `json:"has_response"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// PreHookInput is the input for pre_hook
|
||||
type PreHookInput struct {
|
||||
Context map[string]interface{} `json:"context"`
|
||||
Request *schemas.BifrostRequest `json:"request,omitempty"` // Keep raw for pass-through
|
||||
}
|
||||
|
||||
// PreHookOutput is the output for pre_hook
|
||||
type PreHookOutput struct {
|
||||
Context map[string]interface{} `json:"context"`
|
||||
Request *schemas.BifrostRequest `json:"request,omitempty"`
|
||||
ShortCircuit *schemas.LLMPluginShortCircuit `json:"short_circuit,omitempty"`
|
||||
HasShortCircuit bool `json:"has_short_circuit"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// PostHookInput is the input for post_hook
|
||||
type PostHookInput struct {
|
||||
Context map[string]interface{} `json:"context"`
|
||||
Response *schemas.BifrostResponse `json:"response,omitempty"`
|
||||
Error *schemas.BifrostError `json:"error,omitempty"`
|
||||
HasError bool `json:"has_error"`
|
||||
}
|
||||
|
||||
// PostHookOutput is the output for post_hook
|
||||
type PostHookOutput struct {
|
||||
Context map[string]interface{} `json:"context"`
|
||||
Response *schemas.BifrostResponse `json:"response,omitempty"`
|
||||
Error *schemas.BifrostError `json:"error,omitempty"`
|
||||
HasError bool `json:"has_error"`
|
||||
HookError string `json:"hook_error"`
|
||||
}
|
||||
|
||||
// HTTPStreamChunkHookInput is the input for http_stream_chunk_hook
|
||||
type HTTPStreamChunkHookInput struct {
|
||||
Context map[string]interface{} `json:"context"`
|
||||
Request *schemas.HTTPRequest `json:"request,omitempty"`
|
||||
Chunk *schemas.BifrostStreamChunk `json:"chunk,omitempty"`
|
||||
}
|
||||
|
||||
// HTTPStreamChunkHookOutput is the output for http_stream_chunk_hook
|
||||
type HTTPStreamChunkHookOutput struct {
|
||||
Context map[string]interface{} `json:"context"`
|
||||
Chunk *schemas.BifrostStreamChunk `json:"chunk,omitempty"`
|
||||
HasChunk bool `json:"has_chunk"`
|
||||
Skip bool `json:"skip"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
Reference in New Issue
Block a user