diff --git a/packages/types/src/providers/watsonx.ts b/packages/types/src/providers/watsonx.ts index 1f78730128..8f0302750f 100644 --- a/packages/types/src/providers/watsonx.ts +++ b/packages/types/src/providers/watsonx.ts @@ -4,7 +4,7 @@ export type WatsonxAIModelId = keyof typeof watsonxAiModels export const watsonxAiDefaultModelId: WatsonxAIModelId = "ibm/granite-3-3-8b-instruct" // Common model properties -const baseModelInfo: ModelInfo = { +export const baseModelInfo: ModelInfo = { maxTokens: 4096, contextWindow: 128000, supportsImages: false, diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index a98b446f99..a2aaa5d154 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -92,7 +92,7 @@ export const getModels = async (options: GetModelsOptions): Promise models = await getVercelAiGatewayModels() break case "watsonx": - models = await getWatsonxModels(options.apiKey) + models = await getWatsonxModels(options.apiKey, false) break default: { // Ensures router is exhaustively checked if RouterName is a strict union diff --git a/src/api/providers/fetchers/watsonx.ts b/src/api/providers/fetchers/watsonx.ts index 98da1003a7..374cb7cf54 100644 --- a/src/api/providers/fetchers/watsonx.ts +++ b/src/api/providers/fetchers/watsonx.ts @@ -1,28 +1,65 @@ import { ModelInfo } from "@roo-code/types" -import { IamAuthenticator } from "ibm-cloud-sdk-core" +import { IamAuthenticator, CloudPakForDataAuthenticator } from "ibm-cloud-sdk-core" import { WatsonXAI } from "@ibm-cloud/watsonx-ai" /** * Fetches available watsonx models * - * @param apiKey - The watsonx API key + * @param apiKey - The watsonx API key (for IBM Cloud or Cloud Pak with API key auth) + * @param embedded - Whether to fetch embedding models (true) or LLM models (false) * @param projectId - Optional project ID for watsonx * @param baseUrl - Optional base URL for the watsonx API + * @param platform - Optional platform type (ibmCloud or cloudPak) + * @param username - Optional username for Cloud Pak for Data + * @param password - Optional password for Cloud Pak for Data (when using password auth) * @returns A promise resolving to an object with model IDs as keys and model info as values */ export async function getWatsonxModels( apiKey: string, + embedded: boolean, projectId?: string, baseUrl?: string, + platform: "ibmCloud" | "cloudPak" = "ibmCloud", + username?: string, + password?: string, ): Promise> { try { - const service = WatsonXAI.newInstance({ + let options: any = { version: "2024-05-31", serviceUrl: baseUrl || "https://us-south.ml.cloud.ibm.com", - authenticator: new IamAuthenticator({ + } + + if (platform === "ibmCloud" || !platform) { + options.authenticator = new IamAuthenticator({ apikey: apiKey, - }), - }) + }) + } else if (platform === "cloudPak") { + if (!baseUrl) { + throw new Error("Base URL is required for IBM Cloud Pak for Data") + } + + if (username) { + if (password) { + options.authenticator = new CloudPakForDataAuthenticator({ + url: baseUrl, + username: username, + password: password, + }) + } else if (apiKey) { + options.authenticator = new CloudPakForDataAuthenticator({ + url: baseUrl, + username: username, + apikey: apiKey, + }) + } + } else { + options.authenticator = new IamAuthenticator({ + apikey: apiKey, + }) + } + } + + const service = WatsonXAI.newInstance(options) let knownModels: Record = {} @@ -36,7 +73,7 @@ export async function getWatsonxModels( for (const model of modelsList) { const modelId = model.id || model.name || model.model_id const modelInfo = JSON.stringify(model).toLowerCase() - if (modelId && !modelInfo.includes("embed") && !modelInfo.includes("rtrvr")) { + if (modelId && !embedded && !modelInfo.includes("embed") && !modelInfo.includes("rtrvr")) { const contextWindow = model.context_length || model.max_input_tokens || 8192 const maxTokens = model.max_output_tokens || Math.floor(contextWindow / 2) @@ -45,17 +82,28 @@ export async function getWatsonxModels( maxTokens, supportsPromptCache: false, } + } else { + if (modelId && embedded && modelInfo.includes("embed") && modelInfo.includes("rtrvr")) { + const contextWindow = model.context_length || model.max_input_tokens || 8192 + const maxTokens = model.max_output_tokens || Math.floor(contextWindow / 2) + + knownModels[modelId] = { + contextWindow, + maxTokens, + supportsPromptCache: false, + } + } } } } } - } catch (apiError) { - console.warn("Error fetching models from IBM watsonx API:", apiError) + } catch (error) { + console.warn("Error fetching models from IBM watsonx API:", error) + return {} } - return knownModels - } catch (error) { - console.error("Error fetching IBM watsonx models:", error) + } catch (apiError) { + console.error("Error fetching IBM watsonx models:", apiError) return {} } } diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 8aa90ab4b6..3ae18670ae 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -11,7 +11,7 @@ import { type ClineMessage, type TelemetrySetting, TelemetryEventName, - UserSettingsConfig, + UserSettingsConfig } from "@roo-code/types" import { CloudService } from "@roo-code/cloud" import { TelemetryService } from "@roo-code/telemetry" @@ -964,21 +964,58 @@ export const webviewMessageHandler = async ( provider.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels }) break case "requestWatsonxModels": - if (message?.values?.apiKey) { + if (message?.values) { try { - const watsonxModels = await getWatsonxModels(message.values.apiKey, message.values.projectId) - const formattedModels: Record = {} - Object.entries(watsonxModels).forEach(([modelId]) => { - formattedModels[modelId] = { - dimension: 1536, + const { + apiKey, + projectId, + platform = "ibmCloud", + baseUrl, + username, + authType = "apiKey", + password, + region, + } = message.values + + if (!apiKey && !(username && (authType === "password" ? password : apiKey))) { + console.error("Missing authentication credentials for IBM watsonx models") + provider.postMessageToWebview({ + type: "watsonxModels", + watsonxModels: {}, + }) + return + } + + let effectiveBaseUrl = baseUrl + if (platform === "ibmCloud" && region && !baseUrl) { + const regionToUrl: Record = { + "us-south": "https://us-south.ml.cloud.ibm.com", + "eu-de": "https://eu-de.ml.cloud.ibm.com", + "eu-gb": "https://eu-gb.ml.cloud.ibm.com", + "jp-tok": "https://jp-tok.ml.cloud.ibm.com", + "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", } - }) + effectiveBaseUrl = regionToUrl[region] || "https://us-south.ml.cloud.ibm.com" + } + + const watsonxModels = await getWatsonxModels( + apiKey, + false, + projectId, + effectiveBaseUrl, + platform, + username, + authType === "password" ? password : undefined, + ) + provider.postMessageToWebview({ type: "watsonxModels", - watsonxModels: formattedModels, + watsonxModels: watsonxModels, }) } catch (error) { - console.error("Failed to fetch watsonx models:", error) + console.error("Failed to fetch IBM watsonx models:", error) provider.postMessageToWebview({ type: "watsonxModels", watsonxModels: {}, @@ -986,6 +1023,66 @@ export const webviewMessageHandler = async ( } } break + case "requestEmbeddedWatsonxModels": + if (message?.values) { + try { + const { + apiKey, + projectId, + platform = "ibmCloud", + baseUrl, + username, + authType = "apiKey", + password, + region, + } = message.values + + if (!apiKey && !(username && (authType === "password" ? password : apiKey))) { + console.error("Missing authentication credentials for IBM watsonx embedded models") + provider.postMessageToWebview({ + type: "embeddedWatsonxModels", + embeddedWatsonxModels: {}, + }) + return + } + + let effectiveBaseUrl = baseUrl + if (platform === "ibmCloud" && region && !baseUrl) { + const regionToUrl: Record = { + "us-south": "https://us-south.ml.cloud.ibm.com", + "eu-de": "https://eu-de.ml.cloud.ibm.com", + "eu-gb": "https://eu-gb.ml.cloud.ibm.com", + "jp-tok": "https://jp-tok.ml.cloud.ibm.com", + "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", + } + effectiveBaseUrl = regionToUrl[region] || "https://us-south.ml.cloud.ibm.com" + } + + const watsonxModels = await getWatsonxModels( + apiKey, + true, + projectId, + effectiveBaseUrl, + platform as "ibmCloud" | "cloudPak", + username, + authType === "password" ? password : undefined, + ) + + provider.postMessageToWebview({ + type: "embeddedWatsonxModels", + embeddedWatsonxModels: watsonxModels, + }) + } catch (error) { + console.error("Failed to fetch IBM watsonx embedded models:", error) + provider.postMessageToWebview({ + type: "embeddedWatsonxModels", + embeddedWatsonxModels: {}, + }) + } + } + break case "requestHuggingFaceModels": try { const { getHuggingFaceModelsWithMetadata } = await import("../../api/providers/fetchers/huggingface") diff --git a/src/services/code-index/embedders/watsonx.ts b/src/services/code-index/embedders/watsonx.ts index a7f6deb108..e17d6e42a5 100644 --- a/src/services/code-index/embedders/watsonx.ts +++ b/src/services/code-index/embedders/watsonx.ts @@ -4,7 +4,7 @@ import { t } from "../../../i18n" import { TelemetryEventName } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { WatsonXAI } from "@ibm-cloud/watsonx-ai" -import { IamAuthenticator } from "ibm-cloud-sdk-core" +import { IamAuthenticator, CloudPakForDataAuthenticator } from "ibm-cloud-sdk-core" /** * IBM watsonx embedder implementation using the native IBM Cloud watsonx.ai package. @@ -25,21 +25,59 @@ export class WatsonxEmbedder implements IEmbedder { * @param apiKey The watsonx API key for authentication * @param modelId The model ID to use (defaults to ibm/slate-125m-english-rtrvr-v2) * @param projectId Optional IBM Cloud project ID for watsonx - * @param proxyUrl Optional proxy URL for connecting through MCP servers + * @param platform Optional platform type (ibmCloud or cloudPak) + * @param baseUrl Optional base URL for the service (required for cloudPak) + * @param region Optional region for IBM Cloud (defaults to us-south) + * @param username Optional username for Cloud Pak for Data + * @param password Optional password for Cloud Pak for Data */ - constructor(apiKey: string, modelId?: string, projectId?: string) { - if (!apiKey) { + constructor( + apiKey: string, + modelId?: string, + projectId?: string, + platform: "ibmCloud" | "cloudPak" = "ibmCloud", + baseUrl?: string, + region: string = "us-south", + username?: string, + password?: string, + ) { + if (!apiKey && !(username && password)) { throw new Error(t("embeddings:validation.apiKeyRequired")) } this.modelId = modelId || WatsonxEmbedder.DEFAULT_MODEL this.projectId = projectId - const options: any = { + let options: any = { version: WatsonxEmbedder.WATSONX_VERSION, - authenticator: new IamAuthenticator({ + } + + if (platform === "ibmCloud") { + options.authenticator = new IamAuthenticator({ apikey: apiKey, - }), - serviceUrl: `https://${WatsonxEmbedder.WATSONX_REGION}.ml.cloud.ibm.com`, + }) + options.serviceUrl = baseUrl || `https://${region}.ml.cloud.ibm.com` + } else if (platform === "cloudPak") { + if (!baseUrl) { + throw new Error("Base URL is required for IBM Cloud Pak for Data") + } + + if (username) { + if (password) { + options.authenticator = new CloudPakForDataAuthenticator({ + url: baseUrl, + username: username, + password: password, + }) + } else if (apiKey) { + options.authenticator = new CloudPakForDataAuthenticator({ + url: baseUrl, + username: username, + apikey: apiKey, + }) + } + } + + options.serviceUrl = baseUrl } this.watsonxClient = new WatsonXAI(options) diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 164da9e016..91297a7a17 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -9,10 +9,7 @@ import type { ClineMessage, MarketplaceItem, TodoItem, - CloudUserInfo, - OrganizationAllowList, - ShareVisibility, - QueuedMessage, + ModelInfo, } from "@roo-code/types" import { GitCommit } from "../utils/git" @@ -80,6 +77,7 @@ export interface ExtensionMessage { | "lmStudioModels" | "vsCodeLmModels" | "watsonxModels" + | "embeddedWatsonxModels" | "huggingFaceModels" | "vsCodeLmApiAvailable" | "updatePrompt" @@ -153,7 +151,8 @@ export interface ExtensionMessage { ollamaModels?: ModelRecord lmStudioModels?: ModelRecord vsCodeLmModels?: { vendor?: string; family?: string; version?: string; id?: string }[] - watsonxModels?: Record + watsonxModels?: Record + embeddedWatsonxModels?: Record huggingFaceModels?: Array<{ id: string object: string diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 94013c46f8..3ee20786c3 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -71,6 +71,7 @@ export interface WebviewMessage { | "requestLmStudioModels" | "requestVsCodeLmModels" | "requestWatsonxModels" + | "requestEmbeddedWatsonxModels" | "requestHuggingFaceModels" | "openImage" | "saveImage" diff --git a/webview-ui/src/components/chat/CodeIndexPopover.tsx b/webview-ui/src/components/chat/CodeIndexPopover.tsx index 8dda13fdbc..506d769925 100644 --- a/webview-ui/src/components/chat/CodeIndexPopover.tsx +++ b/webview-ui/src/components/chat/CodeIndexPopover.tsx @@ -75,6 +75,12 @@ interface LocalCodeIndexSettings { codebaseIndexVercelAiGatewayApiKey?: string codebaseIndexWatsonxApiKey?: string codebaseIndexWatsonxProjectId?: string + watsonxPlatform?: "ibmCloud" | "cloudPak" + watsonxBaseUrl?: string + watsonxRegion?: string + watsonxUsername?: string + watsonxPassword?: string + watsonxAuthType?: "apiKey" | "password" } // Validation schema for codebase index settings @@ -158,6 +164,12 @@ const createValidationSchema = (provider: EmbedderProvider, t: any) => { codebaseIndexEmbedderModelId: z .string() .min(1, t("settings:codeIndex.validation.modelSelectionRequired")), + watsonxPlatform: z.enum(["ibmCloud", "cloudPak"]).optional().default("ibmCloud"), + watsonxRegion: z.string().optional(), + watsonxBaseUrl: z.string().optional(), + watsonxUsername: z.string().optional(), + watsonxPassword: z.string().optional(), + watsonxAuthType: z.enum(["apiKey", "password"]).optional().default("apiKey"), }) default: @@ -180,6 +192,7 @@ export const CodeIndexPopover: React.FC = ({ const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved" | "error">("idle") const [saveError, setSaveError] = useState(null) + const [refreshingModels, setRefreshingModels] = useState(false) // Form validation state const [formErrors, setFormErrors] = useState>({}) @@ -207,6 +220,12 @@ export const CodeIndexPopover: React.FC = ({ codebaseIndexVercelAiGatewayApiKey: "", codebaseIndexWatsonxApiKey: "", codebaseIndexWatsonxProjectId: "", + watsonxPlatform: "ibmCloud", + watsonxBaseUrl: "https://us-south.ml.cloud.ibm.com", + watsonxRegion: "us-south", + watsonxUsername: "", + watsonxPassword: "", + watsonxAuthType: "apiKey", }) // Initial settings state - stores the settings when popover opens @@ -244,6 +263,12 @@ export const CodeIndexPopover: React.FC = ({ codebaseIndexVercelAiGatewayApiKey: "", codebaseIndexWatsonxApiKey: "", codebaseIndexWatsonxProjectId: "", + watsonxPlatform: "ibmCloud" as "ibmCloud" | "cloudPak", + watsonxBaseUrl: "https://us-south.ml.cloud.ibm.com", + watsonxRegion: "us-south", + watsonxUsername: "", + watsonxPassword: "", + watsonxAuthType: "apiKey" as "apiKey" | "password", } setInitialSettings(settings) setCurrentSettings(settings) @@ -273,27 +298,6 @@ export const CodeIndexPopover: React.FC = ({ return () => window.removeEventListener("message", handleMessage) }, [open]) - // Request WatsonX models when provider is selected and API key is available - useEffect(() => { - if ( - currentSettings.codebaseIndexEmbedderProvider === "watsonx" && - currentSettings.codebaseIndexWatsonxApiKey && - currentSettings.codebaseIndexWatsonxApiKey !== SECRET_PLACEHOLDER - ) { - vscode.postMessage({ - type: "requestWatsonxModels", - values: { - apiKey: currentSettings.codebaseIndexWatsonxApiKey, - projectId: currentSettings.codebaseIndexWatsonxProjectId, - }, - }) - } - }, [ - currentSettings.codebaseIndexEmbedderProvider, - currentSettings.codebaseIndexWatsonxApiKey, - currentSettings.codebaseIndexWatsonxProjectId, - ]) - // Use a ref to capture current settings for the save handler const currentSettingsRef = useRef(currentSettings) currentSettingsRef.current = currentSettings @@ -333,16 +337,47 @@ export const CodeIndexPopover: React.FC = ({ setSaveStatus("idle") setSaveError(null) } - } else if (event.data.type === "watsonxModels" && event.data.watsonxModels) { - // Update the extension state context with the watsonx models - // The models will be automatically available through the codebaseIndexModels context - console.log("Received WatsonX models:", event.data.watsonxModels) + } else if (event.data.type === "embeddedWatsonxModels" && event.data.embeddedWatsonxModels) { + try { + console.log("Received IBM Embeded watsonx models:", event.data.embeddedWatsonxModels) + const embeddedWatsonxModels: Record = {} + if ( + !event.data.embeddedWatsonxModels || + Object.keys(event.data.embeddedWatsonxModels).length === 0 + ) { + console.warn("No models received from server, adding default model") + embeddedWatsonxModels["ibm/slate-125m-english-rtrvr-v2"] = { dimension: 1536 } + } else { + Object.keys(event.data.embeddedWatsonxModels).forEach((modelId) => { + embeddedWatsonxModels[modelId] = { + dimension: 1536, + } + }) + } + if (codebaseIndexModels) { + codebaseIndexModels.watsonx = { ...embeddedWatsonxModels } + console.log("Updated watsonx models in context:", codebaseIndexModels.watsonx) + } + setCurrentSettings((prev) => ({ ...prev })) + } catch (error) { + console.error("Error processing watsonx models:", error) + if (codebaseIndexModels) { + codebaseIndexModels.watsonx = { + "ibm/slate-125m-english-rtrvr-v2": { dimension: 1536 }, + } + } + } finally { + setRefreshingModels(false) + } + } else if (event.data.type === "embeddedWatsonxModelsError") { + console.error("Error fetching watsonx models:", event.data.error) + setRefreshingModels(false) } } window.addEventListener("message", handleMessage) return () => window.removeEventListener("message", handleMessage) - }, [t, cwd]) + }, [t, cwd, codebaseIndexModels, currentSettings.codebaseIndexEmbedderProvider]) // Listen for secret status useEffect(() => { @@ -758,7 +793,7 @@ export const CodeIndexPopover: React.FC = ({ )} -
+
@@ -1123,28 +1158,211 @@ export const CodeIndexPopover: React.FC = ({ {currentSettings.codebaseIndexEmbedderProvider === "watsonx" && ( <> + {/* IBM watsonx Platform Selection */}
- - - updateSetting("codebaseIndexWatsonxApiKey", e.target.value) - } - placeholder={t("settings:codeIndex.watsonxApiKeyPlaceholder")} - className={cn("w-full", { - "border-red-500": formErrors.watsonxApiKey, - })} - /> - {formErrors.watsonxApiKey && ( -

- {formErrors.watsonxApiKey} -

- )} + +
+ {/* IBM Cloud specific fields */} + {(!currentSettings.watsonxPlatform || + currentSettings.watsonxPlatform === "ibmCloud") && ( + <> +
+ + + updateSetting( + "codebaseIndexWatsonxApiKey", + e.target.value, + ) + } + placeholder={t( + "settings:codeIndex.watsonxApiKeyPlaceholder", + )} + className={cn("w-full", { + "border-red-500": formErrors.watsonxApiKey, + })} + /> + {formErrors.watsonxApiKey && ( +

+ {formErrors.watsonxApiKey} +

+ )} +
+ +
+ + +
+ + )} + + {/* Cloud Pak for Data specific fields */} + {currentSettings.watsonxPlatform === "cloudPak" && ( + <> +
+ + + updateSetting("watsonxBaseUrl", e.target.value) + } + placeholder="https://your-cp4d-instance.example.com" + className="w-full" + /> +
+ +
+ + + updateSetting("watsonxUsername", e.target.value) + } + placeholder="Username" + className="w-full" + /> +
+ +
+ + +
+ + {currentSettings.watsonxAuthType === "apiKey" ? ( +
+ + + updateSetting( + "codebaseIndexWatsonxApiKey", + e.target.value, + ) + } + placeholder="API Key" + className="w-full" + /> +
+ ) : ( +
+ + + updateSetting("watsonxPassword", e.target.value) + } + placeholder="Password" + className="w-full" + /> +
+ )} + + )} + + {/* Common fields for both platforms */}
+ {/* Refresh Models Button for IBM watsonx */} +
+ { + setRefreshingModels(true) + vscode.postMessage({ + type: "requestEmbeddedWatsonxModels", + values: { + apiKey: currentSettings.codebaseIndexWatsonxApiKey, + projectId: + currentSettings.codebaseIndexWatsonxProjectId, + platform: currentSettings.watsonxPlatform, + baseUrl: currentSettings.watsonxBaseUrl, + username: currentSettings.watsonxUsername, + authType: currentSettings.watsonxAuthType, + password: currentSettings.watsonxPassword, + region: currentSettings.watsonxRegion, + }, + }) + }} + disabled={ + refreshingModels || + !currentSettings.codebaseIndexWatsonxApiKey || + currentSettings.codebaseIndexWatsonxApiKey === + SECRET_PLACEHOLDER + } + className="w-full"> +
+ {refreshingModels ? ( + + ) : ( + + )} + {refreshingModels ? "Loading Models..." : "Refresh Models"} +
+
+
+