Changeset 0.28.0.9 (#181)

This commit is contained in:
2026-03-13 00:31:05 +00:00
parent aa870f1040
commit 33d76e59ab
13 changed files with 244 additions and 125 deletions

View File

@@ -13,7 +13,6 @@ import (
"git.gobha.me/xcaliber/chat-switchboard/database"
"git.gobha.me/xcaliber/chat-switchboard/health"
"git.gobha.me/xcaliber/chat-switchboard/models"
"git.gobha.me/xcaliber/chat-switchboard/providers"
"git.gobha.me/xcaliber/chat-switchboard/store"
)
@@ -74,7 +73,7 @@ func (h *ModelHandler) ListEnabledModels(c *gin.Context) {
}
}
c.JSON(http.StatusOK, gin.H{"models": userModels, "default_model": defaultModel})
c.JSON(http.StatusOK, gin.H{"data": userModels, "default_model": defaultModel})
}
// buildHealthMap returns a map of configID → ProviderStatus from current health windows.
@@ -147,52 +146,3 @@ func capsFromCatalog(modelID, configID string) (models.ModelCapabilities, bool)
return resolved, true
}
// liveQueryModelCaps queries a provider API to get capabilities for a specific model.
func liveQueryModelCaps(c *gin.Context, configID, modelID string) (models.ModelCapabilities, bool) {
if database.DB == nil {
return models.ModelCapabilities{}, false
}
var providerID, endpoint string
var apiKey *string
var headersJSON []byte
err := database.DB.QueryRow(database.Q(`
SELECT provider, endpoint, api_key_enc, headers
FROM provider_configs WHERE id = $1 AND is_active = true
`), configID).Scan(&providerID, &endpoint, &apiKey, &headersJSON)
if err != nil {
return models.ModelCapabilities{}, false
}
provider, err := providers.Get(providerID)
if err != nil {
return models.ModelCapabilities{}, false
}
key := ""
if apiKey != nil {
key = *apiKey
}
var customHeaders map[string]string
_ = json.Unmarshal(headersJSON, &customHeaders)
modelList, err := provider.ListModels(c.Request.Context(), providers.ProviderConfig{
Endpoint: endpoint,
APIKey: key,
CustomHeaders: customHeaders,
})
if err != nil {
log.Printf("[caps] live query for %s via config %s failed: %v", modelID, configID, err)
return models.ModelCapabilities{}, false
}
for _, m := range modelList {
if m.ID == modelID {
resolved := capspkg.ResolveIntrinsic(modelID, &m.Capabilities, nil)
return resolved, true
}
}
return models.ModelCapabilities{}, false
}

View File

