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"`
|
||||
}
|
||||
107
examples/plugins/hello-world-wasm-rust/Cargo.lock
generated
Normal file
107
examples/plugins/hello-world-wasm-rust/Cargo.lock
generated
Normal file
@@ -0,0 +1,107 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "hello-world-wasm-rust"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.105"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8"
|
||||
18
examples/plugins/hello-world-wasm-rust/Cargo.toml
Normal file
18
examples/plugins/hello-world-wasm-rust/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "hello-world-wasm-rust"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "A minimal Bifrost WASM plugin example in Rust"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
lto = true
|
||||
strip = true
|
||||
panic = "abort"
|
||||
80
examples/plugins/hello-world-wasm-rust/Makefile
Normal file
80
examples/plugins/hello-world-wasm-rust/Makefile
Normal file
@@ -0,0 +1,80 @@
|
||||
.PHONY: all build build-optimized clean help check-rust
|
||||
|
||||
# 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
|
||||
TARGET = wasm32-unknown-unknown
|
||||
|
||||
help: ## Show this help message
|
||||
@echo '$(COLOR_BOLD)Hello World WASM Plugin (Rust)$(COLOR_RESET)'
|
||||
@echo ''
|
||||
@echo '$(COLOR_BOLD)Usage:$(COLOR_RESET) make [target]'
|
||||
@echo ''
|
||||
@echo '$(COLOR_BOLD)Prerequisites:$(COLOR_RESET)'
|
||||
@echo ' - Rust with wasm32-unknown-unknown target'
|
||||
@echo ' rustup target add wasm32-unknown-unknown'
|
||||
@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-rust: ## Check if Rust and WASM target are installed
|
||||
@which cargo > /dev/null 2>&1 || (echo "$(COLOR_ERROR)Error: Rust/Cargo is not installed$(COLOR_RESET)"; \
|
||||
echo "$(COLOR_INFO)Install Rust: https://rustup.rs/$(COLOR_RESET)"; \
|
||||
exit 1)
|
||||
@rustup target list --installed | grep -q $(TARGET) || (echo "$(COLOR_ERROR)Error: WASM target not installed$(COLOR_RESET)"; \
|
||||
echo "$(COLOR_INFO)Install with: rustup target add $(TARGET)$(COLOR_RESET)"; \
|
||||
exit 1)
|
||||
@echo "$(COLOR_SUCCESS)✓ Rust found: $$(rustc --version)$(COLOR_RESET)"
|
||||
@echo "$(COLOR_SUCCESS)✓ WASM target: $(TARGET)$(COLOR_RESET)"
|
||||
|
||||
build: check-rust ## Build the WASM plugin
|
||||
@mkdir -p $(OUTPUT_DIR)
|
||||
@echo "$(COLOR_INFO)Building WASM plugin...$(COLOR_RESET)"
|
||||
cargo build --release --target $(TARGET)
|
||||
@cp target/$(TARGET)/release/hello_world_wasm_rust.wasm $(OUTPUT)
|
||||
@echo "$(COLOR_SUCCESS)✓ Plugin built successfully: $(OUTPUT)$(COLOR_RESET)"
|
||||
@ls -lh $(OUTPUT) | awk '{print " Size: " $$5}'
|
||||
|
||||
build-optimized: check-rust ## Build with wasm-opt optimization (requires wasm-opt)
|
||||
@mkdir -p $(OUTPUT_DIR)
|
||||
@echo "$(COLOR_INFO)Building optimized WASM plugin...$(COLOR_RESET)"
|
||||
cargo build --release --target $(TARGET)
|
||||
@cp target/$(TARGET)/release/hello_world_wasm_rust.wasm $(OUTPUT)
|
||||
@if which wasm-opt > /dev/null 2>&1; then \
|
||||
echo "$(COLOR_INFO)Running wasm-opt...$(COLOR_RESET)"; \
|
||||
wasm-opt -Os -o $(OUTPUT) $(OUTPUT); \
|
||||
else \
|
||||
echo "$(COLOR_WARNING)wasm-opt not found, skipping optimization$(COLOR_RESET)"; \
|
||||
fi
|
||||
@echo "$(COLOR_SUCCESS)✓ Plugin built: $(OUTPUT)$(COLOR_RESET)"
|
||||
@ls -lh $(OUTPUT) | awk '{print " Size: " $$5}'
|
||||
|
||||
clean: ## Remove build artifacts
|
||||
@echo "$(COLOR_INFO)Cleaning build artifacts...$(COLOR_RESET)"
|
||||
@cargo clean
|
||||
@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: $(TARGET)"
|
||||
@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
|
||||
528
examples/plugins/hello-world-wasm-rust/README.md
Normal file
528
examples/plugins/hello-world-wasm-rust/README.md
Normal file
@@ -0,0 +1,528 @@
|
||||
# Bifrost WASM Plugin (Rust)
|
||||
|
||||
A comprehensive example of a Bifrost plugin written in Rust and compiled to WebAssembly. This plugin demonstrates proper structure definitions with serde, JSON parsing, context handling, and request/response modification patterns.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Rust Installation
|
||||
|
||||
Install Rust from [rustup.rs](https://rustup.rs/) and add the WASM target:
|
||||
|
||||
```bash
|
||||
# Install Rust (if not already installed)
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
|
||||
# Add WASM target
|
||||
rustup target add wasm32-unknown-unknown
|
||||
```
|
||||
|
||||
### Optional: wasm-opt
|
||||
|
||||
For smaller binaries, install `wasm-opt` from [binaryen](https://github.com/WebAssembly/binaryen):
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
brew install binaryen
|
||||
|
||||
# Linux
|
||||
apt install binaryen
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
# Build the WASM plugin
|
||||
make build
|
||||
|
||||
# Build with wasm-opt optimization
|
||||
make build-optimized
|
||||
|
||||
# Clean build artifacts
|
||||
make clean
|
||||
```
|
||||
|
||||
The compiled plugin will be at `build/hello-world.wasm`.
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── lib.rs # Plugin implementation (hooks)
|
||||
├── memory.rs # Memory management utilities
|
||||
└── types.rs # Type definitions (mirrors Go SDK)
|
||||
```
|
||||
|
||||
## Plugin Structure
|
||||
|
||||
WASM plugins must export the following functions:
|
||||
|
||||
| Export | Signature | Description |
|
||||
|--------|-----------|-------------|
|
||||
| `malloc` | `(size: u32) -> u32` | Allocate memory for host to write data |
|
||||
| `free` | `(ptr: u32, size: u32)` | Free allocated memory |
|
||||
| `get_name` | `() -> u64` | Returns packed ptr+len of plugin name |
|
||||
| `init` | `(config_ptr, config_len: u32) -> i32` | Initialize with config (optional) |
|
||||
| `http_intercept` | `(input_ptr, input_len: u32) -> u64` | HTTP transport intercept |
|
||||
| `pre_hook` | `(input_ptr, input_len: u32) -> u64` | Pre-request hook |
|
||||
| `post_hook` | `(input_ptr, input_len: u32) -> u64` | Post-response hook |
|
||||
| `cleanup` | `() -> i32` | Cleanup resources (0 = success) |
|
||||
|
||||
### 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 Structures
|
||||
|
||||
This plugin uses `serde` with derive macros for JSON serialization. All structures mirror the Go SDK types:
|
||||
|
||||
### Context
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct BifrostContext {
|
||||
pub request_id: Option<String>,
|
||||
|
||||
// Custom values via HashMap
|
||||
#[serde(flatten)]
|
||||
pub values: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
impl BifrostContext {
|
||||
pub fn set_value(&mut self, key: &str, value: impl Into<serde_json::Value>);
|
||||
pub fn get_string(&self, key: &str) -> Option<&str>;
|
||||
pub fn get_bool(&self, key: &str) -> Option<bool>;
|
||||
}
|
||||
```
|
||||
|
||||
### HTTP Transport Types
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct HTTPRequest {
|
||||
pub method: String,
|
||||
pub path: String,
|
||||
pub headers: HashMap<String, String>,
|
||||
pub query: HashMap<String, String>,
|
||||
pub body: String, // base64 encoded
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct HTTPResponse {
|
||||
pub status_code: i32,
|
||||
pub headers: HashMap<String, String>,
|
||||
pub body: String, // base64 encoded
|
||||
}
|
||||
```
|
||||
|
||||
### Chat Completion Types
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ChatMessageRole {
|
||||
User,
|
||||
Assistant,
|
||||
System,
|
||||
Tool,
|
||||
Developer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum ChatMessageContent {
|
||||
Text(String),
|
||||
Blocks(Vec<ChatContentBlock>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ChatMessage {
|
||||
pub role: ChatMessageRole,
|
||||
pub content: Option<ChatMessageContent>,
|
||||
pub name: Option<String>,
|
||||
pub tool_call_id: Option<String>,
|
||||
pub tool_calls: Option<Vec<ToolCall>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ChatParameters {
|
||||
pub temperature: Option<f64>,
|
||||
pub max_completion_tokens: Option<i32>,
|
||||
pub top_p: Option<f64>,
|
||||
pub frequency_penalty: Option<f64>,
|
||||
pub presence_penalty: Option<f64>,
|
||||
pub stop: Option<Vec<String>>,
|
||||
pub tools: Option<Vec<ChatTool>>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub extra: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct BifrostChatRequest {
|
||||
pub provider: String,
|
||||
pub model: String,
|
||||
pub input: Vec<ChatMessage>,
|
||||
pub params: Option<ChatParameters>,
|
||||
pub fallbacks: Option<Vec<Fallback>>,
|
||||
}
|
||||
```
|
||||
|
||||
### Response Types
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct LLMUsage {
|
||||
pub prompt_tokens: i32,
|
||||
pub completion_tokens: i32,
|
||||
pub total_tokens: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ResponseChoice {
|
||||
pub index: i32,
|
||||
pub message: Option<ChatMessage>,
|
||||
pub delta: Option<ChatMessage>,
|
||||
pub finish_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct BifrostChatResponse {
|
||||
pub id: String,
|
||||
pub model: String,
|
||||
pub choices: Vec<ResponseChoice>,
|
||||
pub usage: Option<LLMUsage>,
|
||||
pub created: Option<i64>,
|
||||
pub object: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct BifrostResponse {
|
||||
pub chat_response: Option<BifrostChatResponse>,
|
||||
}
|
||||
```
|
||||
|
||||
### Error Types
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ErrorField {
|
||||
pub message: String,
|
||||
#[serde(rename = "type")]
|
||||
pub error_type: Option<String>,
|
||||
pub code: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct BifrostError {
|
||||
pub error: ErrorField,
|
||||
pub status_code: Option<i32>,
|
||||
pub allow_fallbacks: Option<bool>,
|
||||
}
|
||||
|
||||
impl BifrostError {
|
||||
pub fn new(message: &str) -> Self;
|
||||
pub fn with_type(self, error_type: &str) -> Self;
|
||||
pub fn with_code(self, code: &str) -> Self;
|
||||
pub fn with_status(self, status: i32) -> Self;
|
||||
}
|
||||
```
|
||||
|
||||
### Short Circuit
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct LLMPluginShortCircuit {
|
||||
pub response: Option<BifrostResponse>,
|
||||
pub error: Option<BifrostError>,
|
||||
}
|
||||
```
|
||||
|
||||
## Hook Input/Output Structures
|
||||
|
||||
### http_intercept
|
||||
|
||||
**Input:**
|
||||
```json
|
||||
{
|
||||
"context": { "request_id": "abc-123" },
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"path": "/v1/chat/completions",
|
||||
"headers": { "Content-Type": "application/json" },
|
||||
"query": {},
|
||||
"body": "<base64-encoded>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```json
|
||||
{
|
||||
"context": { "request_id": "abc-123" },
|
||||
"request": {},
|
||||
"response": { "status_code": 200, "headers": {}, "body": "<base64>" },
|
||||
"has_response": false,
|
||||
"error": ""
|
||||
}
|
||||
```
|
||||
|
||||
### pre_hook
|
||||
|
||||
**Input:**
|
||||
```json
|
||||
{
|
||||
"context": { "request_id": "abc-123" },
|
||||
"request": {
|
||||
"provider": "openai",
|
||||
"model": "gpt-4",
|
||||
"input": [{ "role": "user", "content": "Hello" }],
|
||||
"params": { "temperature": 0.7 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```json
|
||||
{
|
||||
"context": { "request_id": "abc-123", "plugin_processed": true },
|
||||
"request": {},
|
||||
"short_circuit": {
|
||||
"response": { "chat_response": { ... } }
|
||||
},
|
||||
"has_short_circuit": false,
|
||||
"error": ""
|
||||
}
|
||||
```
|
||||
|
||||
### post_hook
|
||||
|
||||
**Input:**
|
||||
```json
|
||||
{
|
||||
"context": { "request_id": "abc-123", "plugin_processed": true },
|
||||
"response": {
|
||||
"chat_response": {
|
||||
"id": "chatcmpl-123",
|
||||
"model": "gpt-4",
|
||||
"choices": [{ "index": 0, "message": { "role": "assistant", "content": "Hi!" } }],
|
||||
"usage": { "prompt_tokens": 5, "completion_tokens": 10, "total_tokens": 15 }
|
||||
}
|
||||
},
|
||||
"error": {},
|
||||
"has_error": false
|
||||
}
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```json
|
||||
{
|
||||
"context": { "request_id": "abc-123", "post_hook_completed": true },
|
||||
"response": {},
|
||||
"error": {},
|
||||
"has_error": false,
|
||||
"hook_error": ""
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Modifying Context
|
||||
|
||||
```rust
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pre_hook(input_ptr: u32, input_len: u32) -> u64 {
|
||||
let input_str = read_string(input_ptr, input_len);
|
||||
let input: PreHookInput = serde_json::from_str(&input_str).unwrap();
|
||||
|
||||
let mut output = PreHookOutput {
|
||||
context: input.context.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Add custom values to context
|
||||
output.context.set_value("plugin_processed", serde_json::json!(true));
|
||||
output.context.set_value("plugin_name", serde_json::json!("my-rust-plugin"));
|
||||
|
||||
write_string(&serde_json::to_string(&output).unwrap())
|
||||
}
|
||||
```
|
||||
|
||||
### Short-Circuit with Mock Response
|
||||
|
||||
```rust
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pre_hook(input_ptr: u32, input_len: u32) -> u64 {
|
||||
let input_str = read_string(input_ptr, input_len);
|
||||
let input: PreHookInput = serde_json::from_str(&input_str).unwrap();
|
||||
|
||||
let (provider, model) = input.get_provider_model();
|
||||
|
||||
// Check if this should be mocked
|
||||
if model == "mock-model" {
|
||||
let mut output = PreHookOutput {
|
||||
context: input.context.clone(),
|
||||
has_short_circuit: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Build mock response
|
||||
let mock_response = BifrostResponse {
|
||||
chat_response: Some(BifrostChatResponse {
|
||||
id: format!("mock-{}", input.context.request_id.unwrap_or_default()),
|
||||
model: "mock-model".to_string(),
|
||||
choices: vec![ResponseChoice {
|
||||
index: 0,
|
||||
message: Some(ChatMessage {
|
||||
role: ChatMessageRole::Assistant,
|
||||
content: Some(ChatMessageContent::Text(
|
||||
"This is a mock response!".to_string()
|
||||
)),
|
||||
..Default::default()
|
||||
}),
|
||||
finish_reason: Some("stop".to_string()),
|
||||
..Default::default()
|
||||
}],
|
||||
usage: Some(LLMUsage {
|
||||
prompt_tokens: 10,
|
||||
completion_tokens: 15,
|
||||
total_tokens: 25,
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
output.short_circuit = Some(LLMPluginShortCircuit {
|
||||
response: Some(mock_response),
|
||||
error: None,
|
||||
});
|
||||
|
||||
return write_string(&serde_json::to_string(&output).unwrap());
|
||||
}
|
||||
|
||||
// Pass through
|
||||
let output = PreHookOutput {
|
||||
context: input.context,
|
||||
..Default::default()
|
||||
};
|
||||
write_string(&serde_json::to_string(&output).unwrap())
|
||||
}
|
||||
```
|
||||
|
||||
### Short-Circuit with Error
|
||||
|
||||
```rust
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pre_hook(input_ptr: u32, input_len: u32) -> u64 {
|
||||
let input_str = read_string(input_ptr, input_len);
|
||||
let input: PreHookInput = serde_json::from_str(&input_str).unwrap();
|
||||
|
||||
// Check rate limit (example)
|
||||
if should_rate_limit(&input.context) {
|
||||
let mut output = PreHookOutput {
|
||||
context: input.context.clone(),
|
||||
has_short_circuit: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
output.short_circuit = Some(LLMPluginShortCircuit {
|
||||
response: None,
|
||||
error: Some(
|
||||
BifrostError::new("Rate limit exceeded")
|
||||
.with_type("rate_limit")
|
||||
.with_code("429")
|
||||
.with_status(429)
|
||||
),
|
||||
});
|
||||
|
||||
return write_string(&serde_json::to_string(&output).unwrap());
|
||||
}
|
||||
|
||||
// Pass through
|
||||
let output = PreHookOutput {
|
||||
context: input.context,
|
||||
..Default::default()
|
||||
};
|
||||
write_string(&serde_json::to_string(&output).unwrap())
|
||||
}
|
||||
```
|
||||
|
||||
### Modifying Responses in post_hook
|
||||
|
||||
```rust
|
||||
#[no_mangle]
|
||||
pub extern "C" fn post_hook(input_ptr: u32, input_len: u32) -> u64 {
|
||||
let input_str = read_string(input_ptr, input_len);
|
||||
let input: PostHookInput = serde_json::from_str(&input_str).unwrap();
|
||||
|
||||
let mut output = PostHookOutput {
|
||||
context: input.context.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Handle errors
|
||||
if input.has_error {
|
||||
output.has_error = true;
|
||||
output.error = input.error.clone();
|
||||
|
||||
// Optionally modify the error
|
||||
if let Some(mut error) = input.parse_error() {
|
||||
error.error.message = format!("{} (via rust plugin)", error.error.message);
|
||||
output.error = serde_json::to_value(&error).unwrap_or_default();
|
||||
}
|
||||
|
||||
return write_string(&serde_json::to_string(&output).unwrap());
|
||||
}
|
||||
|
||||
// Pass through or modify response
|
||||
if let Some(mut response) = input.parse_response() {
|
||||
if let Some(ref mut chat) = response.chat_response {
|
||||
// Add a marker to the model name
|
||||
chat.model = format!("{} (via rust-wasm)", chat.model);
|
||||
}
|
||||
output.response = serde_json::to_value(&response).unwrap_or_default();
|
||||
}
|
||||
|
||||
write_string(&serde_json::to_string(&output).unwrap())
|
||||
}
|
||||
```
|
||||
|
||||
## Usage with Bifrost
|
||||
|
||||
Configure the plugin in your Bifrost config:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": [
|
||||
{
|
||||
"path": "/path/to/hello-world.wasm",
|
||||
"name": "hello-world-wasm-rust",
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"custom_option": "value"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
The plugin includes unit tests that can be run with:
|
||||
|
||||
```bash
|
||||
cargo test
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Performance**: Rust compiles to highly optimized WASM
|
||||
2. **Safety**: Memory safety without garbage collection
|
||||
3. **Small binaries**: Rust WASM binaries are typically very small
|
||||
4. **Cross-platform**: Single `.wasm` binary runs on any OS/architecture
|
||||
5. **Security**: WASM provides sandboxed execution
|
||||
6. **Type Safety**: Strongly typed structures with serde derive macros
|
||||
7. **Excellent JSON**: serde_json provides robust JSON handling
|
||||
327
examples/plugins/hello-world-wasm-rust/src/lib.rs
Normal file
327
examples/plugins/hello-world-wasm-rust/src/lib.rs
Normal file
@@ -0,0 +1,327 @@
|
||||
//! Bifrost WASM Plugin for Rust
|
||||
//!
|
||||
//! This plugin demonstrates the proper structure for parsing inputs,
|
||||
//! building responses, and handling context - similar to Go plugin patterns.
|
||||
//!
|
||||
//! Build with: cargo build --release --target wasm32-unknown-unknown
|
||||
|
||||
mod memory;
|
||||
mod types;
|
||||
|
||||
use memory::{read_string, write_string};
|
||||
use types::*;
|
||||
|
||||
// Global configuration storage
|
||||
static mut PLUGIN_CONFIG: Option<PluginConfig> = None;
|
||||
|
||||
// =============================================================================
|
||||
// Exported Plugin Functions
|
||||
// =============================================================================
|
||||
|
||||
/// Return the plugin name
|
||||
#[no_mangle]
|
||||
pub extern "C" fn get_name() -> u64 {
|
||||
write_string("hello-world-wasm-rust")
|
||||
}
|
||||
|
||||
/// Initialize the plugin with config
|
||||
/// Returns 0 on success, non-zero on error
|
||||
#[no_mangle]
|
||||
pub extern "C" fn init(config_ptr: u32, config_len: u32) -> i32 {
|
||||
let config_str = read_string(config_ptr, config_len);
|
||||
|
||||
// Parse configuration
|
||||
let config: PluginConfig = if config_str.is_empty() {
|
||||
PluginConfig::default()
|
||||
} else {
|
||||
match serde_json::from_str(&config_str) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return 1, // Config parse error
|
||||
}
|
||||
};
|
||||
|
||||
// Store configuration
|
||||
unsafe {
|
||||
PLUGIN_CONFIG = Some(config);
|
||||
}
|
||||
|
||||
0 // Success
|
||||
}
|
||||
|
||||
/// HTTP transport intercept
|
||||
/// Called at the HTTP layer before request enters Bifrost core.
|
||||
/// Can modify headers, query params, or short-circuit with a response.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn http_intercept(input_ptr: u32, input_len: u32) -> u64 {
|
||||
let input_str = read_string(input_ptr, input_len);
|
||||
|
||||
// Parse input
|
||||
let input: HTTPInterceptInput = match serde_json::from_str(&input_str) {
|
||||
Ok(i) => i,
|
||||
Err(e) => {
|
||||
// Include context around the error position for debugging
|
||||
let error_context = if let Some(col) = extract_column(&e.to_string()) {
|
||||
let start = col.saturating_sub(50);
|
||||
let end = (col + 50).min(input_str.len());
|
||||
format!(" | context: ...{}...", &input_str[start..end])
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let output = HTTPInterceptOutput {
|
||||
error: format!("Failed to parse input: {}{}", e, error_context),
|
||||
..Default::default()
|
||||
};
|
||||
return write_string(&serde_json::to_string(&output).unwrap_or_default());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Add context value like Go plugin does
|
||||
let mut context = input.context;
|
||||
context.set_value("from-http", serde_json::json!("123"));
|
||||
|
||||
// Create output with context and request preserved (pass-through)
|
||||
// Serialize request to Value to ensure proper JSON structure
|
||||
let request_value = serde_json::to_value(&input.request).ok();
|
||||
|
||||
let output = HTTPInterceptOutput {
|
||||
context: input.context,
|
||||
request: input.request,
|
||||
has_response: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Pass through
|
||||
write_string(&serde_json::to_string(&output).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Pre-request hook
|
||||
/// Called before request is sent to the provider.
|
||||
/// Can modify the request or short-circuit with a response/error.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pre_hook(input_ptr: u32, input_len: u32) -> u64 {
|
||||
let input_str = read_string(input_ptr, input_len);
|
||||
|
||||
// Parse input
|
||||
let input: PreHookInput = match serde_json::from_str(&input_str) {
|
||||
Ok(i) => i,
|
||||
Err(e) => {
|
||||
let output = PreHookOutput {
|
||||
error: format!("Failed to parse input: {}", e),
|
||||
..Default::default()
|
||||
};
|
||||
return write_string(&serde_json::to_string(&output).unwrap_or_default());
|
||||
}
|
||||
};
|
||||
|
||||
// Create output with context preserved
|
||||
let mut output = PreHookOutput {
|
||||
context: input.context.clone(),
|
||||
request: input.request.clone(),
|
||||
has_short_circuit: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Get provider and model for potential modifications
|
||||
let (_provider, model) = input.get_provider_model();
|
||||
|
||||
// Example: Short-circuit with mock response for specific model
|
||||
// Uncomment to test:
|
||||
/*
|
||||
if model == "mock-model" {
|
||||
output.has_short_circuit = true;
|
||||
|
||||
let mock_response = BifrostResponse {
|
||||
chat_response: Some(BifrostChatResponse {
|
||||
id: format!("mock-{}", input.context.request_id.unwrap_or_default()),
|
||||
model: "mock-model".to_string(),
|
||||
choices: vec![ResponseChoice {
|
||||
index: 0,
|
||||
message: Some(ChatMessage {
|
||||
role: ChatMessageRole::Assistant,
|
||||
content: Some(ChatMessageContent::Text(
|
||||
"This is a mock response from the Rust WASM plugin!".to_string()
|
||||
)),
|
||||
..Default::default()
|
||||
}),
|
||||
finish_reason: Some("stop".to_string()),
|
||||
..Default::default()
|
||||
}],
|
||||
usage: Some(LLMUsage {
|
||||
prompt_tokens: 10,
|
||||
completion_tokens: 15,
|
||||
total_tokens: 25,
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
output.short_circuit = Some(LLMPluginShortCircuit {
|
||||
response: Some(mock_response),
|
||||
error: None,
|
||||
});
|
||||
|
||||
return write_string(&serde_json::to_string(&output).unwrap_or_default());
|
||||
}
|
||||
*/
|
||||
|
||||
// Example: Short-circuit with rate limit error
|
||||
// Uncomment to test:
|
||||
/*
|
||||
if should_rate_limit(&input.context) {
|
||||
output.has_short_circuit = true;
|
||||
output.short_circuit = Some(LLMPluginShortCircuit {
|
||||
response: None,
|
||||
error: Some(
|
||||
BifrostError::new("Rate limit exceeded")
|
||||
.with_type("rate_limit")
|
||||
.with_code("429")
|
||||
.with_status(429)
|
||||
),
|
||||
});
|
||||
return write_string(&serde_json::to_string(&output).unwrap_or_default());
|
||||
}
|
||||
*/
|
||||
|
||||
// Silence unused variable warning in example code
|
||||
let _ = model;
|
||||
|
||||
// Pass through - empty request means use original
|
||||
write_string(&serde_json::to_string(&output).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Post-response hook
|
||||
/// Called after response is received from provider.
|
||||
/// Can modify the response or error.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn post_hook(input_ptr: u32, input_len: u32) -> u64 {
|
||||
let input_str = read_string(input_ptr, input_len);
|
||||
|
||||
// Parse input
|
||||
let input: PostHookInput = match serde_json::from_str(&input_str) {
|
||||
Ok(i) => i,
|
||||
Err(e) => {
|
||||
let output = PostHookOutput {
|
||||
hook_error: format!("Failed to parse input: {}", e),
|
||||
..Default::default()
|
||||
};
|
||||
return write_string(&serde_json::to_string(&output).unwrap_or_default());
|
||||
}
|
||||
};
|
||||
|
||||
// Add context value like Go plugin does
|
||||
let mut context = input.context.clone();
|
||||
context.set_value("from-post-hook", serde_json::json!("456"));
|
||||
|
||||
// Create output with context and response/error preserved (pass-through)
|
||||
// This matches Go plugin behavior exactly
|
||||
let output = PostHookOutput {
|
||||
context,
|
||||
response: Some(input.response.clone()),
|
||||
error: Some(input.error.clone()),
|
||||
has_error: input.has_error,
|
||||
hook_error: String::new(),
|
||||
};
|
||||
|
||||
// Example: Modify error message when has_error is true
|
||||
// Uncomment to test:
|
||||
/*
|
||||
if input.has_error {
|
||||
if let Some(mut error) = input.parse_error() {
|
||||
error.error.message = format!("{} (processed by Rust WASM plugin)", error.error.message);
|
||||
let mut output = output;
|
||||
output.error = Some(serde_json::to_value(&error).unwrap_or_default());
|
||||
return write_string(&serde_json::to_string(&output).unwrap_or_default());
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// Example: Modify response
|
||||
// Uncomment to test:
|
||||
/*
|
||||
if let Some(mut response) = input.parse_response() {
|
||||
// Add custom metadata, modify model name, etc.
|
||||
if let Some(ref mut chat) = response.chat_response {
|
||||
// Add a marker to the model name
|
||||
chat.model = format!("{} (via rust-wasm)", chat.model);
|
||||
}
|
||||
let mut output = output;
|
||||
output.response = Some(serde_json::to_value(&response).unwrap_or_default());
|
||||
return write_string(&serde_json::to_string(&output).unwrap_or_default());
|
||||
}
|
||||
*/
|
||||
|
||||
write_string(&serde_json::to_string(&output).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// HTTP stream chunk hook
|
||||
/// Called for each chunk during streaming responses.
|
||||
/// Can modify, skip, or stop streaming based on return values.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn http_stream_chunk_hook(input_ptr: u32, input_len: u32) -> u64 {
|
||||
let input_str = read_string(input_ptr, input_len);
|
||||
|
||||
// Parse input
|
||||
let input: HTTPStreamChunkHookInput = match serde_json::from_str(&input_str) {
|
||||
Ok(i) => i,
|
||||
Err(e) => {
|
||||
let output = HTTPStreamChunkHookOutput {
|
||||
error: format!("Failed to parse input: {}", e),
|
||||
..Default::default()
|
||||
};
|
||||
return write_string(&serde_json::to_string(&output).unwrap_or_default());
|
||||
}
|
||||
};
|
||||
|
||||
// Add context value like Go plugin does
|
||||
let mut context = input.context.clone();
|
||||
context.set_value("from-stream-chunk", serde_json::json!("rust-wasm"));
|
||||
|
||||
// Pass through chunk unchanged
|
||||
let output = HTTPStreamChunkHookOutput {
|
||||
context,
|
||||
chunk: Some(input.chunk),
|
||||
has_chunk: true,
|
||||
skip: false,
|
||||
error: String::new(),
|
||||
};
|
||||
|
||||
write_string(&serde_json::to_string(&output).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Cleanup resources
|
||||
/// Called when plugin is being unloaded.
|
||||
/// Returns 0 on success, non-zero on error
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cleanup() -> i32 {
|
||||
// Clear stored configuration
|
||||
unsafe {
|
||||
PLUGIN_CONFIG = None;
|
||||
}
|
||||
|
||||
0 // Success
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Helper Functions
|
||||
// =============================================================================
|
||||
|
||||
/// Extract column number from serde error message for debugging
|
||||
fn extract_column(error_msg: &str) -> Option<usize> {
|
||||
// Error format: "... at line X column Y"
|
||||
if let Some(idx) = error_msg.rfind("column ") {
|
||||
let col_str = &error_msg[idx + 7..];
|
||||
col_str.split_whitespace().next()?.parse().ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Example rate limit check function
|
||||
#[allow(dead_code)]
|
||||
fn should_rate_limit(_context: &BifrostContext) -> bool {
|
||||
// Implement your rate limiting logic here
|
||||
false
|
||||
}
|
||||
70
examples/plugins/hello-world-wasm-rust/src/memory.rs
Normal file
70
examples/plugins/hello-world-wasm-rust/src/memory.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
//! Memory management utilities for WASM plugins.
|
||||
//! Handles allocation, deallocation, and string read/write operations.
|
||||
|
||||
use std::alloc::{alloc, dealloc, Layout};
|
||||
use std::slice;
|
||||
|
||||
/// Pack a pointer and length into a single u64
|
||||
/// Upper 32 bits: pointer, Lower 32 bits: length
|
||||
pub fn pack_result(ptr: u32, len: u32) -> u64 {
|
||||
((ptr as u64) << 32) | (len as u64)
|
||||
}
|
||||
|
||||
/// Write a string to WASM memory and return packed pointer+length
|
||||
pub fn write_string(s: &str) -> u64 {
|
||||
if s.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
let bytes = s.as_bytes();
|
||||
let ptr = unsafe { malloc(bytes.len() as u32) };
|
||||
if ptr == 0 {
|
||||
return 0;
|
||||
}
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr as *mut u8, bytes.len());
|
||||
}
|
||||
pack_result(ptr, bytes.len() as u32)
|
||||
}
|
||||
|
||||
/// Read a string from WASM memory given pointer and length
|
||||
pub fn read_string(ptr: u32, len: u32) -> String {
|
||||
if len == 0 {
|
||||
return String::new();
|
||||
}
|
||||
let bytes = unsafe { slice::from_raw_parts(ptr as *const u8, len as usize) };
|
||||
String::from_utf8_lossy(bytes).into_owned()
|
||||
}
|
||||
|
||||
/// Allocate memory for the host to write data
|
||||
///
|
||||
/// # Safety
|
||||
/// This function is marked as safe but performs unsafe operations internally.
|
||||
/// It is intended to be called from WASM host.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn malloc(size: u32) -> u32 {
|
||||
if size == 0 {
|
||||
return 0;
|
||||
}
|
||||
let layout = match Layout::from_size_align(size as usize, 1) {
|
||||
Ok(l) => l,
|
||||
Err(_) => return 0,
|
||||
};
|
||||
unsafe { alloc(layout) as u32 }
|
||||
}
|
||||
|
||||
/// Free allocated memory
|
||||
///
|
||||
/// # Safety
|
||||
/// This function is marked as safe but performs unsafe operations internally.
|
||||
/// It is intended to be called from WASM host.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn free(ptr: u32, size: u32) {
|
||||
if ptr == 0 || size == 0 {
|
||||
return;
|
||||
}
|
||||
let layout = match Layout::from_size_align(size as usize, 1) {
|
||||
Ok(l) => l,
|
||||
Err(_) => return,
|
||||
};
|
||||
unsafe { dealloc(ptr as *mut u8, layout) }
|
||||
}
|
||||
870
examples/plugins/hello-world-wasm-rust/src/types.rs
Normal file
870
examples/plugins/hello-world-wasm-rust/src/types.rs
Normal file
@@ -0,0 +1,870 @@
|
||||
//! Type definitions for Bifrost WASM plugins.
|
||||
//! These structures mirror the Go SDK types for interoperability.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
// =============================================================================
|
||||
// Nullable Deserializers
|
||||
// =============================================================================
|
||||
|
||||
/// Helper module for deserializing fields that may be null in JSON.
|
||||
/// Go's JSON encoder outputs `null` for nil slices/maps, but Rust's serde
|
||||
/// with `#[serde(default)]` only handles missing fields, not explicit nulls.
|
||||
mod nullable {
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Deserialize a string that may be null, converting null to empty string.
|
||||
pub fn string<'de, D>(deserializer: D) -> Result<String, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Option::<String>::deserialize(deserializer).map(|opt| opt.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Deserialize a HashMap<String, String> that may be null or contain null values.
|
||||
/// Handles both `null` (entire map is null) and `{"key": null}` (value is null).
|
||||
pub fn string_map<'de, D>(deserializer: D) -> Result<HashMap<String, String>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
// First deserialize as Option<HashMap<String, Option<String>>> to handle null values
|
||||
let opt_map: Option<HashMap<String, Option<String>>> = Option::deserialize(deserializer)?;
|
||||
|
||||
match opt_map {
|
||||
None => Ok(HashMap::new()),
|
||||
Some(map) => {
|
||||
// Filter out null values and unwrap the rest
|
||||
Ok(map
|
||||
.into_iter()
|
||||
.filter_map(|(k, v)| v.map(|val| (k, val)))
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserialize an i32 that may be null, converting null to 0.
|
||||
pub fn i32_field<'de, D>(deserializer: D) -> Result<i32, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Option::<i32>::deserialize(deserializer).map(|opt| opt.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Deserialize an HTTPRequest that may be null, converting null to default.
|
||||
pub fn http_request<'de, D>(deserializer: D) -> Result<super::HTTPRequest, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Option::<super::HTTPRequest>::deserialize(deserializer).map(|opt| opt.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Deserialize a BifrostContext that may be null, converting null to default.
|
||||
pub fn context<'de, D>(deserializer: D) -> Result<super::BifrostContext, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Option::<super::BifrostContext>::deserialize(deserializer).map(|opt| opt.unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Context Structure
|
||||
// =============================================================================
|
||||
|
||||
/// BifrostContext holds request-scoped values passed between hooks.
|
||||
/// This is a dynamic map (map[string]any in Go) that can hold any JSON values.
|
||||
/// Common keys include:
|
||||
/// - request_id: Unique identifier for the request
|
||||
/// - Custom plugin values can be added and will be persisted across hooks
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(transparent)]
|
||||
pub struct BifrostContext(pub HashMap<String, serde_json::Value>);
|
||||
|
||||
impl BifrostContext {
|
||||
pub fn new() -> Self {
|
||||
Self(HashMap::new())
|
||||
}
|
||||
|
||||
/// Set a custom value in the context
|
||||
pub fn set_value(&mut self, key: &str, value: impl Into<serde_json::Value>) {
|
||||
self.0.insert(key.to_string(), value.into());
|
||||
}
|
||||
|
||||
/// Get a value from the context
|
||||
pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
|
||||
self.0.get(key)
|
||||
}
|
||||
|
||||
/// Get a string value from the context
|
||||
pub fn get_string(&self, key: &str) -> Option<&str> {
|
||||
self.0.get(key).and_then(|v| v.as_str())
|
||||
}
|
||||
|
||||
/// Get a boolean value from the context
|
||||
pub fn get_bool(&self, key: &str) -> Option<bool> {
|
||||
self.0.get(key).and_then(|v| v.as_bool())
|
||||
}
|
||||
|
||||
/// Get an i64 value from the context
|
||||
pub fn get_i64(&self, key: &str) -> Option<i64> {
|
||||
self.0.get(key).and_then(|v| v.as_i64())
|
||||
}
|
||||
|
||||
/// Check if a key exists in the context
|
||||
pub fn contains_key(&self, key: &str) -> bool {
|
||||
self.0.contains_key(key)
|
||||
}
|
||||
|
||||
/// Remove a value from the context
|
||||
pub fn remove(&mut self, key: &str) -> Option<serde_json::Value> {
|
||||
self.0.remove(key)
|
||||
}
|
||||
|
||||
/// Get the underlying HashMap for iteration
|
||||
pub fn inner(&self) -> &HashMap<String, serde_json::Value> {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Get mutable access to the underlying HashMap
|
||||
pub fn inner_mut(&mut self) -> &mut HashMap<String, serde_json::Value> {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HTTP Transport Structures
|
||||
// =============================================================================
|
||||
|
||||
/// HTTPRequest represents an incoming HTTP request at the transport layer.
|
||||
/// Body is base64-encoded.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct HTTPRequest {
|
||||
#[serde(default, deserialize_with = "nullable::string")]
|
||||
pub method: String,
|
||||
|
||||
#[serde(default, deserialize_with = "nullable::string")]
|
||||
pub path: String,
|
||||
|
||||
#[serde(default, deserialize_with = "nullable::string_map")]
|
||||
pub headers: HashMap<String, String>,
|
||||
|
||||
#[serde(default, deserialize_with = "nullable::string_map")]
|
||||
pub query: HashMap<String, String>,
|
||||
|
||||
/// Base64-encoded request body
|
||||
#[serde(default, deserialize_with = "nullable::string")]
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
/// HTTPResponse represents an HTTP response to return.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct HTTPResponse {
|
||||
#[serde(default, deserialize_with = "nullable::i32_field")]
|
||||
pub status_code: i32,
|
||||
|
||||
#[serde(default, deserialize_with = "nullable::string_map")]
|
||||
pub headers: HashMap<String, String>,
|
||||
|
||||
/// Base64-encoded response body
|
||||
#[serde(default, deserialize_with = "nullable::string")]
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
/// HTTPInterceptInput is the input for http_intercept hook.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct HTTPInterceptInput {
|
||||
#[serde(default, deserialize_with = "nullable::context")]
|
||||
pub context: BifrostContext,
|
||||
|
||||
#[serde(default, deserialize_with = "nullable::http_request")]
|
||||
pub request: HTTPRequest,
|
||||
}
|
||||
|
||||
/// HTTPInterceptOutput is the output for http_intercept hook.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct HTTPInterceptOutput {
|
||||
pub context: BifrostContext,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub request: Option<serde_json::Value>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub response: Option<HTTPResponse>,
|
||||
|
||||
#[serde(default)]
|
||||
pub has_response: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Chat Completion Structures (BifrostRequest)
|
||||
// =============================================================================
|
||||
|
||||
/// ChatMessageRole represents the role of a message sender.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ChatMessageRole {
|
||||
User,
|
||||
Assistant,
|
||||
System,
|
||||
Tool,
|
||||
Developer,
|
||||
}
|
||||
|
||||
impl Default for ChatMessageRole {
|
||||
fn default() -> Self {
|
||||
ChatMessageRole::User
|
||||
}
|
||||
}
|
||||
|
||||
/// ChatMessageContent can be either a string or an array of content blocks.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum ChatMessageContent {
|
||||
Text(String),
|
||||
Blocks(Vec<ChatContentBlock>),
|
||||
}
|
||||
|
||||
impl Default for ChatMessageContent {
|
||||
fn default() -> Self {
|
||||
ChatMessageContent::Text(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
/// ChatContentBlock represents a content block in a message.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ChatContentBlock {
|
||||
#[serde(rename = "type")]
|
||||
pub block_type: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub text: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image_url: Option<ImageUrl>,
|
||||
}
|
||||
|
||||
/// ImageUrl represents an image URL in a content block.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ImageUrl {
|
||||
pub url: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub detail: Option<String>,
|
||||
}
|
||||
|
||||
/// ChatMessage represents a message in the conversation.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ChatMessage {
|
||||
#[serde(default)]
|
||||
pub role: ChatMessageRole,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub content: Option<ChatMessageContent>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_call_id: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_calls: Option<Vec<ToolCall>>,
|
||||
}
|
||||
|
||||
/// ToolCall represents a tool call made by the assistant.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ToolCall {
|
||||
#[serde(default)]
|
||||
pub id: Option<String>,
|
||||
|
||||
#[serde(rename = "type", default)]
|
||||
pub call_type: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub function: ToolCallFunction,
|
||||
}
|
||||
|
||||
/// ToolCallFunction represents the function being called.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ToolCallFunction {
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub arguments: String,
|
||||
}
|
||||
|
||||
/// ChatParameters contains optional parameters for chat completion.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ChatParameters {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub temperature: Option<f64>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_completion_tokens: Option<i32>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub top_p: Option<f64>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub frequency_penalty: Option<f64>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub presence_penalty: Option<f64>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stop: Option<Vec<String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tools: Option<Vec<ChatTool>>,
|
||||
|
||||
/// Catch-all for additional parameters
|
||||
#[serde(flatten)]
|
||||
pub extra: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
/// ChatTool represents a tool definition.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ChatTool {
|
||||
#[serde(rename = "type")]
|
||||
pub tool_type: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub function: Option<ChatToolFunction>,
|
||||
}
|
||||
|
||||
/// ChatToolFunction represents a function definition.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ChatToolFunction {
|
||||
pub name: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub parameters: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// BifrostChatRequest represents a chat completion request.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct BifrostChatRequest {
|
||||
#[serde(default)]
|
||||
pub provider: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub model: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub input: Vec<ChatMessage>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub params: Option<ChatParameters>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub fallbacks: Option<Vec<Fallback>>,
|
||||
}
|
||||
|
||||
/// Fallback represents a fallback provider/model.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct Fallback {
|
||||
pub provider: String,
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
/// BifrostRequest is the unified request structure.
|
||||
/// Only one of the request types should be present.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct BifrostRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub chat_request: Option<BifrostChatRequest>,
|
||||
|
||||
// Add other request types as needed
|
||||
#[serde(flatten)]
|
||||
pub extra: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
impl BifrostRequest {
|
||||
/// Get provider and model from the request
|
||||
pub fn get_provider_model(&self) -> (String, String) {
|
||||
if let Some(ref chat) = self.chat_request {
|
||||
return (chat.provider.clone(), chat.model.clone());
|
||||
}
|
||||
(String::new(), String::new())
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Response Structures (BifrostResponse)
|
||||
// =============================================================================
|
||||
|
||||
/// LLMUsage contains token usage information.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct LLMUsage {
|
||||
#[serde(default)]
|
||||
pub prompt_tokens: i32,
|
||||
|
||||
#[serde(default)]
|
||||
pub completion_tokens: i32,
|
||||
|
||||
#[serde(default)]
|
||||
pub total_tokens: i32,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub prompt_tokens_details: Option<serde_json::Value>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub completion_tokens_details: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// ResponseChoice represents a single completion choice.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ResponseChoice {
|
||||
#[serde(default)]
|
||||
pub index: i32,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub message: Option<ChatMessage>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub delta: Option<ChatMessage>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub finish_reason: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub logprobs: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// BifrostChatResponse represents a chat completion response.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct BifrostChatResponse {
|
||||
#[serde(default)]
|
||||
pub id: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub model: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub choices: Vec<ResponseChoice>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub usage: Option<LLMUsage>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub created: Option<i64>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub object: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub system_fingerprint: Option<String>,
|
||||
}
|
||||
|
||||
/// BifrostResponse is the unified response structure.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct BifrostResponse {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub chat_response: Option<BifrostChatResponse>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub extra: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Error Structure
|
||||
// =============================================================================
|
||||
|
||||
/// ErrorField contains the error details.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ErrorField {
|
||||
#[serde(default)]
|
||||
pub message: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none", rename = "type")]
|
||||
pub error_type: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub code: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub param: Option<String>,
|
||||
}
|
||||
|
||||
/// BifrostError represents an error response.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct BifrostError {
|
||||
#[serde(default)]
|
||||
pub error: ErrorField,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub status_code: Option<i32>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub allow_fallbacks: Option<bool>,
|
||||
}
|
||||
|
||||
impl BifrostError {
|
||||
/// Create a new error with a message
|
||||
pub fn new(message: &str) -> Self {
|
||||
Self {
|
||||
error: ErrorField {
|
||||
message: message.to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the error type
|
||||
pub fn with_type(mut self, error_type: &str) -> Self {
|
||||
self.error.error_type = Some(error_type.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the error code
|
||||
pub fn with_code(mut self, code: &str) -> Self {
|
||||
self.error.code = Some(code.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the status code
|
||||
pub fn with_status(mut self, status: i32) -> Self {
|
||||
self.status_code = Some(status);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Short Circuit Structure
|
||||
// =============================================================================
|
||||
|
||||
/// LLMPluginShortCircuit allows plugins to short-circuit the request flow.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct LLMPluginShortCircuit {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub response: Option<BifrostResponse>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<BifrostError>,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Hook Input/Output Structures
|
||||
// =============================================================================
|
||||
|
||||
/// PreHookInput is the input for pre_hook.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct PreHookInput {
|
||||
#[serde(default)]
|
||||
pub context: BifrostContext,
|
||||
|
||||
#[serde(default)]
|
||||
pub request: serde_json::Value,
|
||||
}
|
||||
|
||||
impl PreHookInput {
|
||||
/// Parse the request as a BifrostRequest
|
||||
pub fn parse_request(&self) -> Option<BifrostRequest> {
|
||||
serde_json::from_value(self.request.clone()).ok()
|
||||
}
|
||||
|
||||
/// Get provider and model from the request
|
||||
pub fn get_provider_model(&self) -> (String, String) {
|
||||
if let Some(req) = self.parse_request() {
|
||||
return req.get_provider_model();
|
||||
}
|
||||
// Try direct access for simpler structures
|
||||
let provider = self.request.get("provider")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let model = self.request.get("model")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
(provider, model)
|
||||
}
|
||||
}
|
||||
|
||||
/// PreHookOutput is the output for pre_hook.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct PreHookOutput {
|
||||
pub context: BifrostContext,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub request: Option<serde_json::Value>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub short_circuit: Option<LLMPluginShortCircuit>,
|
||||
|
||||
#[serde(default)]
|
||||
pub has_short_circuit: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
/// PostHookInput is the input for post_hook.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct PostHookInput {
|
||||
#[serde(default)]
|
||||
pub context: BifrostContext,
|
||||
|
||||
#[serde(default)]
|
||||
pub response: serde_json::Value,
|
||||
|
||||
#[serde(default)]
|
||||
pub error: serde_json::Value,
|
||||
|
||||
#[serde(default)]
|
||||
pub has_error: bool,
|
||||
}
|
||||
|
||||
impl PostHookInput {
|
||||
/// Parse the response as a BifrostResponse
|
||||
pub fn parse_response(&self) -> Option<BifrostResponse> {
|
||||
serde_json::from_value(self.response.clone()).ok()
|
||||
}
|
||||
|
||||
/// Parse the error as a BifrostError
|
||||
pub fn parse_error(&self) -> Option<BifrostError> {
|
||||
if self.has_error {
|
||||
serde_json::from_value(self.error.clone()).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// PostHookOutput is the output for post_hook.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct PostHookOutput {
|
||||
pub context: BifrostContext,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub response: Option<serde_json::Value>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<serde_json::Value>,
|
||||
|
||||
#[serde(default)]
|
||||
pub has_error: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub hook_error: String,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HTTP Stream Chunk Hook Input/Output Structures
|
||||
// =============================================================================
|
||||
|
||||
/// HTTPStreamChunkHookInput is the input for http_stream_chunk_hook.
|
||||
/// Called for each chunk during streaming responses.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct HTTPStreamChunkHookInput {
|
||||
#[serde(default)]
|
||||
pub context: BifrostContext,
|
||||
|
||||
#[serde(default)]
|
||||
pub request: serde_json::Value,
|
||||
|
||||
#[serde(default)]
|
||||
pub chunk: serde_json::Value, // BifrostStreamChunk as JSON
|
||||
}
|
||||
|
||||
/// HTTPStreamChunkHookOutput is the output for http_stream_chunk_hook.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct HTTPStreamChunkHookOutput {
|
||||
pub context: BifrostContext,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub chunk: Option<serde_json::Value>, // BifrostStreamChunk as JSON, None to skip
|
||||
|
||||
#[serde(default)]
|
||||
pub has_chunk: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub skip: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Plugin Configuration
|
||||
// =============================================================================
|
||||
|
||||
/// Plugin configuration (customize as needed)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct PluginConfig {
|
||||
#[serde(flatten)]
|
||||
pub values: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tests
|
||||
// =============================================================================
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_context_serialization() {
|
||||
let mut ctx = BifrostContext::new();
|
||||
ctx.set_value("request_id", "test-123");
|
||||
ctx.set_value("custom_key", "custom_value");
|
||||
ctx.set_value("is_enabled", true);
|
||||
ctx.set_value("count", 42);
|
||||
|
||||
let json = serde_json::to_string(&ctx).unwrap();
|
||||
assert!(json.contains("request_id"));
|
||||
assert!(json.contains("custom_key"));
|
||||
assert!(json.contains("is_enabled"));
|
||||
assert!(json.contains("count"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_context_deserialization() {
|
||||
let json = r#"{"request_id": "test-123", "custom_key": "custom_value", "is_enabled": true}"#;
|
||||
let ctx: BifrostContext = serde_json::from_str(json).unwrap();
|
||||
|
||||
assert_eq!(ctx.get_string("request_id"), Some("test-123"));
|
||||
assert_eq!(ctx.get_string("custom_key"), Some("custom_value"));
|
||||
assert_eq!(ctx.get_bool("is_enabled"), Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_context_methods() {
|
||||
let mut ctx = BifrostContext::new();
|
||||
ctx.set_value("key1", "value1");
|
||||
ctx.set_value("enabled", true);
|
||||
ctx.set_value("count", 42);
|
||||
|
||||
assert_eq!(ctx.get_string("key1"), Some("value1"));
|
||||
assert_eq!(ctx.get_bool("enabled"), Some(true));
|
||||
assert_eq!(ctx.get_i64("count"), Some(42));
|
||||
assert!(ctx.contains_key("key1"));
|
||||
assert!(!ctx.contains_key("nonexistent"));
|
||||
|
||||
ctx.remove("key1");
|
||||
assert!(!ctx.contains_key("key1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chat_message() {
|
||||
let msg = ChatMessage {
|
||||
role: ChatMessageRole::User,
|
||||
content: Some(ChatMessageContent::Text("Hello!".to_string())),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&msg).unwrap();
|
||||
assert!(json.contains("user"));
|
||||
assert!(json.contains("Hello!"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bifrost_error() {
|
||||
let error = BifrostError::new("Test error")
|
||||
.with_type("test_type")
|
||||
.with_code("500")
|
||||
.with_status(500);
|
||||
|
||||
let json = serde_json::to_string(&error).unwrap();
|
||||
assert!(json.contains("Test error"));
|
||||
assert!(json.contains("test_type"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pre_hook_input_parsing() {
|
||||
let json = r#"{
|
||||
"context": {"request_id": "test-123", "custom": "value"},
|
||||
"request": {"provider": "openai", "model": "gpt-4"}
|
||||
}"#;
|
||||
|
||||
let input: PreHookInput = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(input.context.get_string("request_id"), Some("test-123"));
|
||||
assert_eq!(input.context.get_string("custom"), Some("value"));
|
||||
|
||||
let (provider, model) = input.get_provider_model();
|
||||
assert_eq!(provider, "openai");
|
||||
assert_eq!(model, "gpt-4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_request_with_null_fields() {
|
||||
// Simulates Go sending null for nil []byte and nil maps
|
||||
let json = r#"{
|
||||
"method": "POST",
|
||||
"path": "/v1/chat/completions",
|
||||
"headers": null,
|
||||
"query": null,
|
||||
"body": null
|
||||
}"#;
|
||||
|
||||
let req: HTTPRequest = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(req.method, "POST");
|
||||
assert_eq!(req.path, "/v1/chat/completions");
|
||||
assert!(req.headers.is_empty());
|
||||
assert!(req.query.is_empty());
|
||||
assert_eq!(req.body, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_request_with_missing_fields() {
|
||||
// Test that missing fields also work (default behavior)
|
||||
let json = r#"{
|
||||
"method": "GET",
|
||||
"path": "/health"
|
||||
}"#;
|
||||
|
||||
let req: HTTPRequest = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(req.method, "GET");
|
||||
assert_eq!(req.path, "/health");
|
||||
assert!(req.headers.is_empty());
|
||||
assert!(req.query.is_empty());
|
||||
assert_eq!(req.body, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_intercept_input_with_nulls() {
|
||||
// Simulates a full HTTP intercept input with null body from Go
|
||||
let json = r#"{
|
||||
"context": {"request_id": "abc-123"},
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"path": "/v1/chat/completions",
|
||||
"headers": {"content-type": "application/json"},
|
||||
"query": {},
|
||||
"body": null
|
||||
}
|
||||
}"#;
|
||||
|
||||
let input: HTTPInterceptInput = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(input.context.get_string("request_id"), Some("abc-123"));
|
||||
assert_eq!(input.request.method, "POST");
|
||||
assert_eq!(input.request.path, "/v1/chat/completions");
|
||||
assert_eq!(input.request.headers.get("content-type"), Some(&"application/json".to_string()));
|
||||
assert_eq!(input.request.body, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_response_with_null_fields() {
|
||||
let json = r#"{
|
||||
"status_code": null,
|
||||
"headers": null,
|
||||
"body": null
|
||||
}"#;
|
||||
|
||||
let resp: HTTPResponse = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(resp.status_code, 0);
|
||||
assert!(resp.headers.is_empty());
|
||||
assert_eq!(resp.body, "");
|
||||
}
|
||||
}
|
||||
70
examples/plugins/hello-world-wasm-typescript/Makefile
Normal file
70
examples/plugins/hello-world-wasm-typescript/Makefile
Normal file
@@ -0,0 +1,70 @@
|
||||
.PHONY: all build build-debug clean help install check-node
|
||||
|
||||
# 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
|
||||
|
||||
help: ## Show this help message
|
||||
@echo '$(COLOR_BOLD)Hello World WASM Plugin (TypeScript/AssemblyScript)$(COLOR_RESET)'
|
||||
@echo ''
|
||||
@echo '$(COLOR_BOLD)Usage:$(COLOR_RESET) make [target]'
|
||||
@echo ''
|
||||
@echo '$(COLOR_BOLD)Prerequisites:$(COLOR_RESET)'
|
||||
@echo ' - Node.js (https://nodejs.org/)'
|
||||
@echo ' - npm install (to install AssemblyScript)'
|
||||
@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-node: ## Check if Node.js is installed
|
||||
@which node > /dev/null 2>&1 || (echo "$(COLOR_ERROR)Error: Node.js is not installed$(COLOR_RESET)"; \
|
||||
echo "$(COLOR_INFO)Install Node.js: https://nodejs.org/$(COLOR_RESET)"; \
|
||||
exit 1)
|
||||
@echo "$(COLOR_SUCCESS)✓ Node.js found: $$(node --version)$(COLOR_RESET)"
|
||||
|
||||
install: check-node ## Install dependencies
|
||||
@echo "$(COLOR_INFO)Installing dependencies...$(COLOR_RESET)"
|
||||
npm install
|
||||
@echo "$(COLOR_SUCCESS)✓ Dependencies installed$(COLOR_RESET)"
|
||||
|
||||
build: install ## Build the WASM plugin
|
||||
@mkdir -p $(OUTPUT_DIR)
|
||||
@echo "$(COLOR_INFO)Building WASM plugin...$(COLOR_RESET)"
|
||||
npm run build
|
||||
@echo "$(COLOR_SUCCESS)✓ Plugin built successfully: $(OUTPUT)$(COLOR_RESET)"
|
||||
@ls -lh $(OUTPUT) | awk '{print " Size: " $$5}'
|
||||
|
||||
build-debug: install ## Build with debug info
|
||||
@mkdir -p $(OUTPUT_DIR)
|
||||
@echo "$(COLOR_INFO)Building WASM plugin (debug)...$(COLOR_RESET)"
|
||||
npm run build:debug
|
||||
@echo "$(COLOR_SUCCESS)✓ Debug 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) node_modules
|
||||
@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 ""
|
||||
@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
|
||||
453
examples/plugins/hello-world-wasm-typescript/README.md
Normal file
453
examples/plugins/hello-world-wasm-typescript/README.md
Normal file
@@ -0,0 +1,453 @@
|
||||
# Bifrost WASM Plugin (TypeScript/AssemblyScript)
|
||||
|
||||
A comprehensive example of a Bifrost plugin written in TypeScript and compiled to WebAssembly using AssemblyScript. This plugin demonstrates proper structure definitions, JSON parsing, context handling, and request/response modification patterns.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Node.js Installation
|
||||
|
||||
Node.js is required to run AssemblyScript:
|
||||
|
||||
**macOS:**
|
||||
```bash
|
||||
brew install node
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
sudo apt install -y nodejs
|
||||
```
|
||||
|
||||
**Other platforms:**
|
||||
See [Node.js Downloads](https://nodejs.org/en/download/)
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
# Install dependencies and build
|
||||
make build
|
||||
|
||||
# Build with debug info
|
||||
make build-debug
|
||||
|
||||
# Clean build artifacts
|
||||
make clean
|
||||
```
|
||||
|
||||
The compiled plugin will be at `build/hello-world.wasm`.
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
assembly/
|
||||
├── index.ts # Plugin implementation (hooks)
|
||||
├── memory.ts # Memory management utilities
|
||||
├── types.ts # Type definitions (mirrors Go SDK)
|
||||
└── tsconfig.json # AssemblyScript config
|
||||
```
|
||||
|
||||
## Plugin Structure
|
||||
|
||||
WASM plugins must export the following functions:
|
||||
|
||||
| Export | Signature | Description |
|
||||
|--------|-----------|-------------|
|
||||
| `malloc` | `(size: u32) -> u32` | Allocate memory for host to write data |
|
||||
| `free` | `(ptr: u32)` | Free allocated memory |
|
||||
| `get_name` | `() -> u64` | Returns packed ptr+len of plugin name |
|
||||
| `init` | `(config_ptr, config_len: u32) -> i32` | Initialize with config (optional) |
|
||||
| `http_intercept` | `(input_ptr, input_len: u32) -> u64` | HTTP transport intercept |
|
||||
| `pre_hook` | `(input_ptr, input_len: u32) -> u64` | Pre-request hook |
|
||||
| `post_hook` | `(input_ptr, input_len: u32) -> u64` | Post-response hook |
|
||||
| `cleanup` | `() -> i32` | Cleanup resources (0 = success) |
|
||||
|
||||
### 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 Structures
|
||||
|
||||
This plugin uses `json-as` with `@json` decorators for automatic JSON serialization. All structures mirror the Go SDK types:
|
||||
|
||||
### Context
|
||||
|
||||
```typescript
|
||||
@json
|
||||
class BifrostContext {
|
||||
request_id: string = '' // Unique request identifier
|
||||
plugin_processed: string = '' // Custom plugin values
|
||||
plugin_name: string = ''
|
||||
}
|
||||
```
|
||||
|
||||
### HTTP Transport Types
|
||||
|
||||
```typescript
|
||||
@json
|
||||
class HTTPRequest {
|
||||
method: string = '' // GET, POST, etc.
|
||||
path: string = '' // /v1/chat/completions
|
||||
body: string = '' // base64 encoded
|
||||
}
|
||||
|
||||
@json
|
||||
class HTTPResponse {
|
||||
status_code: i32 = 200 // HTTP status code
|
||||
body: string = '' // base64 encoded
|
||||
}
|
||||
```
|
||||
|
||||
### Chat Completion Types
|
||||
|
||||
```typescript
|
||||
@json
|
||||
class ChatMessage {
|
||||
role: string = '' // "user", "assistant", "system", "tool"
|
||||
content: string = ''
|
||||
name: string = ''
|
||||
tool_call_id: string = ''
|
||||
}
|
||||
|
||||
@json
|
||||
class ChatParameters {
|
||||
temperature: f64 = 0
|
||||
max_completion_tokens: i32 = 0
|
||||
top_p: f64 = 0
|
||||
}
|
||||
|
||||
@json
|
||||
class BifrostChatRequest {
|
||||
provider: string = '' // "openai", "anthropic", etc.
|
||||
model: string = '' // "gpt-4", "claude-3", etc.
|
||||
input: ChatMessage[] = []
|
||||
params: ChatParameters = new ChatParameters()
|
||||
}
|
||||
```
|
||||
|
||||
### Response Types
|
||||
|
||||
```typescript
|
||||
@json
|
||||
class LLMUsage {
|
||||
prompt_tokens: i32 = 0
|
||||
completion_tokens: i32 = 0
|
||||
total_tokens: i32 = 0
|
||||
}
|
||||
|
||||
@json
|
||||
class ResponseChoice {
|
||||
index: i32 = 0
|
||||
message: ChatMessage = new ChatMessage()
|
||||
finish_reason: string = 'stop' // "stop", "length", "tool_calls"
|
||||
}
|
||||
|
||||
@json
|
||||
class BifrostChatResponse {
|
||||
id: string = ''
|
||||
model: string = ''
|
||||
choices: ResponseChoice[] = []
|
||||
usage: LLMUsage = new LLMUsage()
|
||||
}
|
||||
```
|
||||
|
||||
### Error Types
|
||||
|
||||
```typescript
|
||||
@json
|
||||
class ErrorField {
|
||||
message: string = ''
|
||||
type: string = '' // "rate_limit", "auth_error", etc.
|
||||
code: string = '' // "429", "401", etc.
|
||||
}
|
||||
|
||||
@json
|
||||
class BifrostError {
|
||||
error: ErrorField = new ErrorField()
|
||||
status_code: i32 = 0
|
||||
}
|
||||
```
|
||||
|
||||
### Short Circuit
|
||||
|
||||
```typescript
|
||||
@json
|
||||
class LLMPluginShortCircuit {
|
||||
response: BifrostResponse | null = null // Success short-circuit
|
||||
error: BifrostError | null = null // Error short-circuit
|
||||
}
|
||||
```
|
||||
|
||||
## Hook Input/Output Structures
|
||||
|
||||
### http_intercept
|
||||
|
||||
**Input:**
|
||||
```json
|
||||
{
|
||||
"context": { "request_id": "abc-123" },
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"path": "/v1/chat/completions",
|
||||
"headers": { "Content-Type": "application/json" },
|
||||
"query": {},
|
||||
"body": "<base64-encoded>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```json
|
||||
{
|
||||
"context": { "request_id": "abc-123", "custom_key": "value" },
|
||||
"request": {},
|
||||
"response": { "status_code": 200, "headers": {}, "body": "<base64>" },
|
||||
"has_response": false,
|
||||
"error": ""
|
||||
}
|
||||
```
|
||||
|
||||
### pre_hook
|
||||
|
||||
**Input:**
|
||||
```json
|
||||
{
|
||||
"context": { "request_id": "abc-123" },
|
||||
"request": {
|
||||
"provider": "openai",
|
||||
"model": "gpt-4",
|
||||
"input": [{ "role": "user", "content": "Hello" }],
|
||||
"params": { "temperature": 0.7 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```json
|
||||
{
|
||||
"context": { "request_id": "abc-123", "plugin_processed": "true" },
|
||||
"request": {},
|
||||
"short_circuit": {
|
||||
"response": { "chat_response": { ... } }
|
||||
},
|
||||
"has_short_circuit": false,
|
||||
"error": ""
|
||||
}
|
||||
```
|
||||
|
||||
### post_hook
|
||||
|
||||
**Input:**
|
||||
```json
|
||||
{
|
||||
"context": { "request_id": "abc-123", "plugin_processed": "true" },
|
||||
"response": {
|
||||
"chat_response": {
|
||||
"id": "chatcmpl-123",
|
||||
"model": "gpt-4",
|
||||
"choices": [{ "index": 0, "message": { "role": "assistant", "content": "Hi!" } }],
|
||||
"usage": { "prompt_tokens": 5, "completion_tokens": 10, "total_tokens": 15 }
|
||||
}
|
||||
},
|
||||
"error": {},
|
||||
"has_error": false
|
||||
}
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```json
|
||||
{
|
||||
"context": { "request_id": "abc-123", "post_hook_completed": "true" },
|
||||
"response": {},
|
||||
"error": {},
|
||||
"has_error": false,
|
||||
"hook_error": ""
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Modifying Context
|
||||
|
||||
```typescript
|
||||
import { JSON } from 'json-as'
|
||||
|
||||
export function pre_hook(inputPtr: u32, inputLen: u32): u64 {
|
||||
const inputJson = readString(inputPtr, inputLen)
|
||||
const input = JSON.parse<PreHookInput>(inputJson)
|
||||
|
||||
const output = new PreHookOutput()
|
||||
output.context = input.context
|
||||
|
||||
// Add custom values to context
|
||||
output.context.plugin_processed = 'true'
|
||||
output.context.plugin_name = 'my-plugin'
|
||||
|
||||
return writeString(JSON.stringify(output))
|
||||
}
|
||||
```
|
||||
|
||||
### Short-Circuit with Mock Response
|
||||
|
||||
```typescript
|
||||
import { JSON } from 'json-as'
|
||||
|
||||
export function pre_hook(inputPtr: u32, inputLen: u32): u64 {
|
||||
const inputJson = readString(inputPtr, inputLen)
|
||||
const input = JSON.parse<PreHookInput>(inputJson)
|
||||
|
||||
// Check if this should be mocked
|
||||
const model = input.request.model
|
||||
if (model === 'mock-model') {
|
||||
const output = new PreHookOutput()
|
||||
output.context = input.context
|
||||
output.has_short_circuit = true
|
||||
output.short_circuit = new LLMPluginShortCircuit()
|
||||
|
||||
// Build mock response
|
||||
const mockResponse = new BifrostResponse()
|
||||
mockResponse.chat_response = new BifrostChatResponse()
|
||||
mockResponse.chat_response!.id = 'mock-' + input.context.request_id
|
||||
mockResponse.chat_response!.model = 'mock-model'
|
||||
|
||||
const choice = new ResponseChoice()
|
||||
choice.message.role = 'assistant'
|
||||
choice.message.content = 'This is a mock response!'
|
||||
mockResponse.chat_response!.choices.push(choice)
|
||||
|
||||
mockResponse.chat_response!.usage.prompt_tokens = 10
|
||||
mockResponse.chat_response!.usage.completion_tokens = 15
|
||||
mockResponse.chat_response!.usage.total_tokens = 25
|
||||
|
||||
output.short_circuit!.response = mockResponse
|
||||
return writeString(JSON.stringify(output))
|
||||
}
|
||||
|
||||
// Pass through
|
||||
const output = new PreHookOutput()
|
||||
output.context = input.context
|
||||
return writeString(JSON.stringify(output))
|
||||
}
|
||||
```
|
||||
|
||||
### Short-Circuit with Error
|
||||
|
||||
```typescript
|
||||
import { JSON } from 'json-as'
|
||||
|
||||
export function pre_hook(inputPtr: u32, inputLen: u32): u64 {
|
||||
const inputJson = readString(inputPtr, inputLen)
|
||||
const input = JSON.parse<PreHookInput>(inputJson)
|
||||
|
||||
// Check rate limit (example)
|
||||
if (shouldRateLimit(input.context.request_id)) {
|
||||
const output = new PreHookOutput()
|
||||
output.context = input.context
|
||||
output.has_short_circuit = true
|
||||
output.short_circuit = new LLMPluginShortCircuit()
|
||||
|
||||
const error = new BifrostError()
|
||||
error.error.message = 'Rate limit exceeded'
|
||||
error.error.type = 'rate_limit'
|
||||
error.error.code = '429'
|
||||
error.status_code = 429
|
||||
|
||||
output.short_circuit!.error = error
|
||||
return writeString(JSON.stringify(output))
|
||||
}
|
||||
|
||||
// Pass through
|
||||
const output = new PreHookOutput()
|
||||
output.context = input.context
|
||||
return writeString(JSON.stringify(output))
|
||||
}
|
||||
```
|
||||
|
||||
### Modifying Responses in post_hook
|
||||
|
||||
```typescript
|
||||
import { JSON } from 'json-as'
|
||||
|
||||
export function post_hook(inputPtr: u32, inputLen: u32): u64 {
|
||||
const inputJson = readString(inputPtr, inputLen)
|
||||
const input = JSON.parse<PostHookInput>(inputJson)
|
||||
|
||||
const output = new PostHookOutput()
|
||||
output.context = input.context
|
||||
|
||||
// Handle errors
|
||||
if (input.has_error && input.error !== null) {
|
||||
output.has_error = true
|
||||
output.error = input.error
|
||||
// Could modify error here if needed
|
||||
return writeString(JSON.stringify(output))
|
||||
}
|
||||
|
||||
// Modify response
|
||||
if (input.response !== null && input.response!.chat_response !== null) {
|
||||
output.response = input.response
|
||||
// Could add logging, metrics, or modify response here
|
||||
}
|
||||
|
||||
return writeString(JSON.stringify(output))
|
||||
}
|
||||
```
|
||||
|
||||
## Usage with Bifrost
|
||||
|
||||
Configure the plugin in your Bifrost config:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": [
|
||||
{
|
||||
"path": "/path/to/hello-world.wasm",
|
||||
"name": "hello-world-wasm-typescript",
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"custom_option": "value"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## AssemblyScript Notes
|
||||
|
||||
AssemblyScript is similar to TypeScript but with some differences:
|
||||
|
||||
1. **Types are required**: All variables must have explicit types
|
||||
2. **No closures**: Functions cannot capture variables from outer scope
|
||||
3. **Limited stdlib**: Not all JavaScript/TypeScript features are available
|
||||
4. **Strict null handling**: Null checks are required
|
||||
5. **JSON via json-as**: Uses the `json-as` package with `@json` decorators for serialization
|
||||
|
||||
This plugin uses `json-as` for JSON parsing/serialization:
|
||||
|
||||
```typescript
|
||||
import { JSON } from 'json-as'
|
||||
|
||||
@json
|
||||
class MyClass {
|
||||
name: string = ''
|
||||
value: i32 = 0
|
||||
}
|
||||
|
||||
// Parse JSON
|
||||
const obj = JSON.parse<MyClass>('{"name":"test","value":42}')
|
||||
|
||||
// Stringify to JSON
|
||||
const json = JSON.stringify(obj)
|
||||
```
|
||||
|
||||
See [AssemblyScript Documentation](https://www.assemblyscript.org/introduction.html) and [json-as Documentation](https://github.com/JairusSW/as-json) for more details.
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Familiar syntax**: TypeScript-like syntax for JS/TS developers
|
||||
2. **Cross-platform**: Single `.wasm` binary runs on any OS/architecture
|
||||
3. **Security**: WASM provides sandboxed execution
|
||||
4. **Type Safety**: Strongly typed structures catch errors at compile time
|
||||
5. **npm ecosystem**: Can use npm for dependency management
|
||||
128
examples/plugins/hello-world-wasm-typescript/assembly/index.ts
Normal file
128
examples/plugins/hello-world-wasm-typescript/assembly/index.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Bifrost WASM Plugin for TypeScript/AssemblyScript
|
||||
*
|
||||
* This plugin uses json-as for safe JSON parsing with @json decorators.
|
||||
*
|
||||
* Build with: npm run build
|
||||
*/
|
||||
|
||||
import { JSON } from 'json-as'
|
||||
import { free as _free, malloc as _malloc, readString, writeString } from './memory'
|
||||
import {
|
||||
HTTPInterceptInput,
|
||||
HTTPInterceptOutput,
|
||||
PreHookInput,
|
||||
PreHookOutput,
|
||||
PostHookInput,
|
||||
PostHookOutput,
|
||||
HTTPStreamChunkHookInput,
|
||||
HTTPStreamChunkHookOutput
|
||||
} from './types'
|
||||
|
||||
// =============================================================================
|
||||
// Re-export memory functions for WASM host
|
||||
// =============================================================================
|
||||
|
||||
export function malloc(size: u32): u32 {
|
||||
return _malloc(size)
|
||||
}
|
||||
|
||||
export function free(ptr: u32): void {
|
||||
_free(ptr)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Plugin Configuration
|
||||
// =============================================================================
|
||||
|
||||
let pluginConfig: string = ''
|
||||
|
||||
// =============================================================================
|
||||
// Exported Plugin Functions
|
||||
// =============================================================================
|
||||
|
||||
export function get_name(): u64 {
|
||||
return writeString('hello-world-wasm-typescript')
|
||||
}
|
||||
|
||||
export function init(configPtr: u32, configLen: u32): i32 {
|
||||
pluginConfig = readString(configPtr, configLen)
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP transport intercept
|
||||
* Pass through the request with added context value
|
||||
*/
|
||||
export function http_intercept(inputPtr: u32, inputLen: u32): u64 {
|
||||
const inputJson = readString(inputPtr, inputLen)
|
||||
const input = JSON.parse<HTTPInterceptInput>(inputJson)
|
||||
|
||||
const output = new HTTPInterceptOutput()
|
||||
output.context = input.context
|
||||
output.context.set<string>('from-http', '123')
|
||||
output.request = input.request
|
||||
|
||||
return writeString(JSON.stringify(output))
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-request hook
|
||||
* Pass through the request with added context value
|
||||
*/
|
||||
export function pre_hook(inputPtr: u32, inputLen: u32): u64 {
|
||||
const inputJson = readString(inputPtr, inputLen)
|
||||
const input = JSON.parse<PreHookInput>(inputJson)
|
||||
|
||||
const output = new PreHookOutput()
|
||||
output.context = input.context
|
||||
output.context.set<string>('from-pre-hook', '789')
|
||||
output.request = input.request
|
||||
|
||||
return writeString(JSON.stringify(output))
|
||||
}
|
||||
|
||||
/**
|
||||
* Post-response hook
|
||||
* Pass through the response/error with added context value
|
||||
*/
|
||||
export function post_hook(inputPtr: u32, inputLen: u32): u64 {
|
||||
const inputJson = readString(inputPtr, inputLen)
|
||||
const input = JSON.parse<PostHookInput>(inputJson)
|
||||
|
||||
const output = new PostHookOutput()
|
||||
output.context = input.context
|
||||
output.context.set<string>('from-post-hook', '456')
|
||||
output.response = input.response
|
||||
output.error = input.error
|
||||
output.has_error = input.has_error
|
||||
|
||||
return writeString(JSON.stringify(output))
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP stream chunk hook
|
||||
* Called for each chunk during streaming responses.
|
||||
* Pass through the chunk with added context value.
|
||||
*/
|
||||
export function http_stream_chunk_hook(inputPtr: u32, inputLen: u32): u64 {
|
||||
const inputJson = readString(inputPtr, inputLen)
|
||||
const input = JSON.parse<HTTPStreamChunkHookInput>(inputJson)
|
||||
|
||||
const output = new HTTPStreamChunkHookOutput()
|
||||
output.context = input.context
|
||||
output.context.set<string>('from-stream-chunk', 'wasm-plugin')
|
||||
output.chunk = input.chunk
|
||||
output.has_chunk = true
|
||||
output.skip = false
|
||||
|
||||
return writeString(JSON.stringify(output))
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup resources
|
||||
*/
|
||||
export function cleanup(): i32 {
|
||||
pluginConfig = ''
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Memory management utilities for WASM plugins.
|
||||
* Handles allocation, deallocation, and string read/write operations.
|
||||
*/
|
||||
|
||||
// Pack a pointer and length into a single u64
|
||||
// Upper 32 bits: pointer, Lower 32 bits: length
|
||||
export function packResult(ptr: u32, len: u32): u64 {
|
||||
return (u64(ptr) << 32) | u64(len)
|
||||
}
|
||||
|
||||
// Write a string to memory and return packed pointer+length
|
||||
export function writeString(s: string): u64 {
|
||||
if (s.length === 0) {
|
||||
return 0
|
||||
}
|
||||
const encoded = String.UTF8.encode(s)
|
||||
const ptr = changetype<u32>(encoded)
|
||||
return packResult(ptr, encoded.byteLength)
|
||||
}
|
||||
|
||||
// Read a string from memory given pointer and length
|
||||
export function readString(ptr: u32, len: u32): string {
|
||||
if (len === 0) {
|
||||
return ''
|
||||
}
|
||||
const buffer = new ArrayBuffer(len)
|
||||
memory.copy(changetype<usize>(buffer), ptr, len)
|
||||
return String.UTF8.decode(buffer)
|
||||
}
|
||||
|
||||
// Allocate memory for the host to write data
|
||||
export function malloc(size: u32): u32 {
|
||||
if (size === 0) {
|
||||
return 0
|
||||
}
|
||||
const buffer = new ArrayBuffer(size)
|
||||
return changetype<u32>(buffer)
|
||||
}
|
||||
|
||||
// Free allocated memory (handled by AssemblyScript runtime)
|
||||
export function free(_ptr: u32): void {
|
||||
// AssemblyScript handles garbage collection
|
||||
// This is provided for API compatibility
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "assemblyscript/std/assembly.json",
|
||||
"include": ["./**/*.ts"]
|
||||
}
|
||||
157
examples/plugins/hello-world-wasm-typescript/assembly/types.ts
Normal file
157
examples/plugins/hello-world-wasm-typescript/assembly/types.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Type definitions for Bifrost WASM plugins.
|
||||
*
|
||||
* Uses json-as library with @json decorators for safe JSON parsing.
|
||||
* These types mirror the Go SDK types for interoperability.
|
||||
*/
|
||||
|
||||
import { JSON } from 'json-as'
|
||||
|
||||
// =============================================================================
|
||||
// HTTP Transport Input/Output Types
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* BifrostContext holds request-scoped values passed between hooks.
|
||||
* Common keys include:
|
||||
* - request_id: Unique identifier for the request
|
||||
* - Custom plugin values can be added and will be persisted across hooks
|
||||
*/
|
||||
@json
|
||||
export class BifrostContext {
|
||||
request_id: string = ''
|
||||
|
||||
// Custom values for plugin use (add more as needed)
|
||||
plugin_processed: string = ''
|
||||
plugin_name: string = ''
|
||||
post_hook_completed: string = ''
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HTTP Transport Structures
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* HTTPRequest represents an incoming HTTP request at the transport layer.
|
||||
* Body is base64-encoded.
|
||||
*/
|
||||
@json
|
||||
export class HTTPRequest {
|
||||
method: string = ''
|
||||
path: string = ''
|
||||
body: string = '' // base64 encoded
|
||||
headers: Map<string, string> = new Map<string, string>()
|
||||
query: Map<string, string> = new Map<string, string>()
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTPResponse represents an HTTP response to return.
|
||||
*/
|
||||
@json
|
||||
export class HTTPResponse {
|
||||
status_code: i32 = 200
|
||||
body: string = '' // base64 encoded
|
||||
headers: Map<string, string> = new Map<string, string>()
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTPInterceptInput is the input for http_intercept hook.
|
||||
* Context is a dynamic object (JSON.Obj) since Go sends map[string]interface{}.
|
||||
* Request is kept as JSON.Raw to pass through without full parsing.
|
||||
*/
|
||||
@json
|
||||
export class HTTPInterceptInput {
|
||||
context: JSON.Obj = new JSON.Obj()
|
||||
request: JSON.Raw = new JSON.Raw('null')
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTPInterceptOutput is the output for http_intercept hook.
|
||||
*/
|
||||
@json
|
||||
export class HTTPInterceptOutput {
|
||||
context: JSON.Obj = new JSON.Obj()
|
||||
request: JSON.Raw = new JSON.Raw('null')
|
||||
response: JSON.Raw = new JSON.Raw('null')
|
||||
has_response: bool = false
|
||||
error: string = ''
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Pre-Hook Input/Output Types
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* PreHookInput is the input for pre_hook.
|
||||
*/
|
||||
@json
|
||||
export class PreHookInput {
|
||||
context: JSON.Obj = new JSON.Obj()
|
||||
request: JSON.Raw = new JSON.Raw('null')
|
||||
}
|
||||
|
||||
/**
|
||||
* PreHookOutput is the output for pre_hook.
|
||||
*/
|
||||
@json
|
||||
export class PreHookOutput {
|
||||
context: JSON.Obj = new JSON.Obj()
|
||||
request: JSON.Raw = new JSON.Raw('null')
|
||||
short_circuit: JSON.Raw = new JSON.Raw('null')
|
||||
has_short_circuit: bool = false
|
||||
error: string = ''
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Post-Hook Input/Output Types
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* PostHookInput is the input for post_hook.
|
||||
*/
|
||||
@json
|
||||
export class PostHookInput {
|
||||
context: JSON.Obj = new JSON.Obj()
|
||||
response: JSON.Raw = new JSON.Raw('null')
|
||||
error: JSON.Raw = new JSON.Raw('null')
|
||||
has_error: bool = false
|
||||
}
|
||||
|
||||
/**
|
||||
* PostHookOutput is the output for post_hook.
|
||||
*/
|
||||
@json
|
||||
export class PostHookOutput {
|
||||
context: JSON.Obj = new JSON.Obj()
|
||||
response: JSON.Raw = new JSON.Raw('null')
|
||||
error: JSON.Raw = new JSON.Raw('null')
|
||||
has_error: bool = false
|
||||
hook_error: string = ''
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HTTP Stream Chunk Hook Input/Output Types
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* HTTPStreamChunkHookInput is the input for http_stream_chunk_hook.
|
||||
* Called for each chunk during streaming responses.
|
||||
*/
|
||||
@json
|
||||
export class HTTPStreamChunkHookInput {
|
||||
context: JSON.Obj = new JSON.Obj()
|
||||
request: JSON.Raw = new JSON.Raw('null')
|
||||
chunk: JSON.Raw = new JSON.Raw('null') // BifrostStreamChunk as JSON
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTPStreamChunkHookOutput is the output for http_stream_chunk_hook.
|
||||
*/
|
||||
@json
|
||||
export class HTTPStreamChunkHookOutput {
|
||||
context: JSON.Obj = new JSON.Obj()
|
||||
chunk: JSON.Raw = new JSON.Raw('null') // BifrostStreamChunk as JSON, or null to skip
|
||||
has_chunk: bool = false
|
||||
skip: bool = false
|
||||
error: string = ''
|
||||
}
|
||||
65
examples/plugins/hello-world-wasm-typescript/package-lock.json
generated
Normal file
65
examples/plugins/hello-world-wasm-typescript/package-lock.json
generated
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "hello-world-wasm-typescript",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hello-world-wasm-typescript",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"json-as": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"assemblyscript": "^0.27.29"
|
||||
}
|
||||
},
|
||||
"node_modules/assemblyscript": {
|
||||
"version": "0.27.37",
|
||||
"resolved": "https://registry.npmjs.org/assemblyscript/-/assemblyscript-0.27.37.tgz",
|
||||
"integrity": "sha512-YtY5k3PiV3SyUQ6gRlR2OCn8dcVRwkpiG/k2T5buoL2ymH/Z/YbaYWbk/f9mO2HTgEtGWjPiAQrIuvA7G/63Gg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"binaryen": "116.0.0-nightly.20240114",
|
||||
"long": "^5.2.4"
|
||||
},
|
||||
"bin": {
|
||||
"asc": "bin/asc.js",
|
||||
"asinit": "bin/asinit.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18",
|
||||
"npm": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/assemblyscript"
|
||||
}
|
||||
},
|
||||
"node_modules/binaryen": {
|
||||
"version": "116.0.0-nightly.20240114",
|
||||
"resolved": "https://registry.npmjs.org/binaryen/-/binaryen-116.0.0-nightly.20240114.tgz",
|
||||
"integrity": "sha512-0GZrojJnuhoe+hiwji7QFaL3tBlJoA+KFUN7ouYSDGZLSo9CKM8swQX8n/UcbR0d1VuZKU+nhogNzv423JEu5A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"wasm-opt": "bin/wasm-opt",
|
||||
"wasm2js": "bin/wasm2js"
|
||||
}
|
||||
},
|
||||
"node_modules/json-as": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json-as/-/json-as-1.2.3.tgz",
|
||||
"integrity": "sha512-yvRkR0Lv8597jHbsf+e93fo+pQctbsiDl7HGuBl71GqKhNT9KtyqtNzal7L7nEIfUq1NNkdACaT1O5D8KtX2zw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
15
examples/plugins/hello-world-wasm-typescript/package.json
Normal file
15
examples/plugins/hello-world-wasm-typescript/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "hello-world-wasm-typescript",
|
||||
"version": "0.1.0",
|
||||
"description": "A Bifrost WASM plugin example in TypeScript (AssemblyScript)",
|
||||
"scripts": {
|
||||
"build": "asc assembly/index.ts --outFile build/hello-world.wasm --optimize --runtime stub --use abort=",
|
||||
"build:debug": "asc assembly/index.ts --outFile build/hello-world.wasm --debug --runtime stub --use abort="
|
||||
},
|
||||
"dependencies": {
|
||||
"json-as": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"assemblyscript": "^0.27.29"
|
||||
}
|
||||
}
|
||||
15
examples/plugins/hello-world/.gitignore
vendored
Normal file
15
examples/plugins/hello-world/.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# Build artifacts
|
||||
build/
|
||||
*.so
|
||||
*.dll
|
||||
*.dylib
|
||||
|
||||
# Go build cache
|
||||
*.exe
|
||||
*.exe~
|
||||
*.test
|
||||
*.out
|
||||
|
||||
# Dependency directories
|
||||
vendor/
|
||||
|
||||
161
examples/plugins/hello-world/Makefile
Normal file
161
examples/plugins/hello-world/Makefile
Normal file
@@ -0,0 +1,161 @@
|
||||
.PHONY: all build dev clean install help test deps
|
||||
|
||||
# Note: Go plugins only support Linux and macOS (Darwin)
|
||||
# - Native builds: Use native Go compiler
|
||||
# - Linux cross-platform: Use Docker with appropriate platform
|
||||
# - macOS cross-arch: Use GOOS/GOARCH (amd64 <-> arm64 works on macOS)
|
||||
# - macOS builds require macOS host
|
||||
|
||||
# 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 name
|
||||
PLUGIN_NAME = hello-world
|
||||
OUTPUT_DIR = build
|
||||
|
||||
# Platform detection
|
||||
UNAME_S := $(shell uname -s)
|
||||
ifeq ($(UNAME_S),Linux)
|
||||
PLUGIN_EXT = .so
|
||||
PLATFORM = linux
|
||||
HOST_OS = linux
|
||||
endif
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
PLUGIN_EXT = .so
|
||||
PLATFORM = darwin
|
||||
HOST_OS = darwin
|
||||
endif
|
||||
|
||||
# Architecture detection
|
||||
UNAME_M := $(shell uname -m)
|
||||
ifeq ($(UNAME_M),x86_64)
|
||||
ARCH = amd64
|
||||
HOST_ARCH = amd64
|
||||
endif
|
||||
ifeq ($(UNAME_M),arm64)
|
||||
ARCH = arm64
|
||||
HOST_ARCH = arm64
|
||||
endif
|
||||
ifeq ($(UNAME_M),aarch64)
|
||||
ARCH = arm64
|
||||
HOST_ARCH = arm64
|
||||
endif
|
||||
|
||||
# Build configuration (can be overridden via command line)
|
||||
# Example: make build GOOS=linux GOARCH=amd64
|
||||
GOOS ?=
|
||||
GOARCH ?=
|
||||
|
||||
# Output file
|
||||
OUTPUT = $(OUTPUT_DIR)/$(PLUGIN_NAME)$(PLUGIN_EXT)
|
||||
|
||||
help: ## Show this help message
|
||||
@echo '$(COLOR_BOLD)Usage:$(COLOR_RESET) make [target] [GOOS=...] [GOARCH=...]'
|
||||
@echo ''
|
||||
@echo '$(COLOR_BOLD)Examples:$(COLOR_RESET)'
|
||||
@echo ' make dev # Build for development (fast, no optimizations)'
|
||||
@echo ' make build # Build for current platform (production)'
|
||||
@echo ' make build GOOS=linux GOARCH=amd64 # Build for Linux AMD64'
|
||||
@echo ' make build GOOS=darwin GOARCH=arm64 # Build for macOS ARM64'
|
||||
@echo ''
|
||||
@echo '$(COLOR_BOLD)Host System:$(COLOR_RESET)'
|
||||
@echo ' OS: $(HOST_OS)'
|
||||
@echo ' Architecture: $(HOST_ARCH)'
|
||||
@echo ''
|
||||
@echo '$(COLOR_BOLD)Build Notes:$(COLOR_RESET)'
|
||||
@echo ' - Native builds: Uses local Go compiler with CGO enabled'
|
||||
@echo ' - Cross-compilation: Uses Docker (Linux targets only)'
|
||||
@echo ' - macOS cross-compilation: Only works on macOS hosts'
|
||||
@echo ''
|
||||
@echo '$(COLOR_BOLD)Available targets:$(COLOR_RESET)'
|
||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " $(COLOR_INFO)%-20s$(COLOR_RESET) %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
|
||||
_clean_build_dir:
|
||||
@echo "$(COLOR_INFO)Cleaning build directory...$(COLOR_RESET)"
|
||||
@rm -rf $(OUTPUT_DIR)
|
||||
@echo "$(COLOR_SUCCESS)✓ Build directory cleaned$(COLOR_RESET)"
|
||||
|
||||
build: _clean_build_dir ## Build the plugin (supports: make build GOOS=linux GOARCH=amd64)
|
||||
@mkdir -p $(OUTPUT_DIR)
|
||||
@TARGET_OS="$(GOOS)"; \
|
||||
TARGET_ARCH="$(GOARCH)"; \
|
||||
ACTUAL_OS=$$(uname -s | tr '[:upper:]' '[:lower:]'); \
|
||||
ACTUAL_ARCH=$$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/;s/arm64/arm64/'); \
|
||||
if [ -z "$$TARGET_OS" ]; then \
|
||||
TARGET_OS=$$ACTUAL_OS; \
|
||||
fi; \
|
||||
if [ -z "$$TARGET_ARCH" ]; then \
|
||||
TARGET_ARCH=$$ACTUAL_ARCH; \
|
||||
fi; \
|
||||
HOST_OS=$$ACTUAL_OS; \
|
||||
HOST_ARCH=$$ACTUAL_ARCH; \
|
||||
echo "$(COLOR_INFO)Host: $$HOST_OS/$$HOST_ARCH | Target: $$TARGET_OS/$$TARGET_ARCH$(COLOR_RESET)"; \
|
||||
if [ "$$TARGET_OS" = "$$HOST_OS" ] && [ "$$TARGET_ARCH" = "$$HOST_ARCH" ]; then \
|
||||
echo "$(COLOR_INFO)Building plugin for $$TARGET_OS/$$TARGET_ARCH (native build)...$(COLOR_RESET)"; \
|
||||
CGO_ENABLED=1 GOOS=$$TARGET_OS GOARCH=$$TARGET_ARCH \
|
||||
go build -buildmode=plugin -o $(OUTPUT) main.go; \
|
||||
echo "$(COLOR_SUCCESS)✓ Plugin built successfully: $(OUTPUT)$(COLOR_RESET)"; \
|
||||
elif [ "$$HOST_OS" = "darwin" ] && [ "$$TARGET_OS" = "darwin" ]; then \
|
||||
echo "$(COLOR_INFO)Building plugin for $$TARGET_OS/$$TARGET_ARCH (macOS cross-arch)...$(COLOR_RESET)"; \
|
||||
CGO_ENABLED=1 GOOS=$$TARGET_OS GOARCH=$$TARGET_ARCH \
|
||||
go build -buildmode=plugin -ldflags="-w -s" -trimpath -o $(OUTPUT) main.go; \
|
||||
echo "$(COLOR_SUCCESS)✓ Plugin built successfully: $(OUTPUT)$(COLOR_RESET)"; \
|
||||
else \
|
||||
echo "$(COLOR_WARNING)Cross-compilation detected: $$HOST_OS/$$HOST_ARCH -> $$TARGET_OS/$$TARGET_ARCH$(COLOR_RESET)"; \
|
||||
echo "$(COLOR_INFO)Using Docker for cross-compilation...$(COLOR_RESET)"; \
|
||||
$(MAKE) _build-with-docker TARGET_OS=$$TARGET_OS TARGET_ARCH=$$TARGET_ARCH; \
|
||||
fi
|
||||
|
||||
dev: _clean_build_dir ## Build the plugin for development (no optimization flags)
|
||||
@mkdir -p $(OUTPUT_DIR)
|
||||
@echo "$(COLOR_INFO)Building plugin for development (native build, no optimizations)...$(COLOR_RESET)"
|
||||
CGO_ENABLED=1 go build -buildmode=plugin -o $(OUTPUT) main.go
|
||||
@echo "$(COLOR_SUCCESS)✓ Plugin built successfully: $(OUTPUT)$(COLOR_RESET)"
|
||||
|
||||
_build-with-docker: # Internal target for Docker-based cross-compilation
|
||||
@if [ "$(TARGET_OS)" = "linux" ]; then \
|
||||
echo "$(COLOR_INFO)Building for $(TARGET_OS)/$(TARGET_ARCH) in Docker container...$(COLOR_RESET)"; \
|
||||
docker run --rm \
|
||||
--platform linux/$(TARGET_ARCH) \
|
||||
-v "$(PWD):/work" \
|
||||
-w /work \
|
||||
-e CGO_ENABLED=1 \
|
||||
-e GOOS=$(TARGET_OS) \
|
||||
-e GOARCH=$(TARGET_ARCH) \
|
||||
golang:1.26.1-alpine3.23 \
|
||||
sh -c "apk add --no-cache gcc musl-dev && \
|
||||
go build -buildmode=plugin -ldflags='-w -s' -trimpath -o $(OUTPUT) main.go"; \
|
||||
echo "$(COLOR_SUCCESS)✓ Plugin built successfully: $(OUTPUT) ($(TARGET_OS)/$(TARGET_ARCH))$(COLOR_RESET)"; \
|
||||
else \
|
||||
echo "$(COLOR_ERROR)✗ Docker cross-compilation only supports Linux targets$(COLOR_RESET)"; \
|
||||
echo "$(COLOR_WARNING)For $(TARGET_OS), please build on a native $(TARGET_OS) machine$(COLOR_RESET)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
clean: _clean_build_dir ## Remove build artifacts
|
||||
@echo "$(COLOR_INFO)Cleaning build artifacts...$(COLOR_RESET)"
|
||||
@rm -rf $(OUTPUT_DIR)
|
||||
@echo "$(COLOR_SUCCESS)✓ Clean complete$(COLOR_RESET)"
|
||||
|
||||
install: build ## Build and install the plugin to Bifrost plugins directory
|
||||
@echo "$(COLOR_INFO)Installing plugin...$(COLOR_RESET)"
|
||||
@mkdir -p ~/.bifrost/plugins
|
||||
@cp $(OUTPUT) ~/.bifrost/plugins/
|
||||
@echo "$(COLOR_SUCCESS)✓ Plugin installed to ~/.bifrost/plugins/$(PLUGIN_NAME)$(PLUGIN_EXT)$(COLOR_RESET)"
|
||||
|
||||
test: _clean_build_dir ## Run tests
|
||||
@echo "$(COLOR_INFO)Running tests...$(COLOR_RESET)"
|
||||
go test -v ./...
|
||||
|
||||
deps: ## Download dependencies
|
||||
@echo "$(COLOR_INFO)Downloading dependencies...$(COLOR_RESET)"
|
||||
go mod download
|
||||
go mod tidy
|
||||
@echo "$(COLOR_SUCCESS)✓ Dependencies updated$(COLOR_RESET)"
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
36
examples/plugins/hello-world/go.mod
Normal file
36
examples/plugins/hello-world/go.mod
Normal file
@@ -0,0 +1,36 @@
|
||||
module github.com/maximhq/bifrost/examples/plugins/hello-world
|
||||
|
||||
go 1.26.2
|
||||
|
||||
require github.com/maximhq/bifrost/core v1.5.4
|
||||
|
||||
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/go-cmp v0.7.0 // 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/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // 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/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
91
examples/plugins/hello-world/go.sum
Normal file
91
examples/plugins/hello-world/go.sum
Normal file
@@ -0,0 +1,91 @@
|
||||
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.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
|
||||
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.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
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.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
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.5.4 h1:hf0BhoHVVpY1EQ4FkyRzW4IBYjrolxdZV0ucgWfHhcE=
|
||||
github.com/maximhq/bifrost/core v1.5.4/go.mod h1:z1/vOalbDAD7v7sYbXQsqR+2qIFP0jKOSIStw6Q4P4U=
|
||||
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/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
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.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
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=
|
||||
82
examples/plugins/hello-world/main.go
Normal file
82
examples/plugins/hello-world/main.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/maximhq/bifrost/core/schemas"
|
||||
)
|
||||
|
||||
const (
|
||||
transportPreHookKey schemas.BifrostContextKey = "hello-world-plugin-transport-pre-hook"
|
||||
transportPostHookKey schemas.BifrostContextKey = "hello-world-plugin-transport-post-hook"
|
||||
preHookKey schemas.BifrostContextKey = "hello-world-plugin-pre-hook"
|
||||
)
|
||||
|
||||
func Init(config any) error {
|
||||
fmt.Println("Init called")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetName returns the name of the plugin (required)
|
||||
// This is the system identifier - not editable by users
|
||||
// Users can set a custom display_name in the config for the UI
|
||||
func GetName() string {
|
||||
return "hello-world"
|
||||
}
|
||||
|
||||
func HTTPTransportPreHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest) (*schemas.HTTPResponse, error) {
|
||||
fmt.Println("HTTPTransportPreHook called")
|
||||
// Modify request in-place
|
||||
req.Headers["x-hello-world-plugin"] = "transport-pre-hook-value"
|
||||
// Store value in context for PreLLMHook/PostLLMHook
|
||||
ctx.SetValue(transportPreHookKey, "transport-pre-hook-value")
|
||||
// Return nil to continue processing, or return &schemas.HTTPResponse{} to short-circuit
|
||||
ctx.Log(schemas.LogLevelInfo, "HTTPTransportPreHook called")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func HTTPTransportPostHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
|
||||
fmt.Println("HTTPTransportPostHook called")
|
||||
// Modify response in-place
|
||||
resp.Headers["x-hello-world-plugin"] = "transport-post-hook-value"
|
||||
// Store value in context
|
||||
ctx.Log(schemas.LogLevelInfo, "HTTPTransportPostHook called")
|
||||
ctx.SetValue(transportPostHookKey, "transport-post-hook-value")
|
||||
// Return nil to continue processing
|
||||
return nil
|
||||
}
|
||||
|
||||
func HTTPTransportStreamChunkHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, chunk *schemas.BifrostStreamChunk) (*schemas.BifrostStreamChunk, error) {
|
||||
fmt.Println("HTTPTransportStreamChunkHook called")
|
||||
// Modify chunk in-place
|
||||
ctx.Log(schemas.LogLevelInfo, "HTTPTransportStreamChunkHook called")
|
||||
if chunk.BifrostChatResponse != nil && chunk.BifrostChatResponse.Choices != nil && len(chunk.BifrostChatResponse.Choices) > 0 && chunk.BifrostChatResponse.Choices[0].ChatStreamResponseChoice != nil && chunk.BifrostChatResponse.Choices[0].ChatStreamResponseChoice.Delta != nil && chunk.BifrostChatResponse.Choices[0].ChatStreamResponseChoice.Delta.Content != nil {
|
||||
*chunk.BifrostChatResponse.Choices[0].ChatStreamResponseChoice.Delta.Content += " - modified by hello-world-plugin"
|
||||
}
|
||||
// Return the modified chunk
|
||||
return chunk, nil
|
||||
}
|
||||
|
||||
func PreLLMHook(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.LLMPluginShortCircuit, error) {
|
||||
value1 := ctx.Value(transportPreHookKey)
|
||||
fmt.Println("value1:", value1)
|
||||
ctx.SetValue(preHookKey, "pre-hook-value")
|
||||
ctx.Log(schemas.LogLevelInfo, "PreLLMHook called")
|
||||
fmt.Println("PreLLMHook called")
|
||||
return req, nil, nil
|
||||
}
|
||||
|
||||
func PostLLMHook(ctx *schemas.BifrostContext, resp *schemas.BifrostResponse, bifrostErr *schemas.BifrostError) (*schemas.BifrostResponse, *schemas.BifrostError, error) {
|
||||
fmt.Println("PostLLMHook called")
|
||||
value1 := ctx.Value(transportPreHookKey)
|
||||
fmt.Println("value1:", value1)
|
||||
value2 := ctx.Value(preHookKey)
|
||||
fmt.Println("value2:", value2)
|
||||
ctx.Log(schemas.LogLevelInfo, "PostLLMHook called")
|
||||
return resp, bifrostErr, nil
|
||||
}
|
||||
|
||||
func Cleanup() error {
|
||||
fmt.Println("Cleanup called")
|
||||
return nil
|
||||
}
|
||||
12
examples/plugins/http-transport-only/Makefile
Normal file
12
examples/plugins/http-transport-only/Makefile
Normal file
@@ -0,0 +1,12 @@
|
||||
.PHONY: build clean
|
||||
|
||||
build:
|
||||
@echo "Building HTTP-Transport-Only plugin..."
|
||||
@mkdir -p build
|
||||
@go build -buildmode=plugin -o build/http-transport-only.so main.go
|
||||
@echo "Plugin built successfully: build/http-transport-only.so"
|
||||
|
||||
clean:
|
||||
@echo "Cleaning build directory..."
|
||||
@rm -rf build
|
||||
@echo "Clean complete"
|
||||
127
examples/plugins/http-transport-only/README.md
Normal file
127
examples/plugins/http-transport-only/README.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# HTTP-Transport-Only Plugin Example
|
||||
|
||||
This example demonstrates a plugin that only implements the `HTTPTransportPlugin` interface for HTTP-layer request/response interception.
|
||||
|
||||
## Features
|
||||
|
||||
- **HTTPTransportPreHook**: Intercepts HTTP requests before they enter Bifrost core
|
||||
- Authentication validation
|
||||
- Rate limiting (in-memory, per API key)
|
||||
- Request validation (size limits)
|
||||
- Custom header injection
|
||||
- Request short-circuiting for auth failures
|
||||
|
||||
- **HTTPTransportPostHook**: Intercepts HTTP responses after Bifrost core processing
|
||||
- CORS header injection
|
||||
- Security headers
|
||||
- Request duration tracking
|
||||
- Error response enrichment
|
||||
- Response logging
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **Security**
|
||||
- Authentication/Authorization
|
||||
- API key validation
|
||||
- Request sanitization
|
||||
|
||||
- **Rate Limiting**
|
||||
- Per-user limits
|
||||
- Per-endpoint limits
|
||||
- Burst protection
|
||||
|
||||
- **Observability**
|
||||
- Request/response logging
|
||||
- Performance monitoring
|
||||
- Access tracking
|
||||
|
||||
- **Compliance**
|
||||
- CORS enforcement
|
||||
- Security headers
|
||||
- Request/response auditing
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
This creates `build/http-transport-only.so`
|
||||
|
||||
## Configuration
|
||||
|
||||
Add to your Bifrost config:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": [
|
||||
{
|
||||
"path": "/path/to/http-transport-only.so",
|
||||
"name": "http-transport-only",
|
||||
"display_name": "Security & Rate Limiting",
|
||||
"enabled": true,
|
||||
"type": "http_transport",
|
||||
"config": {
|
||||
"require_auth": true,
|
||||
"rate_limit": 100,
|
||||
"rate_window": 60,
|
||||
"max_body_size": 1048576
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Note:**
|
||||
- `name` is the system identifier (from `GetName()`) and is **not editable**
|
||||
- `display_name` is shown in the UI and is **editable** by users
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `require_auth` | boolean | `true` | Enable/disable authentication header enforcement |
|
||||
| `rate_limit` | integer | `10` | Maximum requests per window (0 = unlimited) |
|
||||
| `rate_window` | integer | `60` | Rate limit window in seconds |
|
||||
| `max_body_size` | integer | `1048576` | Maximum request body size in bytes (0 = unlimited) |
|
||||
|
||||
### Example Configurations
|
||||
|
||||
**Disable authentication:**
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"require_auth": false,
|
||||
"rate_limit": 1000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Unlimited rate limiting:**
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"require_auth": true,
|
||||
"rate_limit": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Strict limits:**
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"require_auth": true,
|
||||
"rate_limit": 10,
|
||||
"rate_window": 60,
|
||||
"max_body_size": 512000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- This plugin operates at the HTTP transport layer only
|
||||
- Works only when using bifrost-http, not when using Bifrost as a Go SDK
|
||||
- Rate limiter is in-memory (resets on restart)
|
||||
- For production, consider using Redis for distributed rate limiting
|
||||
32
examples/plugins/http-transport-only/go.mod
Normal file
32
examples/plugins/http-transport-only/go.mod
Normal file
@@ -0,0 +1,32 @@
|
||||
module github.com/maximhq/bifrost/examples/plugins/http-transport-only
|
||||
|
||||
go 1.26.2
|
||||
|
||||
replace github.com/maximhq/bifrost/core => ../../../core
|
||||
|
||||
require github.com/maximhq/bifrost/core v0.0.0-00010101000000-000000000000
|
||||
|
||||
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
|
||||
)
|
||||
80
examples/plugins/http-transport-only/go.sum
Normal file
80
examples/plugins/http-transport-only/go.sum
Normal file
@@ -0,0 +1,80 @@
|
||||
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/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=
|
||||
235
examples/plugins/http-transport-only/main.go
Normal file
235
examples/plugins/http-transport-only/main.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/maximhq/bifrost/core/schemas"
|
||||
)
|
||||
|
||||
// Plugin configuration
|
||||
type PluginConfig struct {
|
||||
RequireAuth bool `json:"require_auth"` // Toggle auth header enforcement
|
||||
RateLimit int `json:"rate_limit"` // Max requests per window (0 = unlimited)
|
||||
RateWindow int `json:"rate_window"` // Rate limit window in seconds (default: 60)
|
||||
MaxBodySize int `json:"max_body_size"` // Max request body size in bytes (0 = unlimited)
|
||||
}
|
||||
|
||||
var (
|
||||
// Default configuration
|
||||
pluginConfig = &PluginConfig{
|
||||
RequireAuth: true, // Require auth by default
|
||||
RateLimit: 10, // 10 requests per window by default
|
||||
RateWindow: 60, // 60 second window by default
|
||||
MaxBodySize: 1024 * 1024, // 1MB by default
|
||||
}
|
||||
|
||||
rateLimiter = &RateLimiter{
|
||||
requests: make(map[string][]time.Time),
|
||||
}
|
||||
)
|
||||
|
||||
type RateLimiter struct {
|
||||
mu sync.Mutex
|
||||
requests map[string][]time.Time
|
||||
}
|
||||
|
||||
func (rl *RateLimiter) Allow(key string, limit int, window int) bool {
|
||||
// If rate limiting is disabled (limit = 0), allow all requests
|
||||
if limit <= 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
rl.mu.Lock()
|
||||
defer rl.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
windowStart := now.Add(-time.Duration(window) * time.Second)
|
||||
|
||||
// Clean old requests
|
||||
if reqs, ok := rl.requests[key]; ok {
|
||||
validReqs := []time.Time{}
|
||||
for _, t := range reqs {
|
||||
if t.After(windowStart) {
|
||||
validReqs = append(validReqs, t)
|
||||
}
|
||||
}
|
||||
rl.requests[key] = validReqs
|
||||
|
||||
// Check if limit exceeded
|
||||
if len(validReqs) >= limit {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Add new request
|
||||
rl.requests[key] = append(rl.requests[key], now)
|
||||
return true
|
||||
}
|
||||
|
||||
// Init is called when the plugin is loaded (optional)
|
||||
func Init(config any) error {
|
||||
fmt.Println("[HTTP-Transport-Only Plugin] Init called")
|
||||
|
||||
// Parse configuration
|
||||
if configMap, ok := config.(map[string]interface{}); ok {
|
||||
// Parse require_auth toggle
|
||||
if requireAuth, ok := configMap["require_auth"].(bool); ok {
|
||||
pluginConfig.RequireAuth = requireAuth
|
||||
fmt.Printf("[HTTP-Transport-Only Plugin] Auth enforcement: %v\n", pluginConfig.RequireAuth)
|
||||
}
|
||||
|
||||
// Parse rate_limit
|
||||
if rateLimit, ok := configMap["rate_limit"].(float64); ok {
|
||||
pluginConfig.RateLimit = int(rateLimit)
|
||||
if pluginConfig.RateLimit <= 0 {
|
||||
fmt.Println("[HTTP-Transport-Only Plugin] Rate limiting disabled")
|
||||
} else {
|
||||
fmt.Printf("[HTTP-Transport-Only Plugin] Rate limit: %d requests per %d seconds\n",
|
||||
pluginConfig.RateLimit, pluginConfig.RateWindow)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse rate_window
|
||||
if rateWindow, ok := configMap["rate_window"].(float64); ok {
|
||||
pluginConfig.RateWindow = int(rateWindow)
|
||||
fmt.Printf("[HTTP-Transport-Only Plugin] Rate window: %d seconds\n", pluginConfig.RateWindow)
|
||||
}
|
||||
|
||||
// Parse max_body_size
|
||||
if maxBodySize, ok := configMap["max_body_size"].(float64); ok {
|
||||
pluginConfig.MaxBodySize = int(maxBodySize)
|
||||
if pluginConfig.MaxBodySize <= 0 {
|
||||
fmt.Println("[HTTP-Transport-Only Plugin] Request size validation disabled")
|
||||
} else {
|
||||
fmt.Printf("[HTTP-Transport-Only Plugin] Max body size: %d bytes\n", pluginConfig.MaxBodySize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[HTTP-Transport-Only Plugin] Configuration loaded: %+v\n", pluginConfig)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetName returns the name of the plugin (required)
|
||||
// This is the system identifier - not editable by users
|
||||
// Users can set a custom display_name in the config for the UI
|
||||
func GetName() string {
|
||||
return "http-transport-only"
|
||||
}
|
||||
|
||||
// HTTPTransportPreHook is called at the HTTP layer before requests enter Bifrost core
|
||||
// This example demonstrates authentication, rate limiting, and request validation
|
||||
func HTTPTransportPreHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest) (*schemas.HTTPResponse, error) {
|
||||
fmt.Println("[HTTP-Transport-Only Plugin] HTTPTransportPreHook called")
|
||||
fmt.Printf("[HTTP-Transport-Only Plugin] Method: %s, Path: %s\n", req.Method, req.Path)
|
||||
|
||||
// Example 1: Authentication check (configurable)
|
||||
authHeader := req.CaseInsensitiveHeaderLookup("Authorization")
|
||||
if pluginConfig.RequireAuth && authHeader == "" {
|
||||
fmt.Println("[HTTP-Transport-Only Plugin] Missing authorization header")
|
||||
return &schemas.HTTPResponse{
|
||||
StatusCode: 401,
|
||||
Headers: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
Body: []byte(`{"error": "Unauthorized: Missing authorization header"}`),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Example 2: Rate limiting by API key (configurable)
|
||||
if pluginConfig.RateLimit > 0 {
|
||||
apiKey := authHeader // In real implementation, extract from Bearer token
|
||||
if apiKey == "" {
|
||||
apiKey = "anonymous" // Default key for unauthenticated requests
|
||||
}
|
||||
|
||||
if !rateLimiter.Allow(apiKey, pluginConfig.RateLimit, pluginConfig.RateWindow) {
|
||||
fmt.Println("[HTTP-Transport-Only Plugin] Rate limit exceeded")
|
||||
return &schemas.HTTPResponse{
|
||||
StatusCode: 429,
|
||||
Headers: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"Retry-After": fmt.Sprintf("%d", pluginConfig.RateWindow),
|
||||
"X-RateLimit-Limit": fmt.Sprintf("%d", pluginConfig.RateLimit),
|
||||
},
|
||||
Body: []byte(`{"error": "Rate limit exceeded. Please try again later."}`),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Example 3: Request validation (configurable)
|
||||
if pluginConfig.MaxBodySize > 0 && req.Method == "POST" && len(req.Body) > pluginConfig.MaxBodySize {
|
||||
fmt.Printf("[HTTP-Transport-Only Plugin] Request body too large: %d bytes (max: %d)\n",
|
||||
len(req.Body), pluginConfig.MaxBodySize)
|
||||
return &schemas.HTTPResponse{
|
||||
StatusCode: 413,
|
||||
Headers: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
Body: []byte(fmt.Sprintf(`{"error": "Request body too large. Max size: %d bytes"}`, pluginConfig.MaxBodySize)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Example 4: Add custom headers
|
||||
req.Headers["X-Plugin-Processed"] = "true"
|
||||
req.Headers["X-Request-Time"] = time.Now().Format(time.RFC3339)
|
||||
|
||||
// Store metadata in context for PostHook
|
||||
ctx.SetValue(schemas.BifrostContextKey("http-plugin-start-time"), time.Now())
|
||||
|
||||
// Return nil to continue processing
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// HTTPTransportPostHook is called at the HTTP layer after Bifrost core processes the request
|
||||
// This example demonstrates response modification and logging
|
||||
func HTTPTransportPostHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
|
||||
fmt.Println("[HTTP-Transport-Only Plugin] HTTPTransportPostHook called")
|
||||
|
||||
// Calculate request duration
|
||||
startTime := ctx.Value(schemas.BifrostContextKey("http-plugin-start-time"))
|
||||
if t, ok := startTime.(time.Time); ok {
|
||||
duration := time.Since(t)
|
||||
fmt.Printf("[HTTP-Transport-Only Plugin] Request duration: %v\n", duration)
|
||||
|
||||
// Add duration header
|
||||
resp.Headers["X-Request-Duration-Ms"] = fmt.Sprintf("%d", duration.Milliseconds())
|
||||
}
|
||||
|
||||
// Example: Add CORS headers
|
||||
resp.Headers["Access-Control-Allow-Origin"] = "*"
|
||||
resp.Headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
|
||||
resp.Headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
|
||||
|
||||
// Example: Add security headers
|
||||
resp.Headers["X-Content-Type-Options"] = "nosniff"
|
||||
resp.Headers["X-Frame-Options"] = "DENY"
|
||||
resp.Headers["X-XSS-Protection"] = "1; mode=block"
|
||||
|
||||
// Example: Log response details
|
||||
fmt.Printf("[HTTP-Transport-Only Plugin] Response status: %d, size: %d bytes\n",
|
||||
resp.StatusCode, len(resp.Body))
|
||||
|
||||
// Example: Modify error responses to add custom metadata
|
||||
if resp.StatusCode >= 400 {
|
||||
var errorBody map[string]interface{}
|
||||
if err := json.Unmarshal(resp.Body, &errorBody); err == nil {
|
||||
errorBody["timestamp"] = time.Now().Format(time.RFC3339)
|
||||
errorBody["request_id"] = ctx.Value(schemas.BifrostContextKey("request_id"))
|
||||
if newBody, err := json.Marshal(errorBody); err == nil {
|
||||
resp.Body = newBody
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup is called when the plugin is unloaded (required)
|
||||
func Cleanup() error {
|
||||
fmt.Println("[HTTP-Transport-Only Plugin] Cleanup called")
|
||||
return nil
|
||||
}
|
||||
12
examples/plugins/llm-only/Makefile
Normal file
12
examples/plugins/llm-only/Makefile
Normal file
@@ -0,0 +1,12 @@
|
||||
.PHONY: build clean
|
||||
|
||||
build:
|
||||
@echo "Building LLM-Only plugin..."
|
||||
@mkdir -p build
|
||||
@go build -buildmode=plugin -o build/llm-only.so main.go
|
||||
@echo "Plugin built successfully: build/llm-only.so"
|
||||
|
||||
clean:
|
||||
@echo "Cleaning build directory..."
|
||||
@rm -rf build
|
||||
@echo "Clean complete"
|
||||
103
examples/plugins/llm-only/README.md
Normal file
103
examples/plugins/llm-only/README.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# LLM-Only Plugin Example
|
||||
|
||||
This example demonstrates a plugin that only implements the `LLMPlugin` interface.
|
||||
|
||||
## Features
|
||||
|
||||
- **PreLLMHook**: Intercepts requests before they reach the LLM provider
|
||||
- Logs request details
|
||||
- Modifies requests (adds system message)
|
||||
- Stores metadata in context
|
||||
|
||||
- **PostLLMHook**: Intercepts responses after the LLM provider responds
|
||||
- Logs response details
|
||||
- Accesses context metadata
|
||||
- Handles errors
|
||||
|
||||
## Use Cases
|
||||
|
||||
- Request/response logging
|
||||
- Adding default system messages
|
||||
- Request validation
|
||||
- Response filtering
|
||||
- Token counting
|
||||
- Cost tracking
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
This creates `build/llm-only.so`
|
||||
|
||||
## Configuration
|
||||
|
||||
Add to your Bifrost config:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": [
|
||||
{
|
||||
"path": "/path/to/llm-only.so",
|
||||
"name": "llm-only",
|
||||
"display_name": "LLM Request Logger",
|
||||
"enabled": true,
|
||||
"type": "llm",
|
||||
"config": {
|
||||
"inject_system_message": true,
|
||||
"system_message_text": "You are a helpful assistant.",
|
||||
"enable_logging": true,
|
||||
"log_requests": true,
|
||||
"log_responses": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Note:**
|
||||
- `name` is the system identifier (from `GetName()`) and is **not editable**
|
||||
- `display_name` is shown in the UI and is **editable** by users
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `inject_system_message` | boolean | `true` | Enable/disable automatic system message injection |
|
||||
| `system_message_text` | string | `"You are a helpful assistant..."` | Custom system message to inject |
|
||||
| `enable_logging` | boolean | `true` | Enable/disable detailed logging |
|
||||
| `log_requests` | boolean | `true` | Log request details (provider, model) |
|
||||
| `log_responses` | boolean | `true` | Log response details (ID, choices) |
|
||||
|
||||
### Example Configurations
|
||||
|
||||
**Minimal logging:**
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"enable_logging": false,
|
||||
"log_requests": false,
|
||||
"log_responses": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Custom system message:**
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"inject_system_message": true,
|
||||
"system_message_text": "You are a technical expert. Provide detailed, accurate answers."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**No system message injection:**
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"inject_system_message": false
|
||||
}
|
||||
}
|
||||
```
|
||||
32
examples/plugins/llm-only/go.mod
Normal file
32
examples/plugins/llm-only/go.mod
Normal file
@@ -0,0 +1,32 @@
|
||||
module github.com/maximhq/bifrost/examples/plugins/llm-only
|
||||
|
||||
go 1.26.2
|
||||
|
||||
replace github.com/maximhq/bifrost/core => ../../../core
|
||||
|
||||
require github.com/maximhq/bifrost/core v0.0.0-00010101000000-000000000000
|
||||
|
||||
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
|
||||
)
|
||||
80
examples/plugins/llm-only/go.sum
Normal file
80
examples/plugins/llm-only/go.sum
Normal file
@@ -0,0 +1,80 @@
|
||||
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/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=
|
||||
142
examples/plugins/llm-only/main.go
Normal file
142
examples/plugins/llm-only/main.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/maximhq/bifrost/core/schemas"
|
||||
)
|
||||
|
||||
// Plugin configuration
|
||||
type PluginConfig struct {
|
||||
InjectSystemMessage bool `json:"inject_system_message"` // Toggle system message injection
|
||||
SystemMessageText string `json:"system_message_text"` // Custom system message
|
||||
EnableLogging bool `json:"enable_logging"` // Toggle detailed logging
|
||||
LogRequests bool `json:"log_requests"` // Log request details
|
||||
LogResponses bool `json:"log_responses"` // Log response details
|
||||
}
|
||||
|
||||
var (
|
||||
// Default configuration
|
||||
pluginConfig = &PluginConfig{
|
||||
InjectSystemMessage: true,
|
||||
SystemMessageText: "You are a helpful assistant. This message was added by an LLM plugin.",
|
||||
EnableLogging: true,
|
||||
LogRequests: true,
|
||||
LogResponses: true,
|
||||
}
|
||||
)
|
||||
|
||||
// Init is called when the plugin is loaded (optional)
|
||||
func Init(config any) error {
|
||||
fmt.Println("[LLM-Only Plugin] Init called")
|
||||
|
||||
// Parse configuration
|
||||
if configMap, ok := config.(map[string]interface{}); ok {
|
||||
if injectMsg, ok := configMap["inject_system_message"].(bool); ok {
|
||||
pluginConfig.InjectSystemMessage = injectMsg
|
||||
fmt.Printf("[LLM-Only Plugin] System message injection: %v\n", pluginConfig.InjectSystemMessage)
|
||||
}
|
||||
|
||||
if msgText, ok := configMap["system_message_text"].(string); ok {
|
||||
pluginConfig.SystemMessageText = msgText
|
||||
fmt.Printf("[LLM-Only Plugin] System message: %s\n", pluginConfig.SystemMessageText)
|
||||
}
|
||||
|
||||
if enableLogging, ok := configMap["enable_logging"].(bool); ok {
|
||||
pluginConfig.EnableLogging = enableLogging
|
||||
fmt.Printf("[LLM-Only Plugin] Logging enabled: %v\n", pluginConfig.EnableLogging)
|
||||
}
|
||||
|
||||
if logReq, ok := configMap["log_requests"].(bool); ok {
|
||||
pluginConfig.LogRequests = logReq
|
||||
}
|
||||
|
||||
if logResp, ok := configMap["log_responses"].(bool); ok {
|
||||
pluginConfig.LogResponses = logResp
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[LLM-Only Plugin] Configuration loaded: %+v\n", pluginConfig)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetName returns the name of the plugin (required)
|
||||
// This is the system identifier - not editable by users
|
||||
// Users can set a custom display_name in the config for the UI
|
||||
func GetName() string {
|
||||
return "llm-only"
|
||||
}
|
||||
|
||||
// PreLLMHook is called before the LLM provider is invoked
|
||||
// This example demonstrates request modification and logging
|
||||
func PreLLMHook(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.LLMPluginShortCircuit, error) {
|
||||
if pluginConfig.EnableLogging {
|
||||
fmt.Println("[LLM-Only Plugin] PreLLMHook called")
|
||||
}
|
||||
|
||||
// Example: Log the request (configurable)
|
||||
if pluginConfig.LogRequests && req.ChatRequest != nil {
|
||||
fmt.Printf("[LLM-Only Plugin] Provider: %s, Model: %s\n",
|
||||
req.ChatRequest.Provider, req.ChatRequest.Model)
|
||||
if pluginConfig.EnableLogging {
|
||||
fmt.Printf("[LLM-Only Plugin] Message count: %d\n", len(req.ChatRequest.Input))
|
||||
}
|
||||
}
|
||||
|
||||
// Example: Store metadata in context
|
||||
ctx.SetValue(schemas.BifrostContextKey("llm-plugin-timestamp"), "pre-hook-timestamp")
|
||||
|
||||
// Example: Modify the request (add a system message) - configurable
|
||||
if pluginConfig.InjectSystemMessage && req.ChatRequest != nil && req.ChatRequest.Input != nil {
|
||||
systemMsg := schemas.ChatMessage{
|
||||
Role: "system",
|
||||
Content: &schemas.ChatMessageContent{ContentStr: &pluginConfig.SystemMessageText},
|
||||
}
|
||||
req.ChatRequest.Input = append([]schemas.ChatMessage{systemMsg}, req.ChatRequest.Input...)
|
||||
if pluginConfig.EnableLogging {
|
||||
fmt.Println("[LLM-Only Plugin] System message injected")
|
||||
}
|
||||
}
|
||||
|
||||
// Return modified request, no short-circuit, no error
|
||||
return req, nil, nil
|
||||
}
|
||||
|
||||
// PostLLMHook is called after the LLM provider responds
|
||||
// This example demonstrates response modification and logging
|
||||
func PostLLMHook(ctx *schemas.BifrostContext, resp *schemas.BifrostResponse, bifrostErr *schemas.BifrostError) (*schemas.BifrostResponse, *schemas.BifrostError, error) {
|
||||
if pluginConfig.EnableLogging {
|
||||
fmt.Println("[LLM-Only Plugin] PostLLMHook called")
|
||||
}
|
||||
|
||||
// Retrieve metadata from context
|
||||
if pluginConfig.EnableLogging {
|
||||
timestamp := ctx.Value(schemas.BifrostContextKey("llm-plugin-timestamp"))
|
||||
fmt.Printf("[LLM-Only Plugin] Request timestamp: %v\n", timestamp)
|
||||
}
|
||||
|
||||
// Example: Log the response (configurable)
|
||||
if pluginConfig.LogResponses && resp != nil && resp.ChatResponse != nil {
|
||||
fmt.Printf("[LLM-Only Plugin] Response ID: %s, Model: %s\n",
|
||||
resp.ChatResponse.ID, resp.ChatResponse.Model)
|
||||
if pluginConfig.EnableLogging && len(resp.ChatResponse.Choices) > 0 {
|
||||
fmt.Printf("[LLM-Only Plugin] Choices count: %d\n", len(resp.ChatResponse.Choices))
|
||||
}
|
||||
}
|
||||
|
||||
// Example: Log errors if present
|
||||
if bifrostErr != nil && bifrostErr.Error != nil {
|
||||
fmt.Printf("[LLM-Only Plugin] Error occurred: %v\n", bifrostErr.Error.Message)
|
||||
}
|
||||
|
||||
// Return unmodified response and error
|
||||
return resp, bifrostErr, nil
|
||||
}
|
||||
|
||||
// Cleanup is called when the plugin is unloaded (required)
|
||||
func Cleanup() error {
|
||||
if pluginConfig.EnableLogging {
|
||||
fmt.Println("[LLM-Only Plugin] Cleanup called")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
12
examples/plugins/mcp-only/Makefile
Normal file
12
examples/plugins/mcp-only/Makefile
Normal file
@@ -0,0 +1,12 @@
|
||||
.PHONY: build clean
|
||||
|
||||
build:
|
||||
@echo "Building MCP-Only plugin..."
|
||||
@mkdir -p build
|
||||
@go build -buildmode=plugin -o build/mcp-only.so main.go
|
||||
@echo "Plugin built successfully: build/mcp-only.so"
|
||||
|
||||
clean:
|
||||
@echo "Cleaning build directory..."
|
||||
@rm -rf build
|
||||
@echo "Clean complete"
|
||||
112
examples/plugins/mcp-only/README.md
Normal file
112
examples/plugins/mcp-only/README.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# MCP-Only Plugin Example
|
||||
|
||||
This example demonstrates a plugin that only implements the `MCPPlugin` interface for Model Context Protocol governance.
|
||||
|
||||
## Features
|
||||
|
||||
- **PreMCPHook**: Intercepts MCP requests before execution
|
||||
- Validates tool/resource calls
|
||||
- Implements governance policies (blocking dangerous tools)
|
||||
- Adds audit trails
|
||||
- Can short-circuit calls with custom responses
|
||||
|
||||
- **PostMCPHook**: Intercepts MCP responses after execution
|
||||
- Logs responses
|
||||
- Transforms error messages
|
||||
- Accesses audit trails from context
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **Security & Governance**
|
||||
- Block unauthorized tool calls
|
||||
- Enforce access control policies
|
||||
- Validate tool parameters
|
||||
|
||||
- **Observability**
|
||||
- Log all MCP interactions
|
||||
- Track tool usage
|
||||
- Monitor resource access
|
||||
|
||||
- **Error Handling**
|
||||
- Transform error messages
|
||||
- Add retry logic
|
||||
- Provide fallback responses
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
This creates `build/mcp-only.so`
|
||||
|
||||
## Configuration
|
||||
|
||||
Add to your Bifrost config:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": [
|
||||
{
|
||||
"path": "/path/to/mcp-only.so",
|
||||
"name": "mcp-only",
|
||||
"display_name": "MCP Tool Governance",
|
||||
"enabled": true,
|
||||
"type": "mcp",
|
||||
"config": {
|
||||
"blocked_tools": ["dangerous_tool", "risky_operation"],
|
||||
"enable_audit": true,
|
||||
"enable_logging": true,
|
||||
"transform_errors": true,
|
||||
"custom_error_message": "Tool is not allowed by security policy"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Note:**
|
||||
- `name` is the system identifier (from `GetName()`) and is **not editable**
|
||||
- `display_name` is shown in the UI and is **editable** by users
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `blocked_tools` | array of strings | `["dangerous_tool"]` | List of tool names to block |
|
||||
| `enable_audit` | boolean | `true` | Enable audit trail logging |
|
||||
| `enable_logging` | boolean | `true` | Enable detailed logging |
|
||||
| `transform_errors` | boolean | `true` | Transform 404 errors to user-friendly messages |
|
||||
| `custom_error_message` | string | `"Tool is not allowed..."` | Custom error message for blocked tools |
|
||||
|
||||
### Example Configurations
|
||||
|
||||
**Block multiple tools:**
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"blocked_tools": ["delete_data", "modify_system", "unsafe_exec"],
|
||||
"custom_error_message": "This tool is disabled for security reasons"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Minimal logging:**
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"enable_audit": false,
|
||||
"enable_logging": false,
|
||||
"transform_errors": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Allow all tools:**
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"blocked_tools": []
|
||||
}
|
||||
}
|
||||
```
|
||||
32
examples/plugins/mcp-only/go.mod
Normal file
32
examples/plugins/mcp-only/go.mod
Normal file
@@ -0,0 +1,32 @@
|
||||
module github.com/maximhq/bifrost/examples/plugins/mcp-only
|
||||
|
||||
go 1.26.2
|
||||
|
||||
replace github.com/maximhq/bifrost/core => ../../../core
|
||||
|
||||
require github.com/maximhq/bifrost/core v0.0.0-00010101000000-000000000000
|
||||
|
||||
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
|
||||
)
|
||||
80
examples/plugins/mcp-only/go.sum
Normal file
80
examples/plugins/mcp-only/go.sum
Normal file
@@ -0,0 +1,80 @@
|
||||
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/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=
|
||||
195
examples/plugins/mcp-only/main.go
Normal file
195
examples/plugins/mcp-only/main.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/maximhq/bifrost/core/schemas"
|
||||
)
|
||||
|
||||
// Plugin configuration
|
||||
type PluginConfig struct {
|
||||
BlockedTools []string `json:"blocked_tools"` // List of tool names to block
|
||||
EnableAudit bool `json:"enable_audit"` // Enable audit trail logging
|
||||
EnableLogging bool `json:"enable_logging"` // Enable detailed logging
|
||||
TransformErrors bool `json:"transform_errors"` // Transform 404 errors to friendly messages
|
||||
CustomErrorMessage string `json:"custom_error_message"` // Custom error message for blocked tools
|
||||
}
|
||||
|
||||
var (
|
||||
// Default configuration
|
||||
pluginConfig = &PluginConfig{
|
||||
BlockedTools: []string{"dangerous_tool"},
|
||||
EnableAudit: true,
|
||||
EnableLogging: true,
|
||||
TransformErrors: true,
|
||||
CustomErrorMessage: "Tool is not allowed by security policy",
|
||||
}
|
||||
)
|
||||
|
||||
// Init is called when the plugin is loaded (optional)
|
||||
func Init(config any) error {
|
||||
fmt.Println("[MCP-Only Plugin] Init called")
|
||||
|
||||
// Parse configuration
|
||||
if configMap, ok := config.(map[string]interface{}); ok {
|
||||
if blockedTools, ok := configMap["blocked_tools"].([]interface{}); ok {
|
||||
pluginConfig.BlockedTools = []string{}
|
||||
for _, tool := range blockedTools {
|
||||
if toolName, ok := tool.(string); ok {
|
||||
pluginConfig.BlockedTools = append(pluginConfig.BlockedTools, toolName)
|
||||
}
|
||||
}
|
||||
fmt.Printf("[MCP-Only Plugin] Blocked tools: %v\n", pluginConfig.BlockedTools)
|
||||
}
|
||||
|
||||
if enableAudit, ok := configMap["enable_audit"].(bool); ok {
|
||||
pluginConfig.EnableAudit = enableAudit
|
||||
fmt.Printf("[MCP-Only Plugin] Audit trail: %v\n", pluginConfig.EnableAudit)
|
||||
}
|
||||
|
||||
if enableLogging, ok := configMap["enable_logging"].(bool); ok {
|
||||
pluginConfig.EnableLogging = enableLogging
|
||||
fmt.Printf("[MCP-Only Plugin] Logging enabled: %v\n", pluginConfig.EnableLogging)
|
||||
}
|
||||
|
||||
if transformErrors, ok := configMap["transform_errors"].(bool); ok {
|
||||
pluginConfig.TransformErrors = transformErrors
|
||||
fmt.Printf("[MCP-Only Plugin] Error transformation: %v\n", pluginConfig.TransformErrors)
|
||||
}
|
||||
|
||||
if customMsg, ok := configMap["custom_error_message"].(string); ok {
|
||||
pluginConfig.CustomErrorMessage = customMsg
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[MCP-Only Plugin] Configuration loaded: %+v\n", pluginConfig)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetName returns the name of the plugin (required)
|
||||
// This is the system identifier - not editable by users
|
||||
// Users can set a custom display_name in the config for the UI
|
||||
func GetName() string {
|
||||
return "mcp-only"
|
||||
}
|
||||
|
||||
// PreMCPHook is called before MCP tool/resource calls are executed
|
||||
// This example demonstrates request validation and governance
|
||||
func PreMCPHook(ctx *schemas.BifrostContext, req *schemas.BifrostMCPRequest) (*schemas.BifrostMCPRequest, *schemas.MCPPluginShortCircuit, error) {
|
||||
if pluginConfig.EnableLogging {
|
||||
fmt.Println("[MCP-Only Plugin] PreMCPHook called")
|
||||
fmt.Printf("[MCP-Only Plugin] Request type: %v\n", req.RequestType)
|
||||
}
|
||||
|
||||
// Example: Governance - check tool calls (configurable)
|
||||
if req.ChatAssistantMessageToolCall != nil {
|
||||
toolName := ""
|
||||
if req.ChatAssistantMessageToolCall.Function.Name != nil {
|
||||
toolName = *req.ChatAssistantMessageToolCall.Function.Name
|
||||
}
|
||||
|
||||
if pluginConfig.EnableLogging {
|
||||
fmt.Printf("[MCP-Only Plugin] Tool call: %s\n", toolName)
|
||||
}
|
||||
|
||||
// Check if tool is in blocked list
|
||||
for _, blockedTool := range pluginConfig.BlockedTools {
|
||||
if toolName == blockedTool {
|
||||
fmt.Printf("[MCP-Only Plugin] Blocked tool call: %s\n", toolName)
|
||||
// Return a short-circuit response to prevent the call
|
||||
errorMsg := fmt.Sprintf("%s: %s", pluginConfig.CustomErrorMessage, toolName)
|
||||
// Get the tool call ID to link the response back to the original call
|
||||
toolCallID := req.ChatAssistantMessageToolCall.ID
|
||||
return req, &schemas.MCPPluginShortCircuit{
|
||||
Response: &schemas.BifrostMCPResponse{
|
||||
// Chat API format - tool result message
|
||||
ChatMessage: &schemas.ChatMessage{
|
||||
Role: schemas.ChatMessageRoleTool,
|
||||
ChatToolMessage: &schemas.ChatToolMessage{
|
||||
ToolCallID: toolCallID,
|
||||
},
|
||||
Content: &schemas.ChatMessageContent{
|
||||
ContentStr: &errorMsg,
|
||||
},
|
||||
},
|
||||
// Responses API format - function_call_output
|
||||
ResponsesMessage: &schemas.ResponsesMessage{
|
||||
Type: schemas.Ptr(schemas.ResponsesMessageTypeFunctionCallOutput),
|
||||
ResponsesToolMessage: &schemas.ResponsesToolMessage{
|
||||
CallID: toolCallID,
|
||||
Output: &schemas.ResponsesToolMessageOutputStruct{
|
||||
ResponsesToolCallOutputStr: &errorMsg,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example: Add audit trail to context (configurable)
|
||||
if pluginConfig.EnableAudit {
|
||||
auditMsg := fmt.Sprintf("MCP request processed at %v", ctx.Value(schemas.BifrostContextKey("request_id")))
|
||||
ctx.SetValue(schemas.BifrostContextKey("mcp-audit-trail"), auditMsg)
|
||||
if pluginConfig.EnableLogging {
|
||||
fmt.Printf("[MCP-Only Plugin] Audit: %s\n", auditMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// Return modified request, no short-circuit, no error
|
||||
return req, nil, nil
|
||||
}
|
||||
|
||||
// PostMCPHook is called after MCP tool/resource calls complete
|
||||
// This example demonstrates response logging and error handling
|
||||
func PostMCPHook(ctx *schemas.BifrostContext, resp *schemas.BifrostMCPResponse, bifrostErr *schemas.BifrostError) (*schemas.BifrostMCPResponse, *schemas.BifrostError, error) {
|
||||
if pluginConfig.EnableLogging {
|
||||
fmt.Println("[MCP-Only Plugin] PostMCPHook called")
|
||||
}
|
||||
|
||||
// Retrieve audit trail from context (if enabled)
|
||||
if pluginConfig.EnableAudit {
|
||||
auditTrail := ctx.Value(schemas.BifrostContextKey("mcp-audit-trail"))
|
||||
if pluginConfig.EnableLogging {
|
||||
fmt.Printf("[MCP-Only Plugin] Audit trail: %v\n", auditTrail)
|
||||
}
|
||||
}
|
||||
|
||||
// Example: Log the response (configurable)
|
||||
if pluginConfig.EnableLogging && resp != nil {
|
||||
if resp.ChatMessage != nil {
|
||||
fmt.Printf("[MCP-Only Plugin] Chat message response received\n")
|
||||
}
|
||||
if resp.ResponsesMessage != nil {
|
||||
fmt.Printf("[MCP-Only Plugin] Responses message received\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Example: Log errors if present
|
||||
if bifrostErr != nil && bifrostErr.Error != nil {
|
||||
fmt.Printf("[MCP-Only Plugin] Error occurred: %v\n", bifrostErr.Error.Message)
|
||||
}
|
||||
|
||||
// Example: Transform error responses (configurable)
|
||||
if pluginConfig.TransformErrors && bifrostErr != nil && bifrostErr.StatusCode != nil && *bifrostErr.StatusCode == 404 {
|
||||
// Convert 404 to a more user-friendly error
|
||||
if bifrostErr.Error != nil {
|
||||
bifrostErr.Error.Message = "The requested MCP resource was not found. Please check your request."
|
||||
if pluginConfig.EnableLogging {
|
||||
fmt.Println("[MCP-Only Plugin] Error message transformed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return modified response and error
|
||||
return resp, bifrostErr, nil
|
||||
}
|
||||
|
||||
// Cleanup is called when the plugin is unloaded (required)
|
||||
func Cleanup() error {
|
||||
if pluginConfig.EnableLogging {
|
||||
fmt.Println("[MCP-Only Plugin] Cleanup called")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
12
examples/plugins/multi-interface/Makefile
Normal file
12
examples/plugins/multi-interface/Makefile
Normal file
@@ -0,0 +1,12 @@
|
||||
.PHONY: build clean
|
||||
|
||||
build:
|
||||
@echo "Building Multi-Interface plugin..."
|
||||
@mkdir -p build
|
||||
@go build -buildmode=plugin -o build/multi-interface.so main.go
|
||||
@echo "Plugin built successfully: build/multi-interface.so"
|
||||
|
||||
clean:
|
||||
@echo "Cleaning build directory..."
|
||||
@rm -rf build
|
||||
@echo "Clean complete"
|
||||
179
examples/plugins/multi-interface/README.md
Normal file
179
examples/plugins/multi-interface/README.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# Multi-Interface Plugin Example
|
||||
|
||||
This example demonstrates a plugin that implements **all plugin interfaces**:
|
||||
- `HTTPTransportPlugin`
|
||||
- `LLMPlugin`
|
||||
- `MCPPlugin`
|
||||
- `ObservabilityPlugin`
|
||||
|
||||
## Features
|
||||
|
||||
### HTTPTransportPlugin
|
||||
- Tracks request count across all requests
|
||||
- Adds request number header
|
||||
- Calculates HTTP request duration
|
||||
- Stores HTTP metadata in context for other hooks
|
||||
|
||||
### LLMPlugin
|
||||
- Accesses HTTP context metadata
|
||||
- Adds dynamic system prompts
|
||||
- Tracks LLM call duration
|
||||
- Logs request/response details
|
||||
|
||||
### MCPPlugin
|
||||
- Accesses HTTP context metadata
|
||||
- Logs all MCP tool/resource calls
|
||||
- Tracks MCP call duration
|
||||
- Implements governance for MCP calls
|
||||
|
||||
### ObservabilityPlugin
|
||||
- Receives completed traces asynchronously
|
||||
- Formats traces as JSON
|
||||
- Ready for integration with OTEL, Datadog, Jaeger, etc.
|
||||
- Demonstrates end-to-end request tracking
|
||||
|
||||
## Context Flow
|
||||
|
||||
This plugin demonstrates how context flows through different hooks:
|
||||
|
||||
1. **HTTPTransportPreHook** → Stores HTTP metadata
|
||||
2. **PreLLMHook/PreMCPHook** → Accesses HTTP metadata, stores LLM/MCP metadata
|
||||
3. **PostLLMHook/PostMCPHook** → Accesses stored timing data
|
||||
4. **HTTPTransportPostHook** → Adds final headers
|
||||
5. **Inject** → Receives complete trace asynchronously
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **Full-stack observability** - Track requests from HTTP to LLM/MCP and back
|
||||
- **Unified governance** - Apply policies at multiple layers
|
||||
- **Performance monitoring** - Measure duration at each layer
|
||||
- **Audit trails** - Complete request/response logging
|
||||
- **Custom analytics** - Correlate HTTP, LLM, and MCP metrics
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
This creates `build/multi-interface.so`
|
||||
|
||||
## Configuration
|
||||
|
||||
Add to your Bifrost config:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": [
|
||||
{
|
||||
"path": "/path/to/multi-interface.so",
|
||||
"name": "multi-interface",
|
||||
"display_name": "Full-Stack Observability",
|
||||
"enabled": true,
|
||||
"type": "auto",
|
||||
"config": {
|
||||
"enable_http_hooks": true,
|
||||
"enable_llm_hooks": true,
|
||||
"enable_mcp_hooks": true,
|
||||
"enable_observability": true,
|
||||
"enable_logging": true,
|
||||
"track_requests": true,
|
||||
"inject_uptime": true,
|
||||
"custom_header_prefix": "X-Multi-Plugin"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Note:**
|
||||
- `name` is the system identifier (from `GetName()`) and is **not editable**
|
||||
- `display_name` is shown in the UI and is **editable** by users
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `enable_http_hooks` | boolean | `true` | Enable HTTP transport layer hooks |
|
||||
| `enable_llm_hooks` | boolean | `true` | Enable LLM request/response hooks |
|
||||
| `enable_mcp_hooks` | boolean | `true` | Enable MCP request/response hooks |
|
||||
| `enable_observability` | boolean | `true` | Enable observability/trace injection |
|
||||
| `enable_logging` | boolean | `true` | Enable detailed logging |
|
||||
| `track_requests` | boolean | `true` | Track and count requests |
|
||||
| `inject_uptime` | boolean | `true` | Inject server uptime in LLM system messages |
|
||||
| `custom_header_prefix` | string | `"X-Multi-Plugin"` | Custom prefix for HTTP response headers |
|
||||
|
||||
### Example Configurations
|
||||
|
||||
**LLM-only mode:**
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"enable_http_hooks": false,
|
||||
"enable_llm_hooks": true,
|
||||
"enable_mcp_hooks": false,
|
||||
"enable_observability": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Observability-focused:**
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"enable_http_hooks": true,
|
||||
"enable_llm_hooks": true,
|
||||
"enable_mcp_hooks": true,
|
||||
"enable_observability": true,
|
||||
"enable_logging": false,
|
||||
"track_requests": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Minimal overhead:**
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"enable_logging": false,
|
||||
"track_requests": false,
|
||||
"inject_uptime": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Custom headers:**
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"custom_header_prefix": "X-Custom-Plugin"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Hook Execution Order
|
||||
|
||||
For a typical LLM request:
|
||||
|
||||
1. `HTTPTransportPreHook` (HTTP layer entry)
|
||||
2. `PreLLMHook` (Before LLM provider)
|
||||
3. *LLM Provider Call*
|
||||
4. `PostLLMHook` (After LLM provider)
|
||||
5. `HTTPTransportPostHook` (HTTP layer exit)
|
||||
6. `Inject` (Asynchronous trace delivery)
|
||||
|
||||
For an MCP request:
|
||||
|
||||
1. `HTTPTransportPreHook` (HTTP layer entry)
|
||||
2. `PreMCPHook` (Before MCP server)
|
||||
3. *MCP Server Call*
|
||||
4. `PostMCPHook` (After MCP server)
|
||||
5. `HTTPTransportPostHook` (HTTP layer exit)
|
||||
6. `Inject` (Asynchronous trace delivery)
|
||||
|
||||
## Notes
|
||||
|
||||
- This plugin tracks state across requests (request count, start time)
|
||||
- Context metadata flows from HTTP → LLM/MCP hooks
|
||||
- `Inject` is called asynchronously after response is sent
|
||||
- Perfect template for building comprehensive observability solutions
|
||||
32
examples/plugins/multi-interface/go.mod
Normal file
32
examples/plugins/multi-interface/go.mod
Normal file
@@ -0,0 +1,32 @@
|
||||
module github.com/maximhq/bifrost/examples/plugins/multi-interface
|
||||
|
||||
go 1.26.2
|
||||
|
||||
replace github.com/maximhq/bifrost/core => ../../../core
|
||||
|
||||
require github.com/maximhq/bifrost/core v0.0.0-00010101000000-000000000000
|
||||
|
||||
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
|
||||
)
|
||||
82
examples/plugins/multi-interface/go.sum
Normal file
82
examples/plugins/multi-interface/go.sum
Normal file
@@ -0,0 +1,82 @@
|
||||
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.11 h1:dMfPxS83BGwjeaK6BUdVHtoJu55dVRcqw0fB+qkqPYE=
|
||||
github.com/maximhq/bifrost/core v1.3.11/go.mod h1:abKQRnJQPZz8/UMxCcbuNHEyq19Db+IX4KlGJdlLY8E=
|
||||
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=
|
||||
314
examples/plugins/multi-interface/main.go
Normal file
314
examples/plugins/multi-interface/main.go
Normal file
@@ -0,0 +1,314 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/maximhq/bifrost/core/schemas"
|
||||
)
|
||||
|
||||
// Plugin configuration
|
||||
type PluginConfig struct {
|
||||
EnableHTTPHooks bool `json:"enable_http_hooks"` // Enable HTTP transport hooks
|
||||
EnableLLMHooks bool `json:"enable_llm_hooks"` // Enable LLM hooks
|
||||
EnableMCPHooks bool `json:"enable_mcp_hooks"` // Enable MCP hooks
|
||||
EnableObservability bool `json:"enable_observability"` // Enable observability/trace injection
|
||||
EnableLogging bool `json:"enable_logging"` // Enable detailed logging
|
||||
TrackRequests bool `json:"track_requests"` // Track request count
|
||||
InjectUptime bool `json:"inject_uptime"` // Inject server uptime in system messages
|
||||
CustomHeaderPrefix string `json:"custom_header_prefix"` // Custom prefix for plugin headers
|
||||
}
|
||||
|
||||
var (
|
||||
// Default configuration
|
||||
pluginConfig = &PluginConfig{
|
||||
EnableHTTPHooks: true,
|
||||
EnableLLMHooks: true,
|
||||
EnableMCPHooks: true,
|
||||
EnableObservability: true,
|
||||
EnableLogging: true,
|
||||
TrackRequests: true,
|
||||
InjectUptime: true,
|
||||
CustomHeaderPrefix: "X-Multi-Plugin",
|
||||
}
|
||||
|
||||
// Plugin state
|
||||
requestCount int64
|
||||
startTime time.Time
|
||||
)
|
||||
|
||||
// Init is called when the plugin is loaded (optional)
|
||||
func Init(config any) error {
|
||||
fmt.Println("[Multi-Interface Plugin] Init called")
|
||||
startTime = time.Now()
|
||||
|
||||
// Parse configuration
|
||||
if configMap, ok := config.(map[string]interface{}); ok {
|
||||
if enableHTTP, ok := configMap["enable_http_hooks"].(bool); ok {
|
||||
pluginConfig.EnableHTTPHooks = enableHTTP
|
||||
}
|
||||
if enableLLM, ok := configMap["enable_llm_hooks"].(bool); ok {
|
||||
pluginConfig.EnableLLMHooks = enableLLM
|
||||
}
|
||||
if enableMCP, ok := configMap["enable_mcp_hooks"].(bool); ok {
|
||||
pluginConfig.EnableMCPHooks = enableMCP
|
||||
}
|
||||
if enableObs, ok := configMap["enable_observability"].(bool); ok {
|
||||
pluginConfig.EnableObservability = enableObs
|
||||
}
|
||||
if enableLogging, ok := configMap["enable_logging"].(bool); ok {
|
||||
pluginConfig.EnableLogging = enableLogging
|
||||
}
|
||||
if trackReq, ok := configMap["track_requests"].(bool); ok {
|
||||
pluginConfig.TrackRequests = trackReq
|
||||
}
|
||||
if injectUptime, ok := configMap["inject_uptime"].(bool); ok {
|
||||
pluginConfig.InjectUptime = injectUptime
|
||||
}
|
||||
if headerPrefix, ok := configMap["custom_header_prefix"].(string); ok {
|
||||
pluginConfig.CustomHeaderPrefix = headerPrefix
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[Multi-Interface Plugin] Configuration loaded:\n")
|
||||
fmt.Printf(" HTTP Hooks: %v\n", pluginConfig.EnableHTTPHooks)
|
||||
fmt.Printf(" LLM Hooks: %v\n", pluginConfig.EnableLLMHooks)
|
||||
fmt.Printf(" MCP Hooks: %v\n", pluginConfig.EnableMCPHooks)
|
||||
fmt.Printf(" Observability: %v\n", pluginConfig.EnableObservability)
|
||||
fmt.Printf(" Request Tracking: %v\n", pluginConfig.TrackRequests)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetName returns the name of the plugin (required)
|
||||
// This is the system identifier - not editable by users
|
||||
// Users can set a custom display_name in the config for the UI
|
||||
func GetName() string {
|
||||
return "multi-interface"
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HTTPTransportPlugin Interface
|
||||
// ============================================================================
|
||||
|
||||
// HTTPTransportPreHook handles HTTP-layer request interception
|
||||
func HTTPTransportPreHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest) (*schemas.HTTPResponse, error) {
|
||||
if !pluginConfig.EnableHTTPHooks {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if pluginConfig.EnableLogging {
|
||||
fmt.Println("[Multi-Interface Plugin] HTTPTransportPreHook called")
|
||||
}
|
||||
|
||||
// Add request tracking (configurable)
|
||||
if pluginConfig.TrackRequests {
|
||||
requestCount++
|
||||
req.Headers[fmt.Sprintf("%s-Request-Number", pluginConfig.CustomHeaderPrefix)] = fmt.Sprintf("%d", requestCount)
|
||||
}
|
||||
|
||||
// Store HTTP metadata in context for later hooks
|
||||
ctx.SetValue(schemas.BifrostContextKey("multi-http-request-time"), time.Now())
|
||||
ctx.SetValue(schemas.BifrostContextKey("multi-http-path"), req.Path)
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// HTTPTransportPostHook handles HTTP-layer response interception
|
||||
func HTTPTransportPostHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
|
||||
if !pluginConfig.EnableHTTPHooks {
|
||||
return nil
|
||||
}
|
||||
|
||||
if pluginConfig.EnableLogging {
|
||||
fmt.Println("[Multi-Interface Plugin] HTTPTransportPostHook called")
|
||||
}
|
||||
|
||||
// Calculate HTTP duration
|
||||
if startTime, ok := ctx.Value(schemas.BifrostContextKey("multi-http-request-time")).(time.Time); ok {
|
||||
duration := time.Since(startTime)
|
||||
resp.Headers[fmt.Sprintf("%s-Duration-Ms", pluginConfig.CustomHeaderPrefix)] = fmt.Sprintf("%d", duration.Milliseconds())
|
||||
}
|
||||
|
||||
// Add plugin info header
|
||||
var interfaces []string
|
||||
if pluginConfig.EnableHTTPHooks {
|
||||
interfaces = append(interfaces, "http")
|
||||
}
|
||||
if pluginConfig.EnableLLMHooks {
|
||||
interfaces = append(interfaces, "llm")
|
||||
}
|
||||
if pluginConfig.EnableMCPHooks {
|
||||
interfaces = append(interfaces, "mcp")
|
||||
}
|
||||
if pluginConfig.EnableObservability {
|
||||
interfaces = append(interfaces, "observability")
|
||||
}
|
||||
resp.Headers[fmt.Sprintf("%s-Interfaces", pluginConfig.CustomHeaderPrefix)] = fmt.Sprintf("%v", interfaces)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// LLMPlugin Interface
|
||||
// ============================================================================
|
||||
|
||||
// PreLLMHook is called before the LLM provider is invoked
|
||||
func PreLLMHook(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.LLMPluginShortCircuit, error) {
|
||||
if !pluginConfig.EnableLLMHooks {
|
||||
return req, nil, nil
|
||||
}
|
||||
|
||||
if pluginConfig.EnableLogging {
|
||||
fmt.Println("[Multi-Interface Plugin] PreLLMHook called")
|
||||
httpPath := ctx.Value(schemas.BifrostContextKey("multi-http-path"))
|
||||
fmt.Printf("[Multi-Interface Plugin] Processing LLM request from path: %v\n", httpPath)
|
||||
}
|
||||
|
||||
// Store LLM metadata
|
||||
ctx.SetValue(schemas.BifrostContextKey("multi-llm-start-time"), time.Now())
|
||||
|
||||
// Example: Add system prompt with uptime (configurable)
|
||||
if pluginConfig.InjectUptime && req.ChatRequest != nil && req.ChatRequest.Input != nil {
|
||||
var content string
|
||||
if pluginConfig.TrackRequests {
|
||||
content = fmt.Sprintf("Processing request #%d. Server uptime: %v", requestCount, time.Since(startTime))
|
||||
} else {
|
||||
content = fmt.Sprintf("Server uptime: %v", time.Since(startTime))
|
||||
}
|
||||
systemMsg := schemas.ChatMessage{
|
||||
Role: "system",
|
||||
Content: &schemas.ChatMessageContent{ContentStr: &content},
|
||||
}
|
||||
req.ChatRequest.Input = append([]schemas.ChatMessage{systemMsg}, req.ChatRequest.Input...)
|
||||
}
|
||||
|
||||
return req, nil, nil
|
||||
}
|
||||
|
||||
// PostLLMHook is called after the LLM provider responds
|
||||
func PostLLMHook(ctx *schemas.BifrostContext, resp *schemas.BifrostResponse, bifrostErr *schemas.BifrostError) (*schemas.BifrostResponse, *schemas.BifrostError, error) {
|
||||
if !pluginConfig.EnableLLMHooks {
|
||||
return resp, bifrostErr, nil
|
||||
}
|
||||
|
||||
if pluginConfig.EnableLogging {
|
||||
fmt.Println("[Multi-Interface Plugin] PostLLMHook called")
|
||||
}
|
||||
|
||||
// Calculate LLM duration
|
||||
if startTime, ok := ctx.Value(schemas.BifrostContextKey("multi-llm-start-time")).(time.Time); ok {
|
||||
duration := time.Since(startTime)
|
||||
if pluginConfig.EnableLogging {
|
||||
fmt.Printf("[Multi-Interface Plugin] LLM call took: %v\n", duration)
|
||||
}
|
||||
|
||||
// Store for observability
|
||||
ctx.SetValue(schemas.BifrostContextKey("multi-llm-duration"), duration)
|
||||
}
|
||||
|
||||
return resp, bifrostErr, nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MCPPlugin Interface
|
||||
// ============================================================================
|
||||
|
||||
// PreMCPHook is called before MCP tool/resource calls are executed
|
||||
func PreMCPHook(ctx *schemas.BifrostContext, req *schemas.BifrostMCPRequest) (*schemas.BifrostMCPRequest, *schemas.MCPPluginShortCircuit, error) {
|
||||
if !pluginConfig.EnableMCPHooks {
|
||||
return req, nil, nil
|
||||
}
|
||||
|
||||
if pluginConfig.EnableLogging {
|
||||
fmt.Println("[Multi-Interface Plugin] PreMCPHook called")
|
||||
httpPath := ctx.Value(schemas.BifrostContextKey("multi-http-path"))
|
||||
fmt.Printf("[Multi-Interface Plugin] Processing MCP request from path: %v\n", httpPath)
|
||||
}
|
||||
|
||||
// Store MCP metadata
|
||||
ctx.SetValue(schemas.BifrostContextKey("multi-mcp-start-time"), time.Now())
|
||||
ctx.SetValue(schemas.BifrostContextKey("multi-mcp-type"), req.RequestType)
|
||||
|
||||
// Example: Log the MCP call
|
||||
if pluginConfig.EnableLogging && req.ChatAssistantMessageToolCall != nil && req.ChatAssistantMessageToolCall.Function.Name != nil {
|
||||
fmt.Printf("[Multi-Interface Plugin] MCP tool call: %s\n", *req.ChatAssistantMessageToolCall.Function.Name)
|
||||
}
|
||||
|
||||
return req, nil, nil
|
||||
}
|
||||
|
||||
// PostMCPHook is called after MCP tool/resource calls complete
|
||||
func PostMCPHook(ctx *schemas.BifrostContext, resp *schemas.BifrostMCPResponse, bifrostErr *schemas.BifrostError) (*schemas.BifrostMCPResponse, *schemas.BifrostError, error) {
|
||||
if !pluginConfig.EnableMCPHooks {
|
||||
return resp, bifrostErr, nil
|
||||
}
|
||||
|
||||
if pluginConfig.EnableLogging {
|
||||
fmt.Println("[Multi-Interface Plugin] PostMCPHook called")
|
||||
}
|
||||
|
||||
// Calculate MCP duration
|
||||
if startTime, ok := ctx.Value(schemas.BifrostContextKey("multi-mcp-start-time")).(time.Time); ok {
|
||||
duration := time.Since(startTime)
|
||||
if pluginConfig.EnableLogging {
|
||||
fmt.Printf("[Multi-Interface Plugin] MCP call took: %v\n", duration)
|
||||
}
|
||||
|
||||
// Store for observability
|
||||
ctx.SetValue(schemas.BifrostContextKey("multi-mcp-duration"), duration)
|
||||
}
|
||||
|
||||
return resp, bifrostErr, nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ObservabilityPlugin Interface
|
||||
// ============================================================================
|
||||
|
||||
// Inject receives completed traces for forwarding to observability backends
|
||||
func Inject(ctx context.Context, trace *schemas.Trace) error {
|
||||
if !pluginConfig.EnableObservability {
|
||||
return nil
|
||||
}
|
||||
|
||||
if pluginConfig.EnableLogging {
|
||||
fmt.Println("[Multi-Interface Plugin] Inject called - sending trace to observability backend")
|
||||
}
|
||||
|
||||
// Example: Format trace as JSON
|
||||
traceJSON, err := json.MarshalIndent(trace, "", " ")
|
||||
if err != nil {
|
||||
fmt.Printf("[Multi-Interface Plugin] Failed to marshal trace: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Example: Log the trace (in production, send to OTEL, Datadog, etc.)
|
||||
if pluginConfig.EnableLogging {
|
||||
fmt.Printf("[Multi-Interface Plugin] Trace data:\n%s\n", string(traceJSON))
|
||||
}
|
||||
|
||||
// In production, you would send this to your observability backend here
|
||||
// Example: sendToDatadog(traceJSON)
|
||||
// Example: sendToOTEL(trace)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Cleanup
|
||||
// ============================================================================
|
||||
|
||||
// Cleanup is called when the plugin is unloaded (required)
|
||||
func Cleanup() error {
|
||||
uptime := time.Since(startTime)
|
||||
if pluginConfig.TrackRequests {
|
||||
fmt.Printf("[Multi-Interface Plugin] Cleanup called - processed %d requests over %v\n",
|
||||
requestCount, uptime)
|
||||
} else {
|
||||
fmt.Printf("[Multi-Interface Plugin] Cleanup called - uptime: %v\n", uptime)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user