소스 검색

Restore profile management work + fix #3434 (#3449)

Co-authored-by: Matt Rubens <[email protected]>
John Richmond 9 달 전
부모
커밋
8cb5c4e9e3
62개의 변경된 파일1497개의 추가작업 그리고 998개의 파일을 삭제
  1. 5 0
      .changeset/thin-penguins-search.md
  2. 1 0
      .gitignore
  3. 1 1
      e2e/package-lock.json
  4. 1 1
      e2e/package.json
  5. 0 1
      evals/config/eslint/package.json
  6. 1 1
      evals/package.json
  7. 245 46
      evals/packages/types/src/roo-code.ts
  8. 29 43
      evals/pnpm-lock.yaml
  9. 0 0
      git
  10. 2 2
      package-lock.json
  11. 2 2
      package.json
  12. 2 2
      src/api/index.ts
  13. 57 31
      src/core/config/ProviderSettingsManager.ts
  14. 72 56
      src/core/config/__tests__/ProviderSettingsManager.test.ts
  15. 3 3
      src/core/task/Task.ts
  16. 2 2
      src/core/task/__tests__/Task.test.ts
  17. 127 61
      src/core/webview/ClineProvider.ts
  18. 40 41
      src/core/webview/__tests__/ClineProvider.test.ts
  19. 46 99
      src/core/webview/webviewMessageHandler.ts
  20. 107 86
      src/exports/api.ts
  21. 61 16
      src/exports/interface.ts
  22. 210 153
      src/exports/roo-code.d.ts
  23. 173 141
      src/exports/types.ts
  24. 149 49
      src/schemas/index.ts
  25. 5 5
      src/shared/ExtensionMessage.ts
  26. 2 3
      src/shared/WebviewMessage.ts
  27. 6 6
      src/shared/__tests__/checkExistApiConfig.test.ts
  28. 1 3
      src/shared/api.ts
  29. 4 4
      src/utils/__tests__/enhance-prompt.test.ts
  30. 2 2
      src/utils/single-completion-handler.ts
  31. 2 2
      webview-ui/src/components/chat/__tests__/TaskHeader.test.tsx
  32. 2 2
      webview-ui/src/components/settings/ApiConfigManager.tsx
  33. 5 5
      webview-ui/src/components/settings/ApiOptions.tsx
  34. 3 3
      webview-ui/src/components/settings/PromptCachingControl.tsx
  35. 3 3
      webview-ui/src/components/settings/ReasoningEffort.tsx
  36. 2 2
      webview-ui/src/components/settings/SettingsView.tsx
  37. 3 3
      webview-ui/src/components/settings/ThinkingBudget.tsx
  38. 2 2
      webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
  39. 5 5
      webview-ui/src/components/settings/providers/Anthropic.tsx
  40. 5 5
      webview-ui/src/components/settings/providers/Bedrock.tsx
  41. 3 3
      webview-ui/src/components/settings/providers/BedrockCustomArn.tsx
  42. 5 5
      webview-ui/src/components/settings/providers/Chutes.tsx
  43. 5 5
      webview-ui/src/components/settings/providers/DeepSeek.tsx
  44. 5 5
      webview-ui/src/components/settings/providers/Gemini.tsx
  45. 5 5
      webview-ui/src/components/settings/providers/Glama.tsx
  46. 5 5
      webview-ui/src/components/settings/providers/Groq.tsx
  47. 5 5
      webview-ui/src/components/settings/providers/LMStudio.tsx
  48. 6 5
      webview-ui/src/components/settings/providers/LiteLLM.tsx
  49. 5 5
      webview-ui/src/components/settings/providers/Mistral.tsx
  50. 5 5
      webview-ui/src/components/settings/providers/Ollama.tsx
  51. 5 5
      webview-ui/src/components/settings/providers/OpenAI.tsx
  52. 5 5
      webview-ui/src/components/settings/providers/OpenAICompatible.tsx
  53. 5 5
      webview-ui/src/components/settings/providers/OpenRouter.tsx
  54. 5 5
      webview-ui/src/components/settings/providers/Requesty.tsx
  55. 5 5
      webview-ui/src/components/settings/providers/Unbound.tsx
  56. 5 5
      webview-ui/src/components/settings/providers/VSCodeLM.tsx
  57. 5 5
      webview-ui/src/components/settings/providers/Vertex.tsx
  58. 5 5
      webview-ui/src/components/settings/providers/XAI.tsx
  59. 3 3
      webview-ui/src/components/ui/hooks/useSelectedModel.ts
  60. 9 7
      webview-ui/src/context/ExtensionStateContext.tsx
  61. 5 5
      webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx
  62. 3 3
      webview-ui/src/utils/validate.ts

+ 5 - 0
.changeset/thin-penguins-search.md

@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Restore profile management work + fix #3434

+ 1 - 0
.gitignore

@@ -4,6 +4,7 @@ out
 out-*
 node_modules
 coverage/
+mock/
 
 .DS_Store
 

+ 1 - 1
e2e/package-lock.json

@@ -12,7 +12,7 @@
 				"@vscode/test-cli": "^0.0.10",
 				"@vscode/test-electron": "^2.4.0",
 				"mocha": "^11.1.0",
-				"typescript": "^5.4.5"
+				"typescript": "5.8.3"
 			}
 		},
 		"node_modules/@bcoe/v8-coverage": {

+ 1 - 1
e2e/package.json

@@ -16,6 +16,6 @@
 		"@vscode/test-cli": "^0.0.10",
 		"@vscode/test-electron": "^2.4.0",
 		"mocha": "^11.1.0",
-		"typescript": "^5.4.5"
+		"typescript": "5.8.3"
 	}
 }

+ 0 - 1
evals/config/eslint/package.json

@@ -16,7 +16,6 @@
 		"eslint-plugin-react-hooks": "^5.2.0",
 		"eslint-plugin-turbo": "^2.4.4",
 		"globals": "^16.0.0",
-		"typescript": "^5",
 		"typescript-eslint": "^8.26.0"
 	}
 }

+ 1 - 1
evals/package.json

@@ -20,7 +20,7 @@
 		"prettier": "^3.5.3",
 		"tsx": "^4.19.4",
 		"turbo": "^2.5.2",
-		"typescript": "^5.8.3",
+		"typescript": "5.8.3",
 		"typescript-eslint": "^8.31.1"
 	}
 }

+ 245 - 46
evals/packages/types/src/roo-code.ts

