diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index f20a56b9c..c3880b565 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -1415,7 +1415,7 @@ func (al *AgentLoop) selectCandidates( history []providers.Message, ) (candidates []providers.FallbackCandidate, model string) { if agent.Router == nil || len(agent.LightCandidates) == 0 { - return agent.Candidates, agent.Model + return agent.Candidates, resolvedDirectModel(agent.Model, agent.Candidates) } _, usedLight, score := agent.Router.SelectModel(userMsg, history, agent.Model) @@ -1426,7 +1426,7 @@ func (al *AgentLoop) selectCandidates( "score": score, "threshold": agent.Router.Threshold(), }) - return agent.Candidates, agent.Model + return agent.Candidates, resolvedDirectModel(agent.Model, agent.Candidates) } logger.InfoCF("agent", "Model routing: light model selected", @@ -1436,7 +1436,14 @@ func (al *AgentLoop) selectCandidates( "score": score, "threshold": agent.Router.Threshold(), }) - return agent.LightCandidates, agent.Router.LightModel() + return agent.LightCandidates, resolvedDirectModel(agent.Router.LightModel(), agent.LightCandidates) +} + +func resolvedDirectModel(raw string, candidates []providers.FallbackCandidate) string { + if len(candidates) == 1 && strings.TrimSpace(candidates[0].Model) != "" { + return candidates[0].Model + } + return raw } // maybeSummarize triggers summarization if the session history exceeds thresholds. @@ -1728,7 +1735,7 @@ func (al *AgentLoop) retryLLMCall( ctx, []providers.Message{{Role: "user", Content: prompt}}, nil, - agent.Model, + resolvedDirectModel(agent.Model, agent.Candidates), map[string]any{ "max_tokens": agent.MaxTokens, "temperature": llmTemperature, diff --git a/pkg/agent/loop_test.go b/pkg/agent/loop_test.go index a6604e87f..0b970670f 100644 --- a/pkg/agent/loop_test.go +++ b/pkg/agent/loop_test.go @@ -220,6 +220,51 @@ func TestToolRegistry_GetDefinitions(t *testing.T) { } } +func TestSelectCandidates_UsesResolvedSingleCandidateModel(t *testing.T) { + al := &AgentLoop{} + agent := &AgentInstance{ + ID: "agent-1", + Model: "nemotron-3-nano-30b-a3b", + Candidates: []providers.FallbackCandidate{ + {Provider: "openrouter", Model: "nvidia/nemotron-3-nano-30b-a3b:free"}, + }, + } + + candidates, model := al.selectCandidates(agent, "hello", []providers.Message{{Role: "user", Content: "hello"}}) + + if len(candidates) != 1 { + t.Fatalf("len(candidates) = %d, want 1", len(candidates)) + } + if model != "nvidia/nemotron-3-nano-30b-a3b:free" { + t.Fatalf("model = %q, want resolved model", model) + } +} + +func TestRetryLLMCall_UsesResolvedSingleCandidateModel(t *testing.T) { + provider := &mockProvider{} + al := &AgentLoop{} + agent := &AgentInstance{ + ID: "agent-1", + Model: "nemotron-3-nano-30b-a3b", + MaxTokens: 1024, + Provider: provider, + Candidates: []providers.FallbackCandidate{ + {Provider: "openrouter", Model: "nvidia/nemotron-3-nano-30b-a3b:free"}, + }, + } + + resp, err := al.retryLLMCall(context.Background(), agent, "hello", 1) + if err != nil { + t.Fatalf("retryLLMCall() error = %v", err) + } + if resp == nil { + t.Fatal("retryLLMCall() response = nil, want non-nil") + } + if got := provider.LastModel(); got != "nvidia/nemotron-3-nano-30b-a3b:free" { + t.Fatalf("provider model = %q, want resolved model", got) + } +} + // TestAgentLoop_GetStartupInfo verifies startup info contains tools func TestAgentLoop_GetStartupInfo(t *testing.T) { tmpDir, err := os.MkdirTemp("", "agent-test-*") diff --git a/pkg/agent/mock_provider_test.go b/pkg/agent/mock_provider_test.go index 4962810dc..61e184248 100644 --- a/pkg/agent/mock_provider_test.go +++ b/pkg/agent/mock_provider_test.go @@ -2,11 +2,15 @@ package agent import ( "context" + "sync" "github.com/sipeed/picoclaw/pkg/providers" ) -type mockProvider struct{} +type mockProvider struct { + mu sync.Mutex + lastModel string +} func (m *mockProvider) Chat( ctx context.Context, @@ -15,6 +19,9 @@ func (m *mockProvider) Chat( model string, opts map[string]any, ) (*providers.LLMResponse, error) { + m.mu.Lock() + m.lastModel = model + m.mu.Unlock() return &providers.LLMResponse{ Content: "Mock response", ToolCalls: []providers.ToolCall{}, @@ -24,3 +31,9 @@ func (m *mockProvider) Chat( func (m *mockProvider) GetDefaultModel() string { return "mock-model" } + +func (m *mockProvider) LastModel() string { + m.mu.Lock() + defer m.mu.Unlock() + return m.lastModel +}