first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

View File

@@ -0,0 +1,454 @@
// Package maxim provides attachment extraction from Bifrost requests for Maxim logging.
package maxim
import (
"encoding/base64"
"log"
"net/http"
"net/url"
"path"
"strings"
"github.com/google/uuid"
"github.com/maximhq/bifrost/core/schemas"
"github.com/maximhq/maxim-go/logging"
)
// ExtractAttachmentsFromRequest extracts image_url, file, and input_audio blocks from
// Chat and Responses API messages, and input_images from image generation requests,
// converting them to maxim-go attachment types.
// Returns a slice of *logging.UrlAttachment or *logging.FileDataAttachment for use with
// Logger.GenerationAddAttachment.
func ExtractAttachmentsFromRequest(req *schemas.BifrostRequest) []interface{} {
if req == nil {
return nil
}
switch req.RequestType {
case schemas.ChatCompletionRequest, schemas.ChatCompletionStreamRequest:
return extractFromChatRequest(req.ChatRequest)
case schemas.ResponsesRequest, schemas.ResponsesStreamRequest:
return extractFromResponsesRequest(req.ResponsesRequest)
case schemas.ImageGenerationRequest, schemas.ImageGenerationStreamRequest:
return extractFromImageGenerationRequest(req.ImageGenerationRequest)
case schemas.ImageEditRequest, schemas.ImageEditStreamRequest:
return extractFromImageEditRequest(req.ImageEditRequest)
default:
return nil
}
}
func extractFromImageGenerationRequest(igr *schemas.BifrostImageGenerationRequest) []interface{} {
if igr == nil || igr.Params == nil || len(igr.Params.InputImages) == 0 {
return nil
}
var attachments []interface{}
for _, img := range igr.Params.InputImages {
if att := inputImageStringToAttachment(img); att != nil {
attachments = append(attachments, att)
}
}
return attachments
}
func extractFromImageEditRequest(ier *schemas.BifrostImageEditRequest) []interface{} {
if ier == nil || ier.Input == nil || len(ier.Input.Images) == 0 {
return nil
}
var attachments []interface{}
for i, img := range ier.Input.Images {
if att := imageInputToAttachment(img, i); att != nil {
attachments = append(attachments, att)
}
}
return attachments
}
// imageInputToAttachment converts a raw ImageInput (binary image bytes) to a maxim FileDataAttachment.
// idx is appended to the filename when greater than zero (for multi-image edit requests).
func imageInputToAttachment(img schemas.ImageInput, idx int) interface{} {
if len(img.Image) == 0 {
return nil
}
mime := http.DetectContentType(img.Image)
if !strings.HasPrefix(mime, "image/") {
mime = "image/png"
}
ext := extFromMime(mime)
var name string
if idx == 0 {
name = "input_image." + ext
} else {
digits := []byte{byte('0' + idx%10)}
if idx >= 10 {
digits = []byte{byte('0' + idx/10), byte('0' + idx%10)}
}
name = "input_image_" + string(digits) + "." + ext
}
return &logging.FileDataAttachment{
BaseAttachmentProps: logging.BaseAttachmentProps{
ID: uuid.New().String(),
Name: name,
MimeType: mime,
},
Type: logging.AttachmentTypeFileData,
Data: img.Image,
}
}
// inputImageStringToAttachment maps ImageGenerationParameters.InputImages entries (URL,
// data URL, or raw base64) to maxim attachment types.
func inputImageStringToAttachment(s string) interface{} {
if s == "" {
return nil
}
if strings.HasPrefix(s, "data:") || strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://") {
return urlToAttachment(s, "image", "")
}
decoded, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return urlToAttachment(s, "image", "")
}
mime := http.DetectContentType(decoded)
if !strings.HasPrefix(mime, "image/") {
mime = "image/png"
}
name := "input_image." + extFromMime(mime)
return &logging.FileDataAttachment{
BaseAttachmentProps: logging.BaseAttachmentProps{
ID: uuid.New().String(),
Name: name,
MimeType: mime,
},
Type: logging.AttachmentTypeFileData,
Data: decoded,
}
}
func extractFromChatRequest(cr *schemas.BifrostChatRequest) []interface{} {
if cr == nil || cr.Input == nil {
return nil
}
var attachments []interface{}
for _, msg := range cr.Input {
if msg.Content == nil || msg.Content.ContentBlocks == nil {
continue
}
for _, block := range msg.Content.ContentBlocks {
if att := chatBlockToAttachment(block); att != nil {
attachments = append(attachments, att)
}
}
}
return attachments
}
func extractFromResponsesRequest(rr *schemas.BifrostResponsesRequest) []interface{} {
if rr == nil || rr.Input == nil {
return nil
}
var attachments []interface{}
for _, msg := range rr.Input {
if msg.Content == nil || msg.Content.ContentBlocks == nil {
continue
}
for _, block := range msg.Content.ContentBlocks {
if att := responsesBlockToAttachment(block); att != nil {
attachments = append(attachments, att)
}
}
}
return attachments
}
func chatBlockToAttachment(block schemas.ChatContentBlock) interface{} {
switch block.Type {
case schemas.ChatContentBlockTypeImage:
if block.ImageURLStruct != nil && block.ImageURLStruct.URL != "" {
return urlToAttachment(block.ImageURLStruct.URL, "image", "")
}
case schemas.ChatContentBlockTypeFile:
if block.File != nil {
return chatFileToAttachment(block.File)
}
case schemas.ChatContentBlockTypeInputAudio:
if block.InputAudio != nil && block.InputAudio.Data != "" {
return audioDataToAttachment(block.InputAudio.Data, block.InputAudio.Format)
}
}
return nil
}
func responsesBlockToAttachment(block schemas.ResponsesMessageContentBlock) interface{} {
switch block.Type {
case schemas.ResponsesInputMessageContentBlockTypeImage:
if block.ImageURL != nil && *block.ImageURL != "" {
return urlToAttachment(*block.ImageURL, "image", "")
}
case schemas.ResponsesInputMessageContentBlockTypeFile:
return responsesFileToAttachment(&block)
case schemas.ResponsesInputMessageContentBlockTypeAudio:
if block.Audio != nil && block.Audio.Data != "" {
format := block.Audio.Format
if format == "" {
format = "mp3"
}
return audioDataToAttachment(block.Audio.Data, &format)
}
}
return nil
}
func responsesFileToAttachment(block *schemas.ResponsesMessageContentBlock) interface{} {
if block.FileURL != nil && *block.FileURL != "" {
name := "attachment"
if block.Filename != nil && *block.Filename != "" {
name = *block.Filename
}
mime := ""
if block.FileType != nil {
mime = *block.FileType
}
urlStr := *block.FileURL
return &logging.UrlAttachment{
BaseAttachmentProps: logging.BaseAttachmentProps{
ID: uuid.New().String(),
Name: name,
MimeType: mime,
Metadata: map[string]string{"url": urlStr},
},
Type: logging.AttachmentTypeURL,
URL: urlStr,
}
}
if block.FileData != nil && *block.FileData != "" {
data, err := base64.StdEncoding.DecodeString(*block.FileData)
if err != nil {
log.Printf("%s failed to decode file_data base64: %v", PluginLoggerPrefix, err)
return nil
}
name := "attachment"
if block.Filename != nil && *block.Filename != "" {
name = *block.Filename
}
mime := "application/octet-stream"
if block.FileType != nil && *block.FileType != "" {
mime = *block.FileType
}
return &logging.FileDataAttachment{
BaseAttachmentProps: logging.BaseAttachmentProps{
ID: uuid.New().String(),
Name: name,
MimeType: mime,
},
Type: logging.AttachmentTypeFileData,
Data: data,
}
}
return nil
}
func chatFileToAttachment(f *schemas.ChatInputFile) interface{} {
if f.FileURL != nil && *f.FileURL != "" {
name := "attachment"
if f.Filename != nil && *f.Filename != "" {
name = *f.Filename
}
mime := ""
if f.FileType != nil {
mime = *f.FileType
}
urlStr := *f.FileURL
return &logging.UrlAttachment{
BaseAttachmentProps: logging.BaseAttachmentProps{
ID: uuid.New().String(),
Name: name,
MimeType: mime,
Metadata: map[string]string{"url": urlStr},
},
Type: logging.AttachmentTypeURL,
URL: urlStr,
}
}
if f.FileData != nil && *f.FileData != "" {
data, err := base64.StdEncoding.DecodeString(*f.FileData)
if err != nil {
log.Printf("%s failed to decode file_data base64: %v", PluginLoggerPrefix, err)
return nil
}
name := "attachment"
if f.Filename != nil && *f.Filename != "" {
name = *f.Filename
}
mime := "application/octet-stream"
if f.FileType != nil && *f.FileType != "" {
mime = *f.FileType
}
return &logging.FileDataAttachment{
BaseAttachmentProps: logging.BaseAttachmentProps{
ID: uuid.New().String(),
Name: name,
MimeType: mime,
},
Type: logging.AttachmentTypeFileData,
Data: data,
}
}
return nil
}
func audioDataToAttachment(data string, format *string) interface{} {
// Data can be base64 or a data URL (data:audio/wav;base64,...)
var b64 string
if strings.HasPrefix(data, "data:") {
idx := strings.Index(data, ";base64,")
if idx == -1 {
log.Printf("%s invalid audio data URL format", PluginLoggerPrefix)
return nil
}
b64 = data[idx+8:]
} else {
b64 = data
}
decoded, err := base64.StdEncoding.DecodeString(b64)
if err != nil {
log.Printf("%s failed to decode audio base64: %v", PluginLoggerPrefix, err)
return nil
}
mime := "audio/mpeg"
if format != nil {
switch strings.ToLower(*format) {
case "wav":
mime = "audio/wav"
case "mp3":
mime = "audio/mpeg"
default:
mime = "audio/" + *format
}
}
return &logging.FileDataAttachment{
BaseAttachmentProps: logging.BaseAttachmentProps{
ID: uuid.New().String(),
Name: "audio." + extFromMime(mime),
MimeType: mime,
},
Type: logging.AttachmentTypeFileData,
Data: decoded,
}
}
// urlToAttachment builds a UrlAttachment (e.g. chat vision image_url). For images, MIME is:
// outputFormat when set → URL rsct query (e.g. Azure) → image/png.
func urlToAttachment(urlStr string, kind string, outputFormat string) interface{} {
if strings.HasPrefix(urlStr, "data:") {
return dataURLToAttachment(urlStr, kind)
}
// HTTP/HTTPS URL
name := "attachment"
mime := ""
u, errParse := url.Parse(urlStr)
if errParse == nil {
if p := path.Base(u.Path); p != "" && p != "." {
name = p
}
}
if kind == "image" {
mime = imageOutputFormatToMime(outputFormat)
if mime == "" && errParse == nil {
mime = imageOutputFormatToMime(strings.TrimPrefix(strings.ToLower(path.Ext(u.Path)), "."))
}
if mime == "" && errParse == nil {
if q := u.Query().Get("rsct"); q != "" {
mime = q
}
}
if mime == "" {
mime = "image/png"
}
}
return &logging.UrlAttachment{
BaseAttachmentProps: logging.BaseAttachmentProps{
ID: uuid.New().String(),
Name: name,
MimeType: mime,
Metadata: map[string]string{"url": urlStr},
},
Type: logging.AttachmentTypeURL,
URL: urlStr,
}
}
// imageOutputFormatToMime maps provider output_format (e.g. png, jpeg, webp) to a MIME type.
func imageOutputFormatToMime(format string) string {
switch strings.ToLower(strings.TrimSpace(format)) {
case "png":
return "image/png"
case "jpeg", "jpg":
return "image/jpeg"
case "webp":
return "image/webp"
case "gif":
return "image/gif"
default:
return ""
}
}
func dataURLToAttachment(dataURL string, kind string) interface{} {
// Format: data:image/png;base64,iVBORw0...
idx := strings.Index(dataURL, ";base64,")
if idx == -1 {
log.Printf("%s invalid data URL format", PluginLoggerPrefix)
return nil
}
header := dataURL[5:idx] // "image/png" or "image/png;charset=..."
mime := strings.Split(header, ";")[0]
if mime == "" {
mime = "application/octet-stream"
}
b64 := dataURL[idx+8:]
decoded, err := base64.StdEncoding.DecodeString(b64)
if err != nil {
log.Printf("%s failed to decode data URL base64: %v", PluginLoggerPrefix, err)
return nil
}
name := kind + "." + extFromMime(mime)
return &logging.FileDataAttachment{
BaseAttachmentProps: logging.BaseAttachmentProps{
ID: uuid.New().String(),
Name: name,
MimeType: mime,
},
Type: logging.AttachmentTypeFileData,
Data: decoded,
}
}
func extFromMime(mime string) string {
switch mime {
case "image/png":
return "png"
case "image/jpeg", "image/jpg":
return "jpg"
case "image/gif":
return "gif"
case "image/webp":
return "webp"
case "audio/wav":
return "wav"
case "audio/mpeg":
return "mp3"
case "application/pdf":
return "pdf"
default:
return "bin"
}
}

