fix: XunFei Spark provider API key verification and model listing (#17652)

This commit is contained in:
euvre
2026-08-03 11:25:25 +08:00
committed by GitHub
parent 4e78f1f440
commit 8353fc7855
4 changed files with 147 additions and 17 deletions

View File

@@ -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) {

View File

@@ -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