diff --git a/core/schemas/plugin.go b/core/schemas/plugin.go index 572ecf05eb..35f05f97af 100644 --- a/core/schemas/plugin.go +++ b/core/schemas/plugin.go @@ -1,7 +1,10 @@ // Package schemas defines the core schemas and types used by the Bifrost system. package schemas -import "context" +import ( + "context" + "encoding/json" +) // PluginShortCircuit represents a plugin's decision to short-circuit the normal flow. // It can contain either a response (success short-circuit) or an error (error short-circuit). @@ -38,11 +41,22 @@ type PluginShortCircuit struct { // - AllowFallbacks = nil: Treated as true by default (allow fallbacks for resilience) // // Plugin authors should ensure their hooks are robust to both response and error being nil, and should not assume either is always present. +// +// STANDARDIZED PLUGIN DEVELOPMENT: +// All plugins should follow these conventions: +// 1. Constructor: NewPlugin(config json.RawMessage) (Plugin, error) +// 2. Implement SetLogger method for dependency injection +// 3. Handle nil logger gracefully (use default logging or no-op) type Plugin interface { // GetName returns the name of the plugin. GetName() string + // SetLogger injects a logger instance into the plugin. + // Plugins should handle nil logger gracefully (use default logging or no-op). + // This method will be called by Bifrost after plugin creation. + SetLogger(logger Logger) + // PreHook is called before a request is processed by a provider. // It allows plugins to modify the request before it is sent to the provider. // The context parameter can be used to maintain state across plugin calls. @@ -60,3 +74,8 @@ type Plugin interface { // Returns any error that occurred during cleanup, which will be logged as a warning by the Bifrost instance. Cleanup() error } + +// PluginConstructor defines the standardized constructor signature for all plugins. +// All plugins should implement: NewPlugin(config json.RawMessage) (Plugin, error) +// This is enforced at development time through documentation and examples. +type PluginConstructor func(config json.RawMessage) (Plugin, error) diff --git a/docs/plugin-system-complete-guide.md b/docs/plugin-system-complete-guide.md new file mode 100644 index 0000000000..f89bcd2ab9 --- /dev/null +++ b/docs/plugin-system-complete-guide.md @@ -0,0 +1,746 @@ +# Complete Plugin System Guide + +This comprehensive guide covers Bifrost's simplified plugin system from all perspectives: core users, transport users, and plugin developers. + +## Table of Contents + +1. [Overview](#overview) +2. [For Core Users (Go Package)](#for-core-users-go-package) +3. [For Transport Users (HTTP Service)](#for-transport-users-http-service) +4. [For Plugin Developers](#for-plugin-developers) +5. [Plugin Distribution](#plugin-distribution) +6. [Advanced Topics](#advanced-topics) +7. [Migration Guide](#migration-guide) +8. [Troubleshooting](#troubleshooting) + +## Overview + +Bifrost supports a simplified plugin system that allows extending functionality without modifying core code. The transports system supports **two plugin types**: + +1. **Local Builds** (for development) - Build and test plugins locally +2. **Go Packages** (for production) - Distribute plugins as Go modules + +### Key Features + +- **Process Isolation**: Plugins run as separate processes via RPC +- **Standardized Interface**: All plugins must implement the standardized `NewPlugin(json.RawMessage)` constructor +- **Auto-Generated RPC Wrappers**: No boilerplate code required - RPC wrappers are generated automatically +- **Configuration-Driven**: All plugins configured via JSON +- **Zero Development Friction**: Plugin developers only write business logic + +### Plugin Architecture + +```mermaid +graph TB + A[Bifrost Core] --> B[Plugin Pipeline] + B --> C[Plugin 1] + B --> D[Plugin 2] + B --> E[Plugin N] + + F[Bifrost Transport] --> G[Plugin Manager] + G --> H[Auto-Generated RPC Binary] + G --> I[Package Plugin RPC Binary] + + J[Plugin Builder] --> K[Go Package] + K --> L[Auto-Generated RPC Wrapper] + L --> M[RPC Binary] + M --> G + + N[Local Development] --> O[Auto-Generated RPC Wrapper] + O --> P[Built Binary] + P --> G +``` + +## For Core Users (Go Package) + +### Direct Plugin Usage + +When using Bifrost as a Go package, you can use plugins directly: + +```go +package main + +import ( + "context" + "encoding/json" + + bifrost "github.com/maximhq/bifrost/core" + "github.com/maximhq/bifrost/core/schemas" + "github.com/maximhq/bifrost/plugins/maxim" +) + +func main() { + // Configure plugin using standardized constructor + maximConfig := json.RawMessage(`{ + "api_key": "your_api_key", + "log_repo_id": "your_repo_id" + }`) + + maximPlugin, err := maxim.NewPlugin(maximConfig) + if err != nil { + panic(err) + } + + // Create account (your implementation) + account := &YourAccount{} + + // Initialize Bifrost with plugins + client, err := bifrost.Init(schemas.BifrostConfig{ + Account: account, + Plugins: []schemas.Plugin{maximPlugin}, + InitialPoolSize: 100, + }) + if err != nil { + panic(err) + } + + // Use Bifrost + response, err := client.ChatCompletionRequest( + schemas.OpenAI, + &schemas.BifrostRequest{ + Model: "gpt-4", + Input: schemas.RequestInput{ + ChatCompletionInput: &[]schemas.BifrostMessage{ + {Role: "user", Content: "Hello!"}, + }, + }, + }, + context.Background(), + ) +} +``` + +## For Transport Users (HTTP Service) + +### Runtime Setup of HTTP Transport with Plugins + +```mermaid +sequenceDiagram + participant Config as config.json + participant Transport as Bifrost HTTP Transport + participant Loader as Plugin Manager + participant Builder as Plugin Builder + participant RPC as RPC Process + participant Core as Bifrost Core + + Note over Config,Core: Transport Startup Phase + + Transport->>Config: Read configuration + Config-->>Transport: Plugin configurations + + Transport->>Loader: LoadPlugins(configs) + + loop For each plugin config + alt Local Plugin (source: "local") + Loader->>Loader: Validate plugin_path exists + Loader->>Loader: Check for NewPlugin function + Loader->>Loader: Auto-generate RPC wrapper + Loader->>Loader: Build plugin binary + Loader->>RPC: Start RPC process + RPC-->>Loader: Plugin instance via RPC + else Package Plugin (source: "package") + Loader->>Builder: BuildPluginFromPackage() + Builder->>Builder: Download Go package + Builder->>Builder: Validate NewPlugin function + Builder->>Builder: Auto-generate RPC wrapper + Builder->>Builder: Build plugin binary + Builder-->>Loader: Built binary path + Loader->>RPC: Start RPC process + RPC-->>Loader: Plugin instance via RPC + end + + Loader->>Loader: Set environment variables + Loader->>Loader: Inject logger + Loader-->>Transport: Loaded plugin instance + end + + Transport->>Core: Initialize Bifrost with plugins + Core-->>Transport: Ready to handle requests + + Note over Config,Core: Request Processing Phase + + Transport->>Core: Incoming HTTP request + Core->>RPC: Plugin.PreHook() via RPC + RPC-->>Core: Modified request/short-circuit + Core->>Core: Process with provider (if not short-circuited) + Core->>RPC: Plugin.PostHook() via RPC + RPC-->>Core: Modified response + Core-->>Transport: Final response + Transport-->>Config: HTTP response to client +``` + +### Plugin Loading Flow Details + +```mermaid +flowchart TD + A[Transport Startup] --> B[Read config.json] + B --> C[Parse Plugin Configurations] + C --> D{For Each Plugin} + + D --> E{Plugin Source?} + + E -->|local| F[Validate plugin_path exists] + E -->|package| G[Download Go package] + + F --> H[Validate NewPlugin function] + G --> H + + H --> I[Generate RPC wrapper in cmd/] + I --> J[Build plugin binary] + J --> K[Start RPC process] + + K --> L[Configure Environment Variables] + L --> M[Inject Logger] + M --> N[Add to Plugin Pipeline] + + N --> O{More Plugins?} + O -->|Yes| D + O -->|No| P[Initialize Bifrost Core] + P --> Q[HTTP Transport Ready] + + Q --> R[Handle HTTP Requests] + R --> S[Execute Plugin Pipeline] + S --> T[Return Response] +``` + +### Configuration-Based Plugin System + +All plugins are configured in your `config.json` using only two types: + +```json +{ + "providers": { + "openAI": { + "keys": [ + { "value": "env.OPENAI_API_KEY", "models": ["gpt-4"], "weight": 1.0 } + ] + } + }, + "plugins": [ + { + "name": "maxim", + "source": "local", + "plugin_path": "./plugins/maxim", + "enabled": true, + "config": { + "api_key": "your_api_key", + "log_repo_id": "your_repo_id" + }, + "env_vars": { + "MAXIM_API_KEY": "env.MAXIM_API_KEY", + "MAXIM_LOG_REPO_ID": "env.MAXIM_LOG_REPO_ID" + } + }, + { + "name": "custom-plugin", + "source": "package", + "package": "github.com/company/custom-bifrost-plugin", + "version": "v1.0.0", + "enabled": true, + "config": { + "custom_setting": "value" + } + } + ] +} +``` + +### Plugin Types + +#### 1. Local Plugins (Development) + +For development and testing: + +```json +{ + "name": "my-plugin", + "source": "local", + "plugin_path": "./plugins/my-plugin", + "enabled": true, + "config": { + "setting": "value" + } +} +``` + +Requirements: + +- `plugin_path` must point to a valid plugin directory +- Plugin directory must contain Go files with `NewPlugin` function +- RPC wrapper is automatically generated during build + +> **⚠️ Docker Limitation**: Local plugins **cannot** be used when running transports via Docker containers. Local plugins require access to the host filesystem for building, which is not available inside Docker containers. If you need to use local plugins, run the transport directly with Go: +> +> ```bash +> # ✅ Works with local plugins +> go run ./transports/bifrost-http -config config.json +> +> # ❌ Local plugins won't work with Docker +> docker run -v $(pwd)/config.json:/app/config.json your-image +> ``` +> +> For production deployments with Docker, use **package plugins** instead. + +#### 2. Package Plugins (Production) + +For production deployments: + +```json +{ + "name": "production-plugin", + "source": "package", + "package": "github.com/company/bifrost-plugin", + "version": "v1.2.0", + "enabled": true, + "config": { + "api_key": "production_key" + } +} +``` + +Requirements: + +- `package` must be a valid Go module path +- Package must implement the standardized `NewPlugin` constructor +- RPC wrapper is automatically generated during build + +### Running the Transport + +```bash +# Simple usage +./bifrost-http -config config.json + +# With additional options +./bifrost-http \ + -config config.json \ + -port 8080 \ + -pool-size 300 +``` + +## For Plugin Developers + +### Mandatory Requirements + +**ALL plugins MUST implement ONLY:** + +1. **Standardized Constructor Pattern:** + +```go +func NewPlugin(configJSON json.RawMessage) (schemas.Plugin, error) +``` + +**That's it!** No boilerplate code, no RPC wrappers required. + +### Simplified Plugin Directory Structure + +Your plugin only needs this minimal structure: + +``` +your-plugin/ +├── plugin.go # Plugin logic and NewPlugin constructor +├── go.mod # Go module definition +├── go.sum # Go module checksums (generated) +└── README.md # Plugin documentation (optional) +``` + +**Note:** The RPC wrapper is automatically generated by the Bifrost plugin system. + +### Plugin Interface Requirements + +All plugins must implement the `schemas.Plugin` interface: + +```go +type Plugin interface { + GetName() string + PreHook(ctx *context.Context, req *BifrostRequest) (*BifrostRequest, *PluginShortCircuit, error) + PostHook(ctx *context.Context, result *BifrostResponse, err *BifrostError) (*BifrostResponse, *BifrostError, error) + Cleanup() error + SetLogger(logger Logger) +} +``` + +### Complete Plugin Implementation Example + +```go +package myplugin + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "github.com/maximhq/bifrost/core/schemas" +) + +// Configuration struct +type PluginConfig struct { + APIKey string `json:"api_key"` + Endpoint string `json:"endpoint"` + Timeout string `json:"timeout"` +} + +// MANDATORY: Standardized constructor - ONLY thing you need to implement! +func NewPlugin(configJSON json.RawMessage) (schemas.Plugin, error) { + var config PluginConfig + if err := json.Unmarshal(configJSON, &config); err != nil { + return nil, fmt.Errorf("invalid config: %w", err) + } + + // Validate configuration + if config.APIKey == "" { + return nil, fmt.Errorf("api_key is required") + } + + return &MyPlugin{ + config: config, + client: &http.Client{}, + }, nil +} + +// Plugin implementation +type MyPlugin struct { + config PluginConfig + client *http.Client + logger schemas.Logger +} + +func (p *MyPlugin) GetName() string { + return "my-plugin" +} + +func (p *MyPlugin) SetLogger(logger schemas.Logger) { + p.logger = logger +} + +func (p *MyPlugin) PreHook(ctx *context.Context, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.PluginShortCircuit, error) { + // Your pre-processing logic here + p.logger.Info("Processing request in my-plugin") + return req, nil, nil +} + +func (p *MyPlugin) PostHook(ctx *context.Context, result *schemas.BifrostResponse, err *schemas.BifrostError) (*schemas.BifrostResponse, *schemas.BifrostError, error) { + // Your post-processing logic here + p.logger.Info("Processing response in my-plugin") + return result, err, nil +} + +func (p *MyPlugin) Cleanup() error { + // Cleanup resources + return nil +} +``` + +### Plugin Testing + +```go +package myplugin + +import ( + "context" + "encoding/json" + "testing" + "github.com/maximhq/bifrost/core/schemas" +) + +func TestNewPlugin(t *testing.T) { + // Test plugin creation with valid config + config := PluginConfig{ + APIKey: "test-key", + Endpoint: "https://api.example.com", + } + configJSON, _ := json.Marshal(config) + + plugin, err := NewPlugin(configJSON) + if err != nil { + t.Fatalf("Failed to create plugin: %v", err) + } + + if plugin.GetName() == "" { + t.Error("Plugin name should not be empty") + } +} + +func TestNewPluginInvalidConfig(t *testing.T) { + // Test with invalid config + invalidConfig := json.RawMessage(`{"invalid": "config"}`) + + _, err := NewPlugin(invalidConfig) + if err == nil { + t.Error("Expected error for invalid config") + } +} +``` + +### Development Workflow + +#### 1. Create Plugin + +```bash +# Create plugin directory +mkdir my-bifrost-plugin +cd my-bifrost-plugin + +# Initialize Go module +go mod init github.com/company/my-bifrost-plugin + +# Create plugin.go with NewPlugin function +# (See example above) +``` + +#### 2. Test Locally + +```bash +# Test with local configuration +# The system will auto-generate RPC wrapper and build for you +``` + +#### 3. Distribute + +```bash +# Push to repository and tag version +git add . +git commit -m "Initial plugin implementation" +git tag v1.0.0 +git push origin main +git push origin v1.0.0 +``` + +## Plugin Distribution + +### 1. Go Package Distribution (Recommended) + +**Create Go Module:** + +```bash +mkdir my-bifrost-plugin +cd my-bifrost-plugin +go mod init github.com/company/my-bifrost-plugin + +# Implement plugin with NewPlugin constructor... +# NO RPC wrapper needed - it's auto-generated! + +git add . +git commit -m "Initial plugin implementation" +git tag v1.0.0 +git push origin main +git push origin v1.0.0 +``` + +**Use in Production:** + +```json +{ + "plugins": [ + { + "name": "my-plugin", + "source": "package", + "package": "github.com/company/my-bifrost-plugin", + "version": "v1.0.0", + "enabled": true, + "config": { + "api_key": "production_key" + } + } + ] +} +``` + +**The system automatically:** + +- Downloads the Go package +- Generates the RPC wrapper +- Builds the plugin binary +- Loads it into the transport + +### 2. Local Development + +**Develop Locally:** + +```bash +# Create plugin in local directory +mkdir ./plugins/my-plugin +cd ./plugins/my-plugin + +# Implement plugin with NewPlugin function +# NO RPC wrapper needed! +``` + +**Use in Config:** + +```json +{ + "plugins": [ + { + "name": "my-plugin", + "source": "local", + "plugin_path": "./plugins/my-plugin", + "enabled": true, + "config": { + "api_key": "dev_key" + } + } + ] +} +``` + +**The system automatically:** + +- Generates the RPC wrapper +- Builds the plugin binary +- Loads it into the transport + +## Advanced Topics + +### Plugin Communication + +```go +// Share data between plugins using context +func (p *Plugin1) PreHook(ctx *context.Context, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.PluginShortCircuit, error) { + // Store data in context + *ctx = context.WithValue(*ctx, "plugin1-data", "some-value") + return req, nil, nil +} + +func (p *Plugin2) PreHook(ctx *context.Context, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.PluginShortCircuit, error) { + // Read data from context + if data := (*ctx).Value("plugin1-data"); data != nil { + // Use data from Plugin1 + } + return req, nil, nil +} +``` + +### Configuration Best Practices + +```go +// Support both config file and environment variables +func NewPlugin(configJSON json.RawMessage) (schemas.Plugin, error) { + var config PluginConfig + if err := json.Unmarshal(configJSON, &config); err != nil { + return nil, fmt.Errorf("invalid config: %w", err) + } + + // Allow environment variable fallback + if config.APIKey == "" { + config.APIKey = os.Getenv("PLUGIN_API_KEY") + } + + if config.APIKey == "" { + return nil, fmt.Errorf("api_key is required") + } + + return &MyPlugin{config: config}, nil +} +``` + +## Migration Guide + +### From Legacy System + +**Before (Legacy):** + +```bash +./bifrost-http -config config.json -plugins maxim +``` + +**After (New):** + +```json +{ + "plugins": [ + { + "name": "maxim", + "source": "local", + "plugin_path": "./plugins/maxim", + "enabled": true, + "config": { + "api_key": "your_key", + "log_repo_id": "your_repo_id" + } + } + ] +} +``` + +### Migration Checklist + +- [ ] Remove `-plugins` command line flags +- [ ] Add plugin configuration to config.json +- [ ] Ensure all plugins implement standardized `NewPlugin` constructor +- [ ] Update configuration to use `plugin_path` for local plugins +- [ ] Test plugin functionality + +## Troubleshooting + +### Common Issues + +**Plugin Not Loading:** + +``` +warning: failed to load plugin: plugin_path is required +``` + +_Solution: Ensure plugin_path points to valid plugin directory for local source_ + +**Missing NewPlugin Constructor:** + +``` +plugin validation failed: plugin must implement 'func NewPlugin(json.RawMessage) (schemas.Plugin, error)' +``` + +_Solution: Ensure plugin implements the mandatory NewPlugin constructor function_ + +**Package Plugin Download Failed:** + +``` +failed to build plugin from package: failed to get package github.com/company/plugin +``` + +_Solution: Ensure package path is correct and accessible. Check network connectivity and Go module proxy settings._ + +**Auto-Generation Failed:** + +``` +failed to generate RPC wrapper: failed to parse package info +``` + +_Solution: Ensure plugin directory contains valid Go files with proper package declaration_ + +### Debugging + +```bash +# Enable debug logging +export BIFROST_LOG_LEVEL=debug + +# The system will show auto-generation steps: +# "Auto-generating RPC wrapper for plugin my-plugin" +# "Successfully built plugin my-plugin from ./plugins/my-plugin" +``` + +### What's Automated + +**✅ Automatically Handled:** + +- RPC wrapper generation (`cmd/main.go`) +- Plugin binary building +- Package downloading and building +- Interface validation + +**❌ Still Required:** + +- Implementing `NewPlugin(json.RawMessage) (schemas.Plugin, error)` +- Plugin business logic +- Configuration validation + +### Supported Plugin Sources + +**✅ Supported:** + +- `local` - Local plugin directories (for development) +- `package` - Go modules with standardized constructor (for production) + +**❌ Not Supported:** + +- Base64 encoded plugins + +This simplified guide covers the new streamlined Bifrost plugin system with auto-generated RPC wrappers. Plugin developers only need to implement the `NewPlugin` constructor - everything else is handled automatically! diff --git a/docs/plugins.md b/docs/plugins.md index a7e796092a..d6536b526e 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -2,6 +2,8 @@ Bifrost provides a powerful plugin system that allows you to extend and customize the request/response pipeline. Plugins can implement rate limiting, caching, authentication, logging, monitoring, and more. +**🎉 New: Auto-Generated RPC Wrappers** - Plugin developers only need to implement business logic. All RPC boilerplate is automatically generated! + ## Table of Contents 1. [Plugin Architecture Overview](#1-plugin-architecture-overview) @@ -28,6 +30,14 @@ Bifrost plugins follow a **PreHook → Provider → PostHook** pattern with supp - **Short-Circuit**: Plugin can skip provider call and return response/error directly - **Fallback Control**: Plugins can control whether fallback providers should be tried - **Pipeline Symmetry**: Every PreHook execution gets a corresponding PostHook call +- **Auto-Generated RPC**: All RPC communication is handled automatically - no boilerplate required + +### Plugin Distribution + +Bifrost supports two plugin deployment methods: + +1. **Local Plugins** (Development): Point to plugin directory, system auto-generates RPC wrapper and builds +2. **Package Plugins** (Production): Reference Go modules, system downloads, generates wrapper, and builds automatically ## 2. Plugin Interface @@ -46,6 +56,9 @@ type Plugin interface { // Cleanup is called on bifrost shutdown Cleanup() error + + // SetLogger provides a logger instance to the plugin + SetLogger(logger Logger) } type PluginShortCircuit struct { @@ -54,12 +67,31 @@ type PluginShortCircuit struct { } ``` +### Mandatory Plugin Constructor + +**ALL plugins MUST implement this standardized constructor:** + +```go +func NewPlugin(configJSON json.RawMessage) (schemas.Plugin, error) { + // Parse configuration + var config YourPluginConfig + if err := json.Unmarshal(configJSON, &config); err != nil { + return nil, fmt.Errorf("invalid config: %w", err) + } + + // Validate and create plugin instance + return &YourPlugin{config: config}, nil +} +``` + ## 3. Plugin Lifecycle ```mermaid stateDiagram-v2 - [*] --> PluginInit: Plugin Creation - PluginInit --> Registered: Add to BifrostConfig + [*] --> PluginInit: Plugin Creation via NewPlugin() + PluginInit --> RPCWrapper: Auto-Generate RPC Wrapper + RPCWrapper --> BuildBinary: Compile RPC Binary + BuildBinary --> Registered: Load into Transport Registered --> PreHookCall: Request Received PreHookCall --> ModifyRequest: Normal Flow @@ -276,63 +308,181 @@ func (p *RetryPlugin) PostHook(ctx *context.Context, result *BifrostResponse, er ## 7. Building Custom Plugins -### Basic Plugin Structure +### Quick Start - Zero Boilerplate Required! + +Creating a plugin is now incredibly simple. You only need to implement the `NewPlugin` constructor - all RPC boilerplate is automatically generated! ```go -type CustomPlugin struct { - config CustomConfig - // Add your fields here +// plugin.go +package myplugin + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os" + "github.com/maximhq/bifrost/core/schemas" +) + +// Configuration for your plugin +type PluginConfig struct { + LogLevel string `json:"log_level"` + Enabled bool `json:"enabled"` +} + +// MANDATORY: This is the ONLY function you need to implement +func NewPlugin(configJSON json.RawMessage) (schemas.Plugin, error) { + var config PluginConfig + if err := json.Unmarshal(configJSON, &config); err != nil { + return nil, fmt.Errorf("invalid config: %w", err) + } + + return &MyPlugin{ + config: config, + logger: log.New(os.Stdout, "[MyPlugin] ", log.LstdFlags), + }, nil } -func NewCustomPlugin(config CustomConfig) *CustomPlugin { - return &CustomPlugin{config: config} +// Your plugin implementation +type MyPlugin struct { + config PluginConfig + logger *log.Logger } -func (p *CustomPlugin) GetName() string { - return "CustomPlugin" +func (p *MyPlugin) GetName() string { + return "my-plugin" } -func (p *CustomPlugin) PreHook(ctx *context.Context, req *BifrostRequest) (*BifrostRequest, *PluginShortCircuit, error) { - // Modify request or short-circuit +func (p *MyPlugin) SetLogger(logger schemas.Logger) { + // Optional: Use Bifrost's logger instead of your own +} + +func (p *MyPlugin) PreHook(ctx *context.Context, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.PluginShortCircuit, error) { + if !p.config.Enabled { + return req, nil, nil + } + + p.logger.Printf("Request: %s to %s", req.Model, req.Provider) return req, nil, nil } -func (p *CustomPlugin) PostHook(ctx *context.Context, result *BifrostResponse, err *BifrostError) (*BifrostResponse, *BifrostError, error) { - // Modify response/error or recover from errors +func (p *MyPlugin) PostHook(ctx *context.Context, result *schemas.BifrostResponse, err *schemas.BifrostError) (*schemas.BifrostResponse, *schemas.BifrostError, error) { + if !p.config.Enabled { + return result, err, nil + } + + if err != nil { + p.logger.Printf("Error: %v", err.Error.Message) + } else { + p.logger.Printf("Success: %d tokens", result.Usage.TotalTokens) + } return result, err, nil } -func (p *CustomPlugin) Cleanup() error { - // Clean up resources +func (p *MyPlugin) Cleanup() error { return nil } ``` +### Simplified Plugin Directory Structure + +Your plugin only needs this minimal structure: + +``` +your-plugin/ +├── plugin.go # Plugin implementation with NewPlugin constructor +├── go.mod # Go module definition +├── go.sum # Go module checksums (generated) +└── README.md # Documentation (optional) +``` + ### Plugin Development Checklist +- [ ] Implement `NewPlugin(json.RawMessage) (schemas.Plugin, error)` constructor - [ ] Handle nil response and error in PostHook - [ ] Set appropriate AllowFallbacks for errors - [ ] Implement proper cleanup in Cleanup() -- [ ] Add configuration validation +- [ ] Add configuration validation in NewPlugin - [ ] Write comprehensive tests - [ ] Document behavior and configuration +### Local Development + +```bash +# Create plugin directory +mkdir ./plugins/my-plugin +cd ./plugins/my-plugin + +# Initialize Go module +go mod init my-plugin + +# Create plugin.go with NewPlugin function +# (See example above) + +# Test locally - no build required! +# System auto-generates RPC wrapper and builds for you +``` + +### Configuration Usage + +```json +{ + "plugins": [ + { + "name": "my-plugin", + "source": "local", + "plugin_path": "./plugins/my-plugin", + "enabled": true, + "config": { + "log_level": "info", + "enabled": true + } + } + ] +} +``` + ## 8. Plugin Examples ### Rate Limiting Plugin ```go -type RateLimitPlugin struct { - limiters map[ModelProvider]*rate.Limiter - mu sync.RWMutex +package ratelimit + +import ( + "encoding/json" + "fmt" + "sync" + "golang.org/x/time/rate" + "github.com/maximhq/bifrost/core/schemas" +) + +type RateLimitConfig struct { + Limits map[string]float64 `json:"limits"` // provider -> requests per second } -func NewRateLimitPlugin(limits map[ModelProvider]float64) *RateLimitPlugin { - limiters := make(map[ModelProvider]*rate.Limiter) - for provider, limit := range limits { +// MANDATORY: Standardized constructor +func NewPlugin(configJSON json.RawMessage) (schemas.Plugin, error) { + var config RateLimitConfig + if err := json.Unmarshal(configJSON, &config); err != nil { + return nil, fmt.Errorf("invalid config: %w", err) + } + + limiters := make(map[schemas.ModelProvider]*rate.Limiter) + for providerStr, limit := range config.Limits { + provider := schemas.ModelProvider(providerStr) limiters[provider] = rate.NewLimiter(rate.Limit(limit), 1) } - return &RateLimitPlugin{limiters: limiters} + + return &RateLimitPlugin{ + limiters: limiters, + }, nil +} + +type RateLimitPlugin struct { + limiters map[schemas.ModelProvider]*rate.Limiter + mu sync.RWMutex } func (p *RateLimitPlugin) GetName() string { @@ -371,12 +521,39 @@ func (p *RateLimitPlugin) Cleanup() error { ### Authentication Plugin ```go -type AuthPlugin struct { - validator TokenValidator +package auth + +import ( + "encoding/json" + "fmt" + "github.com/maximhq/bifrost/core/schemas" +) + +type AuthConfig struct { + RequiredHeader string `json:"required_header"` + ValidTokens []string `json:"valid_tokens"` +} + +// MANDATORY: Standardized constructor +func NewPlugin(configJSON json.RawMessage) (schemas.Plugin, error) { + var config AuthConfig + if err := json.Unmarshal(configJSON, &config); err != nil { + return nil, fmt.Errorf("invalid config: %w", err) + } + + if config.RequiredHeader == "" { + return nil, fmt.Errorf("required_header is mandatory") + } + + return &AuthPlugin{ + config: config, + validator: NewTokenValidator(config.ValidTokens), + }, nil } -func NewAuthPlugin(validator TokenValidator) *AuthPlugin { - return &AuthPlugin{validator: validator} +type AuthPlugin struct { + config AuthConfig + validator TokenValidator } func (p *AuthPlugin) GetName() string { @@ -479,29 +656,72 @@ Each plugin should be organized as follows: ``` plugins/ └── your-plugin-name/ - ├── main.go # Plugin implementation + ├── plugin.go # Plugin implementation with NewPlugin constructor ├── plugin_test.go # Comprehensive tests ├── README.md # Documentation with examples └── go.mod # Module definition ``` -### Using Plugins +**Note:** No RPC wrapper required - it's auto-generated! + +### Using Plugins in Core (Direct Go Usage) ```go import ( + "encoding/json" "github.com/maximhq/bifrost/core" "github.com/your-org/your-plugin" ) +// Configure plugin +pluginConfig := json.RawMessage(`{ + "setting1": "value1", + "setting2": true +}`) + +plugin, err := your_plugin.NewPlugin(pluginConfig) +if err != nil { + panic(err) +} + client, err := bifrost.Init(schemas.BifrostConfig{ Account: &yourAccount, Plugins: []schemas.Plugin{ - your_plugin.NewYourPlugin(config), + plugin, // Add more plugins as needed }, }) ``` +### Using Plugins in Transport (Configuration-Based) + +```json +{ + "plugins": [ + { + "name": "your-plugin", + "source": "local", + "plugin_path": "./plugins/your-plugin", + "enabled": true, + "config": { + "setting1": "value1", + "setting2": true + } + }, + { + "name": "production-plugin", + "source": "package", + "package": "github.com/your-org/your-plugin", + "version": "v1.0.0", + "enabled": true, + "config": { + "production_setting": "prod_value" + } + } + ] +} +``` + ### Plugin Execution Order Plugins execute in the order they are registered: diff --git a/plugins/maxim/go.mod b/plugins/maxim/go.mod index cc4446a354..b4c2964fd3 100644 --- a/plugins/maxim/go.mod +++ b/plugins/maxim/go.mod @@ -7,6 +7,8 @@ require ( github.com/maximhq/maxim-go v0.1.3 ) +replace github.com/maximhq/bifrost/core => ../../core + require github.com/google/uuid v1.6.0 require ( @@ -25,12 +27,39 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect github.com/aws/smithy-go v1.22.3 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.8.1 // indirect + github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/go-playground/validator/v10 v10.10.0 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/invopop/jsonschema v0.12.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/metoro-io/mcp-golang v0.13.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect + github.com/ugorji/go/codec v1.2.7 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.62.0 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + golang.org/x/crypto v0.38.0 // indirect golang.org/x/net v0.40.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.25.0 // indirect + google.golang.org/protobuf v1.28.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/plugins/maxim/go.sum b/plugins/maxim/go.sum index d3e3bafe19..ffcec128f5 100644 --- a/plugins/maxim/go.sum +++ b/plugins/maxim/go.sum @@ -28,29 +28,134 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/Xv github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k= github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= +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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= +github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0= +github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= +github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/maximhq/bifrost/core v1.1.2 h1:NR5zWD+2dMkj1ySmGqcE7VDJUhkvgrjoMoQikxsdXPU= -github.com/maximhq/bifrost/core v1.1.2/go.mod h1:8ycaWQ9bjQezoUT/x6a82VmPjoqLzyGglQ0RnnlZjqo= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +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/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/maximhq/maxim-go v0.1.3 h1:nVzdz3hEjZVxmWHARWIM+Yrn1Jp50qrsK4BA/sz2jj8= github.com/maximhq/maxim-go v0.1.3/go.mod h1:0+UTWM7UZwNNE5VnljLtr/vpRGtYP8r/2q9WDwlLWFw= +github.com/metoro-io/mcp-golang v0.13.0 h1:54TFBJIW76VRB55CJovQQje9x4GnXg0BQQwGRtXrbCE= +github.com/metoro-io/mcp-golang v0.13.0/go.mod h1:ifLP9ZzKpN1UqFWNTpAHOqSvNkMK6b7d1FSZ5Lu0lN0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= +github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/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/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= 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.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0= github.com/valyala/fasthttp v1.62.0/go.mod h1:FCINgr4GKdKqV8Q0xv8b+UxPV+H/O5nNFo3D+r54Htg= +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= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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= diff --git a/plugins/maxim/main.go b/plugins/maxim/main.go index 7b532a8e49..9d36d369ed 100644 --- a/plugins/maxim/main.go +++ b/plugins/maxim/main.go @@ -6,6 +6,7 @@ import ( "context" "encoding/json" "fmt" + "os" "github.com/google/uuid" "github.com/maximhq/bifrost/core/schemas" @@ -17,6 +18,43 @@ import ( // PluginName is the canonical name for the bifrost-maxim plugin. const PluginName = "bifrost-maxim" +// MaximConfig represents the configuration for the Maxim plugin +type MaximConfig struct { + APIKey string `json:"api_key"` + LogRepoID string `json:"log_repo_id"` +} + +// NewPlugin creates a new Maxim plugin instance using standardized configuration +// This is the standardized constructor that all plugins should implement +func NewPlugin(configJSON json.RawMessage) (schemas.Plugin, error) { + var config MaximConfig + + // Parse the JSON configuration + if len(configJSON) > 0 { + if err := json.Unmarshal(configJSON, &config); err != nil { + return nil, fmt.Errorf("failed to parse maxim plugin configuration: %w", err) + } + } + + // Allow configuration from environment variables if not provided in config + if config.APIKey == "" { + config.APIKey = os.Getenv("MAXIM_API_KEY") + } + if config.LogRepoID == "" { + config.LogRepoID = os.Getenv("MAXIM_LOG_REPO_ID") + } + + // Validate required configuration + if config.APIKey == "" { + return nil, fmt.Errorf("API key is required (provide via config.api_key or MAXIM_API_KEY environment variable)") + } + if config.LogRepoID == "" { + return nil, fmt.Errorf("log repo ID is required (provide via config.log_repo_id or MAXIM_LOG_REPO_ID environment variable)") + } + + return NewMaximLoggerPlugin(config.APIKey, config.LogRepoID) +} + // NewMaximLogger initializes and returns a Plugin instance for Maxim's logger. // // Parameters: @@ -111,7 +149,7 @@ func (plugin *Plugin) GetName() string { // Returns: // - *schemas.BifrostRequest: The original request, unmodified // - error: Any error that occurred during trace/generation creation -func (plugin *Plugin) PreHook(ctx *context.Context, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.BifrostResponse, error) { +func (plugin *Plugin) PreHook(ctx *context.Context, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.PluginShortCircuit, error) { var traceID string var sessionID string @@ -287,3 +325,7 @@ func (plugin *Plugin) Cleanup() error { return nil } + +func (plugin *Plugin) SetLogger(logger schemas.Logger) { + // no-op +} diff --git a/plugins/mocker/main.go b/plugins/mocker/main.go index b4f6794fbd..941174f654 100644 --- a/plugins/mocker/main.go +++ b/plugins/mocker/main.go @@ -2,6 +2,7 @@ package mocker import ( "context" + "encoding/json" "fmt" "maps" "math/rand" @@ -46,6 +47,7 @@ type MockerPlugin struct { rules []MockRule compiledRules []compiledRule // Pre-compiled rules for performance mu sync.RWMutex + logger schemas.Logger // Injected logger instance // Atomic counters for high-performance statistics tracking totalRequests int64 @@ -938,3 +940,53 @@ func (p *MockerPlugin) GetStats() MockStats { return statsCopy } + +// NewPlugin creates a new mocker plugin instance using standardized configuration +// This is the standardized constructor that all plugins should implement +func NewPlugin(configJSON json.RawMessage) (schemas.Plugin, error) { + var config MockerConfig + + // Parse the JSON configuration + if len(configJSON) > 0 { + if err := json.Unmarshal(configJSON, &config); err != nil { + return nil, fmt.Errorf("failed to parse mocker plugin configuration: %w", err) + } + } else { + // Default configuration if none provided + config = MockerConfig{ + Enabled: true, + DefaultBehavior: DefaultBehaviorPassthrough, + } + } + + return NewMockerPlugin(config) +} + +// SetLogger injects a logger instance into the plugin +func (p *MockerPlugin) SetLogger(logger schemas.Logger) { + p.mu.Lock() + defer p.mu.Unlock() + p.logger = logger +} + +// log is a helper method that logs messages using the injected logger or falls back to no-op +func (p *MockerPlugin) log(level string, message string, args ...interface{}) { + p.mu.RLock() + logger := p.logger + p.mu.RUnlock() + + if logger != nil { + formattedMsg := fmt.Sprintf(message, args...) + switch level { + case "debug": + logger.Debug(formattedMsg) + case "info": + logger.Info(formattedMsg) + case "warn": + logger.Warn(formattedMsg) + case "error": + logger.Error(fmt.Errorf("%s", formattedMsg)) + } + } + // If no logger is injected, do nothing (graceful degradation) +} diff --git a/transports/Dockerfile b/transports/Dockerfile index a5bad7efbd..bcd3ba7ff5 100644 --- a/transports/Dockerfile +++ b/transports/Dockerfile @@ -43,10 +43,9 @@ USER appuser ENV APP_PORT=8080 \ APP_POOL_SIZE=300 \ APP_DROP_EXCESS_REQUESTS=false \ - APP_PLUGINS="" \ APP_PROMETHEUS_LABELS="" EXPOSE 8080 # Direct entrypoint with environment variable expansion -ENTRYPOINT ["/bin/sh", "-c", "exec /app/main -config /app/config/config.json -port \"${APP_PORT}\" -pool-size \"${APP_POOL_SIZE}\" -drop-excess-requests \"${APP_DROP_EXCESS_REQUESTS}\" -plugins \"${APP_PLUGINS}\" -prometheus-labels \"${APP_PROMETHEUS_LABELS}\""] \ No newline at end of file +ENTRYPOINT ["/bin/sh", "-c", "exec /app/main -config /app/config/config.json -port \"${APP_PORT}\" -pool-size \"${APP_POOL_SIZE}\" -drop-excess-requests \"${APP_DROP_EXCESS_REQUESTS}\" -prometheus-labels \"${APP_PROMETHEUS_LABELS}\""] \ No newline at end of file diff --git a/transports/README.md b/transports/README.md index dfde0f1c45..54cf4b9ba0 100644 --- a/transports/README.md +++ b/transports/README.md @@ -196,7 +196,6 @@ You can also set runtime environment variables for configuration: - `APP_PORT`: Server port (default: 8080) - `APP_POOL_SIZE`: Connection pool size (default: 300) - `APP_DROP_EXCESS_REQUESTS`: Drop excess requests when buffer is full (default: false) -- `APP_PLUGINS`: Comma-separated list of plugins Read more about these [configurations](https://github.com/maximhq/bifrost/tree/main?tab=README-ov-file#additional-configurations). @@ -408,11 +407,49 @@ Values for labels are then picked up from the HTTP request headers with the pref ### Plugin Support -You can explore the [available plugins](https://github.com/maximhq/bifrost/tree/main/plugins). To attach these plugins to your HTTP transport, pass the `-plugins` flag. +Bifrost supports a simplified plugin system with auto-generated RPC wrappers. You can explore the [available plugins](https://github.com/maximhq/bifrost/tree/main/plugins). -e.g., `-plugins maxim` +**Plugin Configuration:** -Note: Check plugin-specific documentation (github.com/maximhq/bifrost/tree/main/plugins/{plugin_name}) for more granular control and additional setup requirements. +```json +{ + "plugins": [ + { + "name": "maxim", + "source": "local", + "plugin_path": "./plugins/maxim", + "enabled": true, + "env_vars": { + "MAXIM_API_KEY": "env.MAXIM_API_KEY", + "MAXIM_LOG_REPO_ID": "env.MAXIM_LOG_REPO_ID" + } + }, + { + "name": "production-plugin", + "source": "package", + "package": "github.com/company/bifrost-plugin", + "version": "v1.0.0", + "enabled": true, + "config": { + "setting": "value" + } + } + ] +} +``` + +**Plugin Types:** + +- **Local plugins** (`source: "local"`): Point to plugin directory, system auto-builds with generated RPC wrapper +- **Package plugins** (`source: "package"`): Reference Go modules, system downloads and builds automatically + +**Key Benefits:** + +- **Zero Boilerplate**: Plugin developers only implement business logic +- **Auto-Generated RPC**: All RPC communication handled automatically +- **Simplified Development**: No manual `cmd/main.go` files required + +For comprehensive plugin development guide, see [Plugin System Documentation](../docs/plugin-system-complete-guide.md). ### Fallbacks diff --git a/transports/bifrost-http/lib/config.go b/transports/bifrost-http/lib/config.go index 40463f2678..586a70388f 100644 --- a/transports/bifrost-http/lib/config.go +++ b/transports/bifrost-http/lib/config.go @@ -10,6 +10,7 @@ import ( "github.com/maximhq/bifrost/core/schemas" "github.com/maximhq/bifrost/core/schemas/meta" + "github.com/maximhq/bifrost/transports/bifrost-http/lib/plugins" ) // ProviderConfig represents the configuration for a specific AI model provider. @@ -25,10 +26,11 @@ type ProviderConfig struct { type ConfigMap map[schemas.ModelProvider]ProviderConfig // BifrostHTTPConfig represents the complete configuration structure for Bifrost HTTP transport. -// It includes both provider configurations and MCP configuration. +// It includes provider configurations, MCP configuration, and plugin configurations. type BifrostHTTPConfig struct { - ProviderConfig ConfigMap `json:"providers"` // Provider configurations - MCPConfig *schemas.MCPConfig `json:"mcp"` // MCP configuration (optional) + ProviderConfig ConfigMap `json:"providers"` // Provider configurations + MCPConfig *schemas.MCPConfig `json:"mcp"` // MCP configuration (optional) + Plugins []plugins.PluginConfig `json:"plugins"` // Plugin configurations (optional) } // readConfig reads and parses the configuration file. @@ -51,7 +53,30 @@ type BifrostHTTPConfig struct { // }, // "mcp": { // "client_configs": [...] -// } +// }, +// "plugins": [ +// { +// "name": "mocker", +// "source": "local", +// "binary_path": "./plugins/mocker-plugin", +// "enabled": true, +// "config": { +// "enabled": true, +// "rules": [...] +// } +// }, +// { +// "name": "maxim", +// "source": "package", +// "package": "github.com/maximhq/bifrost-maxim-plugin", +// "version": "v1.0.0", +// "enabled": true, +// "env_vars": { +// "MAXIM_API_KEY": "env.MAXIM_API_KEY", +// "MAXIM_LOG_REPO_ID": "env.MAXIM_LOG_REPO_ID" +// } +// } +// ] // // In this example, OPENAI_API_KEY refers to a key in the environment variables. At runtime, its value will be used to replace the placeholder. // Same setup applies to keys in meta configs of all the providers. diff --git a/transports/bifrost-http/lib/ctx.go b/transports/bifrost-http/lib/ctx.go index 7a2579dda7..26f3deea83 100644 --- a/transports/bifrost-http/lib/ctx.go +++ b/transports/bifrost-http/lib/ctx.go @@ -3,14 +3,13 @@ // // This package handles the conversion of FastHTTP request contexts to Bifrost contexts, // ensuring that important metadata and tracking information is preserved across the system. -// It supports propagation of both Prometheus metrics and Maxim tracing data through HTTP headers. +// It supports propagation of Prometheus metrics data through HTTP headers. package lib import ( "context" "strings" - "github.com/maximhq/bifrost/plugins/maxim" "github.com/maximhq/bifrost/transports/bifrost-http/tracking" "github.com/valyala/fasthttp" ) @@ -18,16 +17,10 @@ import ( // ConvertToBifrostContext converts a FastHTTP RequestCtx to a Bifrost context, // preserving important header values for monitoring and tracing purposes. // -// The function processes two types of special headers: -// 1. Prometheus Headers (x-bf-prom-*): -// - All headers prefixed with 'x-bf-prom-' are copied to the context -// - The prefix is stripped and the remainder becomes the context key -// - Example: 'x-bf-prom-latency' becomes 'latency' in the context -// -// 2. Maxim Tracing Headers (x-bf-maxim-*): -// - Specifically handles 'x-bf-maxim-traceID' and 'x-bf-maxim-generationID' -// - These headers enable trace correlation across service boundaries -// - Values are stored using Maxim's context keys for consistency +// The function processes Prometheus headers (x-bf-prom-*): +// - All headers prefixed with 'x-bf-prom-' are copied to the context +// - The prefix is stripped and the remainder becomes the context key +// - Example: 'x-bf-prom-latency' becomes 'latency' in the context // // Parameters: // - ctx: The FastHTTP request context containing the original headers @@ -39,33 +32,20 @@ import ( // // fastCtx := &fasthttp.RequestCtx{...} // bifrostCtx := ConvertToBifrostContext(fastCtx) -// // bifrostCtx now contains any prometheus and maxim header values +// // bifrostCtx now contains any prometheus header values func ConvertToBifrostContext(ctx *fasthttp.RequestCtx) *context.Context { bifrostCtx := context.Background() - // Copy all prometheus header values to the new context + // Process all headers and extract relevant ones ctx.Request.Header.VisitAll(func(key, value []byte) { - keyStr := strings.ToLower(string(key)) - - if strings.HasPrefix(keyStr, "x-bf-prom-") { - labelName := strings.TrimPrefix(keyStr, "x-bf-prom-") - bifrostCtx = context.WithValue(bifrostCtx, tracking.PrometheusContextKey(labelName), string(value)) - } - - if strings.HasPrefix(keyStr, "x-bf-maxim-") { - labelName := strings.TrimPrefix(keyStr, "x-bf-maxim-") - - if labelName == string(maxim.GenerationIDKey) { - bifrostCtx = context.WithValue(bifrostCtx, maxim.ContextKey(labelName), string(value)) - } - - if labelName == string(maxim.TraceIDKey) { - bifrostCtx = context.WithValue(bifrostCtx, maxim.ContextKey(labelName), string(value)) - } - - if labelName == string(maxim.SessionIDKey) { - bifrostCtx = context.WithValue(bifrostCtx, maxim.ContextKey(labelName), string(value)) - } + keyStr := string(key) + valueStr := string(value) + + // Handle Prometheus headers (x-bf-prom-*) + if strings.HasPrefix(strings.ToLower(keyStr), "x-bf-prom-") { + // Remove the prefix and use the remainder as the context key + prometheusKey := strings.TrimPrefix(strings.ToLower(keyStr), "x-bf-prom-") + bifrostCtx = context.WithValue(bifrostCtx, tracking.PrometheusContextKey(prometheusKey), valueStr) } }) diff --git a/transports/bifrost-http/lib/plugins/config.go b/transports/bifrost-http/lib/plugins/config.go new file mode 100644 index 0000000000..e51a6029cf --- /dev/null +++ b/transports/bifrost-http/lib/plugins/config.go @@ -0,0 +1,30 @@ +package plugins + +import "encoding/json" + +// PluginSource defines where a plugin comes from +type PluginSource string + +const ( + PluginSourceLocal PluginSource = "local" // Local build (for development) - requires binary_path + PluginSourcePackage PluginSource = "package" // Go package from repository (for production) +) + +// PluginConfig represents the configuration for a single plugin +// Only supports two types: local builds and Go packages +type PluginConfig struct { + Name string `json:"name"` // Plugin name for identification + Source PluginSource `json:"source"` // Plugin source: "local" or "package" + + // For local source (development) + PluginPath string `json:"plugin_path,omitempty"` // Path to plugin directory (preferred for local source) + + // For package source (production) + Package string `json:"package,omitempty"` // Go module path (required for package source) + Version string `json:"version,omitempty"` // Version for package source (optional, defaults to latest) + + // Common configuration + Config json.RawMessage `json:"config,omitempty"` // Plugin-specific configuration (JSON) + EnvVars map[string]string `json:"env_vars,omitempty"` // Environment variables for plugin + Enabled bool `json:"enabled"` // Whether the plugin is enabled +} diff --git a/transports/bifrost-http/lib/plugins/manager.go b/transports/bifrost-http/lib/plugins/manager.go new file mode 100644 index 0000000000..244b1d1bbe --- /dev/null +++ b/transports/bifrost-http/lib/plugins/manager.go @@ -0,0 +1,681 @@ +package plugins + +import ( + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/maximhq/bifrost/core/schemas" +) + +// LoadPlugins loads plugins based on the simplified configuration. +// Supports two types of plugins: +// 1. Local builds (for development) - requires binary_path +// 2. Go packages (for production) - requires package path, builds automatically +// Returns a slice of loaded plugins that can be used with Bifrost. +// +// Safety Features: +// - Checks for user-created 'cmd' directories in plugins to avoid overwriting custom code +// - Checks for user-created 'plugins' directory to avoid conflicts with existing binaries +// - Skips plugin loading if conflicts are detected and logs helpful suggestions +func LoadPlugins(pluginConfigs []PluginConfig) ([]schemas.Plugin, error) { + var loadedPlugins []schemas.Plugin + + for _, config := range pluginConfigs { + if !config.Enabled { + log.Printf("Plugin %s is disabled, skipping", config.Name) + continue + } + + plugin, err := loadPlugin(config) + if err != nil { + // Check if this is a user-created cmd directory issue + if strings.Contains(err.Error(), "user-created cmd directory") { + log.Printf("error: plugin %s skipped due to user-created cmd directory conflict", config.Name) + log.Printf("suggestion: either remove the custom cmd directory or set up the plugin manually without auto-generation") + } else { + log.Printf("warning: failed to load plugin %s: %v", config.Name, err) + } + continue + } + + if plugin != nil { + // Inject logger into the plugin + if logger := getLoggerForPlugin(config.Name); logger != nil { + plugin.SetLogger(logger) + } + + log.Printf("Successfully loaded plugin: %s (source: %s)", config.Name, config.Source) + loadedPlugins = append(loadedPlugins, plugin) + } + } + + return loadedPlugins, nil +} + +// CleanupPluginDirectories cleans up auto-generated cmd directories and compiled binaries for all local plugins +// This should be called when the server shuts down to ensure a clean state +func CleanupPluginDirectories(pluginConfigs []PluginConfig) { + for _, config := range pluginConfigs { + if config.Source == PluginSourceLocal && config.PluginPath != "" { + cmdDir := filepath.Join(config.PluginPath, "cmd") + + // Only cleanup if it's auto-generated + if _, err := os.Stat(cmdDir); err == nil { + if isAutoGeneratedCmdDirectory(cmdDir) { + if err := cleanupCmdDirectory(cmdDir, config.Name); err != nil { + log.Printf("warning: failed to cleanup cmd directory for plugin %s: %v", config.Name, err) + } + } else { + log.Printf("info: skipping cleanup of user-created cmd directory for plugin %s", config.Name) + } + } + + // Also cleanup the compiled binary + pluginsDir := "./plugins" + binaryPath := filepath.Join(pluginsDir, config.Name+"-plugin") + if _, err := os.Stat(binaryPath); err == nil { + if err := os.Remove(binaryPath); err != nil { + log.Printf("warning: failed to cleanup plugin binary %s: %v", binaryPath, err) + } else { + log.Printf("Cleaned up plugin binary: %s", binaryPath) + } + } + } + } + + // Remove the plugins directory if it only contains the marker file or is empty + pluginsDir := "./plugins" + if entries, err := os.ReadDir(pluginsDir); err == nil { + // Check if directory only contains the marker file + hasOnlyMarker := len(entries) == 1 && entries[0].Name() == ".bifrost-generated" + isEmpty := len(entries) == 0 + + if hasOnlyMarker || isEmpty { + // Remove the entire directory + if err := os.RemoveAll(pluginsDir); err != nil { + log.Printf("warning: failed to remove plugins directory: %v", err) + } else { + log.Printf("Removed plugins directory") + } + } + } +} + +// loadPlugin loads a single plugin based on its simplified configuration +func loadPlugin(config PluginConfig) (schemas.Plugin, error) { + switch config.Source { + case PluginSourceLocal: + return loadLocalPlugin(config) + case PluginSourcePackage: + return loadPackagePlugin(config) + default: + return nil, fmt.Errorf("unsupported plugin source: %s. Only 'local' and 'package' sources are supported", config.Source) + } +} + +// loadLocalPlugin loads a plugin from a local directory (for development) +func loadLocalPlugin(config PluginConfig) (schemas.Plugin, error) { + if config.PluginPath == "" { + return nil, fmt.Errorf("plugin_path is required for local source plugins") + } + + // Build the plugin from the local directory + builtBinaryPath, err := buildLocalPlugin(config) + if err != nil { + return nil, fmt.Errorf("failed to build local plugin from %s: %w", config.PluginPath, err) + } + + // Set environment variables for the plugin + if err := setPluginEnvironmentVariables(config); err != nil { + return nil, err + } + + // Load the built plugin + plugin, err := LoadPlugin(builtBinaryPath) + if err != nil { + return nil, fmt.Errorf("failed to load local plugin from %s: %w", config.PluginPath, err) + } + + return plugin, nil +} + +// buildLocalPlugin builds a plugin from a local directory +func buildLocalPlugin(config PluginConfig) (string, error) { + // Validate plugin directory exists + if _, err := os.Stat(config.PluginPath); os.IsNotExist(err) { + return "", fmt.Errorf("plugin directory not found at path: %s", config.PluginPath) + } + + // Check if the plugin implements the required interface + if err := validateLocalPluginInterface(config.PluginPath); err != nil { + return "", fmt.Errorf("plugin validation failed: %w", err) + } + + // Auto-generate the RPC wrapper + if err := generateLocalRPCWrapper(config); err != nil { + return "", fmt.Errorf("failed to generate RPC wrapper: %w", err) + } + + // Check and prepare plugins directory + pluginsDir := "./plugins" + if err := ensurePluginsDirectory(pluginsDir, config.Name); err != nil { + return "", fmt.Errorf("failed to prepare plugins directory: %w", err) + } + + // Get absolute path for output since we'll be running from cmdDir + outputPath := filepath.Join(pluginsDir, config.Name+"-plugin") + absOutputPath, err := filepath.Abs(outputPath) + if err != nil { + return "", fmt.Errorf("failed to get absolute path for output: %w", err) + } + + cmdDir := filepath.Join(config.PluginPath, "cmd") + + // Build using go build + cmd := exec.Command("go", "build", "-o", absOutputPath, ".") + cmd.Dir = cmdDir + cmd.Env = append(os.Environ(), "CGO_ENABLED=1") + + if output, err := cmd.CombinedOutput(); err != nil { + return "", fmt.Errorf("failed to build local plugin: %w\nOutput: %s", err, string(output)) + } + + // Make executable + if err := os.Chmod(absOutputPath, 0755); err != nil { + return "", fmt.Errorf("failed to make plugin executable: %w", err) + } + + log.Printf("Successfully built local plugin %s from %s", config.Name, config.PluginPath) + return absOutputPath, nil +} + +// validateLocalPluginInterface checks if a local plugin implements the required NewPlugin function +func validateLocalPluginInterface(pluginPath string) error { + // Find all Go files in the plugin directory + files, err := filepath.Glob(filepath.Join(pluginPath, "*.go")) + if err != nil { + return fmt.Errorf("failed to find Go files: %w", err) + } + + if len(files) == 0 { + return fmt.Errorf("no Go files found in plugin directory") + } + + // Parse files to check for NewPlugin function (simplified check) + for _, file := range files { + if strings.HasSuffix(file, "_test.go") { + continue // Skip test files + } + + content, err := os.ReadFile(file) + if err != nil { + continue + } + + // Simple text search for NewPlugin function + if strings.Contains(string(content), "func NewPlugin(") { + return nil + } + } + + return fmt.Errorf("plugin must implement 'func NewPlugin(json.RawMessage) (schemas.Plugin, error)'") +} + +// generateLocalRPCWrapper creates the cmd/main.go file for a local plugin +func generateLocalRPCWrapper(config PluginConfig) error { + cmdDir := filepath.Join(config.PluginPath, "cmd") + + // First check if cmd directory already exists and is user-created + if _, err := os.Stat(cmdDir); err == nil { + if !isAutoGeneratedCmdDirectory(cmdDir) { + log.Printf("warning: plugin %s has a user-created cmd directory - will not overwrite user code", config.Name) + log.Printf("suggestion: remove the custom cmd directory or set up the plugin manually without auto-generation") + return fmt.Errorf("plugin %s has user-created cmd directory - cannot auto-generate RPC wrapper", config.Name) + } + } + + // Clean up any existing auto-generated cmd directory to ensure fresh state + if err := cleanupCmdDirectory(cmdDir, config.Name); err != nil { + return fmt.Errorf("failed to cleanup cmd directory: %w", err) + } + + // Create fresh cmd directory + if err := os.MkdirAll(cmdDir, 0755); err != nil { + return fmt.Errorf("failed to create cmd directory: %w", err) + } + + // Get the package name from the plugin directory + packageName, err := parseLocalPackageInfo(config.PluginPath) + if err != nil { + return fmt.Errorf("failed to determine package name: %w", err) + } + + // Generate module-aware import path + importPath := fmt.Sprintf("github.com/maximhq/bifrost/plugins/%s", packageName) + + // Convert config to JSON string for embedding + configJSON := "{}" + if len(config.Config) > 0 { + configJSON = string(config.Config) + } + + // Template for local plugin RPC wrapper - using existing RPC implementation + template := `// Auto-generated RPC wrapper for plugin: %s +// This file is generated by Bifrost plugin-loader +package main + +import ( + "encoding/json" + "log" + + "github.com/maximhq/bifrost/transports/bifrost-http/lib/plugins" + pluginpkg "%s" +) + +func main() { + // Create plugin instance with actual configuration + pluginInstance, err := pluginpkg.NewPlugin(json.RawMessage(%s)) + if err != nil { + log.Fatalf("Failed to create plugin: %%v", err) + } + + // Serve using the existing RPC implementation + plugins.ServePlugin(pluginInstance) +} +` + + // Generate the RPC wrapper content + content := fmt.Sprintf(template, config.Name, importPath, fmt.Sprintf("`%s`", configJSON)) + + // Write to cmd/main.go + mainGoPath := filepath.Join(cmdDir, "main.go") + if err := os.WriteFile(mainGoPath, []byte(content), 0644); err != nil { + return fmt.Errorf("failed to write main.go: %w", err) + } + + // Create go.mod file for the cmd directory + if err := generateCmdGoMod(config, cmdDir, packageName); err != nil { + return fmt.Errorf("failed to generate go.mod for cmd: %w", err) + } + + return nil +} + +// generateCmdGoMod creates a go.mod file for the cmd directory +func generateCmdGoMod(config PluginConfig, cmdDir, packageName string) error { + goModTemplate := `module github.com/maximhq/bifrost/plugins/%s/cmd + +go 1.21 + +require ( + github.com/maximhq/bifrost/plugins/%s v0.0.0 + github.com/hashicorp/go-plugin v1.6.3 + github.com/maximhq/bifrost/core v0.0.0 + github.com/maximhq/bifrost/transports v0.0.0 +) + +replace github.com/maximhq/bifrost/plugins/%s => ../ +replace github.com/maximhq/bifrost/core => ../../../core +replace github.com/maximhq/bifrost/transports => ../../../transports +` + + content := fmt.Sprintf(goModTemplate, packageName, packageName, packageName) + + goModPath := filepath.Join(cmdDir, "go.mod") + if err := os.WriteFile(goModPath, []byte(content), 0644); err != nil { + return fmt.Errorf("failed to write go.mod: %w", err) + } + + // Run go mod tidy to resolve dependencies + cmd := exec.Command("go", "mod", "tidy") + cmd.Dir = cmdDir + cmd.Env = os.Environ() + + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to run go mod tidy in cmd directory: %w\nOutput: %s", err, string(output)) + } + + return nil +} + +// cleanupCmdDirectory removes the cmd directory if it exists +// This should only be called after confirming the directory is auto-generated +func cleanupCmdDirectory(cmdDir, pluginName string) error { + if _, err := os.Stat(cmdDir); err == nil { + log.Printf("Cleaning up existing auto-generated cmd directory for plugin %s", pluginName) + if err := os.RemoveAll(cmdDir); err != nil { + return fmt.Errorf("failed to remove existing cmd directory: %w", err) + } + } + return nil +} + +// isAutoGeneratedCmdDirectory checks if a cmd directory was auto-generated by looking for our markers +func isAutoGeneratedCmdDirectory(cmdDir string) bool { + mainGoPath := filepath.Join(cmdDir, "main.go") + + // Check if main.go exists and contains our auto-generated marker + if content, err := os.ReadFile(mainGoPath); err == nil { + contentStr := string(content) + // Look for our specific auto-generated markers + return strings.Contains(contentStr, "// Auto-generated RPC wrapper for plugin:") && + strings.Contains(contentStr, "// This file is generated by Bifrost plugin-loader") + } + + // If we can't read main.go or it doesn't have our markers, assume it's user-created + return false +} + +// ensurePluginsDirectory checks if plugins directory exists and handles user-created vs auto-generated scenarios +func ensurePluginsDirectory(pluginsDir, pluginName string) error { + // Check if plugins directory already exists + if _, err := os.Stat(pluginsDir); err == nil { + if !isAutoGeneratedPluginsDirectory(pluginsDir) { + log.Printf("warning: plugin %s skipped - user-created plugins directory exists", pluginName) + log.Printf("suggestion: remove the custom plugins directory or manage plugin binaries manually") + return fmt.Errorf("plugin %s skipped due to user-created plugins directory conflict", pluginName) + } + } + + // Create or ensure plugins directory exists + if err := os.MkdirAll(pluginsDir, 0755); err != nil { + return fmt.Errorf("failed to create plugins directory: %w", err) + } + + // Create marker file to indicate this is auto-generated + if err := createPluginsDirectoryMarker(pluginsDir); err != nil { + log.Printf("warning: failed to create plugins directory marker: %v", err) + // Don't fail here - this is just for tracking purposes + } + + return nil +} + +// isAutoGeneratedPluginsDirectory checks if a plugins directory was auto-generated by Bifrost +func isAutoGeneratedPluginsDirectory(pluginsDir string) bool { + markerPath := filepath.Join(pluginsDir, ".bifrost-generated") + + // Check if marker file exists + if _, err := os.Stat(markerPath); err == nil { + return true + } + + // If no marker file and directory is empty, consider it safe to use + if entries, err := os.ReadDir(pluginsDir); err == nil && len(entries) == 0 { + return true + } + + // Check if all files in directory are plugin binaries (end with -plugin) + if entries, err := os.ReadDir(pluginsDir); err == nil { + for _, entry := range entries { + if entry.IsDir() { + return false // User-created if it contains directories + } + if !strings.HasSuffix(entry.Name(), "-plugin") && entry.Name() != ".bifrost-generated" { + return false // User-created if it contains non-plugin files + } + } + return true // All files are plugin binaries, likely auto-generated + } + + // If we can't read the directory, assume it's user-created to be safe + return false +} + +// createPluginsDirectoryMarker creates a marker file to indicate the directory is auto-generated +func createPluginsDirectoryMarker(pluginsDir string) error { + markerPath := filepath.Join(pluginsDir, ".bifrost-generated") + markerContent := `# This file indicates that this plugins directory was auto-generated by Bifrost +# Do not remove this file unless you want to manage plugin binaries manually +# Generated by Bifrost plugin-loader system +` + return os.WriteFile(markerPath, []byte(markerContent), 0644) +} + +// parseLocalPackageInfo extracts the package name from a local Go package directory +func parseLocalPackageInfo(pluginPath string) (string, error) { + // Find all Go files in the plugin directory + files, err := filepath.Glob(filepath.Join(pluginPath, "*.go")) + if err != nil { + return "", err + } + + if len(files) == 0 { + return "", fmt.Errorf("no Go files found in plugin directory") + } + + // Read the first non-test Go file to get the package name + for _, file := range files { + if strings.HasSuffix(file, "_test.go") { + continue + } + + content, err := os.ReadFile(file) + if err != nil { + continue + } + + lines := strings.Split(string(content), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "package ") { + parts := strings.Fields(line) + if len(parts) >= 2 { + return parts[1], nil + } + } + } + } + + return "", fmt.Errorf("could not determine package name") +} + +// loadPackagePlugin loads a plugin from a Go package (for production) +func loadPackagePlugin(config PluginConfig) (schemas.Plugin, error) { + if config.Package == "" { + return nil, fmt.Errorf("package is required for package source plugins") + } + + // Build the plugin from the Go package + builtBinaryPath, err := buildPluginFromPackage(config) + if err != nil { + return nil, fmt.Errorf("failed to build plugin from package %s: %w", config.Package, err) + } + + // Set environment variables for the plugin + if err := setPluginEnvironmentVariables(config); err != nil { + return nil, err + } + + // Load the built plugin + plugin, err := LoadPlugin(builtBinaryPath) + if err != nil { + return nil, fmt.Errorf("failed to load package plugin %s: %w", config.Package, err) + } + + return plugin, nil +} + +// buildPluginFromPackage builds a plugin from a Go package and returns the binary path +func buildPluginFromPackage(config PluginConfig) (string, error) { + // Check and prepare plugins directory + pluginsDir := "./plugins" + if err := ensurePluginsDirectory(pluginsDir, config.Name); err != nil { + return "", fmt.Errorf("failed to prepare plugins directory: %w", err) + } + + // Create a temporary directory for building + tempDir, err := os.MkdirTemp("", "plugin-build-"+config.Name) + if err != nil { + return "", fmt.Errorf("failed to create temp directory: %w", err) + } + defer os.RemoveAll(tempDir) + + // Generate main.go for the plugin + mainContent := generatePackagePluginMain(config) + mainPath := filepath.Join(tempDir, "main.go") + if err := os.WriteFile(mainPath, []byte(mainContent), 0644); err != nil { + return "", fmt.Errorf("failed to write main.go: %w", err) + } + + // Generate go.mod for the plugin + goModContent := generatePackagePluginGoMod(config) + goModPath := filepath.Join(tempDir, "go.mod") + if err := os.WriteFile(goModPath, []byte(goModContent), 0644); err != nil { + return "", fmt.Errorf("failed to write go.mod: %w", err) + } + + // Final binary path + finalBinaryPath := filepath.Join(pluginsDir, config.Name+"-plugin") + absOutputPath, err := filepath.Abs(finalBinaryPath) + if err != nil { + return "", fmt.Errorf("failed to get absolute path for output: %w", err) + } + + // Build the plugin + cmd := exec.Command("go", "build", "-o", absOutputPath, ".") + cmd.Dir = tempDir + cmd.Env = append(os.Environ(), "CGO_ENABLED=1") + + if output, err := cmd.CombinedOutput(); err != nil { + return "", fmt.Errorf("failed to build package plugin: %w\nOutput: %s", err, string(output)) + } + + // Make executable + if err := os.Chmod(absOutputPath, 0755); err != nil { + return "", fmt.Errorf("failed to make plugin executable: %w", err) + } + + log.Printf("Successfully built plugin %s from package %s", config.Name, config.Package) + return absOutputPath, nil +} + +// generatePackagePluginMain generates the main.go content for a package plugin +func generatePackagePluginMain(config PluginConfig) string { + return fmt.Sprintf(`package main + +import ( + "encoding/json" + "log" + + "github.com/maximhq/bifrost/transports/bifrost-http/lib/plugins" + pluginpkg "%s" +) + +func main() { + // Create plugin instance + pluginInstance, err := pluginpkg.NewPlugin(json.RawMessage("{}")) + if err != nil { + log.Fatalf("Failed to create plugin: %%v", err) + } + + // Serve using the existing RPC implementation + plugins.ServePlugin(pluginInstance) +} +`, config.Package) +} + +// generatePackagePluginGoMod generates the go.mod content for a package plugin +func generatePackagePluginGoMod(config PluginConfig) string { + return fmt.Sprintf(`module plugin-%s + +go 1.21 + +require ( + github.com/hashicorp/go-plugin v1.6.3 + github.com/maximhq/bifrost/core v1.1.4 + github.com/maximhq/bifrost/transports v0.0.0 + %s v0.0.0 +) + +replace %s => %s +replace github.com/maximhq/bifrost/transports => ../../transports + +require ( + github.com/fatih/color v1.17.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/oklog/run v1.1.0 // indirect + golang.org/x/net v0.29.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/grpc v1.66.2 // indirect + google.golang.org/protobuf v1.34.2 // indirect +) +`, config.Name, config.Package, config.Package, determinePackagePath(config.Package)) +} + +// determinePackagePath determines the local path for a package +func determinePackagePath(packageName string) string { + // For local development, assume packages are in the same workspace + // This is a simple heuristic - in production you might want more sophisticated logic + if strings.HasPrefix(packageName, "github.com/maximhq/bifrost/") { + // Handle local bifrost plugins + return "../../../" + strings.TrimPrefix(packageName, "github.com/maximhq/bifrost/") + } + // For external packages, don't use replace directive - let go mod download them + return packageName +} + +// setPluginEnvironmentVariables sets environment variables for the plugin +func setPluginEnvironmentVariables(config PluginConfig) error { + for key, value := range config.EnvVars { + // Handle environment variable placeholders + if strings.HasPrefix(value, "env.") { + envKey := strings.TrimPrefix(value, "env.") + envValue := os.Getenv(envKey) + if envValue == "" { + log.Printf("warning: environment variable %s is not set for plugin %s", envKey, config.Name) + } + value = envValue + } + + if err := os.Setenv(key, value); err != nil { + return fmt.Errorf("failed to set environment variable %s for plugin %s: %w", key, config.Name, err) + } + } + return nil +} + +// getLoggerForPlugin returns a logger instance for the given plugin +// This can be customized to provide different loggers for different plugins +func getLoggerForPlugin(pluginName string) schemas.Logger { + // For now, return nil - plugins should handle nil logger gracefully + // In the future, this could return a custom logger per plugin + return nil +} + +// ValidatePluginConfig validates that a plugin configuration is correct +func ValidatePluginConfig(config PluginConfig) error { + if config.Name == "" { + return fmt.Errorf("plugin name is required") + } + + switch config.Source { + case PluginSourceLocal: + if config.PluginPath == "" { + return fmt.Errorf("plugin_path is required for local source plugins") + } + case PluginSourcePackage: + if config.Package == "" { + return fmt.Errorf("package is required for package source plugins") + } + // Validate package path format + if !strings.Contains(config.Package, "/") { + return fmt.Errorf("package path must be a valid Go module path (e.g., github.com/user/plugin)") + } + default: + return fmt.Errorf("unsupported plugin source: %s. Only 'local' and 'package' sources are supported", config.Source) + } + + return nil +} diff --git a/transports/bifrost-http/lib/plugins/rpc.go b/transports/bifrost-http/lib/plugins/rpc.go new file mode 100644 index 0000000000..c3f94f3620 --- /dev/null +++ b/transports/bifrost-http/lib/plugins/rpc.go @@ -0,0 +1,334 @@ +package plugins + +import ( + "context" + "encoding/gob" + "encoding/json" + "fmt" + "net/rpc" + + "github.com/hashicorp/go-plugin" + "github.com/maximhq/bifrost/core/schemas" +) + +func init() { + // Register only generic types with gob for RPC serialization + gob.Register(map[string]interface{}{}) + gob.Register([]interface{}{}) +} + +// Convert schema types to map[string]interface{} for gob serialization +func schemaToMap(v interface{}) (map[string]interface{}, error) { + if v == nil { + return nil, nil + } + + // Convert to JSON first, then to map + jsonBytes, err := json.Marshal(v) + if err != nil { + return nil, err + } + + var result map[string]interface{} + err = json.Unmarshal(jsonBytes, &result) + return result, err +} + +// Convert map[string]interface{} back to schema type +func mapToSchema(m map[string]interface{}, target interface{}) error { + if m == nil { + return nil + } + + // Convert to JSON first, then to target type + jsonBytes, err := json.Marshal(m) + if err != nil { + return err + } + + return json.Unmarshal(jsonBytes, target) +} + +// PluginRPCClient is an implementation of schemas.Plugin that talks over RPC. +type PluginRPCClient struct { + client *rpc.Client +} + +// GetName calls the GetName method over RPC +func (c *PluginRPCClient) GetName() string { + fmt.Printf("[DEBUG] RPC Client GetName called\n") + var resp string + err := c.client.Call("Plugin.GetName", new(interface{}), &resp) + fmt.Printf("[DEBUG] RPC Client GetName returned: resp='%s', err=%v\n", resp, err) + if err != nil { + return "unknown-plugin" + } + return resp +} + +// SetLogger is a no-op for RPC plugins since logger can't be serialized +func (c *PluginRPCClient) SetLogger(logger schemas.Logger) { + // Logger injection is handled at the host level +} + +// PreHook calls the PreHook method over RPC +func (c *PluginRPCClient) PreHook(ctx *context.Context, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.PluginShortCircuit, error) { + // Convert request to map + reqMap, err := schemaToMap(req) + if err != nil { + return req, nil, fmt.Errorf("failed to serialize request: %w", err) + } + + args := map[string]interface{}{ + "Request": reqMap, + } + var resp map[string]interface{} + + err = c.client.Call("Plugin.PreHook", args, &resp) + if err != nil { + return req, nil, err + } + + // Extract response values + modifiedReq := req + if resp["Request"] != nil { + if reqMap, ok := resp["Request"].(map[string]interface{}); ok { + var newReq schemas.BifrostRequest + if err := mapToSchema(reqMap, &newReq); err == nil { + modifiedReq = &newReq + } + } + } + + var shortCircuit *schemas.PluginShortCircuit + if resp["ShortCircuit"] != nil { + if scMap, ok := resp["ShortCircuit"].(map[string]interface{}); ok { + var sc schemas.PluginShortCircuit + if err := mapToSchema(scMap, &sc); err == nil { + shortCircuit = &sc + } + } + } + + var hookErr error + if resp["Error"] != nil { + if errStr, ok := resp["Error"].(string); ok && errStr != "" { + hookErr = fmt.Errorf("%s", errStr) + } + } + + return modifiedReq, shortCircuit, hookErr +} + +// PostHook calls the PostHook method over RPC +func (c *PluginRPCClient) PostHook(ctx *context.Context, result *schemas.BifrostResponse, err *schemas.BifrostError) (*schemas.BifrostResponse, *schemas.BifrostError, error) { + // Convert to maps + resultMap, _ := schemaToMap(result) + errMap, _ := schemaToMap(err) + + args := map[string]interface{}{ + "Result": resultMap, + "Error": errMap, + } + var resp map[string]interface{} + rpcErr := c.client.Call("Plugin.PostHook", args, &resp) + if rpcErr != nil { + return result, err, rpcErr + } + + // Extract response values + modifiedResult := result + if resp["Result"] != nil { + if resultMap, ok := resp["Result"].(map[string]interface{}); ok { + var newResult schemas.BifrostResponse + if mapErr := mapToSchema(resultMap, &newResult); mapErr == nil { + modifiedResult = &newResult + } + } + } + + modifiedErr := err + if resp["Error"] != nil { + if errMap, ok := resp["Error"].(map[string]interface{}); ok { + var newErr schemas.BifrostError + if mapErr := mapToSchema(errMap, &newErr); mapErr == nil { + modifiedErr = &newErr + } + } + } + + var hookErr error + if resp["HookError"] != nil { + if errStr, ok := resp["HookError"].(string); ok && errStr != "" { + hookErr = fmt.Errorf("%s", errStr) + } + } + + return modifiedResult, modifiedErr, hookErr +} + +// Cleanup calls the Cleanup method over RPC +func (c *PluginRPCClient) Cleanup() error { + var resp string + err := c.client.Call("Plugin.Cleanup", new(interface{}), &resp) + if err != nil { + return err + } + if resp != "" { + return fmt.Errorf("%s", resp) + } + return nil +} + +// PluginRPCServer is the RPC server that PluginRPCClient talks to, conforming to +// the requirements of net/rpc +type PluginRPCServer struct { + // This is the real implementation + Impl schemas.Plugin +} + +func (s *PluginRPCServer) GetName(args interface{}, resp *string) error { + *resp = s.Impl.GetName() + return nil +} + +func (s *PluginRPCServer) PreHook(args map[string]interface{}, resp *map[string]interface{}) error { + // Extract request from args + reqMap, ok := args["Request"].(map[string]interface{}) + if !ok { + *resp = map[string]interface{}{"Error": "Invalid request type"} + return nil + } + + // Convert map to BifrostRequest + var req schemas.BifrostRequest + if err := mapToSchema(reqMap, &req); err != nil { + *resp = map[string]interface{}{"Error": "Failed to deserialize request: " + err.Error()} + return nil + } + + // Create a dummy context since we can't serialize it + ctx := context.Background() + + modifiedReq, shortCircuit, err := s.Impl.PreHook(&ctx, &req) + + // Build response - convert back to maps + response := make(map[string]interface{}) + + if modifiedReq != nil { + if reqMap, mapErr := schemaToMap(modifiedReq); mapErr == nil { + response["Request"] = reqMap + } + } + + // Always set ShortCircuit field, even if nil + if shortCircuit != nil { + if scMap, mapErr := schemaToMap(shortCircuit); mapErr == nil { + response["ShortCircuit"] = scMap + } else { + response["ShortCircuit"] = nil + } + } else { + response["ShortCircuit"] = nil + } + + if err != nil { + response["Error"] = err.Error() + } else { + response["Error"] = "" + } + + *resp = response + return nil +} + +func (s *PluginRPCServer) PostHook(args map[string]interface{}, resp *map[string]interface{}) error { + // Extract arguments and convert from maps + var result *schemas.BifrostResponse + var bifrostErr *schemas.BifrostError + + if args["Result"] != nil { + if resultMap, ok := args["Result"].(map[string]interface{}); ok { + var res schemas.BifrostResponse + if err := mapToSchema(resultMap, &res); err == nil { + result = &res + } + } + } + + if args["Error"] != nil { + if errMap, ok := args["Error"].(map[string]interface{}); ok { + var bErr schemas.BifrostError + if err := mapToSchema(errMap, &bErr); err == nil { + bifrostErr = &bErr + } + } + } + + // Create a dummy context since we can't serialize it + ctx := context.Background() + + modifiedResult, modifiedErr, hookErr := s.Impl.PostHook(&ctx, result, bifrostErr) + + // Build response - convert back to maps + response := make(map[string]interface{}) + + if modifiedResult != nil { + if resultMap, mapErr := schemaToMap(modifiedResult); mapErr == nil { + response["Result"] = resultMap + } + } + + if modifiedErr != nil { + if errMap, mapErr := schemaToMap(modifiedErr); mapErr == nil { + response["Error"] = errMap + } + } + + if hookErr != nil { + response["HookError"] = hookErr.Error() + } else { + response["HookError"] = "" + } + + *resp = response + return nil +} + +func (s *PluginRPCServer) Cleanup(args interface{}, resp *string) error { + err := s.Impl.Cleanup() + if err != nil { + *resp = err.Error() + } else { + *resp = "" + } + return nil +} + +// This is the implementation of plugin.Plugin so we can serve/consume this +type PluginPlugin struct { + // Impl Injection + Impl schemas.Plugin +} + +func (p *PluginPlugin) Server(*plugin.MuxBroker) (interface{}, error) { + return &PluginRPCServer{Impl: p.Impl}, nil +} + +func (PluginPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { + return &PluginRPCClient{client: c}, nil +} + +// ServePlugin serves a plugin implementation over RPC +func ServePlugin(impl schemas.Plugin) { + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: plugin.HandshakeConfig{ + ProtocolVersion: 1, + MagicCookieKey: "BIFROST_PLUGIN", + MagicCookieValue: "bifrost", + }, + Plugins: map[string]plugin.Plugin{ + "plugin": &PluginPlugin{Impl: impl}, + }, + }) +} diff --git a/transports/bifrost-http/lib/plugins/runtime.go b/transports/bifrost-http/lib/plugins/runtime.go new file mode 100644 index 0000000000..594ff48525 --- /dev/null +++ b/transports/bifrost-http/lib/plugins/runtime.go @@ -0,0 +1,97 @@ +package plugins + +import ( + "fmt" + "os" + "os/exec" + + "github.com/hashicorp/go-plugin" + "github.com/maximhq/bifrost/core/schemas" +) + +// HandshakeConfig for plugin communication +var HandshakeConfig = plugin.HandshakeConfig{ + ProtocolVersion: 1, + MagicCookieKey: "BIFROST_PLUGIN", + MagicCookieValue: "bifrost", +} + +// PluginMap is the map of plugins we can dispense +var PluginMap = map[string]plugin.Plugin{ + "plugin": &PluginPlugin{}, +} + +// LoadPlugin loads a plugin from the given path and returns the plugin instance +func LoadPlugin(pluginPath string) (schemas.Plugin, error) { + // Check if plugin binary exists + if _, err := os.Stat(pluginPath); os.IsNotExist(err) { + return nil, fmt.Errorf("plugin binary not found at %s", pluginPath) + } + + // Make sure plugin is executable + if err := os.Chmod(pluginPath, 0755); err != nil { + return nil, fmt.Errorf("failed to make plugin executable: %v", err) + } + + // Create the plugin client + client := plugin.NewClient(&plugin.ClientConfig{ + HandshakeConfig: HandshakeConfig, + Plugins: PluginMap, + Cmd: exec.Command(pluginPath), + AllowedProtocols: []plugin.Protocol{ + plugin.ProtocolNetRPC, + }, + }) + + // Connect via RPC + rpcClient, err := client.Client() + if err != nil { + client.Kill() + return nil, fmt.Errorf("failed to connect to plugin: %v", err) + } + + // Request the plugin + raw, err := rpcClient.Dispense("plugin") + if err != nil { + client.Kill() + return nil, fmt.Errorf("failed to dispense plugin: %v", err) + } + + // Type assert to our plugin interface + pluginInstance, ok := raw.(schemas.Plugin) + if !ok { + client.Kill() + return nil, fmt.Errorf("plugin does not implement Plugin interface") + } + + return pluginInstance, nil +} + +// IsPluginBinary checks if a binary is a Bifrost plugin by attempting a quick handshake +func IsPluginBinary(path string) bool { + // Check if file exists and is executable + if info, err := os.Stat(path); err != nil || info.IsDir() { + return false + } + + // Try a quick plugin client connection + client := plugin.NewClient(&plugin.ClientConfig{ + HandshakeConfig: HandshakeConfig, + Plugins: PluginMap, + Cmd: exec.Command(path), + AllowedProtocols: []plugin.Protocol{ + plugin.ProtocolNetRPC, + }, + }) + defer client.Kill() + + // Try to connect - if it fails, it's not a plugin + rpcClient, err := client.Client() + if err != nil { + return false + } + + // Try to dispense - if it fails, it's not our plugin + _, err = rpcClient.Dispense("plugin") + return err == nil +} diff --git a/transports/bifrost-http/main.go b/transports/bifrost-http/main.go index 8941203767..ca21a7d273 100644 --- a/transports/bifrost-http/main.go +++ b/transports/bifrost-http/main.go @@ -39,18 +39,20 @@ import ( "fmt" "log" "os" + "os/signal" "strings" + "syscall" "github.com/fasthttp/router" bifrost "github.com/maximhq/bifrost/core" schemas "github.com/maximhq/bifrost/core/schemas" - "github.com/maximhq/bifrost/plugins/maxim" "github.com/maximhq/bifrost/transports/bifrost-http/integrations" "github.com/maximhq/bifrost/transports/bifrost-http/integrations/anthropic" "github.com/maximhq/bifrost/transports/bifrost-http/integrations/genai" "github.com/maximhq/bifrost/transports/bifrost-http/integrations/litellm" "github.com/maximhq/bifrost/transports/bifrost-http/integrations/openai" "github.com/maximhq/bifrost/transports/bifrost-http/lib" + "github.com/maximhq/bifrost/transports/bifrost-http/lib/plugins" "github.com/maximhq/bifrost/transports/bifrost-http/tracking" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" @@ -65,7 +67,6 @@ var ( dropExcessRequests bool // Drop excess requests port string // Port to run the server on configPath string // Path to the config file - pluginsToLoad []string // Path to the plugins prometheusLabels []string // Labels to add to Prometheus metrics (optional) ) @@ -76,19 +77,15 @@ var ( // - config: Path to config file (required) // - drop-excess-requests: Whether to drop excess requests func init() { - pluginString := "" var prometheusLabelsString string flag.IntVar(&initialPoolSize, "pool-size", 300, "Initial pool size for Bifrost") flag.StringVar(&port, "port", "8080", "Port to run the server on") flag.StringVar(&configPath, "config", "", "Path to the config file") flag.BoolVar(&dropExcessRequests, "drop-excess-requests", false, "Drop excess requests") - flag.StringVar(&pluginString, "plugins", "", "Comma separated list of plugins to load") flag.StringVar(&prometheusLabelsString, "prometheus-labels", "", "Labels to add to Prometheus metrics") flag.Parse() - pluginsToLoad = strings.Split(pluginString, ",") - if configPath == "" { log.Fatalf("config path is required") } @@ -157,28 +154,14 @@ func main() { loadedPlugins := []schemas.Plugin{} - for _, plugin := range pluginsToLoad { - switch strings.ToLower(plugin) { - case "maxim": - if os.Getenv("MAXIM_LOG_REPO_ID") == "" { - log.Println("warning: maxim log repo id is required to initialize maxim plugin") - continue - } - if os.Getenv("MAXIM_API_KEY") == "" { - log.Println("warning: maxim api key is required in environment variable MAXIM_API_KEY to initialize maxim plugin") - continue - } - - maximPlugin, err := maxim.NewMaximLoggerPlugin(os.Getenv("MAXIM_API_KEY"), os.Getenv("MAXIM_LOG_REPO_ID")) - if err != nil { - log.Printf("warning: failed to initialize maxim plugin: %v", err) - continue - } - - loadedPlugins = append(loadedPlugins, maximPlugin) - } + // Load plugins from configuration + rpcPlugins, err := plugins.LoadPlugins(config.Plugins) + if err != nil { + log.Printf("warning: failed to load plugins: %v", err) } + loadedPlugins = append(loadedPlugins, rpcPlugins...) + // Always add Prometheus plugin promPlugin := tracking.NewPrometheusPlugin() loadedPlugins = append(loadedPlugins, promPlugin) @@ -238,12 +221,40 @@ func main() { }, } + // Set up graceful shutdown + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + + go func() { + <-quit + log.Println("Shutting down server...") + + // Cleanup plugins first + for _, plugin := range loadedPlugins { + if err := plugin.Cleanup(); err != nil { + log.Printf("warning: failed to cleanup plugin %s: %v", plugin.GetName(), err) + } + } + + // Cleanup auto-generated plugin directories + plugins.CleanupPluginDirectories(config.Plugins) + + // Cleanup bifrost client + client.Cleanup() + + // Shutdown server + if err := server.Shutdown(); err != nil { + log.Printf("warning: failed to shutdown server gracefully: %v", err) + } + + log.Println("Server shutdown complete") + os.Exit(0) + }() + log.Println("Started Bifrost HTTP server on port", port) if err := server.ListenAndServe(fmt.Sprintf(":%s", port)); err != nil { log.Fatalf("failed to start server: %v", err) } - - client.Cleanup() } // handleCompletion processes both text and chat completion requests. diff --git a/transports/bifrost-http/tracking/plugin.go b/transports/bifrost-http/tracking/plugin.go index 417d4f5cf6..9197117d85 100644 --- a/transports/bifrost-http/tracking/plugin.go +++ b/transports/bifrost-http/tracking/plugin.go @@ -48,7 +48,7 @@ func (p *PrometheusPlugin) GetName() string { // PreHook records the start time of the request in the context. // This time is used later in PostHook to calculate request duration. -func (p *PrometheusPlugin) PreHook(ctx *context.Context, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.BifrostResponse, error) { +func (p *PrometheusPlugin) PreHook(ctx *context.Context, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.PluginShortCircuit, error) { *ctx = context.WithValue(*ctx, startTimeKey, time.Now()) if req.Input.ChatCompletionInput != nil { @@ -109,3 +109,7 @@ func (p *PrometheusPlugin) PostHook(ctx *context.Context, result *schemas.Bifros func (p *PrometheusPlugin) Cleanup() error { return nil } + +func (p *PrometheusPlugin) SetLogger(logger schemas.Logger) { + // no-op +} diff --git a/transports/config.example.json b/transports/config.example.json index ffc7152303..12e9b61c98 100644 --- a/transports/config.example.json +++ b/transports/config.example.json @@ -1,19 +1,10 @@ { "providers": { - "openai": { + "openAI": { "keys": [ { "value": "env.OPENAI_API_KEY", - "models": [ - "gpt-3.5-turbo", - "gpt-3.5-turbo-preview", - "gpt-4", - "gpt-4o", - "gpt-4o-mini", - "gpt-4-turbo", - "gpt-4-turbo-preview", - "gpt-4-vision-preview" - ], + "models": ["gpt-4o-mini", "gpt-4-turbo"], "weight": 1.0 } ], @@ -36,14 +27,7 @@ "keys": [ { "value": "env.ANTHROPIC_API_KEY", - "models": [ - "claude-2.1", - "claude-3-sonnet-20240229", - "claude-3-haiku-20240307", - "claude-3-opus-20240229", - "claude-3-5-sonnet-20240620", - "claude-3-7-sonnet-20250219" - ], + "models": ["claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022"], "weight": 1.0 } ], @@ -147,14 +131,73 @@ "mcp": { "client_configs": [ { - "name": "your-mcp-server-name", - "connection_type": "stdio", - "stdio_config": { - "command": "npx", - "args": ["-y", "your-mcp-server-name"], - "envs": ["YOUR_MCP_SERVER_ENV_VAR"] + "name": "filesystem", + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-filesystem", + "/path/to/allowed/files" + ], + "env": { + "NODE_ENV": "production" } } ] - } + }, + "plugins": [ + { + "name": "mocker", + "source": "local", + "plugin_path": "../../plugins/mocker", + "enabled": true, + "config": { + "enabled": true, + "default_behavior": "passthrough", + "rules": [ + { + "name": "test-mock-rule", + "enabled": true, + "priority": 1, + "probability": 1.0, + "conditions": { + "message_regex": "test.*" + }, + "responses": [ + { + "type": "success", + "weight": 1.0, + "content": { + "message": "This is a mocked response for testing" + } + } + ] + } + ] + } + }, + { + "name": "maxim", + "source": "local", + "plugin_path": "../../plugins/maxim", + "enabled": false, + "env_vars": { + "MAXIM_API_KEY": "env.MAXIM_API_KEY", + "MAXIM_LOG_REPO_ID": "env.MAXIM_LOG_REPO_ID" + } + }, + { + "name": "production-plugin", + "source": "package", + "package": "github.com/company/bifrost-production-plugin", + "version": "v1.2.0", + "enabled": true, + "config": { + "timeout": "30s", + "retries": 3 + }, + "env_vars": { + "API_KEY": "env.PRODUCTION_API_KEY" + } + } + ] } diff --git a/transports/go.mod b/transports/go.mod index ca8849f121..611c426bbb 100644 --- a/transports/go.mod +++ b/transports/go.mod @@ -4,8 +4,8 @@ go 1.24.1 require ( github.com/fasthttp/router v1.5.4 + github.com/hashicorp/go-plugin v1.6.3 github.com/maximhq/bifrost/core v1.1.4 - github.com/maximhq/bifrost/plugins/maxim v1.0.5 github.com/prometheus/client_golang v1.22.0 github.com/valyala/fasthttp v1.62.0 google.golang.org/genai v1.4.0 @@ -33,6 +33,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/fatih/color v1.7.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-gonic/gin v1.8.1 // indirect @@ -42,23 +43,27 @@ require ( github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/validator/v10 v10.10.0 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect + github.com/hashicorp/go-hclog v0.14.1 // indirect + github.com/hashicorp/go-plugin v1.6.3 // indirect + github.com/hashicorp/yamux v0.1.1 // indirect github.com/invopop/jsonschema v0.12.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect - github.com/maximhq/maxim-go v0.1.3 // indirect + github.com/mattn/go-colorable v0.1.4 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect github.com/metoro-io/mcp-golang v0.13.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oklog/run v1.0.0 // indirect github.com/pelletier/go-toml/v2 v2.0.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect @@ -88,3 +93,5 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/maximhq/bifrost/core => ../core diff --git a/transports/go.sum b/transports/go.sum index abd0584e99..9968116274 100644 --- a/transports/go.sum +++ b/transports/go.sum @@ -36,6 +36,8 @@ github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPn github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= +github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= 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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -46,6 +48,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fasthttp/router v1.5.4 h1:oxdThbBwQgsDIYZ3wR1IavsNl6ZS9WdjKukeMikOnC8= github.com/fasthttp/router v1.5.4/go.mod h1:3/hysWq6cky7dTfzaaEPZGdptwjwx0qzTgFCKEWRjgc= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= @@ -82,8 +86,16 @@ github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrk github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-hclog v0.14.1 h1:nQcJDQwIAGnmoUWp8ubocEX40cCml/17YkF6csQLReU= +github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0UUrwg= +github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0= +github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= +github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= +github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -104,14 +116,12 @@ github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/maximhq/bifrost/core v1.1.4 h1:XcqNAnltXni01duipDgbAEsPHmKcoQ77vwtjnlm2Zoo= -github.com/maximhq/bifrost/core v1.1.4/go.mod h1:QfsUjZursZvFulDRsqXCd1rA8YOOl3rdyvO2i0FjF8Y= -github.com/maximhq/bifrost/plugins/maxim v1.0.5 h1:K67bqb49X0q07UbNCu1jlmdhmQG+gTdC8sxXN5ok5bY= -github.com/maximhq/bifrost/plugins/maxim v1.0.5/go.mod h1:Emik7JHo4BIa6kRWDEqOHgFp8M1BQv13bSOepoWw4aw= -github.com/maximhq/maxim-go v0.1.3 h1:nVzdz3hEjZVxmWHARWIM+Yrn1Jp50qrsK4BA/sz2jj8= -github.com/maximhq/maxim-go v0.1.3/go.mod h1:0+UTWM7UZwNNE5VnljLtr/vpRGtYP8r/2q9WDwlLWFw= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/metoro-io/mcp-golang v0.13.0 h1:54TFBJIW76VRB55CJovQQje9x4GnXg0BQQwGRtXrbCE= github.com/metoro-io/mcp-golang v0.13.0/go.mod h1:ifLP9ZzKpN1UqFWNTpAHOqSvNkMK6b7d1FSZ5Lu0lN0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -121,6 +131,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -143,6 +155,7 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc= github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -194,10 +207,12 @@ golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=