diff --git a/core/providers/anthropic.go b/core/providers/anthropic.go
index 0c9c9d239d..2d4c7dd14c 100644
--- a/core/providers/anthropic.go
+++ b/core/providers/anthropic.go
@@ -1198,7 +1198,7 @@ func handleAnthropicStreaming(
logger.Warn(fmt.Sprintf("Error reading %s stream: %v", providerType, err))
processAndSendError(ctx, postHookRunner, err, responseChan, logger)
} else {
- response := createBifrostChatCompletionChunkResponse(usage, finishReason, chunkIndex, params, providerType)
+ response := createBifrostChatCompletionChunkResponse(messageID, usage, finishReason, chunkIndex, params, providerType)
handleStreamEndWithSuccess(ctx, response, postHookRunner, responseChan, logger)
}
}()
diff --git a/core/providers/bedrock.go b/core/providers/bedrock.go
index d4686316fd..6d0520a84d 100644
--- a/core/providers/bedrock.go
+++ b/core/providers/bedrock.go
@@ -259,7 +259,9 @@ func (provider *BedrockProvider) GetProviderKey() schemas.ModelProvider {
// CompleteRequest sends a request to Bedrock's API and handles the response.
// It constructs the API URL, sets up AWS authentication, and processes the response.
// Returns the response body or an error if the request fails.
-func (provider *BedrockProvider) completeRequest(ctx context.Context, requestBody map[string]interface{}, path string, config schemas.BedrockKeyConfig) ([]byte, *schemas.BifrostError) {
+func (provider *BedrockProvider) completeRequest(ctx context.Context, requestBody map[string]interface{}, path string, key schemas.Key) ([]byte, *schemas.BifrostError) {
+ config := key.BedrockKeyConfig
+
region := "us-east-1"
if config.Region != nil {
region = *config.Region
@@ -301,9 +303,14 @@ func (provider *BedrockProvider) completeRequest(ctx context.Context, requestBod
// Set any extra headers from network config
setExtraHeadersHTTP(req, provider.networkConfig.ExtraHeaders, nil)
- // Sign the request using either explicit credentials or IAM role authentication
- if err := signAWSRequest(ctx, req, config.AccessKey, config.SecretKey, config.SessionToken, region, "bedrock", provider.GetProviderKey()); err != nil {
- return nil, err
+ // If Value is set, use API Key authentication - else use IAM role authentication
+ if key.Value != "" {
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", key.Value))
+ } else {
+ // Sign the request using either explicit credentials or IAM role authentication
+ if err := signAWSRequest(ctx, req, config.AccessKey, config.SecretKey, config.SessionToken, region, "bedrock", provider.GetProviderKey()); err != nil {
+ return nil, err
+ }
}
// Execute the request
@@ -834,7 +841,8 @@ func (provider *BedrockProvider) TextCompletion(ctx context.Context, model strin
"prompt": text,
}, preparedParams)
- body, err := provider.completeRequest(ctx, requestBody, fmt.Sprintf("%s/invoke", model), *key.BedrockKeyConfig)
+ path := provider.getModelPath("invoke", model, key)
+ body, err := provider.completeRequest(ctx, requestBody, path, key)
if err != nil {
return nil, err
}
@@ -1018,19 +1026,10 @@ func (provider *BedrockProvider) ChatCompletion(ctx context.Context, model strin
requestBody := mergeConfig(messageBody, preparedParams)
// Format the path with proper model identifier
- path := fmt.Sprintf("%s/converse", model)
-
- if key.BedrockKeyConfig.Deployments != nil {
- if inferenceProfileId, ok := key.BedrockKeyConfig.Deployments[model]; ok {
- if key.BedrockKeyConfig.ARN != nil {
- encodedModelIdentifier := url.QueryEscape(fmt.Sprintf("%s/%s", *key.BedrockKeyConfig.ARN, inferenceProfileId))
- path = fmt.Sprintf("%s/converse", encodedModelIdentifier)
- }
- }
- }
+ path := provider.getModelPath("converse", model, key)
// Create the signed request
- responseBody, err := provider.completeRequest(ctx, requestBody, path, *key.BedrockKeyConfig)
+ responseBody, err := provider.completeRequest(ctx, requestBody, path, key)
if err != nil {
return nil, err
}
@@ -1223,16 +1222,16 @@ func (provider *BedrockProvider) Embedding(ctx context.Context, model string, ke
switch {
case strings.Contains(model, "amazon.titan-embed-text"):
- return provider.handleTitanEmbedding(ctx, model, *key.BedrockKeyConfig, input, params, providerName)
+ return provider.handleTitanEmbedding(ctx, model, key, input, params, providerName)
case strings.Contains(model, "cohere.embed"):
- return provider.handleCohereEmbedding(ctx, model, *key.BedrockKeyConfig, input, params, providerName)
+ return provider.handleCohereEmbedding(ctx, model, key, input, params, providerName)
default:
return nil, newConfigurationError("embedding is not supported for this Bedrock model", providerName)
}
}
// handleTitanEmbedding handles embedding requests for Amazon Titan models.
-func (provider *BedrockProvider) handleTitanEmbedding(ctx context.Context, model string, config schemas.BedrockKeyConfig, input *schemas.EmbeddingInput, params *schemas.ModelParameters, providerName schemas.ModelProvider) (*schemas.BifrostResponse, *schemas.BifrostError) {
+func (provider *BedrockProvider) handleTitanEmbedding(ctx context.Context, model string, key schemas.Key, input *schemas.EmbeddingInput, params *schemas.ModelParameters, providerName schemas.ModelProvider) (*schemas.BifrostResponse, *schemas.BifrostError) {
// Titan Text Embeddings V1/V2 - only supports single text input
if len(input.Texts) == 0 {
return nil, newConfigurationError("no input text provided for embedding", providerName)
@@ -1258,8 +1257,8 @@ func (provider *BedrockProvider) handleTitanEmbedding(ctx context.Context, model
}
// Properly escape model name for URL path to ensure AWS SIGv4 signing works correctly
- path := url.PathEscape(model) + "/invoke"
- rawResponse, err := provider.completeRequest(ctx, requestBody, path, config)
+ path := provider.getModelPath("invoke", model, key)
+ rawResponse, err := provider.completeRequest(ctx, requestBody, path, key)
if err != nil {
return nil, err
}
@@ -1306,7 +1305,7 @@ func (provider *BedrockProvider) handleTitanEmbedding(ctx context.Context, model
}
// handleCohereEmbedding handles embedding requests for Cohere models on Bedrock.
-func (provider *BedrockProvider) handleCohereEmbedding(ctx context.Context, model string, config schemas.BedrockKeyConfig, input *schemas.EmbeddingInput, params *schemas.ModelParameters, providerName schemas.ModelProvider) (*schemas.BifrostResponse, *schemas.BifrostError) {
+func (provider *BedrockProvider) handleCohereEmbedding(ctx context.Context, model string, key schemas.Key, input *schemas.EmbeddingInput, params *schemas.ModelParameters, providerName schemas.ModelProvider) (*schemas.BifrostResponse, *schemas.BifrostError) {
if len(input.Texts) == 0 {
return nil, newConfigurationError("no input text provided for embedding", providerName)
}
@@ -1320,8 +1319,8 @@ func (provider *BedrockProvider) handleCohereEmbedding(ctx context.Context, mode
}
// Properly escape model name for URL path to ensure AWS SIGv4 signing works correctly
- path := url.PathEscape(model) + "/invoke"
- rawResponse, err := provider.completeRequest(ctx, requestBody, path, config)
+ path := provider.getModelPath("invoke", model, key)
+ rawResponse, err := provider.completeRequest(ctx, requestBody, path, key)
if err != nil {
return nil, err
}
@@ -1433,16 +1432,7 @@ func (provider *BedrockProvider) ChatCompletionStream(ctx context.Context, postH
requestBody := mergeConfig(messageBody, preparedParams)
// Format the path with proper model identifier for streaming
- path := fmt.Sprintf("%s/converse-stream", model)
-
- if key.BedrockKeyConfig.Deployments != nil {
- if inferenceProfileId, ok := key.BedrockKeyConfig.Deployments[model]; ok {
- if key.BedrockKeyConfig.ARN != nil {
- encodedModelIdentifier := url.PathEscape(fmt.Sprintf("%s/%s", *key.BedrockKeyConfig.ARN, inferenceProfileId))
- path = fmt.Sprintf("%s/converse-stream", encodedModelIdentifier)
- }
- }
- }
+ path := provider.getModelPath("converse-stream", model, key)
region := "us-east-1"
if key.BedrockKeyConfig.Region != nil {
@@ -1464,9 +1454,14 @@ func (provider *BedrockProvider) ChatCompletionStream(ctx context.Context, postH
// Set any extra headers from network config
setExtraHeadersHTTP(req, provider.networkConfig.ExtraHeaders, nil)
- // Sign the request using either explicit credentials or IAM role authentication
- if signErr := signAWSRequest(ctx, req, key.BedrockKeyConfig.AccessKey, key.BedrockKeyConfig.SecretKey, key.BedrockKeyConfig.SessionToken, region, "bedrock", providerName); signErr != nil {
- return nil, signErr
+ // If Value is set, use API Key authentication - else use IAM role authentication
+ if key.Value != "" {
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", key.Value))
+ } else {
+ // Sign the request using either explicit credentials or IAM role authentication
+ if err := signAWSRequest(ctx, req, key.BedrockKeyConfig.AccessKey, key.BedrockKeyConfig.SecretKey, key.BedrockKeyConfig.SessionToken, region, "bedrock", providerName); err != nil {
+ return nil, err
+ }
}
// Make the request
@@ -1531,7 +1526,7 @@ func (provider *BedrockProvider) ChatCompletionStream(ctx context.Context, postH
}
// Send final response
- response := createBifrostChatCompletionChunkResponse(usage, finishReason, chunkIndex, params, providerName)
+ response := createBifrostChatCompletionChunkResponse(messageID, usage, finishReason, chunkIndex, params, providerName)
handleStreamEndWithSuccess(ctx, response, postHookRunner, responseChan, provider.logger)
}()
@@ -1827,3 +1822,19 @@ func (provider *BedrockProvider) Transcription(ctx context.Context, model string
func (provider *BedrockProvider) TranscriptionStream(ctx context.Context, postHookRunner schemas.PostHookRunner, model string, key schemas.Key, input *schemas.TranscriptionInput, params *schemas.ModelParameters) (chan *schemas.BifrostStream, *schemas.BifrostError) {
return nil, newUnsupportedOperationError("transcription stream", "bedrock")
}
+
+func (provider *BedrockProvider) getModelPath(basePath string, model string, key schemas.Key) string {
+ // Format the path with proper model identifier for streaming
+ path := fmt.Sprintf("%s/%s", model, basePath)
+
+ if key.BedrockKeyConfig.Deployments != nil {
+ if inferenceProfileId, ok := key.BedrockKeyConfig.Deployments[model]; ok {
+ if key.BedrockKeyConfig.ARN != nil {
+ encodedModelIdentifier := url.PathEscape(fmt.Sprintf("%s/%s", *key.BedrockKeyConfig.ARN, inferenceProfileId))
+ path = fmt.Sprintf("%s/%s", encodedModelIdentifier, basePath)
+ }
+ }
+ }
+
+ return path
+}
diff --git a/core/providers/openai.go b/core/providers/openai.go
index 1440c3588a..b5cc638956 100644
--- a/core/providers/openai.go
+++ b/core/providers/openai.go
@@ -438,6 +438,7 @@ func handleOpenAIStreaming(
usage := &schemas.LLMUsage{}
var finishReason *string
+ var id string
for scanner.Scan() {
line := scanner.Text()
@@ -513,6 +514,10 @@ func handleOpenAIStreaming(
response.Choices[0].FinishReason = nil
}
+ if response.ID != "" && id == "" {
+ id = response.ID
+ }
+
// Handle regular content chunks
if choice.BifrostStreamResponseChoice != nil && (choice.BifrostStreamResponseChoice.Delta.Content != nil || len(choice.BifrostStreamResponseChoice.Delta.ToolCalls) > 0) {
chunkIndex++
@@ -529,7 +534,7 @@ func handleOpenAIStreaming(
logger.Warn(fmt.Sprintf("Error reading stream: %v", err))
processAndSendError(ctx, postHookRunner, err, responseChan, logger)
} else {
- response := createBifrostChatCompletionChunkResponse(usage, finishReason, chunkIndex, params, providerName)
+ response := createBifrostChatCompletionChunkResponse(id, usage, finishReason, chunkIndex, params, providerName)
handleStreamEndWithSuccess(ctx, response, postHookRunner, responseChan, logger)
}
}()
diff --git a/core/providers/utils.go b/core/providers/utils.go
index c8673c0d26..54e64b4d21 100644
--- a/core/providers/utils.go
+++ b/core/providers/utils.go
@@ -804,6 +804,7 @@ func processAndSendError(
}
func createBifrostChatCompletionChunkResponse(
+ id string,
usage *schemas.LLMUsage,
finishReason *string,
currentChunkIndex int,
@@ -811,6 +812,7 @@ func createBifrostChatCompletionChunkResponse(
providerName schemas.ModelProvider,
) *schemas.BifrostResponse {
response := &schemas.BifrostResponse{
+ ID: id,
Object: "chat.completion.chunk",
Usage: usage,
Choices: []schemas.BifrostResponseChoice{
diff --git a/core/schemas/account.go b/core/schemas/account.go
index aa4d66a45a..44563ca7be 100644
--- a/core/schemas/account.go
+++ b/core/schemas/account.go
@@ -31,6 +31,8 @@ type VertexKeyConfig struct {
AuthCredentials string `json:"auth_credentials,omitempty"`
}
+// NOTE: To use Vertex IAM role authentication, set AuthCredentials to empty string.
+
// BedrockKeyConfig represents the AWS Bedrock-specific configuration.
// It contains AWS-specific settings required for authentication and service access.
type BedrockKeyConfig struct {
@@ -42,6 +44,9 @@ type BedrockKeyConfig struct {
Deployments map[string]string `json:"deployments,omitempty"` // Mapping of model identifiers to inference profiles
}
+// NOTE: To use Bedrock IAM role authentication, set both AccessKey and SecretKey to empty strings.
+// To use Bedrock API Key authentication, set Value in Key struct instead.
+
// Account defines the interface for managing provider accounts and their configurations.
// It provides methods to access provider-specific settings, API keys, and configurations.
type Account interface {
diff --git a/core/utils.go b/core/utils.go
index ab359ac39f..e0bddf0aa6 100644
--- a/core/utils.go
+++ b/core/utils.go
@@ -2,7 +2,6 @@ package bifrost
import (
"context"
- "encoding/json"
"math/rand"
"time"
@@ -14,30 +13,6 @@ func Ptr[T any](v T) *T {
return &v
}
-// MarshalToString marshals the given value to a JSON string.
-func MarshalToString(v any) (string, error) {
- if v == nil {
- return "", nil
- }
- data, err := json.Marshal(v)
- if err != nil {
- return "", err
- }
- return string(data), nil
-}
-
-// MarshalToStringPtr marshals the given value to a JSON string and returns a pointer to the string.
-func MarshalToStringPtr(v any) (*string, error) {
- if v == nil {
- return nil, nil
- }
- data, err := MarshalToString(v)
- if err != nil {
- return nil, err
- }
- return &data, nil
-}
-
func attachContextKeys(ctx context.Context, req *schemas.BifrostRequest, requestType schemas.RequestType) context.Context {
ctx = context.WithValue(ctx, schemas.BifrostContextKeyRequestType, requestType)
ctx = context.WithValue(ctx, schemas.BifrostContextKeyRequestProvider, req.Provider)
diff --git a/docs/quickstart/gateway/provider-configuration.mdx b/docs/quickstart/gateway/provider-configuration.mdx
index 97b64049b5..d43f244e55 100644
--- a/docs/quickstart/gateway/provider-configuration.mdx
+++ b/docs/quickstart/gateway/provider-configuration.mdx
@@ -764,12 +764,13 @@ AWS Bedrock supports both explicit credentials and IAM role authentication:

1. Navigate to **"Providers"** → **"AWS Bedrock"**
-2. Set **Access Key**: AWS Access Key ID (or leave empty for IAM)
-3. Set **Secret Key**: AWS Secret Access Key (or leave empty for IAM)
-4. Set **Region**: e.g., `us-east-1`
-5. Configure **Deployments**: Map model names to inference profiles
-6. Set **ARN**: Required for deployments mapping
-7. Save configuration
+2. Set **API Key**: AWS API Key (or leave empty if using IAM role authentication)
+3. Set **Access Key**: AWS Access Key ID (or leave empty to use IAM in environment)
+4. Set **Secret Key**: AWS Secret Access Key (or leave empty to use IAM in environment)
+5. Set **Region**: e.g., `us-east-1`
+6. Configure **Deployments**: Map model names to inference profiles
+7. Set **ARN**: Required for deployments mapping
+8. Save configuration
@@ -833,9 +834,10 @@ curl --location 'http://localhost:8080/api/providers' \
**Notes:**
-- If both `access_key` and `secret_key` are empty, Bifrost uses IAM role authentication from environment
-- `arn` is required for URL formation - `deployments` mapping is ignored without it
-- When using `arn` + `deployments`, Bifrost uses model profiles; otherwise forms path with incoming model name directly
+- If using API Key authentication, set `value` field to the API key, else leave it empty for IAM role authentication.
+- In IAM role authentication, if both `access_key` and `secret_key` are empty, Bifrost uses IAM role authentication from the environment.
+- `arn` is required for URL formation - `deployments` mapping is ignored without it.
+- When using `arn` + `deployments`, Bifrost uses model profiles; otherwise forms path with incoming model name directly.
### Google Vertex
diff --git a/docs/quickstart/go-sdk/provider-configuration.mdx b/docs/quickstart/go-sdk/provider-configuration.mdx
index ef25539270..1c7af8456b 100644
--- a/docs/quickstart/go-sdk/provider-configuration.mdx
+++ b/docs/quickstart/go-sdk/provider-configuration.mdx
@@ -307,9 +307,10 @@ func (a *MyAccount) GetKeysForProvider(ctx *context.Context, provider schemas.Mo
{
Models: []string{"anthropic.claude-3-sonnet-20240229-v1:0", "anthropic.claude-v2:1"},
Weight: 1.0,
+ Value: os.Getenv("AWS_API_KEY"), // Leave empty for IAM role authentication
BedrockKeyConfig: &schemas.BedrockKeyConfig{
- AccessKey: os.Getenv("AWS_ACCESS_KEY_ID"), // Leave empty for IAM role
- SecretKey: os.Getenv("AWS_SECRET_ACCESS_KEY"), // Leave empty for IAM role
+ AccessKey: os.Getenv("AWS_ACCESS_KEY_ID"), // Leave empty for API Key authentication or system's IAM pickup
+ SecretKey: os.Getenv("AWS_SECRET_ACCESS_KEY"), // Leave empty for API Key authentication or system's IAM pickup
SessionToken: bifrost.Ptr(os.Getenv("AWS_SESSION_TOKEN")), // Optional
Region: bifrost.Ptr("us-east-1"),
// For model profiles (inference profiles)
@@ -327,9 +328,10 @@ func (a *MyAccount) GetKeysForProvider(ctx *context.Context, provider schemas.Mo
```
**Notes:**
-- If both `AccessKey` and `SecretKey` are empty, Bifrost uses IAM role authentication from environment
-- `ARN` is required for URL formation - `Deployments` mapping is ignored without it
-- When using `ARN` + `Deployments`, Bifrost uses model profiles; otherwise forms path with incoming model name directly
+- If using API Key authentication, set `Value` field to the API key, else leave it empty for IAM role authentication.
+- In IAM role authentication, if both `AccessKey` and `SecretKey` are empty, Bifrost uses IAM from the environment.
+- `ARN` is required for URL formation - `Deployments` mapping is ignored without it.
+- When using `ARN` + `Deployments`, Bifrost uses model profiles; otherwise forms path with incoming model name directly.
diff --git a/framework/configstore/migrations.go b/framework/configstore/migrations.go
index 89ce7ddecb..b6a7e5e8c1 100644
--- a/framework/configstore/migrations.go
+++ b/framework/configstore/migrations.go
@@ -9,9 +9,7 @@ import (
// Migrate performs the necessary database migrations.
func triggerMigrations(db *gorm.DB) error {
- var err error
- err = migrationInit(db)
- if err != nil {
+ if err := migrationInit(db); err != nil {
return err
}
return nil
diff --git a/framework/configstore/sqlite.go b/framework/configstore/sqlite.go
index cfd6603036..76514bbba3 100644
--- a/framework/configstore/sqlite.go
+++ b/framework/configstore/sqlite.go
@@ -7,7 +7,6 @@ import (
"os"
"strings"
- bifrost "github.com/maximhq/bifrost/core"
"github.com/maximhq/bifrost/core/schemas"
"github.com/maximhq/bifrost/framework/logstore"
"github.com/maximhq/bifrost/framework/vectorstore"
@@ -603,7 +602,7 @@ func (s *SQLiteConfigStore) UpdateVectorStoreConfig(config *vectorstore.Config)
if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&TableVectorStoreConfig{}).Error; err != nil {
return err
}
- jsonConfig, err := bifrost.MarshalToStringPtr(config.Config)
+ jsonConfig, err := marshalToStringPtr(config.Config)
if err != nil {
return err
}
@@ -642,7 +641,7 @@ func (s *SQLiteConfigStore) UpdateLogsStoreConfig(config *logstore.Config) error
if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&TableLogStoreConfig{}).Error; err != nil {
return err
}
- jsonConfig, err := bifrost.MarshalToStringPtr(config)
+ jsonConfig, err := marshalToStringPtr(config)
if err != nil {
return err
}
diff --git a/framework/configstore/utils.go b/framework/configstore/utils.go
index 78aa44ddc2..fe7137d763 100644
--- a/framework/configstore/utils.go
+++ b/framework/configstore/utils.go
@@ -8,6 +8,30 @@ import (
"github.com/maximhq/bifrost/core/schemas"
)
+// marshalToString marshals the given value to a JSON string.
+func marshalToString(v any) (string, error) {
+ if v == nil {
+ return "", nil
+ }
+ data, err := json.Marshal(v)
+ if err != nil {
+ return "", err
+ }
+ return string(data), nil
+}
+
+// marshalToStringPtr marshals the given value to a JSON string and returns a pointer to the string.
+func marshalToStringPtr(v any) (*string, error) {
+ if v == nil {
+ return nil, nil
+ }
+ data, err := marshalToString(v)
+ if err != nil {
+ return nil, err
+ }
+ return &data, nil
+}
+
// deepCopy creates a deep copy of a given type
func deepCopy[T any](in T) (T, error) {
var out T
@@ -134,4 +158,4 @@ func substituteMCPEnvVars(config *schemas.MCPConfig, envKeys map[string][]EnvKey
}
}
}
-}
\ No newline at end of file
+}
diff --git a/ui/app/providers/fragments/apiKeysFormFragment.tsx b/ui/app/providers/fragments/apiKeysFormFragment.tsx
index 4abe6714a8..b3838b4d61 100644
--- a/ui/app/providers/fragments/apiKeysFormFragment.tsx
+++ b/ui/app/providers/fragments/apiKeysFormFragment.tsx
@@ -3,6 +3,7 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
+import { Separator } from "@/components/ui/separator";
import { TagInput } from "@/components/ui/tagInput";
import { Textarea } from "@/components/ui/textarea";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
@@ -20,7 +21,7 @@ const MODEL_PLACEHOLDERS = {
default: "e.g. gpt-4, gpt-3.5-turbo. Leave blank for all models.",
openai: "e.g. gpt-4, gpt-3.5-turbo, gpt-4-turbo, gpt-4o",
azure: "e.g. gpt-4, gpt-3.5-turbo (must match deployment mappings)",
- bedrock: "e.g. anthropic.claude-v2, amazon.titan-text-express-v1",
+ bedrock: "e.g. claude-v2, titan-text-express-v1",
vertex: "e.g. gemini-pro, text-bison, chat-bison",
};
@@ -41,15 +42,15 @@ export function ApiKeyFormFragment({ control, providerName }: Props) {
{isBedrock && (