> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getbifrost.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Writing WASM Plugins

> Build cross-platform Bifrost plugins using WebAssembly with TypeScript, Go, or Rust

<Warning>
  **Beta Feature - Enterprise Only**

  WASM plugins are currently in beta and only available in Bifrost Enterprise builds. Contact your account team for access.
</Warning>

## Overview

WebAssembly (WASM) plugins offer a powerful alternative to native Go plugins, providing cross-platform compatibility and sandboxed execution. Unlike native `.so` plugins, WASM plugins:

* **Run anywhere** - Single `.wasm` binary works on any OS/architecture
* **No version matching** - No need to match Go versions or dependency versions
* **Sandboxed execution** - WASM provides memory-safe, isolated execution
* **Multi-language support** - Write plugins in TypeScript, Go, Rust, or any WASM-compatible language

## Plugin Interface

All WASM plugins must export these functions:

| Export                   | Signature                               | Description                                                                |
| ------------------------ | --------------------------------------- | -------------------------------------------------------------------------- |
| `malloc`                 | `(size: u32) -> u32`                    | Allocate memory for host to write data                                     |
| `free`                   | `(ptr: u32)` or `(ptr: u32, size: u32)` | Free allocated memory (Rust requires size for dealloc)                     |
| `get_name`               | `() -> u64`                             | Returns packed ptr+len of plugin name                                      |
| `init`                   | `(config_ptr, config_len: u32) -> i32`  | Initialize with config (0 = success)                                       |
| `http_pre_hook`          | `(input_ptr, input_len: u32) -> u64`    | HTTP transport pre-hook (request interception)                             |
| `http_post_hook`         | `(input_ptr, input_len: u32) -> u64`    | HTTP transport post-hook (non-streaming response interception)             |
| `http_stream_chunk_hook` | `(input_ptr, input_len: u32) -> u64`    | HTTP streaming chunk hook (per-chunk interception for streaming responses) |
| `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 Exchange

All complex data is exchanged as JSON strings. The host allocates memory using `malloc`, writes JSON data, and passes pointers to the plugin functions.

## Getting Started

Choose your preferred language:

<Tabs>
  <Tab title="TypeScript">
    ### Prerequisites

    Install Node.js (v18+) for AssemblyScript compilation:

    **macOS:**

    ```bash theme={null}
    brew install node
    ```

    **Linux (Ubuntu/Debian):**

    ```bash theme={null}
    curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
    sudo apt install -y nodejs
    ```

    ### Project Structure

    ```
    my-wasm-plugin/
    ├── assembly/
    │   ├── index.ts       # Plugin implementation
    │   ├── memory.ts      # Memory management utilities
    │   ├── types.ts       # Type definitions
    │   └── tsconfig.json  # AssemblyScript config
    ├── package.json
    └── Makefile
    ```

    ### Step 1: Initialize Project

    ```bash theme={null}
    mkdir my-wasm-plugin && cd my-wasm-plugin
    npm init -y
    npm install --save-dev assemblyscript json-as
    npx asinit .
    ```

    ### Step 2: Implement the Plugin

    Create `assembly/index.ts`:

    ```typescript theme={null}
    import { JSON } from 'json-as'

    // Memory management (simplified)
    let heap: ArrayBuffer = new ArrayBuffer(65536)
    let heapOffset: u32 = 0

    export function malloc(size: u32): u32 {
      const ptr = heapOffset
      heapOffset += size
      return ptr
    }

    export function free(ptr: u32): void {
      // Simple allocator - no-op for free
    }

    function readString(ptr: u32, len: u32): string {
      const bytes = new Uint8Array(len)
      for (let i: u32 = 0; i < len; i++) {
        bytes[i] = load<u8>(ptr + i)
      }
      return String.UTF8.decode(bytes.buffer)
    }

    function writeString(str: string): u64 {
      const encoded = String.UTF8.encode(str)
      const bytes = Uint8Array.wrap(encoded)
      const ptr = malloc(bytes.length)
      for (let i = 0; i < bytes.length; i++) {
        store<u8>(ptr + i, bytes[i])
      }
      // Pack pointer (upper 32 bits) and length (lower 32 bits)
      return (u64(ptr) << 32) | u64(bytes.length)
    }

    // Plugin configuration
    let pluginConfig: string = ''

    export function get_name(): u64 {
      return writeString('my-typescript-wasm-plugin')
    }

    export function init(configPtr: u32, configLen: u32): i32 {
      pluginConfig = readString(configPtr, configLen)
      return 0 // Success
    }

    export function http_pre_hook(inputPtr: u32, inputLen: u32): u64 {
      const input = readString(inputPtr, inputLen)
      
      // Parse and modify as needed
      // For pass-through, return the input with has_response: false
      const output = '{"context":{},"request":null,"response":null,"has_response":false,"error":""}'
      
      return writeString(output)
    }

    export function http_post_hook(inputPtr: u32, inputLen: u32): u64 {
      const input = readString(inputPtr, inputLen)
      
      // Parse input which includes both request and response
      // For pass-through, just return context and empty error
      const output = '{"context":{},"error":""}'
      
      return writeString(output)
    }

    // Input structure for http_stream_chunk_hook
    @json
    class StreamChunkInput {
      context: JSON.Obj = new JSON.Obj()
      request: JSON.Raw = new JSON.Raw('null')
      chunk: JSON.Raw = new JSON.Raw('null') // BifrostStreamChunk as JSON (see below)
    }

    // Output structure for http_stream_chunk_hook
    @json
    class StreamChunkOutput {
      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 = ''
    }

    // BifrostStreamChunk is one of: BifrostChatResponse, BifrostTextCompletionResponse,
    // BifrostResponsesStreamResponse, BifrostSpeechStreamResponse, BifrostTranscriptionStreamResponse,
    // BifrostImageGenerationStreamResponse, or BifrostError.
    // For chat completions, the chunk JSON looks like:
    // {
    //   "id": "chatcmpl-xxx",
    //   "object": "chat.completion.chunk",
    //   "created": 1234567890,
    //   "model": "gpt-4",
    //   "choices": [{"index": 0, "delta": {"content": "Hello"}, "finish_reason": null}],
    //   ...
    // }

    export function http_stream_chunk_hook(inputPtr: u32, inputLen: u32): u64 {
      const inputJson = readString(inputPtr, inputLen)
      const input = JSON.parse<StreamChunkInput>(inputJson)

      // For pass-through, return chunk unchanged with skip: false
      // To skip a chunk, set skip: true and chunk: null
      const output = new StreamChunkOutput()
      output.context = input.context
      output.chunk = input.chunk
      output.has_chunk = true
      output.skip = false
      output.error = ''

      return writeString(JSON.stringify(output))
    }

    export function pre_hook(inputPtr: u32, inputLen: u32): u64 {
      const input = readString(inputPtr, inputLen)
      
      // Parse and modify as needed
      // For pass-through, return with has_short_circuit: false
      const output = '{"context":{},"request":null,"short_circuit":null,"has_short_circuit":false,"error":""}'
      
      return writeString(output)
    }

    export function post_hook(inputPtr: u32, inputLen: u32): u64 {
      const input = readString(inputPtr, inputLen)
      
      // Parse and modify as needed
      // For pass-through, return with has_error matching input
      const output = '{"context":{},"response":null,"error":null,"has_error":false,"hook_error":""}'
      
      return writeString(output)
    }

    export function cleanup(): i32 {
      pluginConfig = ''
      return 0 // Success
    }
    ```

    ### Step 3: Build

    Add to `package.json`:

    ```json theme={null}
    {
      "scripts": {
        "build": "asc assembly/index.ts -o build/plugin.wasm --runtime stub --optimize"
      }
    }
    ```

    Build:

    ```bash theme={null}
    npm run build
    ```

    Output: `build/plugin.wasm`
  </Tab>

  <Tab title="Go (TinyGo)">
    ### Prerequisites

    Install TinyGo for WASM compilation:

    **macOS:**

    ```bash theme={null}
    brew install tinygo
    ```

    **Linux (Ubuntu/Debian):**

    ```bash theme={null}
    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
    ```

    ### Project Structure

    ```
    my-wasm-plugin/
    ├── main.go        # Plugin implementation
    ├── memory.go      # Memory management utilities
    ├── types.go       # Type definitions
    ├── go.mod
    └── Makefile
    ```

    ### Step 1: Initialize Project

    ```bash theme={null}
    mkdir my-wasm-plugin && cd my-wasm-plugin
    go mod init github.com/yourusername/my-wasm-plugin
    ```

    ### Step 2: Implement Memory Management

    Create `memory.go`:

    ```go theme={null}
    package main

    import "unsafe"

    var heap = make([]byte, 1024*1024) // 1MB heap
    var heapOffset uint32 = 0

    //export plugin_malloc
    func plugin_malloc(size uint32) uint32 {
    	ptr := heapOffset
    	heapOffset += size
    	return ptr
    }

    //export plugin_free
    func plugin_free(ptr uint32) {
    	// Simple allocator - no-op
    }

    func readInput(ptr, length uint32) []byte {
    	if length == 0 {
    		return nil
    	}
    	data := make([]byte, length)
    	for i := uint32(0); i < length; i++ {
    		data[i] = *(*byte)(unsafe.Pointer(uintptr(ptr + i)))
    	}
    	return data
    }

    func writeBytes(data []byte) uint64 {
    	ptr := plugin_malloc(uint32(len(data)))
    	for i, b := range data {
    		*(*byte)(unsafe.Pointer(uintptr(ptr + uint32(i)))) = b
    	}
    	// Pack pointer (upper 32 bits) and length (lower 32 bits)
    	return (uint64(ptr) << 32) | uint64(len(data))
    }
    ```

    ### Step 3: Implement the Plugin

    Create `main.go`:

    ```go theme={null}
    package main

    import (
    	"encoding/json"
    )

    //export get_name
    func get_name() uint64 {
    	return writeBytes([]byte("my-go-wasm-plugin"))
    }

    //export init
    func init_plugin(configPtr, configLen uint32) int32 {
    	if configLen > 0 {
    		configData := readInput(configPtr, configLen)
    		// Parse and store config as needed
    		_ = configData
    	}
    	return 0 // Success
    }

    // HTTPPreHookInput represents the input to http_pre_hook
    type HTTPPreHookInput struct {
    	Context map[string]interface{} `json:"context"`
    	Request json.RawMessage        `json:"request"`
    }

    // HTTPPreHookOutput represents the output from http_pre_hook
    type HTTPPreHookOutput struct {
    	Context     map[string]interface{} `json:"context"`
    	Request     json.RawMessage        `json:"request,omitempty"`
    	Response    json.RawMessage        `json:"response,omitempty"`
    	HasResponse bool                   `json:"has_response"`
    	Error       string                 `json:"error"`
    }

    //export http_pre_hook
    func http_pre_hook(inputPtr, inputLen uint32) uint64 {
    	inputData := readInput(inputPtr, inputLen)
    	
    	var input HTTPPreHookInput
    	if err := json.Unmarshal(inputData, &input); err != nil {
    		output := HTTPPreHookOutput{Error: err.Error()}
    		data, _ := json.Marshal(output)
    		return writeBytes(data)
    	}
    	
    	// Add custom context value
    	input.Context["from-http-pre"] = "wasm-plugin"
    	
    	// Pass through
    	output := HTTPPreHookOutput{
    		Context:     input.Context,
    		Request:     input.Request,
    		HasResponse: false,
    	}
    	
    	data, _ := json.Marshal(output)
    	return writeBytes(data)
    }

    // HTTPPostHookInput represents the input to http_post_hook
    type HTTPPostHookInput struct {
    	Context  map[string]interface{} `json:"context"`
    	Request  json.RawMessage        `json:"request"`
    	Response json.RawMessage        `json:"response"`
    }

    // HTTPPostHookOutput represents the output from http_post_hook
    type HTTPPostHookOutput struct {
    	Context map[string]interface{} `json:"context"`
    	Error   string                 `json:"error"`
    }

    //export http_post_hook
    func http_post_hook(inputPtr, inputLen uint32) uint64 {
    	inputData := readInput(inputPtr, inputLen)
    	
    	var input HTTPPostHookInput
    	if err := json.Unmarshal(inputData, &input); err != nil {
    		output := HTTPPostHookOutput{Error: err.Error()}
    		data, _ := json.Marshal(output)
    		return writeBytes(data)
    	}
    	
    	// Add custom context value
    	input.Context["from-http-post"] = "wasm-plugin"
    	
    	// Pass through
    	output := HTTPPostHookOutput{
    		Context: input.Context,
    	}
    	
    	data, _ := json.Marshal(output)
    	return writeBytes(data)
    }

    // HTTPStreamChunkHookInput represents the input to http_stream_chunk_hook
    type HTTPStreamChunkHookInput struct {
    	Context map[string]interface{} `json:"context"`
    	Request json.RawMessage        `json:"request"`
    	Chunk   json.RawMessage        `json:"chunk"` // BifrostStreamChunk JSON
    }

    // HTTPStreamChunkHookOutput represents the output from http_stream_chunk_hook
    type HTTPStreamChunkHookOutput struct {
    	Context  map[string]interface{} `json:"context"`
    	Chunk    json.RawMessage        `json:"chunk,omitempty"` // BifrostStreamChunk JSON, nil to skip
    	HasChunk bool                   `json:"has_chunk"`
    	Skip     bool                   `json:"skip"`
    	Error    string                 `json:"error"`
    }

    //export http_stream_chunk_hook
    func http_stream_chunk_hook(inputPtr, inputLen uint32) uint64 {
    	inputData := readInput(inputPtr, inputLen)

    	var input HTTPStreamChunkHookInput
    	if err := json.Unmarshal(inputData, &input); err != nil {
    		output := HTTPStreamChunkHookOutput{Error: err.Error()}
    		data, _ := json.Marshal(output)
    		return writeBytes(data)
    	}

    	// Pass through chunk unchanged
    	output := HTTPStreamChunkHookOutput{
    		Context:  input.Context,
    		Chunk:    input.Chunk,
    		HasChunk: true,
    		Skip:     false,
    	}

    	data, _ := json.Marshal(output)
    	return writeBytes(data)
    }

    // PreHookInput represents the input to pre_hook
    type PreHookInput struct {
    	Context map[string]interface{} `json:"context"`
    	Request json.RawMessage        `json:"request"`
    }

    // PreHookOutput represents the output from pre_hook
    type PreHookOutput struct {
    	Context         map[string]interface{} `json:"context"`
    	Request         json.RawMessage        `json:"request,omitempty"`
    	ShortCircuit    json.RawMessage        `json:"short_circuit,omitempty"`
    	HasShortCircuit bool                   `json:"has_short_circuit"`
    	Error           string                 `json:"error"`
    }

    //export pre_hook
    func pre_hook(inputPtr, inputLen uint32) uint64 {
    	inputData := readInput(inputPtr, inputLen)
    	
    	var input PreHookInput
    	if err := json.Unmarshal(inputData, &input); err != nil {
    		output := PreHookOutput{Error: err.Error()}
    		data, _ := json.Marshal(output)
    		return writeBytes(data)
    	}
    	
    	// Add custom context value
    	input.Context["from-pre-hook"] = "wasm-plugin"
    	
    	// Pass through
    	output := PreHookOutput{
    		Context:         input.Context,
    		Request:         input.Request,
    		HasShortCircuit: false,
    	}
    	
    	data, _ := json.Marshal(output)
    	return writeBytes(data)
    }

    // PostHookInput represents the input to post_hook
    type PostHookInput struct {
    	Context  map[string]interface{} `json:"context"`
    	Response json.RawMessage        `json:"response"`
    	Error    json.RawMessage        `json:"error"`
    	HasError bool                   `json:"has_error"`
    }

    // PostHookOutput represents the output from post_hook
    type PostHookOutput struct {
    	Context   map[string]interface{} `json:"context"`
    	Response  json.RawMessage        `json:"response,omitempty"`
    	Error     json.RawMessage        `json:"error,omitempty"`
    	HasError  bool                   `json:"has_error"`
    	HookError string                 `json:"hook_error"`
    }

    //export post_hook
    func post_hook(inputPtr, inputLen uint32) uint64 {
    	inputData := readInput(inputPtr, inputLen)
    	
    	var input PostHookInput
    	if err := json.Unmarshal(inputData, &input); err != nil {
    		output := PostHookOutput{HookError: err.Error()}
    		data, _ := json.Marshal(output)
    		return writeBytes(data)
    	}
    	
    	// Add custom context value
    	input.Context["from-post-hook"] = "wasm-plugin"
    	
    	// Pass through
    	output := PostHookOutput{
    		Context:  input.Context,
    		Response: input.Response,
    		Error:    input.Error,
    		HasError: input.HasError,
    	}
    	
    	data, _ := json.Marshal(output)
    	return writeBytes(data)
    }

    //export cleanup
    func cleanup() int32 {
    	return 0 // Success
    }

    func main() {}
    ```

    ### Step 4: Build

    ```bash theme={null}
    tinygo build -o build/plugin.wasm -target=wasi -scheduler=none .
    ```

    Or create a `Makefile`:

    ```makefile theme={null}
    build:
    	@mkdir -p build
    	GOWORK=off tinygo build -o build/plugin.wasm -target=wasi -scheduler=none .

    clean:
    	@rm -rf build
    ```

    Output: `build/plugin.wasm`
  </Tab>

  <Tab title="Rust">
    ### Prerequisites

    Install Rust and add the WASM target:

    ```bash theme={null}
    # 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 - Install `wasm-opt` for smaller binaries:

    ```bash theme={null}
    # macOS
    brew install binaryen

    # Linux
    apt install binaryen
    ```

    ### Project Structure

    ```
    my-wasm-plugin/
    ├── src/
    │   ├── lib.rs       # Plugin implementation
    │   ├── memory.rs    # Memory management
    │   └── types.rs     # Type definitions
    ├── Cargo.toml
    └── Makefile
    ```

    ### Step 1: Initialize Project

    ```bash theme={null}
    cargo new --lib my-wasm-plugin
    cd my-wasm-plugin
    ```

    Update `Cargo.toml`:

    ```toml theme={null}
    [package]
    name = "my-wasm-plugin"
    version = "0.1.0"
    edition = "2021"

    [lib]
    crate-type = ["cdylib"]

    [dependencies]
    serde = { version = "1.0", features = ["derive"] }
    serde_json = "1.0"

    [profile.release]
    opt-level = "s"
    lto = true
    ```

    ### Step 2: Implement Memory Management

    Create `src/memory.rs`:

    ```rust theme={null}
    use std::alloc::{alloc, dealloc, Layout};

    #[no_mangle]
    pub extern "C" fn malloc(size: u32) -> u32 {
        let layout = Layout::from_size_align(size as usize, 1).unwrap();
        unsafe { alloc(layout) as u32 }
    }

    #[no_mangle]
    pub extern "C" fn free(ptr: u32, size: u32) {
        let layout = Layout::from_size_align(size as usize, 1).unwrap();
        unsafe { dealloc(ptr as *mut u8, layout) }
    }

    pub fn read_string(ptr: u32, len: u32) -> String {
        let slice = unsafe {
            std::slice::from_raw_parts(ptr as *const u8, len as usize)
        };
        String::from_utf8_lossy(slice).to_string()
    }

    pub fn write_string(s: &str) -> u64 {
        let bytes = s.as_bytes();
        let ptr = malloc(bytes.len() as u32);
        unsafe {
            std::ptr::copy_nonoverlapping(
                bytes.as_ptr(),
                ptr as *mut u8,
                bytes.len()
            );
        }
        // Pack pointer (upper 32 bits) and length (lower 32 bits)
        ((ptr as u64) << 32) | (bytes.len() as u64)
    }
    ```

    ### Step 3: Implement the Plugin

    Create `src/lib.rs`:

    ```rust theme={null}
    mod memory;

    use memory::{read_string, write_string};
    use serde::{Deserialize, Serialize};
    use std::collections::HashMap;

    // Plugin configuration storage
    static mut CONFIG: Option<String> = None;

    #[no_mangle]
    pub extern "C" fn get_name() -> u64 {
        write_string("my-rust-wasm-plugin")
    }

    #[no_mangle]
    pub extern "C" fn init(config_ptr: u32, config_len: u32) -> i32 {
        let config = read_string(config_ptr, config_len);
        unsafe { CONFIG = Some(config); }
        0 // Success
    }

    #[derive(Deserialize)]
    struct HTTPPreHookInput {
        context: HashMap<String, serde_json::Value>,
        request: serde_json::Value,
    }

    #[derive(Serialize, Default)]
    struct HTTPPreHookOutput {
        context: HashMap<String, serde_json::Value>,
        #[serde(skip_serializing_if = "Option::is_none")]
        request: Option<serde_json::Value>,
        #[serde(skip_serializing_if = "Option::is_none")]
        response: Option<serde_json::Value>,
        has_response: bool,
        error: String,
    }

    #[no_mangle]
    pub extern "C" fn http_pre_hook(input_ptr: u32, input_len: u32) -> u64 {
        let input_str = read_string(input_ptr, input_len);
        
        let input: HTTPPreHookInput = match serde_json::from_str(&input_str) {
            Ok(i) => i,
            Err(e) => {
                let output = HTTPPreHookOutput {
                    error: format!("Parse error: {}", e),
                    ..Default::default()
                };
                return write_string(&serde_json::to_string(&output).unwrap());
            }
        };
        
        let mut context = input.context;
        context.insert("from-http-pre".to_string(), serde_json::json!("rust-wasm"));
        
        let output = HTTPPreHookOutput {
            context,
            request: Some(input.request),
            has_response: false,
            ..Default::default()
        };
        
        write_string(&serde_json::to_string(&output).unwrap())
    }

    #[derive(Deserialize)]
    struct HTTPPostHookInput {
        context: HashMap<String, serde_json::Value>,
        request: serde_json::Value,
        response: serde_json::Value,
    }

    #[derive(Serialize, Default)]
    struct HTTPPostHookOutput {
        context: HashMap<String, serde_json::Value>,
        error: String,
    }

    #[no_mangle]
    pub extern "C" fn http_post_hook(input_ptr: u32, input_len: u32) -> u64 {
        let input_str = read_string(input_ptr, input_len);
        
        let input: HTTPPostHookInput = match serde_json::from_str(&input_str) {
            Ok(i) => i,
            Err(e) => {
                let output = HTTPPostHookOutput {
                    error: format!("Parse error: {}", e),
                    ..Default::default()
                };
                return write_string(&serde_json::to_string(&output).unwrap());
            }
        };
        
        let mut context = input.context;
        context.insert("from-http-post".to_string(), serde_json::json!("rust-wasm"));
        
        let output = HTTPPostHookOutput {
            context,
            error: String::new(),
        };
        
        write_string(&serde_json::to_string(&output).unwrap())
    }

    #[derive(Deserialize)]
    struct HTTPStreamChunkHookInput {
        context: HashMap<String, serde_json::Value>,
        request: serde_json::Value,
        chunk: String, // base64-encoded chunk
    }

    #[derive(Serialize, Default)]
    struct HTTPStreamChunkHookOutput {
        context: HashMap<String, serde_json::Value>,
        #[serde(skip_serializing_if = "Option::is_none")]
        chunk: Option<String>, // base64-encoded chunk, None to skip
        has_chunk: bool,
        skip: bool,
        error: String,
    }

    #[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);
        
        let input: HTTPStreamChunkHookInput = match serde_json::from_str(&input_str) {
            Ok(i) => i,
            Err(e) => {
                let output = HTTPStreamChunkHookOutput {
                    error: format!("Parse error: {}", e),
                    ..Default::default()
                };
                return write_string(&serde_json::to_string(&output).unwrap());
            }
        };
        
        // Pass through chunk unchanged
        let output = HTTPStreamChunkHookOutput {
            context: input.context,
            chunk: Some(input.chunk),
            has_chunk: true,
            skip: false,
            error: String::new(),
        };
        
        write_string(&serde_json::to_string(&output).unwrap())
    }

    #[derive(Deserialize)]
    struct PreHookInput {
        context: HashMap<String, serde_json::Value>,
        request: serde_json::Value,
    }

    #[derive(Serialize, Default)]
    struct PreHookOutput {
        context: HashMap<String, serde_json::Value>,
        #[serde(skip_serializing_if = "Option::is_none")]
        request: Option<serde_json::Value>,
        #[serde(skip_serializing_if = "Option::is_none")]
        short_circuit: Option<serde_json::Value>,
        has_short_circuit: bool,
        error: String,
    }

    #[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 = match serde_json::from_str(&input_str) {
            Ok(i) => i,
            Err(e) => {
                let output = PreHookOutput {
                    error: format!("Parse error: {}", e),
                    ..Default::default()
                };
                return write_string(&serde_json::to_string(&output).unwrap());
            }
        };
        
        let mut context = input.context;
        context.insert("from-pre-hook".to_string(), serde_json::json!("rust-wasm"));
        
        let output = PreHookOutput {
            context,
            request: Some(input.request),
            has_short_circuit: false,
            ..Default::default()
        };
        
        write_string(&serde_json::to_string(&output).unwrap())
    }

    #[derive(Deserialize)]
    struct PostHookInput {
        context: HashMap<String, serde_json::Value>,
        response: serde_json::Value,
        error: serde_json::Value,
        has_error: bool,
    }

    #[derive(Serialize, Default)]
    struct PostHookOutput {
        context: HashMap<String, serde_json::Value>,
        #[serde(skip_serializing_if = "Option::is_none")]
        response: Option<serde_json::Value>,
        #[serde(skip_serializing_if = "Option::is_none")]
        error: Option<serde_json::Value>,
        has_error: bool,
        hook_error: String,
    }

    #[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 = match serde_json::from_str(&input_str) {
            Ok(i) => i,
            Err(e) => {
                let output = PostHookOutput {
                    hook_error: format!("Parse error: {}", e),
                    ..Default::default()
                };
                return write_string(&serde_json::to_string(&output).unwrap());
            }
        };
        
        let mut context = input.context;
        context.insert("from-post-hook".to_string(), serde_json::json!("rust-wasm"));
        
        let output = PostHookOutput {
            context,
            response: Some(input.response),
            error: Some(input.error),
            has_error: input.has_error,
            hook_error: String::new(),
        };
        
        write_string(&serde_json::to_string(&output).unwrap())
    }

    #[no_mangle]
    pub extern "C" fn cleanup() -> i32 {
        unsafe { CONFIG = None; }
        0 // Success
    }
    ```

    ### Step 4: Build

    ```bash theme={null}
    cargo build --release --target wasm32-unknown-unknown
    cp target/wasm32-unknown-unknown/release/my_wasm_plugin.wasm build/plugin.wasm
    ```

    Optional - Optimize with wasm-opt:

    ```bash theme={null}
    wasm-opt -Os -o build/plugin.wasm build/plugin.wasm
    ```

    Output: `build/plugin.wasm`
  </Tab>
