feat(provider): add SiliconFlow provider support (#2885)

This commit is contained in:
LC 2026-05-18 10:16:09 +08:00 committed by GitHub
parent 789f907f6d
commit 57876248e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 147 additions and 17 deletions

View file

@ -37,6 +37,7 @@ var protocolMetaByName = map[string]protocolMeta{
"ollama": {defaultAPIBase: "http://localhost:11434/v1", emptyAPIKeyAllowed: true}, "ollama": {defaultAPIBase: "http://localhost:11434/v1", emptyAPIKeyAllowed: true},
"moonshot": {defaultAPIBase: "https://api.moonshot.cn/v1"}, "moonshot": {defaultAPIBase: "https://api.moonshot.cn/v1"},
"shengsuanyun": {defaultAPIBase: "https://router.shengsuanyun.com/api/v1"}, "shengsuanyun": {defaultAPIBase: "https://router.shengsuanyun.com/api/v1"},
"siliconflow": {defaultAPIBase: "https://api.siliconflow.cn/v1"},
"deepseek": {defaultAPIBase: "https://api.deepseek.com/v1"}, "deepseek": {defaultAPIBase: "https://api.deepseek.com/v1"},
"cerebras": {defaultAPIBase: "https://api.cerebras.ai/v1"}, "cerebras": {defaultAPIBase: "https://api.cerebras.ai/v1"},
"vivgrid": {defaultAPIBase: "https://api.vivgrid.com/v1"}, "vivgrid": {defaultAPIBase: "https://api.vivgrid.com/v1"},
@ -239,7 +240,7 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
return finalizeProviderFromConfig(provider, modelID, cfg) return finalizeProviderFromConfig(provider, modelID, cfg)
case "litellm", "lmstudio", "openrouter", "groq", "zhipu", "nvidia", "venice", case "litellm", "lmstudio", "openrouter", "groq", "zhipu", "nvidia", "venice",
"ollama", "moonshot", "shengsuanyun", "deepseek", "cerebras", "ollama", "moonshot", "shengsuanyun", "siliconflow", "deepseek", "cerebras",
"vivgrid", "volcengine", "vllm", "qwen", "qwen-portal", "qwen-intl", "qwen-international", "dashscope-intl", "vivgrid", "volcengine", "vllm", "qwen", "qwen-portal", "qwen-intl", "qwen-international", "dashscope-intl",
"qwen-us", "dashscope-us", "mistral", "avian", "longcat", "modelscope", "novita", "qwen-us", "dashscope-us", "mistral", "avian", "longcat", "modelscope", "novita",
"coding-plan", "alibaba-coding", "qwen-coding", "zai", "mimo": "coding-plan", "alibaba-coding", "qwen-coding", "zai", "mimo":

View file

@ -204,6 +204,7 @@ func TestCreateProviderFromConfig_DefaultAPIBase(t *testing.T) {
{"openrouter", "openrouter"}, {"openrouter", "openrouter"},
{"cerebras", "cerebras"}, {"cerebras", "cerebras"},
{"vivgrid", "vivgrid"}, {"vivgrid", "vivgrid"},
{"siliconflow", "siliconflow"},
{"qwen", "qwen"}, {"qwen", "qwen"},
{"vllm", "vllm"}, {"vllm", "vllm"},
{"deepseek", "deepseek"}, {"deepseek", "deepseek"},
@ -253,6 +254,12 @@ func TestGetDefaultAPIBase_Venice(t *testing.T) {
} }
} }
func TestGetDefaultAPIBase_SiliconFlow(t *testing.T) {
if got := getDefaultAPIBase("siliconflow"); got != "https://api.siliconflow.cn/v1" {
t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", "siliconflow", got, "https://api.siliconflow.cn/v1")
}
}
func TestCreateProviderFromConfig_LiteLLM(t *testing.T) { func TestCreateProviderFromConfig_LiteLLM(t *testing.T) {
cfg := &config.ModelConfig{ cfg := &config.ModelConfig{
ModelName: "test-litellm", ModelName: "test-litellm",
@ -477,6 +484,28 @@ func TestCreateProviderFromConfig_Venice(t *testing.T) {
} }
} }
func TestCreateProviderFromConfig_SiliconFlow(t *testing.T) {
cfg := &config.ModelConfig{
ModelName: "test-siliconflow",
Model: "siliconflow/deepseek-ai/DeepSeek-V3",
}
cfg.SetAPIKey("test-key")
provider, modelID, err := CreateProviderFromConfig(cfg)
if err != nil {
t.Fatalf("CreateProviderFromConfig() error = %v", err)
}
if provider == nil {
t.Fatal("CreateProviderFromConfig() returned nil provider")
}
if modelID != "deepseek-ai/DeepSeek-V3" {
t.Errorf("modelID = %q, want %q", modelID, "deepseek-ai/DeepSeek-V3")
}
if _, ok := provider.(*HTTPProvider); !ok {
t.Fatalf("expected *HTTPProvider, got %T", provider)
}
}
func TestGetDefaultAPIBase_Mimo(t *testing.T) { func TestGetDefaultAPIBase_Mimo(t *testing.T) {
if got := getDefaultAPIBase("mimo"); got != "https://api.xiaomimimo.com/v1" { if got := getDefaultAPIBase("mimo"); got != "https://api.xiaomimimo.com/v1" {
t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", "mimo", got, "https://api.xiaomimimo.com/v1") t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", "mimo", got, "https://api.xiaomimimo.com/v1")
@ -974,6 +1003,15 @@ func TestModelProviderOptions(t *testing.T) {
} else if !option.EmptyAPIKeyAllowed { } else if !option.EmptyAPIKeyAllowed {
t.Fatal("lmstudio should allow empty API keys") t.Fatal("lmstudio should allow empty API keys")
} }
if option, ok := seen["siliconflow"]; !ok {
t.Fatal("siliconflow option missing")
} else if option.DefaultAPIBase != "https://api.siliconflow.cn/v1" {
t.Fatalf(
"siliconflow default_api_base = %q, want %q",
option.DefaultAPIBase,
"https://api.siliconflow.cn/v1",
)
}
if option, ok := seen["anthropic"]; !ok { if option, ok := seen["anthropic"]; !ok {
t.Fatal("anthropic option missing") t.Fatal("anthropic option missing")
} else if option.DefaultAPIBase != "https://api.anthropic.com/v1" { } else if option.DefaultAPIBase != "https://api.anthropic.com/v1" {

View file

@ -48,21 +48,22 @@ type Option func(*Provider)
const defaultRequestTimeout = common.DefaultRequestTimeout const defaultRequestTimeout = common.DefaultRequestTimeout
var stripModelPrefixProviders = map[string]struct{}{ var stripModelPrefixProviders = map[string]struct{}{
"litellm": {}, "litellm": {},
"venice": {}, "venice": {},
"moonshot": {}, "moonshot": {},
"nvidia": {}, "nvidia": {},
"groq": {}, "groq": {},
"ollama": {}, "ollama": {},
"deepseek": {}, "deepseek": {},
"google": {}, "google": {},
"openrouter": {}, "openrouter": {},
"zhipu": {}, "siliconflow": {},
"mistral": {}, "zhipu": {},
"vivgrid": {}, "mistral": {},
"minimax": {}, "vivgrid": {},
"novita": {}, "minimax": {},
"lmstudio": {}, "novita": {},
"lmstudio": {},
} }
func WithMaxTokensField(maxTokensField string) Option { func WithMaxTokensField(maxTokensField string) Option {

View file

@ -931,6 +931,11 @@ func TestProviderChat_StripsKnownProviderPrefixes(t *testing.T) {
input: "vivgrid/auto", input: "vivgrid/auto",
wantModel: "auto", wantModel: "auto",
}, },
{
name: "strips siliconflow prefix and keeps nested model",
input: "siliconflow/deepseek-ai/DeepSeek-V3",
wantModel: "deepseek-ai/DeepSeek-V3",
},
{ {
name: "strips novita prefix deepseek model", name: "strips novita prefix deepseek model",
input: "novita/deepseek/deepseek-v3.2", input: "novita/deepseek/deepseek-v3.2",
@ -1041,6 +1046,16 @@ func TestNormalizeModel_UsesAPIBase(t *testing.T) {
if got := normalizeModel("vivgrid/auto", "https://api.vivgrid.com/v1"); got != "auto" { if got := normalizeModel("vivgrid/auto", "https://api.vivgrid.com/v1"); got != "auto" {
t.Fatalf("normalizeModel(vivgrid auto) = %q, want %q", got, "auto") t.Fatalf("normalizeModel(vivgrid auto) = %q, want %q", got, "auto")
} }
if got := normalizeModel(
"siliconflow/deepseek-ai/DeepSeek-V3",
"https://api.siliconflow.cn/v1",
); got != "deepseek-ai/DeepSeek-V3" {
t.Fatalf(
"normalizeModel(siliconflow) = %q, want %q",
got,
"deepseek-ai/DeepSeek-V3",
)
}
if got := normalizeModel( if got := normalizeModel(
"novita/deepseek/deepseek-v3.2", "novita/deepseek/deepseek-v3.2",
"https://api.novita.ai/openai", "https://api.novita.ai/openai",
@ -1606,6 +1621,7 @@ func TestProviderChat_PromptCacheKeyOmittedForNonOpenAI(t *testing.T) {
{"gemini", "https://generativelanguage.googleapis.com/v1beta"}, {"gemini", "https://generativelanguage.googleapis.com/v1beta"},
{"deepseek", "https://api.deepseek.com/v1"}, {"deepseek", "https://api.deepseek.com/v1"},
{"groq", "https://api.groq.com/openai/v1"}, {"groq", "https://api.groq.com/openai/v1"},
{"siliconflow", "https://api.siliconflow.cn/v1"},
{"minimax", "https://api.minimaxi.com/v1"}, {"minimax", "https://api.minimaxi.com/v1"},
{"ollama_local", "http://localhost:11434/v1"}, {"ollama_local", "http://localhost:11434/v1"},
} }

View file

@ -25,7 +25,8 @@ var fetchableProviders = map[string]bool{
"volcengine": true, "zhipu": true, "groq": true, "volcengine": true, "zhipu": true, "groq": true,
"mistral": true, "nvidia": true, "cerebras": true, "mistral": true, "nvidia": true, "cerebras": true,
"venice": true, "shengsuanyun": true, "vivgrid": true, "venice": true, "shengsuanyun": true, "vivgrid": true,
"minimax": true, "longcat": true, "modelscope": true, "siliconflow": true,
"minimax": true, "longcat": true, "modelscope": true,
"mimo": true, "avian": true, "zai": true, "novita": true, "mimo": true, "avian": true, "zai": true, "novita": true,
"litellm": true, "vllm": true, "lmstudio": true, "ollama": true, "litellm": true, "vllm": true, "lmstudio": true, "ollama": true,
} }

View file

@ -1819,6 +1819,15 @@ func TestHandleListModels_ReturnsProviderOptionsWithoutPersistingLegacyMigration
} else if !option.EmptyAPIKeyAllowed { } else if !option.EmptyAPIKeyAllowed {
t.Fatal("lmstudio should allow empty api keys") t.Fatal("lmstudio should allow empty api keys")
} }
if option, ok := optionsByID["siliconflow"]; !ok {
t.Fatal("siliconflow provider option missing")
} else if option.DefaultAPIBase != "https://api.siliconflow.cn/v1" {
t.Fatalf(
"siliconflow default_api_base = %q, want %q",
option.DefaultAPIBase,
"https://api.siliconflow.cn/v1",
)
}
if option, ok := optionsByID["bedrock"]; !ok { if option, ok := optionsByID["bedrock"]; !ok {
t.Fatal("bedrock provider option missing") t.Fatal("bedrock provider option missing")
} else if !option.CreateAllowed { } else if !option.CreateAllowed {
@ -2391,3 +2400,56 @@ func TestFetchOpenAICompatibleModels_NoAuthHeaderWhenKeyEmpty(t *testing.T) {
t.Fatalf("Authorization = %q, want empty", gotAuth) t.Fatalf("Authorization = %q, want empty", gotAuth)
} }
} }
func TestHandleFetchModels_SiliconFlowUsesOpenAICompatibleEndpoint(t *testing.T) {
configPath, cleanup := setupOAuthTestEnv(t)
defer cleanup()
var gotPath string
var gotAuth string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
gotAuth = r.Header.Get("Authorization")
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"data":[{"id":"deepseek-ai/DeepSeek-V3","owned_by":"siliconflow"}]}`)
}))
defer srv.Close()
h := NewHandler(configPath)
mux := http.NewServeMux()
h.RegisterRoutes(mux)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/models/fetch", bytes.NewBufferString(fmt.Sprintf(`{
"provider":"siliconflow",
"api_key":"sk-siliconflow",
"api_base":"%s"
}`, srv.URL)))
req.Header.Set("Content-Type", "application/json")
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
if gotPath != "/models" {
t.Fatalf("path = %q, want %q", gotPath, "/models")
}
if gotAuth != "Bearer sk-siliconflow" {
t.Fatalf("Authorization = %q, want %q", gotAuth, "Bearer sk-siliconflow")
}
var resp struct {
Models []upstreamModel `json:"models"`
Total int `json:"total"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("Unmarshal() error = %v", err)
}
if resp.Total != 1 || len(resp.Models) != 1 {
t.Fatalf("response = %+v, want one fetched model", resp)
}
if resp.Models[0].ID != "deepseek-ai/DeepSeek-V3" {
t.Fatalf("model id = %q, want %q", resp.Models[0].ID, "deepseek-ai/DeepSeek-V3")
}
}

View file

@ -289,6 +289,17 @@ export const PROVIDERS: ProviderDefinition[] = [
priority: 44, priority: 44,
supportsFetch: true, supportsFetch: true,
}, },
{
key: "siliconflow",
label: "SiliconFlow",
labelZh: "硅基流动",
domain: "siliconflow.cn",
defaultApiBase: "https://api.siliconflow.cn/v1",
requiresApiKey: true,
isLocal: false,
priority: 43.5,
supportsFetch: true,
},
{ {
key: "vivgrid", key: "vivgrid",
label: "Vivgrid", label: "Vivgrid",