diff --git a/pkg/agent/instance.go b/pkg/agent/instance.go index ac2955334..63aac150b 100644 --- a/pkg/agent/instance.go +++ b/pkg/agent/instance.go @@ -150,6 +150,7 @@ func NewAgentInstance( subagents = agentCfg.Subagents skillsFilter = resolveAgentSkillsFilter(agentCfg, definition) } + provider = resolvePrimaryProviderForAgent(cfg, workspace, agentID, model, provider) warnOnUnknownAgentMCPServerDeclarations(agentID, workspace, cfg, definition) maxIter := defaults.MaxToolIterations @@ -305,6 +306,47 @@ func populateCandidateProvidersFromNames( } } +// resolvePrimaryProviderForAgent resolves a dedicated provider for the active +// primary model when the model points at a model_list entry. This keeps the +// agent's single-candidate path aligned with the selected model's own +// provider/api_base/api_key instead of inheriting the process default provider. +func resolvePrimaryProviderForAgent( + cfg *config.Config, + workspace string, + agentID string, + model string, + fallback providers.LLMProvider, +) providers.LLMProvider { + model = strings.TrimSpace(model) + if cfg == nil || model == "" { + return fallback + } + + modelCfg := lookupModelConfigByRef(cfg, model) + if modelCfg == nil { + return fallback + } + clone := *modelCfg + if clone.Workspace == "" { + clone.Workspace = workspace + } + + resolvedProvider, _, err := providers.CreateProviderFromConfig(&clone) + if err != nil { + logger.WarnCF("agent", "Primary model provider init failed; using injected provider", + map[string]any{ + "agent_id": agentID, + "model": model, + "error": err.Error(), + }) + return fallback + } + if resolvedProvider == nil { + return fallback + } + return resolvedProvider +} + // resolveAgentWorkspace determines the workspace directory for an agent. func resolveAgentWorkspace(agentCfg *config.AgentConfig, defaults *config.AgentDefaults) string { if agentCfg != nil && strings.TrimSpace(agentCfg.Workspace) != "" { diff --git a/pkg/agent/instance_test.go b/pkg/agent/instance_test.go index 2b144914e..97a5dde67 100644 --- a/pkg/agent/instance_test.go +++ b/pkg/agent/instance_test.go @@ -666,6 +666,57 @@ Use frontmatter identity. } } +func TestNewAgentInstance_UsesResolvedProviderForFrontmatterPrimaryModel(t *testing.T) { + workspace := setupWorkspace(t, map[string]string{ + "AGENT.md": `--- +model: claude-frontmatter +--- +# Agent +`, + }) + defer cleanupWorkspace(t, workspace) + + cfg := &config.Config{ + Agents: config.AgentsConfig{ + Defaults: config.AgentDefaults{ + Workspace: workspace, + Provider: "openai", + ModelName: "default-model", + }, + }, + ModelList: []*config.ModelConfig{ + { + ModelName: "claude-frontmatter", + Model: "anthropic/claude-3-7-sonnet", + APIKeys: config.SimpleSecureStrings("test-anthropic-key"), + Workspace: workspace, + }, + }, + } + + defaultProvider := &mockProvider{} + agent := NewAgentInstance(&config.AgentConfig{ + ID: "research", + Workspace: workspace, + }, &cfg.Agents.Defaults, cfg, defaultProvider) + + if agent.Model != "claude-frontmatter" { + t.Fatalf("agent.Model = %q, want %q", agent.Model, "claude-frontmatter") + } + if len(agent.Candidates) != 1 { + t.Fatalf("len(agent.Candidates) = %d, want 1", len(agent.Candidates)) + } + if got := agent.Candidates[0].Provider; got != "anthropic" { + t.Fatalf("primary candidate provider = %q, want %q", got, "anthropic") + } + if got := agent.Candidates[0].Model; got != "claude-3-7-sonnet" { + t.Fatalf("primary candidate model = %q, want %q", got, "claude-3-7-sonnet") + } + if agent.Provider == defaultProvider { + t.Fatal("expected primary provider to be resolved from model_list instead of using injected default provider") + } +} + func TestNewAgentInstance_InvalidFrontmatterFailsClosedForToolsAndMCPServers(t *testing.T) { workspace := setupWorkspace(t, map[string]string{ "AGENT.md": `---