Changeset 0.5.0 (#35)
This commit is contained in:
@@ -525,7 +525,7 @@ func (h *AdminHandler) ListModelConfigs(c *gin.Context) {
|
||||
func (h *AdminHandler) FetchModels(c *gin.Context) {
|
||||
// Load all global api_configs
|
||||
rows, err := database.DB.Query(`
|
||||
SELECT id, provider, endpoint, api_key_encrypted
|
||||
SELECT id, provider, endpoint, api_key_encrypted, custom_headers
|
||||
FROM api_configs
|
||||
WHERE user_id IS NULL AND is_active = true
|
||||
`)
|
||||
@@ -539,6 +539,7 @@ func (h *AdminHandler) FetchModels(c *gin.Context) {
|
||||
ConfigID string `json:"config_id"`
|
||||
Provider string `json:"provider"`
|
||||
Added int `json:"added"`
|
||||
Updated int `json:"updated"`
|
||||
Skipped int `json:"skipped"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
@@ -548,7 +549,8 @@ func (h *AdminHandler) FetchModels(c *gin.Context) {
|
||||
for rows.Next() {
|
||||
var cfgID, providerID, endpoint string
|
||||
var apiKey *string
|
||||
if err := rows.Scan(&cfgID, &providerID, &endpoint, &apiKey); err != nil {
|
||||
var customHeadersJSON []byte
|
||||
if err := rows.Scan(&cfgID, &providerID, &endpoint, &apiKey, &customHeadersJSON); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -563,9 +565,15 @@ func (h *AdminHandler) FetchModels(c *gin.Context) {
|
||||
key = *apiKey
|
||||
}
|
||||
|
||||
customHeaders := make(map[string]string)
|
||||
if customHeadersJSON != nil {
|
||||
_ = json.Unmarshal(customHeadersJSON, &customHeaders)
|
||||
}
|
||||
|
||||
models, err := prov.ListModels(c.Request.Context(), providers.ProviderConfig{
|
||||
Endpoint: endpoint,
|
||||
APIKey: key,
|
||||
Endpoint: endpoint,
|
||||
APIKey: key,
|
||||
CustomHeaders: customHeaders,
|
||||
})
|
||||
if err != nil {
|
||||
results = append(results, fetchResult{ConfigID: cfgID, Provider: providerID, Error: err.Error()})
|
||||
@@ -574,11 +582,18 @@ func (h *AdminHandler) FetchModels(c *gin.Context) {
|
||||
|
||||
fr := fetchResult{ConfigID: cfgID, Provider: providerID}
|
||||
for _, m := range models {
|
||||
// Serialize capabilities to JSONB
|
||||
capsJSON, _ := json.Marshal(m.Capabilities)
|
||||
|
||||
result, err := database.DB.Exec(`
|
||||
INSERT INTO model_configs (api_config_id, model_id, display_name)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (api_config_id, model_id) DO NOTHING
|
||||
`, cfgID, m.ID, m.Name)
|
||||
INSERT INTO model_configs (api_config_id, model_id, display_name, capabilities)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (api_config_id, model_id)
|
||||
DO UPDATE SET
|
||||
display_name = COALESCE(NULLIF(model_configs.display_name, ''), EXCLUDED.display_name),
|
||||
capabilities = EXCLUDED.capabilities,
|
||||
updated_at = NOW()
|
||||
`, cfgID, m.ID, m.Name, capsJSON)
|
||||
if err != nil {
|
||||
fr.Skipped++
|
||||
continue
|
||||
|
||||
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -363,11 +364,12 @@ func (h *APIConfigHandler) ListAllModels(c *gin.Context) {
|
||||
defer rows.Close()
|
||||
|
||||
type modelEntry struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
OwnedBy string `json:"owned_by,omitempty"`
|
||||
ConfigID string `json:"config_id"`
|
||||
Provider string `json:"provider"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
OwnedBy string `json:"owned_by,omitempty"`
|
||||
ConfigID string `json:"config_id"`
|
||||
Provider string `json:"provider"`
|
||||
Capabilities providers.ModelCapabilities `json:"capabilities"`
|
||||
}
|
||||
|
||||
allModels := make([]modelEntry, 0)
|
||||
@@ -398,12 +400,17 @@ func (h *APIConfigHandler) ListAllModels(c *gin.Context) {
|
||||
}
|
||||
|
||||
for _, m := range models {
|
||||
// Provider caps are authoritative; fill gaps from known table
|
||||
caps := providers.MergeCapabilities(m.Capabilities, m.ID)
|
||||
caps.MaxOutputTokens = providers.ResolveMaxOutput(m.ID, caps)
|
||||
|
||||
allModels = append(allModels, modelEntry{
|
||||
ID: m.ID,
|
||||
Name: m.Name,
|
||||
OwnedBy: m.OwnedBy,
|
||||
ConfigID: cfgID,
|
||||
Provider: providerID,
|
||||
ID: m.ID,
|
||||
Name: m.Name,
|
||||
OwnedBy: m.OwnedBy,
|
||||
ConfigID: cfgID,
|
||||
Provider: providerID,
|
||||
Capabilities: caps,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -414,16 +421,22 @@ func (h *APIConfigHandler) ListAllModels(c *gin.Context) {
|
||||
// ── List Enabled Models (from model_configs) ─
|
||||
|
||||
func (h *APIConfigHandler) ListEnabledModels(c *gin.Context) {
|
||||
userID := getUserID(c)
|
||||
|
||||
type enabledModel struct {
|
||||
ID string `json:"id"`
|
||||
ModelID string `json:"model_id"`
|
||||
DisplayName *string `json:"display_name"`
|
||||
Provider string `json:"provider"`
|
||||
ProviderName string `json:"provider_name"`
|
||||
ConfigID string `json:"config_id"`
|
||||
Capabilities map[string]interface{} `json:"capabilities"`
|
||||
ID string `json:"id"`
|
||||
ModelID string `json:"model_id"`
|
||||
DisplayName *string `json:"display_name"`
|
||||
Provider string `json:"provider"`
|
||||
ProviderName string `json:"provider_name"`
|
||||
ConfigID string `json:"config_id"`
|
||||
Capabilities providers.ModelCapabilities `json:"capabilities"`
|
||||
Pricing *providers.ModelPricing `json:"pricing,omitempty"`
|
||||
}
|
||||
|
||||
models := make([]enabledModel, 0)
|
||||
|
||||
// ── 1. Admin model_configs (pre-synced via FetchModels) ──
|
||||
rows, err := database.DB.Query(`
|
||||
SELECT mc.id, mc.model_id, mc.display_name, ac.provider, ac.name, mc.api_config_id, mc.capabilities
|
||||
FROM model_configs mc
|
||||
@@ -431,22 +444,92 @@ func (h *APIConfigHandler) ListEnabledModels(c *gin.Context) {
|
||||
WHERE mc.is_enabled = true AND ac.is_active = true AND ac.user_id IS NULL
|
||||
ORDER BY ac.name, mc.model_id
|
||||
`)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"models": []interface{}{}})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
if err == nil {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var m enabledModel
|
||||
var capsJSON []byte
|
||||
if err := rows.Scan(&m.ID, &m.ModelID, &m.DisplayName, &m.Provider, &m.ProviderName, &m.ConfigID, &capsJSON); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
models := make([]enabledModel, 0)
|
||||
for rows.Next() {
|
||||
var m enabledModel
|
||||
var capsJSON []byte
|
||||
if err := rows.Scan(&m.ID, &m.ModelID, &m.DisplayName, &m.Provider, &m.ProviderName, &m.ConfigID, &capsJSON); err != nil {
|
||||
continue
|
||||
// Parse DB capabilities (from provider at sync time)
|
||||
var dbCaps providers.ModelCapabilities
|
||||
_ = json.Unmarshal(capsJSON, &dbCaps)
|
||||
|
||||
// Provider-reported caps are authoritative; fill gaps from known table/heuristics
|
||||
if dbCaps.HasProviderData() {
|
||||
m.Capabilities = providers.MergeCapabilities(dbCaps, m.ModelID)
|
||||
} else {
|
||||
// No provider data — use known table or heuristics as base
|
||||
knownCaps, found := providers.LookupKnownModel(m.ModelID)
|
||||
if !found {
|
||||
knownCaps = providers.InferCapabilities(m.ModelID)
|
||||
}
|
||||
m.Capabilities = knownCaps
|
||||
}
|
||||
|
||||
m.Capabilities.MaxOutputTokens = providers.ResolveMaxOutput(m.ModelID, m.Capabilities)
|
||||
models = append(models, m)
|
||||
}
|
||||
}
|
||||
|
||||
// ── 2. User provider models (live query) ──
|
||||
userRows, err := database.DB.Query(`
|
||||
SELECT id, name, provider, endpoint, api_key_encrypted, custom_headers
|
||||
FROM api_configs
|
||||
WHERE user_id = $1 AND is_active = true
|
||||
`, userID)
|
||||
if err == nil {
|
||||
defer userRows.Close()
|
||||
for userRows.Next() {
|
||||
var cfgID, name, providerID, endpoint string
|
||||
var apiKey *string
|
||||
var headersJSON []byte
|
||||
if err := userRows.Scan(&cfgID, &name, &providerID, &endpoint, &apiKey, &headersJSON); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
provider, err := providers.Get(providerID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
key := ""
|
||||
if apiKey != nil {
|
||||
key = *apiKey
|
||||
}
|
||||
|
||||
var customHeaders map[string]string
|
||||
_ = json.Unmarshal(headersJSON, &customHeaders)
|
||||
|
||||
provModels, err := provider.ListModels(c.Request.Context(), providers.ProviderConfig{
|
||||
Endpoint: endpoint,
|
||||
APIKey: key,
|
||||
CustomHeaders: customHeaders,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("[models] user provider %q (%s) list failed: %v", name, providerID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, pm := range provModels {
|
||||
caps := pm.Capabilities
|
||||
// Provider-reported caps are authoritative; fill gaps
|
||||
caps = providers.MergeCapabilities(caps, pm.ID)
|
||||
caps.MaxOutputTokens = providers.ResolveMaxOutput(pm.ID, caps)
|
||||
|
||||
models = append(models, enabledModel{
|
||||
ID: pm.ID,
|
||||
ModelID: pm.ID,
|
||||
Provider: providerID,
|
||||
ProviderName: name,
|
||||
ConfigID: cfgID,
|
||||
Capabilities: caps,
|
||||
Pricing: pm.Pricing,
|
||||
})
|
||||
}
|
||||
}
|
||||
m.Capabilities = make(map[string]interface{})
|
||||
_ = json.Unmarshal(capsJSON, &m.Capabilities)
|
||||
models = append(models, m)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"models": models})
|
||||
|
||||
@@ -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 ─────────────────────
|
||||
|
||||
Reference in New Issue
Block a user