Skip to content

Commit ec9703b

Browse files
committed
plugins v2 architecture
1 parent 18c9299 commit ec9703b

File tree

22 files changed

+163
-153
lines changed

22 files changed

+163
-153
lines changed

core/schemas/plugin.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
// Package schemas defines the core schemas and types used by the Bifrost system.
22
package schemas
33

4-
import "context"
4+
import (
5+
"context"
6+
7+
"github.com/valyala/fasthttp"
8+
)
59

610
// PluginShortCircuit represents a plugin's decision to short-circuit the normal flow.
711
// It can contain either a response (success short-circuit), a stream (streaming short-circuit), or an error (error short-circuit).
@@ -64,11 +68,11 @@ type Plugin interface {
6468
// GetName returns the name of the plugin.
6569
GetName() string
6670

67-
// TransportInterceptor is called at the HTTP transport layer before requests enter Bifrost core.
68-
// It allows plugins to modify raw HTTP headers and body before transformation into BifrostRequest.
71+
// HTTPTransportMiddleware is called at the HTTP transport layer before requests enter Bifrost core.
72+
// It allows plugins to modify the request and response before they are processed by the next middleware.
6973
// Only invoked when using HTTP transport (bifrost-http), not when using Bifrost as a Go SDK directly.
70-
// Returns modified headers, modified body, and any error that occurred during interception.
71-
TransportInterceptor(ctx *context.Context, url string, headers map[string]string, body map[string]any) (map[string]string, map[string]any, error)
74+
// Returns a new handler that will be called next in the middleware chain.
75+
HTTPTransportMiddleware() func(next fasthttp.RequestHandler) fasthttp.RequestHandler
7276

7377
// PreHook is called before a request is processed by a provider.
7478
// It allows plugins to modify the request before it is sent to the provider.

examples/plugins/hello-world/go.mod

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,21 @@ module github.com/maximhq/bifrost/examples/plugins/hello-world
22

33
go 1.24.3
44

5-
require github.com/maximhq/bifrost/core v1.2.27
5+
require (
6+
github.com/maximhq/bifrost/core v1.2.27
7+
github.com/valyala/fasthttp v1.67.0
8+
)
69

710
require (
11+
github.com/andybalholm/brotli v1.2.0 // indirect
812
github.com/bytedance/gopkg v0.1.3 // indirect
913
github.com/bytedance/sonic v1.14.1 // indirect
1014
github.com/bytedance/sonic/loader v0.3.0 // indirect
1115
github.com/cloudwego/base64x v0.1.6 // indirect
16+
github.com/klauspost/compress v1.18.0 // indirect
1217
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
1318
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
19+
github.com/valyala/bytebufferpool v1.0.0 // indirect
1420
golang.org/x/arch v0.22.0 // indirect
1521
golang.org/x/sys v0.37.0 // indirect
1622
)

examples/plugins/hello-world/go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
2+
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
13
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
24
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
35
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
@@ -10,6 +12,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
1012
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1113
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
1214
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
15+
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
16+
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
1317
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
1418
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
1519
github.com/maximhq/bifrost/core v1.2.27 h1:1mHCeABfaAqcrEDcbkV+Ppdx4P9/AVoZkBowzf8Fn9k=
@@ -27,6 +31,12 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
2731
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
2832
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
2933
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
34+
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
35+
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
36+
github.com/valyala/fasthttp v1.67.0 h1:tqKlJMUP6iuNG8hGjK/s9J4kadH7HLV4ijEcPGsezac=
37+
github.com/valyala/fasthttp v1.67.0/go.mod h1:qYSIpqt/0XNmShgo/8Aq8E3UYWVVwNS2QYmzd8WIEPM=
38+
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
39+
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
3040
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
3141
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
3242
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=

examples/plugins/hello-world/main.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66

77
"github.com/maximhq/bifrost/core/schemas"
8+
"github.com/valyala/fasthttp"
89
)
910

