fix: keep utility default agent and model paired (#1318)

This commit is contained in:
Carlos Florêncio
2026-06-09 00:05:26 +01:00
committed by GitHub
parent 68e7fc7c27
commit 525e932df7
4 changed files with 224 additions and 13 deletions
+7 -5
View File
@@ -179,11 +179,13 @@ func (f fakeUtility) PreparePromptRequest(
return nil, err
}
agentCLI, model := a.AgentID, a.Model
if agentCLI == "" && defaults != nil {
agentCLI = defaults.AgentID
}
if model == "" && defaults != nil {
model = defaults.Model
if defaults != nil {
if model == "" {
agentCLI = defaults.AgentID
model = defaults.Model
} else if agentCLI == "" {
agentCLI = defaults.AgentID
}
}
return &utilityservice.PromptRequest{
UtilityID: utilityID,
@@ -92,7 +92,8 @@ func (c *Controller) GetTemplateVariables(ctx context.Context) *dto.TemplateVari
}
// PreparePromptRequest prepares a prompt request by resolving the template.
// If defaults is provided and the utility agent has empty agent_id/model, the defaults are used.
// If defaults is provided and the utility agent has no model, the default
// agent/model pair is used.
func (c *Controller) PreparePromptRequest(ctx context.Context, req dto.ExecutePromptRequest, defaults *service.DefaultUtilitySettings, sessionless bool) (*service.PromptRequest, error) {
tmplCtx := &template.Context{
GitDiff: req.GitDiff,
@@ -172,7 +172,7 @@ type DefaultUtilitySettings struct {
}
// PreparePromptRequest prepares a prompt request by resolving the template.
// If the utility agent has empty AgentID/Model, the defaults are used.
// If the utility agent has no Model, the default AgentID/Model pair is used.
// When sessionless is true, missing template variables are substituted with
// empty strings instead of being left as {{Var}}.
func (s *Service) PreparePromptRequest(ctx context.Context, utilityID string, tmplCtx *template.Context, defaults *DefaultUtilitySettings, sessionless bool) (*PromptRequest, error) {
@@ -189,14 +189,18 @@ func (s *Service) PreparePromptRequest(ctx context.Context, utilityID string, tm
return nil, err
}
// Use agent's configured values, fall back to defaults if empty
// Use agent-specific values when fully configured. If the model is empty,
// treat the default agent/model as an inseparable pair so a default model
// from one provider is never sent to another provider's ACP config.
agentCLI := agent.AgentID
model := agent.Model
if agentCLI == "" && defaults != nil {
agentCLI = defaults.AgentID
}
if model == "" && defaults != nil {
model = defaults.Model
if defaults != nil {
if model == "" {
agentCLI = defaults.AgentID
model = defaults.Model
} else if agentCLI == "" {
agentCLI = defaults.AgentID
}
}
return &PromptRequest{
@@ -0,0 +1,204 @@
package service
import (
"context"
"database/sql"
"testing"
"github.com/kandev/kandev/internal/utility/models"
"github.com/kandev/kandev/internal/utility/template"
)
type fakeRepository struct {
agents map[string]*models.UtilityAgent
}
func (r *fakeRepository) ListAgents(context.Context) ([]*models.UtilityAgent, error) {
out := make([]*models.UtilityAgent, 0, len(r.agents))
for _, agent := range r.agents {
out = append(out, agent)
}
return out, nil
}
func (r *fakeRepository) GetAgentByID(_ context.Context, id string) (*models.UtilityAgent, error) {
agent, ok := r.agents[id]
if !ok {
return nil, sql.ErrNoRows
}
return agent, nil
}
func (r *fakeRepository) GetAgentByName(_ context.Context, name string) (*models.UtilityAgent, error) {
for _, agent := range r.agents {
if agent.Name == name {
return agent, nil
}
}
return nil, sql.ErrNoRows
}
func (r *fakeRepository) CreateAgent(_ context.Context, agent *models.UtilityAgent) error {
r.agents[agent.ID] = agent
return nil
}
func (r *fakeRepository) UpdateAgent(_ context.Context, agent *models.UtilityAgent) error {
r.agents[agent.ID] = agent
return nil
}
func (r *fakeRepository) DeleteAgent(_ context.Context, id string) error {
delete(r.agents, id)
return nil
}
func (r *fakeRepository) ListCalls(context.Context, string, int) ([]*models.UtilityAgentCall, error) {
return nil, nil
}
func (r *fakeRepository) GetCallByID(context.Context, string) (*models.UtilityAgentCall, error) {
return nil, sql.ErrNoRows
}
func (r *fakeRepository) CreateCall(context.Context, *models.UtilityAgentCall) error {
return nil
}
func (r *fakeRepository) UpdateCall(context.Context, *models.UtilityAgentCall) error {
return nil
}
func TestPreparePromptRequest_UsesDefaultAgentAndModelAsPairWhenModelIsUnset(t *testing.T) {
t.Parallel()
svc := NewService(&fakeRepository{agents: map[string]*models.UtilityAgent{
"builtin-enhance-prompt": {
ID: "builtin-enhance-prompt",
Prompt: "Improve {{UserPrompt}}",
AgentID: "claude-acp",
Model: "",
Builtin: true,
},
}})
req, err := svc.PreparePromptRequest(
context.Background(),
"builtin-enhance-prompt",
&template.Context{UserPrompt: "fix the bug"},
&DefaultUtilitySettings{
AgentID: "opencode-acp",
Model: "opencode-go/deepseek-v4-flash",
},
false,
)
if err != nil {
t.Fatalf("PreparePromptRequest() error = %v", err)
}
if req.AgentCLI != "opencode-acp" {
t.Fatalf("AgentCLI = %q, want %q", req.AgentCLI, "opencode-acp")
}
if req.Model != "opencode-go/deepseek-v4-flash" {
t.Fatalf("Model = %q, want %q", req.Model, "opencode-go/deepseek-v4-flash")
}
}
func TestPreparePromptRequest_UsesDefaultAgentWhenAgentIDIsUnset(t *testing.T) {
t.Parallel()
svc := NewService(&fakeRepository{agents: map[string]*models.UtilityAgent{
"custom": {
ID: "custom",
Prompt: "Do {{UserPrompt}}",
AgentID: "",
Model: "custom-model",
},
}})
req, err := svc.PreparePromptRequest(
context.Background(),
"custom",
&template.Context{UserPrompt: "fix the bug"},
&DefaultUtilitySettings{
AgentID: "default-acp",
Model: "default-model",
},
false,
)
if err != nil {
t.Fatalf("PreparePromptRequest() error = %v", err)
}
if req.AgentCLI != "default-acp" {
t.Fatalf("AgentCLI = %q, want %q", req.AgentCLI, "default-acp")
}
if req.Model != "custom-model" {
t.Fatalf("Model = %q, want %q", req.Model, "custom-model")
}
}
func TestPreparePromptRequest_PreservesConfiguredAgentAndModelWithoutDefaults(t *testing.T) {
t.Parallel()
svc := NewService(&fakeRepository{agents: map[string]*models.UtilityAgent{
"custom": {
ID: "custom",
Prompt: "Do {{UserPrompt}}",
AgentID: "custom-acp",
Model: "custom-model",
},
}})
req, err := svc.PreparePromptRequest(
context.Background(),
"custom",
&template.Context{UserPrompt: "fix the bug"},
nil,
false,
)
if err != nil {
t.Fatalf("PreparePromptRequest() error = %v", err)
}
if req.AgentCLI != "custom-acp" {
t.Fatalf("AgentCLI = %q, want %q", req.AgentCLI, "custom-acp")
}
if req.Model != "custom-model" {
t.Fatalf("Model = %q, want %q", req.Model, "custom-model")
}
}
func TestPreparePromptRequest_IgnoresDefaultsWhenAgentAndModelAreConfigured(t *testing.T) {
t.Parallel()
svc := NewService(&fakeRepository{agents: map[string]*models.UtilityAgent{
"custom": {
ID: "custom",
Prompt: "Do {{UserPrompt}}",
AgentID: "custom-acp",
Model: "custom-model",
},
}})
req, err := svc.PreparePromptRequest(
context.Background(),
"custom",
&template.Context{UserPrompt: "fix the bug"},
&DefaultUtilitySettings{
AgentID: "default-acp",
Model: "default-model",
},
false,
)
if err != nil {
t.Fatalf("PreparePromptRequest() error = %v", err)
}
if req.AgentCLI != "custom-acp" {
t.Fatalf("AgentCLI = %q, want %q", req.AgentCLI, "custom-acp")
}
if req.Model != "custom-model" {
t.Fatalf("Model = %q, want %q", req.Model, "custom-model")
}
}