Files
bifrost/plugins/logging/utils.go
Beyhan Oğur 880f412e2c first commit
2026-04-26 21:52:23 +03:00

755 lines
30 KiB
Go

// 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()
}