Changeset 0.22.8 (#150)

This commit is contained in:
2026-03-04 16:06:12 +00:00
parent 389e47b0f9
commit 7e26a2a261
114 changed files with 3700 additions and 7572 deletions

View File

@@ -41,7 +41,7 @@ type completionRequest struct {
Temperature *float64 `json:"temperature,omitempty"`
TopP *float64 `json:"top_p,omitempty"`
Stream *bool `json:"stream,omitempty"`
AttachmentIDs []string `json:"attachment_ids,omitempty"` // staged attachment UUIDs to include in request
FileIDs []string `json:"file_ids,omitempty"` // staged file UUIDs to include in request
DisabledTools []string `json:"disabled_tools,omitempty"` // tool names to exclude from this request
}
@@ -50,7 +50,7 @@ type CompletionHandler struct {
vault *crypto.KeyResolver
stores store.Stores
hub *events.Hub // WebSocket hub for browser tool bridge
objStore storage.ObjectStore // file storage for attachment content (nil = disabled)
objStore storage.ObjectStore // file storage for uploaded/generated content (nil = disabled)
embedder *knowledge.Embedder // for memory semantic recall (v0.18.0)
health HealthRecorder // provider health tracking (v0.22.0, nil = disabled)
healthStore HealthStatusQuerier // health status queries for routing (v0.22.2, nil = disabled)
@@ -341,14 +341,14 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
// Resolve capabilities early — needed for vision gating below
caps := h.getModelCapabilities(c, model, configID)
// Build user message — multimodal if attachments are present
// Build user message — multimodal if files are present
userMsg := providers.Message{
Role: "user",
Content: req.Content,
}
if len(req.AttachmentIDs) > 0 && h.objStore != nil {
parts, augContent, _, err := h.buildMultimodalParts(c, channelID, req.Content, req.AttachmentIDs, caps)
if len(req.FileIDs) > 0 && h.objStore != nil {
parts, augContent, _, err := h.buildMultimodalParts(c, channelID, req.Content, req.FileIDs, caps)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
@@ -370,11 +370,11 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
log.Printf("Failed to persist user message: %v", err)
}
// Link attachments to the persisted message
if msgID != "" && len(req.AttachmentIDs) > 0 {
for _, attID := range req.AttachmentIDs {
if err := h.stores.Attachments.SetMessageID(c.Request.Context(), attID, msgID); err != nil {
log.Printf("Failed to link attachment %s to message %s: %v", attID, msgID, err)
// Link files to the persisted message
if msgID != "" && len(req.FileIDs) > 0 {
for _, fID := range req.FileIDs {
if err := h.stores.Files.SetMessageID(c.Request.Context(), fID, msgID); err != nil {
log.Printf("Failed to link file %s to message %s: %v", fID, msgID, err)
}
}
}
@@ -865,13 +865,13 @@ func (h *CompletionHandler) getModelCapabilities(c *gin.Context, model, apiConfi
}
// ── Multimodal Assembly ─────────────────────
// Builds content parts from attachments for the user message.
// Builds content parts from files for the user message.
//
// Returns:
// - parts: ContentParts array (non-nil only when images are present)
// - augContent: enriched text content with document context (for doc-only case)
// - validIDs: attachment IDs that were successfully processed
// - error: if any attachment is invalid or vision is needed but missing
// - validIDs: file IDs that were successfully processed
// - error: if any file is invalid or vision is needed but missing
//
// Rules:
// - Images → base64 data URI (requires vision capability)
@@ -882,7 +882,7 @@ func (h *CompletionHandler) getModelCapabilities(c *gin.Context, model, apiConfi
func (h *CompletionHandler) buildMultimodalParts(
c *gin.Context,
channelID, textContent string,
attachmentIDs []string,
fileIDs []string,
caps models.ModelCapabilities,
) ([]providers.ContentPart, string, []string, error) {
@@ -893,39 +893,39 @@ func (h *CompletionHandler) buildMultimodalParts(
var validIDs []string
hasImage := false
for _, attID := range attachmentIDs {
att, err := h.stores.Attachments.GetByID(c.Request.Context(), attID)
for _, fileID := range fileIDs {
att, err := h.stores.Files.GetByID(c.Request.Context(), fileID)
if err != nil {
return nil, "", nil, fmt.Errorf("attachment %s not found", attID)
return nil, "", nil, fmt.Errorf("file %s not found", fileID)
}
// Security: verify attachment belongs to this channel
// Security: verify file belongs to this channel
if att.ChannelID != channelID {
return nil, "", nil, fmt.Errorf("attachment %s does not belong to this channel", attID)
return nil, "", nil, fmt.Errorf("file %s does not belong to this channel", fileID)
}
if isImageContentType(att.ContentType) {
// Vision gating: reject images if model lacks vision
if !caps.Vision {
return nil, "", nil, fmt.Errorf("model does not support image input; remove image attachments or choose a vision-capable model")
return nil, "", nil, fmt.Errorf("model does not support image input; remove image files or choose a vision-capable model")
}
// Read image from storage, base64 encode, build data URI
reader, _, _, err := h.objStore.Get(c.Request.Context(), att.StorageKey)
if err != nil {
log.Printf("Failed to read attachment %s from storage: %v", attID, err)
log.Printf("Failed to read file %s from storage: %v", fileID, err)
parts = append(parts, providers.ContentPart{
Type: "text",
Text: fmt.Sprintf("[Image: %s — failed to read from storage]", att.Filename),
})
validIDs = append(validIDs, attID)
validIDs = append(validIDs, fileID)
continue
}
data, err := io.ReadAll(reader)
reader.Close()
if err != nil {
log.Printf("Failed to read attachment %s bytes: %v", attID, err)
validIDs = append(validIDs, attID)
log.Printf("Failed to read file %s bytes: %v", fileID, err)
validIDs = append(validIDs, fileID)
continue
}
@@ -964,7 +964,7 @@ func (h *CompletionHandler) buildMultimodalParts(
docTexts = append(docTexts, placeholder)
}
validIDs = append(validIDs, attID)
validIDs = append(validIDs, fileID)
}
// If images present → use ContentParts (multimodal array)