@@ -128,18 +128,6 @@ export const modelInfoSchema = z.object({
 
 export type ModelInfo = z.infer<typeof modelInfoSchema>
 
-/**
- * ApiConfigMeta
- */
-
-export const apiConfigMetaSchema = z.object({
-	id: z.string(),
-	name: z.string(),
-	apiProvider: providerNamesSchema.optional(),
-})
-
-export type ApiConfigMeta = z.infer<typeof apiConfigMetaSchema>
-
 /**
  * HistoryItem
  */
@@ -330,26 +318,55 @@ export type Experiments = z.infer<typeof experimentsSchema>
 type _AssertExperiments = AssertEqual<Equals<ExperimentId, Keys<Experiments>>>
 
 /**
- * ProviderSettings
+ * ProviderSettingsEntry
  */
 
-export const providerSettingsSchema = z.object({
+export const providerSettingsEntrySchema = z.object({
+	id: z.string(),
+	name: z.string(),
 	apiProvider: providerNamesSchema.optional(),
-	// Anthropic
+})
+
+export type ProviderSettingsEntry = z.infer<typeof providerSettingsEntrySchema>
+
+/**
+ * ProviderSettings
+ */
+
+const genericProviderSettingsSchema = z.object({
+	includeMaxTokens: z.boolean().optional(),
+	reasoningEffort: reasoningEffortsSchema.optional(),
+	promptCachingDisabled: z.boolean().optional(),
+	diffEnabled: z.boolean().optional(),
+	fuzzyMatchThreshold: z.number().optional(),
+	modelTemperature: z.number().nullish(),
+	rateLimitSeconds: z.number().optional(),
+	// Claude 3.7 Sonnet Thinking
+	modelMaxTokens: z.number().optional(),
+	modelMaxThinkingTokens: z.number().optional(),
+})
+
+const anthropicSchema = z.object({
 	apiModelId: z.string().optional(),
 	apiKey: z.string().optional(),
 	anthropicBaseUrl: z.string().optional(),
 	anthropicUseAuthToken: z.boolean().optional(),
-	// Glama
+})
+
+const glamaSchema = z.object({
 	glamaModelId: z.string().optional(),
 	glamaApiKey: z.string().optional(),
-	// OpenRouter
+})
+
+const openRouterSchema = z.object({
 	openRouterApiKey: z.string().optional(),
 	openRouterModelId: z.string().optional(),
 	openRouterBaseUrl: z.string().optional(),
 	openRouterSpecificProvider: z.string().optional(),
 	openRouterUseMiddleOutTransform: z.boolean().optional(),
-	// Amazon Bedrock
+})
+
+const bedrockSchema = z.object({
 	awsAccessKey: z.string().optional(),
 	awsSecretKey: z.string().optional(),
 	awsSessionToken: z.string().optional(),
@@ -359,15 +376,18 @@ export const providerSettingsSchema = z.object({
 	awsProfile: z.string().optional(),
 	awsUseProfile: z.boolean().optional(),
 	awsCustomArn: z.string().optional(),
-	// Google Vertex
+})
+
+const vertexSchema = z.object({
 	vertexKeyFile: z.string().optional(),
 	vertexJsonCredentials: z.string().optional(),
 	vertexProjectId: z.string().optional(),
 	vertexRegion: z.string().optional(),
-	// OpenAI
+})
+
+const openAiSchema = z.object({
 	openAiBaseUrl: z.string().optional(),
 	openAiApiKey: z.string().optional(),
-	openAiHostHeader: z.string().optional(),
 	openAiLegacyFormat: z.boolean().optional(),
 	openAiR1FormatEnabled: z.boolean().optional(),
 	openAiModelId: z.string().optional(),
@@ -376,10 +396,16 @@ export const providerSettingsSchema = z.object({
 	azureApiVersion: z.string().optional(),
 	openAiStreamingEnabled: z.boolean().optional(),
 	enableReasoningEffort: z.boolean().optional(),
-	// Ollama
+	openAiHostHeader: z.string().optional(), // Keep temporarily for backward compatibility during migration.
+	openAiHeaders: z.record(z.string(), z.string()).optional(),
+})
+
+const ollamaSchema = z.object({
 	ollamaModelId: z.string().optional(),
 	ollamaBaseUrl: z.string().optional(),
-	// VS Code LM
+})
+
+const vsCodeLmSchema = z.object({
 	vsCodeLmModelSelector: z
 		.object({
 			vendor: z.string().optional(),
@@ -388,46 +414,210 @@ export const providerSettingsSchema = z.object({
 			id: z.string().optional(),
 		})
 		.optional(),
-	// LM Studio
+})
+
+const lmStudioSchema = z.object({
 	lmStudioModelId: z.string().optional(),
 	lmStudioBaseUrl: z.string().optional(),
 	lmStudioDraftModelId: z.string().optional(),
 	lmStudioSpeculativeDecodingEnabled: z.boolean().optional(),
-	// Gemini
+})
+
+const geminiSchema = z.object({
 	geminiApiKey: z.string().optional(),
 	googleGeminiBaseUrl: z.string().optional(),
-	// OpenAI Native
+})
+
+const openAiNativeSchema = z.object({
 	openAiNativeApiKey: z.string().optional(),
 	openAiNativeBaseUrl: z.string().optional(),
-	// Mistral
+})
+
+const mistralSchema = z.object({
 	mistralApiKey: z.string().optional(),
 	mistralCodestralUrl: z.string().optional(),
-	// DeepSeek
+})
+
+const deepSeekSchema = z.object({
 	deepSeekBaseUrl: z.string().optional(),
 	deepSeekApiKey: z.string().optional(),
-	// Unbound
+})
+
+const unboundSchema = z.object({
 	unboundApiKey: z.string().optional(),
 	unboundModelId: z.string().optional(),
-	// Requesty
+})
+
+const requestySchema = z.object({
 	requestyApiKey: z.string().optional(),
 	requestyModelId: z.string().optional(),
-	// X.AI (Grok)
-	xaiApiKey: z.string().optional(),
-	// Claude 3.7 Sonnet Thinking
-	modelMaxTokens: z.number().optional(),
-	modelMaxThinkingTokens: z.number().optional(),
-	// Generic
-	includeMaxTokens: z.boolean().optional(),
-	reasoningEffort: reasoningEffortsSchema.optional(),
-	promptCachingDisabled: z.boolean().optional(),
-	diffEnabled: z.boolean().optional(),
-	fuzzyMatchThreshold: z.number().optional(),
-	modelTemperature: z.number().nullish(),
-	rateLimitSeconds: z.number().optional(),
-	// Fake AI
+})
+
+const humanRelaySchema = z.object({})
+
+const fakeAiSchema = z.object({
 	fakeAi: z.unknown().optional(),
 })
 
+const xaiSchema = z.object({
+	xaiApiKey: z.string().optional(),
+})
+
+const groqSchema = z.object({
+	groqApiKey: z.string().optional(),
+})
+
+const chutesSchema = z.object({
+	chutesApiKey: z.string().optional(),
+})
+
+const litellmSchema = z.object({
+	litellmBaseUrl: z.string().optional(),
+	litellmApiKey: z.string().optional(),
+	litellmModelId: z.string().optional(),
+})
+
+const defaultSchema = z.object({
+	apiProvider: z.undefined(),
+})
+
+export const providerSettingsSchemaDiscriminated = z
+	.discriminatedUnion("apiProvider", [
+		anthropicSchema.merge(
+			z.object({
+				apiProvider: z.literal("anthropic"),
+			}),
+		),
+		glamaSchema.merge(
+			z.object({
+				apiProvider: z.literal("glama"),
+			}),
+		),
+		openRouterSchema.merge(
+			z.object({
+				apiProvider: z.literal("openrouter"),
+			}),
+		),
+		bedrockSchema.merge(
+			z.object({
+				apiProvider: z.literal("bedrock"),
+			}),
+		),
+		vertexSchema.merge(
+			z.object({
+				apiProvider: z.literal("vertex"),
+			}),
+		),
+		openAiSchema.merge(
+			z.object({
+				apiProvider: z.literal("openai"),
+			}),
+		),
+		ollamaSchema.merge(
+			z.object({
+				apiProvider: z.literal("ollama"),
+			}),
+		),
+		vsCodeLmSchema.merge(
+			z.object({
+				apiProvider: z.literal("vscode-lm"),
+			}),
+		),
+		lmStudioSchema.merge(
+			z.object({
+				apiProvider: z.literal("lmstudio"),
+			}),
+		),
+		geminiSchema.merge(
+			z.object({
+				apiProvider: z.literal("gemini"),
+			}),
+		),
+		openAiNativeSchema.merge(
+			z.object({
+				apiProvider: z.literal("openai-native"),
+			}),
+		),
+		mistralSchema.merge(
+			z.object({
+				apiProvider: z.literal("mistral"),
+			}),
+		),
+		deepSeekSchema.merge(
+			z.object({
+				apiProvider: z.literal("deepseek"),
+			}),
+		),
+		unboundSchema.merge(
+			z.object({
+				apiProvider: z.literal("unbound"),
+			}),
+		),
+		requestySchema.merge(
+			z.object({
+				apiProvider: z.literal("requesty"),
+			}),
+		),
+		humanRelaySchema.merge(
+			z.object({
+				apiProvider: z.literal("human-relay"),
+			}),
+		),
+		fakeAiSchema.merge(
+			z.object({
+				apiProvider: z.literal("fake-ai"),
+			}),
+		),
+		xaiSchema.merge(
+			z.object({
+				apiProvider: z.literal("xai"),
+			}),
+		),
+		groqSchema.merge(
+			z.object({
+				apiProvider: z.literal("groq"),
+			}),
+		),
+		chutesSchema.merge(
+			z.object({
+				apiProvider: z.literal("chutes"),
+			}),
+		),
+		litellmSchema.merge(
+			z.object({
+				apiProvider: z.literal("litellm"),
+			}),
+		),
+		defaultSchema,
+	])
+	.and(genericProviderSettingsSchema)
+
+export const providerSettingsSchema = z.object({
+	apiProvider: providerNamesSchema.optional(),
+	...anthropicSchema.shape,
+	...glamaSchema.shape,
+	...openRouterSchema.shape,
+	...bedrockSchema.shape,
+	...vertexSchema.shape,
+	...openAiSchema.shape,
+	...ollamaSchema.shape,
+	...vsCodeLmSchema.shape,
+	...lmStudioSchema.shape,
+	...geminiSchema.shape,
+	...openAiNativeSchema.shape,
+	...mistralSchema.shape,
+	...deepSeekSchema.shape,
+	...unboundSchema.shape,
+	...requestySchema.shape,
+	...humanRelaySchema.shape,
+	...fakeAiSchema.shape,
+	...xaiSchema.shape,
+	...groqSchema.shape,
+	...chutesSchema.shape,
+	...litellmSchema.shape,
+	...genericProviderSettingsSchema.shape,
+})
+
 export type ProviderSettings = z.infer<typeof providerSettingsSchema>
 
 type ProviderSettingsRecord = Record<Keys<ProviderSettings>, undefined>
@@ -466,7 +656,6 @@ const providerSettingsRecord: ProviderSettingsRecord = {
 	// OpenAI
 	openAiBaseUrl: undefined,
 	openAiApiKey: undefined,
-	openAiHostHeader: undefined,
 	openAiLegacyFormat: undefined,
 	openAiR1FormatEnabled: undefined,
 	openAiModelId: undefined,
@@ -475,6 +664,8 @@ const providerSettingsRecord: ProviderSettingsRecord = {
 	azureApiVersion: undefined,
 	openAiStreamingEnabled: undefined,
 	enableReasoningEffort: undefined,
+	openAiHostHeader: undefined, // Keep temporarily for backward compatibility during migration
+	openAiHeaders: undefined,
 	// Ollama
 	ollamaModelId: undefined,
 	ollamaBaseUrl: undefined,
@@ -517,6 +708,14 @@ const providerSettingsRecord: ProviderSettingsRecord = {
 	fakeAi: undefined,
 	// X.AI (Grok)
 	xaiApiKey: undefined,
+	// Groq
+	groqApiKey: undefined,
+	// Chutes AI
+	chutesApiKey: undefined,
+	// LiteLLM
+	litellmBaseUrl: undefined,
+	litellmApiKey: undefined,
+	litellmModelId: undefined,
 }
 
 export const PROVIDER_SETTINGS_KEYS = Object.keys(providerSettingsRecord) as Keys<ProviderSettings>[]
@@ -527,7 +726,7 @@ export const PROVIDER_SETTINGS_KEYS = Object.keys(providerSettingsRecord) as Key
 
 export const globalSettingsSchema = z.object({
 	currentApiConfigName: z.string().optional(),
-	listApiConfigMeta: z.array(apiConfigMetaSchema).optional(),
+	listApiConfigMeta: z.array(providerSettingsEntrySchema).optional(),
 	pinnedApiConfigs: z.record(z.string(), z.boolean()).optional(),
 
 	lastShownAnnouncementId: z.string().optional(),

+ 29 - 43
evals/pnpm-lock.yaml

@@ -30,7 +30,7 @@ importers:
         specifier: ^2.5.2
         version: 2.5.3
       typescript:
-        specifier: ^5.8.3
+        specifier: 5.8.3
         version: 5.8.3
       typescript-eslint:
         specifier: ^8.31.1
@@ -235,12 +235,9 @@ importers:
       globals:
         specifier: ^16.0.0
         version: 16.0.0
-      typescript:
-        specifier: ^5
-        version: 5.8.2
       typescript-eslint:
         specifier: ^8.26.0
-        version: 8.26.1([email protected]([email protected]))([email protected].2)
+        version: 8.26.1([email protected]([email protected]))([email protected])
 
   config/typescript: {}
 
@@ -4377,11 +4374,6 @@ packages:
       eslint: ^8.57.0 || ^9.0.0
       typescript: '>=4.8.4 <5.9.0'
 
-  [email protected]:
-    resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==}
-    engines: {node: '>=14.17'}
-    hasBin: true
-
   [email protected]:
     resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
     engines: {node: '>=14.17'}
@@ -6037,20 +6029,20 @@ snapshots:
     dependencies:
       '@types/node': 20.17.24
 
-  '@typescript-eslint/[email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected].2))([email protected]([email protected]))([email protected])':
+  '@typescript-eslint/[email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected].3))([email protected]([email protected]))([email protected])':
     dependencies:
       '@eslint-community/regexpp': 4.12.1
-      '@typescript-eslint/parser': 8.26.1([email protected]([email protected]))([email protected].2)
+      '@typescript-eslint/parser': 8.26.1([email protected]([email protected]))([email protected].3)
       '@typescript-eslint/scope-manager': 8.26.1
-      '@typescript-eslint/type-utils': 8.26.1([email protected]([email protected]))([email protected].2)
-      '@typescript-eslint/utils': 8.26.1([email protected]([email protected]))([email protected].2)
+      '@typescript-eslint/type-utils': 8.26.1([email protected]([email protected]))([email protected].3)
+      '@typescript-eslint/utils': 8.26.1([email protected]([email protected]))([email protected].3)
       '@typescript-eslint/visitor-keys': 8.26.1
       eslint: 9.22.0([email protected])
       graphemer: 1.4.0
       ignore: 5.3.2
       natural-compare: 1.4.0
-      ts-api-utils: 2.0.1([email protected].2)
-      typescript: 5.8.2
+      ts-api-utils: 2.0.1([email protected].3)
+      typescript: 5.8.3
     transitivePeerDependencies:
       - supports-color
 
@@ -6071,15 +6063,15 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/[email protected]([email protected]([email protected]))([email protected].2)':
+  '@typescript-eslint/[email protected]([email protected]([email protected]))([email protected].3)':
     dependencies:
       '@typescript-eslint/scope-manager': 8.26.1
       '@typescript-eslint/types': 8.26.1
-      '@typescript-eslint/typescript-estree': 8.26.1([email protected].2)
+      '@typescript-eslint/typescript-estree': 8.26.1([email protected].3)
       '@typescript-eslint/visitor-keys': 8.26.1
       debug: 4.4.0
       eslint: 9.22.0([email protected])
-      typescript: 5.8.2
+      typescript: 5.8.3
     transitivePeerDependencies:
       - supports-color
 
@@ -6105,14 +6097,14 @@ snapshots:
       '@typescript-eslint/types': 8.32.0
       '@typescript-eslint/visitor-keys': 8.32.0
 
-  '@typescript-eslint/[email protected]([email protected]([email protected]))([email protected].2)':
+  '@typescript-eslint/[email protected]([email protected]([email protected]))([email protected].3)':
     dependencies:
-      '@typescript-eslint/typescript-estree': 8.26.1([email protected].2)
-      '@typescript-eslint/utils': 8.26.1([email protected]([email protected]))([email protected].2)
+      '@typescript-eslint/typescript-estree': 8.26.1([email protected].3)
+      '@typescript-eslint/utils': 8.26.1([email protected]([email protected]))([email protected].3)
       debug: 4.4.0
       eslint: 9.22.0([email protected])
-      ts-api-utils: 2.0.1([email protected].2)
-      typescript: 5.8.2
+      ts-api-utils: 2.0.1([email protected].3)
+      typescript: 5.8.3
     transitivePeerDependencies:
       - supports-color
 
@@ -6131,7 +6123,7 @@ snapshots:
 
   '@typescript-eslint/[email protected]': {}
 
-  '@typescript-eslint/[email protected]([email protected].2)':
+  '@typescript-eslint/[email protected]([email protected].3)':
     dependencies:
       '@typescript-eslint/types': 8.26.1
       '@typescript-eslint/visitor-keys': 8.26.1
@@ -6140,8 +6132,8 @@ snapshots:
       is-glob: 4.0.3
       minimatch: 9.0.5
       semver: 7.7.1
-      ts-api-utils: 2.1.0([email protected].2)
-      typescript: 5.8.2
+      ts-api-utils: 2.1.0([email protected].3)
+      typescript: 5.8.3
     transitivePeerDependencies:
       - supports-color
 
@@ -6159,14 +6151,14 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/[email protected]([email protected]([email protected]))([email protected].2)':
+  '@typescript-eslint/[email protected]([email protected]([email protected]))([email protected].3)':
     dependencies:
       '@eslint-community/eslint-utils': 4.6.1([email protected]([email protected]))
       '@typescript-eslint/scope-manager': 8.26.1
       '@typescript-eslint/types': 8.26.1
-      '@typescript-eslint/typescript-estree': 8.26.1([email protected].2)
+      '@typescript-eslint/typescript-estree': 8.26.1([email protected].3)
       eslint: 9.22.0([email protected])
-      typescript: 5.8.2
+      typescript: 5.8.3
     transitivePeerDependencies:
       - supports-color
 
@@ -8577,13 +8569,9 @@ snapshots:
 
   [email protected]: {}
 
-  [email protected]([email protected]):
-    dependencies:
-      typescript: 5.8.2
-
-  [email protected]([email protected]):
+  [email protected]([email protected]):
     dependencies:
-      typescript: 5.8.2
+      typescript: 5.8.3
 
   [email protected]([email protected]):
     dependencies:
@@ -8670,13 +8658,13 @@ snapshots:
       possible-typed-array-names: 1.1.0
       reflect.getprototypeof: 1.0.10
 
-  [email protected]([email protected]([email protected]))([email protected].2):
+  [email protected]([email protected]([email protected]))([email protected].3):
     dependencies:
-      '@typescript-eslint/eslint-plugin': 8.26.1(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected].2))([email protected]([email protected]))([email protected])
-      '@typescript-eslint/parser': 8.26.1([email protected]([email protected]))([email protected].2)
-      '@typescript-eslint/utils': 8.26.1([email protected]([email protected]))([email protected].2)
+      '@typescript-eslint/eslint-plugin': 8.26.1(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected].3))([email protected]([email protected]))([email protected])
+      '@typescript-eslint/parser': 8.26.1([email protected]([email protected]))([email protected].3)
+      '@typescript-eslint/utils': 8.26.1([email protected]([email protected]))([email protected].3)
       eslint: 9.22.0([email protected])
-      typescript: 5.8.2
+      typescript: 5.8.3
     transitivePeerDependencies:
       - supports-color
 
@@ -8690,8 +8678,6 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  [email protected]: {}
-
   [email protected]: {}
 
   [email protected]:

+ 0 - 0
git


+ 2 - 2
package-lock.json

@@ -65,7 +65,7 @@
 				"vscode-material-icons": "^0.1.1",
 				"web-tree-sitter": "^0.22.6",
 				"workerpool": "^9.2.0",
-				"zod": "^3.23.8"
+				"zod": "^3.24.2"
 			},
 			"devDependencies": {
 				"@changesets/cli": "^2.27.10",
@@ -103,7 +103,7 @@
 				"ts-jest": "^29.2.5",
 				"tsup": "^8.4.0",
 				"tsx": "^4.19.3",
-				"typescript": "^5.4.5",
+				"typescript": "5.8.3",
 				"vitest": "^3.1.3",
 				"zod-to-ts": "^1.2.0"
 			},

+ 2 - 2
package.json

@@ -421,7 +421,7 @@
 		"vscode-material-icons": "^0.1.1",
 		"web-tree-sitter": "^0.22.6",
 		"workerpool": "^9.2.0",
-		"zod": "^3.23.8"
+		"zod": "^3.24.2"
 	},
 	"devDependencies": {
 		"@changesets/cli": "^2.27.10",
@@ -459,7 +459,7 @@
 		"ts-jest": "^29.2.5",
 		"tsup": "^8.4.0",
 		"tsx": "^4.19.3",
-		"typescript": "^5.4.5",
+		"typescript": "5.8.3",
 		"vitest": "^3.1.3",
 		"zod-to-ts": "^1.2.0"
 	},

+ 2 - 2
src/api/index.ts

@@ -1,7 +1,7 @@
 import { Anthropic } from "@anthropic-ai/sdk"
 import { BetaThinkingConfigParam } from "@anthropic-ai/sdk/resources/beta/messages/index.mjs"
 
-import { ApiConfiguration, ModelInfo, ApiHandlerOptions } from "../shared/api"
+import { ProviderSettings, ModelInfo, ApiHandlerOptions } from "../shared/api"
 import { ANTHROPIC_DEFAULT_MAX_TOKENS } from "./providers/constants"
 import { GlamaHandler } from "./providers/glama"
 import { AnthropicHandler } from "./providers/anthropic"
@@ -47,7 +47,7 @@ export interface ApiHandler {
 	countTokens(content: Array<Anthropic.Messages.ContentBlockParam>): Promise<number>
 }
 
-export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
+export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
 	const { apiProvider, ...options } = configuration
 
 	switch (apiProvider) {

+ 57 - 31
src/core/config/ProviderSettingsManager.ts

@@ -1,11 +1,14 @@
 import { ExtensionContext } from "vscode"
 import { z, ZodError } from "zod"
 
-import { providerSettingsSchema, ApiConfigMeta } from "../../schemas"
+import { providerSettingsSchema, ProviderSettingsEntry, providerSettingsSchemaDiscriminated } from "../../schemas"
 import { Mode, modes } from "../../shared/modes"
 import { telemetryService } from "../../services/telemetry/TelemetryService"
 
 const providerSettingsWithIdSchema = providerSettingsSchema.extend({ id: z.string().optional() })
+const discriminatedProviderSettingsWithIdSchema = providerSettingsSchemaDiscriminated.and(
+	z.object({ id: z.string().optional() }),
+)
 
 type ProviderSettingsWithId = z.infer<typeof providerSettingsWithIdSchema>
 
@@ -223,7 +226,7 @@ export class ProviderSettingsManager {
 	/**
 	 * List all available configs with metadata.
 	 */
-	public async listConfig(): Promise<ApiConfigMeta[]> {
+	public async listConfig(): Promise<ProviderSettingsEntry[]> {
 		try {
 			return await this.lock(async () => {
 				const providerProfiles = await this.load()
@@ -244,66 +247,81 @@ export class ProviderSettingsManager {
 	 * Preserves the ID from the input 'config' object if it exists,
 	 * otherwise generates a new one (for creation scenarios).
 	 */
-	public async saveConfig(name: string, config: ProviderSettingsWithId) {
+	public async saveConfig(name: string, config: ProviderSettingsWithId): Promise<string> {
 		try {
 			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
-				providerProfiles.apiConfigs[name] = { ...config, id: config.id || existingId || this.generateId() }
+				const id = config.id || existingId || this.generateId()
+
+				// Filter out settings from other providers.
+				const filteredConfig = providerSettingsSchemaDiscriminated.parse(config)
+				providerProfiles.apiConfigs[name] = { ...filteredConfig, id }
 				await this.store(providerProfiles)
+				return id
 			})
 		} catch (error) {
 			throw new Error(`Failed to save config: ${error}`)
 		}
 	}
 
-	/**
-	 * Load a config by name and set it as the current config.
-	 */
-	public async loadConfig(name: string) {
+	public async getProfile(
+		params: { name: string } | { id: string },
+	): Promise<ProviderSettingsWithId & { name: string }> {
 		try {
 			return await this.lock(async () => {
 				const providerProfiles = await this.load()
-				const providerSettings = providerProfiles.apiConfigs[name]
+				let name: string
+				let providerSettings: ProviderSettingsWithId
 
-				if (!providerSettings) {
-					throw new Error(`Config '${name}' not found`)
-				}
+				if ("name" in params) {
+					name = params.name
 
-				providerProfiles.currentApiConfigName = name
-				await this.store(providerProfiles)
+					if (!providerProfiles.apiConfigs[name]) {
+						throw new Error(`Config with name '${name}' not found`)
+					}
+
+					providerSettings = providerProfiles.apiConfigs[name]
+				} else {
+					const id = params.id
+
+					const entry = Object.entries(providerProfiles.apiConfigs).find(
+						([_, apiConfig]) => apiConfig.id === id,
+					)
+
+					if (!entry) {
+						throw new Error(`Config with ID '${id}' not found`)
+					}
+
+					name = entry[0]
+					providerSettings = entry[1]
+				}
 
-				return providerSettings
+				return { name, ...providerSettings }
 			})
 		} catch (error) {
-			throw new Error(`Failed to load config: ${error}`)
+			throw new Error(`Failed to get profile: ${error instanceof Error ? error.message : error}`)
 		}
 	}
 
 	/**
-	 * Load a config by ID and set it as the current config.
+	 * Activate a profile by name or ID.
 	 */
-	public async loadConfigById(id: string) {
+	public async activateProfile(
+		params: { name: string } | { id: string },
+	): Promise<ProviderSettingsWithId & { name: string }> {
+		const { name, ...providerSettings } = await this.getProfile(params)
+
 		try {
 			return await this.lock(async () => {
 				const providerProfiles = await this.load()
-				const providerSettings = Object.entries(providerProfiles.apiConfigs).find(
-					([_, apiConfig]) => apiConfig.id === id,
-				)
-
-				if (!providerSettings) {
-					throw new Error(`Config with ID '${id}' not found`)
-				}
-
-				const [name, apiConfig] = providerSettings
 				providerProfiles.currentApiConfigName = name
 				await this.store(providerProfiles)
-
-				return { config: apiConfig, name }
+				return { name, ...providerSettings }
 			})
 		} catch (error) {
-			throw new Error(`Failed to load config by ID: ${error}`)
+			throw new Error(`Failed to activate profile: ${error instanceof Error ? error.message : error}`)
 		}
 	}
 
@@ -381,7 +399,15 @@ export class ProviderSettingsManager {
 
 	public async export() {
 		try {
-			return await this.lock(async () => providerProfilesSchema.parse(await this.load()))
+			return await this.lock(async () => {
+				const profiles = providerProfilesSchema.parse(await this.load())
+				const configs = profiles.apiConfigs
+				for (const name in configs) {
+					// Avoid leaking properties from other providers.
+					configs[name] = discriminatedProviderSettingsWithIdSchema.parse(configs[name])
+				}
+				return profiles
+			})
 		} catch (error) {
 			throw new Error(`Failed to export provider profiles: ${error}`)
 		}

+ 72 - 56
src/core/config/__tests__/ProviderSettingsManager.test.ts

@@ -221,8 +221,9 @@ describe("ProviderSettingsManager", () => {
 			)
 
 			const newConfig: ProviderSettings = {
-				apiProvider: "anthropic",
-				apiKey: "test-key",
+				apiProvider: "vertex",
+				apiModelId: "gemini-2.5-flash-preview-04-17",
+				vertexKeyFile: "test-key-file",
 			}
 
 			await providerSettingsManager.saveConfig("test", newConfig)
@@ -247,10 +248,58 @@ describe("ProviderSettingsManager", () => {
 				},
 			}
 
-			expect(mockSecrets.store).toHaveBeenCalledWith(
-				"roo_cline_config_api_config",
-				JSON.stringify(expectedConfig, null, 2),
+			expect(mockSecrets.store.mock.calls[0][0]).toEqual("roo_cline_config_api_config")
+			expect(storedConfig).toEqual(expectedConfig)
+		})
+
+		it("should only save provider relevant settings", async () => {
+			mockSecrets.get.mockResolvedValue(
+				JSON.stringify({
+					currentApiConfigName: "default",
+					apiConfigs: {
+						default: {},
+					},
+					modeApiConfigs: {
+						code: "default",
+						architect: "default",
+						ask: "default",
+					},
+				}),
 			)
+
+			const newConfig: ProviderSettings = {
+				apiProvider: "anthropic",
+				apiKey: "test-key",
+			}
+			const newConfigWithExtra: ProviderSettings = {
+				...newConfig,
+				openRouterApiKey: "another-key",
+			}
+
+			await providerSettingsManager.saveConfig("test", newConfigWithExtra)
+
+			// Get the actual stored config to check the generated ID
+			const storedConfig = JSON.parse(mockSecrets.store.mock.lastCall[1])
+			const testConfigId = storedConfig.apiConfigs.test.id
+
+			const expectedConfig = {
+				currentApiConfigName: "default",
+				apiConfigs: {
+					default: {},
+					test: {
+						...newConfig,
+						id: testConfigId,
+					},
+				},
+				modeApiConfigs: {
+					code: "default",
+					architect: "default",
+					ask: "default",
+				},
+			}
+
+			expect(mockSecrets.store.mock.calls[0][0]).toEqual("roo_cline_config_api_config")
+			expect(storedConfig).toEqual(expectedConfig)
 		})
 
 		it("should update existing config", async () => {
@@ -291,10 +340,9 @@ describe("ProviderSettingsManager", () => {
 				},
 			}
 
-			expect(mockSecrets.store).toHaveBeenCalledWith(
-				"roo_cline_config_api_config",
-				JSON.stringify(expectedConfig, null, 2),
-			)
+			const storedConfig = JSON.parse(mockSecrets.store.mock.lastCall[1])
+			expect(mockSecrets.store.mock.lastCall[0]).toEqual("roo_cline_config_api_config")
+			expect(storedConfig).toEqual(expectedConfig)
 		})
 
 		it("should throw error if secrets storage fails", async () => {
@@ -391,17 +439,15 @@ describe("ProviderSettingsManager", () => {
 			mockGlobalState.get.mockResolvedValue(42)
 			mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
 
-			const config = await providerSettingsManager.loadConfig("test")
+			const { name, ...providerSettings } = await providerSettingsManager.activateProfile({ name: "test" })
 
-			expect(config).toEqual({
-				apiProvider: "anthropic",
-				apiKey: "test-key",
-				id: "test-id",
-			})
+			expect(name).toBe("test")
+			expect(providerSettings).toEqual({ apiProvider: "anthropic", apiKey: "test-key", id: "test-id" })
 
-			// Get the stored config to check the structure
+			// Get the stored config to check the structure.
 			const storedConfig = JSON.parse(mockSecrets.store.mock.calls[1][1])
 			expect(storedConfig.currentApiConfigName).toBe("test")
+
 			expect(storedConfig.apiConfigs.test).toEqual({
 				apiProvider: "anthropic",
 				apiKey: "test-key",
@@ -413,17 +459,12 @@ describe("ProviderSettingsManager", () => {
 			mockSecrets.get.mockResolvedValue(
 				JSON.stringify({
 					currentApiConfigName: "default",
-					apiConfigs: {
-						default: {
-							config: {},
-							id: "default",
-						},
-					},
+					apiConfigs: { default: { config: {}, id: "default" } },
 				}),
 			)
 
-			await expect(providerSettingsManager.loadConfig("nonexistent")).rejects.toThrow(
-				"Config 'nonexistent' not found",
+			await expect(providerSettingsManager.activateProfile({ name: "nonexistent" })).rejects.toThrow(
+				"Config with name 'nonexistent' not found",
 			)
 		})
 
@@ -431,20 +472,13 @@ describe("ProviderSettingsManager", () => {
 			mockSecrets.get.mockResolvedValue(
 				JSON.stringify({
 					currentApiConfigName: "default",
-					apiConfigs: {
-						test: {
-							config: {
-								apiProvider: "anthropic",
-							},
-							id: "test-id",
-						},
-					},
+					apiConfigs: { test: { config: { apiProvider: "anthropic" }, id: "test-id" } },
 				}),
 			)
 			mockSecrets.store.mockRejectedValueOnce(new Error("Storage failed"))
 
-			await expect(providerSettingsManager.loadConfig("test")).rejects.toThrow(
-				"Failed to load config: Error: Failed to write provider profiles to secrets: Error: Storage failed",
+			await expect(providerSettingsManager.activateProfile({ name: "test" })).rejects.toThrow(
+				"Failed to activate profile: Failed to write provider profiles to secrets: Error: Storage failed",
 			)
 		})
 
@@ -494,12 +528,7 @@ describe("ProviderSettingsManager", () => {
 			mockSecrets.get.mockResolvedValue(
 				JSON.stringify({
 					currentApiConfigName: "test",
-					apiConfigs: {
-						test: {
-							apiProvider: "anthropic",
-							id: "test-id",
-						},
-					},
+					apiConfigs: { test: { apiProvider: "anthropic", id: "test-id" } },
 				}),
 			)
 
@@ -514,18 +543,8 @@ describe("ProviderSettingsManager", () => {
 		it("should return true for existing config", async () => {
 			const existingConfig: ProviderProfiles = {
 				currentApiConfigName: "default",
-				apiConfigs: {
-					default: {
-						id: "default",
-					},
-					test: {
-						apiProvider: "anthropic",
-						id: "test-id",
-					},
-				},
-				migrations: {
-					rateLimitSecondsMigrated: false,
-				},
+				apiConfigs: { default: { id: "default" }, test: { apiProvider: "anthropic", id: "test-id" } },
+				migrations: { rateLimitSecondsMigrated: false },
 			}
 
 			mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
@@ -536,10 +555,7 @@ describe("ProviderSettingsManager", () => {
 
 		it("should return false for non-existent config", async () => {
 			mockSecrets.get.mockResolvedValue(
-				JSON.stringify({
-					currentApiConfigName: "default",
-					apiConfigs: { default: {} },
-				}),
+				JSON.stringify({ currentApiConfigName: "default", apiConfigs: { default: {} } }),
 			)
 
 			const hasConfig = await providerSettingsManager.hasConfig("nonexistent")

+ 3 - 3
src/core/task/Task.ts

@@ -16,7 +16,7 @@ import { ApiHandler, buildApiHandler } from "../../api"
 import { ApiStream } from "../../api/transform/stream"
 
 // shared
-import { ApiConfiguration } from "../../shared/api"
+import { ProviderSettings } from "../../shared/api"
 import { findLastIndex } from "../../shared/array"
 import { combineApiRequests } from "../../shared/combineApiRequests"
 import { combineCommandSequences } from "../../shared/combineCommandSequences"
@@ -92,7 +92,7 @@ export type ClineEvents = {
 
 export type TaskOptions = {
 	provider: ClineProvider
-	apiConfiguration: ApiConfiguration
+	apiConfiguration: ProviderSettings
 	customInstructions?: string
 	enableDiff?: boolean
 	enableCheckpoints?: boolean
@@ -130,7 +130,7 @@ export class Task extends EventEmitter<ClineEvents> {
 	customInstructions?: string
 
 	// API
-	readonly apiConfiguration: ApiConfiguration
+	readonly apiConfiguration: ProviderSettings
 	api: ApiHandler
 	private promptCacheKey: string
 	private lastApiRequestTime?: number

+ 2 - 2
src/core/task/__tests__/Task.test.ts

@@ -9,7 +9,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
 import { GlobalState } from "../../../schemas"
 import { Task } from "../Task"
 import { ClineProvider } from "../../webview/ClineProvider"
-import { ApiConfiguration, ModelInfo } from "../../../shared/api"
+import { ProviderSettings, ModelInfo } from "../../../shared/api"
 import { ApiStreamChunk } from "../../../api/transform/stream"
 import { ContextProxy } from "../../config/ContextProxy"
 import { processUserContentMentions } from "../../mentions/processUserContentMentions"
@@ -156,7 +156,7 @@ const mockMessages = [
 
 describe("Cline", () => {
 	let mockProvider: jest.Mocked<ClineProvider>
-	let mockApiConfig: ApiConfiguration
+	let mockApiConfig: ProviderSettings
 	let mockOutputChannel: any
 	let mockExtensionContext: vscode.ExtensionContext
 

+ 127 - 61
src/core/webview/ClineProvider.ts

@@ -9,16 +9,10 @@ import axios from "axios"
 import pWaitFor from "p-wait-for"
 import * as vscode from "vscode"
 
-import { GlobalState, ProviderSettings, RooCodeSettings } from "../../schemas"
+import type { GlobalState, ProviderName, ProviderSettings, RooCodeSettings, ProviderSettingsEntry } from "../../schemas"
 import { t } from "../../i18n"
 import { setPanel } from "../../activate/registerCommands"
-import {
-	ProviderName,
-	ApiConfiguration,
-	requestyDefaultModelId,
-	openRouterDefaultModelId,
-	glamaDefaultModelId,
-} from "../../shared/api"
+import { requestyDefaultModelId, openRouterDefaultModelId, glamaDefaultModelId } from "../../shared/api"
 import { findLast } from "../../shared/array"
 import { supportPrompt } from "../../shared/support-prompt"
 import { GlobalFileNames } from "../../shared/globalFileNames"
@@ -252,7 +246,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
 			return false
 		}
 
-		// check if there is a cline instance in the stack (if this provider has an active task)
+		// Check if there is a cline instance in the stack (if this provider has an active task)
 		if (visibleProvider.getCurrentCline()) {
 			return true
 		}
@@ -762,7 +756,6 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
 	 * @param newMode The mode to switch to
 	 */
 	public async handleModeSwitch(newMode: Mode) {
-		// Capture mode switch telemetry event
 		const cline = this.getCurrentCline()
 
 		if (cline) {
@@ -779,24 +772,19 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
 		// Update listApiConfigMeta first to ensure UI has latest data
 		await this.updateGlobalState("listApiConfigMeta", listApiConfig)
 
-		// If this mode has a saved config, use it
+		// If this mode has a saved config, use it.
 		if (savedConfigId) {
-			const config = listApiConfig?.find((c) => c.id === savedConfigId)
+			const profile = listApiConfig.find(({ id }) => id === savedConfigId)
 
-			if (config?.name) {
-				const apiConfig = await this.providerSettingsManager.loadConfig(config.name)
-
-				await Promise.all([
-					this.updateGlobalState("currentApiConfigName", config.name),
-					this.updateApiConfiguration(apiConfig),
-				])
+			if (profile?.name) {
+				await this.activateProviderProfile({ name: profile.name })
 			}
 		} else {
-			// If no saved config for this mode, save current config as default
+			// If no saved config for this mode, save current config as default.
 			const currentApiConfigName = this.getGlobalState("currentApiConfigName")
 
 			if (currentApiConfigName) {
-				const config = listApiConfig?.find((c) => c.name === currentApiConfigName)
+				const config = listApiConfig.find((c) => c.name === currentApiConfigName)
 
 				if (config?.id) {
 					await this.providerSettingsManager.setModeConfig(newMode, config.id)
@@ -807,27 +795,127 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
 		await this.postStateToWebview()
 	}
 
-	async updateApiConfiguration(providerSettings: ProviderSettings) {
-		// Update mode's default config.
-		const { mode } = await this.getState()
+	// Provider Profile Management
 
-		if (mode) {
-			const currentApiConfigName = this.getGlobalState("currentApiConfigName")
-			const listApiConfig = await this.providerSettingsManager.listConfig()
-			const config = listApiConfig?.find((c) => c.name === currentApiConfigName)
+	getProviderProfileEntries(): ProviderSettingsEntry[] {
+		return this.contextProxy.getValues().listApiConfigMeta || []
+	}
+
+	getProviderProfileEntry(name: string): ProviderSettingsEntry | undefined {
+		return this.getProviderProfileEntries().find((profile) => profile.name === name)
+	}
+
+	public hasProviderProfileEntry(name: string): boolean {
+		return !!this.getProviderProfileEntry(name)
+	}
+
+	async upsertProviderProfile(
+		name: string,
+		providerSettings: ProviderSettings,
+		activate: boolean = true,
+	): Promise<string | undefined> {
+		try {
+			// TODO: Do we need to be calling `activateProfile`? It's not
+			// clear to me what the source of truth should be; in some cases
+			// we rely on the `ContextProxy`'s data store and in other cases
+			// we rely on the `ProviderSettingsManager`'s data store. It might
+			// be simpler to unify these two.
+			const id = await this.providerSettingsManager.saveConfig(name, providerSettings)
+
+			if (activate) {
+				const { mode } = await this.getState()
+
+				// These promises do the following:
+				// 1. Adds or updates the list of provider profiles.
+				// 2. Sets the current provider profile.
+				// 3. Sets the current mode's provider profile.
+				// 4. Copies the provider settings to the context.
+				//
+				// Note: 1, 2, and 4 can be done in one `ContextProxy` call:
+				// this.contextProxy.setValues({ ...providerSettings, listApiConfigMeta: ..., currentApiConfigName: ... })
+				// We should probably switch to that and verify that it works.
+				// I left the original implementation in just to be safe.
+				await Promise.all([
+					this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig()),
+					this.updateGlobalState("currentApiConfigName", name),
+					this.providerSettingsManager.setModeConfig(mode, id),
+					this.contextProxy.setProviderSettings(providerSettings),
+				])
+
+				// Change the provider for the current task.
+				// TODO: We should rename `buildApiHandler` for clarity (e.g. `getProviderClient`).
+				const task = this.getCurrentCline()
 
-			if (config?.id) {
-				await this.providerSettingsManager.setModeConfig(mode, config.id)
+				if (task) {
+					task.api = buildApiHandler(providerSettings)
+				}
+			} else {
+				await this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig())
 			}
+
+			await this.postStateToWebview()
+			return id
+		} catch (error) {
+			this.log(
+				`Error create new api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
+			)
+
+			vscode.window.showErrorMessage(t("common:errors.create_api_config"))
+			return undefined
 		}
+	}
 
-		await this.contextProxy.setProviderSettings(providerSettings)
+	async deleteProviderProfile(profileToDelete: ProviderSettingsEntry) {
+		const globalSettings = this.contextProxy.getValues()
+		let profileToActivate: string | undefined = globalSettings.currentApiConfigName
 
-		if (this.getCurrentCline()) {
-			this.getCurrentCline()!.api = buildApiHandler(providerSettings)
+		if (profileToDelete.name === profileToActivate) {
+			profileToActivate = this.getProviderProfileEntries().find(({ name }) => name !== profileToDelete.name)?.name
+		}
+
+		if (!profileToActivate) {
+			throw new Error("You cannot delete the last profile")
 		}
+
+		const entries = this.getProviderProfileEntries().filter(({ name }) => name !== profileToDelete.name)
+
+		await this.contextProxy.setValues({
+			...globalSettings,
+			currentApiConfigName: profileToActivate,
+			listApiConfigMeta: entries,
+		})
+
+		await this.postStateToWebview()
 	}
 
+	async activateProviderProfile(args: { name: string } | { id: string }) {
+		const { name, id, ...providerSettings } = await this.providerSettingsManager.activateProfile(args)
+
+		// See `upsertProviderProfile` for a description of what this is doing.
+		await Promise.all([
+			this.contextProxy.setValue("listApiConfigMeta", await this.providerSettingsManager.listConfig()),
+			this.contextProxy.setValue("currentApiConfigName", name),
+			this.contextProxy.setProviderSettings(providerSettings),
+		])
+
+		const { mode } = await this.getState()
+
+		if (id) {
+			await this.providerSettingsManager.setModeConfig(mode, id)
+		}
+
+		// Change the provider for the current task.
+		const task = this.getCurrentCline()
+
+		if (task) {
+			task.api = buildApiHandler(providerSettings)
+		}
+
+		await this.postStateToWebview()
+	}
+
+	// Task Management
+
 	async cancelTask() {
 		const cline = this.getCurrentCline()
 
@@ -936,14 +1024,14 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
 			throw error
 		}
 
-		const newConfiguration: ApiConfiguration = {
+		const newConfiguration: ProviderSettings = {
 			...apiConfiguration,
 			apiProvider: "openrouter",
 			openRouterApiKey: apiKey,
 			openRouterModelId: apiConfiguration?.openRouterModelId || openRouterDefaultModelId,
 		}
 
-		await this.upsertApiConfiguration(currentApiConfigName, newConfiguration)
+		await this.upsertProviderProfile(currentApiConfigName, newConfiguration)
 	}
 
 	// Glama
@@ -966,14 +1054,14 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
 
 		const { apiConfiguration, currentApiConfigName } = await this.getState()
 
-		const newConfiguration: ApiConfiguration = {
+		const newConfiguration: ProviderSettings = {
 			...apiConfiguration,
 			apiProvider: "glama",
 			glamaApiKey: apiKey,
 			glamaModelId: apiConfiguration?.glamaModelId || glamaDefaultModelId,
 		}
 
-		await this.upsertApiConfiguration(currentApiConfigName, newConfiguration)
+		await this.upsertProviderProfile(currentApiConfigName, newConfiguration)
 	}
 
 	// Requesty
@@ -981,36 +1069,14 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
 	async handleRequestyCallback(code: string) {
 		let { apiConfiguration, currentApiConfigName } = await this.getState()
 
-		const newConfiguration: ApiConfiguration = {
+		const newConfiguration: ProviderSettings = {
 			...apiConfiguration,
 			apiProvider: "requesty",
 			requestyApiKey: code,
 			requestyModelId: apiConfiguration?.requestyModelId || requestyDefaultModelId,
 		}
 
-		await this.upsertApiConfiguration(currentApiConfigName, newConfiguration)
-	}
-
-	// Save configuration
-
-	async upsertApiConfiguration(configName: string, apiConfiguration: ApiConfiguration) {
-		try {
-			await this.providerSettingsManager.saveConfig(configName, apiConfiguration)
-			const listApiConfig = await this.providerSettingsManager.listConfig()
-
-			await Promise.all([
-				this.updateGlobalState("listApiConfigMeta", listApiConfig),
-				this.updateApiConfiguration(apiConfiguration),
-				this.updateGlobalState("currentApiConfigName", configName),
-			])
-
-			await this.postStateToWebview()
-		} catch (error) {
-			this.log(
-				`Error create new api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
-			)
-			vscode.window.showErrorMessage(t("common:errors.create_api_config"))
-		}
+		await this.upsertProviderProfile(currentApiConfigName, newConfiguration)
 	}
 
 	// Task history

+ 40 - 41
src/core/webview/__tests__/ClineProvider.test.ts

@@ -5,7 +5,7 @@ import * as vscode from "vscode"
 import axios from "axios"
 
 import { ClineProvider } from "../ClineProvider"
-import { ClineMessage, ExtensionMessage, ExtensionState } from "../../../shared/ExtensionMessage"
+import { ProviderSettingsEntry, ClineMessage, ExtensionMessage, ExtensionState } from "../../../shared/ExtensionMessage"
 import { setSoundEnabled } from "../../../utils/sound"
 import { setTtsEnabled } from "../../../utils/tts"
 import { defaultModeSlug } from "../../../shared/modes"
@@ -227,12 +227,6 @@ jest.mock("../../../integrations/misc/extract-text", () => ({
 	}),
 }))
 
-// Spy on console.error and console.log to suppress expected messages
-beforeAll(() => {
-	jest.spyOn(console, "error").mockImplementation(() => {})
-	jest.spyOn(console, "log").mockImplementation(() => {})
-})
-
 afterAll(() => {
 	jest.restoreAllMocks()
 })
@@ -596,14 +590,16 @@ describe("ClineProvider", () => {
 		expect(state.alwaysApproveResubmit).toBe(false)
 	})
 
-	test("loads saved API config when switching modes", async () => {
+	it("loads saved API config when switching modes", async () => {
 		await provider.resolveWebviewView(mockWebviewView)
 		const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
 
+		const profile: ProviderSettingsEntry = { name: "test-config", id: "test-id", apiProvider: "anthropic" }
+
 		;(provider as any).providerSettingsManager = {
 			getModeConfigId: jest.fn().mockResolvedValue("test-id"),
-			listConfig: jest.fn().mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
-			loadConfig: jest.fn().mockResolvedValue({ apiProvider: "anthropic" }),
+			listConfig: jest.fn().mockResolvedValue([profile]),
+			activateProfile: jest.fn().mockResolvedValue(profile),
 			setModeConfig: jest.fn(),
 		} as any
 
@@ -612,11 +608,11 @@ describe("ClineProvider", () => {
 
 		// Should load the saved config for architect mode
 		expect(provider.providerSettingsManager.getModeConfigId).toHaveBeenCalledWith("architect")
-		expect(provider.providerSettingsManager.loadConfig).toHaveBeenCalledWith("test-config")
+		expect(provider.providerSettingsManager.activateProfile).toHaveBeenCalledWith({ name: "test-config" })
 		expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "test-config")
 	})
 
-	test("saves current config when switching to mode without config", async () => {
+	it("saves current config when switching to mode without config", async () => {
 		await provider.resolveWebviewView(mockWebviewView)
 		const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
 
@@ -637,16 +633,15 @@ describe("ClineProvider", () => {
 		expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("architect", "current-id")
 	})
 
-	test("saves config as default for current mode when loading config", async () => {
+	it("saves config as default for current mode when loading config", async () => {
 		await provider.resolveWebviewView(mockWebviewView)
 		const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
 
+		const profile: ProviderSettingsEntry = { apiProvider: "anthropic", id: "new-id", name: "new-config" }
+
 		;(provider as any).providerSettingsManager = {
-			loadConfig: jest.fn().mockResolvedValue({ apiProvider: "anthropic", id: "new-id" }),
-			loadConfigById: jest
-				.fn()
-				.mockResolvedValue({ config: { apiProvider: "anthropic", id: "new-id" }, name: "new-config" }),
-			listConfig: jest.fn().mockResolvedValue([{ name: "new-config", id: "new-id", apiProvider: "anthropic" }]),
+			activateProfile: jest.fn().mockResolvedValue(profile),
+			listConfig: jest.fn().mockResolvedValue([profile]),
 			setModeConfig: jest.fn(),
 			getModeConfigId: jest.fn().mockResolvedValue(undefined),
 		} as any
@@ -661,18 +656,19 @@ describe("ClineProvider", () => {
 		expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("architect", "new-id")
 	})
 
-	test("load API configuration by ID works and updates mode config", async () => {
+	it("load API configuration by ID works and updates mode config", async () => {
 		await provider.resolveWebviewView(mockWebviewView)
 		const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
 
+		const profile: ProviderSettingsEntry = {
+			name: "config-by-id",
+			id: "config-id-123",
+			apiProvider: "anthropic",
+		}
+
 		;(provider as any).providerSettingsManager = {
-			loadConfigById: jest.fn().mockResolvedValue({
-				config: { apiProvider: "anthropic", id: "config-id-123" },
-				name: "config-by-id",
-			}),
-			listConfig: jest
-				.fn()
-				.mockResolvedValue([{ name: "config-by-id", id: "config-id-123", apiProvider: "anthropic" }]),
+			activateProfile: jest.fn().mockResolvedValue(profile),
+			listConfig: jest.fn().mockResolvedValue([profile]),
 			setModeConfig: jest.fn(),
 			getModeConfigId: jest.fn().mockResolvedValue(undefined),
 		} as any
@@ -686,8 +682,8 @@ describe("ClineProvider", () => {
 		// Should save new config as default for architect mode
 		expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("architect", "config-id-123")
 
-		// Ensure the loadConfigById method was called with the correct ID
-		expect(provider.providerSettingsManager.loadConfigById).toHaveBeenCalledWith("config-id-123")
+		// Ensure the `activateProfile` method was called with the correct ID
+		expect(provider.providerSettingsManager.activateProfile).toHaveBeenCalledWith({ id: "config-id-123" })
 	})
 
 	test("handles browserToolEnabled setting", async () => {
@@ -886,7 +882,7 @@ describe("ClineProvider", () => {
 		})
 	})
 
-	test("saves mode config when updating API configuration", async () => {
+	it("saves mode config when updating API configuration", async () => {
 		// Setup mock context with mode and config name
 		mockContext = {
 			...mockContext,
@@ -912,12 +908,14 @@ describe("ClineProvider", () => {
 
 		;(provider as any).providerSettingsManager = {
 			listConfig: jest.fn().mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
+			saveConfig: jest.fn().mockResolvedValue("test-id"),
 			setModeConfig: jest.fn(),
 		} as any
 
 		// Update API configuration
 		await messageHandler({
-			type: "apiConfiguration",
+			type: "upsertApiConfiguration",
+			text: "test-config",
 			apiConfiguration: { apiProvider: "anthropic" },
 		})
 
@@ -1542,13 +1540,17 @@ describe("ClineProvider", () => {
 			await provider.resolveWebviewView(mockWebviewView)
 		})
 
-		test("loads saved API config when switching modes", async () => {
+		it("loads saved API config when switching modes", async () => {
+			const profile: ProviderSettingsEntry = {
+				name: "saved-config",
+				id: "saved-config-id",
+				apiProvider: "anthropic",
+			}
+
 			;(provider as any).providerSettingsManager = {
 				getModeConfigId: jest.fn().mockResolvedValue("saved-config-id"),
-				listConfig: jest
-					.fn()
-					.mockResolvedValue([{ name: "saved-config", id: "saved-config-id", apiProvider: "anthropic" }]),
-				loadConfig: jest.fn().mockResolvedValue({ apiProvider: "anthropic" }),
+				listConfig: jest.fn().mockResolvedValue([profile]),
+				activateProfile: jest.fn().mockResolvedValue(profile),
 				setModeConfig: jest.fn(),
 			} as any
 
@@ -1560,7 +1562,7 @@ describe("ClineProvider", () => {
 
 			// Verify saved config was loaded
 			expect(provider.providerSettingsManager.getModeConfigId).toHaveBeenCalledWith("architect")
-			expect(provider.providerSettingsManager.loadConfig).toHaveBeenCalledWith("saved-config")
+			expect(provider.providerSettingsManager.activateProfile).toHaveBeenCalledWith({ name: "saved-config" })
 			expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "saved-config")
 
 			// Verify state was posted to webview
@@ -1676,14 +1678,11 @@ describe("ClineProvider", () => {
 				currentApiConfigName: "test-config",
 			} as any)
 
-			// Trigger updateApiConfiguration
+			// Trigger upsertApiConfiguration
 			await messageHandler({
 				type: "upsertApiConfiguration",
 				text: "test-config",
-				apiConfiguration: {
-					apiProvider: "anthropic",
-					apiKey: "test-key",
-				},
+				apiConfiguration: { apiProvider: "anthropic", apiKey: "test-key" },
 			})
 
 			// Verify error was logged and user was notified

+ 46 - 99
src/core/webview/webviewMessageHandler.ts

@@ -4,9 +4,9 @@ import pWaitFor from "p-wait-for"
 import * as vscode from "vscode"
 
 import { ClineProvider } from "./ClineProvider"
-import { Language, ApiConfigMeta } from "../../schemas"
+import { Language, ProviderSettings } from "../../schemas"
 import { changeLanguage, t } from "../../i18n"
-import { ApiConfiguration, RouterName, toRouterName } from "../../shared/api"
+import { RouterName, toRouterName } from "../../shared/api"
 import { supportPrompt } from "../../shared/support-prompt"
 
 import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage"
@@ -89,19 +89,12 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
 
 					if (currentConfigName) {
 						if (!(await provider.providerSettingsManager.hasConfig(currentConfigName))) {
-							// current config name not valid, get first config in list
-							await updateGlobalState("currentApiConfigName", listApiConfig?.[0]?.name)
-							if (listApiConfig?.[0]?.name) {
-								const apiConfig = await provider.providerSettingsManager.loadConfig(
-									listApiConfig?.[0]?.name,
-								)
+							// Current config name not valid, get first config in list.
+							const name = listApiConfig[0]?.name
+							await updateGlobalState("currentApiConfigName", name)
 
-								await Promise.all([
-									updateGlobalState("listApiConfigMeta", listApiConfig),
-									provider.postMessageToWebview({ type: "listApiConfig", listApiConfig }),
-									provider.updateApiConfiguration(apiConfig),
-								])
-								await provider.postStateToWebview()
+							if (name) {
+								await provider.activateProviderProfile({ name })
 								return
 							}
 						}
@@ -133,12 +126,6 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
 			// task. This essentially creates a fresh slate for the new task.
 			await provider.initClineWithTask(message.text, message.images)
 			break
-		case "apiConfiguration":
-			if (message.apiConfiguration) {
-				await provider.updateApiConfiguration(message.apiConfiguration)
-			}
-			await provider.postStateToWebview()
-			break
 		case "customInstructions":
 			await provider.updateCustomInstructions(message.text)
 			break
@@ -939,45 +926,36 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
 					const { apiConfiguration, customSupportPrompts, listApiConfigMeta, enhancementApiConfigId } =
 						await provider.getState()
 
-					// Try to get enhancement config first, fall back to current config
-					let configToUse: ApiConfiguration = apiConfiguration
-					if (enhancementApiConfigId) {
-						const config = listApiConfigMeta?.find((c: ApiConfigMeta) => c.id === enhancementApiConfigId)
-						if (config?.name) {
-							const loadedConfig = await provider.providerSettingsManager.loadConfig(config.name)
-							if (loadedConfig.apiProvider) {
-								configToUse = loadedConfig
-							}
+					// Try to get enhancement config first, fall back to current config.
+					let configToUse: ProviderSettings = apiConfiguration
+
+					if (enhancementApiConfigId && !!listApiConfigMeta.find(({ id }) => id === enhancementApiConfigId)) {
+						const { name: _, ...providerSettings } = await provider.providerSettingsManager.getProfile({
+							id: enhancementApiConfigId,
+						})
+
+						if (providerSettings.apiProvider) {
+							configToUse = providerSettings
 						}
 					}
 
 					const enhancedPrompt = await singleCompletionHandler(
 						configToUse,
-						supportPrompt.create(
-							"ENHANCE",
-							{
-								userInput: message.text,
-							},
-							customSupportPrompts,
-						),
+						supportPrompt.create("ENHANCE", { userInput: message.text }, customSupportPrompts),
 					)
 
-					// Capture telemetry for prompt enhancement
+					// Capture telemetry for prompt enhancement.
 					const currentCline = provider.getCurrentCline()
 					telemetryService.capturePromptEnhanced(currentCline?.taskId)
 
-					await provider.postMessageToWebview({
-						type: "enhancedPrompt",
-						text: enhancedPrompt,
-					})
+					await provider.postMessageToWebview({ type: "enhancedPrompt", text: enhancedPrompt })
 				} catch (error) {
 					provider.log(
 						`Error enhancing prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
 					)
+
 					vscode.window.showErrorMessage(t("common:errors.enhance_prompt"))
-					await provider.postMessageToWebview({
-						type: "enhancedPrompt",
-					})
+					await provider.postMessageToWebview({ type: "enhancedPrompt" })
 				}
 			}
 			break
@@ -1084,7 +1062,7 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
 			break
 		case "upsertApiConfiguration":
 			if (message.text && message.apiConfiguration) {
-				await provider.upsertApiConfiguration(message.text, message.apiConfiguration)
+				await provider.upsertProviderProfile(message.text, message.apiConfiguration)
 			}
 			break
 		case "renameApiConfiguration":
@@ -1096,30 +1074,23 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
 						break
 					}
 
-					// Load the old configuration to get its ID
-					const oldConfig = await provider.providerSettingsManager.loadConfig(oldName)
+					// Load the old configuration to get its ID.
+					const { id } = await provider.providerSettingsManager.getProfile({ name: oldName })
 
-					// Create a new configuration with the same ID
-					const newConfig = {
-						...message.apiConfiguration,
-						id: oldConfig.id, // Preserve the ID
-					}
+					// Create a new configuration with the new name and old ID.
+					await provider.providerSettingsManager.saveConfig(newName, { ...message.apiConfiguration, id })
 
-					// Save with the new name but same ID
-					await provider.providerSettingsManager.saveConfig(newName, newConfig)
+					// Delete the old configuration.
 					await provider.providerSettingsManager.deleteConfig(oldName)
 
-					const listApiConfig = await provider.providerSettingsManager.listConfig()
-
-					// Update listApiConfigMeta first to ensure UI has latest data
-					await updateGlobalState("listApiConfigMeta", listApiConfig)
-					await updateGlobalState("currentApiConfigName", newName)
-
-					await provider.postStateToWebview()
+					// Re-activate to update the global settings related to the
+					// currently activated provider profile.
+					await provider.activateProviderProfile({ name: newName })
 				} catch (error) {
 					provider.log(
 						`Error rename api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
 					)
+
 					vscode.window.showErrorMessage(t("common:errors.rename_api_config"))
 				}
 			}
@@ -1127,16 +1098,7 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
 		case "loadApiConfiguration":
 			if (message.text) {
 				try {
-					const apiConfig = await provider.providerSettingsManager.loadConfig(message.text)
-					const listApiConfig = await provider.providerSettingsManager.listConfig()
-
-					await Promise.all([
-						updateGlobalState("listApiConfigMeta", listApiConfig),
-						updateGlobalState("currentApiConfigName", message.text),
-						provider.updateApiConfiguration(apiConfig),
-					])
-
-					await provider.postStateToWebview()
+					await provider.activateProviderProfile({ name: message.text })
 				} catch (error) {
 					provider.log(
 						`Error load api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
@@ -1148,18 +1110,7 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
 		case "loadApiConfigurationById":
 			if (message.text) {
 				try {
-					const { config: apiConfig, name } = await provider.providerSettingsManager.loadConfigById(
-						message.text,
-					)
-					const listApiConfig = await provider.providerSettingsManager.listConfig()
-
-					await Promise.all([
-						updateGlobalState("listApiConfigMeta", listApiConfig),
-						updateGlobalState("currentApiConfigName", name),
-						provider.updateApiConfiguration(apiConfig),
-					])
-
-					await provider.postStateToWebview()
+					await provider.activateProviderProfile({ id: message.text })
 				} catch (error) {
 					provider.log(
 						`Error load api configuration by ID: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
@@ -1180,29 +1131,25 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
 					break
 				}
 
-				try {
-					await provider.providerSettingsManager.deleteConfig(message.text)
-					const listApiConfig = await provider.providerSettingsManager.listConfig()
-
-					// Update listApiConfigMeta first to ensure UI has latest data
-					await updateGlobalState("listApiConfigMeta", listApiConfig)
+				const oldName = message.text
 
-					// If this was the current config, switch to first available
-					const currentApiConfigName = getGlobalState("currentApiConfigName")
+				const newName = (await provider.providerSettingsManager.listConfig()).filter(
+					(c) => c.name !== oldName,
+				)[0]?.name
 
-					if (message.text === currentApiConfigName && listApiConfig?.[0]?.name) {
-						const apiConfig = await provider.providerSettingsManager.loadConfig(listApiConfig[0].name)
-						await Promise.all([
-							updateGlobalState("currentApiConfigName", listApiConfig[0].name),
-							provider.updateApiConfiguration(apiConfig),
-						])
-					}
+				if (!newName) {
+					vscode.window.showErrorMessage(t("common:errors.delete_api_config"))
+					return
+				}
 
-					await provider.postStateToWebview()
+				try {
+					await provider.providerSettingsManager.deleteConfig(oldName)
+					await provider.activateProviderProfile({ name: newName })
 				} catch (error) {
 					provider.log(
 						`Error delete api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
 					)
+
 					vscode.window.showErrorMessage(t("common:errors.delete_api_config"))
 				}
 			}

+ 107 - 86
src/exports/api.ts

@@ -6,7 +6,14 @@ import * as path from "path"
 import { getWorkspacePath } from "../utils/path"
 import { ClineProvider } from "../core/webview/ClineProvider"
 import { openClineInNewTab } from "../activate/registerCommands"
-import { RooCodeSettings, RooCodeEvents, RooCodeEventName } from "../schemas"
+import {
+	RooCodeSettings,
+	RooCodeEvents,
+	RooCodeEventName,
+	ProviderSettings,
+	ProviderSettingsEntry,
+	isSecretStateKey,
+} from "../schemas"
 import { IpcOrigin, IpcMessageType, TaskCommandName, TaskEvent } from "../schemas/ipc"
 
 import { RooCodeAPI } from "./interface"
@@ -178,91 +185,6 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
 		await this.sidebarProvider.postMessageToWebview({ type: "invoke", invoke: "secondaryButtonClick" })
 	}
 
-	public getConfiguration() {
-		return this.sidebarProvider.getValues()
-	}
-
-	public async setConfiguration(values: RooCodeSettings) {
-		await this.sidebarProvider.setValues(values)
-		await this.sidebarProvider.providerSettingsManager.saveConfig(values.currentApiConfigName || "default", values)
-		await this.sidebarProvider.postStateToWebview()
-	}
-
-	public async createProfile(name: string) {
-		if (!name || !name.trim()) {
-			throw new Error("Profile name cannot be empty")
-		}
-
-		const currentSettings = this.getConfiguration()
-		const profiles = currentSettings.listApiConfigMeta || []
-
-		if (profiles.some((profile) => profile.name === name)) {
-			throw new Error(`A profile with the name "${name}" already exists`)
-		}
-
-		const id = this.sidebarProvider.providerSettingsManager.generateId()
-
-		await this.setConfiguration({
-			...currentSettings,
-			listApiConfigMeta: [
-				...profiles,
-				{
-					id,
-					name: name.trim(),
-					apiProvider: "openai" as const,
-				},
-			],
-		})
-
-		return id
-	}
-
-	public getProfiles() {
-		return (this.getConfiguration().listApiConfigMeta || []).map((profile) => profile.name)
-	}
-
-	public async setActiveProfile(name: string) {
-		const currentSettings = this.getConfiguration()
-		const profiles = currentSettings.listApiConfigMeta || []
-
-		const profile = profiles.find((p) => p.name === name)
-
-		if (!profile) {
-			throw new Error(`Profile with name "${name}" does not exist`)
-		}
-
-		await this.setConfiguration({ ...currentSettings, currentApiConfigName: profile.name })
-	}
-
-	public getActiveProfile() {
-		return this.getConfiguration().currentApiConfigName
-	}
-
-	public async deleteProfile(name: string) {
-		const currentSettings = this.getConfiguration()
-		const profiles = currentSettings.listApiConfigMeta || []
-		const targetIndex = profiles.findIndex((p) => p.name === name)
-
-		if (targetIndex === -1) {
-			throw new Error(`Profile with name "${name}" does not exist`)
-		}
-
-		const profileToDelete = profiles[targetIndex]
-		profiles.splice(targetIndex, 1)
-
-		// If we're deleting the active profile, clear the currentApiConfigName.
-		const newSettings: RooCodeSettings = {
-			...currentSettings,
-			listApiConfigMeta: profiles,
-			currentApiConfigName:
-				currentSettings.currentApiConfigName === profileToDelete.name
-					? undefined
-					: currentSettings.currentApiConfigName,
-		}
-
-		await this.setConfiguration(newSettings)
-	}
-
 	public isReady() {
 		return this.sidebarProvider.viewLaunched
 	}
@@ -328,4 +250,103 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
 			this.logfile = undefined
 		}
 	}
+
+	// Global Settings Management
+
+	public getConfiguration(): RooCodeSettings {
+		return Object.fromEntries(
+			Object.entries(this.sidebarProvider.getValues()).filter(([key]) => !isSecretStateKey(key)),
+		)
+	}
+
+	public async setConfiguration(values: RooCodeSettings) {
+		await this.sidebarProvider.contextProxy.setValues(values)
+		await this.sidebarProvider.providerSettingsManager.saveConfig(values.currentApiConfigName || "default", values)
+		await this.sidebarProvider.postStateToWebview()
+	}
+
+	// Provider Profile Management
+
+	public getProfiles(): string[] {
+		return this.sidebarProvider.getProviderProfileEntries().map(({ name }) => name)
+	}
+
+	public getProfileEntry(name: string): ProviderSettingsEntry | undefined {
+		return this.sidebarProvider.getProviderProfileEntry(name)
+	}
+
+	public async createProfile(name: string, profile?: ProviderSettings, activate: boolean = true) {
+		const entry = this.getProfileEntry(name)
+
+		if (entry) {
+			throw new Error(`Profile with name "${name}" already exists`)
+		}
+
+		const id = await this.sidebarProvider.upsertProviderProfile(name, profile ?? {}, activate)
+
+		if (!id) {
+			throw new Error(`Failed to create profile with name "${name}"`)
+		}
+
+		return id
+	}
+
+	public async updateProfile(
+		name: string,
+		profile: ProviderSettings,
+		activate: boolean = true,
+	): Promise<string | undefined> {
+		const entry = this.getProfileEntry(name)
+
+		if (!entry) {
+			throw new Error(`Profile with name "${name}" does not exist`)
+		}
+
+		const id = await this.sidebarProvider.upsertProviderProfile(name, profile, activate)
+
+		if (!id) {
+			throw new Error(`Failed to update profile with name "${name}"`)
+		}
+
+		return id
+	}
+
+	public async upsertProfile(
+		name: string,
+		profile: ProviderSettings,
+		activate: boolean = true,
+	): Promise<string | undefined> {
+		const id = await this.sidebarProvider.upsertProviderProfile(name, profile, activate)
+
+		if (!id) {
+			throw new Error(`Failed to upsert profile with name "${name}"`)
+		}
+
+		return id
+	}
+
+	public async deleteProfile(name: string): Promise<void> {
+		const entry = this.getProfileEntry(name)
+
+		if (!entry) {
+			throw new Error(`Profile with name "${name}" does not exist`)
+		}
+
+		await this.sidebarProvider.deleteProviderProfile(entry)
+	}
+
+	public getActiveProfile(): string | undefined {
+		return this.getConfiguration().currentApiConfigName
+	}
+
+	public async setActiveProfile(name: string): Promise<string | undefined> {
+		const entry = this.getProfileEntry(name)
+
+		if (!entry) {
+			throw new Error(`Profile with name "${name}" does not exist`)
+		}
+
+		await this.sidebarProvider.activateProviderProfile({ name })
+		return this.getActiveProfile()
+	}
 }

+ 61 - 16
src/exports/interface.ts

@@ -1,7 +1,23 @@
 import { EventEmitter } from "events"
 
-import type { ProviderSettings, GlobalSettings, ClineMessage, TokenUsage, RooCodeEvents } from "./types"
-export type { RooCodeSettings, ProviderSettings, GlobalSettings, ClineMessage, TokenUsage, RooCodeEvents }
+import type {
+	GlobalSettings,
+	ProviderSettings,
+	ProviderSettingsEntry,
+	ClineMessage,
+	TokenUsage,
+	RooCodeEvents,
+} from "./types"
+
+export type {
+	RooCodeSettings,
+	GlobalSettings,
+	ProviderSettings,
+	ProviderSettingsEntry,
+	ClineMessage,
+	TokenUsage,
+	RooCodeEvents,
+}
 
 import { RooCodeEventName } from "../schemas"
 export type { RooCodeEventName }
@@ -74,6 +90,11 @@ export interface RooCodeAPI extends EventEmitter<RooCodeEvents> {
 	 */
 	pressSecondaryButton(): Promise<void>
 
+	/**
+	 * Returns true if the API is ready to use.
+	 */
+	isReady(): boolean
+
 	/**
 	 * Returns the current configuration.
 	 * @returns The current configuration.
@@ -87,30 +108,46 @@ export interface RooCodeAPI extends EventEmitter<RooCodeEvents> {
 	setConfiguration(values: RooCodeSettings): Promise<void>
 
 	/**
-	 * Creates a new API configuration profile
+	 * Returns a list of all configured profile names
+	 * @returns Array of profile names
+	 */
+	getProfiles(): string[]
+
+	/**
+	 * Returns the profile entry for a given name
 	 * @param name The name of the profile
-	 * @returns The ID of the created profile
+	 * @returns The profile entry, or undefined if the profile does not exist
 	 */
-	createProfile(name: string): Promise<string>
+	getProfileEntry(name: string): ProviderSettingsEntry | undefined
 
 	/**
-	 * Returns a list of all configured profile names
-	 * @returns Array of profile names
+	 * Creates a new API configuration profile
+	 * @param name The name of the profile
+	 * @param profile The profile to create; defaults to an empty object
+	 * @param activate Whether to activate the profile after creation; defaults to true
+	 * @returns The ID of the created profile
+	 * @throws Error if the profile already exists
 	 */
-	getProfiles(): string[]
+	createProfile(name: string, profile?: ProviderSettings, activate?: boolean): Promise<string>
 
 	/**
-	 * Changes the active API configuration profile
-	 * @param name The name of the profile to activate
+	 * Updates an existing API configuration profile
+	 * @param name The name of the profile
+	 * @param profile The profile to update
+	 * @param activate Whether to activate the profile after update; defaults to true
+	 * @returns The ID of the updated profile
 	 * @throws Error if the profile does not exist
 	 */
-	setActiveProfile(name: string): Promise<void>
+	updateProfile(name: string, profile: ProviderSettings, activate?: boolean): Promise<string | undefined>
 
 	/**
-	 * Returns the name of the currently active profile
-	 * @returns The profile name, or undefined if no profile is active
+	 * Creates a new API configuration profile or updates an existing one
+	 * @param name The name of the profile
+	 * @param profile The profile to create or update; defaults to an empty object
+	 * @param activate Whether to activate the profile after upsert; defaults to true
+	 * @returns The ID of the upserted profile
 	 */
-	getActiveProfile(): string | undefined
+	upsertProfile(name: string, profile: ProviderSettings, activate?: boolean): Promise<string | undefined>
 
 	/**
 	 * Deletes a profile by name
@@ -120,7 +157,15 @@ export interface RooCodeAPI extends EventEmitter<RooCodeEvents> {
 	deleteProfile(name: string): Promise<void>
 
 	/**
-	 * Returns true if the API is ready to use.
+	 * Returns the name of the currently active profile
+	 * @returns The profile name, or undefined if no profile is active
 	 */
-	isReady(): boolean
+	getActiveProfile(): string | undefined
+
+	/**
+	 * Changes the active API configuration profile
+	 * @param name The name of the profile to activate
+	 * @throws Error if the profile does not exist
+	 */
+	setActiveProfile(name: string): Promise<string | undefined>
 }

+ 210 - 153
src/exports/roo-code.d.ts

@@ -1,144 +1,5 @@
 import { EventEmitter } from "events"
 
-type ProviderSettings = {
-	apiProvider?:
-		| (
-				| "anthropic"
-				| "glama"
-				| "openrouter"
-				| "bedrock"
-				| "vertex"
-				| "openai"
-				| "ollama"
-				| "vscode-lm"
-				| "lmstudio"
-				| "gemini"
-				| "openai-native"
-				| "mistral"
-				| "deepseek"
-				| "unbound"
-				| "requesty"
-				| "human-relay"
-				| "fake-ai"
-				| "xai"
-				| "groq"
-				| "chutes"
-				| "litellm"
-		  )
-		| undefined
-	apiModelId?: string | undefined
-	apiKey?: string | undefined
-	anthropicBaseUrl?: string | undefined
-	anthropicUseAuthToken?: boolean | undefined
-	glamaModelId?: string | undefined
-	glamaApiKey?: string | undefined
-	openRouterApiKey?: string | undefined
-	openRouterModelId?: string | undefined
-	openRouterBaseUrl?: string | undefined
-	openRouterSpecificProvider?: string | undefined
-	openRouterUseMiddleOutTransform?: boolean | undefined
-	awsAccessKey?: string | undefined
-	awsSecretKey?: string | undefined
-	awsSessionToken?: string | undefined
-	awsRegion?: string | undefined
-	awsUseCrossRegionInference?: boolean | undefined
-	awsUsePromptCache?: boolean | undefined
-	awsProfile?: string | undefined
-	awsUseProfile?: boolean | undefined
-	awsCustomArn?: string | undefined
-	vertexKeyFile?: string | undefined
-	vertexJsonCredentials?: string | undefined
-	vertexProjectId?: string | undefined
-	vertexRegion?: string | undefined
-	openAiBaseUrl?: string | undefined
-	openAiApiKey?: string | undefined
-	openAiLegacyFormat?: boolean | undefined
-	openAiR1FormatEnabled?: boolean | undefined
-	openAiModelId?: string | undefined
-	openAiCustomModelInfo?:
-		| ({
-				maxTokens?: (number | null) | undefined
-				maxThinkingTokens?: (number | null) | undefined
-				contextWindow: number
-				supportsImages?: boolean | undefined
-				supportsComputerUse?: boolean | undefined
-				supportsPromptCache: boolean
-				isPromptCacheOptional?: boolean | undefined
-				inputPrice?: number | undefined
-				outputPrice?: number | undefined
-				cacheWritesPrice?: number | undefined
-				cacheReadsPrice?: number | undefined
-				description?: string | undefined
-				reasoningEffort?: ("low" | "medium" | "high") | undefined
-				thinking?: boolean | undefined
-				minTokensPerCachePoint?: number | undefined
-				maxCachePoints?: number | undefined
-				cachableFields?: string[] | undefined
-				tiers?:
-					| {
-							contextWindow: number
-							inputPrice?: number | undefined
-							outputPrice?: number | undefined
-							cacheWritesPrice?: number | undefined
-							cacheReadsPrice?: number | undefined
-					  }[]
-					| undefined
-		  } | null)
-		| undefined
-	openAiUseAzure?: boolean | undefined
-	azureApiVersion?: string | undefined
-	openAiStreamingEnabled?: boolean | undefined
-	enableReasoningEffort?: boolean | undefined
-	openAiHostHeader?: string | undefined
-	openAiHeaders?:
-		| {
-				[x: string]: string
-		  }
-		| undefined
-	ollamaModelId?: string | undefined
-	ollamaBaseUrl?: string | undefined
-	vsCodeLmModelSelector?:
-		| {
-				vendor?: string | undefined
-				family?: string | undefined
-				version?: string | undefined
-				id?: string | undefined
-		  }
-		| undefined
-	lmStudioModelId?: string | undefined
-	lmStudioBaseUrl?: string | undefined
-	lmStudioDraftModelId?: string | undefined
-	lmStudioSpeculativeDecodingEnabled?: boolean | undefined
-	geminiApiKey?: string | undefined
-	googleGeminiBaseUrl?: string | undefined
-	openAiNativeApiKey?: string | undefined
-	openAiNativeBaseUrl?: string | undefined
-	mistralApiKey?: string | undefined
-	mistralCodestralUrl?: string | undefined
-	deepSeekBaseUrl?: string | undefined
-	deepSeekApiKey?: string | undefined
-	unboundApiKey?: string | undefined
-	unboundModelId?: string | undefined
-	requestyApiKey?: string | undefined
-	requestyModelId?: string | undefined
-	xaiApiKey?: string | undefined
-	groqApiKey?: string | undefined
-	chutesApiKey?: string | undefined
-	litellmBaseUrl?: string | undefined
-	litellmApiKey?: string | undefined
-	litellmModelId?: string | undefined
-	modelMaxTokens?: number | undefined
-	modelMaxThinkingTokens?: number | undefined
-	includeMaxTokens?: boolean | undefined
-	reasoningEffort?: ("low" | "medium" | "high") | undefined
-	promptCachingDisabled?: boolean | undefined
-	diffEnabled?: boolean | undefined
-	fuzzyMatchThreshold?: number | undefined
-	modelTemperature?: (number | null) | undefined
-	rateLimitSeconds?: number | undefined
-	fakeAi?: unknown | undefined
-}
-
 type GlobalSettings = {
 	currentApiConfigName?: string | undefined
 	listApiConfigMeta?:
@@ -309,6 +170,175 @@ type GlobalSettings = {
 	historyPreviewCollapsed?: boolean | undefined
 }
 
+type ProviderSettings = {
+	apiProvider?:
+		| (
+				| "anthropic"
+				| "glama"
+				| "openrouter"
+				| "bedrock"
+				| "vertex"
+				| "openai"
+				| "ollama"
+				| "vscode-lm"
+				| "lmstudio"
+				| "gemini"
+				| "openai-native"
+				| "mistral"
+				| "deepseek"
+				| "unbound"
+				| "requesty"
+				| "human-relay"
+				| "fake-ai"
+				| "xai"
+				| "groq"
+				| "chutes"
+				| "litellm"
+		  )
+		| undefined
+	includeMaxTokens?: boolean | undefined
+	reasoningEffort?: ("low" | "medium" | "high") | undefined
+	promptCachingDisabled?: boolean | undefined
+	diffEnabled?: boolean | undefined
+	fuzzyMatchThreshold?: number | undefined
+	modelTemperature?: (number | null) | undefined
+	rateLimitSeconds?: number | undefined
+	modelMaxTokens?: number | undefined
+	modelMaxThinkingTokens?: number | undefined
+	apiModelId?: string | undefined
+	apiKey?: string | undefined
+	anthropicBaseUrl?: string | undefined
+	anthropicUseAuthToken?: boolean | undefined
+	glamaModelId?: string | undefined
+	glamaApiKey?: string | undefined
+	openRouterApiKey?: string | undefined
+	openRouterModelId?: string | undefined
+	openRouterBaseUrl?: string | undefined
+	openRouterSpecificProvider?: string | undefined
+	openRouterUseMiddleOutTransform?: boolean | undefined
+	awsAccessKey?: string | undefined
+	awsSecretKey?: string | undefined
+	awsSessionToken?: string | undefined
+	awsRegion?: string | undefined
+	awsUseCrossRegionInference?: boolean | undefined
+	awsUsePromptCache?: boolean | undefined
+	awsProfile?: string | undefined
+	awsUseProfile?: boolean | undefined
+	awsCustomArn?: string | undefined
+	vertexKeyFile?: string | undefined
+	vertexJsonCredentials?: string | undefined
+	vertexProjectId?: string | undefined
+	vertexRegion?: string | undefined
+	openAiBaseUrl?: string | undefined
+	openAiApiKey?: string | undefined
+	openAiLegacyFormat?: boolean | undefined
+	openAiR1FormatEnabled?: boolean | undefined
+	openAiModelId?: string | undefined
+	openAiCustomModelInfo?:
+		| ({
+				maxTokens?: (number | null) | undefined
+				maxThinkingTokens?: (number | null) | undefined
+				contextWindow: number
+				supportsImages?: boolean | undefined
+				supportsComputerUse?: boolean | undefined
+				supportsPromptCache: boolean
+				isPromptCacheOptional?: boolean | undefined
+				inputPrice?: number | undefined
+				outputPrice?: number | undefined
+				cacheWritesPrice?: number | undefined
+				cacheReadsPrice?: number | undefined
+				description?: string | undefined
+				reasoningEffort?: ("low" | "medium" | "high") | undefined
+				thinking?: boolean | undefined
+				minTokensPerCachePoint?: number | undefined
+				maxCachePoints?: number | undefined
+				cachableFields?: string[] | undefined
+				tiers?:
+					| {
+							contextWindow: number
+							inputPrice?: number | undefined
+							outputPrice?: number | undefined
+							cacheWritesPrice?: number | undefined
+							cacheReadsPrice?: number | undefined
+					  }[]
+					| undefined
+		  } | null)
+		| undefined
+	openAiUseAzure?: boolean | undefined
+	azureApiVersion?: string | undefined
+	openAiStreamingEnabled?: boolean | undefined
+	enableReasoningEffort?: boolean | undefined
+	openAiHostHeader?: string | undefined
+	openAiHeaders?:
+		| {
+				[x: string]: string
+		  }
+		| undefined
+	ollamaModelId?: string | undefined
+	ollamaBaseUrl?: string | undefined
+	vsCodeLmModelSelector?:
+		| {
+				vendor?: string | undefined
+				family?: string | undefined
+				version?: string | undefined
+				id?: string | undefined
+		  }
+		| undefined
+	lmStudioModelId?: string | undefined
+	lmStudioBaseUrl?: string | undefined
+	lmStudioDraftModelId?: string | undefined
+	lmStudioSpeculativeDecodingEnabled?: boolean | undefined
+	geminiApiKey?: string | undefined
+	googleGeminiBaseUrl?: string | undefined
+	openAiNativeApiKey?: string | undefined
+	openAiNativeBaseUrl?: string | undefined
+	mistralApiKey?: string | undefined
+	mistralCodestralUrl?: string | undefined
+	deepSeekBaseUrl?: string | undefined
+	deepSeekApiKey?: string | undefined
+	unboundApiKey?: string | undefined
+	unboundModelId?: string | undefined
+	requestyApiKey?: string | undefined
+	requestyModelId?: string | undefined
+	fakeAi?: unknown | undefined
+	xaiApiKey?: string | undefined
+	groqApiKey?: string | undefined
+	chutesApiKey?: string | undefined
+	litellmBaseUrl?: string | undefined
+	litellmApiKey?: string | undefined
+	litellmModelId?: string | undefined
+}
+
+type ProviderSettingsEntry = {
+	id: string
+	name: string
+	apiProvider?:
+		| (
+				| "anthropic"
+				| "glama"
+				| "openrouter"
+				| "bedrock"
+				| "vertex"
+				| "openai"
+				| "ollama"
+				| "vscode-lm"
+				| "lmstudio"
+				| "gemini"
+				| "openai-native"
+				| "mistral"
+				| "deepseek"
+				| "unbound"
+				| "requesty"
+				| "human-relay"
+				| "fake-ai"
+				| "xai"
+				| "groq"
+				| "chutes"
+				| "litellm"
+		  )
+		| undefined
+}
+
 type ClineMessage = {
 	ts: number
 	type: "ask" | "say"
@@ -583,6 +613,10 @@ interface RooCodeAPI extends EventEmitter<RooCodeEvents> {
 	 * Simulates pressing the secondary button in the chat interface.
 	 */
 	pressSecondaryButton(): Promise<void>
+	/**
+	 * Returns true if the API is ready to use.
+	 */
+	isReady(): boolean
 	/**
 	 * Returns the current configuration.
 	 * @returns The current configuration.
@@ -593,28 +627,43 @@ interface RooCodeAPI extends EventEmitter<RooCodeEvents> {
 	 * @param values An object containing key-value pairs to set.
 	 */
 	setConfiguration(values: RooCodeSettings): Promise<void>
-	/**
-	 * Creates a new API configuration profile
-	 * @param name The name of the profile
-	 * @returns The ID of the created profile
-	 */
-	createProfile(name: string): Promise<string>
 	/**
 	 * Returns a list of all configured profile names
 	 * @returns Array of profile names
 	 */
 	getProfiles(): string[]
 	/**
-	 * Changes the active API configuration profile
-	 * @param name The name of the profile to activate
+	 * Returns the profile entry for a given name
+	 * @param name The name of the profile
+	 * @returns The profile entry, or undefined if the profile does not exist
+	 */
+	getProfileEntry(name: string): ProviderSettingsEntry | undefined
+	/**
+	 * Creates a new API configuration profile
+	 * @param name The name of the profile
+	 * @param profile The profile to create; defaults to an empty object
+	 * @param activate Whether to activate the profile after creation; defaults to true
+	 * @returns The ID of the created profile
+	 * @throws Error if the profile already exists
+	 */
+	createProfile(name: string, profile?: ProviderSettings, activate?: boolean): Promise<string>
+	/**
+	 * Updates an existing API configuration profile
+	 * @param name The name of the profile
+	 * @param profile The profile to update
+	 * @param activate Whether to activate the profile after update; defaults to true
+	 * @returns The ID of the updated profile
 	 * @throws Error if the profile does not exist
 	 */
-	setActiveProfile(name: string): Promise<void>
+	updateProfile(name: string, profile: ProviderSettings, activate?: boolean): Promise<string | undefined>
 	/**
-	 * Returns the name of the currently active profile
-	 * @returns The profile name, or undefined if no profile is active
+	 * Creates a new API configuration profile or updates an existing one
+	 * @param name The name of the profile
+	 * @param profile The profile to create or update; defaults to an empty object
+	 * @param activate Whether to activate the profile after upsert; defaults to true
+	 * @returns The ID of the upserted profile
 	 */
-	getActiveProfile(): string | undefined
+	upsertProfile(name: string, profile: ProviderSettings, activate?: boolean): Promise<string | undefined>
 	/**
 	 * Deletes a profile by name
 	 * @param name The name of the profile to delete
@@ -622,15 +671,23 @@ interface RooCodeAPI extends EventEmitter<RooCodeEvents> {
 	 */
 	deleteProfile(name: string): Promise<void>
 	/**
-	 * Returns true if the API is ready to use.
+	 * Returns the name of the currently active profile
+	 * @returns The profile name, or undefined if no profile is active
 	 */
-	isReady(): boolean
+	getActiveProfile(): string | undefined
+	/**
+	 * Changes the active API configuration profile
+	 * @param name The name of the profile to activate
+	 * @throws Error if the profile does not exist
+	 */
+	setActiveProfile(name: string): Promise<string | undefined>
 }
 
 export {
 	type ClineMessage,
 	type GlobalSettings,
 	type ProviderSettings,
+	type ProviderSettingsEntry,
 	type RooCodeAPI,
 	RooCodeEventName,
 	type RooCodeEvents,

+ 173 - 141
src/exports/types.ts

@@ -1,147 +1,6 @@
 // This file is automatically generated by running `npm run generate-types`
 // Do not edit it directly.
 
-type ProviderSettings = {
-	apiProvider?:
-		| (
-				| "anthropic"
-				| "glama"
-				| "openrouter"
-				| "bedrock"
-				| "vertex"
-				| "openai"
-				| "ollama"
-				| "vscode-lm"
-				| "lmstudio"
-				| "gemini"
-				| "openai-native"
-				| "mistral"
-				| "deepseek"
-				| "unbound"
-				| "requesty"
-				| "human-relay"
-				| "fake-ai"
-				| "xai"
-				| "groq"
-				| "chutes"
-				| "litellm"
-		  )
-		| undefined
-	apiModelId?: string | undefined
-	apiKey?: string | undefined
-	anthropicBaseUrl?: string | undefined
-	anthropicUseAuthToken?: boolean | undefined
-	glamaModelId?: string | undefined
-	glamaApiKey?: string | undefined
-	openRouterApiKey?: string | undefined
-	openRouterModelId?: string | undefined
-	openRouterBaseUrl?: string | undefined
-	openRouterSpecificProvider?: string | undefined
-	openRouterUseMiddleOutTransform?: boolean | undefined
-	awsAccessKey?: string | undefined
-	awsSecretKey?: string | undefined
-	awsSessionToken?: string | undefined
-	awsRegion?: string | undefined
-	awsUseCrossRegionInference?: boolean | undefined
-	awsUsePromptCache?: boolean | undefined
-	awsProfile?: string | undefined
-	awsUseProfile?: boolean | undefined
-	awsCustomArn?: string | undefined
-	vertexKeyFile?: string | undefined
-	vertexJsonCredentials?: string | undefined
-	vertexProjectId?: string | undefined
-	vertexRegion?: string | undefined
-	openAiBaseUrl?: string | undefined
-	openAiApiKey?: string | undefined
-	openAiLegacyFormat?: boolean | undefined
-	openAiR1FormatEnabled?: boolean | undefined
-	openAiModelId?: string | undefined
-	openAiCustomModelInfo?:
-		| ({
-				maxTokens?: (number | null) | undefined
-				maxThinkingTokens?: (number | null) | undefined
-				contextWindow: number
-				supportsImages?: boolean | undefined
-				supportsComputerUse?: boolean | undefined
-				supportsPromptCache: boolean
-				isPromptCacheOptional?: boolean | undefined
-				inputPrice?: number | undefined
-				outputPrice?: number | undefined
-				cacheWritesPrice?: number | undefined
-				cacheReadsPrice?: number | undefined
-				description?: string | undefined
-				reasoningEffort?: ("low" | "medium" | "high") | undefined
-				thinking?: boolean | undefined
-				minTokensPerCachePoint?: number | undefined
-				maxCachePoints?: number | undefined
-				cachableFields?: string[] | undefined
-				tiers?:
-					| {
-							contextWindow: number
-							inputPrice?: number | undefined
-							outputPrice?: number | undefined
-							cacheWritesPrice?: number | undefined
-							cacheReadsPrice?: number | undefined
-					  }[]
-					| undefined
-		  } | null)
-		| undefined
-	openAiUseAzure?: boolean | undefined
-	azureApiVersion?: string | undefined
-	openAiStreamingEnabled?: boolean | undefined
-	enableReasoningEffort?: boolean | undefined
-	openAiHostHeader?: string | undefined
-	openAiHeaders?:
-		| {
-				[x: string]: string
-		  }
-		| undefined
-	ollamaModelId?: string | undefined
-	ollamaBaseUrl?: string | undefined
-	vsCodeLmModelSelector?:
-		| {
-				vendor?: string | undefined
-				family?: string | undefined
-				version?: string | undefined
-				id?: string | undefined
-		  }
-		| undefined
-	lmStudioModelId?: string | undefined
-	lmStudioBaseUrl?: string | undefined
-	lmStudioDraftModelId?: string | undefined
-	lmStudioSpeculativeDecodingEnabled?: boolean | undefined
-	geminiApiKey?: string | undefined
-	googleGeminiBaseUrl?: string | undefined
-	openAiNativeApiKey?: string | undefined
-	openAiNativeBaseUrl?: string | undefined
-	mistralApiKey?: string | undefined
-	mistralCodestralUrl?: string | undefined
-	deepSeekBaseUrl?: string | undefined
-	deepSeekApiKey?: string | undefined
-	unboundApiKey?: string | undefined
-	unboundModelId?: string | undefined
-	requestyApiKey?: string | undefined
-	requestyModelId?: string | undefined
-	xaiApiKey?: string | undefined
-	groqApiKey?: string | undefined
-	chutesApiKey?: string | undefined
-	litellmBaseUrl?: string | undefined
-	litellmApiKey?: string | undefined
-	litellmModelId?: string | undefined
-	modelMaxTokens?: number | undefined
-	modelMaxThinkingTokens?: number | undefined
-	includeMaxTokens?: boolean | undefined
-	reasoningEffort?: ("low" | "medium" | "high") | undefined
-	promptCachingDisabled?: boolean | undefined
-	diffEnabled?: boolean | undefined
-	fuzzyMatchThreshold?: number | undefined
-	modelTemperature?: (number | null) | undefined
-	rateLimitSeconds?: number | undefined
-	fakeAi?: unknown | undefined
-}
-
-export type { ProviderSettings }
-
 type GlobalSettings = {
 	currentApiConfigName?: string | undefined
 	listApiConfigMeta?:
@@ -314,6 +173,179 @@ type GlobalSettings = {
 
 export type { GlobalSettings }
 
+type ProviderSettings = {
+	apiProvider?:
+		| (
+				| "anthropic"
+				| "glama"
+				| "openrouter"
+				| "bedrock"
+				| "vertex"
+				| "openai"
+				| "ollama"
+				| "vscode-lm"
+				| "lmstudio"
+				| "gemini"
+				| "openai-native"
+				| "mistral"
+				| "deepseek"
+				| "unbound"
+				| "requesty"
+				| "human-relay"
+				| "fake-ai"
+				| "xai"
+				| "groq"
+				| "chutes"
+				| "litellm"
+		  )
+		| undefined
+	includeMaxTokens?: boolean | undefined
+	reasoningEffort?: ("low" | "medium" | "high") | undefined
+	promptCachingDisabled?: boolean | undefined
+	diffEnabled?: boolean | undefined
+	fuzzyMatchThreshold?: number | undefined
+	modelTemperature?: (number | null) | undefined
+	rateLimitSeconds?: number | undefined
+	modelMaxTokens?: number | undefined
+	modelMaxThinkingTokens?: number | undefined
+	apiModelId?: string | undefined
+	apiKey?: string | undefined
+	anthropicBaseUrl?: string | undefined
+	anthropicUseAuthToken?: boolean | undefined
+	glamaModelId?: string | undefined
+	glamaApiKey?: string | undefined
+	openRouterApiKey?: string | undefined
+	openRouterModelId?: string | undefined
+	openRouterBaseUrl?: string | undefined
+	openRouterSpecificProvider?: string | undefined
+	openRouterUseMiddleOutTransform?: boolean | undefined
+	awsAccessKey?: string | undefined
+	awsSecretKey?: string | undefined
+	awsSessionToken?: string | undefined
+	awsRegion?: string | undefined
+	awsUseCrossRegionInference?: boolean | undefined
+	awsUsePromptCache?: boolean | undefined
+	awsProfile?: string | undefined
+	awsUseProfile?: boolean | undefined
+	awsCustomArn?: string | undefined
+	vertexKeyFile?: string | undefined
+	vertexJsonCredentials?: string | undefined
+	vertexProjectId?: string | undefined
+	vertexRegion?: string | undefined
+	openAiBaseUrl?: string | undefined
+	openAiApiKey?: string | undefined
+	openAiLegacyFormat?: boolean | undefined
+	openAiR1FormatEnabled?: boolean | undefined
+	openAiModelId?: string | undefined
+	openAiCustomModelInfo?:
+		| ({
+				maxTokens?: (number | null) | undefined
+				maxThinkingTokens?: (number | null) | undefined
+				contextWindow: number
+				supportsImages?: boolean | undefined
+				supportsComputerUse?: boolean | undefined
+				supportsPromptCache: boolean
+				isPromptCacheOptional?: boolean | undefined
+				inputPrice?: number | undefined
+				outputPrice?: number | undefined
+				cacheWritesPrice?: number | undefined
+				cacheReadsPrice?: number | undefined
+				description?: string | undefined
+				reasoningEffort?: ("low" | "medium" | "high") | undefined
+				thinking?: boolean | undefined
+				minTokensPerCachePoint?: number | undefined
+				maxCachePoints?: number | undefined
+				cachableFields?: string[] | undefined
+				tiers?:
+					| {
+							contextWindow: number
+							inputPrice?: number | undefined
+							outputPrice?: number | undefined
+							cacheWritesPrice?: number | undefined
+							cacheReadsPrice?: number | undefined
+					  }[]
+					| undefined
+		  } | null)
+		| undefined
+	openAiUseAzure?: boolean | undefined
+	azureApiVersion?: string | undefined
+	openAiStreamingEnabled?: boolean | undefined
+	enableReasoningEffort?: boolean | undefined
+	openAiHostHeader?: string | undefined
+	openAiHeaders?:
+		| {
+				[x: string]: string
+		  }
+		| undefined
+	ollamaModelId?: string | undefined
+	ollamaBaseUrl?: string | undefined
+	vsCodeLmModelSelector?:
+		| {
+				vendor?: string | undefined
+				family?: string | undefined
+				version?: string | undefined
+				id?: string | undefined
+		  }
+		| undefined
+	lmStudioModelId?: string | undefined
+	lmStudioBaseUrl?: string | undefined
+	lmStudioDraftModelId?: string | undefined
+	lmStudioSpeculativeDecodingEnabled?: boolean | undefined
+	geminiApiKey?: string | undefined
+	googleGeminiBaseUrl?: string | undefined
+	openAiNativeApiKey?: string | undefined
+	openAiNativeBaseUrl?: string | undefined
+	mistralApiKey?: string | undefined
+	mistralCodestralUrl?: string | undefined
+	deepSeekBaseUrl?: string | undefined
+	deepSeekApiKey?: string | undefined
+	unboundApiKey?: string | undefined
+	unboundModelId?: string | undefined
+	requestyApiKey?: string | undefined
+	requestyModelId?: string | undefined
+	fakeAi?: unknown | undefined
+	xaiApiKey?: string | undefined
+	groqApiKey?: string | undefined
+	chutesApiKey?: string | undefined
+	litellmBaseUrl?: string | undefined
+	litellmApiKey?: string | undefined
+	litellmModelId?: string | undefined
+}
+
+export type { ProviderSettings }
+
+type ProviderSettingsEntry = {
+	id: string
+	name: string
+	apiProvider?:
+		| (
+				| "anthropic"
+				| "glama"
+				| "openrouter"
+				| "bedrock"
+				| "vertex"
+				| "openai"
+				| "ollama"
+				| "vscode-lm"
+				| "lmstudio"
+				| "gemini"
+				| "openai-native"
+				| "mistral"
+				| "deepseek"
+				| "unbound"
+				| "requesty"
+				| "human-relay"
+				| "fake-ai"
+				| "xai"
+				| "groq"
+				| "chutes"
+				| "litellm"
+		  )
+		| undefined
+}
+
+export type { ProviderSettingsEntry }
+
 type ClineMessage = {
 	ts: number
 	type: "ask" | "say"

+ 149 - 49
src/schemas/index.ts

@@ -135,18 +135,6 @@ export const modelInfoSchema = z.object({
 
 export type ModelInfo = z.infer<typeof modelInfoSchema>
 
-/**
- * ApiConfigMeta
- */
-
-export const apiConfigMetaSchema = z.object({
-	id: z.string(),
-	name: z.string(),
-	apiProvider: providerNamesSchema.optional(),
-})
-
-export type ApiConfigMeta = z.infer<typeof apiConfigMetaSchema>
-
 /**
  * HistoryItem
  */
@@ -342,26 +330,59 @@ export type Experiments = z.infer<typeof experimentsSchema>
 type _AssertExperiments = AssertEqual<Equals<ExperimentId, Keys<Experiments>>>
 
 /**
- * ProviderSettings
+ * ProviderSettingsEntry
  */
 
-export const providerSettingsSchema = z.object({
+export const providerSettingsEntrySchema = z.object({
+	id: z.string(),
+	name: z.string(),
 	apiProvider: providerNamesSchema.optional(),
-	// Anthropic
+})
+
+export type ProviderSettingsEntry = z.infer<typeof providerSettingsEntrySchema>
+
+/**
+ * ProviderSettings
+ */
+
+const baseProviderSettingsSchema = z.object({
+	includeMaxTokens: z.boolean().optional(),
+	reasoningEffort: reasoningEffortsSchema.optional(),
+	promptCachingDisabled: z.boolean().optional(),
+	diffEnabled: z.boolean().optional(),
+	fuzzyMatchThreshold: z.number().optional(),
+	modelTemperature: z.number().nullish(),
+	rateLimitSeconds: z.number().optional(),
+	// Claude 3.7 Sonnet Thinking
+	modelMaxTokens: z.number().optional(),
+	modelMaxThinkingTokens: z.number().optional(),
+})
+
+// Several of the providers share common model config properties.
+const apiModelIdProviderModelSchema = baseProviderSettingsSchema.extend({
 	apiModelId: z.string().optional(),
+})
+
+const anthropicSchema = apiModelIdProviderModelSchema.extend({
 	apiKey: z.string().optional(),
 	anthropicBaseUrl: z.string().optional(),
 	anthropicUseAuthToken: z.boolean().optional(),
-	// Glama
+})
+
+const glamaSchema = baseProviderSettingsSchema.extend({
 	glamaModelId: z.string().optional(),
 	glamaApiKey: z.string().optional(),
-	// OpenRouter
+})
+
+const openRouterSchema = baseProviderSettingsSchema.extend({
 	openRouterApiKey: z.string().optional(),
 	openRouterModelId: z.string().optional(),
 	openRouterBaseUrl: z.string().optional(),
 	openRouterSpecificProvider: z.string().optional(),
 	openRouterUseMiddleOutTransform: z.boolean().optional(),
-	// Amazon Bedrock
+})
+
+const bedrockSchema = apiModelIdProviderModelSchema.extend({
 	awsAccessKey: z.string().optional(),
 	awsSecretKey: z.string().optional(),
 	awsSessionToken: z.string().optional(),
@@ -371,12 +392,16 @@ export const providerSettingsSchema = z.object({
 	awsProfile: z.string().optional(),
 	awsUseProfile: z.boolean().optional(),
 	awsCustomArn: z.string().optional(),
-	// Google Vertex
+})
+
+const vertexSchema = apiModelIdProviderModelSchema.extend({
 	vertexKeyFile: z.string().optional(),
 	vertexJsonCredentials: z.string().optional(),
 	vertexProjectId: z.string().optional(),
 	vertexRegion: z.string().optional(),
-	// OpenAI
+})
+
+const openAiSchema = baseProviderSettingsSchema.extend({
 	openAiBaseUrl: z.string().optional(),
 	openAiApiKey: z.string().optional(),
 	openAiLegacyFormat: z.boolean().optional(),
@@ -387,12 +412,16 @@ export const providerSettingsSchema = z.object({
 	azureApiVersion: z.string().optional(),
 	openAiStreamingEnabled: z.boolean().optional(),
 	enableReasoningEffort: z.boolean().optional(),
-	openAiHostHeader: z.string().optional(), // Keep temporarily for backward compatibility during migration
+	openAiHostHeader: z.string().optional(), // Keep temporarily for backward compatibility during migration.
 	openAiHeaders: z.record(z.string(), z.string()).optional(),
-	// Ollama
+})
+
+const ollamaSchema = baseProviderSettingsSchema.extend({
 	ollamaModelId: z.string().optional(),
 	ollamaBaseUrl: z.string().optional(),
-	// VS Code LM
+})
+
+const vsCodeLmSchema = baseProviderSettingsSchema.extend({
 	vsCodeLmModelSelector: z
 		.object({
 			vendor: z.string().optional(),
@@ -401,54 +430,124 @@ export const providerSettingsSchema = z.object({
 			id: z.string().optional(),
 		})
 		.optional(),
-	// LM Studio
+})
+
+const lmStudioSchema = baseProviderSettingsSchema.extend({
 	lmStudioModelId: z.string().optional(),
 	lmStudioBaseUrl: z.string().optional(),
 	lmStudioDraftModelId: z.string().optional(),
 	lmStudioSpeculativeDecodingEnabled: z.boolean().optional(),
-	// Gemini
+})
+
+const geminiSchema = apiModelIdProviderModelSchema.extend({
 	geminiApiKey: z.string().optional(),
 	googleGeminiBaseUrl: z.string().optional(),
-	// OpenAI Native
+})
+
+const openAiNativeSchema = apiModelIdProviderModelSchema.extend({
 	openAiNativeApiKey: z.string().optional(),
 	openAiNativeBaseUrl: z.string().optional(),
-	// Mistral
+})
+
+const mistralSchema = apiModelIdProviderModelSchema.extend({
 	mistralApiKey: z.string().optional(),
 	mistralCodestralUrl: z.string().optional(),
-	// DeepSeek
+})
+
+const deepSeekSchema = apiModelIdProviderModelSchema.extend({
 	deepSeekBaseUrl: z.string().optional(),
 	deepSeekApiKey: z.string().optional(),
-	// Unbound
+})
+
+const unboundSchema = baseProviderSettingsSchema.extend({
 	unboundApiKey: z.string().optional(),
 	unboundModelId: z.string().optional(),
-	// Requesty
+})
+
+const requestySchema = baseProviderSettingsSchema.extend({
 	requestyApiKey: z.string().optional(),
 	requestyModelId: z.string().optional(),
-	// X.AI (Grok)
+})
+
+const humanRelaySchema = baseProviderSettingsSchema
+
+const fakeAiSchema = baseProviderSettingsSchema.extend({
+	fakeAi: z.unknown().optional(),
+})
+
+const xaiSchema = apiModelIdProviderModelSchema.extend({
 	xaiApiKey: z.string().optional(),
-	// Groq
+})
+
+const groqSchema = apiModelIdProviderModelSchema.extend({
 	groqApiKey: z.string().optional(),
-	// Chutes AI
+})
+
+const chutesSchema = apiModelIdProviderModelSchema.extend({
 	chutesApiKey: z.string().optional(),
-	// LiteLLM
+})
+
+const litellmSchema = baseProviderSettingsSchema.extend({
 	litellmBaseUrl: z.string().optional(),
 	litellmApiKey: z.string().optional(),
 	litellmModelId: z.string().optional(),
-	// Claude 3.7 Sonnet Thinking
-	modelMaxTokens: z.number().optional(),
-	modelMaxThinkingTokens: z.number().optional(),
-	// Generic
-	includeMaxTokens: z.boolean().optional(),
-	reasoningEffort: reasoningEffortsSchema.optional(),
-	promptCachingDisabled: z.boolean().optional(),
-	diffEnabled: z.boolean().optional(),
-	fuzzyMatchThreshold: z.number().optional(),
-	modelTemperature: z.number().nullish(),
-	rateLimitSeconds: z.number().optional(),
-	// Fake AI
-	fakeAi: z.unknown().optional(),
 })
 
+const defaultSchema = z.object({
+	apiProvider: z.undefined(),
+})
+
+export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProvider", [
+	anthropicSchema.merge(z.object({ apiProvider: z.literal("anthropic") })),
+	glamaSchema.merge(z.object({ apiProvider: z.literal("glama") })),
+	openRouterSchema.merge(z.object({ apiProvider: z.literal("openrouter") })),
+	bedrockSchema.merge(z.object({ apiProvider: z.literal("bedrock") })),
+	vertexSchema.merge(z.object({ apiProvider: z.literal("vertex") })),
+	openAiSchema.merge(z.object({ apiProvider: z.literal("openai") })),
+	ollamaSchema.merge(z.object({ apiProvider: z.literal("ollama") })),
+	vsCodeLmSchema.merge(z.object({ apiProvider: z.literal("vscode-lm") })),
+	lmStudioSchema.merge(z.object({ apiProvider: z.literal("lmstudio") })),
+	geminiSchema.merge(z.object({ apiProvider: z.literal("gemini") })),
+	openAiNativeSchema.merge(z.object({ apiProvider: z.literal("openai-native") })),
+	mistralSchema.merge(z.object({ apiProvider: z.literal("mistral") })),
+	deepSeekSchema.merge(z.object({ apiProvider: z.literal("deepseek") })),
+	unboundSchema.merge(z.object({ apiProvider: z.literal("unbound") })),
+	requestySchema.merge(z.object({ apiProvider: z.literal("requesty") })),
+	humanRelaySchema.merge(z.object({ apiProvider: z.literal("human-relay") })),
+	fakeAiSchema.merge(z.object({ apiProvider: z.literal("fake-ai") })),
+	xaiSchema.merge(z.object({ apiProvider: z.literal("xai") })),
+	groqSchema.merge(z.object({ apiProvider: z.literal("groq") })),
+	chutesSchema.merge(z.object({ apiProvider: z.literal("chutes") })),
+	litellmSchema.merge(z.object({ apiProvider: z.literal("litellm") })),
+	defaultSchema,
+])
+
+export const providerSettingsSchema = z
+	.object({
+		apiProvider: providerNamesSchema.optional(),
+	})
+	.merge(anthropicSchema)
+	.merge(glamaSchema)
+	.merge(openRouterSchema)
+	.merge(bedrockSchema)
+	.merge(vertexSchema)
+	.merge(openAiSchema)
+	.merge(ollamaSchema)
+	.merge(vsCodeLmSchema)
+	.merge(lmStudioSchema)
+	.merge(geminiSchema)
+	.merge(openAiNativeSchema)
+	.merge(mistralSchema)
+	.merge(deepSeekSchema)
+	.merge(unboundSchema)
+	.merge(requestySchema)
+	.merge(humanRelaySchema)
+	.merge(fakeAiSchema)
+	.merge(xaiSchema)
+	.merge(groqSchema)
+	.merge(chutesSchema)
+	.merge(litellmSchema)
+
 export type ProviderSettings = z.infer<typeof providerSettingsSchema>
 
 type ProviderSettingsRecord = Record<Keys<ProviderSettings>, undefined>
@@ -557,7 +656,7 @@ export const PROVIDER_SETTINGS_KEYS = Object.keys(providerSettingsRecord) as Key
 
 export const globalSettingsSchema = z.object({
 	currentApiConfigName: z.string().optional(),
-	listApiConfigMeta: z.array(apiConfigMetaSchema).optional(),
+	listApiConfigMeta: z.array(providerSettingsEntrySchema).optional(),
 	pinnedApiConfigs: z.record(z.string(), z.boolean()).optional(),
 
 	lastShownAnnouncementId: z.string().optional(),
@@ -976,8 +1075,9 @@ export type TypeDefinition = {
 }
 
 export const typeDefinitions: TypeDefinition[] = [
-	{ schema: providerSettingsSchema, identifier: "ProviderSettings" },
 	{ schema: globalSettingsSchema, identifier: "GlobalSettings" },
+	{ schema: providerSettingsSchema, identifier: "ProviderSettings" },
+	{ schema: providerSettingsEntrySchema, identifier: "ProviderSettingsEntry" },
 	{ schema: clineMessageSchema, identifier: "ClineMessage" },
 	{ schema: tokenUsageSchema, identifier: "TokenUsage" },
 	{ schema: rooCodeEventsSchema, identifier: "RooCodeEvents" },

+ 5 - 5
src/shared/ExtensionMessage.ts

@@ -2,8 +2,8 @@ import { GitCommit } from "../utils/git"
 
 import {
 	GlobalSettings,
-	ApiConfigMeta,
-	ProviderSettings as ApiConfiguration,
+	ProviderSettingsEntry,
+	ProviderSettings,
 	HistoryItem,
 	ModeConfig,
 	TelemetrySetting,
@@ -17,7 +17,7 @@ import { McpServer } from "./mcp"
 import { Mode } from "./modes"
 import { RouterModels } from "./api"
 
-export type { ApiConfigMeta, ToolProgressStatus }
+export type { ProviderSettingsEntry, ToolProgressStatus }
 
 export interface LanguageModelChatSelector {
 	vendor?: string
@@ -95,7 +95,7 @@ export interface ExtensionMessage {
 	vsCodeLmModels?: { vendor?: string; family?: string; version?: string; id?: string }[]
 	mcpServers?: McpServer[]
 	commits?: GitCommit[]
-	listApiConfig?: ApiConfigMeta[]
+	listApiConfig?: ProviderSettingsEntry[]
 	mode?: Mode
 	customMode?: ModeConfig
 	slug?: string
@@ -172,7 +172,7 @@ export type ExtensionState = Pick<
 	version: string
 	clineMessages: ClineMessage[]
 	currentTaskItem?: HistoryItem
-	apiConfiguration?: ApiConfiguration
+	apiConfiguration?: ProviderSettings
 	uriScheme?: string
 	shouldShowAnnouncement: boolean
 

+ 2 - 3
src/shared/WebviewMessage.ts

@@ -1,6 +1,6 @@
 import { z } from "zod"
 
-import { ApiConfiguration } from "./api"
+import { ProviderSettings } from "./api"
 import { Mode, PromptComponent, ModeConfig } from "./modes"
 
 export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
@@ -11,7 +11,6 @@ export type AudioType = "notification" | "celebration" | "progress_loop"
 
 export interface WebviewMessage {
 	type:
-		| "apiConfiguration"
 		| "deleteMultipleTasksWithIds"
 		| "currentApiConfigName"
 		| "saveApiConfiguration"
@@ -134,7 +133,7 @@ export interface WebviewMessage {
 	text?: string
 	disabled?: boolean
 	askResponse?: ClineAskResponse
-	apiConfiguration?: ApiConfiguration
+	apiConfiguration?: ProviderSettings
 	images?: string[]
 	bool?: boolean
 	value?: number

+ 6 - 6
src/shared/__tests__/checkExistApiConfig.test.ts

@@ -1,5 +1,5 @@
 import { checkExistKey } from "../checkExistApiConfig"
-import { ApiConfiguration } from "../api"
+import { ProviderSettings } from "../api"
 
 describe("checkExistKey", () => {
 	it("should return false for undefined config", () => {
@@ -7,19 +7,19 @@ describe("checkExistKey", () => {
 	})
 
 	it("should return false for empty config", () => {
-		const config: ApiConfiguration = {}
+		const config: ProviderSettings = {}
 		expect(checkExistKey(config)).toBe(false)
 	})
 
 	it("should return true when one key is defined", () => {
-		const config: ApiConfiguration = {
+		const config: ProviderSettings = {
 			apiKey: "test-key",
 		}
 		expect(checkExistKey(config)).toBe(true)
 	})
 
 	it("should return true when multiple keys are defined", () => {
-		const config: ApiConfiguration = {
+		const config: ProviderSettings = {
 			apiKey: "test-key",
 			glamaApiKey: "glama-key",
 			openRouterApiKey: "openrouter-key",
@@ -28,7 +28,7 @@ describe("checkExistKey", () => {
 	})
 
 	it("should return true when only non-key fields are undefined", () => {
-		const config: ApiConfiguration = {
+		const config: ProviderSettings = {
 			apiKey: "test-key",
 			apiProvider: undefined,
 			anthropicBaseUrl: undefined,
@@ -38,7 +38,7 @@ describe("checkExistKey", () => {
 	})
 
 	it("should return false when all key fields are undefined", () => {
-		const config: ApiConfiguration = {
+		const config: ProviderSettings = {
 			apiKey: undefined,
 			glamaApiKey: undefined,
 			openRouterApiKey: undefined,

+ 1 - 3
src/shared/api.ts

@@ -1,11 +1,9 @@
 import { ModelInfo, ProviderName, ProviderSettings } from "../schemas"
 
-export type { ModelInfo, ProviderName }
+export type { ModelInfo, ProviderName, ProviderSettings }
 
 export type ApiHandlerOptions = Omit<ProviderSettings, "apiProvider" | "id">
 
-export type ApiConfiguration = ProviderSettings
-
 // Anthropic
 // https://docs.anthropic.com/en/docs/about-claude/models
 export type AnthropicModelId = keyof typeof anthropicModels

+ 4 - 4
src/utils/__tests__/enhance-prompt.test.ts

@@ -1,5 +1,5 @@
 import { singleCompletionHandler } from "../single-completion-handler"
-import { ApiConfiguration } from "../../shared/api"
+import { ProviderSettings } from "../../shared/api"
 import { buildApiHandler, SingleCompletionHandler } from "../../api"
 import { supportPrompt } from "../../shared/support-prompt"
 
@@ -9,7 +9,7 @@ jest.mock("../../api", () => ({
 }))
 
 describe("enhancePrompt", () => {
-	const mockApiConfig: ApiConfiguration = {
+	const mockApiConfig: ProviderSettings = {
 		apiProvider: "openai",
 		openAiApiKey: "test-key",
 		openAiBaseUrl: "https://api.openai.com/v1",
@@ -69,7 +69,7 @@ describe("enhancePrompt", () => {
 	})
 
 	it("throws error for missing API configuration", async () => {
-		await expect(singleCompletionHandler({} as ApiConfiguration, "Test prompt")).rejects.toThrow(
+		await expect(singleCompletionHandler({} as ProviderSettings, "Test prompt")).rejects.toThrow(
 			"No valid API configuration provided",
 		)
 	})
@@ -94,7 +94,7 @@ describe("enhancePrompt", () => {
 	})
 
 	it("uses appropriate model based on provider", async () => {
-		const openRouterConfig: ApiConfiguration = {
+		const openRouterConfig: ProviderSettings = {
 			apiProvider: "openrouter",
 			openRouterApiKey: "test-key",
 			openRouterModelId: "test-model",

+ 2 - 2
src/utils/single-completion-handler.ts

@@ -1,11 +1,11 @@
-import { ApiConfiguration } from "../shared/api"
+import { ProviderSettings } from "../shared/api"
 import { buildApiHandler, SingleCompletionHandler } from "../api"
 
 /**
  * Enhances a prompt using the configured API without creating a full Cline instance or task history.
  * This is a lightweight alternative that only uses the API's completion functionality.
  */
-export async function singleCompletionHandler(apiConfiguration: ApiConfiguration, promptText: string): Promise<string> {
+export async function singleCompletionHandler(apiConfiguration: ProviderSettings, promptText: string): Promise<string> {
 	if (!promptText) {
 		throw new Error("No prompt text provided")
 	}

+ 2 - 2
webview-ui/src/components/chat/__tests__/TaskHeader.test.tsx

@@ -4,7 +4,7 @@ import React from "react"
 import { render, screen } from "@testing-library/react"
 import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
 
-import { ApiConfiguration } from "@roo/shared/api"
+import { ProviderSettings } from "@roo/shared/api"
 
 import TaskHeader, { TaskHeaderProps } from "../TaskHeader"
 
@@ -27,7 +27,7 @@ jest.mock("@src/context/ExtensionStateContext", () => ({
 			apiProvider: "anthropic",
 			apiKey: "test-api-key", // Add relevant fields
 			apiModelId: "claude-3-opus-20240229", // Add relevant fields
-		} as ApiConfiguration, // Optional: Add type assertion if ApiConfiguration is imported
+		} as ProviderSettings, // Optional: Add type assertion if ProviderSettings is imported
 		currentTaskItem: null,
 	}),
 }))

+ 2 - 2
webview-ui/src/components/settings/ApiConfigManager.tsx

@@ -2,7 +2,7 @@ import { memo, useEffect, useRef, useState } from "react"
 import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
 import { ChevronsUpDown, Check, X } from "lucide-react"
 
-import { ApiConfigMeta } from "@roo/shared/ExtensionMessage"
+import { ProviderSettingsEntry } from "@roo/shared/ExtensionMessage"
 
 import { useAppTranslation } from "@/i18n/TranslationContext"
 import { cn } from "@/lib/utils"
@@ -25,7 +25,7 @@ import {
 
 interface ApiConfigManagerProps {
 	currentApiConfigName?: string
-	listApiConfigMeta?: ApiConfigMeta[]
+	listApiConfigMeta?: ProviderSettingsEntry[]
 	onSelectConfig: (configName: string) => void
 	onDeleteConfig: (configName: string) => void
 	onRenameConfig: (oldName: string, newName: string) => void

+ 5 - 5
webview-ui/src/components/settings/ApiOptions.tsx

@@ -5,7 +5,7 @@ import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
 
 import {
 	type ProviderName,
-	type ApiConfiguration,
+	type ProviderSettings,
 	openRouterDefaultModelId,
 	requestyDefaultModelId,
 	glamaDefaultModelId,
@@ -56,8 +56,8 @@ import { BedrockCustomArn } from "./providers/BedrockCustomArn"
 
 export interface ApiOptionsProps {
 	uriScheme: string | undefined
-	apiConfiguration: ApiConfiguration
-	setApiConfigurationField: <K extends keyof ApiConfiguration>(field: K, value: ApiConfiguration[K]) => void
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: <K extends keyof ProviderSettings>(field: K, value: ProviderSettings[K]) => void
 	fromWelcomeView?: boolean
 	errorMessage: string | undefined
 	setErrorMessage: React.Dispatch<React.SetStateAction<string | undefined>>
@@ -107,9 +107,9 @@ const ApiOptions = ({
 	const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
 
 	const handleInputChange = useCallback(
-		<K extends keyof ApiConfiguration, E>(
+		<K extends keyof ProviderSettings, E>(
 			field: K,
-			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
 		) =>
 			(event: E | Event) => {
 				setApiConfigurationField(field, transform(event as E))

+ 3 - 3
webview-ui/src/components/settings/PromptCachingControl.tsx

@@ -1,12 +1,12 @@
 import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
 
-import { ApiConfiguration } from "@roo/shared/api"
+import { ProviderSettings } from "@roo/shared/api"
 
 import { useAppTranslation } from "@src/i18n/TranslationContext"
 
 interface PromptCachingControlProps {
-	apiConfiguration: ApiConfiguration
-	setApiConfigurationField: <K extends keyof ApiConfiguration>(field: K, value: ApiConfiguration[K]) => void
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: <K extends keyof ProviderSettings>(field: K, value: ProviderSettings[K]) => void
 }
 
 export const PromptCachingControl = ({ apiConfiguration, setApiConfigurationField }: PromptCachingControlProps) => {

+ 3 - 3
webview-ui/src/components/settings/ReasoningEffort.tsx

@@ -2,12 +2,12 @@ import { useAppTranslation } from "@/i18n/TranslationContext"
 
 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui"
 
-import { ApiConfiguration } from "@roo/shared/api"
+import { ProviderSettings } from "@roo/shared/api"
 import { reasoningEfforts, ReasoningEffort as ReasoningEffortType } from "@roo/schemas"
 
 interface ReasoningEffortProps {
-	apiConfiguration: ApiConfiguration
-	setApiConfigurationField: <K extends keyof ApiConfiguration>(field: K, value: ApiConfiguration[K]) => void
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: <K extends keyof ProviderSettings>(field: K, value: ProviderSettings[K]) => void
 }
 
 export const ReasoningEffort = ({ apiConfiguration, setApiConfigurationField }: ReasoningEffortProps) => {

+ 2 - 2
webview-ui/src/components/settings/SettingsView.tsx

@@ -27,7 +27,7 @@ import {
 
 import { ExperimentId } from "@roo/shared/experiments"
 import { TelemetrySetting } from "@roo/shared/TelemetrySetting"
-import { ApiConfiguration } from "@roo/shared/api"
+import { ProviderSettings } from "@roo/shared/api"
 
 import { vscode } from "@/utils/vscode"
 import { ExtensionStateContextType, useExtensionState } from "@/context/ExtensionStateContext"
@@ -195,7 +195,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
 	}, [])
 
 	const setApiConfigurationField = useCallback(
-		<K extends keyof ApiConfiguration>(field: K, value: ApiConfiguration[K]) => {
+		<K extends keyof ProviderSettings>(field: K, value: ProviderSettings[K]) => {
 			setCachedState((prevState) => {
 				if (prevState.apiConfiguration?.[field] === value) {
 					return prevState

+ 3 - 3
webview-ui/src/components/settings/ThinkingBudget.tsx

@@ -3,14 +3,14 @@ import { useAppTranslation } from "@/i18n/TranslationContext"
 
 import { Slider } from "@/components/ui"
 
-import { ApiConfiguration, ModelInfo } from "@roo/shared/api"
+import { ProviderSettings, ModelInfo } from "@roo/shared/api"
 
 const DEFAULT_MAX_OUTPUT_TOKENS = 16_384
 const DEFAULT_MAX_THINKING_TOKENS = 8_192
 
 interface ThinkingBudgetProps {
-	apiConfiguration: ApiConfiguration
-	setApiConfigurationField: <K extends keyof ApiConfiguration>(field: K, value: ApiConfiguration[K]) => void
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: <K extends keyof ProviderSettings>(field: K, value: ProviderSettings[K]) => void
 	modelInfo?: ModelInfo
 }
 

+ 2 - 2
webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx

@@ -3,7 +3,7 @@
 import { render, screen, fireEvent } from "@testing-library/react"
 import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
 
-import { ApiConfiguration } from "@roo/shared/api"
+import { ProviderSettings } from "@roo/shared/api"
 
 import { ExtensionStateContextProvider } from "@/context/ExtensionStateContext"
 import { openAiModelInfoSaneDefaults } from "@roo/shared/api"
@@ -148,7 +148,7 @@ jest.mock("../DiffSettingsControl", () => ({
 }))
 
 jest.mock("@src/components/ui/hooks/useSelectedModel", () => ({
-	useSelectedModel: jest.fn((apiConfiguration: ApiConfiguration) => {
+	useSelectedModel: jest.fn((apiConfiguration: ProviderSettings) => {
 		if (apiConfiguration.apiModelId?.includes("thinking")) {
 			return {
 				provider: apiConfiguration.apiProvider,

+ 5 - 5
webview-ui/src/components/settings/providers/Anthropic.tsx

@@ -2,7 +2,7 @@ import { useCallback, useState } from "react"
 import { Checkbox } from "vscrui"
 import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
 
-import { ApiConfiguration } from "@roo/shared/api"
+import { ProviderSettings } from "@roo/shared/api"
 
 import { useAppTranslation } from "@src/i18n/TranslationContext"
 import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
@@ -10,8 +10,8 @@ import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
 import { inputEventTransform, noTransform } from "../transforms"
 
 type AnthropicProps = {
-	apiConfiguration: ApiConfiguration
-	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
 }
 
 export const Anthropic = ({ apiConfiguration, setApiConfigurationField }: AnthropicProps) => {
@@ -20,9 +20,9 @@ export const Anthropic = ({ apiConfiguration, setApiConfigurationField }: Anthro
 	const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
 
 	const handleInputChange = useCallback(
-		<K extends keyof ApiConfiguration, E>(
+		<K extends keyof ProviderSettings, E>(
 			field: K,
-			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
 		) =>
 			(event: E | Event) => {
 				setApiConfigurationField(field, transform(event as E))

+ 5 - 5
webview-ui/src/components/settings/providers/Bedrock.tsx

@@ -2,7 +2,7 @@ import { useCallback } from "react"
 import { Checkbox } from "vscrui"
 import { VSCodeTextField, VSCodeRadio, VSCodeRadioGroup } from "@vscode/webview-ui-toolkit/react"
 
-import { ApiConfiguration, ModelInfo } from "@roo/shared/api"
+import { ProviderSettings, ModelInfo } from "@roo/shared/api"
 
 import { useAppTranslation } from "@src/i18n/TranslationContext"
 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui"
@@ -11,8 +11,8 @@ import { AWS_REGIONS } from "../constants"
 import { inputEventTransform, noTransform } from "../transforms"
 
 type BedrockProps = {
-	apiConfiguration: ApiConfiguration
-	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
 	selectedModelInfo?: ModelInfo
 }
 
@@ -20,9 +20,9 @@ export const Bedrock = ({ apiConfiguration, setApiConfigurationField, selectedMo
 	const { t } = useAppTranslation()
 
 	const handleInputChange = useCallback(
-		<K extends keyof ApiConfiguration, E>(
+		<K extends keyof ProviderSettings, E>(
 			field: K,
-			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
 		) =>
 			(event: E | Event) => {
 				setApiConfigurationField(field, transform(event as E))

+ 3 - 3
webview-ui/src/components/settings/providers/BedrockCustomArn.tsx

@@ -1,14 +1,14 @@
 import { useMemo } from "react"
 import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
 
-import { ApiConfiguration } from "@roo/shared/api"
+import { ProviderSettings } from "@roo/shared/api"
 
 import { validateBedrockArn } from "@src/utils/validate"
 import { useAppTranslation } from "@src/i18n/TranslationContext"
 
 type BedrockCustomArnProps = {
-	apiConfiguration: ApiConfiguration
-	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
 }
 
 export const BedrockCustomArn = ({ apiConfiguration, setApiConfigurationField }: BedrockCustomArnProps) => {

+ 5 - 5
webview-ui/src/components/settings/providers/Chutes.tsx

@@ -1,7 +1,7 @@
 import { useCallback } from "react"
 import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
 
-import { ApiConfiguration } from "@roo/shared/api"
+import { ProviderSettings } from "@roo/shared/api"
 
 import { useAppTranslation } from "@src/i18n/TranslationContext"
 import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
@@ -9,17 +9,17 @@ import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
 import { inputEventTransform } from "../transforms"
 
 type ChutesProps = {
-	apiConfiguration: ApiConfiguration
-	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
 }
 
 export const Chutes = ({ apiConfiguration, setApiConfigurationField }: ChutesProps) => {
 	const { t } = useAppTranslation()
 
 	const handleInputChange = useCallback(
-		<K extends keyof ApiConfiguration, E>(
+		<K extends keyof ProviderSettings, E>(
 			field: K,
-			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
 		) =>
 			(event: E | Event) => {
 				setApiConfigurationField(field, transform(event as E))

+ 5 - 5
webview-ui/src/components/settings/providers/DeepSeek.tsx

@@ -1,7 +1,7 @@
 import { useCallback } from "react"
 import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
 
-import { ApiConfiguration } from "@roo/shared/api"
+import { ProviderSettings } from "@roo/shared/api"
 
 import { useAppTranslation } from "@src/i18n/TranslationContext"
 import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
@@ -9,17 +9,17 @@ import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
 import { inputEventTransform } from "../transforms"
 
 type DeepSeekProps = {
-	apiConfiguration: ApiConfiguration
-	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
 }
 
 export const DeepSeek = ({ apiConfiguration, setApiConfigurationField }: DeepSeekProps) => {
 	const { t } = useAppTranslation()
 
 	const handleInputChange = useCallback(
-		<K extends keyof ApiConfiguration, E>(
+		<K extends keyof ProviderSettings, E>(
 			field: K,
-			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
 		) =>
 			(event: E | Event) => {
 				setApiConfigurationField(field, transform(event as E))

+ 5 - 5
webview-ui/src/components/settings/providers/Gemini.tsx

@@ -2,7 +2,7 @@ import { useCallback, useState } from "react"
 import { Checkbox } from "vscrui"
 import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
 
-import { ApiConfiguration } from "@roo/shared/api"
+import { ProviderSettings } from "@roo/shared/api"
 
 import { useAppTranslation } from "@src/i18n/TranslationContext"
 import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
@@ -10,8 +10,8 @@ import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
 import { inputEventTransform } from "../transforms"
 
 type GeminiProps = {
-	apiConfiguration: ApiConfiguration
-	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
 }
 
 export const Gemini = ({ apiConfiguration, setApiConfigurationField }: GeminiProps) => {
@@ -22,9 +22,9 @@ export const Gemini = ({ apiConfiguration, setApiConfigurationField }: GeminiPro
 	)
 
 	const handleInputChange = useCallback(
-		<K extends keyof ApiConfiguration, E>(
+		<K extends keyof ProviderSettings, E>(
 			field: K,
-			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
 		) =>
 			(event: E | Event) => {
 				setApiConfigurationField(field, transform(event as E))

+ 5 - 5
webview-ui/src/components/settings/providers/Glama.tsx

@@ -1,7 +1,7 @@
 import { useCallback } from "react"
 import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
 
-import { ApiConfiguration, RouterModels, glamaDefaultModelId } from "@roo/shared/api"
+import { ProviderSettings, RouterModels, glamaDefaultModelId } from "@roo/shared/api"
 
 import { useAppTranslation } from "@src/i18n/TranslationContext"
 import { getGlamaAuthUrl } from "@src/oauth/urls"
@@ -11,8 +11,8 @@ import { inputEventTransform } from "../transforms"
 import { ModelPicker } from "../ModelPicker"
 
 type GlamaProps = {
-	apiConfiguration: ApiConfiguration
-	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
 	routerModels?: RouterModels
 	uriScheme?: string
 }
@@ -21,9 +21,9 @@ export const Glama = ({ apiConfiguration, setApiConfigurationField, routerModels
 	const { t } = useAppTranslation()
 
 	const handleInputChange = useCallback(
-		<K extends keyof ApiConfiguration, E>(
+		<K extends keyof ProviderSettings, E>(
 			field: K,
-			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
 		) =>
 			(event: E | Event) => {
 				setApiConfigurationField(field, transform(event as E))

+ 5 - 5
webview-ui/src/components/settings/providers/Groq.tsx

@@ -1,7 +1,7 @@
 import { useCallback } from "react"
 import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
 
-import { ApiConfiguration } from "@roo/shared/api"
+import { ProviderSettings } from "@roo/shared/api"
 
 import { useAppTranslation } from "@src/i18n/TranslationContext"
 import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
@@ -9,17 +9,17 @@ import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
 import { inputEventTransform } from "../transforms"
 
 type GroqProps = {
-	apiConfiguration: ApiConfiguration
-	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
 }
 
 export const Groq = ({ apiConfiguration, setApiConfigurationField }: GroqProps) => {
 	const { t } = useAppTranslation()
 
 	const handleInputChange = useCallback(
-		<K extends keyof ApiConfiguration, E>(
+		<K extends keyof ProviderSettings, E>(
 			field: K,
-			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
 		) =>
 			(event: E | Event) => {
 				setApiConfigurationField(field, transform(event as E))

+ 5 - 5
webview-ui/src/components/settings/providers/LMStudio.tsx

@@ -4,7 +4,7 @@ import { Trans } from "react-i18next"
 import { Checkbox } from "vscrui"
 import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
 
-import { ApiConfiguration } from "@roo/shared/api"
+import { ProviderSettings } from "@roo/shared/api"
 
 import { useAppTranslation } from "@src/i18n/TranslationContext"
 import { ExtensionMessage } from "@roo/shared/ExtensionMessage"
@@ -12,8 +12,8 @@ import { ExtensionMessage } from "@roo/shared/ExtensionMessage"
 import { inputEventTransform } from "../transforms"
 
 type LMStudioProps = {
-	apiConfiguration: ApiConfiguration
-	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
 }
 
 export const LMStudio = ({ apiConfiguration, setApiConfigurationField }: LMStudioProps) => {
@@ -22,9 +22,9 @@ export const LMStudio = ({ apiConfiguration, setApiConfigurationField }: LMStudi
 	const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
 
 	const handleInputChange = useCallback(
-		<K extends keyof ApiConfiguration, E>(
+		<K extends keyof ProviderSettings, E>(
 			field: K,
-			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
 		) =>
 			(event: E | Event) => {
 				setApiConfigurationField(field, transform(event as E))

+ 6 - 5
webview-ui/src/components/settings/providers/LiteLLM.tsx

@@ -1,7 +1,7 @@
 import { useCallback } from "react"
 import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
 
-import { ApiConfiguration, RouterModels, litellmDefaultModelId } from "@roo/shared/api"
+import { ProviderSettings, RouterModels, litellmDefaultModelId } from "@roo/shared/api"
 
 import { useAppTranslation } from "@src/i18n/TranslationContext"
 
@@ -9,8 +9,8 @@ import { inputEventTransform } from "../transforms"
 import { ModelPicker } from "../ModelPicker"
 
 type LiteLLMProps = {
-	apiConfiguration: ApiConfiguration
-	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
 	routerModels?: RouterModels
 }
 
@@ -18,9 +18,9 @@ export const LiteLLM = ({ apiConfiguration, setApiConfigurationField, routerMode
 	const { t } = useAppTranslation()
 
 	const handleInputChange = useCallback(
-		<K extends keyof ApiConfiguration, E>(
+		<K extends keyof ProviderSettings, E>(
 			field: K,
-			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
 		) =>
 			(event: E | Event) => {
 				setApiConfigurationField(field, transform(event as E))
@@ -46,6 +46,7 @@ export const LiteLLM = ({ apiConfiguration, setApiConfigurationField, routerMode
 				className="w-full">
 				<label className="block font-medium mb-1">{t("settings:providers.litellmApiKey")}</label>
 			</VSCodeTextField>
+
 			<div className="text-sm text-vscode-descriptionForeground -mt-2">
 				{t("settings:providers.apiKeyStorageNotice")}
 			</div>

+ 5 - 5
webview-ui/src/components/settings/providers/Mistral.tsx

@@ -1,7 +1,7 @@
 import { useCallback } from "react"
 import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
 
-import { ApiConfiguration, RouterModels, mistralDefaultModelId } from "@roo/shared/api"
+import { ProviderSettings, RouterModels, mistralDefaultModelId } from "@roo/shared/api"
 
 import { useAppTranslation } from "@src/i18n/TranslationContext"
 import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
@@ -9,8 +9,8 @@ import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
 import { inputEventTransform } from "../transforms"
 
 type MistralProps = {
-	apiConfiguration: ApiConfiguration
-	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
 	routerModels?: RouterModels
 }
 
@@ -18,9 +18,9 @@ export const Mistral = ({ apiConfiguration, setApiConfigurationField }: MistralP
 	const { t } = useAppTranslation()
 
 	const handleInputChange = useCallback(
-		<K extends keyof ApiConfiguration, E>(
+		<K extends keyof ProviderSettings, E>(
 			field: K,
-			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
 		) =>
 			(event: E | Event) => {
 				setApiConfigurationField(field, transform(event as E))

+ 5 - 5
webview-ui/src/components/settings/providers/Ollama.tsx

@@ -2,7 +2,7 @@ import { useState, useCallback } from "react"
 import { useEvent } from "react-use"
 import { VSCodeTextField, VSCodeRadioGroup, VSCodeRadio } from "@vscode/webview-ui-toolkit/react"
 
-import { ApiConfiguration } from "@roo/shared/api"
+import { ProviderSettings } from "@roo/shared/api"
 import { ExtensionMessage } from "@roo/shared/ExtensionMessage"
 
 import { useAppTranslation } from "@src/i18n/TranslationContext"
@@ -10,8 +10,8 @@ import { useAppTranslation } from "@src/i18n/TranslationContext"
 import { inputEventTransform } from "../transforms"
 
 type OllamaProps = {
-	apiConfiguration: ApiConfiguration
-	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
 }
 
 export const Ollama = ({ apiConfiguration, setApiConfigurationField }: OllamaProps) => {
@@ -20,9 +20,9 @@ export const Ollama = ({ apiConfiguration, setApiConfigurationField }: OllamaPro
 	const [ollamaModels, setOllamaModels] = useState<string[]>([])
 
 	const handleInputChange = useCallback(
-		<K extends keyof ApiConfiguration, E>(
+		<K extends keyof ProviderSettings, E>(
 			field: K,
-			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
 		) =>
 			(event: E | Event) => {
 				setApiConfigurationField(field, transform(event as E))

+ 5 - 5
webview-ui/src/components/settings/providers/OpenAI.tsx

@@ -2,7 +2,7 @@ import { useCallback, useState } from "react"
 import { Checkbox } from "vscrui"
 import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
 
-import { ApiConfiguration } from "@roo/shared/api"
+import { ProviderSettings } from "@roo/shared/api"
 
 import { useAppTranslation } from "@src/i18n/TranslationContext"
 import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
@@ -10,8 +10,8 @@ import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
 import { inputEventTransform } from "../transforms"
 
 type OpenAIProps = {
-	apiConfiguration: ApiConfiguration
-	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
 }
 
 export const OpenAI = ({ apiConfiguration, setApiConfigurationField }: OpenAIProps) => {
@@ -22,9 +22,9 @@ export const OpenAI = ({ apiConfiguration, setApiConfigurationField }: OpenAIPro
 	)
 
 	const handleInputChange = useCallback(
-		<K extends keyof ApiConfiguration, E>(
+		<K extends keyof ProviderSettings, E>(
 			field: K,
-			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
 		) =>
 			(event: E | Event) => {
 				setApiConfigurationField(field, transform(event as E))

+ 5 - 5
webview-ui/src/components/settings/providers/OpenAICompatible.tsx

@@ -5,7 +5,7 @@ import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
 import { convertHeadersToObject } from "../utils/headers"
 
 import { ModelInfo, ReasoningEffort as ReasoningEffortType } from "@roo/schemas"
-import { ApiConfiguration, azureOpenAiDefaultApiVersion, openAiModelInfoSaneDefaults } from "@roo/shared/api"
+import { ProviderSettings, azureOpenAiDefaultApiVersion, openAiModelInfoSaneDefaults } from "@roo/shared/api"
 import { ExtensionMessage } from "@roo/shared/ExtensionMessage"
 
 import { useAppTranslation } from "@src/i18n/TranslationContext"
@@ -17,8 +17,8 @@ import { R1FormatSetting } from "../R1FormatSetting"
 import { ReasoningEffort } from "../ReasoningEffort"
 
 type OpenAICompatibleProps = {
-	apiConfiguration: ApiConfiguration
-	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
 }
 
 export const OpenAICompatible = ({ apiConfiguration, setApiConfigurationField }: OpenAICompatibleProps) => {
@@ -81,9 +81,9 @@ export const OpenAICompatible = ({ apiConfiguration, setApiConfigurationField }:
 	}, [customHeaders, setApiConfigurationField])
 
 	const handleInputChange = useCallback(
-		<K extends keyof ApiConfiguration, E>(
+		<K extends keyof ProviderSettings, E>(
 			field: K,
-			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
 		) =>
 			(event: E | Event) => {
 				setApiConfigurationField(field, transform(event as E))

+ 5 - 5
webview-ui/src/components/settings/providers/OpenRouter.tsx

@@ -4,7 +4,7 @@ import { Checkbox } from "vscrui"
 import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
 import { ExternalLinkIcon } from "@radix-ui/react-icons"
 
-import { ApiConfiguration, RouterModels, openRouterDefaultModelId } from "@roo/shared/api"
+import { ProviderSettings, RouterModels, openRouterDefaultModelId } from "@roo/shared/api"
 
 import { useAppTranslation } from "@src/i18n/TranslationContext"
 import { getOpenRouterAuthUrl } from "@src/oauth/urls"
@@ -21,8 +21,8 @@ import { ModelPicker } from "../ModelPicker"
 import { OpenRouterBalanceDisplay } from "./OpenRouterBalanceDisplay"
 
 type OpenRouterProps = {
-	apiConfiguration: ApiConfiguration
-	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
 	routerModels?: RouterModels
 	selectedModelId: string
 	uriScheme: string | undefined
@@ -42,9 +42,9 @@ export const OpenRouter = ({
 	const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl)
 
 	const handleInputChange = useCallback(
-		<K extends keyof ApiConfiguration, E>(
+		<K extends keyof ProviderSettings, E>(
 			field: K,
-			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
 		) =>
 			(event: E | Event) => {
 				setApiConfigurationField(field, transform(event as E))

+ 5 - 5
webview-ui/src/components/settings/providers/Requesty.tsx

@@ -1,7 +1,7 @@
 import { useCallback, useState } from "react"
 import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
 
-import { ApiConfiguration, RouterModels, requestyDefaultModelId } from "@roo/shared/api"
+import { ProviderSettings, RouterModels, requestyDefaultModelId } from "@roo/shared/api"
 
 import { vscode } from "@src/utils/vscode"
 import { useAppTranslation } from "@src/i18n/TranslationContext"
@@ -13,8 +13,8 @@ import { ModelPicker } from "../ModelPicker"
 import { RequestyBalanceDisplay } from "./RequestyBalanceDisplay"
 
 type RequestyProps = {
-	apiConfiguration: ApiConfiguration
-	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
 	routerModels?: RouterModels
 	refetchRouterModels: () => void
 }
@@ -30,9 +30,9 @@ export const Requesty = ({
 	const [didRefetch, setDidRefetch] = useState<boolean>()
 
 	const handleInputChange = useCallback(
-		<K extends keyof ApiConfiguration, E>(
+		<K extends keyof ProviderSettings, E>(
 			field: K,
-			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
 		) =>
 			(event: E | Event) => {
 				setApiConfigurationField(field, transform(event as E))

+ 5 - 5
webview-ui/src/components/settings/providers/Unbound.tsx

@@ -1,7 +1,7 @@
 import { useCallback } from "react"
 import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
 
-import { ApiConfiguration, RouterModels, unboundDefaultModelId } from "@roo/shared/api"
+import { ProviderSettings, RouterModels, unboundDefaultModelId } from "@roo/shared/api"
 
 import { useAppTranslation } from "@src/i18n/TranslationContext"
 import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
@@ -10,8 +10,8 @@ import { inputEventTransform } from "../transforms"
 import { ModelPicker } from "../ModelPicker"
 
 type UnboundProps = {
-	apiConfiguration: ApiConfiguration
-	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
 	routerModels?: RouterModels
 }
 
@@ -19,9 +19,9 @@ export const Unbound = ({ apiConfiguration, setApiConfigurationField, routerMode
 	const { t } = useAppTranslation()
 
 	const handleInputChange = useCallback(
-		<K extends keyof ApiConfiguration, E>(
+		<K extends keyof ProviderSettings, E>(
 			field: K,
-			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
 		) =>
 			(event: E | Event) => {
 				setApiConfigurationField(field, transform(event as E))

+ 5 - 5
webview-ui/src/components/settings/providers/VSCodeLM.tsx

@@ -2,7 +2,7 @@ import { useState, useCallback } from "react"
 import { useEvent } from "react-use"
 import { LanguageModelChatSelector } from "vscode"
 
-import { ApiConfiguration } from "@roo/shared/api"
+import { ProviderSettings } from "@roo/shared/api"
 import { ExtensionMessage } from "@roo/shared/ExtensionMessage"
 
 import { useAppTranslation } from "@src/i18n/TranslationContext"
@@ -11,8 +11,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
 import { inputEventTransform } from "../transforms"
 
 type VSCodeLMProps = {
-	apiConfiguration: ApiConfiguration
-	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
 }
 
 export const VSCodeLM = ({ apiConfiguration, setApiConfigurationField }: VSCodeLMProps) => {
@@ -21,9 +21,9 @@ export const VSCodeLM = ({ apiConfiguration, setApiConfigurationField }: VSCodeL
 	const [vsCodeLmModels, setVsCodeLmModels] = useState<LanguageModelChatSelector[]>([])
 
 	const handleInputChange = useCallback(
-		<K extends keyof ApiConfiguration, E>(
+		<K extends keyof ProviderSettings, E>(
 			field: K,
-			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
 		) =>
 			(event: E | Event) => {
 				setApiConfigurationField(field, transform(event as E))

+ 5 - 5
webview-ui/src/components/settings/providers/Vertex.tsx

@@ -1,7 +1,7 @@
 import { useCallback } from "react"
 import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
 
-import { ApiConfiguration } from "@roo/shared/api"
+import { ProviderSettings } from "@roo/shared/api"
 
 import { useAppTranslation } from "@src/i18n/TranslationContext"
 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui"
@@ -10,17 +10,17 @@ import { inputEventTransform } from "../transforms"
 import { VERTEX_REGIONS } from "../constants"
 
 type VertexProps = {
-	apiConfiguration: ApiConfiguration
-	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
 }
 
 export const Vertex = ({ apiConfiguration, setApiConfigurationField }: VertexProps) => {
 	const { t } = useAppTranslation()
 
 	const handleInputChange = useCallback(
-		<K extends keyof ApiConfiguration, E>(
+		<K extends keyof ProviderSettings, E>(
 			field: K,
-			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
 		) =>
 			(event: E | Event) => {
 				setApiConfigurationField(field, transform(event as E))

+ 5 - 5
webview-ui/src/components/settings/providers/XAI.tsx

@@ -1,7 +1,7 @@
 import { useCallback } from "react"
 import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
 
-import { ApiConfiguration } from "@roo/shared/api"
+import { ProviderSettings } from "@roo/shared/api"
 
 import { useAppTranslation } from "@src/i18n/TranslationContext"
 import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
@@ -9,17 +9,17 @@ import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
 import { inputEventTransform } from "../transforms"
 
 type XAIProps = {
-	apiConfiguration: ApiConfiguration
-	setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
 }
 
 export const XAI = ({ apiConfiguration, setApiConfigurationField }: XAIProps) => {
 	const { t } = useAppTranslation()
 
 	const handleInputChange = useCallback(
-		<K extends keyof ApiConfiguration, E>(
+		<K extends keyof ProviderSettings, E>(
 			field: K,
-			transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
+			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
 		) =>
 			(event: E | Event) => {
 				setApiConfigurationField(field, transform(event as E))

+ 3 - 3
webview-ui/src/components/ui/hooks/useSelectedModel.ts

@@ -1,6 +1,6 @@
 import {
 	type ProviderName,
-	type ApiConfiguration,
+	type ProviderSettings,
 	type RouterModels,
 	type ModelInfo,
 	anthropicDefaultModelId,
@@ -35,7 +35,7 @@ import {
 
 import { useRouterModels } from "./useRouterModels"
 
-export const useSelectedModel = (apiConfiguration?: ApiConfiguration) => {
+export const useSelectedModel = (apiConfiguration?: ProviderSettings) => {
 	const { data: routerModels, isLoading, isError } = useRouterModels()
 	const provider = apiConfiguration?.apiProvider || "anthropic"
 
@@ -53,7 +53,7 @@ function getSelectedModel({
 	routerModels,
 }: {
 	provider: ProviderName
-	apiConfiguration: ApiConfiguration
+	apiConfiguration: ProviderSettings
 	routerModels: RouterModels
 }): { id: string; info: ModelInfo } {
 	switch (provider) {

+ 9 - 7
webview-ui/src/context/ExtensionStateContext.tsx

@@ -1,9 +1,8 @@
 import React, { createContext, useCallback, useContext, useEffect, useState } from "react"
 import { useEvent } from "react-use"
-import { ApiConfigMeta, ExtensionMessage, ExtensionState } from "@roo/shared/ExtensionMessage"
-import { ApiConfiguration } from "@roo/shared/api"
-import { vscode } from "@src/utils/vscode"
-import { convertTextMateToHljs } from "@src/utils/textMateToHljs"
+
+import { ProviderSettingsEntry, ExtensionMessage, ExtensionState } from "@roo/shared/ExtensionMessage"
+import { ProviderSettings } from "@roo/shared/api"
 import { findLastIndex } from "@roo/shared/array"
 import { McpServer } from "@roo/shared/mcp"
 import { checkExistKey } from "@roo/shared/checkExistApiConfig"
@@ -12,6 +11,9 @@ import { CustomSupportPrompts } from "@roo/shared/support-prompt"
 import { experimentDefault, ExperimentId } from "@roo/shared/experiments"
 import { TelemetrySetting } from "@roo/shared/TelemetrySetting"
 
+import { vscode } from "@src/utils/vscode"
+import { convertTextMateToHljs } from "@src/utils/textMateToHljs"
+
 export interface ExtensionStateContextType extends ExtensionState {
 	historyPreviewCollapsed?: boolean // Add the new state property
 	didHydrateState: boolean
@@ -22,7 +24,7 @@ export interface ExtensionStateContextType extends ExtensionState {
 	currentCheckpoint?: string
 	filePaths: string[]
 	openedTabs: Array<{ label: string; isActive: boolean; path?: string }>
-	setApiConfiguration: (config: ApiConfiguration) => void
+	setApiConfiguration: (config: ProviderSettings) => void
 	setCustomInstructions: (value?: string) => void
 	setAlwaysAllowReadOnly: (value: boolean) => void
 	setAlwaysAllowReadOnlyOutsideWorkspace: (value: boolean) => void
@@ -65,7 +67,7 @@ export interface ExtensionStateContextType extends ExtensionState {
 	requestDelaySeconds: number
 	setRequestDelaySeconds: (value: number) => void
 	setCurrentApiConfigName: (value: string) => void
-	setListApiConfigMeta: (value: ApiConfigMeta[]) => void
+	setListApiConfigMeta: (value: ProviderSettingsEntry[]) => void
 	mode: Mode
 	setMode: (value: Mode) => void
 	setCustomModePrompts: (value: CustomModePrompts) => void
@@ -182,7 +184,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 	const [currentCheckpoint, setCurrentCheckpoint] = useState<string>()
 
 	const setListApiConfigMeta = useCallback(
-		(value: ApiConfigMeta[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })),
+		(value: ProviderSettingsEntry[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })),
 		[],
 	)
 

+ 5 - 5
webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx

@@ -1,16 +1,16 @@
-// cd webview-ui && npx jest src/context/__tests__/ExtensionStateContext.test.tsx
+// npx jest src/context/__tests__/ExtensionStateContext.test.tsx
 
 import { render, screen, act } from "@testing-library/react"
 
 import { ExtensionState } from "@roo/shared/ExtensionMessage"
 import { ExtensionStateContextProvider, useExtensionState, mergeExtensionState } from "../ExtensionStateContext"
 import { ExperimentId } from "@roo/shared/experiments"
-import { ApiConfiguration } from "@roo/shared/api"
+import { ProviderSettings } from "@roo/shared/api"
 
-// Test component that consumes the context
 const TestComponent = () => {
 	const { allowedCommands, setAllowedCommands, soundEnabled, showRooIgnoredFiles, setShowRooIgnoredFiles } =
 		useExtensionState()
+
 	return (
 		<div>
 			<div data-testid="allowed-commands">{JSON.stringify(allowedCommands)}</div>
@@ -26,9 +26,9 @@ const TestComponent = () => {
 	)
 }
 
-// Test component for API configuration
 const ApiConfigTestComponent = () => {
 	const { apiConfiguration, setApiConfiguration } = useExtensionState()
+
 	return (
 		<div>
 			<div data-testid="api-configuration">{JSON.stringify(apiConfiguration)}</div>
@@ -197,7 +197,7 @@ describe("mergeExtensionState", () => {
 			customModes: [],
 			maxOpenTabsContext: 20,
 			maxWorkspaceFiles: 100,
-			apiConfiguration: { providerId: "openrouter" } as ApiConfiguration,
+			apiConfiguration: { providerId: "openrouter" } as ProviderSettings,
 			telemetrySetting: "unset",
 			showRooIgnoredFiles: true,
 			renderContext: "sidebar",

+ 3 - 3
webview-ui/src/utils/validate.ts

@@ -1,8 +1,8 @@
 import i18next from "i18next"
 
-import { ApiConfiguration, isRouterName, RouterModels } from "@roo/shared/api"
+import { ProviderSettings, isRouterName, RouterModels } from "@roo/shared/api"
 
-export function validateApiConfiguration(apiConfiguration: ApiConfiguration): string | undefined {
+export function validateApiConfiguration(apiConfiguration: ProviderSettings): string | undefined {
 	switch (apiConfiguration.apiProvider) {
 		case "openrouter":
 			if (!apiConfiguration.openRouterApiKey) {
@@ -118,7 +118,7 @@ export function validateBedrockArn(arn: string, region?: string) {
 	return { isValid: true, arnRegion, errorMessage: undefined }
 }
 
-export function validateModelId(apiConfiguration: ApiConfiguration, routerModels?: RouterModels): string | undefined {
+export function validateModelId(apiConfiguration: ProviderSettings, routerModels?: RouterModels): string | undefined {
 	const provider = apiConfiguration.apiProvider ?? ""
 
 	if (!isRouterName(provider)) {