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
1 change: 0 additions & 1 deletion core/providers/vertex.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,6 @@ func (provider *VertexProvider) ChatCompletion(model, key string, messages []sch
if err := json.Unmarshal(body, &openAIErr); err != nil {
// Try Vertex error format if OpenAI format fails
if err := json.Unmarshal(body, &vertexErr); err != nil {
fmt.Printf("error unmarshalling vertex error: %s, body: %s", err, string(body))
return nil, &schemas.BifrostError{
IsBifrostError: true,
StatusCode: &resp.StatusCode,
Expand Down
2 changes: 1 addition & 1 deletion transports/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ docker build \
3. Run the Docker container:

```bash
docker run -p 8080:8080 bifrost-transports
docker run --name bifrost-api -p 8080:8080 bifrost-transports
```

You can also add a flag for `DROP_EXCESS_REQUESTS=false` in your Docker build command to drop excess requests when the buffer is full. Read more about `DROP_EXCESS_REQUESTS` and `POOL_SIZE` [here](https://github.com/maximhq/bifrost/tree/main?tab=README-ov-file#additional-configurations).
Expand Down
6 changes: 5 additions & 1 deletion transports/bifrost-http/integrations/genai/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/fasthttp/router"
bifrost "github.com/maximhq/bifrost/core"
"github.com/maximhq/bifrost/core/schemas"
"github.com/maximhq/bifrost/transports/bifrost-http/tracking"
"github.com/valyala/fasthttp"
)

Expand Down Expand Up @@ -46,7 +47,10 @@ func (g *GenAIRouter) handleChatCompletion(ctx *fasthttp.RequestCtx) {
}

bifrostReq := req.ConvertToBifrostRequest("google/" + modelStr)
result, err := g.client.ChatCompletionRequest(schemas.Vertex, bifrostReq, ctx)

bifrostCtx := tracking.ConvertToBifrostContext(ctx)

result, err := g.client.ChatCompletionRequest(schemas.Vertex, bifrostReq, *bifrostCtx)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
json.NewEncoder(ctx).Encode(err)
Expand Down
176 changes: 176 additions & 0 deletions transports/bifrost-http/lib/account.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// Package lib provides core functionality for the Bifrost HTTP service,
// including configuration management and account handling.
package lib

import (
"errors"
"fmt"
"reflect"
"strings"
"sync"

"github.com/joho/godotenv"
"github.com/maximhq/bifrost/core/schemas"
)

// BaseAccount implements the Account interface for Bifrost.
// It manages provider configurations and API keys.
type BaseAccount struct {
Config ConfigMap // Map of provider configurations
mu sync.Mutex // Mutex to protect Config access
}

// GetConfiguredProviders returns a list of all configured providers.
// Implements the Account interface.
func (baseAccount *BaseAccount) GetConfiguredProviders() ([]schemas.ModelProvider, error) {
baseAccount.mu.Lock()
defer baseAccount.mu.Unlock()

providers := make([]schemas.ModelProvider, 0, len(baseAccount.Config))
for provider := range baseAccount.Config {
providers = append(providers, provider)
}
return providers, nil
}

// GetKeysForProvider returns the API keys configured for a specific provider.
// Implements the Account interface.
func (baseAccount *BaseAccount) GetKeysForProvider(providerKey schemas.ModelProvider) ([]schemas.Key, error) {
baseAccount.mu.Lock()
defer baseAccount.mu.Unlock()

return baseAccount.Config[providerKey].Keys, nil
}

// GetConfigForProvider returns the complete configuration for a specific provider.
// Implements the Account interface.
func (baseAccount *BaseAccount) GetConfigForProvider(providerKey schemas.ModelProvider) (*schemas.ProviderConfig, error) {
baseAccount.mu.Lock()
defer baseAccount.mu.Unlock()

config, exists := baseAccount.Config[providerKey]
if !exists {
return nil, errors.New("config for provider not found")
}

providerConfig := &schemas.ProviderConfig{}

if config.NetworkConfig != nil {
providerConfig.NetworkConfig = *config.NetworkConfig
}

if config.MetaConfig != nil {
providerConfig.MetaConfig = *config.MetaConfig
}

if config.ConcurrencyAndBufferSize != nil {
providerConfig.ConcurrencyAndBufferSize = *config.ConcurrencyAndBufferSize
}

return providerConfig, nil
}

// readKeys reads environment variables from a .env file and updates the provider configurations.
// It replaces values starting with "env." in the config with actual values from the environment.
// Returns an error if any required environment variable is missing.
func (baseAccount *BaseAccount) ReadKeys(envLocation string) error {
envVars, err := godotenv.Read(envLocation)
if err != nil {
return fmt.Errorf("failed to read .env file: %w", err)
}

// Helper function to check and replace env values
replaceEnvValue := func(value string) (string, error) {
if strings.HasPrefix(value, "env.") {
envKey := strings.TrimPrefix(value, "env.")
if envValue, exists := envVars[envKey]; exists {
return envValue, nil
}
return "", fmt.Errorf("environment variable %s not found in .env file", envKey)
}
return value, nil
}

// Helper function to recursively check and replace env values in a struct
var processStruct func(interface{}) error
processStruct = func(v interface{}) error {
val := reflect.ValueOf(v)

// Dereference pointer if present
if val.Kind() == reflect.Ptr {
val = val.Elem()
}

// Handle interface types
if val.Kind() == reflect.Interface {
val = val.Elem()
// If the interface value is a pointer, dereference it
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
}

if val.Kind() != reflect.Struct {
return nil
}

typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
fieldType := typ.Field(i)

// Skip unexported fields
if !field.CanSet() {
continue
}

switch field.Kind() {
case reflect.String:
if field.CanSet() {
value := field.String()
if strings.HasPrefix(value, "env.") {
newValue, err := replaceEnvValue(value)
if err != nil {
return fmt.Errorf("field %s: %w", fieldType.Name, err)
}
field.SetString(newValue)
}
}
case reflect.Interface:
if !field.IsNil() {
if err := processStruct(field.Interface()); err != nil {
return err
}
}
}
}
return nil
}

// Lock the config map for the entire update operation
baseAccount.mu.Lock()
defer baseAccount.mu.Unlock()

// Check and replace values in provider configs
for provider, config := range baseAccount.Config {
// Check keys
for i, key := range config.Keys {
newValue, err := replaceEnvValue(key.Value)
if err != nil {
return fmt.Errorf("provider %s: %w", provider, err)
}
config.Keys[i].Value = newValue
}

// Check meta config if it exists
if config.MetaConfig != nil {
if err := processStruct(config.MetaConfig); err != nil {
return fmt.Errorf("provider %s: %w", provider, err)
}
}

baseAccount.Config[provider] = config
}

return nil
}
120 changes: 120 additions & 0 deletions transports/bifrost-http/lib/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Package lib provides core functionality for the Bifrost HTTP service,
// including configuration management and account handling.
package lib

import (
"encoding/json"
"log"
"os"
"strings"

"github.com/maximhq/bifrost/core/schemas"
"github.com/maximhq/bifrost/core/schemas/meta"
)

// ProviderConfig represents the configuration for a specific AI model provider.
// It includes API keys, network settings, provider-specific metadata, and concurrency settings.
type ProviderConfig struct {
Keys []schemas.Key `json:"keys"` // API keys for the provider
NetworkConfig *schemas.NetworkConfig `json:"network_config,omitempty"` // Network-related settings
MetaConfig *schemas.MetaConfig `json:"-"` // Provider-specific metadata
ConcurrencyAndBufferSize *schemas.ConcurrencyAndBufferSize `json:"concurrency_and_buffer_size,omitempty"` // Concurrency settings
}

// ConfigMap maps provider names to their configurations.
type ConfigMap map[schemas.ModelProvider]ProviderConfig

// readConfig reads and parses the configuration file.
// It handles case conversion for provider names and sets up provider-specific metadata.
// Returns a ConfigMap containing all provider configurations.
// Panics if the config file cannot be read or parsed.
//
// In the config file, use placeholder keys (e.g., env.OPENAI_API_KEY) instead of hardcoding actual values.
// These placeholders will be replaced with the corresponding values from the .env file.
// Location of the .env file is specified by the -env flag. It
// Example:
//
// "keys":[{
// "value": "env.OPENAI_API_KEY"
// "models": ["gpt-4o-mini", "gpt-4-turbo"],
// "weight": 1.0
// }]
//
// In this example, OPENAI_API_KEY refers to a key in the .env file. At runtime, its value will be used to replace the placeholder.
// Same setup applies to keys in meta configs of all the providers.
// Example:
//
// "meta_config": {
// "secret_access_key": "env.BEDROCK_ACCESS_KEY"
// "region": "env.BEDROCK_REGION"
// }
//
// In this example, BEDROCK_ACCESS_KEY and BEDROCK_REGION refer to keys in the .env file.
func ReadConfig(configLocation string) ConfigMap {
data, err := os.ReadFile(configLocation)
if err != nil {
log.Fatalf("failed to read config JSON file: %v", err)
}

// First unmarshal into a map with string keys to handle case conversion
var rawConfig map[string]ProviderConfig
if err := json.Unmarshal(data, &rawConfig); err != nil {
log.Fatalf("failed to unmarshal JSON: %v", err)
}

if rawConfig == nil {
log.Fatalf("provided config is nil")
}

// Create a new config map with lowercase provider names
config := make(ConfigMap)
for rawProvider, cfg := range rawConfig {
provider := schemas.ModelProvider(strings.ToLower(rawProvider))

switch provider {
case schemas.Azure:
var azureMetaConfig meta.AzureMetaConfig
if err := json.Unmarshal(data, &struct {
Azure struct {
MetaConfig *meta.AzureMetaConfig `json:"meta_config"`
} `json:"Azure"`
}{Azure: struct {
MetaConfig *meta.AzureMetaConfig `json:"meta_config"`
}{&azureMetaConfig}}); err != nil {
log.Printf("warning: failed to unmarshal Azure meta config: %v", err)
}
var metaConfig schemas.MetaConfig = &azureMetaConfig
cfg.MetaConfig = &metaConfig
case schemas.Bedrock:
var bedrockMetaConfig meta.BedrockMetaConfig
if err := json.Unmarshal(data, &struct {
Bedrock struct {
MetaConfig *meta.BedrockMetaConfig `json:"meta_config"`
} `json:"Bedrock"`
}{Bedrock: struct {
MetaConfig *meta.BedrockMetaConfig `json:"meta_config"`
}{&bedrockMetaConfig}}); err != nil {
log.Printf("warning: failed to unmarshal Bedrock meta config: %v", err)
}
var metaConfig schemas.MetaConfig = &bedrockMetaConfig
cfg.MetaConfig = &metaConfig
case schemas.Vertex:
var vertexMetaConfig meta.VertexMetaConfig
if err := json.Unmarshal(data, &struct {
Vertex struct {
MetaConfig *meta.VertexMetaConfig `json:"meta_config"`
} `json:"Vertex"`
}{Vertex: struct {
MetaConfig *meta.VertexMetaConfig `json:"meta_config"`
}{&vertexMetaConfig}}); err != nil {
log.Printf("warning: failed to unmarshal Vertex meta config: %v", err)
}
var metaConfig schemas.MetaConfig = &vertexMetaConfig
cfg.MetaConfig = &metaConfig
}

config[provider] = cfg
}

return config
}
Loading