View File

@@ -0,0 +1,650 @@
package maxim
import (
"context"
"encoding/base64"
"io"
"log"
"net/http"
"os"
"strings"
"testing"
"time"
bifrost "github.com/maximhq/bifrost/core"
"github.com/maximhq/bifrost/core/schemas"
"github.com/maximhq/maxim-go/logging"
)
// Test URLs and assets from core/internal/llmtests/utils.go for consistency across Bifrost tests.
var (
testFileURL = "https://www.berkshirehathaway.com/letters/2024ltr.pdf"
testImageURL = "https://pestworldcdn-dcf2a8gbggazaghf.z01.azurefd.net/media/561791/carpenter-ant4.jpg"
testImageBase64 = "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAIAAoDASIAAhEBAxEB/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAb/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k="
)
func strPtr(s string) *string { return &s }
func responsesUserRole() *schemas.ResponsesMessageRoleType {
r := schemas.ResponsesInputMessageRoleUser
return &r
}
func TestExtractAttachmentsFromRequest_ChatImageUrlHttp(t *testing.T) {
req := &schemas.BifrostRequest{
RequestType: schemas.ChatCompletionRequest,
ChatRequest: &schemas.BifrostChatRequest{
Input: []schemas.ChatMessage{
{
Role: schemas.ChatMessageRoleUser,
Content: &schemas.ChatMessageContent{
ContentBlocks: []schemas.ChatContentBlock{
{
Type: schemas.ChatContentBlockTypeImage,
ImageURLStruct: &schemas.ChatInputImage{
URL: testImageURL,
},
},
},
},
},
},
},
}
attachments := ExtractAttachmentsFromRequest(req)
if len(attachments) != 1 {
t.Fatalf("expected 1 attachment, got %d", len(attachments))
}
if _, ok := attachments[0].(*logging.UrlAttachment); !ok {
t.Errorf("expected *logging.UrlAttachment, got %T", attachments[0])
}
ua := attachments[0].(*logging.UrlAttachment)
if ua.URL != testImageURL {
t.Errorf("expected URL %q, got %q", testImageURL, ua.URL)
}
}
func TestExtractAttachmentsFromRequest_ChatImageUrlData(t *testing.T) {
b64 := base64.StdEncoding.EncodeToString([]byte("fake-png-bytes"))
dataURL := "data:image/png;base64," + b64
req := &schemas.BifrostRequest{
RequestType: schemas.ChatCompletionRequest,
ChatRequest: &schemas.BifrostChatRequest{
Input: []schemas.ChatMessage{
{
Role: schemas.ChatMessageRoleUser,
Content: &schemas.ChatMessageContent{
ContentBlocks: []schemas.ChatContentBlock{
{
Type: schemas.ChatContentBlockTypeImage,
ImageURLStruct: &schemas.ChatInputImage{
URL: dataURL,
},
},
},
},
},
},
},
}
attachments := ExtractAttachmentsFromRequest(req)
if len(attachments) != 1 {
t.Fatalf("expected 1 attachment, got %d", len(attachments))
}
if _, ok := attachments[0].(*logging.FileDataAttachment); !ok {
t.Errorf("expected *logging.FileDataAttachment, got %T", attachments[0])
}
fda := attachments[0].(*logging.FileDataAttachment)
if string(fda.Data) != "fake-png-bytes" {
t.Errorf("expected data 'fake-png-bytes', got %q", string(fda.Data))
}
if fda.MimeType != "image/png" {
t.Errorf("expected mime image/png, got %q", fda.MimeType)
}
}
func TestExtractAttachmentsFromRequest_ChatFileUrl(t *testing.T) {
fileURL := testFileURL
req := &schemas.BifrostRequest{
RequestType: schemas.ChatCompletionRequest,
ChatRequest: &schemas.BifrostChatRequest{
Input: []schemas.ChatMessage{
{
Role: schemas.ChatMessageRoleUser,
Content: &schemas.ChatMessageContent{
ContentBlocks: []schemas.ChatContentBlock{
{
Type: schemas.ChatContentBlockTypeFile,
File: &schemas.ChatInputFile{
FileURL: &fileURL,
Filename: strPtr("2024ltr.pdf"),
},
},
},
},
},
},
},
}
attachments := ExtractAttachmentsFromRequest(req)
if len(attachments) != 1 {
t.Fatalf("expected 1 attachment, got %d", len(attachments))
}
if _, ok := attachments[0].(*logging.UrlAttachment); !ok {
t.Errorf("expected *logging.UrlAttachment, got %T", attachments[0])
}
ua := attachments[0].(*logging.UrlAttachment)
if ua.URL != fileURL {
t.Errorf("expected URL %q, got %q", fileURL, ua.URL)
}
}
func TestExtractAttachmentsFromRequest_ChatFileData(t *testing.T) {
b64 := base64.StdEncoding.EncodeToString([]byte("pdf content"))
req := &schemas.BifrostRequest{
RequestType: schemas.ChatCompletionRequest,
ChatRequest: &schemas.BifrostChatRequest{
Input: []schemas.ChatMessage{
{
Role: schemas.ChatMessageRoleUser,
Content: &schemas.ChatMessageContent{
ContentBlocks: []schemas.ChatContentBlock{
{
Type: schemas.ChatContentBlockTypeFile,
File: &schemas.ChatInputFile{
FileData: &b64,
Filename: strPtr("doc.pdf"),
FileType: strPtr("application/pdf"),
},
},
},
},
},
},
},
}
attachments := ExtractAttachmentsFromRequest(req)
if len(attachments) != 1 {
t.Fatalf("expected 1 attachment, got %d", len(attachments))
}
if _, ok := attachments[0].(*logging.FileDataAttachment); !ok {
t.Errorf("expected *logging.FileDataAttachment, got %T", attachments[0])
}
fda := attachments[0].(*logging.FileDataAttachment)
if string(fda.Data) != "pdf content" {
t.Errorf("expected data 'pdf content', got %q", string(fda.Data))
}
}
func TestExtractAttachmentsFromRequest_ChatInputAudio(t *testing.T) {
b64 := base64.StdEncoding.EncodeToString([]byte("audio-bytes"))
format := "wav"
req := &schemas.BifrostRequest{
RequestType: schemas.ChatCompletionRequest,
ChatRequest: &schemas.BifrostChatRequest{
Input: []schemas.ChatMessage{
{
Role: schemas.ChatMessageRoleUser,
Content: &schemas.ChatMessageContent{
ContentBlocks: []schemas.ChatContentBlock{
{
Type: schemas.ChatContentBlockTypeInputAudio,
InputAudio: &schemas.ChatInputAudio{
Data: b64,
Format: &format,
},
},
},
},
},
},
},
}
attachments := ExtractAttachmentsFromRequest(req)
if len(attachments) != 1 {
t.Fatalf("expected 1 attachment, got %d", len(attachments))
}
if _, ok := attachments[0].(*logging.FileDataAttachment); !ok {
t.Errorf("expected *logging.FileDataAttachment, got %T", attachments[0])
}
fda := attachments[0].(*logging.FileDataAttachment)
if string(fda.Data) != "audio-bytes" {
t.Errorf("expected data 'audio-bytes', got %q", string(fda.Data))
}
if fda.MimeType != "audio/wav" {
t.Errorf("expected mime audio/wav, got %q", fda.MimeType)
}
}
func TestExtractAttachmentsFromRequest_ResponsesInputImage(t *testing.T) {
req := &schemas.BifrostRequest{
RequestType: schemas.ResponsesRequest,
ResponsesRequest: &schemas.BifrostResponsesRequest{
Input: []schemas.ResponsesMessage{
{
Role: responsesUserRole(),
Content: &schemas.ResponsesMessageContent{
ContentBlocks: []schemas.ResponsesMessageContentBlock{
{
Type: schemas.ResponsesInputMessageContentBlockTypeImage,
ResponsesInputMessageContentBlockImage: &schemas.ResponsesInputMessageContentBlockImage{
ImageURL: &testImageURL,
},
},
},
},
},
},
},
}
attachments := ExtractAttachmentsFromRequest(req)
if len(attachments) != 1 {
t.Fatalf("expected 1 attachment, got %d", len(attachments))
}
if _, ok := attachments[0].(*logging.UrlAttachment); !ok {
t.Errorf("expected *logging.UrlAttachment, got %T", attachments[0])
}
}
func TestExtractAttachmentsFromRequest_ResponsesInputFile(t *testing.T) {
fileURL := testFileURL
req := &schemas.BifrostRequest{
RequestType: schemas.ResponsesRequest,
ResponsesRequest: &schemas.BifrostResponsesRequest{
Input: []schemas.ResponsesMessage{
{
Role: responsesUserRole(),
Content: &schemas.ResponsesMessageContent{
ContentBlocks: []schemas.ResponsesMessageContentBlock{
{
Type: schemas.ResponsesInputMessageContentBlockTypeFile,
ResponsesInputMessageContentBlockFile: &schemas.ResponsesInputMessageContentBlockFile{
FileURL: &fileURL,
Filename: strPtr("2024ltr.pdf"),
FileType: strPtr("application/pdf"),
},
},
},
},
},
},
},
}
attachments := ExtractAttachmentsFromRequest(req)
if len(attachments) != 1 {
t.Fatalf("expected 1 attachment, got %d", len(attachments))
}
if _, ok := attachments[0].(*logging.UrlAttachment); !ok {
t.Errorf("expected *logging.UrlAttachment, got %T", attachments[0])
}
}
func TestExtractAttachmentsFromRequest_ResponsesInputAudio(t *testing.T) {
b64 := base64.StdEncoding.EncodeToString([]byte("mp3-bytes"))
req := &schemas.BifrostRequest{
RequestType: schemas.ResponsesRequest,
ResponsesRequest: &schemas.BifrostResponsesRequest{
Input: []schemas.ResponsesMessage{
{
Role: responsesUserRole(),
Content: &schemas.ResponsesMessageContent{
ContentBlocks: []schemas.ResponsesMessageContentBlock{
{
Type: schemas.ResponsesInputMessageContentBlockTypeAudio,
Audio: &schemas.ResponsesInputMessageContentBlockAudio{
Format: "mp3",
Data: b64,
},
},
},
},
},
},
},
}
attachments := ExtractAttachmentsFromRequest(req)
if len(attachments) != 1 {
t.Fatalf("expected 1 attachment, got %d", len(attachments))
}
if _, ok := attachments[0].(*logging.FileDataAttachment); !ok {
t.Errorf("expected *logging.FileDataAttachment, got %T", attachments[0])
}
fda := attachments[0].(*logging.FileDataAttachment)
if string(fda.Data) != "mp3-bytes" {
t.Errorf("expected data 'mp3-bytes', got %q", string(fda.Data))
}
if fda.MimeType != "audio/mpeg" {
t.Errorf("expected mime audio/mpeg, got %q", fda.MimeType)
}
}
func TestExtractAttachmentsFromRequest_NoAttachments(t *testing.T) {
req := &schemas.BifrostRequest{
RequestType: schemas.ChatCompletionRequest,
ChatRequest: &schemas.BifrostChatRequest{
Input: []schemas.ChatMessage{
{
Role: schemas.ChatMessageRoleUser,
Content: &schemas.ChatMessageContent{
ContentStr: strPtr("Hello, world!"),
},
},
},
},
}
attachments := ExtractAttachmentsFromRequest(req)
if len(attachments) != 0 {
t.Fatalf("expected 0 attachments, got %d", len(attachments))
}
}
func TestExtractAttachmentsFromRequest_TextCompletion(t *testing.T) {
req := &schemas.BifrostRequest{
RequestType: schemas.TextCompletionRequest,
ChatRequest: &schemas.BifrostChatRequest{
Input: []schemas.ChatMessage{
{
Role: schemas.ChatMessageRoleUser,
Content: &schemas.ChatMessageContent{
ContentBlocks: []schemas.ChatContentBlock{
{
Type: schemas.ChatContentBlockTypeImage,
ImageURLStruct: &schemas.ChatInputImage{
URL: "https://example.com/image.png",
},
},
},
},
},
},
},
}
attachments := ExtractAttachmentsFromRequest(req)
if len(attachments) != 0 {
t.Fatalf("expected 0 attachments for text completion, got %d", len(attachments))
}
}
func TestExtractAttachmentsFromRequest_ImageGenerationInputImagesUrl(t *testing.T) {
req := &schemas.BifrostRequest{
RequestType: schemas.ImageGenerationRequest,
ImageGenerationRequest: &schemas.BifrostImageGenerationRequest{
Input: &schemas.ImageGenerationInput{Prompt: "make it pop"},
Params: &schemas.ImageGenerationParameters{
InputImages: []string{testImageURL},
},
},
}
attachments := ExtractAttachmentsFromRequest(req)
if len(attachments) != 1 {
t.Fatalf("expected 1 attachment, got %d", len(attachments))
}
ua, ok := attachments[0].(*logging.UrlAttachment)
if !ok {
t.Fatalf("expected *logging.UrlAttachment, got %T", attachments[0])
}
if ua.URL != testImageURL {
t.Errorf("expected URL %q, got %q", testImageURL, ua.URL)
}
}
func TestExtractAttachmentsFromRequest_ImageGenerationInputImagesDataURL(t *testing.T) {
req := &schemas.BifrostRequest{
RequestType: schemas.ImageGenerationStreamRequest,
ImageGenerationRequest: &schemas.BifrostImageGenerationRequest{
Input: &schemas.ImageGenerationInput{Prompt: "variation"},
Params: &schemas.ImageGenerationParameters{
InputImages: []string{testImageBase64},
},
},
}
attachments := ExtractAttachmentsFromRequest(req)
if len(attachments) != 1 {
t.Fatalf("expected 1 attachment, got %d", len(attachments))
}
if _, ok := attachments[0].(*logging.FileDataAttachment); !ok {
t.Errorf("expected *logging.FileDataAttachment, got %T", attachments[0])
}
}
func TestExtractAttachmentsFromRequest_ImageGenerationInputImagesRawBase64(t *testing.T) {
idx := strings.Index(testImageBase64, ";base64,")
if idx == -1 {
t.Fatal("invalid testImageBase64 format")
}
rawB64 := testImageBase64[idx+8:]
req := &schemas.BifrostRequest{
RequestType: schemas.ImageGenerationRequest,
ImageGenerationRequest: &schemas.BifrostImageGenerationRequest{
Input: &schemas.ImageGenerationInput{Prompt: "edit"},
Params: &schemas.ImageGenerationParameters{
InputImages: []string{rawB64},
},
},
}
attachments := ExtractAttachmentsFromRequest(req)
if len(attachments) != 1 {
t.Fatalf("expected 1 attachment, got %d", len(attachments))
}
fda, ok := attachments[0].(*logging.FileDataAttachment)
if !ok {
t.Fatalf("expected *logging.FileDataAttachment, got %T", attachments[0])
}
if fda.MimeType != "image/jpeg" {
t.Errorf("expected image/jpeg from DetectContentType, got %q", fda.MimeType)
}
if len(fda.Data) == 0 {
t.Error("expected non-empty decoded image bytes")
}
}
func TestExtractAttachmentsFromRequest_ImageGenerationNoInputImages(t *testing.T) {
req := &schemas.BifrostRequest{
RequestType: schemas.ImageGenerationRequest,
ImageGenerationRequest: &schemas.BifrostImageGenerationRequest{
Input: &schemas.ImageGenerationInput{Prompt: "text only"},
Params: &schemas.ImageGenerationParameters{},
},
}
attachments := ExtractAttachmentsFromRequest(req)
if len(attachments) != 0 {
t.Fatalf("expected 0 attachments, got %d", len(attachments))
}
}
func TestExtractAttachmentsFromRequest_NilRequest(t *testing.T) {
attachments := ExtractAttachmentsFromRequest(nil)
if attachments != nil {
t.Fatalf("expected nil for nil request, got %v", attachments)
}
}
// TestExtractAttachmentsFromRequest_ChatFileDataFromBase64 uses testImageBase64
// and verifies FileData extraction from base64-encoded content.
func TestExtractAttachmentsFromRequest_ChatFileDataFromBase64(t *testing.T) {
// Extract base64 from data URL (format: data:image/jpeg;base64,...)
idx := strings.Index(testImageBase64, ";base64,")
if idx == -1 {
t.Fatal("invalid testImageBase64 format")
}
b64 := testImageBase64[idx+8:]
data, err := base64.StdEncoding.DecodeString(b64)
if err != nil {
t.Fatalf("failed to decode test image base64: %v", err)
}
b64ForRequest := base64.StdEncoding.EncodeToString(data)
req := &schemas.BifrostRequest{
RequestType: schemas.ChatCompletionRequest,
ChatRequest: &schemas.BifrostChatRequest{
Input: []schemas.ChatMessage{
{
Role: schemas.ChatMessageRoleUser,
Content: &schemas.ChatMessageContent{
ContentBlocks: []schemas.ChatContentBlock{
{
Type: schemas.ChatContentBlockTypeFile,
File: &schemas.ChatInputFile{
FileData: &b64ForRequest,
Filename: strPtr("grey_solid.jpg"),
FileType: strPtr("image/jpeg"),
},
},
},
},
},
},
},
}
attachments := ExtractAttachmentsFromRequest(req)
if len(attachments) != 1 {
t.Fatalf("expected 1 attachment, got %d", len(attachments))
}
if _, ok := attachments[0].(*logging.FileDataAttachment); !ok {
t.Errorf("expected *logging.FileDataAttachment, got %T", attachments[0])
}
fda := attachments[0].(*logging.FileDataAttachment)
if len(fda.Data) != len(data) {
t.Errorf("expected data length %d, got %d", len(data), len(fda.Data))
}
if fda.MimeType != "image/jpeg" {
t.Errorf("expected mime image/jpeg, got %q", fda.MimeType)
}
}
// requireIntegrationEnv skips the test if required env vars for real API calls are not set.
func requireIntegrationEnv(t *testing.T) {
t.Helper()
if os.Getenv("OPENAI_API_KEY") == "" {
t.Skip("OPENAI_API_KEY not set, skipping integration test")
}
if os.Getenv("MAXIM_API_KEY") == "" {
t.Skip("MAXIM_API_KEY not set, skipping integration test")
}
if os.Getenv("MAXIM_LOG_REPO_ID") == "" {
t.Skip("MAXIM_LOG_REPO_ID not set, skipping integration test")
}
}
// TestVisionWithImageUrl_Integration sends a real OpenAI vision request via Bifrost
// with the maxim plugin. The plugin extracts the image_url and logs it as an attachment
// to Maxim. Verify attachments in the Maxim dashboard after the test.
func TestVisionWithImageUrl_Integration(t *testing.T) {
requireIntegrationEnv(t)
plugin, err := getPlugin()
if err != nil {
t.Fatalf("failed to get plugin: %v", err)
}
client, err := bifrost.Init(context.Background(), schemas.BifrostConfig{
Account: &BaseAccount{},
LLMPlugins: []schemas.LLMPlugin{plugin},
Logger: bifrost.NewDefaultLogger(schemas.LogLevelDebug),
})
if err != nil {
t.Fatalf("failed to init Bifrost: %v", err)
}
defer client.Shutdown()
ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline)
_, bifrostErr := client.ChatCompletionRequest(ctx, &schemas.BifrostChatRequest{
Provider: schemas.OpenAI,
Model: "gpt-4o-mini",
Input: []schemas.ChatMessage{
{
Role: schemas.ChatMessageRoleUser,
Content: &schemas.ChatMessageContent{
ContentBlocks: []schemas.ChatContentBlock{
{
Type: schemas.ChatContentBlockTypeText,
Text: bifrost.Ptr("Describe this image in one sentence."),
},
{
Type: schemas.ChatContentBlockTypeImage,
ImageURLStruct: &schemas.ChatInputImage{
URL: testImageURL,
},
},
},
},
},
},
})
if bifrostErr != nil {
t.Fatalf("ChatCompletionRequest failed: %v", bifrostErr)
}
log.Printf("Vision request with image URL completed. Check Maxim dashboard for trace with attachment.")
}
// TestVisionWithImageData_Integration fetches testImageURL, encodes it as a data URL,
// and sends to OpenAI vision via Bifrost. The maxim plugin extracts the data URL and
// logs it as a FileDataAttachment. Uses a real image (carpenter ant) since OpenAI
// rejects minimal test images like the grey solid.
func TestVisionWithImageData_Integration(t *testing.T) {
requireIntegrationEnv(t)
// Fetch real image and encode as data URL (OpenAI rejects minimal grey solid)
httpClient := &http.Client{Timeout: 20 * time.Second}
resp, err := httpClient.Get(testImageURL)
if err != nil {
t.Skipf("failed to fetch test image: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Skipf("test image URL returned %d", resp.StatusCode)
}
imgData, err := io.ReadAll(resp.Body)
if err != nil {
t.Skipf("failed to read test image: %v", err)
}
dataURL := "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(imgData)
plugin, err := getPlugin()
if err != nil {
t.Fatalf("failed to get plugin: %v", err)
}
client, err := bifrost.Init(context.Background(), schemas.BifrostConfig{
Account: &BaseAccount{},
LLMPlugins: []schemas.LLMPlugin{plugin},
Logger: bifrost.NewDefaultLogger(schemas.LogLevelDebug),
})
if err != nil {
t.Fatalf("failed to init Bifrost: %v", err)
}
defer client.Shutdown()
ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline)
_, bifrostErr := client.ChatCompletionRequest(ctx, &schemas.BifrostChatRequest{
Provider: schemas.OpenAI,
Model: "gpt-4o-mini",
Input: []schemas.ChatMessage{
{
Role: schemas.ChatMessageRoleUser,
Content: &schemas.ChatMessageContent{
ContentBlocks: []schemas.ChatContentBlock{
{
Type: schemas.ChatContentBlockTypeText,
Text: bifrost.Ptr("What do you see in this image? One sentence."),
},
{
Type: schemas.ChatContentBlockTypeImage,
ImageURLStruct: &schemas.ChatInputImage{
URL: dataURL,
},
},
},
},
},
},
})
if bifrostErr != nil {
t.Fatalf("ChatCompletionRequest failed: %v", bifrostErr)
}
log.Printf("Vision request with base64 image completed. Check Maxim dashboard for trace with FileData attachment.")
}

