diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index a66aae08a24..7c782c94427 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -21,6 +21,7 @@ import { qwenCodeModels, rooModels, sambaNovaModels, + siliconCloudModels, vertexModels, vscodeLlmModels, xaiModels, @@ -135,6 +136,7 @@ export const providerNames = [ "qwen-code", "roo", "sambanova", + "siliconcloud", "vertex", "xai", "zai", @@ -377,6 +379,15 @@ const sambaNovaSchema = apiModelIdProviderModelSchema.extend({ sambaNovaApiKey: z.string().optional(), }) +export const siliconCloudApiLineSchema = z.enum(["china", "china-overseas", "international"]) + +export type SiliconCloudApiLine = z.infer + +const siliconCloudSchema = apiModelIdProviderModelSchema.extend({ + siliconCloudApiKey: z.string().optional(), + siliconCloudApiLine: siliconCloudApiLineSchema.optional(), +}) + export const zaiApiLineSchema = z.enum(["international_coding", "international", "china_coding", "china"]) export type ZaiApiLine = z.infer @@ -446,6 +457,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv litellmSchema.merge(z.object({ apiProvider: z.literal("litellm") })), cerebrasSchema.merge(z.object({ apiProvider: z.literal("cerebras") })), sambaNovaSchema.merge(z.object({ apiProvider: z.literal("sambanova") })), + siliconCloudSchema.merge(z.object({ apiProvider: z.literal("siliconcloud") })), zaiSchema.merge(z.object({ apiProvider: z.literal("zai") })), fireworksSchema.merge(z.object({ apiProvider: z.literal("fireworks") })), featherlessSchema.merge(z.object({ apiProvider: z.literal("featherless") })), @@ -487,6 +499,7 @@ export const providerSettingsSchema = z.object({ ...litellmSchema.shape, ...cerebrasSchema.shape, ...sambaNovaSchema.shape, + ...siliconCloudSchema.shape, ...zaiSchema.shape, ...fireworksSchema.shape, ...featherlessSchema.shape, @@ -573,6 +586,7 @@ export const modelIdKeysByProvider: Record = { huggingface: "huggingFaceModelId", cerebras: "apiModelId", sambanova: "apiModelId", + siliconcloud: "apiModelId", zai: "apiModelId", fireworks: "apiModelId", featherless: "apiModelId", @@ -683,6 +697,7 @@ export const MODELS_BY_PROVIDER: Record< label: "SambaNova", models: Object.keys(sambaNovaModels), }, + siliconcloud: { id: "siliconcloud", label: "SiliconCloud", models: Object.keys(siliconCloudModels) }, vertex: { id: "vertex", label: "GCP Vertex AI", diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index 21e43aaa99a..16cb4e532eb 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -23,6 +23,7 @@ export * from "./qwen-code.js" export * from "./requesty.js" export * from "./roo.js" export * from "./sambanova.js" +export * from "./siliconcloud.js" export * from "./unbound.js" export * from "./vertex.js" export * from "./vscode-llm.js" diff --git a/packages/types/src/providers/siliconcloud.ts b/packages/types/src/providers/siliconcloud.ts new file mode 100644 index 00000000000..a162b9fafe9 --- /dev/null +++ b/packages/types/src/providers/siliconcloud.ts @@ -0,0 +1,229 @@ +import type { ModelInfo } from "../model.js" +import type { SiliconCloudApiLine } from "../provider-settings.js" + +export const siliconCloudDefaultModelId = "zai-org/GLM-4.6" + +export const siliconCloudApiLineConfigs = { + china: { name: "国内版", baseUrl: "https://api.siliconflow.cn/v1" }, + "china-overseas": { name: "国内版(海外访问)", baseUrl: "https://api-st.siliconflow.cn/v1" }, + international: { name: "国际版", baseUrl: "https://api.siliconflow.com/v1" }, +} satisfies Record + +const siliconCloudChinaModels: Record = { + "Pro/deepseek-ai/DeepSeek-V3.1-Terminus": { + contextWindow: 163840, + maxTokens: 8192, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.14, + outputPrice: 0.28, + supportsReasoningBudget: true, + }, + "zai-org/GLM-4.6": { + contextWindow: 131072, + maxTokens: 8192, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + supportsReasoningBudget: true, + }, + "Qwen/QwQ-32B-Preview": { + contextWindow: 32768, + maxTokens: 32768, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + supportsReasoningBudget: true, + }, + "Qwen/Qwen2.5-Coder-32B-Instruct": { + contextWindow: 131072, + maxTokens: 8192, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, + "deepseek-ai/DeepSeek-V2.5": { + contextWindow: 131072, + maxTokens: 8192, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.14, + outputPrice: 0.28, + }, + "deepseek-ai/DeepSeek-Coder-V2-Instruct": { + contextWindow: 131072, + maxTokens: 8192, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.14, + outputPrice: 0.28, + }, + "Qwen/Qwen2.5-72B-Instruct": { + contextWindow: 131072, + maxTokens: 8192, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.35, + outputPrice: 1.4, + }, + "meta-llama/Meta-Llama-3.1-70B-Instruct": { + contextWindow: 131072, + maxTokens: 8192, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.35, + outputPrice: 0.42, + }, + "meta-llama/Meta-Llama-3.1-405B-Instruct": { + contextWindow: 131072, + maxTokens: 8192, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 2.1, + outputPrice: 2.1, + }, + "google/gemma-2-27b-it": { + contextWindow: 8192, + maxTokens: 8192, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.14, + outputPrice: 0.28, + }, + "01-ai/Yi-1.5-34B-Chat-16K": { + contextWindow: 16384, + maxTokens: 4096, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.14, + outputPrice: 0.84, + }, + "internlm/internlm2_5-20b-chat": { + contextWindow: 32768, + maxTokens: 8192, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.14, + outputPrice: 0.28, + }, +} + +const siliconCloudChinaOverseasModels: Record = { + ...siliconCloudChinaModels, +} + +const siliconCloudInternationalModels: Record = { + "Pro/deepseek-ai/DeepSeek-V3.1-Terminus": { + contextWindow: 163840, + maxTokens: 8192, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.02, + outputPrice: 0.04, + supportsReasoningBudget: true, + }, + "zai-org/GLM-4.6": { + contextWindow: 131072, + maxTokens: 8192, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + supportsReasoningBudget: true, + }, + "Qwen/QwQ-32B-Preview": { + contextWindow: 32768, + maxTokens: 32768, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + supportsReasoningBudget: true, + }, + "Qwen/Qwen2.5-Coder-32B-Instruct": { + contextWindow: 131072, + maxTokens: 8192, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, + "deepseek-ai/DeepSeek-V2.5": { + contextWindow: 131072, + maxTokens: 8192, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.02, + outputPrice: 0.04, + }, + "deepseek-ai/DeepSeek-Coder-V2-Instruct": { + contextWindow: 131072, + maxTokens: 8192, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.02, + outputPrice: 0.04, + }, + "Qwen/Qwen2.5-72B-Instruct": { + contextWindow: 131072, + maxTokens: 8192, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.05, + outputPrice: 0.2, + }, + "meta-llama/Meta-Llama-3.1-70B-Instruct": { + contextWindow: 131072, + maxTokens: 8192, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.05, + outputPrice: 0.06, + }, + "meta-llama/Meta-Llama-3.1-405B-Instruct": { + contextWindow: 131072, + maxTokens: 8192, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.3, + outputPrice: 0.3, + }, + "google/gemma-2-27b-it": { + contextWindow: 8192, + maxTokens: 8192, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.02, + outputPrice: 0.04, + }, + "01-ai/Yi-1.5-34B-Chat-16K": { + contextWindow: 16384, + maxTokens: 4096, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.02, + outputPrice: 0.12, + }, + "internlm/internlm2_5-20b-chat": { + contextWindow: 32768, + maxTokens: 8192, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.02, + outputPrice: 0.04, + }, +} + +export const siliconCloudModelsByApiLine = { + china: siliconCloudChinaModels, + "china-overseas": siliconCloudChinaOverseasModels, + international: siliconCloudInternationalModels, +} satisfies Record> + +// Export all models for the default list +export const siliconCloudModels = siliconCloudChinaModels + +export type SiliconCloudModelId = keyof typeof siliconCloudModels diff --git a/src/api/index.ts b/src/api/index.ts index ac009676762..c582fb81d53 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -32,6 +32,7 @@ import { ClaudeCodeHandler, QwenCodeHandler, SambaNovaHandler, + SiliconCloudHandler, IOIntelligenceHandler, DoubaoHandler, ZAiHandler, @@ -151,6 +152,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { return new CerebrasHandler(options) case "sambanova": return new SambaNovaHandler(options) + case "siliconcloud": + return new SiliconCloudHandler(options) case "zai": return new ZAiHandler(options) case "fireworks": diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index 85d877b6bc7..41130f6980b 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -24,6 +24,7 @@ export { OpenRouterHandler } from "./openrouter" export { QwenCodeHandler } from "./qwen-code" export { RequestyHandler } from "./requesty" export { SambaNovaHandler } from "./sambanova" +export { SiliconCloudHandler } from "./siliconcloud" export { UnboundHandler } from "./unbound" export { VertexHandler } from "./vertex" export { VsCodeLmHandler } from "./vscode-lm" diff --git a/src/api/providers/siliconcloud.ts b/src/api/providers/siliconcloud.ts new file mode 100644 index 00000000000..f15b32beda4 --- /dev/null +++ b/src/api/providers/siliconcloud.ts @@ -0,0 +1,117 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" + +import { + type SiliconCloudModelId, + type ModelInfo, + siliconCloudDefaultModelId, + siliconCloudModelsByApiLine, + siliconCloudApiLineConfigs, + type SiliconCloudApiLine, +} from "@roo-code/types" + +import type { ApiHandlerOptions } from "../../shared/api" +import { ApiStream } from "../transform/stream" +import { convertToOpenAiMessages } from "../transform/openai-format" + +import type { ApiHandlerCreateMessageMetadata } from "../index" +import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider" +import { handleOpenAIError } from "./utils/openai-error-handler" + +const SILICONCLOUD_DEFAULT_TEMPERATURE = 0 + +export class SiliconCloudHandler extends BaseOpenAiCompatibleProvider { + private apiLine: SiliconCloudApiLine + private models: Record + + constructor(options: ApiHandlerOptions) { + const apiLine = options.siliconCloudApiLine || "china" + const models = siliconCloudModelsByApiLine[apiLine] + const config = siliconCloudApiLineConfigs[apiLine] + + super({ + ...options, + apiKey: options.siliconCloudApiKey, + providerName: "SiliconCloud", + baseURL: config.baseUrl, + defaultProviderModelId: siliconCloudDefaultModelId, + providerModels: models, + defaultTemperature: SILICONCLOUD_DEFAULT_TEMPERATURE, + }) + + this.apiLine = apiLine + this.models = models + } + + override async *createMessage( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + metadata?: ApiHandlerCreateMessageMetadata, + ): ApiStream { + const { + id: model, + info: { maxTokens: max_tokens, supportsReasoningBudget }, + } = this.getModel() + + const temperature = this.options.modelTemperature ?? SILICONCLOUD_DEFAULT_TEMPERATURE + + // Build the request parameters + const params: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { + model, + max_tokens, + temperature, + messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)], + stream: true, + stream_options: { include_usage: true }, + } + + // Add SiliconCloud-specific reasoning parameters if the model supports it + // Note: For now, we enable thinking by default for models that support it + // In the future, this could be configurable via UI settings + if (supportsReasoningBudget) { + // SiliconCloud uses different parameter names than OpenAI + ;(params as any).enable_thinking = true + // Default thinking budget could be added here if needed + } + + try { + const stream = await this.client.chat.completions.create(params) + + for await (const chunk of stream) { + const delta = chunk.choices[0]?.delta + + if (delta?.content) { + yield { + type: "text", + text: delta.content, + } + } + + if (chunk.usage) { + yield { + type: "usage", + inputTokens: chunk.usage.prompt_tokens || 0, + outputTokens: chunk.usage.completion_tokens || 0, + } + } + } + } catch (error) { + throw handleOpenAIError(error, "SiliconCloud") + } + } + + override async completePrompt(prompt: string): Promise { + const { id: modelId } = this.getModel() + + try { + const response = await this.client.chat.completions.create({ + model: modelId, + messages: [{ role: "user", content: prompt }], + }) + + return response.choices[0]?.message.content || "" + } catch (error) { + throw handleOpenAIError(error, "SiliconCloud") + } + } +} diff --git a/src/shared/ProfileValidator.ts b/src/shared/ProfileValidator.ts index 78ff6ed9fe1..968d145487f 100644 --- a/src/shared/ProfileValidator.ts +++ b/src/shared/ProfileValidator.ts @@ -67,6 +67,7 @@ export class ProfileValidator { case "xai": case "groq": case "sambanova": + case "siliconcloud": case "chutes": case "fireworks": case "featherless": diff --git a/webview-ui/src/components/settings/providers/SiliconCloud.tsx b/webview-ui/src/components/settings/providers/SiliconCloud.tsx new file mode 100644 index 00000000000..d71f931a2fa --- /dev/null +++ b/webview-ui/src/components/settings/providers/SiliconCloud.tsx @@ -0,0 +1,71 @@ +import { VSCodeDropdown, VSCodeOption, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" +import { useCallback } from "react" + +import type { ProviderSettings } from "@roo-code/types" +import { siliconCloudApiLineConfigs, siliconCloudApiLineSchema } from "@roo-code/types" + +import { useAppTranslation } from "@src/i18n/TranslationContext" +import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" + +import { inputEventTransform } from "../transforms" +import { cn } from "@src/lib/utils" + +type SiliconCloudProps = { + apiConfiguration: ProviderSettings + setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void +} + +export const SiliconCloud = ({ apiConfiguration, setApiConfigurationField }: SiliconCloudProps) => { + const { t } = useAppTranslation() + + const handleInputChange = useCallback( + ( + field: K, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, + ) => + (event: E | Event) => { + setApiConfigurationField(field, transform(event as E)) + }, + [setApiConfigurationField], + ) + + return ( + <> +
+ + + {siliconCloudApiLineSchema.options.map((apiLine) => { + const config = siliconCloudApiLineConfigs[apiLine] + return ( + + {config.name} ({config.baseUrl}) + + ) + })} + +
+ {t("settings:providers.siliconCloudEntrypointDescription")} +
+
+ + + +
+ {t("settings:providers.apiKeyStorageNotice")} +
+ {!apiConfiguration?.siliconCloudApiKey && ( + + {t("settings:providers.getSiliconCloudApiKey")} + + )} + + ) +} diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index fe0e6cecf96..b47d9d96d98 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -30,3 +30,4 @@ export { Fireworks } from "./Fireworks" export { Featherless } from "./Featherless" export { VercelAiGateway } from "./VercelAiGateway" export { DeepInfra } from "./DeepInfra" +export { SiliconCloud } from "./SiliconCloud" diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 0d0514b4d66..f3233f07ec0 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -38,6 +38,8 @@ import { claudeCodeModels, sambaNovaModels, sambaNovaDefaultModelId, + siliconCloudDefaultModelId, + siliconCloudModelsByApiLine, doubaoModels, doubaoDefaultModelId, internationalZAiDefaultModelId, @@ -313,6 +315,13 @@ function getSelectedModel({ const info = sambaNovaModels[id as keyof typeof sambaNovaModels] return { id, info } } + case "siliconcloud": { + const apiLine = apiConfiguration.siliconCloudApiLine || "china" + const models = siliconCloudModelsByApiLine[apiLine] + const id = apiConfiguration.apiModelId ?? siliconCloudDefaultModelId + const info = models[id] + return { id, info } + } case "fireworks": { const id = apiConfiguration.apiModelId ?? fireworksDefaultModelId const info = fireworksModels[id as keyof typeof fireworksModels] diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index dfccc49cc4c..0a1119d885a 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -288,6 +288,10 @@ "getFireworksApiKey": "Get Fireworks API Key", "featherlessApiKey": "Featherless API Key", "getFeatherlessApiKey": "Get Featherless API Key", + "siliconCloudApiKey": "SiliconCloud API Key", + "getSiliconCloudApiKey": "Get SiliconCloud API Key", + "siliconCloudEntrypoint": "SiliconCloud Entrypoint", + "siliconCloudEntrypointDescription": "Please select the appropriate API entrypoint based on your location.", "ioIntelligenceApiKey": "IO Intelligence API Key", "ioIntelligenceApiKeyPlaceholder": "Enter your IO Intelligence API key", "getIoIntelligenceApiKey": "Get IO Intelligence API Key", diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index d15f82e4caf..f4a90f0719a 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -9,6 +9,7 @@ import { isDynamicProvider, isFauxProvider, isCustomProvider, + siliconCloudModelsByApiLine, } from "@roo-code/types" import type { RouterModels } from "@roo/api" @@ -156,6 +157,17 @@ function validateModelsAndKeysProvided(apiConfiguration: ProviderSettings): stri return i18next.t("settings:validation.apiKey") } break + case "siliconcloud": + if (!apiConfiguration.siliconCloudApiKey) { + return i18next.t("settings:validation.apiKey") + } + if (apiConfiguration.siliconCloudApiLine) { + const models = siliconCloudModelsByApiLine[apiConfiguration.siliconCloudApiLine] + if (apiConfiguration.apiModelId && !models[apiConfiguration.apiModelId as keyof typeof models]) { + return i18next.t("settings:validation.modelAvailability", { modelId: apiConfiguration.apiModelId }) + } + } + break } return undefined