Skip to content
Open
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
18 changes: 17 additions & 1 deletion core/schemas/bifrost.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ type RequestInput struct {
ChatCompletionInput *[]BifrostMessage `json:"chat_completion_input,omitempty"`
EmbeddingInput *EmbeddingInput `json:"embedding_input,omitempty"`
SpeechInput *SpeechInput `json:"speech_input,omitempty"`
TranscriptionInput *TranscriptionInput `json:"transcription_input,omitempty"`
TranscriptionInput *TranscriptionInput `json:"transcription_input,omitempty"`
ManagementInput *ManagementInput `json:"management_input,omitempty"`
}

// EmbeddingInput represents the input for an embedding request.
Expand Down Expand Up @@ -198,6 +199,13 @@ type TranscriptionInput struct {
Format *string `json:"file_format,omitempty"` // Type of file, not required in openai, but required in gemini
}

// ManagementInput represents the input for a management API request.
type ManagementInput struct {
Endpoint string `json:"endpoint"`
QueryParams map[string]string `json:"query_params,omitempty"`
APIKey string `json:"api_key,omitempty"`
}

// BifrostRequest represents a request to be processed by Bifrost.
// It must be provided when calling the Bifrost for text completion, chat completion, or embedding.
// It contains the model identifier, input data, and parameters for the request.
Expand Down Expand Up @@ -449,6 +457,7 @@ type BifrostResponse struct {
Data []BifrostEmbedding `json:"data,omitempty"` // Maps to "data" field in provider responses (e.g., OpenAI embedding format)
Speech *BifrostSpeech `json:"speech,omitempty"` // Maps to "speech" field in provider responses (e.g., OpenAI speech format)
Transcribe *BifrostTranscribe `json:"transcribe,omitempty"` // Maps to "transcribe" field in provider responses (e.g., OpenAI transcription format)
Management *ManagementResponse `json:"management,omitempty"` // Maps to "management" field in provider responses (e.g., OpenAI models API format)
Model string `json:"model,omitempty"`
Created int `json:"created,omitempty"` // The Unix timestamp (in seconds).
ServiceTier *string `json:"service_tier,omitempty"`
Expand Down Expand Up @@ -666,6 +675,13 @@ type BifrostTranscribe struct {
*BifrostTranscribeStreamResponse
}

// ManagementResponse represents the response from a management API call
type ManagementResponse struct {
Data []byte `json:"data"`
StatusCode int `json:"status_code"`
Headers map[string]string `json:"headers,omitempty"`
}

Comment on lines +678 to +684
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Deduplicate ManagementResponse type (reuse schemas across packages).

An identical ManagementResponse exists in integrations/utils.go. Keep a single source of truth here and remove the duplicate from integrations to prevent drift.

Apply this diff in transports/bifrost-http/integrations/utils.go to reuse the schema type:

-// ManagementResponse represents the response from a management API call
-type ManagementResponse struct {
-  Data       []byte            `json:"data"`
-  StatusCode int               `json:"status_code"`
-  Headers    map[string]string `json:"headers,omitempty"`
-}

And update ForwardRequest’s return type:

-func (c *ManagementAPIClient) ForwardRequest(
+func (c *ManagementAPIClient) ForwardRequest(
   ctx context.Context,
   provider schemas.ModelProvider,
   endpoint string,
   apiKey string,
   queryParams map[string]string,
-) (*ManagementResponse, error) {
+) (*schemas.ManagementResponse, error) {

Also update the return statement:

- return &ManagementResponse{
+ return &schemas.ManagementResponse{
   Data:       body,
   StatusCode: resp.StatusCode,
   Headers:    headers,
 }, nil
🤖 Prompt for AI Agents
In core/schemas/bifrost.go around lines 678-684 the ManagementResponse struct is
defined; remove the duplicate ManagementResponse definition from
transports/bifrost-http/integrations/utils.go and instead import the
core/schemas package there. Update the ForwardRequest function signature to
return (schemas.ManagementResponse, error) (or *schemas.ManagementResponse if
pointers are used across the codebase) and adjust its return statement to
construct and return a schemas.ManagementResponse (mapping data, status code and
headers into the core schema) before returning, ensuring any header/map types
are converted to match the core type.

// BifrostTranscribeNonStreamResponse represents non-streaming specific fields only
type BifrostTranscribeNonStreamResponse struct {
Task *string `json:"task,omitempty"` // e.g., "transcribe"
Expand Down
144 changes: 121 additions & 23 deletions transports/bifrost-http/integrations/anthropic/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ package anthropic

import (
"errors"
"fmt"
"log"
"strings"

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/lib"
"github.com/valyala/fasthttp"
)

// AnthropicRouter handles Anthropic-compatible API endpoints
Expand All @@ -16,35 +20,43 @@ type AnthropicRouter struct {

// CreateAnthropicRouteConfigs creates route configurations for Anthropic endpoints.
func CreateAnthropicRouteConfigs(pathPrefix string) []integrations.RouteConfig {
return []integrations.RouteConfig{
{
Path: pathPrefix + "/v1/messages",
Method: "POST",
GetRequestTypeInstance: func() interface{} {
return &AnthropicMessageRequest{}
},
RequestConverter: func(req interface{}) (*schemas.BifrostRequest, error) {
if anthropicReq, ok := req.(*AnthropicMessageRequest); ok {
return anthropicReq.ConvertToBifrostRequest(), nil
}
return nil, errors.New("invalid request type")
},
var routes []integrations.RouteConfig

// Messages endpoint
routes = append(routes, integrations.RouteConfig{
Path: pathPrefix + "/v1/messages",
Method: "POST",
GetRequestTypeInstance: func() interface{} {
return &AnthropicMessageRequest{}
},
RequestConverter: func(req interface{}) (*schemas.BifrostRequest, error) {
if anthropicReq, ok := req.(*AnthropicMessageRequest); ok {
return anthropicReq.ConvertToBifrostRequest(), nil
}
return nil, errors.New("invalid request type")
},
ResponseConverter: func(resp *schemas.BifrostResponse) (interface{}, error) {
return DeriveAnthropicFromBifrostResponse(resp), nil
},
ErrorConverter: func(err *schemas.BifrostError) interface{} {
return DeriveAnthropicErrorFromBifrostError(err)
},
StreamConfig: &integrations.StreamConfig{
ResponseConverter: func(resp *schemas.BifrostResponse) (interface{}, error) {
return DeriveAnthropicFromBifrostResponse(resp), nil
return DeriveAnthropicStreamFromBifrostResponse(resp), nil
},
ErrorConverter: func(err *schemas.BifrostError) interface{} {
return DeriveAnthropicErrorFromBifrostError(err)
},
StreamConfig: &integrations.StreamConfig{
ResponseConverter: func(resp *schemas.BifrostResponse) (interface{}, error) {
return DeriveAnthropicStreamFromBifrostResponse(resp), nil
},
ErrorConverter: func(err *schemas.BifrostError) interface{} {
return DeriveAnthropicStreamFromBifrostError(err)
},
return DeriveAnthropicStreamFromBifrostError(err)
},
},
})

// Add management endpoints only for primary Anthropic integration
if pathPrefix == "/anthropic" {
routes = append(routes, createAnthropicManagementRoutes(pathPrefix)...)
}

return routes
}

// NewAnthropicRouter creates a new AnthropicRouter with the given bifrost client.
Expand All @@ -53,3 +65,89 @@ func NewAnthropicRouter(client *bifrost.Bifrost, handlerStore lib.HandlerStore)
GenericRouter: integrations.NewGenericRouter(client, handlerStore, CreateAnthropicRouteConfigs("/anthropic")),
}
}

// createAnthropicManagementRoutes creates route configurations for Anthropic management endpoints
func createAnthropicManagementRoutes(pathPrefix string) []integrations.RouteConfig {
var routes []integrations.RouteConfig

// Management endpoints - following the same for-loop pattern as other routes
for _, path := range []string{
"/v1/models",
"/v1/usage",
} {
log.Printf("Creating management route: %s", pathPrefix + path)
routes = append(routes, integrations.RouteConfig{
Path: pathPrefix + path,
Method: "GET",
GetRequestTypeInstance: func() interface{} {
return &integrations.ManagementRequest{}
},
RequestConverter: func(req interface{}) (*schemas.BifrostRequest, error) {
// For management endpoints, we create a minimal BifrostRequest
// The actual API call is handled by the PreCallback (handleAnthropicManagementRequest)
return &schemas.BifrostRequest{
Provider: schemas.Anthropic,
Model: "management", // Special model type for management requests
Input: schemas.RequestInput{}, // Empty input - management doesn't need chat data
}, nil
},
ResponseConverter: func(resp *schemas.BifrostResponse) (interface{}, error) {
return map[string]interface{}{
"object": "list",
"data": []interface{}{},
}, nil
},
ErrorConverter: func(err *schemas.BifrostError) interface{} {
return map[string]interface{}{
"object": "list",
"data": []interface{}{},
}
},
PreCallback: handleAnthropicManagementRequest,
})
}

return routes
}

// handleAnthropicManagementRequest handles management endpoint requests by forwarding directly to Anthropic API
func handleAnthropicManagementRequest(ctx *fasthttp.RequestCtx, req interface{}) error {
// Extract API key from request
apiKey, err := integrations.ExtractAPIKeyFromContext(ctx)
if err != nil {
integrations.SendManagementError(ctx, err, 401)
return err
}

Comment on lines +116 to +121
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Short‑circuit after writing errors to avoid double handling.
PreCallback currently writes an error response then returns a non‑nil error without marking the request as handled for API‑key missing and unknown‑endpoint branches. This risks duplicate handling by the GenericRouter.

Apply this diff to consistently mark as handled and return nil after writing the response:

@@
-  if err != nil {
-    integrations.SendManagementError(ctx, err, 401)
-    return err
-  }
+  if err != nil {
+    integrations.SendManagementError(ctx, err, 401)
+    ctx.SetUserValue("management_handled", true)
+    return nil
+  }
@@
-  default:
-    integrations.SendManagementError(ctx, fmt.Errorf("unknown management endpoint"), 404)
-    return fmt.Errorf("unknown management endpoint")
+  default:
+    err = fmt.Errorf("unknown management endpoint")
+    integrations.SendManagementError(ctx, err, 404)
+    ctx.SetUserValue("management_handled", true)
+    return nil

Also applies to: 133-136

🤖 Prompt for AI Agents
transports/bifrost-http/integrations/anthropic/router.go lines 116-121 (and
similarly 133-136): after writing an error response with
integrations.SendManagementError(ctx, err, ...) you must mark the request as
handled and return nil to avoid double handling by GenericRouter; update both
branches so that immediately after SendManagementError you call the codepath
that marks the context/request as handled (e.g.,
integrations.MarkRequestHandled(ctx) or set the handled flag on ctx) and then
return nil instead of returning the original error.

// Extract query parameters
queryParams := integrations.ExtractQueryParams(ctx)

// Determine the endpoint based on the path
var endpoint string
path := string(ctx.Path())
switch {
case strings.HasSuffix(path, "/v1/models"):
endpoint = "/v1/models"
case strings.HasSuffix(path, "/v1/usage"):
endpoint = "/v1/usage"
default:
integrations.SendManagementError(ctx, fmt.Errorf("unknown management endpoint"), 404)
return fmt.Errorf("unknown management endpoint")
}

// Create management client and forward the request
client := integrations.NewManagementAPIClient()
response, err := client.ForwardRequest(ctx, schemas.Anthropic, endpoint, apiKey, queryParams)
if err != nil {
log.Printf("Failed to forward request to Anthropic: %v", err)
integrations.SendManagementError(ctx, err, 500)
ctx.SetUserValue("management_handled", true)
return nil
}
log.Printf("Response status code: %v", response.StatusCode)

// Send the successful response
integrations.SendManagementResponse(ctx, response.Data, response.StatusCode)
ctx.SetUserValue("management_handled", true)
return nil
}
98 changes: 98 additions & 0 deletions transports/bifrost-http/integrations/genai/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package genai
import (
"errors"
"fmt"
"log"
"strings"

bifrost "github.com/maximhq/bifrost/core"
Expand Down Expand Up @@ -51,6 +52,11 @@ func CreateGenAIRouteConfigs(pathPrefix string) []integrations.RouteConfig {
PreCallback: extractAndSetModelFromURL,
})

// Add management endpoints only for primary GenAI integration
if pathPrefix == "/genai" {
routes = append(routes, createGenAIManagementRoutes(pathPrefix)...)
}

return routes
}

Expand Down Expand Up @@ -115,3 +121,95 @@ func extractAndSetModelFromURL(ctx *fasthttp.RequestCtx, req interface{}) error

return fmt.Errorf("invalid request type for GenAI")
}

// createGenAIManagementRoutes creates route configurations for Google GenAI management endpoints
func createGenAIManagementRoutes(pathPrefix string) []integrations.RouteConfig {
var routes []integrations.RouteConfig

// Management endpoints - following the same for-loop pattern as other routes
for _, path := range []string{
"/v1beta/models",
"/v1beta/models/{model:*}",
} {
log.Printf("Creating management route: %s", pathPrefix + path)
routes = append(routes, integrations.RouteConfig{
Path: pathPrefix + path,
Method: "GET",
GetRequestTypeInstance: func() interface{} {
return &integrations.ManagementRequest{}
},
RequestConverter: func(req interface{}) (*schemas.BifrostRequest, error) {
// For management endpoints, we create a minimal BifrostRequest
// The actual API call is handled by the PreCallback (handleGenAIManagementRequest)
return &schemas.BifrostRequest{
Provider: schemas.Gemini,
Model: "management", // Special model type for management requests
Input: schemas.RequestInput{}, // Empty input - management doesn't need chat data
}, nil
},
ResponseConverter: func(resp *schemas.BifrostResponse) (interface{}, error) {
return map[string]interface{}{
"object": "list",
"data": []interface{}{},
}, nil
},
ErrorConverter: func(err *schemas.BifrostError) interface{} {
return map[string]interface{}{
"object": "list",
"data": []interface{}{},
}
},
PreCallback: handleGenAIManagementRequest,
})
}

return routes
}

// handleGenAIManagementRequest handles management endpoint requests by forwarding directly to Google GenAI API
func handleGenAIManagementRequest(ctx *fasthttp.RequestCtx, req interface{}) error {
// Extract API key from request
apiKey, err := integrations.ExtractAPIKeyFromContext(ctx)
if err != nil {
integrations.SendManagementError(ctx, err, 401)
return err
}

// Extract query parameters
queryParams := integrations.ExtractQueryParams(ctx)

// Determine the endpoint based on the path
var endpoint string
path := string(ctx.Path())

if strings.HasSuffix(path, "/v1beta/models") && !strings.Contains(path, "/v1beta/models/") {
// List models endpoint
endpoint = "/v1beta/models"
} else if strings.Contains(path, "/v1beta/models/") {
// Model details endpoint - extract model from path
model := ctx.UserValue("model")
if model == nil {
integrations.SendManagementError(ctx, fmt.Errorf("model parameter is required"), 400)
return fmt.Errorf("model parameter is required")
}
modelStr := model.(string)
endpoint = "/v1beta/models/" + modelStr
} else {
integrations.SendManagementError(ctx, fmt.Errorf("unknown management endpoint"), 404)
return fmt.Errorf("unknown management endpoint")
}

// Create management client and forward the request
client := integrations.NewManagementAPIClient()
response, err := client.ForwardRequest(ctx, schemas.Gemini, endpoint, apiKey, queryParams)
if err != nil {
integrations.SendManagementError(ctx, err, 500)
ctx.SetUserValue("management_handled", true)
return nil
}

// Send the successful response
integrations.SendManagementResponse(ctx, response.Data, response.StatusCode)
ctx.SetUserValue("management_handled", true)
return nil
}
Loading