mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-10-12 02:16:39 +00:00
Go: fix xunfei model (#20757)
Signed-off-by: Jin Hai <[email protected]>
This commit is contained in:
@@ -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": [
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
|
||||
Reference in New Issue
Block a user