From bc2934b89d1ebc9ee13e366155c2ef48abcc67e0 Mon Sep 17 00:00:00 2001 From: Jin Hai Date: Sun, 11 Oct 2026 08:53:26 +0800 Subject: [PATCH] Go: fix xunfei model (#20757) Signed-off-by: Jin Hai --- conf/models/xunfei.json | 4 +- internal/entity/models/xunfei.go | 48 ++------------ internal/entity/models/xunfei_test.go | 96 --------------------------- internal/handler/providers.go | 34 ++-------- internal/service/model_service.go | 4 +- 5 files changed, 13 insertions(+), 173 deletions(-) diff --git a/conf/models/xunfei.json b/conf/models/xunfei.json index 5f9d2d58aa..cd2756fd8b 100644 --- a/conf/models/xunfei.json +++ b/conf/models/xunfei.json @@ -1,7 +1,7 @@ { "name": "XunFei Spark", "url": { - "default": "https://spark-api-open.xf-yun.com" + "default": "https://maas-api.cn-huabei-1.xf-yun.com" }, "url_suffix": { "chat": "v2/chat/completions" @@ -9,7 +9,7 @@ "class": "xunfei", "models": [ { - "name": "spark-x", + "name": "spark-x2.5-1.7b", "context_length": 128000, "max_output": 8192, "model_types": [ diff --git a/internal/entity/models/xunfei.go b/internal/entity/models/xunfei.go index 13bf0c4752..b1e630bf7e 100644 --- a/internal/entity/models/xunfei.go +++ b/internal/entity/models/xunfei.go @@ -22,48 +22,8 @@ import ( "fmt" "io" "ragflow/internal/common" - "strings" ) -// sparkModelVersions maps the catalog model names to the version identifiers -// the XunFei Spark HTTP API expects in the request body's "model" field. -// Mirrors SparkChat.model2version in rag/llm/chat_model.py. -var sparkModelVersions = map[string]string{ - "Spark-Max": "generalv3.5", - "Spark-Max-32K": "max-32k", - "Spark-Lite": "lite", - "Spark-Pro": "generalv3", - "Spark-Pro-128K": "pro-128k", - "Spark-4.0-Ultra": "4.0Ultra", -} - -func resolveSparkModel(modelName string) string { - if version, ok := sparkModelVersions[modelName]; ok { - return version - } - return modelName -} - -// resolveBearerToken extracts the credential used as the Bearer token. The -// instance stores the XunFei credential bundle (API password, APPID, API -// secret, API key) as a JSON object string; the Spark HTTP API authenticates -// with the bundle's spark_api_password. -func resolveBearerToken(apiConfig *APIConfig) string { - if apiConfig == nil || apiConfig.ApiKey == nil { - return "" - } - key := strings.TrimSpace(*apiConfig.ApiKey) - if strings.HasPrefix(key, "{") { - var bundle map[string]interface{} - if err := json.Unmarshal([]byte(key), &bundle); err == nil { - if password, ok := bundle["spark_api_password"].(string); ok && password != "" { - return password - } - } - } - return key -} - type XunFeiModel struct { baseModel BaseModel } @@ -77,7 +37,7 @@ func NewXunFeiModel(baseURL map[string]string, urlSuffix URLSuffix) *XunFeiModel // The Spark HTTP API authenticates with the credential bundle's // spark_api_password, not the raw stored key. authHeader: func(cfg *APIConfig) (string, string) { - return "Authorization", "Bearer " + resolveBearerToken(cfg) + return "Authorization", "Bearer " + *cfg.ApiKey }, }, } @@ -105,7 +65,7 @@ func (x *XunFeiModel) ChatWithMessages(ctx context.Context, modelName string, me return nil, err } url := fmt.Sprintf("%s/%s", resolvedBaseURL, x.baseModel.URLSuffix.Chat) - reqBody := buildRequestBody(chatModelConfig, resolveSparkModel(modelName), messages, false) + reqBody := buildRequestBody(chatModelConfig, modelName, messages, false) if chatModelConfig != nil { if chatModelConfig.Thinking != nil { @@ -144,7 +104,7 @@ func (x *XunFeiModel) ChatStreamlyWithSender(ctx context.Context, modelName stri } url := fmt.Sprintf("%s/%s", resolvedBaseURL, x.baseModel.URLSuffix.Chat) - reqBody := buildRequestBody(modelConfig, resolveSparkModel(modelName), messages, true) + reqBody := buildRequestBody(modelConfig, modelName, messages, true) if modelConfig != nil { if modelConfig.Thinking != nil { @@ -242,7 +202,7 @@ func (x *XunFeiModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) // free Spark-Lite model. maxTokens := 1 chatConfig := &ChatConfig{MaxTokens: &maxTokens} - _, err := x.ChatWithMessages(ctx, "Spark-Lite", []Message{{Role: "user", Content: "Hi"}}, apiConfig, chatConfig, nil) + _, err := x.ChatWithMessages(ctx, "spark-x2.5-1.7b", []Message{{Role: "user", Content: "Hi"}}, apiConfig, chatConfig, nil) return err } diff --git a/internal/entity/models/xunfei_test.go b/internal/entity/models/xunfei_test.go index 80d1b623c1..93411aa993 100644 --- a/internal/entity/models/xunfei_test.go +++ b/internal/entity/models/xunfei_test.go @@ -1,7 +1,6 @@ package models import ( - "encoding/json" "io" "net/http" "net/http/httptest" @@ -9,47 +8,6 @@ import ( "testing" ) -func TestResolveSparkModel(t *testing.T) { - cases := map[string]string{ - "Spark-Max": "generalv3.5", - "Spark-Max-32K": "max-32k", - "Spark-Lite": "lite", - "Spark-Pro": "generalv3", - "Spark-Pro-128K": "pro-128k", - "Spark-4.0-Ultra": "4.0Ultra", - // Unknown names pass through unchanged (e.g. "spark-x"). - "spark-x": "spark-x", - } - for name, want := range cases { - if got := resolveSparkModel(name); got != want { - t.Errorf("resolveSparkModel(%q) = %q, want %q", name, got, want) - } - } -} - -func TestResolveBearerToken(t *testing.T) { - bundle := `{"spark_api_password":"pwd","spark_app_id":"app","spark_api_secret":"secret","spark_api_key":"key"}` - cases := []struct { - name string - key *string - want string - }{ - {"nil key", nil, ""}, - {"plain key", strPtr("sk-plain"), "sk-plain"}, - {"bundle uses password", strPtr(bundle), "pwd"}, - {"bundle without password falls back to raw", strPtr(`{"spark_app_id":"app"}`), `{"spark_app_id":"app"}`}, - {"malformed json falls back to raw", strPtr(`{not-json`), `{not-json`}, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - got := resolveBearerToken(&APIConfig{ApiKey: tc.key}) - if got != tc.want { - t.Errorf("resolveBearerToken() = %q, want %q", got, tc.want) - } - }) - } -} - func TestXunFeiCheckConnectionRequiresAPIKey(t *testing.T) { driver := NewXunFeiModel(map[string]string{"default": "http://unused"}, URLSuffix{}). NewInstance(map[string]string{"default": "http://unused"}) @@ -131,60 +89,6 @@ func newXunFeiForTest(baseURL string) *XunFeiModel { ) } -func TestXunFeiChatUsesResolvedBearerToken(t *testing.T) { - withSSRFBypass(t) - ctx := t.Context() - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - t.Errorf("method=%s, want POST", r.Method) - } - // The Spark credential bundle is stored as JSON; the request must - // authenticate with the extracted spark_api_password. - if got := r.Header.Get("Authorization"); got != "Bearer pwd" { - t.Errorf("Authorization=%q, want Bearer pwd", got) - } - var body map[string]interface{} - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - t.Errorf("decode request: %v", err) - return - } - if body["model"] != "lite" { - t.Errorf("model=%v, want lite (resolved Spark-Lite)", body["model"]) - } - if body["stream"] != false { - t.Errorf("stream=%v, want false", body["stream"]) - } - _ = json.NewEncoder(w).Encode(map[string]interface{}{ - "choices": []map[string]interface{}{{ - "message": map[string]interface{}{ - "content": "pong", - "reasoning_content": "\nthought", - }, - }}, - }) - })) - defer srv.Close() - - bundle := `{"spark_api_password":"pwd","spark_app_id":"app","spark_api_secret":"secret","spark_api_key":"key"}` - resp, err := newXunFeiForTest(srv.URL).ChatWithMessages( - ctx, - "Spark-Lite", - []Message{{Role: "user", Content: "ping"}}, - &APIConfig{ApiKey: &bundle}, - nil, - nil, - ) - if err != nil { - t.Fatalf("ChatWithMessages: %v", err) - } - if resp.Answer == nil || *resp.Answer != "pong" { - t.Errorf("Answer=%v, want pong", resp.Answer) - } - if resp.ReasonContent == nil || *resp.ReasonContent != "thought" { - t.Errorf("ReasonContent=%v, want thought", resp.ReasonContent) - } -} - func TestXunFeiStreamHappyPath(t *testing.T) { withSSRFBypass(t) ctx := t.Context() diff --git a/internal/handler/providers.go b/internal/handler/providers.go index 0e2f2eaba6..02fc0cc2fe 100644 --- a/internal/handler/providers.go +++ b/internal/handler/providers.go @@ -17,7 +17,6 @@ package handler import ( - "bytes" "encoding/json" "errors" "fmt" @@ -413,7 +412,7 @@ func (h *ProviderHandler) ShowModel(c *gin.Context) { type CreateProviderInstanceRequest struct { InstanceName string `json:"instance_name" binding:"required"` - APIKey json.RawMessage `json:"api_key"` + APIKey string `json:"api_key"` BaseURL string `json:"base_url"` Region string `json:"region"` ModelInfo []service.CreateInstanceModelInfo `json:"model_info"` @@ -435,26 +434,6 @@ func validateInstanceName(instanceName string) error { return nil } -// normalizeAPIKey accepts api_key as either a JSON string or a JSON object -// (credential bundles such as XunFei Spark's -// {"spark_api_password": ..., "spark_app_id": ..., ...}) and normalizes it to -// the string form persisted on the instance. -func normalizeAPIKey(raw json.RawMessage) string { - trimmed := strings.TrimSpace(string(raw)) - if trimmed == "" || trimmed == "null" { - return "" - } - var s string - if err := json.Unmarshal(raw, &s); err == nil { - return s - } - var buf bytes.Buffer - if err := json.Compact(&buf, raw); err != nil { - return trimmed - } - return buf.String() -} - func (h *ProviderHandler) CreateProviderInstance(c *gin.Context) { providerName := c.Param("provider_id_or_name") if providerName == "" { @@ -475,12 +454,11 @@ func (h *ProviderHandler) CreateProviderInstance(c *gin.Context) { } userID := c.GetString("user_id") - apiKey := normalizeAPIKey(req.APIKey) // If the request body only contains "instance_name", create a name-only // instance without API key validation or model creation. // Mirrors Python's provider_api.py:349 — set(data.keys()) == {"instance_name"}. - if apiKey == "" && req.BaseURL == "" && req.Region == "" && len(req.ModelInfo) == 0 { + if req.APIKey == "" && req.BaseURL == "" && req.Region == "" && len(req.ModelInfo) == 0 { code, err := h.modelProviderService.CreateNameOnlyProviderInstance(ctx, providerName, req.InstanceName, userID) if err != nil { common.ErrorWithCode(c, code, err.Error()) @@ -490,7 +468,7 @@ func (h *ProviderHandler) CreateProviderInstance(c *gin.Context) { return } - code, err := h.modelProviderService.CreateProviderInstance(ctx, providerName, req.InstanceName, apiKey, req.BaseURL, req.Region, userID, req.ModelInfo) + code, err := h.modelProviderService.CreateProviderInstance(ctx, providerName, req.InstanceName, req.APIKey, req.BaseURL, req.Region, userID, req.ModelInfo) if err != nil { common.ErrorWithCode(c, code, err.Error()) return @@ -584,7 +562,7 @@ func (h *ProviderHandler) CheckConnection(c *gin.Context) { } userID := c.GetString("user_id") - errCode, err := h.modelProviderService.CheckConnection(ctx, providerName, normalizeAPIKey(req.APIKey), req.Region, req.BaseURL, req.InstanceID, userID, req.ModelInfo) + errCode, err := h.modelProviderService.CheckConnection(ctx, providerName, req.APIKey, req.Region, req.BaseURL, req.InstanceID, userID, req.ModelInfo) if err != nil { common.ErrorWithCode(c, errCode, err.Error()) return @@ -688,7 +666,7 @@ func (h *ProviderHandler) ShowTask(c *gin.Context) { type AlterProviderInstanceRequest struct { InstanceName string `json:"instance_name"` - APIKey json.RawMessage `json:"api_key"` + APIKey string `json:"api_key"` BaseURL string `json:"base_url"` Region string `json:"region"` ModelInfo *[]service.CreateInstanceModelInfo `json:"model_info" binding:"required"` @@ -720,7 +698,7 @@ func (h *ProviderHandler) AlterProviderInstance(c *gin.Context) { return } - code, err := h.modelProviderService.AlterProviderInstance(ctx, userID, providerName, instanceName, req.InstanceName, normalizeAPIKey(req.APIKey), req.BaseURL, req.Region, *req.ModelInfo) + code, err := h.modelProviderService.AlterProviderInstance(ctx, userID, providerName, instanceName, req.InstanceName, req.APIKey, req.BaseURL, req.Region, *req.ModelInfo) if err != nil { common.ErrorWithCode(c, code, err.Error()) return diff --git a/internal/service/model_service.go b/internal/service/model_service.go index ae51d2c4a0..defb5503ab 100644 --- a/internal/service/model_service.go +++ b/internal/service/model_service.go @@ -90,9 +90,7 @@ type CheckConnectionModelInfo struct { } type CheckConnectionRequest struct { - // APIKey may be a JSON string or a JSON object (credential bundles such - // as XunFei Spark's); the handler normalizes it to a string before use. - APIKey json.RawMessage `json:"api_key"` + APIKey string `json:"api_key"` Region string `json:"region"` BaseURL string `json:"base_url"` InstanceID string `json:"instance_id"`