Skip to content

Commit 35152fe

Browse files
committed
plugins v2 architecture
1 parent 86283ac commit 35152fe

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+5172
-1749
lines changed

.github/workflows/release-pipeline.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: Release Pipeline
33
# Triggers automatically on push to main when any version file changes
44
on:
55
push:
6-
branches: ["main"]
6+
branches: ["main", "v1.4.0"]
77

88
# Prevent concurrent runs
99
concurrency:
@@ -594,7 +594,7 @@ jobs:
594594
fi
595595
596596
# Build the message with proper formatting
597-
MESSAGE=$(printf "🚀 **Release Pipeline Complete**\n\n**Components:**\n• Core: %s\n• Framework: %s\n• Plugins: %s\n• Bifrost HTTP: %s\n\n**Details:**\n• Branch: \`main\`\n• Commit: \`%.8s\`\n• Author: %s\n\n[View Workflow Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" "$CORE_STATUS" "$FRAMEWORK_STATUS" "$PLUGINS_STATUS" "$BIFROST_STATUS" "${{ github.sha }}" "${{ github.actor }}")
597+
MESSAGE=$(printf "🚀 **Release Pipeline Complete**\n\n**Components:**\n• Core: %s\n• Framework: %s\n• Plugins: %s\n• Bifrost HTTP: %s\n\n**Details:**\n• Branch: \`${{ github.ref_name }}\`\n• Commit: \`%.8s\`\n• Author: %s\n\n[View Workflow Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" "$CORE_STATUS" "$FRAMEWORK_STATUS" "$PLUGINS_STATUS" "$BIFROST_STATUS" "${{ github.sha }}" "${{ github.actor }}")
598598
599599
payload="$(jq -n --arg content "$MESSAGE" '{content:$content}')"
600600
curl -sS -H "Content-Type: application/json" -d "$payload" "$DISCORD_WEBHOOK"

.github/workflows/scripts/push-mintlify-changelog.sh

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,8 @@ if ! grep -q "\"$route\"" docs/docs.json; then
236236
fi
237237

238238
# Pulling again before committing
239-
git pull origin main
239+
CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
240+
git pull origin "$CURRENT_BRANCH"
240241
# Commit and push changes
241242
git add docs/changelogs/$VERSION.mdx
242243
git add docs/docs.json
@@ -247,4 +248,4 @@ done
247248
git config user.name "github-actions[bot]"
248249
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
249250
git commit -m "Adds changelog for $VERSION --skip-pipeline"
250-
git push origin main
251+
git push origin "$CURRENT_BRANCH"

.github/workflows/scripts/release-bifrost-http.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,8 @@ echo "✅ Transport build validation successful"
358358

359359
# Commit and push changes if any
360360
# First, pull latest changes to avoid conflicts
361-
git pull origin main
361+
CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
362+
git pull origin "$CURRENT_BRANCH"
362363

363364
# Stage any changes made to transports/
364365
git add transports/

core/bifrost.go

Lines changed: 327 additions & 11 deletions
Large diffs are not rendered by default.

core/bifrost_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ func TestExecuteRequestWithRetries_SuccessScenarios(t *testing.T) {
6868
schemas.ChatCompletionRequest,
6969
schemas.OpenAI,
7070
"gpt-4",
71+
schemas.DefaultTracer(),
72+
nil,
7173
)
7274

