package handlers import ( "context" "fmt" "net/http" "net/http/httptest" "os" "strings" "testing" "time" "git.gobha.me/xcaliber/chat-switchboard/database" "git.gobha.me/xcaliber/chat-switchboard/providers" ) // ═══════════════════════════════════════════ // Live Provider Integration Tests // ═══════════════════════════════════════════ // These tests require: // - TEST_DATABASE_URL or PGHOST+PGUSER // - PROVIDER + PROVIDER_KEY (or legacy VENICE_API_KEY) // // Provider config env vars: // PROVIDER — provider type: "venice", "openai", "anthropic" (default: "venice") // PROVIDER_KEY — API key (falls back to VENICE_API_KEY for compat) // PROVIDER_URL — endpoint override (optional, uses default for known providers) // // They exercise the full flow: create provider → // fetch models → enable model → resolve → complete. // ═══════════════════════════════════════════ // liveProviderConfig holds resolved provider settings for live tests. type liveProviderConfig struct { Provider string // "venice", "openai", "anthropic" Key string Endpoint string } // defaultEndpoints maps provider names to their default API endpoints. var defaultEndpoints = map[string]string{ "venice": "https://api.venice.ai/api/v1", "openai": "https://api.openai.com/v1", "anthropic": "https://api.anthropic.com/v1", "openrouter": "https://openrouter.ai/api/v1", } // providerKeyEnvs maps provider names to their API key env var names. var providerKeyEnvs = map[string]string{ "venice": "VENICE_API_KEY", "openai": "OPENAI_API_KEY", "anthropic": "ANTHROPIC_API_KEY", "openrouter": "OPENROUTER_API_KEY", } // requireLiveProvider resolves the primary provider config from env vars. // Kept for backward compat — tests that only need one provider use this. func requireLiveProvider(t *testing.T) liveProviderConfig { t.Helper() key := os.Getenv("PROVIDER_KEY") if key == "" { // Legacy fallback key = os.Getenv("VENICE_API_KEY") } if key == "" { t.Skip("PROVIDER_KEY (or VENICE_API_KEY) not set — skipping live provider test") } provider := os.Getenv("PROVIDER") if provider == "" { provider = "venice" // default } endpoint := os.Getenv("PROVIDER_URL") if endpoint == "" { var ok bool endpoint, ok = defaultEndpoints[provider] if !ok { t.Fatalf("PROVIDER=%q has no default endpoint — set PROVIDER_URL", provider) } } t.Logf(" Live provider: %s @ %s", provider, endpoint) return liveProviderConfig{Provider: provider, Key: key, Endpoint: endpoint} } // requireLiveProviders resolves all configured providers for failover tests. // Reads LIVE_PROVIDERS (comma-separated, e.g. "venice,openai") and resolves // API keys from {PROVIDER}_API_KEY env vars. Falls back to requireLiveProvider // if LIVE_PROVIDERS is not set. func requireLiveProviders(t *testing.T) []liveProviderConfig { t.Helper() list := os.Getenv("LIVE_PROVIDERS") if list == "" { // Fallback: just the primary provider return []liveProviderConfig{requireLiveProvider(t)} } var configs []liveProviderConfig for _, name := range splitTrim(list, ",") { keyEnv := providerKeyEnvs[name] if keyEnv == "" { keyEnv = strings.ToUpper(name) + "_API_KEY" } key := os.Getenv(keyEnv) if key == "" { t.Logf(" Skipping provider %s: %s not set", name, keyEnv) continue } endpoint := os.Getenv(strings.ToUpper(name) + "_API_URL") if endpoint == "" { endpoint = defaultEndpoints[name] } if endpoint == "" { t.Logf(" Skipping provider %s: no endpoint", name) continue } configs = append(configs, liveProviderConfig{Provider: name, Key: key, Endpoint: endpoint}) } if len(configs) == 0 { t.Skip("no live providers configured — set LIVE_PROVIDERS + API keys") } t.Logf(" Live providers: %d configured", len(configs)) return configs } // splitTrim splits s by sep and trims whitespace from each element. func splitTrim(s, sep string) []string { parts := strings.Split(s, sep) var out []string for _, p := range parts { p = strings.TrimSpace(p) if p != "" { out = append(out, p) } } return out } // providerModel holds a resolved (configID, modelID) pair from a provider. type providerModel struct { ConfigID string ModelID string Provider string } // setupAllProviders creates configs and enables a model for each provider. // Tolerant: if a provider fails setup, it's skipped. Fails only if zero succeed. func setupAllProviders(t *testing.T, h *testHarness, adminToken string, providerList []liveProviderConfig) []providerModel { t.Helper() var models []providerModel for _, pc := range providerList { configID, modelID, err := trySetupProvider(h, adminToken, pc) if err != nil { t.Logf(" ⚠ %s setup failed: %v — skipping", pc.Provider, err) continue } t.Logf(" Provider %s ready, model %s enabled", pc.Provider, modelID) models = append(models, providerModel{ConfigID: configID, ModelID: modelID, Provider: pc.Provider}) } if len(models) == 0 { t.Fatal("no providers available after setup — all failed") } return models } // trySetupProvider is like setupProviderWithModel but returns error instead of t.Fatal. func trySetupProvider(h *testHarness, adminToken string, pc liveProviderConfig) (string, string, error) { // Create provider w := h.request("POST", "/api/v1/admin/configs", adminToken, map[string]interface{}{ "name": pc.Provider + " Test", "provider": pc.Provider, "endpoint": pc.Endpoint, "api_key": pc.Key, }) if w.Code != http.StatusCreated { return "", "", fmt.Errorf("create config: %d: %s", w.Code, w.Body.String()) } var cfg map[string]interface{} decode(w, &cfg) configID := cfg["id"].(string) // Fetch models w = h.request("POST", "/api/v1/admin/models/fetch", adminToken, map[string]interface{}{"provider_config_id": configID}) if w.Code != http.StatusOK { return "", "", fmt.Errorf("fetch models: %d: %s", w.Code, w.Body.String()) } // Find and enable a non-reasoning model w = h.request("GET", "/api/v1/admin/models", adminToken, nil) var modelsResp map[string]interface{} decode(w, &modelsResp) var catalogID, modelID string var fallbackCatalogID, fallbackModelID string for _, raw := range modelsResp["models"].([]interface{}) { m := raw.(map[string]interface{}) if m["visibility"].(string) != "disabled" { continue } // Only pick models from this provider if m["provider_config_id"] != nil && m["provider_config_id"].(string) != configID { continue } mid := m["model_id"].(string) cid := m["id"].(string) if fallbackCatalogID == "" { fallbackCatalogID = cid fallbackModelID = mid } isReasoning := false if caps, ok := m["capabilities"].(map[string]interface{}); ok { if r, exists := caps["reasoning"]; exists && r == true { isReasoning = true } } if !isReasoning { catalogID = cid modelID = mid break } } if catalogID == "" { catalogID = fallbackCatalogID modelID = fallbackModelID } if catalogID == "" { return "", "", fmt.Errorf("no disabled model found") } w = h.request("PUT", "/api/v1/admin/models/"+catalogID, adminToken, map[string]interface{}{"visibility": "enabled"}) if w.Code != http.StatusOK { return "", "", fmt.Errorf("enable model: %d: %s", w.Code, w.Body.String()) } return configID, modelID, nil } // tryCompletion attempts a completion against each provider/model in order. // Returns the first successful response and the provider that worked. // Fails only if ALL providers fail. func tryCompletion(t *testing.T, h *testHarness, token, channelID string, models []providerModel, stream bool) (*httptest.ResponseRecorder, providerModel) { t.Helper() var lastW *httptest.ResponseRecorder for _, pm := range models { w := h.request("POST", "/api/v1/chat/completions", token, map[string]interface{}{ "channel_id": channelID, "content": "Say ok", "model": pm.ModelID, "provider_config_id": pm.ConfigID, "stream": &stream, "max_tokens": 1200, }) if w.Code == http.StatusOK { t.Logf(" ✓ Completion via %s/%s (status %d)", pm.Provider, pm.ModelID, w.Code) return w, pm } t.Logf(" ✗ %s/%s returned %d — trying next", pm.Provider, pm.ModelID, w.Code) lastW = w } // All failed body := "" if lastW != nil { body = lastW.Body.String() } t.Fatalf("all %d providers failed; last: %s", len(models), body) return nil, providerModel{} // unreachable } // setupProviderWithModel creates a provider config, fetches models, and enables // the first available model. Returns (configID, enabledModelID). func setupProviderWithModel(t *testing.T, h *testHarness, adminToken string, pc liveProviderConfig) (string, string) { t.Helper() // Create provider w := h.request("POST", "/api/v1/admin/configs", adminToken, map[string]interface{}{ "name": pc.Provider + " Test", "provider": pc.Provider, "endpoint": pc.Endpoint, "api_key": pc.Key, }) if w.Code != http.StatusCreated { t.Fatalf("create %s config: want 201, got %d: %s", pc.Provider, w.Code, w.Body.String()) } var cfg map[string]interface{} decode(w, &cfg) configID := cfg["id"].(string) // Fetch models w = h.request("POST", "/api/v1/admin/models/fetch", adminToken, map[string]interface{}{"provider_config_id": configID}) if w.Code != http.StatusOK { t.Fatalf("fetch models: %d: %s", w.Code, w.Body.String()) } // Find and enable a model. // Prefer non-reasoning models: they're cheaper and don't require // minimum thinking budget tokens (which causes 400s with low max_tokens). w = h.request("GET", "/api/v1/admin/models", adminToken, nil) var modelsResp map[string]interface{} decode(w, &modelsResp) var catalogID, modelID string var fallbackCatalogID, fallbackModelID string for _, raw := range modelsResp["models"].([]interface{}) { m := raw.(map[string]interface{}) if m["visibility"].(string) != "disabled" { continue } mid := m["model_id"].(string) cid := m["id"].(string) // Save first disabled model as fallback if fallbackCatalogID == "" { fallbackCatalogID = cid fallbackModelID = mid } // Check if this is a reasoning model — skip it if possible isReasoning := false if caps, ok := m["capabilities"].(map[string]interface{}); ok { if r, exists := caps["reasoning"]; exists && r == true { isReasoning = true } } if !isReasoning { catalogID = cid modelID = mid break } } // Fall back to any disabled model if all are reasoning models if catalogID == "" { catalogID = fallbackCatalogID modelID = fallbackModelID } if catalogID == "" { t.Fatal("no disabled model found to enable after fetch") } w = h.request("PUT", "/api/v1/admin/models/"+catalogID, adminToken, map[string]interface{}{"visibility": "enabled"}) if w.Code != http.StatusOK { t.Fatalf("enable model: %d: %s", w.Code, w.Body.String()) } t.Logf(" Provider %s ready, model %s enabled", configID, modelID) return configID, modelID } // TestLive_ProviderFullFlow exercises the complete admin workflow: // create provider → fetch models → enable a model → user sees it → chat completion func TestLive_ProviderFullFlow(t *testing.T) { h := setupHarness(t) pc := requireLiveProvider(t) _, adminToken := h.createAdminUser("admin", "admin@test.com") // ── 1. Create provider config ──────────── t.Log("Step 1: Creating provider config") w := h.request("POST", "/api/v1/admin/configs", adminToken, map[string]interface{}{ "name": pc.Provider + " Live Test", "provider": pc.Provider, "endpoint": pc.Endpoint, "api_key": pc.Key, }) if w.Code != http.StatusCreated { t.Fatalf("create config: want 201, got %d: %s", w.Code, w.Body.String()) } var configResp map[string]interface{} decode(w, &configResp) configID := configResp["id"].(string) t.Logf(" Created config: %s", configID) // ── 2. Fetch models ───────────────────── t.Log("Step 2: Fetching models from provider API") w = h.request("POST", "/api/v1/admin/models/fetch", adminToken, map[string]interface{}{ "provider_config_id": configID, }) if w.Code != http.StatusOK { t.Fatalf("fetch models: want 200, got %d: %s", w.Code, w.Body.String()) } var fetchResp map[string]interface{} decode(w, &fetchResp) totalFetched := fetchResp["total"].(float64) if totalFetched < 1 { t.Fatalf("provider should return at least 1 model, got %.0f", totalFetched) } t.Logf(" Fetched %.0f models", totalFetched) // ── 3. List + enable first model ──────── t.Log("Step 3: Enabling first available model") w = h.request("GET", "/api/v1/admin/models", adminToken, nil) var modelsResp map[string]interface{} decode(w, &modelsResp) catalogModels := modelsResp["models"].([]interface{}) var enableID, enableModelID string var fallbackID, fallbackModelID string for _, raw := range catalogModels { m := raw.(map[string]interface{}) if m["visibility"].(string) != "disabled" { continue } mid := m["model_id"].(string) cid := m["id"].(string) if fallbackID == "" { fallbackID = cid fallbackModelID = mid } // Prefer non-reasoning models (cheaper, no thinking budget requirement) isReasoning := false if caps, ok := m["capabilities"].(map[string]interface{}); ok { if r, exists := caps["reasoning"]; exists && r == true { isReasoning = true } } if !isReasoning { enableID = cid enableModelID = mid break } } if enableID == "" { enableID = fallbackID enableModelID = fallbackModelID } if enableID == "" { t.Fatal("no disabled model found to enable") } t.Logf(" Enabling: %s (catalog ID: %s)", enableModelID, enableID) w = h.request("PUT", "/api/v1/admin/models/"+enableID, adminToken, map[string]interface{}{"visibility": "enabled"}) if w.Code != http.StatusOK { t.Fatalf("enable model: want 200, got %d: %s", w.Code, w.Body.String()) } // ── 4. Admin sees enabled model ───────── t.Log("Step 4: Verifying models/enabled (admin)") w = h.request("GET", "/api/v1/models/enabled", adminToken, nil) var enabledResp map[string]interface{} decode(w, &enabledResp) enabledModels := enabledResp["data"].([]interface{}) if len(enabledModels) < 1 { t.Fatal("models/enabled should return at least 1 model") } found := false for _, raw := range enabledModels { m := raw.(map[string]interface{}) if m["model_id"] == enableModelID { found = true if m["config_id"] == nil || m["config_id"] == "" { t.Error("enabled model must have config_id") } if m["provider_name"] == nil || m["provider_name"] == "" { t.Error("enabled model must have provider_name") } break } } if !found { t.Errorf("model %s not found in models/enabled", enableModelID) } // ── 5. Regular user also sees model ───── t.Log("Step 5: Verifying models/enabled (regular user)") userID := database.SeedTestUser(t, "liveuser", "liveuser@test.com") database.TestDB.Exec("UPDATE users SET is_active = true WHERE id = "+database.PH(1), userID) userToken := makeToken(userID, "liveuser@test.com", "user") w = h.request("GET", "/api/v1/models/enabled", userToken, nil) var userResp map[string]interface{} decode(w, &userResp) userModels := userResp["data"].([]interface{}) if len(userModels) < 1 { t.Fatal("regular user should see at least 1 enabled model") } } // TestLive_FetchModelsCapabilities verifies that model capabilities // are correctly parsed into the catalog. func TestLive_FetchModelsCapabilities(t *testing.T) { h := setupHarness(t) pc := requireLiveProvider(t) _, adminToken := h.createAdminUser("admin", "admin@test.com") w := h.request("POST", "/api/v1/admin/configs", adminToken, map[string]interface{}{ "name": pc.Provider + " Caps Test", "provider": pc.Provider, "endpoint": pc.Endpoint, "api_key": pc.Key, }) var cfg map[string]interface{} decode(w, &cfg) configID := cfg["id"].(string) h.request("POST", "/api/v1/admin/models/fetch", adminToken, map[string]interface{}{"provider_config_id": configID}) w = h.request("GET", "/api/v1/admin/models", adminToken, nil) var resp map[string]interface{} decode(w, &resp) for _, raw := range resp["models"].([]interface{}) { m := raw.(map[string]interface{}) caps, ok := m["capabilities"].(map[string]interface{}) if !ok { t.Errorf("model %s: capabilities must be an object", m["model_id"]) continue } // streaming should be true for most providers if caps["streaming"] != true { t.Logf(" note: model %s streaming=%v", m["model_id"], caps["streaming"]) } // Verify capabilities are actual booleans for _, key := range []string{"streaming", "vision", "tool_calling", "reasoning"} { if v, exists := caps[key]; exists { if _, ok := v.(bool); !ok { t.Errorf("model %s: capability %s should be bool, got %T", m["model_id"], key, v) } } } } } // TestLive_ChatCompletion sends an actual non-streaming chat completion. func TestLive_ChatCompletion(t *testing.T) { h := setupHarness(t) liveProvs := requireLiveProviders(t) _, adminToken := h.createAdminUser("admin", "admin@test.com") models := setupAllProviders(t, h, adminToken, liveProvs) w := h.request("POST", "/api/v1/channels", adminToken, map[string]interface{}{ "title": "Chat Test", "type": "direct", }) if w.Code != http.StatusCreated { t.Skipf("channel creation failed: %d %s", w.Code, w.Body.String()) } var ch map[string]interface{} decode(w, &ch) channelID := ch["id"].(string) w, _ = tryCompletion(t, h, adminToken, channelID, models, false) t.Logf(" ✓ Completion succeeded: %s", w.Body.String()[:min(200, w.Body.Len())]) } // TestLive_UsageLogging verifies that a non-streaming completion // creates a usage_log row with token counts. func TestLive_UsageLogging(t *testing.T) { h := setupHarness(t) liveProvs := requireLiveProviders(t) _, adminToken := h.createAdminUser("admin", "admin@test.com") models := setupAllProviders(t, h, adminToken, liveProvs) w := h.request("POST", "/api/v1/channels", adminToken, map[string]interface{}{ "title": "Usage Test", "type": "direct", }) if w.Code != http.StatusCreated { t.Skipf("channel creation failed: %d %s", w.Code, w.Body.String()) } var ch map[string]interface{} decode(w, &ch) _, used := tryCompletion(t, h, adminToken, ch["id"].(string), models, false) var rowCount, promptTokens, completionTokens int err := database.TestDB.QueryRow( "SELECT COUNT(*), COALESCE(SUM(prompt_tokens), 0), COALESCE(SUM(completion_tokens), 0) FROM usage_log WHERE provider_config_id = "+database.PH(1), used.ConfigID).Scan(&rowCount, &promptTokens, &completionTokens) if err != nil { t.Fatalf("query usage_log: %v", err) } if rowCount == 0 { t.Fatal("usage_log should have a row after completion") } if promptTokens == 0 { t.Fatal("completion should report prompt tokens") } t.Logf(" ✓ Usage: %d row(s), prompt=%d completion=%d (via %s)", rowCount, promptTokens, completionTokens, used.Provider) } // TestLive_StreamingUsageLogging verifies streaming completions log usage. func TestLive_StreamingUsageLogging(t *testing.T) { h := setupHarness(t) liveProvs := requireLiveProviders(t) _, adminToken := h.createAdminUser("admin", "admin@test.com") models := setupAllProviders(t, h, adminToken, liveProvs) w := h.request("POST", "/api/v1/channels", adminToken, map[string]interface{}{ "title": "Stream Usage Test", "type": "direct", }) if w.Code != http.StatusCreated { t.Skipf("channel creation failed: %d %s", w.Code, w.Body.String()) } var ch map[string]interface{} decode(w, &ch) _, used := tryCompletion(t, h, adminToken, ch["id"].(string), models, true) var rowCount, promptTokens, completionTokens int err := database.TestDB.QueryRow( "SELECT COUNT(*), COALESCE(SUM(prompt_tokens), 0), COALESCE(SUM(completion_tokens), 0) FROM usage_log WHERE provider_config_id = "+database.PH(1), used.ConfigID).Scan(&rowCount, &promptTokens, &completionTokens) if err != nil { t.Fatalf("query usage_log: %v", err) } if rowCount == 0 { t.Fatal("usage_log should have a row after streaming completion") } if promptTokens == 0 { t.Fatal("streaming completion should report prompt tokens") } t.Logf(" ✓ Streaming usage: %d row(s), prompt=%d completion=%d (via %s)", rowCount, promptTokens, completionTokens, used.Provider) } // TestLive_PricingFromCatalog verifies model sync populates pricing. func TestLive_PricingFromCatalog(t *testing.T) { h := setupHarness(t) pc := requireLiveProvider(t) _, adminToken := h.createAdminUser("admin", "admin@test.com") configID, _ := setupProviderWithModel(t, h, adminToken, pc) var pricingCount int database.TestDB.QueryRow( "SELECT COUNT(*) FROM model_pricing WHERE provider_config_id = "+database.PH(1), configID).Scan(&pricingCount) if pricingCount == 0 { t.Skip("model sync did not populate pricing — provider may not support pricing data") } t.Logf(" ✓ Catalog sync populated %d pricing entries", pricingCount) w := h.request("GET", "/api/v1/admin/pricing", adminToken, nil) if w.Code != http.StatusOK { t.Fatalf("admin pricing: %d: %s", w.Code, w.Body.String()) } var entries []interface{} decode(w, &entries) if len(entries) == 0 { t.Error("admin pricing API should return catalog-synced entries") } t.Logf(" ✓ Admin pricing API returns %d entries", len(entries)) } // TestLive_Embeddings tests the embeddings endpoint directly. // Only runs for providers that support embeddings (currently Venice). func TestLive_Embeddings(t *testing.T) { pc := requireLiveProvider(t) // Only Venice has a known embedding model for now if pc.Provider != "venice" { t.Skipf("embedding test only implemented for Venice (got %s)", pc.Provider) } provider := &providers.VeniceProvider{} cfg := providers.ProviderConfig{ Endpoint: pc.Endpoint, APIKey: pc.Key, } // Retry up to 3 times — Venice embedding endpoint can return transient 500s var resp *providers.EmbeddingResponse var err error for attempt := 1; attempt <= 3; attempt++ { resp, err = provider.Embed( context.Background(), cfg, providers.EmbeddingRequest{ Model: "text-embedding-bge-m3", Input: []string{"test embedding"}, }, ) if err == nil { break } t.Logf(" Embed attempt %d: %v", attempt, err) if attempt < 3 { time.Sleep(time.Duration(attempt) * 2 * time.Second) } } if err != nil { t.Skipf("Embed failed after 3 attempts (transient): %v", err) } if len(resp.Embeddings) == 0 || len(resp.Embeddings[0]) == 0 { t.Fatal("expected non-empty embedding vector") } t.Logf(" ✓ Embedding: %d dimensions, input_tokens=%d", len(resp.Embeddings[0]), resp.InputTokens) } // TestLive_ModelDeletion tests cleanup: delete provider removes catalog entries. func TestLive_ModelDeletion(t *testing.T) { h := setupHarness(t) pc := requireLiveProvider(t) _, adminToken := h.createAdminUser("admin", "admin@test.com") w := h.request("POST", "/api/v1/admin/configs", adminToken, map[string]interface{}{ "name": pc.Provider + " Delete Test", "provider": pc.Provider, "endpoint": pc.Endpoint, "api_key": pc.Key, }) var cfg map[string]interface{} decode(w, &cfg) configID := cfg["id"].(string) h.request("POST", "/api/v1/admin/models/fetch", adminToken, map[string]interface{}{"provider_config_id": configID}) var count int database.TestDB.QueryRow( "SELECT COUNT(*) FROM model_catalog WHERE provider_config_id = "+database.PH(1), configID).Scan(&count) if count == 0 { t.Fatal("catalog should have models after fetch") } t.Logf(" %d models before delete", count) w = h.request("DELETE", "/api/v1/admin/configs/"+configID, adminToken, nil) if w.Code != http.StatusOK { t.Fatalf("delete config: want 200, got %d: %s", w.Code, w.Body.String()) } database.TestDB.QueryRow( "SELECT COUNT(*) FROM model_catalog WHERE provider_config_id = "+database.PH(1), configID).Scan(&count) if count != 0 { t.Errorf("catalog should be empty after delete, got %d", count) } t.Log(" ✓ Cascade delete cleaned up catalog entries") } // TestLive_BYOK_AutoFetch exercises the user experience: // user creates a BYOK provider → auto-fetch triggers → models appear. func TestLive_BYOK_AutoFetch(t *testing.T) { h := setupHarness(t) pc := requireLiveProvider(t) _, adminToken := h.createAdminUser("admin", "admin@test.com") // Enable BYOK policy h.request("PUT", "/api/v1/admin/settings/allow_user_byok", adminToken, map[string]interface{}{"value": "true"}) userID := database.SeedTestUser(t, "byokuser", "byokuser@test.com") database.TestDB.Exec("UPDATE users SET is_active = true WHERE id = "+database.PH(1), userID) userToken := makeToken(userID, "byokuser@test.com", "user") // User creates BYOK provider w := h.request("POST", "/api/v1/api-configs", userToken, map[string]interface{}{ "name": "My " + pc.Provider, "provider": pc.Provider, "endpoint": pc.Endpoint, "api_key": pc.Key, }) if w.Code != http.StatusCreated { t.Fatalf("create BYOK: want 201, got %d: %s", w.Code, w.Body.String()) } var created map[string]interface{} decode(w, &created) cfgID := created["id"].(string) if created["warning"] != nil { t.Fatalf("auto-fetch should succeed, got warning: %v", created["warning"]) } modelsFetched := created["models_fetched"] if modelsFetched == nil || modelsFetched.(float64) < 1 { t.Fatalf("auto-fetch should return models_fetched > 0, got: %v", modelsFetched) } t.Logf(" Auto-fetched %.0f models", modelsFetched.(float64)) // Verify user sees personal models w = h.request("GET", "/api/v1/models/enabled", userToken, nil) var resp map[string]interface{} decode(w, &resp) userModels := resp["data"].([]interface{}) personalCount := 0 for _, raw := range userModels { m := raw.(map[string]interface{}) if m["scope"] == "personal" { personalCount++ } } if personalCount < 1 { t.Fatalf("user should see personal BYOK models, got %d", personalCount) } t.Logf(" ✓ User sees %d personal BYOK models", personalCount) // Cleanup h.request("DELETE", fmt.Sprintf("/api/v1/api-configs/%s", cfgID), userToken, nil) }