Changeset 0.22.8 (#150)
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user