Changeset 0.6.0 (#36)

This commit is contained in:
2026-02-20 22:24:47 +00:00
parent 30d0c11219
commit 925b70f98c
34 changed files with 2575 additions and 637 deletions

View File

@@ -17,7 +17,8 @@ import (
// ── Request Types ───────────────────────────
type completionRequest struct {
ChatID string `json:"chat_id" binding:"required"`
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"`
APIConfigID string `json:"api_config_id,omitempty"`
@@ -41,7 +42,7 @@ func NewCompletionHandler() *CompletionHandler {
// Flow:
// 1. Validate request, verify chat ownership
// 2. Resolve api_config (from request, chat, or user default)
// 3. Load conversation history from chat_messages
// 3. Load conversation history from messages
// 4. Persist user message
// 5. Call provider (stream or non-stream)
// 6. Stream SSE to client / return JSON
@@ -54,15 +55,25 @@ 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
}
userID := getUserID(c)
// Verify chat ownership
if !userOwnsChat(c, req.ChatID, userID) {
// Verify channel ownership
if !userOwnsChannel(c, channelID, userID) {
return
}
// Resolve provider config
providerCfg, providerID, model, configID, err := h.resolveConfig(userID, req)
providerCfg, providerID, model, configID, err := h.resolveConfig(userID, channelID, req)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
@@ -75,7 +86,7 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
}
// Load conversation history
messages, err := h.loadConversation(req.ChatID)
messages, err := h.loadConversation(channelID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load conversation"})
return
@@ -88,7 +99,7 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
})
// Persist user message
if err := h.persistMessage(req.ChatID, "user", req.Content, "", 0, 0); err != nil {
if err := h.persistMessage(channelID, "user", req.Content, "", 0, 0); err != nil {
log.Printf("Failed to persist user message: %v", err)
}
@@ -122,9 +133,9 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
}
if stream {
h.streamCompletion(c, provider, providerCfg, provReq, req.ChatID, model)
h.streamCompletion(c, provider, providerCfg, provReq, channelID, model)
} else {
h.syncCompletion(c, provider, providerCfg, provReq, req.ChatID, model)
h.syncCompletion(c, provider, providerCfg, provReq, channelID, model)
}
}
@@ -135,7 +146,7 @@ func (h *CompletionHandler) streamCompletion(
provider providers.Provider,
cfg providers.ProviderConfig,
req providers.CompletionRequest,
chatID, model string,
channelID, model string,
) {
ch, err := provider.StreamCompletion(c.Request.Context(), cfg, req)
if err != nil {
@@ -190,7 +201,7 @@ func (h *CompletionHandler) streamCompletion(
// Persist assistant response
if fullContent != "" {
if err := h.persistMessage(chatID, "assistant", fullContent, model, 0, 0); err != nil {
if err := h.persistMessage(channelID, "assistant", fullContent, model, 0, 0); err != nil {
log.Printf("Failed to persist assistant message: %v", err)
}
}
@@ -203,7 +214,7 @@ func (h *CompletionHandler) syncCompletion(
provider providers.Provider,
cfg providers.ProviderConfig,
req providers.CompletionRequest,
chatID, model string,
channelID, model string,
) {
resp, err := provider.ChatCompletion(c.Request.Context(), cfg, req)
if err != nil {
@@ -212,7 +223,7 @@ func (h *CompletionHandler) syncCompletion(
}
// Persist assistant response
if err := h.persistMessage(chatID, "assistant", resp.Content, model, resp.InputTokens, resp.OutputTokens); err != nil {
if err := h.persistMessage(channelID, "assistant", resp.Content, model, resp.InputTokens, resp.OutputTokens); err != nil {
log.Printf("Failed to persist assistant message: %v", err)
}
@@ -271,7 +282,7 @@ func (h *CompletionHandler) getModelCapabilities(model, apiConfigID string) prov
// ── Config Resolution ───────────────────────
// Priority: request.api_config_id → chat.api_config_id → user's first active config
func (h *CompletionHandler) resolveConfig(userID string, req completionRequest) (providers.ProviderConfig, string, string, string, error) {
func (h *CompletionHandler) resolveConfig(userID string, channelID string, req completionRequest) (providers.ProviderConfig, string, string, string, error) {
var configID string
// 1. Explicit config from request
@@ -279,14 +290,14 @@ func (h *CompletionHandler) resolveConfig(userID string, req completionRequest)
configID = req.APIConfigID
}
// 2. Config from chat
// 2. Config from channel
if configID == "" {
var chatConfigID *string
var channelConfigID *string
err := database.DB.QueryRow(
`SELECT api_config_id FROM chats WHERE id = $1`, req.ChatID,
).Scan(&chatConfigID)
if err == nil && chatConfigID != nil {
configID = *chatConfigID
`SELECT api_config_id FROM channels WHERE id = $1`, channelID,
).Scan(&channelConfigID)
if err == nil && channelConfigID != nil {
configID = *channelConfigID
}
}
@@ -294,7 +305,7 @@ func (h *CompletionHandler) resolveConfig(userID string, req completionRequest)
if configID == "" {
err := database.DB.QueryRow(`
SELECT id FROM api_configs
WHERE (user_id = $1 OR is_global = true) AND is_active = true
WHERE (user_id = $1 OR is_global = true OR user_id IS NULL) AND is_active = true
ORDER BY user_id NULLS LAST, created_at ASC
LIMIT 1
`, userID).Scan(&configID)
@@ -310,7 +321,7 @@ func (h *CompletionHandler) resolveConfig(userID string, req completionRequest)
err := database.DB.QueryRow(`
SELECT provider, endpoint, api_key_encrypted, model_default, custom_headers, provider_settings
FROM api_configs
WHERE id = $1 AND (user_id = $2 OR is_global = true) AND is_active = true
WHERE id = $1 AND (user_id = $2 OR is_global = true OR user_id IS NULL) AND is_active = true
`, configID, userID).Scan(&providerID, &endpoint, &apiKey, &modelDefault, &customHeadersJSON, &providerSettingsJSON)
if err == sql.ErrNoRows {
@@ -356,11 +367,11 @@ func (h *CompletionHandler) resolveConfig(userID string, req completionRequest)
// ── Conversation Loader ─────────────────────
func (h *CompletionHandler) loadConversation(chatID string) ([]providers.Message, error) {
// Load system prompt from chat
func (h *CompletionHandler) loadConversation(channelID string) ([]providers.Message, error) {
// Load system prompt from channel
var systemPrompt *string
_ = database.DB.QueryRow(
`SELECT system_prompt FROM chats WHERE id = $1`, chatID,
`SELECT system_prompt FROM channels WHERE id = $1`, channelID,
).Scan(&systemPrompt)
messages := make([]providers.Message, 0)
@@ -374,10 +385,10 @@ func (h *CompletionHandler) loadConversation(chatID string) ([]providers.Message
// Load message history (oldest first)
rows, err := database.DB.Query(`
SELECT role, content FROM chat_messages
WHERE chat_id = $1
SELECT role, content FROM messages
WHERE channel_id = $1
ORDER BY created_at ASC
`, chatID)
`, channelID)
if err != nil {
return nil, err
}
@@ -396,22 +407,42 @@ func (h *CompletionHandler) loadConversation(chatID string) ([]providers.Message
// ── Message Persistence ─────────────────────
func (h *CompletionHandler) persistMessage(chatID, role, content, model string, inputTokens, outputTokens int) error {
func (h *CompletionHandler) persistMessage(channelID, role, content, model string, inputTokens, outputTokens int) error {
var tokensUsed *int
if inputTokens > 0 || outputTokens > 0 {
total := inputTokens + outputTokens
tokensUsed = &total
}
// Find current leaf for parent_id
var parentID *string
_ = database.DB.QueryRow(
`SELECT id FROM messages WHERE channel_id = $1 ORDER BY created_at DESC LIMIT 1`,
channelID,
).Scan(&parentID)
participantType := "user"
participantID := channelID // will be overridden below
if role == "assistant" {
participantType = "model"
participantID = model
} else {
// Get channel owner as participant_id for user messages
var ownerID string
if err := database.DB.QueryRow(`SELECT user_id FROM channels WHERE id = $1`, channelID).Scan(&ownerID); err == nil {
participantID = ownerID
}
}
_, err := database.DB.Exec(`
INSERT INTO chat_messages (chat_id, role, content, model, tokens_used)
VALUES ($1, $2, $3, NULLIF($4, ''), $5)
`, chatID, role, content, model, tokensUsed)
INSERT INTO messages (channel_id, role, content, model, tokens_used, parent_id, participant_type, participant_id)
VALUES ($1, $2, $3, NULLIF($4, ''), $5, $6, $7, $8)
`, channelID, role, content, model, tokensUsed, parentID, participantType, participantID)
if err != nil {
return err
}
// Touch chat updated_at
_, _ = database.DB.Exec(`UPDATE chats SET updated_at = NOW() WHERE id = $1`, chatID)
// Touch channel updated_at
_, _ = database.DB.Exec(`UPDATE channels SET updated_at = NOW() WHERE id = $1`, channelID)
return nil
}