</Tabs>

## Hook Input/Output Structures

### http\_pre\_hook

<Note>
  **Header and Query Parameter Handling**: Headers and query parameters in `request.headers` and `request.query` preserve the original casing sent by the client. When looking up headers/query params, you should perform case-insensitive comparisons in your WASM plugin code to handle various casing (e.g., `Content-Type`, `content-type`, `CONTENT-TYPE`).

  For Go native plugins, use the built-in `CaseInsensitiveHeaderLookup()` and `CaseInsensitiveQueryLookup()` helper methods.
</Note>

**Input:**

```json theme={null}
{
  "context": {
    "request_id": "abc-123"
  },
  "request": {
    "method": "POST",
    "path": "/v1/chat/completions",
    "headers": { "content-type": "application/json" },
    "query": {},
    "body": "<base64-encoded>"
  }
}
```

**Output:**

```json theme={null}
{
  "context": { "request_id": "abc-123", "custom_key": "value" },
  "request": { ... },
  "response": null,
  "has_response": false,
  "error": ""
}
```

To short-circuit with a response:

```json theme={null}
{
  "context": { ... },
  "request": null,
  "response": {
    "status_code": 200,
    "headers": { "Content-Type": "application/json" },
    "body": "<base64-encoded>"
  },
  "has_response": true,
  "error": ""
}
```

