1240 lines
42 KiB
Go
1240 lines
42 KiB
Go
package configstore
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"maps"
|
|
"sort"
|
|
"strconv"
|
|
|
|
"github.com/bytedance/sonic"
|
|
bifrost "github.com/maximhq/bifrost/core"
|
|
"github.com/maximhq/bifrost/core/schemas"
|
|
"github.com/maximhq/bifrost/framework/configstore/tables"
|
|
)
|
|
|
|
type EnvKeyType string
|
|
|
|
const (
|
|
EnvKeyTypeAPIKey EnvKeyType = "api_key"
|
|
EnvKeyTypeAzureConfig EnvKeyType = "azure_config"
|
|
EnvKeyTypeVertexConfig EnvKeyType = "vertex_config"
|
|
EnvKeyTypeBedrockConfig EnvKeyType = "bedrock_config"
|
|
EnvKeyTypeConnection EnvKeyType = "connection_string"
|
|
EnvKeyTypeMCPHeader EnvKeyType = "mcp_header"
|
|
)
|
|
|
|
// EnvKeyInfo stores information about a key sourced from environment
|
|
type EnvKeyInfo struct {
|
|
EnvVar string // The environment variable name (without env. prefix)
|
|
Provider schemas.ModelProvider // The provider this key belongs to (empty for core/mcp configs)
|
|
KeyType EnvKeyType // Type of key (e.g., "api_key", "azure_config", "vertex_config", "bedrock_config", "connection_string", "mcp_header")
|
|
ConfigPath string // Path in config where this env var is used
|
|
KeyID string // The key ID this env var belongs to (empty for non-key configs like bedrock_config, connection_string)
|
|
}
|
|
|
|
// CompatConfig holds the compat plugin feature flags.
|
|
type CompatConfig struct {
|
|
ConvertTextToChat bool `json:"convert_text_to_chat"`
|
|
ConvertChatToResponses bool `json:"convert_chat_to_responses"`
|
|
ShouldDropParams bool `json:"should_drop_params"`
|
|
ShouldConvertParams bool `json:"should_convert_params"`
|
|
}
|
|
|
|
// ClientConfig represents the core configuration for Bifrost HTTP transport and the Bifrost Client.
|
|
// It includes settings for excess request handling, Prometheus metrics, and initial pool size.
|
|
type ClientConfig struct {
|
|
DropExcessRequests bool `json:"drop_excess_requests"` // Drop excess requests if the provider queue is full
|
|
InitialPoolSize int `json:"initial_pool_size"` // The initial pool size for the bifrost client
|
|
PrometheusLabels []string `json:"prometheus_labels"` // The labels to be used for prometheus metrics
|
|
EnableLogging *bool `json:"enable_logging"` // Enable logging of requests and responses
|
|
DisableContentLogging bool `json:"disable_content_logging"` // Disable logging of content
|
|
DisableDBPingsInHealth bool `json:"disable_db_pings_in_health"`
|
|
LogRetentionDays int `json:"log_retention_days" validate:"min=1"` // Number of days to retain logs (minimum 1 day)
|
|
EnforceAuthOnInference bool `json:"enforce_auth_on_inference"` // Require auth (VK, API key, or user token) on inference endpoints
|
|
EnforceGovernanceHeader bool `json:"enforce_governance_header,omitempty"` // Deprecated: use EnforceAuthOnInference
|
|
EnforceSCIMAuth bool `json:"enforce_scim_auth,omitempty"` // Deprecated: use EnforceAuthOnInference
|
|
AllowDirectKeys bool `json:"allow_direct_keys"` // Allow direct keys to be used for requests
|
|
AllowedOrigins []string `json:"allowed_origins,omitempty"` // Additional allowed origins for CORS and WebSocket (localhost is always allowed)
|
|
AllowedHeaders []string `json:"allowed_headers,omitempty"` // Additional allowed headers for CORS and WebSocket
|
|
MaxRequestBodySizeMB int `json:"max_request_body_size_mb"` // The maximum request body size in MB
|
|
Compat CompatConfig `json:"compat"` // Compat plugin configuration
|
|
MCPAgentDepth int `json:"mcp_agent_depth"` // The maximum depth for MCP agent mode tool execution
|
|
MCPToolExecutionTimeout int `json:"mcp_tool_execution_timeout"` // The timeout for individual tool execution in seconds
|
|
MCPCodeModeBindingLevel string `json:"mcp_code_mode_binding_level"` // Code mode binding level: "server" or "tool"
|
|
MCPToolSyncInterval int `json:"mcp_tool_sync_interval"` // Global tool sync interval in minutes (default: 10, 0 = disabled)
|
|
MCPDisableAutoToolInject bool `json:"mcp_disable_auto_tool_inject"` // When true, MCP tools are not injected into requests by default
|
|
HeaderFilterConfig *tables.GlobalHeaderFilterConfig `json:"header_filter_config,omitempty"` // Global header filtering configuration for x-bf-eh-* headers
|
|
AsyncJobResultTTL int `json:"async_job_result_ttl"` // Default TTL for async job results in seconds (default: 3600 = 1 hour)
|
|
RequiredHeaders []string `json:"required_headers,omitempty"` // Headers that must be present on every request (case-insensitive)
|
|
LoggingHeaders []string `json:"logging_headers,omitempty"` // Headers to capture in log metadata
|
|
WhitelistedRoutes []string `json:"whitelisted_routes,omitempty"` // Routes that bypass auth middleware
|
|
HideDeletedVirtualKeysInFilters bool `json:"hide_deleted_virtual_keys_in_filters"` // Hide deleted virtual keys from logs/MCP filter data
|
|
RoutingChainMaxDepth int `json:"routing_chain_max_depth"` // Maximum depth for routing rule chain evaluation (default: 10)
|
|
ConfigHash string `json:"-"` // Config hash for reconciliation (not serialized)
|
|
}
|
|
|
|
// GenerateClientConfigHash generates a SHA256 hash of the client configuration.
|
|
// This is used to detect changes between config.json and database config.
|
|
func (c *ClientConfig) GenerateClientConfigHash() (string, error) {
|
|
hash := sha256.New()
|
|
|
|
// Hash boolean fields
|
|
if c.DropExcessRequests {
|
|
hash.Write([]byte("dropExcessRequests:true"))
|
|
} else {
|
|
hash.Write([]byte("dropExcessRequests:false"))
|
|
}
|
|
|
|
enableLogging := c.EnableLogging == nil || *c.EnableLogging
|
|
if enableLogging {
|
|
hash.Write([]byte("enableLogging:true"))
|
|
} else {
|
|
hash.Write([]byte("enableLogging:false"))
|
|
}
|
|
|
|
if c.DisableContentLogging {
|
|
hash.Write([]byte("disableContentLogging:true"))
|
|
} else {
|
|
hash.Write([]byte("disableContentLogging:false"))
|
|
}
|
|
|
|
if c.DisableDBPingsInHealth {
|
|
hash.Write([]byte("disableDBPingsInHealth:true"))
|
|
} else {
|
|
hash.Write([]byte("disableDBPingsInHealth:false"))
|
|
}
|
|
|
|
if c.EnforceAuthOnInference {
|
|
hash.Write([]byte("enforceAuthOnInference:true"))
|
|
} else {
|
|
hash.Write([]byte("enforceAuthOnInference:false"))
|
|
}
|
|
|
|
if c.AllowDirectKeys {
|
|
hash.Write([]byte("allowDirectKeys:true"))
|
|
} else {
|
|
hash.Write([]byte("allowDirectKeys:false"))
|
|
}
|
|
|
|
if c.Compat.ConvertTextToChat {
|
|
hash.Write([]byte("compatConvertTextToChat:true"))
|
|
}
|
|
if c.Compat.ConvertChatToResponses {
|
|
hash.Write([]byte("compatConvertChatToResponses:true"))
|
|
}
|
|
if c.Compat.ShouldDropParams {
|
|
hash.Write([]byte("compatShouldDropParams:true"))
|
|
}
|
|
if c.Compat.ShouldConvertParams {
|
|
hash.Write([]byte("compatShouldConvertParams:true"))
|
|
}
|
|
|
|
// Only hash non-default value to avoid legacy config hash churn.
|
|
if c.HideDeletedVirtualKeysInFilters {
|
|
hash.Write([]byte("hideDeletedVirtualKeysInFilters:true"))
|
|
}
|
|
|
|
// Always hash when non-zero — explicitly setting the default (10) is a meaningful
|
|
// config change that should be reflected in the hash. The migration that introduces
|
|
// this field backfills existing rows with RoutingChainMaxDepth=10 and regenerates
|
|
// their config_hash so there is no hash churn on upgrade for unmodified configs.
|
|
if c.RoutingChainMaxDepth > 0 {
|
|
hash.Write([]byte("routingChainMaxDepth:" + strconv.Itoa(c.RoutingChainMaxDepth)))
|
|
}
|
|
|
|
if c.MCPAgentDepth > 0 {
|
|
hash.Write([]byte("mcpAgentDepth:" + strconv.Itoa(c.MCPAgentDepth)))
|
|
} else {
|
|
hash.Write([]byte("mcpAgentDepth:0"))
|
|
}
|
|
|
|
if c.MCPToolExecutionTimeout > 0 {
|
|
hash.Write([]byte("mcpToolExecutionTimeout:" + strconv.Itoa(c.MCPToolExecutionTimeout)))
|
|
} else {
|
|
hash.Write([]byte("mcpToolExecutionTimeout:0"))
|
|
}
|
|
|
|
if c.MCPCodeModeBindingLevel != "" {
|
|
hash.Write([]byte("mcpCodeModeBindingLevel:" + c.MCPCodeModeBindingLevel))
|
|
} else {
|
|
hash.Write([]byte("mcpCodeModeBindingLevel:server"))
|
|
}
|
|
|
|
if c.MCPToolSyncInterval > 0 {
|
|
hash.Write([]byte("mcpToolSyncInterval:" + strconv.Itoa(c.MCPToolSyncInterval)))
|
|
} else {
|
|
hash.Write([]byte("mcpToolSyncInterval:0"))
|
|
}
|
|
|
|
// Only hash non-default value to avoid legacy config hash churn on upgrade.
|
|
if c.MCPDisableAutoToolInject {
|
|
hash.Write([]byte("mcpDisableAutoToolInject:true"))
|
|
}
|
|
|
|
if c.AsyncJobResultTTL > 0 {
|
|
hash.Write([]byte("asyncJobResultTTL:" + strconv.Itoa(c.AsyncJobResultTTL)))
|
|
} else {
|
|
hash.Write([]byte("asyncJobResultTTL:0"))
|
|
}
|
|
|
|
// Hash integer fields
|
|
data, err := sonic.Marshal(c.InitialPoolSize)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
|
|
data, err = sonic.Marshal(c.LogRetentionDays)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
|
|
data, err = sonic.Marshal(c.MaxRequestBodySizeMB)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
|
|
// Hash PrometheusLabels (sorted for deterministic hashing)
|
|
if len(c.PrometheusLabels) > 0 {
|
|
sortedLabels := make([]string, len(c.PrometheusLabels))
|
|
copy(sortedLabels, c.PrometheusLabels)
|
|
sort.Strings(sortedLabels)
|
|
data, err := sonic.Marshal(sortedLabels)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
}
|
|
|
|
// Hash AllowedOrigins (sorted for deterministic hashing)
|
|
if len(c.AllowedOrigins) > 0 {
|
|
sortedOrigins := make([]string, len(c.AllowedOrigins))
|
|
copy(sortedOrigins, c.AllowedOrigins)
|
|
sort.Strings(sortedOrigins)
|
|
data, err := sonic.Marshal(sortedOrigins)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
}
|
|
|
|
// Hash AllowedHeaders (sorted for deterministic hashing)
|
|
if len(c.AllowedHeaders) > 0 {
|
|
sortedHeaders := make([]string, len(c.AllowedHeaders))
|
|
copy(sortedHeaders, c.AllowedHeaders)
|
|
sort.Strings(sortedHeaders)
|
|
data, err := sonic.Marshal(sortedHeaders)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
}
|
|
|
|
// Hash LoggingHeaders (sorted for deterministic hashing)
|
|
if len(c.LoggingHeaders) > 0 {
|
|
sortedLogging := make([]string, len(c.LoggingHeaders))
|
|
copy(sortedLogging, c.LoggingHeaders)
|
|
sort.Strings(sortedLogging)
|
|
data, err := sonic.Marshal(sortedLogging)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write([]byte("loggingHeaders:"))
|
|
hash.Write(data)
|
|
}
|
|
|
|
// Hash RequiredHeaders (sorted for deterministic hashing)
|
|
if len(c.RequiredHeaders) > 0 {
|
|
sortedRequired := make([]string, len(c.RequiredHeaders))
|
|
copy(sortedRequired, c.RequiredHeaders)
|
|
sort.Strings(sortedRequired)
|
|
data, err := sonic.Marshal(sortedRequired)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write([]byte("requiredHeaders:"))
|
|
hash.Write(data)
|
|
}
|
|
|
|
// Hash WhitelistedRoutes (sorted for deterministic hashing)
|
|
if len(c.WhitelistedRoutes) > 0 {
|
|
sortedRoutes := make([]string, len(c.WhitelistedRoutes))
|
|
copy(sortedRoutes, c.WhitelistedRoutes)
|
|
sort.Strings(sortedRoutes)
|
|
data, err := sonic.Marshal(sortedRoutes)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write([]byte("whitelistedRoutes:"))
|
|
hash.Write(data)
|
|
}
|
|
|
|
// Hash HeaderFilterConfig
|
|
if c.HeaderFilterConfig != nil {
|
|
// Hash Allowlist (sorted for deterministic hashing)
|
|
if len(c.HeaderFilterConfig.Allowlist) > 0 {
|
|
sortedAllowlist := make([]string, len(c.HeaderFilterConfig.Allowlist))
|
|
copy(sortedAllowlist, c.HeaderFilterConfig.Allowlist)
|
|
sort.Strings(sortedAllowlist)
|
|
data, err := sonic.Marshal(sortedAllowlist)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write([]byte("headerFilterConfig.allowlist:"))
|
|
hash.Write(data)
|
|
}
|
|
// Hash Denylist (sorted for deterministic hashing)
|
|
if len(c.HeaderFilterConfig.Denylist) > 0 {
|
|
sortedDenylist := make([]string, len(c.HeaderFilterConfig.Denylist))
|
|
copy(sortedDenylist, c.HeaderFilterConfig.Denylist)
|
|
sort.Strings(sortedDenylist)
|
|
data, err := sonic.Marshal(sortedDenylist)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write([]byte("headerFilterConfig.denylist:"))
|
|
hash.Write(data)
|
|
}
|
|
}
|
|
|
|
return hex.EncodeToString(hash.Sum(nil)), nil
|
|
}
|
|
|
|
// ProviderConfig represents the configuration for a specific AI model provider.
|
|
// It includes API keys, network settings, and concurrency settings.
|
|
type ProviderConfig struct {
|
|
Keys []schemas.Key `json:"keys"` // API keys for the provider with UUIDs
|
|
NetworkConfig *schemas.NetworkConfig `json:"network_config,omitempty"` // Network-related settings
|
|
ConcurrencyAndBufferSize *schemas.ConcurrencyAndBufferSize `json:"concurrency_and_buffer_size,omitempty"` // Concurrency settings
|
|
ProxyConfig *schemas.ProxyConfig `json:"proxy_config,omitempty"` // Proxy configuration
|
|
SendBackRawRequest bool `json:"send_back_raw_request"` // Include raw request in BifrostResponse
|
|
SendBackRawResponse bool `json:"send_back_raw_response"` // Include raw response in BifrostResponse
|
|
StoreRawRequestResponse bool `json:"store_raw_request_response"` // Capture raw request/response for internal logging only; strip from API responses returned to clients
|
|
CustomProviderConfig *schemas.CustomProviderConfig `json:"custom_provider_config,omitempty"` // Custom provider configuration
|
|
OpenAIConfig *schemas.OpenAIConfig `json:"openai_config,omitempty"` // OpenAI-specific configuration
|
|
ConfigHash string `json:"config_hash,omitempty"` // Hash of config.json version, used for change detection
|
|
Status string `json:"status,omitempty"` // Model discovery status for keyless providers
|
|
Description string `json:"description,omitempty"` // Model discovery error message for keyless providers
|
|
}
|
|
|
|
// Redacted returns a redacted copy of the provider configuration.
|
|
func (p *ProviderConfig) Redacted() *ProviderConfig {
|
|
// Create redacted config with same structure but redacted values
|
|
var redactedNetworkConfig *schemas.NetworkConfig
|
|
if p.NetworkConfig != nil {
|
|
redactedNetworkConfig = p.NetworkConfig.Redacted()
|
|
}
|
|
redactedConfig := ProviderConfig{
|
|
NetworkConfig: redactedNetworkConfig,
|
|
ConcurrencyAndBufferSize: p.ConcurrencyAndBufferSize,
|
|
SendBackRawRequest: p.SendBackRawRequest,
|
|
SendBackRawResponse: p.SendBackRawResponse,
|
|
StoreRawRequestResponse: p.StoreRawRequestResponse,
|
|
CustomProviderConfig: p.CustomProviderConfig,
|
|
OpenAIConfig: p.OpenAIConfig,
|
|
ConfigHash: p.ConfigHash,
|
|
Status: p.Status,
|
|
Description: p.Description,
|
|
}
|
|
|
|
if p.ProxyConfig != nil {
|
|
redactedConfig.ProxyConfig = p.ProxyConfig.Redacted()
|
|
}
|
|
|
|
// Create redacted keys
|
|
redactedConfig.Keys = make([]schemas.Key, len(p.Keys))
|
|
for i, key := range p.Keys {
|
|
models := key.Models
|
|
if models == nil {
|
|
models = []string{} // Ensure models is never nil in JSON response
|
|
}
|
|
blacklistedModels := key.BlacklistedModels
|
|
if blacklistedModels == nil {
|
|
blacklistedModels = []string{} // Match models: empty JSON array, not null
|
|
}
|
|
redactedConfig.Keys[i] = schemas.Key{
|
|
ID: key.ID,
|
|
Name: key.Name,
|
|
Models: models,
|
|
BlacklistedModels: blacklistedModels,
|
|
Weight: key.Weight,
|
|
ConfigHash: key.ConfigHash,
|
|
}
|
|
if key.Enabled != nil {
|
|
enabled := *key.Enabled
|
|
redactedConfig.Keys[i].Enabled = &enabled
|
|
}
|
|
if key.Aliases != nil {
|
|
redactedConfig.Keys[i].Aliases = maps.Clone(key.Aliases)
|
|
}
|
|
redactedConfig.Keys[i].Value = *key.Value.Redacted()
|
|
// Add back use for batch api
|
|
if key.UseForBatchAPI != nil {
|
|
redactedConfig.Keys[i].UseForBatchAPI = key.UseForBatchAPI
|
|
} else {
|
|
redactedConfig.Keys[i].UseForBatchAPI = bifrost.Ptr(false)
|
|
}
|
|
|
|
// Add model discovery status and error
|
|
redactedConfig.Keys[i].Status = key.Status
|
|
redactedConfig.Keys[i].Description = key.Description
|
|
|
|
// Redact Azure key config if present
|
|
if key.AzureKeyConfig != nil {
|
|
azureConfig := &schemas.AzureKeyConfig{}
|
|
azureConfig.Endpoint = *key.AzureKeyConfig.Endpoint.Redacted()
|
|
azureConfig.APIVersion = key.AzureKeyConfig.APIVersion
|
|
if key.AzureKeyConfig.ClientID != nil {
|
|
azureConfig.ClientID = key.AzureKeyConfig.ClientID.Redacted()
|
|
}
|
|
if key.AzureKeyConfig.ClientSecret != nil {
|
|
azureConfig.ClientSecret = key.AzureKeyConfig.ClientSecret.Redacted()
|
|
}
|
|
if key.AzureKeyConfig.TenantID != nil {
|
|
azureConfig.TenantID = key.AzureKeyConfig.TenantID.Redacted()
|
|
}
|
|
if len(key.AzureKeyConfig.Scopes) > 0 {
|
|
azureConfig.Scopes = key.AzureKeyConfig.Scopes
|
|
}
|
|
redactedConfig.Keys[i].AzureKeyConfig = azureConfig
|
|
}
|
|
|
|
// Redact Vertex key config if present
|
|
if key.VertexKeyConfig != nil {
|
|
vertexConfig := &schemas.VertexKeyConfig{}
|
|
vertexConfig.ProjectID = *key.VertexKeyConfig.ProjectID.Redacted()
|
|
vertexConfig.ProjectNumber = *key.VertexKeyConfig.ProjectNumber.Redacted()
|
|
vertexConfig.Region = *key.VertexKeyConfig.Region.Redacted()
|
|
vertexConfig.AuthCredentials = *key.VertexKeyConfig.AuthCredentials.Redacted()
|
|
redactedConfig.Keys[i].VertexKeyConfig = vertexConfig
|
|
}
|
|
|
|
// Redact Bedrock key config if present
|
|
if key.BedrockKeyConfig != nil {
|
|
bedrockConfig := &schemas.BedrockKeyConfig{}
|
|
bedrockConfig.AccessKey = *key.BedrockKeyConfig.AccessKey.Redacted()
|
|
bedrockConfig.SecretKey = *key.BedrockKeyConfig.SecretKey.Redacted()
|
|
if key.BedrockKeyConfig.SessionToken != nil {
|
|
bedrockConfig.SessionToken = key.BedrockKeyConfig.SessionToken.Redacted()
|
|
}
|
|
if key.BedrockKeyConfig.Region != nil {
|
|
bedrockConfig.Region = key.BedrockKeyConfig.Region.Redacted()
|
|
}
|
|
if key.BedrockKeyConfig.ARN != nil {
|
|
bedrockConfig.ARN = key.BedrockKeyConfig.ARN.Redacted()
|
|
}
|
|
if key.BedrockKeyConfig.RoleARN != nil {
|
|
bedrockConfig.RoleARN = key.BedrockKeyConfig.RoleARN.Redacted()
|
|
}
|
|
if key.BedrockKeyConfig.ExternalID != nil {
|
|
bedrockConfig.ExternalID = key.BedrockKeyConfig.ExternalID.Redacted()
|
|
}
|
|
if key.BedrockKeyConfig.RoleSessionName != nil {
|
|
bedrockConfig.RoleSessionName = key.BedrockKeyConfig.RoleSessionName.Redacted()
|
|
}
|
|
// Add back s3 config
|
|
if key.BedrockKeyConfig.BatchS3Config != nil {
|
|
bedrockConfig.BatchS3Config = key.BedrockKeyConfig.BatchS3Config
|
|
}
|
|
redactedConfig.Keys[i].BedrockKeyConfig = bedrockConfig
|
|
}
|
|
|
|
if key.VLLMKeyConfig != nil {
|
|
vllmConfig := &schemas.VLLMKeyConfig{
|
|
ModelName: key.VLLMKeyConfig.ModelName,
|
|
}
|
|
vllmConfig.URL = *key.VLLMKeyConfig.URL.Redacted()
|
|
redactedConfig.Keys[i].VLLMKeyConfig = vllmConfig
|
|
}
|
|
|
|
if key.ReplicateKeyConfig != nil {
|
|
replicateConfig := &schemas.ReplicateKeyConfig{
|
|
UseDeploymentsEndpoint: key.ReplicateKeyConfig.UseDeploymentsEndpoint,
|
|
}
|
|
redactedConfig.Keys[i].ReplicateKeyConfig = replicateConfig
|
|
}
|
|
|
|
if key.OllamaKeyConfig != nil {
|
|
ollamaConfig := &schemas.OllamaKeyConfig{}
|
|
ollamaConfig.URL = *key.OllamaKeyConfig.URL.Redacted()
|
|
redactedConfig.Keys[i].OllamaKeyConfig = ollamaConfig
|
|
}
|
|
|
|
if key.SGLKeyConfig != nil {
|
|
sglConfig := &schemas.SGLKeyConfig{}
|
|
sglConfig.URL = *key.SGLKeyConfig.URL.Redacted()
|
|
redactedConfig.Keys[i].SGLKeyConfig = sglConfig
|
|
}
|
|
}
|
|
return &redactedConfig
|
|
}
|
|
|
|
// GenerateConfigHash generates a SHA256 hash of the provider configuration.
|
|
// This is used to detect changes between config.json and database config.
|
|
// Keys are excluded as they are hashed separately.
|
|
func (p *ProviderConfig) GenerateConfigHash(providerName string) (string, error) {
|
|
hash := sha256.New()
|
|
|
|
// Hash provider name
|
|
hash.Write([]byte(providerName))
|
|
|
|
// Hash NetworkConfig
|
|
if p.NetworkConfig != nil {
|
|
data, err := sonic.Marshal(p.NetworkConfig)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
}
|
|
|
|
// Hash ConcurrencyAndBufferSize
|
|
if p.ConcurrencyAndBufferSize != nil {
|
|
data, err := sonic.Marshal(p.ConcurrencyAndBufferSize)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
}
|
|
|
|
// Hash ProxyConfig
|
|
if p.ProxyConfig != nil {
|
|
data, err := sonic.Marshal(p.ProxyConfig)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
}
|
|
|
|
// Hash CustomProviderConfig
|
|
if p.CustomProviderConfig != nil {
|
|
data, err := sonic.Marshal(p.CustomProviderConfig)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
}
|
|
|
|
// Hash OpenAIConfig
|
|
if p.OpenAIConfig != nil {
|
|
data, err := sonic.Marshal(p.OpenAIConfig)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
}
|
|
|
|
// Hash SendBackRawRequest
|
|
if p.SendBackRawRequest {
|
|
hash.Write([]byte("sendBackRawRequest"))
|
|
}
|
|
|
|
// Hash SendBackRawResponse
|
|
if p.SendBackRawResponse {
|
|
hash.Write([]byte("sendBackRawResponse"))
|
|
}
|
|
|
|
// Hash StoreRawRequestResponse
|
|
if p.StoreRawRequestResponse {
|
|
hash.Write([]byte("storeRawRequestResponse"))
|
|
}
|
|
|
|
return hex.EncodeToString(hash.Sum(nil)), nil
|
|
}
|
|
|
|
// GenerateKeyHash generates a SHA256 hash for an individual key.
|
|
// This is used to detect changes to keys between config.json and database.
|
|
// Skips: ID (dynamic UUID), timestamps
|
|
func GenerateKeyHash(key schemas.Key) (string, error) {
|
|
hash := sha256.New()
|
|
// Hash Name
|
|
hash.Write([]byte(key.Name))
|
|
// Hash Value (prefix with source type to prevent collisions between env and literal)
|
|
if key.Value.IsFromEnv() {
|
|
hash.Write([]byte("env:" + key.Value.EnvVar))
|
|
} else {
|
|
hash.Write([]byte("val:" + key.Value.Val))
|
|
}
|
|
// Hash Models (key-level model restrictions)
|
|
if len(key.Models) > 0 {
|
|
sortedModels := make([]string, len(key.Models))
|
|
copy(sortedModels, key.Models)
|
|
sort.Strings(sortedModels)
|
|
data, err := sonic.Marshal(sortedModels)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
}
|
|
// Hash BlacklistedModels (key-level deny list)
|
|
if len(key.BlacklistedModels) > 0 {
|
|
sortedBlacklistedModels := make([]string, len(key.BlacklistedModels))
|
|
copy(sortedBlacklistedModels, key.BlacklistedModels)
|
|
sort.Strings(sortedBlacklistedModels)
|
|
data, err := sonic.Marshal(sortedBlacklistedModels)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write([]byte("blacklistedModels:"))
|
|
hash.Write(data)
|
|
}
|
|
// Hash Weight
|
|
data, err := sonic.Marshal(key.Weight)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
// Hash AzureKeyConfig
|
|
if key.AzureKeyConfig != nil {
|
|
data, err := sonic.Marshal(key.AzureKeyConfig)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
}
|
|
// Hash VertexKeyConfig
|
|
if key.VertexKeyConfig != nil {
|
|
data, err := sonic.Marshal(key.VertexKeyConfig)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
}
|
|
// Hash BedrockKeyConfig
|
|
if key.BedrockKeyConfig != nil {
|
|
data, err := sonic.Marshal(key.BedrockKeyConfig)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
}
|
|
// Hash Aliases
|
|
if key.Aliases != nil {
|
|
data, err := sonic.Marshal(key.Aliases)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
}
|
|
// Hash VLLMKeyConfig
|
|
if key.VLLMKeyConfig != nil {
|
|
data, err := sonic.Marshal(key.VLLMKeyConfig)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
}
|
|
// Hash ReplicateKeyConfig
|
|
if key.ReplicateKeyConfig != nil {
|
|
data, err := sonic.Marshal(key.ReplicateKeyConfig)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
}
|
|
// Hash OllamaKeyConfig
|
|
if key.OllamaKeyConfig != nil {
|
|
data, err := sonic.Marshal(key.OllamaKeyConfig)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
}
|
|
// Hash SGLKeyConfig
|
|
if key.SGLKeyConfig != nil {
|
|
data, err := sonic.Marshal(key.SGLKeyConfig)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
}
|
|
// Hash Enabled (nil = false, only true produces different hash)
|
|
if key.Enabled != nil && *key.Enabled {
|
|
hash.Write([]byte("enabled:true"))
|
|
}
|
|
// Hash UseForBatchAPI (nil = default false for new keys)
|
|
useForBatchAPI := false
|
|
if key.UseForBatchAPI != nil {
|
|
useForBatchAPI = *key.UseForBatchAPI
|
|
}
|
|
if useForBatchAPI {
|
|
hash.Write([]byte("useForBatchAPI:true"))
|
|
}
|
|
return hex.EncodeToString(hash.Sum(nil)), nil
|
|
}
|
|
|
|
// VirtualKeyHashInput represents the fields used for virtual key hash generation.
|
|
// This struct is used to create a consistent hash from TableVirtualKey,
|
|
// excluding dynamic fields like ID, timestamps, and relationship objects.
|
|
type VirtualKeyHashInput struct {
|
|
Name string
|
|
Description string
|
|
IsActive bool
|
|
TeamID *string
|
|
CustomerID *string
|
|
RateLimitID *string
|
|
// ProviderConfigs and MCPConfigs are hashed separately as they contain nested data
|
|
ProviderConfigs []VirtualKeyProviderConfigHashInput
|
|
MCPConfigs []VirtualKeyMCPConfigHashInput
|
|
}
|
|
|
|
// VirtualKeyProviderConfigHashInput represents provider config fields for hashing
|
|
type VirtualKeyProviderConfigHashInput struct {
|
|
Provider string
|
|
Weight *float64
|
|
AllowedModels []string
|
|
RateLimitID *string
|
|
KeyIDs []string // Only key IDs, not full key objects
|
|
}
|
|
|
|
// VirtualKeyMCPConfigHashInput represents MCP config fields for hashing
|
|
type VirtualKeyMCPConfigHashInput struct {
|
|
MCPClientID uint
|
|
ToolsToExecute []string
|
|
}
|
|
|
|
// GenerateVirtualKeyHash generates a SHA256 hash for a virtual key.
|
|
// This is used to detect changes to virtual keys between config.json and database.
|
|
// Skips: ID (primary key), CreatedAt, UpdatedAt, and relationship objects (Team, Customer, Budget, RateLimit)
|
|
func GenerateVirtualKeyHash(vk tables.TableVirtualKey) (string, error) {
|
|
hash := sha256.New()
|
|
// Hash Name
|
|
hash.Write([]byte(vk.Name))
|
|
// Hash Description
|
|
hash.Write([]byte(vk.Description))
|
|
// Hash Value
|
|
hash.Write([]byte(vk.Value))
|
|
// Hash IsActive
|
|
if vk.IsActive {
|
|
hash.Write([]byte("isActive:true"))
|
|
} else {
|
|
hash.Write([]byte("isActive:false"))
|
|
}
|
|
// Hash TeamID
|
|
if vk.TeamID != nil {
|
|
hash.Write([]byte("teamID:" + *vk.TeamID))
|
|
}
|
|
// Hash CustomerID
|
|
if vk.CustomerID != nil {
|
|
hash.Write([]byte("customerID:" + *vk.CustomerID))
|
|
}
|
|
// Hash RateLimitID
|
|
if vk.RateLimitID != nil {
|
|
hash.Write([]byte("rateLimitID:" + *vk.RateLimitID))
|
|
}
|
|
// Hash ProviderConfigs
|
|
if len(vk.ProviderConfigs) > 0 {
|
|
// Copy and sort provider configs for deterministic hashing
|
|
sortedProviderConfigs := make([]tables.TableVirtualKeyProviderConfig, len(vk.ProviderConfigs))
|
|
copy(sortedProviderConfigs, vk.ProviderConfigs)
|
|
sort.Slice(sortedProviderConfigs, func(i, j int) bool {
|
|
if sortedProviderConfigs[i].Provider != sortedProviderConfigs[j].Provider {
|
|
return sortedProviderConfigs[i].Provider < sortedProviderConfigs[j].Provider
|
|
}
|
|
ri, rj := "", ""
|
|
if sortedProviderConfigs[i].RateLimitID != nil {
|
|
ri = *sortedProviderConfigs[i].RateLimitID
|
|
}
|
|
if sortedProviderConfigs[j].RateLimitID != nil {
|
|
rj = *sortedProviderConfigs[j].RateLimitID
|
|
}
|
|
if ri != rj {
|
|
return ri < rj
|
|
}
|
|
wi, wj := sortedProviderConfigs[i].Weight, sortedProviderConfigs[j].Weight
|
|
if (wi == nil) != (wj == nil) {
|
|
return wi == nil
|
|
}
|
|
if wi != nil && wj != nil && *wi != *wj {
|
|
return *wi < *wj
|
|
}
|
|
return false
|
|
})
|
|
// Filter out provider configs that are not available
|
|
providerConfigsForHash := make([]VirtualKeyProviderConfigHashInput, len(sortedProviderConfigs))
|
|
for i, pc := range sortedProviderConfigs {
|
|
// Sort key IDs for deterministic hashing
|
|
keyIDs := make([]string, len(pc.Keys))
|
|
for j, k := range pc.Keys {
|
|
keyIDs[j] = k.KeyID
|
|
}
|
|
sort.Strings(keyIDs)
|
|
|
|
// Sort allowed models for deterministic hashing
|
|
sortedAllowedModels := make([]string, len(pc.AllowedModels))
|
|
copy(sortedAllowedModels, pc.AllowedModels)
|
|
sort.Strings(sortedAllowedModels)
|
|
providerConfigsForHash[i] = VirtualKeyProviderConfigHashInput{
|
|
Provider: pc.Provider,
|
|
Weight: pc.Weight,
|
|
AllowedModels: sortedAllowedModels,
|
|
RateLimitID: pc.RateLimitID,
|
|
KeyIDs: keyIDs,
|
|
}
|
|
}
|
|
data, err := sonic.Marshal(providerConfigsForHash)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
}
|
|
// Hash MCPConfigs
|
|
if len(vk.MCPConfigs) > 0 {
|
|
// Copy and sort MCP configs for deterministic hashing
|
|
sortedMCPConfigs := make([]tables.TableVirtualKeyMCPConfig, len(vk.MCPConfigs))
|
|
copy(sortedMCPConfigs, vk.MCPConfigs)
|
|
sort.Slice(sortedMCPConfigs, func(i, j int) bool {
|
|
return sortedMCPConfigs[i].MCPClientID < sortedMCPConfigs[j].MCPClientID
|
|
})
|
|
|
|
mcpConfigsForHash := make([]VirtualKeyMCPConfigHashInput, len(sortedMCPConfigs))
|
|
for i, mc := range sortedMCPConfigs {
|
|
// Sort tools for deterministic hashing
|
|
sortedTools := make([]string, len(mc.ToolsToExecute))
|
|
copy(sortedTools, mc.ToolsToExecute)
|
|
sort.Strings(sortedTools)
|
|
|
|
mcpConfigsForHash[i] = VirtualKeyMCPConfigHashInput{
|
|
MCPClientID: mc.MCPClientID,
|
|
ToolsToExecute: sortedTools,
|
|
}
|
|
}
|
|
data, err := sonic.Marshal(mcpConfigsForHash)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
}
|
|
return hex.EncodeToString(hash.Sum(nil)), nil
|
|
}
|
|
|
|
// GenerateBudgetHash generates a SHA256 hash for a budget.
|
|
// This is used to detect changes to budgets between config.json and database.
|
|
// Skips: LastReset, CurrentUsage, CreatedAt, UpdatedAt (dynamic fields)
|
|
func GenerateBudgetHash(b tables.TableBudget) (string, error) {
|
|
hash := sha256.New()
|
|
|
|
// Hash ID
|
|
hash.Write([]byte(b.ID))
|
|
|
|
// Hash MaxLimit
|
|
data, err := sonic.Marshal(b.MaxLimit)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
|
|
// Hash ResetDuration
|
|
hash.Write([]byte(b.ResetDuration))
|
|
|
|
return hex.EncodeToString(hash.Sum(nil)), nil
|
|
}
|
|
|
|
// GenerateRateLimitHash generates a SHA256 hash for a rate limit.
|
|
// This is used to detect changes to rate limits between config.json and database.
|
|
// Skips: CurrentUsage, LastReset, CreatedAt, UpdatedAt (dynamic fields)
|
|
func GenerateRateLimitHash(rl tables.TableRateLimit) (string, error) {
|
|
hash := sha256.New()
|
|
|
|
// Hash ID
|
|
hash.Write([]byte(rl.ID))
|
|
|
|
// Hash TokenMaxLimit
|
|
if rl.TokenMaxLimit != nil {
|
|
data, err := sonic.Marshal(*rl.TokenMaxLimit)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
}
|
|
|
|
// Hash TokenResetDuration
|
|
if rl.TokenResetDuration != nil {
|
|
hash.Write([]byte(*rl.TokenResetDuration))
|
|
}
|
|
|
|
// Hash RequestMaxLimit
|
|
if rl.RequestMaxLimit != nil {
|
|
data, err := sonic.Marshal(*rl.RequestMaxLimit)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
}
|
|
|
|
// Hash RequestResetDuration
|
|
if rl.RequestResetDuration != nil {
|
|
hash.Write([]byte(*rl.RequestResetDuration))
|
|
}
|
|
|
|
return hex.EncodeToString(hash.Sum(nil)), nil
|
|
}
|
|
|
|
// GenerateCustomerHash generates a SHA256 hash for a customer.
|
|
// This is used to detect changes to customers between config.json and database.
|
|
// Skips: CreatedAt, UpdatedAt, and relationship objects (dynamic fields)
|
|
func GenerateCustomerHash(c tables.TableCustomer) (string, error) {
|
|
hash := sha256.New()
|
|
|
|
// Hash ID
|
|
hash.Write([]byte(c.ID))
|
|
|
|
// Hash Name
|
|
hash.Write([]byte(c.Name))
|
|
|
|
// Hash BudgetID
|
|
if c.BudgetID != nil {
|
|
hash.Write([]byte("budgetID:" + *c.BudgetID))
|
|
}
|
|
|
|
return hex.EncodeToString(hash.Sum(nil)), nil
|
|
}
|
|
|
|
// GenerateTeamHash generates a SHA256 hash for a team.
|
|
// This is used to detect changes to teams between config.json and database.
|
|
// Skips: CreatedAt, UpdatedAt, and relationship objects (dynamic fields)
|
|
func GenerateTeamHash(t tables.TableTeam) (string, error) {
|
|
hash := sha256.New()
|
|
|
|
// Hash ID
|
|
hash.Write([]byte(t.ID))
|
|
|
|
// Hash Name
|
|
hash.Write([]byte(t.Name))
|
|
|
|
// Hash CustomerID
|
|
if t.CustomerID != nil {
|
|
hash.Write([]byte("customerID:" + *t.CustomerID))
|
|
}
|
|
|
|
// Hash sorted budget IDs — team now owns multiple budgets; slice order must not
|
|
// affect the hash, otherwise config-sync would flip the hash on every reload.
|
|
if len(t.Budgets) > 0 {
|
|
ids := make([]string, len(t.Budgets))
|
|
for i, b := range t.Budgets {
|
|
ids[i] = b.ID
|
|
}
|
|
sort.Strings(ids)
|
|
hash.Write([]byte("budgetIDs:"))
|
|
for i, id := range ids {
|
|
if i > 0 {
|
|
hash.Write([]byte{','})
|
|
}
|
|
hash.Write([]byte(id))
|
|
}
|
|
}
|
|
|
|
// Hash Profile - use Profile if set, else marshal ParsedProfile
|
|
// (Profile has json:"-" so when loading from JSON, only ParsedProfile is populated)
|
|
// Use encoding/json for consistency with BeforeSave hook serialization
|
|
if t.Profile != nil {
|
|
hash.Write([]byte("profile:" + *t.Profile))
|
|
} else if t.ParsedProfile != nil {
|
|
data, err := json.Marshal(t.ParsedProfile)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write([]byte("profile:" + string(data)))
|
|
}
|
|
|
|
// Hash Config - use Config if set, else marshal ParsedConfig
|
|
// Use encoding/json for consistency with BeforeSave hook serialization
|
|
if t.Config != nil {
|
|
hash.Write([]byte("config:" + *t.Config))
|
|
} else if t.ParsedConfig != nil {
|
|
data, err := json.Marshal(t.ParsedConfig)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write([]byte("config:" + string(data)))
|
|
}
|
|
|
|
// Hash Claims - use Claims if set, else marshal ParsedClaims
|
|
// Use encoding/json for consistency with BeforeSave hook serialization
|
|
if t.Claims != nil {
|
|
hash.Write([]byte("claims:" + *t.Claims))
|
|
} else if t.ParsedClaims != nil {
|
|
data, err := json.Marshal(t.ParsedClaims)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write([]byte("claims:" + string(data)))
|
|
}
|
|
|
|
return hex.EncodeToString(hash.Sum(nil)), nil
|
|
}
|
|
|
|
// GenerateRoutingRuleHash generates a SHA256 hash for a routing rule.
|
|
// This is used to detect changes to routing rules between config.json and database.
|
|
// routingTargetHashPayload is a canonical struct for hashing a routing target.
|
|
// Used to ensure deterministic hashes regardless of slice order.
|
|
// Fields use plain string (not *string) so nil and "" both marshal to "" and produce the same hash.
|
|
type routingTargetHashPayload struct {
|
|
Provider string `json:"provider"`
|
|
Model string `json:"model"`
|
|
KeyID string `json:"key_id"`
|
|
Weight float64 `json:"weight"`
|
|
}
|
|
|
|
// derefStr returns the dereferenced value of s, or "" if s is nil.
|
|
func derefStr(s *string) string {
|
|
if s == nil {
|
|
return ""
|
|
}
|
|
return *s
|
|
}
|
|
|
|
// Skips: CreatedAt, UpdatedAt (dynamic fields)
|
|
func GenerateRoutingRuleHash(r tables.TableRoutingRule) (string, error) {
|
|
hash := sha256.New()
|
|
|
|
// Hash ID
|
|
hash.Write([]byte(r.ID))
|
|
|
|
// Hash Name
|
|
hash.Write([]byte(r.Name))
|
|
|
|
// Hash Description
|
|
hash.Write([]byte(r.Description))
|
|
|
|
// Hash Enabled
|
|
if r.Enabled {
|
|
hash.Write([]byte("enabled:true"))
|
|
} else {
|
|
hash.Write([]byte("enabled:false"))
|
|
}
|
|
|
|
// Hash CelExpression
|
|
hash.Write([]byte(r.CelExpression))
|
|
|
|
// Hash Targets: sort by canonical marshaled payload for determinism, then hash each target as a single blob
|
|
targets := make([]tables.TableRoutingTarget, len(r.Targets))
|
|
copy(targets, r.Targets)
|
|
sort.Slice(targets, func(i, j int) bool {
|
|
pi := routingTargetHashPayload{Provider: derefStr(targets[i].Provider), Model: derefStr(targets[i].Model), KeyID: derefStr(targets[i].KeyID), Weight: targets[i].Weight}
|
|
pj := routingTargetHashPayload{Provider: derefStr(targets[j].Provider), Model: derefStr(targets[j].Model), KeyID: derefStr(targets[j].KeyID), Weight: targets[j].Weight}
|
|
di, err := sonic.Marshal(pi)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
dj, err := sonic.Marshal(pj)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return string(di) < string(dj)
|
|
})
|
|
for _, t := range targets {
|
|
payload := routingTargetHashPayload{Provider: derefStr(t.Provider), Model: derefStr(t.Model), KeyID: derefStr(t.KeyID), Weight: t.Weight}
|
|
data, err := sonic.Marshal(payload)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
}
|
|
|
|
// Hash Fallbacks: use DB string when set, else marshal ParsedFallbacks (config-origin)
|
|
if r.Fallbacks != nil {
|
|
hash.Write([]byte(*r.Fallbacks))
|
|
} else if len(r.ParsedFallbacks) > 0 {
|
|
data, err := sonic.Marshal(r.ParsedFallbacks)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
}
|
|
|
|
// Hash Query: use raw string when set, else marshal ParsedQuery (config-origin)
|
|
// Use OrderedMap's deterministic marshalling to ensure consistent hashes across runs
|
|
if r.Query != nil {
|
|
hash.Write([]byte(*r.Query))
|
|
} else if len(r.ParsedQuery) > 0 {
|
|
// Convert map to OrderedMap and use sorted marshalling for deterministic hashes
|
|
orderedMap := schemas.OrderedMapFromMap(r.ParsedQuery)
|
|
data, err := orderedMap.MarshalSorted()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
}
|
|
|
|
// Hash ChainRule
|
|
if r.ChainRule {
|
|
hash.Write([]byte("chain_rule:true"))
|
|
} else {
|
|
hash.Write([]byte("chain_rule:false"))
|
|
}
|
|
|
|
// Hash Scope
|
|
hash.Write([]byte(r.Scope))
|
|
|
|
// Hash ScopeID (nil = global)
|
|
scopeID := ""
|
|
if r.ScopeID != nil {
|
|
scopeID = *r.ScopeID
|
|
}
|
|
hash.Write([]byte(scopeID))
|
|
|
|
// Hash Priority
|
|
hash.Write([]byte(strconv.Itoa(r.Priority)))
|
|
|
|
return hex.EncodeToString(hash.Sum(nil)), nil
|
|
}
|
|
|
|
// GeneratePricingOverrideHash generates a SHA256 hash for a pricing override.
|
|
// Skips: CreatedAt, UpdatedAt, ConfigHash (dynamic/meta fields).
|
|
func GeneratePricingOverrideHash(p tables.TablePricingOverride) (string, error) {
|
|
hash := sha256.New()
|
|
hash.Write([]byte(p.ID))
|
|
hash.Write([]byte(p.Name))
|
|
hash.Write([]byte(p.ScopeKind))
|
|
hash.Write([]byte(derefStr(p.VirtualKeyID)))
|
|
hash.Write([]byte(derefStr(p.ProviderID)))
|
|
hash.Write([]byte(derefStr(p.ProviderKeyID)))
|
|
hash.Write([]byte(p.MatchType))
|
|
hash.Write([]byte(p.Pattern))
|
|
hash.Write([]byte(p.RequestTypesJSON))
|
|
hash.Write([]byte(p.PricingPatchJSON))
|
|
return hex.EncodeToString(hash.Sum(nil)), nil
|
|
}
|
|
|
|
// GenerateMCPClientHash generates a SHA256 hash for an MCP client.
|
|
// This is used to detect changes to MCP clients between config.json and database.
|
|
// Skips: ID (autoIncrement), CreatedAt, UpdatedAt (dynamic fields)
|
|
func GenerateMCPClientHash(m tables.TableMCPClient) (string, error) {
|
|
hash := sha256.New()
|
|
|
|
// Hash ClientID
|
|
hash.Write([]byte(m.ClientID))
|
|
|
|
// Hash Name
|
|
hash.Write([]byte(m.Name))
|
|
|
|
// Hash ConnectionType
|
|
hash.Write([]byte(m.ConnectionType))
|
|
|
|
// Hash ConnectionString
|
|
if m.ConnectionString != nil {
|
|
if m.ConnectionString.IsFromEnv() {
|
|
hash.Write([]byte(m.ConnectionString.EnvVar))
|
|
} else {
|
|
hash.Write([]byte(m.ConnectionString.Val))
|
|
}
|
|
}
|
|
|
|
// Hash StdioConfig
|
|
if m.StdioConfig != nil {
|
|
data, err := sonic.Marshal(m.StdioConfig)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
}
|
|
|
|
// Hash ToolsToExecute (sorted for deterministic hashing)
|
|
if len(m.ToolsToExecute) > 0 {
|
|
sortedTools := make([]string, len(m.ToolsToExecute))
|
|
copy(sortedTools, m.ToolsToExecute)
|
|
sort.Strings(sortedTools)
|
|
data, err := sonic.Marshal(sortedTools)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
}
|
|
|
|
// Hash Headers (sorted for deterministic hashing)
|
|
if len(m.Headers) > 0 {
|
|
keys := make([]string, 0, len(m.Headers))
|
|
for k := range m.Headers {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, k := range keys {
|
|
val := m.Headers[k]
|
|
if val.FromEnv {
|
|
hash.Write([]byte(k + ":env:" + val.EnvVar))
|
|
} else {
|
|
hash.Write([]byte(k + ":val:" + val.Val))
|
|
}
|
|
}
|
|
}
|
|
return hex.EncodeToString(hash.Sum(nil)), nil
|
|
}
|
|
|
|
// GeneratePluginHash generates a SHA256 hash for a plugin.
|
|
// This is used to detect changes to plugins between config.json and database.
|
|
// Skips: ID (autoIncrement), CreatedAt, UpdatedAt, IsCustom (dynamic fields)
|
|
func GeneratePluginHash(p tables.TablePlugin) (string, error) {
|
|
hash := sha256.New()
|
|
|
|
// Hash Name
|
|
hash.Write([]byte(p.Name))
|
|
|
|
// Hash Enabled
|
|
if p.Enabled {
|
|
hash.Write([]byte("enabled:true"))
|
|
} else {
|
|
hash.Write([]byte("enabled:false"))
|
|
}
|
|
|
|
// Hash Path
|
|
if p.Path != nil {
|
|
hash.Write([]byte("path:" + *p.Path))
|
|
}
|
|
|
|
// Hash Config (use ConfigJSON for consistent hashing)
|
|
// Normalize: nil and empty map ({}) are treated as equivalent (no hash contribution)
|
|
if p.ConfigJSON != "" && p.ConfigJSON != "{}" {
|
|
hash.Write([]byte(p.ConfigJSON))
|
|
} else if p.Config != nil {
|
|
// Check if Config is a non-empty map before hashing
|
|
// Use encoding/json for consistency with BeforeSave hook serialization
|
|
data, err := json.Marshal(p.Config)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
// Only hash if it's not an empty object
|
|
if string(data) != "{}" && string(data) != "null" {
|
|
hash.Write(data)
|
|
}
|
|
}
|
|
|
|
// Hash Version
|
|
data, err := sonic.Marshal(p.Version)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash.Write(data)
|
|
|
|
return hex.EncodeToString(hash.Sum(nil)), nil
|
|
}
|
|
|
|
// AuthConfig represents configured auth config for Bifrost dashboard
|
|
type AuthConfig struct {
|
|
AdminUserName *schemas.EnvVar `json:"admin_username"`
|
|
AdminPassword *schemas.EnvVar `json:"admin_password"`
|
|
IsEnabled bool `json:"is_enabled"`
|
|
DisableAuthOnInference bool `json:"disable_auth_on_inference"`
|
|
}
|
|
|
|
// ConfigMap maps provider names to their configurations.
|
|
type ConfigMap map[schemas.ModelProvider]ProviderConfig
|
|
|
|
// GovernanceConfig contains governance entities loaded from the config store or
|
|
// reconciled from config.json.
|
|
type GovernanceConfig struct {
|
|
VirtualKeys []tables.TableVirtualKey `json:"virtual_keys"`
|
|
Teams []tables.TableTeam `json:"teams"`
|
|
Customers []tables.TableCustomer `json:"customers"`
|
|
Budgets []tables.TableBudget `json:"budgets"`
|
|
RateLimits []tables.TableRateLimit `json:"rate_limits"`
|
|
ModelConfigs []tables.TableModelConfig `json:"model_configs"`
|
|
Providers []tables.TableProvider `json:"providers"`
|
|
RoutingRules []tables.TableRoutingRule `json:"routing_rules"`
|
|
PricingOverrides []tables.TablePricingOverride `json:"pricing_overrides,omitempty"`
|
|
AuthConfig *AuthConfig `json:"auth_config,omitempty"`
|
|
}
|