Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 14 additions & 13 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1298,27 +1298,28 @@ export class ClineProvider

/**
* Updates the current task's API handler if the provider or model has changed.
* This prevents unnecessary context condensing when only non-model settings change.
* Also synchronizes the task.apiConfiguration so subsequent comparisons and logic
* (protocol selection, reasoning display, model metadata) use the latest profile.
* @param providerSettings The new provider settings to apply
*/
private updateTaskApiHandlerIfNeeded(providerSettings: ProviderSettings): void {
const task = this.getCurrentTask()
if (!task) return

if (task && task.apiConfiguration) {
// Only rebuild API handler if provider or model actually changed
// to avoid triggering unnecessary context condensing
const currentProvider = task.apiConfiguration.apiProvider
const newProvider = providerSettings.apiProvider
const currentModelId = getModelId(task.apiConfiguration)
const newModelId = getModelId(providerSettings)
// Determine if we need to rebuild using the previous configuration snapshot
const prevConfig = task.apiConfiguration
const prevProvider = prevConfig?.apiProvider
const prevModelId = prevConfig ? getModelId(prevConfig) : undefined
const newProvider = providerSettings.apiProvider
const newModelId = getModelId(providerSettings)

if (currentProvider !== newProvider || currentModelId !== newModelId) {
task.api = buildApiHandler(providerSettings)
}
} else if (task) {
// Fallback: rebuild if apiConfiguration is not available
if (prevProvider !== newProvider || prevModelId !== newModelId) {
task.api = buildApiHandler(providerSettings)
}

// Always sync the task's apiConfiguration with the latest provider settings.
// Note: Task.apiConfiguration is declared readonly in types, so we cast to any for runtime update.
;(task as any).apiConfiguration = providerSettings
}

getProviderProfileEntries(): ProviderSettingsEntry[] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ describe("ClineProvider - API Handler Rebuild Guard", () => {
})

describe("upsertProviderProfile", () => {
test("does NOT rebuild API handler when provider and model unchanged", async () => {
test("does NOT rebuild API handler when provider and model unchanged, but task.apiConfiguration is synced", async () => {
// Create a task with the current config
const mockTask = new Task({
...defaultTaskOptions,
Expand Down Expand Up @@ -289,6 +289,10 @@ describe("ClineProvider - API Handler Rebuild Guard", () => {
expect(buildApiHandlerMock).not.toHaveBeenCalled()
// Verify the task's api property was NOT reassigned (still same reference)
expect(mockTask.api).toBe(originalApi)
// Verify task.apiConfiguration was synchronized with non-model fields
expect((mockTask as any).apiConfiguration.openRouterModelId).toBe("openai/gpt-4")
expect((mockTask as any).apiConfiguration.rateLimitSeconds).toBe(5)
expect((mockTask as any).apiConfiguration.modelTemperature).toBe(0.7)
})

test("rebuilds API handler when provider changes", async () => {
Expand Down Expand Up @@ -386,12 +390,13 @@ describe("ClineProvider - API Handler Rebuild Guard", () => {
})

describe("activateProviderProfile", () => {
test("does NOT rebuild API handler when provider and model unchanged", async () => {
test("does NOT rebuild API handler when provider and model unchanged, but task.apiConfiguration is synced", async () => {
const mockTask = new Task({
...defaultTaskOptions,
apiConfiguration: {
apiProvider: "openrouter",
openRouterModelId: "openai/gpt-4",
modelTemperature: 0.3,
},
})
const originalApi = {
Expand All @@ -406,12 +411,14 @@ describe("ClineProvider - API Handler Rebuild Guard", () => {

buildApiHandlerMock.mockClear()

// Mock activateProfile to return same provider/model
// Mock activateProfile to return same provider/model but different non-model setting
;(provider as any).providerSettingsManager.activateProfile = vi.fn().mockResolvedValue({
name: "test-config",
id: "test-id",
apiProvider: "openrouter",
openRouterModelId: "openai/gpt-4",
modelTemperature: 0.9,
rateLimitSeconds: 7,
})

await provider.activateProviderProfile({ name: "test-config" })
Expand All @@ -420,9 +427,13 @@ describe("ClineProvider - API Handler Rebuild Guard", () => {
expect(buildApiHandlerMock).not.toHaveBeenCalled()
// Verify the API reference wasn't changed
expect(mockTask.api).toBe(originalApi)
// Verify task.apiConfiguration was synchronized
expect((mockTask as any).apiConfiguration.openRouterModelId).toBe("openai/gpt-4")
expect((mockTask as any).apiConfiguration.modelTemperature).toBe(0.9)
expect((mockTask as any).apiConfiguration.rateLimitSeconds).toBe(7)
})

test("rebuilds API handler when provider changes", async () => {
test("rebuilds API handler when provider changes and syncs task.apiConfiguration", async () => {
const mockTask = new Task({
...defaultTaskOptions,
apiConfiguration: {
Expand Down Expand Up @@ -458,9 +469,12 @@ describe("ClineProvider - API Handler Rebuild Guard", () => {
apiModelId: "claude-3-5-sonnet-20241022",
}),
)
// And task.apiConfiguration synced
expect((mockTask as any).apiConfiguration.apiProvider).toBe("anthropic")
expect((mockTask as any).apiConfiguration.apiModelId).toBe("claude-3-5-sonnet-20241022")
})

test("rebuilds API handler when model changes", async () => {
test("rebuilds API handler when model changes and syncs task.apiConfiguration", async () => {
const mockTask = new Task({
...defaultTaskOptions,
apiConfiguration: {
Expand Down Expand Up @@ -496,6 +510,57 @@ describe("ClineProvider - API Handler Rebuild Guard", () => {
openRouterModelId: "anthropic/claude-3-5-sonnet-20241022",
}),
)
// And task.apiConfiguration synced
expect((mockTask as any).apiConfiguration.apiProvider).toBe("openrouter")
expect((mockTask as any).apiConfiguration.openRouterModelId).toBe("anthropic/claude-3-5-sonnet-20241022")
})
})

describe("profile switching sequence", () => {
test("A -> B -> A updates task.apiConfiguration each time", async () => {
const mockTask = new Task({
...defaultTaskOptions,
apiConfiguration: {
apiProvider: "openrouter",
openRouterModelId: "openai/gpt-4",
},
})
mockTask.api = {
getModel: vi.fn().mockReturnValue({
id: "openai/gpt-4",
info: { contextWindow: 128000 },
}),
} as any

await provider.addClineToStack(mockTask)

// First switch: A -> B (openrouter -> anthropic)
buildApiHandlerMock.mockClear()
;(provider as any).providerSettingsManager.activateProfile = vi.fn().mockResolvedValue({
name: "anthropic-config",
id: "anthropic-id",
apiProvider: "anthropic",
apiModelId: "claude-3-5-sonnet-20241022",
})
await provider.activateProviderProfile({ name: "anthropic-config" })

expect(buildApiHandlerMock).toHaveBeenCalled()
expect((mockTask as any).apiConfiguration.apiProvider).toBe("anthropic")
expect((mockTask as any).apiConfiguration.apiModelId).toBe("claude-3-5-sonnet-20241022")

// Second switch: B -> A (anthropic -> openrouter gpt-4)
buildApiHandlerMock.mockClear()
;(provider as any).providerSettingsManager.activateProfile = vi.fn().mockResolvedValue({
name: "test-config",
id: "test-id",
apiProvider: "openrouter",
openRouterModelId: "openai/gpt-4",
})
await provider.activateProviderProfile({ name: "test-config" })

// API handler may or may not rebuild depending on mock model id, but apiConfiguration must be updated
expect((mockTask as any).apiConfiguration.apiProvider).toBe("openrouter")
expect((mockTask as any).apiConfiguration.openRouterModelId).toBe("openai/gpt-4")
})
})

Expand Down