### http\_post\_hook

Called after the response is received from the LLM provider. Receives both the original request and the response.

**Input:**

```json theme={null}
{
  "context": {
    "request_id": "abc-123",
    "custom_key": "value"
  },
  "request": {
    "method": "POST",
    "path": "/v1/chat/completions",
    "headers": { "content-type": "application/json" },
    "query": {},
    "body": "<base64-encoded>"
  },
  "response": {
    "status_code": 200,
    "headers": { "content-type": "application/json" },
    "body": "<base64-encoded>"
  }
}
```

**Output:**

```json theme={null}
{
  "context": { "request_id": "abc-123", "custom_key": "value", "post_processed": true },
  "error": ""
}
```

<Note>
  The `http_post_hook` is called in **reverse order** of `http_pre_hook`. Context values set in `http_pre_hook` are available in `http_post_hook`.
</Note>

<Warning>
  `http_post_hook` is **NOT called** for streaming responses. Use `http_stream_chunk_hook` instead.
</Warning>

### http\_stream\_chunk\_hook

Called for each chunk during streaming responses, BEFORE the chunk is written to the client. This hook allows plugins to modify or filter streaming chunks in real-time.

**Input:**

```json theme={null}
{
  "context": {
    "request_id": "abc-123",
    "custom_key": "value"
  },
  "request": {
    "method": "POST",
    "path": "/v1/chat/completions",
    "headers": { "content-type": "application/json" },
    "query": {},
    "body": "<base64-encoded>"
  },
  "chunk": {
    "id": "chatcmpl-xxx",
    "object": "chat.completion.chunk",
    "created": 1234567890,
    "model": "gpt-4",
    "choices": [{"index": 0, "delta": {"content": "Hello"}, "finish_reason": null}]
  }
}
```

