Skip to content

Commit 2c91724

Browse files
fix: Model switch re-applies selected profile (sync task.apiConfiguration) (#9179) (#9181)
1 parent 6e63413 commit 2c91724

File tree

2 files changed

+84
-18
lines changed

2 files changed

+84
-18
lines changed

src/core/webview/ClineProvider.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,27 +1298,28 @@ export class ClineProvider
12981298

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

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

1315-
if (currentProvider !== newProvider || currentModelId !== newModelId) {
1316-
task.api = buildApiHandler(providerSettings)
1317-
}
1318-
} else if (task) {
1319-
// Fallback: rebuild if apiConfiguration is not available
1316+
if (prevProvider !== newProvider || prevModelId !== newModelId) {
13201317
task.api = buildApiHandler(providerSettings)
13211318
}
1319+
1320+
// Always sync the task's apiConfiguration with the latest provider settings.
1321+
// Note: Task.apiConfiguration is declared readonly in types, so we cast to any for runtime update.
1322+
;(task as any).apiConfiguration = providerSettings
13221323
}
13231324

13241325
getProviderProfileEntries(): ProviderSettingsEntry[] {

src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ describe("ClineProvider - API Handler Rebuild Guard", () => {
250250
})
251251

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

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

388392
describe("activateProviderProfile", () => {
389-
test("does NOT rebuild API handler when provider and model unchanged", async () => {
393+
test("does NOT rebuild API handler when provider and model unchanged, but task.apiConfiguration is synced", async () => {
390394
const mockTask = new Task({
391395
...defaultTaskOptions,
392396
apiConfiguration: {
393397
apiProvider: "openrouter",
394398
openRouterModelId: "openai/gpt-4",
399+
modelTemperature: 0.3,
395400
},
396401
})
397402
const originalApi = {
@@ -406,12 +411,14 @@ describe("ClineProvider - API Handler Rebuild Guard", () => {
406411

407412
buildApiHandlerMock.mockClear()
408413

409-
// Mock activateProfile to return same provider/model
414+
// Mock activateProfile to return same provider/model but different non-model setting
410415
;(provider as any).providerSettingsManager.activateProfile = vi.fn().mockResolvedValue({
411416
name: "test-config",
412417
id: "test-id",
413418
apiProvider: "openrouter",
414419
openRouterModelId: "openai/gpt-4",
420+
modelTemperature: 0.9,
421+
rateLimitSeconds: 7,
415422
})
416423

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

425-
test("rebuilds API handler when provider changes", async () => {
436+
test("rebuilds API handler when provider changes and syncs task.apiConfiguration", async () => {
426437
const mockTask = new Task({
427438
...defaultTaskOptions,
428439
apiConfiguration: {
@@ -458,9 +469,12 @@ describe("ClineProvider - API Handler Rebuild Guard", () => {
458469
apiModelId: "claude-3-5-sonnet-20241022",
459470
}),
460471
)
472+
// And task.apiConfiguration synced
473+
expect((mockTask as any).apiConfiguration.apiProvider).toBe("anthropic")
474+
expect((mockTask as any).apiConfiguration.apiModelId).toBe("claude-3-5-sonnet-20241022")
461475
})
462476

463-
test("rebuilds API handler when model changes", async () => {
477+
test("rebuilds API handler when model changes and syncs task.apiConfiguration", async () => {
464478
const mockTask = new Task({
465479
...defaultTaskOptions,
466480
apiConfiguration: {
@@ -496,6 +510,57 @@ describe("ClineProvider - API Handler Rebuild Guard", () => {
496510
openRouterModelId: "anthropic/claude-3-5-sonnet-20241022",
497511
}),
498512
)
513+
// And task.apiConfiguration synced
514+
expect((mockTask as any).apiConfiguration.apiProvider).toBe("openrouter")
515+
expect((mockTask as any).apiConfiguration.openRouterModelId).toBe("anthropic/claude-3-5-sonnet-20241022")
516+
})
517+
})
518+
519+
describe("profile switching sequence", () => {
520+
test("A -> B -> A updates task.apiConfiguration each time", async () => {
521+
const mockTask = new Task({
522+
...defaultTaskOptions,
523+
apiConfiguration: {
524+
apiProvider: "openrouter",
525+
openRouterModelId: "openai/gpt-4",
526+
},
527+
})
528+
mockTask.api = {
529+
getModel: vi.fn().mockReturnValue({
530+
id: "openai/gpt-4",
531+
info: { contextWindow: 128000 },
532+
}),
533+
} as any
534+
535+
await provider.addClineToStack(mockTask)
536+
537+
// First switch: A -> B (openrouter -> anthropic)
538+
buildApiHandlerMock.mockClear()
539+
;(provider as any).providerSettingsManager.activateProfile = vi.fn().mockResolvedValue({
540+
name: "anthropic-config",
541+
id: "anthropic-id",
542+
apiProvider: "anthropic",
543+
apiModelId: "claude-3-5-sonnet-20241022",
544+
})
545+
await provider.activateProviderProfile({ name: "anthropic-config" })
546+
547+
expect(buildApiHandlerMock).toHaveBeenCalled()
548+
expect((mockTask as any).apiConfiguration.apiProvider).toBe("anthropic")
549+
expect((mockTask as any).apiConfiguration.apiModelId).toBe("claude-3-5-sonnet-20241022")
550+
551+
// Second switch: B -> A (anthropic -> openrouter gpt-4)
552+
buildApiHandlerMock.mockClear()
553+
;(provider as any).providerSettingsManager.activateProfile = vi.fn().mockResolvedValue({
554+
name: "test-config",
555+
id: "test-id",
556+
apiProvider: "openrouter",
557+
openRouterModelId: "openai/gpt-4",
558+
})
559+
await provider.activateProviderProfile({ name: "test-config" })
560+
561+
// API handler may or may not rebuild depending on mock model id, but apiConfiguration must be updated
562+
expect((mockTask as any).apiConfiguration.apiProvider).toBe("openrouter")
563+
expect((mockTask as any).apiConfiguration.openRouterModelId).toBe("openai/gpt-4")
499564
})
500565
})
501566

0 commit comments

Comments
 (0)