From 37c55843c4d52acfaee33454c7d9250203551e6b Mon Sep 17 00:00:00 2001 From: Prasang Prajapati Date: Wed, 27 Aug 2025 12:43:50 -0400 Subject: [PATCH] add support for IBM Cloud Pak for Data --- packages/types/src/provider-settings.ts | 5 + src/api/providers/watsonx.ts | 60 +++- .../settings/providers/WatsonxAI.tsx | 271 ++++++++++++++---- webview-ui/src/i18n/locales/en/settings.json | 10 +- webview-ui/src/utils/validate.ts | 26 +- 5 files changed, 304 insertions(+), 68 deletions(-) diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 4960f7ef03..7b997ae19a 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -289,10 +289,15 @@ const litellmSchema = baseProviderSettingsSchema.extend({ }) const watsonxSchema = baseProviderSettingsSchema.extend({ + watsonxPlatform: z.string().optional(), watsonxBaseUrl: z.string().optional(), watsonxApiKey: z.string().optional(), watsonxProjectId: z.string().optional(), watsonxModelId: z.string().optional(), + watsonxUsername: z.string().optional(), + watsonxAuthType: z.string().optional(), + watsonxPassword: z.string().optional(), + watsonxRegion: z.string().optional(), }) const cerebrasSchema = apiModelIdProviderModelSchema.extend({ diff --git a/src/api/providers/watsonx.ts b/src/api/providers/watsonx.ts index 94691da4b2..4b1ec48d06 100644 --- a/src/api/providers/watsonx.ts +++ b/src/api/providers/watsonx.ts @@ -2,7 +2,7 @@ import * as vscode from "vscode" import { Anthropic } from "@anthropic-ai/sdk" import { ModelInfo, watsonxAiDefaultModelId, watsonxAiModels, WatsonxAIModelId } from "@roo-code/types" import type { ApiHandlerOptions } from "../../shared/api" -import { IamAuthenticator } from "ibm-cloud-sdk-core" +import { IamAuthenticator, CloudPakForDataAuthenticator } from "ibm-cloud-sdk-core" import { ApiStream } from "../transform/stream" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" @@ -17,26 +17,66 @@ export class WatsonxAIHandler extends BaseProvider implements SingleCompletionHa constructor(options: ApiHandlerOptions) { super() this.options = options + this.projectId = (this.options as any).watsonxProjectId if (!this.projectId) { throw new Error("You must provide a valid IBM watsonx project ID.") } - const apiKey = (this.options as any).watsonxApiKey - if (!apiKey) { - throw new Error("You must provide a valid IBM watsonx API key.") - } - const serviceUrl = (this.options as any).watsonxBaseUrl || "https://us-south.ml.cloud.ibm.com" + + const serviceUrl = (this.options as any).watsonxBaseUrl + const platform = (this.options as any).watsonxPlatform try { const serviceOptions: any = { version: "2024-05-31", serviceUrl: serviceUrl, - authenticator: new IamAuthenticator({ - apikey: apiKey, - }), } - this.service = WatsonXAI.newInstance(serviceOptions) + // Choose authenticator based on platform + if (platform === "cloudPak") { + const username = this.options.watsonxUsername + if (!username) { + throw new Error("You must provide a valid username for IBM Cloud Pak for Data.") + } + + const authType = this.options.watsonxAuthType + + if (authType === "apiKey") { + const apiKey = this.options.watsonxApiKey + if (!apiKey) { + throw new Error("You must provide a valid API key for IBM Cloud Pak for Data.") + } + + serviceOptions.authenticator = new CloudPakForDataAuthenticator({ + username: username, + apikey: apiKey, + url: serviceUrl, + }) + } else { + const password = this.options.watsonxPassword + if (!password) { + throw new Error("You must provide a valid password for IBM Cloud Pak for Data.") + } + + serviceOptions.authenticator = new CloudPakForDataAuthenticator({ + username: username, + password: password, + url: serviceUrl, + }) + } + } else { + // Default to IBM Cloud with IAM authentication + const apiKey = this.options.watsonxApiKey + if (!apiKey) { + throw new Error("You must provide a valid IBM watsonx API key.") + } + + serviceOptions.authenticator = new IamAuthenticator({ + apikey: apiKey, + }) + } + + this.service = WatsonXAI.newInstance(serviceOptions) this.service.getAuthenticator().authenticate() } catch (error) { throw new Error( diff --git a/webview-ui/src/components/settings/providers/WatsonxAI.tsx b/webview-ui/src/components/settings/providers/WatsonxAI.tsx index 2258e698cc..4ed447454b 100644 --- a/webview-ui/src/components/settings/providers/WatsonxAI.tsx +++ b/webview-ui/src/components/settings/providers/WatsonxAI.tsx @@ -15,7 +15,6 @@ import { useExtensionState } from "@src/context/ExtensionStateContext" import { RouterName } from "@roo/api" import { ModelPicker } from "../ModelPicker" -// Define the available regions const WATSONX_REGIONS = { "us-south": "Dallas (us-south.ml.cloud.ibm.com)", "eu-de": "Frankfurt (eu-de.ml.cloud.ibm.com)", @@ -26,7 +25,6 @@ const WATSONX_REGIONS = { "ap-south-1": "Mumbai (ap-south-1.aws.wxai.ibm.com)", } -// Map region codes to full URLs const REGION_TO_URL = { "us-south": "https://us-south.ml.cloud.ibm.com", "eu-de": "https://eu-de.ml.cloud.ibm.com", @@ -35,7 +33,7 @@ const REGION_TO_URL = { "au-syd": "https://au-syd.ml.cloud.ibm.com", "ca-tor": "https://ca-tor.ml.cloud.ibm.com", "ap-south-1": "https://ap-south-1.aws.wxai.ibm.com", - custom: "", // For custom URL input + custom: "", } type WatsonxAIProps = { @@ -57,27 +55,60 @@ export const WatsonxAI = ({ const [refreshError, setRefreshError] = useState() const watsonxErrorJustReceived = useRef(false) - // Determine the current region based on the base URL + useEffect(() => { + if (!apiConfiguration.watsonxPlatform) { + setApiConfigurationField("watsonxPlatform", "ibmCloud") + } + }, [apiConfiguration.watsonxPlatform, setApiConfigurationField]) + const getCurrentRegion = () => { const baseUrl = apiConfiguration?.watsonxBaseUrl || "" - - // Find the region that matches the current base URL const regionEntry = Object.entries(REGION_TO_URL).find(([_, url]) => url === baseUrl) - - // Return the region code or 'us-south' as default if not found return regionEntry ? regionEntry[0] : "us-south" } const [selectedRegion, setSelectedRegion] = useState(getCurrentRegion()) - // Handle region selection const handleRegionSelect = useCallback( (region: string) => { setSelectedRegion(region) - - // Update the base URL in the API configuration const baseUrl = REGION_TO_URL[region as keyof typeof REGION_TO_URL] || "" setApiConfigurationField("watsonxBaseUrl", baseUrl) + setApiConfigurationField("watsonxRegion", region) + }, + [setApiConfigurationField], + ) + + const handlePlatformChange = useCallback( + (newPlatform: "ibmCloud" | "cloudPak") => { + setApiConfigurationField("watsonxPlatform", newPlatform) + + if (newPlatform === "ibmCloud") { + const defaultRegion = "us-south" + setSelectedRegion(defaultRegion) + setApiConfigurationField("watsonxRegion", defaultRegion) + setApiConfigurationField("watsonxBaseUrl", REGION_TO_URL[defaultRegion]) + setApiConfigurationField("watsonxUsername", "") + setApiConfigurationField("watsonxPassword", "") + setApiConfigurationField("watsonxAuthType", "apiKey") + } else { + setSelectedRegion("custom") + setApiConfigurationField("watsonxBaseUrl", "") + setApiConfigurationField("watsonxAuthType", "apiKey") + setApiConfigurationField("watsonxRegion", "") + } + }, + [setApiConfigurationField], + ) + + const handleAuthTypeChange = useCallback( + (newAuthType: "apiKey" | "password") => { + setApiConfigurationField("watsonxAuthType", newAuthType) + if (newAuthType === "apiKey") { + setApiConfigurationField("watsonxPassword", "") + } else { + setApiConfigurationField("watsonxApiKey", "") + } }, [setApiConfigurationField], ) @@ -93,7 +124,6 @@ export const WatsonxAI = ({ setRefreshError(message.error) } } else if (message.type === "routerModels") { - // When router models are updated, update the refresh status if (refreshStatus === "loading") { if (!watsonxErrorJustReceived.current) { setRefreshStatus("success") @@ -121,44 +151,190 @@ export const WatsonxAI = ({ setRefreshError(undefined) const apiKey = apiConfiguration.watsonxApiKey - const projectId = apiConfiguration.watsonxProjectId - const baseUrl = REGION_TO_URL[selectedRegion as keyof typeof REGION_TO_URL] + const platform = apiConfiguration.watsonxPlatform + const customUrl = apiConfiguration.watsonxBaseUrl || "" + const username = apiConfiguration.watsonxUsername + const authType = apiConfiguration.watsonxAuthType + const password = apiConfiguration.watsonxPassword - if (!apiKey) { + let baseUrl = "" + if (platform === "ibmCloud") { + baseUrl = REGION_TO_URL[selectedRegion as keyof typeof REGION_TO_URL] + } else { + baseUrl = customUrl + setApiConfigurationField("watsonxBaseUrl", baseUrl) + } + + if (platform === "ibmCloud" && (!apiKey || !baseUrl)) { setRefreshStatus("error") setRefreshError(t("settings:providers.refreshModels.missingConfig")) return } + if (platform === "cloudPak") { + if (!baseUrl) { + setRefreshStatus("error") + setRefreshError("URL is required for IBM Cloud Pak for Data") + return + } + + if (!username) { + setRefreshStatus("error") + setRefreshError("Username is required for IBM Cloud Pak for Data") + return + } + + if (authType === "apiKey" && !apiKey) { + setRefreshStatus("error") + setRefreshError("API Key is required for IBM Cloud Pak for Data") + return + } + + if (authType === "password" && !password) { + setRefreshStatus("error") + setRefreshError("Password is required for IBM Cloud Pak for Data") + return + } + } + vscode.postMessage({ type: "requestRouterModels", values: { - watsonxApiKey: apiKey, - watsonxProjectId: projectId, - watsonxBaseUrl: baseUrl, + watsonxPlatform: apiConfiguration.watsonxPlatform, + watsonxBaseUrl: apiConfiguration.watsonxBaseUrl, + watsonxApiKey: apiConfiguration.watsonxApiKey, + watsonxProjectId: apiConfiguration.watsonxProjectId, + watsonxModelId: apiConfiguration.watsonxModelId, + watsonxUsername: apiConfiguration.watsonxUsername, + watsonxAuthType: apiConfiguration.watsonxAuthType, + watsonxPassword: apiConfiguration.watsonxPassword, + watsonxRegion: apiConfiguration.watsonxRegion, }, }) - }, [apiConfiguration, setRefreshStatus, setRefreshError, t, selectedRegion]) + }, [apiConfiguration, setRefreshStatus, setRefreshError, t, selectedRegion, setApiConfigurationField]) return ( <> - - - -
- {t("settings:providers.apiKeyStorageNotice")} + {/* Platform Selection */} +
+ +
- {!apiConfiguration?.watsonxApiKey && ( - - Get WatsonX API Key - + + {/* IBM Cloud specific fields */} + {apiConfiguration.watsonxPlatform === "ibmCloud" && ( + <> + + + +
+ {t("settings:providers.apiKeyStorageNotice")} +
+ {!apiConfiguration?.watsonxApiKey && ( + + Get WatsonX API Key + + )} + +
+ + +
+ Selected endpoint: {REGION_TO_URL[selectedRegion as keyof typeof REGION_TO_URL]} +
+
+ )} + {/* IBM Cloud Pak for Data specific fields */} + {apiConfiguration.watsonxPlatform === "cloudPak" && ( + <> + + + +
+ Enter the full URL of your IBM Cloud Pak for Data instance +
+ + + + + +
+ + +
+ + {apiConfiguration.watsonxAuthType === "apiKey" ? ( + + + + ) : ( + + + + )} +
+ {t("settings:providers.apiKeyStorageNotice")} +
+ + )} + + {/* Common fields for both platforms */} -
- - -
- Selected endpoint: {REGION_TO_URL[selectedRegion as keyof typeof REGION_TO_URL]} -
-
-