diff --git a/core/schemas/bifrost.go b/core/schemas/bifrost.go index cbf8e3538..6e6768147 100644 --- a/core/schemas/bifrost.go +++ b/core/schemas/bifrost.go @@ -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. @@ -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. @@ -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"` @@ -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"` +} + // BifrostTranscribeNonStreamResponse represents non-streaming specific fields only type BifrostTranscribeNonStreamResponse struct { Task *string `json:"task,omitempty"` // e.g., "transcribe" diff --git a/transports/bifrost-http/integrations/anthropic/router.go b/transports/bifrost-http/integrations/anthropic/router.go index 8c9e8a36b..4feeaa6cf 100644 --- a/transports/bifrost-http/integrations/anthropic/router.go +++ b/transports/bifrost-http/integrations/anthropic/router.go @@ -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 @@ -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. @@ -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 + } + + // 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 +} diff --git a/transports/bifrost-http/integrations/genai/router.go b/transports/bifrost-http/integrations/genai/router.go index 5192cea80..f1f4167ff 100644 --- a/transports/bifrost-http/integrations/genai/router.go +++ b/transports/bifrost-http/integrations/genai/router.go @@ -3,6 +3,7 @@ package genai import ( "errors" "fmt" + "log" "strings" bifrost "github.com/maximhq/bifrost/core" @@ -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 } @@ -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 +} diff --git a/transports/bifrost-http/integrations/openai/router.go b/transports/bifrost-http/integrations/openai/router.go index 03f470bf2..04c261fb5 100644 --- a/transports/bifrost-http/integrations/openai/router.go +++ b/transports/bifrost-http/integrations/openai/router.go @@ -2,6 +2,8 @@ package openai import ( "errors" + "fmt" + "log" "strconv" "strings" @@ -234,6 +236,59 @@ func CreateOpenAIRouteConfigs(pathPrefix string, handlerStore lib.HandlerStore) }) } + // Add management endpoints only for primary OpenAI integration + if pathPrefix == "/openai" { + routes = append(routes, createOpenAIManagementRoutes(pathPrefix)...) + } + + return routes +} + +// createOpenAIManagementRoutes creates route configurations for OpenAI management endpoints +func createOpenAIManagementRoutes(pathPrefix string) []integrations.RouteConfig { + var routes []integrations.RouteConfig + log.Printf("createOpenAIManagementRoutes called with pathPrefix: %s", pathPrefix) + + // Management endpoints - following the same for-loop pattern as other routes + for _, path := range []string{ + "/v1/models", + "/v1/organizations", + "/v1/usage", + "/v1/models/{model}", + } { + fullPath := pathPrefix + path + log.Printf("Creating management route: %s", fullPath) + routes = append(routes, integrations.RouteConfig{ + Path: fullPath, + 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 (handleOpenAIManagementRequest) + return &schemas.BifrostRequest{ + Provider: schemas.OpenAI, + 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: handleOpenAIManagementRequest, + }) + } + return routes } @@ -344,3 +399,72 @@ func parseTranscriptionMultipartRequest(ctx *fasthttp.RequestCtx, req interface{ return nil } + + +// handleOpenAIManagementRequest handles management endpoint requests by forwarding directly to OpenAI API +func handleOpenAIManagementRequest(ctx *fasthttp.RequestCtx, req interface{}) error { + log.Printf("handleOpenAIManagementRequest called with path: %s", string(ctx.Path())) + log.Printf("Request method: %s", string(ctx.Method())) + + // Extract API key from request + apiKey, err := integrations.ExtractAPIKeyFromContext(ctx) + if err != nil { + integrations.SendManagementError(ctx, err, 401) + ctx.SetUserValue("management_handled", true) + return nil // Don't return error, we've handled the request + } + + // Extract query parameters + queryParams := integrations.ExtractQueryParams(ctx) + + // Determine the endpoint based on the path + var endpoint string + path := string(ctx.Path()) + + // Remove the path prefix to get the actual endpoint + if strings.HasPrefix(path, "/openai") { + endpoint = strings.TrimPrefix(path, "/openai") + } else if strings.HasPrefix(path, "/litellm") { + endpoint = strings.TrimPrefix(path, "/litellm") + } else if strings.HasPrefix(path, "/langchain") { + endpoint = strings.TrimPrefix(path, "/langchain") + } else { + endpoint = path + } + + log.Println("endpoint", endpoint) + // Validate that it's a known management endpoint + switch { + case endpoint == "/v1/models": + case strings.HasPrefix(endpoint, "/v1/models/"): + case endpoint == "/v1/organizations": + case endpoint == "/v1/usage": + default: + integrations.SendManagementError(ctx, fmt.Errorf("unknown management endpoint: %s", endpoint), 404) + ctx.SetUserValue("management_handled", true) + return nil // Don't return error, we've handled the request + } + log.Println("endpoint", endpoint) + + // Create management client and forward the request + client := integrations.NewManagementAPIClient() + response, err := client.ForwardRequest(ctx, schemas.OpenAI, endpoint, apiKey, queryParams) + if err != nil { + integrations.SendManagementError(ctx, err, 500) + ctx.SetUserValue("management_handled", true) + return nil + } + + // Check if the response indicates an error (4xx, 5xx status codes) + if response.StatusCode >= 400 { + log.Printf("OpenAI API returned error status %d: %s", response.StatusCode, string(response.Data)) + integrations.SendManagementResponse(ctx, response.Data, response.StatusCode) + 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 +} diff --git a/transports/bifrost-http/integrations/utils.go b/transports/bifrost-http/integrations/utils.go index 322bd8942..b26d6e52d 100644 --- a/transports/bifrost-http/integrations/utils.go +++ b/transports/bifrost-http/integrations/utils.go @@ -51,10 +51,13 @@ import ( "context" "encoding/json" "fmt" + "io" "log" + "net/http" "regexp" "strconv" "strings" + "time" "bufio" @@ -603,6 +606,11 @@ func (g *GenericRouter) createHandler(config RouteConfig) fasthttp.RequestHandle } } + // Check if the request was handled by the PreCallback (e.g., management requests) + if ctx.UserValue("management_handled") != nil { + return // Request was handled by PreCallback, don't continue with normal processing + } + // Convert the integration-specific request to Bifrost format bifrostReq, err := config.RequestConverter(req) if err != nil { @@ -1203,3 +1211,178 @@ func mapFinishReasonToAnthropic(finishReason string) string { return finishReason } } + +// Management Request Types and Utilities + +// ManagementRequest represents a management API request that will be forwarded directly to providers +type ManagementRequest struct { + Provider schemas.ModelProvider `json:"provider"` + Endpoint string `json:"endpoint"` + QueryParams map[string]string `json:"query_params,omitempty"` + APIKey string `json:"api_key,omitempty"` +} + +// ConvertToBifrostRequest converts a management request to a BifrostRequest +// For management requests, we'll handle them directly without going through Bifrost +func (r *ManagementRequest) ConvertToBifrostRequest() *schemas.BifrostRequest { + // Return a special request that will be handled by the management handler + return &schemas.BifrostRequest{ + Provider: r.Provider, + Model: "management", // Special model type for management requests + Input: schemas.RequestInput{ + // We'll handle this specially in the router + }, + } +} + +// 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"` +} + +// ManagementAPIClient handles direct API calls to provider management endpoints +type ManagementAPIClient struct { + httpClient *http.Client +} + +// NewManagementAPIClient creates a new management API client +func NewManagementAPIClient() *ManagementAPIClient { + return &ManagementAPIClient{ + httpClient: &http.Client{ + Timeout: 30 * time.Second, + }, + } +} + +// ProviderEndpoints defines the base URLs for different providers +var ProviderEndpoints = map[schemas.ModelProvider]string{ + schemas.OpenAI: "https://api.openai.com", + schemas.Anthropic: "https://api.anthropic.com", + schemas.Gemini: "https://generativelanguage.googleapis.com", +} + +// ForwardRequest forwards a GET request directly to the provider's API +func (c *ManagementAPIClient) ForwardRequest( + ctx context.Context, + provider schemas.ModelProvider, + endpoint string, + apiKey string, + queryParams map[string]string, +) (*ManagementResponse, error) { + baseURL, exists := ProviderEndpoints[provider] + if !exists { + return nil, fmt.Errorf("unsupported provider: %s", provider) + } + log.Println("baseURL", baseURL) + + // Build the full URL + fullURL := baseURL + endpoint + + // Add query parameters if any + if len(queryParams) > 0 { + fullURL += "?" + first := true + for key, value := range queryParams { + if !first { + fullURL += "&" + } + fullURL += fmt.Sprintf("%s=%s", key, value) + first = false + } + } + + // Create the HTTP request + req, err := http.NewRequestWithContext(ctx, "GET", fullURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + // Set the authorization header + req.Header.Set("Authorization", "Bearer "+apiKey) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", "Bifrost-Management-Client/1.0") + + // Make the request + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to make request: %w", err) + } + defer resp.Body.Close() + + // Read the response body + body, err := io.ReadAll(resp.Body) + log.Printf("Response body: %s", string(body)) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + // Extract headers + headers := make(map[string]string) + for key, values := range resp.Header { + if len(values) > 0 { + headers[key] = values[0] + } + } + log.Printf("Response status code: %d", resp.StatusCode) + log.Printf("Response headers: %v", resp.Header) + return &ManagementResponse{ + Data: body, + StatusCode: resp.StatusCode, + Headers: headers, + }, nil +} + +// ExtractAPIKeyFromContext extracts the API key from the request context +func ExtractAPIKeyFromContext(ctx *fasthttp.RequestCtx) (string, error) { + // Try to get the API key from the Authorization header + authHeader := ctx.Request.Header.Peek("Authorization") + if len(authHeader) > 0 { + // Remove "Bearer " prefix if present + key := string(authHeader) + if len(key) > 7 && key[:7] == "Bearer " { + return key[7:], nil + } + return key, nil + } + + // Try to get from X-API-Key header + apiKeyHeader := ctx.Request.Header.Peek("X-API-Key") + if len(apiKeyHeader) > 0 { + return string(apiKeyHeader), nil + } + + return "", fmt.Errorf("no API key found in request headers") +} + +// ExtractQueryParams extracts query parameters from the request +func ExtractQueryParams(ctx *fasthttp.RequestCtx) map[string]string { + params := make(map[string]string) + ctx.QueryArgs().VisitAll(func(key, value []byte) { + params[string(key)] = string(value) + }) + return params +} + +// SendManagementResponse sends a management API response to the client +func SendManagementResponse(ctx *fasthttp.RequestCtx, data []byte, statusCode int) { + ctx.SetStatusCode(statusCode) + ctx.SetContentType("application/json") + ctx.SetBody(data) +} + +// SendManagementError sends an error response for management endpoints +func SendManagementError(ctx *fasthttp.RequestCtx, err error, statusCode int) { + errorResponse := map[string]interface{}{ + "error": map[string]interface{}{ + "message": err.Error(), + "type": "management_api_error", + }, + } + + errorJSON, _ := json.Marshal(errorResponse) + ctx.SetStatusCode(statusCode) + ctx.SetContentType("application/json") + ctx.SetBody(errorJSON) +} diff --git a/transports/bifrost-http/main.go b/transports/bifrost-http/main.go index a7947640e..a550b28b2 100644 --- a/transports/bifrost-http/main.go +++ b/transports/bifrost-http/main.go @@ -101,6 +101,7 @@ var ( logLevel string // Logger level: debug, info, warn, error logOutputStyle string // Logger output style: json, pretty + apiOnly bool // Run in API-only mode without UI ) const ( @@ -156,6 +157,7 @@ func init() { flag.StringVar(&appDir, "app-dir", DefaultAppDir, "Application data directory (contains config.json and logs)") flag.StringVar(&logLevel, "log-level", DefaultLogLevel, "Logger level (debug, info, warn, error). Default is info.") flag.StringVar(&logOutputStyle, "log-style", DefaultLogOutputStyle, "Logger output type (json or pretty). Default is JSON.") + flag.BoolVar(&apiOnly, "api-only", false, "Run in API-only mode without UI") flag.Parse() // Configure logger from flags @@ -523,9 +525,14 @@ func main() { // Add Prometheus /metrics endpoint r.GET("/metrics", fasthttpadaptor.NewFastHTTPHandler(promhttp.Handler())) - // Add UI routes - serve the embedded Next.js build - r.GET("/", uiHandler) - r.GET("/{filepath:*}", uiHandler) + // Add UI routes - serve the embedded Next.js build (only if not in API-only mode) + if !apiOnly { + logger.Info("Registering UI routes") + // r.GET("/", uiHandler) + // r.GET("/{filepath:*}", uiHandler) + } else { + logger.Info("API-only mode: UI routes disabled") + } r.NotFound = func(ctx *fasthttp.RequestCtx) { handlers.SendError(ctx, fasthttp.StatusNotFound, "Route not found: "+string(ctx.Path()), logger) @@ -548,7 +555,6 @@ func main() { // Start server in a goroutine serverAddr := net.JoinHostPort(host, port) go func() { - logger.Info("successfully started bifrost, serving UI on http://%s:%s", host, port) if err := server.ListenAndServe(serverAddr); err != nil { errChan <- err }