Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/integrations/vector-databases/qdrant.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ store, err := vectorstore.NewVectorStore(context.Background(), vectorConfig, log

</Tabs>

### Configuration Reference

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `host` | string | — | Qdrant server host. Supports `env.VAR_NAME` syntax. **Required.** |
| `port` | integer | `6334` | gRPC port. Bifrost always uses the gRPC interface. |
| `api_key` | string | — | API key for Qdrant Cloud authentication. Supports `env.VAR_NAME` syntax. |
| `use_tls` | boolean | `false` | Enable TLS. Required for Qdrant Cloud. |
| `max_recv_msg_size_mb` | integer | `64` | gRPC max receive message size in MB. Increase when caching large payloads (e.g. base64-encoded image generation responses exceed the default). |

<Note>
Qdrant uses port 6334 for gRPC and port 6333 for REST. Bifrost uses the gRPC port.
</Note>
Expand Down
2 changes: 1 addition & 1 deletion framework/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ require (
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/grpc v1.80.0 // indirect
google.golang.org/grpc v1.80.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/postgres v1.6.0
Expand Down
39 changes: 35 additions & 4 deletions framework/vectorstore/qdrant.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,21 @@ import (
"github.com/google/uuid"
"github.com/maximhq/bifrost/core/schemas"
"github.com/qdrant/go-client/qdrant"
"google.golang.org/grpc"
)

// qdrantMaxRecvMsgSize is the gRPC max receive message size for Qdrant.
// The default 4 MB is too small for payloads that embed large responses
// (e.g. base64-encoded images from image generation providers).
const qdrantMaxRecvMsgSize = 64 * 1024 * 1024 // 64 MB

// QdrantConfig represents the configuration for the Qdrant vector store.
type QdrantConfig struct {
Host schemas.EnvVar `json:"host"` // Qdrant server host - REQUIRED
Port schemas.EnvVar `json:"port"` // Qdrant server port (fallback to 6334 for gRPC)
APIKey schemas.EnvVar `json:"api_key,omitempty"` // API key for authentication - Optional
UseTLS schemas.EnvVar `json:"use_tls,omitempty"` // Use TLS for connection - Optional
Host schemas.EnvVar `json:"host"` // Qdrant server host - REQUIRED
Port schemas.EnvVar `json:"port"` // Qdrant server port (fallback to 6334 for gRPC)
APIKey schemas.EnvVar `json:"api_key,omitempty"` // API key for authentication - Optional
UseTLS schemas.EnvVar `json:"use_tls,omitempty"` // Use TLS for connection - Optional
MaxRecvMsgSizeMB schemas.EnvVar `json:"max_recv_msg_size_mb,omitempty"` // gRPC max receive message size in MB (default: 64). Increase when caching large payloads such as image generation responses.
}

// QdrantStore represents the Qdrant vector store.
Expand All @@ -37,6 +44,22 @@ func (s *QdrantStore) CreateNamespace(ctx context.Context, namespace string, dim
return fmt.Errorf("failed to check collection existence: %w", err)
}

if exists {
info, infoErr := s.client.GetCollectionInfo(ctx, namespace)
if infoErr != nil {
s.logger.Warn(fmt.Sprintf("could not inspect existing collection %q for dimension validation (check skipped): %v", namespace, infoErr))
} else if params := info.GetConfig().GetParams().GetVectorsConfig().GetParams(); params == nil {
// Named-vector collections use GetParamsMap(); Bifrost only creates unnamed vectors so
// this collection was not created by Bifrost. Dimension validation is skipped.
s.logger.Debug(fmt.Sprintf("collection %q uses named vectors — dimension check skipped (Bifrost always creates unnamed vectors)", namespace))
} else {
existingDim := int(params.GetSize())
if existingDim != dimension {
return fmt.Errorf("namespace %q already exists with dimension %d but config requires %d — update vector_store_namespace to a new name or drop the existing collection manually", namespace, existingDim, dimension)
}
}
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Comment thread
TejasGhatte marked this conversation as resolved.

if !exists {
err = s.client.CreateCollection(ctx, &qdrant.CreateCollection{
CollectionName: namespace,
Expand Down Expand Up @@ -354,12 +377,20 @@ func newQdrantStore(ctx context.Context, config *QdrantConfig, logger schemas.Lo
if strings.TrimSpace(config.Host.GetValue()) == "" {
return nil, fmt.Errorf("qdrant host is required")
}
maxRecvMsgSize := qdrantMaxRecvMsgSize
if mb := config.MaxRecvMsgSizeMB.CoerceInt(0); mb > 0 {
maxRecvMsgSize = mb * 1024 * 1024
}

client, err := qdrant.NewClient(&qdrant.Config{
Host: config.Host.GetValue(),
Port: config.Port.CoerceInt(6334),
APIKey: config.APIKey.GetValue(),
UseTLS: config.UseTLS.CoerceBool(false),
SkipCompatibilityCheck: true,
GrpcOptions: []grpc.DialOption{
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxRecvMsgSize)),
},
})
if err != nil {
return nil, fmt.Errorf("failed to create qdrant client: %w", err)
Expand Down
20 changes: 15 additions & 5 deletions framework/vectorstore/redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ type RedisConfig struct {
// Connection pool and timeout settings (passed directly to Redis client).
// All duration fields accept either a Go duration string (e.g. "5s", "500ms",
// "1m30s") or a plain integer nanosecond value for backward compatibility.
PoolSize int `json:"pool_size,omitempty"` // Maximum number of socket connections (optional)
MaxActiveConns int `json:"max_active_conns,omitempty"` // Maximum number of active connections (optional)
MinIdleConns int `json:"min_idle_conns,omitempty"` // Minimum number of idle connections (optional)
MaxIdleConns int `json:"max_idle_conns,omitempty"` // Maximum number of idle connections (optional)
PoolSize int `json:"pool_size,omitempty"` // Maximum number of socket connections (optional)
MaxActiveConns int `json:"max_active_conns,omitempty"` // Maximum number of active connections (optional)
MinIdleConns int `json:"min_idle_conns,omitempty"` // Minimum number of idle connections (optional)
MaxIdleConns int `json:"max_idle_conns,omitempty"` // Maximum number of idle connections (optional)
ConnMaxLifetime schemas.Duration `json:"conn_max_lifetime,omitempty"` // Connection maximum lifetime (optional)
ConnMaxIdleTime schemas.Duration `json:"conn_max_idle_time,omitempty"` // Connection maximum idle time (optional)
DialTimeout schemas.Duration `json:"dial_timeout,omitempty"` // Timeout for socket connection (optional)
Expand Down Expand Up @@ -79,8 +79,18 @@ func (s *RedisStore) CreateNamespace(ctx context.Context, namespace string, dime
// Check if index already exists
infoResult := s.client.Do(ctx, "FT.INFO", namespace)
if infoResult.Err() == nil {
ftInfo, ftInfoErr := s.client.FTInfo(ctx, namespace).Result()
if ftInfoErr != nil {
s.logger.Warn(fmt.Sprintf("could not inspect existing index %q for dimension validation (check skipped): %v", namespace, ftInfoErr))
} else {
for _, attr := range ftInfo.Attributes {
if strings.EqualFold(attr.Type, "VECTOR") && attr.Dim > 0 && attr.Dim != dimension {
return fmt.Errorf("namespace %q already exists with dimension %d but config requires %d — update vector_store_namespace to a new name or drop the existing index manually", namespace, attr.Dim, dimension)
}
}
}
s.cacheNamespaceFieldTypes(namespace, properties)
return nil // Index already exists
return nil // Index already exists with matching dimension
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
if err := infoResult.Err(); err != nil && strings.Contains(strings.ToLower(err.Error()), "unknown command") {
return fmt.Errorf("search module not available: please use Redis Stack or a Valkey bundle with search support (FT.* commands required). original error: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion framework/vectorstore/weaviate.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ func (s *WeaviateStore) CreateNamespace(ctx context.Context, className string, d
}

if exists {
return nil // Schema already exists
return nil
}

// Create properties
Expand Down
17 changes: 11 additions & 6 deletions plugins/semanticcache/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ func (plugin *Plugin) PreLLMHook(ctx *schemas.BifrostContext, req *schemas.Bifro
// semantic-only path — otherwise a misconfigured plugin wastes one
// generateEmbedding round-trip per request before failing downstream.
if !canDoSemanticSearch {
plugin.setZeroVectorIfRequired(state)
plugin.setPlaceholderVectorIfRequired(state)
} else {
shortCircuit, err := plugin.performSemanticSearch(ctx, state, req, cacheKey, paramsHash)
if err != nil {
Expand All @@ -442,7 +442,7 @@ func (plugin *Plugin) PreLLMHook(ctx *schemas.BifrostContext, req *schemas.Bifro
// direct-only entries onto the same point in vector space, so a
// semantic search across cache types under the same cache_key/params
// could surface them. params_hash filtering is the actual isolation.
plugin.setZeroVectorIfRequired(state)
plugin.setPlaceholderVectorIfRequired(state)
}

return req, nil, nil
Expand Down Expand Up @@ -481,13 +481,18 @@ func (plugin *Plugin) resolveCacheTypes(ctx *schemas.BifrostContext) (direct boo
return
}

// setZeroVectorIfRequired writes a zero embedding placeholder when the store
// mandates a vector per entry. See PreLLMHook for the isolation caveat.
func (plugin *Plugin) setZeroVectorIfRequired(state *cacheState) {
// setPlaceholderVectorIfRequired writes a fixed unit-vector placeholder when
// the store mandates a non-zero vector per entry (e.g. Pinecone rejects
// all-zero vectors). The first element is set to 1 so the vector satisfies
// that constraint. All direct-cache entries share the same placeholder, so
// isolation is provided entirely by params_hash filtering — not proximity.
func (plugin *Plugin) setPlaceholderVectorIfRequired(state *cacheState) {
if !plugin.store.RequiresVectors() || plugin.config.Dimension <= 0 {
return
}
state.Embeddings = make([]float32, plugin.config.Dimension)
vec := make([]float32, plugin.config.Dimension)
vec[0] = 1.0
state.Embeddings = vec
}

// PostLLMHook caches the upstream response keyed by the storageID resolved
Expand Down
6 changes: 6 additions & 0 deletions transports/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3255,6 +3255,12 @@
"type": "boolean",
"description": "Use TLS for connection (optional)",
"default": false
},
"max_recv_msg_size_mb": {
"type": "integer",
"description": "gRPC max receive message size in MB (default: 64). Increase when caching large payloads such as base64-encoded image generation responses.",
"minimum": 1,
"default": 64
}
},
"required": ["host"],
Expand Down
Loading