| package jsonparser |
|
|
| import ( |
| "strings" |
| "sync" |
| "time" |
|
|
| bifrost "github.com/maximhq/bifrost/core" |
| "github.com/maximhq/bifrost/core/schemas" |
| ) |
|
|
| const ( |
| PluginName = "streaming-json-parser" |
| ) |
|
|
| type Usage string |
|
|
| const ( |
| AllRequests Usage = "all_requests" |
| PerRequest Usage = "per_request" |
| ) |
|
|
| |
| type AccumulatedContent struct { |
| Content *strings.Builder |
| Timestamp time.Time |
| } |
|
|
| |
| |
| type JsonParserPlugin struct { |
| usage Usage |
| |
| accumulatedContent map[string]*AccumulatedContent |
| mutex sync.RWMutex |
| |
| cleanupInterval time.Duration |
| maxAge time.Duration |
| stopCleanup chan struct{} |
| stopOnce sync.Once |
| } |
|
|
| |
| type PluginConfig struct { |
| Usage Usage |
| CleanupInterval time.Duration |
| MaxAge time.Duration |
| } |
|
|
| const ( |
| EnableStreamingJSONParser schemas.BifrostContextKey = "enable-streaming-json-parser" |
| ) |
|
|
| |
| func Init(config PluginConfig) (*JsonParserPlugin, error) { |
| |
| if config.CleanupInterval <= 0 { |
| config.CleanupInterval = 5 * time.Minute |
| } |
| if config.MaxAge <= 0 { |
| config.MaxAge = 30 * time.Minute |
| } |
| if config.Usage == "" { |
| config.Usage = PerRequest |
| } |
|
|
| plugin := &JsonParserPlugin{ |
| usage: config.Usage, |
| accumulatedContent: make(map[string]*AccumulatedContent), |
| cleanupInterval: config.CleanupInterval, |
| maxAge: config.MaxAge, |
| stopCleanup: make(chan struct{}), |
| } |
|
|
| |
| go plugin.startCleanupGoroutine() |
|
|
| return plugin, nil |
| } |
|
|
| |
| func (p *JsonParserPlugin) GetName() string { |
| return PluginName |
| } |
|
|
| |
| func (p *JsonParserPlugin) HTTPTransportPreHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest) (*schemas.HTTPResponse, error) { |
| return nil, nil |
| } |
|
|
| |
| func (p *JsonParserPlugin) HTTPTransportPostHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error { |
| return nil |
| } |
|
|
| |
| func (p *JsonParserPlugin) HTTPTransportStreamChunkHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, chunk *schemas.BifrostStreamChunk) (*schemas.BifrostStreamChunk, error) { |
| return chunk, nil |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| func (p *JsonParserPlugin) PreLLMHook(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.LLMPluginShortCircuit, error) { |
| return req, nil, nil |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| func (p *JsonParserPlugin) PostLLMHook(ctx *schemas.BifrostContext, result *schemas.BifrostResponse, err *schemas.BifrostError) (*schemas.BifrostResponse, *schemas.BifrostError, error) { |
| |
| if err != nil { |
| return result, err, nil |
| } |
|
|
| extraFields := result.GetExtraFields() |
|
|
| |
| if !p.shouldRun(ctx, extraFields.RequestType) { |
| return result, err, nil |
| } |
|
|
| |
| if result == nil || result.ChatResponse == nil { |
| return result, err, nil |
| } |
|
|
| |
| requestID := p.getRequestID(ctx, result) |
| if requestID == "" { |
| return result, err, nil |
| } |
|
|
| |
| |
| resultCopy := p.deepCopyBifrostResponse(result) |
| if resultCopy == nil || resultCopy.ChatResponse == nil { |
| return result, err, nil |
| } |
|
|
| |
| if len(resultCopy.ChatResponse.Choices) > 0 { |
| for i := range resultCopy.ChatResponse.Choices { |
| choice := &resultCopy.ChatResponse.Choices[i] |
|
|
| |
| if choice.ChatStreamResponseChoice != nil { |
| if choice.ChatStreamResponseChoice.Delta.Content != nil { |
| content := *choice.ChatStreamResponseChoice.Delta.Content |
| if content != "" { |
| |
| accumulated := p.accumulateContent(requestID, content) |
|
|
| |
| fixedContent := p.parsePartialJSON(accumulated) |
|
|
| if !p.isValidJSON(fixedContent) { |
| err = &schemas.BifrostError{ |
| Error: &schemas.ErrorField{ |
| Message: "Invalid JSON in streaming response", |
| }, |
| StreamControl: &schemas.StreamControl{ |
| SkipStream: bifrost.Ptr(true), |
| }, |
| } |
|
|
| return nil, err, nil |
| } |
|
|
| |
| choice.ChatStreamResponseChoice.Delta.Content = &fixedContent |
| } |
| } |
| } |
| } |
| } |
|
|
| |
| if streamEndIndicatorValue := ctx.Value(schemas.BifrostContextKeyStreamEndIndicator); streamEndIndicatorValue != nil { |
| isFinalChunk, ok := streamEndIndicatorValue.(bool) |
| if ok && isFinalChunk { |
| p.ClearRequestState(requestID) |
| } |
| } |
|
|
| |
| return resultCopy, err, nil |
| } |
|
|
| |
| func (p *JsonParserPlugin) Cleanup() error { |
| |
| p.StopCleanup() |
|
|
| p.mutex.Lock() |
| defer p.mutex.Unlock() |
|
|
| |
| p.accumulatedContent = make(map[string]*AccumulatedContent) |
| return nil |
| } |
|
|
| |
| func (p *JsonParserPlugin) ClearRequestState(requestID string) { |
| p.mutex.Lock() |
| defer p.mutex.Unlock() |
|
|
| delete(p.accumulatedContent, requestID) |
| } |
|
|
| |
|
|
| |
| func (p *JsonParserPlugin) startCleanupGoroutine() { |
| ticker := time.NewTicker(p.cleanupInterval) |
| defer ticker.Stop() |
|
|
| for { |
| select { |
| case <-ticker.C: |
| p.cleanupOldEntries() |
| case <-p.stopCleanup: |
| return |
| } |
| } |
| } |
|
|
| |
| func (p *JsonParserPlugin) cleanupOldEntries() { |
| p.mutex.Lock() |
| defer p.mutex.Unlock() |
|
|
| now := time.Now() |
| cutoff := now.Add(-p.maxAge) |
|
|
| for requestID, content := range p.accumulatedContent { |
| if content.Timestamp.Before(cutoff) { |
| delete(p.accumulatedContent, requestID) |
| } |
| } |
| } |
|
|
| |
| func (p *JsonParserPlugin) StopCleanup() { |
| p.stopOnce.Do(func() { |
| close(p.stopCleanup) |
| }) |
| } |
|
|