| --- |
| 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> |
|
|
| |
|
|
| 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. |
|
|
| |
|
|
| 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. |
|
|
| |
|
|
| The directory structure differs based on whether the provider is OpenAI API compatible: |
|
|
| |
|
|
| 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 |
| βββ [provider_name]_test.go |
| βββ types.go |
| βββ utils.go |
| βββ errors.go |
| βββ chat.go |
| βββ speech.go |
| βββ transcription.go |
| βββ embedding.go |
| βββ images.go |
| βββ batches.go |
| βββ files.go |
| βββ models.go |
| βββ responses.go |
| ``` |
|
|
| **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 |
|
|
| |
|
|
| 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 |
| βββ [provider_name]_test.go |
| ``` |
|
|
| These providers reuse the OpenAI converter logic from `core/providers/openai/`. |
|
|
| |
|
|
| We enforce **strict separation of concerns** to keep providers maintainable and consistent. Each file has a specific purpose and must follow these rules. |
|
|
| --- |
|
|
| |
|
|
| **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"` |
| } |
|
|
| // |
|
|
| // 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 |
|
|
| --- |
|
|
| |
|
|
| **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 |
|
|
| --- |
|
|
| |
|
|
| **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) |
| } |
| ``` |
|
|
| --- |
|
|
| |
|
|
| **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) |
|
|
| |
|
|
| 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 |
| } |
| ``` |
|
|
| --- |
|
|
| |
|
|
| 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 |
| |
| |
| |
| - Method: Bearer token / API key in header |
| - Header name: Authorization / X-API-Key |
| |
| |
| - Production: https://api.provider.com |
| - Staging: https://staging.provider.com (if applicable) |
| |
| |
| - Chat Completions: POST /v1/chat/completions |
| - Embeddings: POST /v1/embeddings |
| - Models: GET /v1/models |
| |
| |
| |
| - model (required): string |
| - messages (required): array |
| - temperature (optional): float |
| - max_tokens (optional): int |
| - [provider_specific_field] (optional): type |
| |
| |
| |
| - id: string |
| - choices: array |
| - usage: object |
| - [provider_specific_field]: type |
| ``` |
|
|
| --- |
|
|
| |
|
|
| 3. **Create Provider Directory**: |
| ```bash |
| mkdir -p core/providers/[provider_name] |
| cd core/providers/[provider_name] |
| ``` |
|
|
| --- |
|
|
| |
|
|
| 4. **Create `types.go` - Define ALL Provider-Specific Types**: |
|
|
| **Order of Type Definitions**: |
| ```go |
| package [provider_name] |
|
|
| import "encoding/json" |
|
|
| // |
| // Define model-related types first |
| type [ProviderName]Model struct { ... } |
| type [ProviderName]ListModelsResponse struct { ... } |
|
|
| // |
| // Define chat-related types |
| type [ProviderName]ChatRequest struct { ... } |
| type [ProviderName]ChatResponse struct { ... } |
| type [ProviderName]ChatMessage struct { ... } |
| type [ProviderName]ChatChoice struct { ... } |
|
|
| // |
| // Define embedding-related types |
| type [ProviderName]EmbeddingRequest struct { ... } |
| type [ProviderName]EmbeddingResponse struct { ... } |
|
|
| // |
| // Define speech-related types |
|
|
| // |
| // Define transcription-related 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"` |
|
|
| --- |
|
|
| |
|
|
| 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 |
|
|
| --- |
|
|
| |
|
|
| 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> |
|
|
| --- |
|
|
| |
|
|
| 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 |
|
|
| --- |
|
|
| |
|
|
| 8. **Create `[provider_name]_test.go`**: |
|
|
| See "Adding Automated Tests" section below for complete details. |
|
|
| --- |
|
|
| |
|
|
| 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` |
|
|
|
|
| |
|
|
| Once your provider is implemented and tested, you need to integrate it into the Bifrost UI and CI/CD pipelines. |
|
|
| --- |
|
|
| |
|
|
| |
|
|
| 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", |
| ``` |
|
|
| |
|
|
| 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 |
|
|
| --- |
|
|
| |
|
|
| |
|
|
| ```typescript |
| export const KnownProvidersNames = [ |
| "anthropic", |
| "azure", |
| "bedrock", |
| // ... other providers |
| "[providername]", // Add your provider name (lowercase) |
| ] as const; |
| ``` |
|
|
| |
|
|
| ```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", |
| ``` |
|
|
| --- |
|
|
| |
|
|
| 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. |
|
|
| |
|
|
| 1. **`.github/workflows/pr-tests.yml`** |
| 2. **`.github/workflows/release-pipeline.yml`** (multiple jobs) |
|
|
| |
|
|
| Add the environment variable to the `env:` section: |
|
|
| ```yaml |
| env: |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} |
| |
| 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`. |
|
|
| --- |
|
|
| |
|
|
| 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) |
|
|
| --- |
|
|
| |
|
|
| **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. |
|
|
| --- |
|
|
| |
|
|
| 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` |
|
|
| --- |
|
|
| |
|
|
| 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) |
|
|
| |
|
|
| |
|
|
| ```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" |
| --- |
| ``` |
|
|
| |
|
|
| 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 |
| ``` |
|
|
| |
|
|
| Create a table showing which operations are supported: |
|
|
| ```markdown |
| |
|
|
| | Operation | Non-Streaming | Streaming | Endpoint | Notes | |
| |-----------|---------------|-----------|----------|-------| |
| | Chat Completions | β
| β
| `/v1/chat/completions` | | |
| | Text Completions | β | β | Not supported | | |
| | Embeddings | β
| β | `/v1/embeddings` | | |
| | List Models | β
| β | `/v1/models` | | |
| ``` |
|
|
| |
|
|
| For each major feature (Chat Completions, Embeddings, etc.): |
|
|
| |
| ```markdown |
| |
|
|
| |
|
|
| | 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 |
| <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) |
|
|
| <Tip>See complete test examples in `core/providers/cerebras/cerebras_test.go`, `core/providers/huggingface/huggingface_test.go`, or other existing providers.</Tip> |
|
|
| --- |
|
|
| |
|
|
| The `llmtests.TestScenarios` struct defines which tests to run. Set each field based on provider capabilities: |
|
|
| |
|
|
| | 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> |
|
|
| --- |
|
|
| |
|
|
| **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 |
|
|
| --- |
|
|
| |
|
|
| **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 |
| ``` |
|
|
| --- |
|
|
| |
|
|
| 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 |
|
|
| --- |
|
|
| |
|
|
| **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 |
|
|
| --- |
|
|
| |
|
|
| 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 |
| |
| --- |
| |
| |
| |