diff --git a/framework/configstore/migrations.go b/framework/configstore/migrations.go
index bced2ec3f5..42ca3f3c8d 100644
--- a/framework/configstore/migrations.go
+++ b/framework/configstore/migrations.go
@@ -359,6 +359,9 @@ func triggerMigrations(ctx context.Context, db *gorm.DB) error {
if err := migrationAddRoutingChainMaxDepthColumn(ctx, db); err != nil {
return err
}
+ if err := migrationAddPromptVariablesColumns(ctx, db); err != nil {
+ return err
+ }
if err := migrationAddModelCapabilityColumns(ctx, db); err != nil {
return err
}
@@ -5466,6 +5469,50 @@ func migrationAddOpenAIConfigJSONColumn(ctx context.Context, db *gorm.DB) error
return nil
}
+// migrationAddPromptVariablesColumns adds variables_json column to prompt_sessions and prompt_versions
+func migrationAddPromptVariablesColumns(ctx context.Context, db *gorm.DB) error {
+ m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
+ ID: "add_prompt_variables_columns",
+ Migrate: func(tx *gorm.DB) error {
+ tx = tx.WithContext(ctx)
+ migrator := tx.Migrator()
+
+ if !migrator.HasColumn(&tables.TablePromptSession{}, "variables_json") {
+ if err := migrator.AddColumn(&tables.TablePromptSession{}, "VariablesJSON"); err != nil {
+ return fmt.Errorf("failed to add variables_json column to prompt_sessions: %w", err)
+ }
+ }
+
+ if !migrator.HasColumn(&tables.TablePromptVersion{}, "variables_json") {
+ if err := migrator.AddColumn(&tables.TablePromptVersion{}, "VariablesJSON"); err != nil {
+ return fmt.Errorf("failed to add variables_json column to prompt_versions: %w", err)
+ }
+ }
+
+ return nil
+ },
+ Rollback: func(tx *gorm.DB) error {
+ tx = tx.WithContext(ctx)
+ migrator := tx.Migrator()
+ if migrator.HasColumn(&tables.TablePromptSession{}, "variables_json") {
+ if err := migrator.DropColumn(&tables.TablePromptSession{}, "variables_json"); err != nil {
+ return err
+ }
+ }
+ if migrator.HasColumn(&tables.TablePromptVersion{}, "variables_json") {
+ if err := migrator.DropColumn(&tables.TablePromptVersion{}, "variables_json"); err != nil {
+ return err
+ }
+ }
+ return nil
+ },
+ }})
+ if err := m.Migrate(); err != nil {
+ return fmt.Errorf("error while running add_prompt_variables_columns migration: %s", err.Error())
+ }
+ return nil
+}
+
// migrationAddKeyBlacklistedModelsJSONColumn adds blacklisted_models_json to config_keys
// for per-key model deny lists (JSON array of model ids, default []).
func migrationAddKeyBlacklistedModelsJSONColumn(ctx context.Context, db *gorm.DB) error {
diff --git a/framework/configstore/tables/promptSessions.go b/framework/configstore/tables/promptSessions.go
index df96db09aa..4618704ebf 100644
--- a/framework/configstore/tables/promptSessions.go
+++ b/framework/configstore/tables/promptSessions.go
@@ -22,6 +22,8 @@ type TablePromptSession struct {
ModelParams ModelParams `gorm:"-" json:"model_params"`
Provider string `gorm:"type:varchar(100)" json:"provider"`
Model string `gorm:"type:varchar(100)" json:"model"`
+ VariablesJSON *string `gorm:"type:text;column:variables_json" json:"-"`
+ Variables PromptVariables `gorm:"-" json:"variables,omitempty"` // {key: value} map for Jinja2 variables
CreatedAt time.Time `gorm:"not null" json:"created_at"`
UpdatedAt time.Time `gorm:"not null" json:"updated_at"`
@@ -40,6 +42,17 @@ func (s *TablePromptSession) BeforeSave(tx *gorm.DB) error {
}
paramsStr := string(data)
s.ModelParamsJSON = ¶msStr
+
+ if s.Variables != nil {
+ varsData, err := json.Marshal(s.Variables)
+ if err != nil {
+ return err
+ }
+ varsStr := string(varsData)
+ s.VariablesJSON = &varsStr
+ } else {
+ s.VariablesJSON = nil
+ }
return nil
}
@@ -52,6 +65,15 @@ func (s *TablePromptSession) AfterFind(tx *gorm.DB) error {
return err
}
}
+ if s.VariablesJSON != nil && *s.VariablesJSON != "" {
+ var vars PromptVariables
+ if err := json.Unmarshal([]byte(*s.VariablesJSON), &vars); err != nil {
+ return err
+ }
+ s.Variables = vars
+ } else {
+ s.Variables = nil
+ }
return nil
}
diff --git a/framework/configstore/tables/promptVersions.go b/framework/configstore/tables/promptVersions.go
index 1703be58f3..ca9e41e039 100644
--- a/framework/configstore/tables/promptVersions.go
+++ b/framework/configstore/tables/promptVersions.go
@@ -12,17 +12,19 @@ import (
// TablePromptVersion represents an immutable version of a prompt
// Once created, a version cannot be modified - to make changes, create a new version
type TablePromptVersion struct {
- ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
- PromptID string `gorm:"type:varchar(36);not null;index;uniqueIndex:idx_prompt_version" json:"prompt_id"`
- Prompt *TablePrompt `gorm:"foreignKey:PromptID" json:"prompt,omitempty"`
- VersionNumber int `gorm:"not null;uniqueIndex:idx_prompt_version" json:"version_number"`
- CommitMessage string `gorm:"type:text" json:"commit_message"`
- ModelParamsJSON *string `gorm:"type:text;column:model_params_json" json:"-"`
- ModelParams ModelParams `gorm:"-" json:"model_params"`
- Provider string `gorm:"type:varchar(100)" json:"provider"`
- Model string `gorm:"type:varchar(100)" json:"model"`
- IsLatest bool `gorm:"not null;default:false" json:"is_latest"`
- CreatedAt time.Time `gorm:"not null" json:"created_at"`
+ ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
+ PromptID string `gorm:"type:varchar(36);not null;index;uniqueIndex:idx_prompt_version" json:"prompt_id"`
+ Prompt *TablePrompt `gorm:"foreignKey:PromptID" json:"prompt,omitempty"`
+ VersionNumber int `gorm:"not null;uniqueIndex:idx_prompt_version" json:"version_number"`
+ CommitMessage string `gorm:"type:text" json:"commit_message"`
+ ModelParamsJSON *string `gorm:"type:text;column:model_params_json" json:"-"`
+ ModelParams ModelParams `gorm:"-" json:"model_params"`
+ Provider string `gorm:"type:varchar(100)" json:"provider"`
+ Model string `gorm:"type:varchar(100)" json:"model"`
+ VariablesJSON *string `gorm:"type:text;column:variables_json" json:"-"`
+ Variables PromptVariables `gorm:"-" json:"variables,omitempty"` // {key: value} map for Jinja2 variables
+ IsLatest bool `gorm:"not null;default:false" json:"is_latest"`
+ CreatedAt time.Time `gorm:"not null" json:"created_at"`
// No UpdatedAt - versions are immutable
// Relationships
@@ -36,6 +38,10 @@ func (TablePromptVersion) TableName() string { return "prompt_versions" }
// so that any provider-specific params (response_format, seed, logprobs, etc.) are preserved.
type ModelParams map[string]interface{}
+// PromptVariables represents a map of Jinja2 variable names to their values.
+// Sessions store full {key: value} pairs; versions store {key: ""} (keys only).
+type PromptVariables map[string]string
+
// BeforeSave GORM hook to serialize JSON fields
func (v *TablePromptVersion) BeforeSave(tx *gorm.DB) error {
if v.ModelParams != nil {
@@ -46,6 +52,14 @@ func (v *TablePromptVersion) BeforeSave(tx *gorm.DB) error {
paramsStr := string(data)
v.ModelParamsJSON = ¶msStr
}
+ if v.Variables != nil {
+ varsData, err := json.Marshal(v.Variables)
+ if err != nil {
+ return err
+ }
+ varsStr := string(varsData)
+ v.VariablesJSON = &varsStr
+ }
return nil
}
@@ -58,6 +72,11 @@ func (v *TablePromptVersion) AfterFind(tx *gorm.DB) error {
return err
}
}
+ if v.VariablesJSON != nil && *v.VariablesJSON != "" {
+ if err := json.Unmarshal([]byte(*v.VariablesJSON), &v.Variables); err != nil {
+ return err
+ }
+ }
return nil
}
diff --git a/transports/bifrost-http/handlers/prompts.go b/transports/bifrost-http/handlers/prompts.go
index e5b96f0c38..c5e5737f0f 100644
--- a/transports/bifrost-http/handlers/prompts.go
+++ b/transports/bifrost-http/handlers/prompts.go
@@ -137,25 +137,28 @@ type CreateVersionRequest struct {
ModelParams tables.ModelParams `json:"model_params"`
Provider string `json:"provider"`
Model string `json:"model"`
+ Variables tables.PromptVariables `json:"variables,omitempty"`
}
// CreateSessionRequest represents the request body for creating a session
type CreateSessionRequest struct {
- Name string `json:"name"`
- VersionID *uint `json:"version_id,omitempty"`
- Messages []tables.PromptMessage `json:"messages,omitempty"`
- ModelParams tables.ModelParams `json:"model_params"`
- Provider string `json:"provider"`
- Model string `json:"model"`
+ Name string `json:"name"`
+ VersionID *uint `json:"version_id,omitempty"`
+ Messages []tables.PromptMessage `json:"messages,omitempty"`
+ ModelParams tables.ModelParams `json:"model_params"`
+ Provider string `json:"provider"`
+ Model string `json:"model"`
+ Variables tables.PromptVariables `json:"variables,omitempty"`
}
// UpdateSessionRequest represents the request body for updating a session
type UpdateSessionRequest struct {
- Name string `json:"name"`
- Messages []tables.PromptMessage `json:"messages"`
- ModelParams tables.ModelParams `json:"model_params"`
- Provider string `json:"provider"`
- Model string `json:"model"`
+ Name string `json:"name"`
+ Messages []tables.PromptMessage `json:"messages"`
+ ModelParams tables.ModelParams `json:"model_params"`
+ Provider string `json:"provider"`
+ Model string `json:"model"`
+ Variables tables.PromptVariables `json:"variables,omitempty"`
}
// RenameSessionRequest represents the request body for renaming a session
@@ -631,12 +634,22 @@ func (h *PromptsHandler) createVersion(ctx *fasthttp.RequestCtx) {
})
}
+ // Strip variable values — versions store keys only; values live in sessions
+ var versionVars tables.PromptVariables
+ if len(req.Variables) > 0 {
+ versionVars = make(tables.PromptVariables, len(req.Variables))
+ for key := range req.Variables {
+ versionVars[key] = ""
+ }
+ }
+
version := &tables.TablePromptVersion{
PromptID: promptID,
CommitMessage: req.CommitMessage,
ModelParams: req.ModelParams,
Provider: req.Provider,
Model: req.Model,
+ Variables: versionVars,
Messages: messages,
}
@@ -835,6 +848,7 @@ func (h *PromptsHandler) createSession(ctx *fasthttp.RequestCtx) {
ModelParams: req.ModelParams,
Provider: req.Provider,
Model: req.Model,
+ Variables: req.Variables,
Messages: messages,
}
@@ -890,6 +904,7 @@ func (h *PromptsHandler) updateSession(ctx *fasthttp.RequestCtx) {
session.ModelParams = req.ModelParams
session.Provider = req.Provider
session.Model = req.Model
+ session.Variables = req.Variables
// Update messages
var messages []tables.TablePromptSessionMessage
@@ -1066,12 +1081,22 @@ func (h *PromptsHandler) commitSession(ctx *fasthttp.RequestCtx) {
return
}
+ // Copy variable keys from session with empty values for the version
+ var versionVars tables.PromptVariables
+ if len(session.Variables) > 0 {
+ versionVars = make(tables.PromptVariables, len(session.Variables))
+ for key := range session.Variables {
+ versionVars[key] = ""
+ }
+ }
+
version := &tables.TablePromptVersion{
PromptID: session.PromptID,
CommitMessage: req.CommitMessage,
ModelParams: session.ModelParams,
Provider: session.Provider,
Model: session.Model,
+ Variables: versionVars,
Messages: messages,
}
diff --git a/ui/app/_fallbacks/enterprise/components/prompt-deployments/promptDeploymentView.tsx b/ui/app/_fallbacks/enterprise/components/prompt-deployments/promptDeploymentView.tsx
index 8f392276e8..628e44439a 100644
--- a/ui/app/_fallbacks/enterprise/components/prompt-deployments/promptDeploymentView.tsx
+++ b/ui/app/_fallbacks/enterprise/components/prompt-deployments/promptDeploymentView.tsx
@@ -3,10 +3,11 @@ import ContactUsView from "../views/contactUsView";
export default function PromptDeploymentView() {
return (
-
+
}
+ align="top"
+ className="justify-start gap-3 rounded-md border p-4"
+ icon={}
title="Unlock prompt deployments for better prompt versioning and A/B testing."
description="This feature is a part of the Bifrost enterprise license. We would love to know more about your use case and how we can help you."
readmeLink="https://docs.getbifrost.ai/enterprise/prompt-deployments"
diff --git a/ui/app/globals.css b/ui/app/globals.css
index 2a043151fb..96650d22a5 100644
--- a/ui/app/globals.css
+++ b/ui/app/globals.css
@@ -3,6 +3,7 @@
@source "../app/**/*.tsx";
@source "../node_modules/streamdown/dist/*.js";
+@source "../../../bifrost-enterprise/ui/**/*.tsx";
@custom-variant dark (&:is(.dark *));
diff --git a/ui/app/workspace/mcp-logs/views/bifrost.code-workspace b/ui/app/workspace/mcp-logs/views/bifrost.code-workspace
new file mode 100644
index 0000000000..8c5511061b
--- /dev/null
+++ b/ui/app/workspace/mcp-logs/views/bifrost.code-workspace
@@ -0,0 +1,11 @@
+{
+ "folders": [
+ {
+ "path": "../../../../.."
+ },
+ {
+ "path": "../../../../../../bifrost-enterprise"
+ }
+ ],
+ "settings": {}
+}
\ No newline at end of file
diff --git a/ui/app/workspace/prompt-repo/deployments/page.tsx b/ui/app/workspace/prompt-repo/deployments/page.tsx
deleted file mode 100644
index 26adfda683..0000000000
--- a/ui/app/workspace/prompt-repo/deployments/page.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import PromptDeploymentView from "@enterprise/components/prompt-deployments/promptDeploymentView";
-
-export default function PromptDeploymentsPage() {
- return (
-
-
-
- );
-}
\ No newline at end of file
diff --git a/ui/app/workspace/prompt-repo/page.tsx b/ui/app/workspace/prompt-repo/page.tsx
new file mode 100644
index 0000000000..935bff8f59
--- /dev/null
+++ b/ui/app/workspace/prompt-repo/page.tsx
@@ -0,0 +1,12 @@
+"use client";
+
+import { PromptProvider } from "@/components/prompts/context";
+import PromptsView from "@/components/prompts/promptsView";
+
+export default function PromptRepoPage() {
+ return (
+
+
+
+ );
+}
diff --git a/ui/app/workspace/prompt-repo/prompts/page.tsx b/ui/app/workspace/prompt-repo/prompts/page.tsx
index fa96bb86f7..b2d6f8a7db 100644
--- a/ui/app/workspace/prompt-repo/prompts/page.tsx
+++ b/ui/app/workspace/prompt-repo/prompts/page.tsx
@@ -1,10 +1,9 @@
-import { PromptProvider } from "@/components/prompts/context";
-import PromptsView from "@/components/prompts/promptsView";
+"use client";
+
+import { redirect, useSearchParams } from "next/navigation";
export default function PromptsPage() {
- return (
-
-
-
- );
-}
\ No newline at end of file
+ const searchParams = useSearchParams();
+ const queryString = searchParams.toString();
+ redirect(`/workspace/prompt-repo${queryString ? `?${queryString}` : ""}`);
+}
diff --git a/ui/app/workspace/routing-rules/components/celBuilder/celRuleBuilder.tsx b/ui/app/workspace/routing-rules/components/celBuilder/celRuleBuilder.tsx
index 4660699352..29f034d5d9 100644
--- a/ui/app/workspace/routing-rules/components/celBuilder/celRuleBuilder.tsx
+++ b/ui/app/workspace/routing-rules/components/celBuilder/celRuleBuilder.tsx
@@ -1,25 +1,16 @@
/**
- * CEL Rule Builder Component for Routing Rules
- * Visual query builder for creating CEL expressions
+ * CEL Rule Builder for Routing Rules
+ * Thin wrapper around the reusable CELRuleBuilder with routing-specific config
*/
-import { Button } from "@/components/ui/button";
-import { Label } from "@/components/ui/label";
-import { Textarea } from "@/components/ui/textarea";
+"use client";
+
+import { CELRuleBuilder as BaseCELRuleBuilder } from "@/components/ui/custom/celBuilder";
import { getRoutingFields } from "@/lib/config/celFieldsRouting";
import { celOperatorsRouting } from "@/lib/config/celOperatorsRouting";
-import { convertRuleGroupToCEL } from "@/lib/utils/celConverterRouting";
-import { useCopyToClipboard } from "@/hooks/useCopyToClipboard";
-import { Check, Copy, Loader2 } from "lucide-react";
-import { useEffect, useMemo, useRef, useState } from "react";
-import { Field, QueryBuilder, RuleGroupType } from "react-querybuilder";
-import "react-querybuilder/dist/query-builder.css";
-import { ActionButton } from "./actionButton";
-import { CombinatorSelector } from "./combinatorSelector";
-import { FieldSelector } from "./fieldSelector";
-import { OperatorSelector } from "./operatorSelector";
-import { QueryBuilderWrapper } from "./queryBuilderWrapper";
-import { ValueEditor } from "./valueEditor";
+import { convertRuleGroupToCEL, validateRegexPattern } from "@/lib/utils/celConverterRouting";
+import { useMemo } from "react";
+import { RuleGroupType } from "react-querybuilder";
interface CELRuleBuilderProps {
onChange?: (celExpression: string, query: RuleGroupType) => void;
@@ -30,11 +21,6 @@ interface CELRuleBuilderProps {
isLoading?: boolean;
}
-const defaultQuery: RuleGroupType = {
- combinator: "and",
- rules: [],
-};
-
export function CELRuleBuilder({
onChange,
initialQuery,
@@ -43,96 +29,18 @@ export function CELRuleBuilder({
isLoading = false,
allowCustomModels = false,
}: CELRuleBuilderProps) {
- const [query, setQuery] = useState(initialQuery || defaultQuery);
- const [celExpression, setCelExpression] = useState("");
- const { copy, copied } = useCopyToClipboard();
- const onChangeRef = useRef(onChange);
-
- // Keep ref updated so the query effect always invokes the latest callback
- useEffect(() => {
- onChangeRef.current = onChange;
- }, [onChange]);
-
- // Generate fields with dynamic providers and models
- const fields = useMemo(() => {
- const celFields = getRoutingFields(providers, models);
- return celFields.map((field) => ({
- ...field,
- value: field.name,
- })) as Field[];
- }, [providers, models]);
-
- useEffect(() => {
- const expression = convertRuleGroupToCEL(query);
- setCelExpression(expression);
- onChangeRef.current?.(expression, query);
- }, [query]);
-
- const handleCopy = () => copy(celExpression);
-
- // Show loading state
- if (isLoading) {
- return (
-