Changeset 0.5.0 (#35)

This commit is contained in:
2026-02-19 15:03:20 +00:00
parent a93a6b9635
commit 30d0c11219
65 changed files with 5345 additions and 8070 deletions

View File

@@ -2,6 +2,7 @@ package handlers
import (
"database/sql"
"encoding/json"
"fmt"
"io"
"log"
@@ -61,7 +62,7 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
}
// Resolve provider config
providerCfg, providerID, model, err := h.resolveConfig(userID, req)
providerCfg, providerID, model, configID, err := h.resolveConfig(userID, req)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
@@ -96,9 +97,17 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
Model: model,
Messages: messages,
}
// Resolve capabilities for this model — auto-set defaults
caps := h.getModelCapabilities(model, configID)
if req.MaxTokens > 0 {
provReq.MaxTokens = req.MaxTokens
} else {
// ResolveMaxOutput checks: caps → known models → context/8 → 4096
provReq.MaxTokens = providers.ResolveMaxOutput(model, caps)
}
if req.Temperature != nil {
provReq.Temperature = req.Temperature
}
@@ -227,10 +236,42 @@ func (h *CompletionHandler) syncCompletion(
})
}
// ── Model Capabilities ──────────────────────
// getModelCapabilities looks up capabilities from model_configs DB,
// then overlays with known model defaults and heuristic detection.
func (h *CompletionHandler) getModelCapabilities(model, apiConfigID string) providers.ModelCapabilities {
// Start with known table or heuristic
// Start with known model table or heuristics
caps, found := providers.LookupKnownModel(model)
if !found {
caps = providers.InferCapabilities(model)
}
// Overlay with DB-stored capabilities (from provider sync or admin edit — authoritative)
var capsJSON []byte
err := database.DB.QueryRow(`
SELECT capabilities FROM model_configs
WHERE model_id = $1 AND api_config_id = $2
`, model, apiConfigID).Scan(&capsJSON)
if err == nil && capsJSON != nil {
var dbCaps providers.ModelCapabilities
if err := json.Unmarshal(capsJSON, &dbCaps); err == nil {
if dbCaps.HasProviderData() {
// DB has real provider data — use as authoritative, fill gaps
caps = providers.MergeCapabilities(dbCaps, model)
}
}
}
return caps
}
// ── 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, error) {
func (h *CompletionHandler) resolveConfig(userID string, req completionRequest) (providers.ProviderConfig, string, string, string, error) {
var configID string
// 1. Explicit config from request
@@ -249,33 +290,34 @@ func (h *CompletionHandler) resolveConfig(userID string, req completionRequest)
}
}
// 3. User's first active config
// 3. User's first active config (personal first, then global)
if configID == "" {
err := database.DB.QueryRow(`
SELECT id FROM api_configs
WHERE (user_id = $1 OR user_id IS NULL) AND is_active = true
WHERE (user_id = $1 OR is_global = true) AND is_active = true
ORDER BY user_id NULLS LAST, created_at ASC
LIMIT 1
`, userID).Scan(&configID)
if err != nil {
return providers.ProviderConfig{}, "", "", fmt.Errorf("no API config found — add one at /api-configs")
return providers.ProviderConfig{}, "", "", "", fmt.Errorf("no API config found — add one at /api-configs")
}
}
// Load the config
// Load the config including custom headers and provider settings
var providerID, endpoint string
var apiKey, modelDefault *string
var customHeadersJSON, providerSettingsJSON []byte
err := database.DB.QueryRow(`
SELECT provider, endpoint, api_key_encrypted, model_default
SELECT provider, endpoint, api_key_encrypted, model_default, custom_headers, provider_settings
FROM api_configs
WHERE id = $1 AND (user_id = $2 OR user_id IS NULL) AND is_active = true
`, configID, userID).Scan(&providerID, &endpoint, &apiKey, &modelDefault)
WHERE id = $1 AND (user_id = $2 OR is_global = true) AND is_active = true
`, configID, userID).Scan(&providerID, &endpoint, &apiKey, &modelDefault, &customHeadersJSON, &providerSettingsJSON)
if err == sql.ErrNoRows {
return providers.ProviderConfig{}, "", "", fmt.Errorf("API config not found or not accessible")
return providers.ProviderConfig{}, "", "", "", fmt.Errorf("API config not found or not accessible")
}
if err != nil {
return providers.ProviderConfig{}, "", "", fmt.Errorf("failed to load API config: %w", err)
return providers.ProviderConfig{}, "", "", "", fmt.Errorf("failed to load API config: %w", err)
}
// Resolve model: request > config default
@@ -284,7 +326,7 @@ func (h *CompletionHandler) resolveConfig(userID string, req completionRequest)
model = *modelDefault
}
if model == "" {
return providers.ProviderConfig{}, "", "", fmt.Errorf("no model specified and no default model in config")
return providers.ProviderConfig{}, "", "", "", fmt.Errorf("no model specified and no default model in config")
}
key := ""
@@ -292,10 +334,24 @@ func (h *CompletionHandler) resolveConfig(userID string, req completionRequest)
key = *apiKey
}
// Parse custom headers
customHeaders := make(map[string]string)
if customHeadersJSON != nil {
_ = json.Unmarshal(customHeadersJSON, &customHeaders)
}
// Parse provider-specific settings
providerSettings := make(map[string]interface{})
if providerSettingsJSON != nil {
_ = json.Unmarshal(providerSettingsJSON, &providerSettings)
}
return providers.ProviderConfig{
Endpoint: endpoint,
APIKey: key,
}, providerID, model, nil
Endpoint: endpoint,
APIKey: key,
CustomHeaders: customHeaders,
Settings: providerSettings,
}, providerID, model, configID, nil
}
// ── Conversation Loader ─────────────────────