Skip to content

Adding a new provider

This guide will walk you through creating a provider for DeepIntShield, testing locally, adding it to frontend and CI/CD.

  1. Fork and Clone:
  2. Initialize:
    • Run make dev at the root of the project to set up dependencies and tools.

DeepIntShield 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 DeepIntShield response format.

To implement a new provider, first step is to add the provider name in core/schemas/deepintshield.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/):

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

If the provider is OpenAI API compatible, you only need a minimal structure:

Minimal Reference Structure (see core/providers/cerebras/):

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/.

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:

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

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:

package providername
import (
"context"
"strings"
"time"
providerUtils "github.com/maximhq/deepintshield/core/providers/utils"
schemas "github.com/maximhq/deepintshield/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)

Section titled “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):

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:

package providername
import (
"context"
"strings"
"sync"
"time"
"github.com/bytedance/sonic"
providerUtils "github.com/maximhq/deepintshield/core/providers/utils"
schemas "github.com/maximhq/deepintshield/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.DeepIntShieldError
  6. Convert Response: Call ToDeepIntShield[Feature]Response() from feature file
  7. Return Result: Return DeepIntShield response or error

Example Method (generic pattern):

func (p *ProviderNameProvider) ChatCompletion(
ctx context.Context,
key schemas.Key,
request *schemas.DeepIntShieldChatRequest,
) (*schemas.DeepIntShieldChatResponse, *schemas.DeepIntShieldError) {
// 1. Convert Request
providerReq := ToProviderNameChatCompletionRequest(request)
// 2. Build HTTP Request
body, err := sonic.Marshal(providerReq)
if err != nil {
return nil, &schemas.DeepIntShieldError{/* ... */}
}
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.DeepIntShieldError{/* ... */}
}
// 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.DeepIntShieldError{/* ... */}
}
return ToDeepIntShieldChatResponse(&providerResp)
}

4. Feature Files (chat.go, embedding.go, speech.go, etc.) (The Converter Layer)

Section titled “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.DeepIntShield[Feature]Request) *[ProviderName][Feature]Request
  • To DeepIntShield Format: ToDeepIntShield[Feature]Response(providerResp *[ProviderName][Feature]Response) (*schemas.DeepIntShield[Feature]Response, *schemas.DeepIntShieldError)

Examples:

  • ToHuggingFaceChatCompletionRequest
  • ToDeepIntShieldChatResponse
  • ToHuggingFaceEmbeddingRequest
  • ToDeepIntShieldEmbeddingResponse

Required Converter Pairs (if feature is supported):

  • Request converter: DeepIntShield → Provider
  • Response converter: Provider → DeepIntShield

Real Example from core/providers/huggingface/chat.go:

