mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-29 04:08:12 +08:00
fix: allow local FunASR without API key (#17388)
### Summary The FunASR provider added in #17171 defaults to a local `http://localhost:8000/v1` server, but it still inherited the global API-key requirement and unconditionally built an Authorization header. This prevented the default unauthenticated self-hosted deployment from working. The transcription path also dereferenced a missing model name while building its multipart request. This change: - allows an empty API key for FunASR, matching other local providers - omits the Authorization header when no key is configured while preserving trimmed Bearer authentication when one is provided - validates and trims the ASR model name before building the multipart request, returning an error instead of panicking - adds HTTP-level regression coverage for unauthenticated transcription/model listing and optional authentication
This commit is contained in:
@@ -38,9 +38,10 @@ type FunASR struct {
|
||||
func NewFunASRModel(baseURL map[string]string, urlSuffix URLSuffix) *FunASR {
|
||||
return &FunASR{
|
||||
baseModel: BaseModel{
|
||||
BaseURL: baseURL,
|
||||
URLSuffix: urlSuffix,
|
||||
httpClient: NewDriverHTTPClient(),
|
||||
BaseURL: baseURL,
|
||||
URLSuffix: urlSuffix,
|
||||
AllowEmptyAPIKey: true,
|
||||
httpClient: NewDriverHTTPClient(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -77,6 +78,10 @@ func (f *FunASR) TranscribeAudio(ctx context.Context, modelName *string, file *s
|
||||
if file == nil || *file == "" {
|
||||
return nil, fmt.Errorf("file is missing")
|
||||
}
|
||||
if modelName == nil || strings.TrimSpace(*modelName) == "" {
|
||||
return nil, fmt.Errorf("model name is missing")
|
||||
}
|
||||
model := strings.TrimSpace(*modelName)
|
||||
|
||||
resolvedBaseURL, err := f.baseModel.GetBaseURL(apiConfig)
|
||||
if err != nil {
|
||||
@@ -109,7 +114,7 @@ func (f *FunASR) TranscribeAudio(ctx context.Context, modelName *string, file *s
|
||||
}
|
||||
|
||||
// model field
|
||||
if err := writer.WriteField("model", *modelName); err != nil {
|
||||
if err := writer.WriteField("model", model); err != nil {
|
||||
return nil, fmt.Errorf("failed to write model field: %w", err)
|
||||
}
|
||||
|
||||
@@ -155,7 +160,9 @@ func (f *FunASR) TranscribeAudio(ctx context.Context, modelName *string, file *s
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey))
|
||||
if auth := BearerAuth(apiConfig); auth != "" {
|
||||
req.Header.Set("Authorization", auth)
|
||||
}
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
@@ -210,8 +217,6 @@ func (f *FunASR) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListMo
|
||||
if err := f.baseModel.APIConfigCheck(apiConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
apiKey := strings.TrimSpace(*apiConfig.ApiKey)
|
||||
|
||||
resolvedBaseURL, err := f.baseModel.GetBaseURL(apiConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -226,7 +231,9 @@ func (f *FunASR) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListMo
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
|
||||
if auth := BearerAuth(apiConfig); auth != "" {
|
||||
req.Header.Set("Authorization", auth)
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := f.baseModel.httpClient.Do(req)
|
||||
|
||||
130
internal/entity/models/funasr_test.go
Normal file
130
internal/entity/models/funasr_test.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func newFunASRForTest(baseURL string) *FunASR {
|
||||
return NewFunASRModel(
|
||||
map[string]string{"default": baseURL},
|
||||
URLSuffix{
|
||||
ASR: "audio/transcriptions",
|
||||
Models: "models",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func writeFunASRTestAudio(t *testing.T) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "audio.wav")
|
||||
if err := os.WriteFile(path, []byte("test audio"), 0o600); err != nil {
|
||||
t.Fatalf("write test audio: %v", err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func TestFunASRTranscribeAudioWithoutAPIKey(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
if r.URL.Path != "/audio/transcriptions" {
|
||||
t.Errorf("path=%s, want /audio/transcriptions", r.URL.Path)
|
||||
}
|
||||
if got := r.Header.Get("Authorization"); got != "" {
|
||||
t.Errorf("Authorization=%q, want no header", got)
|
||||
}
|
||||
if err := r.ParseMultipartForm(1 << 20); err != nil {
|
||||
t.Errorf("parse multipart form: %v", err)
|
||||
return
|
||||
}
|
||||
if got := r.FormValue("model"); got != "fun-asr-nano" {
|
||||
t.Errorf("model=%q, want fun-asr-nano", got)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"text": "hello"})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
modelName := " fun-asr-nano "
|
||||
file := writeFunASRTestAudio(t)
|
||||
resp, err := newFunASRForTest(srv.URL).TranscribeAudio(
|
||||
context.Background(), &modelName, &file, &APIConfig{}, nil, nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("TranscribeAudio: %v", err)
|
||||
}
|
||||
if resp == nil || resp.Text != "hello" {
|
||||
t.Fatalf("response=%v, want text hello", resp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFunASRTranscribeAudioRequiresModelName(t *testing.T) {
|
||||
file := writeFunASRTestAudio(t)
|
||||
apiKey := "test-key"
|
||||
blankModelName := " "
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
modelName *string
|
||||
}{
|
||||
{name: "nil", modelName: nil},
|
||||
{name: "whitespace", modelName: &blankModelName},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := newFunASRForTest("http://unused").TranscribeAudio(
|
||||
context.Background(), tc.modelName, &file, &APIConfig{ApiKey: &apiKey}, nil, nil,
|
||||
)
|
||||
if err == nil || !strings.Contains(err.Error(), "model name is missing") {
|
||||
t.Fatalf("expected missing-model-name error, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFunASRListModelsWithoutAPIKey(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("method=%s, want GET", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/models" {
|
||||
t.Errorf("path=%s, want /models", r.URL.Path)
|
||||
}
|
||||
if got := r.Header.Get("Authorization"); got != "" {
|
||||
t.Errorf("Authorization=%q, want no header", got)
|
||||
}
|
||||
_, _ = io.WriteString(w, `{"object":"list","data":[{"id":"fun-asr-nano","owned_by":"funasr"}]}`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
models, err := newFunASRForTest(srv.URL).ListModels(context.Background(), &APIConfig{})
|
||||
if err != nil {
|
||||
t.Fatalf("ListModels: %v", err)
|
||||
}
|
||||
if len(models) != 1 || models[0].Name != "fun-asr-nano@funasr" {
|
||||
t.Fatalf("models=%v, want fun-asr-nano@funasr", models)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFunASRListModelsSendsAuthWhenProvided(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := r.Header.Get("Authorization"); got != "Bearer secret" {
|
||||
t.Errorf("Authorization=%q, want Bearer secret", got)
|
||||
}
|
||||
_, _ = io.WriteString(w, `{"object":"list","data":[{"id":"fun-asr-nano"}]}`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
apiKey := " secret "
|
||||
if _, err := newFunASRForTest(srv.URL).ListModels(
|
||||
context.Background(), &APIConfig{ApiKey: &apiKey},
|
||||
); err != nil {
|
||||
t.Fatalf("ListModels: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user