// Package logging provides utility functions and interfaces for the GORM-based logging plugin package logging import ( "context" "errors" "fmt" "strings" "time" bifrost "github.com/maximhq/bifrost/core" "github.com/maximhq/bifrost/core/schemas" "github.com/maximhq/bifrost/framework/logstore" "github.com/maximhq/bifrost/framework/streaming" ) // KeyPair represents an ID-Name pair for keys type KeyPair struct { ID string `json:"id"` Name string `json:"name"` } // LogManager defines the main interface that combines all logging functionality type LogManager interface { // GetLog retrieves a single log entry by ID (includes all fields, including raw_request/raw_response) GetLog(ctx context.Context, id string) (*logstore.Log, error) // Search searches for log entries based on filters and pagination Search(ctx context.Context, filters *logstore.SearchFilters, pagination *logstore.PaginationOptions) (*logstore.SearchResult, error) // GetSessionLogs returns paginated logs for a single parent_request_id session. GetSessionLogs(ctx context.Context, sessionID string, pagination *logstore.PaginationOptions) (*logstore.SessionDetailResult, error) // GetSessionSummary returns aggregate totals for a single parent_request_id session. GetSessionSummary(ctx context.Context, sessionID string) (*logstore.SessionSummaryResult, error) // GetStats calculates statistics for logs matching the given filters GetStats(ctx context.Context, filters *logstore.SearchFilters) (*logstore.SearchStats, error) // GetHistogram returns time-bucketed request counts for the given filters GetHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64) (*logstore.HistogramResult, error) // GetTokenHistogram returns time-bucketed token usage for the given filters GetTokenHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64) (*logstore.TokenHistogramResult, error) // GetCostHistogram returns time-bucketed cost data with model breakdown for the given filters GetCostHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64) (*logstore.CostHistogramResult, error) // GetModelHistogram returns time-bucketed model usage with success/error breakdown for the given filters GetModelHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64) (*logstore.ModelHistogramResult, error) // GetLatencyHistogram returns time-bucketed latency percentiles for the given filters GetLatencyHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64) (*logstore.LatencyHistogramResult, error) // GetProviderCostHistogram returns time-bucketed cost data with provider breakdown for the given filters GetProviderCostHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64) (*logstore.ProviderCostHistogramResult, error) // GetProviderTokenHistogram returns time-bucketed token usage with provider breakdown for the given filters GetProviderTokenHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64) (*logstore.ProviderTokenHistogramResult, error) // GetProviderLatencyHistogram returns time-bucketed latency percentiles with provider breakdown for the given filters GetProviderLatencyHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64) (*logstore.ProviderLatencyHistogramResult, error) // GetModelRankings returns models ranked by usage with trend comparison GetModelRankings(ctx context.Context, filters *logstore.SearchFilters) (*logstore.ModelRankingResult, error) // Get the number of dropped requests GetDroppedRequests(ctx context.Context) int64 // GetAvailableModels returns all unique models from logs GetAvailableModels(ctx context.Context) []string // GetAvailableAliases returns all unique alias values from logs GetAvailableAliases(ctx context.Context) []string // GetAvailableSelectedKeys returns all unique selected key ID-Name pairs from logs GetAvailableSelectedKeys(ctx context.Context) []KeyPair // GetAvailableVirtualKeys returns all unique virtual key ID-Name pairs from logs GetAvailableVirtualKeys(ctx context.Context) []KeyPair // GetAvailableRoutingRules returns all unique routing rule ID-Name pairs from logs GetAvailableRoutingRules(ctx context.Context) []KeyPair // GetAvailableRoutingEngines returns all unique routing engine types from logs GetAvailableRoutingEngines(ctx context.Context) []string // GetAvailableTeams returns all unique team ID-Name pairs from logs GetAvailableTeams(ctx context.Context) []KeyPair // GetAvailableCustomers returns all unique customer ID-Name pairs from logs GetAvailableCustomers(ctx context.Context) []KeyPair // GetAvailableUsers returns all unique user IDs from logs GetAvailableUsers(ctx context.Context) []KeyPair // GetAvailableBusinessUnits returns all unique business unit ID-Name pairs from logs GetAvailableBusinessUnits(ctx context.Context) []KeyPair // GetAvailableMetadataKeys returns distinct metadata keys and their values from recent logs GetAvailableMetadataKeys(ctx context.Context) (map[string][]string, error) // GetDimensionCostHistogram returns time-bucketed cost data grouped by the specified dimension GetDimensionCostHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64, dimension logstore.HistogramDimension) (*logstore.DimensionCostHistogramResult, error) // GetDimensionTokenHistogram returns time-bucketed token usage grouped by the specified dimension GetDimensionTokenHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64, dimension logstore.HistogramDimension) (*logstore.DimensionTokenHistogramResult, error) // GetDimensionLatencyHistogram returns time-bucketed latency percentiles grouped by the specified dimension GetDimensionLatencyHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64, dimension logstore.HistogramDimension) (*logstore.DimensionLatencyHistogramResult, error) // DeleteLog deletes a log entry by its ID DeleteLog(ctx context.Context, id string) error // DeleteLogs deletes multiple log entries by their IDs DeleteLogs(ctx context.Context, ids []string) error // RecalculateCosts recomputes missing costs for logs matching the filters RecalculateCosts(ctx context.Context, filters *logstore.SearchFilters, limit int) (*RecalculateCostResult, error) // MCP Tool Log methods // SearchMCPToolLogs searches for MCP tool log entries based on filters and pagination SearchMCPToolLogs(ctx context.Context, filters *logstore.MCPToolLogSearchFilters, pagination *logstore.PaginationOptions) (*logstore.MCPToolLogSearchResult, error) // GetMCPToolLogStats calculates statistics for MCP tool logs matching the given filters GetMCPToolLogStats(ctx context.Context, filters *logstore.MCPToolLogSearchFilters) (*logstore.MCPToolLogStats, error) // GetAvailableToolNames returns all unique tool names from MCP tool logs GetAvailableToolNames(ctx context.Context) ([]string, error) // GetAvailableServerLabels returns all unique server labels from MCP tool logs GetAvailableServerLabels(ctx context.Context) ([]string, error) // GetAvailableMCPVirtualKeys returns all unique virtual key ID-Name pairs from MCP tool logs GetAvailableMCPVirtualKeys(ctx context.Context) []KeyPair // GetMCPHistogram returns time-bucketed MCP tool call volume GetMCPHistogram(ctx context.Context, filters logstore.MCPToolLogSearchFilters, bucketSizeSeconds int64) (*logstore.MCPHistogramResult, error) // GetMCPCostHistogram returns time-bucketed MCP cost data GetMCPCostHistogram(ctx context.Context, filters logstore.MCPToolLogSearchFilters, bucketSizeSeconds int64) (*logstore.MCPCostHistogramResult, error) // GetMCPTopTools returns the top N MCP tools by call count GetMCPTopTools(ctx context.Context, filters logstore.MCPToolLogSearchFilters, limit int) (*logstore.MCPTopToolsResult, error) // DeleteMCPToolLogs deletes multiple MCP tool log entries by their IDs DeleteMCPToolLogs(ctx context.Context, ids []string) error } // PluginLogManager implements LogManager interface wrapping the plugin type PluginLogManager struct { plugin *LoggerPlugin } func (p *PluginLogManager) GetLog(ctx context.Context, id string) (*logstore.Log, error) { return p.plugin.GetLog(ctx, id) } func (p *PluginLogManager) Search(ctx context.Context, filters *logstore.SearchFilters, pagination *logstore.PaginationOptions) (*logstore.SearchResult, error) { if filters == nil || pagination == nil { return nil, fmt.Errorf("filters and pagination cannot be nil") } return p.plugin.SearchLogs(ctx, *filters, *pagination) } func (p *PluginLogManager) GetSessionLogs(ctx context.Context, sessionID string, pagination *logstore.PaginationOptions) (*logstore.SessionDetailResult, error) { if pagination == nil { return nil, fmt.Errorf("pagination cannot be nil") } if strings.TrimSpace(sessionID) == "" { return nil, fmt.Errorf("sessionID cannot be empty") } return p.plugin.GetSessionLogs(ctx, sessionID, *pagination) } func (p *PluginLogManager) GetSessionSummary(ctx context.Context, sessionID string) (*logstore.SessionSummaryResult, error) { if strings.TrimSpace(sessionID) == "" { return nil, fmt.Errorf("sessionID cannot be empty") } return p.plugin.GetSessionSummary(ctx, sessionID) } func (p *PluginLogManager) GetStats(ctx context.Context, filters *logstore.SearchFilters) (*logstore.SearchStats, error) { if filters == nil { return nil, fmt.Errorf("filters cannot be nil") } return p.plugin.GetStats(ctx, *filters) } func (p *PluginLogManager) GetHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64) (*logstore.HistogramResult, error) { if filters == nil { return nil, fmt.Errorf("filters cannot be nil") } return p.plugin.GetHistogram(ctx, *filters, bucketSizeSeconds) } func (p *PluginLogManager) GetTokenHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64) (*logstore.TokenHistogramResult, error) { if filters == nil { return nil, fmt.Errorf("filters cannot be nil") } return p.plugin.GetTokenHistogram(ctx, *filters, bucketSizeSeconds) } func (p *PluginLogManager) GetCostHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64) (*logstore.CostHistogramResult, error) { if filters == nil { return nil, fmt.Errorf("filters cannot be nil") } return p.plugin.GetCostHistogram(ctx, *filters, bucketSizeSeconds) } func (p *PluginLogManager) GetModelHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64) (*logstore.ModelHistogramResult, error) { if filters == nil { return nil, fmt.Errorf("filters cannot be nil") } return p.plugin.GetModelHistogram(ctx, *filters, bucketSizeSeconds) } func (p *PluginLogManager) GetLatencyHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64) (*logstore.LatencyHistogramResult, error) { if filters == nil { return nil, fmt.Errorf("filters cannot be nil") } return p.plugin.GetLatencyHistogram(ctx, *filters, bucketSizeSeconds) } func (p *PluginLogManager) GetProviderCostHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64) (*logstore.ProviderCostHistogramResult, error) { if filters == nil { return nil, fmt.Errorf("filters cannot be nil") } return p.plugin.GetProviderCostHistogram(ctx, *filters, bucketSizeSeconds) } func (p *PluginLogManager) GetProviderTokenHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64) (*logstore.ProviderTokenHistogramResult, error) { if filters == nil { return nil, fmt.Errorf("filters cannot be nil") } return p.plugin.GetProviderTokenHistogram(ctx, *filters, bucketSizeSeconds) } func (p *PluginLogManager) GetProviderLatencyHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64) (*logstore.ProviderLatencyHistogramResult, error) { if filters == nil { return nil, fmt.Errorf("filters cannot be nil") } return p.plugin.GetProviderLatencyHistogram(ctx, *filters, bucketSizeSeconds) } func (p *PluginLogManager) GetModelRankings(ctx context.Context, filters *logstore.SearchFilters) (*logstore.ModelRankingResult, error) { if filters == nil { return nil, fmt.Errorf("filters cannot be nil") } return p.plugin.GetModelRankings(ctx, *filters) } func (p *PluginLogManager) GetDroppedRequests(ctx context.Context) int64 { return p.plugin.droppedRequests.Load() } // GetAvailableModels returns all unique models from logs func (p *PluginLogManager) GetAvailableModels(ctx context.Context) []string { return p.plugin.GetAvailableModels(ctx) } // GetAvailableAliases returns all unique alias values from logs func (p *PluginLogManager) GetAvailableAliases(ctx context.Context) []string { return p.plugin.GetAvailableAliases(ctx) } // GetAvailableSelectedKeys returns all unique selected key ID-Name pairs from logs func (p *PluginLogManager) GetAvailableSelectedKeys(ctx context.Context) []KeyPair { return p.plugin.GetAvailableSelectedKeys(ctx) } // GetAvailableVirtualKeys returns all unique virtual key ID-Name pairs from logs func (p *PluginLogManager) GetAvailableVirtualKeys(ctx context.Context) []KeyPair { return p.plugin.GetAvailableVirtualKeys(ctx) } // GetAvailableRoutingRules returns all unique routing rule ID-Name pairs from logs func (p *PluginLogManager) GetAvailableRoutingRules(ctx context.Context) []KeyPair { return p.plugin.GetAvailableRoutingRules(ctx) } // GetAvailableRoutingEngines returns all unique routing engine types from logs func (p *PluginLogManager) GetAvailableRoutingEngines(ctx context.Context) []string { return p.plugin.GetAvailableRoutingEngines(ctx) } // GetAvailableTeams returns all unique team ID-Name pairs from logs. func (p *PluginLogManager) GetAvailableTeams(ctx context.Context) []KeyPair { return p.plugin.GetAvailableTeams(ctx) } // GetAvailableCustomers returns all unique customer ID-Name pairs from logs. func (p *PluginLogManager) GetAvailableCustomers(ctx context.Context) []KeyPair { return p.plugin.GetAvailableCustomers(ctx) } // GetAvailableUsers returns all unique user IDs from logs. func (p *PluginLogManager) GetAvailableUsers(ctx context.Context) []KeyPair { return p.plugin.GetAvailableUsers(ctx) } // GetAvailableBusinessUnits returns all unique business unit ID-Name pairs from logs. func (p *PluginLogManager) GetAvailableBusinessUnits(ctx context.Context) []KeyPair { return p.plugin.GetAvailableBusinessUnits(ctx) } // GetDimensionCostHistogram returns time-bucketed cost data grouped by the specified dimension. func (p *PluginLogManager) GetDimensionCostHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64, dimension logstore.HistogramDimension) (*logstore.DimensionCostHistogramResult, error) { if filters == nil { return nil, fmt.Errorf("filters cannot be nil") } return p.plugin.GetDimensionCostHistogram(ctx, *filters, bucketSizeSeconds, dimension) } // GetDimensionTokenHistogram returns time-bucketed token usage grouped by the specified dimension. func (p *PluginLogManager) GetDimensionTokenHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64, dimension logstore.HistogramDimension) (*logstore.DimensionTokenHistogramResult, error) { if filters == nil { return nil, fmt.Errorf("filters cannot be nil") } return p.plugin.GetDimensionTokenHistogram(ctx, *filters, bucketSizeSeconds, dimension) } // GetDimensionLatencyHistogram returns time-bucketed latency percentiles grouped by the specified dimension. func (p *PluginLogManager) GetDimensionLatencyHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64, dimension logstore.HistogramDimension) (*logstore.DimensionLatencyHistogramResult, error) { if filters == nil { return nil, fmt.Errorf("filters cannot be nil") } return p.plugin.GetDimensionLatencyHistogram(ctx, *filters, bucketSizeSeconds, dimension) } func (p *PluginLogManager) GetAvailableMetadataKeys(ctx context.Context) (map[string][]string, error) { if p.plugin == nil || p.plugin.store == nil { return map[string][]string{}, nil } return p.plugin.store.GetDistinctMetadataKeys(ctx) } // DeleteLog deletes a log from the log store func (p *PluginLogManager) DeleteLog(ctx context.Context, id string) error { if p.plugin == nil || p.plugin.store == nil { return fmt.Errorf("log store not initialized") } return p.plugin.store.DeleteLog(ctx, id) } // DeleteLogs deletes multiple logs from the log store func (p *PluginLogManager) DeleteLogs(ctx context.Context, ids []string) error { if p.plugin == nil || p.plugin.store == nil { return fmt.Errorf("log store not initialized") } return p.plugin.store.DeleteLogs(ctx, ids) } func (p *PluginLogManager) RecalculateCosts(ctx context.Context, filters *logstore.SearchFilters, limit int) (*RecalculateCostResult, error) { if filters == nil { return nil, fmt.Errorf("filters cannot be nil") } return p.plugin.RecalculateCosts(ctx, *filters, limit) } // SearchMCPToolLogs searches for MCP tool log entries based on filters and pagination func (p *PluginLogManager) SearchMCPToolLogs(ctx context.Context, filters *logstore.MCPToolLogSearchFilters, pagination *logstore.PaginationOptions) (*logstore.MCPToolLogSearchResult, error) { if filters == nil || pagination == nil { return nil, fmt.Errorf("filters and pagination cannot be nil") } return p.plugin.store.SearchMCPToolLogs(ctx, *filters, *pagination) } // GetMCPToolLogStats calculates statistics for MCP tool logs matching the given filters func (p *PluginLogManager) GetMCPToolLogStats(ctx context.Context, filters *logstore.MCPToolLogSearchFilters) (*logstore.MCPToolLogStats, error) { if filters == nil { return nil, fmt.Errorf("filters cannot be nil") } return p.plugin.store.GetMCPToolLogStats(ctx, *filters) } // GetAvailableToolNames returns all unique tool names from MCP tool logs func (p *PluginLogManager) GetAvailableToolNames(ctx context.Context) ([]string, error) { if p == nil || p.plugin == nil || p.plugin.store == nil { return []string{}, nil } return p.plugin.store.GetAvailableToolNames(ctx) } // GetAvailableServerLabels returns all unique server labels from MCP tool logs func (p *PluginLogManager) GetAvailableServerLabels(ctx context.Context) ([]string, error) { if p == nil || p.plugin == nil || p.plugin.store == nil { return []string{}, nil } return p.plugin.store.GetAvailableServerLabels(ctx) } // GetAvailableMCPVirtualKeys returns all unique virtual key ID-Name pairs from MCP tool logs func (p *PluginLogManager) GetAvailableMCPVirtualKeys(ctx context.Context) []KeyPair { if p == nil || p.plugin == nil { return []KeyPair{} } return p.plugin.GetAvailableMCPVirtualKeys(ctx) } // GetMCPHistogram returns time-bucketed MCP tool call volume func (p *PluginLogManager) GetMCPHistogram(ctx context.Context, filters logstore.MCPToolLogSearchFilters, bucketSizeSeconds int64) (*logstore.MCPHistogramResult, error) { if p.plugin == nil || p.plugin.store == nil { return &logstore.MCPHistogramResult{}, nil } return p.plugin.store.GetMCPHistogram(ctx, filters, bucketSizeSeconds) } // GetMCPCostHistogram returns time-bucketed MCP cost data func (p *PluginLogManager) GetMCPCostHistogram(ctx context.Context, filters logstore.MCPToolLogSearchFilters, bucketSizeSeconds int64) (*logstore.MCPCostHistogramResult, error) { if p.plugin == nil || p.plugin.store == nil { return &logstore.MCPCostHistogramResult{}, nil } return p.plugin.store.GetMCPCostHistogram(ctx, filters, bucketSizeSeconds) } // GetMCPTopTools returns the top N MCP tools by call count func (p *PluginLogManager) GetMCPTopTools(ctx context.Context, filters logstore.MCPToolLogSearchFilters, limit int) (*logstore.MCPTopToolsResult, error) { if p.plugin == nil || p.plugin.store == nil { return &logstore.MCPTopToolsResult{}, nil } return p.plugin.store.GetMCPTopTools(ctx, filters, limit) } // DeleteMCPToolLogs deletes multiple MCP tool log entries by their IDs func (p *PluginLogManager) DeleteMCPToolLogs(ctx context.Context, ids []string) error { if p.plugin == nil || p.plugin.store == nil { return fmt.Errorf("log store not initialized") } return p.plugin.store.DeleteMCPToolLogs(ctx, ids) } // GetPluginLogManager returns a LogManager interface for this plugin func (p *LoggerPlugin) GetPluginLogManager() *PluginLogManager { return &PluginLogManager{ plugin: p, } } // retryOnNotFound retries a function up to 3 times with 1-second delays if it returns logstore.ErrNotFound func retryOnNotFound(ctx context.Context, operation func() error) error { const maxRetries = 3 const retryDelay = time.Second var lastErr error for attempt := range maxRetries { err := operation() if err == nil { return nil } // Check if the error is logstore.ErrNotFound if !errors.Is(err, logstore.ErrNotFound) { return err } lastErr = err // Don't wait after the last attempt if attempt < maxRetries-1 { select { case <-ctx.Done(): return ctx.Err() case <-time.After(retryDelay): // Continue to next retry } } } return lastErr } // extractInputHistory extracts input history from request input func (p *LoggerPlugin) extractInputHistory(request *schemas.BifrostRequest) ([]schemas.ChatMessage, []schemas.ResponsesMessage) { if request.ChatRequest != nil { return request.ChatRequest.Input, []schemas.ResponsesMessage{} } if request.RequestType == schemas.RealtimeRequest && request.ResponsesRequest != nil { return extractRealtimeInputHistory(request.ResponsesRequest.Input), []schemas.ResponsesMessage{} } if request.ResponsesRequest != nil && len(request.ResponsesRequest.Input) > 0 { return []schemas.ChatMessage{}, request.ResponsesRequest.Input } if request.TextCompletionRequest != nil { if request.TextCompletionRequest.Input == nil { return []schemas.ChatMessage{}, []schemas.ResponsesMessage{} } var text string if request.TextCompletionRequest.Input.PromptStr != nil { text = *request.TextCompletionRequest.Input.PromptStr } else { var stringBuilder strings.Builder for _, prompt := range request.TextCompletionRequest.Input.PromptArray { stringBuilder.WriteString(prompt) } text = stringBuilder.String() } return []schemas.ChatMessage{ { Role: schemas.ChatMessageRoleUser, Content: &schemas.ChatMessageContent{ ContentStr: &text, }, }, }, []schemas.ResponsesMessage{} } if request.EmbeddingRequest != nil { // Large payload passthrough can intentionally leave Input nil to avoid // materializing giant request bodies. Logging should degrade gracefully. if request.EmbeddingRequest.Input == nil { return []schemas.ChatMessage{}, []schemas.ResponsesMessage{} } texts := request.EmbeddingRequest.Input.Texts if len(texts) == 0 && request.EmbeddingRequest.Input.Text != nil { texts = []string{*request.EmbeddingRequest.Input.Text} } contentBlocks := make([]schemas.ChatContentBlock, len(texts)) for i, text := range texts { // Create a per-iteration copy to avoid reusing the same memory address t := text contentBlocks[i] = schemas.ChatContentBlock{ Type: schemas.ChatContentBlockTypeText, Text: &t, } } return []schemas.ChatMessage{ { Role: schemas.ChatMessageRoleUser, Content: &schemas.ChatMessageContent{ ContentBlocks: contentBlocks, }, }, }, []schemas.ResponsesMessage{} } if request.RerankRequest != nil { query := request.RerankRequest.Query return []schemas.ChatMessage{ { Role: schemas.ChatMessageRoleUser, Content: &schemas.ChatMessageContent{ ContentStr: &query, }, }, }, []schemas.ResponsesMessage{} } if request.CountTokensRequest != nil && len(request.CountTokensRequest.Input) > 0 { return []schemas.ChatMessage{}, request.CountTokensRequest.Input } return []schemas.ChatMessage{}, []schemas.ResponsesMessage{} } func extractRealtimeInputHistory(input []schemas.ResponsesMessage) []schemas.ChatMessage { messages := make([]schemas.ChatMessage, 0, len(input)) for _, item := range input { if item.Type == nil { continue } switch *item.Type { case schemas.ResponsesMessageTypeMessage: if item.Role == nil || item.Content == nil { continue } content := extractRealtimeResponsesContent(item.Content) if content == "" { continue } messages = append(messages, schemas.ChatMessage{ Role: mapRealtimeResponsesRole(*item.Role), Content: &schemas.ChatMessageContent{ ContentStr: schemas.Ptr(content), }, }) case schemas.ResponsesMessageTypeFunctionCallOutput, schemas.ResponsesMessageTypeCustomToolCallOutput, schemas.ResponsesMessageTypeLocalShellCallOutput, schemas.ResponsesMessageTypeComputerCallOutput: content := extractRealtimeToolOutputContent(item.ResponsesToolMessage) if content == "" { continue } messages = append(messages, schemas.ChatMessage{ Role: schemas.ChatMessageRoleTool, Content: &schemas.ChatMessageContent{ ContentStr: schemas.Ptr(content), }, ChatToolMessage: &schemas.ChatToolMessage{ ToolCallID: item.ResponsesToolMessage.CallID, }, }) } } return messages } func mapRealtimeResponsesRole(role schemas.ResponsesMessageRoleType) schemas.ChatMessageRole { switch role { case schemas.ResponsesInputMessageRoleAssistant: return schemas.ChatMessageRoleAssistant case schemas.ResponsesInputMessageRoleSystem: return schemas.ChatMessageRoleSystem case schemas.ResponsesInputMessageRoleDeveloper: return schemas.ChatMessageRoleDeveloper default: return schemas.ChatMessageRoleUser } } func extractRealtimeResponsesContent(content *schemas.ResponsesMessageContent) string { if content == nil { return "" } if content.ContentStr != nil { return strings.TrimSpace(*content.ContentStr) } parts := make([]string, 0, len(content.ContentBlocks)) for _, block := range content.ContentBlocks { switch { case block.Text != nil && strings.TrimSpace(*block.Text) != "": parts = append(parts, strings.TrimSpace(*block.Text)) case block.ResponsesOutputMessageContentRefusal != nil && strings.TrimSpace(block.Refusal) != "": parts = append(parts, strings.TrimSpace(block.Refusal)) } } return strings.TrimSpace(strings.Join(parts, "\n")) } func extractRealtimeToolOutputContent(toolMessage *schemas.ResponsesToolMessage) string { if toolMessage == nil || toolMessage.Output == nil { return "" } switch { case toolMessage.Output.ResponsesToolCallOutputStr != nil: return strings.TrimSpace(*toolMessage.Output.ResponsesToolCallOutputStr) case len(toolMessage.Output.ResponsesFunctionToolCallOutputBlocks) > 0: content := &schemas.ResponsesMessageContent{ContentBlocks: toolMessage.Output.ResponsesFunctionToolCallOutputBlocks} return extractRealtimeResponsesContent(content) default: return "" } } // convertToProcessedStreamResponse converts a StreamAccumulatorResult to ProcessedStreamResponse // for use with the logging plugin's streaming log update functionality. func convertToProcessedStreamResponse(result *schemas.StreamAccumulatorResult, requestType schemas.RequestType) *streaming.ProcessedStreamResponse { if result == nil { return nil } // Determine stream type from request type var streamType streaming.StreamType switch requestType { case schemas.TextCompletionStreamRequest: streamType = streaming.StreamTypeText case schemas.ChatCompletionStreamRequest: streamType = streaming.StreamTypeChat case schemas.ResponsesStreamRequest: streamType = streaming.StreamTypeResponses case schemas.SpeechStreamRequest: streamType = streaming.StreamTypeAudio case schemas.TranscriptionStreamRequest: streamType = streaming.StreamTypeTranscription case schemas.ImageGenerationStreamRequest: streamType = streaming.StreamTypeImage default: streamType = streaming.StreamTypeChat } // Build accumulated data data := &streaming.AccumulatedData{ RequestID: result.RequestID, Model: result.RequestedModel, Status: result.Status, Stream: true, Latency: result.Latency, TimeToFirstToken: result.TimeToFirstToken, OutputMessage: result.OutputMessage, OutputMessages: result.OutputMessages, ErrorDetails: result.ErrorDetails, TokenUsage: result.TokenUsage, Cost: result.Cost, AudioOutput: result.AudioOutput, TranscriptionOutput: result.TranscriptionOutput, ImageGenerationOutput: result.ImageGenerationOutput, FinishReason: result.FinishReason, RawResponse: result.RawResponse, } // Handle tool calls if present if result.OutputMessage != nil && result.OutputMessage.ChatAssistantMessage != nil { data.ToolCalls = result.OutputMessage.ChatAssistantMessage.ToolCalls } resp := &streaming.ProcessedStreamResponse{ RequestID: result.RequestID, StreamType: streamType, Provider: result.Provider, RequestedModel: result.RequestedModel, ResolvedModel: result.ResolvedModel, Data: data, } if result.RawRequest != nil { rawReq := result.RawRequest resp.RawRequest = &rawReq } return resp } func mergeRealtimeMetadata(metadata map[string]interface{}, ctx *schemas.BifrostContext) map[string]interface{} { if ctx == nil { return metadata } set := func(key string, ctxKey schemas.BifrostContextKey) { if value := bifrost.GetStringFromContext(ctx, ctxKey); value != "" { if metadata == nil { metadata = make(map[string]interface{}) } metadata[key] = value } } set("realtime_session_id", schemas.BifrostContextKeyRealtimeSessionID) set("provider_session_id", schemas.BifrostContextKeyRealtimeProviderSessionID) set("realtime_source", schemas.BifrostContextKeyRealtimeSource) set("realtime_event_type", schemas.BifrostContextKeyRealtimeEventType) if bifrost.GetStringFromContext(ctx, schemas.BifrostContextKeyRealtimeSessionID) != "" { if metadata == nil { metadata = make(map[string]interface{}) } metadata["realtime"] = true } return metadata } // formatRoutingEngineLogs formats routing engine logs into a human-readable string. // Format: [timestamp] [engine] - message // Parameters: // - logs: Slice of routing engine log entries // // Returns: // - string: Formatted log string (empty string if no logs) func formatRoutingEngineLogs(logs []schemas.RoutingEngineLogEntry) string { if len(logs) == 0 { return "" } var sb strings.Builder for _, log := range logs { sb.WriteString(fmt.Sprintf("[%d] [%s] - %s\n", log.Timestamp, log.Engine, log.Message)) } return sb.String() }