feat(provider): add SiliconFlow provider support (#2885)
This commit is contained in:
parent
789f907f6d
commit
57876248e2
7 changed files with 147 additions and 17 deletions
|
|
@ -37,6 +37,7 @@ var protocolMetaByName = map[string]protocolMeta{
|
|||
"ollama": {defaultAPIBase: "http://localhost:11434/v1", emptyAPIKeyAllowed: true},
|
||||
"moonshot": {defaultAPIBase: "https://api.moonshot.cn/v1"},
|
||||
"shengsuanyun": {defaultAPIBase: "https://router.shengsuanyun.com/api/v1"},
|
||||
"siliconflow": {defaultAPIBase: "https://api.siliconflow.cn/v1"},
|
||||
"deepseek": {defaultAPIBase: "https://api.deepseek.com/v1"},
|
||||
"cerebras": {defaultAPIBase: "https://api.cerebras.ai/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)
|
||||
|
||||
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",
|
||||
"qwen-us", "dashscope-us", "mistral", "avian", "longcat", "modelscope", "novita",
|
||||
"coding-plan", "alibaba-coding", "qwen-coding", "zai", "mimo":
|
||||
|
|
|
|||
|
|
@ -204,6 +204,7 @@ func TestCreateProviderFromConfig_DefaultAPIBase(t *testing.T) {
|
|||
{"openrouter", "openrouter"},
|
||||
{"cerebras", "cerebras"},
|
||||
{"vivgrid", "vivgrid"},
|
||||
{"siliconflow", "siliconflow"},
|
||||
{"qwen", "qwen"},
|
||||
{"vllm", "vllm"},
|
||||
{"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) {
|
||||
cfg := &config.ModelConfig{
|
||||
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) {
|
||||
if got := getDefaultAPIBase("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 {
|
||||
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 {
|
||||
t.Fatal("anthropic option missing")
|
||||
} else if option.DefaultAPIBase != "https://api.anthropic.com/v1" {
|
||||
|
|
|
|||
|
|
@ -48,21 +48,22 @@ type Option func(*Provider)
|
|||
const defaultRequestTimeout = common.DefaultRequestTimeout
|
||||
|
||||
var stripModelPrefixProviders = map[string]struct{}{
|
||||
"litellm": {},
|
||||
"venice": {},
|
||||
"moonshot": {},
|
||||
"nvidia": {},
|
||||
"groq": {},
|
||||
"ollama": {},
|
||||
"deepseek": {},
|
||||
"google": {},
|
||||
"openrouter": {},
|
||||
"zhipu": {},
|
||||
"mistral": {},
|
||||
"vivgrid": {},
|
||||
"minimax": {},
|
||||
"novita": {},
|
||||
"lmstudio": {},
|
||||
"litellm": {},
|
||||
"venice": {},
|
||||
"moonshot": {},
|
||||
"nvidia": {},
|
||||
"groq": {},
|
||||
"ollama": {},
|
||||
"deepseek": {},
|
||||
"google": {},
|
||||
"openrouter": {},
|
||||
"siliconflow": {},
|
||||
"zhipu": {},
|
||||
"mistral": {},
|
||||
"vivgrid": {},
|
||||
"minimax": {},
|
||||
"novita": {},
|
||||
"lmstudio": {},
|
||||
}
|
||||
|
||||
func WithMaxTokensField(maxTokensField string) Option {
|
||||
|
|
|
|||
|
|
@ -931,6 +931,11 @@ func TestProviderChat_StripsKnownProviderPrefixes(t *testing.T) {
|
|||
input: "vivgrid/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",
|
||||
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" {
|
||||
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(
|
||||
"novita/deepseek/deepseek-v3.2",
|
||||
"https://api.novita.ai/openai",
|
||||
|
|
@ -1606,6 +1621,7 @@ func TestProviderChat_PromptCacheKeyOmittedForNonOpenAI(t *testing.T) {
|
|||
{"gemini", "https://generativelanguage.googleapis.com/v1beta"},
|
||||
{"deepseek", "https://api.deepseek.com/v1"},
|
||||
{"groq", "https://api.groq.com/openai/v1"},
|
||||
{"siliconflow", "https://api.siliconflow.cn/v1"},
|
||||
{"minimax", "https://api.minimaxi.com/v1"},
|
||||
{"ollama_local", "http://localhost:11434/v1"},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ var fetchableProviders = map[string]bool{
|
|||
"volcengine": true, "zhipu": true, "groq": true,
|
||||
"mistral": true, "nvidia": true, "cerebras": 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,
|
||||
"litellm": true, "vllm": true, "lmstudio": true, "ollama": true,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1819,6 +1819,15 @@ func TestHandleListModels_ReturnsProviderOptionsWithoutPersistingLegacyMigration
|
|||
} else if !option.EmptyAPIKeyAllowed {
|
||||
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 {
|
||||
t.Fatal("bedrock provider option missing")
|
||||
} else if !option.CreateAllowed {
|
||||
|
|
@ -2391,3 +2400,56 @@ func TestFetchOpenAICompatibleModels_NoAuthHeaderWhenKeyEmpty(t *testing.T) {
|
|||
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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -289,6 +289,17 @@ export const PROVIDERS: ProviderDefinition[] = [
|
|||
priority: 44,
|
||||
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",
|
||||
label: "Vivgrid",
|
||||
|
|
|
|||
Loading…
Reference in a new issue