mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
feat: add IBM watsonx AI provider support
- Add watsonx to provider names and schemas - Create watsonx model definitions with IBM Granite models - Implement WatsonxHandler for API integration - Update UI components to support watsonx provider - Add watsonx to MODELS_BY_PROVIDER configuration Addresses #8087
This commit is contained in:
parent
c837025d48
commit
c69452c883
7 changed files with 355 additions and 1 deletions
|
|
@ -25,6 +25,7 @@ import {
|
|||
vscodeLlmModels,
|
||||
xaiModels,
|
||||
internationalZAiModels,
|
||||
watsonxModels,
|
||||
} from "./providers/index.js"
|
||||
|
||||
/**
|
||||
|
|
@ -68,6 +69,7 @@ export const providerNames = [
|
|||
"io-intelligence",
|
||||
"roo",
|
||||
"vercel-ai-gateway",
|
||||
"watsonx",
|
||||
] as const
|
||||
|
||||
export const providerNamesSchema = z.enum(providerNames)
|
||||
|
|
@ -343,6 +345,13 @@ const vercelAiGatewaySchema = baseProviderSettingsSchema.extend({
|
|||
vercelAiGatewayModelId: z.string().optional(),
|
||||
})
|
||||
|
||||
const watsonxSchema = apiModelIdProviderModelSchema.extend({
|
||||
watsonxApiKey: z.string().optional(),
|
||||
watsonxProjectId: z.string().optional(),
|
||||
watsonxBaseUrl: z.string().optional(),
|
||||
watsonxRegion: z.string().optional(),
|
||||
})
|
||||
|
||||
const defaultSchema = z.object({
|
||||
apiProvider: z.undefined(),
|
||||
})
|
||||
|
|
@ -384,6 +393,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
|
|||
qwenCodeSchema.merge(z.object({ apiProvider: z.literal("qwen-code") })),
|
||||
rooSchema.merge(z.object({ apiProvider: z.literal("roo") })),
|
||||
vercelAiGatewaySchema.merge(z.object({ apiProvider: z.literal("vercel-ai-gateway") })),
|
||||
watsonxSchema.merge(z.object({ apiProvider: z.literal("watsonx") })),
|
||||
defaultSchema,
|
||||
])
|
||||
|
||||
|
|
@ -425,6 +435,7 @@ export const providerSettingsSchema = z.object({
|
|||
...qwenCodeSchema.shape,
|
||||
...rooSchema.shape,
|
||||
...vercelAiGatewaySchema.shape,
|
||||
...watsonxSchema.shape,
|
||||
...codebaseIndexProviderSchema.shape,
|
||||
})
|
||||
|
||||
|
|
@ -578,6 +589,7 @@ export const MODELS_BY_PROVIDER: Record<
|
|||
unbound: { id: "unbound", label: "Unbound", models: [] },
|
||||
deepinfra: { id: "deepinfra", label: "DeepInfra", models: [] },
|
||||
"vercel-ai-gateway": { id: "vercel-ai-gateway", label: "Vercel AI Gateway", models: [] },
|
||||
watsonx: { id: "watsonx", label: "IBM watsonx", models: Object.keys(watsonxModels) },
|
||||
}
|
||||
|
||||
export const dynamicProviders = [
|
||||
|
|
@ -589,6 +601,7 @@ export const dynamicProviders = [
|
|||
"unbound",
|
||||
"deepinfra",
|
||||
"vercel-ai-gateway",
|
||||
"watsonx",
|
||||
] as const satisfies readonly ProviderName[]
|
||||
|
||||
export type DynamicProvider = (typeof dynamicProviders)[number]
|
||||
|
|
|
|||
|
|
@ -30,3 +30,4 @@ export * from "./xai.js"
|
|||
export * from "./vercel-ai-gateway.js"
|
||||
export * from "./zai.js"
|
||||
export * from "./deepinfra.js"
|
||||
export * from "./watsonx.js"
|
||||
|
|
|
|||
148
packages/types/src/providers/watsonx.ts
Normal file
148
packages/types/src/providers/watsonx.ts
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import type { ModelInfo } from "../model.js"
|
||||
|
||||
// IBM watsonx.ai models
|
||||
// https://www.ibm.com/products/watsonx-ai
|
||||
export type WatsonxModelId = keyof typeof watsonxModels
|
||||
|
||||
export const watsonxDefaultModelId: WatsonxModelId = "ibm/granite-3-8b-instruct"
|
||||
|
||||
export const watsonxModels = {
|
||||
// Granite models - IBM's foundation models
|
||||
"ibm/granite-3-8b-instruct": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 8192,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0002,
|
||||
outputPrice: 0.0006,
|
||||
description: "IBM Granite 3.0 8B Instruct - Optimized for enterprise tasks",
|
||||
},
|
||||
"ibm/granite-3-2b-instruct": {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 4096,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0001,
|
||||
outputPrice: 0.0003,
|
||||
description: "IBM Granite 3.0 2B Instruct - Lightweight model for simple tasks",
|
||||
},
|
||||
"ibm/granite-20b-multilingual": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 8192,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0006,
|
||||
outputPrice: 0.0018,
|
||||
description: "IBM Granite 20B Multilingual - Supports multiple languages",
|
||||
},
|
||||
"ibm/granite-13b-chat-v2": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 8192,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0004,
|
||||
outputPrice: 0.0012,
|
||||
description: "IBM Granite 13B Chat v2 - Optimized for conversational AI",
|
||||
},
|
||||
"ibm/granite-13b-instruct-v2": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 8192,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0004,
|
||||
outputPrice: 0.0012,
|
||||
description: "IBM Granite 13B Instruct v2 - General purpose instruction following",
|
||||
},
|
||||
"ibm/granite-7b-lab": {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 4096,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0002,
|
||||
outputPrice: 0.0006,
|
||||
description: "IBM Granite 7B Lab - Experimental model for research",
|
||||
},
|
||||
// Granite Code models - specialized for code generation
|
||||
"ibm/granite-34b-code-instruct": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 8192,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.001,
|
||||
outputPrice: 0.003,
|
||||
description: "IBM Granite 34B Code Instruct - Specialized for code generation and understanding",
|
||||
},
|
||||
"ibm/granite-20b-code-instruct": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 8192,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0006,
|
||||
outputPrice: 0.0018,
|
||||
description: "IBM Granite 20B Code Instruct - Code generation model",
|
||||
},
|
||||
"ibm/granite-8b-code-instruct": {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 4096,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0002,
|
||||
outputPrice: 0.0006,
|
||||
description: "IBM Granite 8B Code Instruct - Lightweight code model",
|
||||
},
|
||||
"ibm/granite-3b-code-instruct": {
|
||||
maxTokens: 2048,
|
||||
contextWindow: 2048,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0001,
|
||||
outputPrice: 0.0003,
|
||||
description: "IBM Granite 3B Code Instruct - Fast code completion",
|
||||
},
|
||||
// Third-party models available on watsonx
|
||||
"meta-llama/llama-3-70b-instruct": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 8192,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0029,
|
||||
outputPrice: 0.0087,
|
||||
description: "Meta Llama 3 70B Instruct on watsonx",
|
||||
},
|
||||
"meta-llama/llama-3-8b-instruct": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 8192,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0002,
|
||||
outputPrice: 0.0006,
|
||||
description: "Meta Llama 3 8B Instruct on watsonx",
|
||||
},
|
||||
"mistralai/mixtral-8x7b-instruct-v01": {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 32768,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0005,
|
||||
outputPrice: 0.0015,
|
||||
description: "Mistral Mixtral 8x7B Instruct on watsonx",
|
||||
},
|
||||
"mistralai/mistral-large": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 32768,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.003,
|
||||
outputPrice: 0.009,
|
||||
description: "Mistral Large on watsonx",
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
export const watsonxModelInfoSaneDefaults: ModelInfo = {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 8192,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
}
|
||||
|
|
@ -40,6 +40,7 @@ import {
|
|||
FeatherlessHandler,
|
||||
VercelAiGatewayHandler,
|
||||
DeepInfraHandler,
|
||||
WatsonxHandler,
|
||||
} from "./providers"
|
||||
import { NativeOllamaHandler } from "./providers/native-ollama"
|
||||
|
||||
|
|
@ -165,6 +166,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
|
|||
return new FeatherlessHandler(options)
|
||||
case "vercel-ai-gateway":
|
||||
return new VercelAiGatewayHandler(options)
|
||||
case "watsonx":
|
||||
return new WatsonxHandler(options)
|
||||
default:
|
||||
apiProvider satisfies "gemini-cli" | undefined
|
||||
return new AnthropicHandler(options)
|
||||
|
|
|
|||
|
|
@ -34,3 +34,4 @@ export { RooHandler } from "./roo"
|
|||
export { FeatherlessHandler } from "./featherless"
|
||||
export { VercelAiGatewayHandler } from "./vercel-ai-gateway"
|
||||
export { DeepInfraHandler } from "./deepinfra"
|
||||
export { WatsonxHandler } from "./watsonx"
|
||||
|
|
|
|||
181
src/api/providers/watsonx.ts
Normal file
181
src/api/providers/watsonx.ts
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
|
||||
import {
|
||||
type ModelInfo,
|
||||
type WatsonxModelId,
|
||||
watsonxDefaultModelId,
|
||||
watsonxModels,
|
||||
watsonxModelInfoSaneDefaults,
|
||||
} from "@roo-code/types"
|
||||
|
||||
import type { ApiHandlerOptions } from "../../shared/api"
|
||||
|
||||
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { getModelParams } from "../transform/model-params"
|
||||
|
||||
import { DEFAULT_HEADERS } from "./constants"
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import { getApiRequestTimeout } from "./utils/timeout-config"
|
||||
import { handleOpenAIError } from "./utils/openai-error-handler"
|
||||
|
||||
export class WatsonxHandler extends BaseProvider implements SingleCompletionHandler {
|
||||
protected options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
private readonly providerName = "IBM watsonx"
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
super()
|
||||
this.options = options
|
||||
|
||||
// Construct the base URL for watsonx API
|
||||
// Default to US South region if not specified
|
||||
const region = this.options.watsonxRegion || "us-south"
|
||||
const baseURL = this.options.watsonxBaseUrl || `https://${region}.ml.cloud.ibm.com/ml/v1`
|
||||
const apiKey = this.options.watsonxApiKey || "not-provided"
|
||||
|
||||
const headers = {
|
||||
...DEFAULT_HEADERS,
|
||||
"X-Watson-Project-Id": this.options.watsonxProjectId || "",
|
||||
}
|
||||
|
||||
const timeout = getApiRequestTimeout()
|
||||
|
||||
this.client = new OpenAI({
|
||||
baseURL,
|
||||
apiKey,
|
||||
defaultHeaders: headers,
|
||||
timeout,
|
||||
})
|
||||
}
|
||||
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const { id: modelId, info: modelInfo } = this.getModel()
|
||||
|
||||
// Combine system prompt with messages for watsonx format
|
||||
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
|
||||
role: "system",
|
||||
content: systemPrompt,
|
||||
}
|
||||
|
||||
const convertedMessages = [systemMessage, ...convertToOpenAiMessages(messages)]
|
||||
|
||||
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
|
||||
model: modelId,
|
||||
temperature: this.options.modelTemperature ?? 0.7,
|
||||
messages: convertedMessages,
|
||||
stream: true as const,
|
||||
stream_options: { include_usage: true },
|
||||
}
|
||||
|
||||
// Add max_tokens if needed
|
||||
if (this.options.includeMaxTokens === true) {
|
||||
requestOptions.max_tokens = this.options.modelMaxTokens || modelInfo.maxTokens
|
||||
}
|
||||
|
||||
let stream
|
||||
try {
|
||||
stream = await this.client.chat.completions.create(requestOptions)
|
||||
} catch (error) {
|
||||
throw handleOpenAIError(error, this.providerName)
|
||||
}
|
||||
|
||||
let lastUsage
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta ?? {}
|
||||
|
||||
if (delta.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
lastUsage = chunk.usage
|
||||
}
|
||||
}
|
||||
|
||||
if (lastUsage) {
|
||||
yield this.processUsageMetrics(lastUsage, modelInfo)
|
||||
}
|
||||
}
|
||||
|
||||
protected processUsageMetrics(usage: any, modelInfo?: ModelInfo): ApiStreamUsageChunk {
|
||||
return {
|
||||
type: "usage",
|
||||
inputTokens: usage?.prompt_tokens || 0,
|
||||
outputTokens: usage?.completion_tokens || 0,
|
||||
}
|
||||
}
|
||||
|
||||
override getModel() {
|
||||
const modelId = this.options.apiModelId
|
||||
let id = modelId && modelId in watsonxModels ? (modelId as WatsonxModelId) : watsonxDefaultModelId
|
||||
let info: ModelInfo = watsonxModels[id]
|
||||
const params = getModelParams({ format: "openai", modelId: id, model: info, settings: this.options })
|
||||
return { id, info, ...params }
|
||||
}
|
||||
|
||||
async completePrompt(prompt: string): Promise<string> {
|
||||
try {
|
||||
const model = this.getModel()
|
||||
const modelInfo = model.info
|
||||
|
||||
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = {
|
||||
model: model.id,
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
}
|
||||
|
||||
// Add max_tokens if needed
|
||||
if (this.options.includeMaxTokens === true) {
|
||||
requestOptions.max_tokens = this.options.modelMaxTokens || modelInfo.maxTokens
|
||||
}
|
||||
|
||||
let response
|
||||
try {
|
||||
response = await this.client.chat.completions.create(requestOptions)
|
||||
} catch (error) {
|
||||
throw handleOpenAIError(error, this.providerName)
|
||||
}
|
||||
|
||||
return response.choices[0]?.message.content || ""
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`${this.providerName} completion error: ${error.message}`)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get available watsonx models (if API endpoint is available)
|
||||
export async function getWatsonxModels(
|
||||
baseUrl?: string,
|
||||
apiKey?: string,
|
||||
projectId?: string,
|
||||
region?: string,
|
||||
): Promise<string[]> {
|
||||
try {
|
||||
if (!apiKey || !projectId) {
|
||||
return Object.keys(watsonxModels)
|
||||
}
|
||||
|
||||
const actualRegion = region || "us-south"
|
||||
const actualBaseUrl = baseUrl || `https://${actualRegion}.ml.cloud.ibm.com/ml/v1`
|
||||
|
||||
// Note: This is a placeholder - IBM watsonx may have a different endpoint for listing models
|
||||
// For now, return the static list
|
||||
return Object.keys(watsonxModels)
|
||||
} catch (error) {
|
||||
return Object.keys(watsonxModels)
|
||||
}
|
||||
}
|
||||
|
|
@ -57,6 +57,8 @@ import {
|
|||
vercelAiGatewayDefaultModelId,
|
||||
BEDROCK_CLAUDE_SONNET_4_MODEL_ID,
|
||||
deepInfraDefaultModelId,
|
||||
watsonxDefaultModelId,
|
||||
watsonxModels,
|
||||
} from "@roo-code/types"
|
||||
|
||||
import type { ModelRecord, RouterModels } from "@roo/api"
|
||||
|
|
@ -348,11 +350,16 @@ function getSelectedModel({
|
|||
const info = routerModels["vercel-ai-gateway"]?.[id]
|
||||
return { id, info }
|
||||
}
|
||||
case "watsonx": {
|
||||
const id = apiConfiguration.apiModelId ?? watsonxDefaultModelId
|
||||
const info = watsonxModels[id as keyof typeof watsonxModels]
|
||||
return { id, info }
|
||||
}
|
||||
// case "anthropic":
|
||||
// case "human-relay":
|
||||
// case "fake-ai":
|
||||
default: {
|
||||
provider satisfies "anthropic" | "gemini-cli" | "qwen-code" | "human-relay" | "fake-ai"
|
||||
provider satisfies "anthropic" | "gemini-cli" | "human-relay" | "fake-ai"
|
||||
const id = apiConfiguration.apiModelId ?? anthropicDefaultModelId
|
||||
const baseInfo = anthropicModels[id as keyof typeof anthropicModels]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue