diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 956ea3e6b2..4960f7ef03 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -25,7 +25,6 @@ import { vscodeLlmModels, xaiModels, internationalZAiModels, - watsonxAiModels, } from "./providers/index.js" /** @@ -543,11 +542,6 @@ export const MODELS_BY_PROVIDER: Record< label: "VS Code LM API", models: Object.keys(vscodeLlmModels), }, - watsonx: { - id: "watsonx", - label: "IBM watsonx", - models: Object.keys(watsonxAiModels), - }, xai: { id: "xai", label: "xAI (Grok)", models: Object.keys(xaiModels) }, zai: { id: "zai", label: "Zai", models: Object.keys(internationalZAiModels) }, @@ -558,6 +552,7 @@ export const MODELS_BY_PROVIDER: Record< openrouter: { id: "openrouter", label: "OpenRouter", models: [] }, requesty: { id: "requesty", label: "Requesty", models: [] }, unbound: { id: "unbound", label: "Unbound", models: [] }, + watsonx: { id: "watsonx", label: "IBM watsonx", models: [] }, } export const dynamicProviders = [ diff --git a/src/api/providers/fetchers/watsonx.ts b/src/api/providers/fetchers/watsonx.ts index dcf049eb29..98da1003a7 100644 --- a/src/api/providers/fetchers/watsonx.ts +++ b/src/api/providers/fetchers/watsonx.ts @@ -24,7 +24,6 @@ export async function getWatsonxModels( }), }) - await service.getAuthenticator().authenticate() let knownModels: Record = {} try { @@ -37,12 +36,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") && - !modelInfo.includes("retriev") - ) { + if (modelId && !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) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 23710cd805..89352df249 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -549,6 +549,7 @@ export const webviewMessageHandler = async ( litellm: {}, ollama: {}, lmstudio: {}, + watsonx: {}, } const safeGetModels = async (options: GetModelsOptions): Promise => { @@ -574,6 +575,14 @@ export const webviewMessageHandler = async ( }, }, { key: "glama", options: { provider: "glama" } }, + { + key: "watsonx", + options: { + provider: "watsonx", + apiKey: apiConfiguration.watsonxApiKey!, + baseUrl: apiConfiguration.watsonxBaseUrl!, + }, + }, { key: "unbound", options: { provider: "unbound", apiKey: apiConfiguration.unboundApiKey } }, ] @@ -598,6 +607,16 @@ export const webviewMessageHandler = async ( }) } + const watsonxApiKey = apiConfiguration.watsonxApiKey + const watsonxBaseUrl = apiConfiguration.watsonxBaseUrl + + if (watsonxApiKey && watsonxBaseUrl) { + modelFetchPromises.push({ + key: "watsonx", + options: { provider: "watsonx", apiKey: watsonxApiKey, baseUrl: watsonxBaseUrl }, + }) + } + const results = await Promise.allSettled( modelFetchPromises.map(async ({ key, options }) => { const models = await safeGetModels(options) diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 158c6000c6..e0366db203 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -226,6 +226,8 @@ const ApiOptions = ({ vscode.postMessage({ type: "requestVsCodeLmModels" }) } else if (selectedProvider === "litellm") { vscode.postMessage({ type: "requestRouterModels" }) + } else if (selectedProvider === "watsonx") { + vscode.postMessage({ type: "requestWatsonxModels" }) } }, 250, @@ -238,6 +240,9 @@ const ApiOptions = ({ apiConfiguration?.lmStudioBaseUrl, apiConfiguration?.litellmBaseUrl, apiConfiguration?.litellmApiKey, + apiConfiguration.watsonxApiKey, + apiConfiguration.watsonxProjectId, + apiConfiguration.watsonxBaseUrl, customHeaders, ], ) @@ -340,7 +345,7 @@ const ApiOptions = ({ openai: { field: "openAiModelId" }, ollama: { field: "ollamaModelId" }, lmstudio: { field: "lmStudioModelId" }, - watsonx: { field: "apiModelId", default: watsonxAiDefaultModelId }, + watsonx: { field: "watsonxModelId", default: watsonxAiDefaultModelId }, } const config = PROVIDER_MODEL_CONFIG[value] @@ -594,7 +599,12 @@ const ApiOptions = ({ )} {selectedProvider === "watsonx" && ( - + )} {selectedProvider === "sambanova" && ( @@ -652,7 +662,7 @@ const ApiOptions = ({ )} - {selectedProviderModels.length > 0 && ( + {selectedProviderModels.length > 0 && selectedProvider !== "watsonx" && ( <>
diff --git a/webview-ui/src/components/settings/ModelPicker.tsx b/webview-ui/src/components/settings/ModelPicker.tsx index 0753f9fc2b..dc051d9f35 100644 --- a/webview-ui/src/components/settings/ModelPicker.tsx +++ b/webview-ui/src/components/settings/ModelPicker.tsx @@ -37,6 +37,7 @@ type ModelIdKey = keyof Pick< | "openAiModelId" | "litellmModelId" | "ioIntelligenceModelId" + | "watsonxModelId" > interface ModelPickerProps { diff --git a/webview-ui/src/components/settings/providers/WatsonxAI.tsx b/webview-ui/src/components/settings/providers/WatsonxAI.tsx index 5c44223c77..2258e698cc 100644 --- a/webview-ui/src/components/settings/providers/WatsonxAI.tsx +++ b/webview-ui/src/components/settings/providers/WatsonxAI.tsx @@ -1,21 +1,112 @@ -import { useCallback } from "react" +import { useCallback, useState, useEffect, useRef } from "react" import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" -import type { ProviderSettings } from "@roo-code/types" -import { watsonxAiDefaultModelId, watsonxAiModels } from "@roo-code/types" +import { 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" + +// 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)", + "eu-gb": "London (eu-gb.ml.cloud.ibm.com)", + "jp-tok": "Tokyo (jp-tok.ml.cloud.ibm.com)", + "au-syd": "Sydney (au-syd.ml.cloud.ibm.com)", + "ca-tor": "Toronto (ca-tor.ml.cloud.ibm.com)", + "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", + "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", + custom: "", // For custom URL input +} type WatsonxAIProps = { apiConfiguration: ProviderSettings - setApiConfigurationField: (field: K, value: ProviderSettings[K]) => void + setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void + organizationAllowList: OrganizationAllowList + modelValidationError?: string } -export const WatsonxAI = ({ apiConfiguration, setApiConfigurationField }: WatsonxAIProps) => { +export const WatsonxAI = ({ + apiConfiguration, + setApiConfigurationField, + organizationAllowList, + modelValidationError, +}: WatsonxAIProps) => { const { t } = useAppTranslation() + const { routerModels } = useExtensionState() + const [refreshStatus, setRefreshStatus] = useState<"idle" | "loading" | "success" | "error">("idle") + const [refreshError, setRefreshError] = useState() + const watsonxErrorJustReceived = useRef(false) + + // Determine the current region based on the base URL + 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], + ) + + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + const message = event.data + if (message.type === "singleRouterModelFetchResponse" && !message.success) { + const providerName = message.values?.provider as RouterName + if (providerName === "watsonx") { + watsonxErrorJustReceived.current = true + setRefreshStatus("error") + 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") + } + } + } + } + + window.addEventListener("message", handleMessage) + return () => { + window.removeEventListener("message", handleMessage) + } + }, [refreshStatus, refreshError, t]) const handleInputChange = useCallback( (field: keyof ProviderSettings, transform: (event: E) => any = inputEventTransform) => @@ -25,12 +116,29 @@ export const WatsonxAI = ({ apiConfiguration, setApiConfigurationField }: Watson [setApiConfigurationField], ) - const defaultModel = watsonxAiDefaultModelId - const modelInfo = watsonxAiModels[defaultModel] || {} - const defaultModelDescription = - typeof modelInfo === "object" && "contextWindow" in modelInfo - ? `Context window: ${modelInfo.contextWindow} tokens` - : "IBM watsonx model" + const handleRefreshModels = useCallback(() => { + setRefreshStatus("loading") + setRefreshError(undefined) + + const apiKey = apiConfiguration.watsonxApiKey + const projectId = apiConfiguration.watsonxProjectId + const baseUrl = REGION_TO_URL[selectedRegion as keyof typeof REGION_TO_URL] + + if (!apiKey) { + setRefreshStatus("error") + setRefreshError(t("settings:providers.refreshModels.missingConfig")) + return + } + + vscode.postMessage({ + type: "requestRouterModels", + values: { + watsonxApiKey: apiKey, + watsonxProjectId: projectId, + watsonxBaseUrl: baseUrl, + }, + }) + }, [apiConfiguration, setRefreshStatus, setRefreshError, t, selectedRegion]) return ( <> @@ -62,25 +170,66 @@ export const WatsonxAI = ({ apiConfiguration, setApiConfigurationField }: Watson Project ID is required for IBM watsonx integration
- - - -
- Default: https://us-south.ml.cloud.ibm.com -

Default Model Information

-
-
- Model ID: {defaultModel} -
-
- Description: {defaultModelDescription} -
+
+ + +
+ Selected endpoint: {REGION_TO_URL[selectedRegion as keyof typeof REGION_TO_URL]}
+ + + {refreshStatus === "loading" && ( +
+ {t("settings:providers.refreshModels.loading") || "Loading models..."} +
+ )} + {refreshStatus === "success" && ( +
+ {t("settings:providers.refreshModels.success") || "Models refreshed successfully"} +
+ )} + {refreshStatus === "error" && ( +
+ {refreshError || t("settings:providers.refreshModels.error") || "Failed to refresh models"} +
+ )} + + ) } diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 87548008d5..5588a1c1c8 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -36,8 +36,6 @@ import { litellmDefaultModelId, claudeCodeDefaultModelId, claudeCodeModels, - watsonxAiModels, - watsonxAiDefaultModelId, sambaNovaModels, sambaNovaDefaultModelId, doubaoModels, @@ -57,6 +55,8 @@ import { qwenCodeDefaultModelId, qwenCodeModels, BEDROCK_CLAUDE_SONNET_4_MODEL_ID, + watsonxAiDefaultModelId, + watsonxAiModels, } from "@roo-code/types" import type { ModelRecord, RouterModels } from "@roo/api" @@ -71,6 +71,7 @@ export const useSelectedModel = (apiConfiguration?: ProviderSettings) => { const lmStudioModelId = provider === "lmstudio" ? apiConfiguration?.lmStudioModelId : undefined const routerModels = useRouterModels() + const openRouterModelProviders = useOpenRouterModelProviders(openRouterModelId) const lmStudioModels = useLmStudioModels(lmStudioModelId) @@ -284,11 +285,11 @@ function getSelectedModel({ return { id, info: { ...openAiModelInfoSaneDefaults, ...info } } } case "watsonx": { - const id = apiConfiguration.apiModelId ?? watsonxAiDefaultModelId + const id = apiConfiguration.watsonxModelId ?? watsonxAiDefaultModelId const info = watsonxAiModels[id as keyof typeof watsonxAiModels] return { id, - info: info || undefined, + info: info, } } case "cerebras": { diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index fedd3c9df2..6a3ca214d6 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -212,6 +212,8 @@ function getModelIdForProvider(apiConfiguration: ProviderSettings, provider: str return apiConfiguration.huggingFaceModelId case "io-intelligence": return apiConfiguration.ioIntelligenceModelId + case "watsonx": + return apiConfiguration.watsonxModelId default: return apiConfiguration.apiModelId } @@ -282,6 +284,9 @@ export function validateModelId(apiConfiguration: ProviderSettings, routerModels case "litellm": modelId = apiConfiguration.litellmModelId break + case "watsonx": + modelId = apiConfiguration.watsonxModelId + break case "io-intelligence": modelId = apiConfiguration.ioIntelligenceModelId break