fix: resolve correct provider for plan_model with different provider prefix

When plan_model uses a different provider than the main model (e.g.,
plan_model="openai/gpt-5.2" with main provider=minimax), the single-
candidate code path was sending the raw model string to the wrong
provider. Now uses resolveProvider() to create the correct provider
instance and pass the stripped model name.

Also adds "openai" to normalizeModel's prefix list as a defensive
measure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-02-24 06:59:18 +09:00
parent 1ebdd51d7e
commit 886673415a
4 changed files with 81 additions and 1 deletions

View file

@ -1668,6 +1668,11 @@ func (al *AgentLoop) runLLMIteration(
}
return fbResult.Response, nil
}
if len(candidates) > 0 {
c := candidates[0]
p := al.resolveProvider(c.Provider, c.Model, agent.Provider)
return doCall(ctx, p, c.Model)
}
return doCall(ctx, agent.Provider, primaryModel)
}

View file

@ -2505,6 +2505,75 @@ func TestAgentLoop_PlanModel_NotUsedDuringExecuting(t *testing.T) {
}
}
func TestAgentLoop_PlanModel_ResolvesProviderForSingleCandidate(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "agent-test-planmodel-resolve-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
cfg := &config.Config{
Agents: config.AgentsConfig{
Defaults: config.AgentDefaults{
Workspace: tmpDir,
Model: "MiniMax-M2.5",
PlanModel: "openai/gpt-5.2",
MaxTokens: 4096,
MaxToolIterations: 2,
},
},
}
msgBus := bus.NewMessageBus()
// The main provider simulates the wrong provider (e.g. MiniMax).
mainProvider := &modelCapturingMockProvider{response: "wrong provider response"}
al := NewAgentLoop(cfg, msgBus, mainProvider)
// Inject a mock provider into the cache so resolveProvider returns it
// for the "openai/gpt-5.2" candidate (provider="openai", model="gpt-5.2").
resolvedProvider := &modelCapturingMockProvider{response: "correct provider response"}
al.providerCache["openai/gpt-5.2"] = resolvedProvider
// Write MEMORY.md with interviewing status to activate plan model
memoryDir := filepath.Join(tmpDir, "memory")
os.MkdirAll(memoryDir, 0o755)
memoryPath := filepath.Join(memoryDir, "MEMORY.md")
memoryContent := "# Active Plan\n\n> Task: Test provider resolution\n> Status: interviewing\n> Phase: 1\n"
if err := os.WriteFile(memoryPath, []byte(memoryContent), 0o644); err != nil {
t.Fatalf("Failed to write MEMORY.md: %v", err)
}
_, err = al.ProcessDirectWithChannel(
context.Background(),
"Hello, resolve provider test",
"test-resolve-session",
"test",
"test-chat",
)
if err != nil {
t.Fatalf("ProcessDirectWithChannel failed: %v", err)
}
resolvedProvider.mu.Lock()
defer resolvedProvider.mu.Unlock()
mainProvider.mu.Lock()
defer mainProvider.mu.Unlock()
// The resolved provider should have been called with the stripped model name
if len(resolvedProvider.models) == 0 {
t.Fatal("Expected resolved provider to receive Chat call, but it got none")
}
if resolvedProvider.models[0] != "gpt-5.2" {
t.Errorf("Expected resolved provider to receive model 'gpt-5.2', got %q", resolvedProvider.models[0])
}
// The main provider should NOT have been called for the LLM request
if len(mainProvider.models) > 0 {
t.Errorf("Expected main provider to receive no Chat calls during plan model phase, got %d calls with models %v",
len(mainProvider.models), mainProvider.models)
}
}
func TestPlanCommand_StartClear(t *testing.T) {
al, cleanup := newTestAgentLoop(t)
defer cleanup()

View file

@ -470,7 +470,7 @@ func normalizeModel(model, apiBase string) string {
prefix := strings.ToLower(model[:idx])
switch prefix {
case "moonshot", "nvidia", "groq", "ollama", "deepseek", "google", "openrouter", "zhipu", "minimax", "mistral":
case "openai", "moonshot", "nvidia", "groq", "ollama", "deepseek", "google", "openrouter", "zhipu", "minimax", "mistral":
return model[idx+1:]
default:
return model

View file

@ -287,6 +287,12 @@ func TestNormalizeModel_UsesAPIBase(t *testing.T) {
}
}
func TestNormalizeModel_OpenAIPrefix(t *testing.T) {
if got := normalizeModel("openai/gpt-5.2", "https://api.openai.com/v1"); got != "gpt-5.2" {
t.Fatalf("normalizeModel(openai/gpt-5.2) = %q, want %q", got, "gpt-5.2")
}
}
func TestProviderChat_StreamingTextResponse(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/text/chatcompletion_v2" {