From a3a4581be32af43a94022313e587bf6476f445da Mon Sep 17 00:00:00 2001 From: Roo Code Date: Thu, 13 Nov 2025 21:13:50 +0000 Subject: [PATCH] fix: preserve credentials when switching between providers - Modified ProviderSettingsManager.saveConfig to preserve credential fields from existing configs - Added comprehensive list of credential and configuration fields to preserve - Added tests to verify Vertex AI, AWS Bedrock, and other provider credentials are preserved - Fixes issue where GCP Vertex credentials were being cleared intermittently Fixes #9244 --- src/core/config/ProviderSettingsManager.ts | 91 +++++++++++- .../__tests__/ProviderSettingsManager.spec.ts | 140 +++++++++++++++++- 2 files changed, 228 insertions(+), 3 deletions(-) diff --git a/src/core/config/ProviderSettingsManager.ts b/src/core/config/ProviderSettingsManager.ts index ea3e18f3ef..ed3732f3dd 100644 --- a/src/core/config/ProviderSettingsManager.ts +++ b/src/core/config/ProviderSettingsManager.ts @@ -375,12 +375,99 @@ export class ProviderSettingsManager { return await this.lock(async () => { const providerProfiles = await this.load() // Preserve the existing ID if this is an update to an existing config. - const existingId = providerProfiles.apiConfigs[name]?.id + const existingConfig = providerProfiles.apiConfigs[name] + const existingId = existingConfig?.id const id = config.id || existingId || this.generateId() // Filter out settings from other providers. const filteredConfig = discriminatedProviderSettingsWithIdSchema.parse(config) - providerProfiles.apiConfigs[name] = { ...filteredConfig, id } + + // Preserve credentials and sensitive fields from the existing config + // This prevents losing credentials when switching providers + let mergedConfig: ProviderSettingsWithId = { ...filteredConfig, id } + if (existingConfig) { + // Cast to any to allow dynamic property access + const existingAny = existingConfig as any + const mergedAny = mergedConfig as any + + // List of fields to preserve when switching providers + // These are credential and configuration fields that should persist + const fieldsToPreserve = [ + // Vertex AI credentials + "vertexJsonCredentials", + "vertexKeyFile", + "vertexProjectId", + "vertexRegion", + // AWS credentials + "awsAccessKey", + "awsSecretKey", + "awsSessionToken", + "awsRegion", + "awsProfile", + // Other API keys and credentials + "apiKey", + "openAiApiKey", + "anthropicApiKey", + "geminiApiKey", + "claudeCodePath", + "openRouterApiKey", + "glamaApiKey", + "mistralApiKey", + "deepSeekApiKey", + "doubaoApiKey", + "moonshotApiKey", + "minimaxApiKey", + "unboundApiKey", + "requestyApiKey", + "xaiApiKey", + "groqApiKey", + "huggingFaceApiKey", + "chutesApiKey", + "litellmApiKey", + "cerebrasApiKey", + "sambaNovaApiKey", + "zaiApiKey", + "fireworksApiKey", + "featherlessApiKey", + "ioIntelligenceApiKey", + "qwenCodeOauthPath", + "vercelAiGatewayApiKey", + "deepInfraApiKey", + "ollamaApiKey", + "openAiNativeApiKey", + // Base URLs and endpoints + "lmStudioBaseUrl", + "ollamaBaseUrl", + "openAiBaseUrl", + "openAiNativeBaseUrl", + "deepInfraBaseUrl", + "deepSeekBaseUrl", + "anthropicBaseUrl", + "moonshotBaseUrl", + "minimaxBaseUrl", + "googleGeminiBaseUrl", + "mistralCodestralUrl", + "requestyBaseUrl", + "litellmBaseUrl", + "doubaoBaseUrl", + ] + + // Preserve these fields if they exist in the existing config + for (const field of fieldsToPreserve) { + if (existingAny[field] !== undefined && mergedAny[field] === undefined) { + mergedAny[field] = existingAny[field] + } + } + + // Preserve any field that isSecretStateKey identifies as a secret + for (const [key, value] of Object.entries(existingConfig)) { + if (isSecretStateKey(key) && value !== undefined && mergedAny[key] === undefined) { + mergedAny[key] = value + } + } + } + + providerProfiles.apiConfigs[name] = mergedConfig await this.store(providerProfiles) return id }) diff --git a/src/core/config/__tests__/ProviderSettingsManager.spec.ts b/src/core/config/__tests__/ProviderSettingsManager.spec.ts index b710dc6cca..e4a49bc2d3 100644 --- a/src/core/config/__tests__/ProviderSettingsManager.spec.ts +++ b/src/core/config/__tests__/ProviderSettingsManager.spec.ts @@ -2,7 +2,7 @@ import { ExtensionContext } from "vscode" -import type { ProviderSettings } from "@roo-code/types" +import type { ProviderSettings, ProviderSettingsWithId } from "@roo-code/types" import { ProviderSettingsManager, ProviderProfiles, SyncCloudProfilesResult } from "../ProviderSettingsManager" @@ -794,6 +794,144 @@ describe("ProviderSettingsManager", () => { }) }) + describe("preserve credentials on provider switch", () => { + it("should preserve Vertex AI credentials when switching providers", async () => { + const vertexConfig: ProviderSettingsWithId = { + apiProvider: "vertex", + apiModelId: "gemini-2.5-flash-preview-05-20", + vertexKeyFile: "/path/to/key.json", + vertexJsonCredentials: '{"type": "service_account", "project_id": "test-project"}', + vertexProjectId: "test-project", + vertexRegion: "us-central1", + } + + // Save Vertex config with credentials + const vertexId = await providerSettingsManager.saveConfig("vertex-profile", vertexConfig) + + // Switch to a different provider + const openAiConfig: ProviderSettingsWithId = { + apiProvider: "openai", + openAiApiKey: "openai-key-123", + openAiModelId: "gpt-4", + } + await providerSettingsManager.saveConfig("vertex-profile", openAiConfig) + + // Switch back to Vertex + const switchBackConfig: ProviderSettingsWithId = { + apiProvider: "vertex", + apiModelId: "gemini-2.5-flash-preview-05-20", + } + await providerSettingsManager.saveConfig("vertex-profile", switchBackConfig) + + // Get the final config + const finalConfig = await providerSettingsManager.getProfile({ name: "vertex-profile" }) + + // Vertex credentials should be preserved + expect(finalConfig.vertexKeyFile).toBe("/path/to/key.json") + expect(finalConfig.vertexJsonCredentials).toBe('{"type": "service_account", "project_id": "test-project"}') + expect(finalConfig.vertexProjectId).toBe("test-project") + expect(finalConfig.vertexRegion).toBe("us-central1") + + // OpenAI key should also be preserved + expect((finalConfig as any).openAiApiKey).toBe("openai-key-123") + }) + + it("should preserve AWS Bedrock credentials when switching providers", async () => { + const bedrockConfig: ProviderSettingsWithId = { + apiProvider: "bedrock", + apiModelId: "claude-3-5-sonnet", + awsAccessKey: "AWS_ACCESS_KEY_123", + awsSecretKey: "AWS_SECRET_KEY_456", + awsSessionToken: "SESSION_TOKEN_789", + awsRegion: "us-east-1", + awsProfile: "default", + } + + // Save Bedrock config with credentials + await providerSettingsManager.saveConfig("aws-profile", bedrockConfig) + + // Switch to Anthropic + const anthropicConfig: ProviderSettingsWithId = { + apiProvider: "anthropic", + apiKey: "anthropic-key-xyz", + apiModelId: "claude-3-5-sonnet-20241022", + } + await providerSettingsManager.saveConfig("aws-profile", anthropicConfig) + + // Switch back to Bedrock without credentials + const switchBackConfig: ProviderSettingsWithId = { + apiProvider: "bedrock", + apiModelId: "claude-3-5-sonnet", + } + await providerSettingsManager.saveConfig("aws-profile", switchBackConfig) + + // Get the final config + const finalConfig = await providerSettingsManager.getProfile({ name: "aws-profile" }) + + // AWS credentials should be preserved + expect(finalConfig.awsAccessKey).toBe("AWS_ACCESS_KEY_123") + expect(finalConfig.awsSecretKey).toBe("AWS_SECRET_KEY_456") + expect(finalConfig.awsSessionToken).toBe("SESSION_TOKEN_789") + expect(finalConfig.awsRegion).toBe("us-east-1") + expect(finalConfig.awsProfile).toBe("default") + + // Anthropic key should also be preserved + expect((finalConfig as any).apiKey).toBe("anthropic-key-xyz") + }) + + it("should preserve multiple provider credentials across multiple switches", async () => { + // Start with OpenAI + await providerSettingsManager.saveConfig("multi", { + apiProvider: "openai", + openAiApiKey: "openai-key", + openAiBaseUrl: "https://api.openai.com", + }) + + // Switch to Vertex with credentials + await providerSettingsManager.saveConfig("multi", { + apiProvider: "vertex", + vertexProjectId: "gcp-project", + vertexRegion: "us-central1", + vertexKeyFile: "/path/to/gcp-key.json", + }) + + // Switch to AWS Bedrock + await providerSettingsManager.saveConfig("multi", { + apiProvider: "bedrock", + awsAccessKey: "aws-access", + awsSecretKey: "aws-secret", + awsRegion: "us-west-2", + }) + + // Switch to DeepSeek + await providerSettingsManager.saveConfig("multi", { + apiProvider: "deepseek", + deepSeekApiKey: "deepseek-key", + deepSeekBaseUrl: "https://api.deepseek.com", + }) + + // Switch back to OpenAI without any credentials + await providerSettingsManager.saveConfig("multi", { + apiProvider: "openai", + openAiModelId: "gpt-4-turbo", + }) + + const finalConfig = await providerSettingsManager.getProfile({ name: "multi" }) + + // All credentials should be preserved + expect((finalConfig as any).openAiApiKey).toBe("openai-key") + expect((finalConfig as any).openAiBaseUrl).toBe("https://api.openai.com") + expect((finalConfig as any).vertexProjectId).toBe("gcp-project") + expect((finalConfig as any).vertexRegion).toBe("us-central1") + expect((finalConfig as any).vertexKeyFile).toBe("/path/to/gcp-key.json") + expect((finalConfig as any).awsAccessKey).toBe("aws-access") + expect((finalConfig as any).awsSecretKey).toBe("aws-secret") + expect((finalConfig as any).awsRegion).toBe("us-west-2") + expect((finalConfig as any).deepSeekApiKey).toBe("deepseek-key") + expect((finalConfig as any).deepSeekBaseUrl).toBe("https://api.deepseek.com") + }) + }) + describe("syncCloudProfiles", () => { it("should add new cloud profiles without secret keys", async () => { const existingConfig: ProviderProfiles = {