| |
| package logging |
|
|
| import ( |
| "context" |
| "errors" |
| "fmt" |
| "strings" |
| "time" |
|
|
| "github.com/maximhq/bifrost/core/schemas" |
| "github.com/maximhq/bifrost/framework/logstore" |
| "github.com/maximhq/bifrost/framework/streaming" |
| ) |
|
|
| |
| type KeyPair struct { |
| ID string `json:"id"` |
| Name string `json:"name"` |
| } |
|
|
| |
| type LogManager interface { |
| |
| GetLog(ctx context.Context, id string) (*logstore.Log, error) |
|
|
| |
| Search(ctx context.Context, filters *logstore.SearchFilters, pagination *logstore.PaginationOptions) (*logstore.SearchResult, error) |
|
|
| |
| GetStats(ctx context.Context, filters *logstore.SearchFilters) (*logstore.SearchStats, error) |
|
|
| |
| GetHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64) (*logstore.HistogramResult, error) |
|
|
| |
| GetTokenHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64) (*logstore.TokenHistogramResult, error) |
|
|
| |
| GetCostHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64) (*logstore.CostHistogramResult, error) |
|
|
| |
| GetModelHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64) (*logstore.ModelHistogramResult, error) |
|
|
| |
| GetLatencyHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64) (*logstore.LatencyHistogramResult, error) |
|
|
| |
| GetProviderCostHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64) (*logstore.ProviderCostHistogramResult, error) |
|
|
| |
| GetProviderTokenHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64) (*logstore.ProviderTokenHistogramResult, error) |
|
|
| |
| GetProviderLatencyHistogram(ctx context.Context, filters *logstore.SearchFilters, bucketSizeSeconds int64) (*logstore.ProviderLatencyHistogramResult, error) |
|
|
| |
| GetModelRankings(ctx context.Context, filters *logstore.SearchFilters) (*logstore.ModelRankingResult, error) |
|
|
| |
| GetDroppedRequests(ctx context.Context) int64 |
|
|
| |
| GetAvailableModels(ctx context.Context) []string |
|
|
| |
| GetAvailableSelectedKeys(ctx context.Context) []KeyPair |
|
|
| |
| GetAvailableVirtualKeys(ctx context.Context) []KeyPair |
|
|
| |
| GetAvailableRoutingRules(ctx context.Context) []KeyPair |
|
|
| |
| GetAvailableRoutingEngines(ctx context.Context) []string |
|
|
| |
| GetAvailableMetadataKeys(ctx context.Context) (map[string][]string, error) |
|
|
| |
| DeleteLog(ctx context.Context, id string) error |
|
|
| |
| DeleteLogs(ctx context.Context, ids []string) error |
|
|
| |
| RecalculateCosts(ctx context.Context, filters *logstore.SearchFilters, limit int) (*RecalculateCostResult, error) |
|
|
| |
| |
| SearchMCPToolLogs(ctx context.Context, filters *logstore.MCPToolLogSearchFilters, pagination *logstore.PaginationOptions) (*logstore.MCPToolLogSearchResult, error) |
|
|
| |
| GetMCPToolLogStats(ctx context.Context, filters *logstore.MCPToolLogSearchFilters) (*logstore.MCPToolLogStats, error) |
|
|
| |
| GetAvailableToolNames(ctx context.Context) ([]string, error) |
|
|
| |
| GetAvailableServerLabels(ctx context.Context) ([]string, error) |
|
|
| |
| GetAvailableMCPVirtualKeys(ctx context.Context) []KeyPair |
|
|
| |
| GetMCPHistogram(ctx context.Context, filters logstore.MCPToolLogSearchFilters, bucketSizeSeconds int64) (*logstore.MCPHistogramResult, error) |
|
|
| |
| GetMCPCostHistogram(ctx context.Context, filters logstore.MCPToolLogSearchFilters, bucketSizeSeconds int64) (*logstore.MCPCostHistogramResult, error) |
|
|
| |
| GetMCPTopTools(ctx context.Context, filters logstore.MCPToolLogSearchFilters, limit int) (*logstore.MCPTopToolsResult, error) |
|
|
| |
| DeleteMCPToolLogs(ctx context.Context, ids []string) error |
| } |
|
|
| |
| 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) 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() |
| } |
|
|
| |
| func (p *PluginLogManager) GetAvailableModels(ctx context.Context) []string { |
| return p.plugin.GetAvailableModels(ctx) |
| } |
|
|
| |
| func (p *PluginLogManager) GetAvailableSelectedKeys(ctx context.Context) []KeyPair { |
| return p.plugin.GetAvailableSelectedKeys(ctx) |
| } |
|
|
| |
| func (p *PluginLogManager) GetAvailableVirtualKeys(ctx context.Context) []KeyPair { |
| return p.plugin.GetAvailableVirtualKeys(ctx) |
| } |
|
|
| |
| func (p *PluginLogManager) GetAvailableRoutingRules(ctx context.Context) []KeyPair { |
| return p.plugin.GetAvailableRoutingRules(ctx) |
| } |
|
|
| |
| func (p *PluginLogManager) GetAvailableRoutingEngines(ctx context.Context) []string { |
| return p.plugin.GetAvailableRoutingEngines(ctx) |
| } |
|
|
| 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) |
| } |
|
|
| |
| 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) |
| } |
|
|
| |
| 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) |
| } |
|
|
| |
| 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) |
| } |
|
|
| |
| 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) |
| } |
|
|
| |
| 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) |
| } |
|
|
| |
| 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) |
| } |
|
|
| |
| func (p *PluginLogManager) GetAvailableMCPVirtualKeys(ctx context.Context) []KeyPair { |
| if p == nil || p.plugin == nil { |
| return []KeyPair{} |
| } |
| return p.plugin.GetAvailableMCPVirtualKeys(ctx) |
| } |
|
|
| |
| 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) |
| } |
|
|
| |
| 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) |
| } |
|
|
| |
| 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) |
| } |
|
|
| |
| 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) |
| } |
|
|
| |
| func (p *LoggerPlugin) GetPluginLogManager() *PluginLogManager { |
| return &PluginLogManager{ |
| plugin: p, |
| } |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| if !errors.Is(err, logstore.ErrNotFound) { |
| return err |
| } |
|
|
| lastErr = err |
|
|
| |
| if attempt < maxRetries-1 { |
| select { |
| case <-ctx.Done(): |
| return ctx.Err() |
| case <-time.After(retryDelay): |
| |
| } |
| } |
| } |
|
|
| return lastErr |
| } |
|
|
| |
| func (p *LoggerPlugin) extractInputHistory(request *schemas.BifrostRequest) ([]schemas.ChatMessage, []schemas.ResponsesMessage) { |
| if request.ChatRequest != nil { |
| return request.ChatRequest.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 { |
| |
| |
| 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 { |
| |
| 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 convertToProcessedStreamResponse(result *schemas.StreamAccumulatorResult, requestType schemas.RequestType) *streaming.ProcessedStreamResponse { |
| if result == nil { |
| return nil |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| data := &streaming.AccumulatedData{ |
| RequestID: result.RequestID, |
| Model: result.Model, |
| 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, |
| } |
|
|
| |
| 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, |
| Model: result.Model, |
| Data: data, |
| } |
|
|
| if result.RawRequest != nil { |
| rawReq := result.RawRequest |
| resp.RawRequest = &rawReq |
| } |
|
|
| return resp |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| 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() |
| } |
|
|