--- title: "Adding a new provider" description: "Learn how to contribute a new provider to Bifrost." icon: "box" --- This guide will walk you through creating a provider for Bifrost, testing locally, adding it to frontend and CI/CD. **Quick Reference**: This guide uses simplified generic examples for clarity. For complete, production-ready implementations: - **OpenAI-compatible providers**: See `core/providers/cerebras/` or `core/providers/groq/` - **Custom API providers**: See `core/providers/huggingface/` or `core/providers/anthropic/` ## Setup 1. **Fork and Clone**: * Fork the repository: https://github.com/maximhq/bifrost/ * Clone your fork: `git clone https://github.com//bifrost/` 2. **Initialize**: * Run `make dev` at the root of the project to set up dependencies and tools. ## Provider Structure Bifrost acts as a gateway: 1. Receives a request in a standard format (defined in `core/schemas/`). 2. Converts it to the provider-specific format. 3. Sends the request to the provider's API. 4. Receives the provider's response. 5. Converts it back to the standard Bifrost response format. To implement a new provider, first step is to add the provider name in `core/schemas/bifrost.go` - Add it in const declaration of `ModelProvider` type in the format `[ProviderName] ModelProvider = "[providername]"` - Then, add it in the StandardProviders array in the same file and if needed add in SupportedBaseProviders. Next, you will create a directory in `core/providers/` and populate it with specific files following our strict conventions. ### Directory Structure The directory structure differs based on whether the provider is OpenAI API compatible: #### Non-OpenAI-compatible Providers If the provider has a **custom API format** (not OpenAI-compatible), create a new folder `core/providers/[provider_name]/`. **Complete Reference Structure** (see `core/providers/huggingface/`): ```text core/providers/ └─ [provider_name]/ # e.g., huggingface/ ├── [provider_name].go # Main provider implementation (REQUIRED) ├── [provider_name]_test.go # Provider automated tests (REQUIRED) ├── types.go # ALL provider-specific types/structs (REQUIRED) ├── utils.go # ALL utility functions and constants (REQUIRED) ├── errors.go # Error handling (if supported) ├── chat.go # Converters for Chat Completion (if supported) ├── speech.go # Converters for Text-to-Speech (if supported) ├── transcription.go # Converters for Speech-to-Text (if supported) ├── embedding.go # Converters for Embeddings (if supported) ├── images.go # Converters for Images (if supported) ├── batches.go # Converters for Batches (if supported) ├── files.go # Converters for Files (if supported) ├── models.go # Converters for List Models (if supported) └── responses.go # Converters for Response Models (if supported) ``` **File Creation Order (CRITICAL)**: 1. Create `types.go` FIRST - Define all provider-specific request/response structures 2. Create `utils.go` SECOND - Define constants, base URLs, and helper functions 3. Create feature files (`chat.go`, `embedding.go`, etc.) THIRD - Implement converters 4. Create `[provider_name].go` FOURTH - Wire everything together 5. Create `[provider_name]_test.go` LAST - Add comprehensive tests #### OpenAI-compatible Providers If the provider is **OpenAI API compatible**, you only need a minimal structure: **Minimal Reference Structure** (see `core/providers/cerebras/`): ```text core/providers/ └─ [provider_name]/ # e.g., cerebras/ ├── [provider_name].go # Main provider implementation (REQUIRED) └── [provider_name]_test.go # Provider automated tests (REQUIRED) ``` These providers reuse the OpenAI converter logic from `core/providers/openai/`. ### File Conventions & Responsibilities We enforce **strict separation of concerns** to keep providers maintainable and consistent. Each file has a specific purpose and must follow these rules. --- #### 1. `types.go` (The Data Layer) **CRITICAL RULE**: All provider-specific structs (Request/Response DTOs) **MUST** go here. **NEVER** define types in other files. **Naming Convention**: - Prefix ALL types with the provider name in PascalCase: `[ProviderName][StructName]` - Examples: `HuggingFaceChatRequest`, `HuggingFaceModel`, `HuggingFaceToolCall` **JSON Tag Requirements**: - Use `json` tags that **exactly match** the provider's API field names - Use `omitempty` for optional fields - Use pointers for nullable fields to distinguish between "not set" and "zero value" **Organization**: - Group related types together with comments (e.g., `// # CHAT TYPES`, `// # MODELS TYPES`) - Define request types before response types - Keep nested types near their parent types **Generic Example Structure**: ```go package providername import "encoding/json" // # MODELS TYPES // ProviderNameModel represents a model from the provider's catalog type ProviderNameModel struct { ID string `json:"id"` Name string `json:"name"` Description *string `json:"description,omitempty"` CreatedAt string `json:"created_at"` } // # CHAT TYPES // ProviderNameChatRequest represents the request payload for chat completion type ProviderNameChatRequest struct { Model string `json:"model" validate:"required"` Messages []ProviderNameChatMessage `json:"messages"` MaxTokens *int `json:"max_tokens,omitempty"` Temperature *float64 `json:"temperature,omitempty"` TopP *float64 `json:"top_p,omitempty"` Stream *bool `json:"stream,omitempty"` Tools []ProviderNameTool `json:"tools,omitempty"` ToolChoice json.RawMessage `json:"tool_choice,omitempty"` // flexible: enum or object } // ProviderNameChatMessage represents a single message in a chat type ProviderNameChatMessage struct { Role *string `json:"role,omitempty"` Content json.RawMessage `json:"content,omitempty"` // flexible: string or []content items Name *string `json:"name,omitempty"` ToolCalls []ProviderNameToolCall `json:"tool_calls,omitempty"` } ``` **Key Points**: - Use `json.RawMessage` for fields that can be multiple types (string or object/array) - Use pointers (`*float64`, `*bool`) for optional fields - Add validation tags when appropriate (`validate:"required"`) - Include comments for complex or non-obvious types --- #### 2. `utils.go` (The Helper Layer) **CRITICAL RULE**: All shared utility functions, constants, and configuration helpers **MUST** go here. **Constants Naming Convention**: - Use camelCase for unexported constants: `defaultInferenceBaseURL` - Use SCREAMING_SNAKE_CASE for exported constants: `INFERENCE_PROVIDERS` - Group related constants together **Function Naming Convention**: - Use camelCase for unexported helpers: `convertTypeToLowerCase`, `parseErrorResponse` - Use PascalCase for exported utilities: `ConfigureProxy`, `BuildHeaders` **Required Contents**: 1. Base URLs and API endpoints 2. Default values and limits 3. Provider-specific constants (like model names, inference providers) 4. HTTP request helpers (headers, authentication) 5. Error handling utilities 6. Data transformation helpers **Generic Example Structure**: ```go package providername import ( "context" "strings" "time" providerUtils "github.com/maximhq/bifrost/core/providers/utils" schemas "github.com/maximhq/bifrost/core/schemas" "github.com/valyala/fasthttp" ) const ( defaultBaseURL = "https://api.provider.com" ) const ( defaultLimit = 100 maxLimit = 500 ) // Helper to parse provider-specific model format func parseModelString(model string) (string, string) { parts := strings.Split(model, "/") if len(parts) == 2 { return parts[0], parts[1] } return "", model } // Helper to convert type fields to lowercase in JSON schemas func convertTypeToLowerCase(schema map[string]interface{}) { // Implementation for schema normalization... } ``` **Organization Tips**: - Group constants by category (URLs, limits, enums) - Document the source/reason for constants (API docs, limits) - Keep helper functions focused and single-purpose - Include error handling in utility functions --- #### 3. `[provider_name].go` (The Controller Layer) **CRITICAL RULE**: This is the **orchestration layer**. It coordinates the request flow but **delegates** all conversion logic to feature files. **Naming Convention**: - Provider struct: `[ProviderName]Provider` (e.g., `HuggingFaceProvider`) - Constructor: `New[ProviderName]Provider(config *schemas.ProviderConfig, logger schemas.Logger)` - Methods: Match interface exactly: `ChatCompletion`, `ChatCompletionStream`, `ListModels`, etc. **Required Struct Fields** (in order): ```go type [ProviderName]Provider struct { logger schemas.Logger // ALWAYS first client *fasthttp.Client // HTTP client networkConfig schemas.NetworkConfig // Network settings sendBackRawResponse bool // Debug flag customProviderConfig *schemas.CustomProviderConfig // Optional } ``` **Constructor Requirements**: 1. Accept `*schemas.ProviderConfig` and `schemas.Logger` 2. Call `config.CheckAndSetDefaults()` 3. Initialize `fasthttp.Client` with timeouts and limits 4. Configure proxy using `providerUtils.ConfigureProxy` 5. Set default BaseURL if not provided 6. Trim trailing slashes from BaseURL 7. Pre-warm response pools if using sync.Pool 8. Return provider instance (and error for OpenAI-compatible providers) **Generic Example Structure**: ```go package providername import ( "context" "strings" "sync" "time" "github.com/bytedance/sonic" providerUtils "github.com/maximhq/bifrost/core/providers/utils" schemas "github.com/maximhq/bifrost/core/schemas" "github.com/valyala/fasthttp" ) // ProviderNameProvider implements the Provider interface type ProviderNameProvider struct { logger schemas.Logger client *fasthttp.Client networkConfig schemas.NetworkConfig sendBackRawResponse bool customProviderConfig *schemas.CustomProviderConfig } // Response pools for memory efficiency (optional but recommended) var chatResponsePool = sync.Pool{ New: func() any { return &ProviderNameChatResponse{} }, } // NewProviderNameProvider creates a new provider instance func NewProviderNameProvider(config *schemas.ProviderConfig, logger schemas.Logger) *ProviderNameProvider { config.CheckAndSetDefaults() client := &fasthttp.Client{ ReadTimeout: time.Second * time.Duration(config.NetworkConfig.DefaultRequestTimeoutInSeconds), WriteTimeout: time.Second * time.Duration(config.NetworkConfig.DefaultRequestTimeoutInSeconds), MaxConnsPerHost: 5000, MaxIdleConnDuration: 30 * time.Second, MaxConnWaitTimeout: 10 * time.Second, } // Configure proxy if provided client = providerUtils.ConfigureProxy(client, config.ProxyConfig, logger) // Set default BaseURL if not provided if config.NetworkConfig.BaseURL == "" { config.NetworkConfig.BaseURL = defaultBaseURL } config.NetworkConfig.BaseURL = strings.TrimRight(config.NetworkConfig.BaseURL, "/") // Pre-warm response pools (optional optimization) for i := 0; i < config.ConcurrencyAndBufferSize.Concurrency; i++ { chatResponsePool.Put(&ProviderNameChatResponse{}) } return &ProviderNameProvider{ logger: logger, client: client, networkConfig: config.NetworkConfig, sendBackRawResponse: config.SendBackRawResponse, customProviderConfig: config.CustomProviderConfig, } } // GetProviderKey returns the provider identifier func (provider *ProviderNameProvider) GetProviderKey() schemas.ModelProvider { return schemas.ProviderName } ``` **Method Implementation Pattern** (STRICT ORDER): 1. **Validation**: Check request validity (optional, usually done in converter) 2. **Convert Request**: Call `To[Provider][Feature]Request()` from feature file 3. **Build HTTP Request**: Construct URL, headers, body 4. **Execute Request**: Use `provider.client.Do()` or streaming logic 5. **Handle Errors**: Parse and convert provider errors to `schemas.BifrostError` 6. **Convert Response**: Call `ToBifrost[Feature]Response()` from feature file 7. **Return Result**: Return Bifrost response or error **Example Method** (generic pattern): ```go func (p *ProviderNameProvider) ChatCompletion( ctx context.Context, key schemas.Key, request *schemas.BifrostChatRequest, ) (*schemas.BifrostChatResponse, *schemas.BifrostError) { // 1. Convert Request providerReq := ToProviderNameChatCompletionRequest(request) // 2. Build HTTP Request body, err := sonic.Marshal(providerReq) if err != nil { return nil, &schemas.BifrostError{/* ... */} } req := fasthttp.AcquireRequest() defer fasthttp.ReleaseRequest(req) req.SetRequestURI(p.networkConfig.BaseURL + "/v1/chat/completions") req.Header.SetMethod("POST") req.Header.Set("Authorization", "Bearer "+key.Value) req.Header.Set("Content-Type", "application/json") req.SetBody(body) // 3. Execute Request resp := fasthttp.AcquireResponse() defer fasthttp.ReleaseResponse(resp) if err := p.client.Do(req, resp); err != nil { return nil, &schemas.BifrostError{/* ... */} } // 4. Handle Errors if resp.StatusCode() != 200 { return nil, parseErrorResponse(resp.Body()) } // 5. Convert Response var providerResp ProviderNameChatResponse if err := sonic.Unmarshal(resp.Body(), &providerResp); err != nil { return nil, &schemas.BifrostError{/* ... */} } return ToBifrostChatResponse(&providerResp) } ``` --- #### 4. Feature Files (`chat.go`, `embedding.go`, `speech.go`, etc.) (The Converter Layer) **CRITICAL RULE**: These files contain **pure transformation functions** ONLY. No HTTP calls, no logging, no side effects. **File Naming Convention**: - `chat.go` - Chat completion converters - `embedding.go` - Embedding converters - `speech.go` - Text-to-speech converters - `transcription.go` - Speech-to-text converters - `models.go` - List models converters - `responses.go` - Response format converters **Function Naming Convention** (STRICT): - **To Provider Format**: `To[ProviderName][Feature]Request(bifrostReq *schemas.Bifrost[Feature]Request) *[ProviderName][Feature]Request` - **To Bifrost Format**: `ToBifrost[Feature]Response(providerResp *[ProviderName][Feature]Response) (*schemas.Bifrost[Feature]Response, *schemas.BifrostError)` **Examples**: - `ToHuggingFaceChatCompletionRequest` - `ToBifrostChatResponse` - `ToHuggingFaceEmbeddingRequest` - `ToBifrostEmbeddingResponse` **Required Converter Pairs** (if feature is supported): - Request converter: Bifrost → Provider - Response converter: Provider → Bifrost **Real Example from `core/providers/huggingface/chat.go`**: ```go package huggingface import ( "encoding/json" "fmt" "github.com/bytedance/sonic" schemas "github.com/maximhq/bifrost/core/schemas" ) // ToHuggingFaceChatCompletionRequest converts a Bifrost chat request to HuggingFace format func ToHuggingFaceChatCompletionRequest(bifrostReq *schemas.BifrostChatRequest) *HuggingFaceChatRequest { if bifrostReq == nil || bifrostReq.Input == nil { return nil } // Convert messages from Bifrost format to HuggingFace format hfMessages := make([]HuggingFaceChatMessage, 0, len(bifrostReq.Input)) for _, msg := range bifrostReq.Input { hfMsg := HuggingFaceChatMessage{} // Set role if msg.Role != "" { role := string(msg.Role) hfMsg.Role = &role } // Set name if present if msg.Name != nil { hfMsg.Name = msg.Name } // Convert content (can be string or structured blocks) if msg.Content != nil { if msg.Content.ContentStr != nil { // Simple string content contentJSON, _ := sonic.Marshal(*msg.Content.ContentStr) hfMsg.Content = json.RawMessage(contentJSON) } else if msg.Content.ContentBlocks != nil { // Structured content blocks (text, images, etc.) contentItems := make([]HuggingFaceContentItem, 0, len(msg.Content.ContentBlocks)) for _, block := range msg.Content.ContentBlocks { item := HuggingFaceContentItem{} blockType := string(block.Type) item.Type = &blockType switch block.Type { case schemas.ChatContentBlockTypeText: if block.Text != nil { item.Text = block.Text } case schemas.ChatContentBlockTypeImage: if block.ImageURLStruct != nil { item.ImageURL = &HuggingFaceImageRef{ URL: block.ImageURLStruct.URL, } } } contentItems = append(contentItems, item) } contentJSON, _ := sonic.Marshal(contentItems) hfMsg.Content = json.RawMessage(contentJSON) } } // Handle tool calls for assistant messages if msg.ChatAssistantMessage != nil && len(msg.ChatAssistantMessage.ToolCalls) > 0 { hfToolCalls := make([]HuggingFaceToolCall, 0, len(msg.ChatAssistantMessage.ToolCalls)) for _, tc := range msg.ChatAssistantMessage.ToolCalls { hfToolCall := HuggingFaceToolCall{ ID: tc.ID, Type: tc.Type, Function: HuggingFaceFunction{ Name: *tc.Function.Name, Arguments: tc.Function.Arguments, }, } hfToolCalls = append(hfToolCalls, hfToolCall) } hfMsg.ToolCalls = hfToolCalls } hfMessages = append(hfMessages, hfMsg) } // Build the request hfReq := &HuggingFaceChatRequest{ Model: bifrostReq.Model, Messages: hfMessages, } // Map parameters if bifrostReq.Params != nil { params := bifrostReq.Params // Map standard parameters if params.Temperature != nil { hfReq.Temperature = params.Temperature } if params.MaxTokens != nil { hfReq.MaxTokens = params.MaxTokens } // ... other standard parameters // Handle provider-specific ExtraParams if params.ExtraParams != nil { if customParam, ok := params.ExtraParams["custom_param"].(string); ok { hfReq.CustomParam = &customParam } } } return hfReq } ``` **Generic Example - Embedding Converter**: ```go package providername import ( "github.com/maximhq/bifrost/core/schemas" ) // ToProviderNameEmbeddingRequest converts a Bifrost embedding request to provider format func ToProviderNameEmbeddingRequest(bifrostReq *schemas.BifrostEmbeddingRequest) *ProviderNameEmbeddingRequest { if bifrostReq == nil { return nil } providerReq := &ProviderNameEmbeddingRequest{ Model: bifrostReq.Model, } // Convert input if bifrostReq.Input != nil { if bifrostReq.Input.Text != nil { providerReq.Input = *bifrostReq.Input.Text } else if bifrostReq.Input.Texts != nil { providerReq.Input = bifrostReq.Input.Texts } } // Map provider-specific parameters from ExtraParams if bifrostReq.Params != nil && bifrostReq.Params.ExtraParams != nil { if normalize, ok := bifrostReq.Params.ExtraParams["normalize"].(bool); ok { providerReq.Normalize = &normalize } } return providerReq } ``` **Generic Example - List Models Converter**: ```go package providername import ( "strings" schemas "github.com/maximhq/bifrost/core/schemas" ) // ToBifrostListModelsResponse converts provider models list to Bifrost format func ToBifrostListModelsResponse( providerResp *ProviderNameListModelsResponse, providerKey schemas.ModelProvider, ) *schemas.BifrostListModelsResponse { if providerResp == nil { return nil } bifrostResponse := &schemas.BifrostListModelsResponse{ Data: make([]schemas.Model, 0, len(providerResp.Models)), } for _, model := range providerResp.Models { // Determine supported methods based on model capabilities supported := determineSupportedMethods(model) if len(supported) == 0 { continue } newModel := schemas.Model{ ID: model.ID, Name: &model.Name, SupportedMethods: supported, } bifrostResponse.Data = append(bifrostResponse.Data, newModel) } return bifrostResponse } // Helper to determine which Bifrost methods a model supports func determineSupportedMethods(model ProviderNameModel) []string { methods := []string{} // Logic to derive supported methods from model metadata // This varies by provider return methods } ``` **Converter Best Practices**: 1. **Always check for nil** inputs at the start 2. **Pre-allocate slices** with known capacity for performance 3. **Handle optional fields** using pointers in types 4. **Use ExtraParams** for provider-specific fields not in standard schema 5. **Document complex conversions** with inline comments 6. **Keep functions pure** - no side effects, no external state 7. **Return errors** when conversion fails (for response converters) ### OpenAI-compatible Providers If you are implementing a provider that is **strictly OpenAI API compatible**, the implementation is significantly simpler. You reuse all the conversion logic from `core/providers/openai/`. **When to Use This Approach**: - Provider's API is 100% OpenAI-compatible - Same request/response formats - Same endpoint paths (`/v1/chat/completions`, `/v1/completions`, etc.) - Only differences are: base URL, authentication, and possibly some extra headers **Complete Reference: `core/providers/cerebras/cerebras.go`** --- #### Step 1: Create the Provider File Create `core/providers/[provider_name]/[provider_name].go`: ```go // Package cerebras implements the Cerebras LLM provider. package cerebras import ( "context" "strings" "time" "github.com/maximhq/bifrost/core/providers/openai" providerUtils "github.com/maximhq/bifrost/core/providers/utils" schemas "github.com/maximhq/bifrost/core/schemas" "github.com/valyala/fasthttp" ) // CerebrasProvider implements the Provider interface for Cerebras's API. type CerebrasProvider struct { logger schemas.Logger // Logger for provider operations client *fasthttp.Client // HTTP client for API requests networkConfig schemas.NetworkConfig // Network configuration including extra headers sendBackRawResponse bool // Whether to include raw response in BifrostResponse } // NewCerebrasProvider creates a new Cerebras provider instance. // It initializes the HTTP client with the provided configuration and sets up response pools. func NewCerebrasProvider(config *schemas.ProviderConfig, logger schemas.Logger) (*CerebrasProvider, error) { config.CheckAndSetDefaults() client := &fasthttp.Client{ ReadTimeout: time.Second * time.Duration(config.NetworkConfig.DefaultRequestTimeoutInSeconds), WriteTimeout: time.Second * time.Duration(config.NetworkConfig.DefaultRequestTimeoutInSeconds), MaxConnsPerHost: 5000, MaxIdleConnDuration: 30 * time.Second, MaxConnWaitTimeout: 10 * time.Second, } // Configure proxy if provided client = providerUtils.ConfigureProxy(client, config.ProxyConfig, logger) // Set default BaseURL if not provided if config.NetworkConfig.BaseURL == "" { config.NetworkConfig.BaseURL = "https://api.cerebras.ai" } config.NetworkConfig.BaseURL = strings.TrimRight(config.NetworkConfig.BaseURL, "/") return &CerebrasProvider{ logger: logger, client: client, networkConfig: config.NetworkConfig, sendBackRawResponse: config.SendBackRawResponse, }, nil } // GetProviderKey returns the provider identifier for Cerebras. func (provider *CerebrasProvider) GetProviderKey() schemas.ModelProvider { return schemas.Cerebras } ``` --- #### Step 2: Implement Required Methods Using OpenAI Handlers For each supported feature, delegate to the corresponding OpenAI handler: **Chat Completion (Non-Streaming)**: ```go // ChatCompletion performs a chat completion request to the Cerebras API. func (provider *CerebrasProvider) ChatCompletion( ctx context.Context, key schemas.Key, request *schemas.BifrostChatRequest, ) (*schemas.BifrostChatResponse, *schemas.BifrostError) { return openai.HandleOpenAIChatCompletionRequest( ctx, provider.client, provider.networkConfig.BaseURL+providerUtils.GetPathFromContext(ctx, "/v1/chat/completions"), request, key, provider.networkConfig.ExtraHeaders, providerUtils.ShouldSendBackRawResponse(ctx, provider.sendBackRawResponse), provider.GetProviderKey(), provider.logger, ) } ``` **Chat Completion (Streaming)**: ```go // ChatCompletionStream performs a streaming chat completion request to the Cerebras API. // It supports real-time streaming of responses using Server-Sent Events (SSE). func (provider *CerebrasProvider) ChatCompletionStream( ctx context.Context, postHookRunner schemas.PostHookRunner, key schemas.Key, request *schemas.BifrostChatRequest, ) (chan *schemas.BifrostStream, *schemas.BifrostError) { var authHeader map[string]string if key.Value != "" { authHeader = map[string]string{"Authorization": "Bearer " + key.Value} } // Use shared OpenAI-compatible streaming logic return openai.HandleOpenAIChatCompletionStreaming( ctx, provider.client, provider.networkConfig.BaseURL+"/v1/chat/completions", request, authHeader, provider.networkConfig.ExtraHeaders, providerUtils.ShouldSendBackRawResponse(ctx, provider.sendBackRawResponse), provider.GetProviderKey(), postHookRunner, nil, // customStreamParser - use nil for standard OpenAI format provider.logger, ) } ``` **Text Completion (Non-Streaming)**: ```go // TextCompletion performs a text completion request to Cerebras's API. func (provider *CerebrasProvider) TextCompletion( ctx context.Context, key schemas.Key, request *schemas.BifrostTextCompletionRequest, ) (*schemas.BifrostTextCompletionResponse, *schemas.BifrostError) { return openai.HandleOpenAITextCompletionRequest( ctx, provider.client, provider.networkConfig.BaseURL+providerUtils.GetPathFromContext(ctx, "/v1/completions"), request, key, provider.networkConfig.ExtraHeaders, provider.GetProviderKey(), providerUtils.ShouldSendBackRawResponse(ctx, provider.sendBackRawResponse), provider.logger, ) } ``` **Text Completion (Streaming)**: ```go // TextCompletionStream performs a streaming text completion request to Cerebras's API. func (provider *CerebrasProvider) TextCompletionStream( ctx context.Context, postHookRunner schemas.PostHookRunner, key schemas.Key, request *schemas.BifrostTextCompletionRequest, ) (chan *schemas.BifrostStream, *schemas.BifrostError) { var authHeader map[string]string if key.Value != "" { authHeader = map[string]string{"Authorization": "Bearer " + key.Value} } return openai.HandleOpenAITextCompletionStreaming( ctx, provider.client, provider.networkConfig.BaseURL+"/v1/completions", request, authHeader, provider.networkConfig.ExtraHeaders, providerUtils.ShouldSendBackRawResponse(ctx, provider.sendBackRawResponse), provider.GetProviderKey(), postHookRunner, nil, // customStreamParser provider.logger, ) } ``` **List Models**: ```go // ListModels performs a list models request to Cerebras's API. func (provider *CerebrasProvider) ListModels( ctx context.Context, keys []schemas.Key, request *schemas.BifrostListModelsRequest, ) (*schemas.BifrostListModelsResponse, *schemas.BifrostError) { return openai.HandleOpenAIListModelsRequest( ctx, provider.client, request, provider.networkConfig.BaseURL+providerUtils.GetPathFromContext(ctx, "/v1/models"), keys, provider.networkConfig.ExtraHeaders, provider.GetProviderKey(), providerUtils.ShouldSendBackRawResponse(ctx, provider.sendBackRawResponse), provider.logger, ) } ``` --- #### Step 3: Implement Unsupported Methods For features not supported by the provider, return appropriate errors: ```go // Embedding is not supported by Cerebras func (provider *CerebrasProvider) Embedding( ctx context.Context, key schemas.Key, request *schemas.BifrostEmbeddingRequest, ) (*schemas.BifrostEmbeddingResponse, *schemas.BifrostError) { return nil, &schemas.BifrostError{ StatusCode: http.StatusNotImplemented, Message: "Embedding is not supported by Cerebras", Type: "unsupported_feature", } } // Speech is not supported by Cerebras func (provider *CerebrasProvider) Speech( ctx context.Context, key schemas.Key, request *schemas.BifrostSpeechRequest, ) (*schemas.BifrostSpeechResponse, *schemas.BifrostError) { return nil, &schemas.BifrostError{ StatusCode: http.StatusNotImplemented, Message: "Speech synthesis is not supported by Cerebras", Type: "unsupported_feature", } } // SpeechStream is not supported by Cerebras func (provider *CerebrasProvider) SpeechStream( ctx context.Context, postHookRunner schemas.PostHookRunner, key schemas.Key, request *schemas.BifrostSpeechRequest, ) (chan *schemas.BifrostStream, *schemas.BifrostError) { return nil, &schemas.BifrostError{ StatusCode: http.StatusNotImplemented, Message: "Speech synthesis streaming is not supported by Cerebras", Type: "unsupported_feature", } } // Transcription is not supported by Cerebras func (provider *CerebrasProvider) Transcription( ctx context.Context, key schemas.Key, request *schemas.BifrostTranscriptionRequest, ) (*schemas.BifrostTranscriptionResponse, *schemas.BifrostError) { return nil, &schemas.BifrostError{ StatusCode: http.StatusNotImplemented, Message: "Transcription is not supported by Cerebras", Type: "unsupported_feature", } } // TranscriptionStream is not supported by Cerebras func (provider *CerebrasProvider) TranscriptionStream( ctx context.Context, postHookRunner schemas.PostHookRunner, key schemas.Key, request *schemas.BifrostTranscriptionRequest, ) (chan *schemas.BifrostStream, *schemas.BifrostError) { return nil, &schemas.BifrostError{ StatusCode: http.StatusNotImplemented, Message: "Transcription streaming is not supported by Cerebras", Type: "unsupported_feature", } } // Responses is not supported by Cerebras func (provider *CerebrasProvider) Responses( ctx context.Context, key schemas.Key, request *schemas.BifrostResponsesRequest, ) (*schemas.BifrostResponsesResponse, *schemas.BifrostError) { return nil, &schemas.BifrostError{ StatusCode: http.StatusNotImplemented, Message: "Responses is not supported by Cerebras", Type: "unsupported_feature", } } // ResponsesStream is not supported by Cerebras func (provider *CerebrasProvider) ResponsesStream( ctx context.Context, postHookRunner schemas.PostHookRunner, key schemas.Key, request *schemas.BifrostResponsesRequest, ) (chan *schemas.BifrostStream, *schemas.BifrostError) { return nil, &schemas.BifrostError{ StatusCode: http.StatusNotImplemented, Message: "Responses streaming is not supported by Cerebras", Type: "unsupported_feature", } } ``` --- #### Key Points for OpenAI-compatible Providers **Constructor Differences**: - Returns `(*[ProviderName]Provider, error)` instead of just `*[ProviderName]Provider` - Must set a default `BaseURL` specific to the provider - Must trim trailing slashes from `BaseURL` **URL Construction**: - Use `provider.networkConfig.BaseURL + "/v1/[endpoint]"` for direct paths - Use `providerUtils.GetPathFromContext(ctx, "/v1/[endpoint]")` when path might be overridden in context **Authentication Headers**: - Create `authHeader map[string]string` with `Authorization: Bearer {key}` - Pass to OpenAI handlers separately from `ExtraHeaders` **Custom Stream Parsers**: - Pass `nil` for `customStreamParser` if using standard OpenAI SSE format - Only implement custom parser if provider uses non-standard streaming format **Error Handling**: - OpenAI handlers return `*schemas.BifrostError` - propagate directly - For unsupported features, return custom error with `StatusNotImplemented` **Advantages of This Approach**: - **Automatic updates** - benefits from OpenAI handler improvements - **Consistent behavior** - same conversion logic as OpenAI - **Easy maintenance** - only provider-specific config in your file ## Implementation Steps Follow this **exact order** when implementing a new provider. --- ### For Non-OpenAI-compatible Providers #### Phase 1: Research & Planning (Before Writing Code) 1. **Study the Provider's API Documentation**: - Identify all supported endpoints (chat, embeddings, speech, etc.) - Note authentication method (API key, bearer token, custom headers) - Document base URL and endpoint paths - List all request/response fields - Identify provider-specific parameters not in OpenAI schema 2. **Create a Mapping Document** (recommended): ```markdown # Provider: [ProviderName] ## Authentication - Method: Bearer token / API key in header - Header name: Authorization / X-API-Key ## Base URL - Production: https://api.provider.com - Staging: https://staging.provider.com (if applicable) ## Endpoints - Chat Completions: POST /v1/chat/completions - Embeddings: POST /v1/embeddings - Models: GET /v1/models ## Request Fields ### Chat Completions - model (required): string - messages (required): array - temperature (optional): float - max_tokens (optional): int - [provider_specific_field] (optional): type ## Response Fields ### Chat Completions - id: string - choices: array - usage: object - [provider_specific_field]: type ``` --- #### Phase 2: Create Directory Structure 3. **Create Provider Directory**: ```bash mkdir -p core/providers/[provider_name] cd core/providers/[provider_name] ``` --- #### Phase 3: Define Types (types.go) 4. **Create `types.go` - Define ALL Provider-Specific Types**: **Order of Type Definitions**: ```go package [provider_name] import "encoding/json" // # MODELS TYPES // Define model-related types first type [ProviderName]Model struct { ... } type [ProviderName]ListModelsResponse struct { ... } // # CHAT TYPES // Define chat-related types type [ProviderName]ChatRequest struct { ... } type [ProviderName]ChatResponse struct { ... } type [ProviderName]ChatMessage struct { ... } type [ProviderName]ChatChoice struct { ... } // # EMBEDDING TYPES // Define embedding-related types type [ProviderName]EmbeddingRequest struct { ... } type [ProviderName]EmbeddingResponse struct { ... } // # SPEECH TYPES (if applicable) // Define speech-related types // # TRANSCRIPTION TYPES (if applicable) // Define transcription-related types // # ERROR TYPES // Define error response types type [ProviderName]ErrorResponse struct { ... } ``` **Type Naming Checklist**: - ✅ All types prefixed with provider name: `HuggingFaceChatRequest` - ✅ JSON tags match provider API exactly: `json:"model_name"` - ✅ Optional fields use `omitempty`: `json:"temperature,omitempty"` - ✅ Nullable fields use pointers: `*float64`, `*string` - ✅ Flexible fields use `json.RawMessage`: `Content json.RawMessage` - ✅ Required fields have validation tags: `validate:"required"` --- #### Phase 4: Define Utilities (utils.go) 5. **Create `utils.go` - Define Constants and Helper Functions**: **Order of Definitions**: ```go package [provider_name] import ( "context" "encoding/json" "fmt" providerUtils "github.com/maximhq/bifrost/core/providers/utils" schemas "github.com/maximhq/bifrost/core/schemas" "github.com/valyala/fasthttp" ) // 1. BASE URLs (ALWAYS FIRST) const ( defaultBaseURL = "https://api.provider.com" alternateURL = "https://alternate.provider.com" ) // 2. DEFAULT VALUES AND LIMITS const ( defaultTimeout = 60 maxRequestSize = 1024 * 1024 * 10 // 10MB defaultModelLimit = 100 maxConcurrentCalls = 5000 ) // 3. PROVIDER-SPECIFIC ENUMS/CONSTANTS const ( providerVersion = "v1" apiVersion = "2024-01" ) // 4. CUSTOM TYPES FOR CONSTANTS (if needed) type inferenceProvider string const ( providerA inferenceProvider = "provider-a" providerB inferenceProvider = "provider-b" ) // 5. HELPER FUNCTIONS // Function to build authentication headers func buildAuthHeaders(apiKey string) map[string]string { ... } // Function to parse error responses func parseErrorResponse(body []byte) *schemas.BifrostError { ... } // Function to validate model names func validateModelName(model string) error { ... } // Function to split composite model identifiers func splitModelProvider(model string) (provider, modelName string) { ... } ``` **Utility Function Checklist**: - ✅ All base URLs defined as constants - ✅ Helper functions use camelCase (unexported) or PascalCase (exported) - ✅ Error handling utilities included - ✅ HTTP header builders included - ✅ Constants grouped logically with comments --- #### Phase 5: Implement Converters (Feature Files) 6. **Create Feature Files in Order of Complexity** (simplest first): **a. Create `models.go` (if supported)**: ```go package [provider_name] import ( "fmt" schemas "github.com/maximhq/bifrost/core/schemas" ) // ToBifrostListModelsResponse converts provider models to Bifrost format func (response *[ProviderName]ListModelsResponse) ToBifrostListModelsResponse( providerKey schemas.ModelProvider, ) *schemas.BifrostListModelsResponse { if response == nil { return nil } bifrostResponse := &schemas.BifrostListModelsResponse{ Data: make([]schemas.Model, 0, len(response.Models)), } for _, model := range response.Models { // Validation if model.ID == "" { continue } // Conversion logic bifrostModel := schemas.Model{ ID: fmt.Sprintf("%s/%s", providerKey, model.ID), Name: &model.Name, SupportedMethods: deriveSupportedMethods(model), } bifrostResponse.Data = append(bifrostResponse.Data, bifrostModel) } return bifrostResponse } // Helper function to determine supported methods func deriveSupportedMethods(model [ProviderName]Model) []string { // Implementation } ``` **b. Create `embedding.go` (if supported)**: ```go package [provider_name] import schemas "github.com/maximhq/bifrost/core/schemas" // To[ProviderName]EmbeddingRequest converts Bifrost request to provider format func To[ProviderName]EmbeddingRequest( bifrostReq *schemas.BifrostEmbeddingRequest, ) *[ProviderName]EmbeddingRequest { if bifrostReq == nil { return nil } providerReq := &[ProviderName]EmbeddingRequest{ Model: bifrostReq.Model, } // Convert input if bifrostReq.Input != nil { if bifrostReq.Input.Text != nil { providerReq.Input = *bifrostReq.Input.Text } else if bifrostReq.Input.Texts != nil { providerReq.Input = bifrostReq.Input.Texts } } // Map parameters if bifrostReq.Params != nil { // Standard parameters if bifrostReq.Params.Dimensions != nil { providerReq.Dimensions = bifrostReq.Params.Dimensions } // Provider-specific parameters from ExtraParams if bifrostReq.Params.ExtraParams != nil { if val, ok := bifrostReq.Params.ExtraParams["provider_param"].(string); ok { providerReq.ProviderParam = &val } } } return providerReq } // ToBifrostEmbeddingResponse converts provider response to Bifrost format func ToBifrostEmbeddingResponse( providerResp *[ProviderName]EmbeddingResponse, ) (*schemas.BifrostEmbeddingResponse, *schemas.BifrostError) { if providerResp == nil { return nil, &schemas.BifrostError{ Message: "Provider response is nil", Type: "invalid_response", } } bifrostResp := &schemas.BifrostEmbeddingResponse{ Data: make([]schemas.EmbeddingData, 0, len(providerResp.Data)), } for i, embedding := range providerResp.Data { bifrostResp.Data = append(bifrostResp.Data, schemas.EmbeddingData{ Index: i, Embedding: embedding.Values, }) } // Map usage if available if providerResp.Usage != nil { bifrostResp.Usage = &schemas.Usage{ PromptTokens: providerResp.Usage.InputTokens, TotalTokens: providerResp.Usage.TotalTokens, } } return bifrostResp, nil } ``` **c. Create `chat.go` (most complex)**: ```go package [provider_name] import ( "encoding/json" "github.com/bytedance/sonic" schemas "github.com/maximhq/bifrost/core/schemas" ) // To[ProviderName]ChatCompletionRequest converts Bifrost chat request to provider format func To[ProviderName]ChatCompletionRequest( bifrostReq *schemas.BifrostChatRequest, ) *[ProviderName]ChatRequest { if bifrostReq == nil || bifrostReq.Input == nil { return nil } // Convert messages providerMessages := make([][ProviderName]ChatMessage, 0, len(bifrostReq.Input)) for _, msg := range bifrostReq.Input { providerMsg := [ProviderName]ChatMessage{} // Set role if msg.Role != "" { role := string(msg.Role) providerMsg.Role = &role } // Set name if present if msg.Name != nil { providerMsg.Name = msg.Name } // Convert content (can be string or structured) if msg.Content != nil { if msg.Content.ContentStr != nil { // Simple string content contentJSON, _ := sonic.Marshal(*msg.Content.ContentStr) providerMsg.Content = json.RawMessage(contentJSON) } else if msg.Content.ContentBlocks != nil { // Structured content (text, images, etc.) contentItems := make([][ProviderName]ContentItem, 0, len(msg.Content.ContentBlocks)) for _, block := range msg.Content.ContentBlocks { item := [ProviderName]ContentItem{} blockType := string(block.Type) item.Type = &blockType switch block.Type { case schemas.ChatContentBlockTypeText: if block.Text != nil { item.Text = block.Text } case schemas.ChatContentBlockTypeImage: if block.ImageURLStruct != nil { item.ImageURL = &[ProviderName]ImageRef{ URL: block.ImageURLStruct.URL, } } } contentItems = append(contentItems, item) } contentJSON, _ := sonic.Marshal(contentItems) providerMsg.Content = json.RawMessage(contentJSON) } } // Handle tool calls for assistant messages if msg.ChatAssistantMessage != nil && len(msg.ChatAssistantMessage.ToolCalls) > 0 { providerToolCalls := make([][ProviderName]ToolCall, 0, len(msg.ChatAssistantMessage.ToolCalls)) for _, tc := range msg.ChatAssistantMessage.ToolCalls { providerToolCall := [ProviderName]ToolCall{ ID: tc.ID, Type: tc.Type, Function: [ProviderName]Function{ Name: *tc.Function.Name, Arguments: tc.Function.Arguments, }, } providerToolCalls = append(providerToolCalls, providerToolCall) } providerMsg.ToolCalls = providerToolCalls } // Handle tool call responses if msg.ChatToolMessage != nil && msg.ChatToolMessage.ToolCallID != nil { providerMsg.ToolCallID = msg.ChatToolMessage.ToolCallID } providerMessages = append(providerMessages, providerMsg) } // Build the request providerReq := &[ProviderName]ChatRequest{ Model: bifrostReq.Model, Messages: providerMessages, } // Map parameters if bifrostReq.Params != nil { params := bifrostReq.Params // Standard parameters if params.Temperature != nil { providerReq.Temperature = params.Temperature } if params.MaxTokens != nil { providerReq.MaxTokens = params.MaxTokens } if params.TopP != nil { providerReq.TopP = params.TopP } if params.FrequencyPenalty != nil { providerReq.FrequencyPenalty = params.FrequencyPenalty } if params.PresencePenalty != nil { providerReq.PresencePenalty = params.PresencePenalty } if params.Stop != nil { providerReq.Stop = params.Stop } if params.Seed != nil { providerReq.Seed = params.Seed } // Tool/Function calling - omitted for brevity; see complete provider examples } return providerReq } ``` **Key conversion patterns to implement**: ```go // Request Converter - Maps Bifrost standard to provider format func To[ProviderName][Feature]Request(bifrostReq) *[ProviderName]Request { // 1. Nil check // 2. Convert messages/input // 3. Map standard parameters (temp, max_tokens, etc.) // 4. Map tools/functions if supported // 5. Map ExtraParams to provider-specific fields return providerReq } // Response Converter - Maps provider format back to Bifrost func ToBifrost[Feature]Response(providerResp) (*schemas.BifrostResponse, *schemas.BifrostError) { // 1. Nil check with error return // 2. Convert choices/results // 3. Convert messages/content // 4. Convert tool calls if present // 5. Convert usage/metadata return bifrostResp, nil } ``` **Converter Checklist for Each Feature File**: - ✅ Request converter: `To[ProviderName][Feature]Request` - ✅ Response converter: `ToBifrost[Feature]Response` - ✅ Nil checks at start of every function - ✅ Pre-allocate slices with capacity - ✅ Handle all optional fields with nil checks - ✅ Map ExtraParams to provider-specific fields - ✅ Return errors for response converters - ✅ Document complex transformations See actual implementation examples in `core/providers/huggingface/`, `core/providers/anthropic/`, or other existing providers for complete patterns. --- #### Phase 6: Implement Provider (provider_name.go) 7. **Create `[provider_name].go` - Wire Everything Together**: See detailed structure in "File Conventions & Responsibilities" section above. **Implementation Checklist**: - ✅ Package comment at top - ✅ All imports organized (stdlib, external, internal) - ✅ Provider struct with correct field order - ✅ Response pools (if using sync.Pool) - ✅ Constructor with proper initialization - ✅ `GetProviderKey()` method - ✅ All interface methods implemented - ✅ Each method follows the strict order: convert → execute → handle errors → convert back --- #### Phase 7: Add Tests 8. **Create `[provider_name]_test.go`**: See "Adding Automated Tests" section below for complete details. --- ### For OpenAI-compatible Providers For OpenAI-compatible providers, follow the simpler structure shown in the "OpenAI-compatible Providers" section above. **Implementation Checklist**: - ✅ Create `[provider_name].go` only - ✅ Import `github.com/maximhq/bifrost/core/providers/openai` - ✅ Implement constructor returning `(*Provider, error)` - ✅ Set default BaseURL specific to provider - ✅ Delegate all methods to `openai.HandleOpenAI*` functions - ✅ Return errors for unsupported features - ✅ Create `[provider_name]_test.go` ## Adding to UI Once your provider is implemented and tested, you need to integrate it into the Bifrost UI and CI/CD pipelines. --- ### Step 1: Update UI Constants #### a. Add Model Placeholder (`ui/lib/constants/config.ts`) Add a model placeholder example for your provider to help users understand the expected model format: ```typescript export const ModelPlaceholders = { openai: "e.g. gpt-4, gpt-3.5-turbo", anthropic: "e.g. claude-3-opus, claude-3-sonnet", // ... other providers [providername]: "e.g. model-1, model-2", // Add your provider here }; ``` **Example**: ```typescript huggingface: "e.g. google/gemma-2-2b-it, nebius/Qwen/Qwen3-Embedding-8B", ``` #### b. Set Key Requirement (`ui/lib/constants/config.ts`) Specify whether your provider requires an API key: ```typescript export const isKeyRequiredByProvider: Record = { openai: true, anthropic: true, // ... other providers [providername]: true, // Set to true if API key is required, false otherwise }; ``` **Example**: ```typescript huggingface: true, // HuggingFace requires API key ollama: false, // Ollama doesn't require API key (local) ``` --- ### Step 2: Add Provider Icon (`ui/lib/constants/icons.tsx`) Create an SVG icon for your provider. You can use the provider's official brand icon or a placeholder. ```typescript export const ProviderIcons = { // ... existing providers [providername]: ({ size = "md", className = "" }: IconProps) => { const resolvedSize = resolveSize(size); return ( ProviderName {/* Add your SVG path here */} ); }, } as const; ``` **Tips**: - Get the official icon from the provider's brand assets or press kit - Ensure the SVG is properly formatted and viewBox is set to "0 0 24 24" - Use the provider's brand color for the fill attribute - Keep the icon simple and recognizable at small sizes --- ### Step 3: Register Provider Name (`ui/lib/constants/logs.ts`) #### a. Add to Known Providers List ```typescript export const KnownProvidersNames = [ "anthropic", "azure", "bedrock", // ... other providers "[providername]", // Add your provider name (lowercase) ] as const; ``` #### b. Add Provider Label ```typescript export const ProviderLabels: Record = { anthropic: "Anthropic", azure: "Azure", bedrock: "AWS Bedrock", // ... other providers [providername]: "ProviderName", // Add display name (proper capitalization) } as const; ``` **Example**: ```typescript huggingface: "HuggingFace", cerebras: "Cerebras", ``` --- ### Step 4: Update OpenAPI Specification (`docs/openapi/openapi.json`) Add your provider to the API documentation's provider enum: ```json { "type": "string", "enum": [ "openai", "anthropic", "azure", "bedrock", // ... other providers "[providername]" ], "description": "AI model provider", "example": "openai" } ``` **Location**: Search for the `"AI model provider"` description in `docs/openapi/openapi.json` and add your provider to the enum array. --- ### Step 5: Update Configuration Schema (`transports/config.schema.json`) #### a. Add Provider to Providers Object ```json { "providers": { "type": "object", "properties": { "openai": { "$ref": "#/$defs/provider" }, "anthropic": { "$ref": "#/$defs/provider" }, // ... other providers "[providername]": { "$ref": "#/$defs/provider" } } } } ``` #### b. Add to Fallback Provider Enum ```json { "fallbacks": { "items": { "properties": { "provider": { "type": "string", "enum": [ "openai", "anthropic", // ... other providers "[providername]" ] } } } } } ``` **Location**: Search for `"fallbacks"` in `transports/config.schema.json` and add your provider to both locations. --- ### Step 6: Update UI README (`ui/README.md`) Add your provider to the list of supported providers: ```markdown ## Provider Configuration Manage all your AI providers from a unified interface: - **Supported Providers**: OpenAI, Azure, Anthropic, AWS Bedrock, Cohere, Google Vertex AI, Mistral, Ollama, Parasail, Elevenlabs, SGLang, Cerebras, Groq, Gemini, OpenRouter, ProviderName ``` **Example**: ```markdown - **Supported Providers**: OpenAI, Azure, Anthropic, AWS Bedrock, Cohere, Google Vertex AI, Mistral, Ollama, Parasail, Elevenlabs, SGLang, Cerebras, Groq, Gemini, OpenRouter, HuggingFace ``` --- ### Step 7: Register Provider in Core (`core/bifrost.go`) #### a. Add Provider Import ```go import ( // ... existing imports "github.com/maximhq/bifrost/core/providers/[providername]" ) ``` #### b. Add Case to createBaseProvider ```go func (bifrost *Bifrost) createBaseProvider(providerKey schemas.ModelProvider, config *schemas.ProviderConfig) (schemas.Provider, error) { // ... existing cases case schemas.ProviderName: return providername.NewProviderNameProvider(config, bifrost.logger), nil default: return nil, fmt.Errorf("unsupported provider: %s", targetProviderKey) } ``` **For OpenAI-compatible providers** (returns error): ```go case schemas.ProviderName: return providername.NewProviderNameProvider(config, bifrost.logger) ``` **For non-OpenAI-compatible providers** (no error): ```go case schemas.ProviderName: return providername.NewProviderNameProvider(config, bifrost.logger), nil ``` --- ### Step 8: Add CI/CD Environment Variables Add your provider's API key to all GitHub Actions workflow files that run tests. #### Files to Update: 1. **`.github/workflows/pr-tests.yml`** 2. **`.github/workflows/release-pipeline.yml`** (multiple jobs) #### Changes Required: Add the environment variable to the `env:` section: ```yaml env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} # ... other API keys PROVIDER_NAME_API_KEY: ${{ secrets.PROVIDER_NAME_API_KEY }} ``` **Example from pr-tests.yml**: ```yaml env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }} HUGGING_FACE_API_KEY: ${{ secrets.HUGGING_FACE_API_KEY }} ``` **Locations in release-pipeline.yml**: - `core-release` job - `framework-release` job - `plugins-release` job - `bifrost-http-release` job **Note**: Repository maintainers need to add the actual secret value in GitHub repository settings under `Settings > Secrets and variables > Actions`. --- ### UI Integration Checklist Before submitting your PR, verify all UI changes: - ✅ Model placeholder added to `ui/lib/constants/config.ts` - ✅ Key requirement set in `ui/lib/constants/config.ts` - ✅ Provider icon added to `ui/lib/constants/icons.tsx` - ✅ Provider name added to `ui/lib/constants/logs.ts` (KnownProvidersNames) - ✅ Provider label added to `ui/lib/constants/logs.ts` (ProviderLabels) - ✅ Provider added to OpenAPI spec enum (`docs/openapi/openapi.json`) - ✅ Provider added to config schema (`transports/config.schema.json`) - 2 locations - ✅ Provider listed in UI README (`ui/README.md`) - ✅ Provider import added to `core/bifrost.go` - ✅ Provider case added to `createBaseProvider` in `core/bifrost.go` - ✅ Environment variable added to `.github/workflows/pr-tests.yml` - ✅ Environment variable added to `.github/workflows/release-pipeline.yml` (4 jobs) --- ## Creating Provider Documentation **MANDATORY**: Every new provider must have comprehensive documentation in the docs directory. This documentation helps users understand how the provider works, what parameters it supports, and any special considerations. --- ### Documentation File Location Create a new MDX file at: `docs/providers/supported-providers/[provider_name].mdx` **Example**: For a provider named "example", create: `docs/providers/supported-providers/example.mdx` --- ### Documentation Structure Your provider documentation should follow this structure for consistency. **Reference complete examples**: - **Groq**: `docs/providers/supported-providers/groq.mdx` (OpenAI-compatible provider) - **Bedrock**: `docs/providers/supported-providers/bedrock.mdx` (Custom API provider with multiple features) - **Cerebras**: `docs/providers/supported-providers/cerebras.mdx` (OpenAI-compatible, simple) - **Mistral**: `docs/providers/supported-providers/mistral.mdx` (Transcription + chat support) - **Ollama**: `docs/providers/supported-providers/ollama.mdx` (Local-first infrastructure) ### Required Sections #### 1. Front Matter (Frontmatter) ```yaml --- title: "[Provider Full Name]" description: "[Brief description] - parameter mapping, [key features], and [auth method]" icon: "[icon letter or emoji]" --- ``` **Example**: ```yaml --- title: "Groq" description: "Groq API conversion guide - OpenAI-compatible format, parameter handling, text completion fallback, streaming, and tool support" icon: "g" --- ``` #### 2. Overview Section Start with a brief overview explaining: - What the provider is and its key characteristics - How Bifrost converts requests to/from this provider's format - List of major transformation features **Template**: ```markdown ## Overview [Provider Name] is a **[type: OpenAI-compatible/custom API/local-first]** provider offering [key features]. Bifrost converts requests to [Provider]'s expected format with [specific features]. Key characteristics: - **[Feature 1]** - brief description - **[Feature 2]** - brief description - **[Feature 3]** - brief description ``` #### 3. Supported Operations Table Create a table showing which operations are supported: ```markdown ### Supported Operations | Operation | Non-Streaming | Streaming | Endpoint | Notes | |-----------|---------------|-----------|----------|-------| | Chat Completions | ✅ | ✅ | `/v1/chat/completions` | | | Text Completions | ❌ | ❌ | Not supported | | | Embeddings | ✅ | ❌ | `/v1/embeddings` | | | List Models | ✅ | ❌ | `/v1/models` | | ``` #### 4. Feature Sections (One per Supported Feature) For each major feature (Chat Completions, Embeddings, etc.): ##### a. Request Parameters ```markdown ## Request Parameters ### Parameter Mapping | Parameter | Transformation | Notes | |-----------|----------------|-------| | `max_completion_tokens` | Direct pass-through | Minimum X tokens | | `temperature` | Renamed to `temp` | Provider-specific name | ``` Include: - OpenAI parameter name - How it's transformed for the provider (renamed, dropped, etc.) - Any special notes or constraints ##### b. Filtered/Dropped Parameters ```markdown ### Filtered Parameters Removed for [Provider] compatibility: - `prompt_cache_key` - Not supported - `store` - Not supported ``` ##### c. Special Features ```markdown ### [Feature Name] Document any provider-specific features like: - Reasoning/thinking support - Special authentication - Unique parameters - Format conversions ``` ##### d. Message Conversion ```markdown ## Message Conversion Content types supported: - ✅ Text content - ✅ Images (URL and base64) - ❌ Audio input ``` ##### e. Response Conversion ```markdown ## Response Conversion Field mapping from provider format back to Bifrost standard. ``` #### 5. Streaming Section (If Supported) ```markdown ## Streaming [Provider] uses **[protocol: SSE/WebSocket/custom]** streaming with: - Request configuration: stream: true - Event format: [description] - End marker: [description] ``` #### 6. Authentication Section ```markdown ## Authentication **[Authentication Type]:** ``` Authorization: [format] ``` [Additional details about key management, etc.] ``` #### 7. Configuration Section ```markdown ## Configuration **HTTP Settings:** - **Base URL**: `[default URL]` (default) - **API Version**: [version info] - **Max Connections**: 5000 per host - **Idle Timeout**: 60 seconds ``` #### 8. Caveats/Important Notes Use collapsible accordion sections for limitations: ```markdown ## Caveats **Severity**: [High/Medium/Low] **Behavior**: [What happens] **Impact**: [What breaks/changes] **Code**: [File references if relevant] ... ``` Common caveats to document: - Unsupported content types (images, audio, etc.) - Parameter limitations - Streaming restrictions - Special handling required - Breaking behavioral differences from OpenAI standard #### 9. Warnings/Notes Use special callouts for important information: ```markdown **Unsupported Operations**: [List operations], [List operations]. [Provider] requires [special setup/configuration]. [Helpful tip for using this provider effectively]. ``` ### Code Examples in Documentation Include examples in both formats where applicable: ```markdown <Tabs> <Tab title="Gateway"> `````bash curl -X POST http://localhost:8080/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "[provider]/[model]", "messages": [{"role": "user", "content": "Hello"}] }' ````` </Tab> <Tab title="Go SDK"> `````go response, err := client.ChatCompletion(ctx, &schemas.BifrostChatRequest{ Provider: schemas.ProviderName, Model: "model-name", Input: messages, }) ````` </Tab> </Tabs> ``` ### Documentation Formatting Standards - **Markdown**: Use standard MDX syntax compatible with the documentation site - **Code blocks**: Always specify language (bash, go, json, yaml) - **Tables**: Use pipes for alignment and clarity - **Sections**: Use H1 (#) for main provider, H2 (##) for major sections, H3 (###) for subsections - **Lists**: Use bullets (-) for unordered, numbers (1.) for ordered - **Emphasis**: Use **bold** for important terms, `code` for inline code ### Documentation Checklist Before submitting your documentation: - ✅ File created: `docs/providers/supported-providers/[provider_name].mdx` - ✅ Front matter with title, description, and icon - ✅ Overview section explaining the provider - ✅ Supported Operations table (accurate for your implementation) - ✅ Parameter mapping documented for each supported feature - ✅ Filtered parameters listed - ✅ Message conversion explained (content types) - ✅ Tool/function support documented (if applicable) - ✅ Response conversion patterns explained - ✅ Streaming behavior documented (if supported) - ✅ Authentication method clearly explained - ✅ Configuration section with base URL and settings - ✅ Caveats documented with severity ratings - ✅ Code examples for Gateway and Go SDK (where applicable) - ✅ All special features explained - ✅ Links to reference existing implementations (if applicable) - ✅ Warnings for unsupported features --- ## Adding Automated Tests Testing is **MANDATORY** for all providers. Tests ensure your provider works correctly and continues to work as the codebase evolves. --- ### Test File Structure Create `core/providers/[provider_name]/[provider_name]_test.go`: **Example Test File Structure**: ```go package providername_test import ( "os" "testing" "github.com/maximhq/bifrost/core/internal/llmtests" "github.com/maximhq/bifrost/core/schemas" ) func TestProviderName(t *testing.T) { t.Parallel() // Check for API key - skip if not available if os.Getenv("PROVIDER_API_KEY") == "" { t.Skip("Skipping tests because PROVIDER_API_KEY is not set") } // Initialize test client client, ctx, cancel, err := llmtests.SetupTest() if err != nil { t.Fatalf("Error initializing test setup: %v", err) } defer cancel() // Configure test scenarios testConfig := llmtests.ComprehensiveTestConfig{ Provider: schemas.ProviderName, ChatModel: "model-name", Fallbacks: []schemas.Fallback{ {Provider: schemas.ProviderName, Model: "fallback-model"}, }, Scenarios: llmtests.TestScenarios{ SimpleChat: true, CompletionStream: true, ToolCalls: true, TextCompletion: false, // Not supported Embedding: false, // Not supported ListModels: true, // ... configure based on provider capabilities }, } // Run all tests t.Run("ProviderNameTests", func(t *testing.T) { llmtests.RunAllComprehensiveTests(t, client, ctx, testConfig) }) client.Shutdown() } ``` --- ### Test Configuration Requirements **Environment Variables**: - **REQUIRED**: `[PROVIDER_NAME]_API_KEY` - API key for the provider - **Optional**: `PROVIDER_BASE_URL` - Custom base URL for testing **Example**: ```bash export CEREBRAS_API_KEY="your-api-key-here" export HUGGING_FACE_API_KEY="your-hf-token-here" ``` **Package Declaration**: - Use `package [provider_name]_test` (note the `_test` suffix) - This ensures tests don't access unexported functions (tests external behavior) See complete test examples in `core/providers/cerebras/cerebras_test.go`, `core/providers/huggingface/huggingface_test.go`, or other existing providers. --- ### Test Scenarios Configuration The `llmtests.TestScenarios` struct defines which tests to run. Set each field based on provider capabilities: #### Core Test Scenarios | Scenario | Enable if... | |----------|-------------| | `SimpleChat` | Provider supports basic chat completion | | `CompletionStream` | Provider supports streaming chat | | `TextCompletion` | Provider supports text completions (legacy) | | `TextCompletionStream` | Provider supports streaming text completions | | `ToolCalls` | Provider supports function/tool calling | | `ToolCallsStreaming` | Provider supports streaming with tool calls | | `Embedding` | Provider supports text embeddings | | `ListModels` | Provider has a list models endpoint | | `ImageURL` | Provider accepts image URLs in messages | | `ImageBase64` | Provider accepts base64-encoded images | For a complete list of all available test scenarios and their descriptions, check the `llmtests.TestScenarios` struct in `core/internal/llmtests/`. --- ### Model Configuration **ChatModel** (REQUIRED if any chat scenario is enabled): ```go ChatModel: "llama-3.3-70b", // Primary model for chat tests ``` **TextModel** (REQUIRED if any text completion scenario is enabled): ```go TextModel: "llama3.1-8b", // Model for text completion tests ``` **EmbeddingModel** (REQUIRED if Embedding scenario is enabled): ```go EmbeddingModel: "text-embedding-ada-002", // Model for embedding tests ``` **Fallbacks** (OPTIONAL but recommended): ```go Fallbacks: []schemas.Fallback{ {Provider: schemas.Cerebras, Model: "llama3.1-8b"}, {Provider: schemas.Cerebras, Model: "gpt-oss-120b"}, }, ``` - Fallbacks are tested if primary model fails - Tests that fallback mechanism works correctly --- ### Running Tests **Run all tests for your provider**: ```bash cd core/providers/[provider_name] go test -v ``` **Run with API key**: ```bash PROVIDER_API_KEY="your-key" go test -v ``` **Run specific test**: ```bash go test -v -run TestProviderName ``` **Run with timeout** (for slow providers): ```bash go test -v -timeout 5m ``` **Skip integration tests** (if API key not set): ```bash go test -v -short ``` --- ### Test Checklist Before submitting your provider, ensure: - ✅ Test file named `[provider_name]_test.go` - ✅ Package is `[provider_name]_test` - ✅ `t.Parallel()` called at start - ✅ API key check with `t.Skip()` if not available - ✅ All supported scenarios enabled in config - ✅ All unsupported scenarios disabled (set to `false`) - ✅ Appropriate models specified (ChatModel, TextModel, EmbeddingModel) - ✅ Fallback models configured (at least 1-2) - ✅ `client.Shutdown()` called at end - ✅ Tests pass locally with valid API key - ✅ Tests skip gracefully without API key --- ### Common Test Failures and Solutions **Test hangs indefinitely**: - **Solution**: Add timeout: `go test -v -timeout 2m` - **Cause**: Provider not responding or network issue **"API key not set" skip message**: - **Solution**: Export the required environment variable - **Not a failure**: Tests correctly skip when credentials unavailable **"Unsupported feature" errors**: - **Solution**: Set the scenario to `false` in `TestScenarios` - **Cause**: Test trying to run unsupported feature **"Model not found" errors**: - **Solution**: Update ChatModel/TextModel to valid model for provider - **Cause**: Model name incorrect or not available **Streaming tests fail but non-streaming pass**: - **Solution**: Check streaming implementation in provider - **Cause**: SSE parsing error or incorrect stream handling **Tool calling tests fail**: - **Solution**: Verify tool/function conversion in `chat.go` - **Cause**: Tool format doesn't match provider's expected structure --- ### CI/CD Integration After your tests pass locally, ensure they'll run in the CI/CD pipeline: #### GitHub Actions Setup **Required Secret**: Your provider's API key must be added to GitHub repository secrets by a maintainer: - Secret name: `PROVIDER_NAME_API_KEY` (uppercase, underscores) - Example: `HUGGING_FACE_API_KEY`, `CEREBRAS_API_KEY` **Workflow Files**: The API key environment variable should already be added if you followed Step 8 in "Adding to UI". Verify it's present in: - `.github/workflows/pr-tests.yml` - `.github/workflows/release-pipeline.yml` (4 jobs) **Test Execution**: Tests will automatically run on: - Pull requests (PR tests workflow) - Release builds (release pipeline workflow) - Manual workflow triggers **Skipping Tests**: If the API key secret is not set, your tests will be skipped (not fail) thanks to the `t.Skip()` check in your test file. --- ### Final Pre-Submission Checklist Before creating a pull request, verify everything is complete: **Provider Implementation**: - ✅ Provider code follows file structure conventions - ✅ All supported features implemented correctly - ✅ Error handling properly converts to `schemas.BifrostError` - ✅ OpenAI handlers used if provider is compatible - ✅ Code is well-commented and documented **Tests**: - ✅ Test file created: `[provider_name]_test.go` - ✅ All supported scenarios enabled - ✅ All unsupported scenarios disabled - ✅ Tests pass locally with valid API key - ✅ Tests skip gracefully without API key - ✅ Appropriate models configured **Schema & Core**: - ✅ Provider added to `core/schemas/bifrost.go` (ModelProvider type + arrays) - ✅ Provider registered in `core/bifrost.go` (import + case) **UI Integration**: - ✅ All 7 UI files updated (config.ts, icons.tsx, logs.ts, etc.) - ✅ Provider icon looks good and is recognizable - ✅ Model placeholders are helpful examples **CI/CD**: - ✅ Environment variables added to workflow files - ✅ API key secret name follows convention **Documentation**: - ✅ Provider-specific parameters documented (if any) - ✅ Example usage added (optional but helpful) - ✅ Any special setup instructions noted ---