first commit
This commit is contained in:
454
plugins/maxim/attachments.go
Normal file
454
plugins/maxim/attachments.go
Normal 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"
|
||||
}
|
||||
}
|
||||
650
plugins/maxim/attachments_test.go
Normal file
650
plugins/maxim/attachments_test.go
Normal 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.")
|
||||
}
|
||||
0
plugins/maxim/changelog.md
Normal file
0
plugins/maxim/changelog.md
Normal file
163
plugins/maxim/go.mod
Normal file
163
plugins/maxim/go.mod
Normal 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
391
plugins/maxim/go.sum
Normal 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
688
plugins/maxim/main.go
Normal 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
|
||||
}
|
||||
258
plugins/maxim/plugin_test.go
Normal file
258
plugins/maxim/plugin_test.go
Normal 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
1
plugins/maxim/version
Normal file
@@ -0,0 +1 @@
|
||||
1.6.4
|
||||
Reference in New Issue
Block a user