From 8cb5c4e9e31b11e84ca3da14bd2158bf26705803 Mon Sep 17 00:00:00 2001 From: John Richmond <5629+jr@users.noreply.github.com> Date: Mon, 12 May 2025 12:47:09 -0700 Subject: [PATCH] Restore profile management work + fix #3434 (#3449) Co-authored-by: Matt Rubens --- .changeset/thin-penguins-search.md | 5 + .gitignore | 1 + e2e/package-lock.json | 2 +- e2e/package.json | 2 +- evals/config/eslint/package.json | 1 - evals/package.json | 2 +- evals/packages/types/src/roo-code.ts | 291 +++++++++++--- evals/pnpm-lock.yaml | 72 ++-- git | 0 package-lock.json | 4 +- package.json | 4 +- src/api/index.ts | 4 +- src/core/config/ProviderSettingsManager.ts | 88 +++-- .../__tests__/ProviderSettingsManager.test.ts | 128 +++--- src/core/task/Task.ts | 6 +- src/core/task/__tests__/Task.test.ts | 4 +- src/core/webview/ClineProvider.ts | 190 ++++++--- .../webview/__tests__/ClineProvider.test.ts | 81 ++-- src/core/webview/webviewMessageHandler.ts | 149 +++---- src/exports/api.ts | 193 +++++----- src/exports/interface.ts | 81 +++- src/exports/roo-code.d.ts | 363 ++++++++++-------- src/exports/types.ts | 314 ++++++++------- src/schemas/index.ts | 198 +++++++--- src/shared/ExtensionMessage.ts | 10 +- src/shared/WebviewMessage.ts | 5 +- .../__tests__/checkExistApiConfig.test.ts | 12 +- src/shared/api.ts | 4 +- src/utils/__tests__/enhance-prompt.test.ts | 8 +- src/utils/single-completion-handler.ts | 4 +- .../chat/__tests__/TaskHeader.test.tsx | 4 +- .../components/settings/ApiConfigManager.tsx | 4 +- .../src/components/settings/ApiOptions.tsx | 10 +- .../settings/PromptCachingControl.tsx | 6 +- .../components/settings/ReasoningEffort.tsx | 6 +- .../src/components/settings/SettingsView.tsx | 4 +- .../components/settings/ThinkingBudget.tsx | 6 +- .../settings/__tests__/ApiOptions.test.tsx | 4 +- .../settings/providers/Anthropic.tsx | 10 +- .../components/settings/providers/Bedrock.tsx | 10 +- .../settings/providers/BedrockCustomArn.tsx | 6 +- .../components/settings/providers/Chutes.tsx | 10 +- .../settings/providers/DeepSeek.tsx | 10 +- .../components/settings/providers/Gemini.tsx | 10 +- .../components/settings/providers/Glama.tsx | 10 +- .../components/settings/providers/Groq.tsx | 10 +- .../settings/providers/LMStudio.tsx | 10 +- .../components/settings/providers/LiteLLM.tsx | 11 +- .../components/settings/providers/Mistral.tsx | 10 +- .../components/settings/providers/Ollama.tsx | 10 +- .../components/settings/providers/OpenAI.tsx | 10 +- .../settings/providers/OpenAICompatible.tsx | 10 +- .../settings/providers/OpenRouter.tsx | 10 +- .../settings/providers/Requesty.tsx | 10 +- .../components/settings/providers/Unbound.tsx | 10 +- .../settings/providers/VSCodeLM.tsx | 10 +- .../components/settings/providers/Vertex.tsx | 10 +- .../src/components/settings/providers/XAI.tsx | 10 +- .../components/ui/hooks/useSelectedModel.ts | 6 +- .../src/context/ExtensionStateContext.tsx | 16 +- .../__tests__/ExtensionStateContext.test.tsx | 10 +- webview-ui/src/utils/validate.ts | 6 +- 62 files changed, 1502 insertions(+), 1003 deletions(-) create mode 100644 .changeset/thin-penguins-search.md delete mode 100644 git diff --git a/.changeset/thin-penguins-search.md b/.changeset/thin-penguins-search.md new file mode 100644 index 0000000000..36a383cdea --- /dev/null +++ b/.changeset/thin-penguins-search.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Restore profile management work + fix #3434 diff --git a/.gitignore b/.gitignore index 1277732969..3a541f1b09 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ out out-* node_modules coverage/ +mock/ .DS_Store diff --git a/e2e/package-lock.json b/e2e/package-lock.json index bcee0b2676..e7f75013fd 100644 --- a/e2e/package-lock.json +++ b/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": { diff --git a/e2e/package.json b/e2e/package.json index a8007b1c4b..1acec6bba5 100644 --- a/e2e/package.json +++ b/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" } } diff --git a/evals/config/eslint/package.json b/evals/config/eslint/package.json index 3b84b039de..b706de1d95 100644 --- a/evals/config/eslint/package.json +++ b/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" } } diff --git a/evals/package.json b/evals/package.json index 2e7c21d977..92c4ab99cd 100644 --- a/evals/package.json +++ b/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" } } diff --git a/evals/packages/types/src/roo-code.ts b/evals/packages/types/src/roo-code.ts index 312040cf09..32b54c4c81 100644 --- a/evals/packages/types/src/roo-code.ts +++ b/evals/packages/types/src/roo-code.ts @@ -128,18 +128,6 @@ export const modelInfoSchema = z.object({ export type ModelInfo = z.infer -/** - * ApiConfigMeta - */ - -export const apiConfigMetaSchema = z.object({ - id: z.string(), - name: z.string(), - apiProvider: providerNamesSchema.optional(), -}) - -export type ApiConfigMeta = z.infer - /** * HistoryItem */ @@ -329,27 +317,56 @@ export type Experiments = z.infer // eslint-disable-next-line @typescript-eslint/no-unused-vars type _AssertExperiments = AssertEqual>> +/** + * ProviderSettingsEntry + */ + +export const providerSettingsEntrySchema = z.object({ + id: z.string(), + name: z.string(), + apiProvider: providerNamesSchema.optional(), +}) + +export type ProviderSettingsEntry = z.infer + /** * ProviderSettings */ -export const providerSettingsSchema = z.object({ - apiProvider: providerNamesSchema.optional(), - // Anthropic +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 type ProviderSettingsRecord = Record, 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[] @@ -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(), diff --git a/evals/pnpm-lock.yaml b/evals/pnpm-lock.yaml index 52842d0439..85bb109866 100644 --- a/evals/pnpm-lock.yaml +++ b/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(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2) + version: 8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3) config/typescript: {} @@ -4377,11 +4374,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - typescript@5.8.2: - resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} - engines: {node: '>=14.17'} - hasBin: true - typescript@5.8.3: resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} engines: {node: '>=14.17'} @@ -6037,20 +6029,20 @@ snapshots: dependencies: '@types/node': 20.17.24 - '@typescript-eslint/eslint-plugin@8.26.1(@typescript-eslint/parser@8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)': + '@typescript-eslint/eslint-plugin@8.26.1(@typescript-eslint/parser@8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2) + '@typescript-eslint/parser': 8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.26.1 - '@typescript-eslint/type-utils': 8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2) - '@typescript-eslint/utils': 8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2) + '@typescript-eslint/type-utils': 8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.26.1 eslint: 9.22.0(jiti@2.4.2) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 2.0.1(typescript@5.8.2) - typescript: 5.8.2 + ts-api-utils: 2.0.1(typescript@5.8.3) + typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -6071,15 +6063,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)': + '@typescript-eslint/parser@8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@typescript-eslint/scope-manager': 8.26.1 '@typescript-eslint/types': 8.26.1 - '@typescript-eslint/typescript-estree': 8.26.1(typescript@5.8.2) + '@typescript-eslint/typescript-estree': 8.26.1(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.26.1 debug: 4.4.0 eslint: 9.22.0(jiti@2.4.2) - 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/type-utils@8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)': + '@typescript-eslint/type-utils@8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@typescript-eslint/typescript-estree': 8.26.1(typescript@5.8.2) - '@typescript-eslint/utils': 8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2) + '@typescript-eslint/typescript-estree': 8.26.1(typescript@5.8.3) + '@typescript-eslint/utils': 8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3) debug: 4.4.0 eslint: 9.22.0(jiti@2.4.2) - ts-api-utils: 2.0.1(typescript@5.8.2) - typescript: 5.8.2 + ts-api-utils: 2.0.1(typescript@5.8.3) + typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -6131,7 +6123,7 @@ snapshots: '@typescript-eslint/types@8.32.0': {} - '@typescript-eslint/typescript-estree@8.26.1(typescript@5.8.2)': + '@typescript-eslint/typescript-estree@8.26.1(typescript@5.8.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(typescript@5.8.2) - typescript: 5.8.2 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -6159,14 +6151,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)': + '@typescript-eslint/utils@8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@eslint-community/eslint-utils': 4.6.1(eslint@9.22.0(jiti@2.4.2)) '@typescript-eslint/scope-manager': 8.26.1 '@typescript-eslint/types': 8.26.1 - '@typescript-eslint/typescript-estree': 8.26.1(typescript@5.8.2) + '@typescript-eslint/typescript-estree': 8.26.1(typescript@5.8.3) eslint: 9.22.0(jiti@2.4.2) - typescript: 5.8.2 + typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -8577,13 +8569,9 @@ snapshots: toidentifier@1.0.1: {} - ts-api-utils@2.0.1(typescript@5.8.2): + ts-api-utils@2.0.1(typescript@5.8.3): dependencies: - typescript: 5.8.2 - - ts-api-utils@2.1.0(typescript@5.8.2): - dependencies: - typescript: 5.8.2 + typescript: 5.8.3 ts-api-utils@2.1.0(typescript@5.8.3): dependencies: @@ -8670,13 +8658,13 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2): + typescript-eslint@8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.26.1(@typescript-eslint/parser@8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2) - '@typescript-eslint/parser': 8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2) - '@typescript-eslint/utils': 8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2) + '@typescript-eslint/eslint-plugin': 8.26.1(@typescript-eslint/parser@8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3) eslint: 9.22.0(jiti@2.4.2) - typescript: 5.8.2 + typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -8690,8 +8678,6 @@ snapshots: transitivePeerDependencies: - supports-color - typescript@5.8.2: {} - typescript@5.8.3: {} unbox-primitive@1.1.0: diff --git a/git b/git deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/package-lock.json b/package-lock.json index 4da28bb5b4..59a098aa46 100644 --- a/package-lock.json +++ b/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" }, diff --git a/package.json b/package.json index 30114c4a5b..c2f26b739d 100644 --- a/package.json +++ b/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" }, diff --git a/src/api/index.ts b/src/api/index.ts index c337dff768..10cbe14429 100644 --- a/src/api/index.ts +++ b/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): Promise } -export function buildApiHandler(configuration: ApiConfiguration): ApiHandler { +export function buildApiHandler(configuration: ProviderSettings): ApiHandler { const { apiProvider, ...options } = configuration switch (apiProvider) { diff --git a/src/core/config/ProviderSettingsManager.ts b/src/core/config/ProviderSettingsManager.ts index 53ae585b3c..d22a87e097 100644 --- a/src/core/config/ProviderSettingsManager.ts +++ b/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 @@ -223,7 +226,7 @@ export class ProviderSettingsManager { /** * List all available configs with metadata. */ - public async listConfig(): Promise { + public async listConfig(): Promise { 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 { 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 { 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 + + 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] } - providerProfiles.currentApiConfigName = name - await this.store(providerProfiles) - - 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 { + 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}`) } diff --git a/src/core/config/__tests__/ProviderSettingsManager.test.ts b/src/core/config/__tests__/ProviderSettingsManager.test.ts index 064157da02..7e50544095 100644 --- a/src/core/config/__tests__/ProviderSettingsManager.test.ts +++ b/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") diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index f90d692617..05c9785452 100644 --- a/src/core/task/Task.ts +++ b/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 { customInstructions?: string // API - readonly apiConfiguration: ApiConfiguration + readonly apiConfiguration: ProviderSettings api: ApiHandler private promptCacheKey: string private lastApiRequestTime?: number diff --git a/src/core/task/__tests__/Task.test.ts b/src/core/task/__tests__/Task.test.ts index f70edfdefa..0bffc4a179 100644 --- a/src/core/task/__tests__/Task.test.ts +++ b/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 - let mockApiConfig: ApiConfiguration + let mockApiConfig: ProviderSettings let mockOutputChannel: any let mockExtensionContext: vscode.ExtensionContext diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 771eb2952c..e9953052f6 100644 --- a/src/core/webview/ClineProvider.ts +++ b/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 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 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 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 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 || [] + } - if (config?.id) { - await this.providerSettingsManager.setModeConfig(mode, config.id) + 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 { + 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 (task) { + task.api = buildApiHandler(providerSettings) + } + } else { + await this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig()) } - } - await this.contextProxy.setProviderSettings(providerSettings) + await this.postStateToWebview() + return id + } catch (error) { + this.log( + `Error create new api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) - if (this.getCurrentCline()) { - this.getCurrentCline()!.api = buildApiHandler(providerSettings) + vscode.window.showErrorMessage(t("common:errors.create_api_config")) + return undefined } } + async deleteProviderProfile(profileToDelete: ProviderSettingsEntry) { + const globalSettings = this.contextProxy.getValues() + let profileToActivate: string | undefined = globalSettings.currentApiConfigName + + 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 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 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 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 diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index 1897a7e7e8..3bd3a11db4 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/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 diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 6560a5cac6..cde453d4fc 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/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 } + const oldName = message.text + + const newName = (await provider.providerSettingsManager.listConfig()).filter( + (c) => c.name !== oldName, + )[0]?.name + + if (!newName) { + vscode.window.showErrorMessage(t("common:errors.delete_api_config")) + return + } + 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) - - // If this was the current config, switch to first available - const currentApiConfigName = getGlobalState("currentApiConfigName") - - 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), - ]) - } - - await provider.postStateToWebview() + 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")) } } diff --git a/src/exports/api.ts b/src/exports/api.ts index 0d70d7dc04..ca30c0f3fb 100644 --- a/src/exports/api.ts +++ b/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 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 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 { + 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 { + 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 { + 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 { + 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() + } } diff --git a/src/exports/interface.ts b/src/exports/interface.ts index a0ef46a226..eb30ea1773 100644 --- a/src/exports/interface.ts +++ b/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 { */ pressSecondaryButton(): Promise + /** + * Returns true if the API is ready to use. + */ + isReady(): boolean + /** * Returns the current configuration. * @returns The current configuration. @@ -86,13 +107,6 @@ export interface RooCodeAPI extends EventEmitter { */ setConfiguration(values: RooCodeSettings): Promise - /** - * Creates a new API configuration profile - * @param name The name of the profile - * @returns The ID of the created profile - */ - createProfile(name: string): Promise - /** * Returns a list of all configured profile names * @returns Array of profile names @@ -100,17 +114,40 @@ export interface RooCodeAPI extends EventEmitter { getProfiles(): string[] /** - * Changes the active API configuration profile - * @param name The name of the profile to activate - * @throws Error if the profile does not exist + * 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 */ - setActiveProfile(name: string): Promise + getProfileEntry(name: string): ProviderSettingsEntry | 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 + * @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 */ - getActiveProfile(): string | undefined + createProfile(name: string, profile?: ProviderSettings, activate?: boolean): Promise + + /** + * 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 + */ + updateProfile(name: string, profile: ProviderSettings, activate?: boolean): Promise + + /** + * 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 + */ + upsertProfile(name: string, profile: ProviderSettings, activate?: boolean): Promise /** * Deletes a profile by name @@ -120,7 +157,15 @@ export interface RooCodeAPI extends EventEmitter { deleteProfile(name: string): Promise /** - * 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 } diff --git a/src/exports/roo-code.d.ts b/src/exports/roo-code.d.ts index d0d82fa39e..59ca2fe240 100644 --- a/src/exports/roo-code.d.ts +++ b/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 { * Simulates pressing the secondary button in the chat interface. */ pressSecondaryButton(): Promise + /** + * 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 { * @param values An object containing key-value pairs to set. */ setConfiguration(values: RooCodeSettings): Promise - /** - * Creates a new API configuration profile - * @param name The name of the profile - * @returns The ID of the created profile - */ - createProfile(name: string): Promise /** * 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 + /** + * 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 + updateProfile(name: string, profile: ProviderSettings, activate?: boolean): Promise /** - * 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 /** * Deletes a profile by name * @param name The name of the profile to delete @@ -622,15 +671,23 @@ interface RooCodeAPI extends EventEmitter { */ deleteProfile(name: string): Promise /** - * 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 } export { type ClineMessage, type GlobalSettings, type ProviderSettings, + type ProviderSettingsEntry, type RooCodeAPI, RooCodeEventName, type RooCodeEvents, diff --git a/src/exports/types.ts b/src/exports/types.ts index 05d492b8cc..8d1a02e979 100644 --- a/src/exports/types.ts +++ b/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" diff --git a/src/schemas/index.ts b/src/schemas/index.ts index 094b11b10d..8f37eca244 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -135,18 +135,6 @@ export const modelInfoSchema = z.object({ export type ModelInfo = z.infer -/** - * ApiConfigMeta - */ - -export const apiConfigMetaSchema = z.object({ - id: z.string(), - name: z.string(), - apiProvider: providerNamesSchema.optional(), -}) - -export type ApiConfigMeta = z.infer - /** * HistoryItem */ @@ -341,27 +329,60 @@ export type Experiments = z.infer type _AssertExperiments = AssertEqual>> +/** + * ProviderSettingsEntry + */ + +export const providerSettingsEntrySchema = z.object({ + id: z.string(), + name: z.string(), + apiProvider: providerNamesSchema.optional(), +}) + +export type ProviderSettingsEntry = z.infer + /** * ProviderSettings */ -export const providerSettingsSchema = z.object({ - apiProvider: providerNamesSchema.optional(), - // Anthropic +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 type ProviderSettingsRecord = Record, 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" }, diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index d118dd3989..6330556024 100644 --- a/src/shared/ExtensionMessage.ts +++ b/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 diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index fe8dfff40c..22fe5c7d3e 100644 --- a/src/shared/WebviewMessage.ts +++ b/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 diff --git a/src/shared/__tests__/checkExistApiConfig.test.ts b/src/shared/__tests__/checkExistApiConfig.test.ts index c99ddddbc4..a8e603db8a 100644 --- a/src/shared/__tests__/checkExistApiConfig.test.ts +++ b/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, diff --git a/src/shared/api.ts b/src/shared/api.ts index 9f64d7913f..17d2b14dd4 100644 --- a/src/shared/api.ts +++ b/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 -export type ApiConfiguration = ProviderSettings - // Anthropic // https://docs.anthropic.com/en/docs/about-claude/models export type AnthropicModelId = keyof typeof anthropicModels diff --git a/src/utils/__tests__/enhance-prompt.test.ts b/src/utils/__tests__/enhance-prompt.test.ts index adda8860eb..524f99c817 100644 --- a/src/utils/__tests__/enhance-prompt.test.ts +++ b/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", diff --git a/src/utils/single-completion-handler.ts b/src/utils/single-completion-handler.ts index 5e049d4726..7434d07e19 100644 --- a/src/utils/single-completion-handler.ts +++ b/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 { +export async function singleCompletionHandler(apiConfiguration: ProviderSettings, promptText: string): Promise { if (!promptText) { throw new Error("No prompt text provided") } diff --git a/webview-ui/src/components/chat/__tests__/TaskHeader.test.tsx b/webview-ui/src/components/chat/__tests__/TaskHeader.test.tsx index 625bad1169..f9fb71b066 100644 --- a/webview-ui/src/components/chat/__tests__/TaskHeader.test.tsx +++ b/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, }), })) diff --git a/webview-ui/src/components/settings/ApiConfigManager.tsx b/webview-ui/src/components/settings/ApiConfigManager.tsx index 841de5d8a2..05b14bc586 100644 --- a/webview-ui/src/components/settings/ApiConfigManager.tsx +++ b/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 diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 33bbdce78f..52fd1c0d3b 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/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: (field: K, value: ApiConfiguration[K]) => void + apiConfiguration: ProviderSettings + setApiConfigurationField: (field: K, value: ProviderSettings[K]) => void fromWelcomeView?: boolean errorMessage: string | undefined setErrorMessage: React.Dispatch> @@ -107,9 +107,9 @@ const ApiOptions = ({ const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false) const handleInputChange = useCallback( - ( + ( field: K, - transform: (event: E) => ApiConfiguration[K] = inputEventTransform, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, ) => (event: E | Event) => { setApiConfigurationField(field, transform(event as E)) diff --git a/webview-ui/src/components/settings/PromptCachingControl.tsx b/webview-ui/src/components/settings/PromptCachingControl.tsx index e7600915ec..db5687a57d 100644 --- a/webview-ui/src/components/settings/PromptCachingControl.tsx +++ b/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: (field: K, value: ApiConfiguration[K]) => void + apiConfiguration: ProviderSettings + setApiConfigurationField: (field: K, value: ProviderSettings[K]) => void } export const PromptCachingControl = ({ apiConfiguration, setApiConfigurationField }: PromptCachingControlProps) => { diff --git a/webview-ui/src/components/settings/ReasoningEffort.tsx b/webview-ui/src/components/settings/ReasoningEffort.tsx index 8e3c399ae9..53d21a2c49 100644 --- a/webview-ui/src/components/settings/ReasoningEffort.tsx +++ b/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: (field: K, value: ApiConfiguration[K]) => void + apiConfiguration: ProviderSettings + setApiConfigurationField: (field: K, value: ProviderSettings[K]) => void } export const ReasoningEffort = ({ apiConfiguration, setApiConfigurationField }: ReasoningEffortProps) => { diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index eeb11ea6eb..735a5b67ba 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/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(({ onDone, t }, []) const setApiConfigurationField = useCallback( - (field: K, value: ApiConfiguration[K]) => { + (field: K, value: ProviderSettings[K]) => { setCachedState((prevState) => { if (prevState.apiConfiguration?.[field] === value) { return prevState diff --git a/webview-ui/src/components/settings/ThinkingBudget.tsx b/webview-ui/src/components/settings/ThinkingBudget.tsx index 1ee2f310e0..6c4352f22b 100644 --- a/webview-ui/src/components/settings/ThinkingBudget.tsx +++ b/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: (field: K, value: ApiConfiguration[K]) => void + apiConfiguration: ProviderSettings + setApiConfigurationField: (field: K, value: ProviderSettings[K]) => void modelInfo?: ModelInfo } diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx index 115c623a27..b9b1d1d21e 100644 --- a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx +++ b/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, diff --git a/webview-ui/src/components/settings/providers/Anthropic.tsx b/webview-ui/src/components/settings/providers/Anthropic.tsx index bf310d1cb7..4d5957b562 100644 --- a/webview-ui/src/components/settings/providers/Anthropic.tsx +++ b/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( - ( + ( field: K, - transform: (event: E) => ApiConfiguration[K] = inputEventTransform, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, ) => (event: E | Event) => { setApiConfigurationField(field, transform(event as E)) diff --git a/webview-ui/src/components/settings/providers/Bedrock.tsx b/webview-ui/src/components/settings/providers/Bedrock.tsx index fc69e74dde..2ddc5797ca 100644 --- a/webview-ui/src/components/settings/providers/Bedrock.tsx +++ b/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( - ( + ( field: K, - transform: (event: E) => ApiConfiguration[K] = inputEventTransform, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, ) => (event: E | Event) => { setApiConfigurationField(field, transform(event as E)) diff --git a/webview-ui/src/components/settings/providers/BedrockCustomArn.tsx b/webview-ui/src/components/settings/providers/BedrockCustomArn.tsx index f24d9ef81c..1caf95b8c7 100644 --- a/webview-ui/src/components/settings/providers/BedrockCustomArn.tsx +++ b/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) => { diff --git a/webview-ui/src/components/settings/providers/Chutes.tsx b/webview-ui/src/components/settings/providers/Chutes.tsx index f87b9650cf..10ca2c202f 100644 --- a/webview-ui/src/components/settings/providers/Chutes.tsx +++ b/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( - ( + ( field: K, - transform: (event: E) => ApiConfiguration[K] = inputEventTransform, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, ) => (event: E | Event) => { setApiConfigurationField(field, transform(event as E)) diff --git a/webview-ui/src/components/settings/providers/DeepSeek.tsx b/webview-ui/src/components/settings/providers/DeepSeek.tsx index 8b1e2e91ef..ffb48aec09 100644 --- a/webview-ui/src/components/settings/providers/DeepSeek.tsx +++ b/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( - ( + ( field: K, - transform: (event: E) => ApiConfiguration[K] = inputEventTransform, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, ) => (event: E | Event) => { setApiConfigurationField(field, transform(event as E)) diff --git a/webview-ui/src/components/settings/providers/Gemini.tsx b/webview-ui/src/components/settings/providers/Gemini.tsx index 490ab73a51..0a9af31568 100644 --- a/webview-ui/src/components/settings/providers/Gemini.tsx +++ b/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( - ( + ( field: K, - transform: (event: E) => ApiConfiguration[K] = inputEventTransform, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, ) => (event: E | Event) => { setApiConfigurationField(field, transform(event as E)) diff --git a/webview-ui/src/components/settings/providers/Glama.tsx b/webview-ui/src/components/settings/providers/Glama.tsx index 1150e14936..7a7f5dcc49 100644 --- a/webview-ui/src/components/settings/providers/Glama.tsx +++ b/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( - ( + ( field: K, - transform: (event: E) => ApiConfiguration[K] = inputEventTransform, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, ) => (event: E | Event) => { setApiConfigurationField(field, transform(event as E)) diff --git a/webview-ui/src/components/settings/providers/Groq.tsx b/webview-ui/src/components/settings/providers/Groq.tsx index 45f827df14..eaf29a4572 100644 --- a/webview-ui/src/components/settings/providers/Groq.tsx +++ b/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( - ( + ( field: K, - transform: (event: E) => ApiConfiguration[K] = inputEventTransform, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, ) => (event: E | Event) => { setApiConfigurationField(field, transform(event as E)) diff --git a/webview-ui/src/components/settings/providers/LMStudio.tsx b/webview-ui/src/components/settings/providers/LMStudio.tsx index 0979ec9242..df58f903bb 100644 --- a/webview-ui/src/components/settings/providers/LMStudio.tsx +++ b/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([]) const handleInputChange = useCallback( - ( + ( field: K, - transform: (event: E) => ApiConfiguration[K] = inputEventTransform, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, ) => (event: E | Event) => { setApiConfigurationField(field, transform(event as E)) diff --git a/webview-ui/src/components/settings/providers/LiteLLM.tsx b/webview-ui/src/components/settings/providers/LiteLLM.tsx index 5fc466116c..8f98d97e5d 100644 --- a/webview-ui/src/components/settings/providers/LiteLLM.tsx +++ b/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( - ( + ( 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"> +
{t("settings:providers.apiKeyStorageNotice")}
diff --git a/webview-ui/src/components/settings/providers/Mistral.tsx b/webview-ui/src/components/settings/providers/Mistral.tsx index c8c27c1d55..1669166043 100644 --- a/webview-ui/src/components/settings/providers/Mistral.tsx +++ b/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( - ( + ( field: K, - transform: (event: E) => ApiConfiguration[K] = inputEventTransform, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, ) => (event: E | Event) => { setApiConfigurationField(field, transform(event as E)) diff --git a/webview-ui/src/components/settings/providers/Ollama.tsx b/webview-ui/src/components/settings/providers/Ollama.tsx index 5646fae18c..bda3c441cc 100644 --- a/webview-ui/src/components/settings/providers/Ollama.tsx +++ b/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([]) const handleInputChange = useCallback( - ( + ( field: K, - transform: (event: E) => ApiConfiguration[K] = inputEventTransform, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, ) => (event: E | Event) => { setApiConfigurationField(field, transform(event as E)) diff --git a/webview-ui/src/components/settings/providers/OpenAI.tsx b/webview-ui/src/components/settings/providers/OpenAI.tsx index 501dc9df55..771d77ef6a 100644 --- a/webview-ui/src/components/settings/providers/OpenAI.tsx +++ b/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( - ( + ( field: K, - transform: (event: E) => ApiConfiguration[K] = inputEventTransform, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, ) => (event: E | Event) => { setApiConfigurationField(field, transform(event as E)) diff --git a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx index 43bdc2d9bd..774a5681ce 100644 --- a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx +++ b/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( - ( + ( field: K, - transform: (event: E) => ApiConfiguration[K] = inputEventTransform, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, ) => (event: E | Event) => { setApiConfigurationField(field, transform(event as E)) diff --git a/webview-ui/src/components/settings/providers/OpenRouter.tsx b/webview-ui/src/components/settings/providers/OpenRouter.tsx index 696fcca331..7bb7c30095 100644 --- a/webview-ui/src/components/settings/providers/OpenRouter.tsx +++ b/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( - ( + ( field: K, - transform: (event: E) => ApiConfiguration[K] = inputEventTransform, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, ) => (event: E | Event) => { setApiConfigurationField(field, transform(event as E)) diff --git a/webview-ui/src/components/settings/providers/Requesty.tsx b/webview-ui/src/components/settings/providers/Requesty.tsx index a8c8fc8099..9c6ba2844e 100644 --- a/webview-ui/src/components/settings/providers/Requesty.tsx +++ b/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() const handleInputChange = useCallback( - ( + ( field: K, - transform: (event: E) => ApiConfiguration[K] = inputEventTransform, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, ) => (event: E | Event) => { setApiConfigurationField(field, transform(event as E)) diff --git a/webview-ui/src/components/settings/providers/Unbound.tsx b/webview-ui/src/components/settings/providers/Unbound.tsx index 7467d2b7c6..77b24bb7cc 100644 --- a/webview-ui/src/components/settings/providers/Unbound.tsx +++ b/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( - ( + ( field: K, - transform: (event: E) => ApiConfiguration[K] = inputEventTransform, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, ) => (event: E | Event) => { setApiConfigurationField(field, transform(event as E)) diff --git a/webview-ui/src/components/settings/providers/VSCodeLM.tsx b/webview-ui/src/components/settings/providers/VSCodeLM.tsx index ca701ce024..7f9426e5ce 100644 --- a/webview-ui/src/components/settings/providers/VSCodeLM.tsx +++ b/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([]) const handleInputChange = useCallback( - ( + ( field: K, - transform: (event: E) => ApiConfiguration[K] = inputEventTransform, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, ) => (event: E | Event) => { setApiConfigurationField(field, transform(event as E)) diff --git a/webview-ui/src/components/settings/providers/Vertex.tsx b/webview-ui/src/components/settings/providers/Vertex.tsx index f0723f9d31..b43c793816 100644 --- a/webview-ui/src/components/settings/providers/Vertex.tsx +++ b/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( - ( + ( field: K, - transform: (event: E) => ApiConfiguration[K] = inputEventTransform, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, ) => (event: E | Event) => { setApiConfigurationField(field, transform(event as E)) diff --git a/webview-ui/src/components/settings/providers/XAI.tsx b/webview-ui/src/components/settings/providers/XAI.tsx index 39b0e53d34..c42bde1b1e 100644 --- a/webview-ui/src/components/settings/providers/XAI.tsx +++ b/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( - ( + ( field: K, - transform: (event: E) => ApiConfiguration[K] = inputEventTransform, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, ) => (event: E | Event) => { setApiConfigurationField(field, transform(event as E)) diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 3b1d792c93..fb1b277981 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/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) { diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index f3a1f69969..63c895f034 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/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() const setListApiConfigMeta = useCallback( - (value: ApiConfigMeta[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })), + (value: ProviderSettingsEntry[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })), [], ) diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx index 1ba2d87e9a..02e57b5e35 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx +++ b/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 (
{JSON.stringify(allowedCommands)}
@@ -26,9 +26,9 @@ const TestComponent = () => { ) } -// Test component for API configuration const ApiConfigTestComponent = () => { const { apiConfiguration, setApiConfiguration } = useExtensionState() + return (
{JSON.stringify(apiConfiguration)}
@@ -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", diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index 89abe3dedd..0765fffeda 100644 --- a/webview-ui/src/utils/validate.ts +++ b/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)) {