package huggingface
import (
"encoding/json"
"fmt"
"github.com/bytedance/sonic"
schemas "github.com/maximhq/deepintshield/core/schemas"
)
// ToHuggingFaceChatCompletionRequest converts a DeepIntShield chat request to HuggingFace format
func ToHuggingFaceChatCompletionRequest(bifrostReq *schemas.DeepIntShieldChatRequest) *HuggingFaceChatRequest {
if bifrostReq == nil || bifrostReq.Input == nil {
return nil
}
// Convert messages from DeepIntShield 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:

package providername
import (
"github.com/maximhq/deepintshield/core/schemas"
)
// ToProviderNameEmbeddingRequest converts a DeepIntShield embedding request to provider format
func ToProviderNameEmbeddingRequest(bifrostReq *schemas.DeepIntShieldEmbeddingRequest) *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:

package providername
import (
"strings"
schemas "github.com/maximhq/deepintshield/core/schemas"
)
// ToDeepIntShieldListModelsResponse converts provider models list to DeepIntShield format
func ToDeepIntShieldListModelsResponse(
providerResp *ProviderNameListModelsResponse,
providerKey schemas.ModelProvider,
) *schemas.DeepIntShieldListModelsResponse {
if providerResp == nil {
return nil
}
bifrostResponse := &schemas.DeepIntShieldListModelsResponse{
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 DeepIntShield 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


Create core/providers/[provider_name]/[provider_name].go:

// Package cerebras implements the Cerebras LLM provider.
package cerebras
import (
"context"
"strings"
"time"
"github.com/maximhq/deepintshield/core/providers/openai"
providerUtils "github.com/maximhq/deepintshield/core/providers/utils"
schemas "github.com/maximhq/deepintshield/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 DeepIntShieldResponse
}
// 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

Section titled “Step 2: Implement Required Methods Using OpenAI Handlers”

For each supported feature, delegate to the corresponding OpenAI handler:

Chat Completion (Non-Streaming):

// ChatCompletion performs a chat completion request to the Cerebras API.
func (provider *CerebrasProvider) ChatCompletion(
ctx context.Context,
key schemas.Key,
request *schemas.DeepIntShieldChatRequest,
) (*schemas.DeepIntShieldChatResponse, *schemas.DeepIntShieldError) {
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):

// 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.DeepIntShieldChatRequest,
) (chan *schemas.DeepIntShieldStream, *schemas.DeepIntShieldError) {
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):

// TextCompletion performs a text completion request to Cerebras's API.
func (provider *CerebrasProvider) TextCompletion(
ctx context.Context,
key schemas.Key,
request *schemas.DeepIntShieldTextCompletionRequest,
) (*schemas.DeepIntShieldTextCompletionResponse, *schemas.DeepIntShieldError) {
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):

// 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.DeepIntShieldTextCompletionRequest,
) (chan *schemas.DeepIntShieldStream, *schemas.DeepIntShieldError) {
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:

// ListModels performs a list models request to Cerebras's API.
func (provider *CerebrasProvider) ListModels(
ctx context.Context,
keys []schemas.Key,
request *schemas.DeepIntShieldListModelsRequest,
) (*schemas.DeepIntShieldListModelsResponse, *schemas.DeepIntShieldError) {
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,
)
}

For features not supported by the provider, return appropriate errors:

// Embedding is not supported by Cerebras
func (provider *CerebrasProvider) Embedding(
ctx context.Context,
key schemas.Key,
request *schemas.DeepIntShieldEmbeddingRequest,
) (*schemas.DeepIntShieldEmbeddingResponse, *schemas.DeepIntShieldError) {
return nil, &schemas.DeepIntShieldError{
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.DeepIntShieldSpeechRequest,
) (*schemas.DeepIntShieldSpeechResponse, *schemas.DeepIntShieldError) {
return nil, &schemas.DeepIntShieldError{
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.DeepIntShieldSpeechRequest,
) (chan *schemas.DeepIntShieldStream, *schemas.DeepIntShieldError) {
return nil, &schemas.DeepIntShieldError{
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.DeepIntShieldTranscriptionRequest,
) (*schemas.DeepIntShieldTranscriptionResponse, *schemas.DeepIntShieldError) {
return nil, &schemas.DeepIntShieldError{
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.DeepIntShieldTranscriptionRequest,
) (chan *schemas.DeepIntShieldStream, *schemas.DeepIntShieldError) {
return nil, &schemas.DeepIntShieldError{
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.DeepIntShieldResponsesRequest,
) (*schemas.DeepIntShieldResponsesResponse, *schemas.DeepIntShieldError) {
return nil, &schemas.DeepIntShieldError{
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.DeepIntShieldResponsesRequest,
) (chan *schemas.DeepIntShieldStream, *schemas.DeepIntShieldError) {
return nil, &schemas.DeepIntShieldError{
StatusCode: http.StatusNotImplemented,
Message: "Responses streaming is not supported by Cerebras",
Type: "unsupported_feature",
}
}

Key Points for OpenAI-compatible Providers

Section titled “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.DeepIntShieldError - 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

Follow this exact order when implementing a new provider.


Phase 1: Research & Planning (Before Writing Code)

Section titled “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):

    # 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

  1. Create Provider Directory:
    Terminal window
    mkdir -p core/providers/[provider_name]
    cd core/providers/[provider_name]

  1. Create types.go - Define ALL Provider-Specific Types:

    Order of Type Definitions:

    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"

  1. Create utils.go - Define Constants and Helper Functions:

    Order of Definitions:

    package [provider_name]
    import (
    "context"
    "encoding/json"
    "fmt"
    providerUtils "github.com/maximhq/deepintshield/core/providers/utils"
    schemas "github.com/maximhq/deepintshield/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.DeepIntShieldError { ... }
    // 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)

Section titled “Phase 5: Implement Converters (Feature Files)”
  1. Create Feature Files in Order of Complexity (simplest first):

    a. Create models.go (if supported):

    package [provider_name]
    import (
    "fmt"
    schemas "github.com/maximhq/deepintshield/core/schemas"
    )
    // ToDeepIntShieldListModelsResponse converts provider models to DeepIntShield format
    func (response *[ProviderName]ListModelsResponse) ToDeepIntShieldListModelsResponse(
    providerKey schemas.ModelProvider,
    ) *schemas.DeepIntShieldListModelsResponse {
    if response == nil {
    return nil
    }
    bifrostResponse := &schemas.DeepIntShieldListModelsResponse{
    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):

    package [provider_name]
    import schemas "github.com/maximhq/deepintshield/core/schemas"
    // To[ProviderName]EmbeddingRequest converts DeepIntShield request to provider format
    func To[ProviderName]EmbeddingRequest(
    bifrostReq *schemas.DeepIntShieldEmbeddingRequest,
    ) *[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
    }
    // ToDeepIntShieldEmbeddingResponse converts provider response to DeepIntShield format
    func ToDeepIntShieldEmbeddingResponse(
    providerResp *[ProviderName]EmbeddingResponse,
    ) (*schemas.DeepIntShieldEmbeddingResponse, *schemas.DeepIntShieldError) {
    if providerResp == nil {
    return nil, &schemas.DeepIntShieldError{
    Message: "Provider response is nil",
    Type: "invalid_response",
    }
    }
    bifrostResp := &schemas.DeepIntShieldEmbeddingResponse{
    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):

    package [provider_name]
    import (
    "encoding/json"
    "github.com/bytedance/sonic"
    schemas "github.com/maximhq/deepintshield/core/schemas"
    )
    // To[ProviderName]ChatCompletionRequest converts DeepIntShield chat request to provider format
    func To[ProviderName]ChatCompletionRequest(
    bifrostReq *schemas.DeepIntShieldChatRequest,
    ) *[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:

// Request Converter - Maps DeepIntShield 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 DeepIntShield
func ToDeepIntShield[Feature]Response(providerResp) (*schemas.DeepIntShieldResponse, *schemas.DeepIntShieldError) {
// 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: ToDeepIntShield[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.


Phase 6: Implement Provider (provider_name.go)

Section titled “Phase 6: Implement Provider (provider_name.go)”
  1. 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

  1. 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/deepintshield/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 DeepIntShield UI and CI/CD pipelines.


a. Add Model Placeholder (ui/lib/constants/config.ts)

Section titled “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:

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:

huggingface: "e.g. google/gemma-2-2b-it, nebius/Qwen/Qwen3-Embedding-8B",

b. Set Key Requirement (ui/lib/constants/config.ts)

Section titled “b. Set Key Requirement (ui/lib/constants/config.ts)”

Specify whether your provider requires an API key:

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:

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)

Section titled “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.

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)

Section titled “Step 3: Register Provider Name (ui/lib/constants/logs.ts)”
export const KnownProvidersNames = [
"anthropic",
"azure",
"bedrock",
// ... other providers
"[providername]", // Add your provider name (lowercase)
] as const;
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:

huggingface: "HuggingFace",
cerebras: "Cerebras",

Step 4: Update OpenAPI Specification (docs/openapi/openapi.json)

Section titled “Step 4: Update OpenAPI Specification (docs/openapi/openapi.json)”

Add your provider to the API documentation’s provider enum:

{
"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)

Section titled “Step 5: Update Configuration Schema (transports/config.schema.json)”
{
"providers": {
"type": "object",
"properties": {
"openai": { "$ref": "#/$defs/provider" },
"anthropic": { "$ref": "#/$defs/provider" },
// ... other providers
"[providername]": { "$ref": "#/$defs/provider" }
}
}
}
{
"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.


Add your provider to the list of supported providers:

## 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:

- **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/deepintshield.go)

Section titled “Step 7: Register Provider in Core (core/deepintshield.go)”
import (
// ... existing imports
"github.com/maximhq/deepintshield/core/providers/[providername]"
)
func (deepintshield *DeepIntShield) createBaseProvider(providerKey schemas.ModelProvider, config *schemas.ProviderConfig) (schemas.Provider, error) {
// ... existing cases
case schemas.ProviderName:
return providername.NewProviderNameProvider(config, deepintshield.logger), nil
default:
return nil, fmt.Errorf("unsupported provider: %s", targetProviderKey)
}

For OpenAI-compatible providers (returns error):

case schemas.ProviderName:
return providername.NewProviderNameProvider(config, deepintshield.logger)

For non-OpenAI-compatible providers (no error):

case schemas.ProviderName:
return providername.NewProviderNameProvider(config, deepintshield.logger), nil

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:

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:

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
  • deepintshield-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/deepintshield.go
  • ✅ Provider case added to createBaseProvider in core/deepintshield.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)
---
title: "[Provider Full Name]"
description: "[Brief description] - parameter mapping, [key features], and [auth method]"
icon: "[icon letter or emoji]"
---

Example:

---
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 DeepIntShield converts requests to/from this provider’s format
  • List of major transformation features

Template:

## Overview
[Provider Name] is a **[type: OpenAI-compatible/custom API/local-first]** provider offering [key features]. DeepIntShield 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:

### 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)

Section titled “4. Feature Sections (One per Supported Feature)”

For each major feature (Chat Completions, Embeddings, etc.):

## 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
### Filtered Parameters
Removed for [Provider] compatibility:
- `prompt_cache_key` - Not supported
- `store` - Not supported
### [Feature Name]
Document any provider-specific features like:
- Reasoning/thinking support
- Special authentication
- Unique parameters
- Format conversions
## Message Conversion
Content types supported:
- ✅ Text content
- ✅ Images (URL and base64)
- ❌ Audio input
## Response Conversion
Field mapping from provider format back to DeepIntShield standard.
## Streaming
[Provider] uses **[protocol: SSE/WebSocket/custom]** streaming with:
- Request configuration: stream: true
- Event format: [description]
- End marker: [description]
## Authentication
**[Authentication Type]:**

Authorization: [format]

[Additional details about key management, etc.]
## Configuration
**HTTP Settings:**
- **Base URL**: `[default URL]` (default)
- **API Version**: [version info]
- **Max Connections**: 5000 per host
- **Idle Timeout**: 60 seconds

Use collapsible accordion sections for limitations:

## Caveats
<details>
<summary>[Caveat Title]</summary>
**Severity**: [High/Medium/Low]
**Behavior**: [What happens]
**Impact**: [What breaks/changes]
**Code**: [File references if relevant]
</details>
<details>
<summary>[Another Caveat]</summary>
...
</details>

Common caveats to document:

  • Unsupported content types (images, audio, etc.)
  • Parameter limitations
  • Streaming restrictions
  • Special handling required
  • Breaking behavioral differences from OpenAI standard

Use special callouts for important information:

<Aside type="caution">
**Unsupported Operations**: [List operations], [List operations].
</Aside>
<Aside type="note">
[Provider] requires [special setup/configuration].
</Aside>
<Aside type="tip">
[Helpful tip for using this provider effectively].
</Aside>

Include examples in both formats where applicable:

&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"}]
}'

</Tab> <Tab title=“Go SDK”>

response, err := client.ChatCompletion(ctx, &schemas.DeepIntShieldChatRequest{
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/deepintshield/core/internal/llmtests"
"github.com/maximhq/deepintshield/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()
}

Environment Variables:

  • REQUIRED: [PROVIDER_NAME]_API_KEY - API key for the provider
  • Optional: PROVIDER_BASE_URL - Custom base URL for testing

Example:

Terminal window
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)

The llmtests.TestScenarios struct defines which tests to run. Set each field based on provider capabilities:

ScenarioEnable if…
SimpleChatProvider supports basic chat completion
CompletionStreamProvider supports streaming chat
TextCompletionProvider supports text completions (legacy)
TextCompletionStreamProvider supports streaming text completions
ToolCallsProvider supports function/tool calling
ToolCallsStreamingProvider supports streaming with tool calls
EmbeddingProvider supports text embeddings
ListModelsProvider has a list models endpoint
ImageURLProvider accepts image URLs in messages
ImageBase64Provider accepts base64-encoded images

ChatModel (REQUIRED if any chat scenario is enabled):

ChatModel: "llama-3.3-70b", // Primary model for chat tests

TextModel (REQUIRED if any text completion scenario is enabled):

TextModel: "llama3.1-8b", // Model for text completion tests

EmbeddingModel (REQUIRED if Embedding scenario is enabled):

EmbeddingModel: "text-embedding-ada-002", // Model for embedding tests

Fallbacks (OPTIONAL but recommended):

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:

Terminal window
cd core/providers/[provider_name]
go test -v

Run with API key:

Terminal window
PROVIDER_API_KEY="your-key" go test -v

Run specific test:

Terminal window
go test -v -run TestProviderName

Run with timeout (for slow providers):

Terminal window
go test -v -timeout 5m

Skip integration tests (if API key not set):

Terminal window
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:

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.


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.DeepIntShieldError
  • ✅ 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/deepintshield.go (ModelProvider type + arrays)
  • ✅ Provider registered in core/deepintshield.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