---
title: "Writing WASM Plugins"
description: "Build cross-platform Bifrost plugins using WebAssembly with TypeScript, Go, or Rust"
icon: "puzzle-piece"
---

<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
brew install node
```

**Linux (Ubuntu/Debian):**
```bash
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
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
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
{
  "scripts": {
    "build": "asc assembly/index.ts -o build/plugin.wasm --runtime stub --optimize"
  }
}
```

Build:

```bash
npm run build
```

Output: `build/plugin.wasm`

  </Tab>
  <Tab title="Go (TinyGo)">

### Prerequisites

Install TinyGo for WASM compilation:

**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
```

### 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
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
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
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
tinygo build -o build/plugin.wasm -target=wasi -scheduler=none .
```

Or create a `Makefile`:

```makefile
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
# 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
# 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
cargo new --lib my-wasm-plugin
cd my-wasm-plugin
```

Update `Cargo.toml`:

```toml
[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
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
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
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
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
{
  "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": null,
  "has_response": false,
  "error": ""
}
```

To short-circuit with a response:
```json
{
  "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
{
  "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
{
  "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
{
  "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
{
  "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
{
  "context": { "request_id": "abc-123" },
  "chunk": null,
  "has_chunk": false,
  "skip": true,
  "error": ""
}
```

**Output (modify chunk):**
```json
{
  "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
{
  "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": null,
  "has_short_circuit": false,
  "error": ""
}
```

To short-circuit with a response:
```json
{
  "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
{
  "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
{
  "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
{
  "plugins": [
    {
      "path": "/path/to/plugin.wasm",
      "name": "my-wasm-plugin",
      "enabled": true,
      "config": {
        "custom_option": "value"
      }
    }
  ]
}
```

You can also load plugins from URLs:

```json
{
  "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
# 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](/)