View File

163
plugins/maxim/go.mod Normal file
View File

@@ -0,0 +1,163 @@
module github.com/maximhq/bifrost/plugins/maxim
go 1.26.2
require (
github.com/maximhq/bifrost/core v1.5.4
github.com/maximhq/bifrost/framework v1.3.4
github.com/maximhq/maxim-go v0.2.1
)
require (
github.com/bytedance/sonic v1.15.0
github.com/google/uuid v1.6.0
)
require (
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.5.3 // indirect
cloud.google.com/go/monitoring v1.24.3 // indirect
cloud.google.com/go/storage v1.61.3 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.11 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.14 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.2 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.24.2 // indirect
github.com/go-openapi/errors v0.22.5 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/loads v0.23.2 // indirect
github.com/go-openapi/runtime v0.29.2 // indirect
github.com/go-openapi/spec v0.22.2 // indirect
github.com/go-openapi/strfmt v0.25.0 // indirect
github.com/go-openapi/swag v0.25.4 // indirect
github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/fileutils v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
github.com/go-openapi/swag/loading v0.25.4 // indirect
github.com/go-openapi/swag/mangling v0.25.4 // indirect
github.com/go-openapi/swag/netutils v0.25.4 // indirect
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/go-openapi/validate v0.25.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/gax-go/v2 v2.19.0 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.9.1 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/mark3labs/mcp-go v0.43.2 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.32 // indirect
github.com/oapi-codegen/runtime v1.1.1 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/pinecone-io/go-pinecone/v5 v5.3.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/qdrant/go-client v1.16.2 // indirect
github.com/redis/go-redis/v9 v9.17.2 // indirect
github.com/rs/zerolog v1.34.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.68.0 // indirect
github.com/weaviate/weaviate v1.36.5 // indirect
github.com/weaviate/weaviate-go-client/v5 v5.7.1 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.mongodb.org/mongo-driver v1.17.6 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.40.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.starlark.net v0.0.0-20260102030733-3fee463870c9 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/api v0.274.0 // indirect
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/grpc v1.80.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/postgres v1.6.0 // indirect
gorm.io/driver/sqlite v1.6.0 // indirect
gorm.io/gorm v1.31.1 // indirect
)

