feat(settings): Add separate Auto-Fix Model setting for remediation

Add configurable model specifically for automatic remediation actions:

Backend (internal/config/ai.go):
- Add AutoFixModel field to AIConfig
- Add GetAutoFixModel() getter with fallback chain:
  AutoFixModel -> PatrolModel -> Model

Frontend (AISettings.tsx, types/ai.ts):
- Add auto_fix_model to AISettings types
- Add Auto-Fix Model dropdown (only shows when patrol_auto_fix enabled)
- Falls back to patrol model if not set

API (ai_handlers.go):
- Add auto_fix_model to response and update request
- Handle saving/loading the new field

Rationale:
- Auto-fix takes real actions, may warrant a more capable model
- Patrol observation can use cheaper models for cost savings
- Gives users granular control over model costs vs reliability
- Model hierarchy: Chat > AutoFix > Patrol > Default
This commit is contained in:
rcourtman 2025-12-12 14:35:28 +00:00
parent f98fd845e1
commit de8b36d65d
4 changed files with 78 additions and 0 deletions

View file

@ -79,6 +79,7 @@ export const AISettings: Component = () => {
model: '',
chatModel: '', // Empty means use default model
patrolModel: '', // Empty means use default model
autoFixModel: '', // Empty means use patrol model
baseUrl: '', // Legacy - kept for compatibility
clearApiKey: false,
autonomousMode: false,
@ -105,6 +106,7 @@ export const AISettings: Component = () => {
model: '', // Will be set when provider is configured
chatModel: '',
patrolModel: '',
autoFixModel: '',
baseUrl: '',
clearApiKey: false,
autonomousMode: false,
@ -130,6 +132,7 @@ export const AISettings: Component = () => {
model: data.model || '', // User must select a model
chatModel: data.chat_model || '',
patrolModel: data.patrol_model || '',
autoFixModel: data.auto_fix_model || '',
baseUrl: data.base_url || '',
clearApiKey: false,
autonomousMode: data.autonomous_mode || false,
@ -280,6 +283,10 @@ export const AISettings: Component = () => {
payload.patrol_model = form.patrolModel;
}
if (form.autoFixModel !== (settings()?.auto_fix_model || '')) {
payload.auto_fix_model = form.autoFixModel;
}
// Include multi-provider credentials if set (non-empty)
if (form.anthropicApiKey.trim()) {
payload.anthropic_api_key = form.anthropicApiKey.trim();
@ -607,6 +614,48 @@ export const AISettings: Component = () => {
</p>
</div>
{/* Auto-Fix Model Override */}
<Show when={form.patrolAutoFix}>
<div class={formField}>
<label class={labelClass()}>Auto-Fix Model (Remediation)</label>
<Show when={availableModels().length > 0} fallback={
<input
type="text"
value={form.autoFixModel}
onInput={(e) => setForm('autoFixModel', e.currentTarget.value)}
placeholder="Leave empty to use patrol model"
class={controlClass()}
disabled={saving()}
/>
}>
<select
value={form.autoFixModel}
onChange={(e) => setForm('autoFixModel', e.currentTarget.value)}
class={controlClass()}
disabled={saving()}
>
<option value="">Use patrol model ({form.patrolModel || form.model || 'not set'})</option>
<For each={Array.from(groupModelsByProvider(availableModels()).entries())}>
{([provider, models]) => (
<optgroup label={PROVIDER_DISPLAY_NAMES[provider] || provider}>
<For each={models}>
{(model) => (
<option value={model.id}>
{model.name || model.id.split(':').pop()}
</option>
)}
</For>
</optgroup>
)}
</For>
</select>
</Show>
<p class={formHelpText}>
Model for automatic remediation actions. Consider a more capable model since it takes action.
</p>
</div>
</Show>
{/* AI Provider Configuration - Configure API keys for all providers */}
<div class={`${formField} p-4 rounded-lg border bg-gradient-to-br from-purple-50 to-indigo-50 dark:from-purple-900/20 dark:to-indigo-900/20 border-purple-200 dark:border-purple-800`}>
<div class="mb-3">

View file

@ -17,6 +17,7 @@ export interface AISettings {
model: string;
chat_model?: string; // Model for interactive chat (empty = use default)
patrol_model?: string; // Model for background patrol (empty = use default)
auto_fix_model?: string; // Model for auto-fix remediation (empty = use patrol model)
base_url?: string; // DEPRECATED: legacy base URL
configured: boolean; // true if AI is ready to use
autonomous_mode: boolean; // true if AI can execute commands without approval
@ -55,6 +56,7 @@ export interface AISettingsUpdateRequest {
// Model overrides for different use cases
chat_model?: string; // Model for interactive chat
patrol_model?: string; // Model for background patrol
auto_fix_model?: string; // Model for auto-fix remediation
// Patrol settings for token efficiency
patrol_schedule_preset?: string; // DEPRECATED: use patrol_interval_minutes
patrol_interval_minutes?: number; // Custom interval in minutes (0 = disabled, minimum 10)

View file

@ -144,6 +144,7 @@ type AISettingsResponse struct {
Model string `json:"model"`
ChatModel string `json:"chat_model,omitempty"` // Model for interactive chat (empty = use default)
PatrolModel string `json:"patrol_model,omitempty"` // Model for patrol (empty = use default)
AutoFixModel string `json:"auto_fix_model,omitempty"` // Model for auto-fix (empty = use patrol model)
BaseURL string `json:"base_url,omitempty"` // DEPRECATED: legacy base URL
Configured bool `json:"configured"` // true if AI is ready to use
AutonomousMode bool `json:"autonomous_mode"` // true if AI can execute without approval
@ -154,6 +155,7 @@ type AISettingsResponse struct {
// Patrol settings for token efficiency
PatrolSchedulePreset string `json:"patrol_schedule_preset"` // DEPRECATED: legacy preset
PatrolIntervalMinutes int `json:"patrol_interval_minutes"` // Patrol interval in minutes (0 = disabled)
PatrolAutoFix bool `json:"patrol_auto_fix"` // true if patrol can auto-fix issues
AlertTriggeredAnalysis bool `json:"alert_triggered_analysis"` // true if AI analyzes when alerts fire
AvailableModels []config.ModelInfo `json:"available_models"` // List of models for current provider
// Multi-provider credentials - shows which providers are configured
@ -176,6 +178,7 @@ type AISettingsUpdateRequest struct {
Model *string `json:"model,omitempty"`
ChatModel *string `json:"chat_model,omitempty"` // Model for interactive chat
PatrolModel *string `json:"patrol_model,omitempty"` // Model for background patrol
AutoFixModel *string `json:"auto_fix_model,omitempty"` // Model for auto-fix remediation
BaseURL *string `json:"base_url,omitempty"` // DEPRECATED: use per-provider URLs
AutonomousMode *bool `json:"autonomous_mode,omitempty"`
CustomContext *string `json:"custom_context,omitempty"` // user-provided infrastructure context
@ -183,6 +186,7 @@ type AISettingsUpdateRequest struct {
// Patrol settings for token efficiency
PatrolSchedulePreset *string `json:"patrol_schedule_preset,omitempty"` // DEPRECATED: use patrol_interval_minutes
PatrolIntervalMinutes *int `json:"patrol_interval_minutes,omitempty"` // Custom interval in minutes (0 = disabled, minimum 10)
PatrolAutoFix *bool `json:"patrol_auto_fix,omitempty"` // true if patrol can auto-fix issues
AlertTriggeredAnalysis *bool `json:"alert_triggered_analysis,omitempty"` // true if AI analyzes when alerts fire
// Multi-provider credentials
AnthropicAPIKey *string `json:"anthropic_api_key,omitempty"` // Set Anthropic API key
@ -230,6 +234,7 @@ func (h *AISettingsHandler) HandleGetAISettings(w http.ResponseWriter, r *http.R
Model: settings.GetModel(),
ChatModel: settings.ChatModel,
PatrolModel: settings.PatrolModel,
AutoFixModel: settings.AutoFixModel,
BaseURL: settings.BaseURL,
Configured: settings.IsConfigured(),
AutonomousMode: settings.AutonomousMode,
@ -239,6 +244,7 @@ func (h *AISettingsHandler) HandleGetAISettings(w http.ResponseWriter, r *http.R
// Patrol settings
PatrolSchedulePreset: settings.PatrolSchedulePreset,
PatrolIntervalMinutes: settings.PatrolIntervalMinutes,
PatrolAutoFix: settings.PatrolAutoFix,
AlertTriggeredAnalysis: settings.AlertTriggeredAnalysis,
AvailableModels: nil, // Now populated via /api/ai/models endpoint
// Multi-provider configuration
@ -332,6 +338,14 @@ func (h *AISettingsHandler) HandleUpdateAISettings(w http.ResponseWriter, r *htt
settings.PatrolModel = strings.TrimSpace(*req.PatrolModel)
}
if req.AutoFixModel != nil {
settings.AutoFixModel = strings.TrimSpace(*req.AutoFixModel)
}
if req.PatrolAutoFix != nil {
settings.PatrolAutoFix = *req.PatrolAutoFix
}
if req.BaseURL != nil {
settings.BaseURL = strings.TrimSpace(*req.BaseURL)
}
@ -474,6 +488,7 @@ func (h *AISettingsHandler) HandleUpdateAISettings(w http.ResponseWriter, r *htt
Model: settings.GetModel(),
ChatModel: settings.ChatModel,
PatrolModel: settings.PatrolModel,
AutoFixModel: settings.AutoFixModel,
BaseURL: settings.BaseURL,
Configured: settings.IsConfigured(),
AutonomousMode: settings.AutonomousMode,
@ -482,6 +497,7 @@ func (h *AISettingsHandler) HandleUpdateAISettings(w http.ResponseWriter, r *htt
OAuthConnected: settings.OAuthAccessToken != "",
PatrolSchedulePreset: settings.PatrolSchedulePreset,
PatrolIntervalMinutes: settings.PatrolIntervalMinutes,
PatrolAutoFix: settings.PatrolAutoFix,
AlertTriggeredAnalysis: settings.AlertTriggeredAnalysis,
AvailableModels: nil, // Now populated via /api/ai/models endpoint
// Multi-provider configuration

View file

@ -47,6 +47,7 @@ type AIConfig struct {
PatrolAnalyzeDocker bool `json:"patrol_analyze_docker,omitempty"` // Include Docker hosts in patrol
PatrolAnalyzeStorage bool `json:"patrol_analyze_storage,omitempty"` // Include storage in patrol
PatrolAutoFix bool `json:"patrol_auto_fix,omitempty"` // When true, patrol can attempt automatic remediation (default: false, observe only)
AutoFixModel string `json:"auto_fix_model,omitempty"` // Model for automatic remediation (defaults to PatrolModel, may want more capable model)
// Alert-triggered AI analysis - analyze specific resources when alerts fire
AlertTriggeredAnalysis bool `json:"alert_triggered_analysis,omitempty"` // Enable AI analysis when alerts fire (token-efficient)
@ -317,6 +318,16 @@ func (c *AIConfig) GetPatrolModel() string {
return c.GetModel()
}
// GetAutoFixModel returns the model for automatic remediation actions
// Falls back to PatrolModel, then to the main Model if AutoFixModel is not set
// Auto-fix may warrant a more capable model since it takes actions
func (c *AIConfig) GetAutoFixModel() string {
if c.AutoFixModel != "" {
return c.AutoFixModel
}
return c.GetPatrolModel()
}
// ClearOAuthTokens clears OAuth tokens (used when switching back to API key auth)
func (c *AIConfig) ClearOAuthTokens() {
c.OAuthAccessToken = ""