Files
bifrost/docs/contributing/adding-a-provider.mdx
Beyhan Oğur 880f412e2c first commit
2026-04-26 21:52:23 +03:00

2354 lines
73 KiB
Plaintext

---
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.
<Note>
**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/`
</Note>
## Setup
1. **Fork and Clone**:
* Fork the repository: https://github.com/maximhq/bifrost/
* Clone your fork: `git clone https://github.com/<your_github_username>/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
<Tip>See actual implementation examples in `core/providers/huggingface/`, `core/providers/anthropic/`, or other existing providers for complete patterns.</Tip>
---
#### 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<ProviderName, boolean> = {
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 (
<svg height="1em" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg">
<title>ProviderName</title>
{/* Add your SVG path here */}
<path d="..." fill="#HexColor"></path>
</svg>
);
},
} 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<ProviderName, string> = {
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
<Accordion title="[Caveat Title]">
**Severity**: [High/Medium/Low]
**Behavior**: [What happens]
**Impact**: [What breaks/changes]
**Code**: [File references if relevant]
</Accordion>
<Accordion title="[Another Caveat]">
...
</Accordion>
```
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
<Warning>
**Unsupported Operations**: [List operations], [List operations].
</Warning>
<Note>
[Provider] requires [special setup/configuration].
</Note>
<Tip>
[Helpful tip for using this provider effectively].
</Tip>
```
### Code Examples in Documentation
Include examples in both formats where applicable:
```markdown
&lt;Tabs&gt;
&lt;Tab title="Gateway"&gt;
`````bash
curl -X POST http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "[provider]/[model]",
"messages": [{"role": "user", "content": "Hello"}]
}'
`````
&lt;/Tab&gt;
&lt;Tab title="Go SDK"&gt;
`````go
response, err := client.ChatCompletion(ctx, &schemas.BifrostChatRequest{
Provider: schemas.ProviderName,
Model: "model-name",
Input: messages,
})
`````
&lt;/Tab&gt;
&lt;/Tabs&gt;
```
### 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)
<Tip>See complete test examples in `core/providers/cerebras/cerebras_test.go`, `core/providers/huggingface/huggingface_test.go`, or other existing providers.</Tip>
---
### 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 |
<Tip>For a complete list of all available test scenarios and their descriptions, check the `llmtests.TestScenarios` struct in `core/internal/llmtests/`.</Tip>
---
### 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
---