Changeset 0.22.0.1 (#94)

This commit is contained in:
2026-03-02 01:55:19 +00:00
parent 12e03cec8b
commit 06c4e2a5a1
33 changed files with 1929 additions and 418 deletions

View File

@@ -10,6 +10,7 @@ import (
"log"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
@@ -50,6 +51,15 @@ type CompletionHandler struct {
hub *events.Hub // WebSocket hub for browser tool bridge
objStore storage.ObjectStore // file storage for attachment content (nil = disabled)
embedder *knowledge.Embedder // for memory semantic recall (v0.18.0)
health HealthRecorder // provider health tracking (v0.22.0, nil = disabled)
}
// HealthRecorder is the interface for recording provider call outcomes.
// Matches health.Accumulator methods without importing the package.
type HealthRecorder interface {
RecordSuccess(providerConfigID string, latencyMs int)
RecordError(providerConfigID string, latencyMs int, errMsg string)
RecordTimeout(providerConfigID string, latencyMs int, errMsg string)
}
// NewCompletionHandler creates a new handler.
@@ -57,6 +67,11 @@ func NewCompletionHandler(vault *crypto.KeyResolver, stores store.Stores, hub *e
return &CompletionHandler{vault: vault, stores: stores, hub: hub, objStore: objStore, embedder: embedder}
}
// SetHealthRecorder attaches the provider health accumulator.
func (h *CompletionHandler) SetHealthRecorder(hr HealthRecorder) {
h.health = hr
}
// ── Chat Completion ─────────────────────────
// POST /api/v1/chat/completions
//
@@ -371,7 +386,7 @@ func (h *CompletionHandler) multiModelStream(
}
// Stream this model's response using the shared streaming core
result := streamModelResponse(c, provider, providerCfg, &provReq, model, target.DisplayName, userID, channelID, personaID, workspaceID, h.hub)
result := streamModelResponse(c, provider, providerCfg, &provReq, model, target.DisplayName, userID, channelID, personaID, workspaceID, configID, h.hub, h.health)
// Persist assistant message with model attribution
if result.Content != "" {
@@ -536,7 +551,7 @@ func (h *CompletionHandler) streamCompletion(
req providers.CompletionRequest,
channelID, userID, model, configID, providerScope, personaID, workspaceID string,
) {
result := streamWithToolLoop(c, provider, cfg, &req, model, userID, channelID, personaID, workspaceID, h.hub)
result := streamWithToolLoop(c, provider, cfg, &req, model, userID, channelID, personaID, workspaceID, configID, h.hub, h.health)
// Persist assistant response
if result.Content != "" {
@@ -566,7 +581,9 @@ func (h *CompletionHandler) syncCompletion(
var allToolActivity []map[string]interface{}
for iteration := 0; iteration < maxToolIterations; iteration++ {
callStart := time.Now()
resp, err := provider.ChatCompletion(c.Request.Context(), cfg, req)
h.recordHealth(configID, callStart, err)
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": "provider error: " + err.Error()})
return
@@ -1175,3 +1192,21 @@ func calcCost(tokens int, pricePerM *float64) *float64 {
cost := float64(tokens) * *pricePerM / 1_000_000.0
return &cost
}
// recordHealth records provider call outcome for health tracking (v0.22.0).
func (h *CompletionHandler) recordHealth(configID string, start time.Time, err error) {
if h.health == nil || configID == "" {
return
}
latencyMs := int(time.Since(start).Milliseconds())
if err != nil {
errMsg := err.Error()
if strings.Contains(errMsg, "context deadline exceeded") || strings.Contains(errMsg, "timeout") {
h.health.RecordTimeout(configID, latencyMs, errMsg)
} else {
h.health.RecordError(configID, latencyMs, errMsg)
}
} else {
h.health.RecordSuccess(configID, latencyMs)
}
}