first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

View 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

View 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

View 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
)

View 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=

View 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() {}

View 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)
}

View 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"`
}

View 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"

View 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"

View 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

View 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

View 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
}

View 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) }
}

View 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, "");
}
}

View 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

View 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

View 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
}

View File

@@ -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
}

View File

@@ -0,0 +1,4 @@
{
"extends": "assemblyscript/std/assembly.json",
"include": ["./**/*.ts"]
}

View 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 = ''
}

View 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"
}
}
}

View 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
View File

@@ -0,0 +1,15 @@
# Build artifacts
build/
*.so
*.dll
*.dylib
# Go build cache
*.exe
*.exe~
*.test
*.out
# Dependency directories
vendor/

View 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

View 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
)

View 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=

View 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
}

View 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"

View 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

View 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
)

View 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=

View 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
}

View 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"

View 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
}
}
```

View 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
)

View 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=

View 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
}

View 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"

View 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": []
}
}
```

View 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
)

View 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=

View 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
}

View 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"

View 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

View 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
)

View 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=

View 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
}