Respect the patrol model's provider in QuickAnalysis (#1360)

Back-port v5 fix 5f372e257 to v6. QuickAnalysis now builds a provider for
the configured PatrolModel via providers.NewForModel (matching the 8 other
call sites that already do so), falling back to the default provider if it
cannot. Previously a patrol model on a different provider than the default
(e.g. default Gemini, patrol OpenAI) was sent to the wrong backend. Guard
changed from provider==nil to cfg disabled, with a re-check after provider
selection. Updates the existing fallback test and adds a positive
patrol-provider test (httptest OpenAI server).
This commit is contained in:
rcourtman
2026-06-04 10:12:07 +01:00
parent 8855b78c0d
commit b39cfc3a0e
2 changed files with 94 additions and 12 deletions
+21 -7
View File
@@ -1752,22 +1752,36 @@ func (s *Service) createPatrolProviderForModel(modelStr string) (providers.Strea
// It uses a single-turn, no-tools call for efficiency.
func (s *Service) QuickAnalysis(ctx context.Context, req QuickAnalysisRequest) (string, error) {
s.mu.RLock()
provider := s.provider
defaultProvider := s.provider
cfg := s.cfg
costStore := s.costStore
s.mu.RUnlock()
if cfg == nil || !cfg.Enabled {
return "", fmt.Errorf("Pulse Assistant is not enabled or configured")
}
// Use the configured patrol model and create a provider for that exact model,
// so quick patrol analysis goes to the selected Patrol provider instead of
// whichever default provider the service booted with (#1360).
model := ""
if cfg.PatrolModel != "" {
model = cfg.PatrolModel
}
provider := defaultProvider
if model != "" {
if modelProvider, err := providers.NewForModel(cfg, model); err == nil {
provider = modelProvider
} else {
log.Debug().Err(err).Str("model", model).Msg("Could not create provider for patrol quick analysis, using default")
}
}
if provider == nil {
return "", fmt.Errorf("Pulse Assistant is not enabled or configured")
}
// Use a fast model for quick analysis if available
model := ""
if cfg != nil && cfg.PatrolModel != "" {
model = cfg.PatrolModel
}
sanitizerModel := model
if sanitizerModel == "" && cfg != nil {
if sanitizerModel == "" {
sanitizerModel = cfg.GetChatModel()
}
+73 -5
View File
@@ -2,6 +2,9 @@ package ai
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
@@ -23,11 +26,12 @@ func TestService_QuickAnalysis(t *testing.T) {
t.Errorf("Expected error about provider not enabled, got: %v", err)
}
// Case 2: Configured
// Case 2: Configured without a patrol model -> falls back to the default
// provider with no model override.
mockProv := &mockProvider{
chatFunc: func(ctx context.Context, req providers.ChatRequest) (*providers.ChatResponse, error) {
if req.Model != "fast-model" {
return nil, nil // Should use patrol model
if req.Model != "" {
t.Fatalf("expected default-provider fallback with empty model override, got %q", req.Model)
}
if req.ExecutionID != "patrol-run-123" {
t.Fatalf("execution_id=%q want patrol-run-123", req.ExecutionID)
@@ -39,8 +43,7 @@ func TestService_QuickAnalysis(t *testing.T) {
}
svc.provider = mockProv
svc.cfg = &config.AIConfig{
Enabled: true,
PatrolModel: "fast-model",
Enabled: true,
}
res, err := svc.QuickAnalysis(context.Background(), QuickAnalysisRequest{
@@ -65,6 +68,71 @@ func TestService_QuickAnalysis(t *testing.T) {
}
}
func TestService_QuickAnalysis_UsesPatrolModelProviderInsteadOfDefaultProvider(t *testing.T) {
t.Parallel()
openAI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/chat/completions" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
var req map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
t.Fatalf("decode request: %v", err)
}
if got, _ := req["model"].(string); got != "gpt-4o-mini" {
t.Fatalf("model = %q, want gpt-4o-mini", got)
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"id": "chatcmpl-test",
"object": "chat.completion",
"created": 1,
"model": "gpt-4o-mini",
"choices": []map[string]interface{}{
{
"index": 0,
"message": map[string]interface{}{
"role": "assistant",
"content": "analysis from patrol model",
},
"finish_reason": "stop",
},
},
"usage": map[string]interface{}{
"prompt_tokens": 11,
"completion_tokens": 7,
"total_tokens": 18,
},
})
}))
defer openAI.Close()
svc := NewService(nil, nil)
svc.provider = &mockProvider{
chatFunc: func(ctx context.Context, req providers.ChatRequest) (*providers.ChatResponse, error) {
t.Fatal("expected QuickAnalysis to avoid the stale default provider")
return nil, nil
},
}
svc.cfg = &config.AIConfig{
Enabled: true,
Model: "gemini:gemini-2.5-pro",
PatrolModel: "openai:gpt-4o-mini",
OpenAIAPIKey: "test-key",
OpenAIBaseURL: openAI.URL,
}
res, err := svc.QuickAnalysis(context.Background(), QuickAnalysisRequest{Prompt: "Analysis prompt"})
if err != nil {
t.Fatalf("QuickAnalysis failed: %v", err)
}
if res != "analysis from patrol model" {
t.Fatalf("unexpected result: %s", res)
}
}
func TestService_QuickAnalysisSanitizesExternalModelRequest(t *testing.T) {
svc := NewService(nil, nil)
resource := unifiedresources.Resource{