@@ -829,7 +829,7 @@ func TestIntegration_ModelVisibilityResolution(t *testing.T) {
}
var modelsResp map[string]interface{}
decode(w, &modelsResp)
modelsList := modelsResp["models"].([]interface{})
modelsList := modelsResp["data"].([]interface{})
if len(modelsList) != 0 {
t.Errorf("disabled model should not appear, got %d models", len(modelsList))
}
@@ -849,7 +849,7 @@ func TestIntegration_ModelVisibilityResolution(t *testing.T) {
t.Fatalf("models/enabled after enable: want 200, got %d: %s", w.Code, w.Body.String())
}
decode(w, &modelsResp)
modelsList = modelsResp["models"].([]interface{})
modelsList = modelsResp["data"].([]interface{})
if len(modelsList) != 1 {
t.Errorf("enabled model should appear, want 1 got %d", len(modelsList))
}
@@ -1163,13 +1163,13 @@ func TestIntegration_AdminModelFetchEnableUserSees(t *testing.T) {
}
var userResp map[string]interface{}
decode(w, &userResp)
userModels := userResp["models"].([]interface{})
userModels := userResp["data"].([]interface{})
if len(userModels) != 0 {
t.Errorf("disabled models must not appear for user, got %d", len(userModels))
}
// Verify user response is non-null array
if userResp["models"] == nil {
if userResp["data"] == nil {
t.Fatal("user models must be [] not null — causes '📋 Loaded 0 models' to crash")
}
@@ -1192,7 +1192,7 @@ func TestIntegration_AdminModelFetchEnableUserSees(t *testing.T) {
t.Fatalf("user models/enabled after enable: want 200, got %d: %s", w.Code, w.Body.String())
}
decode(w, &userResp)
userModels = userResp["models"].([]interface{})
userModels = userResp["data"].([]interface{})
if len(userModels) != 1 {
t.Fatalf("user should see 1 enabled model, got %d", len(userModels))
}
@@ -1260,10 +1260,10 @@ func (h *testHarness) getModels(token string) []interface{} {
}
var resp map[string]interface{}
decode(w, &resp)
if resp["models"] == nil {
if resp["data"] == nil {
h.t.Fatal("models response must never be null")
}
return resp["models"].([]interface{})
return resp["data"].([]interface{})
}
func getModelIDs(models []interface{}) []string {

View File

@@ -444,7 +444,7 @@ func TestLive_ProviderFullFlow(t *testing.T) {
w = h.request("GET", "/api/v1/models/enabled", adminToken, nil)
var enabledResp map[string]interface{}
decode(w, &enabledResp)
enabledModels := enabledResp["models"].([]interface{})
enabledModels := enabledResp["data"].([]interface{})
if len(enabledModels) < 1 {
t.Fatal("models/enabled should return at least 1 model")
}
@@ -476,7 +476,7 @@ func TestLive_ProviderFullFlow(t *testing.T) {
w = h.request("GET", "/api/v1/models/enabled", userToken, nil)
var userResp map[string]interface{}
decode(w, &userResp)
userModels := userResp["models"].([]interface{})
userModels := userResp["data"].([]interface{})
if len(userModels) < 1 {
t.Fatal("regular user should see at least 1 enabled model")
}
@@ -778,7 +778,7 @@ func TestLive_BYOK_AutoFetch(t *testing.T) {
w = h.request("GET", "/api/v1/models/enabled", userToken, nil)
var resp map[string]interface{}
decode(w, &resp)
userModels := resp["models"].([]interface{})
userModels := resp["data"].([]interface{})
personalCount := 0
for _, raw := range userModels {

View File

@@ -28,7 +28,11 @@ func (h *ModelPrefsHandler) GetPreferences(c *gin.Context) {
return
}
c.JSON(http.StatusOK, gin.H{"preferences": prefs})
if prefs == nil {
prefs = []models.UserModelSetting{}
}
c.JSON(http.StatusOK, gin.H{"data": prefs})
}
// SetPreference upserts a single model preference.
@@ -37,7 +41,7 @@ func (h *ModelPrefsHandler) SetPreference(c *gin.Context) {
var req struct {
ModelID string `json:"model_id" binding:"required"`
ProviderConfigID *string `json:"provider_config_id,omitempty"`
ProviderConfigID string `json:"provider_config_id" binding:"required"`
Hidden *bool `json:"hidden,omitempty"`
PreferredTemperature *float64 `json:"preferred_temperature,omitempty"`
PreferredMaxTokens *int `json:"preferred_max_tokens,omitempty"`
@@ -48,15 +52,16 @@ func (h *ModelPrefsHandler) SetPreference(c *gin.Context) {
return
}
pcid := req.ProviderConfigID
patch := models.UserModelSettingPatch{
ProviderConfigID: req.ProviderConfigID,
ProviderConfigID: &pcid,
Hidden: req.Hidden,
PreferredTemperature: req.PreferredTemperature,
PreferredMaxTokens: req.PreferredMaxTokens,
SortOrder: req.SortOrder,
}
if err := h.stores.UserSettings.Set(c.Request.Context(), userID, req.ModelID, req.ProviderConfigID, patch); err != nil {
if err := h.stores.UserSettings.Set(c.Request.Context(), userID, req.ModelID, &pcid, patch); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to set preference"})
return
}

View File

@@ -3,7 +3,6 @@ package postgres
import (
"context"
"fmt"
"strings"
"git.gobha.me/xcaliber/chat-switchboard/models"
)
@@ -148,5 +147,3 @@ func patchIntOrNil(i *int) interface{} {
return *i
}
// unused but keeping for reference - will be used in ListOptions-based queries
var _ = strings.Join

View File

@@ -3,7 +3,6 @@ package sqlite
import (
"context"
"fmt"
"strings"
"git.gobha.me/xcaliber/chat-switchboard/models"
"git.gobha.me/xcaliber/chat-switchboard/store"
@@ -149,5 +148,3 @@ func patchIntOrNil(i *int) interface{} {
return *i
}
// unused but keeping for reference - will be used in ListOptions-based queries
var _ = strings.Join