The `chunk` field contains a `BifrostStreamChunk` struct serialized as JSON. It will contain the data from whichever response type is active:

* Chat completion streaming: `{"id":"...","object":"chat.completion.chunk","choices":[...],"model":"..."}`
* Text completion streaming: `{"id":"...","choices":[...]}`
* Responses API streaming: `{"type":"...","item":...}`
* Speech/Transcription/Image streaming: respective response fields
* Error: `{"error":{"type":"...","message":"..."}}`

It does NOT include SSE framing (no `data: ` prefix or `\n\n` suffix).

<Note>
  **Go Native vs WASM Plugins**: In Go native plugins (`.so`), you work directly with `*schemas.BifrostStreamChunk` typed structs. In WASM plugins, this struct is serialized to JSON for crossing the WASM boundary. The underlying data structure is the same.
</Note>

**Output (pass through unchanged):**

```json theme={null}
{
  "context": { "request_id": "abc-123", "custom_key": "value" },
  "chunk": {
    "id": "chatcmpl-xxx",
    "object": "chat.completion.chunk",
    "created": 1234567890,
    "model": "gpt-4",
    "choices": [{"index": 0, "delta": {"content": "Hello"}, "finish_reason": null}]
  },
  "has_chunk": true,
  "skip": false,
  "error": ""
}
```