7375
if callCount != 1 {
@@ -101,6 +103,8 @@ func TestExecuteRequestWithRetries_SuccessScenarios(t *testing.T) {
101103
schemas.ChatCompletionRequest,
102104
schemas.OpenAI,
103105
"gpt-4",
106+
schemas.DefaultTracer(),
107+
nil,
104108
)
105109

106110
if callCount != 3 {
@@ -134,6 +138,8 @@ func TestExecuteRequestWithRetries_RetryLimits(t *testing.T) {
134138
schemas.ChatCompletionRequest,
135139
schemas.OpenAI,
136140
"gpt-4",
141+
schemas.DefaultTracer(),
142+
nil,
137143
)
138144

139145
// Should try: initial + 2 retries = 3 total attempts
@@ -196,6 +202,8 @@ func TestExecuteRequestWithRetries_NonRetryableErrors(t *testing.T) {
196202
schemas.ChatCompletionRequest,
197203
schemas.OpenAI,
198204
"gpt-4",
205+
schemas.DefaultTracer(),
206+
nil,
199207
)
200208

201209
if callCount != 1 {
@@ -268,6 +276,8 @@ func TestExecuteRequestWithRetries_RetryableConditions(t *testing.T) {
268276
schemas.ChatCompletionRequest,
269277
schemas.OpenAI,
270278
"gpt-4",
279+
schemas.DefaultTracer(),
280+
nil,
271281
)
272282

273283
// Should try: initial + 1 retry = 2 total attempts
@@ -496,6 +506,8 @@ func TestExecuteRequestWithRetries_LoggingAndCounting(t *testing.T) {
496506
schemas.ChatCompletionRequest,
497507
schemas.OpenAI,
498508
"gpt-4",
509+
schemas.DefaultTracer(),
510+
nil,
499511
)
500512

501513
// Verify call progression

core/changelog.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,35 @@
11
- feat: added code mode to mcp
22
- feat: added health monitoring to mcp
3-
- feat: added responses format tool execution support to mcp
3+
- feat: added responses format tool execution support to mcp
4+
- feat: adds central tracer for e2e tracing
5+
6+
### BREAKING CHANGES
7+
8+
- **Plugin Interface: TransportInterceptor removed, replaced with HTTPTransportMiddleware**
9+
10+
The `TransportInterceptor` method has been removed from the `Plugin` interface in `schemas/plugin.go`. All plugins must now implement `HTTPTransportMiddleware()` instead.
11+
12+
**Old API (removed in core v1.3.0):**
13+
```go
14+
TransportInterceptor(ctx *BifrostContext, url string, headers map[string]string, body map[string]any) (map[string]string, map[string]any, error)
15+
```
16+
17+
**New API (core v1.3.0+):**
18+
```go
19+
HTTPTransportMiddleware() BifrostHTTPMiddleware
20+
// where BifrostHTTPMiddleware = func(next fasthttp.RequestHandler) fasthttp.RequestHandler
21+
```
22+
23+
**Key changes:**
24+
- Method renamed: `TransportInterceptor` -> `HTTPTransportMiddleware`
25+
- Return type changed: Now returns a middleware function instead of modified headers/body
26+
- New import required: `github.com/valyala/fasthttp`
27+
- Flow control: Must call `next(ctx)` explicitly to continue the middleware chain
28+
- New capability: Can now intercept and modify responses (not just requests)
29+
30+
**Migration for plugin consumers:**
31+
1. Update your plugin to implement `HTTPTransportMiddleware()` instead of `TransportInterceptor()`
32+
2. If your plugin doesn't need HTTP transport interception, return `nil` from `HTTPTransportMiddleware()`
33+
3. Update tests to verify the new middleware signature
34+
35+
See [Plugin Migration Guide](/docs/plugins/migration-guide) for complete instructions and code examples.

core/mcp.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1135,7 +1135,6 @@ func (m *MCPManager) createInProcessConnection(config schemas.MCPClientConfig) (
11351135
if config.InProcessServer == nil {
11361136
return nil, MCPClientConnectionInfo{}, fmt.Errorf("InProcess connection requires a server instance")
11371137
}
1138-
11391138
// Create in-process client directly connected to the provided server
11401139
inProcessClient, err := client.NewInProcessClient(config.InProcessServer)
11411140
if err != nil {

core/providers/utils/utils.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -877,12 +877,27 @@ func SendInProgressEventResponsesChunk(ctx context.Context, postHookRunner schem
877877
// This utility reduces code duplication across streaming implementations by encapsulating
878878
// the common pattern of running post hooks, handling errors, and sending responses with
879879
// proper context cancellation handling.
880+
// It also completes the deferred LLM span when the final chunk is sent (StreamEndIndicator is true).
880881
func ProcessAndSendResponse(
881882
ctx context.Context,
882883
postHookRunner schemas.PostHookRunner,
883884
response *schemas.BifrostResponse,
884885
responseChan chan *schemas.BifrostStream,
885886
) {
887+
// Accumulate chunk for tracing (common for all providers)
888+
if tracer, ok := ctx.Value(schemas.BifrostContextKeyTracer).(schemas.Tracer); ok && tracer != nil {
889+
if traceID, ok := ctx.Value(schemas.BifrostContextKeyTraceID).(string); ok && traceID != "" {
890+
tracer.AddStreamingChunk(traceID, response)
891+
}
892+
}
893+
894+
// Check if this is the final chunk and complete deferred span if so
895+
if isFinalChunk := ctx.Value(schemas.BifrostContextKeyStreamEndIndicator); isFinalChunk != nil {
896+
if final, ok := isFinalChunk.(bool); ok && final {
897+
completeDeferredSpan(&ctx, response, nil)
898+
}
899+
}
900+
886901
// Run post hooks on the response
887902
processedResponse, processedError := postHookRunner(&ctx, response, nil)
888903

@@ -913,13 +928,21 @@ func ProcessAndSendResponse(
913928
// This utility reduces code duplication across streaming implementations by encapsulating
914929
// the common pattern of running post hooks, handling errors, and sending responses with
915930
// proper context cancellation handling.
931+
// It also completes the deferred LLM span when the final chunk is sent (StreamEndIndicator is true).
916932
func ProcessAndSendBifrostError(
917933
ctx context.Context,
918934
postHookRunner schemas.PostHookRunner,
919935
bifrostErr *schemas.BifrostError,
920936
responseChan chan *schemas.BifrostStream,
921937
logger schemas.Logger,
922938
) {
939+
// Check if this is the final chunk and complete deferred span if so
940+
if isFinalChunk := ctx.Value(schemas.BifrostContextKeyStreamEndIndicator); isFinalChunk != nil {
941+
if final, ok := isFinalChunk.(bool); ok && final {
942+
completeDeferredSpan(&ctx, nil, bifrostErr)
943+
}
944+
}
945+
923946
// Send scanner error through channel
924947
processedResponse, processedError := postHookRunner(&ctx, nil, bifrostErr)
925948

@@ -1385,3 +1408,94 @@ func GetBudgetTokensFromReasoningEffort(
13851408

13861409
return budget, nil
13871410
}
1411+
1412+
// completeDeferredSpan completes the deferred LLM span for streaming requests.
1413+
// This is called when the final chunk is processed (when StreamEndIndicator is true).
1414+
// It retrieves the deferred span handle from TraceStore using the trace ID from context,
1415+
// populates response attributes from accumulated chunks, and ends the span.
1416+
func completeDeferredSpan(ctx *context.Context, result *schemas.BifrostResponse, err *schemas.BifrostError) {
1417+
if ctx == nil {
1418+
return
1419+
}
1420+
1421+
// Get the trace ID from context (this IS available in the provider's goroutine)
1422+
traceID, ok := (*ctx).Value(schemas.BifrostContextKeyTraceID).(string)
1423+
if !ok || traceID == "" {
1424+
return
1425+
}
1426+
1427+
// Get the tracer from context
1428+
tracerVal := (*ctx).Value(schemas.BifrostContextKeyTracer)
1429+
if tracerVal == nil {
1430+
return
1431+
}
1432+
tracer, ok := tracerVal.(schemas.Tracer)
1433+
if !ok || tracer == nil {
1434+
return
1435+
}
1436+
1437+
// Get the deferred span handle from TraceStore using trace ID
1438+
handle := tracer.GetDeferredSpanHandle(traceID)
1439+
if handle == nil {
1440+
return
1441+
}
1442+
1443+
// Set total latency from the final chunk
1444+
if result != nil {
1445+
extraFields := result.GetExtraFields()
1446+
if extraFields.Latency > 0 {
1447+
tracer.SetAttribute(handle, "gen_ai.response.total_latency_ms", extraFields.Latency)
1448+
}
1449+
}
1450+
1451+
// Get accumulated response with full data (content, tool calls, reasoning, etc.)
1452+
// This builds a complete BifrostResponse from all the streaming chunks
1453+
accumulatedResp, ttftMs, chunkCount := tracer.GetAccumulatedChunks(traceID)
1454+
if accumulatedResp != nil {
1455+
// Use accumulated response for attributes (includes full content, tool calls, etc.)
1456+
tracer.PopulateLLMResponseAttributes(handle, accumulatedResp, err)
1457+
1458+
// Set Time to First Token (TTFT) attribute
1459+
if ttftMs > 0 {
1460+
tracer.SetAttribute(handle, schemas.AttrTimeToFirstToken, ttftMs)
1461+
}
1462+
1463+
// Set total chunks attribute
1464+
if chunkCount > 0 {
1465+
tracer.SetAttribute(handle, schemas.AttrTotalChunks, chunkCount)
1466+
}
1467+
} else if result != nil {
1468+
// Fall back to final chunk if no accumulated data (shouldn't happen normally)
1469+
tracer.PopulateLLMResponseAttributes(handle, result, err)
1470+
}
1471+
1472+
// Finalize aggregated post-hook spans before ending the LLM span
1473+
// This creates one span per plugin with average execution time
1474+
// We need to set the llm.call span ID in context so post-hook spans become its children
1475+
if finalizer, ok := (*ctx).Value(schemas.BifrostContextKeyPostHookSpanFinalizer).(func(context.Context)); ok && finalizer != nil {
1476+
// Get the deferred span ID (the llm.call span) to set as parent for post-hook spans
1477+
spanID := tracer.GetDeferredSpanID(traceID)
1478+
if spanID != "" {
1479+
finalizerCtx := context.WithValue(*ctx, schemas.BifrostContextKeySpanID, spanID)
1480+
finalizer(finalizerCtx)
1481+
} else {
1482+
finalizer(*ctx)
1483+
}
1484+
}
1485+
1486+
// End span with appropriate status
1487+
if err != nil {
1488+
if err.Error != nil {
1489+
tracer.SetAttribute(handle, "error", err.Error.Message)
1490+
}
1491+
if err.StatusCode != nil {
1492+
tracer.SetAttribute(handle, "status_code", *err.StatusCode)
1493+
}
1494+
tracer.EndSpan(handle, schemas.SpanStatusError, "streaming request failed")
1495+
} else {
1496+
tracer.EndSpan(handle, schemas.SpanStatusOk, "")
1497+
}
1498+
1499+
// Clear the deferred span from TraceStore
1500+
tracer.ClearDeferredSpan(traceID)
1501+
}

core/schemas/bifrost.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type BifrostConfig struct {
2222
Account Account
2323
Plugins []Plugin
2424
Logger Logger
25+
Tracer Tracer // Tracer for distributed tracing (nil = NoOpTracer)
2526
InitialPoolSize int // Initial pool size for sync pools in Bifrost. Higher values will reduce memory allocations but will increase memory usage.
2627
DropExcessRequests bool // If true, in cases where the queue is full, requests will not wait for the queue to be empty and will be dropped instead.
2728
MCPConfig *MCPConfig // MCP (Model Context Protocol) configuration for tool integration
@@ -139,6 +140,13 @@ const (
139140
BifrostMCPAgentOriginalRequestID BifrostContextKey = "bifrost-mcp-agent-original-request-id" // string (to store the original request ID for MCP agent mode)
140141
BifrostContextKeyStructuredOutputToolName BifrostContextKey = "bifrost-structured-output-tool-name" // string (to store the name of the structured output tool (set by bifrost))
141142
BifrostContextKeyUserAgent BifrostContextKey = "bifrost-user-agent" // string (set by bifrost)
143+
BifrostContextKeyTraceID BifrostContextKey = "bifrost-trace-id" // string (trace ID for distributed tracing - set by tracing middleware)
144+
BifrostContextKeySpanID BifrostContextKey = "bifrost-span-id" // string (current span ID for child span creation - set by tracer)
145+
BifrostContextKeyStreamStartTime BifrostContextKey = "bifrost-stream-start-time" // time.Time (start time for streaming TTFT calculation - set by bifrost)
146+
BifrostContextKeyTracer BifrostContextKey = "bifrost-tracer" // Tracer (tracer instance for completing deferred spans - set by bifrost)
147+
BifrostContextKeyDeferTraceCompletion BifrostContextKey = "bifrost-defer-trace-completion" // bool (signals trace completion should be deferred for streaming - set by streaming handlers)
148+
BifrostContextKeyTraceCompleter BifrostContextKey = "bifrost-trace-completer" // func() (callback to complete trace after streaming - set by tracing middleware)
149+
BifrostContextKeyPostHookSpanFinalizer BifrostContextKey = "bifrost-posthook-span-finalizer" // func(context.Context) (callback to finalize post-hook spans after streaming - set by bifrost)
142150
)
143151

144152
// NOTE: for custom plugin implementation dealing with streaming short circuit,

core/schemas/context.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ var reservedKeys = []any{
2323
BifrostContextKeySkipKeySelection,
2424
BifrostContextKeyExtraHeaders,
2525
BifrostContextKeyURLPath,
26+
BifrostContextKeyDeferTraceCompletion,
2627
}
2728

2829
// BifrostContext is a custom context.Context implementation that tracks user-set values.

0 commit comments

Comments
 (0)