Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Build and Release

on:
push:
branches: [main]
tags:
- 'v*'
pull_request:
branches: [main]

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.26.1'

- name: Download dependencies
run: go mod download

- name: Run tests
run: go test -v ./...

- name: Build plugin (Linux AMD64)
run: |
mkdir -p build
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \
go build -buildmode=plugin -ldflags='-w -s' -trimpath \
-o build/encoding-format.so main.go

- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: encoding-format-linux-amd64
path: build/encoding-format.so

release:
needs: build
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
steps:
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: encoding-format-linux-amd64

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: encoding-format.so
generate_release_notes: true
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ COLOR_ERROR = \033[31m
COLOR_BOLD = \033[1m

# Plugin name
PLUGIN_NAME = hello-world
PLUGIN_NAME = encoding-format
OUTPUT_DIR = build

# Platform detection
Expand Down
87 changes: 86 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,87 @@
# bifrost-encoding-format-plugin
Plugin for bifrost to inject missing encoding_format parameters into embedding requests

A [Bifrost](https://github.com/maximhq/bifrost) plugin that injects missing `encoding_format` parameters into embedding API requests.

## Problem

Some downstream LLM providers (e.g. older versions of LiteLLM) require the `encoding_format` parameter in embedding requests, but many upstream platforms like Open WebUI don't include it. This mismatch results in errors. This plugin bridges that gap by automatically adding the parameter when it's missing.

## How It Works

The plugin intercepts HTTP requests at the transport level before they reach the downstream provider. For any request targeting an embedding endpoint (path containing `embedding`):

1. If `encoding_format` is **missing** from the request body → injects the configured value
2. If `encoding_format` is **null** → replaces it with the configured value
3. If `encoding_format` is **already set** to a non-null value → leaves the request unchanged
4. Non-embedding requests are passed through without modification

## Configuration

Add the plugin to your Bifrost configuration:

```yaml
plugins:
- name: encoding-format
enabled: true
path: /path/to/encoding-format.so
config:
encoding_format: "float" # or "base64"
```

### Supported Values

| Value | Description |
|----------|------------------------------------|
| `float` | Return embeddings as float arrays (default) |
| `base64` | Return embeddings as base64-encoded strings |

If no configuration is provided, the plugin defaults to `float`.

## Building

### Prerequisites

- Go 1.26.1+
- CGO enabled (required for Go plugins)

### Build locally

```bash
make build
```

### Build for Linux AMD64 (cross-compilation via Docker)

```bash
make build GOOS=linux GOARCH=amd64
```

### Install to Bifrost plugins directory

```bash
make install
```

## GitHub Actions

The repository includes a GitHub Actions workflow that:

- **On push/PR to main**: Builds and tests the plugin on Linux AMD64
- **On tag push (`v*`)**: Builds the plugin and publishes it as a GitHub Release asset

## Development

```bash
# Download dependencies
make deps

# Build for development (no optimizations)
make dev

# Run tests
make test
```

## License

[MIT](LICENSE)
10 changes: 6 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
module github.com/maximhq/bifrost/examples/plugins/hello-world
module github.com/lukoerfer/bifrost-encoding-format-plugin

go 1.26.1

require github.com/maximhq/bifrost/core v1.4.14
require (
github.com/maximhq/bifrost/core v1.4.14
github.com/tidwall/gjson v1.18.0
github.com/tidwall/sjson v1.2.5
)

require (
github.com/andybalholm/brotli v1.2.0 // indirect
Expand All @@ -20,10 +24,8 @@ require (
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
Expand Down
116 changes: 72 additions & 44 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,71 +1,99 @@
package main

import (
"encoding/json"
"fmt"
"strings"

"github.com/maximhq/bifrost/core/schemas"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)

// supportedFormats lists the valid encoding_format values.
var supportedFormats = map[string]bool{
"float": true,
"base64": true,
}

// pluginConfig holds the parsed plugin configuration.
type pluginConfig struct {
EncodingFormat string `json:"encoding_format"`
}

// activeConfig stores the plugin configuration after initialization.
var activeConfig pluginConfig

// Init parses the plugin configuration and validates the encoding_format value.
// The config is expected to be a JSON object with an "encoding_format" field.
// If no config is provided, the default encoding_format "float" is used.
func Init(config any) error {
fmt.Println("Init called")
activeConfig.EncodingFormat = "float"

if config == nil {
fmt.Printf("[encoding-format] No config provided, using default encoding_format: %s\n", activeConfig.EncodingFormat)
return nil
}

cfgBytes, err := json.Marshal(config)
if err != nil {
return fmt.Errorf("[encoding-format] failed to marshal config: %w", err)
}

if err := json.Unmarshal(cfgBytes, &activeConfig); err != nil {
return fmt.Errorf("[encoding-format] failed to parse config: %w", err)
}

if activeConfig.EncodingFormat == "" {
activeConfig.EncodingFormat = "float"
}

if !supportedFormats[activeConfig.EncodingFormat] {
return fmt.Errorf("[encoding-format] unsupported encoding_format: %s (supported: float, base64)", activeConfig.EncodingFormat)
}

fmt.Printf("[encoding-format] Initialized with encoding_format: %s\n", activeConfig.EncodingFormat)
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
// GetName returns the system identifier for this plugin (required).
func GetName() string {
return "hello-world"
return "encoding-format"
}

// HTTPTransportPreHook intercepts HTTP requests before they are sent to the
// downstream provider. For embedding requests that are missing the
// encoding_format parameter (or have it set to null), it injects the
// configured encoding_format value into the request body.
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(schemas.BifrostContextKey("hello-world-plugin-transport-pre-hook"), "transport-pre-hook-value")
// Return nil to continue processing, or return &schemas.HTTPResponse{} to short-circuit
return nil, nil
}
if !isEmbeddingRequest(req) {
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.SetValue(schemas.BifrostContextKey("hello-world-plugin-transport-post-hook"), "transport-post-hook-value")
// Return nil to continue processing
return nil
}
if len(req.Body) == 0 {
return nil, nil
}

func HTTPTransportStreamChunkHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, chunk *schemas.BifrostStreamChunk) (*schemas.BifrostStreamChunk, error) {
fmt.Println("HTTPTransportStreamChunkHook called")
// Modify chunk in-place
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"
result := gjson.GetBytes(req.Body, "encoding_format")
if result.Exists() && result.Type != gjson.Null {
return nil, nil
}
// Return the modified chunk
return chunk, nil
}

func PreLLMHook(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.LLMPluginShortCircuit, error) {
value1 := ctx.Value(schemas.BifrostContextKey("hello-world-plugin-transport-pre-hook"))
fmt.Println("value1:", value1)
ctx.SetValue(schemas.BifrostContextKey("hello-world-plugin-pre-hook"), "pre-hook-value")
fmt.Println("PreLLMHook called")
return req, nil, nil
newBody, err := sjson.SetBytes(req.Body, "encoding_format", activeConfig.EncodingFormat)
if err != nil {
return nil, fmt.Errorf("[encoding-format] failed to set encoding_format in request body: %w", err)
}

req.Body = newBody
return 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(schemas.BifrostContextKey("hello-world-plugin-transport-pre-hook"))
fmt.Println("value1:", value1)
value2 := ctx.Value(schemas.BifrostContextKey("hello-world-plugin-pre-hook"))
fmt.Println("value2:", value2)
return resp, bifrostErr, nil
// isEmbeddingRequest checks whether the HTTP request targets an embedding endpoint.
func isEmbeddingRequest(req *schemas.HTTPRequest) bool {
return strings.Contains(strings.ToLower(req.Path), "embedding")
}

// Cleanup performs any necessary teardown when the plugin is unloaded.
func Cleanup() error {
fmt.Println("Cleanup called")
return nil
}
Loading
Loading