add IBM cloud pak for data support for embedded watsonx models for code indexing..

This commit is contained in:
Prasang Prajapati 2025-08-27 17:09:50 -04:00
parent 0e44af6836
commit a190c73cdf
9 changed files with 583 additions and 108 deletions

View file

@ -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,

View file

@ -92,7 +92,7 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
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

View file

@ -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<Record<string, ModelInfo>> {
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<string, ModelInfo> = {}
@ -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 {}
}
}

View file

@ -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<string, { dimension: number }> = {}
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<string, string> = {
"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<string, string> = {
"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")

View file

@ -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)

View file

@ -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<string, { dimension: number }>
watsonxModels?: Record<string, ModelInfo>
embeddedWatsonxModels?: Record<string, ModelInfo>
huggingFaceModels?: Array<{
id: string
object: string

View file

@ -71,6 +71,7 @@ export interface WebviewMessage {
| "requestLmStudioModels"
| "requestVsCodeLmModels"
| "requestWatsonxModels"
| "requestEmbeddedWatsonxModels"
| "requestHuggingFaceModels"
| "openImage"
| "saveImage"

View file

@ -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<CodeIndexPopoverProps> = ({
const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved" | "error">("idle")
const [saveError, setSaveError] = useState<string | null>(null)
const [refreshingModels, setRefreshingModels] = useState(false)
// Form validation state
const [formErrors, setFormErrors] = useState<Record<string, string>>({})
@ -207,6 +220,12 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
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<CodeIndexPopoverProps> = ({
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<CodeIndexPopoverProps> = ({
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<CodeIndexPopoverProps> = ({
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<string, { dimension: number }> = {}
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<CodeIndexPopoverProps> = ({
)}
</div>
<div className="space-y-2">
<div className="space-y-2 mt-4">
<label className="text-sm font-medium">
{t("settings:codeIndex.modelLabel")}
</label>
@ -1123,28 +1158,211 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
{currentSettings.codebaseIndexEmbedderProvider === "watsonx" && (
<>
{/* IBM watsonx Platform Selection */}
<div className="space-y-2">
<label className="text-sm font-medium">
{t("settings:codeIndex.watsonxApiKeyLabel")}
</label>
<VSCodeTextField
type="password"
value={currentSettings.codebaseIndexWatsonxApiKey || ""}
onInput={(e: any) =>
updateSetting("codebaseIndexWatsonxApiKey", e.target.value)
}
placeholder={t("settings:codeIndex.watsonxApiKeyPlaceholder")}
className={cn("w-full", {
"border-red-500": formErrors.watsonxApiKey,
})}
/>
{formErrors.watsonxApiKey && (
<p className="text-xs text-vscode-errorForeground mt-1 mb-0">
{formErrors.watsonxApiKey}
</p>
)}
<label className="text-sm font-medium">IBM watsonx Platform</label>
<Select
value={currentSettings.watsonxPlatform || "ibmCloud"}
onValueChange={(value) => {
updateSetting("watsonxPlatform", value)
if (value === "ibmCloud") {
// Set IBM Cloud defaults
updateSetting("watsonxRegion", "us-south")
updateSetting(
"watsonxBaseUrl",
"https://us-south.ml.cloud.ibm.com",
)
updateSetting("watsonxUsername", "")
updateSetting("watsonxPassword", "")
updateSetting("watsonxAuthType", "apiKey")
} else {
// Set Cloud Pak for Data defaults
updateSetting("watsonxRegion", "")
updateSetting("watsonxBaseUrl", "")
updateSetting("watsonxAuthType", "apiKey")
}
}}>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="ibmCloud">IBM Cloud</SelectItem>
<SelectItem value="cloudPak">IBM Cloud Pak for Data</SelectItem>
</SelectContent>
</Select>
</div>
{/* IBM Cloud specific fields */}
{(!currentSettings.watsonxPlatform ||
currentSettings.watsonxPlatform === "ibmCloud") && (
<>
<div className="space-y-2">
<label className="text-sm font-medium">
{t("settings:codeIndex.watsonxApiKeyLabel")}
</label>
<VSCodeTextField
type="password"
value={currentSettings.codebaseIndexWatsonxApiKey || ""}
onInput={(e: any) =>
updateSetting(
"codebaseIndexWatsonxApiKey",
e.target.value,
)
}
placeholder={t(
"settings:codeIndex.watsonxApiKeyPlaceholder",
)}
className={cn("w-full", {
"border-red-500": formErrors.watsonxApiKey,
})}
/>
{formErrors.watsonxApiKey && (
<p className="text-xs text-vscode-errorForeground mt-1 mb-0">
{formErrors.watsonxApiKey}
</p>
)}
</div>
<div className="space-y-2">
<label className="text-sm font-medium">
IBM watsonx Region
</label>
<Select
value={currentSettings.watsonxRegion || "us-south"}
onValueChange={(region: string) => {
updateSetting("watsonxRegion", region)
const regionToUrl: Record<string, string> = {
"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",
}
updateSetting(
"watsonxBaseUrl",
regionToUrl[region] || "",
)
}}>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="us-south">
Dallas (us-south.ml.cloud.ibm.com)
</SelectItem>
<SelectItem value="eu-de">
Frankfurt (eu-de.ml.cloud.ibm.com)
</SelectItem>
<SelectItem value="eu-gb">
London (eu-gb.ml.cloud.ibm.com)
</SelectItem>
<SelectItem value="jp-tok">
Tokyo (jp-tok.ml.cloud.ibm.com)
</SelectItem>
<SelectItem value="au-syd">
Sydney (au-syd.ml.cloud.ibm.com)
</SelectItem>
<SelectItem value="ca-tor">
Toronto (ca-tor.ml.cloud.ibm.com)
</SelectItem>
<SelectItem value="ap-south-1">
Mumbai (ap-south-1.aws.wxai.ibm.com)
</SelectItem>
</SelectContent>
</Select>
</div>
</>
)}
{/* Cloud Pak for Data specific fields */}
{currentSettings.watsonxPlatform === "cloudPak" && (
<>
<div className="space-y-2">
<label className="text-sm font-medium">
IBM Cloud Pak for Data URL
</label>
<VSCodeTextField
value={currentSettings.watsonxBaseUrl || ""}
onInput={(e: any) =>
updateSetting("watsonxBaseUrl", e.target.value)
}
placeholder="https://your-cp4d-instance.example.com"
className="w-full"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Username</label>
<VSCodeTextField
value={currentSettings.watsonxUsername || ""}
onInput={(e: any) =>
updateSetting("watsonxUsername", e.target.value)
}
placeholder="Username"
className="w-full"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">
Authentication Type
</label>
<Select
value={currentSettings.watsonxAuthType || "apiKey"}
onValueChange={(authType) => {
updateSetting("watsonxAuthType", authType)
if (authType === "apiKey") {
updateSetting("watsonxPassword", "")
} else {
updateSetting("codebaseIndexWatsonxApiKey", "")
}
}}>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="apiKey">API Key</SelectItem>
<SelectItem value="password">Password</SelectItem>
</SelectContent>
</Select>
</div>
{currentSettings.watsonxAuthType === "apiKey" ? (
<div className="space-y-2">
<label className="text-sm font-medium">API Key</label>
<VSCodeTextField
type="password"
value={currentSettings.codebaseIndexWatsonxApiKey || ""}
onInput={(e: any) =>
updateSetting(
"codebaseIndexWatsonxApiKey",
e.target.value,
)
}
placeholder="API Key"
className="w-full"
/>
</div>
) : (
<div className="space-y-2">
<label className="text-sm font-medium">Password</label>
<VSCodeTextField
type="password"
value={currentSettings.watsonxPassword || ""}
onInput={(e: any) =>
updateSetting("watsonxPassword", e.target.value)
}
placeholder="Password"
className="w-full"
/>
</div>
)}
</>
)}
{/* Common fields for both platforms */}
<div className="space-y-2">
<label className="text-sm font-medium">
{t("settings:codeIndex.watsonxProjectIdLabel") || "Project ID"}
@ -1156,7 +1374,7 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
}
placeholder={
t("settings:codeIndex.watsonxProjectIdPlaceholder") ||
"Optional IBM Cloud project ID"
"IBM Cloud project ID"
}
className={cn("w-full", {
"border-red-500": formErrors.watsonxProjectId,
@ -1169,6 +1387,45 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
)}
</div>
{/* Refresh Models Button for IBM watsonx */}
<div className="space-y-2 mt-4">
<VSCodeButton
appearance="secondary"
onClick={() => {
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">
<div className="flex items-center gap-2">
{refreshingModels ? (
<span className="codicon codicon-loading codicon-modifier-spin" />
) : (
<span className="codicon codicon-refresh" />
)}
{refreshingModels ? "Loading Models..." : "Refresh Models"}
</div>
</VSCodeButton>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">
{t("settings:codeIndex.modelLabel")}

View file

@ -1,17 +1,13 @@
import { useCallback, useState, useEffect, useRef } from "react"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { watsonxAiDefaultModelId, type ProviderSettings } from "@roo-code/types"
import { ModelInfo, watsonxAiDefaultModelId, type ProviderSettings } from "@roo-code/types"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { vscode } from "@src/utils/vscode"
import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui"
import { ExtensionMessage } from "@roo/ExtensionMessage"
import { inputEventTransform } from "../transforms"
import { OrganizationAllowList } from "@roo/cloud"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { RouterName } from "@roo/api"
import { ModelPicker } from "../ModelPicker"
@ -50,10 +46,11 @@ export const WatsonxAI = ({
modelValidationError,
}: WatsonxAIProps) => {
const { t } = useAppTranslation()
const { routerModels } = useExtensionState()
const [watsonxModels, setWatsonxModels] = useState<Record<string, ModelInfo> | null>(null)
const [refreshStatus, setRefreshStatus] = useState<"idle" | "loading" | "success" | "error">("idle")
const [refreshError, setRefreshError] = useState<string | undefined>()
const watsonxErrorJustReceived = useRef(false)
const initialModelFetchAttempted = useRef(false)
useEffect(() => {
if (!apiConfiguration.watsonxPlatform) {
@ -116,17 +113,27 @@ export const WatsonxAI = ({
useEffect(() => {
const handleMessage = (event: MessageEvent<ExtensionMessage>) => {
const message = event.data
console.log("Received message:", message.type, message)
if (message.type === "singleRouterModelFetchResponse" && !message.success) {
const providerName = message.values?.provider as RouterName
if (providerName === "watsonx") {
console.log("Received error response for watsonx:", message.error)
watsonxErrorJustReceived.current = true
setRefreshStatus("error")
setRefreshError(message.error)
}
} else if (message.type === "routerModels") {
} else if (message.type === "watsonxModels") {
console.log("Received watsonxModels:", message.watsonxModels)
setWatsonxModels(message.watsonxModels ?? {})
if (refreshStatus === "loading") {
if (!watsonxErrorJustReceived.current) {
console.log("Setting refresh status to success")
setRefreshStatus("success")
} else {
// Reset the flag after handling the error
console.log("Resetting error flag")
watsonxErrorJustReceived.current = false
}
}
}
@ -147,24 +154,36 @@ export const WatsonxAI = ({
)
const handleRefreshModels = useCallback(() => {
console.log("Refresh models clicked")
setRefreshStatus("loading")
setRefreshError(undefined)
watsonxErrorJustReceived.current = false
const apiKey = apiConfiguration.watsonxApiKey
const platform = apiConfiguration.watsonxPlatform
const customUrl = apiConfiguration.watsonxBaseUrl || ""
const username = apiConfiguration.watsonxUsername
const authType = apiConfiguration.watsonxAuthType
const password = apiConfiguration.watsonxPassword
const projectId = apiConfiguration.watsonxProjectId
let baseUrl = ""
if (platform === "ibmCloud") {
baseUrl = REGION_TO_URL[selectedRegion as keyof typeof REGION_TO_URL]
} else {
baseUrl = customUrl
setApiConfigurationField("watsonxBaseUrl", baseUrl)
baseUrl = apiConfiguration.watsonxBaseUrl || ""
}
console.log("Refresh models config:", {
platform,
baseUrl,
hasApiKey: !!apiKey,
hasUsername: !!username,
authType,
hasPassword: !!password,
hasProjectId: !!projectId,
selectedRegion,
})
if (platform === "ibmCloud" && (!apiKey || !baseUrl)) {
setRefreshStatus("error")
setRefreshError(t("settings:providers.refreshModels.missingConfig"))
@ -197,21 +216,33 @@ export const WatsonxAI = ({
}
}
console.log("Sending requestWatsonxModels message")
vscode.postMessage({
type: "requestRouterModels",
type: "requestWatsonxModels",
values: {
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,
apiKey: apiKey,
projectId: projectId,
platform: platform,
baseUrl: baseUrl,
username: username,
authType: authType,
password: password,
region: selectedRegion,
},
})
}, [apiConfiguration, setRefreshStatus, setRefreshError, t, selectedRegion, setApiConfigurationField])
}, [apiConfiguration, setRefreshStatus, setRefreshError, t, selectedRegion])
// Refresh models when component mounts if API key is available
useEffect(() => {
if (
!initialModelFetchAttempted.current &&
apiConfiguration.watsonxApiKey &&
(!watsonxModels || Object.keys(watsonxModels).length === 0)
) {
initialModelFetchAttempted.current = true
handleRefreshModels()
}
}, [apiConfiguration.watsonxApiKey, watsonxModels, handleRefreshModels])
return (
<>
@ -348,7 +379,10 @@ export const WatsonxAI = ({
<Button
variant="outline"
onClick={handleRefreshModels}
onClick={() => {
console.log("Refresh button clicked")
handleRefreshModels()
}}
disabled={
refreshStatus === "loading" ||
(apiConfiguration.watsonxPlatform === "ibmCloud" && !apiConfiguration.watsonxApiKey) ||
@ -358,7 +392,8 @@ export const WatsonxAI = ({
(apiConfiguration.watsonxAuthType === "apiKey" && !apiConfiguration.watsonxApiKey) ||
(apiConfiguration.watsonxAuthType === "password" && !apiConfiguration.watsonxPassword)))
}
className="w-full mt-4">
className="w-full mt-4"
title={t("settings:providers.refreshModels.tooltip") || "Refresh available models"}>
<div className="flex items-center gap-2">
{refreshStatus === "loading" ? (
<span className="codicon codicon-loading codicon-modifier-spin" />
@ -387,7 +422,7 @@ export const WatsonxAI = ({
<ModelPicker
apiConfiguration={apiConfiguration}
defaultModelId={watsonxAiDefaultModelId}
models={routerModels?.watsonx ?? {}}
models={watsonxModels && Object.keys(watsonxModels).length > 0 ? watsonxModels : {}}
modelIdKey="watsonxModelId"
serviceName="IBM watsonx"
serviceUrl="https://cloud.ibm.com/apidocs/watsonx-ai#list-foundation-model-specs"