391
plugins/maxim/go.sum Normal file
View File

@@ -0,0 +1,391 @@
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA=
cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak=
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
cloud.google.com/go/storage v1.61.3 h1:VS//ZfBuPGDvakfD9xyPW1RGF1Vy3BWUoVZXgW1KMOg=
cloud.google.com/go/storage v1.61.3/go.mod h1:JtqK8BBB7TWv0HVGHubtUdzYYrakOQIsMLffZ2Z/HWk=
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
github.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs=
github.com/aws/aws-sdk-go-v2/config v1.32.11/go.mod h1:twF11+6ps9aNRKEDimksp923o44w/Thk9+8YIlzWMmo=
github.com/aws/aws-sdk-go-v2/credentials v1.19.14 h1:n+UcGWAIZHkXzYt87uMFBv/l8THYELoX6gVcUvgl6fI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.14/go.mod h1:cJKuyWB59Mqi0jM3nFYQRmnHVQIcgoxjEMAbLkpr62w=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 h1:NUS3K4BTDArQqNu2ih7yeDLaS3bmHD0YndtA6UP884g=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21/go.mod h1:YWNWJQNjKigKY1RHVJCuupeWDrrHjRqHm0N9rdrWzYI=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4b6rx+5jzhgX9HrI=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 h1:HwxWTbTrIHm5qY+CAEur0s/figc3qwvLWsNkF4RPToo=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 h1:QKZH0S178gCmFEgst8hN0mCX1KxLgHBKKY/CLqwP8lg=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9/go.mod h1:7yuQJoT+OoH8aqIxw9vwF+8KpvLZ8AWmvmUWHsGQZvI=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 h1:lFd1+ZSEYJZYvv9d6kXzhkZu07si3f+GQ1AaYwa2LUM=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.15/go.mod h1:WSvS1NLr7JaPunCXqpJnWk1Bjo7IxzZXrZi1QQCkuqM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 h1:dzztQ1YmfPrxdrOiuZRMF6fuOwWlWpD2StNLTceKpys=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19/go.mod h1:YO8TrYtFdl5w/4vmjL8zaBSsiNp3w0L1FfKVKenZT7w=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 h1:p8ogvvLugcR/zLBXTXrTkj0RYBUdErbMnAFFp12Lm/U=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10/go.mod h1:60dv0eZJfeVXfbT1tFJinbHrDfSJ2GZl4Q//OSSNAVw=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE=
github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/analysis v0.24.2 h1:6p7WXEuKy1llDgOH8FooVeO+Uq2za9qoAOq4ZN08B50=
github.com/go-openapi/analysis v0.24.2/go.mod h1:x27OOHKANE0lutg2ml4kzYLoHGMKgRm1Cj2ijVOjJuE=
github.com/go-openapi/errors v0.22.5 h1:Yfv4O/PRYpNF3BNmVkEizcHb3uLVVsrDt3LNdgAKRY4=
github.com/go-openapi/errors v0.22.5/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk=
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
github.com/go-openapi/loads v0.23.2 h1:rJXAcP7g1+lWyBHC7iTY+WAF0rprtM+pm8Jxv1uQJp4=
github.com/go-openapi/loads v0.23.2/go.mod h1:IEVw1GfRt/P2Pplkelxzj9BYFajiWOtY2nHZNj4UnWY=
github.com/go-openapi/runtime v0.29.2 h1:UmwSGWNmWQqKm1c2MGgXVpC2FTGwPDQeUsBMufc5Yj0=
github.com/go-openapi/runtime v0.29.2/go.mod h1:biq5kJXRJKBJxTDJXAa00DOTa/anflQPhT0/wmjuy+0=
github.com/go-openapi/spec v0.22.2 h1:KEU4Fb+Lp1qg0V4MxrSCPv403ZjBl8Lx1a83gIPU8Qc=
github.com/go-openapi/spec v0.22.2/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ=
github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8=
github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU=
github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ=
github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4=
github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y=
github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48=
github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg=
github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0=
github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg=
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-openapi/validate v0.25.1 h1:sSACUI6Jcnbo5IWqbYHgjibrhhmt3vR6lCzKZnmAgBw=
github.com/go-openapi/validate v0.25.1/go.mod h1:RMVyVFYte0gbSTaZ0N4KmTn6u/kClvAFp+mAVfS/DQc=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE=
github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA=
github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68=
github.com/hajimehoshi/go-mp3 v0.3.4/go.mod h1:fRtZraRFcWb0pu7ok0LqyFhCUrPeMsGRSVop0eemFmo=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc=
github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I=
github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/maximhq/bifrost/core v1.5.4 h1:hf0BhoHVVpY1EQ4FkyRzW4IBYjrolxdZV0ucgWfHhcE=
github.com/maximhq/bifrost/core v1.5.4/go.mod h1:z1/vOalbDAD7v7sYbXQsqR+2qIFP0jKOSIStw6Q4P4U=
github.com/maximhq/bifrost/framework v1.3.4 h1:nZPv1FYry1njexZ0Hb6CZQXybwRFKGMTRyGWz2HGcio=
github.com/maximhq/bifrost/framework v1.3.4/go.mod h1:e0defDjWWFi6c2Zs3AOkMcRbYzjww4sjkyZtARrP4Zk=
github.com/maximhq/maxim-go v0.2.1 h1:hCp8dQ4HsyyNC+y5HCUuY/HFD0sOnGkjL5MdYCHkgEQ=
github.com/maximhq/maxim-go v0.2.1/go.mod h1:nwFznXy0Dn4mxXGU4X+BCnE3VP68L+FPEaW0yUgk96o=
github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pinecone-io/go-pinecone/v5 v5.3.0 h1:0YQlEtmXGWK/I8ztkOVM6PuBYgFJZhjSdb0ddU+bHPE=
github.com/pinecone-io/go-pinecone/v5 v5.3.0/go.mod h1:6Fg85fcyvMUQFf9KW7zniN81kelSYvsjF+KPLdc1MGA=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/qdrant/go-client v1.16.2 h1:UUMJJfvXTByhwhH1DwWdbkhZ2cTdvSqVkXSIfBrVWSg=
github.com/qdrant/go-client v1.16.2/go.mod h1:I+EL3h4HRoRTeHtbfOd/4kDXwCukZfkd41j/9wryGkw=
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287 h1:qIQ0tWF9vxGtkJa24bR+2i53WBCz1nW/Pc47oVYauC4=
github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
github.com/weaviate/weaviate v1.36.5 h1:lCiuEfQ08+5wK0DkTCUBb6ayNep9QpBH6JJhmZaRfzk=
github.com/weaviate/weaviate v1.36.5/go.mod h1:ljzrgEmGKn3CRzDdcxvhmBUUZIcghwIYd1Lmn54f3Z8=
github.com/weaviate/weaviate-go-client/v5 v5.7.1 h1:vEMxh486QqRqWaq58UEe/TiTbGbo9T5x7ZPFd5QENvQ=
github.com/weaviate/weaviate-go-client/v5 v5.7.1/go.mod h1:T/JDErjN074GrnYIa0AgK1TGUGP/6A/8vqXNPlv4c6E=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.40.0 h1:Awaf8gmW99tZTOWqkLCOl6aw1/rxAWVlHsHIZ3fT2sA=
go.opentelemetry.io/contrib/detectors/gcp v1.40.0/go.mod h1:99OY9ZCqyLkzJLTh5XhECpLRSxcZl+ZDKBEO+jMBFR4=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.starlark.net v0.0.0-20260102030733-3fee463870c9 h1:nV1OyvU+0CYrp5eKfQ3rD03TpFYYhH08z31NK1HmtTk=
go.starlark.net v0.0.0-20260102030733-3fee463870c9/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/api v0.274.0 h1:aYhycS5QQCwxHLwfEHRRLf9yNsfvp1JadKKWBE54RFA=
google.golang.org/api v0.274.0/go.mod h1:JbAt7mF+XVmWu6xNP8/+CTiGH30ofmCmk9nM8d8fHew=
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 h1:JNfk58HZ8lfmXbYK2vx/UvsqIL59TzByCxPIX4TDmsE=
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:x5julN69+ED4PcFk/XWayw35O0lf/nGa4aNgODCmNmw=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=

