mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-05 07:10:29 +08:00
fix: XunFei Spark provider API key verification and model listing (#17652)
This commit is contained in:
@@ -24,8 +24,48 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"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
|
||||
}
|
||||
@@ -62,7 +102,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, modelName, messages, false)
|
||||
reqBody := buildRequestBody(chatModelConfig, resolveSparkModel(modelName), messages, false)
|
||||
|
||||
if chatModelConfig != nil {
|
||||
if chatModelConfig.Thinking != nil {
|
||||
@@ -92,7 +132,7 @@ func (x *XunFeiModel) ChatWithMessages(ctx context.Context, modelName string, me
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey))
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", resolveBearerToken(apiConfig)))
|
||||
|
||||
resp, err := x.baseModel.httpClient.Do(req)
|
||||
if err != nil {
|
||||
@@ -105,6 +145,10 @@ func (x *XunFeiModel) ChatWithMessages(ctx context.Context, modelName string, me
|
||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// Parse Response
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
@@ -167,7 +211,7 @@ func (x *XunFeiModel) ChatStreamlyWithSender(ctx context.Context, modelName stri
|
||||
}
|
||||
url := fmt.Sprintf("%s/%s", resolvedBaseURL, x.baseModel.URLSuffix.Chat)
|
||||
|
||||
reqBody := buildRequestBody(modelConfig, modelName, messages, true)
|
||||
reqBody := buildRequestBody(modelConfig, resolveSparkModel(modelName), messages, true)
|
||||
|
||||
if modelConfig != nil {
|
||||
if modelConfig.Thinking != nil {
|
||||
@@ -197,7 +241,7 @@ func (x *XunFeiModel) ChatStreamlyWithSender(ctx context.Context, modelName stri
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey))
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", resolveBearerToken(apiConfig)))
|
||||
|
||||
resp, err := x.baseModel.httpClient.Do(req)
|
||||
if err != nil {
|
||||
@@ -323,7 +367,7 @@ func (x *XunFeiModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]L
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey))
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", resolveBearerToken(apiConfig)))
|
||||
|
||||
resp, err := x.baseModel.httpClient.Do(req)
|
||||
if err != nil {
|
||||
@@ -358,7 +402,16 @@ func (x *XunFeiModel) Balance(ctx context.Context, apiConfig *APIConfig) (map[st
|
||||
}
|
||||
|
||||
func (x *XunFeiModel) CheckConnection(ctx context.Context, apiConfig *APIConfig) error {
|
||||
return fmt.Errorf("%s, no such method", x.Name())
|
||||
if err := x.baseModel.APIConfigCheck(apiConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify the credential bundle with a minimal chat request against the
|
||||
// free Spark-Lite model.
|
||||
maxTokens := 1
|
||||
chatConfig := &ChatConfig{MaxTokens: &maxTokens}
|
||||
_, err := x.ChatWithMessages(ctx, "Spark-Lite", []Message{{Role: "user", Content: "Hi"}}, apiConfig, chatConfig, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (x *XunFeiModel) ListTasks(ctx context.Context, apiConfig *APIConfig) ([]ListTaskStatus, error) {
|
||||
|
||||
@@ -1,6 +1,61 @@
|
||||
package models
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"strings"
|
||||
"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"})
|
||||
err := driver.CheckConnection(t.Context(), &APIConfig{})
|
||||
if err == nil || !strings.Contains(err.Error(), "api key is required") {
|
||||
t.Errorf("CheckConnection with empty key = %v, want 'api key is required'", err)
|
||||
}
|
||||
}
|
||||
|
||||
func strPtr(s string) *string { return &s }
|
||||
|
||||
func TestXunFeiUnsupportedMethodsReturnNoSuchMethod(t *testing.T) {
|
||||
withSSRFBypass(t)
|
||||
@@ -48,9 +103,6 @@ func TestXunFeiUnsupportedMethodsReturnNoSuchMethod(t *testing.T) {
|
||||
_, err := driver.Balance(ctx, &APIConfig{})
|
||||
return err
|
||||
}},
|
||||
{"CheckConnection", func() error {
|
||||
return driver.CheckConnection(ctx, &APIConfig{})
|
||||
}},
|
||||
{"ListTasks", func() error {
|
||||
_, err := driver.ListTasks(ctx, &APIConfig{})
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user