Changeset 0.8.6 (#49)

This commit is contained in:
2026-02-22 16:52:19 +00:00
parent 633421708f
commit 15be26c516
17 changed files with 870 additions and 152 deletions

View File

@@ -64,10 +64,10 @@ func (h *APIConfigHandler) ListConfigs(c *gin.Context) {
userID := getUserID(c)
page, perPage, offset := parsePagination(c)
// Count: user's configs + global configs
// Count: user's configs + global configs (exclude team-scoped)
var total int
err := database.DB.QueryRow(
`SELECT COUNT(*) FROM api_configs WHERE user_id = $1 OR user_id IS NULL`,
`SELECT COUNT(*) FROM api_configs WHERE (user_id = $1 OR user_id IS NULL) AND team_id IS NULL`,
userID,
).Scan(&total)
if err != nil {
@@ -79,7 +79,7 @@ func (h *APIConfigHandler) ListConfigs(c *gin.Context) {
SELECT id, user_id, name, provider, endpoint, api_key_encrypted,
model_default, config, is_active, created_at, updated_at
FROM api_configs
WHERE user_id = $1 OR user_id IS NULL
WHERE (user_id = $1 OR user_id IS NULL) AND team_id IS NULL
ORDER BY user_id NULLS LAST, name ASC
LIMIT $2 OFFSET $3
`, userID, perPage, offset)
@@ -169,7 +169,7 @@ func (h *APIConfigHandler) GetConfig(c *gin.Context) {
SELECT id, user_id, name, provider, endpoint, api_key_encrypted,
model_default, config::text, is_active, created_at, updated_at
FROM api_configs
WHERE id = $1 AND (user_id = $2 OR user_id IS NULL)
WHERE id = $1 AND (user_id = $2 OR user_id IS NULL) AND team_id IS NULL
`, configID, userID)
var cfg apiConfigResponse
@@ -310,7 +310,7 @@ func (h *APIConfigHandler) ListModels(c *gin.Context) {
err := database.DB.QueryRow(`
SELECT provider, endpoint, api_key_encrypted
FROM api_configs
WHERE id = $1 AND (user_id = $2 OR user_id IS NULL) AND is_active = true
WHERE id = $1 AND (user_id = $2 OR user_id IS NULL) AND team_id IS NULL AND is_active = true
`, configID, userID).Scan(&providerID, &endpoint, &apiKey)
if err == sql.ErrNoRows {
@@ -357,7 +357,7 @@ func (h *APIConfigHandler) ListAllModels(c *gin.Context) {
rows, err := database.DB.Query(`
SELECT id, name, provider, endpoint, api_key_encrypted
FROM api_configs
WHERE (user_id = $1 OR user_id IS NULL) AND is_active = true
WHERE (user_id = $1 OR user_id IS NULL) AND is_active = true AND team_id IS NULL
`, userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list configs"})
@@ -422,26 +422,29 @@ func (h *APIConfigHandler) ListAllModels(c *gin.Context) {
// ── List Enabled Models (from model_configs) ─
// enabledModel is the unified model entry returned by ListEnabledModels.
// Used across apiconfigs, capabilities, and preset resolution.
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 providers.ModelCapabilities `json:"capabilities"`
Pricing *providers.ModelPricing `json:"pricing,omitempty"`
Source string `json:"source,omitempty"`
TeamName string `json:"team_name,omitempty"`
IsPreset bool `json:"is_preset,omitempty"`
PresetID string `json:"preset_id,omitempty"`
PresetScope string `json:"preset_scope,omitempty"`
PresetAvatar string `json:"preset_avatar,omitempty"`
PresetTeamName string `json:"preset_team_name,omitempty"`
}
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 providers.ModelCapabilities `json:"capabilities"`
Pricing *providers.ModelPricing `json:"pricing,omitempty"`
Source string `json:"source,omitempty"`
IsPreset bool `json:"is_preset,omitempty"`
PresetID string `json:"preset_id,omitempty"`
PresetScope string `json:"preset_scope,omitempty"`
PresetAvatar string `json:"preset_avatar,omitempty"`
PresetTeamName string `json:"preset_team_name,omitempty"`
}
models := make([]enabledModel, 0)
// ── 1. Admin model_configs (pre-synced via FetchModels) ──
@@ -449,7 +452,7 @@ func (h *APIConfigHandler) ListEnabledModels(c *gin.Context) {
SELECT mc.id, mc.model_id, mc.display_name, ac.provider, ac.name, mc.api_config_id, mc.capabilities
FROM model_configs mc
JOIN api_configs ac ON mc.api_config_id = ac.id
WHERE mc.visibility = 'enabled' AND ac.is_active = true AND ac.user_id IS NULL
WHERE mc.visibility = 'enabled' AND ac.is_active = true AND ac.is_global = true
ORDER BY ac.name, mc.model_id
`)
if err == nil {
@@ -484,10 +487,13 @@ func (h *APIConfigHandler) ListEnabledModels(c *gin.Context) {
}
// ── 2. User provider models (live query) ──
// NOTE: Team provider models are NOT listed here. They are only
// available to team admins for building presets (via ListAvailableModels).
// Team members access team models through curated presets only.
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
WHERE user_id = $1 AND is_active = true AND team_id IS NULL
`, userID)
if err == nil {
defer userRows.Close()
@@ -574,41 +580,12 @@ func (h *APIConfigHandler) ListEnabledModels(c *gin.Context) {
continue
}
// Inherit capabilities from the base model already loaded in sections 1/2
var caps providers.ModelCapabilities
capsFound := false
for _, existing := range models {
if existing.ModelID == baseModelID {
caps = existing.Capabilities
capsFound = true
break
}
}
if !capsFound {
// Base model not in enabled list — try synced model_configs
var capsJSON []byte
err := database.DB.QueryRow(`
SELECT capabilities FROM model_configs
WHERE model_id = $1 ORDER BY updated_at DESC LIMIT 1
`, baseModelID).Scan(&capsJSON)
if err == nil && len(capsJSON) > 0 {
_ = json.Unmarshal(capsJSON, &caps)
capsFound = true
}
}
if !capsFound {
var found bool
caps, found = providers.LookupKnownModel(baseModelID)
if !found {
caps = providers.InferCapabilities(baseModelID)
}
}
caps.MaxOutputTokens = providers.ResolveMaxOutput(baseModelID, caps)
// Inherit capabilities from base model via shared resolver
cfgID := ""
if apiConfigID != nil {
cfgID = *apiConfigID
}
caps := ResolveModelCapsFromLoaded(c, baseModelID, cfgID, models)
// Build display name: "icon Name (base-model)"
displayName := name
@@ -665,3 +642,5 @@ func parseJSONBConfig(raw string) map[string]interface{} {
_ = json.Unmarshal([]byte(raw), &result)
return result
}