Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions transports/bifrost-http/integrations/anthropic/router.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package anthropic

import (
"errors"

bifrost "github.com/maximhq/bifrost/core"
"github.com/maximhq/bifrost/core/schemas"
"github.com/maximhq/bifrost/transports/bifrost-http/integrations"
Expand All @@ -21,14 +23,14 @@ func NewAnthropicRouter(client *bifrost.Bifrost) *AnthropicRouter {
GetRequestTypeInstance: func() interface{} {
return &AnthropicMessageRequest{}
},
RequestConverter: func(req interface{}) *schemas.BifrostRequest {
RequestConverter: func(req interface{}) (*schemas.BifrostRequest, error) {
if anthropicReq, ok := req.(*AnthropicMessageRequest); ok {
return anthropicReq.ConvertToBifrostRequest()
return anthropicReq.ConvertToBifrostRequest(), nil
}
return nil
return nil, errors.New("invalid request type")
},
ResponseFunc: func(resp *schemas.BifrostResponse) interface{} {
return DeriveAnthropicFromBifrostResponse(resp)
ResponseConverter: func(resp *schemas.BifrostResponse) (interface{}, error) {
return DeriveAnthropicFromBifrostResponse(resp), nil
},
Comment thread
Pratham-Mishra04 marked this conversation as resolved.
},
}
Expand Down
37 changes: 27 additions & 10 deletions transports/bifrost-http/integrations/anthropic/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,7 @@ func (r *AnthropicMessageRequest) ConvertToBifrostRequest() *schemas.BifrostRequ
case "image":
if content.Source != nil {
bifrostMsg.UserMessage = &schemas.UserMessage{
ImageContent: &schemas.ImageContent{
Type: bifrost.Ptr(content.Source.Type),
URL: content.Source.Data,
MediaType: &content.Source.MediaType,
},
ImageContent: convertAnthropicImageSource(content.Source),
}
}
case "tool_use":
Expand Down Expand Up @@ -171,11 +167,7 @@ func (r *AnthropicMessageRequest) ConvertToBifrostRequest() *schemas.BifrostRequ
}
}
if content.Source != nil {
bifrostMsg.ToolMessage.ImageContent = &schemas.ImageContent{
Type: bifrost.Ptr(content.Source.Type),
URL: content.Source.Data,
MediaType: &content.Source.MediaType,
}
bifrostMsg.ToolMessage.ImageContent = convertAnthropicImageSource(content.Source)
}
bifrostMsg.Role = schemas.ModelChatMessageRoleTool
}
Expand Down Expand Up @@ -416,3 +408,28 @@ func DeriveAnthropicFromBifrostResponse(bifrostResp *schemas.BifrostResponse) *A
anthropicResp.Content = content
return anthropicResp
}

// convertAnthropicImageSource converts an Anthropic image source to Bifrost ImageContent format
func convertAnthropicImageSource(source *AnthropicImageSource) *schemas.ImageContent {
if source == nil {
return nil
}

// Convert Anthropic source type to Bifrost ImageContentType
var contentType schemas.ImageContentType
switch source.Type {
case "base64":
contentType = schemas.ImageContentTypeBase64
case "url":
contentType = schemas.ImageContentTypeURL
default:
// Default to base64 if unknown type, as this is more common in Anthropic
contentType = schemas.ImageContentTypeBase64
}

return &schemas.ImageContent{
Type: contentType,
URL: source.Data,
MediaType: &source.MediaType,
}
}
11 changes: 6 additions & 5 deletions transports/bifrost-http/integrations/genai/router.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package genai

