mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-19 00:01:19 +00:00
Kyma API (kymaapi.com) is an OpenAI-compatible LLM gateway with 20+ open-source models, free credits on signup, and pay-per-token billing. - Add KymaHandler extending BaseOpenAiCompatibleProvider - Register 8 top models (qwen-3.6-plus, deepseek-v3/r1, kimi-k2.5, gemma-4-31b, llama-3.3-70b, qwen-3-32b, minimax-m2.5) - Add kymaApiKey to provider settings schema and secret storage - Add Kyma settings UI component with API key input - Wire into buildApiHandler switch, PROVIDERS list, and model config
395 lines
14 KiB
TypeScript
395 lines
14 KiB
TypeScript
import {
|
|
type ProviderName,
|
|
type ProviderSettings,
|
|
type ModelInfo,
|
|
type ModelRecord,
|
|
type RouterModels,
|
|
anthropicModels,
|
|
bedrockModels,
|
|
deepSeekModels,
|
|
moonshotModels,
|
|
minimaxModels,
|
|
geminiModels,
|
|
mistralModels,
|
|
openAiModelInfoSaneDefaults,
|
|
openAiNativeModels,
|
|
vertexModels,
|
|
xaiModels,
|
|
vscodeLlmModels,
|
|
vscodeLlmDefaultModelId,
|
|
openAiCodexModels,
|
|
sambaNovaModels,
|
|
kymaModels,
|
|
internationalZAiModels,
|
|
mainlandZAiModels,
|
|
fireworksModels,
|
|
basetenModels,
|
|
qwenCodeModels,
|
|
litellmDefaultModelInfo,
|
|
lMStudioDefaultModelInfo,
|
|
BEDROCK_1M_CONTEXT_MODEL_IDS,
|
|
VERTEX_1M_CONTEXT_MODEL_IDS,
|
|
isDynamicProvider,
|
|
isRetiredProvider,
|
|
getProviderDefaultModelId,
|
|
} from "@roo-code/types"
|
|
|
|
import { useRouterModels } from "./useRouterModels"
|
|
import { useOpenRouterModelProviders } from "./useOpenRouterModelProviders"
|
|
import { useLmStudioModels } from "./useLmStudioModels"
|
|
import { useOllamaModels } from "./useOllamaModels"
|
|
|
|
/**
|
|
* Helper to get a validated model ID for dynamic providers.
|
|
* Returns the configured model ID if it exists in the available models, otherwise returns the default.
|
|
*/
|
|
function getValidatedModelId(
|
|
configuredId: string | undefined,
|
|
availableModels: ModelRecord | undefined,
|
|
defaultModelId: string,
|
|
): string {
|
|
return configuredId && availableModels?.[configuredId] ? configuredId : defaultModelId
|
|
}
|
|
|
|
export const useSelectedModel = (apiConfiguration?: ProviderSettings) => {
|
|
const provider = apiConfiguration?.apiProvider || "anthropic"
|
|
const activeProvider: ProviderName | undefined = isRetiredProvider(provider) ? undefined : provider
|
|
const dynamicProvider = activeProvider && isDynamicProvider(activeProvider) ? activeProvider : undefined
|
|
const openRouterModelId = activeProvider === "openrouter" ? apiConfiguration?.openRouterModelId : undefined
|
|
const lmStudioModelId = activeProvider === "lmstudio" ? apiConfiguration?.lmStudioModelId : undefined
|
|
const ollamaModelId = activeProvider === "ollama" ? apiConfiguration?.ollamaModelId : undefined
|
|
|
|
// Only fetch router models for dynamic providers
|
|
const shouldFetchRouterModels = !!dynamicProvider
|
|
const routerModels = useRouterModels({
|
|
provider: dynamicProvider,
|
|
enabled: shouldFetchRouterModels,
|
|
})
|
|
|
|
const openRouterModelProviders = useOpenRouterModelProviders(openRouterModelId)
|
|
const lmStudioModels = useLmStudioModels(lmStudioModelId)
|
|
const ollamaModels = useOllamaModels(ollamaModelId)
|
|
|
|
// Compute readiness only for the data actually needed for the selected provider
|
|
const needRouterModels = shouldFetchRouterModels
|
|
const needOpenRouterProviders = activeProvider === "openrouter"
|
|
const needLmStudio = typeof lmStudioModelId !== "undefined"
|
|
const needOllama = typeof ollamaModelId !== "undefined"
|
|
|
|
const hasValidRouterData =
|
|
needRouterModels && dynamicProvider
|
|
? routerModels.data &&
|
|
routerModels.data[dynamicProvider] !== undefined &&
|
|
typeof routerModels.data[dynamicProvider] === "object" &&
|
|
!routerModels.isLoading
|
|
: true
|
|
|
|
const isReady =
|
|
(!needLmStudio || typeof lmStudioModels.data !== "undefined") &&
|
|
(!needOllama || typeof ollamaModels.data !== "undefined") &&
|
|
hasValidRouterData &&
|
|
(!needOpenRouterProviders || typeof openRouterModelProviders.data !== "undefined")
|
|
|
|
const { id, info } =
|
|
apiConfiguration && isReady && activeProvider
|
|
? getSelectedModel({
|
|
provider: activeProvider,
|
|
apiConfiguration,
|
|
routerModels: (routerModels.data || {}) as RouterModels,
|
|
openRouterModelProviders: (openRouterModelProviders.data || {}) as Record<string, ModelInfo>,
|
|
lmStudioModels: (lmStudioModels.data || undefined) as ModelRecord | undefined,
|
|
ollamaModels: (ollamaModels.data || undefined) as ModelRecord | undefined,
|
|
})
|
|
: { id: getProviderDefaultModelId(activeProvider ?? "anthropic"), info: undefined }
|
|
|
|
return {
|
|
provider,
|
|
id,
|
|
info,
|
|
isLoading:
|
|
(needRouterModels && routerModels.isLoading) ||
|
|
(needOpenRouterProviders && openRouterModelProviders.isLoading) ||
|
|
(needLmStudio && lmStudioModels!.isLoading) ||
|
|
(needOllama && ollamaModels!.isLoading),
|
|
isError:
|
|
(needRouterModels && routerModels.isError) ||
|
|
(needOpenRouterProviders && openRouterModelProviders.isError) ||
|
|
(needLmStudio && lmStudioModels!.isError) ||
|
|
(needOllama && ollamaModels!.isError),
|
|
}
|
|
}
|
|
|
|
function getSelectedModel({
|
|
provider,
|
|
apiConfiguration,
|
|
routerModels,
|
|
openRouterModelProviders,
|
|
lmStudioModels,
|
|
ollamaModels,
|
|
}: {
|
|
provider: ProviderName
|
|
apiConfiguration: ProviderSettings
|
|
routerModels: RouterModels
|
|
openRouterModelProviders: Record<string, ModelInfo>
|
|
lmStudioModels: ModelRecord | undefined
|
|
ollamaModels: ModelRecord | undefined
|
|
}): { id: string; info: ModelInfo | undefined } {
|
|
// the `undefined` case are used to show the invalid selection to prevent
|
|
// users from seeing the default model if their selection is invalid
|
|
// this gives a better UX than showing the default model
|
|
const defaultModelId = getProviderDefaultModelId(provider)
|
|
switch (provider) {
|
|
case "openrouter": {
|
|
const id = getValidatedModelId(apiConfiguration.openRouterModelId, routerModels.openrouter, defaultModelId)
|
|
let info = routerModels.openrouter?.[id]
|
|
const specificProvider = apiConfiguration.openRouterSpecificProvider
|
|
|
|
if (specificProvider && openRouterModelProviders[specificProvider]) {
|
|
// Overwrite the info with the specific provider info. Some
|
|
// fields are missing the model info for `openRouterModelProviders`
|
|
// so we need to merge the two.
|
|
info = info
|
|
? { ...info, ...openRouterModelProviders[specificProvider] }
|
|
: openRouterModelProviders[specificProvider]
|
|
}
|
|
|
|
return { id, info }
|
|
}
|
|
case "requesty": {
|
|
const id = getValidatedModelId(apiConfiguration.requestyModelId, routerModels.requesty, defaultModelId)
|
|
const routerInfo = routerModels.requesty?.[id]
|
|
return { id, info: routerInfo }
|
|
}
|
|
case "unbound": {
|
|
const id = getValidatedModelId(apiConfiguration.unboundModelId, routerModels.unbound, defaultModelId)
|
|
const routerInfo = routerModels.unbound?.[id]
|
|
return { id, info: routerInfo }
|
|
}
|
|
case "litellm": {
|
|
const id = getValidatedModelId(apiConfiguration.litellmModelId, routerModels.litellm, defaultModelId)
|
|
const routerInfo = routerModels.litellm?.[id]
|
|
return { id, info: routerInfo ?? litellmDefaultModelInfo }
|
|
}
|
|
case "xai": {
|
|
const id = apiConfiguration.apiModelId ?? defaultModelId
|
|
const info = xaiModels[id as keyof typeof xaiModels]
|
|
return info ? { id, info } : { id, info: undefined }
|
|
}
|
|
case "baseten": {
|
|
const id = apiConfiguration.apiModelId ?? defaultModelId
|
|
const info = basetenModels[id as keyof typeof basetenModels]
|
|
return { id, info }
|
|
}
|
|
case "bedrock": {
|
|
const id = apiConfiguration.apiModelId ?? defaultModelId
|
|
const baseInfo = bedrockModels[id as keyof typeof bedrockModels]
|
|
|
|
// Special case for custom ARN.
|
|
if (id === "custom-arn") {
|
|
return {
|
|
id,
|
|
info: { maxTokens: 5000, contextWindow: 128_000, supportsPromptCache: true, supportsImages: true },
|
|
}
|
|
}
|
|
|
|
// Apply 1M context for supported Claude 4 models when enabled
|
|
if (BEDROCK_1M_CONTEXT_MODEL_IDS.includes(id as any) && apiConfiguration.awsBedrock1MContext && baseInfo) {
|
|
// Create a new ModelInfo object with updated context window
|
|
const info: ModelInfo = {
|
|
...baseInfo,
|
|
contextWindow: 1_000_000,
|
|
}
|
|
return { id, info }
|
|
}
|
|
|
|
return { id, info: baseInfo }
|
|
}
|
|
case "vertex": {
|
|
const id = apiConfiguration.apiModelId ?? defaultModelId
|
|
const baseInfo = vertexModels[id as keyof typeof vertexModels]
|
|
|
|
// Apply 1M context for supported Claude 4 models when enabled
|
|
if (VERTEX_1M_CONTEXT_MODEL_IDS.includes(id as any) && apiConfiguration.vertex1MContext && baseInfo) {
|
|
const modelInfo: ModelInfo = baseInfo
|
|
const tier = modelInfo.tiers?.[0]
|
|
if (tier) {
|
|
const info: ModelInfo = {
|
|
...modelInfo,
|
|
contextWindow: tier.contextWindow,
|
|
inputPrice: tier.inputPrice,
|
|
outputPrice: tier.outputPrice,
|
|
cacheWritesPrice: tier.cacheWritesPrice,
|
|
cacheReadsPrice: tier.cacheReadsPrice,
|
|
}
|
|
return { id, info }
|
|
}
|
|
}
|
|
|
|
return { id, info: baseInfo }
|
|
}
|
|
case "gemini": {
|
|
const id = apiConfiguration.apiModelId ?? defaultModelId
|
|
const info = geminiModels[id as keyof typeof geminiModels]
|
|
return { id, info }
|
|
}
|
|
case "deepseek": {
|
|
const id = apiConfiguration.apiModelId ?? defaultModelId
|
|
const info = deepSeekModels[id as keyof typeof deepSeekModels]
|
|
return { id, info }
|
|
}
|
|
case "moonshot": {
|
|
const id = apiConfiguration.apiModelId ?? defaultModelId
|
|
const info = moonshotModels[id as keyof typeof moonshotModels]
|
|
return { id, info }
|
|
}
|
|
case "minimax": {
|
|
const id = apiConfiguration.apiModelId ?? defaultModelId
|
|
const info = minimaxModels[id as keyof typeof minimaxModels]
|
|
return { id, info }
|
|
}
|
|
case "zai": {
|
|
const isChina = apiConfiguration.zaiApiLine === "china_coding"
|
|
const models = isChina ? mainlandZAiModels : internationalZAiModels
|
|
const defaultModelId = getProviderDefaultModelId(provider, { isChina })
|
|
const id = apiConfiguration.apiModelId ?? defaultModelId
|
|
const info = models[id as keyof typeof models]
|
|
return { id, info }
|
|
}
|
|
case "openai-native": {
|
|
const id = apiConfiguration.apiModelId ?? defaultModelId
|
|
const info = openAiNativeModels[id as keyof typeof openAiNativeModels]
|
|
return { id, info }
|
|
}
|
|
case "mistral": {
|
|
const id = apiConfiguration.apiModelId ?? defaultModelId
|
|
const info = mistralModels[id as keyof typeof mistralModels]
|
|
return { id, info }
|
|
}
|
|
case "openai": {
|
|
const id = apiConfiguration.openAiModelId ?? ""
|
|
const customInfo = apiConfiguration?.openAiCustomModelInfo
|
|
const info = customInfo ?? openAiModelInfoSaneDefaults
|
|
return { id, info }
|
|
}
|
|
case "ollama": {
|
|
const id = apiConfiguration.ollamaModelId ?? ""
|
|
const info = ollamaModels && ollamaModels[apiConfiguration.ollamaModelId!]
|
|
|
|
const adjustedInfo =
|
|
info?.contextWindow &&
|
|
apiConfiguration?.ollamaNumCtx &&
|
|
apiConfiguration.ollamaNumCtx < info.contextWindow
|
|
? { ...info, contextWindow: apiConfiguration.ollamaNumCtx }
|
|
: info
|
|
|
|
return {
|
|
id,
|
|
info: adjustedInfo || undefined,
|
|
}
|
|
}
|
|
case "lmstudio": {
|
|
const id = apiConfiguration.lmStudioModelId ?? ""
|
|
const modelInfo = lmStudioModels && lmStudioModels[apiConfiguration.lmStudioModelId!]
|
|
return {
|
|
id,
|
|
info: modelInfo ? { ...lMStudioDefaultModelInfo, ...modelInfo } : undefined,
|
|
}
|
|
}
|
|
case "vscode-lm": {
|
|
const id = apiConfiguration?.vsCodeLmModelSelector
|
|
? `${apiConfiguration.vsCodeLmModelSelector.vendor}/${apiConfiguration.vsCodeLmModelSelector.family}`
|
|
: vscodeLlmDefaultModelId
|
|
const modelFamily = apiConfiguration?.vsCodeLmModelSelector?.family ?? vscodeLlmDefaultModelId
|
|
const info = vscodeLlmModels[modelFamily as keyof typeof vscodeLlmModels]
|
|
return { id, info: { ...openAiModelInfoSaneDefaults, ...info, supportsImages: false } } // VSCode LM API currently doesn't support images.
|
|
}
|
|
case "sambanova": {
|
|
const id = apiConfiguration.apiModelId ?? defaultModelId
|
|
const info = sambaNovaModels[id as keyof typeof sambaNovaModels]
|
|
return { id, info }
|
|
}
|
|
case "kyma": {
|
|
const id = apiConfiguration.apiModelId ?? defaultModelId
|
|
const info = kymaModels[id as keyof typeof kymaModels]
|
|
return { id, info }
|
|
}
|
|
case "fireworks": {
|
|
const id = apiConfiguration.apiModelId ?? defaultModelId
|
|
const info = fireworksModels[id as keyof typeof fireworksModels]
|
|
return { id, info }
|
|
}
|
|
case "roo": {
|
|
const id = getValidatedModelId(apiConfiguration.apiModelId, routerModels.roo, defaultModelId)
|
|
const info = routerModels.roo?.[id]
|
|
return { id, info }
|
|
}
|
|
case "poe": {
|
|
const id = apiConfiguration.apiModelId ?? defaultModelId
|
|
const info = routerModels.poe?.[id]
|
|
return { id, info }
|
|
}
|
|
case "qwen-code": {
|
|
const id = apiConfiguration.apiModelId ?? defaultModelId
|
|
const info = qwenCodeModels[id as keyof typeof qwenCodeModels]
|
|
return { id, info }
|
|
}
|
|
case "openai-codex": {
|
|
const id = apiConfiguration.apiModelId ?? defaultModelId
|
|
const info = openAiCodexModels[id as keyof typeof openAiCodexModels]
|
|
return { id, info }
|
|
}
|
|
case "vercel-ai-gateway": {
|
|
const id = getValidatedModelId(
|
|
apiConfiguration.vercelAiGatewayModelId,
|
|
routerModels["vercel-ai-gateway"],
|
|
defaultModelId,
|
|
)
|
|
const info = routerModels["vercel-ai-gateway"]?.[id]
|
|
return { id, info }
|
|
}
|
|
// case "anthropic":
|
|
// case "fake-ai":
|
|
default: {
|
|
provider satisfies "anthropic" | "gemini-cli" | "fake-ai"
|
|
const id = apiConfiguration.apiModelId ?? defaultModelId
|
|
const baseInfo = anthropicModels[id as keyof typeof anthropicModels]
|
|
|
|
// Apply 1M context beta tier pricing for supported Claude 4 models
|
|
if (
|
|
provider === "anthropic" &&
|
|
(id === "claude-sonnet-4-20250514" ||
|
|
id === "claude-sonnet-4-5" ||
|
|
id === "claude-sonnet-4-6" ||
|
|
id === "claude-opus-4-6") &&
|
|
apiConfiguration.anthropicBeta1MContext &&
|
|
baseInfo
|
|
) {
|
|
// Type assertion since supported Claude 4 models include 1M context pricing tiers.
|
|
const modelWithTiers = baseInfo as typeof baseInfo & {
|
|
tiers?: Array<{
|
|
contextWindow: number
|
|
inputPrice?: number
|
|
outputPrice?: number
|
|
cacheWritesPrice?: number
|
|
cacheReadsPrice?: number
|
|
}>
|
|
}
|
|
const tier = modelWithTiers.tiers?.[0]
|
|
if (tier) {
|
|
// Create a new ModelInfo object with updated values
|
|
const info: ModelInfo = {
|
|
...baseInfo,
|
|
contextWindow: tier.contextWindow,
|
|
inputPrice: tier.inputPrice ?? baseInfo.inputPrice,
|
|
outputPrice: tier.outputPrice ?? baseInfo.outputPrice,
|
|
cacheWritesPrice: tier.cacheWritesPrice ?? baseInfo.cacheWritesPrice,
|
|
cacheReadsPrice: tier.cacheReadsPrice ?? baseInfo.cacheReadsPrice,
|
|
}
|
|
return { id, info }
|
|
}
|
|
}
|
|
|
|
return { id, info: baseInfo }
|
|
}
|
|
}
|
|
}
|