**Output (skip/filter chunk):**

```json theme={null}
{
  "context": { "request_id": "abc-123" },
  "chunk": null,
  "has_chunk": false,
  "skip": true,
  "error": ""
}
```

**Output (modify chunk):**

```json theme={null}
{
  "context": { "request_id": "abc-123" },
  "chunk": {
    "id": "chatcmpl-xxx",
    "object": "chat.completion.chunk",
    "created": 1234567890,
    "model": "gpt-4",
    "choices": [{"index": 0, "delta": {"content": "Modified!"}, "finish_reason": null}]
  },
  "has_chunk": true,
  "skip": false,
  "error": ""
}
```

<Note>
  The `http_stream_chunk_hook` is called in **reverse order** of `http_pre_hook`, same as other post-hooks.
</Note>

### pre\_hook

**Input:**

```json theme={null}
{
  "context": { "request_id": "abc-123" },
  "request": {
    "provider": "openai",
    "model": "gpt-4",
    "input": [{ "role": "user", "content": "Hello" }],
    "params": { "temperature": 0.7 }
  }
}
```

**Output:**

```json theme={null}
{
  "context": { "request_id": "abc-123", "plugin_processed": true },
  "request": { ... },
  "short_circuit": null,
  "has_short_circuit": false,
  "error": ""
}
```

To short-circuit with a response:

```json theme={null}
{
  "context": { ... },
  "request": null,
  "short_circuit": {
    "response": {
      "chat_response": {
        "id": "mock-123",
        "model": "gpt-4",
        "choices": [{ "index": 0, "message": { "role": "assistant", "content": "Mock response" } }]
      }
    }
  },
  "has_short_circuit": true,
  "error": ""
}
```

### post\_hook

**Input:**

```json theme={null}
{
  "context": { "request_id": "abc-123" },
  "response": {
    "chat_response": {
      "id": "chatcmpl-123",
      "model": "gpt-4",
      "choices": [{ "index": 0, "message": { "role": "assistant", "content": "Hello!" } }],
      "usage": { "prompt_tokens": 5, "completion_tokens": 10, "total_tokens": 15 }
    }
  },
  "error": {},
  "has_error": false
}
```

**Output:**

```json theme={null}
{
  "context": { "request_id": "abc-123", "post_processed": true },
  "response": { ... },
  "error": {},
  "has_error": false,
  "hook_error": ""
}
```

## Configuration

Configure your WASM plugin in Bifrost's `config.json`:

```json theme={null}
{
  "plugins": [
    {
      "path": "/path/to/plugin.wasm",
      "name": "my-wasm-plugin",
      "enabled": true,
      "config": {
        "custom_option": "value"
      }
    }
  ]
}
```