688
plugins/maxim/main.go Normal file
View File

@@ -0,0 +1,688 @@
// Package maxim provides integration for Maxim's SDK as a Bifrost plugin.
// This file contains the main plugin implementation.
package maxim
import (
"fmt"
"strings"
"sync"
"time"
"github.com/bytedance/sonic"
"github.com/google/uuid"
bifrost "github.com/maximhq/bifrost/core"
"github.com/maximhq/bifrost/core/schemas"
"github.com/maximhq/bifrost/framework/streaming"
"github.com/maximhq/maxim-go"
"github.com/maximhq/maxim-go/logging"
maximSchemas "github.com/maximhq/maxim-go/schemas"
)
// PluginName is the canonical name for the maxim plugin.
const (
PluginName string = "maxim"
PluginLoggerPrefix string = "[Maxim Plugin]"
)
// Config is the configuration for the maxim plugin.
// - APIKey: API key for Maxim SDK authentication
// - LogRepoID: Optional default ID for the Maxim logger instance
type Config struct {
LogRepoID string `json:"log_repo_id,omitempty"` // Optional - can be empty
APIKey string `json:"api_key"`
}
// Plugin implements the schemas.LLMPlugin interface for Maxim's logger.
// It provides request and response tracing functionality using Maxim logger,
// allowing detailed tracking of requests and responses across different log repositories.
//
// Fields:
// - mx: The Maxim SDK instance for creating new loggers
// - defaultLogRepoId: Default log repository ID from config (optional)
// - loggers: Map of log repo ID to logger instances
// - loggerMutex: RW mutex for thread-safe access to loggers map
type Plugin struct {
mx *maxim.Maxim
defaultLogRepoID string
loggers map[string]*logging.Logger
loggerMutex *sync.RWMutex
logger schemas.Logger
}
// Init initializes and returns a Plugin instance for Maxim's logger.
//
// Parameters:
// - config: Configuration for the maxim plugin
//
// Returns:
// - schemas.LLMPlugin: A configured plugin instance for request/response tracing
// - error: Any error that occurred during plugin initialization
func Init(config *Config, logger schemas.Logger) (schemas.LLMPlugin, error) {
if config == nil {
return nil, fmt.Errorf("config is required")
}
// check if Maxim Logger variables are set
if config.APIKey == "" {
return nil, fmt.Errorf("apiKey is not set")
}
mx := maxim.Init(&maxim.MaximSDKConfig{ApiKey: config.APIKey})
plugin := &Plugin{
mx: mx,
defaultLogRepoID: config.LogRepoID,
loggers: make(map[string]*logging.Logger),
loggerMutex: &sync.RWMutex{},
logger: logger,
}
// Initialize default logger if LogRepoId is provided
if config.LogRepoID != "" {
logger, err := mx.GetLogger(&logging.LoggerConfig{Id: config.LogRepoID})
if err != nil {
return nil, fmt.Errorf("failed to initialize default logger: %w", err)
}
plugin.loggers[config.LogRepoID] = logger
}
return plugin, nil
}
// TraceIDKey is the context key used to store and retrieve trace IDs.
// This constant provides a consistent key for tracking request traces
// throughout the request/response lifecycle.
const (
SessionIDKey schemas.BifrostContextKey = "session-id"
TraceIDKey schemas.BifrostContextKey = "trace-id"
TraceNameKey schemas.BifrostContextKey = "trace-name"
GenerationIDKey schemas.BifrostContextKey = "generation-id"
GenerationNameKey schemas.BifrostContextKey = "generation-name"
TagsKey schemas.BifrostContextKey = "maxim-tags"
LogRepoIDKey schemas.BifrostContextKey = "log-repo-id"
)
// convertAccResultToProcessedStreamResponse converts StreamAccumulatorResult to ProcessedStreamResponse
func convertAccResultToProcessedStreamResponse(accResult *schemas.StreamAccumulatorResult) *streaming.ProcessedStreamResponse {
if accResult == nil {
return nil
}
// Determine StreamType based on the response content
streamType := streaming.StreamTypeChat
if accResult.AudioOutput != nil {
streamType = streaming.StreamTypeAudio
} else if accResult.TranscriptionOutput != nil {
streamType = streaming.StreamTypeTranscription
} else if len(accResult.OutputMessages) > 0 {
streamType = streaming.StreamTypeResponses
} else if accResult.ImageGenerationOutput != nil {
streamType = streaming.StreamTypeImage
}
return &streaming.ProcessedStreamResponse{
RequestID: accResult.RequestID,
StreamType: streamType,
RequestedModel: accResult.RequestedModel,
ResolvedModel: accResult.ResolvedModel,
Provider: accResult.Provider,
Data: &streaming.AccumulatedData{
Status: accResult.Status,
Latency: accResult.Latency,
TimeToFirstToken: accResult.TimeToFirstToken,
OutputMessage: accResult.OutputMessage,
OutputMessages: accResult.OutputMessages,
TokenUsage: accResult.TokenUsage,
Cost: accResult.Cost,
ErrorDetails: accResult.ErrorDetails,
AudioOutput: accResult.AudioOutput,
TranscriptionOutput: accResult.TranscriptionOutput,
FinishReason: accResult.FinishReason,
RawResponse: accResult.RawResponse,
},
RawRequest: &accResult.RawRequest,
}
}
// The plugin provides request/response tracing functionality by integrating with Maxim's logging system.
// It supports both chat completion and text completion requests, tracking the entire lifecycle of each request
// including inputs, parameters, and responses.
//
// Key Features:
// - Automatic trace and generation ID management
// - Support for both chat and text completion requests
// - Contextual tracking across request lifecycle
// - Graceful handling of existing trace/generation IDs
//
// The plugin uses context values to maintain trace and generation IDs throughout the request lifecycle.
// These IDs can be propagated from external systems through HTTP headers (x-bf-maxim-trace-id and x-bf-maxim-generation-id).
// GetName returns the name of the plugin.
func (plugin *Plugin) GetName() string {
return PluginName
}
// HTTPTransportPreHook is not used for this plugin
func (plugin *Plugin) HTTPTransportPreHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest) (*schemas.HTTPResponse, error) {
return nil, nil
}
// HTTPTransportPostHook is not used for this plugin
func (plugin *Plugin) HTTPTransportPostHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
return nil
}
// HTTPTransportStreamChunkHook passes through streaming chunks unchanged
func (plugin *Plugin) HTTPTransportStreamChunkHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, chunk *schemas.BifrostStreamChunk) (*schemas.BifrostStreamChunk, error) {
return chunk, nil
}
// getEffectiveLogRepoID determines which single log repo ID to use based on priority:
// 1. Header log repo ID (if provided)
// 2. Default log repo ID from config (if configured)
// 3. Empty string (skip logging)
func (plugin *Plugin) getEffectiveLogRepoID(ctx *schemas.BifrostContext) string {
// Check for header log repo ID first (highest priority)
if ctx != nil {
if headerRepoID, ok := ctx.Value(LogRepoIDKey).(string); ok && headerRepoID != "" {
return headerRepoID
}
}
// Fall back to default log repo ID from config
if plugin.defaultLogRepoID != "" {
return plugin.defaultLogRepoID
}
// Return empty string if neither header nor default is available
return ""
}
// getOrCreateLogger gets an existing logger or creates a new one for the given log repo ID
func (plugin *Plugin) getOrCreateLogger(logRepoID string) (*logging.Logger, error) {
// First, try to get existing logger (read lock)
plugin.loggerMutex.RLock()
if logger, exists := plugin.loggers[logRepoID]; exists {
plugin.loggerMutex.RUnlock()
return logger, nil
}
plugin.loggerMutex.RUnlock()
// Logger doesn't exist, create it (write lock)
plugin.loggerMutex.Lock()
defer plugin.loggerMutex.Unlock()
// Double-check in case another goroutine created it while we were waiting
if logger, exists := plugin.loggers[logRepoID]; exists {
return logger, nil
}
// Create new logger
logger, err := plugin.mx.GetLogger(&logging.LoggerConfig{Id: logRepoID})
if err != nil {
return nil, fmt.Errorf("failed to create logger for repo ID %s: %w", logRepoID, err)
}
plugin.loggers[logRepoID] = logger
return logger, nil
}
// PreLLMHook is called before a request is processed by Bifrost.
// It manages trace and generation tracking for incoming requests by either:
// - Creating a new trace if none exists
// - Reusing an existing trace ID from the context
// - Creating a new generation within an existing trace
// - Skipping trace/generation creation if they already exist
//
// The function handles both chat completion and text completion requests,
// capturing relevant metadata such as:
// - Request type (chat/text completion)
// - Model information
// - Message content and role
// - Model parameters
//
// Parameters:
// - ctx: Pointer to the schemas.BifrostContext that may contain existing trace/generation IDs
// - req: The incoming Bifrost request to be traced
//
// Returns:
// - *schemas.BifrostRequest: The original request, unmodified
// - error: Any error that occurred during trace/generation creation
func (plugin *Plugin) PreLLMHook(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.LLMPluginShortCircuit, error) {
if req != nil && req.RequestType == schemas.RealtimeRequest {
return req, nil, nil
}
var traceID string
var traceName string
var sessionID string
var generationName string
// Get effective log repo ID (header > default > skip)
effectiveLogRepoID := plugin.getEffectiveLogRepoID(ctx)
// If no log repo ID available, skip logging
if effectiveLogRepoID == "" {
return req, nil, nil
}
// Check if context already has traceID and generationID
if ctx != nil {
if existingGenerationID, ok := ctx.Value(GenerationIDKey).(string); ok && existingGenerationID != "" {
// If generationID exists, return early
return req, nil, nil
}
if existingTraceID, ok := ctx.Value(TraceIDKey).(string); ok && existingTraceID != "" {
// If traceID exists, and no generationID, create a new generation on the trace
traceID = existingTraceID
}
if existingSessionID, ok := ctx.Value(SessionIDKey).(string); ok && existingSessionID != "" {
sessionID = existingSessionID
}
if existingTraceName, ok := ctx.Value(TraceNameKey).(string); ok && existingTraceName != "" {
traceName = existingTraceName
}
if existingGenerationName, ok := ctx.Value(GenerationNameKey).(string); ok && existingGenerationName != "" {
generationName = existingGenerationName
}
}
provider, model, _ := req.GetRequestFields()
// Determine request type and set appropriate tags
var messages []maximSchemas.CompletionRequest
var latestMessage string
modelParams := make(map[string]interface{})
switch req.RequestType {
case schemas.TextCompletionRequest, schemas.TextCompletionStreamRequest:
messages = append(messages, maximSchemas.CompletionRequest{
Role: string(schemas.ChatMessageRoleUser),
Content: req.TextCompletionRequest.Input,
})
if req.TextCompletionRequest.Input.PromptStr != nil {
latestMessage = *req.TextCompletionRequest.Input.PromptStr
} else {
var stringBuilder strings.Builder
for _, prompt := range req.TextCompletionRequest.Input.PromptArray {
stringBuilder.WriteString(prompt)
}
latestMessage = stringBuilder.String()
}
if req.TextCompletionRequest.Params != nil {
// Convert the struct to a map using reflection or JSON marshaling
jsonData, err := sonic.Marshal(req.TextCompletionRequest.Params)
if err == nil {
sonic.Unmarshal(jsonData, &modelParams)
}
}
case schemas.ChatCompletionRequest, schemas.ChatCompletionStreamRequest:
for _, message := range req.ChatRequest.Input {
messages = append(messages, maximSchemas.CompletionRequest{
Role: string(message.Role),
Content: message.Content,
})
}
if len(req.ChatRequest.Input) > 0 {
lastMsg := req.ChatRequest.Input[len(req.ChatRequest.Input)-1]
if lastMsg.Content.ContentStr != nil {
latestMessage = *lastMsg.Content.ContentStr
} else if lastMsg.Content.ContentBlocks != nil {
// Find the last text content block
for i := len(lastMsg.Content.ContentBlocks) - 1; i >= 0; i-- {
block := (lastMsg.Content.ContentBlocks)[i]
if block.Type == schemas.ChatContentBlockTypeText && block.Text != nil {
latestMessage = *block.Text
break
}
}
// If no text block found, use placeholder
if latestMessage == "" {
latestMessage = "-"
}
}
}
if req.ChatRequest.Params != nil {
// Convert the struct to a map using reflection or JSON marshaling
jsonData, err := sonic.Marshal(req.ChatRequest.Params)
if err == nil {
sonic.Unmarshal(jsonData, &modelParams)
}
}
case schemas.ResponsesRequest, schemas.ResponsesStreamRequest, schemas.WebSocketResponsesRequest:
for _, message := range req.ResponsesRequest.Input {
if message.Content != nil {
role := schemas.ChatMessageRoleUser
if message.Role != nil {
role = schemas.ChatMessageRole(*message.Role)
}
messages = append(messages, maximSchemas.CompletionRequest{
Role: string(role),
Content: message.Content,
})
}
}
if len(req.ResponsesRequest.Input) > 0 {
lastMsg := req.ResponsesRequest.Input[len(req.ResponsesRequest.Input)-1]
// Initialize to placeholder in case content is missing or empty
latestMessage = "-"
// Check if Content is nil before accessing its fields
if lastMsg.Content != nil {
if lastMsg.Content.ContentStr != nil {
latestMessage = *lastMsg.Content.ContentStr
} else if lastMsg.Content.ContentBlocks != nil {
// Find the last text content block
for i := len(lastMsg.Content.ContentBlocks) - 1; i >= 0; i-- {
block := (lastMsg.Content.ContentBlocks)[i]
if block.Text != nil {
latestMessage = *block.Text
break
}
}
// If no text block found, keep the placeholder
}
}
}
if req.ResponsesRequest.Params != nil {
// Convert the struct to a map using reflection or JSON marshaling
jsonData, err := sonic.Marshal(req.ResponsesRequest.Params)
if err == nil {
sonic.Unmarshal(jsonData, &modelParams)
}
}
case schemas.ImageGenerationRequest, schemas.ImageGenerationStreamRequest:
if req.ImageGenerationRequest == nil || req.ImageGenerationRequest.Input == nil {
break
}
messages = append(messages, maximSchemas.CompletionRequest{
Role: string(schemas.ChatMessageRoleUser),
Content: req.ImageGenerationRequest.Input.Prompt,
})
latestMessage = req.ImageGenerationRequest.Input.Prompt
if req.ImageGenerationRequest.Params != nil {
jsonData, err := sonic.Marshal(req.ImageGenerationRequest.Params)
if err == nil {
sonic.Unmarshal(jsonData, &modelParams)
}
}
case schemas.ImageEditRequest, schemas.ImageEditStreamRequest:
if req.ImageEditRequest == nil || req.ImageEditRequest.Input == nil {
break
}
messages = append(messages, maximSchemas.CompletionRequest{
Role: string(schemas.ChatMessageRoleUser),
Content: req.ImageEditRequest.Input.Prompt,
})
latestMessage = req.ImageEditRequest.Input.Prompt
if req.ImageEditRequest.Params != nil {
jsonData, err := sonic.Marshal(req.ImageEditRequest.Params)
if err == nil {
sonic.Unmarshal(jsonData, &modelParams)
}
}
}
if traceID == "" {
// If traceID is not set, create a new trace
traceID = uuid.New().String()
}
name := fmt.Sprintf("bifrost_%s", string(req.RequestType))
if traceName != "" {
name = traceName
}
traceConfig := logging.TraceConfig{
Id: traceID,
Name: maxim.StrPtr(name),
}
if sessionID != "" {
traceConfig.SessionId = &sessionID
}
// Create trace in the effective log repository
logger, err := plugin.getOrCreateLogger(effectiveLogRepoID)
if err != nil {
return req, nil, fmt.Errorf("failed to create trace: %w", err)
}
trace := logger.Trace(&traceConfig)
trace.SetInput(latestMessage)
generationID := uuid.New().String()
generationConfig := logging.GenerationConfig{
Id: generationID,
Model: model,
Provider: string(provider),
Messages: messages,
ModelParameters: modelParams,
}
if generationName != "" {
generationConfig.Name = &generationName
}
// Add generation to the effective log repository
logger.AddGenerationToTrace(traceID, &generationConfig)
// Extract and log attachments from message content
for _, att := range ExtractAttachmentsFromRequest(req) {
if att != nil {
logger.GenerationAddAttachment(generationID, att)
}
}
if ctx != nil {
if _, ok := ctx.Value(TraceIDKey).(string); !ok {
ctx.SetValue(TraceIDKey, traceID)
}
ctx.SetValue(GenerationIDKey, generationID)
// Extract request ID from context, if not present, create a new one
requestID, ok := ctx.Value(schemas.BifrostContextKeyRequestID).(string)
if !ok || requestID == "" {
// This should never happen since core/bifrost.go guarantees it's set before PreHooks
requestID = uuid.New().String()
plugin.logger.Warn("%s request ID missing in PreLLMHook, using fallback: %s", PluginLoggerPrefix, requestID)
}
// If streaming, create accumulator via central tracer using traceID
if bifrost.IsStreamRequestType(req.RequestType) {
tracer, bifrostTraceID, err := bifrost.GetTracerFromContext(ctx)
if err == nil && tracer != nil && bifrostTraceID != "" {
tracer.CreateStreamAccumulator(bifrostTraceID, time.Now())
}
}
}
return req, nil, nil
}
// PostLLMHook is called after a request has been processed by Bifrost.
// It completes the request trace by:
// - Adding response data to the generation if a generation ID exists
// - Logging error details if bifrostErr is provided
// - Ending the generation if it exists
// - Ending the trace if a trace ID exists
// - Flushing all pending log data
//
// The function gracefully handles cases where trace or generation IDs may be missing,
// ensuring that partial logging is still performed when possible.
//
// Parameters:
// - ctx: Pointer to the schemas.BifrostContext containing trace/generation IDs
// - result: The Bifrost response to be traced
// - bifrostErr: The BifrostError returned by the request, if any
//
// Returns:
// - *schemas.BifrostResponse: The original response, unmodified
// - *schemas.BifrostError: The original error, unmodified
// - error: Never returns an error as it handles missing IDs gracefully
func (plugin *Plugin) PostLLMHook(ctx *schemas.BifrostContext, result *schemas.BifrostResponse, bifrostErr *schemas.BifrostError) (*schemas.BifrostResponse, *schemas.BifrostError, error) {
requestType, _, _, _ := bifrost.GetResponseFields(result, bifrostErr)
if requestType == schemas.RealtimeRequest {
return result, bifrostErr, nil
}
// Get effective log repo ID for this request
effectiveLogRepoID := plugin.getEffectiveLogRepoID(ctx)
if effectiveLogRepoID == "" {
return result, bifrostErr, nil
}
if ctx == nil {
return result, bifrostErr, nil
}
requestID, ok := ctx.Value(schemas.BifrostContextKeyRequestID).(string)
if !ok || requestID == "" {
return result, bifrostErr, nil
}
// Capture context values BEFORE goroutine to avoid race conditions
// when the same context is reused across multiple requests
generationID, hasGenerationID := ctx.Value(GenerationIDKey).(string)
traceID, hasTraceID := ctx.Value(TraceIDKey).(string)
tags, hasTags := ctx.Value(TagsKey).(map[string]string)
isFinalChunk := bifrost.IsFinalChunk(ctx)
go func() {
requestType, _, originalModel, resolvedModel := bifrost.GetResponseFields(result, bifrostErr)
modelTag := resolvedModel
if modelTag == "" {
modelTag = originalModel
}
var streamResponse *streaming.ProcessedStreamResponse
if bifrost.IsStreamRequestType(requestType) {
// Use central tracer's accumulator
tracer, bifrostTraceID, err := bifrost.GetTracerFromContext(ctx)
if err == nil && tracer != nil && bifrostTraceID != "" {
accResult := tracer.ProcessStreamingChunk(bifrostTraceID, isFinalChunk, result, bifrostErr)
if accResult != nil {
streamResponse = convertAccResultToProcessedStreamResponse(accResult)
}
}
// For streaming: only process on final chunk. Skip intermediate chunks.
// When there's an error, streamResponse may be nil but we must still log bifrostErr.
if !isFinalChunk {
return
}
}
logger, err := plugin.getOrCreateLogger(effectiveLogRepoID)
if err != nil {
return
}
if hasGenerationID {
if bifrostErr != nil {
// Safely extract message from nested error
message := ""
code := ""
errorType := ""
if bifrostErr.Error != nil {
message = bifrostErr.Error.Message
if bifrostErr.Error.Code != nil {
code = *bifrostErr.Error.Code
}
if bifrostErr.Error.Type != nil {
errorType = *bifrostErr.Error.Type
}
}
genErr := maximSchemas.GenerationError{
Message: message,
Code: &code,
Type: &errorType,
}
logger.SetGenerationError(generationID, &genErr)
if bifrost.IsStreamRequestType(requestType) {
// Cleanup via central tracer
tracer, bifrostTraceID, err := bifrost.GetTracerFromContext(ctx)
if err == nil && tracer != nil && bifrostTraceID != "" {
tracer.CleanupStreamAccumulator(bifrostTraceID)
}
}
} else if result != nil {
switch requestType {
case schemas.TextCompletionRequest, schemas.TextCompletionStreamRequest:
if streamResponse != nil {
logger.AddResultToGeneration(generationID, streamResponse.ToBifrostResponse().TextCompletionResponse)
} else {
logger.AddResultToGeneration(generationID, result.TextCompletionResponse)
}
case schemas.ChatCompletionRequest, schemas.ChatCompletionStreamRequest:
if streamResponse != nil {
logger.AddResultToGeneration(generationID, streamResponse.ToBifrostResponse().ChatResponse)
} else {
logger.AddResultToGeneration(generationID, result.ChatResponse)
}
case schemas.ResponsesRequest, schemas.ResponsesStreamRequest, schemas.WebSocketResponsesRequest:
if streamResponse != nil {
logger.AddResultToGeneration(generationID, streamResponse.ToBifrostResponse().ResponsesResponse)
} else {
logger.AddResultToGeneration(generationID, result.ResponsesResponse)
}
case schemas.ImageGenerationRequest, schemas.ImageGenerationStreamRequest,
schemas.ImageEditRequest, schemas.ImageEditStreamRequest:
if streamResponse != nil {
logger.AddResultToGeneration(generationID, streamResponse.ToBifrostResponse().ImageGenerationResponse)
} else if result != nil {
logger.AddResultToGeneration(generationID, result.ImageGenerationResponse)
}
}
if streamResponse != nil && isFinalChunk {
// Cleanup via central tracer
tracer, bifrostTraceID, err := bifrost.GetTracerFromContext(ctx)
if err == nil && tracer != nil && bifrostTraceID != "" {
tracer.CleanupStreamAccumulator(bifrostTraceID)
}
}
}
}
if hasTraceID {
logger.EndTrace(traceID)
}
// add tags to the generation and trace
if hasTags {
for key, value := range tags {
if generationID != "" {
logger.AddTagToGeneration(generationID, key, value)
}
if traceID != "" {
logger.AddTagToTrace(traceID, key, value)
}
}
}
if hasGenerationID && generationID != "" && modelTag != "" {
logger.AddTagToGeneration(generationID, "model", string(modelTag))
}
if hasTraceID && traceID != "" && modelTag != "" {
logger.AddTagToTrace(traceID, "model", string(modelTag))
}
// Flush only the effective logger that was used for this request
logger.Flush()
}()
return result, bifrostErr, nil
}
func (plugin *Plugin) Cleanup() error {
// Flush all loggers
plugin.loggerMutex.RLock()
for _, logger := range plugin.loggers {
logger.Flush()
}
plugin.loggerMutex.RUnlock()
return nil
}

