feat: add SiliconCloud provider support

- Add SiliconCloud provider implementation based on OpenAI-compatible API
- Support multiple API endpoints (China, China-overseas, International)
- Add model definitions with region-specific pricing
- Integrate with UI settings and validation
- Add i18n translations for SiliconCloud settings

Closes #8550
This commit is contained in:
Roo Code 2025-10-07 12:34:12 +00:00
parent 5a3f911321
commit 2ba90bf30b
12 changed files with 472 additions and 0 deletions

View file

@ -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<typeof siliconCloudApiLineSchema>
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<typeof zaiApiLineSchema>
@ -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<TypicalProvider, ModelIdKey> = {
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",

View file

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

View file

@ -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<SiliconCloudApiLine, { name: string; baseUrl: string }>
const siliconCloudChinaModels: Record<string, ModelInfo> = {
"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<string, ModelInfo> = {
...siliconCloudChinaModels,
}
const siliconCloudInternationalModels: Record<string, ModelInfo> = {
"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<SiliconCloudApiLine, Record<string, ModelInfo>>
// Export all models for the default list
export const siliconCloudModels = siliconCloudChinaModels
export type SiliconCloudModelId = keyof typeof siliconCloudModels

View file

@ -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":

View file

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

View file

@ -0,0 +1,125 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import {
type SiliconCloudModelId,
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, SingleCompletionHandler } from "../index"
import { BaseProvider } from "./base-provider"
import { DEFAULT_HEADERS } from "./constants"
import { handleOpenAIError } from "./utils/openai-error-handler"
const SILICONCLOUD_DEFAULT_TEMPERATURE = 0
export class SiliconCloudHandler extends BaseProvider implements SingleCompletionHandler {
private client: OpenAI
private options: ApiHandlerOptions
private apiLine: SiliconCloudApiLine
private models: Record<string, any>
constructor(options: ApiHandlerOptions) {
super()
this.options = options
this.apiLine = options.siliconCloudApiLine || "china"
this.models = siliconCloudModelsByApiLine[this.apiLine]
const apiKey = options.siliconCloudApiKey
if (!apiKey) {
throw new Error("SiliconCloud API key is required")
}
const config = siliconCloudApiLineConfigs[this.apiLine]
this.client = new OpenAI({
baseURL: config.baseUrl,
apiKey,
defaultHeaders: DEFAULT_HEADERS,
})
}
override getModel() {
const id = (this.options.apiModelId as SiliconCloudModelId) || siliconCloudDefaultModelId
const info = this.models[id] || this.models[siliconCloudDefaultModelId]
return { id, info }
}
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")
}
}
async completePrompt(prompt: string): Promise<string> {
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")
}
}
}

View file

@ -67,6 +67,7 @@ export class ProfileValidator {
case "xai":
case "groq":
case "sambanova":
case "siliconcloud":
case "chutes":
case "fireworks":
case "featherless":

View file

@ -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(
<K extends keyof ProviderSettings, E>(
field: K,
transform: (event: E) => ProviderSettings[K] = inputEventTransform,
) =>
(event: E | Event) => {
setApiConfigurationField(field, transform(event as E))
},
[setApiConfigurationField],
)
return (
<>
<div>
<label className="block font-medium mb-1">{t("settings:providers.siliconCloudEntrypoint")}</label>
<VSCodeDropdown
value={apiConfiguration.siliconCloudApiLine || "china"}
onChange={handleInputChange("siliconCloudApiLine")}
className={cn("w-full")}>
{siliconCloudApiLineSchema.options.map((apiLine) => {
const config = siliconCloudApiLineConfigs[apiLine]
return (
<VSCodeOption key={apiLine} value={apiLine} className="p-2">
{config.name} ({config.baseUrl})
</VSCodeOption>
)
})}
</VSCodeDropdown>
<div className="text-xs text-vscode-descriptionForeground mt-1">
{t("settings:providers.siliconCloudEntrypointDescription")}
</div>
</div>
<VSCodeTextField
value={apiConfiguration?.siliconCloudApiKey || ""}
type="password"
onInput={handleInputChange("siliconCloudApiKey")}
placeholder={t("settings:placeholders.apiKey")}
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.siliconCloudApiKey")}</label>
</VSCodeTextField>
<div className="text-sm text-vscode-descriptionForeground -mt-2">
{t("settings:providers.apiKeyStorageNotice")}
</div>
{!apiConfiguration?.siliconCloudApiKey && (
<VSCodeButtonLink href="https://siliconflow.cn/" appearance="secondary">
{t("settings:providers.getSiliconCloudApiKey")}
</VSCodeButtonLink>
)}
</>
)
}

View file

@ -30,3 +30,4 @@ export { Fireworks } from "./Fireworks"
export { Featherless } from "./Featherless"
export { VercelAiGateway } from "./VercelAiGateway"
export { DeepInfra } from "./DeepInfra"
export { SiliconCloud } from "./SiliconCloud"

View file

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

View file

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

View file

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