diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index aa459bd737..676f6cbd0e 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1314,10 +1314,16 @@ export class ClineProvider if (currentProvider !== newProvider || currentModelId !== newModelId) { task.api = buildApiHandler(providerSettings) + // Update task.apiConfiguration to keep it in sync with the active API handler + // This prevents the "switch back" bug where the comparison would incorrectly + // skip rebuilding when switching back to a previously used model + task.apiConfiguration = providerSettings } } else if (task) { // Fallback: rebuild if apiConfiguration is not available task.api = buildApiHandler(providerSettings) + // Also update apiConfiguration in the fallback case + task.apiConfiguration = providerSettings } } diff --git a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts index 4037d9df26..513288892a 100644 --- a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts @@ -497,6 +497,99 @@ describe("ClineProvider - API Handler Rebuild Guard", () => { }), ) }) + + test("handles A→B→A switching correctly (regression test for #9179)", async () => { + // Start with Model A (reasoning-enabled) + const mockTask = new Task({ + ...defaultTaskOptions, + apiConfiguration: { + apiProvider: "openrouter", + openRouterModelId: "openai/o1-preview", + }, + }) + const originalApiA = { + getModel: vi.fn().mockReturnValue({ + id: "openai/o1-preview", + info: { contextWindow: 128000 }, + }), + } + mockTask.api = originalApiA as any + + await provider.addClineToStack(mockTask) + + buildApiHandlerMock.mockClear() + + // Switch to Model B (no reasoning) + const newApiB = { + getModel: vi.fn().mockReturnValue({ + id: "openai/gpt-4", + info: { contextWindow: 128000 }, + }), + } + buildApiHandlerMock.mockReturnValue(newApiB) + + await provider.upsertProviderProfile( + "model-b-config", + { + apiProvider: "openrouter", + openRouterModelId: "openai/gpt-4", + }, + true, + ) + + // Verify API handler was rebuilt for Model B + expect(buildApiHandlerMock).toHaveBeenCalledWith( + expect.objectContaining({ + apiProvider: "openrouter", + openRouterModelId: "openai/gpt-4", + }), + ) + expect(mockTask.api).toBe(newApiB) + // Verify task.apiConfiguration was updated to Model B + expect(mockTask.apiConfiguration).toEqual( + expect.objectContaining({ + apiProvider: "openrouter", + openRouterModelId: "openai/gpt-4", + }), + ) + + buildApiHandlerMock.mockClear() + + // Switch back to Model A (this is where the bug occurred) + const newApiA = { + getModel: vi.fn().mockReturnValue({ + id: "openai/o1-preview", + info: { contextWindow: 128000 }, + }), + } + buildApiHandlerMock.mockReturnValue(newApiA) + + await provider.upsertProviderProfile( + "model-a-config", + { + apiProvider: "openrouter", + openRouterModelId: "openai/o1-preview", + }, + true, + ) + + // Verify API handler WAS rebuilt when switching back to Model A + // This is the key test - without the fix, this would fail + expect(buildApiHandlerMock).toHaveBeenCalledWith( + expect.objectContaining({ + apiProvider: "openrouter", + openRouterModelId: "openai/o1-preview", + }), + ) + expect(mockTask.api).toBe(newApiA) + // Verify task.apiConfiguration was updated back to Model A + expect(mockTask.apiConfiguration).toEqual( + expect.objectContaining({ + apiProvider: "openrouter", + openRouterModelId: "openai/o1-preview", + }), + ) + }) }) describe("getModelId helper", () => {