1011
func Init(config any) error {
@@ -16,9 +17,11 @@ func GetName() string {
1617
return "Hello World Plugin"
1718
}
1819

19-
func TransportInterceptor(ctx *context.Context, url string, headers map[string]string, body map[string]any) (map[string]string, map[string]any, error) {
20-
fmt.Println("TransportInterceptor called")
21-
return headers, body, nil
20+
func HTTPTransportMiddleware(next fasthttp.RequestHandler) fasthttp.RequestHandler {
21+
return func(ctx *fasthttp.RequestCtx) {
22+
fmt.Println("HTTPTransportMiddleware called")
23+
next(ctx)
24+
}
2225
}
2326

2427
func PreHook(ctx *context.Context, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.PluginShortCircuit, error) {

framework/plugins/dynamicplugin.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ type DynamicPlugin struct {
2323
plugin *plugin.Plugin
2424

2525
getName func() string
26-
transportInterceptor func(ctx *context.Context, url string, headers map[string]string, body map[string]any) (map[string]string, map[string]any, error)
26+
httpTransportMiddleware func(next fasthttp.RequestHandler) fasthttp.RequestHandler
2727
preHook func(ctx *context.Context, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.PluginShortCircuit, error)
2828
postHook func(ctx *context.Context, resp *schemas.BifrostResponse, bifrostErr *schemas.BifrostError) (*schemas.BifrostResponse, *schemas.BifrostError, error)
2929
cleanup func() error
@@ -35,8 +35,8 @@ func (dp *DynamicPlugin) GetName() string {
3535
}
3636

3737
// TransportInterceptor is not used for dynamic plugins
38-
func (dp *DynamicPlugin) TransportInterceptor(ctx *context.Context, url string, headers map[string]string, body map[string]any) (map[string]string, map[string]any, error) {
39-
return dp.transportInterceptor(ctx, url, headers, body)
38+
func (dp *DynamicPlugin) HTTPTransportMiddleware(next fasthttp.RequestHandler) fasthttp.RequestHandler {
39+
return dp.httpTransportMiddleware(next)
4040
}
4141

4242
// PreHook is not used for dynamic plugins
@@ -138,13 +138,13 @@ func loadDynamicPlugin(path string, config any) (schemas.Plugin, error) {
138138
if dp.getName, ok = getNameSym.(func() string); !ok {
139139
return nil, fmt.Errorf("failed to cast GetName to func() string")
140140
}
141-
// Looking up for TransportInterceptor method
142-
transportInterceptorSym, err := plugin.Lookup("TransportInterceptor")
141+
// Looking up for HTTPTransportMiddleware method
142+
httpTransportMiddlewareSym, err := plugin.Lookup("HTTPTransportMiddleware")
143143
if err != nil {
144144
return nil, err
145145
}
146-
if dp.transportInterceptor, ok = transportInterceptorSym.(func(ctx *context.Context, url string, headers map[string]string, body map[string]any) (map[string]string, map[string]any, error)); !ok {
147-
return nil, fmt.Errorf("failed to cast TransportInterceptor to func(ctx *context.Context, url string, headers map[string]string, body map[string]any) (map[string]string, map[string]any, error)")
146+
if dp.httpTransportMiddleware, ok = httpTransportMiddlewareSym.(func(next fasthttp.RequestHandler) fasthttp.RequestHandler); !ok {
147+
return nil, fmt.Errorf("failed to cast HTTPTransportMiddleware to func(next fasthttp.RequestHandler) fasthttp.RequestHandler")
148148
}
149149
// Looking up for PreHook method
150150
preHookSym, err := plugin.Lookup("PreHook")

framework/plugins/dynamicplugin_test.go

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/maximhq/bifrost/core/schemas"
1313
"github.com/stretchr/testify/assert"
1414
"github.com/stretchr/testify/require"
15+
"github.com/valyala/fasthttp"
1516
)
1617

1718
const (
@@ -49,25 +50,32 @@ func TestDynamicPluginLifecycle(t *testing.T) {
4950
assert.Equal(t, "Hello World Plugin", name, "Plugin name should match")
5051
})
5152

52-
// Test TransportInterceptor
53-
t.Run("TransportInterceptor", func(t *testing.T) {
54-
ctx := context.Background()
55-
url := "http://example.com/api"
56-
headers := map[string]string{
57-
"Content-Type": "application/json",
58-
"Authorization": "Bearer token123",
59-
}
60-
body := map[string]any{
61-
"model": "gpt-4",
62-
"messages": []map[string]string{
63-
{"role": "user", "content": "Hello"},
64-
},
53+
// Test HTTPTransportMiddleware
54+
t.Run("HTTPTransportMiddleware", func(t *testing.T) {
55+
// Track if the next handler was called
56+
nextHandlerCalled := false
57+
58+
// Create a mock next handler
59+
nextHandler := func(ctx *fasthttp.RequestCtx) {
60+
nextHandlerCalled = true
6561
}
66-
67-
modifiedHeaders, modifiedBody, err := plugin.TransportInterceptor(&ctx, url, headers, body)
68-
require.NoError(t, err, "TransportInterceptor should not return error")
69-
assert.Equal(t, headers, modifiedHeaders, "Headers should be unchanged")
70-
assert.Equal(t, body, modifiedBody, "Body should be unchanged")
62+
63+
// Get the middleware-wrapped handler
64+
wrappedHandler := plugin.HTTPTransportMiddleware(nextHandler)
65+
require.NotNil(t, wrappedHandler, "HTTPTransportMiddleware should return a handler")
66+
67+
// Create a test request context
68+
ctx := &fasthttp.RequestCtx{}
69+
ctx.Request.SetRequestURI("http://example.com/api")
70+
ctx.Request.Header.SetMethod("POST")
71+
ctx.Request.Header.Set("Content-Type", "application/json")
72+
ctx.Request.Header.Set("Authorization", "Bearer token123")
73+
74+
// Call the wrapped handler
75+
wrappedHandler(ctx)
76+
77+
// Verify the next handler was called
78+
assert.True(t, nextHandlerCalled, "Next handler should have been called")
7179
})
7280

7381
// Test PreHook

plugins/governance/go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ go 1.24.3
55
require gorm.io/gorm v1.31.1
66

77
require (
8+
github.com/bytedance/sonic v1.14.1
89
github.com/maximhq/bifrost/core v1.2.26
910
github.com/maximhq/bifrost/framework v1.1.33
11+
github.com/valyala/fasthttp v1.67.0
1012
)
1113

1214
require (
@@ -30,7 +32,6 @@ require (
3032
github.com/bahlo/generic-list-go v0.2.0 // indirect
3133
github.com/buger/jsonparser v1.1.1 // indirect
3234
github.com/bytedance/gopkg v0.1.3 // indirect
33-
github.com/bytedance/sonic v1.14.1 // indirect
3435
github.com/bytedance/sonic/loader v0.3.0 // indirect
3536
github.com/cespare/xxhash/v2 v2.3.0 // indirect
3637
github.com/cloudwego/base64x v0.1.6 // indirect
@@ -81,7 +82,6 @@ require (
8182
github.com/spf13/cast v1.10.0 // indirect
8283
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
8384
github.com/valyala/bytebufferpool v1.0.0 // indirect
84-
github.com/valyala/fasthttp v1.67.0 // indirect
8585
github.com/weaviate/weaviate v1.33.1 // indirect
8686
github.com/weaviate/weaviate-go-client/v5 v5.5.0 // indirect
8787
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect

plugins/governance/main.go

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ import (
1010
"strings"
1111
"sync"
1212

13+
"github.com/bytedance/sonic"
1314
bifrost "github.com/maximhq/bifrost/core"
1415
"github.com/maximhq/bifrost/core/schemas"
1516
"github.com/maximhq/bifrost/framework/configstore"
1617
configstoreTables "github.com/maximhq/bifrost/framework/configstore/tables"
1718
"github.com/maximhq/bifrost/framework/modelcatalog"
19+
"github.com/valyala/fasthttp"
1820
)
1921

2022
// PluginName is the name of the governance plugin
@@ -153,37 +155,57 @@ func (p *GovernancePlugin) GetName() string {
153155
return PluginName
154156
}
155157

156-
// TransportInterceptor intercepts requests before they are processed (governance decision point)
157-
func (p *GovernancePlugin) TransportInterceptor(ctx *context.Context, url string, headers map[string]string, body map[string]any) (map[string]string, map[string]any, error) {
158-
var virtualKeyValue string
159-
var err error
160-
161-
for header, value := range headers {
162-
if strings.ToLower(string(header)) == string(schemas.BifrostContextKeyVirtualKey) {
163-
virtualKeyValue = string(value)
164-
break
158+
// HTTPTransportMiddleware intercepts requests before they are processed (governance decision point)
159+
func (p *GovernancePlugin) HTTPTransportMiddleware(next fasthttp.RequestHandler) fasthttp.RequestHandler {
160+
return func(ctx *fasthttp.RequestCtx) {
161+
var virtualKeyValue string
162+
vkHeader := ctx.Request.Header.Peek("x-bf-vk")
163+
if string(vkHeader) == "" {
164+
next(ctx)
165+
return
165166
}
167+
virtualKeyValue = string(vkHeader)
168+
// Get the virtual key from the store
169+
virtualKey, ok := p.store.GetVirtualKey(virtualKeyValue)
170+
if !ok || virtualKey == nil || !virtualKey.IsActive {
171+
next(ctx)
172+
return
173+
}
174+
headers, err := p.addMCPIncludeTools(nil, virtualKey)
175+
if err != nil {
176+
p.logger.Error("failed to add MCP include tools: %v", err)
177+
next(ctx)
178+
return
179+
}
180+
for header, value := range headers {
181+
ctx.Request.Header.Set(header, value)
182+
}
183+
if ctx.Request.Body() == nil {
184+
next(ctx)
185+
return
186+
}
187+
var payload map[string]any
188+
err = sonic.Unmarshal(ctx.Request.Body(), &payload)
189+
if err != nil {
190+
p.logger.Error("failed to marshal request body to check for virtual key: %v", err)
191+
next(ctx)
192+
return
193+
}
194+
payload, err = p.loadBalanceProvider(payload, virtualKey)
195+
if err != nil {
196+
p.logger.Error("failed to load balance provider: %v", err)
197+
next(ctx)
198+
return
199+
}
200+
body, err := sonic.Marshal(payload)
201+
if err != nil {
202+
p.logger.Error("failed to marshal request body to check for virtual key: %v", err)
203+
next(ctx)
204+
return
205+
}
206+
ctx.Request.SetBody(body)
207+
next(ctx)
166208
}
167-
if virtualKeyValue == "" {
168-
return headers, body, nil
169-
}
170-
171-
virtualKey, ok := p.store.GetVirtualKey(virtualKeyValue)
172-
if !ok || virtualKey == nil || !virtualKey.IsActive {
173-
return headers, body, nil
174-
}
175-
176-
body, err = p.loadBalanceProvider(body, virtualKey)
177-
if err != nil {
178-
return headers, body, err
179-
}
180-
181-
headers, err = p.addMCPIncludeTools(headers, virtualKey)
182-
if err != nil {
183-
return headers, body, err
184-
}
185-
186-
return headers, body, nil
187209
}
188210

189211
func (p *GovernancePlugin) loadBalanceProvider(body map[string]any, virtualKey *configstoreTables.TableVirtualKey) (map[string]any, error) {

plugins/jsonparser/go.mod

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ module github.com/maximhq/bifrost/plugins/jsonparser
22

33
go 1.24.3
44

5-
require github.com/maximhq/bifrost/core v1.2.26
5+
require (
6+
github.com/maximhq/bifrost/core v1.2.26
7+
github.com/valyala/fasthttp v1.67.0
8+
)
69

710
require (
811
cloud.google.com/go/compute/metadata v0.9.0 // indirect
@@ -39,7 +42,6 @@ require (
3942
github.com/spf13/cast v1.10.0 // indirect
4043
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
4144
github.com/valyala/bytebufferpool v1.0.0 // indirect
42-
github.com/valyala/fasthttp v1.67.0 // indirect
4345
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
4446
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
4547
golang.org/x/arch v0.22.0 // indirect

plugins/jsonparser/main.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
bifrost "github.com/maximhq/bifrost/core"
1010
"github.com/maximhq/bifrost/core/schemas"
11+
"github.com/valyala/fasthttp"
1112
)
1213

1314
const (
@@ -84,9 +85,9 @@ func (p *JsonParserPlugin) GetName() string {
8485
return PluginName
8586
}
8687

87-
// TransportInterceptor is not used for this plugin
88-
func (p *JsonParserPlugin) TransportInterceptor(ctx *context.Context, url string, headers map[string]string, body map[string]any) (map[string]string, map[string]any, error) {
89-
return headers, body, nil
88+
// HTTPTransportMiddleware is not used for this plugin
89+
func (p *JsonParserPlugin) HTTPTransportMiddleware(next fasthttp.RequestHandler) fasthttp.RequestHandler {
90+
return nil
9091
}
9192

9293
// PreHook is not used for this plugin as we only process responses

0 commit comments

Comments
 (0)