Changeset 0.22.7 (#149)
This commit is contained in:
@@ -32,12 +32,11 @@ import (
|
||||
// ── Request Types ───────────────────────────
|
||||
|
||||
type completionRequest struct {
|
||||
ChannelID string `json:"channel_id"` // preferred; validated manually below
|
||||
ChatID string `json:"chat_id"` // deprecated alias — maps to channel_id
|
||||
Content string `json:"content" binding:"required"`
|
||||
Model string `json:"model,omitempty"`
|
||||
PresetID string `json:"preset_id,omitempty"` // if set, unwraps preset → base model + config
|
||||
APIConfigID string `json:"provider_config_id,omitempty"`
|
||||
ChannelID string `json:"channel_id"`
|
||||
Content string `json:"content" binding:"required"`
|
||||
Model string `json:"model,omitempty"`
|
||||
PersonaID string `json:"persona_id,omitempty"`
|
||||
ProviderConfigID string `json:"provider_config_id,omitempty"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"`
|
||||
Temperature *float64 `json:"temperature,omitempty"`
|
||||
TopP *float64 `json:"top_p,omitempty"`
|
||||
@@ -194,11 +193,7 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Support chat_id as alias during frontend transition
|
||||
channelID := req.ChannelID
|
||||
if channelID == "" {
|
||||
channelID = req.ChatID
|
||||
}
|
||||
if channelID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "channel_id is required"})
|
||||
return
|
||||
@@ -211,60 +206,60 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// ── Preset unwrap: preset overrides defaults, explicit request fields win ──
|
||||
var presetSystemPrompt string
|
||||
// ── Persona unwrap: persona overrides defaults, explicit request fields win ──
|
||||
var personaSystemPrompt string
|
||||
var personaID string // tracks active persona for KB scoping
|
||||
var presetThinkingBudget *int // persona-level thinking budget for hook injection (v0.22.1)
|
||||
if req.PresetID != "" {
|
||||
preset := ResolvePreset(h.stores, req.PresetID, userID)
|
||||
if preset == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "preset not found or not accessible"})
|
||||
var personaThinkingBudget *int // persona-level thinking budget for hook injection (v0.22.1)
|
||||
if req.PersonaID != "" {
|
||||
persona := ResolvePersona(h.stores, req.PersonaID, userID)
|
||||
if persona == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "persona not found or not accessible"})
|
||||
return
|
||||
}
|
||||
personaID = preset.ID
|
||||
// Preset provides defaults; explicit request fields take priority
|
||||
personaID = persona.ID
|
||||
// Persona provides defaults; explicit request fields take priority
|
||||
if req.Model == "" {
|
||||
req.Model = preset.BaseModelID
|
||||
req.Model = persona.BaseModelID
|
||||
}
|
||||
if req.APIConfigID == "" && preset.ProviderConfigID != nil {
|
||||
req.APIConfigID = *preset.ProviderConfigID
|
||||
if req.ProviderConfigID == "" && persona.ProviderConfigID != nil {
|
||||
req.ProviderConfigID = *persona.ProviderConfigID
|
||||
}
|
||||
if req.Temperature == nil && preset.Temperature != nil {
|
||||
req.Temperature = preset.Temperature
|
||||
if req.Temperature == nil && persona.Temperature != nil {
|
||||
req.Temperature = persona.Temperature
|
||||
}
|
||||
if req.MaxTokens == 0 && preset.MaxTokens != nil {
|
||||
req.MaxTokens = *preset.MaxTokens
|
||||
if req.MaxTokens == 0 && persona.MaxTokens != nil {
|
||||
req.MaxTokens = *persona.MaxTokens
|
||||
}
|
||||
if preset.SystemPrompt != "" {
|
||||
presetSystemPrompt = preset.SystemPrompt
|
||||
if persona.SystemPrompt != "" {
|
||||
personaSystemPrompt = persona.SystemPrompt
|
||||
}
|
||||
presetThinkingBudget = preset.ThinkingBudget
|
||||
personaThinkingBudget = persona.ThinkingBudget
|
||||
}
|
||||
|
||||
// ── Project persona fallback (v0.19.2): if no explicit preset, check project ──
|
||||
if req.PresetID == "" && h.stores.Projects != nil {
|
||||
// ── Project persona fallback (v0.19.2): if no explicit persona, check project ──
|
||||
if req.PersonaID == "" && h.stores.Projects != nil {
|
||||
projID, _ := h.stores.Projects.GetProjectIDForChannel(context.Background(), channelID)
|
||||
if projID != "" {
|
||||
if proj, err := h.stores.Projects.GetByID(context.Background(), projID); err == nil {
|
||||
if pid, ok := proj.Settings["persona_id"].(string); ok && pid != "" {
|
||||
if preset := ResolvePreset(h.stores, pid, userID); preset != nil {
|
||||
personaID = preset.ID
|
||||
if persona := ResolvePersona(h.stores, pid, userID); persona != nil {
|
||||
personaID = persona.ID
|
||||
if req.Model == "" {
|
||||
req.Model = preset.BaseModelID
|
||||
req.Model = persona.BaseModelID
|
||||
}
|
||||
if req.APIConfigID == "" && preset.ProviderConfigID != nil {
|
||||
req.APIConfigID = *preset.ProviderConfigID
|
||||
if req.ProviderConfigID == "" && persona.ProviderConfigID != nil {
|
||||
req.ProviderConfigID = *persona.ProviderConfigID
|
||||
}
|
||||
if req.Temperature == nil && preset.Temperature != nil {
|
||||
req.Temperature = preset.Temperature
|
||||
if req.Temperature == nil && persona.Temperature != nil {
|
||||
req.Temperature = persona.Temperature
|
||||
}
|
||||
if req.MaxTokens == 0 && preset.MaxTokens != nil {
|
||||
req.MaxTokens = *preset.MaxTokens
|
||||
if req.MaxTokens == 0 && persona.MaxTokens != nil {
|
||||
req.MaxTokens = *persona.MaxTokens
|
||||
}
|
||||
if preset.SystemPrompt != "" {
|
||||
presetSystemPrompt = preset.SystemPrompt
|
||||
if persona.SystemPrompt != "" {
|
||||
personaSystemPrompt = persona.SystemPrompt
|
||||
}
|
||||
presetThinkingBudget = preset.ThinkingBudget
|
||||
personaThinkingBudget = persona.ThinkingBudget
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -287,7 +282,7 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
|
||||
routingDecision = dec
|
||||
if winConfigID != configID {
|
||||
// Routing selected a different provider — reload its credentials
|
||||
req.APIConfigID = winConfigID
|
||||
req.ProviderConfigID = winConfigID
|
||||
providerCfg2, providerID2, model2, configID2, providerScope2, err := h.resolveConfig(userID, channelID, req)
|
||||
if err == nil {
|
||||
providerCfg = providerCfg2
|
||||
@@ -307,12 +302,12 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
|
||||
// ── Inject persona-level thinking budget into provider settings (v0.22.1) ──
|
||||
// When a persona specifies a thinking budget, promote it into provider
|
||||
// settings so hooks can activate extended thinking automatically.
|
||||
if presetThinkingBudget != nil && *presetThinkingBudget > 0 {
|
||||
if personaThinkingBudget != nil && *personaThinkingBudget > 0 {
|
||||
if providerCfg.Settings == nil {
|
||||
providerCfg.Settings = make(map[string]interface{})
|
||||
}
|
||||
providerCfg.Settings["extended_thinking"] = true
|
||||
providerCfg.Settings["thinking_budget"] = *presetThinkingBudget
|
||||
providerCfg.Settings["thinking_budget"] = *personaThinkingBudget
|
||||
}
|
||||
|
||||
// ── Team policy: require_private_providers ──
|
||||
@@ -337,7 +332,7 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Load conversation history
|
||||
messages, err := h.loadConversation(channelID, userID, presetSystemPrompt, personaID)
|
||||
messages, err := h.loadConversation(channelID, userID, personaSystemPrompt, personaID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load conversation"})
|
||||
return
|
||||
@@ -396,7 +391,7 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
|
||||
parsed := mentions.Parse(req.Content, roster)
|
||||
targets := mentions.ResolvedModels(parsed)
|
||||
if len(targets) > 1 {
|
||||
h.multiModelStream(c, targets, messages, channelID, userID, personaID, presetSystemPrompt, workspaceID, req)
|
||||
h.multiModelStream(c, targets, messages, channelID, userID, personaID, personaSystemPrompt, workspaceID, req)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -449,7 +444,7 @@ func (h *CompletionHandler) multiModelStream(
|
||||
c *gin.Context,
|
||||
targets []models.ChannelModel,
|
||||
messages []providers.Message,
|
||||
channelID, userID, personaID, presetSystemPrompt, workspaceID string,
|
||||
channelID, userID, personaID, personaSystemPrompt, workspaceID string,
|
||||
req completionRequest,
|
||||
) {
|
||||
// Set SSE headers once for the entire multi-model stream
|
||||
@@ -485,7 +480,7 @@ func (h *CompletionHandler) multiModelStream(
|
||||
targetReq := req
|
||||
targetReq.Model = target.ModelID
|
||||
if target.ProviderConfigID != "" {
|
||||
targetReq.APIConfigID = target.ProviderConfigID
|
||||
targetReq.ProviderConfigID = target.ProviderConfigID
|
||||
}
|
||||
|
||||
providerCfg, providerID, model, configID, providerScope, err := h.resolveConfig(userID, channelID, targetReq)
|
||||
@@ -998,8 +993,8 @@ func (h *CompletionHandler) resolveConfig(userID string, channelID string, req c
|
||||
var configID string
|
||||
|
||||
// 1. Explicit config from request
|
||||
if req.APIConfigID != "" {
|
||||
configID = req.APIConfigID
|
||||
if req.ProviderConfigID != "" {
|
||||
configID = req.ProviderConfigID
|
||||
}
|
||||
|
||||
// 2. Config from channel
|
||||
@@ -1115,7 +1110,7 @@ func (h *CompletionHandler) resolveConfig(userID string, channelID string, req c
|
||||
//
|
||||
// Summary-aware: if the path contains a summary node (metadata.type = "summary"),
|
||||
// messages before it are replaced by the summary content as a system message.
|
||||
func (h *CompletionHandler) loadConversation(channelID, userID, presetSystemPrompt, personaID string) ([]providers.Message, error) {
|
||||
func (h *CompletionHandler) loadConversation(channelID, userID, personaSystemPrompt, personaID string) ([]providers.Message, error) {
|
||||
messages := make([]providers.Message, 0)
|
||||
|
||||
// ── Admin system prompt (always injected first, no opt out) ──
|
||||
@@ -1129,17 +1124,17 @@ func (h *CompletionHandler) loadConversation(channelID, userID, presetSystemProm
|
||||
}
|
||||
}
|
||||
|
||||
// ── User/preset system prompt (appended after admin prompt) ──
|
||||
// ── User/persona system prompt (appended after admin prompt) ──
|
||||
var systemPrompt *string
|
||||
_ = database.DB.QueryRow(
|
||||
database.Q(`SELECT system_prompt FROM channels WHERE id = $1`), channelID,
|
||||
).Scan(&systemPrompt)
|
||||
|
||||
// Preset system prompt takes priority; channel system prompt is fallback
|
||||
if presetSystemPrompt != "" {
|
||||
// Persona system prompt takes priority; channel system prompt is fallback
|
||||
if personaSystemPrompt != "" {
|
||||
messages = append(messages, providers.Message{
|
||||
Role: "system",
|
||||
Content: presetSystemPrompt,
|
||||
Content: personaSystemPrompt,
|
||||
})
|
||||
} else if systemPrompt != nil && *systemPrompt != "" {
|
||||
messages = append(messages, providers.Message{
|
||||
|
||||
Reference in New Issue
Block a user