import (
"errors"
"fmt"
"strings"

Expand All @@ -24,14 +25,14 @@ func NewGenAIRouter(client *bifrost.Bifrost) *GenAIRouter {
GetRequestTypeInstance: func() interface{} {
return &GeminiChatRequest{}
},
RequestConverter: func(req interface{}) *schemas.BifrostRequest {
RequestConverter: func(req interface{}) (*schemas.BifrostRequest, error) {
if geminiReq, ok := req.(*GeminiChatRequest); ok {
return geminiReq.ConvertToBifrostRequest()
return geminiReq.ConvertToBifrostRequest(), nil
}
return nil
return nil, errors.New("invalid request type")
},
Comment thread
Pratham-Mishra04 marked this conversation as resolved.
ResponseFunc: func(resp *schemas.BifrostResponse) interface{} {
return DeriveGenAIFromBifrostResponse(resp)
ResponseConverter: func(resp *schemas.BifrostResponse) (interface{}, error) {
return DeriveGenAIFromBifrostResponse(resp), nil
},
PreCallback: extractAndSetModelFromURL,
},
Expand Down
6 changes: 2 additions & 4 deletions transports/bifrost-http/integrations/genai/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,19 +171,17 @@ func (r *GeminiChatRequest) convertContentToBifrostMessages(content genai_sdk.Co

case part.InlineData != nil:
// Handle inline images/media
imageType := "base64"
imageContent := schemas.ImageContent{
Type: &imageType,
Type: schemas.ImageContentTypeBase64,
URL: fmt.Sprintf("data:%s;base64,%s", part.InlineData.MIMEType, base64.StdEncoding.EncodeToString(part.InlineData.Data)),
MediaType: &part.InlineData.MIMEType,
}
imageContents = append(imageContents, imageContent)

case part.FileData != nil:
// Handle file references
imageType := "url"
imageContent := schemas.ImageContent{
Type: &imageType,
Type: schemas.ImageContentTypeURL,
URL: part.FileData.FileURI,
MediaType: &part.FileData.MIMEType,
}
Expand Down
158 changes: 158 additions & 0 deletions transports/bifrost-http/integrations/litellm/router.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package litellm

import (
"encoding/json"
"errors"
"slices"

bifrost "github.com/maximhq/bifrost/core"
"github.com/maximhq/bifrost/core/schemas"
"github.com/maximhq/bifrost/transports/bifrost-http/integrations"
"github.com/maximhq/bifrost/transports/bifrost-http/integrations/anthropic"
"github.com/maximhq/bifrost/transports/bifrost-http/integrations/genai"
"github.com/maximhq/bifrost/transports/bifrost-http/integrations/openai"
"github.com/valyala/fasthttp"
)

// LiteLLMRequestWrapper wraps any provider-specific request type
type LiteLLMRequestWrapper struct {
Model string `json:"model"`
ActualRequest interface{} `json:"-"` // This will hold the actual provider-specific request
Provider schemas.ModelProvider `json:"-"`
}

// LiteLLMRouter holds route registrations for LiteLLM endpoints.
// It supports standard chat completions and image-enabled vision capabilities.
// LiteLLM is fully OpenAI-compatible, so we reuse OpenAI types
// with aliases for clarity and minimal LiteLLM-specific extensions
type LiteLLMRouter struct {
*integrations.GenericRouter
}

// NewLiteLLMRouter creates a new LiteLLMRouter with the given bifrost client.
func NewLiteLLMRouter(client *bifrost.Bifrost) *LiteLLMRouter {
paths := []string{
"/chat/completions",
"/v1/messages",
}

getRequestTypeInstance := func() interface{} {
return &LiteLLMRequestWrapper{}
}

availableProviders := []schemas.ModelProvider{
schemas.OpenAI,
schemas.Anthropic,
schemas.Vertex,
schemas.Azure,
}

// Pre-hook to determine provider and parse request with correct type
preHook := func(ctx *fasthttp.RequestCtx, req interface{}) error {
wrapper, ok := req.(*LiteLLMRequestWrapper)
if !ok {
return errors.New("invalid request wrapper type")
}

if wrapper.Model == "" {
return errors.New("model field is required")
}

// Determine provider from model
provider := integrations.GetProviderFromModel(wrapper.Model)
if !slices.Contains(availableProviders, provider) {
return errors.New("unsupported provider: " + string(provider))
}

// Get the request body
body := ctx.Request.Body()
if len(body) == 0 {
return errors.New("request body is required")
}

// Create the appropriate request type based on provider and re-parse
var actualReq interface{}
switch provider {
case schemas.OpenAI, schemas.Azure:
actualReq = &openai.OpenAIChatRequest{}
case schemas.Anthropic:
actualReq = &anthropic.AnthropicMessageRequest{}
case schemas.Vertex:
actualReq = &genai.GeminiChatRequest{}
default:
return errors.New("unsupported provider: " + string(provider))
}
Comment thread
Pratham-Mishra04 marked this conversation as resolved.

// Parse the body into the correct request type
if err := json.Unmarshal(body, actualReq); err != nil {
return errors.New("failed to parse request for provider " + string(provider) + ": " + err.Error())
}

// Store the parsed request and provider in the wrapper
wrapper.ActualRequest = actualReq
wrapper.Provider = provider

return nil
}

requestConverter := func(req interface{}) (*schemas.BifrostRequest, error) {
wrapper, ok := req.(*LiteLLMRequestWrapper)
if !ok {
return nil, errors.New("invalid request wrapper type")
}

if wrapper.ActualRequest == nil {
return nil, errors.New("request was not properly processed by pre-hook")
}

// Handle different provider-specific request types
switch actualReq := wrapper.ActualRequest.(type) {
case *openai.OpenAIChatRequest:
bifrostReq := actualReq.ConvertToBifrostRequest()
bifrostReq.Provider = wrapper.Provider
return bifrostReq, nil

case *anthropic.AnthropicMessageRequest:
bifrostReq := actualReq.ConvertToBifrostRequest()
bifrostReq.Provider = wrapper.Provider
return bifrostReq, nil

case *genai.GeminiChatRequest:
bifrostReq := actualReq.ConvertToBifrostRequest()
bifrostReq.Provider = wrapper.Provider
return bifrostReq, nil

default:
return nil, errors.New("unsupported request type")
}
}

responseConverter := func(resp *schemas.BifrostResponse) (interface{}, error) {
switch resp.ExtraFields.Provider {
case schemas.OpenAI, schemas.Azure:
return openai.DeriveOpenAIFromBifrostResponse(resp), nil
case schemas.Anthropic:
return anthropic.DeriveAnthropicFromBifrostResponse(resp), nil
case schemas.Vertex:
return genai.DeriveGenAIFromBifrostResponse(resp), nil
default:
return resp, nil
}
}

routes := []integrations.RouteConfig{}
for _, path := range paths {
routes = append(routes, integrations.RouteConfig{
Path: "/litellm" + path,
Method: "POST",
GetRequestTypeInstance: getRequestTypeInstance,
RequestConverter: requestConverter,
ResponseConverter: responseConverter,
PreCallback: preHook,
})
}

return &LiteLLMRouter{
GenericRouter: integrations.NewGenericRouter(client, routes),
}
}
12 changes: 7 additions & 5 deletions transports/bifrost-http/integrations/openai/router.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package openai

import (
"errors"

bifrost "github.com/maximhq/bifrost/core"
"github.com/maximhq/bifrost/core/schemas"
"github.com/maximhq/bifrost/transports/bifrost-http/integrations"
Expand All @@ -21,14 +23,14 @@ func NewOpenAIRouter(client *bifrost.Bifrost) *OpenAIRouter {
GetRequestTypeInstance: func() interface{} {
return &OpenAIChatRequest{}
},
RequestConverter: func(req interface{}) *schemas.BifrostRequest {
RequestConverter: func(req interface{}) (*schemas.BifrostRequest, error) {
if openaiReq, ok := req.(*OpenAIChatRequest); ok {
return openaiReq.ConvertToBifrostRequest()
return openaiReq.ConvertToBifrostRequest(), nil
}
return nil
return nil, errors.New("invalid request type")
},
ResponseFunc: func(resp *schemas.BifrostResponse) interface{} {
return DeriveOpenAIFromBifrostResponse(resp)
ResponseConverter: func(resp *schemas.BifrostResponse) (interface{}, error) {
return DeriveOpenAIFromBifrostResponse(resp), nil
},
},
}
Expand Down
Loading