You can also load plugins from URLs:

```json theme={null}
{
  "plugins": [
    {
      "path": "https://example.com/plugins/my-plugin.wasm",
      "name": "my-wasm-plugin",
      "enabled": true
    }
  ]
}
```

## Limitations vs Native Plugins

WASM plugins have some trade-offs compared to native Go plugins:

| Aspect               | Native (.so)                    | WASM                        |
| -------------------- | ------------------------------- | --------------------------- |
| **Performance**      | Fastest (in-process)            | JSON serialization overhead |
| **Cross-platform**   | Build per platform              | Single binary everywhere    |
| **Version matching** | Exact Go/package match required | No version requirements     |
| **Memory**           | Shared process memory           | Linear memory (limited)     |
| **Languages**        | Go only                         | TypeScript, Go, Rust, etc.  |
| **Debugging**        | Full Go tooling                 | Limited debugging support   |
| **Security**         | Full process access             | Sandboxed execution         |

## Source Code Reference

Complete hello-world examples are available in the Bifrost repository:

* **TypeScript**: [examples/plugins/hello-world-wasm-typescript](https://github.com/maximhq/bifrost/tree/main/examples/plugins/hello-world-wasm-typescript)
* **Go (TinyGo)**: [examples/plugins/hello-world-wasm-go](https://github.com/maximhq/bifrost/tree/main/examples/plugins/hello-world-wasm-go)
* **Rust**: [examples/plugins/hello-world-wasm-rust](https://github.com/maximhq/bifrost/tree/main/examples/plugins/hello-world-wasm-rust)

## Troubleshooting

### Module fails to load

**Error**: `failed to instantiate WASM module`

**Solution**: Ensure all required exports are present. Use a WASM inspection tool:

```bash theme={null}
# List exports
wasm-objdump -x plugin.wasm | grep -A 20 "Export"
```

### Memory allocation errors

**Error**: `out of memory` or `invalid memory access`

**Solution**:

* Increase heap size in your allocator
* Ensure you're freeing memory after use
* Check for memory leaks in long-running plugins

### JSON parsing errors

**Error**: `failed to parse input JSON`

**Solution**:

* Validate your JSON structures match expected schemas
* Handle optional/nullable fields properly
* Add error logging to identify malformed data

### Build errors (TinyGo)

**Error**: `package not supported by TinyGo`

**Solution**: TinyGo doesn't support all Go standard library packages. Avoid:

* `reflect` (limited support)
* `net/http` (use raw JSON instead)
* Complex generics

### Build errors (Rust)

**Error**: `cannot find -lc`

**Solution**: For `wasm32-unknown-unknown` target, don't link to libc. Ensure your `Cargo.toml` doesn't require native dependencies.

## Need Help?

* **Discord Community**: [Join our Discord](https://discord.gg/exN5KAydbU)
* **GitHub Issues**: [Report bugs or request features](https://github.com/maximhq/bifrost/issues)
* **Documentation**: [Browse all docs](/)