View File

@@ -0,0 +1,258 @@
// Package maxim provides integration for Maxim's SDK as a Bifrost plugin.
// It includes tests for plugin initialization, Bifrost integration, and request/response tracing.
package maxim
import (
"context"
"fmt"
"log"
"os"
"testing"
bifrost "github.com/maximhq/bifrost/core"
"github.com/maximhq/bifrost/core/schemas"
)
// getPlugin initializes and returns a Plugin instance for testing purposes.
// It sets up the Maxim logger with configuration from environment variables.
//
// Environment Variables:
// - MAXIM_API_KEY: API key for Maxim SDK authentication
// - MAXIM_LOG_REPO_ID: ID for the Maxim logger instance
//
// Returns:
// - schemas.LLMPlugin: A configured plugin instance for request/response tracing
// - error: Any error that occurred during plugin initialization
func getPlugin() (schemas.LLMPlugin, error) {
// check if Maxim Logger variables are set
if os.Getenv("MAXIM_API_KEY") == "" {
return nil, fmt.Errorf("MAXIM_API_KEY is not set, please set it in your environment variables")
}
logger := bifrost.NewDefaultLogger(schemas.LogLevelDebug)
plugin, err := Init(&Config{
APIKey: os.Getenv("MAXIM_API_KEY"),
LogRepoID: os.Getenv("MAXIM_LOG_REPO_ID"),
}, logger)
if err != nil {
return nil, err
}
return plugin, nil
}
// BaseAccount implements the schemas.Account interface for testing purposes.
// It provides mock implementations of the required methods to test the Maxim plugin
// with a basic OpenAI configuration.
type BaseAccount struct{}
// GetConfiguredProviders returns a list of supported providers for testing.
// Currently only supports OpenAI for simplicity in testing. You are free to add more providers as needed.
func (baseAccount *BaseAccount) GetConfiguredProviders() ([]schemas.ModelProvider, error) {
return []schemas.ModelProvider{schemas.OpenAI}, nil
}
// GetKeysForProvider returns a mock API key configuration for testing.
// Uses the OPENAI_API_KEY environment variable for authentication.
func (baseAccount *BaseAccount) GetKeysForProvider(ctx context.Context, providerKey schemas.ModelProvider) ([]schemas.Key, error) {
return []schemas.Key{
{
Value: *schemas.NewEnvVar("env.OPENAI_API_KEY"),
Models: []string{"gpt-4o-mini", "gpt-4-turbo"},
Weight: 1.0,
},
}, nil
}
// GetConfigForProvider returns default provider configuration for testing.
// Uses standard network and concurrency settings.
func (baseAccount *BaseAccount) GetConfigForProvider(providerKey schemas.ModelProvider) (*schemas.ProviderConfig, error) {
return &schemas.ProviderConfig{
NetworkConfig: schemas.DefaultNetworkConfig,
ConcurrencyAndBufferSize: schemas.DefaultConcurrencyAndBufferSize,
}, nil
}
// TestMaximLoggerPlugin tests the integration of the Maxim Logger plugin with Bifrost.
// It performs the following steps:
// 1. Initializes the Maxim plugin with environment variables
// 2. Sets up a test Bifrost instance with the plugin
// 3. Makes a test chat completion request
//
// Required environment variables:
// - MAXIM_API_KEY: Your Maxim API key
// - MAXIM_LOGGER_ID: Your Maxim logger repository ID
// - OPENAI_API_KEY: Your OpenAI API key for the test request
func TestMaximLoggerPlugin(t *testing.T) {
ctx := context.Background()
// Initialize the Maxim plugin
plugin, err := getPlugin()
if err != nil {
t.Fatalf("Error setting up the plugin: %v", err)
}
account := BaseAccount{}
// Initialize Bifrost with the plugin
client, err := bifrost.Init(ctx, schemas.BifrostConfig{
Account: &account,
LLMPlugins: []schemas.LLMPlugin{plugin},
Logger: bifrost.NewDefaultLogger(schemas.LogLevelDebug),
})
if err != nil {
t.Fatalf("Error initializing Bifrost: %v", err)
}
// Make a test chat completion request
_, bifrostErr := client.ChatCompletionRequest(schemas.NewBifrostContext(context.Background(), schemas.NoDeadline), &schemas.BifrostChatRequest{
Provider: schemas.OpenAI,
Model: "gpt-4o-mini",
Input: []schemas.ChatMessage{
{
Role: "user",
Content: &schemas.ChatMessageContent{
ContentStr: bifrost.Ptr("Hello, how are you?"),
},
},
},
})
if bifrostErr != nil {
log.Printf("Error in Bifrost request: %v", bifrostErr)
}
log.Println("Bifrost request completed, check your Maxim Dashboard for the trace")
client.Shutdown()
}
// TestLogRepoIDSelection tests the single repository selection logic
func TestLogRepoIDSelection(t *testing.T) {
tests := []struct {
name string
defaultRepo string
headerRepo string
expectedRepo string
shouldLog bool
}{
{
name: "Header repo takes priority",
defaultRepo: "default-repo",
headerRepo: "header-repo",
expectedRepo: "header-repo",
shouldLog: true,
},
{
name: "Fall back to default repo when no header",
defaultRepo: "default-repo",
headerRepo: "",
expectedRepo: "default-repo",
shouldLog: true,
},
{
name: "Use header repo when no default",
defaultRepo: "",
headerRepo: "header-repo",
expectedRepo: "header-repo",
shouldLog: true,
},
{
name: "Skip logging when neither available",
defaultRepo: "",
headerRepo: "",
expectedRepo: "",
shouldLog: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create plugin with default repo
plugin := &Plugin{
defaultLogRepoID: tt.defaultRepo,
}
// Create context with header repo if provided
ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline)
if tt.headerRepo != "" {
ctx.SetValue(LogRepoIDKey, tt.headerRepo)
}
// Test the selection logic
result := plugin.getEffectiveLogRepoID(ctx)
if result != tt.expectedRepo {
t.Errorf("Expected repo '%s', got '%s'", tt.expectedRepo, result)
}
shouldLog := result != ""
if shouldLog != tt.shouldLog {
t.Errorf("Expected shouldLog=%t, got shouldLog=%t", tt.shouldLog, shouldLog)
}
})
}
}
// TestPluginInitialization tests plugin initialization with different configs
func TestPluginInitialization(t *testing.T) {
logger := bifrost.NewDefaultLogger(schemas.LogLevelDebug)
tests := []struct {
name string
config Config
expectError bool
}{
{
name: "Valid config with both fields",
config: Config{
APIKey: "test-api-key",
LogRepoID: "test-repo-id",
},
expectError: false,
},
{
name: "Valid config with only API key",
config: Config{
APIKey: "test-api-key",
LogRepoID: "",
},
expectError: false,
},
{
name: "Invalid config - missing API key",
config: Config{
APIKey: "",
LogRepoID: "test-repo-id",
},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Skip actual Maxim SDK initialization in tests
if tt.expectError {
_, err := Init(&tt.config, logger)
if err == nil {
t.Error("Expected error but got none")
}
} else {
// For valid configs, we can't test actual initialization without real API key
// Just test the validation logic
if tt.config.APIKey == "" {
t.Skip("Skipping valid config test - would need real Maxim API key")
}
}
})
}
}
// TestPluginName tests the plugin name functionality
func TestPluginName(t *testing.T) {
plugin := &Plugin{}
if plugin.GetName() != PluginName {
t.Errorf("Expected plugin name '%s', got '%s'", PluginName, plugin.GetName())
}
if PluginName != "maxim" {
t.Errorf("Expected PluginName constant to be 'maxim', got '%s'", PluginName)
}
}

1
plugins/maxim/version Normal file
View File

@@ -0,0 +1 @@
1.6.4